pax_global_header00006660000000000000000000000064126306161050014512gustar00rootroot0000000000000052 comment=29a88be2a4846d27c1613aed0c9071dfad7b5938 super_closure-2.2.0/000077500000000000000000000000001263061610500144055ustar00rootroot00000000000000super_closure-2.2.0/LICENSE.md000066400000000000000000000020731263061610500160130ustar00rootroot00000000000000# MIT License Copyright (c) 2010-2015 Jeremy Lindblom Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. **THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.** super_closure-2.2.0/composer.json000066400000000000000000000020201263061610500171210ustar00rootroot00000000000000{ "name": "jeremeamia/superclosure", "type": "library", "description": "Serialize Closure objects, including their context and binding", "keywords": ["closure", "serialize", "serializable", "function", "parser", "tokenizer", "lambda"], "homepage": "https://github.com/jeremeamia/super_closure", "license": "MIT", "authors": [ { "name": "Jeremy Lindblom", "email": "jeremeamia@gmail.com", "homepage": "https://github.com/jeremeamia", "role": "Developer" } ], "require": { "php": ">=5.4", "nikic/php-parser": "^1.2|^2.0", "symfony/polyfill-php56": "^1.0" }, "require-dev": { "phpunit/phpunit": "^4.0|^5.0" }, "autoload": { "psr-4": { "SuperClosure\\": "src/" } }, "autoload-dev": { "psr-4": { "SuperClosure\\Test\\": "tests/" } }, "extra": { "branch-alias": { "dev-master": "2.2-dev" } } } super_closure-2.2.0/src/000077500000000000000000000000001263061610500151745ustar00rootroot00000000000000super_closure-2.2.0/src/Analyzer/000077500000000000000000000000001263061610500167615ustar00rootroot00000000000000super_closure-2.2.0/src/Analyzer/AstAnalyzer.php000066400000000000000000000113121263061610500217250ustar00rootroot00000000000000locateClosure($data); // Make a second pass through the AST, but only through the closure's // nodes, to resolve any magic constants to literal values. $traverser = new NodeTraverser; $traverser->addVisitor(new MagicConstantVisitor($data['location'])); $traverser->addVisitor($thisDetector = new ThisDetectorVisitor); $data['ast'] = $traverser->traverse([$data['ast']])[0]; $data['hasThis'] = $thisDetector->detected; // Bounce the updated AST down to a string representation of the code. $data['code'] = (new NodePrinter)->prettyPrint([$data['ast']]); } /** * Parses the closure's code and produces an abstract syntax tree (AST). * * @param array $data * * @throws ClosureAnalysisException if there is an issue finding the closure */ private function locateClosure(array &$data) { try { $locator = new ClosureLocatorVisitor($data['reflection']); $fileAst = $this->getFileAst($data['reflection']); $fileTraverser = new NodeTraverser; $fileTraverser->addVisitor(new NameResolver); $fileTraverser->addVisitor($locator); $fileTraverser->traverse($fileAst); } catch (ParserError $e) { // @codeCoverageIgnoreStart throw new ClosureAnalysisException( 'There was an error analyzing the closure code.', 0, $e ); // @codeCoverageIgnoreEnd } $data['ast'] = $locator->closureNode; if (!$data['ast']) { // @codeCoverageIgnoreStart throw new ClosureAnalysisException( 'The closure was not found within the abstract syntax tree.' ); // @codeCoverageIgnoreEnd } $data['location'] = $locator->location; } /** * Returns the variables that in the "use" clause of the closure definition. * These are referred to as the "used variables", "static variables", or * "closed upon variables", "context" of the closure. * * @param array $data */ protected function determineContext(array &$data) { // Get the variable names defined in the AST $refs = 0; $vars = array_map(function ($node) use (&$refs) { if ($node->byRef) { $refs++; } return $node->var; }, $data['ast']->uses); $data['hasRefs'] = ($refs > 0); // Get the variable names and values using reflection $values = $data['reflection']->getStaticVariables(); // Combine the names and values to create the canonical context. foreach ($vars as $name) { if (isset($values[$name])) { $data['context'][$name] = $values[$name]; } } } /** * @param \ReflectionFunction $reflection * * @throws ClosureAnalysisException * * @return \PhpParser\Node[] */ private function getFileAst(\ReflectionFunction $reflection) { $fileName = $reflection->getFileName(); if (!file_exists($fileName)) { throw new ClosureAnalysisException( "The file containing the closure, \"{$fileName}\" did not exist." ); } return $this->getParser()->parse(file_get_contents($fileName)); } /** * @return CodeParser */ private function getParser() { if (class_exists('PhpParser\ParserFactory')) { return (new ParserFactory)->create(ParserFactory::PREFER_PHP7); } return new CodeParser(new EmulativeLexer); } } super_closure-2.2.0/src/Analyzer/ClosureAnalyzer.php000066400000000000000000000034201263061610500226130ustar00rootroot00000000000000 new \ReflectionFunction($closure), 'code' => null, 'hasThis' => false, 'context' => [], 'hasRefs' => false, 'binding' => null, 'scope' => null, 'isStatic' => $this->isClosureStatic($closure), ]; $this->determineCode($data); $this->determineContext($data); $this->determineBinding($data); return $data; } abstract protected function determineCode(array &$data); /** * Returns the variables that are in the "use" clause of the closure. * * These variables are referred to as the "used variables", "static * variables", "closed upon variables", or "context" of the closure. * * @param array $data */ abstract protected function determineContext(array &$data); private function determineBinding(array &$data) { $data['binding'] = $data['reflection']->getClosureThis(); if ($scope = $data['reflection']->getClosureScopeClass()) { $data['scope'] = $scope->getName(); } } private function isClosureStatic(\Closure $closure) { $closure = @$closure->bindTo(new \stdClass); if ($closure === null) { return true; } $rebound = new \ReflectionFunction($closure); return $rebound->getClosureThis() === null; } } super_closure-2.2.0/src/Analyzer/Token.php000066400000000000000000000032041263061610500205510ustar00rootroot00000000000000code = $code; $this->value = $value; $this->line = $line; $this->name = $value ? token_name($value) : null; } /** * Determines if the token's value/code is equal to the specified value. * * @param mixed $value The value to check. * * @return bool True if the token is equal to the value. */ public function is($value) { return ($this->code === $value || $this->value === $value); } public function __toString() { return $this->code; } } super_closure-2.2.0/src/Analyzer/TokenAnalyzer.php000066400000000000000000000101061263061610500222560ustar00rootroot00000000000000determineTokens($data); $data['code'] = implode('', $data['tokens']); $data['hasThis'] = (strpos($data['code'], '$this') !== false); } private function determineTokens(array &$data) { $potential = $this->determinePotentialTokens($data['reflection']); $braceLevel = $index = $step = $insideUse = 0; $data['tokens'] = $data['context'] = []; foreach ($potential as $token) { $token = new Token($token); switch ($step) { // Handle tokens before the function declaration. case 0: if ($token->is(T_FUNCTION)) { $data['tokens'][] = $token; $step++; } break; // Handle tokens inside the function signature. case 1: $data['tokens'][] = $token; if ($insideUse) { if ($token->is(T_VARIABLE)) { $varName = trim($token, '$ '); $data['context'][$varName] = null; } elseif ($token->is('&')) { $data['hasRefs'] = true; } } elseif ($token->is(T_USE)) { $insideUse++; } if ($token->is('{')) { $step++; $braceLevel++; } break; // Handle tokens inside the function body. case 2: $data['tokens'][] = $token; if ($token->is('{')) { $braceLevel++; } elseif ($token->is('}')) { $braceLevel--; if ($braceLevel === 0) { $step++; } } break; // Handle tokens after the function declaration. case 3: if ($token->is(T_FUNCTION)) { throw new ClosureAnalysisException('Multiple closures ' . 'were declared on the same line of code. Could not ' . 'determine which closure was the intended target.' ); } break; } } } private function determinePotentialTokens(\ReflectionFunction $reflection) { // Load the file containing the code for the function. $fileName = $reflection->getFileName(); if (!is_readable($fileName)) { throw new ClosureAnalysisException( "Cannot read the file containing the closure: \"{$fileName}\"." ); } $code = ''; $file = new \SplFileObject($fileName); $file->seek($reflection->getStartLine() - 1); while ($file->key() < $reflection->getEndLine()) { $code .= $file->current(); $file->next(); } $code = trim($code); if (strpos($code, 'getStaticVariables(); // Construct the context by combining the variable names and values. foreach ($data['context'] as $name => &$value) { if (isset($values[$name])) { $value = $values[$name]; } } } } super_closure-2.2.0/src/Analyzer/Visitor/000077500000000000000000000000001263061610500204205ustar00rootroot00000000000000super_closure-2.2.0/src/Analyzer/Visitor/ClosureLocatorVisitor.php000066400000000000000000000104451263061610500254550ustar00rootroot00000000000000reflection = $reflection; $this->location = [ 'class' => null, 'directory' => dirname($this->reflection->getFileName()), 'file' => $this->reflection->getFileName(), 'function' => $this->reflection->getName(), 'line' => $this->reflection->getStartLine(), 'method' => null, 'namespace' => null, 'trait' => null, ]; } public function enterNode(AstNode $node) { // Determine information about the closure's location if (!$this->closureNode) { if ($node instanceof NamespaceNode) { $namespace = ($node->name && is_array($node->name->parts)) ? implode('\\', $node->name->parts) : null; $this->location['namespace'] = $namespace; } if ($node instanceof TraitNode) { $this->location['trait'] = $node->name; $this->location['class'] = null; } elseif ($node instanceof ClassNode) { $this->location['class'] = $node->name; $this->location['trait'] = null; } } // Locate the node of the closure if ($node instanceof ClosureNode) { if ($node->getAttribute('startLine') == $this->location['line']) { if ($this->closureNode) { $line = $this->location['file'] . ':' . $node->getAttribute('startLine'); throw new ClosureAnalysisException("Two closures were " . "declared on the same line ({$line}) of code. Cannot " . "determine which closure was the intended target."); } else { $this->closureNode = $node; } } } } public function leaveNode(AstNode $node) { // Determine information about the closure's location if (!$this->closureNode) { if ($node instanceof NamespaceNode) { $this->location['namespace'] = null; } if ($node instanceof TraitNode) { $this->location['trait'] = null; } elseif ($node instanceof ClassNode) { $this->location['class'] = null; } } } public function afterTraverse(array $nodes) { if ($this->location['class']) { $this->location['class'] = $this->location['namespace'] . '\\' . $this->location['class']; $this->location['method'] = "{$this->location['class']}::{$this->location['function']}"; } elseif ($this->location['trait']) { $this->location['trait'] = $this->location['namespace'] . '\\' . $this->location['trait']; $this->location['method'] = "{$this->location['trait']}::{$this->location['function']}"; // If the closure was declared in a trait, then we will do a best // effort guess on the name of the class that used the trait. It's // actually impossible at this point to know for sure what it is. if ($closureScope = $this->reflection->getClosureScopeClass()) { $this->location['class'] = $closureScope ? $closureScope->getName() : null; } elseif ($closureThis = $this->reflection->getClosureThis()) { $this->location['class'] = get_class($closureThis); } } } } super_closure-2.2.0/src/Analyzer/Visitor/MagicConstantVisitor.php000066400000000000000000000031461263061610500252470ustar00rootroot00000000000000location = $location; } public function leaveNode(AstNode $node) { switch ($node->getType()) { case 'Scalar_MagicConst_Class' : return new StringNode($this->location['class']); case 'Scalar_MagicConst_Dir' : return new StringNode($this->location['directory']); case 'Scalar_MagicConst_File' : return new StringNode($this->location['file']); case 'Scalar_MagicConst_Function' : return new StringNode($this->location['function']); case 'Scalar_MagicConst_Line' : return new NumberNode($node->getAttribute('startLine')); case 'Scalar_MagicConst_Method' : return new StringNode($this->location['method']); case 'Scalar_MagicConst_Namespace' : return new StringNode($this->location['namespace']); case 'Scalar_MagicConst_Trait' : return new StringNode($this->location['trait']); } } } super_closure-2.2.0/src/Analyzer/Visitor/ThisDetectorVisitor.php000066400000000000000000000011171263061610500251120ustar00rootroot00000000000000name === 'this') { $this->detected = true; } } } } super_closure-2.2.0/src/Exception/000077500000000000000000000000001263061610500171325ustar00rootroot00000000000000super_closure-2.2.0/src/Exception/ClosureAnalysisException.php000066400000000000000000000003371263061610500246450ustar00rootroot00000000000000closure = $closure; $this->serializer = $serializer ?: new Serializer; } /** * Return the original closure object. * * @return Closure */ public function getClosure() { return $this->closure; } /** * Delegates the closure invocation to the actual closure object. * * Important Notes: * * - `ReflectionFunction::invokeArgs()` should not be used here, because it * does not work with closure bindings. * - Args passed-by-reference lose their references when proxied through * `__invoke()`. This is an unfortunate, but understandable, limitation * of PHP that will probably never change. * * @return mixed */ public function __invoke() { return call_user_func_array($this->closure, func_get_args()); } /** * Clones the SerializableClosure with a new bound object and class scope. * * The method is essentially a wrapped proxy to the Closure::bindTo method. * * @param mixed $newthis The object to which the closure should be bound, * or NULL for the closure to be unbound. * @param mixed $newscope The class scope to which the closure is to be * associated, or 'static' to keep the current one. * If an object is given, the type of the object will * be used instead. This determines the visibility of * protected and private methods of the bound object. * * @return SerializableClosure * @link http://www.php.net/manual/en/closure.bindto.php */ public function bindTo($newthis, $newscope = 'static') { return new self( $this->closure->bindTo($newthis, $newscope), $this->serializer ); } /** * Serializes the code, context, and binding of the closure. * * @return string|null * @link http://php.net/manual/en/serializable.serialize.php */ public function serialize() { try { $this->data = $this->data ?: $this->serializer->getData($this->closure, true); return serialize($this->data); } catch (\Exception $e) { trigger_error( 'Serialization of closure failed: ' . $e->getMessage(), E_USER_NOTICE ); // Note: The serialize() method of Serializable must return a string // or null and cannot throw exceptions. return null; } } /** * Unserializes the closure. * * Unserializes the closure's data and recreates the closure using a * simulation of its original context. The used variables (context) are * extracted into a fresh scope prior to redefining the closure. The * closure is also rebound to its former object and scope. * * @param string $serialized * * @throws ClosureUnserializationException * @link http://php.net/manual/en/serializable.unserialize.php */ public function unserialize($serialized) { // Unserialize the closure data and reconstruct the closure object. $this->data = unserialize($serialized); $this->closure = __reconstruct_closure($this->data); // Throw an exception if the closure could not be reconstructed. if (!$this->closure instanceof Closure) { throw new ClosureUnserializationException( 'The closure is corrupted and cannot be unserialized.' ); } // Rebind the closure to its former binding and scope. if ($this->data['binding'] || $this->data['isStatic']) { $this->closure = $this->closure->bindTo( $this->data['binding'], $this->data['scope'] ); } } /** * Returns closure data for `var_dump()`. * * @return array */ public function __debugInfo() { return $this->data ?: $this->serializer->getData($this->closure, true); } } /** * Reconstruct a closure. * * HERE BE DRAGONS! * * The infamous `eval()` is used in this method, along with the error * suppression operator, and variable variables (i.e., double dollar signs) to * perform the unserialization logic. I'm sorry, world! * * This is also done inside a plain function instead of a method so that the * binding and scope of the closure are null. * * @param array $__data Unserialized closure data. * * @return Closure|null * @internal */ function __reconstruct_closure(array $__data) { // Simulate the original context the closure was created in. foreach ($__data['context'] as $__var_name => &$__value) { if ($__value instanceof SerializableClosure) { // Unbox any SerializableClosures in the context. $__value = $__value->getClosure(); } elseif ($__value === Serializer::RECURSION) { // Track recursive references (there should only be one). $__recursive_reference = $__var_name; } // Import the variable into this scope. ${$__var_name} = $__value; } // Evaluate the code to recreate the closure. try { if (isset($__recursive_reference)) { // Special handling for recursive closures. @eval("\${$__recursive_reference} = {$__data['code']};"); $__closure = ${$__recursive_reference}; } else { @eval("\$__closure = {$__data['code']};"); } } catch (\ParseError $e) { // Discard the parse error. } return isset($__closure) ? $__closure : null; } super_closure-2.2.0/src/Serializer.php000066400000000000000000000157271263061610500200320ustar00rootroot00000000000000 true, 'context' => true, 'binding' => true, 'scope' => true, 'isStatic' => true, ]; /** * The closure analyzer instance. * * @var ClosureAnalyzer */ private $analyzer; /** * The HMAC key to sign serialized closures. * * @var string */ private $signingKey; /** * Create a new serializer instance. * * @param ClosureAnalyzer|null $analyzer Closure analyzer instance. * @param string|null $signingKey HMAC key to sign closure data. */ public function __construct( ClosureAnalyzer $analyzer = null, $signingKey = null ) { $this->analyzer = $analyzer ?: new DefaultAnalyzer; $this->signingKey = $signingKey; } /** * @inheritDoc */ public function serialize(\Closure $closure) { $serialized = serialize(new SerializableClosure($closure, $this)); if ($this->signingKey) { $signature = $this->calculateSignature($serialized); $serialized = '%' . base64_encode($signature) . $serialized; } return $serialized; } /** * @inheritDoc */ public function unserialize($serialized) { // Strip off the signature from the front of the string. $signature = null; if ($serialized[0] === '%') { $signature = base64_decode(substr($serialized, 1, 44)); $serialized = substr($serialized, 45); } // If a key was provided, then verify the signature. if ($this->signingKey) { $this->verifySignature($signature, $serialized); } set_error_handler(function () {}); $unserialized = unserialize($serialized); restore_error_handler(); if ($unserialized === false) { throw new ClosureUnserializationException( 'The closure could not be unserialized.' ); } elseif (!$unserialized instanceof SerializableClosure) { throw new ClosureUnserializationException( 'The closure did not unserialize to a SuperClosure.' ); } return $unserialized->getClosure(); } /** * @inheritDoc */ public function getData(\Closure $closure, $forSerialization = false) { // Use the closure analyzer to get data about the closure. $data = $this->analyzer->analyze($closure); // If the closure data is getting retrieved solely for the purpose of // serializing the closure, then make some modifications to the data. if ($forSerialization) { // If there is no reference to the binding, don't serialize it. if (!$data['hasThis']) { $data['binding'] = null; } // Remove data about the closure that does not get serialized. $data = array_intersect_key($data, self::$dataToKeep); // Wrap any other closures within the context. foreach ($data['context'] as &$value) { if ($value instanceof \Closure) { $value = ($value === $closure) ? self::RECURSION : new SerializableClosure($value, $this); } } } return $data; } /** * Recursively traverses and wraps all Closure objects within the value. * * NOTE: THIS MAY NOT WORK IN ALL USE CASES, SO USE AT YOUR OWN RISK. * * @param mixed $data Any variable that contains closures. * @param SerializerInterface $serializer The serializer to use. */ public static function wrapClosures(&$data, SerializerInterface $serializer) { if ($data instanceof \Closure) { // Handle and wrap closure objects. $reflection = new \ReflectionFunction($data); if ($binding = $reflection->getClosureThis()) { self::wrapClosures($binding, $serializer); $scope = $reflection->getClosureScopeClass(); $scope = $scope ? $scope->getName() : 'static'; $data = $data->bindTo($binding, $scope); } $data = new SerializableClosure($data, $serializer); } elseif (is_array($data) || $data instanceof \stdClass || $data instanceof \Traversable) { // Handle members of traversable values. foreach ($data as &$value) { self::wrapClosures($value, $serializer); } } elseif (is_object($data) && !$data instanceof \Serializable) { // Handle objects that are not already explicitly serializable. $reflection = new \ReflectionObject($data); if (!$reflection->hasMethod('__sleep')) { foreach ($reflection->getProperties() as $property) { if ($property->isPrivate() || $property->isProtected()) { $property->setAccessible(true); } $value = $property->getValue($data); self::wrapClosures($value, $serializer); $property->setValue($data, $value); } } } } /** * Calculates a signature for a closure's serialized data. * * @param string $data Serialized closure data. * * @return string Signature of the closure's data. */ private function calculateSignature($data) { return hash_hmac('sha256', $data, $this->signingKey, true); } /** * Verifies the signature for a closure's serialized data. * * @param string $signature The provided signature of the data. * @param string $data The data for which to verify the signature. * * @throws ClosureUnserializationException if the signature is invalid. */ private function verifySignature($signature, $data) { // Verify that the provided signature matches the calculated signature. if (!hash_equals($signature, $this->calculateSignature($data))) { throw new ClosureUnserializationException('The signature of the' . ' closure\'s data is invalid, which means the serialized ' . 'closure has been modified and is unsafe to unserialize.' ); } } } super_closure-2.2.0/src/SerializerInterface.php000066400000000000000000000027211263061610500216410ustar00rootroot00000000000000