diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md new file mode 100644 index 00000000..ef7a588d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md @@ -0,0 +1,17 @@ +--- +name: "Bug report" +about: "Report something that's broken. Please ensure your Laravel version is still supported: https://laravel.com/docs/releases#support-policy" +--- + + + + +- Serializable Closure Version: #.#.# +- Laravel Version: #.#.# +- PHP Version: #.#.# +- Database Driver & Version: + +### Description: + + +### Steps To Reproduce: diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md new file mode 100644 index 00000000..dfb10855 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.md @@ -0,0 +1,4 @@ +--- +name: "Feature request" +about: 'For ideas or feature requests: please make a pull request or open an issue' +--- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..6253bb2d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Support Questions & Other + url: https://laravel.com/docs/contributions#support-questions + about: 'This repository is only for reporting bugs. If you have a question or need help using the library, click:' + - name: Documentation issue + url: https://github.com/laravel/docs + about: For documentation issues, open a pull request at the laravel/docs repository diff --git a/README.md b/README.md index a04631a9..216281b4 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,21 @@ Laravel Serializable Closure provides an easy and secure way to **serialize closures in PHP**. -## Installation / Usage +## Official Documentation + +### Installation > **Requires [PHP 7.4+](https://php.net/releases/)** First, install Laravel Serializable Closure via the [Composer](https://getcomposer.org/) package manager: ```bash -composer require laravel/serializable-closure --dev +composer require laravel/serializable-closure ``` -Then, you may serialize a closure this way: +### Usage + +You may serialize a closure this way: ```php use Laravel\SerializableClosure\SerializableClosure; @@ -45,10 +49,22 @@ $closure = unserialize($serialized)->getClosure(); echo $closure(); // james; ``` -## Caveats +### Caveats + +Creating **anonymous classes** within closures is not supported. + +## Contributing + +Thank you for considering contributing to BrowserKit Testing! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities -- Creating **anonymous classes** within closures is not supported. +Please review [our security policy](https://github.com/laravel/browser-kit-testing/security/policy) on how to report security vulnerabilities. ## License -Seriazable Closure is open-sourced software licensed under the [MIT license](LICENSE.md). +Serializable Closure is open-sourced software licensed under the [MIT license](LICENSE.md). diff --git a/phpstan.neon b/phpstan.neon index 6a2f0625..43d44abc 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,11 +2,9 @@ parameters: level: max paths: - src - checkMissingIterableValueType: false checkGenericClassInNonGenericObjectType: false reportUnmatchedIgnoredErrors: true - excludePaths: - 'src/Support/ReflectionClosure' - 'src/Serializers/Native' diff --git a/src/Serializers/Native.php b/src/Serializers/Native.php index 15702b82..c04f68e1 100644 --- a/src/Serializers/Native.php +++ b/src/Serializers/Native.php @@ -82,6 +82,7 @@ class Native implements Serializable public function __construct(Closure $closure) { $this->closure = $closure; + if (static::$context !== null) { $this->scope = static::$context->scope; $this->scope->toSerialize++; @@ -150,11 +151,11 @@ public function __serialize() $this->mapByReference($use); $data = [ - 'use' => $use, + 'use' => $use, 'function' => $code, - 'scope' => $scope, - 'this' => $object, - 'self' => $this->reference, + 'scope' => $scope, + 'this' => $object, + 'self' => $this->reference, ]; if (! --$this->scope->serializations && ! --$this->scope->toSerialize) { @@ -186,7 +187,9 @@ public function __unserialize($data) } $this->mapPointers($this->code['use']); + extract($this->code['use'], EXTR_OVERWRITE | EXTR_REFS); + $this->scope = null; } @@ -226,13 +229,16 @@ public static function wrapClosures(&$data, $storage = null) 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) { @@ -241,10 +247,13 @@ public static function wrapClosures(&$data, $storage = null) 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])) { @@ -252,31 +261,40 @@ public static function wrapClosures(&$data, $storage = null) 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()); @@ -314,7 +332,9 @@ protected function mapPointers(&$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; @@ -326,13 +346,16 @@ protected function mapPointers(&$data) $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; @@ -340,31 +363,39 @@ protected function mapPointers(&$data) $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'][] = [ 'instance' => $data, 'property' => $property, - 'object' => $item instanceof SelfReference ? $this : $item, + 'object' => $item instanceof SelfReference ? $this : $item, ]; } elseif (is_array($item) || is_object($item)) { $this->mapPointers($item); @@ -408,13 +439,17 @@ protected function mapByReference(&$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) { @@ -423,12 +458,14 @@ protected function mapByReference(&$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])) { @@ -439,29 +476,37 @@ protected function mapByReference(&$data) $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()); diff --git a/src/Signers/Hmac.php b/src/Signers/Hmac.php index e9a1fe0b..d94b0a2a 100644 --- a/src/Signers/Hmac.php +++ b/src/Signers/Hmac.php @@ -34,7 +34,7 @@ public function sign($serialized) { return [ 'serializable' => $serialized, - 'hash' => base64_encode(hash_hmac('sha256', $serialized, $this->secret, true)), + 'hash' => base64_encode(hash_hmac('sha256', $serialized, $this->secret, true)), ]; } diff --git a/src/Support/ClosureStream.php b/src/Support/ClosureStream.php index 041d1c0d..8bcceb0a 100644 --- a/src/Support/ClosureStream.php +++ b/src/Support/ClosureStream.php @@ -63,6 +63,7 @@ public function stream_open($path, $mode, $options, &$opened_path) public function stream_read($count) { $value = substr($this->content, $this->pointer, $count); + $this->pointer += $count; return $value; diff --git a/src/Support/ReflectionClosure.php b/src/Support/ReflectionClosure.php index 0c1c77ba..139696d3 100644 --- a/src/Support/ReflectionClosure.php +++ b/src/Support/ReflectionClosure.php @@ -61,9 +61,11 @@ public function isShortClosure() { if ($this->isShortClosure === null) { $code = $this->getCode(); + if ($this->isStatic()) { $code = substr($code, 6); } + $this->isShortClosure = strtolower(substr(trim($code), 0, 2)) === 'fn'; } @@ -122,6 +124,7 @@ public function getCode() for ($i = 0, $l = count($tokens); $i < $l; $i++) { $token = $tokens[$i]; + switch ($state) { case 'start': if ($token[0] === T_FUNCTION || $token[0] === T_STATIC) {