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