MDL-24080 new $notlike parameter added to our new sql_like(), hopefully nobody starte...
[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 Langugae 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     /** @var array cache of {@link self::list_components()} results */
37     protected static $components = null;
39     /**
40      * This class can not be instantiated
41      */
42     private function __construct() {
43     }
45     /**
46      * Returns a list of all components installed on the server
47      *
48      * @return array (string)legacyname => (string)frankenstylename
49      */
50     public static function list_components() {
52         $list['moodle'] = 'core';
54         $coresubsystems = get_core_subsystems();
55         ksort($coresubsystems); // should be but just in case
56         foreach ($coresubsystems as $name => $location) {
57             if ($name != 'moodle.org') {
58                 $list[$name] = 'core_'.$name;
59             }
60         }
62         $plugintypes = get_plugin_types();
63         foreach ($plugintypes as $type => $location) {
64             $pluginlist = get_plugin_list($type);
65             foreach ($pluginlist as $name => $ununsed) {
66                 if ($type == 'mod') {
67                     if (array_key_exists($name, $list)) {
68                         throw new Exception('Activity module and core subsystem name collision');
69                     }
70                     $list[$name] = $type.'_'.$name;
71                 } else {
72                     $list[$type.'_'.$name] = $type.'_'.$name;
73                 }
74             }
75         }
77         return $list;
78     }
80     /**
81      * Updates the translator database with the strings from files
82      *
83      * This should be executed each time before going to the translation page
84      *
85      * @param string $lang language code to checkout
86      */
87     public static function checkout($lang) {
88         global $DB;
90         // make sure that all components are registered
91         $current = $DB->get_records('customlang_components', null, 'name', 'name,version,id');
92         foreach (self::list_components() as $component) {
93             if (empty($current[$component])) {
94                 $record = new stdclass();
95                 $record->name = $component;
96                 if (!$version = get_component_version($component)) {
97                     $record->version = null;
98                 } else {
99                     $record->version = $version;
100                 }
101                 $DB->insert_record('customlang_components', $record);
102             } elseif ($version = get_component_version($component)) {
103                 if (is_null($current[$component]->version) or ($version > $current[$component]->version)) {
104                     $DB->set_field('customlang_components', 'version', $version, array('id' => $current[$component]->id));
105                 }
106             }
107         }
108         unset($current);
110         // reload components and fetch their strings
111         $stringman  = get_string_manager();
112         $components = $DB->get_records('customlang_components');
113         foreach ($components as $component) {
114             $sql = "SELECT stringid, s.*
115                       FROM {customlang} s
116                      WHERE lang = ? AND componentid = ?
117                   ORDER BY stringid";
118             $current = $DB->get_records_sql($sql, array($lang, $component->id));
119             $english = $stringman->load_component_strings($component->name, 'en', true, true);
120             if ($lang == 'en') {
121                 $master =& $english;
122             } else {
123                 $master = $stringman->load_component_strings($component->name, $lang, true, true);
124             }
125             $local = $stringman->load_component_strings($component->name, $lang, true, false);
127             foreach ($english as $stringid => $stringoriginal) {
128                 $stringmaster = isset($master[$stringid]) ? $master[$stringid] : null;
129                 $stringlocal = isset($local[$stringid]) ? $local[$stringid] : null;
130                 $now = time();
132                 if (isset($current[$stringid])) {
133                     $needsupdate     = false;
134                     $currentoriginal = $current[$stringid]->original;
135                     $currentmaster   = $current[$stringid]->master;
136                     $currentlocal    = $current[$stringid]->local;
138                     if ($currentoriginal !== $stringoriginal or $currentmaster !== $stringmaster) {
139                         $needsupdate = true;
140                         $current[$stringid]->original       = $stringoriginal;
141                         $current[$stringid]->master         = $stringmaster;
142                         $current[$stringid]->timemodified   = $now;
143                         $current[$stringid]->outdated       = 1;
144                     }
146                     if ($stringmaster !== $stringlocal) {
147                         $needsupdate = true;
148                         $current[$stringid]->local          = $stringlocal;
149                         $current[$stringid]->timecustomized = $now;
150                     }
152                     if ($needsupdate) {
153                         $DB->update_record('customlang', $current[$stringid]);
154                         continue;
155                     }
157                 } else {
158                     $record                 = new stdclass();
159                     $record->lang           = $lang;
160                     $record->componentid    = $component->id;
161                     $record->stringid       = $stringid;
162                     $record->original       = $stringoriginal;
163                     $record->master         = $stringmaster;
164                     $record->timemodified   = $now;
165                     $record->outdated       = 0;
166                     if ($stringmaster !== $stringlocal) {
167                         $record->local          = $stringlocal;
168                         $record->timecustomized = $now;
169                     } else {
170                         $record->local          = null;
171                         $record->timecustomized = null;
172                     }
174                     $DB->insert_record('customlang', $record);
175                 }
176             }
177         }
178     }
180     /**
181      * Exports the translator database into disk files
182      *
183      * @param mixed $lang language code
184      */
185     public static function checkin($lang) {
186         global $DB, $USER, $CFG;
187         require_once($CFG->libdir.'/filelib.php');
189         if ($lang !== clean_param($lang, PARAM_LANG)) {
190             return false;
191         }
193         // get all customized strings from updated components
194         $sql = "SELECT s.*, c.name AS component
195                   FROM {customlang} s
196                   JOIN {customlang_components} c ON s.componentid = c.id
197                  WHERE s.lang = ?
198                        AND (s.local IS NOT NULL OR s.modified = 1)
199               ORDER BY componentid, stringid";
200         $strings = $DB->get_records_sql($sql, array($lang));
202         $files = array();
203         foreach ($strings as $string) {
204             if (!is_null($string->local)) {
205                 $files[$string->component][$string->stringid] = $string->local;
206             }
207         }
209         fulldelete(self::get_localpack_location($lang));
210         foreach ($files as $component => $strings) {
211             self::dump_strings($lang, $component, $strings);
212         }
214         $DB->set_field_select('customlang', 'modified', 0, 'lang = ?', array($lang));
215         $sm = get_string_manager();
216         $sm->reset_caches();
217     }
219     /**
220      * Returns full path to the directory where local packs are dumped into
221      *
222      * @param string $lang language code
223      * @return string full path
224      */
225     protected static function get_localpack_location($lang) {
226         global $CFG;
228         return $CFG->langlocalroot.'/'.$lang.'_local';
229     }
231     /**
232      * Writes strings into a local language pack file
233      *
234      * @param string $component the name of the component
235      * @param array $strings
236      */
237     protected static function dump_strings($lang, $component, $strings) {
238         global $CFG;
240         if ($lang !== clean_param($lang, PARAM_LANG)) {
241             debugging('Unable to dump local strings for non-installed language pack .'.s($lang));
242             return false;
243         }
244         if ($component !== clean_param($component, PARAM_SAFEDIR)) {
245             throw new coding_exception('Incorrect component name');
246         }
247         if (!$filename = self::get_component_filename($component)) {
248             debugging('Unable to find the filename for the component '.s($component));
249             return false;
250         }
251         if ($filename !== clean_param($filename, PARAM_FILE)) {
252             throw new coding_exception('Incorrect file name '.s($filename));
253         }
254         list($package, $subpackage) = normalize_component($component);
255         $packageinfo = " * @package    $package";
256         if (!is_null($subpackage)) {
257             $packageinfo .= "\n * @subpackage $subpackage";
258         }
259         $filepath = self::get_localpack_location($lang);
260         if ($filepath !== clean_param($filepath, PARAM_SAFEPATH)) {
261             throw new coding_exception('Incorrect file location '.s($filepath));
262         }
263         $filepath = $filepath.'/'.$filename;
264         if (!is_dir(dirname($filepath))) {
265             check_dir_exists(dirname($filepath));
266         }
268         if (!$f = fopen($filepath, 'w')) {
269             debugging('Unable to write '.s($filepath));
270             return false;
271         }
272         fwrite($f, <<<EOF
273 <?php
275 // This file is part of Moodle - http://moodle.org/
276 //
277 // Moodle is free software: you can redistribute it and/or modify
278 // it under the terms of the GNU General Public License as published by
279 // the Free Software Foundation, either version 3 of the License, or
280 // (at your option) any later version.
281 //
282 // Moodle is distributed in the hope that it will be useful,
283 // but WITHOUT ANY WARRANTY; without even the implied warranty of
284 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
285 // GNU General Public License for more details.
286 //
287 // You should have received a copy of the GNU General Public License
288 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
290 /**
291  * Local language pack from $CFG->wwwroot
292  *
293 $packageinfo
294  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
295  */
297 defined('MOODLE_INTERNAL') || die();
300 EOF
301         );
303         foreach ($strings as $stringid => $text) {
304             if ($stringid !== clean_param($stringid, PARAM_STRINGID)) {
305                 debugging('Invalid string identifier '.s($stringid));
306                 continue;
307             }
308             fwrite($f, '$string[\'' . $stringid . '\'] = ');
309             fwrite($f, var_export($text, true));
310             fwrite($f, ";\n");
311         }
312         fclose($f);
313     }
315     /**
316      * Returns the name of the file where the component's local strings should be exported into
317      *
318      * @param string $component normalized name of the component, eg 'core' or 'mod_workshop'
319      * @return string|boolean filename eg 'moodle.php' or 'workshop.php', false if not found
320      */
321     protected static function get_component_filename($component) {
322         if (is_null(self::$components)) {
323             self::$components = self::list_components();
324         }
325         $return = false;
326         foreach (self::$components as $legacy => $normalized) {
327             if ($component === $normalized) {
328                 $return = $legacy.'.php';
329                 break;
330             }
331         }
332         return $return;
333     }
335     /**
336      * Returns the number of modified strings checked out in the translator
337      *
338      * @param string $lang language code
339      * @return int
340      */
341     public static function get_count_of_modified($lang) {
342         global $DB;
344         return $DB->count_records('customlang', array('lang'=>$lang, 'modified'=>1));
345     }
347     /**
348      * Saves filter data into a persistant storage such as user session
349      *
350      * @see self::load_filter()
351      * @param stdclass $data filter values
352      * @param stdclass $persistant storage object
353      */
354     public static function save_filter(stdclass $data, stdclass $persistant) {
355         if (!isset($persistant->report_customlang_filter)) {
356             $persistant->report_customlang_filter = array();
357         }
358         foreach ($data as $key => $value) {
359             if ($key !== 'submit') {
360                 $persistant->report_customlang_filter[$key] = serialize($value);
361             }
362         }
363     }
365     /**
366      * Loads the previously saved filter settings from a persistent storage
367      *
368      * @see self::save_filter()
369      * @param stdclass $persistant storage object
370      * @return stdclass filter data
371      */
372     public static function load_filter(stdclass $persistant) {
373         $data = new stdclass();
374         if (isset($persistant->report_customlang_filter)) {
375             foreach ($persistant->report_customlang_filter as $key => $value) {
376                 $data->{$key} = unserialize($value);
377             }
378         }
379         return $data;
380     }
383 /**
384  * Represents the action menu of the report
385  */
386 class report_customlang_menu implements renderable {
388     /** @var menu items */
389     protected $items = array();
391     public function __construct(array $items = array()) {
392         global $CFG;
394         foreach ($items as $itemkey => $item) {
395             $this->add_item($itemkey, $item['title'], $item['url'], empty($item['method']) ? 'post' : $item['method']);
396         }
397     }
399     /**
400      * Returns the menu items
401      *
402      * @return array (string)key => (object)[->(string)title ->(moodl_url)url ->(string)method]
403      */
404     public function get_items() {
405         return $this->items;
406     }
408     /**
409      * Adds item into the menu
410      *
411      * @param string $key item identifier
412      * @param string $title localized action title
413      * @param moodle_url $url action handler
414      * @param string $method form method
415      */
416     public function add_item($key, $title, moodle_url $url, $method) {
417         if (isset($this->items[$key])) {
418             throw new coding_error('Menu item already exists');
419         }
420         if (empty($title) or empty($key)) {
421             throw new coding_error('Empty title or item key not allowed');
422         }
423         $item = new stdclass();
424         $item->title = $title;
425         $item->url = $url;
426         $item->method = $method;
427         $this->items[$key] = $item;
428     }
431 /**
432  * Represents the translation tool
433  */
434 class report_customlang_translator implements renderable {
436     /** @const int number of rows per page */
437     const PERPAGE = 100;
439     /** @var int total number of the rows int the table */
440     public $numofrows = 0;
442     /** @var moodle_url */
443     public $handler;
445     /** @var string language code */
446     public $lang;
448     /** @var int page to display, starting with page 0 */
449     public $currentpage = 0;
451     /** @var array of stdclass strings to display */
452     public $strings = array();
454     /** @var stdclass */
455     protected $filter;
457     public function __construct(moodle_url $handler, $lang, $filter, $currentpage = 0) {
458         global $DB;
460         $this->handler      = $handler;
461         $this->lang         = $lang;
462         $this->filter       = $filter;
463         $this->currentpage  = $currentpage;
465         if (empty($filter) or empty($filter->component)) {
466             // nothing to do
467             $this->currentpage = 1;
468             return;
469         }
471         list($insql, $inparams) = $DB->get_in_or_equal($filter->component, SQL_PARAMS_NAMED);
473         $csql = "SELECT COUNT(*)";
474         $fsql = "SELECT s.id, s.*, c.name AS component";
475         $sql  = "  FROM {customlang_components} c
476                    JOIN {customlang} s ON s.componentid = c.id
477                   WHERE s.lang = :lang
478                         AND c.name $insql";
480         $params = array_merge(array('lang' => $lang), $inparams);
482         if (!empty($filter->customized)) {
483             $sql .= "   AND s.local IS NOT NULL";
484         }
486         if (!empty($filter->modified)) {
487             $sql .= "   AND s.modified = 1";
488         }
490         if (!empty($filter->stringid)) {
491             $sql .= "   AND s.stringid = :stringid";
492             $params['stringid'] = $filter->stringid;
493         }
495         if (!empty($filter->substring)) {
496             $sql .= "   AND (".$DB->sql_like('s.original', ':substringoriginal', false)." OR
497                              ".$DB->sql_like('s.master', ':substringmaster', false)." OR
498                              ".$DB->sql_like('s.local', ':substringlocal', false).")";
499             $params['substringoriginal'] = '%'.$filter->substring.'%';
500             $params['substringmaster']   = '%'.$filter->substring.'%';
501             $params['substringlocal']    = '%'.$filter->substring.'%';
502         }
504         if (!empty($filter->helps)) {
505             $sql .= "   AND ".$DB->sql_like('s.stringid', '%\\\\_help', false);
506         } else {
507             $sql .= "   AND s.stringid NOT LIKE '%\\\\_link'"; //TODO: MDL-24080
508         }
510         $osql = " ORDER BY c.name, s.stringid";
512         $this->numofrows = $DB->count_records_sql($csql.$sql, $params);
513         $this->strings = $DB->get_records_sql($fsql.$sql.$osql, $params, ($this->currentpage) * self::PERPAGE, self::PERPAGE);
514     }