diff --git a/.gitignore b/.gitignore index 0a20fff..665ec0c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,3 @@ tests/clover.xml # Node node_modules - -# Vendor -vendor diff --git a/README.md b/README.md index cc0fc30..4fed1de 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ **Requires at least:** 3.0.1 -**Tested up to:** 6.3.1 +**Tested up to:** 6.4 **Stable tag:** trunk diff --git a/includes/class-bsr-db.php b/includes/class-bsr-db.php index b2d09cf..6d44de4 100644 --- a/includes/class-bsr-db.php +++ b/includes/class-bsr-db.php @@ -330,7 +330,6 @@ public function srdb( $table, $page, $args ) { */ public function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialised = false, $case_insensitive = false ) { try { - if ( is_string( $data ) && ! is_serialized_string( $data ) && ( $unserialized = $this->unserialize( $data ) ) !== false ) { $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive ); } @@ -346,23 +345,23 @@ public function recursive_unserialize_replace( $from = '', $to = '', $data = '', } // Submitted by Tina Matter - elseif ( is_object( $data ) ) { - if ('__PHP_Incomplete_Class' !== get_class($data)) { - $_tmp = $data; + elseif ( 'object' == gettype( $data ) ) { + if($this->is_object_cloneable($data)) { + $_tmp = clone $data; $props = get_object_vars( $data ); foreach ( $props as $key => $value ) { // Integer properties are crazy and the best thing we can do is to just ignore them. // see http://stackoverflow.com/a/10333200 - if (is_int($key)) { + if ( is_int( $key ) ) { continue; } - + // Skip any representation of a protected property // https://github.com/deliciousbrains/better-search-replace/issues/71#issuecomment-1369195244 - if (is_string($key) && 1 === preg_match("/^([\\][0])?/im", $key)) { + if ( is_string( $key ) && 1 === preg_match( "/^(\\\\0).+/im", preg_quote( $key ) ) ) { continue; } - + $_tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive ); } @@ -445,7 +444,12 @@ public static function unserialize( $serialized_string ) { } $serialized_string = trim( $serialized_string ); - $unserialized_string = @unserialize( $serialized_string ); + + if ( PHP_VERSION_ID >= 70000 ) { + $unserialized_string = @unserialize( $serialized_string, array('allowed_classes' => false ) ); + } else { + $unserialized_string = @BSR\Brumann\Polyfill\Unserialize::unserialize( $serialized_string, array( 'allowed_classes' => false ) ); + } return $unserialized_string; } @@ -480,4 +484,15 @@ public function str_replace( $from, $to, $data, $case_insensitive = false ) { private function table_exists( $table ) { return in_array( $table, $this->get_tables() ); } + + /** + * Check if a given object can be cloned. + * + * @param object $object + * + * @return bool + */ + private function is_object_cloneable( $object ) { + return ( new \ReflectionClass( get_class( $object ) ) )->isCloneable(); + } } diff --git a/includes/class-bsr-main.php b/includes/class-bsr-main.php index 1f9413e..2887de8 100755 --- a/includes/class-bsr-main.php +++ b/includes/class-bsr-main.php @@ -82,6 +82,12 @@ private function load_dependencies() { require_once BSR_PATH . 'includes/class-bsr-compatibility.php'; require_once BSR_PATH . 'includes/class-bsr-plugin-footer.php'; require_once BSR_PATH . 'includes/class-bsr-utils.php'; + + if ( PHP_VERSION_ID < 70000 ) { + require_once BSR_PATH . 'vendor/brumann/polyfill-unserialize/src/Unserialize.php'; + require_once BSR_PATH . 'vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php'; + } + $this->loader = new BSR_Loader(); } diff --git a/vendor/brumann/polyfill-unserialize/LICENSE b/vendor/brumann/polyfill-unserialize/LICENSE new file mode 100644 index 0000000..ba944cc --- /dev/null +++ b/vendor/brumann/polyfill-unserialize/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016-2019 Denis Brumann + +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. diff --git a/vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php b/vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php new file mode 100644 index 0000000..81db256 --- /dev/null +++ b/vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php @@ -0,0 +1,177 @@ +, ]` and + * marks start and end positions of items to be ignored. + * + * @var array[] + */ + private $ignoreItems = array(); + + /** + * @param string $serialized + * @param string[] $allowedClasses + */ + public function __construct($serialized, array $allowedClasses) + { + $this->serialized = $serialized; + $this->allowedClasses = $allowedClasses; + + $this->buildIgnoreItems(); + $this->substituteObjects(); + } + + /** + * @return string + */ + public function getSubstitutedSerialized() + { + return $this->serialized; + } + + /** + * Identifies items to be ignored - like nested serializations in string literals. + */ + private function buildIgnoreItems() + { + $offset = 0; + while (preg_match(self::PATTERN_STRING, $this->serialized, $matches, PREG_OFFSET_CAPTURE, $offset)) { + $length = (int)$matches[1][0]; // given length in serialized data (e.g. `s:123:"` --> 123) + $start = $matches[2][1]; // offset position of quote character + $end = $start + $length + 1; + $offset = $end + 1; + + // serialized string nested in outer serialized string + if ($this->ignore($start, $end)) { + continue; + } + + $this->ignoreItems[] = array($start, $end); + } + } + + /** + * Substitutes disallowed object class names and respects items to be ignored. + */ + private function substituteObjects() + { + $offset = 0; + while (preg_match(self::PATTERN_OBJECT, $this->serialized, $matches, PREG_OFFSET_CAPTURE, $offset)) { + $completeMatch = (string)$matches[0][0]; + $completeLength = strlen($completeMatch); + $start = $matches[0][1]; + $end = $start + $completeLength; + $leftBorder = (string)$matches[1][0]; + $className = (string)$matches[2][0]; + $objectSize = (int)$matches[3][0]; + $offset = $end + 1; + + // class name is actually allowed - skip this item + if (in_array($className, $this->allowedClasses, true)) { + continue; + } + // serialized object nested in outer serialized string + if ($this->ignore($start, $end)) { + continue; + } + + $incompleteItem = $this->sanitizeItem($className, $leftBorder, $objectSize); + $incompleteItemLength = strlen($incompleteItem); + $offset = $start + $incompleteItemLength + 1; + + $this->replace($incompleteItem, $start, $end); + $this->shift($end, $incompleteItemLength - $completeLength); + } + } + + /** + * Replaces sanitized object class names in serialized data. + * + * @param string $replacement Sanitized object data + * @param int $start Start offset in serialized data + * @param int $end End offset in serialized data + */ + private function replace($replacement, $start, $end) + { + $this->serialized = substr($this->serialized, 0, $start) + . $replacement . substr($this->serialized, $end); + } + + /** + * Whether given offset positions should be ignored. + * + * @param int $start + * @param int $end + * @return bool + */ + private function ignore($start, $end) + { + foreach ($this->ignoreItems as $ignoreItem) { + if ($ignoreItem[0] <= $start && $ignoreItem[1] >= $end) { + return true; + } + } + + return false; + } + + /** + * Shifts offset positions of ignore items by `$size`. + * This is necessary whenever object class names have been + * substituted which have a different length than before. + * + * @param int $offset + * @param int $size + */ + private function shift($offset, $size) + { + foreach ($this->ignoreItems as &$ignoreItem) { + // only focus on items starting after given offset + if ($ignoreItem[0] < $offset) { + continue; + } + $ignoreItem[0] += $size; + $ignoreItem[1] += $size; + } + } + + /** + * Sanitizes object class item. + * + * @param string $className + * @param int $leftBorder + * @param int $objectSize + * @return string + */ + private function sanitizeItem($className, $leftBorder, $objectSize) + { + return sprintf( + '%sO:22:"__PHP_Incomplete_Class":%d:{s:27:"__PHP_Incomplete_Class_Name";%s', + $leftBorder, + $objectSize + 1, // size of object + 1 for added string + \serialize($className) + ); + } +} diff --git a/vendor/brumann/polyfill-unserialize/src/Unserialize.php b/vendor/brumann/polyfill-unserialize/src/Unserialize.php new file mode 100644 index 0000000..e7ad9be --- /dev/null +++ b/vendor/brumann/polyfill-unserialize/src/Unserialize.php @@ -0,0 +1,39 @@ += 70000) { + return \unserialize($serialized, $options); + } + if (!array_key_exists('allowed_classes', $options) || true === $options['allowed_classes']) { + return \unserialize($serialized); + } + $allowedClasses = $options['allowed_classes']; + if (false === $allowedClasses) { + $allowedClasses = array(); + } + if (!is_array($allowedClasses)) { + $allowedClasses = array(); + trigger_error( + 'unserialize(): allowed_classes option should be array or boolean', + E_USER_WARNING + ); + } + + $worker = new DisallowedClassesSubstitutor($serialized, $allowedClasses); + + return \unserialize($worker->getSubstitutedSerialized()); + } +}