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
← All changes | php/Utils/Validator.php +107 -9 3.10.1 → trunk View file →
@@ -51,13 +51,36 @@
51 51 */
52 52 private array $exceptions = [];
53 53
54 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 + /**
55 75 * Class constructor.
56 76 *
57 - * @param string $code Snippet code for parsing.
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.
58 80 */
59 - public function __construct( string $code ) {
81 + public function __construct( string $code, array $claimed_identifiers = [] ) {
82 + $this->claimed_identifiers = $claimed_identifiers;
60 83 $this->code = $code;
61 84 $this->tokens = token_get_all( "<?php\n" . $this->code );
62 85 $this->length = count( $this->tokens );
63 86 $this->current = 0;
@@ -63,8 +86,20 @@
63 86 $this->current = 0;
64 87 }
65 88
66 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 + /**
67 102 * Determine whether the parser has reached the end of the list of tokens.
68 103 *
69 104 * @return bool
70 105 */
@@ -101,17 +136,20 @@
101 136 * @return bool true if the identifier is not unique.
102 137 */
103 138 private function check_duplicate_identifier( string $type, string $identifier ): bool {
104 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;
105 144 $namespaced_identifier = 'code_snippets\\' . $identifier;
106 145
107 146 if ( ! isset( $this->defined_identifiers[ $type ] ) ) {
108 147 switch ( $type ) {
109 148 case T_FUNCTION:
110 - $defined_functions = get_defined_functions();
111 149 $this->defined_identifiers[ T_FUNCTION ] = array_map(
112 150 'strtolower',
113 - array_merge( $defined_functions['internal'], $defined_functions['user'] )
151 + array_merge( get_defined_functions()['internal'], get_defined_functions()['user'] )
114 152 );
115 153 break;
116 154
117 155 case T_CLASS:
@@ -126,20 +164,75 @@
126 164 return false;
127 165 }
128 166 }
129 167
130 - $duplicate_identifier = in_array( $identifier, $this->defined_identifiers[ $type ], true );
131 - $duplicate_namespaced = in_array( $namespaced_identifier, $this->defined_identifiers[ $type ], true );
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 );
132 175 $exceptions = $this->exceptions[ $type ] ?? [];
133 - $exception_identifier = in_array( $identifier, $exceptions, true );
134 - $exception_namespaced = in_array( $namespaced_identifier, $exceptions, true );
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 );
135 178
136 - array_unshift( $this->defined_identifiers[ $type ], $identifier );
179 + array_unshift( $this->defined_identifiers[ $type ], $qualified );
180 + $this->claimed_identifiers[ $type ][] = $qualified;
137 181
138 182 return ( $duplicate_identifier && ! $exception_identifier ) || ( $duplicate_namespaced && ! $exception_namespaced );
139 183 }
140 184
141 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 + /**
142 235 * Validate the given PHP code and return the result.
143 236 *
144 237 * @return array<string, mixed>|false Array containing message if an error was encountered, false if validation was successful.
145 238 */
@@ -149,8 +242,13 @@
149 242 $token = $this->peek();
150 243 $this->next();
151 244
152 245 if ( ! is_array( $token ) ) {
246 + continue;
247 + }
248 +
249 + if ( T_NAMESPACE === $token[0] ) {
250 + $this->namespace = $this->read_namespace_declaration();
153 251 continue;
154 252 }
155 253
156 254 // If this is a function or class exists check, then allow this function or class to be defined.