<?php

// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * This file contains classes used to manage the navigation structures in Moodle
 * and was introduced as part of the changes occuring in Moodle 2.0
 *
 * In order to use this tool you need to checkout the lang module from Moodle,
 * then copy the en_utf8/auth* files from Moodle to lang/en_utf8/* so that
 * they are available for this script. Then simple visit this script in your
 * browser. Check the results and commit :)
 *
 * @since 2.0
 * @package tool
 * @copyright 2009 Sam Hemelryk
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

// The path to the reference language pack in this case en_utf8
$path = dirname(__FILE__) . '/en_utf8';
// Check it exists
if (!file_exists($path)) die('Could not locate the English language pack that we will reference');
// Check it is a directory and readable
if (!is_dir($path) || !is_readable($path)) die('The English language pack is not readable');

// Create a new processor
$authprocessor =  new auth_lang_string_processor($path);
// Collect strings
$authprocessor->collect_strings();
// Correct and create language files as required
$authprocessor->correct_lang_files();

// Collect the outcome and display it
$outcome = $authprocessor->log();
echo '<style type=\'text/css\'>.success {font-family:sans-serif;font-size:8pt;color:#123456;} .notice {font-family:sans-serif;font-size:8pt;color:green;} .error {font-family:sans-serif;font-size:8pt;color:red;}</style><table><tr><td>Outcome</td><td>Message</td></tr>';
foreach ($outcome as $entry) {
    echo "<tr class='$entry->class'><td class='class'>$entry->class</td><td class='message'>$entry->message</td></tr>";
}
echo '</table>';

/**
 * Auth lang string processor
 *
 * @since 2.0
 * @package tool
 * @copyright 2009 Sam Hemelryk
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class auth_lang_string_processor {

    /** @var string Absolute path to the reference language pack */
    protected $langdirpath;
    /** @var string Will always be auth.php unless something goes wrong */
    protected $authmainpath = null;
    /** @var array An array of auth_* files (for each module) */
    protected $authmodulepaths = array();
    /** @var array An array of log entries */
    protected $errorlog = array();

    /** @var string A regular expression to match auth files */
    protected static $authmodulepattern = '#auth(_[a-zA-Z0-9]+)?\.php$#i';

    /**
     * Initialise the object
     * 
     * @param string $langdirpath
     */
    public function __construct($langdirpath) {
        $this->langdirpath = $langdirpath;
    }

    /**
     * Collects an organised structure of all of the auth strings and thier
     * associated module file from the reference structure.
     *
     * Also calls harvest_strings and check_main_lang_files
     *
     * @return bool
     */
    public function collect_strings() {
        $langhandle = dir($this->langdirpath);
        while (false !== ($entry = $langhandle->read())) {
            if (preg_match(self::$authmodulepattern, $entry)) {
                $this->authmodulepaths[$entry] = array('path'=>$this->langdirpath.'/'.$entry,'strings'=>array());
                if ($entry === 'auth.php') {
                    $this->authmainpath = 'auth.php';
                }
            }
        }
        $langhandle->close();
        if ($this->authmainpath===null) {
            $this->log('error', 'Exiting as auth.php file was not located for the main language pack', __LINE__);
            return false;
        } else {
            $this->log('success', 'auth.php file located as well as '.count($this->authmodulepaths).' auth module lang files', __LINE__);
            $this->harvest_strings();
            $this->check_main_lang_files();
            return true;
        }
    }

    /**
     * Actually collecs the strings from a language file
     */
    protected function harvest_strings() {
        $files = array_merge(array($this->authmainpath), $this->authmodulepaths);
        foreach ($this->authmodulepaths as $name=>$file) {
            $string = array();
            if (is_file($file['path']) || is_readable($file['path'])) {
                include $file['path'];
                $this->authmodulepaths[$name]['strings'] = $string;
            } else {
                $this->log('warning', 'Skipped over '.$file.' as it was either not a file or not readable by the webserver', __LINE__);
            }
        }
    }

    /**
     * Merges strings in the modules that are not in the main auth.php back into
     * it to ensure backwards compaitility
     */
    protected function check_main_lang_files() {
        $missingstrings = array();
        $mainstrings = $this->authmodulepaths[$this->authmainpath]['strings'];
        foreach ($this->authmodulepaths as $name=>$file) {
            if ($name !== $this->authmainpath) {
                $missingstrings = array_merge($missingstrings, array_diff_key($file['strings'], $mainstrings));
            }
        }
        $this->insert_lang_strings($this->authmodulepaths[$this->authmainpath]['path'], $missingstrings);
    }

    /**
     * Correct the language files for each language pack, create, or merge where
     * required
     */
    public function correct_lang_files() {
        $langdir = dirname($this->langdirpath);
        $langhandle = dir($langdir);
        while (false !== ($entry = $langhandle->read())) {
            if ($entry!=='.' && $entry !== '..' && is_dir($entry) && strpos($langdir.'/'.$entry, $this->langdirpath)===false) {
                $languagepack = array('path'=>$langdir.'/'.$entry,'files'=>array());
                $packhandle = dir($langdir.'/'.$entry);
                while (false !== ($file = $packhandle->read())) {
                    if (preg_match(self::$authmodulepattern, $file)) {
                        $string = array();
                        
                        // Do this incase something goes wrong because of language
                        // file php errors.... there were a couple when I ran this
                        ob_start();
                        include $langdir.'/'.$entry.'/'.$file;
                        ob_end_clean();
                        
                        $languagepack['files'][$file] = array('path'=>$langdir.'/'.$entry.'/'.$file,'strings'=>$string);
                        $string = null;
                    }
                }
                $packhandle->close();
                if (!array_key_exists($this->authmainpath, $languagepack['files'])) {
                    $this->log('error', 'Failed to locate the main auth.php file for '.$entry.' proceeding to next entry', __LINE__);
                    continue;
                }
                $this->copy_lang_file_strings($languagepack);
            }
        }
        $langhandle->close();
    }

    /**
     * This function actually handles the merging of auth.php and the seperating
     * of module files.
     * @param array $lang
     */
    protected function copy_lang_file_strings($lang) {
        // First we need to make sure all strings in module files are copied in
        // the main auth.php file
        $missingstrings = array();
        $mainstrings = $lang['files'][$this->authmainpath]['strings'];
        foreach ($lang['files'] as $name=>$file) {
            if ($name !== $this->authmainpath) {
                $missingstrings = array_merge($missingstrings, array_diff_key($file['strings'], $mainstrings));
            }
        }
        $this->insert_lang_strings( $lang['files'][$this->authmainpath]['path'], $missingstrings);

        foreach ($this->authmodulepaths as $name=>$module) {
            $strings = array_keys($module['strings']);
            if (array_key_exists($name, $lang['files'])) {
                // The file exists so we need to check which strings are in there
                // and copy any module strings defined in auth.php that are not
                // in there already
                $missingstrings = array_intersect_key($mainstrings, $module['strings']);
                $missingstrings = array_diff_key($missingstrings, $lang['files'][$name]['strings']);
            } else {
                // The file does not exist so we need to copy all module strings there if
                // they have been defined
                $missingstrings = array_intersect_key($mainstrings, $module['strings']);
            }
            $this->insert_lang_strings( $lang['path'].'/'.$name, $missingstrings);
        }
    }

    /**
     * This function inserts an array of strings into a php file or creates the
     * file if it does not already exist
     *
     * @param string $file
     * @param array $strings
     * @return bool
     */
    protected function insert_lang_strings($file, $strings) {

        if (count($strings) === 0 && file_exists($file)) {
            return true;
        }

        $php = '';
        foreach ($strings as $key=>$string) {
            $string = preg_replace("#([^\\\\])\"#", '$1\"', $string, -1, $count);
            $php .= "\n\$string['$key'] = \"$string\";";
        }

        if (strpos($file, dirname($this->langdirpath))!==0) {
            $this->log('error', 'The path of the file to be written to was not under the language directory, aborting add', __LINE__);
            return false;
        }

        if (!file_exists($file)) {
            if (count($strings)===0) {
                $this->log('notice','The file '.$file.' will be created without any strings',__LINE__);
            }
            $content = "<?php\n$php";
        } else {

            if (basename($file)==='auth.php') {
                $php =  '// The following strings were automatically merged back into auth.php as part of MDL-19468'
                       .'// These are duplicate strings that can be removed once a better method of'
                       .'// packaging languages is arrived at.'.$php;
            }

            $content = file_get_contents($file);
            if (stripos($content, '<?php')===false) {
                $content = "<?php\n".$content;
            }
            if (strpos($content, '?>') === false) {
                $content .= $php;
            } else {
                $content  = preg_replace('#\?\>#', "\n$php\n\n?>", $content);
            }
            
        }
        
        $outcome = file_put_contents($file, $content);
        
        if ($outcome === false) {
            $this->log('error', 'Failed to write the additional strings to the file '.$file, __LINE__);
            return false;
        } else {
            if (count($strings)>0) {
                $this->log('success', 'Successfully wrote an additional '.count($strings).' strings to '.substr($file, strlen(dirname($this->langdirpath))), __LINE__);
            }
            return true;
        }
    }

    /**
     * Can be used to add a log entry or retrieve an array of logs
     *
     * @param string $class
     * @param string $message
     * @param int $line use __LINE__
     * @return bool|array
     */
    public function log($class='all', $message=null, $line=null) {
        if ($message!=null) {
            $error = new stdClass;
            $error->class = $class;
            $error->message = $message;
            $error->line = $line;
            $this->errorlog[] = $error;
            return true;
        } else {
            if ($class === 'all') {
                return $this->errorlog;
            } else {
                $errorlog = array();
                foreach ($this->errorlog as $error) {
                    if ($error->class === $class) {
                        $errorlog[] = $error;
                    }
                }
                return $errorlog;
            }
        }
    }
}
