comparison vendor/psy/psysh/src/CodeCleaner/ValidClassNamePass.php @ 13:5fb285c0d0e3

Update Drupal core to 8.4.7 via Composer. Security update; I *think* we've been lucky to get away with this so far, as we don't support self-registration which seems to be used by the so-called "drupalgeddon 2" attack that 8.4.5 was vulnerable to.
author Chris Cannam
date Mon, 23 Apr 2018 09:33:26 +0100
parents
children c2387f117808
comparison
equal deleted inserted replaced
12:7a779792577d 13:5fb285c0d0e3
1 <?php
2
3 /*
4 * This file is part of Psy Shell.
5 *
6 * (c) 2012-2018 Justin Hileman
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Psy\CodeCleaner;
13
14 use PhpParser\Node;
15 use PhpParser\Node\Expr;
16 use PhpParser\Node\Expr\ClassConstFetch;
17 use PhpParser\Node\Expr\New_;
18 use PhpParser\Node\Expr\StaticCall;
19 use PhpParser\Node\Stmt;
20 use PhpParser\Node\Stmt\Class_;
21 use PhpParser\Node\Stmt\Do_;
22 use PhpParser\Node\Stmt\If_;
23 use PhpParser\Node\Stmt\Interface_;
24 use PhpParser\Node\Stmt\Switch_;
25 use PhpParser\Node\Stmt\Trait_;
26 use PhpParser\Node\Stmt\While_;
27 use Psy\Exception\FatalErrorException;
28
29 /**
30 * Validate that classes exist.
31 *
32 * This pass throws a FatalErrorException rather than letting PHP run
33 * headfirst into a real fatal error and die.
34 */
35 class ValidClassNamePass extends NamespaceAwarePass
36 {
37 const CLASS_TYPE = 'class';
38 const INTERFACE_TYPE = 'interface';
39 const TRAIT_TYPE = 'trait';
40
41 protected $checkTraits;
42 private $conditionalScopes = 0;
43 private $atLeastPhp55;
44
45 public function __construct()
46 {
47 $this->checkTraits = function_exists('trait_exists');
48 $this->atLeastPhp55 = version_compare(PHP_VERSION, '5.5', '>=');
49 }
50
51 /**
52 * Validate class, interface and trait definitions.
53 *
54 * Validate them upon entering the node, so that we know about their
55 * presence and can validate constant fetches and static calls in class or
56 * trait methods.
57 *
58 * @param Node $node
59 */
60 public function enterNode(Node $node)
61 {
62 parent::enterNode($node);
63
64 if (self::isConditional($node)) {
65 $this->conditionalScopes++;
66 } else {
67 // @todo add an "else" here which adds a runtime check for instances where we can't tell
68 // whether a class is being redefined by static analysis alone.
69 if ($this->conditionalScopes === 0) {
70 if ($node instanceof Class_) {
71 $this->validateClassStatement($node);
72 } elseif ($node instanceof Interface_) {
73 $this->validateInterfaceStatement($node);
74 } elseif ($node instanceof Trait_) {
75 $this->validateTraitStatement($node);
76 }
77 }
78 }
79 }
80
81 /**
82 * Validate `new` expressions, class constant fetches, and static calls.
83 *
84 * @throws FatalErrorException if a class, interface or trait is referenced which does not exist
85 * @throws FatalErrorException if a class extends something that is not a class
86 * @throws FatalErrorException if a class implements something that is not an interface
87 * @throws FatalErrorException if an interface extends something that is not an interface
88 * @throws FatalErrorException if a class, interface or trait redefines an existing class, interface or trait name
89 *
90 * @param Node $node
91 */
92 public function leaveNode(Node $node)
93 {
94 if (self::isConditional($node)) {
95 $this->conditionalScopes--;
96 } elseif ($node instanceof New_) {
97 $this->validateNewExpression($node);
98 } elseif ($node instanceof ClassConstFetch) {
99 $this->validateClassConstFetchExpression($node);
100 } elseif ($node instanceof StaticCall) {
101 $this->validateStaticCallExpression($node);
102 }
103 }
104
105 private static function isConditional(Node $node)
106 {
107 return $node instanceof If_ ||
108 $node instanceof While_ ||
109 $node instanceof Do_ ||
110 $node instanceof Switch_;
111 }
112
113 /**
114 * Validate a class definition statement.
115 *
116 * @param Class_ $stmt
117 */
118 protected function validateClassStatement(Class_ $stmt)
119 {
120 $this->ensureCanDefine($stmt);
121 if (isset($stmt->extends)) {
122 $this->ensureClassExists($this->getFullyQualifiedName($stmt->extends), $stmt);
123 }
124 $this->ensureInterfacesExist($stmt->implements, $stmt);
125 }
126
127 /**
128 * Validate an interface definition statement.
129 *
130 * @param Interface_ $stmt
131 */
132 protected function validateInterfaceStatement(Interface_ $stmt)
133 {
134 $this->ensureCanDefine($stmt);
135 $this->ensureInterfacesExist($stmt->extends, $stmt);
136 }
137
138 /**
139 * Validate a trait definition statement.
140 *
141 * @param Trait_ $stmt
142 */
143 protected function validateTraitStatement(Trait_ $stmt)
144 {
145 $this->ensureCanDefine($stmt);
146 }
147
148 /**
149 * Validate a `new` expression.
150 *
151 * @param New_ $stmt
152 */
153 protected function validateNewExpression(New_ $stmt)
154 {
155 // if class name is an expression or an anonymous class, give it a pass for now
156 if (!$stmt->class instanceof Expr && !$stmt->class instanceof Class_) {
157 $this->ensureClassExists($this->getFullyQualifiedName($stmt->class), $stmt);
158 }
159 }
160
161 /**
162 * Validate a class constant fetch expression's class.
163 *
164 * @param ClassConstFetch $stmt
165 */
166 protected function validateClassConstFetchExpression(ClassConstFetch $stmt)
167 {
168 // there is no need to check exists for ::class const for php 5.5 or newer
169 if (strtolower($stmt->name) === 'class' && $this->atLeastPhp55) {
170 return;
171 }
172
173 // if class name is an expression, give it a pass for now
174 if (!$stmt->class instanceof Expr) {
175 $this->ensureClassOrInterfaceExists($this->getFullyQualifiedName($stmt->class), $stmt);
176 }
177 }
178
179 /**
180 * Validate a class constant fetch expression's class.
181 *
182 * @param StaticCall $stmt
183 */
184 protected function validateStaticCallExpression(StaticCall $stmt)
185 {
186 // if class name is an expression, give it a pass for now
187 if (!$stmt->class instanceof Expr) {
188 $this->ensureMethodExists($this->getFullyQualifiedName($stmt->class), $stmt->name, $stmt);
189 }
190 }
191
192 /**
193 * Ensure that no class, interface or trait name collides with a new definition.
194 *
195 * @throws FatalErrorException
196 *
197 * @param Stmt $stmt
198 */
199 protected function ensureCanDefine(Stmt $stmt)
200 {
201 $name = $this->getFullyQualifiedName($stmt->name);
202
203 // check for name collisions
204 $errorType = null;
205 if ($this->classExists($name)) {
206 $errorType = self::CLASS_TYPE;
207 } elseif ($this->interfaceExists($name)) {
208 $errorType = self::INTERFACE_TYPE;
209 } elseif ($this->traitExists($name)) {
210 $errorType = self::TRAIT_TYPE;
211 }
212
213 if ($errorType !== null) {
214 throw $this->createError(sprintf('%s named %s already exists', ucfirst($errorType), $name), $stmt);
215 }
216
217 // Store creation for the rest of this code snippet so we can find local
218 // issue too
219 $this->currentScope[strtolower($name)] = $this->getScopeType($stmt);
220 }
221
222 /**
223 * Ensure that a referenced class exists.
224 *
225 * @throws FatalErrorException
226 *
227 * @param string $name
228 * @param Stmt $stmt
229 */
230 protected function ensureClassExists($name, $stmt)
231 {
232 if (!$this->classExists($name)) {
233 throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt);
234 }
235 }
236
237 /**
238 * Ensure that a referenced class _or interface_ exists.
239 *
240 * @throws FatalErrorException
241 *
242 * @param string $name
243 * @param Stmt $stmt
244 */
245 protected function ensureClassOrInterfaceExists($name, $stmt)
246 {
247 if (!$this->classExists($name) && !$this->interfaceExists($name)) {
248 throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt);
249 }
250 }
251
252 /**
253 * Ensure that a statically called method exists.
254 *
255 * @throws FatalErrorException
256 *
257 * @param string $class
258 * @param string $name
259 * @param Stmt $stmt
260 */
261 protected function ensureMethodExists($class, $name, $stmt)
262 {
263 $this->ensureClassExists($class, $stmt);
264
265 // let's pretend all calls to self, parent and static are valid
266 if (in_array(strtolower($class), ['self', 'parent', 'static'])) {
267 return;
268 }
269
270 // ... and all calls to classes defined right now
271 if ($this->findInScope($class) === self::CLASS_TYPE) {
272 return;
273 }
274
275 // if method name is an expression, give it a pass for now
276 if ($name instanceof Expr) {
277 return;
278 }
279
280 if (!method_exists($class, $name) && !method_exists($class, '__callStatic')) {
281 throw $this->createError(sprintf('Call to undefined method %s::%s()', $class, $name), $stmt);
282 }
283 }
284
285 /**
286 * Ensure that a referenced interface exists.
287 *
288 * @throws FatalErrorException
289 *
290 * @param Interface_[] $interfaces
291 * @param Stmt $stmt
292 */
293 protected function ensureInterfacesExist($interfaces, $stmt)
294 {
295 foreach ($interfaces as $interface) {
296 /** @var string $name */
297 $name = $this->getFullyQualifiedName($interface);
298 if (!$this->interfaceExists($name)) {
299 throw $this->createError(sprintf('Interface \'%s\' not found', $name), $stmt);
300 }
301 }
302 }
303
304 /**
305 * Get a symbol type key for storing in the scope name cache.
306 *
307 * @param Stmt $stmt
308 *
309 * @return string
310 */
311 protected function getScopeType(Stmt $stmt)
312 {
313 if ($stmt instanceof Class_) {
314 return self::CLASS_TYPE;
315 } elseif ($stmt instanceof Interface_) {
316 return self::INTERFACE_TYPE;
317 } elseif ($stmt instanceof Trait_) {
318 return self::TRAIT_TYPE;
319 }
320 }
321
322 /**
323 * Check whether a class exists, or has been defined in the current code snippet.
324 *
325 * Gives `self`, `static` and `parent` a free pass.
326 *
327 * @param string $name
328 *
329 * @return bool
330 */
331 protected function classExists($name)
332 {
333 // Give `self`, `static` and `parent` a pass. This will actually let
334 // some errors through, since we're not checking whether the keyword is
335 // being used in a class scope.
336 if (in_array(strtolower($name), ['self', 'static', 'parent'])) {
337 return true;
338 }
339
340 return class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE;
341 }
342
343 /**
344 * Check whether an interface exists, or has been defined in the current code snippet.
345 *
346 * @param string $name
347 *
348 * @return bool
349 */
350 protected function interfaceExists($name)
351 {
352 return interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE;
353 }
354
355 /**
356 * Check whether a trait exists, or has been defined in the current code snippet.
357 *
358 * @param string $name
359 *
360 * @return bool
361 */
362 protected function traitExists($name)
363 {
364 return $this->checkTraits && (trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE);
365 }
366
367 /**
368 * Find a symbol in the current code snippet scope.
369 *
370 * @param string $name
371 *
372 * @return string|null
373 */
374 protected function findInScope($name)
375 {
376 $name = strtolower($name);
377 if (isset($this->currentScope[$name])) {
378 return $this->currentScope[$name];
379 }
380 }
381
382 /**
383 * Error creation factory.
384 *
385 * @param string $msg
386 * @param Stmt $stmt
387 *
388 * @return FatalErrorException
389 */
390 protected function createError($msg, $stmt)
391 {
392 return new FatalErrorException($msg, 0, E_ERROR, null, $stmt->getLine());
393 }
394 }