aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--includes/AutoLoader.php1
-rw-r--r--includes/MediaWikiServices.php9
-rw-r--r--includes/ServiceWiring.php6
-rw-r--r--includes/json/FormatJson.php20
-rw-r--r--includes/json/JsonUnserializable.php48
-rw-r--r--includes/json/JsonUnserializableTrait.php50
-rw-r--r--includes/json/JsonUnserializer.php107
-rw-r--r--includes/parser/CacheTime.php41
-rw-r--r--includes/parser/ParserCache.php68
-rw-r--r--includes/parser/ParserCacheFactory.php8
-rw-r--r--includes/parser/ParserOutput.php65
-rw-r--r--tests/common/TestsAutoLoader.php4
-rw-r--r--tests/phpunit/data/ParserCache/1.36-CacheTime-cacheExpiry.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-CacheTime-cacheRevisionId.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-CacheTime-cacheTime.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-CacheTime-empty.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-CacheTime-usedOptions.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-ParserOutput-binaryPageProperties.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-ParserOutput-empty.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-ParserOutput-extensionData.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-ParserOutput-pageProperties.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-ParserOutput-text.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-ParserOutput-usedOptions.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadata.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadataPost1_31.json2
-rw-r--r--tests/phpunit/data/ParserCache/1.36-ParserOutput-withMetadataPost1_34.json2
-rw-r--r--tests/phpunit/includes/page/ParserOutputAccessTest.php19
-rw-r--r--tests/phpunit/includes/parser/ParserCacheSerializationTestCases.php6
-rw-r--r--tests/phpunit/includes/parser/ParserCacheTest.php9
-rw-r--r--tests/phpunit/mocks/json/JsonUnserializableSubClass.php35
-rw-r--r--tests/phpunit/mocks/json/JsonUnserializableSuperClass.php35
-rw-r--r--tests/phpunit/unit/includes/json/FormatJsonTest.php52
-rw-r--r--tests/phpunit/unit/includes/json/JsonUnserializerTest.php99
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 );
+ }
+}