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