MDL-21696 Added new admin report for language customization
[moodle.git] / admin / report / customlang / locallib.php
CommitLineData
03ff3b4f
DM
1<?php
2
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/>.
17
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 */
26
27defined('MOODLE_INTERNAL') || die();
28
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 */
34class report_customlang_utils {
35
36 /** @var array cache of {@link self::list_components()} results */
37 protected static $components = null;
38
39 /**
40 * This class can not be instantiated
41 */
42 private function __construct() {
43 }
44
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() {
51
52 $list['moodle'] = 'core';
53
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 }
61
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 }
76
77 return $list;
78 }
79
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;
89
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);
109
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 $current = $DB->get_records('customlang', array('lang'=>$lang, 'componentid'=>$component->id), 'stringid', 'stringid, *');
115 $english = $stringman->load_component_strings($component->name, 'en', true, true);
116 if ($lang == 'en') {
117 $master =& $english;
118 } else {
119 $master = $stringman->load_component_strings($component->name, $lang, true, true);
120 }
121 $local = $stringman->load_component_strings($component->name, $lang, true, false);
122
123 foreach ($english as $stringid => $stringoriginal) {
124 $stringmaster = isset($master[$stringid]) ? $master[$stringid] : null;
125 $stringlocal = isset($local[$stringid]) ? $local[$stringid] : null;
126 $now = time();
127
128 if (isset($current[$stringid])) {
129 $needsupdate = false;
130 $currentoriginal = $current[$stringid]->original;
131 $currentmaster = $current[$stringid]->master;
132 $currentlocal = $current[$stringid]->local;
133
134 if ($currentoriginal !== $stringoriginal or $currentmaster !== $stringmaster) {
135 $needsupdate = true;
136 $current[$stringid]->original = $stringoriginal;
137 $current[$stringid]->master = $stringmaster;
138 $current[$stringid]->timemodified = $now;
139 $current[$stringid]->outdated = 1;
140 }
141
142 if ($stringmaster !== $stringlocal) {
143 $needsupdate = true;
144 $current[$stringid]->local = $stringlocal;
145 $current[$stringid]->timecustomized = $now;
146 }
147
148 if ($needsupdate) {
149 $DB->update_record('customlang', $current[$stringid]);
150 continue;
151 }
152
153 } else {
154 $record = new stdclass();
155 $record->lang = $lang;
156 $record->componentid = $component->id;
157 $record->stringid = $stringid;
158 $record->original = $stringoriginal;
159 $record->master = $stringmaster;
160 $record->timemodified = $now;
161 $record->outdated = 0;
162 if ($stringmaster !== $stringlocal) {
163 $record->local = $stringlocal;
164 $record->timecustomized = $now;
165 } else {
166 $record->local = null;
167 $record->timecustomized = null;
168 }
169
170 $DB->insert_record('customlang', $record);
171 }
172 }
173 }
174 }
175
176 /**
177 * Exports the translator database into disk files
178 *
179 * @param mixed $lang language code
180 */
181 public static function checkin($lang) {
182 global $DB, $USER, $CFG;
183 require_once($CFG->libdir.'/filelib.php');
184
185 if ($lang !== clean_param($lang, PARAM_LANG)) {
186 return false;
187 }
188
189 // get all customized strings from updated components
190 $sql = "SELECT s.*, c.name AS component
191 FROM {customlang} s
192 JOIN {customlang_components} c ON s.componentid = c.id
193 WHERE s.lang = ?
194 AND (s.local IS NOT NULL OR s.modified = 1)
195 ORDER BY componentid, stringid";
196 $strings = $DB->get_records_sql($sql, array($lang));
197
198 $files = array();
199 foreach ($strings as $string) {
200 if (!is_null($string->local)) {
201 $files[$string->component][$string->stringid] = $string->local;
202 }
203 }
204
205 fulldelete(self::get_localpack_location($lang));
206 foreach ($files as $component => $strings) {
207 self::dump_strings($lang, $component, $strings);
208 }
209
210 $DB->set_field_select('customlang', 'modified', 0, 'lang = ?', array($lang));
211 $sm = get_string_manager();
212 $sm->reset_caches();
213 }
214
215 /**
216 * Returns full path to the directory where local packs are dumped into
217 *
218 * @param string $lang language code
219 * @return string full path
220 */
221 protected static function get_localpack_location($lang) {
222 global $CFG;
223
224 return $CFG->langlocalroot.'/'.$lang.'_local';
225 }
226
227 /**
228 * Writes strings into a local language pack file
229 *
230 * @param string $component the name of the component
231 * @param array $strings
232 */
233 protected static function dump_strings($lang, $component, $strings) {
234 global $CFG;
235
236 if ($lang !== clean_param($lang, PARAM_LANG)) {
237 debugging('Unable to dump local strings for non-installed language pack .'.s($lang));
238 return false;
239 }
240 if ($component !== clean_param($component, PARAM_SAFEDIR)) {
241 throw new coding_exception('Incorrect component name');
242 }
243 if (!$filename = self::get_component_filename($component)) {
244 debugging('Unable to find the filename for the component '.s($component));
245 return false;
246 }
247 if ($filename !== clean_param($filename, PARAM_FILE)) {
248 throw new coding_exception('Incorrect file name '.s($filename));
249 }
250 list($package, $subpackage) = normalize_component($component);
251 $packageinfo = " * @package $package";
252 if (!is_null($subpackage)) {
253 $packageinfo .= "\n * @subpackage $subpackage";
254 }
255 $filepath = self::get_localpack_location($lang);
256 if ($filepath !== clean_param($filepath, PARAM_SAFEPATH)) {
257 throw new coding_exception('Incorrect file location '.s($filepath));
258 }
259 $filepath = $filepath.'/'.$filename;
260 if (!is_dir(dirname($filepath))) {
261 mkdir(dirname($filepath), 0755, true);
262 }
263
264 if (!$f = fopen($filepath, 'w')) {
265 debugging('Unable to write '.s($filepath));
266 return false;
267 }
268 fwrite($f, <<<EOF
269<?php
270
271// This file is part of Moodle - http://moodle.org/
272//
273// Moodle is free software: you can redistribute it and/or modify
274// it under the terms of the GNU General Public License as published by
275// the Free Software Foundation, either version 3 of the License, or
276// (at your option) any later version.
277//
278// Moodle is distributed in the hope that it will be useful,
279// but WITHOUT ANY WARRANTY; without even the implied warranty of
280// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
281// GNU General Public License for more details.
282//
283// You should have received a copy of the GNU General Public License
284// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
285
286/**
287 * Local language pack from $CFG->wwwroot
288 *
289$packageinfo
290 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
291 */
292
293defined('MOODLE_INTERNAL') || die();
294
295
296EOF
297 );
298
299 foreach ($strings as $stringid => $text) {
300 if ($stringid !== clean_param($stringid, PARAM_STRINGID)) {
301 debugging('Invalid string identifier '.s($stringid));
302 continue;
303 }
304 fwrite($f, '$string[\'' . $stringid . '\'] = ');
305 fwrite($f, var_export($text, true));
306 fwrite($f, ";\n");
307 }
308 fclose($f);
309 }
310
311 /**
312 * Returns the name of the file where the component's local strings should be exported into
313 *
314 * @param string $component normalized name of the component, eg 'core' or 'mod_workshop'
315 * @return string|boolean filename eg 'moodle.php' or 'workshop.php', false if not found
316 */
317 protected static function get_component_filename($component) {
318 if (is_null(self::$components)) {
319 self::$components = self::list_components();
320 }
321 $return = false;
322 foreach (self::$components as $legacy => $normalized) {
323 if ($component === $normalized) {
324 $return = $legacy.'.php';
325 break;
326 }
327 }
328 return $return;
329 }
330
331 /**
332 * Returns the number of modified strings checked out in the translator
333 *
334 * @param string $lang language code
335 * @return int
336 */
337 public static function get_count_of_modified($lang) {
338 global $DB;
339
340 return $DB->count_records('customlang', array('lang'=>$lang, 'modified'=>1));
341 }
342
343 /**
344 * Saves filter data into a persistant storage such as user session
345 *
346 * @see self::load_filter()
347 * @param stdclass $data filter values
348 * @param stdclass $persistant storage object
349 */
350 public static function save_filter(stdclass $data, stdclass $persistant) {
351 if (!isset($persistant->report_customlang_filter)) {
352 $persistant->report_customlang_filter = array();
353 }
354 foreach ($data as $key => $value) {
355 if ($key !== 'submit') {
356 $persistant->report_customlang_filter[$key] = serialize($value);
357 }
358 }
359 }
360
361 /**
362 * Loads the previously saved filter settings from a persistent storage
363 *
364 * @see self::save_filter()
365 * @param stdclass $persistant storage object
366 * @return stdclass filter data
367 */
368 public static function load_filter(stdclass $persistant) {
369 $data = new stdclass();
370 if (isset($persistant->report_customlang_filter)) {
371 foreach ($persistant->report_customlang_filter as $key => $value) {
372 $data->{$key} = unserialize($value);
373 }
374 }
375 return $data;
376 }
377}
378
379/**
380 * Represents the action menu of the report
381 */
382class report_customlang_menu implements renderable {
383
384 /** @var menu items */
385 protected $items = array();
386
387 public function __construct(array $items = array()) {
388 global $CFG;
389
390 foreach ($items as $itemkey => $item) {
391 $this->add_item($itemkey, $item['title'], $item['url'], empty($item['method']) ? 'post' : $item['method']);
392 }
393 }
394
395 /**
396 * Returns the menu items
397 *
398 * @return array (string)key => (object)[->(string)title ->(moodl_url)url ->(string)method]
399 */
400 public function get_items() {
401 return $this->items;
402 }
403
404 /**
405 * Adds item into the menu
406 *
407 * @param string $key item identifier
408 * @param string $title localized action title
409 * @param moodle_url $url action handler
410 * @param string $method form method
411 */
412 public function add_item($key, $title, moodle_url $url, $method) {
413 if (isset($this->items[$key])) {
414 throw new coding_error('Menu item already exists');
415 }
416 if (empty($title) or empty($key)) {
417 throw new coding_error('Empty title or item key not allowed');
418 }
419 $item = new stdclass();
420 $item->title = $title;
421 $item->url = $url;
422 $item->method = $method;
423 $this->items[$key] = $item;
424 }
425}
426
427/**
428 * Represents the translation tool
429 */
430class report_customlang_translator implements renderable {
431
432 /** @const int number of rows per page */
433 const PERPAGE = 100;
434
435 /** @var int total number of the rows int the table */
436 public $numofrows = 0;
437
438 /** @var moodle_url */
439 public $handler;
440
441 /** @var string language code */
442 public $lang;
443
444 /** @var int page to display, starting with page 0 */
445 public $currentpage = 0;
446
447 /** @var array of stdclass strings to display */
448 public $strings = array();
449
450 /** @var stdclass */
451 protected $filter;
452
453 public function __construct(moodle_url $handler, $lang, $filter, $currentpage = 0) {
454 global $DB;
455
456 $this->handler = $handler;
457 $this->lang = $lang;
458 $this->filter = $filter;
459 $this->currentpage = $currentpage;
460
461 if (empty($filter) or empty($filter->component)) {
462 // nothing to do
463 $this->currentpage = 1;
464 return;
465 }
466
467 list($insql, $inparams) = $DB->get_in_or_equal($filter->component, SQL_PARAMS_NAMED);
468
469 $csql = "SELECT COUNT(*)";
470 $fsql = "SELECT s.id, s.*, c.name AS component";
471 $sql = " FROM {customlang_components} c
472 JOIN {customlang} s ON s.componentid = c.id
473 WHERE s.lang = :lang
474 AND c.name $insql";
475
476 $params = array_merge(array('lang' => $lang), $inparams);
477
478 if (!empty($filter->customized)) {
479 $sql .= " AND s.local IS NOT NULL";
480 }
481
482 if (!empty($filter->modified)) {
483 $sql .= " AND s.modified = 1";
484 }
485
486 if (!empty($filter->stringid)) {
487 $sql .= " AND s.stringid = :stringid";
488 $params['stringid'] = $filter->stringid;
489 }
490
491 if (!empty($filter->substring)) {
492 $sql .= " AND (s.original ".$DB->sql_ilike()." :substringoriginal OR
493 s.master ".$DB->sql_ilike()." :substringmaster OR
494 s.local ".$DB->sql_ilike()." :substringlocal)";
495 $params['substringoriginal'] = '%'.$filter->substring.'%';
496 $params['substringmaster'] = '%'.$filter->substring.'%';
497 $params['substringlocal'] = '%'.$filter->substring.'%';
498 }
499
500 if (!empty($filter->helps)) {
501 $sql .= " AND s.stringid ".$DB->sql_ilike()." '%\\\\_help'";
502 } else {
503 $sql .= " AND s.stringid NOT ".$DB->sql_ilike()." '%\\\\_link'";
504 }
505
506 $osql = " ORDER BY c.name, s.stringid";
507
508 $this->numofrows = $DB->count_records_sql($csql.$sql, $params);
509 $this->strings = $DB->get_records_sql($fsql.$sql.$osql, $params, ($this->currentpage) * self::PERPAGE, self::PERPAGE);
510 }
511}