base = $base; $this->tests = $includeTests; $cacheClass = defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE : 'ManifestCache_File'; $this->cache = new $cacheClass('staticmanifest'.($includeTests ? '_tests' : '')); $this->key = sha1($base); if(!$forceRegen) { $this->index = $this->cache->load($this->key); } if($this->index) { $this->statics = $this->index['$statics']; } else { $this->regenerate($cache); } } public function get($class, $name, $default) { if (!isset($this->statics[$class])) { if (isset($this->index[$class])) { $info = $this->index[$class]; if (isset($info['key']) && $details = $this->cache->load($this->key.'_'.$info['key'])) { $this->statics += $details; } if (!isset($this->statics[$class])) { $this->handleFile(null, $info['path'], null); } } else { $this->statics[$class] = false; } } if (isset($this->statics[$class][$name])) { $static = $this->statics[$class][$name]; if ($static['access'] != T_PRIVATE) { Deprecation::notice('4.0', "Config static $class::\$$name must be marked as private", Deprecation::SCOPE_GLOBAL); // Don't warn more than once per static $this->statics[$class][$name]['access'] = T_PRIVATE; } return $static['value']; } return $default; } /** * Completely regenerates the manifest file. */ public function regenerate($cache = true) { $this->index = array('$statics' => array()); $this->statics = array(); $finder = new ManifestFileFinder(); $finder->setOptions(array( 'name_regex' => '/^([^_].*\.php)$/', 'ignore_files' => array('index.php', 'main.php', 'cli-script.php', 'SSTemplateParser.php'), 'ignore_tests' => !$this->tests, 'file_callback' => array($this, 'handleFile') )); $finder->find($this->base); if($cache) { $keysets = array(); foreach ($this->statics as $class => $details) { if (in_array($class, self::$initial_classes)) { $this->index['$statics'][$class] = $details; } else { $key = sha1($class); $this->index[$class]['key'] = $key; $keysets[$key][$class] = $details; } } foreach ($keysets as $key => $details) { $this->cache->save($details, $this->key.'_'.$key); } $this->cache->save($this->index, $this->key); } } public function handleFile($basename, $pathname, $depth) { $parser = new SS_ConfigStaticManifest_Parser($pathname); $parser->parse(); $this->index = array_merge($this->index, $parser->getInfo()); $this->statics = array_merge($this->statics, $parser->getStatics()); } public function getStatics() { return $this->statics; } } /** * A parser that processes a PHP file, using PHP's built in parser to get a string of tokens, * then processing them to find the static class variables, their access levels & values * * We can't do this using TokenisedRegularExpression because we need to keep track of state * as we process the token list (when we enter and leave a namespace or class, when we see * an access level keyword, etc) * * @package framework * @subpackage manifest */ class SS_ConfigStaticManifest_Parser { protected $info = array(); protected $statics = array(); protected $path; protected $tokens; protected $length; protected $pos; function __construct($path) { $this->path = $path; $file = file_get_contents($path); $this->tokens = token_get_all($file); $this->length = count($this->tokens); $this->pos = 0; } function getInfo() { return $this->info; } function getStatics() { return $this->statics; } /** * Get the next token to process, incrementing the pointer * * @param bool $ignoreWhitespace - if true will skip any whitespace tokens & only return non-whitespace ones * @return null | mixed - Either the next token or null if there isn't one */ protected function next($ignoreWhitespace = true) { do { if($this->pos >= $this->length) return null; $next = $this->tokens[$this->pos++]; } while($ignoreWhitespace && is_array($next) && $next[0] == T_WHITESPACE); return $next; } /** * Get the next set of tokens that form a string to process, * incrementing the pointer * * @param bool $ignoreWhitespace - if true will skip any whitespace tokens * & only return non-whitespace ones * @return null|string - Either the next string or null if there isn't one */ protected function nextString($ignoreWhitespace = true) { static $stop = array('{', '}', '(', ')', '[', ']'); $string = ''; while ($this->pos < $this->length) { $next = $this->tokens[$this->pos]; if (is_string($next)) { if (!in_array($next, $stop)) { $string .= $next; } else { break; } } else if ($next[0] == T_STRING) { $string .= $next[1]; } else if ($next[0] != T_WHITESPACE || !$ignoreWhitespace) { break; } $this->pos++; } if ($string === '') { return null; } else { return $string; } } /** * Parse the given file to find the static variables declared in it, along with their access & values */ function parse() { $depth = 0; $namespace = null; $class = null; $clsdepth = null; $access = 0; while($token = $this->next()) { $type = ($token === (array)$token) ? $token[0] : $token; if($type == T_CLASS) { // Fetch the last token $pos = $this->pos - 1; // Subtract 1 as the pointer is always 1 place ahead of the current token do { $pos--; $prev = $this->tokens[$pos]; } while ($pos > 0 && is_array($prev) && $prev[0] == T_WHITESPACE); // Ignore class keyword if it's being used for class name resolution: ClassName::class $lastType = ($prev === (array)$prev) ? $prev[0] : $prev; if ($lastType === T_PAAMAYIM_NEKUDOTAYIM) { continue; } $next = $this->nextString(); if($next === null) { user_error("Couldn\'t parse {$this->path} when building config static manifest", E_USER_ERROR); } $class = $next; } else if($type == T_NAMESPACE) { $namespace = ''; while(true) { $next = $this->next(); if($next == ';') { break; } elseif($next[0] == T_NS_SEPARATOR) { $namespace .= $next[1]; $next = $this->next(); } if(!is_string($next) && $next[0] != T_STRING) { user_error("Couldn\'t parse {$this->path} when building config static manifest", E_USER_ERROR); } $namespace .= is_string($next) ? $next : $next[1]; } } else if($type == '{' || $type == T_CURLY_OPEN || $type == T_DOLLAR_OPEN_CURLY_BRACES){ $depth += 1; if($class && !$clsdepth) $clsdepth = $depth; } else if($type == '}') { $depth -= 1; if($depth < $clsdepth) $class = $clsdepth = null; if($depth < 0) user_error("Hmm - depth calc wrong, hit negatives, see: ".$this->path, E_USER_ERROR); } else if($type == T_PUBLIC || $type == T_PRIVATE || $type == T_PROTECTED) { $access = $type; } else if($type == T_STATIC && $class && $depth == $clsdepth) { $this->parseStatic($access, $namespace ? $namespace.'\\'.$class : $class); $access = 0; } else { $access = 0; } } } /** * During parsing we've found a "static" keyword. Parse out the variable names and value * assignments that follow. * * Seperated out from parse partially so that we can recurse if there are multiple statics * being declared in a comma seperated list */ function parseStatic($access, $class) { $variable = null; $value = ''; while($token = $this->next()) { $type = ($token === (array)$token) ? $token[0] : $token; if($type == T_PUBLIC || $type == T_PRIVATE || $type == T_PROTECTED) { $access = $type; } else if($type == T_FUNCTION) { return; } else if($type == T_VARIABLE) { $variable = substr($token[1], 1); // Cut off initial "$" } else if($type == ';' || $type == ',' || $type == '=') { break; } else if($type == T_COMMENT || $type == T_DOC_COMMENT) { // NOP } else { user_error('Unexpected token ("' . token_name($type) . '") when building static manifest in class "' . $class . '": '.print_r($token, true), E_USER_ERROR); } } if($token == '=') { $depth = 0; while($token = ($this->pos >= $this->length) ? null : $this->tokens[$this->pos++]) { $type = ($token === (array)$token) ? $token[0] : $token; // Track array nesting depth if($type == T_ARRAY || $type == '[') { $depth += 1; } elseif($type == ')' || $type == ']') { $depth -= 1; } // Parse out the assignment side of a static declaration, // ending on either a ';' or a ',' outside an array if($type == T_WHITESPACE) { $value .= ' '; } else if($type == ';' || ($type == ',' && !$depth)) { break; } // Statics can reference class constants with self:: (and that won't work in eval) else if($type == T_STRING && $token[1] == 'self') { $value .= $class; } else { $value .= ($token === (array)$token) ? $token[1] : $token; } } } if (!isset($this->info[$class])) { $this->info[$class] = array( 'path' => $this->path, 'mtime' => filemtime($this->path), ); } if(!isset($this->statics[$class])) { $this->statics[$class] = array(); } $value = trim($value); if ($value) { $value = eval('static $temp = '.$value.";\n".'return $temp'.";\n"); } else { $value = null; } $this->statics[$class][$variable] = array( 'access' => $access, 'value' => $value ); if($token == ',') $this->parseStatic($access, $class); } }