diff options
33 files changed, 544 insertions, 166 deletions
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 459e6d6f75e3..356a0e8c6e59 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -142,6 +142,7 @@ class AutoLoader { 'MediaWiki\\Edit\\' => __DIR__ . '/edit/', 'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/', 'MediaWiki\\FileBackend\\LockManager\\' => __DIR__ . '/filebackend/lockmanager/', + 'MediaWiki\\Json\\' => __DIR__ . '/json/', 'MediaWiki\\Http\\' => __DIR__ . '/http/', 'MediaWiki\\Installer\\' => __DIR__ . '/installer/', 'MediaWiki\\Interwiki\\' => __DIR__ . '/interwiki/', diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 663fc8e4cc43..9b946ff153e0 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -44,6 +44,7 @@ use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Http\HttpRequestFactory; use MediaWiki\Interwiki\InterwikiLookup; +use MediaWiki\Json\JsonUnserializer; use MediaWiki\Languages\LanguageConverterFactory; use MediaWiki\Languages\LanguageFactory; use MediaWiki\Languages\LanguageFallback; @@ -813,6 +814,14 @@ class MediaWikiServices extends ServiceContainer { } /** + * @since 1.36 + * @return JsonUnserializer + */ + public function getJsonUnserializer() : JsonUnserializer { + return $this->getService( 'JsonUnserializer' ); + } + + /** * @since 1.35 * @return LanguageConverterFactory */ diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index a201729fe1dc..f80d3850e493 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -72,6 +72,7 @@ use MediaWiki\HookContainer\HookRunner; use MediaWiki\Http\HttpRequestFactory; use MediaWiki\Interwiki\ClassicInterwikiLookup; use MediaWiki\Interwiki\InterwikiLookup; +use MediaWiki\Json\JsonUnserializer; use MediaWiki\Languages\LanguageConverterFactory; use MediaWiki\Languages\LanguageFactory; use MediaWiki\Languages\LanguageFallback; @@ -528,6 +529,10 @@ return [ ); }, + 'JsonUnserializer' => function ( MediaWikiServices $services ) : JsonUnserializer { + return new JsonUnserializer(); + }, + 'LanguageConverterFactory' => function ( MediaWikiServices $services ) : LanguageConverterFactory { $usePigLatinVariant = $services->getMainConfig()->get( 'UsePigLatinVariant' ); return new LanguageConverterFactory( $usePigLatinVariant, function () use ( $services ) { @@ -902,6 +907,7 @@ return [ $cache, $config->get( 'CacheEpoch' ), $services->getHookContainer(), + $services->getJsonUnserializer(), $services->getStatsdDataFactory(), LoggerFactory::getInstance( 'ParserCache' ), $config->get( 'ParserCacheUseJson' ) diff --git a/includes/json/FormatJson.php b/includes/json/FormatJson.php index 4ee709fe9949..409a1cc032c9 100644 --- a/includes/json/FormatJson.php +++ b/includes/json/FormatJson.php @@ -20,6 +20,8 @@ * @file */ +use MediaWiki\Json\JsonUnserializable; + /** * JSON formatter wrapper class */ @@ -328,11 +330,13 @@ class FormatJson { * * * @param mixed $value + * @param bool $expectUnserialize * @param string $accumulatedPath * @return string|null JSON path to first encountered non-serializable property or null. */ private static function detectNonSerializableDataInternal( $value, + bool $expectUnserialize, string $accumulatedPath ): ?string { if ( is_array( $value ) || @@ -340,13 +344,19 @@ class FormatJson { foreach ( $value as $key => $propValue ) { $propValueNonSerializablePath = self::detectNonSerializableDataInternal( $propValue, + $expectUnserialize, $accumulatedPath . '.' . $key ); if ( $propValueNonSerializablePath ) { return $propValueNonSerializablePath; } } - // Instances of classes other the \stdClass can not be serialized to JSON + } elseif ( ( $expectUnserialize && $value instanceof JsonUnserializable ) + // Trust that JsonSerializable will correctly serialize. + || ( !$expectUnserialize && $value instanceof JsonSerializable ) + ) { + return null; + // Instances of classes other the \stdClass or JsonSerializable can not be serialized to JSON. } elseif ( !is_scalar( $value ) && $value !== null ) { return $accumulatedPath; } @@ -357,11 +367,13 @@ class FormatJson { * Checks if the $value is JSON-serializable (contains only scalar values) * and returns a JSON-path to the first non-serializable property encountered. * - * @since 1.36 * @param mixed $value + * @param bool $expectUnserialize whether to expect the $value to be unserializable with JsonUnserializer. * @return string|null JSON path to first encountered non-serializable property or null. + * @see \MediaWiki\Json\JsonUnserializer + * @since 1.36 */ - public static function detectNonSerializableData( $value ): ?string { - return self::detectNonSerializableDataInternal( $value, '$' ); + public static function detectNonSerializableData( $value, bool $expectUnserialize = false ): ?string { + return self::detectNonSerializableDataInternal( $value, $expectUnserialize, '$' ); } } diff --git a/includes/json/JsonUnserializable.php b/includes/json/JsonUnserializable.php new file mode 100644 index 000000000000..cdfe2ba19799 --- /dev/null +++ b/includes/json/JsonUnserializable.php @@ -0,0 +1,48 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Json + */ + +namespace MediaWiki\Json; + +use JsonSerializable; + +/** + * Classes implementing this interface support round-trip JSON serialization/unserialization + * using the JsonUnserializer utility. + * + * The resulting JSON must be annotated with class information for unserialization to work. + * Use JsonUnserializableTrait in implementing classes which annotates the JSON automatically. + * + * @see JsonUnserializer + * @see JsonUnserializableTrait + * @since 1.36 + * @package MediaWiki\Json + */ +interface JsonUnserializable extends JsonSerializable { + + /** + * Creates a new instance of the class and initialized it from the $json array. + * @param JsonUnserializer $unserializer an instance of JsonUnserializer to use + * for nested properties if they need special care. + * @param array $json + * @return JsonUnserializable + */ + public static function newFromJsonArray( JsonUnserializer $unserializer, array $json ); +} diff --git a/includes/json/JsonUnserializableTrait.php b/includes/json/JsonUnserializableTrait.php new file mode 100644 index 000000000000..8f757bbfe1c3 --- /dev/null +++ b/includes/json/JsonUnserializableTrait.php @@ -0,0 +1,50 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Json + */ + +namespace MediaWiki\Json; + +trait JsonUnserializableTrait { + + public function jsonSerialize() { + return $this->annotateJsonForDeserialization( + $this->toJsonArray() + ); + } + + /** + * Annotate the $json array with class metadata. + * + * @param array $json + * @return array + */ + private function annotateJsonForDeserialization( array $json ) : array { + $json[JsonUnserializer::TYPE_ANNOTATION] = get_class( $this ); + return $json; + } + + /** + * Prepare this object for JSON serialization. + * The returned array will be passed to self::newFromJsonArray + * upon JSON deserialization. + * @return array + */ + abstract protected function toJsonArray(): array; +} diff --git a/includes/json/JsonUnserializer.php b/includes/json/JsonUnserializer.php new file mode 100644 index 000000000000..c47f4aa78215 --- /dev/null +++ b/includes/json/JsonUnserializer.php @@ -0,0 +1,107 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Json + */ + +namespace MediaWiki\Json; + +use FormatJson; +use InvalidArgumentException; + +/** + * Helper class to unserialize instances of JsonUnserializable. + * + * @package MediaWiki\Json + */ +class JsonUnserializer { + + /** + * Name of the property where the class information is stored. + * @internal + */ + public const TYPE_ANNOTATION = '_type_'; + + /** + * Restore an instance of JsonUnserializable subclass from the JSON serialization. + * + * @param array|string|object $json + * @param string|null $expectedClass What class to expect in unserialization. If null, no expectation. + * @throws InvalidArgumentException if the passed $json can't be unserialized. + * @return JsonUnserializable + */ + public function unserialize( $json, string $expectedClass = null ) : JsonUnserializable { + if ( is_string( $json ) ) { + $json = FormatJson::decode( $json, true ); + if ( !$json ) { + // TODO: in PHP 7.3, we can use JsonException + throw new InvalidArgumentException( 'Bad JSON' ); + } + } + + if ( is_object( $json ) ) { + $json = (array)$json; + } + + if ( !$this->canMakeNewFromValue( $json ) ) { + throw new InvalidArgumentException( 'JSON did not have ' . self::TYPE_ANNOTATION ); + } + + $class = $json[self::TYPE_ANNOTATION]; + if ( !class_exists( $class ) || !is_subclass_of( $class, JsonUnserializable::class ) ) { + throw new InvalidArgumentException( "Target class {$class} does not exist" ); + } + + $obj = $class::newFromJsonArray( $this, $json ); + + // Check we haven't accidentally unserialized a godzilla if we were told we are not expecting it. + if ( $expectedClass && !is_a( $obj, $expectedClass ) ) { + $actualClass = get_class( $obj ); + throw new InvalidArgumentException( "Expected {$expectedClass}, got {$actualClass}" ); + } + return $obj; + } + + /** + * Helper to unserialize an array of JsonUnserializable instances or scalars. + * @param array $array + * @return array + */ + public function unserializeArray( array $array ) : array { + $unserializedExtensionData = []; + foreach ( $array as $key => $value ) { + if ( $this->canMakeNewFromValue( $value ) ) { + $unserializedExtensionData[$key] = $this->unserialize( $value ); + } else { + $unserializedExtensionData[$key] = $value; + } + } + return $unserializedExtensionData; + } + + /** + * Is it likely possible to make a new instance from $json serialization? + * @param mixed $json + * @return bool + */ + private function canMakeNewFromValue( $json ) : bool { + $classAnnotation = self::TYPE_ANNOTATION; + return ( is_array( $json ) && array_key_exists( $classAnnotation, $json ) ) || + ( is_object( $json ) && isset( $json->$classAnnotation ) ); + } +} diff --git a/includes/parser/CacheTime.php b/includes/parser/CacheTime.php index d6b2e15054cd..43c0beacaaa6 100644 --- a/includes/parser/CacheTime.php +++ b/includes/parser/CacheTime.php @@ -21,6 +21,9 @@ * @ingroup Parser */ +use MediaWiki\Json\JsonUnserializable; +use MediaWiki\Json\JsonUnserializableTrait; +use MediaWiki\Json\JsonUnserializer; use MediaWiki\Parser\ParserCacheMetadata; use Wikimedia\Reflection\GhostFieldAccessTrait; @@ -29,8 +32,9 @@ use Wikimedia\Reflection\GhostFieldAccessTrait; * * @ingroup Parser */ -class CacheTime implements ParserCacheMetadata, JsonSerializable { +class CacheTime implements ParserCacheMetadata, JsonUnserializable { use GhostFieldAccessTrait; + use JsonUnserializableTrait; /** * @var string[] ParserOptions which have been taken into account to produce output. @@ -216,14 +220,11 @@ class CacheTime implements ParserCacheMetadata, JsonSerializable { /** * Returns a JSON serializable structure representing this CacheTime instance. * @see newFromJson() - * @since 1.36 * * @return array */ - public function jsonSerialize() { + protected function toJsonArray(): array { return [ - '_type_' => 'CacheTime', - 'UsedOptions' => $this->mUsedOptions, 'CacheExpiry' => $this->mCacheExpiry, 'CacheTime' => $this->mCacheTime, @@ -232,40 +233,18 @@ class CacheTime implements ParserCacheMetadata, JsonSerializable { ]; } - /** - * Construct a CacheTime instance from a structure returned by jsonSerialize() - * - * @see jsonSerialize() - * @since 1.36 - * - * @param string|array|object $jsonData - * @return CacheTime - * @throws InvalidArgumentException - */ - public static function newFromJson( $jsonData ) { - if ( is_string( $jsonData ) ) { - $jsonData = FormatJson::decode( $jsonData, true ); - if ( !$jsonData ) { - // TODO: in PHP 7.3, we can use JsonException - throw new InvalidArgumentException( 'Bad JSON' ); - } - } - - if ( is_object( $jsonData ) ) { - $jsonData = (array)$jsonData; - } - + public static function newFromJsonArray( JsonUnserializer $unserializer, array $json ) { $cacheTime = new CacheTime(); - $cacheTime->initFromJson( $jsonData ); - + $cacheTime->initFromJson( $unserializer, $json ); return $cacheTime; } /** * Initialize member fields from an array returned by jsonSerialize(). + * @param JsonUnserializer $unserializer * @param array $jsonData */ - protected function initFromJson( array $jsonData ) { + protected function initFromJson( JsonUnserializer $unserializer, array $jsonData ) { if ( array_key_exists( 'ParseUsedOptions', $jsonData ) ) { // Forward compatibility $this->mUsedOptions = array_keys( $jsonData['ParseUsedOptions'] ?: [] ); diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php index 699b3f95bd18..ab04497e3f4c 100644 --- a/includes/parser/ParserCache.php +++ b/includes/parser/ParserCache.php @@ -23,9 +23,9 @@ use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; +use MediaWiki\Json\JsonUnserializer; use MediaWiki\Parser\ParserCacheMetadata; use Psr\Log\LoggerInterface; -use Wikimedia\Assert\Assert; /** * Cache for ParserOutput objects corresponding to the latest page revisions. @@ -96,6 +96,9 @@ class ParserCache { /** @var HookRunner */ private $hookRunner; + /** @var JsonUnserializer */ + private $jsonUnserializer; + /** @var IBufferingStatsdDataFactory */ private $stats; @@ -124,6 +127,7 @@ class ParserCache { * @param BagOStuff $cache * @param string $cacheEpoch Anything before this timestamp is invalidated * @param HookContainer $hookContainer + * @param JsonUnserializer $jsonUnserializer * @param IBufferingStatsdDataFactory $stats * @param LoggerInterface $logger * @param bool $useJson Temporary feature flag, remove before 1.36 is released. @@ -133,6 +137,7 @@ class ParserCache { BagOStuff $cache, string $cacheEpoch, HookContainer $hookContainer, + JsonUnserializer $jsonUnserializer, IBufferingStatsdDataFactory $stats, LoggerInterface $logger, $useJson = false @@ -141,6 +146,7 @@ class ParserCache { $this->cache = $cache; $this->cacheEpoch = $cacheEpoch; $this->hookRunner = new HookRunner( $hookContainer ); + $this->jsonUnserializer = $jsonUnserializer; $this->stats = $stats; $this->logger = $logger; $this->readJson = $useJson; @@ -276,7 +282,7 @@ class ParserCache { // deployed a while before starting to write JSON to the cache, // in case we have to revert either change. if ( is_string( $metadata ) && $this->readJson ) { - $metadata = $this->restoreFromJson( $metadata, $pageKey ); + $metadata = $this->restoreFromJson( $metadata, $pageKey, CacheTime::class ); } if ( $metadata instanceof CacheTime ) { @@ -417,7 +423,7 @@ class ParserCache { // deployed a while before starting to write JSON to the cache, // in case we have to revert either change. if ( is_string( $value ) && $this->readJson ) { - $value = $this->restoreFromJson( $value, $parserOutputKey ); + $value = $this->restoreFromJson( $value, $parserOutputKey, ParserOutput::class ); } if ( !$value instanceof ParserOutput ) { @@ -585,50 +591,21 @@ class ParserCache { /** * @param string $jsonData * @param string $key - * - * @return CacheTime|null + * @param string $expectedClass + * @return CacheTime|ParserOutput|null */ - private function restoreFromJson( string $jsonData, string $key ) { - $jsonData = FormatJson::decode( $jsonData, true ); - if ( !$jsonData ) { - $this->logger->error( - "Invalid JSON", - [ 'cache_key' => $key, 'json_error' => json_last_error(), ] ); - return null; - } - - if ( !isset( $jsonData['_type_'] ) ) { - $this->logger->error( "No _type_ field in JSON data", - [ 'cache_key' => $key ] - ); - return null; - } - - // NOTE: Allowing the factory method to be specified directly in the data is tempting, - // but would be insecure. Information in $jsonData must be considered unsafe, - // since an attacker gaining access to the network could write to e.g. memcache. - $type = $jsonData['_type_']; - $factoryMapping = [ - 'CacheTime' => [ CacheTime::class, 'newFromJson' ], - 'ParserOutput' => [ ParserOutput::class, 'newFromJson' ], - ]; - - if ( !isset( $factoryMapping[$type] ) ) { - $this->logger->error( "Unknown value in _type_ field in JSON data", - [ 'cache_key' => $key, 'json_type' => $type ] - ); + private function restoreFromJson( string $jsonData, string $key, string $expectedClass ) { + try { + /** @var CacheTime $obj */ + $obj = $this->jsonUnserializer->unserialize( $jsonData, $expectedClass ); + return $obj; + } catch ( InvalidArgumentException $e ) { + $this->logger->error( "Unable to unserialize JSON", [ + 'cache_key' => $key, + 'message' => $e->getMessage() + ] ); return null; } - - $factory = $factoryMapping[$type]; - $obj = $factory( $jsonData ); - - Assert::postcondition( - $obj instanceof CacheTime, - 'Factory method must return a CacheTime instance' - ); - - return $obj; } /** @@ -639,7 +616,6 @@ class ParserCache { */ private function encodeAsJson( CacheTime $obj, string $key ) { $data = $obj->jsonSerialize(); - $json = FormatJson::encode( $data, false, FormatJson::ALL_OK ); if ( !$json ) { $this->logger->error( "JSON encoding failed", [ @@ -654,7 +630,7 @@ class ParserCache { // to json. We will not be able to deserialize the value correctly // anyway, so return null. This is done after calling FormatJson::encode // to avoid walking over circular structures. - $unserializablePath = FormatJson::detectNonSerializableData( $data ); + $unserializablePath = FormatJson::detectNonSerializableData( $data, true ); if ( $unserializablePath ) { $this->logger->error( 'Non-serializable {class} property set', [ 'class' => get_class( $obj ), diff --git a/includes/parser/ParserCacheFactory.php b/includes/parser/ParserCacheFactory.php index 12703d933be0..427e3ba80dc5 100644 --- a/includes/parser/ParserCacheFactory.php +++ b/includes/parser/ParserCacheFactory.php @@ -24,6 +24,7 @@ namespace MediaWiki\Parser; use BagOStuff; use IBufferingStatsdDataFactory; use MediaWiki\HookContainer\HookContainer; +use MediaWiki\Json\JsonUnserializer; use ParserCache; use Psr\Log\LoggerInterface; @@ -46,6 +47,9 @@ class ParserCacheFactory { /** @var HookContainer */ private $hookContainer; + /** @var JsonUnserializer */ + private $jsonUnserializer; + /** @var IBufferingStatsdDataFactory */ private $stats; @@ -65,6 +69,7 @@ class ParserCacheFactory { * @param BagOStuff $cacheBackend * @param string $cacheEpoch * @param HookContainer $hookContainer + * @param JsonUnserializer $jsonUnserializer * @param IBufferingStatsdDataFactory $stats * @param LoggerInterface $logger * @param bool $useJson Temporary feature flag, remove before 1.36 is released. @@ -73,6 +78,7 @@ class ParserCacheFactory { BagOStuff $cacheBackend, string $cacheEpoch, HookContainer $hookContainer, + JsonUnserializer $jsonUnserializer, IBufferingStatsdDataFactory $stats, LoggerInterface $logger, $useJson = false @@ -80,6 +86,7 @@ class ParserCacheFactory { $this->cacheBackend = $cacheBackend; $this->cacheEpoch = $cacheEpoch; $this->hookContainer = $hookContainer; + $this->jsonUnserializer = $jsonUnserializer; $this->stats = $stats; $this->logger = $logger; $this->useJson = $useJson; @@ -98,6 +105,7 @@ class ParserCacheFactory { $this->cacheBackend, $this->cacheEpoch, $this->hookContainer, + $this->jsonUnserializer, $this->stats, $this->logger, $this->useJson diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index 296336df8925..f019c9504ccf 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -1,5 +1,8 @@ <?php +use MediaWiki\Json\JsonUnserializable; +use MediaWiki\Json\JsonUnserializableTrait; +use MediaWiki\Json\JsonUnserializer; use MediaWiki\Logger\LoggerFactory; use Wikimedia\Reflection\GhostFieldAccessTrait; @@ -27,6 +30,7 @@ use Wikimedia\Reflection\GhostFieldAccessTrait; class ParserOutput extends CacheTime { use GhostFieldAccessTrait; + use JsonUnserializableTrait; /** * Feature flags to indicate to extensions that MediaWiki core supports and @@ -1239,17 +1243,16 @@ class ParserOutput extends CacheTime { * $parser->getOutput()->my_ext_foo = '...'; * @endcode * - * @note Only scalar values, e.g. numbers, strings, arrays are supported - * as a value. Attempt to set a class instance as a extension data will - * break ParserCache for the page. - * - * @since 1.21 + * @note Only scalar values, e.g. numbers, strings, arrays or MediaWiki\Json\JsonUnserializable + * instances are supported as a value. Attempt to set other class instance as a extension data + * will break ParserCache for the page. * * @param string $key The key for accessing the data. Extensions should take care to avoid * conflicts in naming keys. It is suggested to use the extension's name as a prefix. * - * @param mixed $value The value to set. Setting a value to null is equivalent to removing - * the value. + * @param mixed|JsonUnserializable $value The value to set. + * Setting a value to null is equivalent to removing the value. + * @since 1.21 */ public function setExtensionData( $key, $value ) { if ( $value === null ) { @@ -1687,14 +1690,11 @@ class ParserOutput extends CacheTime { /** * Returns a JSON serializable structure representing this ParserOutput instance. * @see newFromJson() - * @since 1.36 * * @return array */ - public function jsonSerialize() { + protected function toJsonArray(): array { $data = [ - '_type_' => 'ParserOutput', - 'Text' => $this->mText, 'LanguageLinks' => $this->mLanguageLinks, 'Categories' => $this->mCategories, @@ -1724,7 +1724,8 @@ class ParserOutput extends CacheTime { 'EnableOOUI' => $this->mEnableOOUI, 'IndexPolicy' => $this->mIndexPolicy, 'AccessedOptions' => $this->mAccessedOptions, - 'ExtensionData' => $this->mExtensionData, // may contain arbitrary structures! + // may contain arbitrary structures! + 'ExtensionData' => $this->mExtensionData, 'LimitReportData' => $this->mLimitReportData, 'LimitReportJSData' => $this->mLimitReportJSData, 'ParseStartTime' => $this->mParseStartTime, @@ -1741,7 +1742,7 @@ class ParserOutput extends CacheTime { ]; // Fill in missing fields from parents. Array addition does not override existing fields. - $data += parent::jsonSerialize(); + $data += parent::toJsonArray(); // TODO: make more fields optional! @@ -1753,41 +1754,19 @@ class ParserOutput extends CacheTime { return $data; } - /** - * Construct a ParserOutput instance from a structure returned by jsonSerialize() - * - * @see jsonSerialize() - * @since 1.36 - * - * @param string|array|object $jsonData - * @return CacheTime - * @throws InvalidArgumentException - */ - public static function newFromJson( $jsonData ) { - if ( is_string( $jsonData ) ) { - $jsonData = FormatJson::decode( $jsonData, true ); - if ( !$jsonData ) { - // TODO: in PHP 7.3, we can use JsonException - throw new InvalidArgumentException( 'Bad JSON' ); - } - } - - if ( is_object( $jsonData ) ) { - $jsonData = (array)$jsonData; - } - - $output = new ParserOutput(); - $output->initFromJson( $jsonData ); - - return $output; + public static function newFromJsonArray( JsonUnserializer $unserializer, array $json ) { + $parserOutput = new ParserOutput(); + $parserOutput->initFromJson( $unserializer, $json ); + return $parserOutput; } /** * Initialize member fields from an array returned by jsonSerialize(). + * @param JsonUnserializer $unserializer * @param array $jsonData */ - protected function initFromJson( array $jsonData ) { - parent::initFromJson( $jsonData ); + protected function initFromJson( JsonUnserializer $unserializer, array $jsonData ) { + parent::initFromJson( $unserializer, $jsonData ); $this->mUsedOptions = null; $this->mText = $jsonData['Text']; @@ -1824,7 +1803,7 @@ class ParserOutput extends CacheTime { } else { $this->mAccessedOptions = $jsonData['AccessedOptions']; } - $this->mExtensionData = $jsonData['ExtensionData']; + $this->mExtensionData = $unserializer->unserializeArray( $jsonData['ExtensionData'] ?? [] ); $this->mLimitReportData = $jsonData['LimitReportData']; $this->mLimitReportJSData = $jsonData['LimitReportJSData']; $this->mParseStartTime = $jsonData['ParseStartTime']; diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index f776c7fa96fb..4dc21850c71a 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -231,6 +231,10 @@ $wgAutoloadClasses += [ # tests/phpunit/unit/includes/filebackend 'FileBackendGroupTestTrait' => "$testDir/phpunit/unit/includes/filebackend/FileBackendGroupTestTrait.php", + # tests/phpunit/unit/includes/json + 'MediaWiki\\Tests\\Json\\JsonUnserializableSuperClass' => "$testDir/phpunit/mocks/json/JsonUnserializableSuperClass.php", + 'MediaWiki\\Tests\\Json\\JsonUnserializableSubClass' => "$testDir/phpunit/mocks/json/JsonUnserializableSubClass.php", + # tests/phpunit/unit/includes/language 'LanguageFallbackTestTrait' => "$testDir/phpunit/unit/includes/language/LanguageFallbackTestTrait.php", 'LanguageNameUtilsTestTrait' => "$testDir/phpunit/unit/includes/language/LanguageNameUtilsTestTrait.php", diff --git a/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheExpiry.json b/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheExpiry.json index 9df33193f309..1f10ad3613f8 100644 --- a/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheExpiry.json +++ b/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheExpiry.json @@ -1 +1 @@ -{"_type_":"CacheTime","UsedOptions":null,"CacheExpiry":10,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"UsedOptions":null,"CacheExpiry":10,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"CacheTime"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheRevisionId.json b/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheRevisionId.json index 74c08ba650d2..35e560eb569d 100644 --- a/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheRevisionId.json +++ b/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheRevisionId.json @@ -1 +1 @@ -{"_type_":"CacheTime","UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":1234,"Version":"1.6.4"}
\ No newline at end of file +{"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":1234,"Version":"1.6.4","_type_":"CacheTime"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheTime.json b/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheTime.json index 0dd3ae76f3e6..7703169c9c95 100644 --- a/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheTime.json +++ b/tests/phpunit/data/ParserCache/1.36-CacheTime-cacheTime.json @@ -1 +1 @@ -{"_type_":"CacheTime","UsedOptions":null,"CacheExpiry":null,"CacheTime":"20010419042521","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"UsedOptions":null,"CacheExpiry":null,"CacheTime":"20010419042521","CacheRevisionId":null,"Version":"1.6.4","_type_":"CacheTime"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-CacheTime-empty.json b/tests/phpunit/data/ParserCache/1.36-CacheTime-empty.json index a5b4f5e3f2ca..b762bdd09ae1 100644 --- a/tests/phpunit/data/ParserCache/1.36-CacheTime-empty.json +++ b/tests/phpunit/data/ParserCache/1.36-CacheTime-empty.json @@ -1 +1 @@ -{"_type_":"CacheTime","UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"CacheTime"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-CacheTime-usedOptions.json b/tests/phpunit/data/ParserCache/1.36-CacheTime-usedOptions.json index 54484b948f69..67780aa147a6 100644 --- a/tests/phpunit/data/ParserCache/1.36-CacheTime-usedOptions.json +++ b/tests/phpunit/data/ParserCache/1.36-CacheTime-usedOptions.json @@ -1 +1 @@ -{"_type_":"CacheTime","UsedOptions":["optA","optX"],"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"UsedOptions":["optA","optX"],"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"CacheTime"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-ParserOutput-binaryPageProperties.json b/tests/phpunit/data/ParserCache/1.36-ParserOutput-binaryPageProperties.json index 829d5ab7fb6b..a1db5a0f8c54 100644 --- a/tests/phpunit/data/ParserCache/1.36-ParserOutput-binaryPageProperties.json +++ b/tests/phpunit/data/ParserCache/1.36-ParserOutput-binaryPageProperties.json @@ -1 +1 @@ -{"_type_":"ParserOutput","Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":{"empty":"","\\x00":"\u0000","gzip":{"_type_":"string","_encoding_":"base64","_data_":"H4sIAAAAAAAAA8tIzcnJVyjPLycKCQkJLiAnykkBAIURSg0LAAAA"}},"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":{"empty":"","\\x00":"\u0000","gzip":{"_type_":"string","_encoding_":"base64","_data_":"H4sIAAAAAAAAA8tIzcnJVyjPLycKCQkJLiAnykkBAIURSg0LAAAA"}},"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"ParserOutput"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-ParserOutput-empty.json b/tests/phpunit/data/ParserCache/1.36-ParserOutput-empty.json index 5f0dfc56d8fa..d106af048996 100644 --- a/tests/phpunit/data/ParserCache/1.36-ParserOutput-empty.json +++ b/tests/phpunit/data/ParserCache/1.36-ParserOutput-empty.json @@ -1 +1 @@ -{"_type_":"ParserOutput","Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"ParserOutput"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-ParserOutput-extensionData.json b/tests/phpunit/data/ParserCache/1.36-ParserOutput-extensionData.json index 6d16d68a771d..51605f914bf1 100644 --- a/tests/phpunit/data/ParserCache/1.36-ParserOutput-extensionData.json +++ b/tests/phpunit/data/ParserCache/1.36-ParserOutput-extensionData.json @@ -1 +1 @@ -{"_type_":"ParserOutput","Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":{"boolean":true,"number":42,"string":"string","array":[1,2,3],"map":{"key":"value"}},"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":{"boolean":true,"number":42,"string":"string","array":[1,2,3],"map":{"key":"value"}},"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"ParserOutput"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-ParserOutput-pageProperties.json b/tests/phpunit/data/ParserCache/1.36-ParserOutput-pageProperties.json index 5eff9598e931..bde0f3cfbba3 100644 --- a/tests/phpunit/data/ParserCache/1.36-ParserOutput-pageProperties.json +++ b/tests/phpunit/data/ParserCache/1.36-ParserOutput-pageProperties.json @@ -1 +1 @@ -{"_type_":"ParserOutput","Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":{"boolean":true,"null":null,"number":42,"string":"string","array":[1,2,3],"map":{"key":"value"}},"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":{"boolean":true,"null":null,"number":42,"string":"string","array":[1,2,3],"map":{"key":"value"}},"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"ParserOutput"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-ParserOutput-text.json b/tests/phpunit/data/ParserCache/1.36-ParserOutput-text.json index a07f2fb32be0..b20f81113cbc 100644 --- a/tests/phpunit/data/ParserCache/1.36-ParserOutput-text.json +++ b/tests/phpunit/data/ParserCache/1.36-ParserOutput-text.json @@ -1 +1 @@ -{"_type_":"ParserOutput","Text":"Lorem Ipsum","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"Text":"Lorem Ipsum","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"ParserOutput"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-ParserOutput-usedOptions.json b/tests/phpunit/data/ParserCache/1.36-ParserOutput-usedOptions.json index 5fb742723cea..fbc0476f93bb 100644 --- a/tests/phpunit/data/ParserCache/1.36-ParserOutput-usedOptions.json +++ b/tests/phpunit/data/ParserCache/1.36-ParserOutput-usedOptions.json @@ -1 +1 @@ -{"_type_":"ParserOutput","Text":"Dummy","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":{"optA":true,"optX":true},"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"Text":"Dummy","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":{"optA":true,"optX":true},"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"ParserOutput"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadata.json b/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadata.json index 171ffa868d8d..22af27d2caff 100644 --- a/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadata.json +++ b/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadata.json @@ -1 +1 @@ -{"_type_":"ParserOutput","Text":"","LanguageLinks":["link1","link2"],"Categories":{"category2":1,"category1":2},"Indicators":{"indicator1":"indicator1_value"},"TitleText":"title_text1","Links":{"0":{"Link1":42},"2":{"Link2":43}},"LinksSpecial":[],"Templates":{"10":{"Template1":42}},"TemplateIds":{"10":{"Template1":4242}},"Images":{"Image1":1},"FileSearchOptions":{"Image1":{"time":"19731129213309","sha1":"test_sha1"}},"ExternalLinks":{"https:\/\/test.org":1},"InterwikiLinks":{"enwiki":{"interwiki1":1,"interwiki2":1}},"NewSection":true,"HideNewSection":true,"NoGallery":false,"HeadItems":{"tag1":"head_item1"},"Modules":["module1"],"ModuleStyles":["module_style1"],"JsConfigVars":{"key1":"value1"},"OutputHooks":[["hook1",{"boolean":true,"null":null,"number":42,"string":"string","array":[1,2,3],"map":{"key":"value"}}]],"Warnings":{"warning1":1},"Sections":["section1","section2"],"Properties":[],"TOCHTML":"tochtml1","Timestamp":"20010419042521","EnableOOUI":true,"IndexPolicy":"policy1","AccessedOptions":[],"ExtensionData":[],"LimitReportData":{"limit_report_key1":"value1"},"LimitReportJSData":{"limit_report_key1":"value1"},"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":{"test":true},"SpeculativeRevId":42,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"Text":"","LanguageLinks":["link1","link2"],"Categories":{"category2":1,"category1":2},"Indicators":{"indicator1":"indicator1_value"},"TitleText":"title_text1","Links":{"0":{"Link1":42},"2":{"Link2":43}},"LinksSpecial":[],"Templates":{"10":{"Template1":42}},"TemplateIds":{"10":{"Template1":4242}},"Images":{"Image1":1},"FileSearchOptions":{"Image1":{"time":"19731129213309","sha1":"test_sha1"}},"ExternalLinks":{"https:\/\/test.org":1},"InterwikiLinks":{"enwiki":{"interwiki1":1,"interwiki2":1}},"NewSection":true,"HideNewSection":true,"NoGallery":false,"HeadItems":{"tag1":"head_item1"},"Modules":["module1"],"ModuleStyles":["module_style1"],"JsConfigVars":{"key1":"value1"},"OutputHooks":[["hook1",{"boolean":true,"null":null,"number":42,"string":"string","array":[1,2,3],"map":{"key":"value"}}]],"Warnings":{"warning1":1},"Sections":["section1","section2"],"Properties":[],"TOCHTML":"tochtml1","Timestamp":"20010419042521","EnableOOUI":true,"IndexPolicy":"policy1","AccessedOptions":[],"ExtensionData":[],"LimitReportData":{"limit_report_key1":"value1"},"LimitReportJSData":{"limit_report_key1":"value1"},"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":{"test":true},"SpeculativeRevId":42,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"ParserOutput"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadataPost1_31.json b/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadataPost1_31.json index 456d987a9254..f745c92e24a7 100644 --- a/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadataPost1_31.json +++ b/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadataPost1_31.json @@ -1 +1 @@ -{"_type_":"ParserOutput","Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":true,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":4242,"RevisionTimestampUsed":"19731129213309","RevisionUsedSha1Base36":"test_hash","WrapperDivClasses":{"test_wrapper":true},"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":true,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":4242,"RevisionTimestampUsed":"19731129213309","RevisionUsedSha1Base36":"test_hash","WrapperDivClasses":{"test_wrapper":true},"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"ParserOutput"}
\ No newline at end of file diff --git a/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadataPost1_34.json b/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadataPost1_34.json index 3747662e408a..5805e09a902f 100644 --- a/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadataPost1_34.json +++ b/tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadataPost1_34.json @@ -1 +1 @@ -{"_type_":"ParserOutput","Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":{"Link3":1},"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":["script1"],"ExtraDefaultSrcs":["default1"],"ExtraStyleSrcs":["style1"],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}
\ No newline at end of file +{"Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":{"Link3":1},"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","AccessedOptions":[],"ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":["script1"],"ExtraDefaultSrcs":["default1"],"ExtraStyleSrcs":["style1"],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"UsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4","_type_":"ParserOutput"}
\ No newline at end of file diff --git a/tests/phpunit/includes/page/ParserOutputAccessTest.php b/tests/phpunit/includes/page/ParserOutputAccessTest.php index cad169151dae..b24cc507ae25 100644 --- a/tests/phpunit/includes/page/ParserOutputAccessTest.php +++ b/tests/phpunit/includes/page/ParserOutputAccessTest.php @@ -1,5 +1,6 @@ <?php +use MediaWiki\Json\JsonUnserializer; use MediaWiki\Page\ParserOutputAccess; use MediaWiki\Revision\MutableRevisionRecord; use MediaWiki\Revision\RevisionRecord; @@ -58,15 +59,15 @@ class ParserOutputAccessTest extends MediaWikiIntegrationTestCase { } private function getParserCache( $bag = null ) { - $hookContainer = $this->getServiceContainer()->getHookContainer(); - $stats = $this->getServiceContainer()->getStatsdDataFactory(); - $logger = new NullLogger(); - - if ( !$bag ) { - $bag = new HashBagOStuff(); - } - - $parserCache = new ParserCache( 'test', $bag, '', $hookContainer, $stats, $logger ); + $parserCache = new ParserCache( + 'test', + $bag ?: new HashBagOStuff(), + '', + $this->getServiceContainer()->getHookContainer(), + new JsonUnserializer(), + $this->getServiceContainer()->getStatsdDataFactory(), + new NullLogger() + ); // TODO: remove this once PoolWorkArticleView has the ParserCache injected $this->setService( 'ParserCache', $parserCache ); diff --git a/tests/phpunit/includes/parser/ParserCacheSerializationTestCases.php b/tests/phpunit/includes/parser/ParserCacheSerializationTestCases.php index d26f4e0b38f2..01d68f7d2b91 100644 --- a/tests/phpunit/includes/parser/ParserCacheSerializationTestCases.php +++ b/tests/phpunit/includes/parser/ParserCacheSerializationTestCases.php @@ -4,6 +4,7 @@ namespace MediaWiki\Tests\Parser; use CacheTime; use JsonSerializable; +use MediaWiki\Json\JsonUnserializer; use MediaWikiIntegrationTestCase; use MWTimestamp; use ParserOutput; @@ -407,12 +408,15 @@ abstract class ParserCacheSerializationTestCases { 'deserializer' => 'unserialize' ] ]; if ( is_subclass_of( $class, JsonSerializable::class ) ) { + $jsonUnserializer = new JsonUnserializer(); $serializationFormats[] = [ 'ext' => 'json', 'serializer' => function ( JsonSerializable $obj ) { return json_encode( $obj->jsonSerialize() ); }, - 'deserializer' => $class . '::newFromJson' + 'deserializer' => function ( $data ) use ( $jsonUnserializer ) { + return $jsonUnserializer->unserialize( $data ); + } ]; } return $serializationFormats; diff --git a/tests/phpunit/includes/parser/ParserCacheTest.php b/tests/phpunit/includes/parser/ParserCacheTest.php index ee0b45d0bc81..2eaea8fd06c6 100644 --- a/tests/phpunit/includes/parser/ParserCacheTest.php +++ b/tests/phpunit/includes/parser/ParserCacheTest.php @@ -7,6 +7,8 @@ use EmptyBagOStuff; use HashBagOStuff; use InvalidArgumentException; use MediaWiki\HookContainer\HookContainer; +use MediaWiki\Json\JsonUnserializer; +use MediaWiki\Tests\Json\JsonUnserializableSuperClass; use MediaWikiIntegrationTestCase; use MWTimestamp; use NullStatsdDataFactory; @@ -80,6 +82,7 @@ class ParserCacheTest extends MediaWikiIntegrationTestCase { $storage ?: new HashBagOStuff(), '19900220000000', $hookContainer ?: $this->createHookContainer( [] ), + new JsonUnserializer(), new NullStatsdDataFactory(), $logger ?: new NullLogger() ); @@ -517,8 +520,10 @@ class ParserCacheTest extends MediaWikiIntegrationTestCase { public function provideCorruptData() { yield 'PHP serialization, bad data' => [ false, 'bla bla' ]; yield 'JSON serialization, bad data' => [ true, 'bla bla' ]; - yield 'JSON serialization, no _type_' => [ true, '{"test":"test"}' ]; - yield 'JSON serialization, wrong _type_' => [ true, '{"_type_":"bla bla"}' ]; + yield 'JSON serialization, no _class_' => [ true, '{"test":"test"}' ]; + yield 'JSON serialization, non-existing _class_' => [ true, '{"_class_":"NonExistentBogusClass"}' ]; + $wrongInstance = new JsonUnserializableSuperClass( 'test' ); + yield 'JSON serialization, wrong class' => [ true, json_encode( $wrongInstance->jsonSerialize() ) ]; } /** diff --git a/tests/phpunit/mocks/json/JsonUnserializableSubClass.php b/tests/phpunit/mocks/json/JsonUnserializableSubClass.php new file mode 100644 index 000000000000..b20b9c95c77c --- /dev/null +++ b/tests/phpunit/mocks/json/JsonUnserializableSubClass.php @@ -0,0 +1,35 @@ +<?php + +namespace MediaWiki\Tests\Json; + +use MediaWiki\Json\JsonUnserializableTrait; +use MediaWiki\Json\JsonUnserializer; + +/** + * Testing class for JsonUnserializer unit tests. + * @package MediaWiki\Tests\Json + */ +class JsonUnserializableSubClass extends JsonUnserializableSuperClass { + use JsonUnserializableTrait; + + private $subClassField; + + public function __construct( string $superClassFieldValue, string $subClassFieldValue ) { + parent::__construct( $superClassFieldValue ); + $this->subClassField = $subClassFieldValue; + } + + public function getSubClassField(): string { + return $this->subClassField; + } + + public static function newFromJsonArray( JsonUnserializer $unserializer, array $json ) { + return new self( $json['super_class_field'], $json['sub_class_field'] ); + } + + protected function toJsonArray(): array { + return parent::toJsonArray() + [ + 'sub_class_field' => $this->getSubClassField() + ]; + } +} diff --git a/tests/phpunit/mocks/json/JsonUnserializableSuperClass.php b/tests/phpunit/mocks/json/JsonUnserializableSuperClass.php new file mode 100644 index 000000000000..bb91052b914b --- /dev/null +++ b/tests/phpunit/mocks/json/JsonUnserializableSuperClass.php @@ -0,0 +1,35 @@ +<?php + +namespace MediaWiki\Tests\Json; + +use MediaWiki\Json\JsonUnserializable; +use MediaWiki\Json\JsonUnserializableTrait; +use MediaWiki\Json\JsonUnserializer; + +/** + * Testing class for JsonUnserializer unit tests. + * @package MediaWiki\Tests\Json + */ +class JsonUnserializableSuperClass implements JsonUnserializable { + use JsonUnserializableTrait; + + private $superClassField; + + public function __construct( string $superClassFieldValue ) { + $this->superClassField = $superClassFieldValue; + } + + public function getSuperClassField(): string { + return $this->superClassField; + } + + public static function newFromJsonArray( JsonUnserializer $unserializer, array $json ) { + return new self( $json['super_class_field'] ); + } + + protected function toJsonArray(): array { + return [ + 'super_class_field' => $this->getSuperClassField() + ]; + } +} diff --git a/tests/phpunit/unit/includes/json/FormatJsonTest.php b/tests/phpunit/unit/includes/json/FormatJsonTest.php index ea280897bba1..9aeae1fe9c19 100644 --- a/tests/phpunit/unit/includes/json/FormatJsonTest.php +++ b/tests/phpunit/unit/includes/json/FormatJsonTest.php @@ -1,5 +1,7 @@ <?php +use MediaWiki\Tests\Json\JsonUnserializableSuperClass; + /** * @covers FormatJson */ @@ -82,21 +84,38 @@ class FormatJsonTest extends MediaWikiUnitTestCase { public function provideValidateSerializable() { $classInstance = new class() { }; + $serializableClass = new class() implements JsonSerializable { + public function jsonSerialize() { + return []; + } + }; - yield 'Number' => [ 1, null ]; - yield 'Null' => [ null, null ]; - yield 'Class' => [ $classInstance, '$' ]; - yield 'Empty array' => [ [], null ]; - yield 'Empty stdClass' => [ new stdClass(), null ]; - yield 'Non-empty array' => [ [ 1, 2, 3 ], null ]; - yield 'Non-empty map' => [ [ 'a' => 'b' ], null ]; - yield 'Nested, serializable' => [ [ 'a' => [ 'b' => [ 'c' => 'd' ] ] ], null ]; - yield 'Nested, serializable, with null' => [ [ 'a' => [ 'b' => null ] ], null ]; - yield 'Nested, serializable, with stdClass' => [ [ 'a' => (object)[ 'b' => [ 'c' => 'd' ] ] ], null ]; - yield 'Nested, serializable, with stdClass, with null' => [ [ 'a' => (object)[ 'b' => null ] ], null ]; - yield 'Nested, non-serializable' => [ [ 'a' => [ 'b' => $classInstance ] ], '$.a.b' ]; - yield 'Nested, non-serializable, in array' => [ [ 'a' => [ 1, 2, $classInstance ] ], '$.a.2' ]; - yield 'Nested, non-serializable, in stdClass' => [ [ 'a' => (object)[ 1, 2, $classInstance ] ], '$.a.2' ]; + yield 'Number' => [ 1, true, null ]; + yield 'Null' => [ null, true, null ]; + yield 'Class' => [ $classInstance, false, '$' ]; + yield 'Empty array' => [ [], true, null ]; + yield 'Empty stdClass' => [ new stdClass(), true, null ]; + yield 'Non-empty array' => [ [ 1, 2, 3 ], true, null ]; + yield 'Non-empty map' => [ [ 'a' => 'b' ], true, null ]; + yield 'Nested, serializable' => [ [ 'a' => [ 'b' => [ 'c' => 'd' ] ] ], true, null ]; + yield 'Nested, serializable, with null' => [ [ 'a' => [ 'b' => null ] ], true, null ]; + yield 'Nested, serializable, with stdClass' => [ [ 'a' => (object)[ 'b' => [ 'c' => 'd' ] ] ], true, null ]; + yield 'Nested, serializable, with stdClass, with null' => [ [ 'a' => (object)[ 'b' => null ] ], true, null ]; + yield 'Nested, non-serializable' => [ [ 'a' => [ 'b' => $classInstance ] ], true, '$.a.b' ]; + yield 'Nested, non-serializable, in array' => [ [ 'a' => [ 1, 2, $classInstance ] ], true, '$.a.2' ]; + yield 'Nested, non-serializable, in stdClass' => [ [ 'a' => (object)[ 1, 2, $classInstance ] ], true, '$.a.2' ]; + yield 'JsonUnserializable instance' => [ new JsonUnserializableSuperClass( 'Test' ), true, null ]; + yield 'JsonUnserializable instance, in array' => + [ [ new JsonUnserializableSuperClass( 'Test' ) ], true, null ]; + yield 'JsonUnserializable instance, in stdClass' => + [ (object)[ new JsonUnserializableSuperClass( 'Test' ) ], true, null ]; + yield 'JsonSerializable instance' => [ $serializableClass, false, null ]; + yield 'JsonSerializable instance, in array' => [ [ $serializableClass ], false, null ]; + yield 'JsonSerializable instance, in stdClass' => [ (object)[ $serializableClass ], false, null ]; + yield 'JsonSerializable instance, expect unserialize' => [ $serializableClass, true, '$' ]; + yield 'JsonSerializable instance, in array, expect unserialize' => [ [ $serializableClass ], true, '$.0' ]; + yield 'JsonSerializable instance, in stdClass, expect unserialize' => + [ (object)[ $serializableClass ], true, '$.0' ]; } /** @@ -104,10 +123,11 @@ class FormatJsonTest extends MediaWikiUnitTestCase { * @covers FormatJson::detectNonSerializableData * @covers FormatJson::detectNonSerializableDataInternal * @param $value + * @param bool $expectUnserialize * @param string|null $result */ - public function testValidateSerializable( $value, ?string $result ) { - $this->assertSame( $result, FormatJson::detectNonSerializableData( $value ) ); + public function testValidateSerializable( $value, bool $expectUnserialize, ?string $result ) { + $this->assertSame( $result, FormatJson::detectNonSerializableData( $value, $expectUnserialize ) ); } } diff --git a/tests/phpunit/unit/includes/json/JsonUnserializerTest.php b/tests/phpunit/unit/includes/json/JsonUnserializerTest.php new file mode 100644 index 000000000000..b1932e3ee174 --- /dev/null +++ b/tests/phpunit/unit/includes/json/JsonUnserializerTest.php @@ -0,0 +1,99 @@ +<?php + +namespace MediaWiki\Tests\Json; + +use FormatJson; +use InvalidArgumentException; +use MediaWiki\Json\JsonUnserializer; +use MediaWikiUnitTestCase; +use Title; + +/** + * @covers \MediaWiki\Json\JsonUnserializer + * @covers \MediaWiki\Json\JsonUnserializableTrait + * @package MediaWiki\Tests\Json + */ +class JsonUnserializerTest extends MediaWikiUnitTestCase { + + private function getUnserializer(): JsonUnserializer { + return new JsonUnserializer(); + } + + public function provideInvalidJsonData() { + yield 'Bad string' => [ 'bad string' ]; + yield 'Integer???' => [ 1 ]; + yield 'No unserialization metadata' => [ [ 'test' => 'test' ] ]; + yield 'Unserialization metadata, but class not exist' => [ [ + JsonUnserializer::TYPE_ANNOTATION => 'BadClassNotExist' + ] ]; + yield 'Unserialization metadata, but class is not JsonUnserializable' => [ [ + JsonUnserializer::TYPE_ANNOTATION => Title::class + ] ]; + } + + /** + * @dataProvider provideInvalidJsonData + * @param $jsonData + */ + public function testInvalidJsonData( $jsonData ) { + $this->expectException( InvalidArgumentException::class ); + $this->getUnserializer()->unserialize( $jsonData ); + } + + public function testUnexpectedClassUnserialized() { + $this->expectException( InvalidArgumentException::class ); + $superClassInstance = new JsonUnserializableSuperClass( 'Godzilla' ); + $this->getUnserializer()->unserialize( + $superClassInstance->jsonSerialize(), + JsonUnserializableSubClass::class + ); + } + + public function testExpectedClassUnserialized() { + $subClassInstance = new JsonUnserializableSubClass( 'Godzilla', 'But we are ready!' ); + $this->assertNotNull( $this->getUnserializer()->unserialize( + $subClassInstance->jsonSerialize(), + JsonUnserializableSuperClass::class + ) ); + $this->assertNotNull( $this->getUnserializer()->unserialize( + $subClassInstance->jsonSerialize(), + JsonUnserializableSubClass::class + ) ); + } + + public function testRoundTripSuperClass() { + $superClassInstance = new JsonUnserializableSuperClass( 'Super Value' ); + $json = $superClassInstance->jsonSerialize(); + $superClassUnserialized = $this->getUnserializer()->unserialize( $json ); + $this->assertInstanceOf( JsonUnserializableSuperClass::class, $superClassInstance ); + $this->assertSame( $superClassInstance->getSuperClassField(), $superClassUnserialized->getSuperClassField() ); + } + + public function testRoundTripSuperClassObject() { + $superClassInstance = new JsonUnserializableSuperClass( 'Super Value' ); + $json = (object)$superClassInstance->jsonSerialize(); + $superClassUnserialized = $this->getUnserializer()->unserialize( $json ); + $this->assertInstanceOf( JsonUnserializableSuperClass::class, $superClassInstance ); + $this->assertSame( $superClassInstance->getSuperClassField(), $superClassUnserialized->getSuperClassField() ); + } + + public function testRoundTripSubClass() { + $subClassInstance = new JsonUnserializableSubClass( 'Super Value', 'Sub Value' ); + $json = $subClassInstance->jsonSerialize(); + $superClassUnserialized = $this->getUnserializer()->unserialize( $json ); + $this->assertInstanceOf( JsonUnserializableSubClass::class, $subClassInstance ); + $this->assertSame( $subClassInstance->getSuperClassField(), $superClassUnserialized->getSuperClassField() ); + $this->assertSame( $subClassInstance->getSubClassField(), $superClassUnserialized->getSubClassField() ); + } + + public function testArrayRoundTrip() { + $array = [ + new JsonUnserializableSuperClass( 'Super Value' ), + new JsonUnserializableSubClass( 'Super Value', 'Sub Value' ), + 42 + ]; + $serialized = FormatJson::encode( $array ); + $unserialized = $this->getUnserializer()->unserializeArray( FormatJson::decode( $serialized ) ); + $this->assertArrayEquals( $array, $unserialized ); + } +} |