9c5268c768031aa9e0b24c55b96186557b1c4bdf
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiParseTest.php
1 <?php
2
3 /**
4 * @group API
5 * @group Database
6 * @group medium
7 *
8 * @covers ApiParse
9 */
10 class ApiParseTest extends ApiTestCase {
11
12 protected static $pageId;
13 protected static $revIds = [];
14
15 public function addDBDataOnce() {
16 $title = Title::newFromText( __CLASS__ );
17
18 $status = $this->editPage( __CLASS__, 'Test for revdel' );
19 self::$pageId = $status->value['revision']->getPage();
20 self::$revIds['revdel'] = $status->value['revision']->getId();
21
22 $status = $this->editPage( __CLASS__, 'Test for suppressed' );
23 self::$revIds['suppressed'] = $status->value['revision']->getId();
24
25 $status = $this->editPage( __CLASS__, 'Test for oldid' );
26 self::$revIds['oldid'] = $status->value['revision']->getId();
27
28 $status = $this->editPage( __CLASS__, 'Test for latest' );
29 self::$revIds['latest'] = $status->value['revision']->getId();
30
31 $this->revisionDelete( self::$revIds['revdel'] );
32 $this->revisionDelete(
33 self::$revIds['suppressed'],
34 [ Revision::DELETED_TEXT => 1, Revision::DELETED_RESTRICTED => 1 ]
35 );
36
37 Title::clearCaches(); // Otherwise it has the wrong latest revision for some reason
38 }
39
40 /**
41 * Assert that the given result of calling $this->doApiRequest() with
42 * action=parse resulted in $html, accounting for the boilerplate that the
43 * parser adds around the parsed page. Also asserts that warnings match
44 * the provided $warning.
45 *
46 * @param string $html Expected HTML
47 * @param array $res Returned from doApiRequest()
48 * @param string|null $warnings Exact value of expected warnings, null for
49 * no warnings
50 */
51 protected function assertParsedTo( $expected, array $res, $warnings = null ) {
52 $this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertSame' ] );
53 }
54
55 /**
56 * Same as above, but asserts that the HTML matches a regexp instead of a
57 * literal string match.
58 *
59 * @param string $html Expected HTML
60 * @param array $res Returned from doApiRequest()
61 * @param string|null $warnings Exact value of expected warnings, null for
62 * no warnings
63 */
64 protected function assertParsedToRegExp( $expected, array $res, $warnings = null ) {
65 $this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertRegExp' ] );
66 }
67
68 private function doAssertParsedTo( $expected, array $res, $warnings, callable $callback ) {
69 $html = $res[0]['parse']['text'];
70
71 $expectedStart = '<div class="mw-parser-output">';
72 $this->assertSame( $expectedStart, substr( $html, 0, strlen( $expectedStart ) ) );
73
74 $html = substr( $html, strlen( $expectedStart ) );
75
76 if ( $res[1]->getBool( 'disablelimitreport' ) ) {
77 $expectedEnd = "</div>";
78 $this->assertSame( $expectedEnd, substr( $html, -strlen( $expectedEnd ) ) );
79
80 $unexpectedEnd = '#<!-- \nNewPP limit report|' .
81 '<!--\nTransclusion expansion time report#s';
82 $this->assertNotRegExp( $unexpectedEnd, $html );
83
84 $html = substr( $html, 0, strlen( $html ) - strlen( $expectedEnd ) );
85 } else {
86 $expectedEnd = '#\n<!-- \nNewPP limit report\n(?>.+?\n-->)\n' .
87 '<!--\nTransclusion expansion time report \(%,ms,calls,template\)\n(?>.*?\n-->)\n' .
88 '(\n<!-- Saved in parser cache (?>.*?\n -->)\n)?</div>$#s';
89 $this->assertRegExp( $expectedEnd, $html );
90
91 $html = preg_replace( $expectedEnd, '', $html );
92 }
93
94 call_user_func( $callback, $expected, $html );
95
96 if ( $warnings === null ) {
97 $this->assertCount( 1, $res[0] );
98 } else {
99 $this->assertCount( 2, $res[0] );
100 $this->assertSame( [ 'warnings' => $warnings ], $res[0]['warnings']['parse'] );
101 }
102 }
103
104 /**
105 * Set up an interwiki entry for testing.
106 */
107 protected function setupInterwiki() {
108 $dbw = wfGetDB( DB_MASTER );
109 $dbw->insert(
110 'interwiki',
111 [
112 'iw_prefix' => 'madeuplanguage',
113 'iw_url' => "https://example.com/wiki/$1",
114 'iw_api' => '',
115 'iw_wikiid' => '',
116 'iw_local' => false,
117 ],
118 __METHOD__,
119 'IGNORE'
120 );
121
122 $this->setMwGlobals( 'wgExtraInterlanguageLinkPrefixes', [ 'madeuplanguage' ] );
123 $this->tablesUsed[] = 'interwiki';
124 $this->overrideMwServices();
125 }
126
127 /**
128 * Set up a skin for testing.
129 *
130 * @todo Should this code be in MediaWikiTestCase or something?
131 */
132 protected function setupSkin() {
133 $factory = new SkinFactory();
134 $factory->register( 'testing', 'Testing', function () {
135 $skin = $this->getMockBuilder( SkinFallback::class )
136 ->setMethods( [ 'getDefaultModules', 'setupSkinUserCss' ] )
137 ->getMock();
138 $skin->expects( $this->once() )->method( 'getDefaultModules' )
139 ->willReturn( [
140 'styles' => [ 'core' => [ 'quux.styles' ] ],
141 'core' => [ 'foo', 'bar' ],
142 'content' => [ 'baz' ]
143 ] );
144 $skin->expects( $this->once() )->method( 'setupSkinUserCss' )
145 ->will( $this->returnCallback( function ( OutputPage $out ) {
146 $out->addModuleStyles( 'foo.styles' );
147 } ) );
148 return $skin;
149 } );
150 $this->setService( 'SkinFactory', $factory );
151 }
152
153 public function testParseByName() {
154 $res = $this->doApiRequest( [
155 'action' => 'parse',
156 'page' => __CLASS__,
157 ] );
158 $this->assertParsedTo( "<p>Test for latest\n</p>", $res );
159
160 $res = $this->doApiRequest( [
161 'action' => 'parse',
162 'page' => __CLASS__,
163 'disablelimitreport' => 1,
164 ] );
165 $this->assertParsedTo( "<p>Test for latest\n</p>", $res );
166 }
167
168 public function testParseById() {
169 $res = $this->doApiRequest( [
170 'action' => 'parse',
171 'pageid' => self::$pageId,
172 ] );
173 $this->assertParsedTo( "<p>Test for latest\n</p>", $res );
174 }
175
176 public function testParseByOldId() {
177 $res = $this->doApiRequest( [
178 'action' => 'parse',
179 'oldid' => self::$revIds['oldid'],
180 ] );
181 $this->assertParsedTo( "<p>Test for oldid\n</p>", $res );
182 $this->assertArrayNotHasKey( 'textdeleted', $res[0]['parse'] );
183 $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] );
184 }
185
186 public function testRevDel() {
187 $res = $this->doApiRequest( [
188 'action' => 'parse',
189 'oldid' => self::$revIds['revdel'],
190 ] );
191
192 $this->assertParsedTo( "<p>Test for revdel\n</p>", $res );
193 $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] );
194 $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] );
195 }
196
197 public function testRevDelNoPermission() {
198 $this->setExpectedException( ApiUsageException::class,
199 "You don't have permission to view deleted revision text." );
200
201 $this->doApiRequest( [
202 'action' => 'parse',
203 'oldid' => self::$revIds['revdel'],
204 ], null, null, static::getTestUser()->getUser() );
205 }
206
207 public function testSuppressed() {
208 $this->setGroupPermissions( 'sysop', 'viewsuppressed', true );
209 $this->overrideMwServices();
210
211 $res = $this->doApiRequest( [
212 'action' => 'parse',
213 'oldid' => self::$revIds['suppressed']
214 ] );
215
216 $this->assertParsedTo( "<p>Test for suppressed\n</p>", $res );
217 $this->assertArrayHasKey( 'textsuppressed', $res[0]['parse'] );
218 $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] );
219 }
220
221 public function testNonexistentPage() {
222 try {
223 $this->doApiRequest( [
224 'action' => 'parse',
225 'page' => 'DoesNotExist',
226 ] );
227
228 $this->fail( "API did not return an error when parsing a nonexistent page" );
229 } catch ( ApiUsageException $ex ) {
230 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'missingtitle' ),
231 "Parse request for nonexistent page must give 'missingtitle' error: "
232 . var_export( self::getErrorFormatter()->arrayFromStatus( $ex->getStatusValue() ), true )
233 );
234 }
235 }
236
237 public function testTitleProvided() {
238 $res = $this->doApiRequest( [
239 'action' => 'parse',
240 'title' => 'Some interesting page',
241 'text' => '{{PAGENAME}} has attracted my attention',
242 ] );
243
244 $this->assertParsedTo( "<p>Some interesting page has attracted my attention\n</p>", $res );
245 }
246
247 public function testSection() {
248 $name = ucfirst( __FUNCTION__ );
249
250 $this->editPage( $name,
251 "Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" );
252
253 $res = $this->doApiRequest( [
254 'action' => 'parse',
255 'page' => $name,
256 'section' => 1,
257 ] );
258
259 $this->assertParsedToRegExp( '!<h2>.*Section 1.*</h2>\n<p>Content 1\n</p>!', $res );
260 }
261
262 public function testInvalidSection() {
263 $this->setExpectedException( ApiUsageException::class,
264 'The "section" parameter must be a valid section ID or "new".' );
265
266 $this->doApiRequest( [
267 'action' => 'parse',
268 'section' => 'T-new',
269 ] );
270 }
271
272 public function testSectionNoContent() {
273 $name = ucfirst( __FUNCTION__ );
274
275 $status = $this->editPage( $name,
276 "Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" );
277
278 $this->setExpectedException( ApiUsageException::class,
279 "Missing content for page ID {$status->value['revision']->getPage()}." );
280
281 $this->db->delete( 'revision', [ 'rev_id' => $status->value['revision']->getId() ] );
282
283 // Suppress warning in WikiPage::getContentModel
284 Wikimedia\suppressWarnings();
285 try {
286 $this->doApiRequest( [
287 'action' => 'parse',
288 'page' => $name,
289 'section' => 1,
290 ] );
291 } finally {
292 Wikimedia\restoreWarnings();
293 }
294 }
295
296 public function testNewSectionWithPage() {
297 $this->setExpectedException( ApiUsageException::class,
298 '"section=new" cannot be combined with the "oldid", "pageid" or "page" ' .
299 'parameters. Please use "title" and "text".' );
300
301 $this->doApiRequest( [
302 'action' => 'parse',
303 'page' => __CLASS__,
304 'section' => 'new',
305 ] );
306 }
307
308 public function testNonexistentOldId() {
309 $this->setExpectedException( ApiUsageException::class,
310 'There is no revision with ID 2147483647.' );
311
312 $this->doApiRequest( [
313 'action' => 'parse',
314 'oldid' => pow( 2, 31 ) - 1,
315 ] );
316 }
317
318 public function testUnfollowedRedirect() {
319 $name = ucfirst( __FUNCTION__ );
320
321 $this->editPage( $name, "#REDIRECT [[$name 2]]" );
322 $this->editPage( "$name 2", "Some ''text''" );
323
324 $res = $this->doApiRequest( [
325 'action' => 'parse',
326 'page' => $name,
327 ] );
328
329 // Can't use assertParsedTo because the parser output is different for
330 // redirects
331 $this->assertRegExp( "/Redirect to:.*$name 2/", $res[0]['parse']['text'] );
332 $this->assertArrayNotHasKey( 'warnings', $res[0] );
333 }
334
335 public function testFollowedRedirect() {
336 $name = ucfirst( __FUNCTION__ );
337
338 $this->editPage( $name, "#REDIRECT [[$name 2]]" );
339 $this->editPage( "$name 2", "Some ''text''" );
340
341 $res = $this->doApiRequest( [
342 'action' => 'parse',
343 'page' => $name,
344 'redirects' => true,
345 ] );
346
347 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
348 }
349
350 public function testFollowedRedirectById() {
351 $name = ucfirst( __FUNCTION__ );
352
353 $id = $this->editPage( $name, "#REDIRECT [[$name 2]]" )->value['revision']->getPage();
354 $this->editPage( "$name 2", "Some ''text''" );
355
356 $res = $this->doApiRequest( [
357 'action' => 'parse',
358 'pageid' => $id,
359 'redirects' => true,
360 ] );
361
362 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
363 }
364
365 public function testInvalidTitle() {
366 $this->setExpectedException( ApiUsageException::class, 'Bad title "|".' );
367
368 $this->doApiRequest( [
369 'action' => 'parse',
370 'title' => '|',
371 ] );
372 }
373
374 public function testTitleWithNonexistentRevId() {
375 $this->setExpectedException( ApiUsageException::class,
376 'There is no revision with ID 2147483647.' );
377
378 $this->doApiRequest( [
379 'action' => 'parse',
380 'title' => __CLASS__,
381 'revid' => pow( 2, 31 ) - 1,
382 ] );
383 }
384
385 public function testTitleWithNonMatchingRevId() {
386 $name = ucfirst( __FUNCTION__ );
387
388 $res = $this->doApiRequest( [
389 'action' => 'parse',
390 'title' => $name,
391 'revid' => self::$revIds['latest'],
392 'text' => 'Some text',
393 ] );
394
395 $this->assertParsedTo( "<p>Some text\n</p>", $res,
396 'r' . self::$revIds['latest'] . " is not a revision of $name." );
397 }
398
399 public function testRevId() {
400 $res = $this->doApiRequest( [
401 'action' => 'parse',
402 'revid' => self::$revIds['latest'],
403 'text' => 'My revid is {{REVISIONID}}!',
404 ] );
405
406 $this->assertParsedTo( "<p>My revid is " . self::$revIds['latest'] . "!\n</p>", $res );
407 }
408
409 public function testTitleNoText() {
410 $res = $this->doApiRequest( [
411 'action' => 'parse',
412 'title' => 'Special:AllPages',
413 ] );
414
415 $this->assertParsedTo( '', $res,
416 '"title" used without "text", and parsed page properties were requested. ' .
417 'Did you mean to use "page" instead of "title"?' );
418 }
419
420 public function testRevidNoText() {
421 $res = $this->doApiRequest( [
422 'action' => 'parse',
423 'revid' => self::$revIds['latest'],
424 ] );
425
426 $this->assertParsedTo( '', $res,
427 '"revid" used without "text", and parsed page properties were requested. ' .
428 'Did you mean to use "oldid" instead of "revid"?' );
429 }
430
431 public function testTextNoContentModel() {
432 $res = $this->doApiRequest( [
433 'action' => 'parse',
434 'text' => "Some ''text''",
435 ] );
436
437 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res,
438 'No "title" or "contentmodel" was given, assuming wikitext.' );
439 }
440
441 public function testSerializationError() {
442 $this->setExpectedException( APIUsageException::class,
443 'Content serialization failed: Could not unserialize content' );
444
445 $this->mergeMwGlobalArrayValue( 'wgContentHandlers',
446 [ 'testing-serialize-error' => 'DummySerializeErrorContentHandler' ] );
447
448 $this->doApiRequest( [
449 'action' => 'parse',
450 'text' => "Some ''text''",
451 'contentmodel' => 'testing-serialize-error',
452 ] );
453 }
454
455 public function testNewSection() {
456 $res = $this->doApiRequest( [
457 'action' => 'parse',
458 'title' => __CLASS__,
459 'section' => 'new',
460 'sectiontitle' => 'Title',
461 'text' => 'Content',
462 ] );
463
464 $this->assertParsedToRegExp( '!<h2>.*Title.*</h2>\n<p>Content\n</p>!', $res );
465 }
466
467 public function testExistingSection() {
468 $res = $this->doApiRequest( [
469 'action' => 'parse',
470 'title' => __CLASS__,
471 'section' => 1,
472 'text' => "Intro\n\n== Section 1 ==\n\nContent\n\n== Section 2 ==\n\nMore content",
473 ] );
474
475 $this->assertParsedToRegExp( '!<h2>.*Section 1.*</h2>\n<p>Content\n</p>!', $res );
476 }
477
478 public function testNoPst() {
479 $name = ucfirst( __FUNCTION__ );
480
481 $this->editPage( "Template:$name", "Template ''text''" );
482
483 $res = $this->doApiRequest( [
484 'action' => 'parse',
485 'text' => "{{subst:$name}}",
486 'contentmodel' => 'wikitext',
487 ] );
488
489 $this->assertParsedTo( "<p>{{subst:$name}}\n</p>", $res );
490 }
491
492 public function testPst() {
493 $name = ucfirst( __FUNCTION__ );
494
495 $this->editPage( "Template:$name", "Template ''text''" );
496
497 $res = $this->doApiRequest( [
498 'action' => 'parse',
499 'pst' => '',
500 'text' => "{{subst:$name}}",
501 'contentmodel' => 'wikitext',
502 'prop' => 'text|wikitext',
503 ] );
504
505 $this->assertParsedTo( "<p>Template <i>text</i>\n</p>", $res );
506 $this->assertSame( "{{subst:$name}}", $res[0]['parse']['wikitext'] );
507 }
508
509 public function testOnlyPst() {
510 $name = ucfirst( __FUNCTION__ );
511
512 $this->editPage( "Template:$name", "Template ''text''" );
513
514 $res = $this->doApiRequest( [
515 'action' => 'parse',
516 'onlypst' => '',
517 'text' => "{{subst:$name}}",
518 'contentmodel' => 'wikitext',
519 'prop' => 'text|wikitext',
520 'summary' => 'Summary',
521 ] );
522
523 $this->assertSame(
524 [ 'parse' => [
525 'text' => "Template ''text''",
526 'wikitext' => "{{subst:$name}}",
527 'parsedsummary' => 'Summary',
528 ] ],
529 $res[0]
530 );
531 }
532
533 public function testHeadHtml() {
534 $res = $this->doApiRequest( [
535 'action' => 'parse',
536 'page' => __CLASS__,
537 'prop' => 'headhtml',
538 ] );
539
540 // Just do a rough sanity check
541 $this->assertRegExp( '#<!DOCTYPE.*<html.*<head.*</head>.*<body#s',
542 $res[0]['parse']['headhtml'] );
543 $this->assertArrayNotHasKey( 'warnings', $res[0] );
544 }
545
546 public function testCategoriesHtml() {
547 $name = ucfirst( __FUNCTION__ );
548
549 $this->editPage( $name, "[[Category:$name]]" );
550
551 $res = $this->doApiRequest( [
552 'action' => 'parse',
553 'page' => $name,
554 'prop' => 'categorieshtml',
555 ] );
556
557 $this->assertRegExp( "#Category.*Category:$name.*$name#",
558 $res[0]['parse']['categorieshtml'] );
559 $this->assertArrayNotHasKey( 'warnings', $res[0] );
560 }
561
562 public function testEffectiveLangLinks() {
563 $hookRan = false;
564 $this->setTemporaryHook( 'LanguageLinks',
565 function () use ( &$hookRan ) {
566 $hookRan = true;
567 }
568 );
569
570 $res = $this->doApiRequest( [
571 'action' => 'parse',
572 'title' => __CLASS__,
573 'text' => '[[zh:' . __CLASS__ . ']]',
574 'effectivelanglinks' => '',
575 ] );
576
577 $this->assertTrue( $hookRan );
578 $this->assertSame( 'The parameter "effectivelanglinks" has been deprecated.',
579 $res[0]['warnings']['parse']['warnings'] );
580 }
581
582 /**
583 * @param array $arr Extra params to add to API request
584 */
585 private function doTestLangLinks( array $arr = [] ) {
586 $res = $this->doApiRequest( array_merge( [
587 'action' => 'parse',
588 'title' => 'Omelette',
589 'text' => '[[madeuplanguage:Omelette]]',
590 'prop' => 'langlinks',
591 ], $arr ) );
592
593 $langLinks = $res[0]['parse']['langlinks'];
594
595 $this->assertCount( 1, $langLinks );
596 $this->assertSame( 'madeuplanguage', $langLinks[0]['lang'] );
597 $this->assertSame( 'Omelette', $langLinks[0]['title'] );
598 $this->assertSame( 'https://example.com/wiki/Omelette', $langLinks[0]['url'] );
599 $this->assertArrayNotHasKey( 'warnings', $res[0] );
600 }
601
602 public function testLangLinks() {
603 $this->setupInterwiki();
604 $this->doTestLangLinks();
605 }
606
607 public function testLangLinksWithSkin() {
608 $this->setupInterwiki();
609 $this->setupSkin();
610 $this->doTestLangLinks( [ 'useskin' => 'testing' ] );
611 }
612
613 public function testHeadItems() {
614 $res = $this->doApiRequest( [
615 'action' => 'parse',
616 'title' => __CLASS__,
617 'text' => '',
618 'prop' => 'headitems',
619 ] );
620
621 $this->assertSame( [], $res[0]['parse']['headitems'] );
622 $this->assertSame(
623 '"prop=headitems" is deprecated since MediaWiki 1.28. ' .
624 'Use "prop=headhtml" when creating new HTML documents, ' .
625 'or "prop=modules|jsconfigvars" when updating a document client-side.',
626 $res[0]['warnings']['parse']['warnings']
627 );
628 }
629
630 public function testHeadItemsWithSkin() {
631 $this->setupSkin();
632
633 $res = $this->doApiRequest( [
634 'action' => 'parse',
635 'title' => __CLASS__,
636 'text' => '',
637 'prop' => 'headitems',
638 'useskin' => 'testing',
639 ] );
640
641 $this->assertSame( [], $res[0]['parse']['headitems'] );
642 $this->assertSame(
643 '"prop=headitems" is deprecated since MediaWiki 1.28. ' .
644 'Use "prop=headhtml" when creating new HTML documents, ' .
645 'or "prop=modules|jsconfigvars" when updating a document client-side.',
646 $res[0]['warnings']['parse']['warnings']
647 );
648 }
649
650 public function testModules() {
651 $this->setTemporaryHook( 'ParserAfterParse',
652 function ( $parser ) {
653 $output = $parser->getOutput();
654 $output->addModules( [ 'foo', 'bar' ] );
655 $output->addModuleStyles( [ 'aaa', 'zzz' ] );
656 $output->addJsConfigVars( [ 'x' => 'y', 'z' => -3 ] );
657 }
658 );
659 $res = $this->doApiRequest( [
660 'action' => 'parse',
661 'title' => __CLASS__,
662 'text' => 'Content',
663 'prop' => 'modules|jsconfigvars|encodedjsconfigvars',
664 ] );
665
666 $this->assertSame( [ 'foo', 'bar' ], $res[0]['parse']['modules'] );
667 $this->assertSame( [], $res[0]['parse']['modulescripts'] );
668 $this->assertSame( [ 'aaa', 'zzz' ], $res[0]['parse']['modulestyles'] );
669 $this->assertSame( [ 'x' => 'y', 'z' => -3 ], $res[0]['parse']['jsconfigvars'] );
670 $this->assertSame( '{"x":"y","z":-3}', $res[0]['parse']['encodedjsconfigvars'] );
671 $this->assertArrayNotHasKey( 'warnings', $res[0] );
672 }
673
674 public function testModulesWithSkin() {
675 $this->setupSkin();
676
677 $res = $this->doApiRequest( [
678 'action' => 'parse',
679 'pageid' => self::$pageId,
680 'useskin' => 'testing',
681 'prop' => 'modules',
682 ] );
683 $this->assertSame(
684 [ 'foo', 'bar', 'baz' ],
685 $res[0]['parse']['modules'],
686 'resp.parse.modules'
687 );
688 $this->assertSame(
689 [],
690 $res[0]['parse']['modulescripts'],
691 'resp.parse.modulescripts'
692 );
693 $this->assertSame(
694 [ 'foo.styles', 'quux.styles' ],
695 $res[0]['parse']['modulestyles'],
696 'resp.parse.modulestyles'
697 );
698 $this->assertSame(
699 [ 'parse' =>
700 [ 'warnings' =>
701 'Property "modules" was set but not "jsconfigvars" or ' .
702 '"encodedjsconfigvars". Configuration variables are necessary for ' .
703 'proper module usage.'
704 ]
705 ],
706 $res[0]['warnings']
707 );
708 }
709
710 public function testIndicators() {
711 $res = $this->doApiRequest( [
712 'action' => 'parse',
713 'title' => __CLASS__,
714 'text' =>
715 '<indicator name="b">BBB!</indicator>Some text<indicator name="a">aaa</indicator>',
716 'prop' => 'indicators',
717 ] );
718
719 $this->assertSame(
720 // It seems we return in markup order and not display order
721 [ 'b' => 'BBB!', 'a' => 'aaa' ],
722 $res[0]['parse']['indicators']
723 );
724 $this->assertArrayNotHasKey( 'warnings', $res[0] );
725 }
726
727 public function testIndicatorsWithSkin() {
728 $this->setupSkin();
729
730 $res = $this->doApiRequest( [
731 'action' => 'parse',
732 'title' => __CLASS__,
733 'text' =>
734 '<indicator name="b">BBB!</indicator>Some text<indicator name="a">aaa</indicator>',
735 'prop' => 'indicators',
736 'useskin' => 'testing',
737 ] );
738
739 $this->assertSame(
740 // Now we return in display order rather than markup order
741 [ 'a' => 'aaa', 'b' => 'BBB!' ],
742 $res[0]['parse']['indicators']
743 );
744 $this->assertArrayNotHasKey( 'warnings', $res[0] );
745 }
746
747 public function testIwlinks() {
748 $this->setupInterwiki();
749
750 $res = $this->doApiRequest( [
751 'action' => 'parse',
752 'title' => 'Omelette',
753 'text' => '[[:madeuplanguage:Omelette]][[madeuplanguage:Spaghetti]]',
754 'prop' => 'iwlinks',
755 ] );
756
757 $iwlinks = $res[0]['parse']['iwlinks'];
758
759 $this->assertCount( 1, $iwlinks );
760 $this->assertSame( 'madeuplanguage', $iwlinks[0]['prefix'] );
761 $this->assertSame( 'https://example.com/wiki/Omelette', $iwlinks[0]['url'] );
762 $this->assertSame( 'madeuplanguage:Omelette', $iwlinks[0]['title'] );
763 $this->assertArrayNotHasKey( 'warnings', $res[0] );
764 }
765
766 public function testLimitReports() {
767 $res = $this->doApiRequest( [
768 'action' => 'parse',
769 'pageid' => self::$pageId,
770 'prop' => 'limitreportdata|limitreporthtml',
771 ] );
772
773 // We don't bother testing the actual values here
774 $this->assertInternalType( 'array', $res[0]['parse']['limitreportdata'] );
775 $this->assertInternalType( 'string', $res[0]['parse']['limitreporthtml'] );
776 $this->assertArrayNotHasKey( 'warnings', $res[0] );
777 }
778
779 public function testParseTreeNonWikitext() {
780 $this->setExpectedException( ApiUsageException::class,
781 '"prop=parsetree" is only supported for wikitext content.' );
782
783 $this->doApiRequest( [
784 'action' => 'parse',
785 'text' => '',
786 'contentmodel' => 'json',
787 'prop' => 'parsetree',
788 ] );
789 }
790
791 public function testParseTree() {
792 $res = $this->doApiRequest( [
793 'action' => 'parse',
794 'text' => "Some ''text'' is {{nice|to have|i=think}}",
795 'contentmodel' => 'wikitext',
796 'prop' => 'parsetree',
797 ] );
798
799 // Preprocessor_DOM and Preprocessor_Hash give different results here,
800 // so we'll accept either
801 $this->assertRegExp(
802 '#^<root>Some \'\'text\'\' is <template><title>nice</title>' .
803 '<part><name index="1"/><value>to have</value></part>' .
804 '<part><name>i</name>(?:<equals>)?=(?:</equals>)?<value>think</value></part>' .
805 '</template></root>$#',
806 $res[0]['parse']['parsetree']
807 );
808 $this->assertArrayNotHasKey( 'warnings', $res[0] );
809 }
810
811 public function testDisableTidy() {
812 $this->setMwGlobals( 'wgTidyConfig', [ 'driver' => 'RemexHtml' ] );
813
814 // Check that disabletidy doesn't have an effect just because tidying
815 // doesn't work for some other reason
816 $res1 = $this->doApiRequest( [
817 'action' => 'parse',
818 'text' => "<b>Mixed <i>up</b></i>",
819 'contentmodel' => 'wikitext',
820 ] );
821 $this->assertParsedTo( "<p><b>Mixed <i>up</i></b>\n</p>", $res1 );
822
823 $res2 = $this->doApiRequest( [
824 'action' => 'parse',
825 'text' => "<b>Mixed <i>up</b></i>",
826 'contentmodel' => 'wikitext',
827 'disabletidy' => '',
828 ] );
829
830 $this->assertParsedTo( "<p><b>Mixed <i>up</b></i>\n</p>", $res2,
831 'The parameter "disabletidy" has been deprecated.' );
832 }
833
834 public function testFormatCategories() {
835 $name = ucfirst( __FUNCTION__ );
836
837 $this->editPage( "Category:$name", 'Content' );
838 $this->editPage( 'Category:Hidden', '__HIDDENCAT__' );
839
840 $res = $this->doApiRequest( [
841 'action' => 'parse',
842 'title' => __CLASS__,
843 'text' => "[[Category:$name]][[Category:Foo|Sort me]][[Category:Hidden]]",
844 'prop' => 'categories',
845 ] );
846
847 $this->assertSame(
848 [ [ 'sortkey' => '', 'category' => $name ],
849 [ 'sortkey' => 'Sort me', 'category' => 'Foo', 'missing' => true ],
850 [ 'sortkey' => '', 'category' => 'Hidden', 'hidden' => true ] ],
851 $res[0]['parse']['categories']
852 );
853 $this->assertArrayNotHasKey( 'warnings', $res[0] );
854 }
855 }