PluginProbe
Code Snippets / 3.10.1
Code Snippets v3.10.1
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 3.0.1 All 64 releases
code-snippets / php / Utils / Validator.php

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

281 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 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 * Class constructor.
56 *
57 * @param string $code Snippet code for parsing.
58 */
59 public function __construct( string $code ) {
60 $this->code = $code;
61 $this->tokens = token_get_all( "<?php\n" . $this->code );
62 $this->length = count( $this->tokens );
63 $this->current = 0;
64 }
65
66 /**
67 * Determine whether the parser has reached the end of the list of tokens.
68 *
69 * @return bool
70 */
71 private function end(): bool {
72 return $this->current === $this->length;
73 }
74
75 /**
76 * Retrieve the next token without moving the pointer
77 *
78 * @return string|array<string|int>|null The current token if the list has not been expended, null otherwise.
79 */
80 private function peek() {
81 return $this->end() ? null : $this->tokens[ $this->current ];
82 }
83
84 /**
85 * Move the pointer to the next token, if there is one.
86 *
87 * If the first argument is provided, only move the pointer if the tokens match.
88 */
89 private function next() {
90 if ( ! $this->end() ) {
91 ++$this->current;
92 }
93 }
94
95 /**
96 * Check whether a particular identifier has been used previously.
97 *
98 * @param string $type Which type of identifier this is. Supports T_FUNCTION, T_CLASS and T_INTERFACE.
99 * @param string $identifier The name of the identifier itself.
100 *
101 * @return bool true if the identifier is not unique.
102 */
103 private function check_duplicate_identifier( string $type, string $identifier ): bool {
104 $identifier = strtolower( ltrim( $identifier, '\\' ) );
105 $namespaced_identifier = 'code_snippets\\' . $identifier;
106
107 if ( ! isset( $this->defined_identifiers[ $type ] ) ) {
108 switch ( $type ) {
109 case T_FUNCTION:
110 $defined_functions = get_defined_functions();
111 $this->defined_identifiers[ T_FUNCTION ] = array_map(
112 'strtolower',
113 array_merge( $defined_functions['internal'], $defined_functions['user'] )
114 );
115 break;
116
117 case T_CLASS:
118 $this->defined_identifiers[ T_CLASS ] = array_map( 'strtolower', get_declared_classes() );
119 break;
120
121 case T_INTERFACE:
122 $this->defined_identifiers[ T_INTERFACE ] = array_map( 'strtolower', get_declared_interfaces() );
123 break;
124
125 default:
126 return false;
127 }
128 }
129
130 $duplicate_identifier = in_array( $identifier, $this->defined_identifiers[ $type ], true );
131 $duplicate_namespaced = in_array( $namespaced_identifier, $this->defined_identifiers[ $type ], true );
132 $exceptions = $this->exceptions[ $type ] ?? [];
133 $exception_identifier = in_array( $identifier, $exceptions, true );
134 $exception_namespaced = in_array( $namespaced_identifier, $exceptions, true );
135
136 array_unshift( $this->defined_identifiers[ $type ], $identifier );
137
138 return ( $duplicate_identifier && ! $exception_identifier ) || ( $duplicate_namespaced && ! $exception_namespaced );
139 }
140
141 /**
142 * Validate the given PHP code and return the result.
143 *
144 * @return array<string, mixed>|false Array containing message if an error was encountered, false if validation was successful.
145 */
146 public function validate() {
147
148 while ( ! $this->end() ) {
149 $token = $this->peek();
150 $this->next();
151
152 if ( ! is_array( $token ) ) {
153 continue;
154 }
155
156 // If this is a function or class exists check, then allow this function or class to be defined.
157 if ( T_STRING === $token[0] && ( 'function_exists' === $token[1] || 'class_exists' === $token[1] ) ) {
158 $type = 'function_exists' === $token[1] ? T_FUNCTION : T_CLASS;
159
160 // Eat tokens until we find the function or class name.
161 while ( ! $this->end() && T_CONSTANT_ENCAPSED_STRING !== $token[0] ) {
162 $token = $this->peek();
163 $this->next();
164 }
165
166 // Add the identifier to the list of exceptions.
167 $identifier = strtolower( ltrim( trim( $token[1], '\'"' ), '\\' ) );
168
169 if ( '' !== $identifier ) {
170 $this->exceptions[ $type ] = $this->exceptions[ $type ] ?? [];
171 $this->exceptions[ $type ][] = $identifier;
172 }
173 continue;
174 }
175
176 // If we have a double colon, followed by a class, then consume it before the next section.
177 if ( T_DOUBLE_COLON === $token[0] ) {
178 $token = $this->peek();
179 $this->next();
180
181 if ( T_CLASS === $token[0] ) {
182 $this->next();
183 $token = $this->peek();
184 }
185 }
186
187 // Only look for class and function declaration tokens.
188 if ( T_CLASS !== $token[0] && T_FUNCTION !== $token[0] ) {
189 continue;
190 }
191
192 /**
193 * Ensure the type of $token is inferred correctly.
194 *
195 * @var string|array<string|int> $token
196 */
197 $structure_type = $token[0];
198
199 // Continue eating tokens until we find the name of the class or function.
200 while ( ! $this->end() && T_STRING !== $token[0] &&
201 ( T_FUNCTION !== $structure_type || '(' !== $token ) && ( T_CLASS !== $structure_type || '{' !== $token ) ) {
202 $token = $this->peek();
203 $this->next();
204 }
205
206 // If we've eaten all the tokens without discovering a name, then there must be a syntax error, so return appropriately.
207 if ( $this->end() ) {
208 return array(
209 'message' => __( 'Parse error: syntax error, unexpected end of snippet.', 'code-snippets' ),
210 'line' => $token[2],
211 );
212 }
213
214 // If the function or class is anonymous, with no name, then no need to check.
215 if ( ! ( T_FUNCTION === $structure_type && '(' === $token ) && ! ( T_CLASS === $structure_type && '{' === $token ) ) {
216
217 // Check whether the name has already been defined.
218 if ( $this->check_duplicate_identifier( $structure_type, $token[1] ) ) {
219 switch ( $structure_type ) {
220 case T_FUNCTION:
221 /* translators: %s: PHP function name */
222 $message = __( 'Cannot redeclare function %s.', 'code-snippets' );
223 break;
224 case T_CLASS:
225 /* translators: %s: PHP class name */
226 $message = __( 'Cannot redeclare class %s.', 'code-snippets' );
227 break;
228 case T_INTERFACE:
229 /* translators: %s: PHP interface name */
230 $message = __( 'Cannot redeclare interface %s.', 'code-snippets' );
231 break;
232 default:
233 /* translators: %s: PHP identifier name*/
234 $message = __( 'Cannot redeclare %s.', 'code-snippets' );
235 }
236
237 return array(
238 'message' => sprintf( $message, $token[1] ),
239 'line' => $token[2],
240 );
241 }
242 }
243
244 // If we have entered into a class, eat tokens until we find the closing brace.
245 if ( T_CLASS !== $structure_type ) {
246 continue;
247 }
248
249 // Find the opening brace for the class.
250 while ( ! $this->end() && '{' !== $token ) {
251 $token = $this->peek();
252 $this->next();
253 }
254
255 // Continue traversing the class tokens until we have found the class closing brace.
256 $depth = 1;
257 while ( ! $this->end() && $depth > 0 ) {
258 $token = $this->peek();
259
260 if ( '{' === $token ) {
261 ++$depth;
262 } elseif ( '}' === $token ) {
263 --$depth;
264 }
265
266 $this->next();
267 }
268
269 // If we did not make it out of the class, then there's a problem.
270 if ( $depth > 0 ) {
271 return array(
272 'message' => __( 'Parse error: syntax error, unexpected end of snippet.', 'code-snippets' ),
273 'line' => $token[2],
274 );
275 }
276 }
277
278 return false;
279 }
280 }
281