Completed
Push — member-groupset-delete ( a90a9a )
by Loz
11:22
created

SS_ConfigStaticManifest_Parser::lastToken()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 2
nop 1
dl 0
loc 12
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * A utility class which builds a manifest of the statics defined in all classes, along with their
4
 * access levels and values
5
 *
6
 * We use this to make the statics that the Config system uses as default values be truely immutable.
7
 *
8
 * It has the side effect of allowing Config to avoid private-level access restrictions, so we can
9
 * optionally catch attempts to modify the config statics (otherwise the modification will appear
10
 * to work, but won't actually have any effect - the equvilent of failing silently)
11
 *
12
 * @package framework
13
 * @subpackage manifest
14
 */
15
class SS_ConfigStaticManifest {
16
17
	protected $base;
18
	protected $tests;
19
20
	protected $cache;
21
	protected $key;
22
23
	protected $index;
24
	protected $statics;
25
26
	static protected $initial_classes = array(
27
		'Object', 'ViewableData', 'Injector', 'Director'
28
	);
29
30
	/**
31
	 * Constructs and initialises a new config static manifest, either loading the data
32
	 * from the cache or re-scanning for classes.
33
	 *
34
	 * @param string $base The manifest base path.
35
	 * @param bool   $includeTests Include the contents of "tests" directories.
36
	 * @param bool   $forceRegen Force the manifest to be regenerated.
37
	 * @param bool   $cache If the manifest is regenerated, cache it.
38
	 */
39
	public function __construct($base, $includeTests = false, $forceRegen = false, $cache = true) {
40
		$this->base  = $base;
41
		$this->tests = $includeTests;
42
43
		$cacheClass = defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE : 'ManifestCache_File';
44
45
		$this->cache = new $cacheClass('staticmanifest'.($includeTests ? '_tests' : ''));
46
		$this->key = sha1($base);
47
48
		if(!$forceRegen) {
49
			$this->index = $this->cache->load($this->key);
50
		}
51
52
		if($this->index) {
53
			$this->statics = $this->index['$statics'];
54
		}
55
		else {
56
			$this->regenerate($cache);
57
		}
58
	}
59
60
	public function get($class, $name, $default) {
61
		if (!isset($this->statics[$class])) {
62
			if (isset($this->index[$class])) {
63
				$info = $this->index[$class];
64
65
				if (isset($info['key']) && $details = $this->cache->load($this->key.'_'.$info['key'])) {
66
					$this->statics += $details;
67
				}
68
69
				if (!isset($this->statics[$class])) {
70
					$this->handleFile(null, $info['path'], null);
71
				}
72
			}
73
			else {
74
				$this->statics[$class] = false;
75
			}
76
		}
77
78
		if (isset($this->statics[$class][$name])) {
79
			$static = $this->statics[$class][$name];
80
81
			if ($static['access'] != T_PRIVATE) {
82
				Deprecation::notice('4.0', "Config static $class::\$$name must be marked as private",
83
					Deprecation::SCOPE_GLOBAL);
84
				// Don't warn more than once per static
85
				$this->statics[$class][$name]['access'] = T_PRIVATE;
86
			}
87
88
			return $static['value'];
89
		}
90
91
		return $default;
92
	}
93
94
	/**
95
	 * Completely regenerates the manifest file.
96
	 */
97
	public function regenerate($cache = true) {
98
		$this->index = array('$statics' => array());
99
		$this->statics = array();
100
101
		$finder = new ManifestFileFinder();
102
		$finder->setOptions(array(
103
			'name_regex'    => '/^([^_].*\.php)$/',
104
			'ignore_files'  => array('index.php', 'main.php', 'cli-script.php', 'SSTemplateParser.php'),
105
			'ignore_tests'  => !$this->tests,
106
			'file_callback' => array($this, 'handleFile')
107
		));
108
109
		$finder->find($this->base);
110
111
		if($cache) {
112
			$keysets = array();
113
114
			foreach ($this->statics as $class => $details) {
115
				if (in_array($class, self::$initial_classes)) {
116
					$this->index['$statics'][$class] = $details;
117
				}
118
				else {
119
					$key = sha1($class);
120
					$this->index[$class]['key'] = $key;
121
122
					$keysets[$key][$class] = $details;
123
				}
124
			}
125
126
			foreach ($keysets as $key => $details) {
127
				$this->cache->save($details, $this->key.'_'.$key);
128
			}
129
130
			$this->cache->save($this->index, $this->key);
131
		}
132
	}
133
134
	public function handleFile($basename, $pathname, $depth) {
0 ignored issues
show
Unused Code introduced by
The parameter $depth is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
135
		$parser = new SS_ConfigStaticManifest_Parser($pathname);
136
		$parser->parse();
137
138
		$this->index = array_merge($this->index, $parser->getInfo());
139
		$this->statics = array_merge($this->statics, $parser->getStatics());
140
	}
141
142
	public function getStatics() {
143
		return $this->statics;
144
	}
145
}
146
147
/**
148
 * A parser that processes a PHP file, using PHP's built in parser to get a string of tokens,
149
 * then processing them to find the static class variables, their access levels & values
150
 *
151
 * We can't do this using TokenisedRegularExpression because we need to keep track of state
152
 * as we process the token list (when we enter and leave a namespace or class, when we see
153
 * an access level keyword, etc)
154
 *
155
 * @package framework
156
 * @subpackage manifest
157
 */
158
class SS_ConfigStaticManifest_Parser {
159
160
	protected $info = array();
161
	protected $statics = array();
162
163
	protected $path;
164
	protected $tokens;
165
	protected $length;
166
	protected $pos;
167
168
	function __construct($path) {
169
		$this->path = $path;
170
		$file = file_get_contents($path);
171
172
		$this->tokens = token_get_all($file);
173
		$this->length = count($this->tokens);
174
		$this->pos = 0;
175
	}
176
177
	function getInfo() {
178
		return $this->info;
179
	}
180
181
	function getStatics() {
182
		return $this->statics;
183
	}
184
185
	/**
186
	 * Get the next token to process, incrementing the pointer
187
	 *
188
	 * @param bool $ignoreWhitespace - if true will skip any whitespace tokens & only return non-whitespace ones
189
	 * @return null | mixed - Either the next token or null if there isn't one
190
	 */
191
	protected function next($ignoreWhitespace = true) {
192
		do {
193
			if($this->pos >= $this->length) return null;
194
			$next = $this->tokens[$this->pos++];
195
		}
196
		while($ignoreWhitespace && is_array($next) && $next[0] == T_WHITESPACE);
197
198
		return $next;
199
	}
200
201
	/**
202
	 * Get the previous token processed. Does *not* decrement the pointer
203
	 *
204
	 * @param bool $ignoreWhitespace - if true will skip any whitespace tokens & only return non-whitespace ones
205
	 * @return null | mixed - Either the previous token or null if there isn't one
206
	 */
207
	protected function lastToken($ignoreWhitespace = true) {
208
		// Subtract 1 as the pointer is always 1 place ahead of the current token
209
		$pos = $this->pos - 1;
210
		do {
211
			if($pos <= 0) return null;
212
			$pos--;
213
			$prev = $this->tokens[$pos];
214
		}
215
		while($ignoreWhitespace && is_array($prev) && $prev[0] == T_WHITESPACE);
216
217
		return $prev;
218
	}
219
220
	/**
221
	 * Get the next set of tokens that form a string to process,
222
	 * incrementing the pointer
223
	 *
224
	 * @param bool $ignoreWhitespace - if true will skip any whitespace tokens
225
	 *             & only return non-whitespace ones
226
	 * @return null|string - Either the next string or null if there isn't one
227
	 */
228
	protected function nextString($ignoreWhitespace = true) {
229
		static $stop = array('{', '}', '(', ')', '[', ']');
230
231
		$string = '';
232
		while ($this->pos < $this->length) {
233
			$next = $this->tokens[$this->pos];
234
			if (is_string($next)) {
235
				if (!in_array($next, $stop)) {
236
					$string .= $next;
237
				} else {
238
					break;
239
				}
240
			} else if ($next[0] == T_STRING) {
241
				$string .= $next[1];
242
			} else if ($next[0] != T_WHITESPACE || !$ignoreWhitespace) {
243
				break;
244
			}
245
			$this->pos++;
246
		}
247
		if ($string === '') {
248
			return null;
249
		} else {
250
			return $string;
251
		}
252
	}
253
254
	/**
255
	 * Parse the given file to find the static variables declared in it, along with their access & values
256
	 */
257
	function parse() {
258
		$depth = 0; $namespace = null; $class = null; $clsdepth = null; $access = 0;
259
260
		while($token = $this->next()) {
261
			$type = ($token === (array)$token) ? $token[0] : $token;
262
263
			if($type == T_CLASS) {
264
				$lastToken = $this->lastToken();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $lastToken is correct as $this->lastToken() (which targets SS_ConfigStaticManifest_Parser::lastToken()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
265
				$lastType = ($lastToken === (array)$lastToken) ? $lastToken[0] : $lastToken;
266
267
				// Ignore class keyword if it's being used for class name resolution: ClassName::class
268
				if ($lastType === T_PAAMAYIM_NEKUDOTAYIM) {
269
					continue;
270
				}
271
272
				$next = $this->nextString();
273
				if($next === null) {
274
					user_error("Couldn\'t parse {$this->path} when building config static manifest", E_USER_ERROR);
275
				}
276
277
				$class = $next;
278
			}
279
			else if($type == T_NAMESPACE) {
280
				$namespace = '';
281
				while(true) {
282
					$next = $this->next();
283
284
					if($next == ';') {
285
						break;
286
					} elseif($next[0] == T_NS_SEPARATOR) {
287
						$namespace .= $next[1];
288
						$next = $this->next();
289
					}
290
291
					if(!is_string($next) && $next[0] != T_STRING) {
292
						user_error("Couldn\'t parse {$this->path} when building config static manifest", E_USER_ERROR);
293
					}
294
295
					$namespace .= is_string($next) ? $next : $next[1];
296
				}
297
			}
298
			else if($type == '{' || $type == T_CURLY_OPEN || $type == T_DOLLAR_OPEN_CURLY_BRACES){
299
				$depth += 1;
300
				if($class && !$clsdepth) $clsdepth = $depth;
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $clsdepth of type null|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
301
			}
302
			else if($type == '}') {
303
				$depth -= 1;
304
				if($depth < $clsdepth) $class = $clsdepth = null;
305
				if($depth < 0) user_error("Hmm - depth calc wrong, hit negatives, see: ".$this->path, E_USER_ERROR);
306
			}
307
			else if($type == T_PUBLIC || $type == T_PRIVATE || $type == T_PROTECTED) {
308
				$access = $type;
309
			}
310
			else if($type == T_STATIC && $class && $depth == $clsdepth) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
311
				$this->parseStatic($access, $namespace ? $namespace.'\\'.$class : $class);
312
				$access = 0;
313
			}
314
			else {
315
				$access = 0;
316
			}
317
		}
318
	}
319
320
	/**
321
	 * During parsing we've found a "static" keyword. Parse out the variable names and value
322
	 * assignments that follow.
323
	 *
324
	 * Seperated out from parse partially so that we can recurse if there are multiple statics
325
	 * being declared in a comma seperated list
326
	 */
327
	function parseStatic($access, $class) {
328
		$variable = null;
329
		$value = '';
330
331
		while($token = $this->next()) {
332
			$type = ($token === (array)$token) ? $token[0] : $token;
333
334
			if($type == T_PUBLIC || $type == T_PRIVATE || $type == T_PROTECTED) {
335
				$access = $type;
336
			}
337
			else if($type == T_FUNCTION) {
338
				return;
339
			}
340
			else if($type == T_VARIABLE) {
341
				$variable = substr($token[1], 1); // Cut off initial "$"
342
			}
343
			else if($type == ';' || $type == ',' || $type == '=') {
344
				break;
345
			}
346
			else if($type == T_COMMENT || $type == T_DOC_COMMENT) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
347
				// NOP
348
			}
349
			else {
350
				user_error('Unexpected token ("' . token_name($type) . '") when building static manifest in class "'
351
					. $class . '": '.print_r($token, true), E_USER_ERROR);
352
			}
353
		}
354
355
		if($token == '=') {
356
			$depth = 0;
357
358
			while($token = ($this->pos >= $this->length) ? null : $this->tokens[$this->pos++]) {
359
				$type = ($token === (array)$token) ? $token[0] : $token;
360
361
				// Track array nesting depth
362
				if($type == T_ARRAY || $type == '[') {
363
					$depth += 1;
364
				} elseif($type == ')' || $type == ']') {
365
					$depth -= 1;
366
				}
367
368
				// Parse out the assignment side of a static declaration,
369
				// ending on either a ';' or a ',' outside an array
370
				if($type == T_WHITESPACE) {
371
					$value .= ' ';
372
				}
373
				else if($type == ';' || ($type == ',' && !$depth)) {
374
					break;
375
				}
376
				// Statics can reference class constants with self:: (and that won't work in eval)
377
				else if($type == T_STRING && $token[1] == 'self') {
378
					$value .= $class;
379
				}
380
				else {
381
					$value .= ($token === (array)$token) ? $token[1] : $token;
382
				}
383
			}
384
		}
385
386
		if (!isset($this->info[$class])) {
387
			$this->info[$class] = array(
388
				'path' => $this->path,
389
				'mtime' => filemtime($this->path),
390
			);
391
		}
392
393
		if(!isset($this->statics[$class])) {
394
			$this->statics[$class] = array();
395
		}
396
397
		$value = trim($value);
398
		if ($value) {
399
			$value = eval('static $temp = '.$value.";\n".'return $temp'.";\n");
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
400
		}
401
		else {
402
			$value = null;
403
		}
404
405
		$this->statics[$class][$variable] = array(
406
			'access' => $access,
407
			'value' => $value
408
		);
409
410
		if($token == ',') $this->parseStatic($access, $class);
411
	}
412
}
413