Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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