<?php
/* ===========================================================================
* Copyright (c) 2018-2021 Zindex Software
*
* Licensed under the MIT License
* =========================================================================== */
namespace Opis\Closure;
use Closure;
use Serializable;
use SplObjectStorage;
use ReflectionObject;
/**
* Provides a wrapper for serialization of closures
*/
class SerializableClosure implements Serializable
{
/**
* @var Closure Wrapped closure
*
* @see \Opis\Closure\SerializableClosure::getClosure()
*/
protected $closure;
/**
* @var ReflectionClosure A reflection instance for closure
*
* @see \Opis\Closure\SerializableClosure::getReflector()
*/
protected $reflector;
/**
* @var mixed Used at deserialization to hold variables
*
* @see \Opis\Closure\SerializableClosure::unserialize()
* @see \Opis\Closure\SerializableClosure::getReflector()
*/
protected $code;
/**
* @var string Closure's ID
*/
protected $reference;
/**
* @var string Closure scope
*/
protected $scope;
/**
* @var ClosureContext Context of closure, used in serialization
*/
protected static $context;
/**
* @var ISecurityProvider|null
*/
protected static $securityProvider;
/** Array recursive constant*/
const ARRAY_RECURSIVE_KEY = '¯\_(ツ)_/¯';
/**
* Constructor
*
* @param Closure $closure Closure you want to serialize
*/
public function __construct(Closure $closure)
{
$this->closure = $closure;
if (static::$context !== null) {
$this->scope = static::$context->scope;
$this->scope->toserialize++;
}
}
/**
* Get the Closure object
*
* @return Closure The wrapped closure
*/
public function getClosure()
{
return $this->closure;
}
/**
* Get the reflector for closure
*
* @return ReflectionClosure
*/
public function getReflector()
{
if ($this->reflector === null) {
$this->reflector = new ReflectionClosure($this->closure);
$this->code = null;
}
return $this->reflector;
}
/**
* Implementation of magic method __invoke()
*/
public function __invoke()
{
return call_user_func_array($this->closure, func_get_args());
}
/**
* Implementation of Serializable::serialize()
*
* @return string The serialized closure
*/
public function serialize()
{
if ($this->scope === null) {
$this->scope = new ClosureScope();
$this->scope->toserialize++;
}
$this->scope->serializations++;
$scope = $object = null;
$reflector = $this->getReflector();
if($reflector->isBindingRequired()){
$object = $reflector->getClosureThis();
static::wrapClosures($object, $this->scope);
if($scope = $reflector->getClosureScopeClass()){
$scope = $scope->name;
}
} else {
if($scope = $reflector->getClosureScopeClass()){
$scope = $scope->name;
}
}
$this->reference = spl_object_hash($this->closure);
$this->scope[$this->closure] = $this;
$use = $this->transformUseVariables($reflector->getUseVariables());
$code = $reflector->getCode();
$this->mapByReference($use);
$ret = \serialize(array(
'use' => $use,
'function' => $code,
'scope' => $scope,
'this' => $object,
'self' => $this->reference,
));
if (static::$securityProvider !== null) {
$data = static::$securityProvider->sign($ret);
$ret = '@' . $data['hash'] . '.' . $data['closure'];
}
if (!--$this->scope->serializations && !--$this->scope->toserialize) {
$this->scope = null;
}
return $ret;
}
/**
* Transform the use variables before serialization.
*
* @param array $data The Closure's use variables
* @return array
*/
protected function transformUseVariables($data)
{
return $data;
}
/**
* Implementation of Serializable::unserialize()
*
* @param string $data Serialized data
* @throws SecurityException
*/
public function unserialize($data)
{
ClosureStream::register();
if (static::$securityProvider !== null) {
if ($data[0] !== '@') {
throw new SecurityException("The serialized closure is not signed. ".
"Make sure you use a security provider for both serialization and unserialization.");
}
if ($data[1] !== '{') {
$separator = strpos($data, '.');
if ($separator === false) {
throw new SecurityException('Invalid signed closure');
}
$hash = substr($data, 1, $separator - 1);
$closure = substr($data, $separator + 1);
$data = ['hash' => $hash, 'closure' => $closure];
unset($hash, $closure);
} else {
$data = json_decode(substr($data, 1), true);
}
if (!is_array($data) || !static::$securityProvider->verify($data)) {
throw new SecurityException("Your serialized closure might have been modified and it's unsafe to be unserialized. " .
"Make sure you use the same security provider, with the same settings, " .
"both for serialization and unserialization.");
}
$data = $data['closure'];
} elseif ($data[0] === '@') {
if ($data[1] !== '{') {
$separator = strpos($data, '.');
if ($separator === false) {
throw new SecurityException('Invalid signed closure');
}
$hash = substr($data, 1, $separator - 1);
$closure = substr($data, $separator + 1);
$data = ['hash' => $hash, 'closure' => $closure];
unset($hash, $closure);
} else {
$data = json_decode(substr($data, 1), true);
}
if (!is_array($data) || !isset($data['closure']) || !isset($data['hash'])) {
throw new SecurityException('Invalid signed closure');
}
$data = $data['closure'];
}
$this->code = \unserialize($data);
// unset data
unset($data);
$this->code['objects'] = array();
if ($this->code['use']) {
$this->scope = new ClosureScope();
$this->code['use'] = $this->resolveUseVariables($this->code['use']);
$this->mapPointers($this->code['use']);
extract($this->code['use'], EXTR_OVERWRITE | EXTR_REFS);
$this->scope = null;
}
$this->closure = include(ClosureStream::STREAM_PROTO . '://' . $this->code['function']);
if($this->code['this'] === $this){
$this->code['this'] = null;
}
$this->closure = $this->closure->bindTo($this->code['this'], $this->code['scope']);
if(!empty($this->code['objects'])){
foreach ($this->code['objects'] as $item){
$item['property']->setValue($item['instance'], $item['object']->getClosure());
}
}
$this->code = $this->code['function'];
}
/**
* Resolve the use variables after unserialization.
*
* @param array $data The Closure's transformed use variables
* @return array
*/
protected function resolveUseVariables($data)
{
return $data;
}
/**
* Wraps a closure and sets the serialization context (if any)
*
* @param Closure $closure Closure to be wrapped
*
* @return self The wrapped closure
*/
public static function from(Closure $closure)
{
if (static::$context === null) {
$instance = new static($closure);
} elseif (isset(static::$context->scope[$closure])) {
$instance = static::$context->scope[$closure];
} else {
$instance = new static($closure);
static::$context->scope[$closure] = $instance;
}
return $instance;
}
/**
* Increments the context lock counter or creates a new context if none exist
*/
public static function enterContext()
{
if (static::$context === null) {
static::$context = new ClosureContext();
}
static::$context->locks++;
}
/**
* Decrements the context lock counter and destroy the context when it reaches to 0
*/
public static function exitContext()
{
if (static::$context !== null && !--static::$context->locks) {
static::$context = null;
}
}
/**
* @param string $secret
*/
public static function setSecretKey($secret)
{
if(static::$securityProvider === null){
static::$securityProvider = new SecurityProvider($secret);
}
}
/**
* @param ISecurityProvider $securityProvider
*/
public static function addSecurityProvider(ISecurityProvider $securityProvider)
{
static::$securityProvider = $securityProvider;
}
/**
* Remove security provider
*/
public static function removeSecurityProvider()
{
static::$securityProvider = null;
}
/**
* @return null|ISecurityProvider
*/
public static function getSecurityProvider()
{
return static::$securityProvider;
}
/**
* Wrap closures
*
* @internal
* @param $data
* @param ClosureScope|SplObjectStorage|null $storage
*/
public static function wrapClosures(&$data, SplObjectStorage $storage = null)
{
if($storage === null){
$storage = static::$context->scope;
}
if($data instanceof Closure){
$data = static::from($data);
} elseif (is_array($data)){
if(isset($data[self::ARRAY_RECURSIVE_KEY])){
return;
}
$data[self::ARRAY_RECURSIVE_KEY] = true;
foreach ($data as $key => &$value){
if($key === self::ARRAY_RECURSIVE_KEY){
continue;
}
static::wrapClosures($value, $storage);
}
unset($value);
unset($data[self::ARRAY_RECURSIVE_KEY]);
} elseif($data instanceof \stdClass){
if(isset($storage[$data])){
$data = $storage[$data];
return;
}
$data = $storage[$data] = clone($data);
foreach ($data as &$value){
static::wrapClosures($value, $storage);
}
unset($value);
} elseif (is_object($data) && ! $data instanceof static){
if(isset($storage[$data])){
$data = $storage[$data];
return;
}
$instance = $data;
$reflection = new ReflectionObject($instance);
if(!$reflection->isUserDefined()){
$storage[$instance] = $data;
return;
}
$storage[$instance] = $data = $reflection->newInstanceWithoutConstructor();
do{
if(!$reflection->isUserDefined()){
break;
}
foreach ($reflection->getProperties() as $property){
if($property->isStatic() || !$property->getDeclaringClass()->isUserDefined()){
continue;
}
$property->setAccessible(true);
if (PHP_VERSION >= 7.4 && !$property->isInitialized($instance)) {
continue;
}
$value = $property->getValue($instance);
if(is_array($value) || is_object($value)){
static::wrapClosures($value, $storage);
}
$property->setValue($data, $value);
};
} while($reflection = $reflection->getParentClass());
}
}
/**
* Unwrap closures
*
* @internal
* @param $data
* @param SplObjectStorage|null $storage
*/
public static function unwrapClosures(&$data, SplObjectStorage $storage = null)
{
if($storage === null){
$storage = static::$context->scope;
}
if($data instanceof static){
$data = $data->getClosure();
} elseif (is_array($data)){
if(isset($data[self::ARRAY_RECURSIVE_KEY])){
return;
}
$data[self::ARRAY_RECURSIVE_KEY] = true;
foreach ($data as $key => &$value){
if($key === self::ARRAY_RECURSIVE_KEY){
continue;
}
static::unwrapClosures($value, $storage);
}
unset($data[self::ARRAY_RECURSIVE_KEY]);
}elseif ($data instanceof \stdClass){
if(isset($storage[$data])){
return;
}
$storage[$data] = true;
foreach ($data as &$property){
static::unwrapClosures($property, $storage);
}
} elseif (is_object($data) && !($data instanceof Closure)){
if(isset($storage[$data])){
return;
}
$storage[$data] = true;
$reflection = new ReflectionObject($data);
do{
if(!$reflection->isUserDefined()){
break;
}
foreach ($reflection->getProperties() as $property){
if($property->isStatic() || !$property->getDeclaringClass()->isUserDefined()){
continue;
}
$property->setAccessible(true);
if (PHP_VERSION >= 7.4 && !$property->isInitialized($data)) {
continue;
}
$value = $property->getValue($data);
if(is_array($value) || is_object($value)){
static::unwrapClosures($value, $storage);
$property->setValue($data, $value);
}
};
} while($reflection = $reflection->getParentClass());
}
}
/**
* Creates a new closure from arbitrary code,
* emulating create_function, but without using eval
*
* @param string$args
* @param string $code
* @return Closure
*/
public static function createClosure($args, $code)
{
ClosureStream::register();
return include(ClosureStream::STREAM_PROTO . '://function(' . $args. '){' . $code . '};');
}
/**
* Internal method used to map closure pointers
* @internal
* @param $data
*/
protected function mapPointers(&$data)
{
$scope = $this->scope;
if ($data instanceof static) {
$data = &$data->closure;
} elseif (is_array($data)) {
if(isset($data[self::ARRAY_RECURSIVE_KEY])){
return;
}
$data[self::ARRAY_RECURSIVE_KEY] = true;
foreach ($data as $key => &$value){
if($key === self::ARRAY_RECURSIVE_KEY){
continue;
} elseif ($value instanceof static) {
$data[$key] = &$value->closure;
} elseif ($value instanceof SelfReference && $value->hash === $this->code['self']){
$data[$key] = &$this->closure;
} else {
$this->mapPointers($value);
}
}
unset($value);
unset($data[self::ARRAY_RECURSIVE_KEY]);
} elseif ($data instanceof \stdClass) {
if(isset($scope[$data])){
return;
}
$scope[$data] = true;
foreach ($data as $key => &$value){
if ($value instanceof SelfReference && $value->hash === $this->code['self']){
$data->{$key} = &$this->closure;
} elseif(is_array($value) || is_object($value)) {
$this->mapPointers($value);
}
}
unset($value);
} elseif (is_object($data) && !($data instanceof Closure)){
if(isset($scope[$data])){
return;
}
$scope[$data] = true;
$reflection = new ReflectionObject($data);
do{
if(!$reflection->isUserDefined()){
break;
}
foreach ($reflection->getProperties() as $property){
if($property->isStatic() || !$property->getDeclaringClass()->isUserDefined()){
continue;
}
$property->setAccessible(true);
if (PHP_VERSION >= 7.4 && !$property->isInitialized($data)) {
continue;
}
$item = $property->getValue($data);
if ($item instanceof SerializableClosure || ($item instanceof SelfReference && $item->hash === $this->code['self'])) {
$this->code['objects'][] = array(
'instance' => $data,
'property' => $property,
'object' => $item instanceof SelfReference ? $this : $item,
);
} elseif (is_array($item) || is_object($item)) {
$this->mapPointers($item);
$property->setValue($data, $item);
}
}
} while($reflection = $reflection->getParentClass());
}
}
/**
* Internal method used to map closures by reference
*
* @internal
* @param mixed &$data
*/
protected function mapByReference(&$data)
{
if ($data instanceof Closure) {
if($data === $this->closure){
$data = new SelfReference($this->reference);
return;
}
if (isset($this->scope[$data])) {
$data = $this->scope[$data];
return;
}
$instance = new static($data);
if (static::$context !== null) {
static::$context->scope->toserialize--;
} else {
$instance->scope = $this->scope;
}
$data = $this->scope[$data] = $instance;
} elseif (is_array($data)) {
if(isset($data[self::ARRAY_RECURSIVE_KEY])){
return;
}
$data[self::ARRAY_RECURSIVE_KEY] = true;
foreach ($data as $key => &$value){
if($key === self::ARRAY_RECURSIVE_KEY){
continue;
}
$this->mapByReference($value);
}
unset($value);
unset($data[self::ARRAY_RECURSIVE_KEY]);
} elseif ($data instanceof \stdClass) {
if(isset($this->scope[$data])){
$data = $this->scope[$data];
return;
}
$instance = $data;
$this->scope[$instance] = $data = clone($data);
foreach ($data as &$value){
$this->mapByReference($value);
}
unset($value);
} elseif (is_object($data) && !$data instanceof SerializableClosure){
if(isset($this->scope[$data])){
$data = $this->scope[$data];
return;
}
$instance = $data;
$reflection = new ReflectionObject($data);
if(!$reflection->isUserDefined()){
$this->scope[$instance] = $data;
return;
}
$this->scope[$instance] = $data = $reflection->newInstanceWithoutConstructor();
do{
if(!$reflection->isUserDefined()){
break;
}
foreach ($reflection->getProperties() as $property){
if($property->isStatic() || !$property->getDeclaringClass()->isUserDefined()){
continue;
}
$property->setAccessible(true);
if (PHP_VERSION >= 7.4 && !$property->isInitialized($instance)) {
continue;
}
$value = $property->getValue($instance);
if(is_array($value) || is_object($value)){
$this->mapByReference($value);
}
$property->setValue($data, $value);
}
} while($reflection = $reflection->getParentClass());
}
}
}