PluginProbe
Code Snippets / trunk
Code Snippets vtrunk
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / php / Utils / Validator.php

Validator.php in Code Snippets trunk, at php/Utils/Validator.php

379 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets\Utils;
4
5 /**
6 * Validates code prior to execution.
7 *
8 * @package Code_Snippets
9 */
10 class Validator {
11
12 /**
13 * Code to validate.
14 *
15 * @var string
16 */
17 private string $code;
18
19 /**
20 * List of tokens.
21 *
22 * @var array<string>
23 */
24 private array $tokens;
25
26 /**
27 * The index of the token currently being examined.
28 *
29 * @var int
30 */
31 private int $current;
32
33 /**
34 * The total number of tokens.
35 *
36 * @var int
37 */
38 private int $length;
39
40 /**
41 * Array to keep track of the various function, class and interface identifiers which have been defined.
42 *
43 * @var array<string, string[]>
44 */
45 private array $defined_identifiers = [];
46
47 /**
48 * Exclude certain tokens from being checked.
49 *
50 * @var array<string, string[]>
51 */
52 private array $exceptions = [];
53
54 /**
55 * Identifiers already claimed by other snippets being validated alongside
56 * this one.
57 *
58 * A snippet is validated against everything PHP has declared so far, which
59 * does not include a snippet that is about to be activated in the same
60 * batch. Two snippets declaring the same function therefore both passed and
61 * both activated, and the site crashed on the next request.
62 *
63 * @var array<string, string[]>
64 */
65 private array $claimed_identifiers;
66
67 /**
68 * Namespace the code being read currently declares, lower-cased, or empty for the global namespace.
69 *
70 * @var string
71 */
72 private string $namespace = '';
73
74 /**
75 * Class constructor.
76 *
77 * @param string $code Snippet code for parsing.
78 * @param array<string, string[]> $claimed_identifiers Identifiers already claimed by
79 * snippets validated alongside this one.
80 */
81 public function __construct( string $code, array $claimed_identifiers = [] ) {
82 $this->claimed_identifiers = $claimed_identifiers;
83 $this->code = $code;
84 $this->tokens = token_get_all( "<?php\n" . $this->code );
85 $this->length = count( $this->tokens );
86 $this->current = 0;
87 }
88
89 /**
90 * Retrieve the identifiers claimed so far, including this snippet's own.
91 *
92 * Pass the result to the next Validator in a batch so that two snippets
93 * cannot both claim the same name.
94 *
95 * @return array<string, string[]>
96 */
97 public function get_claimed_identifiers(): array {
98 return $this->claimed_identifiers;
99 }
100
101 /**
102 * Determine whether the parser has reached the end of the list of tokens.
103 *
104 * @return bool
105 */
106 private function end(): bool {
107 return $this->current === $this->length;
108 }
109
110 /**
111 * Retrieve the next token without moving the pointer
112 *
113 * @return string|array<string|int>|null The current token if the list has not been expended, null otherwise.
114 */
115 private function peek() {
116 return $this->end() ? null : $this->tokens[ $this->current ];
117 }
118
119 /**
120 * Move the pointer to the next token, if there is one.
121 *
122 * If the first argument is provided, only move the pointer if the tokens match.
123 */
124 private function next() {
125 if ( ! $this->end() ) {
126 ++$this->current;
127 }
128 }
129
130 /**
131 * Check whether a particular identifier has been used previously.
132 *
133 * @param string $type Which type of identifier this is. Supports T_FUNCTION, T_CLASS and T_INTERFACE.
134 * @param string $identifier The name of the identifier itself.
135 *
136 * @return bool true if the identifier is not unique.
137 */
138 private function check_duplicate_identifier( string $type, string $identifier ): bool {
139 $identifier = strtolower( ltrim( $identifier, '\\' ) );
140
141 // PHP keeps declared names fully qualified, so that is the form compared
142 // and claimed: the same short name in two namespaces is two names.
143 $qualified = '' === $this->namespace ? $identifier : $this->namespace . '\\' . $identifier;
144 $namespaced_identifier = 'code_snippets\\' . $identifier;
145
146 if ( ! isset( $this->defined_identifiers[ $type ] ) ) {
147 switch ( $type ) {
148 case T_FUNCTION:
149 $this->defined_identifiers[ T_FUNCTION ] = array_map(
150 'strtolower',
151 array_merge( get_defined_functions()['internal'], get_defined_functions()['user'] )
152 );
153 break;
154
155 case T_CLASS:
156 $this->defined_identifiers[ T_CLASS ] = array_map( 'strtolower', get_declared_classes() );
157 break;
158
159 case T_INTERFACE:
160 $this->defined_identifiers[ T_INTERFACE ] = array_map( 'strtolower', get_declared_interfaces() );
161 break;
162
163 default:
164 return false;
165 }
166 }
167
168 $known = array_merge(
169 $this->defined_identifiers[ $type ],
170 $this->claimed_identifiers[ $type ] ?? []
171 );
172
173 $duplicate_identifier = in_array( $qualified, $known, true );
174 $duplicate_namespaced = '' === $this->namespace && in_array( $namespaced_identifier, $known, true );
175 $exceptions = $this->exceptions[ $type ] ?? [];
176 $exception_identifier = in_array( $identifier, $exceptions, true ) || in_array( $qualified, $exceptions, true );
177 $exception_namespaced = in_array( $identifier, $exceptions, true ) || in_array( $namespaced_identifier, $exceptions, true );
178
179 array_unshift( $this->defined_identifiers[ $type ], $qualified );
180 $this->claimed_identifiers[ $type ][] = $qualified;
181
182 return ( $duplicate_identifier && ! $exception_identifier ) || ( $duplicate_namespaced && ! $exception_namespaced );
183 }
184
185 /**
186 * Read the name a namespace declaration introduces, leaving the cursor after it.
187 *
188 * A bare "namespace {" opens the global namespace; "namespace\\foo()" is a
189 * relative name rather than a declaration and is left alone.
190 *
191 * @return string Lower-cased namespace, or empty for the global namespace.
192 */
193 private function read_namespace_declaration(): string {
194 $name = '';
195
196 while ( ! $this->end() ) {
197 $token = $this->peek();
198
199 if ( is_array( $token ) ) {
200 if ( T_WHITESPACE === $token[0] || T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0] ) {
201 $this->next();
202 continue;
203 }
204
205 if ( T_NS_SEPARATOR === $token[0] && '' === $name ) {
206 return $this->namespace;
207 }
208
209 if ( defined( 'T_NAME_RELATIVE' ) && T_NAME_RELATIVE === $token[0] ) {
210 return $this->namespace;
211 }
212
213 if ( T_STRING === $token[0] || T_NS_SEPARATOR === $token[0]
214 || ( defined( 'T_NAME_QUALIFIED' ) && T_NAME_QUALIFIED === $token[0] ) ) {
215 $name .= $token[1];
216 $this->next();
217 continue;
218 }
219
220 return $this->namespace;
221 }
222
223 if ( ';' === $token || '{' === $token ) {
224 $this->next();
225 return strtolower( trim( $name, '\\' ) );
226 }
227
228 return $this->namespace;
229 }
230
231 return strtolower( trim( $name, '\\' ) );
232 }
233
234 /**
235 * Validate the given PHP code and return the result.
236 *
237 * @return array<string, mixed>|false Array containing message if an error was encountered, false if validation was successful.
238 */
239 public function validate() {
240
241 while ( ! $this->end() ) {
242 $token = $this->peek();
243 $this->next();
244
245 if ( ! is_array( $token ) ) {
246 continue;
247 }
248
249 if ( T_NAMESPACE === $token[0] ) {
250 $this->namespace = $this->read_namespace_declaration();
251 continue;
252 }
253
254 // If this is a function or class exists check, then allow this function or class to be defined.
255 if ( T_STRING === $token[0] && ( 'function_exists' === $token[1] || 'class_exists' === $token[1] ) ) {
256 $type = 'function_exists' === $token[1] ? T_FUNCTION : T_CLASS;
257
258 // Eat tokens until we find the function or class name.
259 while ( ! $this->end() && T_CONSTANT_ENCAPSED_STRING !== $token[0] ) {
260 $token = $this->peek();
261 $this->next();
262 }
263
264 // Add the identifier to the list of exceptions.
265 $identifier = strtolower( ltrim( trim( $token[1], '\'"' ), '\\' ) );
266
267 if ( '' !== $identifier ) {
268 $this->exceptions[ $type ] = $this->exceptions[ $type ] ?? [];
269 $this->exceptions[ $type ][] = $identifier;
270 }
271 continue;
272 }
273
274 // If we have a double colon, followed by a class, then consume it before the next section.
275 if ( T_DOUBLE_COLON === $token[0] ) {
276 $token = $this->peek();
277 $this->next();
278
279 if ( T_CLASS === $token[0] ) {
280 $this->next();
281 $token = $this->peek();
282 }
283 }
284
285 // Only look for class and function declaration tokens.
286 if ( T_CLASS !== $token[0] && T_FUNCTION !== $token[0] ) {
287 continue;
288 }
289
290 /**
291 * Ensure the type of $token is inferred correctly.
292 *
293 * @var string|array<string|int> $token
294 */
295 $structure_type = $token[0];
296
297 // Continue eating tokens until we find the name of the class or function.
298 while ( ! $this->end() && T_STRING !== $token[0] &&
299 ( T_FUNCTION !== $structure_type || '(' !== $token ) && ( T_CLASS !== $structure_type || '{' !== $token ) ) {
300 $token = $this->peek();
301 $this->next();
302 }
303
304 // If we've eaten all the tokens without discovering a name, then there must be a syntax error, so return appropriately.
305 if ( $this->end() ) {
306 return array(
307 'message' => __( 'Parse error: syntax error, unexpected end of snippet.', 'code-snippets' ),
308 'line' => $token[2],
309 );
310 }
311
312 // If the function or class is anonymous, with no name, then no need to check.
313 if ( ! ( T_FUNCTION === $structure_type && '(' === $token ) && ! ( T_CLASS === $structure_type && '{' === $token ) ) {
314
315 // Check whether the name has already been defined.
316 if ( $this->check_duplicate_identifier( $structure_type, $token[1] ) ) {
317 switch ( $structure_type ) {
318 case T_FUNCTION:
319 /* translators: %s: PHP function name */
320 $message = __( 'Cannot redeclare function %s.', 'code-snippets' );
321 break;
322 case T_CLASS:
323 /* translators: %s: PHP class name */
324 $message = __( 'Cannot redeclare class %s.', 'code-snippets' );
325 break;
326 case T_INTERFACE:
327 /* translators: %s: PHP interface name */
328 $message = __( 'Cannot redeclare interface %s.', 'code-snippets' );
329 break;
330 default:
331 /* translators: %s: PHP identifier name*/
332 $message = __( 'Cannot redeclare %s.', 'code-snippets' );
333 }
334
335 return array(
336 'message' => sprintf( $message, $token[1] ),
337 'line' => $token[2],
338 );
339 }
340 }
341
342 // If we have entered into a class, eat tokens until we find the closing brace.
343 if ( T_CLASS !== $structure_type ) {
344 continue;
345 }
346
347 // Find the opening brace for the class.
348 while ( ! $this->end() && '{' !== $token ) {
349 $token = $this->peek();
350 $this->next();
351 }
352
353 // Continue traversing the class tokens until we have found the class closing brace.
354 $depth = 1;
355 while ( ! $this->end() && $depth > 0 ) {
356 $token = $this->peek();
357
358 if ( '{' === $token ) {
359 ++$depth;
360 } elseif ( '}' === $token ) {
361 --$depth;
362 }
363
364 $this->next();
365 }
366
367 // If we did not make it out of the class, then there's a problem.
368 if ( $depth > 0 ) {
369 return array(
370 'message' => __( 'Parse error: syntax error, unexpected end of snippet.', 'code-snippets' ),
371 'line' => $token[2],
372 );
373 }
374 }
375
376 return false;
377 }
378 }
379