Merge branch 'MDL-69583-master' of git://github.com/ferranrecio/moodle
[moodle.git] / admin / tool / customlang / classes / local / mlang / phpparser.php
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/>.
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  */
25 namespace tool_customlang\local\mlang;
27 use coding_exception;
28 use moodle_exception;
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 {
40     /** @var holds the singleton instance of self */
41     private static $instance = null;
43     /**
44      * Prevents direct creation of object
45      */
46     private function __construct() {
47     }
49     /**
50      * Prevent from cloning the instance
51      */
52     public function __clone() {
53         throw new coding_exception('Cloning os singleton is not allowed');
54     }
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     }
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     }
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 {
103         $strings = []; // To be returned.
105         if (empty($data)) {
106             return $strings;
107         }
109         // Tokenize data - we expect valid PHP code.
110         $tokens = token_get_all($data);
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         }
122         $id = null;
123         $text = null;
124         $line = 0;
125         $expect = 'STRING_VAR'; // The first expected token is '$string'.
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                 }
138             } else {
139                 $foundtype = 'char';
140                 $founddata = $token;
141             }
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             }
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             }
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             }
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             }
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             }
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             }
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             }
219         }
221         return $strings;
222     }
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 {
236         if (strlen($text) < 2) {
237             throw new moodle_exception('Parsing error. Expected T_CONSTANT_ENCAPSED_STRING in decapsulate()');
238         }
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;
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;
254         } else {
255             throw new moodle_exception(
256                 'Parsing error. Unexpected quotation in T_CONSTANT_ENCAPSED_STRING in decapsulate(): '.$text
257             );
258         }
259     }