Type hint against LinkTarget in WatchedItemStore
[lhc/web/wiklou.git] / tests / phpunit / includes / json / FormatJsonTest.php
1 <?php
2
3 /**
4 * @covers FormatJson
5 */
6 class FormatJsonTest extends MediaWikiTestCase {
7
8 public static function provideEncoderPrettyPrinting() {
9 return [
10 // Four spaces
11 [ true, ' ' ],
12 [ ' ', ' ' ],
13 // Two spaces
14 [ ' ', ' ' ],
15 // One tab
16 [ "\t", "\t" ],
17 ];
18 }
19
20 /**
21 * @dataProvider provideEncoderPrettyPrinting
22 */
23 public function testEncoderPrettyPrinting( $pretty, $expectedIndent ) {
24 $obj = [
25 'emptyObject' => new stdClass,
26 'emptyArray' => [],
27 'string' => 'foobar\\',
28 'filledArray' => [
29 [
30 123,
31 456,
32 ],
33 // Nested json works without problems
34 '"7":["8",{"9":"10"}]',
35 // Whitespace clean up doesn't touch strings that look alike
36 "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}",
37 ],
38 ];
39
40 // No trailing whitespace, no trailing linefeed
41 $json = '{
42 "emptyObject": {},
43 "emptyArray": [],
44 "string": "foobar\\\\",
45 "filledArray": [
46 [
47 123,
48 456
49 ],
50 "\"7\":[\"8\",{\"9\":\"10\"}]",
51 "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}"
52 ]
53 }';
54
55 $json = str_replace( "\r", '', $json ); // Windows compat
56 $json = str_replace( "\t", $expectedIndent, $json );
57 $this->assertSame( $json, FormatJson::encode( $obj, $pretty ) );
58 }
59
60 public static function provideEncodeDefault() {
61 return self::getEncodeTestCases( [] );
62 }
63
64 /**
65 * @dataProvider provideEncodeDefault
66 */
67 public function testEncodeDefault( $from, $to ) {
68 $this->assertSame( $to, FormatJson::encode( $from ) );
69 }
70
71 public static function provideEncodeUtf8() {
72 return self::getEncodeTestCases( [ 'unicode' ] );
73 }
74
75 /**
76 * @dataProvider provideEncodeUtf8
77 */
78 public function testEncodeUtf8( $from, $to ) {
79 $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) );
80 }
81
82 public static function provideEncodeXmlMeta() {
83 return self::getEncodeTestCases( [ 'xmlmeta' ] );
84 }
85
86 /**
87 * @dataProvider provideEncodeXmlMeta
88 */
89 public function testEncodeXmlMeta( $from, $to ) {
90 $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) );
91 }
92
93 public static function provideEncodeAllOk() {
94 return self::getEncodeTestCases( [ 'unicode', 'xmlmeta' ] );
95 }
96
97 /**
98 * @dataProvider provideEncodeAllOk
99 */
100 public function testEncodeAllOk( $from, $to ) {
101 $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) );
102 }
103
104 public function testEncodePhpBug46944() {
105 $this->assertNotEquals(
106 '\ud840\udc00',
107 strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ),
108 'Test encoding an broken json_encode character (U+20000)'
109 );
110 }
111
112 public function testEncodeFail() {
113 // Set up a recursive object that can't be encoded.
114 $a = new stdClass;
115 $b = new stdClass;
116 $a->b = $b;
117 $b->a = $a;
118 $this->assertFalse( FormatJson::encode( $a ) );
119 }
120
121 public function testDecodeReturnType() {
122 $this->assertInternalType(
123 'object',
124 FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ),
125 'Default to object'
126 );
127
128 $this->assertInternalType(
129 'array',
130 FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ),
131 'Optional array'
132 );
133 }
134
135 public static function provideParse() {
136 return [
137 [ null ],
138 [ true ],
139 [ false ],
140 [ 0 ],
141 [ 1 ],
142 [ 1.2 ],
143 [ '' ],
144 [ 'str' ],
145 [ [ 0, 1, 2 ] ],
146 [ [ 'a' => 'b' ] ],
147 [ [ 'a' => 'b' ] ],
148 [ [ 'a' => 'b', 'x' => [ 'c' => 'd' ] ] ],
149 ];
150 }
151
152 /**
153 * Recursively convert arrays into stdClass
154 * @param array|string|bool|int|float|null $value
155 * @return stdClass|string|bool|int|float|null
156 */
157 public static function toObject( $value ) {
158 return !is_array( $value ) ? $value : (object)array_map( __METHOD__, $value );
159 }
160
161 /**
162 * @dataProvider provideParse
163 * @param mixed $value
164 */
165 public function testParse( $value ) {
166 $expected = self::toObject( $value );
167 $json = FormatJson::encode( $expected, false, FormatJson::ALL_OK );
168 $this->assertJson( $json );
169
170 $st = FormatJson::parse( $json );
171 $this->assertInstanceOf( Status::class, $st );
172 $this->assertTrue( $st->isGood() );
173 $this->assertEquals( $expected, $st->getValue() );
174
175 $st = FormatJson::parse( $json, FormatJson::FORCE_ASSOC );
176 $this->assertInstanceOf( Status::class, $st );
177 $this->assertTrue( $st->isGood() );
178 $this->assertEquals( $value, $st->getValue() );
179 }
180
181 /**
182 * Test data for testParseTryFixing.
183 *
184 * Some PHP interpreters use json-c rather than the JSON.org canonical
185 * parser to avoid being encumbered by the "shall be used for Good, not
186 * Evil" clause of the JSON.org parser's license. By default, json-c
187 * parses in a non-strict mode which allows trailing commas for array and
188 * object delarations among other things, so our JSON_ERROR_SYNTAX rescue
189 * block is not always triggered. It however isn't lenient in exactly the
190 * same ways as our TRY_FIXING mode, so the assertions in this test are
191 * a bit more complicated than they ideally would be:
192 *
193 * Optional third argument: true if json-c parses the value without
194 * intervention, false otherwise. Defaults to true.
195 *
196 * Optional fourth argument: expected cannonical JSON serialization of
197 * json-c parsed result. Defaults to the second argument's value.
198 */
199 public static function provideParseTryFixing() {
200 return [
201 [ "[,]", '[]', false ],
202 [ "[ , ]", '[]', false ],
203 [ "[ , }", false ],
204 [ '[1],', false, true, '[1]' ],
205 [ "[1,]", '[1]' ],
206 [ "[1\n,]", '[1]' ],
207 [ "[1,\n]", '[1]' ],
208 [ "[1,]\n", '[1]' ],
209 [ "[1\n,\n]\n", '[1]' ],
210 [ '["a,",]', '["a,"]' ],
211 [ "[[1,]\n,[2,\n],[3\n,]]", '[[1],[2],[3]]' ],
212 // I wish we could parse this, but would need quote parsing
213 [ '[[1,],[2,],[3,]]', false, true, '[[1],[2],[3]]' ],
214 [ '[1,,]', false, false, '[1]' ],
215 ];
216 }
217
218 /**
219 * @dataProvider provideParseTryFixing
220 * @param string $value
221 * @param string|bool $expected Expected result with strict parser
222 * @param bool $jsoncParses Will json-c parse this value without TRY_FIXING?
223 * @param string|bool $expectedJsonc Expected result with lenient parser
224 * if different from the strict expectation
225 */
226 public function testParseTryFixing(
227 $value, $expected,
228 $jsoncParses = true, $expectedJsonc = null
229 ) {
230 // PHP5 results are always expected to have isGood() === false
231 $expectedGoodStatus = false;
232
233 // Check to see if json parser allows trailing commas
234 if ( json_decode( '[1,]' ) !== null ) {
235 // Use json-c specific expected result if provided
236 $expected = ( $expectedJsonc === null ) ? $expected : $expectedJsonc;
237 // If json-c parses the value natively, expect isGood() === true
238 $expectedGoodStatus = $jsoncParses;
239 }
240
241 $st = FormatJson::parse( $value, FormatJson::TRY_FIXING );
242 $this->assertInstanceOf( Status::class, $st );
243 if ( $expected === false ) {
244 $this->assertFalse( $st->isOK(), 'Expected isOK() == false' );
245 } else {
246 $this->assertSame( $expectedGoodStatus, $st->isGood(),
247 'Expected isGood() == ' . ( $expectedGoodStatus ? 'true' : 'false' )
248 );
249 $this->assertTrue( $st->isOK(), 'Expected isOK == true' );
250 $val = FormatJson::encode( $st->getValue(), false, FormatJson::ALL_OK );
251 $this->assertEquals( $expected, $val );
252 }
253 }
254
255 public static function provideParseErrors() {
256 return [
257 [ 'aaa' ],
258 [ '{"j": 1 ] }' ],
259 ];
260 }
261
262 /**
263 * @dataProvider provideParseErrors
264 * @param mixed $value
265 */
266 public function testParseErrors( $value ) {
267 $st = FormatJson::parse( $value );
268 $this->assertInstanceOf( Status::class, $st );
269 $this->assertFalse( $st->isOK() );
270 }
271
272 public function provideStripComments() {
273 return [
274 [ '{"a":"b"}', '{"a":"b"}' ],
275 [ "{\"a\":\"b\"}\n", "{\"a\":\"b\"}\n" ],
276 [ '/*c*/{"c":"b"}', '{"c":"b"}' ],
277 [ '{"a":"c"}/*c*/', '{"a":"c"}' ],
278 [ '/*c//d*/{"c":"b"}', '{"c":"b"}' ],
279 [ '{/*c*/"c":"b"}', '{"c":"b"}' ],
280 [ "/*\nc\r\n*/{\"c\":\"b\"}", '{"c":"b"}' ],
281 [ "//c\n{\"c\":\"b\"}", '{"c":"b"}' ],
282 [ "//c\r\n{\"c\":\"b\"}", '{"c":"b"}' ],
283 [ '{"a":"c"}//c', '{"a":"c"}' ],
284 [ "{\"a-c\"://c\n\"b\"}", '{"a-c":"b"}' ],
285 [ '{"/*a":"b"}', '{"/*a":"b"}' ],
286 [ '{"a":"//b"}', '{"a":"//b"}' ],
287 [ '{"a":"b/*c*/"}', '{"a":"b/*c*/"}' ],
288 [ "{\"\\\"/*a\":\"b\"}", "{\"\\\"/*a\":\"b\"}" ],
289 [ '', '' ],
290 [ '/*c', '' ],
291 [ '//c', '' ],
292 [ '"http://example.com"', '"http://example.com"' ],
293 [ "\0", "\0" ],
294 [ '"Blåbærsyltetøy"', '"Blåbærsyltetøy"' ],
295 ];
296 }
297
298 /**
299 * @covers FormatJson::stripComments
300 * @dataProvider provideStripComments
301 * @param string $json
302 * @param string $expect
303 */
304 public function testStripComments( $json, $expect ) {
305 $this->assertSame( $expect, FormatJson::stripComments( $json ) );
306 }
307
308 public function provideParseStripComments() {
309 return [
310 [ '/* blah */true', true ],
311 [ "// blah \ntrue", true ],
312 [ '[ "a" , /* blah */ "b" ]', [ 'a', 'b' ] ],
313 ];
314 }
315
316 /**
317 * @covers FormatJson::parse
318 * @covers FormatJson::stripComments
319 * @dataProvider provideParseStripComments
320 * @param string $json
321 * @param mixed $expect
322 */
323 public function testParseStripComments( $json, $expect ) {
324 $st = FormatJson::parse( $json, FormatJson::STRIP_COMMENTS );
325 $this->assertInstanceOf( Status::class, $st );
326 $this->assertTrue( $st->isGood() );
327 $this->assertEquals( $expect, $st->getValue() );
328 }
329
330 /**
331 * Generate a set of test cases for a particular combination of encoder options.
332 *
333 * @param array $unescapedGroups List of character groups to leave unescaped
334 * @return array Arrays of unencoded strings and corresponding encoded strings
335 */
336 private static function getEncodeTestCases( array $unescapedGroups ) {
337 $groups = [
338 'always' => [
339 // Forward slash (always unescaped)
340 '/' => '/',
341
342 // Control characters
343 "\0" => '\u0000',
344 "\x08" => '\b',
345 "\t" => '\t',
346 "\n" => '\n',
347 "\r" => '\r',
348 "\f" => '\f',
349 "\x1f" => '\u001f', // representative example
350
351 // Double quotes
352 '"' => '\"',
353
354 // Backslashes
355 '\\' => '\\\\',
356 '\\\\' => '\\\\\\\\',
357 '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping
358
359 // Line terminators
360 "\xe2\x80\xa8" => '\u2028',
361 "\xe2\x80\xa9" => '\u2029',
362 ],
363 'unicode' => [
364 "\xc3\xa9" => '\u00e9',
365 "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP
366 ],
367 'xmlmeta' => [
368 '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits
369 '>' => '\u003E',
370 '&' => '\u0026',
371 ],
372 ];
373
374 $cases = [];
375 foreach ( $groups as $name => $rules ) {
376 $leaveUnescaped = in_array( $name, $unescapedGroups );
377 foreach ( $rules as $from => $to ) {
378 $cases[] = [ $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' ];
379 }
380 }
381
382 return $cases;
383 }
384
385 public function provideEmptyJsonKeyStrings() {
386 return [
387 [
388 '{"":"foo"}',
389 '{"":"foo"}',
390 ''
391 ],
392 [
393 '{"_empty_":"foo"}',
394 '{"_empty_":"foo"}',
395 '_empty_' ],
396 [
397 '{"\u005F\u0065\u006D\u0070\u0074\u0079\u005F":"foo"}',
398 '{"_empty_":"foo"}',
399 '_empty_'
400 ],
401 [
402 '{"_empty_":"bar","":"foo"}',
403 '{"_empty_":"bar","":"foo"}',
404 ''
405 ],
406 [
407 '{"":"bar","_empty_":"foo"}',
408 '{"":"bar","_empty_":"foo"}',
409 '_empty_'
410 ]
411 ];
412 }
413
414 /**
415 * @covers FormatJson::encode
416 * @covers FormatJson::decode
417 * @dataProvider provideEmptyJsonKeyStrings
418 * @param string $json
419 *
420 * Decoding behavior with empty keys can be surprising.
421 * See https://phabricator.wikimedia.org/T206411
422 */
423 public function testEmptyJsonKeyArray( $json, $expect, $php71Name ) {
424 // Decoding to array is consistent across supported PHP versions
425 $this->assertSame( $expect, FormatJson::encode(
426 FormatJson::decode( $json, true ) ) );
427
428 // Decoding to object differs between supported PHP versions
429 $obj = FormatJson::decode( $json );
430 if ( version_compare( PHP_VERSION, '7.1', '<' ) ) {
431 $this->assertEquals( 'foo', $obj->_empty_ );
432 } else {
433 $this->assertEquals( 'foo', $obj->{$php71Name} );
434 }
435 }
436 }