See Release Notes
Long Term Support Release
<?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Definition of classes used by language customization admin tool * * @package tool * @subpackage customlang * @copyright 2010 David Mudrak <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Provides various utilities to be used by the plugin * * All the public methods here are static ones, this class can not be instantiated */ class tool_customlang_utils { /** * Rough number of strings that are being processed during a full checkout. * This is used to estimate the progress of the checkout. */ const ROUGH_NUMBER_OF_STRINGS = 32000; /** @var array cache of {@link self::list_components()} results */ private static $components = null; /** * This class can not be instantiated */ private function __construct() { } /** * Returns a list of all components installed on the server * * @return array (string)legacyname => (string)frankenstylename */ public static function list_components() { if (self::$components === null) { $list['moodle'] = 'core'; $coresubsystems = core_component::get_core_subsystems(); ksort($coresubsystems); // Should be but just in case. foreach ($coresubsystems as $name => $location) { $list[$name] = 'core_' . $name; } $plugintypes = core_component::get_plugin_types(); foreach ($plugintypes as $type => $location) { $pluginlist = core_component::get_plugin_list($type); foreach ($pluginlist as $name => $ununsed) { if ($type == 'mod') { // Plugin names are now automatically validated. $list[$name] = $type . '_' . $name; } else { $list[$type . '_' . $name] = $type . '_' . $name; } } } self::$components = $list; } return self::$components; } /** * Updates the translator database with the strings from files * * This should be executed each time before going to the translation page * * @param string $lang language code to checkout * @param progress_bar $progressbar optionally, the given progress bar can be updated */ public static function checkout($lang, progress_bar $progressbar = null) { global $DB, $CFG; require_once("{$CFG->libdir}/adminlib.php"); // For behat executions we are going to load only a few components in the // language customisation structures. Using the whole "en" langpack is // too much slow (leads to Selenium 30s timeouts, especially on slow // environments) and we don't really need the whole thing for tests. So, // apart from escaping from the timeouts, we are also saving some good minutes // in tests. See MDL-70014 and linked issues for more info. $behatneeded = ['core', 'core_langconfig', 'tool_customlang']; // make sure that all components are registered $current = $DB->get_records('tool_customlang_components', null, 'name', 'name,version,id'); foreach (self::list_components() as $component) { // Filter out unwanted components when running behat. if (defined('BEHAT_SITE_RUNNING') && !in_array($component, $behatneeded)) { continue; } if (empty($current[$component])) { $record = new stdclass(); $record->name = $component; if (!$version = get_component_version($component)) { $record->version = null; } else { $record->version = $version; } $DB->insert_record('tool_customlang_components', $record); } else if ($version = get_component_version($component)) { if (is_null($current[$component]->version) or ($version > $current[$component]->version)) { $DB->set_field('tool_customlang_components', 'version', $version, array('id' => $current[$component]->id)); } } } unset($current); // initialize the progress counter - stores the number of processed strings $done = 0; $strinprogress = get_string('checkoutinprogress', 'tool_customlang'); // reload components and fetch their strings $stringman = get_string_manager(); $components = $DB->get_records('tool_customlang_components'); foreach ($components as $component) { $sql = "SELECT stringid, id, lang, componentid, original, master, local, timemodified, timecustomized, outdated, modified FROM {tool_customlang} s WHERE lang = ? AND componentid = ? ORDER BY stringid"; $current = $DB->get_records_sql($sql, array($lang, $component->id)); $english = $stringman->load_component_strings($component->name, 'en', true, true); if ($lang == 'en') { $master =& $english; } else { $master = $stringman->load_component_strings($component->name, $lang, true, true); } $local = $stringman->load_component_strings($component->name, $lang, true, false); foreach ($english as $stringid => $stringoriginal) { $stringmaster = isset($master[$stringid]) ? $master[$stringid] : null; $stringlocal = isset($local[$stringid]) ? $local[$stringid] : null; $now = time(); if (!is_null($progressbar)) { $done++; $donepercent = floor(min($done, self::ROUGH_NUMBER_OF_STRINGS) / self::ROUGH_NUMBER_OF_STRINGS * 100); $progressbar->update_full($donepercent, $strinprogress); } if (isset($current[$stringid])) { $needsupdate = false; $currentoriginal = $current[$stringid]->original; $currentmaster = $current[$stringid]->master; $currentlocal = $current[$stringid]->local; if ($currentoriginal !== $stringoriginal or $currentmaster !== $stringmaster) { $needsupdate = true; $current[$stringid]->original = $stringoriginal; $current[$stringid]->master = $stringmaster; $current[$stringid]->timemodified = $now; $current[$stringid]->outdated = 1; } if ($stringmaster !== $stringlocal) { $needsupdate = true; $current[$stringid]->local = $stringlocal; $current[$stringid]->timecustomized = $now; } else if (isset($currentlocal) && $stringlocal !== $currentlocal) { // If local string has been removed, we need to remove also the old local value from DB. $needsupdate = true; $current[$stringid]->local = null; $current[$stringid]->timecustomized = $now; } if ($needsupdate) { $DB->update_record('tool_customlang', $current[$stringid]); continue; } } else { $record = new stdclass(); $record->lang = $lang; $record->componentid = $component->id; $record->stringid = $stringid; $record->original = $stringoriginal; $record->master = $stringmaster; $record->timemodified = $now; $record->outdated = 0; if ($stringmaster !== $stringlocal) { $record->local = $stringlocal; $record->timecustomized = $now; } else { $record->local = null; $record->timecustomized = null; } $DB->insert_record('tool_customlang', $record); } } } if (!is_null($progressbar)) { $progressbar->update_full(100, get_string('checkoutdone', 'tool_customlang')); } } /** * Exports the translator database into disk files * * @param mixed $lang language code */ public static function checkin($lang) { global $DB, $USER, $CFG; require_once($CFG->libdir.'/filelib.php'); if ($lang !== clean_param($lang, PARAM_LANG)) { return false; } list($insql, $inparams) = $DB->get_in_or_equal(self::list_components()); // Get all customized strings from updated valid components. $sql = "SELECT s.*, c.name AS component FROM {tool_customlang} s JOIN {tool_customlang_components} c ON s.componentid = c.id WHERE s.lang = ? AND (s.local IS NOT NULL OR s.modified = 1) AND c.name $insql ORDER BY componentid, stringid"; array_unshift($inparams, $lang); $strings = $DB->get_records_sql($sql, $inparams); $files = array(); foreach ($strings as $string) { if (!is_null($string->local)) { $files[$string->component][$string->stringid] = $string->local; } } fulldelete(self::get_localpack_location($lang)); foreach ($files as $component => $strings) { self::dump_strings($lang, $component, $strings); } $DB->set_field_select('tool_customlang', 'modified', 0, 'lang = ?', array($lang)); $sm = get_string_manager(); $sm->reset_caches(); } /** * Returns full path to the directory where local packs are dumped into * * @param string $lang language code * @return string full path */ public static function get_localpack_location($lang) { global $CFG; return $CFG->langlocalroot.'/'.$lang.'_local'; } /** * Writes strings into a local language pack file * * @param string $component the name of the component * @param array $strings * @return void */ protected static function dump_strings($lang, $component, $strings) { global $CFG; if ($lang !== clean_param($lang, PARAM_LANG)) { throw new moodle_exception('Unable to dump local strings for non-installed language pack .'.s($lang)); } if ($component !== clean_param($component, PARAM_COMPONENT)) { throw new coding_exception('Incorrect component name'); } if (!$filename = self::get_component_filename($component)) { throw new moodle_exception('Unable to find the filename for the component '.s($component)); } if ($filename !== clean_param($filename, PARAM_FILE)) { throw new coding_exception('Incorrect file name '.s($filename)); } list($package, $subpackage) = core_component::normalize_component($component); $packageinfo = " * @package $package"; if (!is_null($subpackage)) { $packageinfo .= "\n * @subpackage $subpackage"; } $filepath = self::get_localpack_location($lang); $filepath = $filepath.'/'.$filename; if (!is_dir(dirname($filepath))) { check_dir_exists(dirname($filepath)); } if (!$f = fopen($filepath, 'w')) { throw new moodle_exception('Unable to write '.s($filepath)); } fwrite($f, <<<EOF <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Local language pack from $CFG->wwwroot * $packageinfo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); EOF ); foreach ($strings as $stringid => $text) { if ($stringid !== clean_param($stringid, PARAM_STRINGID)) { debugging('Invalid string identifier '.s($stringid)); continue; } fwrite($f, '$string[\'' . $stringid . '\'] = '); fwrite($f, var_export($text, true)); fwrite($f, ";\n"); } fclose($f); @chmod($filepath, $CFG->filepermissions); } /** * Returns the name of the file where the component's local strings should be exported into * * @param string $component normalized name of the component, eg 'core' or 'mod_workshop' * @return string|boolean filename eg 'moodle.php' or 'workshop.php', false if not found */ protected static function get_component_filename($component) { $return = false; foreach (self::list_components() as $legacy => $normalized) { if ($component === $normalized) { $return = $legacy.'.php'; break; } } return $return; } /** * Returns the number of modified strings checked out in the translator * * @param string $lang language code * @return int */ public static function get_count_of_modified($lang) { global $DB; return $DB->count_records('tool_customlang', array('lang'=>$lang, 'modified'=>1)); } /** * Saves filter data into a persistant storage such as user session * * @see self::load_filter() * @param stdclass $data filter values * @param stdclass $persistant storage object */ public static function save_filter(stdclass $data, stdclass $persistant) { if (!isset($persistant->tool_customlang_filter)) { $persistant->tool_customlang_filter = array(); } foreach ($data as $key => $value) { if ($key !== 'submit') { $persistant->tool_customlang_filter[$key] = serialize($value); } } } /** * Loads the previously saved filter settings from a persistent storage * * @see self::save_filter() * @param stdclass $persistant storage object * @return stdclass filter data */ public static function load_filter(stdclass $persistant) { $data = new stdclass(); if (isset($persistant->tool_customlang_filter)) { foreach ($persistant->tool_customlang_filter as $key => $value) { $data->{$key} = unserialize($value); } } return $data; } } /** * Represents the action menu of the tool */ class tool_customlang_menu implements renderable { /** @var menu items */ protected $items = array(); public function __construct(array $items = array()) { global $CFG; foreach ($items as $itemkey => $item) { $this->add_item($itemkey, $item['title'], $item['url'], empty($item['method']) ? 'post' : $item['method']); } } /** * Returns the menu items * * @return array (string)key => (object)[->(string)title ->(moodle_url)url ->(string)method] */ public function get_items() { return $this->items; } /** * Adds item into the menu * * @param string $key item identifier * @param string $title localized action title * @param moodle_url $url action handler * @param string $method form method */ public function add_item($key, $title, moodle_url $url, $method) { if (isset($this->items[$key])) { throw new coding_exception('Menu item already exists'); } if (empty($title) or empty($key)) { throw new coding_exception('Empty title or item key not allowed'); } $item = new stdclass(); $item->title = $title; $item->url = $url; $item->method = $method; $this->items[$key] = $item; } } /** * Represents the translation tool */ class tool_customlang_translator implements renderable {< /** @const int number of rows per page */> /** @var int number of rows per page */const PERPAGE = 100; /** @var int total number of the rows int the table */ public $numofrows = 0; /** @var moodle_url */ public $handler; /** @var string language code */ public $lang; /** @var int page to display, starting with page 0 */ public $currentpage = 0; /** @var array of stdclass strings to display */ public $strings = array(); /** @var stdclass */ protected $filter; public function __construct(moodle_url $handler, $lang, $filter, $currentpage = 0) { global $DB; $this->handler = $handler; $this->lang = $lang; $this->filter = $filter; $this->currentpage = $currentpage; if (empty($filter) or empty($filter->component)) { // nothing to do $this->currentpage = 1; return; } list($insql, $inparams) = $DB->get_in_or_equal($filter->component, SQL_PARAMS_NAMED); $csql = "SELECT COUNT(*)"; $fsql = "SELECT s.*, c.name AS component"; $sql = " FROM {tool_customlang_components} c JOIN {tool_customlang} s ON s.componentid = c.id WHERE s.lang = :lang AND c.name $insql"; $params = array_merge(array('lang' => $lang), $inparams); if (!empty($filter->customized)) { $sql .= " AND s.local IS NOT NULL"; } if (!empty($filter->modified)) { $sql .= " AND s.modified = 1"; } if (!empty($filter->stringid)) { $sql .= " AND s.stringid = :stringid"; $params['stringid'] = $filter->stringid; } if (!empty($filter->substring)) { $sql .= " AND (".$DB->sql_like('s.original', ':substringoriginal', false)." OR ".$DB->sql_like('s.master', ':substringmaster', false)." OR ".$DB->sql_like('s.local', ':substringlocal', false).")"; $params['substringoriginal'] = '%'.$filter->substring.'%'; $params['substringmaster'] = '%'.$filter->substring.'%'; $params['substringlocal'] = '%'.$filter->substring.'%'; } if (!empty($filter->helps)) { $sql .= " AND ".$DB->sql_like('s.stringid', ':help', false); //ILIKE $params['help'] = '%\_help'; } else { $sql .= " AND ".$DB->sql_like('s.stringid', ':link', false, true, true); //NOT ILIKE $params['link'] = '%\_link'; } $osql = " ORDER BY c.name, s.stringid"; $this->numofrows = $DB->count_records_sql($csql.$sql, $params); $this->strings = $DB->get_records_sql($fsql.$sql.$osql, $params, ($this->currentpage) * self::PERPAGE, self::PERPAGE); } }