Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.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   * Mlang PHP based on David Mudrak phpparser for local_amos.
  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\mlang;
  26  
  27  use coding_exception;
  28  use moodle_exception;
  29  
  30  /**
  31   * Parser of Moodle strings defined as associative array.
  32   *
  33   * Moodle core just includes this file format directly as normal PHP code. However
  34   * for security reasons, we must not do this for files uploaded by anonymous users.
  35   * This parser reconstructs the associative $string array without actually including
  36   * the file.
  37   */
  38  class phpparser {
  39  
  40      /** @var holds the singleton instance of self */
  41      private static $instance = null;
  42  
  43      /**
  44       * Prevents direct creation of object
  45       */
  46      private function __construct() {
  47      }
  48  
  49      /**
  50       * Prevent from cloning the instance
  51       */
  52      public function __clone() {
  53          throw new coding_exception('Cloning os singleton is not allowed');
  54      }
  55  
  56      /**
  57       * Get the singleton instance fo this class
  58       *
  59       * @return phpparser singleton instance of phpparser
  60       */
  61      public static function get_instance(): phpparser {
  62          if (is_null(self::$instance)) {
  63              self::$instance = new phpparser();
  64          }
  65          return self::$instance;
  66      }
  67  
  68      /**
  69       * Parses the given data in Moodle PHP string format
  70       *
  71       * Note: This method is adapted from local_amos as it is highly tested and robust.
  72       * The priority is keeping it similar to the original one to make it easier to mantain.
  73       *
  74       * @param string $data definition of the associative array
  75       * @param int $format the data format on the input, defaults to the one used since 2.0
  76       * @return langstring[] array of langstrings of this file
  77       */
  78      public function parse(string $data, int $format = 2): array {
  79          $result = [];
  80          $strings = $this->extract_strings($data);
  81          foreach ($strings as $id => $text) {
  82              $cleaned = clean_param($id, PARAM_STRINGID);
  83              if ($cleaned !== $id) {
  84                  continue;
  85              }
  86              $text = langstring::fix_syntax($text, 2, $format);
  87              $result[] = new langstring($id, $text);
  88          }
  89          return $result;
  90      }
  91  
  92      /**
  93       * Low level parsing method
  94       *
  95       * Note: This method is adapted from local_amos as it is highly tested and robust.
  96       * The priority is keeping it similar to the original one to make it easier to mantain.
  97       *
  98       * @param string $data
  99       * @return string[] the data strings
 100       */
 101      protected function extract_strings(string $data): array {
 102  
 103          $strings = []; // To be returned.
 104  
 105          if (empty($data)) {
 106              return $strings;
 107          }
 108  
 109          // Tokenize data - we expect valid PHP code.
 110          $tokens = token_get_all($data);
 111  
 112          // Get rid of all non-relevant tokens.
 113          $rubbish = [T_WHITESPACE, T_INLINE_HTML, T_COMMENT, T_DOC_COMMENT, T_OPEN_TAG, T_CLOSE_TAG];
 114          foreach ($tokens as $i => $token) {
 115              if (is_array($token)) {
 116                  if (in_array($token[0], $rubbish)) {
 117                      unset($tokens[$i]);
 118                  }
 119              }
 120          }
 121  
 122          $id = null;
 123          $text = null;
 124          $line = 0;
 125          $expect = 'STRING_VAR'; // The first expected token is '$string'.
 126  
 127          // Iterate over tokens and look for valid $string array assignment patterns.
 128          foreach ($tokens as $token) {
 129              $foundtype = null;
 130              $founddata = null;
 131              if (is_array($token)) {
 132                  $foundtype = $token[0];
 133                  $founddata = $token[1];
 134                  if (!empty($token[2])) {
 135                      $line = $token[2];
 136                  }
 137  
 138              } else {
 139                  $foundtype = 'char';
 140                  $founddata = $token;
 141              }
 142  
 143              if ($expect == 'STRING_VAR') {
 144                  if ($foundtype === T_VARIABLE and $founddata === '$string') {
 145                      $expect = 'LEFT_BRACKET';
 146                      continue;
 147                  } else {
 148                      // Allow other code at the global level.
 149                      continue;
 150                  }
 151              }
 152  
 153              if ($expect == 'LEFT_BRACKET') {
 154                  if ($foundtype === 'char' and $founddata === '[') {
 155                      $expect = 'STRING_ID';
 156                      continue;
 157                  } else {
 158                      throw new moodle_exception('Parsing error. Expected character [ at line '.$line);
 159                  }
 160              }
 161  
 162              if ($expect == 'STRING_ID') {
 163                  if ($foundtype === T_CONSTANT_ENCAPSED_STRING) {
 164                      $id = $this->decapsulate($founddata);
 165                      $expect = 'RIGHT_BRACKET';
 166                      continue;
 167                  } else {
 168                      throw new moodle_exception('Parsing error. Expected T_CONSTANT_ENCAPSED_STRING array key at line '.$line);
 169                  }
 170              }
 171  
 172              if ($expect == 'RIGHT_BRACKET') {
 173                  if ($foundtype === 'char' and $founddata === ']') {
 174                      $expect = 'ASSIGNMENT';
 175                      continue;
 176                  } else {
 177                      throw new moodle_exception('Parsing error. Expected character ] at line '.$line);
 178                  }
 179              }
 180  
 181              if ($expect == 'ASSIGNMENT') {
 182                  if ($foundtype === 'char' and $founddata === '=') {
 183                      $expect = 'STRING_TEXT';
 184                      continue;
 185                  } else {
 186                      throw new moodle_exception('Parsing error. Expected character = at line '.$line);
 187                  }
 188              }
 189  
 190              if ($expect == 'STRING_TEXT') {
 191                  if ($foundtype === T_CONSTANT_ENCAPSED_STRING) {
 192                      $text = $this->decapsulate($founddata);
 193                      $expect = 'SEMICOLON';
 194                      continue;
 195                  } else {
 196                      throw new moodle_exception(
 197                          'Parsing error. Expected T_CONSTANT_ENCAPSED_STRING array item value at line '.$line
 198                      );
 199                  }
 200              }
 201  
 202              if ($expect == 'SEMICOLON') {
 203                  if (is_null($id) or is_null($text)) {
 204                      throw new moodle_exception('Parsing error. NULL string id or value at line '.$line);
 205                  }
 206                  if ($foundtype === 'char' and $founddata === ';') {
 207                      if (!empty($id)) {
 208                          $strings[$id] = $text;
 209                      }
 210                      $id = null;
 211                      $text = null;
 212                      $expect = 'STRING_VAR';
 213                      continue;
 214                  } else {
 215                      throw new moodle_exception('Parsing error. Expected character ; at line '.$line);
 216                  }
 217              }
 218  
 219          }
 220  
 221          return $strings;
 222      }
 223  
 224      /**
 225       * Given one T_CONSTANT_ENCAPSED_STRING, return its value without quotes
 226       *
 227       * Also processes escaped quotes inside the text.
 228       *
 229       * Note: This method is taken directly from local_amos as it is highly tested and robust.
 230       *
 231       * @param string $text value obtained by token_get_all()
 232       * @return string value without quotes
 233       */
 234      protected function decapsulate(string $text): string {
 235  
 236          if (strlen($text) < 2) {
 237              throw new moodle_exception('Parsing error. Expected T_CONSTANT_ENCAPSED_STRING in decapsulate()');
 238          }
 239  
 240          if (substr($text, 0, 1) == "'" and substr($text, -1) == "'") {
 241              // Single quoted string.
 242              $text = trim($text, "'");
 243              $text = str_replace("\'", "'", $text);
 244              $text = str_replace('\\\\', '\\', $text);
 245              return $text;
 246  
 247          } else if (substr($text, 0, 1) == '"' and substr($text, -1) == '"') {
 248              // Double quoted string.
 249              $text = trim($text, '"');
 250              $text = str_replace('\"', '"', $text);
 251              $text = str_replace('\\\\', '\\', $text);
 252              return $text;
 253  
 254          } else {
 255              throw new moodle_exception(
 256                  'Parsing error. Unexpected quotation in T_CONSTANT_ENCAPSED_STRING in decapsulate(): '.$text
 257              );
 258          }
 259      }
 260  }