Check if non-JSON-serializable data passed to ParserOutput

Bug: T264394
Change-Id: I6eedd03a81b95f6f55d25c00b31e01cbd8658d43
This commit is contained in:
Petr Pchelko 2020-10-02 09:39:37 -06:00
parent d25f1ad0db
commit 1c70cca3ee
3 changed files with 96 additions and 0 deletions

View file

@ -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, '$' );
}
}

View file

@ -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;
}
}

View file

@ -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 ) );
}
}