Check if non-JSON-serializable data passed to ParserOutput
Bug: T264394 Change-Id: I6eedd03a81b95f6f55d25c00b31e01cbd8658d43
This commit is contained in:
parent
d25f1ad0db
commit
1c70cca3ee
3 changed files with 96 additions and 0 deletions
|
|
@ -319,4 +319,49 @@ class FormatJson {
|
|||
// Add final chunk to buffer before returning
|
||||
return $buffer . substr( $str, $mark, $maxLen - $mark );
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive check for ability to serialize $value to JSON via FormatJson::encode().
|
||||
*
|
||||
* @note instances of JsonSerializable interface are not considered serializable
|
||||
* in this method. The $value passed here is a result of JsonSerializable::jsonSerialize().
|
||||
*
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param string $accumulatedPath
|
||||
* @return string|null JSON path to first encountered non-serializable property or null.
|
||||
*/
|
||||
private static function detectNonSerializableDataInternal(
|
||||
$value,
|
||||
string $accumulatedPath
|
||||
): ?string {
|
||||
if ( is_array( $value ) ||
|
||||
( is_object( $value ) && get_class( $value ) === 'stdClass' ) ) {
|
||||
foreach ( $value as $key => $propValue ) {
|
||||
$propValueNonSerializablePath = self::detectNonSerializableDataInternal(
|
||||
$propValue,
|
||||
$accumulatedPath . '.' . $key
|
||||
);
|
||||
if ( $propValueNonSerializablePath ) {
|
||||
return $propValueNonSerializablePath;
|
||||
}
|
||||
}
|
||||
// Instances of classes other the \stdClass can not be serialized to JSON
|
||||
} elseif ( !is_scalar( $value ) && $value !== null ) {
|
||||
return $accumulatedPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return string|null JSON path to first encountered non-serializable property or null.
|
||||
*/
|
||||
public static function detectNonSerializableData( $value ): ?string {
|
||||
return self::detectNonSerializableDataInternal( $value, '$' );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1135,6 +1135,16 @@ class ParserOutput extends CacheTime {
|
|||
* @param mixed $value
|
||||
*/
|
||||
public function setProperty( $name, $value ) {
|
||||
$unserializablePath = FormatJson::detectNonSerializableData( $value );
|
||||
if ( $unserializablePath ) {
|
||||
LoggerFactory::getInstance( 'ParserOutput' )->warning(
|
||||
'Non-serializable page property set',
|
||||
[
|
||||
'name' => $name,
|
||||
'path' => $unserializablePath,
|
||||
]
|
||||
);
|
||||
}
|
||||
$this->mProperties[$name] = $value;
|
||||
}
|
||||
|
||||
|
|
@ -1233,6 +1243,16 @@ class ParserOutput extends CacheTime {
|
|||
if ( $value === null ) {
|
||||
unset( $this->mExtensionData[$key] );
|
||||
} else {
|
||||
$unserializablePath = FormatJson::detectNonSerializableData( $value );
|
||||
if ( $unserializablePath ) {
|
||||
LoggerFactory::getInstance( 'ParserOutput' )->warning(
|
||||
'Non-serializable extension data set',
|
||||
[
|
||||
'key' => $key,
|
||||
'path' => $unserializablePath,
|
||||
]
|
||||
);
|
||||
}
|
||||
$this->mExtensionData[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,4 +79,35 @@ class FormatJsonTest extends MediaWikiUnitTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
public function provideValidateSerializable() {
|
||||
$classInstance = new class() {
|
||||
};
|
||||
|
||||
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' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideValidateSerializable
|
||||
* @covers FormatJson::detectNonSerializableData
|
||||
* @covers FormatJson::detectNonSerializableDataInternal
|
||||
* @param $value
|
||||
* @param string|null $result
|
||||
*/
|
||||
public function testValidateSerializable( $value, ?string $result ) {
|
||||
$this->assertSame( $result, FormatJson::detectNonSerializableData( $value ) );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue