1<?php
2/* ===========================================================================
3 * Copyright (c) 2018-2021 Zindex Software
4 *
5 * Licensed under the MIT License
6 * =========================================================================== */
7
8namespace Opis\Closure;
9
10use Closure;
11use Serializable;
12use SplObjectStorage;
13use ReflectionObject;
14
15/**
16 * Provides a wrapper for serialization of closures
17 */
18class SerializableClosure implements Serializable
19{
20    /**
21     * @var Closure Wrapped closure
22     *
23     * @see \Opis\Closure\SerializableClosure::getClosure()
24     */
25    protected $closure;
26
27    /**
28     * @var ReflectionClosure A reflection instance for closure
29     *
30     * @see \Opis\Closure\SerializableClosure::getReflector()
31     */
32    protected $reflector;
33
34    /**
35     * @var mixed Used at deserialization to hold variables
36     *
37     * @see \Opis\Closure\SerializableClosure::unserialize()
38     * @see \Opis\Closure\SerializableClosure::getReflector()
39     */
40    protected $code;
41
42    /**
43     * @var string Closure's ID
44     */
45    protected $reference;
46
47    /**
48     * @var string Closure scope
49     */
50    protected $scope;
51
52    /**
53     * @var ClosureContext Context of closure, used in serialization
54     */
55    protected static $context;
56
57    /**
58     * @var ISecurityProvider|null
59     */
60    protected static $securityProvider;
61
62    /** Array recursive constant*/
63    const ARRAY_RECURSIVE_KEY = '¯\_(ツ)_/¯';
64
65    /**
66     * Constructor
67     *
68     * @param   Closure $closure Closure you want to serialize
69     */
70    public function __construct(Closure $closure)
71    {
72        $this->closure = $closure;
73        if (static::$context !== null) {
74            $this->scope = static::$context->scope;
75            $this->scope->toserialize++;
76        }
77    }
78
79    /**
80     * Get the Closure object
81     *
82     * @return  Closure The wrapped closure
83     */
84    public function getClosure()
85    {
86        return $this->closure;
87    }
88
89    /**
90     * Get the reflector for closure
91     *
92     * @return  ReflectionClosure
93     */
94    public function getReflector()
95    {
96        if ($this->reflector === null) {
97            $this->reflector = new ReflectionClosure($this->closure);
98            $this->code = null;
99        }
100
101        return $this->reflector;
102    }
103
104    /**
105     * Implementation of magic method __invoke()
106     */
107    public function __invoke()
108    {
109        return call_user_func_array($this->closure, func_get_args());
110    }
111
112    /**
113     * Implementation of Serializable::serialize()
114     *
115     * @return  string  The serialized closure
116     */
117    public function serialize()
118    {
119        if ($this->scope === null) {
120            $this->scope = new ClosureScope();
121            $this->scope->toserialize++;
122        }
123
124        $this->scope->serializations++;
125
126        $scope = $object = null;
127        $reflector = $this->getReflector();
128
129        if($reflector->isBindingRequired()){
130            $object = $reflector->getClosureThis();
131            static::wrapClosures($object, $this->scope);
132            if($scope = $reflector->getClosureScopeClass()){
133                $scope = $scope->name;
134            }
135        } else {
136            if($scope = $reflector->getClosureScopeClass()){
137                $scope = $scope->name;
138            }
139        }
140
141        $this->reference = spl_object_hash($this->closure);
142
143        $this->scope[$this->closure] = $this;
144
145        $use = $this->transformUseVariables($reflector->getUseVariables());
146        $code = $reflector->getCode();
147
148        $this->mapByReference($use);
149
150        $ret = \serialize(array(
151            'use' => $use,
152            'function' => $code,
153            'scope' => $scope,
154            'this' => $object,
155            'self' => $this->reference,
156        ));
157
158        if (static::$securityProvider !== null) {
159            $data = static::$securityProvider->sign($ret);
160            $ret =  '@' . $data['hash'] . '.' . $data['closure'];
161        }
162
163        if (!--$this->scope->serializations && !--$this->scope->toserialize) {
164            $this->scope = null;
165        }
166
167        return $ret;
168    }
169
170    /**
171     * Transform the use variables before serialization.
172     *
173     * @param  array  $data The Closure's use variables
174     * @return array
175     */
176    protected function transformUseVariables($data)
177    {
178        return $data;
179    }
180
181    /**
182     * Implementation of Serializable::unserialize()
183     *
184     * @param   string $data Serialized data
185     * @throws SecurityException
186     */
187    public function unserialize($data)
188    {
189        ClosureStream::register();
190
191        if (static::$securityProvider !== null) {
192            if ($data[0] !== '@') {
193                throw new SecurityException("The serialized closure is not signed. ".
194                    "Make sure you use a security provider for both serialization and unserialization.");
195            }
196
197            if ($data[1] !== '{') {
198                $separator = strpos($data, '.');
199                if ($separator === false) {
200                    throw new SecurityException('Invalid signed closure');
201                }
202                $hash = substr($data, 1, $separator - 1);
203                $closure = substr($data, $separator + 1);
204
205                $data = ['hash' => $hash, 'closure' => $closure];
206
207                unset($hash, $closure);
208            } else {
209                $data = json_decode(substr($data, 1), true);
210            }
211
212            if (!is_array($data) || !static::$securityProvider->verify($data)) {
213                throw new SecurityException("Your serialized closure might have been modified and it's unsafe to be unserialized. " .
214                    "Make sure you use the same security provider, with the same settings, " .
215                    "both for serialization and unserialization.");
216            }
217
218            $data = $data['closure'];
219        } elseif ($data[0] === '@') {
220            if ($data[1] !== '{') {
221                $separator = strpos($data, '.');
222                if ($separator === false) {
223                    throw new SecurityException('Invalid signed closure');
224                }
225                $hash = substr($data, 1, $separator - 1);
226                $closure = substr($data, $separator + 1);
227
228                $data = ['hash' => $hash, 'closure' => $closure];
229
230                unset($hash, $closure);
231            } else {
232                $data = json_decode(substr($data, 1), true);
233            }
234
235            if (!is_array($data) || !isset($data['closure']) || !isset($data['hash'])) {
236                throw new SecurityException('Invalid signed closure');
237            }
238
239            $data = $data['closure'];
240        }
241
242        $this->code = \unserialize($data);
243
244        // unset data
245        unset($data);
246
247        $this->code['objects'] = array();
248
249        if ($this->code['use']) {
250            $this->scope = new ClosureScope();
251            $this->code['use'] = $this->resolveUseVariables($this->code['use']);
252            $this->mapPointers($this->code['use']);
253            extract($this->code['use'], EXTR_OVERWRITE | EXTR_REFS);
254            $this->scope = null;
255        }
256
257        $this->closure = include(ClosureStream::STREAM_PROTO . '://' . $this->code['function']);
258
259        if($this->code['this'] === $this){
260            $this->code['this'] = null;
261        }
262
263        $this->closure = $this->closure->bindTo($this->code['this'], $this->code['scope']);
264
265        if(!empty($this->code['objects'])){
266            foreach ($this->code['objects'] as $item){
267                $item['property']->setValue($item['instance'], $item['object']->getClosure());
268            }
269        }
270
271        $this->code = $this->code['function'];
272    }
273
274    /**
275     * Resolve the use variables after unserialization.
276     *
277     * @param  array  $data The Closure's transformed use variables
278     * @return array
279     */
280    protected function resolveUseVariables($data)
281    {
282        return $data;
283    }
284
285    /**
286     * Wraps a closure and sets the serialization context (if any)
287     *
288     * @param   Closure $closure Closure to be wrapped
289     *
290     * @return  self    The wrapped closure
291     */
292    public static function from(Closure $closure)
293    {
294        if (static::$context === null) {
295            $instance = new static($closure);
296        } elseif (isset(static::$context->scope[$closure])) {
297            $instance = static::$context->scope[$closure];
298        } else {
299            $instance = new static($closure);
300            static::$context->scope[$closure] = $instance;
301        }
302
303        return $instance;
304    }
305
306    /**
307     * Increments the context lock counter or creates a new context if none exist
308     */
309    public static function enterContext()
310    {
311        if (static::$context === null) {
312            static::$context = new ClosureContext();
313        }
314
315        static::$context->locks++;
316    }
317
318    /**
319     * Decrements the context lock counter and destroy the context when it reaches to 0
320     */
321    public static function exitContext()
322    {
323        if (static::$context !== null && !--static::$context->locks) {
324            static::$context = null;
325        }
326    }
327
328    /**
329     * @param string $secret
330     */
331    public static function setSecretKey($secret)
332    {
333        if(static::$securityProvider === null){
334            static::$securityProvider = new SecurityProvider($secret);
335        }
336    }
337
338    /**
339     * @param ISecurityProvider $securityProvider
340     */
341    public static function addSecurityProvider(ISecurityProvider $securityProvider)
342    {
343        static::$securityProvider = $securityProvider;
344    }
345
346    /**
347     * Remove security provider
348     */
349    public static function removeSecurityProvider()
350    {
351        static::$securityProvider = null;
352    }
353
354    /**
355     * @return null|ISecurityProvider
356     */
357    public static function getSecurityProvider()
358    {
359        return static::$securityProvider;
360    }
361
362    /**
363     * Wrap closures
364     *
365     * @internal
366     * @param $data
367     * @param ClosureScope|SplObjectStorage|null $storage
368     */
369    public static function wrapClosures(&$data, SplObjectStorage $storage = null)
370    {
371        if($storage === null){
372            $storage = static::$context->scope;
373        }
374
375        if($data instanceof Closure){
376            $data = static::from($data);
377        } elseif (is_array($data)){
378            if(isset($data[self::ARRAY_RECURSIVE_KEY])){
379                return;
380            }
381            $data[self::ARRAY_RECURSIVE_KEY] = true;
382            foreach ($data as $key => &$value){
383                if($key === self::ARRAY_RECURSIVE_KEY){
384                    continue;
385                }
386                static::wrapClosures($value, $storage);
387            }
388            unset($value);
389            unset($data[self::ARRAY_RECURSIVE_KEY]);
390        } elseif($data instanceof \stdClass){
391            if(isset($storage[$data])){
392                $data = $storage[$data];
393                return;
394            }
395            $data = $storage[$data] = clone($data);
396            foreach ($data as &$value){
397                static::wrapClosures($value, $storage);
398            }
399            unset($value);
400        } elseif (is_object($data) && ! $data instanceof static){
401            if(isset($storage[$data])){
402                $data = $storage[$data];
403                return;
404            }
405            $instance = $data;
406            $reflection = new ReflectionObject($instance);
407            if(!$reflection->isUserDefined()){
408                $storage[$instance] = $data;
409                return;
410            }
411            $storage[$instance] = $data = $reflection->newInstanceWithoutConstructor();
412
413            do{
414                if(!$reflection->isUserDefined()){
415                    break;
416                }
417                foreach ($reflection->getProperties() as $property){
418                    if($property->isStatic() || !$property->getDeclaringClass()->isUserDefined()){
419                        continue;
420                    }
421                    $property->setAccessible(true);
422                    if (PHP_VERSION >= 7.4 && !$property->isInitialized($instance)) {
423                        continue;
424                    }
425                    $value = $property->getValue($instance);
426                    if(is_array($value) || is_object($value)){
427                        static::wrapClosures($value, $storage);
428                    }
429                    $property->setValue($data, $value);
430                };
431            } while($reflection = $reflection->getParentClass());
432        }
433    }
434
435    /**
436     * Unwrap closures
437     *
438     * @internal
439     * @param $data
440     * @param SplObjectStorage|null $storage
441     */
442    public static function unwrapClosures(&$data, SplObjectStorage $storage = null)
443    {
444        if($storage === null){
445            $storage = static::$context->scope;
446        }
447
448        if($data instanceof static){
449            $data = $data->getClosure();
450        } elseif (is_array($data)){
451            if(isset($data[self::ARRAY_RECURSIVE_KEY])){
452                return;
453            }
454            $data[self::ARRAY_RECURSIVE_KEY] = true;
455            foreach ($data as $key => &$value){
456                if($key === self::ARRAY_RECURSIVE_KEY){
457                    continue;
458                }
459                static::unwrapClosures($value, $storage);
460            }
461            unset($data[self::ARRAY_RECURSIVE_KEY]);
462        }elseif ($data instanceof \stdClass){
463            if(isset($storage[$data])){
464                return;
465            }
466            $storage[$data] = true;
467            foreach ($data as &$property){
468                static::unwrapClosures($property, $storage);
469            }
470        } elseif (is_object($data) && !($data instanceof Closure)){
471            if(isset($storage[$data])){
472                return;
473            }
474            $storage[$data] = true;
475            $reflection = new ReflectionObject($data);
476
477            do{
478                if(!$reflection->isUserDefined()){
479                    break;
480                }
481                foreach ($reflection->getProperties() as $property){
482                    if($property->isStatic() || !$property->getDeclaringClass()->isUserDefined()){
483                        continue;
484                    }
485                    $property->setAccessible(true);
486                    if (PHP_VERSION >= 7.4 && !$property->isInitialized($data)) {
487                        continue;
488                    }
489                    $value = $property->getValue($data);
490                    if(is_array($value) || is_object($value)){
491                        static::unwrapClosures($value, $storage);
492                        $property->setValue($data, $value);
493                    }
494                };
495            } while($reflection = $reflection->getParentClass());
496        }
497    }
498
499    /**
500     * Creates a new closure from arbitrary code,
501     * emulating create_function, but without using eval
502     *
503     * @param string$args
504     * @param string $code
505     * @return Closure
506     */
507    public static function createClosure($args, $code)
508    {
509        ClosureStream::register();
510        return include(ClosureStream::STREAM_PROTO . '://function(' . $args. '){' . $code . '};');
511    }
512
513    /**
514     * Internal method used to map closure pointers
515     * @internal
516     * @param $data
517     */
518    protected function mapPointers(&$data)
519    {
520        $scope = $this->scope;
521
522        if ($data instanceof static) {
523            $data = &$data->closure;
524        } elseif (is_array($data)) {
525            if(isset($data[self::ARRAY_RECURSIVE_KEY])){
526                return;
527            }
528            $data[self::ARRAY_RECURSIVE_KEY] = true;
529            foreach ($data as $key => &$value){
530                if($key === self::ARRAY_RECURSIVE_KEY){
531                    continue;
532                } elseif ($value instanceof static) {
533                    $data[$key] = &$value->closure;
534                } elseif ($value instanceof SelfReference && $value->hash === $this->code['self']){
535                    $data[$key] = &$this->closure;
536                } else {
537                    $this->mapPointers($value);
538                }
539            }
540            unset($value);
541            unset($data[self::ARRAY_RECURSIVE_KEY]);
542        } elseif ($data instanceof \stdClass) {
543            if(isset($scope[$data])){
544                return;
545            }
546            $scope[$data] = true;
547            foreach ($data as $key => &$value){
548                if ($value instanceof SelfReference && $value->hash === $this->code['self']){
549                    $data->{$key} = &$this->closure;
550                } elseif(is_array($value) || is_object($value)) {
551                    $this->mapPointers($value);
552                }
553            }
554            unset($value);
555        } elseif (is_object($data) && !($data instanceof Closure)){
556            if(isset($scope[$data])){
557                return;
558            }
559            $scope[$data] = true;
560            $reflection = new ReflectionObject($data);
561            do{
562                if(!$reflection->isUserDefined()){
563                    break;
564                }
565                foreach ($reflection->getProperties() as $property){
566                    if($property->isStatic() || !$property->getDeclaringClass()->isUserDefined()){
567                        continue;
568                    }
569                    $property->setAccessible(true);
570                    if (PHP_VERSION >= 7.4 && !$property->isInitialized($data)) {
571                        continue;
572                    }
573                    $item = $property->getValue($data);
574                    if ($item instanceof SerializableClosure || ($item instanceof SelfReference && $item->hash === $this->code['self'])) {
575                        $this->code['objects'][] = array(
576                            'instance' => $data,
577                            'property' => $property,
578                            'object' => $item instanceof SelfReference ? $this : $item,
579                        );
580                    } elseif (is_array($item) || is_object($item)) {
581                        $this->mapPointers($item);
582                        $property->setValue($data, $item);
583                    }
584                }
585            } while($reflection = $reflection->getParentClass());
586        }
587    }
588
589    /**
590     * Internal method used to map closures by reference
591     *
592     * @internal
593     * @param   mixed &$data
594     */
595    protected function mapByReference(&$data)
596    {
597        if ($data instanceof Closure) {
598            if($data === $this->closure){
599                $data = new SelfReference($this->reference);
600                return;
601            }
602
603            if (isset($this->scope[$data])) {
604                $data = $this->scope[$data];
605                return;
606            }
607
608            $instance = new static($data);
609
610            if (static::$context !== null) {
611                static::$context->scope->toserialize--;
612            } else {
613                $instance->scope = $this->scope;
614            }
615
616            $data = $this->scope[$data] = $instance;
617        } elseif (is_array($data)) {
618            if(isset($data[self::ARRAY_RECURSIVE_KEY])){
619                return;
620            }
621            $data[self::ARRAY_RECURSIVE_KEY] = true;
622            foreach ($data as $key => &$value){
623                if($key === self::ARRAY_RECURSIVE_KEY){
624                    continue;
625                }
626                $this->mapByReference($value);
627            }
628            unset($value);
629            unset($data[self::ARRAY_RECURSIVE_KEY]);
630        } elseif ($data instanceof \stdClass) {
631            if(isset($this->scope[$data])){
632                $data = $this->scope[$data];
633                return;
634            }
635            $instance = $data;
636            $this->scope[$instance] = $data = clone($data);
637
638            foreach ($data as &$value){
639                $this->mapByReference($value);
640            }
641            unset($value);
642        } elseif (is_object($data) && !$data instanceof SerializableClosure){
643            if(isset($this->scope[$data])){
644                $data = $this->scope[$data];
645                return;
646            }
647
648            $instance = $data;
649            $reflection = new ReflectionObject($data);
650            if(!$reflection->isUserDefined()){
651                $this->scope[$instance] = $data;
652                return;
653            }
654            $this->scope[$instance] = $data = $reflection->newInstanceWithoutConstructor();
655
656            do{
657                if(!$reflection->isUserDefined()){
658                    break;
659                }
660                foreach ($reflection->getProperties() as $property){
661                    if($property->isStatic() || !$property->getDeclaringClass()->isUserDefined()){
662                        continue;
663                    }
664                    $property->setAccessible(true);
665                    if (PHP_VERSION >= 7.4 && !$property->isInitialized($instance)) {
666                        continue;
667                    }
668                    $value = $property->getValue($instance);
669                    if(is_array($value) || is_object($value)){
670                        $this->mapByReference($value);
671                    }
672                    $property->setValue($data, $value);
673                }
674            } while($reflection = $reflection->getParentClass());
675        }
676    }
677
678}
679