| @@ -15,8 +15,111 @@ | ||
| 15 | 15 | */ |
| 16 | 16 | class WINP_Code_Validator { |
| 17 | 17 | |
| 18 | 18 | /** |
| 19 | + * Validate executable snippet code by evaluating it once. | |
| 20 | + * | |
| 21 | + * Runs the redeclaration preflight and then evaluates the code with output | |
| 22 | + * discarded, reporting syntax errors, warnings and uncaught errors. The code | |
| 23 | + * is executed in the current request, exactly as the snippet editor does. | |
| 24 | + * | |
| 25 | + * @param string $snippet_code Unslashed snippet code. | |
| 26 | + * @param string $snippet_type Snippet type. | |
| 27 | + * @return array{valid:bool,message:string} Validation result. | |
| 28 | + */ | |
| 29 | + public static function validate_code( $snippet_code, $snippet_type ) { | |
| 30 | + if ( empty( $snippet_code ) ) { | |
| 31 | + return [ | |
| 32 | + 'valid' => true, | |
| 33 | + 'message' => '', | |
| 34 | + ]; | |
| 35 | + } | |
| 36 | + | |
| 37 | + $redeclaration = self::find_function_redeclaration( $snippet_code, $snippet_type ); | |
| 38 | + if ( null !== $redeclaration ) { | |
| 39 | + return [ | |
| 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'] ), | |
| 43 | + ]; | |
| 44 | + } | |
| 45 | + | |
| 46 | + $validation_errors = []; | |
| 47 | + | |
| 48 | + // Set custom error handler to catch warnings and notices. | |
| 49 | + set_error_handler( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler | |
| 50 | + function ( $errno, $errstr, $errfile, $errline ) use ( &$validation_errors ) { | |
| 51 | + // Extract line number from eval'd code if present. | |
| 52 | + if ( strpos( $errfile, "eval()'d code" ) !== false ) { | |
| 53 | + // translators: %1$d is the line number, %2$s is the error message. | |
| 54 | + $validation_errors[] = sprintf( __( 'Line %1$d: %2$s', 'insert-php' ), $errline, $errstr ); | |
| 55 | + } else { | |
| 56 | + $validation_errors[] = $errstr; | |
| 57 | + } | |
| 58 | + return true; // Don't execute PHP internal error handler. | |
| 59 | + } | |
| 60 | + ); | |
| 61 | + | |
| 62 | + ob_start(); | |
| 63 | + | |
| 64 | + try { | |
| 65 | + $result = WINP_SNIPPET_TYPE_UNIVERSAL === $snippet_type | |
| 66 | + ? eval( '?> ' . $snippet_code . ' <?php ' ) | |
| 67 | + : eval( $snippet_code ); | |
| 68 | + } catch ( ParseError $e ) { | |
| 69 | + ob_end_clean(); | |
| 70 | + restore_error_handler(); | |
| 71 | + | |
| 72 | + return [ | |
| 73 | + 'valid' => false, | |
| 74 | + // translators: %1$d is the line number, %2$s is the error message. | |
| 75 | + 'message' => sprintf( __( 'Syntax error on line %1$d: %2$s', 'insert-php' ), $e->getLine(), $e->getMessage() ), | |
| 76 | + ]; | |
| 77 | + } catch ( Throwable $e ) { | |
| 78 | + ob_end_clean(); | |
| 79 | + restore_error_handler(); | |
| 80 | + | |
| 81 | + // For fatal errors in eval'd code, report the actual line number. | |
| 82 | + if ( strpos( $e->getFile(), "eval()'d code" ) !== false ) { | |
| 83 | + return [ | |
| 84 | + 'valid' => false, | |
| 85 | + // translators: %1$d is the line number, %2$s is the error message. | |
| 86 | + 'message' => sprintf( __( 'Error on line %1$d: %2$s', 'insert-php' ), $e->getLine(), $e->getMessage() ), | |
| 87 | + ]; | |
| 88 | + } | |
| 89 | + | |
| 90 | + return [ | |
| 91 | + 'valid' => false, | |
| 92 | + // translators: %s is the error message. | |
| 93 | + 'message' => sprintf( __( 'Error: %s', 'insert-php' ), $e->getMessage() ), | |
| 94 | + ]; | |
| 95 | + } | |
| 96 | + | |
| 97 | + // Discard any output (echo/print statements are normal for snippets). | |
| 98 | + ob_end_clean(); | |
| 99 | + restore_error_handler(); | |
| 100 | + | |
| 101 | + if ( ! empty( $validation_errors ) ) { | |
| 102 | + return [ | |
| 103 | + 'valid' => false, | |
| 104 | + 'message' => implode( '<br>', $validation_errors ), | |
| 105 | + ]; | |
| 106 | + } | |
| 107 | + | |
| 108 | + if ( false === $result ) { | |
| 109 | + return [ | |
| 110 | + 'valid' => false, | |
| 111 | + 'message' => __( 'The code contains syntax errors. Please review and fix them before saving.', 'insert-php' ), | |
| 112 | + ]; | |
| 113 | + } | |
| 114 | + | |
| 115 | + return [ | |
| 116 | + 'valid' => true, | |
| 117 | + 'message' => '', | |
| 118 | + ]; | |
| 119 | + } | |
| 120 | + | |
| 121 | + /** | |
| 19 | 122 | * Find a top-level function declaration that would be redeclared by eval(). |
| 20 | 123 | * |
| 21 | 124 | * PHP terminates the request when eval() declares an already-loaded function, |
| 22 | 125 | * so this conflict has to be detected before evaluating the snippet. |