8889841cREADME.md000064400000005201150514010540006014 0ustar00# Serializable Closure Build Status Total Downloads Latest Stable Version License ## Introduction > This project is a fork of the excellent [opis/closure: 3.x](https://github.com/opis/closure) package. At Laravel, we decided to fork this package as the upcoming version [4.x](https://github.com/opis/closure) is a complete rewrite on top of the [FFI extension](https://www.php.net/manual/en/book.ffi.php). As Laravel is a web framework, and FFI is not enabled by default in web requests, this fork allows us to keep using the `3.x` series while adding support for new PHP versions. Laravel Serializable Closure provides an easy and secure way to **serialize closures in PHP**. ## 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 ``` ### Usage You may serialize a closure this way: ```php use Laravel\SerializableClosure\SerializableClosure; $closure = fn () => 'james'; // Recommended SerializableClosure::setSecretKey('secret'); $serialized = serialize(new SerializableClosure($closure)); $closure = unserialize($serialized)->getClosure(); echo $closure(); // james; ``` ### Caveats Creating **anonymous classes** within closures is not supported. ## Contributing Thank you for considering contributing to Serializable Closure! 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 Please review [our security policy](https://github.com/laravel/serializable-closure/security/policy) on how to report security vulnerabilities. ## License Serializable Closure is open-sourced software licensed under the [MIT license](LICENSE.md). LICENSE.md000064400000002063150514010540006144 0ustar00The MIT License (MIT) Copyright (c) Taylor Otwell 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. src/Support/ClosureStream.php000064400000006754150514010540012317 0ustar00content = "length = strlen($this->content); return true; } /** * Read from stream. * * @param int $count * @return string */ public function stream_read($count) { $value = substr($this->content, $this->pointer, $count); $this->pointer += $count; return $value; } /** * Tests for end-of-file on a file pointer. * * @return bool */ public function stream_eof() { return $this->pointer >= $this->length; } /** * Change stream options. * * @param int $option * @param int $arg1 * @param int $arg2 * @return bool */ public function stream_set_option($option, $arg1, $arg2) { return false; } /** * Retrieve information about a file resource. * * @return array|bool */ public function stream_stat() { $stat = stat(__FILE__); // @phpstan-ignore-next-line $stat[7] = $stat['size'] = $this->length; return $stat; } /** * Retrieve information about a file. * * @param string $path * @param int $flags * @return array|bool */ public function url_stat($path, $flags) { $stat = stat(__FILE__); // @phpstan-ignore-next-line $stat[7] = $stat['size'] = $this->length; return $stat; } /** * Seeks to specific location in a stream. * * @param int $offset * @param int $whence * @return bool */ public function stream_seek($offset, $whence = SEEK_SET) { $crt = $this->pointer; switch ($whence) { case SEEK_SET: $this->pointer = $offset; break; case SEEK_CUR: $this->pointer += $offset; break; case SEEK_END: $this->pointer = $this->length + $offset; break; } if ($this->pointer < 0 || $this->pointer >= $this->length) { $this->pointer = $crt; return false; } return true; } /** * Retrieve the current position of a stream. * * @return int */ public function stream_tell() { return $this->pointer; } /** * Registers the stream. * * @return void */ public static function register() { if (! static::$isRegistered) { static::$isRegistered = stream_wrapper_register(static::STREAM_PROTO, __CLASS__); } } } src/Support/SelfReference.php000064400000000602150514010540012221 0ustar00hash = $hash; } } src/Support/ClosureScope.php000064400000000572150514010540012125 0ustar00isStaticClosure === null) { $this->isStaticClosure = strtolower(substr($this->getCode(), 0, 6)) === 'static'; } return $this->isStaticClosure; } /** * Checks if the closure is a "short closure". * * @return bool */ 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'; } return $this->isShortClosure; } /** * Get the closure's code. * * @return string */ public function getCode() { if ($this->code !== null) { return $this->code; } $fileName = $this->getFileName(); $line = $this->getStartLine() - 1; $className = null; if (null !== $className = $this->getClosureScopeClass()) { $className = '\\'.trim($className->getName(), '\\'); } $builtin_types = self::getBuiltinTypes(); $class_keywords = ['self', 'static', 'parent']; $ns = $this->getNamespaceName(); $nsf = $ns == '' ? '' : ($ns[0] == '\\' ? $ns : '\\'.$ns); $_file = var_export($fileName, true); $_dir = var_export(dirname($fileName), true); $_namespace = var_export($ns, true); $_class = var_export(trim($className ?: '', '\\'), true); $_function = $ns.($ns == '' ? '' : '\\').'{closure}'; $_method = ($className == '' ? '' : trim($className, '\\').'::').$_function; $_function = var_export($_function, true); $_method = var_export($_method, true); $_trait = null; $tokens = $this->getTokens(); $state = $lastState = 'start'; $inside_structure = false; $isShortClosure = false; $inside_structure_mark = 0; $open = 0; $code = ''; $id_start = $id_start_ci = $id_name = $context = ''; $classes = $functions = $constants = null; $use = []; $lineAdd = 0; $isUsingScope = false; $isUsingThisObject = false; 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) { $code .= $token[1]; $state = $token[0] === T_FUNCTION ? 'function' : 'static'; } elseif ($token[0] === T_FN) { $isShortClosure = true; $code .= $token[1]; $state = 'closure_args'; } break; case 'static': if ($token[0] === T_WHITESPACE || $token[0] === T_COMMENT || $token[0] === T_FUNCTION) { $code .= $token[1]; if ($token[0] === T_FUNCTION) { $state = 'function'; } } elseif ($token[0] === T_FN) { $isShortClosure = true; $code .= $token[1]; $state = 'closure_args'; } else { $code = ''; $state = 'start'; } break; case 'function': switch ($token[0]) { case T_STRING: $code = ''; $state = 'named_function'; break; case '(': $code .= '('; $state = 'closure_args'; break; default: $code .= is_array($token) ? $token[1] : $token; } break; case 'named_function': if ($token[0] === T_FUNCTION || $token[0] === T_STATIC) { $code = $token[1]; $state = $token[0] === T_FUNCTION ? 'function' : 'static'; } elseif ($token[0] === T_FN) { $isShortClosure = true; $code .= $token[1]; $state = 'closure_args'; } break; case 'closure_args': switch ($token[0]) { case T_NAME_QUALIFIED: [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); $context = 'args'; $state = 'id_name'; $lastState = 'closure_args'; break; case T_NS_SEPARATOR: case T_STRING: $id_start = $token[1]; $id_start_ci = strtolower($id_start); $id_name = ''; $context = 'args'; $state = 'id_name'; $lastState = 'closure_args'; break; case T_USE: $code .= $token[1]; $state = 'use'; break; case T_DOUBLE_ARROW: $code .= $token[1]; if ($isShortClosure) { $state = 'closure'; } break; case ':': $code .= ':'; $state = 'return'; break; case '{': $code .= '{'; $state = 'closure'; $open++; break; default: $code .= is_array($token) ? $token[1] : $token; } break; case 'use': switch ($token[0]) { case T_VARIABLE: $use[] = substr($token[1], 1); $code .= $token[1]; break; case '{': $code .= '{'; $state = 'closure'; $open++; break; case ':': $code .= ':'; $state = 'return'; break; default: $code .= is_array($token) ? $token[1] : $token; break; } break; case 'return': switch ($token[0]) { case T_WHITESPACE: case T_COMMENT: case T_DOC_COMMENT: $code .= $token[1]; break; case T_NS_SEPARATOR: case T_STRING: $id_start = $token[1]; $id_start_ci = strtolower($id_start); $id_name = ''; $context = 'return_type'; $state = 'id_name'; $lastState = 'return'; break 2; case T_NAME_QUALIFIED: [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); $context = 'return_type'; $state = 'id_name'; $lastState = 'return'; break 2; case T_DOUBLE_ARROW: $code .= $token[1]; if ($isShortClosure) { $state = 'closure'; } break; case '{': $code .= '{'; $state = 'closure'; $open++; break; default: $code .= is_array($token) ? $token[1] : $token; break; } break; case 'closure': switch ($token[0]) { case T_CURLY_OPEN: case T_DOLLAR_OPEN_CURLY_BRACES: case '{': $code .= is_array($token) ? $token[1] : $token; $open++; break; case '}': $code .= '}'; if (--$open === 0 && ! $isShortClosure) { break 3; } elseif ($inside_structure) { $inside_structure = ! ($open === $inside_structure_mark); } break; case '(': case '[': $code .= $token[0]; if ($isShortClosure) { $open++; } break; case ')': case ']': if ($isShortClosure) { if ($open === 0) { break 3; } $open--; } $code .= $token[0]; break; case ',': case ';': if ($isShortClosure && $open === 0) { break 3; } $code .= $token[0]; break; case T_LINE: $code .= $token[2] - $line + $lineAdd; break; case T_FILE: $code .= $_file; break; case T_DIR: $code .= $_dir; break; case T_NS_C: $code .= $_namespace; break; case T_CLASS_C: $code .= $inside_structure ? $token[1] : $_class; break; case T_FUNC_C: $code .= $inside_structure ? $token[1] : $_function; break; case T_METHOD_C: $code .= $inside_structure ? $token[1] : $_method; break; case T_COMMENT: if (substr($token[1], 0, 8) === '#trackme') { $timestamp = time(); $code .= '/**'.PHP_EOL; $code .= '* Date : '.date(DATE_W3C, $timestamp).PHP_EOL; $code .= '* Timestamp : '.$timestamp.PHP_EOL; $code .= '* Line : '.($line + 1).PHP_EOL; $code .= '* File : '.$_file.PHP_EOL.'*/'.PHP_EOL; $lineAdd += 5; } else { $code .= $token[1]; } break; case T_VARIABLE: if ($token[1] == '$this' && ! $inside_structure) { $isUsingThisObject = true; } $code .= $token[1]; break; case T_STATIC: case T_NS_SEPARATOR: case T_STRING: $id_start = $token[1]; $id_start_ci = strtolower($id_start); $id_name = ''; $context = 'root'; $state = 'id_name'; $lastState = 'closure'; break 2; case T_NAME_QUALIFIED: [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); $context = 'root'; $state = 'id_name'; $lastState = 'closure'; break 2; case T_NEW: $code .= $token[1]; $context = 'new'; $state = 'id_start'; $lastState = 'closure'; break 2; case T_USE: $code .= $token[1]; $context = 'use'; $state = 'id_start'; $lastState = 'closure'; break; case T_INSTANCEOF: case T_INSTEADOF: $code .= $token[1]; $context = 'instanceof'; $state = 'id_start'; $lastState = 'closure'; break; case T_OBJECT_OPERATOR: case T_NULLSAFE_OBJECT_OPERATOR: case T_DOUBLE_COLON: $code .= $token[1]; $lastState = 'closure'; $state = 'ignore_next'; break; case T_FUNCTION: $code .= $token[1]; $state = 'closure_args'; if (! $inside_structure) { $inside_structure = true; $inside_structure_mark = $open; } break; case T_TRAIT_C: if ($_trait === null) { $startLine = $this->getStartLine(); $endLine = $this->getEndLine(); $structures = $this->getStructures(); $_trait = ''; foreach ($structures as &$struct) { if ($struct['type'] === 'trait' && $struct['start'] <= $startLine && $struct['end'] >= $endLine ) { $_trait = ($ns == '' ? '' : $ns.'\\').$struct['name']; break; } } $_trait = var_export($_trait, true); } $code .= $_trait; break; default: $code .= is_array($token) ? $token[1] : $token; } break; case 'ignore_next': switch ($token[0]) { case T_WHITESPACE: case T_COMMENT: case T_DOC_COMMENT: $code .= $token[1]; break; case T_CLASS: case T_NEW: case T_STATIC: case T_VARIABLE: case T_STRING: case T_CLASS_C: case T_FILE: case T_DIR: case T_METHOD_C: case T_FUNC_C: case T_FUNCTION: case T_INSTANCEOF: case T_LINE: case T_NS_C: case T_TRAIT_C: case T_USE: $code .= $token[1]; $state = $lastState; break; default: $state = $lastState; $i--; } break; case 'id_start': switch ($token[0]) { case T_WHITESPACE: case T_COMMENT: case T_DOC_COMMENT: $code .= $token[1]; break; case T_NS_SEPARATOR: case T_NAME_FULLY_QUALIFIED: case T_STRING: case T_STATIC: $id_start = $token[1]; $id_start_ci = strtolower($id_start); $id_name = ''; $state = 'id_name'; break 2; case T_NAME_QUALIFIED: [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); $state = 'id_name'; break 2; case T_VARIABLE: $code .= $token[1]; $state = $lastState; break; case T_CLASS: $code .= $token[1]; $state = 'anonymous'; break; default: $i--; //reprocess last $state = 'id_name'; } break; case 'id_name': switch ($token[0]) { // named arguments... case ':': if ($lastState === 'closure' && $context === 'root') { $state = 'ignore_next'; $lastState = 'closure'; $code .= $id_start.$token; } break; case T_NAME_QUALIFIED: case T_NS_SEPARATOR: case T_STRING: case T_WHITESPACE: case T_COMMENT: case T_DOC_COMMENT: $id_name .= $token[1]; break; case '(': if ($isShortClosure) { $open++; } if ($context === 'new' || false !== strpos($id_name, '\\')) { if ($id_start_ci === 'self' || $id_start_ci === 'static') { if (! $inside_structure) { $isUsingScope = true; } } elseif ($id_start !== '\\' && ! in_array($id_start_ci, $class_keywords)) { if ($classes === null) { $classes = $this->getClasses(); } if (isset($classes[$id_start_ci])) { $id_start = $classes[$id_start_ci]; } if ($id_start[0] !== '\\') { $id_start = $nsf.'\\'.$id_start; } } } else { if ($id_start !== '\\') { if ($functions === null) { $functions = $this->getFunctions(); } if (isset($functions[$id_start_ci])) { $id_start = $functions[$id_start_ci]; } elseif ($nsf !== '\\' && function_exists($nsf.'\\'.$id_start)) { $id_start = $nsf.'\\'.$id_start; // Cache it to functions array $functions[$id_start_ci] = $id_start; } } } $code .= $id_start.$id_name.'('; $state = $lastState; break; case T_VARIABLE: case T_DOUBLE_COLON: if ($id_start !== '\\') { if ($id_start_ci === 'self' || $id_start_ci === 'parent') { if (! $inside_structure) { $isUsingScope = true; } } elseif ($id_start_ci === 'static') { if (! $inside_structure) { $isUsingScope = $token[0] === T_DOUBLE_COLON; } } elseif (! (\PHP_MAJOR_VERSION >= 7 && in_array($id_start_ci, $builtin_types))) { if ($classes === null) { $classes = $this->getClasses(); } if (isset($classes[$id_start_ci])) { $id_start = $classes[$id_start_ci]; } if ($id_start[0] !== '\\') { $id_start = $nsf.'\\'.$id_start; } } } $code .= $id_start.$id_name.$token[1]; $state = $token[0] === T_DOUBLE_COLON ? 'ignore_next' : $lastState; break; default: if ($id_start !== '\\' && ! defined($id_start)) { if ($constants === null) { $constants = $this->getConstants(); } if (isset($constants[$id_start])) { $id_start = $constants[$id_start]; } elseif ($context === 'new') { if (in_array($id_start_ci, $class_keywords)) { if (! $inside_structure) { $isUsingScope = true; } } else { if ($classes === null) { $classes = $this->getClasses(); } if (isset($classes[$id_start_ci])) { $id_start = $classes[$id_start_ci]; } if ($id_start[0] !== '\\') { $id_start = $nsf.'\\'.$id_start; } } } elseif ($context === 'use' || $context === 'instanceof' || $context === 'args' || $context === 'return_type' || $context === 'extends' || $context === 'root' ) { if (in_array($id_start_ci, $class_keywords)) { if (! $inside_structure && ! $id_start_ci === 'static') { $isUsingScope = true; } } elseif (! (\PHP_MAJOR_VERSION >= 7 && in_array($id_start_ci, $builtin_types))) { if ($classes === null) { $classes = $this->getClasses(); } if (isset($classes[$id_start_ci])) { $id_start = $classes[$id_start_ci]; } if ($id_start[0] !== '\\') { $id_start = $nsf.'\\'.$id_start; } } } } $code .= $id_start.$id_name; $state = $lastState; $i--; //reprocess last token } break; case 'anonymous': switch ($token[0]) { case T_NS_SEPARATOR: case T_STRING: $id_start = $token[1]; $id_start_ci = strtolower($id_start); $id_name = ''; $state = 'id_name'; $context = 'extends'; $lastState = 'anonymous'; break; case '{': $state = 'closure'; if (! $inside_structure) { $inside_structure = true; $inside_structure_mark = $open; } $i--; break; default: $code .= is_array($token) ? $token[1] : $token; } break; } } if ($isShortClosure) { $this->useVariables = $this->getStaticVariables(); } else { $this->useVariables = empty($use) ? $use : array_intersect_key($this->getStaticVariables(), array_flip($use)); } $this->isShortClosure = $isShortClosure; $this->isBindingRequired = $isUsingThisObject; $this->isScopeRequired = $isUsingScope; $this->code = $code; return $this->code; } /** * Get PHP native built in types. * * @return array */ protected static function getBuiltinTypes() { // PHP 8.1 if (PHP_VERSION_ID >= 80100) { return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void', 'object', 'mixed', 'false', 'null', 'never']; } // PHP 8 if (\PHP_MAJOR_VERSION === 8) { return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void', 'object', 'mixed', 'false', 'null']; } // PHP 7 switch (\PHP_MINOR_VERSION) { case 0: return ['array', 'callable', 'string', 'int', 'bool', 'float']; case 1: return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void']; default: return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void', 'object']; } } /** * Gets the use variables by the closure. * * @return array */ public function getUseVariables() { if ($this->useVariables !== null) { return $this->useVariables; } $tokens = $this->getTokens(); $use = []; $state = 'start'; foreach ($tokens as &$token) { $is_array = is_array($token); switch ($state) { case 'start': if ($is_array && $token[0] === T_USE) { $state = 'use'; } break; case 'use': if ($is_array) { if ($token[0] === T_VARIABLE) { $use[] = substr($token[1], 1); } } elseif ($token == ')') { break 2; } break; } } $this->useVariables = empty($use) ? $use : array_intersect_key($this->getStaticVariables(), array_flip($use)); return $this->useVariables; } /** * Checks if binding is required. * * @return bool */ public function isBindingRequired() { if ($this->isBindingRequired === null) { $this->getCode(); } return $this->isBindingRequired; } /** * Checks if access to the scope is required. * * @return bool */ public function isScopeRequired() { if ($this->isScopeRequired === null) { $this->getCode(); } return $this->isScopeRequired; } /** * The the hash of the current file name. * * @return string */ protected function getHashedFileName() { if ($this->hashedName === null) { $this->hashedName = sha1($this->getFileName()); } return $this->hashedName; } /** * Get the file tokens. * * @return array */ protected function getFileTokens() { $key = $this->getHashedFileName(); if (! isset(static::$files[$key])) { static::$files[$key] = token_get_all(file_get_contents($this->getFileName())); } return static::$files[$key]; } /** * Get the tokens. * * @return array */ protected function getTokens() { if ($this->tokens === null) { $tokens = $this->getFileTokens(); $startLine = $this->getStartLine(); $endLine = $this->getEndLine(); $results = []; $start = false; foreach ($tokens as &$token) { if (! is_array($token)) { if ($start) { $results[] = $token; } continue; } $line = $token[2]; if ($line <= $endLine) { if ($line >= $startLine) { $start = true; $results[] = $token; } continue; } break; } $this->tokens = $results; } return $this->tokens; } /** * Get the classes. * * @return array */ protected function getClasses() { $key = $this->getHashedFileName(); if (! isset(static::$classes[$key])) { $this->fetchItems(); } return static::$classes[$key]; } /** * Get the functions. * * @return array */ protected function getFunctions() { $key = $this->getHashedFileName(); if (! isset(static::$functions[$key])) { $this->fetchItems(); } return static::$functions[$key]; } /** * Gets the constants. * * @return array */ protected function getConstants() { $key = $this->getHashedFileName(); if (! isset(static::$constants[$key])) { $this->fetchItems(); } return static::$constants[$key]; } /** * Get the structures. * * @return array */ protected function getStructures() { $key = $this->getHashedFileName(); if (! isset(static::$structures[$key])) { $this->fetchItems(); } return static::$structures[$key]; } /** * Fetch the items. * * @return void. */ protected function fetchItems() { $key = $this->getHashedFileName(); $classes = []; $functions = []; $constants = []; $structures = []; $tokens = $this->getFileTokens(); $open = 0; $state = 'start'; $lastState = ''; $prefix = ''; $name = ''; $alias = ''; $isFunc = $isConst = false; $startLine = $endLine = 0; $structType = $structName = ''; $structIgnore = false; foreach ($tokens as $token) { switch ($state) { case 'start': switch ($token[0]) { case T_CLASS: case T_INTERFACE: case T_TRAIT: $state = 'before_structure'; $startLine = $token[2]; $structType = $token[0] == T_CLASS ? 'class' : ($token[0] == T_INTERFACE ? 'interface' : 'trait'); break; case T_USE: $state = 'use'; $prefix = $name = $alias = ''; $isFunc = $isConst = false; break; case T_FUNCTION: $state = 'structure'; $structIgnore = true; break; case T_NEW: $state = 'new'; break; case T_OBJECT_OPERATOR: case T_DOUBLE_COLON: $state = 'invoke'; break; } break; case 'use': switch ($token[0]) { case T_FUNCTION: $isFunc = true; break; case T_CONST: $isConst = true; break; case T_NS_SEPARATOR: $name .= $token[1]; break; case T_STRING: $name .= $token[1]; $alias = $token[1]; break; case T_NAME_QUALIFIED: $name .= $token[1]; $pieces = explode('\\', $token[1]); $alias = end($pieces); break; case T_AS: $lastState = 'use'; $state = 'alias'; break; case '{': $prefix = $name; $name = $alias = ''; $state = 'use-group'; break; case ',': case ';': if ($name === '' || $name[0] !== '\\') { $name = '\\'.$name; } if ($alias !== '') { if ($isFunc) { $functions[strtolower($alias)] = $name; } elseif ($isConst) { $constants[$alias] = $name; } else { $classes[strtolower($alias)] = $name; } } $name = $alias = ''; $state = $token === ';' ? 'start' : 'use'; break; } break; case 'use-group': switch ($token[0]) { case T_NS_SEPARATOR: $name .= $token[1]; break; case T_NAME_QUALIFIED: $name .= $token[1]; $pieces = explode('\\', $token[1]); $alias = end($pieces); break; case T_STRING: $name .= $token[1]; $alias = $token[1]; break; case T_AS: $lastState = 'use-group'; $state = 'alias'; break; case ',': case '}': if ($prefix === '' || $prefix[0] !== '\\') { $prefix = '\\'.$prefix; } if ($alias !== '') { if ($isFunc) { $functions[strtolower($alias)] = $prefix.$name; } elseif ($isConst) { $constants[$alias] = $prefix.$name; } else { $classes[strtolower($alias)] = $prefix.$name; } } $name = $alias = ''; $state = $token === '}' ? 'use' : 'use-group'; break; } break; case 'alias': if ($token[0] === T_STRING) { $alias = $token[1]; $state = $lastState; } break; case 'new': switch ($token[0]) { case T_WHITESPACE: case T_COMMENT: case T_DOC_COMMENT: break 2; case T_CLASS: $state = 'structure'; $structIgnore = true; break; default: $state = 'start'; } break; case 'invoke': switch ($token[0]) { case T_WHITESPACE: case T_COMMENT: case T_DOC_COMMENT: break 2; default: $state = 'start'; } break; case 'before_structure': if ($token[0] == T_STRING) { $structName = $token[1]; $state = 'structure'; } break; case 'structure': switch ($token[0]) { case '{': case T_CURLY_OPEN: case T_DOLLAR_OPEN_CURLY_BRACES: $open++; break; case '}': if (--$open == 0) { if (! $structIgnore) { $structures[] = [ 'type' => $structType, 'name' => $structName, 'start' => $startLine, 'end' => $endLine, ]; } $structIgnore = false; $state = 'start'; } break; default: if (is_array($token)) { $endLine = $token[2]; } } break; } } static::$classes[$key] = $classes; static::$functions[$key] = $functions; static::$constants[$key] = $constants; static::$structures[$key] = $structures; } /** * Parse the given token. * * @param string $token * @return array */ protected function parseNameQualified($token) { $pieces = explode('\\', $token); $id_start = array_shift($pieces); $id_start_ci = strtolower($id_start); $id_name = '\\'.implode('\\', $pieces); return [$id_start, $id_start_ci, $id_name]; } } src/Contracts/Signer.php000064400000000604150514010540011226 0ustar00closure = $closure; } /** * Resolve the closure with the given arguments. * * @return mixed */ public function __invoke() { return call_user_func_array($this->closure, func_get_args()); } /** * Gets the closure. * * @return \Closure */ public function getClosure() { return $this->closure; } /** * Get the serializable representation of the closure. * * @return array */ 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; } $this->reference = spl_object_hash($this->closure); $this->scope[$this->closure] = $this; $use = $reflector->getUseVariables(); if (static::$transformUseVariables) { $use = call_user_func(static::$transformUseVariables, $reflector->getUseVariables()); } $code = $reflector->getCode(); $this->mapByReference($use); $data = [ 'use' => $use, 'function' => $code, 'scope' => $scope, 'this' => $object, 'self' => $this->reference, ]; if (! --$this->scope->serializations && ! --$this->scope->toSerialize) { $this->scope = null; } return $data; } /** * Restore the closure after serialization. * * @param array $data * @return void */ public function __unserialize($data) { ClosureStream::register(); $this->code = $data; unset($data); $this->code['objects'] = []; if ($this->code['use']) { $this->scope = new ClosureScope(); if (static::$resolveUseVariables) { $this->code['use'] = call_user_func(static::$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']; } /** * Ensures the given closures are serializable. * * @param mixed $data * @param \Laravel\SerializableClosure\Support\ClosureScope $storage * @return void */ public static function wrapClosures(&$data, $storage) { if ($data instanceof Closure) { $data = new static($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()); } } /** * Gets the closure's reflector. * * @return \Laravel\SerializableClosure\Support\ReflectionClosure */ public function getReflector() { if ($this->reflector === null) { $this->code = null; $this->reflector = new ReflectionClosure($this->closure); } return $this->reflector; } /** * Internal method used to map closure pointers. * * @param mixed $data * @return void */ 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'][] = [ '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. * * @param mixed $data * @return void */ 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); $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; if ($data instanceof UnitEnum) { $this->scope[$instance] = $data; return; } $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()); } } } src/Serializers/Signed.php000064400000003726150514010540011554 0ustar00closure = $closure; } /** * Resolve the closure with the given arguments. * * @return mixed */ public function __invoke() { return call_user_func_array($this->closure, func_get_args()); } /** * Gets the closure. * * @return \Closure */ public function getClosure() { return $this->closure; } /** * Get the serializable representation of the closure. * * @return array */ public function __serialize() { if (! static::$signer) { throw new MissingSecretKeyException(); } return static::$signer->sign( serialize(new Native($this->closure)) ); } /** * Restore the closure after serialization. * * @param array $signature * @return void * * @throws \Laravel\SerializableClosure\Exceptions\InvalidSignatureException */ public function __unserialize($signature) { if (static::$signer && ! static::$signer->verify($signature)) { throw new InvalidSignatureException(); } $this->closure = unserialize($signature['serializable'])->getClosure(); } } src/Signers/Hmac.php000064400000002061150514010540010320 0ustar00secret = $secret; } /** * Sign the given serializable. * * @param string $serialized * @return array */ public function sign($serialized) { return [ 'serializable' => $serialized, 'hash' => base64_encode(hash_hmac('sha256', $serialized, $this->secret, true)), ]; } /** * Verify the given signature. * * @param array $signature * @return bool */ public function verify($signature) { return hash_equals(base64_encode( hash_hmac('sha256', $signature['serializable'], $this->secret, true) ), $signature['hash']); } } src/SerializableClosure.php000064400000005762150514010540012014 0ustar00serializable = Serializers\Signed::$signer ? new Serializers\Signed($closure) : new Serializers\Native($closure); } /** * Resolve the closure with the given arguments. * * @return mixed */ public function __invoke() { if (\PHP_VERSION_ID < 70400) { throw new PhpVersionNotSupportedException(); } return call_user_func_array($this->serializable, func_get_args()); } /** * Gets the closure. * * @return \Closure */ public function getClosure() { if (\PHP_VERSION_ID < 70400) { throw new PhpVersionNotSupportedException(); } return $this->serializable->getClosure(); } /** * Sets the serializable closure secret key. * * @param string|null $secret * @return void */ public static function setSecretKey($secret) { Serializers\Signed::$signer = $secret ? new Hmac($secret) : null; } /** * Sets the serializable closure secret key. * * @param \Closure|null $transformer * @return void */ public static function transformUseVariablesUsing($transformer) { Serializers\Native::$transformUseVariables = $transformer; } /** * Sets the serializable closure secret key. * * @param \Closure|null $resolver * @return void */ public static function resolveUseVariablesUsing($resolver) { Serializers\Native::$resolveUseVariables = $resolver; } /** * Get the serializable representation of the closure. * * @return array */ public function __serialize() { return [ 'serializable' => $this->serializable, ]; } /** * Restore the closure after serialization. * * @param array $data * @return void * * @throws \Laravel\SerializableClosure\Exceptions\InvalidSignatureException */ public function __unserialize($data) { if (Signed::$signer && ! $data['serializable'] instanceof Signed) { throw new InvalidSignatureException(); } $this->serializable = $data['serializable']; } } composer.json000064400000002300150514010540007254 0ustar00{ "name": "laravel/serializable-closure", "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", "keywords": ["laravel", "Serializable", "closure"], "license": "MIT", "support": { "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, "authors": [ { "name": "Taylor Otwell", "email": "taylor@laravel.com" }, { "name": "Nuno Maduro", "email": "nuno@laravel.com" } ], "require": { "php": "^7.3|^8.0" }, "require-dev": { "pestphp/pest": "^1.18", "phpstan/phpstan": "^0.12.98", "symfony/var-dumper": "^5.3" }, "autoload": { "psr-4": { "Laravel\\SerializableClosure\\": "src/" } }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } }, "extra": { "branch-alias": { "dev-master": "1.x-dev" } }, "config": { "sort-packages": true }, "minimum-stability": "dev", "prefer-stable": true }