See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 39 and 401] [Versions 401 and 403]
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/>. 16 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 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 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 { 34 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 = 32000; 40 41 /** @var array cache of {@link self::list_components()} results */ 42 private static $components = null; 43 44 /** 45 * This class can not be instantiated 46 */ 47 private function __construct() { 48 } 49 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() { 56 57 if (self::$components === null) { 58 $list['moodle'] = 'core'; 59 60 $coresubsystems = core_component::get_core_subsystems(); 61 ksort($coresubsystems); // Should be but just in case. 62 foreach ($coresubsystems as $name => $location) { 63 $list[$name] = 'core_' . $name; 64 } 65 66 $plugintypes = core_component::get_plugin_types(); 67 foreach ($plugintypes as $type => $location) { 68 $pluginlist = core_component::get_plugin_list($type); 69 foreach ($pluginlist as $name => $ununsed) { 70 if ($type == 'mod') { 71 // Plugin names are now automatically validated. 72 $list[$name] = $type . '_' . $name; 73 } else { 74 $list[$type . '_' . $name] = $type . '_' . $name; 75 } 76 } 77 } 78 self::$components = $list; 79 } 80 return self::$components; 81 } 82 83 /** 84 * Updates the translator database with the strings from files 85 * 86 * This should be executed each time before going to the translation page 87 * 88 * @param string $lang language code to checkout 89 * @param progress_bar $progressbar optionally, the given progress bar can be updated 90 */ 91 public static function checkout($lang, progress_bar $progressbar = null) { 92 global $DB, $CFG; 93 94 require_once("{$CFG->libdir}/adminlib.php"); 95 96 // For behat executions we are going to load only a few components in the 97 // language customisation structures. Using the whole "en" langpack is 98 // too much slow (leads to Selenium 30s timeouts, especially on slow 99 // environments) and we don't really need the whole thing for tests. So, 100 // apart from escaping from the timeouts, we are also saving some good minutes 101 // in tests. See MDL-70014 and linked issues for more info. 102 $behatneeded = ['core', 'core_langconfig', 'tool_customlang']; 103 104 // make sure that all components are registered 105 $current = $DB->get_records('tool_customlang_components', null, 'name', 'name,version,id'); 106 foreach (self::list_components() as $component) { 107 // Filter out unwanted components when running behat. 108 if (defined('BEHAT_SITE_RUNNING') && !in_array($component, $behatneeded)) { 109 continue; 110 } 111 112 if (empty($current[$component])) { 113 $record = new stdclass(); 114 $record->name = $component; 115 if (!$version = get_component_version($component)) { 116 $record->version = null; 117 } else { 118 $record->version = $version; 119 } 120 $DB->insert_record('tool_customlang_components', $record); 121 } else if ($version = get_component_version($component)) { 122 if (is_null($current[$component]->version) or ($version > $current[$component]->version)) { 123 $DB->set_field('tool_customlang_components', 'version', $version, array('id' => $current[$component]->id)); 124 } 125 } 126 } 127 unset($current); 128 129 // initialize the progress counter - stores the number of processed strings 130 $done = 0; 131 $strinprogress = get_string('checkoutinprogress', 'tool_customlang'); 132 133 // reload components and fetch their strings 134 $stringman = get_string_manager(); 135 $components = $DB->get_records('tool_customlang_components'); 136 foreach ($components as $component) { 137 $sql = "SELECT stringid, id, lang, componentid, original, master, local, timemodified, timecustomized, outdated, modified 138 FROM {tool_customlang} s 139 WHERE lang = ? AND componentid = ? 140 ORDER BY stringid"; 141 $current = $DB->get_records_sql($sql, array($lang, $component->id)); 142 $english = $stringman->load_component_strings($component->name, 'en', true, true); 143 if ($lang == 'en') { 144 $master =& $english; 145 } else { 146 $master = $stringman->load_component_strings($component->name, $lang, true, true); 147 } 148 $local = $stringman->load_component_strings($component->name, $lang, true, false); 149 150 foreach ($english as $stringid => $stringoriginal) { 151 $stringmaster = isset($master[$stringid]) ? $master[$stringid] : null; 152 $stringlocal = isset($local[$stringid]) ? $local[$stringid] : null; 153 $now = time(); 154 155 if (!is_null($progressbar)) { 156 $done++; 157 $donepercent = floor(min($done, self::ROUGH_NUMBER_OF_STRINGS) / self::ROUGH_NUMBER_OF_STRINGS * 100); 158 $progressbar->update_full($donepercent, $strinprogress); 159 } 160 161 if (isset($current[$stringid])) { 162 $needsupdate = false; 163 $currentoriginal = $current[$stringid]->original; 164 $currentmaster = $current[$stringid]->master; 165 $currentlocal = $current[$stringid]->local; 166 167 if ($currentoriginal !== $stringoriginal or $currentmaster !== $stringmaster) { 168 $needsupdate = true; 169 $current[$stringid]->original = $stringoriginal; 170 $current[$stringid]->master = $stringmaster; 171 $current[$stringid]->timemodified = $now; 172 $current[$stringid]->outdated = 1; 173 } 174 175 if ($stringmaster !== $stringlocal) { 176 $needsupdate = true; 177 $current[$stringid]->local = $stringlocal; 178 $current[$stringid]->timecustomized = $now; 179 } else if (isset($currentlocal) && $stringlocal !== $currentlocal) { 180 // If local string has been removed, we need to remove also the old local value from DB. 181 $needsupdate = true; 182 $current[$stringid]->local = null; 183 $current[$stringid]->timecustomized = $now; 184 } 185 186 if ($needsupdate) { 187 $DB->update_record('tool_customlang', $current[$stringid]); 188 continue; 189 } 190 191 } else { 192 $record = new stdclass(); 193 $record->lang = $lang; 194 $record->componentid = $component->id; 195 $record->stringid = $stringid; 196 $record->original = $stringoriginal; 197 $record->master = $stringmaster; 198 $record->timemodified = $now; 199 $record->outdated = 0; 200 if ($stringmaster !== $stringlocal) { 201 $record->local = $stringlocal; 202 $record->timecustomized = $now; 203 } else { 204 $record->local = null; 205 $record->timecustomized = null; 206 } 207 208 $DB->insert_record('tool_customlang', $record); 209 } 210 } 211 } 212 213 if (!is_null($progressbar)) { 214 $progressbar->update_full(100, get_string('checkoutdone', 'tool_customlang')); 215 } 216 } 217 218 /** 219 * Exports the translator database into disk files 220 * 221 * @param mixed $lang language code 222 */ 223 public static function checkin($lang) { 224 global $DB, $USER, $CFG; 225 require_once($CFG->libdir.'/filelib.php'); 226 227 if ($lang !== clean_param($lang, PARAM_LANG)) { 228 return false; 229 } 230 231 list($insql, $inparams) = $DB->get_in_or_equal(self::list_components()); 232 233 // Get all customized strings from updated valid components. 234 $sql = "SELECT s.*, c.name AS component 235 FROM {tool_customlang} s 236 JOIN {tool_customlang_components} c ON s.componentid = c.id 237 WHERE s.lang = ? 238 AND (s.local IS NOT NULL OR s.modified = 1) 239 AND c.name $insql 240 ORDER BY componentid, stringid"; 241 array_unshift($inparams, $lang); 242 $strings = $DB->get_records_sql($sql, $inparams); 243 244 $files = array(); 245 foreach ($strings as $string) { 246 if (!is_null($string->local)) { 247 $files[$string->component][$string->stringid] = $string->local; 248 } 249 } 250 251 fulldelete(self::get_localpack_location($lang)); 252 foreach ($files as $component => $strings) { 253 self::dump_strings($lang, $component, $strings); 254 } 255 256 $DB->set_field_select('tool_customlang', 'modified', 0, 'lang = ?', array($lang)); 257 $sm = get_string_manager(); 258 $sm->reset_caches(); 259 } 260 261 /** 262 * Returns full path to the directory where local packs are dumped into 263 * 264 * @param string $lang language code 265 * @return string full path 266 */ 267 public static function get_localpack_location($lang) { 268 global $CFG; 269 270 return $CFG->langlocalroot.'/'.$lang.'_local'; 271 } 272 273 /** 274 * Writes strings into a local language pack file 275 * 276 * @param string $component the name of the component 277 * @param array $strings 278 * @return void 279 */ 280 protected static function dump_strings($lang, $component, $strings) { 281 global $CFG; 282 283 if ($lang !== clean_param($lang, PARAM_LANG)) { 284 throw new moodle_exception('Unable to dump local strings for non-installed language pack .'.s($lang)); 285 } 286 if ($component !== clean_param($component, PARAM_COMPONENT)) { 287 throw new coding_exception('Incorrect component name'); 288 } 289 if (!$filename = self::get_component_filename($component)) { 290 throw new moodle_exception('Unable to find the filename for the component '.s($component)); 291 } 292 if ($filename !== clean_param($filename, PARAM_FILE)) { 293 throw new coding_exception('Incorrect file name '.s($filename)); 294 } 295 list($package, $subpackage) = core_component::normalize_component($component); 296 $packageinfo = " * @package $package"; 297 if (!is_null($subpackage)) { 298 $packageinfo .= "\n * @subpackage $subpackage"; 299 } 300 $filepath = self::get_localpack_location($lang); 301 $filepath = $filepath.'/'.$filename; 302 if (!is_dir(dirname($filepath))) { 303 check_dir_exists(dirname($filepath)); 304 } 305 306 if (!$f = fopen($filepath, 'w')) { 307 throw new moodle_exception('Unable to write '.s($filepath)); 308 } 309 fwrite($f, <<<EOF 310 <?php 311 312 // This file is part of Moodle - http://moodle.org/ 313 // 314 // Moodle is free software: you can redistribute it and/or modify 315 // it under the terms of the GNU General Public License as published by 316 // the Free Software Foundation, either version 3 of the License, or 317 // (at your option) any later version. 318 // 319 // Moodle is distributed in the hope that it will be useful, 320 // but WITHOUT ANY WARRANTY; without even the implied warranty of 321 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 322 // GNU General Public License for more details. 323 // 324 // You should have received a copy of the GNU General Public License 325 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 326 327 /** 328 * Local language pack from $CFG->wwwroot 329 * 330 $packageinfo 331 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 332 */ 333 334 defined('MOODLE_INTERNAL') || die(); 335 336 337 EOF 338 ); 339 340 foreach ($strings as $stringid => $text) { 341 if ($stringid !== clean_param($stringid, PARAM_STRINGID)) { 342 debugging('Invalid string identifier '.s($stringid)); 343 continue; 344 } 345 fwrite($f, '$string[\'' . $stringid . '\'] = '); 346 fwrite($f, var_export($text, true)); 347 fwrite($f, ";\n"); 348 } 349 fclose($f); 350 @chmod($filepath, $CFG->filepermissions); 351 } 352 353 /** 354 * Returns the name of the file where the component's local strings should be exported into 355 * 356 * @param string $component normalized name of the component, eg 'core' or 'mod_workshop' 357 * @return string|boolean filename eg 'moodle.php' or 'workshop.php', false if not found 358 */ 359 protected static function get_component_filename($component) { 360 361 $return = false; 362 foreach (self::list_components() as $legacy => $normalized) { 363 if ($component === $normalized) { 364 $return = $legacy.'.php'; 365 break; 366 } 367 } 368 return $return; 369 } 370 371 /** 372 * Returns the number of modified strings checked out in the translator 373 * 374 * @param string $lang language code 375 * @return int 376 */ 377 public static function get_count_of_modified($lang) { 378 global $DB; 379 380 return $DB->count_records('tool_customlang', array('lang'=>$lang, 'modified'=>1)); 381 } 382 383 /** 384 * Saves filter data into a persistant storage such as user session 385 * 386 * @see self::load_filter() 387 * @param stdclass $data filter values 388 * @param stdclass $persistant storage object 389 */ 390 public static function save_filter(stdclass $data, stdclass $persistant) { 391 if (!isset($persistant->tool_customlang_filter)) { 392 $persistant->tool_customlang_filter = array(); 393 } 394 foreach ($data as $key => $value) { 395 if ($key !== 'submit') { 396 $persistant->tool_customlang_filter[$key] = serialize($value); 397 } 398 } 399 } 400 401 /** 402 * Loads the previously saved filter settings from a persistent storage 403 * 404 * @see self::save_filter() 405 * @param stdclass $persistant storage object 406 * @return stdclass filter data 407 */ 408 public static function load_filter(stdclass $persistant) { 409 $data = new stdclass(); 410 if (isset($persistant->tool_customlang_filter)) { 411 foreach ($persistant->tool_customlang_filter as $key => $value) { 412 $data->{$key} = unserialize($value); 413 } 414 } 415 return $data; 416 } 417 } 418 419 /** 420 * Represents the action menu of the tool 421 */ 422 class tool_customlang_menu implements renderable { 423 424 /** @var menu items */ 425 protected $items = array(); 426 427 public function __construct(array $items = array()) { 428 global $CFG; 429 430 foreach ($items as $itemkey => $item) { 431 $this->add_item($itemkey, $item['title'], $item['url'], empty($item['method']) ? 'post' : $item['method']); 432 } 433 } 434 435 /** 436 * Returns the menu items 437 * 438 * @return array (string)key => (object)[->(string)title ->(moodle_url)url ->(string)method] 439 */ 440 public function get_items() { 441 return $this->items; 442 } 443 444 /** 445 * Adds item into the menu 446 * 447 * @param string $key item identifier 448 * @param string $title localized action title 449 * @param moodle_url $url action handler 450 * @param string $method form method 451 */ 452 public function add_item($key, $title, moodle_url $url, $method) { 453 if (isset($this->items[$key])) { 454 throw new coding_exception('Menu item already exists'); 455 } 456 if (empty($title) or empty($key)) { 457 throw new coding_exception('Empty title or item key not allowed'); 458 } 459 $item = new stdclass(); 460 $item->title = $title; 461 $item->url = $url; 462 $item->method = $method; 463 $this->items[$key] = $item; 464 } 465 } 466 467 /** 468 * Represents the translation tool 469 */ 470 class tool_customlang_translator implements renderable { 471 472 /** @const int number of rows per page */ 473 const PERPAGE = 100; 474 475 /** @var int total number of the rows int the table */ 476 public $numofrows = 0; 477 478 /** @var moodle_url */ 479 public $handler; 480 481 /** @var string language code */ 482 public $lang; 483 484 /** @var int page to display, starting with page 0 */ 485 public $currentpage = 0; 486 487 /** @var array of stdclass strings to display */ 488 public $strings = array(); 489 490 /** @var stdclass */ 491 protected $filter; 492 493 public function __construct(moodle_url $handler, $lang, $filter, $currentpage = 0) { 494 global $DB; 495 496 $this->handler = $handler; 497 $this->lang = $lang; 498 $this->filter = $filter; 499 $this->currentpage = $currentpage; 500 501 if (empty($filter) or empty($filter->component)) { 502 // nothing to do 503 $this->currentpage = 1; 504 return; 505 } 506 507 list($insql, $inparams) = $DB->get_in_or_equal($filter->component, SQL_PARAMS_NAMED); 508 509 $csql = "SELECT COUNT(*)"; 510 $fsql = "SELECT s.*, c.name AS component"; 511 $sql = " FROM {tool_customlang_components} c 512 JOIN {tool_customlang} s ON s.componentid = c.id 513 WHERE s.lang = :lang 514 AND c.name $insql"; 515 516 $params = array_merge(array('lang' => $lang), $inparams); 517 518 if (!empty($filter->customized)) { 519 $sql .= " AND s.local IS NOT NULL"; 520 } 521 522 if (!empty($filter->modified)) { 523 $sql .= " AND s.modified = 1"; 524 } 525 526 if (!empty($filter->stringid)) { 527 $sql .= " AND s.stringid = :stringid"; 528 $params['stringid'] = $filter->stringid; 529 } 530 531 if (!empty($filter->substring)) { 532 $sql .= " AND (".$DB->sql_like('s.original', ':substringoriginal', false)." OR 533 ".$DB->sql_like('s.master', ':substringmaster', false)." OR 534 ".$DB->sql_like('s.local', ':substringlocal', false).")"; 535 $params['substringoriginal'] = '%'.$filter->substring.'%'; 536 $params['substringmaster'] = '%'.$filter->substring.'%'; 537 $params['substringlocal'] = '%'.$filter->substring.'%'; 538 } 539 540 if (!empty($filter->helps)) { 541 $sql .= " AND ".$DB->sql_like('s.stringid', ':help', false); //ILIKE 542 $params['help'] = '%\_help'; 543 } else { 544 $sql .= " AND ".$DB->sql_like('s.stringid', ':link', false, true, true); //NOT ILIKE 545 $params['link'] = '%\_link'; 546 } 547 548 $osql = " ORDER BY c.name, s.stringid"; 549 550 $this->numofrows = $DB->count_records_sql($csql.$sql, $params); 551 $this->strings = $DB->get_records_sql($fsql.$sql.$osql, $params, ($this->currentpage) * self::PERPAGE, self::PERPAGE); 552 } 553 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body