Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   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   * Custom lang importer.
  19   *
  20   * @package    tool_customlang
  21   * @copyright  2020 Ferran Recio <ferran@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace tool_customlang\local;
  26  
  27  use tool_customlang\local\mlang\phpparser;
  28  use tool_customlang\local\mlang\logstatus;
  29  use tool_customlang\local\mlang\langstring;
  30  use core\output\notification;
  31  use stored_file;
  32  use coding_exception;
  33  use moodle_exception;
  34  use core_component;
  35  use stdClass;
  36  
  37  /**
  38   * Class containing tha custom lang importer
  39   *
  40   * @package    tool_customlang
  41   * @copyright  2020 Ferran Recio <ferran@moodle.com>
  42   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   */
  44  class importer {
  45  
  46      /** @var int imports will only create new customizations */
  47      public const IMPORTNEW = 1;
  48      /** @var int imports will only update the current customizations */
  49      public const IMPORTUPDATE = 2;
  50      /** @var int imports all strings */
  51      public const IMPORTALL = 3;
  52  
  53      /**
  54       * @var string the language name
  55       */
  56      protected $lng;
  57  
  58      /**
  59       * @var int the importation mode (new, update, all)
  60       */
  61      protected $importmode;
  62  
  63      /**
  64       * @var string request folder path
  65       */
  66      private $folder;
  67  
  68      /**
  69       * @var array import log messages
  70       */
  71      private $log;
  72  
  73      /**
  74       * Constructor for the importer class.
  75       *
  76       * @param string $lng the current language to import.
  77       * @param int $importmode the import method (IMPORTALL, IMPORTNEW, IMPORTUPDATE).
  78       */
  79      public function __construct(string $lng, int $importmode = self::IMPORTALL) {
  80          $this->lng = $lng;
  81          $this->importmode = $importmode;
  82          $this->log = [];
  83      }
  84  
  85      /**
  86       * Returns the last parse log.
  87       *
  88       * @return logstatus[] mlang logstatus with the messages
  89       */
  90      public function get_log(): array {
  91          return $this->log;
  92      }
  93  
  94      /**
  95       * Import customlang files.
  96       *
  97       * @param stored_file[] $files array of files to import
  98       */
  99      public function import(array $files): void {
 100          // Create a temporal folder to store the files.
 101          $this->folder = make_request_directory(false);
 102  
 103          $langfiles = $this->deploy_files($files);
 104  
 105          $this->process_files($langfiles);
 106      }
 107  
 108      /**
 109       * Deploy all files into a request folder.
 110       *
 111       * @param stored_file[] $files array of files to deploy
 112       * @return string[] of file paths
 113       */
 114      private function deploy_files(array $files): array {
 115          $result = [];
 116          // Desploy all files.
 117          foreach ($files as $file) {
 118              if ($file->get_mimetype() == 'application/zip') {
 119                  $result = array_merge($result, $this->unzip_file($file));
 120              } else {
 121                  $path = $this->folder.'/'.$file->get_filename();
 122                  $file->copy_content_to($path);
 123                  $result = array_merge($result, [$path]);
 124              }
 125          }
 126          return $result;
 127      }
 128  
 129      /**
 130       * Unzip a file into the request folder.
 131       *
 132       * @param stored_file $file the zip file to unzip
 133       * @return string[] of zip content paths
 134       */
 135      private function unzip_file(stored_file $file): array {
 136          $fp = get_file_packer('application/zip');
 137          $zipcontents = $fp->extract_to_pathname($file, $this->folder);
 138          if (!$zipcontents) {
 139              throw new moodle_exception("Error Unzipping file", 1);
 140          }
 141          $result = [];
 142          foreach ($zipcontents as $contentname => $success) {
 143              if ($success) {
 144                  $result[] = $this->folder.'/'.$contentname;
 145              }
 146          }
 147          return $result;
 148      }
 149  
 150      /**
 151       * Import strings from a list of langfiles.
 152       *
 153       * @param string[] $langfiles an array with file paths
 154       */
 155      private function process_files(array $langfiles): void {
 156          $parser = phpparser::get_instance();
 157          foreach ($langfiles as $filepath) {
 158              $component = $this->component_from_filepath($filepath);
 159              if ($component) {
 160                  $strings = $parser->parse(file_get_contents($filepath));
 161                  $this->import_strings($strings, $component);
 162              }
 163          }
 164      }
 165  
 166      /**
 167       * Try to get the component from a filepath.
 168       *
 169       * @param string $filepath the filepath
 170       * @return stdCalss|null the DB record of that component
 171       */
 172      private function component_from_filepath(string $filepath) {
 173          global $DB;
 174  
 175          // Get component from filename.
 176          $pathparts = pathinfo($filepath);
 177          if (empty($pathparts['filename'])) {
 178              throw new coding_exception("Cannot get filename from $filepath", 1);
 179          }
 180          $filename = $pathparts['filename'];
 181  
 182          $normalized = core_component::normalize_component($filename);
 183          if (count($normalized) == 1 || empty($normalized[1])) {
 184              $componentname = $normalized[0];
 185          } else {
 186              $componentname = implode('_', $normalized);
 187          }
 188  
 189          $result = $DB->get_record('tool_customlang_components', ['name' => $componentname]);
 190  
 191          if (!$result) {
 192              $this->log[] = new logstatus('notice_missingcomponent', notification::NOTIFY_ERROR, null, $componentname);
 193              return null;
 194          }
 195          return $result;
 196      }
 197  
 198      /**
 199       * Import an array of strings into the customlang tables.
 200       *
 201       * @param langstring[] $strings the langstring to set
 202       * @param stdClass $component the target component
 203       */
 204      private function import_strings(array $strings, stdClass $component): void {
 205          global $DB;
 206  
 207          foreach ($strings as $newstring) {
 208              // Check current DB entry.
 209              $customlang = $DB->get_record('tool_customlang', [
 210                  'componentid' => $component->id,
 211                  'stringid' => $newstring->id,
 212                  'lang' => $this->lng,
 213              ]);
 214              if (!$customlang) {
 215                  $customlang = null;
 216              }
 217  
 218              if ($this->can_save_string($customlang, $newstring, $component)) {
 219                  $customlang->local = $newstring->text;
 220                  $customlang->timecustomized = $newstring->timemodified;
 221                  $customlang->outdated = 0;
 222                  $customlang->modified = 1;
 223                  $DB->update_record('tool_customlang', $customlang);
 224              }
 225          }
 226      }
 227  
 228      /**
 229       * Determine if a specific string can be saved based on the current importmode.
 230       *
 231       * @param stdClass $customlang customlang original record
 232       * @param langstring $newstring the new strign to store
 233       * @param stdClass $component the component target
 234       * @return bool if the string can be stored
 235       */
 236      private function can_save_string(?stdClass $customlang, langstring $newstring, stdClass $component): bool {
 237          $result = false;
 238          $message = 'notice_success';
 239          if (empty($customlang)) {
 240              $message = 'notice_inexitentstring';
 241              $this->log[] = new logstatus($message, notification::NOTIFY_ERROR, null, $component->name, $newstring);
 242              return $result;
 243          }
 244  
 245          switch ($this->importmode) {
 246              case self::IMPORTNEW:
 247                  $result = empty($customlang->local);
 248                  $warningmessage = 'notice_ignoreupdate';
 249                  break;
 250              case self::IMPORTUPDATE:
 251                  $result = !empty($customlang->local);
 252                  $warningmessage = 'notice_ignorenew';
 253                  break;
 254              case self::IMPORTALL:
 255                  $result = true;
 256                  break;
 257          }
 258          if ($result) {
 259              $errorlevel = notification::NOTIFY_SUCCESS;
 260          } else {
 261              $errorlevel = notification::NOTIFY_ERROR;
 262              $message = $warningmessage;
 263          }
 264          $this->log[] = new logstatus($message, $errorlevel, null, $component->name, $newstring);
 265  
 266          return $result;
 267      }
 268  }