My first commit of codes

This commit is contained in:
sujitprasad
2015-05-01 13:13:01 +05:30
parent 4c8e5096f1
commit a6e5a69348
8487 changed files with 1317246 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
# 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.**

View File

@@ -0,0 +1,39 @@
{
"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.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0",
"codeclimate/php-test-reporter": "~0.1.2"
},
"autoload": {
"psr-4": {
"SuperClosure\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"SuperClosure\\Test\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./vendor/autoload.php">
<testsuites>
<testsuite name="unit">
<directory>./tests/Unit</directory>
</testsuite>
<testsuite name="integ">
<directory>./tests/Integ</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
<exclude>
<file>src/hash_equals.php</file>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,129 @@
<?php namespace SuperClosure\Analyzer;
use SuperClosure\Analyzer\Visitor\ThisDetectorVisitor;
use SuperClosure\Exception\ClosureAnalysisException;
use SuperClosure\Analyzer\Visitor\ClosureLocatorVisitor;
use SuperClosure\Analyzer\Visitor\MagicConstantVisitor;
use PhpParser\NodeTraverser;
use PhpParser\PrettyPrinter\Standard as NodePrinter;
use PhpParser\Error as ParserError;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Parser as CodeParser;
use PhpParser\Lexer\Emulative as EmulativeLexer;
/**
* This is the AST based analyzer.
*
* We're using reflection and AST-based code parser to analyze a closure and
* determine its code and context using the nikic/php-parser library. The AST
* based analyzer and has more capabilities than the token analyzer, but is,
* unfortunately, about 25 times slower.
*/
class AstAnalyzer extends ClosureAnalyzer
{
protected function determineCode(array &$data)
{
// Find the closure by traversing through a AST of the code.
// Note: This also resolves class names to their FQCNs while traversing.
$this->locateClosure($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."
);
}
$parser = new CodeParser(new EmulativeLexer);
return $parser->parse(file_get_contents($fileName));
}
}

View File

@@ -0,0 +1,62 @@
<?php namespace SuperClosure\Analyzer;
use SuperClosure\Exception\ClosureAnalysisException;
abstract class ClosureAnalyzer
{
/**
* Analyzer a given closure.
*
* @param \Closure $closure
*
* @throws ClosureAnalysisException
*
* @return array
*/
public function analyze(\Closure $closure)
{
$data = [
'reflection' => 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)
{
$rebound = new \ReflectionFunction(@$closure->bindTo(new \stdClass));
return $rebound->getClosureThis() === null;
}
}

View File

@@ -0,0 +1,70 @@
<?php namespace SuperClosure\Analyzer;
/**
* A Token object represents and individual token parsed from PHP code.
*
* Each Token object is a normalized token created from the result of the
* `get_token_all()`. function, which is part of PHP's tokenizer.
*
* @link http://us2.php.net/manual/en/tokens.php
*/
class Token
{
/**
* @var string The token name. Always null for literal tokens.
*/
public $name;
/**
* @var int|null The token's integer value. Always null for literal tokens.
*/
public $value;
/**
* @var string The PHP code of the token.
*/
public $code;
/**
* @var int|null The line number of the token in the original code.
*/
public $line;
/**
* Constructs a token object.
*
* @param string $code
* @param int|null $value
* @param int|null $line
*
* @throws \InvalidArgumentException
*/
public function __construct($code, $value = null, $line = null)
{
if (is_array($code)) {
list($value, $code, $line) = array_pad($code, 3, null);
}
$this->code = $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;
}
}

View File

@@ -0,0 +1,118 @@
<?php namespace SuperClosure\Analyzer;
use SuperClosure\Exception\ClosureAnalysisException;
/**
* This is the token based analyzer.
*
* We're using Uses reflection and tokenization to analyze a closure and
* determine its code and context. This is much faster than the AST based
* implementation.
*/
class TokenAnalyzer extends ClosureAnalyzer
{
public function determineCode(array &$data)
{
$this->determineTokens($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 '
. '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, '<?php') !== 0) {
$code = "<?php\n" . $code;
}
return token_get_all($code);
}
protected function determineContext(array &$data)
{
// Get the values of the variables that are closed upon in "use".
$values = $data['reflection']->getStaticVariables();
// Construct the context by combining the variable names and values.
foreach ($data['context'] as $name => &$value) {
if (isset($values[$name])) {
$value = $values[$name];
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?php namespace SuperClosure\Analyzer\Visitor;
use SuperClosure\Exception\ClosureAnalysisException;
use PhpParser\Node\Stmt\Namespace_ as NamespaceNode;
use PhpParser\Node\Stmt\Trait_ as TraitNode;
use PhpParser\Node\Stmt\Class_ as ClassNode;
use PhpParser\Node\Expr\Closure as ClosureNode;
use PhpParser\Node as AstNode;
use PhpParser\NodeVisitorAbstract as NodeVisitor;
/**
* This is a visitor that extends the nikic/php-parser library and looks for a
* closure node and its location.
*
* @internal
*/
final class ClosureLocatorVisitor extends NodeVisitor
{
/**
* @var \ReflectionFunction
*/
private $reflection;
/**
* @var ClosureNode
*/
public $closureNode;
/**
* @var array
*/
public $location;
/**
* @param \ReflectionFunction $reflection
*/
public function __construct($reflection)
{
$this->reflection = $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 (!$this->location['class']) {
/** @var \ReflectionClass $closureScopeClass */
$closureScopeClass = $this->reflection->getClosureScopeClass();
$this->location['class'] = $closureScopeClass ? $closureScopeClass->getName() : null;
}
}
}

View File

@@ -0,0 +1,50 @@
<?php namespace SuperClosure\Analyzer\Visitor;
use PhpParser\Node\Scalar\LNumber as NumberNode;
use PhpParser\Node\Scalar\String as StringNode;
use PhpParser\Node as AstNode;
use PhpParser\NodeVisitorAbstract as NodeVisitor;
/**
* This is a visitor that resolves magic constants (e.g., __FILE__) to their
* intended values within a closure's AST.
*
* @internal
*/
final class MagicConstantVisitor extends NodeVisitor
{
/**
* @var array
*/
private $location;
/**
* @param array $location
*/
public function __construct(array $location)
{
$this->location = $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']);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php namespace SuperClosure\Analyzer\Visitor;
use PhpParser\Node as AstNode;
use PhpParser\Node\Expr\Variable as VariableNode;
use PhpParser\NodeVisitorAbstract as NodeVisitor;
/**
* Detects if the closure's AST contains a $this variable.
*
* @internal
*/
final class ThisDetectorVisitor extends NodeVisitor
{
/**
* @var bool
*/
public $detected = false;
public function leaveNode(AstNode $node)
{
if ($node instanceof VariableNode) {
if ($node->name === 'this') {
$this->detected = true;
}
}
}
}

View File

@@ -0,0 +1,9 @@
<?php namespace SuperClosure\Exception;
/**
* This exception is thrown when there is a problem analyzing a closure.
*/
class ClosureAnalysisException extends \RuntimeException implements SuperClosureException
{
//
}

View File

@@ -0,0 +1,9 @@
<?php namespace SuperClosure\Exception;
/**
* This exception is thrown when there is a problem unserializing a closure.
*/
class ClosureUnserializationException extends \RuntimeException implements SuperClosureException
{
//
}

View File

@@ -0,0 +1,9 @@
<?php namespace SuperClosure\Exception;
/**
* This is a marker exception for the SuperClosure library.
*/
interface SuperClosureException
{
//
}

View File

@@ -0,0 +1,166 @@
<?php namespace SuperClosure;
use SuperClosure\Exception\ClosureUnserializationException;
/**
* This class acts as a wrapper for a closure, and allows it to be serialized.
*
* With the combined power of the Reflection API, code parsing, and the infamous
* `eval()` function, you can serialize a closure, unserialize it somewhere
* else (even a different PHP process), and execute it.
*/
class SerializableClosure implements \Serializable
{
/**
* The closure being wrapped for serialization.
*
* @var \Closure
*/
private $closure;
/**
* The serializer doing the serialization work.
*
* @var SerializerInterface
*/
private $serializer;
/**
* The data from unserialization.
*
* @var array
*/
private $data;
/**
* Create a new serializable closure instance.
*
* @param \Closure $closure
* @param SerializerInterface|null $serializer
*/
public function __construct(
\Closure $closure,
SerializerInterface $serializer = null
) {
$this->closure = $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 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());
}
/**
* Serializes the code, context, and binding of the closure.
*
* @see http://php.net/manual/en/serializable.serialize.php
*
* @return string|null
*/
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.
*
* @see http://php.net/manual/en/serializable.unserialize.php
*
* @param string $serialized
*
* @throws ClosureUnserializationException
*/
public function unserialize($serialized)
{
// Unserialize the data and reconstruct the SuperClosure.
$this->data = unserialize($serialized);
$this->reconstructClosure();
if (!$this->closure instanceof \Closure) {
throw new ClosureUnserializationException(
'The closure is corrupted and cannot be unserialized.'
);
}
// Rebind the closure to its former binding, if it's not static.
if (!$this->data['isStatic']) {
$this->closure = $this->closure->bindTo(
$this->data['binding'],
$this->data['scope']
);
}
}
/**
* Reconstruct the closure.
*
* HERE BE DRAGONS!
*
* The infamous `eval()` is used in this method, along with `extract()`,
* the error suppression operator, and variable variables (i.e., double
* dollar signs) to perform the unserialization work. I'm sorry, world!
*/
private function reconstructClosure()
{
// Simulate the original context the closure was created in.
extract($this->data['context'], EXTR_OVERWRITE);
// Evaluate the code to recreate the closure.
if ($_fn = array_search(Serializer::RECURSION, $this->data['context'], true)) {
@eval("\${$_fn} = {$this->data['code']};");
$this->closure = $$_fn;
} else {
@eval("\$this->closure = {$this->data['code']};");
}
}
/**
* Returns closure data for `var_dump()`.
*
* @return array
*/
public function __debugInfo()
{
return $this->data ?: $this->serializer->getData($this->closure, true);
}
}

View File

@@ -0,0 +1,211 @@
<?php namespace SuperClosure;
use SuperClosure\Analyzer\AstAnalyzer as DefaultAnalyzer;
use SuperClosure\Analyzer\ClosureAnalyzer;
use SuperClosure\Exception\ClosureUnserializationException;
/**
* This is the serializer class used for serializing Closure objects.
*
* We're abstracting away all the details, impossibilities, and scary things
* that happen within.
*/
class Serializer implements SerializerInterface
{
/**
* The special value marking a recursive reference to a closure.
*
* @var string
*/
const RECURSION = "{{RECURSION}}";
/**
* The keys of closure data required for serialization.
*
* @var array
*/
private static $dataToKeep = [
'code' => 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);
}
/** @var SerializableClosure $unserialized */
$unserialized = unserialize($serialized);
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)
{
// Ensure that hash_equals() is available.
static $hashEqualsFnExists = false;
if (!$hashEqualsFnExists) {
require __DIR__ . '/hash_equals.php';
$hashEqualsFnExists = true;
}
// 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.'
);
}
}
}

View File

@@ -0,0 +1,45 @@
<?php namespace SuperClosure;
use SuperClosure\Exception\ClosureUnserializationException;
/**
* Interface for a serializer that is used to serialize Closure objects.
*/
interface SerializerInterface
{
/**
* Takes a Closure object, decorates it with a SerializableClosure object,
* then performs the serialization.
*
* @param \Closure $closure Closure to serialize.
*
* @return string Serialized closure.
*/
public function serialize(\Closure $closure);
/**
* Takes a serialized closure, performs the unserialization, and then
* extracts and returns a the Closure object.
*
* @param string $serialized Serialized closure.
*
* @throws ClosureUnserializationException if unserialization fails.
* @return \Closure Unserialized closure.
*/
public function unserialize($serialized);
/**
* Retrieves data about a closure including its code, context, and binding.
*
* The data returned is dependant on the `ClosureAnalyzer` implementation
* used and whether the `$forSerialization` parameter is set to true. If
* `$forSerialization` is true, then only data relevant to serializing the
* closure is returned.
*
* @param \Closure $closure Closure to analyze.
* @param bool $forSerialization Include only serialization data.
*
* @return \Closure
*/
public function getData(\Closure $closure, $forSerialization = false);
}

View File

@@ -0,0 +1,59 @@
<?php
if (!function_exists('hash_equals')) {
/**
* An implementation for the `hash_equals()` function.
*
* This exists to support PHP versions prior to 5.6 and is meant to work the
* same as PHP's function. The original code was written by Rouven Weßling.
*
* @param string $knownString Calculated hash.
* @param string $userString User-provided hash.
*
* @return bool
* @copyright Copyright (c) 2013-2014 Rouven Weßling <http://rouvenwessling.de>
* @license http://opensource.org/licenses/MIT MIT
*/
function hash_equals($knownString, $userString)
{
$argc = func_num_args();
if ($argc < 2) {
trigger_error(
"hash_equals() expects at least 2 parameters, {$argc} given",
E_USER_WARNING
);
return null;
}
if (!is_string($knownString)) {
trigger_error(sprintf(
"hash_equals(): Expected known_string to be a string, %s given",
gettype($knownString)
), E_USER_WARNING);
return false;
}
if (!is_string($userString)) {
trigger_error(sprintf(
"hash_equals(): Expected user_string to be a string, %s given",
gettype($knownString)
), E_USER_WARNING);
return false;
}
if (strlen($knownString) !== strlen($userString)) {
return false;
}
$len = strlen($knownString);
$result = 0;
for ($i = 0; $i < $len; $i++) {
$result |= (ord($knownString[$i]) ^ ord($userString[$i]));
}
return 0 === $result;
}
}