PluginProbe
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts / 2.7.8
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts v2.7.8
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
insert-php / includes / class.code-validator.php

class.code-validator.php in Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts 2.7.8, at includes/class.code-validator.php

270 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Preflight checks for executable snippets.
4 *
5 * @package WP_Plugin_Insert_PHP
6 */
7
8 // Exit if accessed directly.
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * WINP_Code_Validator class.
15 */
16 class WINP_Code_Validator {
17
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 /**
122 * Find a top-level function declaration that would be redeclared by eval().
123 *
124 * PHP terminates the request when eval() declares an already-loaded function,
125 * so this conflict has to be detected before evaluating the snippet.
126 *
127 * @param string $snippet_code Snippet code.
128 * @param string $snippet_type Snippet type.
129 * @return array{name:string,line:int}|null Conflict details, or null when none is found.
130 */
131 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 );
136
137 $brace_depth = 0;
138 $namespace = '';
139 $namespace_depth = 0;
140 $reading_namespace = false;
141 $namespace_name = '';
142 $declared = [];
143
144 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 }
150
151 if ( T_NAMESPACE === $token[0] ) {
152 $reading_namespace = true;
153 $namespace_name = '';
154 continue;
155 }
156
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 = '';
163 }
164 continue;
165 }
166
167 if ( T_FUNCTION !== $token[0] || $brace_depth !== $namespace_depth ) {
168 continue;
169 }
170
171 $function_name = self::get_function_name( $tokens, $index );
172 if ( null === $function_name ) {
173 continue;
174 }
175
176 $qualified_name = $namespace ? $namespace . '\\' . $function_name : $function_name;
177 $normalized_name = strtolower( $qualified_name );
178
179 if ( isset( $declared[ $normalized_name ] ) || function_exists( $qualified_name ) ) {
180 return [
181 'name' => $qualified_name,
182 'line' => (int) $token[2],
183 ];
184 }
185
186 $declared[ $normalized_name ] = true;
187 continue;
188 }
189
190 if ( $reading_namespace ) {
191 if ( ';' === $token ) {
192 $namespace = trim( $namespace_name, '\\' );
193 $namespace_depth = 0;
194 $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 $namespace_name = '';
203 }
204 continue;
205 }
206
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;
214 }
215 }
216 }
217
218 return null;
219 }
220
221 /**
222 * Check whether a token can be part of a namespace name.
223 *
224 * @param int $token_id Token identifier.
225 * @return bool
226 */
227 private static function is_namespace_name_token( $token_id ) {
228 $name_tokens = [ T_STRING, T_NS_SEPARATOR ];
229
230 foreach ( [ 'T_NAME_QUALIFIED', 'T_NAME_FULLY_QUALIFIED' ] as $constant_name ) {
231 if ( defined( $constant_name ) ) {
232 $name_tokens[] = constant( $constant_name );
233 }
234 }
235
236 return in_array( $token_id, $name_tokens, true );
237 }
238
239 /**
240 * Get the name following a function token, ignoring whitespace and references.
241 *
242 * @param array<int, array<int, int|string>|string> $tokens Token stream.
243 * @param int $function_index Function token index.
244 * @return string|null
245 */
246 private static function get_function_name( $tokens, $function_index ) {
247 $token_count = count( $tokens );
248
249 for ( $index = $function_index + 1; $index < $token_count; ++$index ) {
250 $token = $tokens[ $index ];
251
252 if ( is_array( $token ) ) {
253 if ( in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true ) || '&' === $token[1] ) {
254 continue;
255 }
256
257 return T_STRING === $token[0] ? (string) $token[1] : null;
258 }
259
260 if ( '&' === $token ) {
261 continue;
262 }
263
264 return null;
265 }
266
267 return null;
268 }
269 }
270