MDL-29509 move customlang to admin tools
[moodle.git] / admin / tool / customlang / locallib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Definition of classes used by language customization admin tool
19  *
20  * @package    tool
21  * @subpackage customlang
22  * @copyright  2010 David Mudrak <david@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 /**
29  * Provides various utilities to be used by the plugin
30  *
31  * All the public methods here are static ones, this class can not be instantiated
32  */
33 class tool_customlang_utils {
35     /**
36      * Rough number of strings that are being processed during a full checkout.
37      * This is used to estimate the progress of the checkout.
38      */
39     const ROUGH_NUMBER_OF_STRINGS = 16500;
41     /** @var array cache of {@link self::list_components()} results */
42     protected static $components = null;
44     /**
45      * This class can not be instantiated
46      */
47     private function __construct() {
48     }
50     /**
51      * Returns a list of all components installed on the server
52      *
53      * @return array (string)legacyname => (string)frankenstylename
54      */
55     public static function list_components() {
57         $list['moodle'] = 'core';
59         $coresubsystems = get_core_subsystems();
60         ksort($coresubsystems); // should be but just in case
61         foreach ($coresubsystems as $name => $location) {
62             if ($name != 'moodle.org') {
63                 $list[$name] = 'core_'.$name;
64             }
65         }
67         $plugintypes = get_plugin_types();
68         foreach ($plugintypes as $type => $location) {
69             $pluginlist = get_plugin_list($type);
70             foreach ($pluginlist as $name => $ununsed) {
71                 if ($type == 'mod') {
72                     if (array_key_exists($name, $list)) {
73                         throw new Exception('Activity module and core subsystem name collision');
74                     }
75                     $list[$name] = $type.'_'.$name;
76                 } else {
77                     $list[$type.'_'.$name] = $type.'_'.$name;
78                 }
79             }
80         }
82         return $list;
83     }
85     /**
86      * Updates the translator database with the strings from files
87      *
88      * This should be executed each time before going to the translation page
89      *
90      * @param string $lang language code to checkout
91      * @param progress_bar $progressbar optionally, the given progress bar can be updated
92      */
93     public static function checkout($lang, progress_bar $progressbar = null) {
94         global $DB;
96         // make sure that all components are registered
97         $current = $DB->get_records('tool_customlang_components', null, 'name', 'name,version,id');
98         foreach (self::list_components() as $component) {
99             if (empty($current[$component])) {
100                 $record = new stdclass();
101                 $record->name = $component;
102                 if (!$version = get_component_version($component)) {
103                     $record->version = null;
104                 } else {
105                     $record->version = $version;
106                 }
107                 $DB->insert_record('tool_customlang_components', $record);
108             } elseif ($version = get_component_version($component)) {
109                 if (is_null($current[$component]->version) or ($version > $current[$component]->version)) {
110                     $DB->set_field('tool_customlang_components', 'version', $version, array('id' => $current[$component]->id));
111                 }
112             }
113         }
114         unset($current);
116         // initialize the progress counter - stores the number of processed strings
117         $done = 0;
118         $strinprogress = get_string('checkoutinprogress', 'tool_customlang');
120         // reload components and fetch their strings
121         $stringman  = get_string_manager();
122         $components = $DB->get_records('tool_customlang_components');
123         foreach ($components as $component) {
124             $sql = "SELECT stringid, id, lang, componentid, original, master, local, timemodified, timecustomized, outdated, modified
125                       FROM {tool_customlang} s
126                      WHERE lang = ? AND componentid = ?
127                   ORDER BY stringid";
128             $current = $DB->get_records_sql($sql, array($lang, $component->id));
129             $english = $stringman->load_component_strings($component->name, 'en', true, true);
130             if ($lang == 'en') {
131                 $master =& $english;
132             } else {
133                 $master = $stringman->load_component_strings($component->name, $lang, true, true);
134             }
135             $local = $stringman->load_component_strings($component->name, $lang, true, false);
137             foreach ($english as $stringid => $stringoriginal) {
138                 $stringmaster = isset($master[$stringid]) ? $master[$stringid] : null;
139                 $stringlocal = isset($local[$stringid]) ? $local[$stringid] : null;
140                 $now = time();
142                 if (!is_null($progressbar)) {
143                     $done++;
144                     $donepercent = floor(min($done, self::ROUGH_NUMBER_OF_STRINGS) / self::ROUGH_NUMBER_OF_STRINGS * 100);
145                     $progressbar->update_full($donepercent, $strinprogress);
146                 }
148                 if (isset($current[$stringid])) {
149                     $needsupdate     = false;
150                     $currentoriginal = $current[$stringid]->original;
151                     $currentmaster   = $current[$stringid]->master;
152                     $currentlocal    = $current[$stringid]->local;
154                     if ($currentoriginal !== $stringoriginal or $currentmaster !== $stringmaster) {
155                         $needsupdate = true;
156                         $current[$stringid]->original       = $stringoriginal;
157                         $current[$stringid]->master         = $stringmaster;
158                         $current[$stringid]->timemodified   = $now;
159                         $current[$stringid]->outdated       = 1;
160                     }
162                     if ($stringmaster !== $stringlocal) {
163                         $needsupdate = true;
164                         $current[$stringid]->local          = $stringlocal;
165                         $current[$stringid]->timecustomized = $now;
166                     }
168                     if ($needsupdate) {
169                         $DB->update_record('tool_customlang', $current[$stringid]);
170                         continue;
171                     }
173                 } else {
174                     $record                 = new stdclass();
175                     $record->lang           = $lang;
176                     $record->componentid    = $component->id;
177                     $record->stringid       = $stringid;
178                     $record->original       = $stringoriginal;
179                     $record->master         = $stringmaster;
180                     $record->timemodified   = $now;
181                     $record->outdated       = 0;
182                     if ($stringmaster !== $stringlocal) {
183                         $record->local          = $stringlocal;
184                         $record->timecustomized = $now;
185                     } else {
186                         $record->local          = null;
187                         $record->timecustomized = null;
188                     }
190                     $DB->insert_record('tool_customlang', $record);
191                 }
192             }
193         }
195         if (!is_null($progressbar)) {
196             $progressbar->update_full(100, get_string('checkoutdone', 'tool_customlang'));
197         }
198     }
200     /**
201      * Exports the translator database into disk files
202      *
203      * @param mixed $lang language code
204      */
205     public static function checkin($lang) {
206         global $DB, $USER, $CFG;
207         require_once($CFG->libdir.'/filelib.php');
209         if ($lang !== clean_param($lang, PARAM_LANG)) {
210             return false;
211         }
213         // get all customized strings from updated components
214         $sql = "SELECT s.*, c.name AS component
215                   FROM {tool_customlang} s
216                   JOIN {tool_customlang_components} c ON s.componentid = c.id
217                  WHERE s.lang = ?
218                        AND (s.local IS NOT NULL OR s.modified = 1)
219               ORDER BY componentid, stringid";
220         $strings = $DB->get_records_sql($sql, array($lang));
222         $files = array();
223         foreach ($strings as $string) {
224             if (!is_null($string->local)) {
225                 $files[$string->component][$string->stringid] = $string->local;
226             }
227         }
229         fulldelete(self::get_localpack_location($lang));
230         foreach ($files as $component => $strings) {
231             self::dump_strings($lang, $component, $strings);
232         }
234         $DB->set_field_select('tool_customlang', 'modified', 0, 'lang = ?', array($lang));
235         $sm = get_string_manager();
236         $sm->reset_caches();
237     }
239     /**
240      * Returns full path to the directory where local packs are dumped into
241      *
242      * @param string $lang language code
243      * @return string full path
244      */
245     protected static function get_localpack_location($lang) {
246         global $CFG;
248         return $CFG->langlocalroot.'/'.$lang.'_local';
249     }
251     /**
252      * Writes strings into a local language pack file
253      *
254      * @param string $component the name of the component
255      * @param array $strings
256      */
257     protected static function dump_strings($lang, $component, $strings) {
258         global $CFG;
260         if ($lang !== clean_param($lang, PARAM_LANG)) {
261             debugging('Unable to dump local strings for non-installed language pack .'.s($lang));
262             return false;
263         }
264         if ($component !== clean_param($component, PARAM_COMPONENT)) {
265             throw new coding_exception('Incorrect component name');
266         }
267         if (!$filename = self::get_component_filename($component)) {
268             debugging('Unable to find the filename for the component '.s($component));
269             return false;
270         }
271         if ($filename !== clean_param($filename, PARAM_FILE)) {
272             throw new coding_exception('Incorrect file name '.s($filename));
273         }
274         list($package, $subpackage) = normalize_component($component);
275         $packageinfo = " * @package    $package";
276         if (!is_null($subpackage)) {
277             $packageinfo .= "\n * @subpackage $subpackage";
278         }
279         $filepath = self::get_localpack_location($lang);
280         $filepath = $filepath.'/'.$filename;
281         if (!is_dir(dirname($filepath))) {
282             check_dir_exists(dirname($filepath));
283         }
285         if (!$f = fopen($filepath, 'w')) {
286             debugging('Unable to write '.s($filepath));
287             return false;
288         }
289         fwrite($f, <<<EOF
290 <?php
292 // This file is part of Moodle - http://moodle.org/
293 //
294 // Moodle is free software: you can redistribute it and/or modify
295 // it under the terms of the GNU General Public License as published by
296 // the Free Software Foundation, either version 3 of the License, or
297 // (at your option) any later version.
298 //
299 // Moodle is distributed in the hope that it will be useful,
300 // but WITHOUT ANY WARRANTY; without even the implied warranty of
301 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
302 // GNU General Public License for more details.
303 //
304 // You should have received a copy of the GNU General Public License
305 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
307 /**
308  * Local language pack from $CFG->wwwroot
309  *
310 $packageinfo
311  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
312  */
314 defined('MOODLE_INTERNAL') || die();
317 EOF
318         );
320         foreach ($strings as $stringid => $text) {
321             if ($stringid !== clean_param($stringid, PARAM_STRINGID)) {
322                 debugging('Invalid string identifier '.s($stringid));
323                 continue;
324             }
325             fwrite($f, '$string[\'' . $stringid . '\'] = ');
326             fwrite($f, var_export($text, true));
327             fwrite($f, ";\n");
328         }
329         fclose($f);
330     }
332     /**
333      * Returns the name of the file where the component's local strings should be exported into
334      *
335      * @param string $component normalized name of the component, eg 'core' or 'mod_workshop'
336      * @return string|boolean filename eg 'moodle.php' or 'workshop.php', false if not found
337      */
338     protected static function get_component_filename($component) {
339         if (is_null(self::$components)) {
340             self::$components = self::list_components();
341         }
342         $return = false;
343         foreach (self::$components as $legacy => $normalized) {
344             if ($component === $normalized) {
345                 $return = $legacy.'.php';
346                 break;
347             }
348         }
349         return $return;
350     }
352     /**
353      * Returns the number of modified strings checked out in the translator
354      *
355      * @param string $lang language code
356      * @return int
357      */
358     public static function get_count_of_modified($lang) {
359         global $DB;
361         return $DB->count_records('tool_customlang', array('lang'=>$lang, 'modified'=>1));
362     }
364     /**
365      * Saves filter data into a persistant storage such as user session
366      *
367      * @see self::load_filter()
368      * @param stdclass $data filter values
369      * @param stdclass $persistant storage object
370      */
371     public static function save_filter(stdclass $data, stdclass $persistant) {
372         if (!isset($persistant->tool_customlang_filter)) {
373             $persistant->tool_customlang_filter = array();
374         }
375         foreach ($data as $key => $value) {
376             if ($key !== 'submit') {
377                 $persistant->tool_customlang_filter[$key] = serialize($value);
378             }
379         }
380     }
382     /**
383      * Loads the previously saved filter settings from a persistent storage
384      *
385      * @see self::save_filter()
386      * @param stdclass $persistant storage object
387      * @return stdclass filter data
388      */
389     public static function load_filter(stdclass $persistant) {
390         $data = new stdclass();
391         if (isset($persistant->tool_customlang_filter)) {
392             foreach ($persistant->tool_customlang_filter as $key => $value) {
393                 $data->{$key} = unserialize($value);
394             }
395         }
396         return $data;
397     }
400 /**
401  * Represents the action menu of the tool
402  */
403 class tool_customlang_menu implements renderable {
405     /** @var menu items */
406     protected $items = array();
408     public function __construct(array $items = array()) {
409         global $CFG;
411         foreach ($items as $itemkey => $item) {
412             $this->add_item($itemkey, $item['title'], $item['url'], empty($item['method']) ? 'post' : $item['method']);
413         }
414     }
416     /**
417      * Returns the menu items
418      *
419      * @return array (string)key => (object)[->(string)title ->(moodle_url)url ->(string)method]
420      */
421     public function get_items() {
422         return $this->items;
423     }
425     /**
426      * Adds item into the menu
427      *
428      * @param string $key item identifier
429      * @param string $title localized action title
430      * @param moodle_url $url action handler
431      * @param string $method form method
432      */
433     public function add_item($key, $title, moodle_url $url, $method) {
434         if (isset($this->items[$key])) {
435             throw new coding_exception('Menu item already exists');
436         }
437         if (empty($title) or empty($key)) {
438             throw new coding_exception('Empty title or item key not allowed');
439         }
440         $item = new stdclass();
441         $item->title = $title;
442         $item->url = $url;
443         $item->method = $method;
444         $this->items[$key] = $item;
445     }
448 /**
449  * Represents the translation tool
450  */
451 class tool_customlang_translator implements renderable {
453     /** @const int number of rows per page */
454     const PERPAGE = 100;
456     /** @var int total number of the rows int the table */
457     public $numofrows = 0;
459     /** @var moodle_url */
460     public $handler;
462     /** @var string language code */
463     public $lang;
465     /** @var int page to display, starting with page 0 */
466     public $currentpage = 0;
468     /** @var array of stdclass strings to display */
469     public $strings = array();
471     /** @var stdclass */
472     protected $filter;
474     public function __construct(moodle_url $handler, $lang, $filter, $currentpage = 0) {
475         global $DB;
477         $this->handler      = $handler;
478         $this->lang         = $lang;
479         $this->filter       = $filter;
480         $this->currentpage  = $currentpage;
482         if (empty($filter) or empty($filter->component)) {
483             // nothing to do
484             $this->currentpage = 1;
485             return;
486         }
488         list($insql, $inparams) = $DB->get_in_or_equal($filter->component, SQL_PARAMS_NAMED);
490         $csql = "SELECT COUNT(*)";
491         $fsql = "SELECT s.id, s.*, c.name AS component";
492         $sql  = "  FROM {tool_customlang_components} c
493                    JOIN {tool_customlang} s ON s.componentid = c.id
494                   WHERE s.lang = :lang
495                         AND c.name $insql";
497         $params = array_merge(array('lang' => $lang), $inparams);
499         if (!empty($filter->customized)) {
500             $sql .= "   AND s.local IS NOT NULL";
501         }
503         if (!empty($filter->modified)) {
504             $sql .= "   AND s.modified = 1";
505         }
507         if (!empty($filter->stringid)) {
508             $sql .= "   AND s.stringid = :stringid";
509             $params['stringid'] = $filter->stringid;
510         }
512         if (!empty($filter->substring)) {
513             $sql .= "   AND (".$DB->sql_like('s.original', ':substringoriginal', false)." OR
514                              ".$DB->sql_like('s.master', ':substringmaster', false)." OR
515                              ".$DB->sql_like('s.local', ':substringlocal', false).")";
516             $params['substringoriginal'] = '%'.$filter->substring.'%';
517             $params['substringmaster']   = '%'.$filter->substring.'%';
518             $params['substringlocal']    = '%'.$filter->substring.'%';
519         }
521         if (!empty($filter->helps)) {
522             $sql .= "   AND ".$DB->sql_like('s.stringid', ':help', false); //ILIKE
523             $params['help'] = '%\_help';
524         } else {
525             $sql .= "   AND ".$DB->sql_like('s.stringid', ':link', false, true, true); //NOT ILIKE
526             $params['link'] = '%\_link';
527         }
529         $osql = " ORDER BY c.name, s.stringid";
531         $this->numofrows = $DB->count_records_sql($csql.$sql, $params);
532         $this->strings = $DB->get_records_sql($fsql.$sql.$osql, $params, ($this->currentpage) * self::PERPAGE, self::PERPAGE);
533     }