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