PluginProbe
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts / 2.7.9
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts v2.7.9
2.7.9 2.7.8 2.7.7 2.7.6 2.7.5 2.7.4 trunk 1.3 2.0.4 2.0.6 2.1.91 2.2.4 2.2.7 2.2.9 2.3.1 2.3.10 2.4.10 2.4.2 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.6.0 All 30 releases
← All changes | includes/class.code-validator.php +360 -63 2.7.8 → 2.7.9 View file →
@@ -33,14 +33,13 @@
33 33 'message' => '',
34 34 ];
35 35 }
36 36
37 - $redeclaration = self::find_function_redeclaration( $snippet_code, $snippet_type );
38 - if ( null !== $redeclaration ) {
37 + $redeclaration_error = self::get_redeclaration_error( $snippet_code, $snippet_type );
38 + if ( null !== $redeclaration_error ) {
39 39 return [
40 40 'valid' => false,
41 - // translators: %1$d is the line number, %2$s is the fully qualified function name.
42 - 'message' => sprintf( __( 'Line %1$d: Cannot redeclare function %2$s(). Rename the function or guard its declaration with function_exists().', 'insert-php' ), $redeclaration['line'], $redeclaration['name'] ),
41 + 'message' => $redeclaration_error->getMessage(),
43 42 ];
44 43 }
45 44
46 45 $validation_errors = [];
@@ -118,8 +117,47 @@
118 117 ];
119 118 }
120 119
121 120 /**
121 + * Check executable snippet code for syntax errors without running it.
122 + *
123 + * The tokenizer parses the code in TOKEN_PARSE mode and never executes it.
124 + * Errors PHP raises only when the code runs, such as a call to an undefined
125 + * function, are left to the check that runs when the snippet is activated.
126 + * Without the tokenizer extension nothing is checked and the result says so.
127 + *
128 + * @param string $snippet_code Snippet code, without the opening tag for PHP snippets.
129 + * @param string $snippet_type Snippet type.
130 + * @return array{valid:bool,checked:bool,message:string} Check result.
131 + */
132 + public static function check_syntax( string $snippet_code, $snippet_type ): array {
133 + if ( ! self::can_tokenize() ) {
134 + return [
135 + 'valid' => true,
136 + 'checked' => false,
137 + 'message' => '',
138 + ];
139 + }
140 +
141 + try {
142 + token_get_all( self::get_source( $snippet_code, $snippet_type ), TOKEN_PARSE );
143 + } catch ( CompileError $e ) {
144 + return [
145 + 'valid' => false,
146 + 'checked' => true,
147 + // translators: %1$d is the line number, %2$s is the error message.
148 + 'message' => sprintf( __( 'Syntax error on line %1$d: %2$s', 'insert-php' ), $e->getLine(), $e->getMessage() ),
149 + ];
150 + }
151 +
152 + return [
153 + 'valid' => true,
154 + 'checked' => true,
155 + 'message' => '',
156 + ];
157 + }
158 +
159 + /**
122 160 * Find a top-level function declaration that would be redeclared by eval().
123 161 *
124 162 * PHP terminates the request when eval() declares an already-loaded function,
125 163 * so this conflict has to be detected before evaluating the snippet.
@@ -128,14 +166,110 @@
128 166 * @param string $snippet_type Snippet type.
129 167 * @return array{name:string,line:int}|null Conflict details, or null when none is found.
130 168 */
131 169 public static function find_function_redeclaration( $snippet_code, $snippet_type ) {
132 - $source = WINP_SNIPPET_TYPE_UNIVERSAL === $snippet_type
133 - ? $snippet_code
134 - : '<?php ' . $snippet_code;
135 - $tokens = token_get_all( $source );
170 + $redeclaration = self::scan_redeclarations( (string) $snippet_code, $snippet_type, false );
171 + if ( null === $redeclaration ) {
172 + return null;
173 + }
136 174
137 - $brace_depth = 0;
175 + return [
176 + 'name' => $redeclaration['name'],
177 + 'line' => $redeclaration['line'],
178 + ];
179 + }
180 +
181 + /**
182 + * Find a top-level function, class, interface, trait or enum that would be redeclared by eval().
183 + *
184 + * PHP terminates the request when eval() declares a name that is already in
185 + * use, so this conflict has to be detected before evaluating the snippet.
186 + *
187 + * @param string $snippet_code Snippet code.
188 + * @param string $snippet_type Snippet type.
189 + * @return array{kind:string,name:string,line:int}|null Conflict details, or null when none is found.
190 + */
191 + public static function find_redeclaration( string $snippet_code, $snippet_type ): ?array {
192 + return self::scan_redeclarations( $snippet_code, $snippet_type, true );
193 + }
194 +
195 + /**
196 + * Describe a redeclaration found by find_redeclaration().
197 + *
198 + * @param array{kind:string,name:string,line:int} $redeclaration Conflict details.
199 + * @return string
200 + */
201 + public static function get_redeclaration_message( array $redeclaration ): string {
202 + switch ( $redeclaration['kind'] ) {
203 + case 'class':
204 + // translators: %s is the fully qualified class name.
205 + return sprintf( __( 'Cannot declare class %s, because the name is already in use. Rename the class or guard its declaration with class_exists().', 'insert-php' ), $redeclaration['name'] );
206 + case 'interface':
207 + // translators: %s is the fully qualified interface name.
208 + return sprintf( __( 'Cannot declare interface %s, because the name is already in use. Rename the interface or guard its declaration with interface_exists().', 'insert-php' ), $redeclaration['name'] );
209 + case 'trait':
210 + // translators: %s is the fully qualified trait name.
211 + return sprintf( __( 'Cannot declare trait %s, because the name is already in use. Rename the trait or guard its declaration with trait_exists().', 'insert-php' ), $redeclaration['name'] );
212 + case 'enum':
213 + // translators: %s is the fully qualified enum name.
214 + return sprintf( __( 'Cannot declare enum %s, because the name is already in use. Rename the enum or guard its declaration with enum_exists().', 'insert-php' ), $redeclaration['name'] );
215 + default:
216 + // translators: %s is the fully qualified function name.
217 + return sprintf( __( 'Cannot redeclare function %s(). Rename the function or guard its declaration with function_exists().', 'insert-php' ), $redeclaration['name'] );
218 + }
219 + }
220 +
221 + /**
222 + * Build the error for a top-level declaration the snippet would redeclare.
223 + *
224 + * @param string $snippet_code Snippet code.
225 + * @param string $snippet_type Snippet type.
226 + * @return ErrorException|null Error carrying the snippet line, or null when none is found.
227 + */
228 + public static function get_redeclaration_error( string $snippet_code, $snippet_type ): ?ErrorException {
229 + $redeclaration = self::find_redeclaration( $snippet_code, $snippet_type );
230 + if ( null === $redeclaration ) {
231 + return null;
232 + }
233 +
234 + return new ErrorException(
235 + // translators: %1$d is the line number, %2$s is the error message.
236 + sprintf( __( 'Line %1$d: %2$s', 'insert-php' ), $redeclaration['line'], self::get_redeclaration_message( $redeclaration ) ),
237 + 0,
238 + E_COMPILE_ERROR,
239 + __FILE__,
240 + $redeclaration['line']
241 + );
242 + }
243 +
244 + /**
245 + * Find the first top-level declaration whose name is already in use.
246 + *
247 + * Top-level functions are bound when eval() compiles the code, so a conflict
248 + * always ends the request. Classes, interfaces, traits and enums are declared
249 + * when execution reaches them, so they are not reported after a return, exit,
250 + * throw or goto outside any function or class body, which may guard them.
251 + * Declarations inside braces are conditional and are skipped, even when the
252 + * condition is always true. Class-like declarations inside alternative-syntax
253 + * blocks, as in if ( ... ): ... endif;, are skipped too, while functions there
254 + * are still reported. Nothing is reported without the tokenizer extension.
255 + *
256 + * @param string $snippet_code Snippet code.
257 + * @param string $snippet_type Snippet type.
258 + * @param bool $include_classes Whether class-like declarations are checked.
259 + * @return array{kind:string,name:string,line:int}|null Conflict details, or null when none is found.
260 + */
261 + private static function scan_redeclarations( string $snippet_code, $snippet_type, bool $include_classes ): ?array {
262 + if ( ! self::can_tokenize() ) {
263 + return null;
264 + }
265 +
266 + $tokens = token_get_all( self::get_source( $snippet_code, $snippet_type ) );
267 + $declaration_kinds = self::get_declaration_kinds();
268 + $blocks = []; // One entry per open brace, true when it opens a function or class body.
269 + $opens_body = false;
270 + $alternative_depth = 0;
271 + $may_stop = false;
138 272 $namespace = '';
139 273 $namespace_depth = 0;
140 274 $reading_namespace = false;
141 275 $namespace_name = '';
@@ -141,79 +275,116 @@
141 275 $namespace_name = '';
142 276 $declared = [];
143 277
144 278 foreach ( $tokens as $index => $token ) {
145 - if ( is_array( $token ) ) {
146 - if ( in_array( $token[0], [ T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES ], true ) ) {
147 - ++$brace_depth;
148 - continue;
149 - }
279 + if ( ! is_array( $token ) ) {
280 + if ( $reading_namespace ) {
281 + $reading_namespace = false;
150 282
151 - if ( T_NAMESPACE === $token[0] ) {
152 - $reading_namespace = true;
153 - $namespace_name = '';
154 - continue;
155 - }
283 + if ( ';' === $token ) {
284 + $namespace = trim( $namespace_name, '\\' );
285 + $namespace_depth = 0;
286 + continue;
287 + }
156 288
157 - if ( $reading_namespace ) {
158 - if ( self::is_namespace_name_token( $token[0] ) ) {
159 - $namespace_name .= $token[1];
160 - } elseif ( ! in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true ) ) {
161 - $reading_namespace = false;
162 - $namespace_name = '';
289 + if ( '{' !== $token ) {
290 + $namespace_name = '';
291 + continue;
163 292 }
164 - continue;
165 - }
166 293
167 - if ( T_FUNCTION !== $token[0] || $brace_depth !== $namespace_depth ) {
294 + $blocks[] = false;
295 + $namespace = trim( $namespace_name, '\\' );
296 + $namespace_depth = count( $blocks );
168 297 continue;
169 298 }
170 299
171 - $function_name = self::get_function_name( $tokens, $index );
172 - if ( null === $function_name ) {
173 - continue;
300 + if ( '{' === $token ) {
301 + $blocks[] = $opens_body;
302 + $opens_body = false;
303 + } elseif ( '}' === $token ) {
304 + array_pop( $blocks );
305 + if ( $namespace_depth > count( $blocks ) ) {
306 + $namespace = '';
307 + $namespace_depth = 0;
308 + }
309 + } elseif ( ';' === $token ) {
310 + // Abstract and interface methods have no body.
311 + $opens_body = false;
174 312 }
313 + continue;
314 + }
175 315
176 - $qualified_name = $namespace ? $namespace . '\\' . $function_name : $function_name;
177 - $normalized_name = strtolower( $qualified_name );
316 + $token_id = $token[0];
178 317
179 - if ( isset( $declared[ $normalized_name ] ) || function_exists( $qualified_name ) ) {
180 - return [
181 - 'name' => $qualified_name,
182 - 'line' => (int) $token[2],
183 - ];
184 - }
318 + if ( in_array( $token_id, [ T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES ], true ) ) {
319 + $blocks[] = false;
320 + continue;
321 + }
185 322
186 - $declared[ $normalized_name ] = true;
323 + if ( T_NAMESPACE === $token_id ) {
324 + $reading_namespace = true;
325 + $namespace_name = '';
187 326 continue;
188 327 }
189 328
190 329 if ( $reading_namespace ) {
191 - if ( ';' === $token ) {
192 - $namespace = trim( $namespace_name, '\\' );
193 - $namespace_depth = 0;
330 + if ( self::is_namespace_name_token( $token_id ) ) {
331 + $namespace_name .= $token[1];
332 + } elseif ( ! self::is_insignificant( $token ) ) {
194 333 $reading_namespace = false;
195 - } elseif ( '{' === $token ) {
196 - ++$brace_depth;
197 - $namespace = trim( $namespace_name, '\\' );
198 - $namespace_depth = $brace_depth;
199 - $reading_namespace = false;
200 - } else {
201 - $reading_namespace = false;
202 334 $namespace_name = '';
203 335 }
204 336 continue;
205 337 }
206 338
207 - if ( '{' === $token ) {
208 - ++$brace_depth;
209 - } elseif ( '}' === $token ) {
210 - $brace_depth = max( 0, $brace_depth - 1 );
211 - if ( $namespace_depth > $brace_depth ) {
212 - $namespace = '';
213 - $namespace_depth = 0;
339 + if ( in_array( $token_id, [ T_RETURN, T_EXIT, T_THROW, T_GOTO ], true ) ) {
340 + $may_stop = $may_stop || ! in_array( true, $blocks, true );
341 + continue;
342 + }
343 +
344 + if ( in_array( $token_id, [ T_IF, T_WHILE, T_FOR, T_FOREACH, T_SWITCH ], true ) ) {
345 + if ( self::opens_alternative_block( $tokens, $index ) ) {
346 + ++$alternative_depth;
214 347 }
348 + continue;
215 349 }
350 +
351 + if ( in_array( $token_id, [ T_ENDIF, T_ENDWHILE, T_ENDFOR, T_ENDFOREACH, T_ENDSWITCH ], true ) ) {
352 + $alternative_depth = max( 0, $alternative_depth - 1 );
353 + continue;
354 + }
355 +
356 + if ( ! isset( $declaration_kinds[ $token_id ] ) || self::is_name_reference( $tokens, $index ) ) {
357 + continue;
358 + }
359 +
360 + // Named and anonymous functions and classes open their body at the next brace.
361 + $opens_body = true;
362 + $kind = $declaration_kinds[ $token_id ];
363 + $is_function = 'function' === $kind;
364 +
365 + if ( count( $blocks ) !== $namespace_depth || ( ! $is_function && ( ! $include_classes || $may_stop || $alternative_depth > 0 ) ) ) {
366 + continue;
367 + }
368 +
369 + $name = self::get_declared_name( $tokens, $index );
370 + if ( null === $name ) {
371 + continue;
372 + }
373 +
374 + $qualified_name = $namespace ? $namespace . '\\' . $name : $name;
375 + $symbol_key = ( $is_function ? 'function:' : 'class:' ) . strtolower( $qualified_name );
376 + $in_use = $is_function ? function_exists( $qualified_name ) : self::is_class_name_in_use( $qualified_name );
377 +
378 + if ( isset( $declared[ $symbol_key ] ) || $in_use ) {
379 + return [
380 + 'kind' => $kind,
381 + 'name' => $qualified_name,
382 + 'line' => (int) $token[2],
383 + ];
384 + }
385 +
386 + $declared[ $symbol_key ] = true;
216 387 }
217 388
218 389 return null;
219 390 }
@@ -218,8 +389,134 @@
218 389 return null;
219 390 }
220 391
221 392 /**
393 + * Check whether the tokenizer extension is available.
394 + *
395 + * @return bool
396 + */
397 + private static function can_tokenize(): bool {
398 + return function_exists( 'token_get_all' );
399 + }
400 +
401 + /**
402 + * Build the source the tokenizer reads: PHP snippets are stored without their opening tag.
403 + *
404 + * @param string $snippet_code Snippet code.
405 + * @param string $snippet_type Snippet type.
406 + * @return string
407 + */
408 + private static function get_source( string $snippet_code, $snippet_type ): string {
409 + return WINP_SNIPPET_TYPE_UNIVERSAL === $snippet_type ? $snippet_code : '<?php ' . $snippet_code;
410 + }
411 +
412 + /**
413 + * Map the declaration tokens to the kind of name they declare.
414 + *
415 + * @return array<int, string>
416 + */
417 + private static function get_declaration_kinds(): array {
418 + $kinds = [
419 + T_FUNCTION => 'function',
420 + T_CLASS => 'class',
421 + T_INTERFACE => 'interface',
422 + T_TRAIT => 'trait',
423 + ];
424 +
425 + if ( defined( 'T_ENUM' ) ) {
426 + $kinds[ constant( 'T_ENUM' ) ] = 'enum';
427 + }
428 +
429 + return $kinds;
430 + }
431 +
432 + /**
433 + * Check whether a class, interface, trait or enum with this name is already loaded.
434 + *
435 + * Autoloading is skipped: eval() only fails on names that are already declared.
436 + *
437 + * @param string $name Fully qualified name.
438 + * @return bool
439 + */
440 + private static function is_class_name_in_use( string $name ): bool {
441 + return class_exists( $name, false ) || interface_exists( $name, false ) || trait_exists( $name, false );
442 + }
443 +
444 + /**
445 + * Check whether a token is whitespace or a comment.
446 + *
447 + * @param array<int, int|string>|string $token Token.
448 + * @return bool
449 + */
450 + private static function is_insignificant( $token ): bool {
451 + return is_array( $token ) && in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true );
452 + }
453 +
454 + /**
455 + * Check whether a declaration keyword only refers to a name, as in Example::class or use function example;.
456 + *
457 + * @param array<int, array<int, int|string>|string> $tokens Token stream.
458 + * @param int $index Keyword token index.
459 + * @return bool
460 + */
461 + private static function is_name_reference( array $tokens, int $index ): bool {
462 + $preceding = [ T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_USE ];
463 +
464 + if ( defined( 'T_NULLSAFE_OBJECT_OPERATOR' ) ) {
465 + $preceding[] = constant( 'T_NULLSAFE_OBJECT_OPERATOR' );
466 + }
467 +
468 + for ( $previous = $index - 1; $previous >= 0; --$previous ) {
469 + if ( ! self::is_insignificant( $tokens[ $previous ] ) ) {
470 + return is_array( $tokens[ $previous ] ) && in_array( $tokens[ $previous ][0], $preceding, true );
471 + }
472 + }
473 +
474 + return false;
475 + }
476 +
477 + /**
478 + * Check whether a control structure uses the alternative syntax, as in if ( ... ): ... endif;.
479 + *
480 + * @param array<int, array<int, int|string>|string> $tokens Token stream.
481 + * @param int $index Control structure token index.
482 + * @return bool
483 + */
484 + private static function opens_alternative_block( array $tokens, int $index ): bool {
485 + $token_count = count( $tokens );
486 + $depth = 0;
487 +
488 + for ( $current = $index + 1; $current < $token_count; ++$current ) {
489 + $token = $tokens[ $current ];
490 +
491 + if ( self::is_insignificant( $token ) ) {
492 + continue;
493 + }
494 +
495 + if ( '(' === $token ) {
496 + ++$depth;
497 + continue;
498 + }
499 +
500 + if ( 0 === $depth ) {
501 + return false;
502 + }
503 +
504 + if ( ')' === $token && 0 === --$depth ) {
505 + for ( $next = $current + 1; $next < $token_count; ++$next ) {
506 + if ( ! self::is_insignificant( $tokens[ $next ] ) ) {
507 + return ':' === $tokens[ $next ];
508 + }
509 + }
510 +
511 + return false;
512 + }
513 + }
514 +
515 + return false;
516 + }
517 +
518 + /**
222 519 * Check whether a token can be part of a namespace name.
223 520 *
224 521 * @param int $token_id Token identifier.
225 522 * @return bool
@@ -236,22 +533,22 @@
236 533 return in_array( $token_id, $name_tokens, true );
237 534 }
238 535
239 536 /**
240 - * Get the name following a function token, ignoring whitespace and references.
537 + * Get the name following a declaration token, ignoring whitespace and references.
241 538 *
242 539 * @param array<int, array<int, int|string>|string> $tokens Token stream.
243 - * @param int $function_index Function token index.
244 - * @return string|null
540 + * @param int $declaration_index Declaration token index.
541 + * @return string|null Null for closures, anonymous classes and other unnamed uses.
245 542 */
246 - private static function get_function_name( $tokens, $function_index ) {
543 + private static function get_declared_name( $tokens, $declaration_index ) {
247 544 $token_count = count( $tokens );
248 545
249 - for ( $index = $function_index + 1; $index < $token_count; ++$index ) {
546 + for ( $index = $declaration_index + 1; $index < $token_count; ++$index ) {
250 547 $token = $tokens[ $index ];
251 548
252 549 if ( is_array( $token ) ) {
253 - if ( in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true ) || '&' === $token[1] ) {
550 + if ( self::is_insignificant( $token ) || '&' === $token[1] ) {
254 551 continue;
255 552 }
256 553
257 554 return T_STRING === $token[0] ? (string) $token[1] : null;