Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / api / ApiParseTest.php
blob214d6c81cb1d81eda2d0357824ef2034b011e07c
1 <?php
3 /**
4 * ApiParse check functions
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
21 * @file
24 namespace MediaWiki\Tests\Api;
26 use MediaWiki\Api\ApiUsageException;
27 use MediaWiki\Context\RequestContext;
28 use MediaWiki\MainConfigNames;
29 use MediaWiki\Revision\RevisionRecord;
30 use MediaWiki\Tests\Unit\DummyServicesTrait;
31 use MediaWiki\Title\TitleValue;
32 use MockPoolCounterFailing;
33 use SkinFactory;
34 use SkinFallback;
36 /**
37 * @group API
38 * @group Database
39 * @group medium
41 * @covers MediaWiki\Api\ApiParse
43 class ApiParseTest extends ApiTestCase {
44 use DummyServicesTrait;
46 /** @var int */
47 protected static $pageId;
48 /** @var int[] */
49 protected static $revIds = [];
51 public function addDBDataOnce() {
52 $page = $this->getServiceContainer()->getWikiPageFactory()
53 ->newFromLinkTarget( new TitleValue( NS_MAIN, __CLASS__ ) );
54 $status = $this->editPage( $page, 'Test for revdel' );
55 self::$pageId = $status->getNewRevision()->getPageId();
56 self::$revIds['revdel'] = $status->getNewRevision()->getId();
58 $status = $this->editPage( $page, 'Test for suppressed' );
59 self::$revIds['suppressed'] = $status->getNewRevision()->getId();
61 $status = $this->editPage( $page, 'Test for oldid' );
62 self::$revIds['oldid'] = $status->getNewRevision()->getId();
64 $status = $this->editPage( $page, 'Test for latest' );
65 self::$revIds['latest'] = $status->getNewRevision()->getId();
67 // Set a user for modifying the visibility, this is needed because
68 // setVisibility generates a log, which cannot be an anonymous user actor
69 // when temporary accounts are enabled.
70 RequestContext::getMain()->setUser( $this->getTestUser()->getUser() );
71 $this->revisionDelete( self::$revIds['revdel'] );
72 $this->revisionDelete(
73 self::$revIds['suppressed'],
75 RevisionRecord::DELETED_TEXT => 1,
76 RevisionRecord::DELETED_RESTRICTED => 1
81 /**
82 * Assert that the given result of calling $this->doApiRequest() with
83 * action=parse resulted in $html, accounting for the boilerplate that the
84 * parser adds around the parsed page. Also asserts that warnings match
85 * the provided $warning.
87 * @param string $expected Expected HTML
88 * @param array $res Returned from doApiRequest()
89 * @param string|null $warnings Exact value of expected warnings, null for
90 * no warnings
92 protected function assertParsedTo( $expected, array $res, $warnings = null ) {
93 $this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertSame' ] );
96 /**
97 * Same as above, but asserts that the HTML matches a regexp instead of a
98 * literal string match.
100 * @param string $expected Expected HTML
101 * @param array $res Returned from doApiRequest()
102 * @param string|null $warnings Exact value of expected warnings, null for
103 * no warnings
105 protected function assertParsedToRegExp( $expected, array $res, $warnings = null ) {
106 $this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertMatchesRegularExpression' ] );
109 private function doAssertParsedTo( $expected, array $res, $warnings, callable $callback ) {
110 $html = $res[0]['parse']['text'];
112 $expectedStart = '<div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr"';
113 $this->assertSame( $expectedStart, substr( $html, 0, strlen( $expectedStart ) ) );
115 $html = substr( $html, strlen( $expectedStart ) );
117 # Parsoid-based transformations may add ID and data-mw-parsoid-version
118 # attributes to the wrapper div
119 $possibleIdAttr = '/^( (id|data-mw[^=]*)="[^"]+")*>/';
120 $html = preg_replace( $possibleIdAttr, '', $html );
122 $possibleParserCache = '/\n<!-- Saved in (?>parser cache|RevisionOutputCache) (?>.*?\n -->)\n/';
123 $html = preg_replace( $possibleParserCache, '', $html );
125 if ( $res[1]->getBool( 'disablelimitreport' ) ) {
126 $expectedEnd = "</div>";
127 $this->assertSame( $expectedEnd, substr( $html, -strlen( $expectedEnd ) ) );
129 $unexpectedEnd = '#<!-- \nNewPP limit report|' .
130 '<!--\nTransclusion expansion time report#';
131 $this->assertDoesNotMatchRegularExpression( $unexpectedEnd, $html );
133 $html = substr( $html, 0, strlen( $html ) - strlen( $expectedEnd ) );
134 } else {
135 $expectedEnd = '#\n<!-- \nNewPP limit report\n(?>.+?\n-->)\n' .
136 '<!--\nTransclusion expansion time report \(%,ms,calls,template\)\n(?>.*?\n-->)\n' .
137 '</div>$#s';
138 $this->assertMatchesRegularExpression( $expectedEnd, $html );
140 $html = preg_replace( $expectedEnd, '', $html );
143 $callback( $expected, $html );
145 if ( $warnings === null ) {
146 $this->assertCount( 1, $res[0] );
147 } else {
148 $this->assertCount( 2, $res[0] );
149 $this->assertSame( [ 'warnings' => $warnings ], $res[0]['warnings']['parse'] );
154 * Set up an interwiki entry for testing.
156 protected function setupInterwiki() {
157 $this->getDb()->newInsertQueryBuilder()
158 ->insertInto( 'interwiki' )
159 ->ignore()
160 ->row( [
161 'iw_prefix' => 'madeuplanguage',
162 'iw_url' => "https://example.com/wiki/$1",
163 'iw_api' => '',
164 'iw_wikiid' => '',
165 'iw_local' => false,
167 // This deliberately conflicts with the Talk namespace
168 // (T204792/T363538)
169 ->row( [
170 'iw_prefix' => 'talk',
171 'iw_url' => "https://talk.example.com/wiki/$1",
172 'iw_api' => '',
173 'iw_wikiid' => '',
174 'iw_local' => false,
176 ->caller( __METHOD__ )
177 ->execute();
179 $this->overrideConfigValue(
180 MainConfigNames::ExtraInterlanguageLinkPrefixes,
181 [ 'madeuplanguage', 'talk' ]
186 * Set up a skin for testing.
188 * @todo Should this code be in MediaWikiIntegrationTestCase or something?
190 protected function setupSkin() {
191 $factory = new SkinFactory( $this->getDummyObjectFactory(), [] );
192 $factory->register( 'testing', 'Testing', function () {
193 $skin = $this->getMockBuilder( SkinFallback::class )
194 ->onlyMethods( [ 'getDefaultModules' ] )
195 ->getMock();
196 $skin->expects( $this->once() )->method( 'getDefaultModules' )
197 ->willReturn( [
198 'styles' => [ 'core' => [ 'quux.styles' ] ],
199 'core' => [ 'foo', 'bar' ],
200 'content' => [ 'baz' ]
201 ] );
202 return $skin;
203 } );
204 $this->setService( 'SkinFactory', $factory );
207 public function testParseByName() {
208 $res = $this->doApiRequest( [
209 'action' => 'parse',
210 'page' => __CLASS__,
211 ] );
212 $this->assertParsedTo( "<p>Test for latest\n</p>", $res );
214 $res = $this->doApiRequest( [
215 'action' => 'parse',
216 'page' => __CLASS__,
217 'disablelimitreport' => 1,
218 ] );
219 $this->assertParsedTo( "<p>Test for latest\n</p>", $res );
222 public function testParseById() {
223 $res = $this->doApiRequest( [
224 'action' => 'parse',
225 'pageid' => self::$pageId,
226 ] );
227 $this->assertParsedTo( "<p>Test for latest\n</p>", $res );
230 public function testParseByOldId() {
231 $res = $this->doApiRequest( [
232 'action' => 'parse',
233 'oldid' => self::$revIds['oldid'],
234 ] );
235 $this->assertParsedTo( "<p>Test for oldid\n</p>", $res );
236 $this->assertArrayNotHasKey( 'textdeleted', $res[0]['parse'] );
237 $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] );
240 public function testRevDel() {
241 $res = $this->doApiRequest( [
242 'action' => 'parse',
243 'oldid' => self::$revIds['revdel'],
244 ] );
246 $this->assertParsedTo( "<p>Test for revdel\n</p>", $res );
247 $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] );
248 $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] );
251 public function testRevDelNoPermission() {
252 $this->expectApiErrorCode( 'permissiondenied' );
254 $this->doApiRequest( [
255 'action' => 'parse',
256 'oldid' => self::$revIds['revdel'],
257 ], null, null, static::getTestUser()->getAuthority() );
260 public function testSuppressed() {
261 $this->setGroupPermissions( 'sysop', 'viewsuppressed', true );
263 $res = $this->doApiRequest( [
264 'action' => 'parse',
265 'oldid' => self::$revIds['suppressed']
266 ] );
268 $this->assertParsedTo( "<p>Test for suppressed\n</p>", $res );
269 $this->assertArrayHasKey( 'textsuppressed', $res[0]['parse'] );
270 $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] );
273 public function testNonexistentPage() {
274 try {
275 $this->doApiRequest( [
276 'action' => 'parse',
277 'page' => 'DoesNotExist',
278 ] );
280 $this->fail( "API did not return an error when parsing a nonexistent page" );
281 } catch ( ApiUsageException $ex ) {
282 $this->assertApiErrorCode( 'missingtitle', $ex );
286 public function testTitleProvided() {
287 $res = $this->doApiRequest( [
288 'action' => 'parse',
289 'title' => 'Some interesting page',
290 'text' => '{{PAGENAME}} has attracted my attention',
291 ] );
293 $this->assertParsedTo( "<p>Some interesting page has attracted my attention\n</p>", $res );
296 public function testSection() {
297 $name = ucfirst( __FUNCTION__ );
299 $this->editPage( $name,
300 "Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" );
302 $res = $this->doApiRequest( [
303 'action' => 'parse',
304 'page' => $name,
305 'section' => 1,
306 ] );
308 $this->assertParsedToRegExp( '!<h2[^>]*>.*Section 1.*</h2>.*\n<p>Content 1\n</p>!', $res );
311 public function testInvalidSection() {
312 $this->expectApiErrorCode( 'invalidsection' );
314 $this->doApiRequest( [
315 'action' => 'parse',
316 'section' => 'T-new',
317 ] );
320 public function testSectionNoContent() {
321 $name = ucfirst( __FUNCTION__ );
323 $status = $this->editPage( $name,
324 "Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" );
326 $this->expectApiErrorCode( 'missingcontent-pageid' );
328 $this->getDb()->newDeleteQueryBuilder()
329 ->deleteFrom( 'revision' )
330 ->where( [ 'rev_id' => $status->getNewRevision()->getId() ] )
331 ->caller( __METHOD__ )
332 ->execute();
334 // Ignore warning from WikiPage::getContentModel
335 @$this->doApiRequest( [
336 'action' => 'parse',
337 'page' => $name,
338 'section' => 1,
339 ] );
342 public function testNewSectionWithPage() {
343 $this->expectApiErrorCode( 'invalidparammix' );
345 $this->doApiRequest( [
346 'action' => 'parse',
347 'page' => __CLASS__,
348 'section' => 'new',
349 ] );
352 public function testNonexistentOldId() {
353 $this->expectApiErrorCode( 'nosuchrevid' );
355 $this->doApiRequest( [
356 'action' => 'parse',
357 'oldid' => pow( 2, 31 ) - 1,
358 ] );
361 public function testUnfollowedRedirect() {
362 $name = ucfirst( __FUNCTION__ );
364 $this->editPage( $name, "#REDIRECT [[$name 2]]" );
365 $this->editPage( "$name 2", "Some ''text''" );
367 $res = $this->doApiRequest( [
368 'action' => 'parse',
369 'page' => $name,
370 ] );
372 // Can't use assertParsedTo because the parser output is different for
373 // redirects
374 $this->assertMatchesRegularExpression( "/Redirect to:.*$name 2/", $res[0]['parse']['text'] );
375 $this->assertArrayNotHasKey( 'warnings', $res[0] );
378 public function testFollowedRedirect() {
379 $name = ucfirst( __FUNCTION__ );
381 $this->editPage( $name, "#REDIRECT [[$name 2]]" );
382 $this->editPage( "$name 2", "Some ''text''" );
384 $res = $this->doApiRequest( [
385 'action' => 'parse',
386 'page' => $name,
387 'redirects' => true,
388 ] );
390 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
393 public function testFollowedRedirectById() {
394 $name = ucfirst( __FUNCTION__ );
396 $id = $this->editPage( $name, "#REDIRECT [[$name 2]]" )
397 ->getNewRevision()->getPageId();
398 $this->editPage( "$name 2", "Some ''text''" );
400 $res = $this->doApiRequest( [
401 'action' => 'parse',
402 'pageid' => $id,
403 'redirects' => true,
404 ] );
406 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
409 public function testNonRedirectOk() {
410 $name = ucfirst( __FUNCTION__ );
412 $this->editPage( $name, "Some ''text''" );
414 $res = $this->doApiRequest( [
415 'action' => 'parse',
416 'page' => $name,
417 'redirects' => true,
418 ] );
420 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
423 public function testNonRedirectByIdOk() {
424 $name = ucfirst( __FUNCTION__ );
426 $id = $this->editPage( $name, "Some ''text''" )->getNewRevision()->getPageId();
428 $res = $this->doApiRequest( [
429 'action' => 'parse',
430 'pageid' => $id,
431 'redirects' => true,
432 ] );
434 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
437 public function testInvalidTitle() {
438 $this->expectApiErrorCode( 'invalidtitle' );
440 $this->doApiRequest( [
441 'action' => 'parse',
442 'title' => '|',
443 ] );
446 public function testTitleWithNonexistentRevId() {
447 $this->expectApiErrorCode( 'nosuchrevid' );
449 $this->doApiRequest( [
450 'action' => 'parse',
451 'title' => __CLASS__,
452 'revid' => pow( 2, 31 ) - 1,
453 ] );
456 public function testTitleWithNonMatchingRevId() {
457 $name = ucfirst( __FUNCTION__ );
459 $res = $this->doApiRequest( [
460 'action' => 'parse',
461 'title' => $name,
462 'revid' => self::$revIds['latest'],
463 'text' => 'Some text',
464 ] );
466 $this->assertParsedTo( "<p>Some text\n</p>", $res,
467 'r' . self::$revIds['latest'] . " is not a revision of $name." );
470 public function testRevId() {
471 $res = $this->doApiRequest( [
472 'action' => 'parse',
473 'revid' => self::$revIds['latest'],
474 'text' => 'My revid is {{REVISIONID}}!',
475 ] );
477 $this->assertParsedTo( "<p>My revid is " . self::$revIds['latest'] . "!\n</p>", $res );
480 public function testTitleNoText() {
481 $res = $this->doApiRequest( [
482 'action' => 'parse',
483 'title' => 'Special:AllPages',
484 ] );
486 $this->assertParsedTo( '', $res,
487 '"title" used without "text", and parsed page properties were requested. ' .
488 'Did you mean to use "page" instead of "title"?' );
491 public function testRevidNoText() {
492 $res = $this->doApiRequest( [
493 'action' => 'parse',
494 'revid' => self::$revIds['latest'],
495 ] );
497 $this->assertParsedTo( '', $res,
498 '"revid" used without "text", and parsed page properties were requested. ' .
499 'Did you mean to use "oldid" instead of "revid"?' );
502 public function testTextNoContentModel() {
503 $res = $this->doApiRequest( [
504 'action' => 'parse',
505 'text' => "Some ''text''",
506 ] );
508 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res,
509 'No "title" or "contentmodel" was given, assuming wikitext.' );
512 public function testSerializationError() {
513 $this->expectApiErrorCode( 'parseerror' );
515 $this->mergeMwGlobalArrayValue( 'wgContentHandlers',
516 [ 'testing-serialize-error' => 'DummySerializeErrorContentHandler' ] );
518 $this->doApiRequest( [
519 'action' => 'parse',
520 'text' => "Some ''text''",
521 'contentmodel' => 'testing-serialize-error',
522 ] );
525 public function testNewSection() {
526 $res = $this->doApiRequest( [
527 'action' => 'parse',
528 'title' => __CLASS__,
529 'section' => 'new',
530 'sectiontitle' => 'Title',
531 'text' => 'Content',
532 ] );
534 $this->assertParsedToRegExp( '!<h2[^>]*>.*Title.*</h2>.*\n<p>Content\n</p>!', $res );
537 public function testExistingSection() {
538 $res = $this->doApiRequest( [
539 'action' => 'parse',
540 'title' => __CLASS__,
541 'section' => 1,
542 'text' => "Intro\n\n== Section 1 ==\n\nContent\n\n== Section 2 ==\n\nMore content",
543 ] );
545 $this->assertParsedToRegExp( '!<h2[^>]*>.*Section 1.*</h2>.*\n<p>Content\n</p>!', $res );
548 public function testNoPst() {
549 $name = ucfirst( __FUNCTION__ );
551 $this->editPage( "Template:$name", "Template ''text''" );
553 $res = $this->doApiRequest( [
554 'action' => 'parse',
555 'text' => "{{subst:$name}}",
556 'contentmodel' => 'wikitext',
557 ] );
559 $this->assertParsedTo( "<p>{{subst:$name}}\n</p>", $res );
562 public function testPst() {
563 $name = ucfirst( __FUNCTION__ );
565 $this->editPage( "Template:$name", "Template ''text''" );
567 $res = $this->doApiRequest( [
568 'action' => 'parse',
569 'pst' => '',
570 'text' => "{{subst:$name}}",
571 'contentmodel' => 'wikitext',
572 'prop' => 'text|wikitext',
573 ] );
575 $this->assertParsedTo( "<p>Template <i>text</i>\n</p>", $res );
576 $this->assertSame( "{{subst:$name}}", $res[0]['parse']['wikitext'] );
579 public function testOnlyPst() {
580 $name = ucfirst( __FUNCTION__ );
582 $this->editPage( "Template:$name", "Template ''text''" );
584 $res = $this->doApiRequest( [
585 'action' => 'parse',
586 'onlypst' => '',
587 'text' => "{{subst:$name}}",
588 'contentmodel' => 'wikitext',
589 'prop' => 'text|wikitext',
590 'summary' => 'Summary',
591 ] );
593 $this->assertSame(
594 [ 'parse' => [
595 'text' => "Template ''text''",
596 'wikitext' => "{{subst:$name}}",
597 'parsedsummary' => 'Summary',
598 ] ],
599 $res[0]
603 /** @dataProvider providerTestParsoid */
604 public function testParsoid( $parsoid, $existing, $expected ) {
605 # For simplicity, ensure that [[Foo]] isn't a redlink.
606 $this->editPage( "Foo", __FUNCTION__ );
607 $res = $this->doApiRequest( [
608 # check that we're using the contents of 'text' not the contents of
609 # [[<title>]] by using pre-existing title __CLASS__ sometimes
610 'title' => $existing ? __CLASS__ : 'Bar',
611 'action' => 'parse',
612 'text' => "[[Foo]]",
613 'contentmodel' => 'wikitext',
614 'parsoid' => $parsoid ?: null,
615 'disablelimitreport' => true,
616 ] );
618 $this->assertParsedToRegexp( $expected, $res );
621 public static function providerTestParsoid() {
622 // Legacy parses, with and without pre-existing content.
623 $expected = '!^<p><a href="[^"]*" title="Foo">Foo</a>\n</p>$!';
624 yield [ false, false, $expected ];
625 yield [ false, true, $expected ];
626 // Parsoid parses, with and without pre-existing content.
627 $expected = '!^<section[^>]*><p[^>]*><a rel="mw:WikiLink" href="[^"]*Foo" title="Foo"[^>]*>Foo</a></p></section>!';
628 yield [ true, false, $expected ];
629 yield [ true, true, $expected ];
632 /** @dataProvider providerTestParsoid */
633 public function testUseArticle( $parsoid, $existing, $expected ) {
634 # For simplicity, ensure that [[Foo]] isn't a redlink.
635 $this->editPage( "Foo", __FUNCTION__ );
636 # Use an ArticleParserOptions hook to set the useParsoid option
637 $this->setTemporaryHook( 'ArticleParserOptions',
638 static function ( $unused, $po ) use ( $parsoid ) {
639 if ( $parsoid ) {
640 $po->setUseParsoid();
645 $res = $this->doApiRequest( [
646 # check that we're using the contents of 'text' not the contents of
647 # [[<title>]] by using pre-existing title __CLASS__ sometimes
648 'title' => $existing ? __CLASS__ : 'Bar',
649 'action' => 'parse',
650 'text' => "[[Foo]]",
651 'contentmodel' => 'wikitext',
652 'usearticle' => true,
653 # Note that we're not passing the 'parsoid' parameter here.
654 'disablelimitreport' => true,
655 ] );
657 $this->assertParsedToRegexp( $expected, $res );
660 public function testHeadHtml() {
661 $res = $this->doApiRequest( [
662 'action' => 'parse',
663 'page' => __CLASS__,
664 'prop' => 'headhtml',
665 ] );
667 // Just do a rough check
668 $this->assertMatchesRegularExpression( '#<!DOCTYPE.*<html.*<head.*</head>.*<body#s',
669 $res[0]['parse']['headhtml'] );
670 $this->assertArrayNotHasKey( 'warnings', $res[0] );
673 public function testCategoriesHtml() {
674 $name = ucfirst( __FUNCTION__ );
676 $this->editPage( $name, "[[Category:$name]]" );
678 $res = $this->doApiRequest( [
679 'action' => 'parse',
680 'page' => $name,
681 'prop' => 'categorieshtml',
682 ] );
684 $this->assertMatchesRegularExpression( "#Category.*Category:$name.*$name#",
685 $res[0]['parse']['categorieshtml'] );
686 $this->assertArrayNotHasKey( 'warnings', $res[0] );
689 public function testEffectiveLangLinks() {
690 $hookRan = false;
691 $this->setTemporaryHook( 'LanguageLinks',
692 static function () use ( &$hookRan ) {
693 $hookRan = true;
697 $res = $this->doApiRequest( [
698 'action' => 'parse',
699 'title' => __CLASS__,
700 'text' => '[[zh:' . __CLASS__ . ']]',
701 'effectivelanglinks' => '',
702 ] );
704 $this->assertTrue( $hookRan );
705 $this->assertSame( 'The parameter "effectivelanglinks" has been deprecated.',
706 $res[0]['warnings']['parse']['warnings'] );
710 * @param array $arr Extra params to add to API request
712 private function doTestLangLinks( array $arr = [] ) {
713 $this->setTemporaryHook( 'ParserAfterParse',
714 static function ( $parser ) {
715 $parserOutput = $parser->getOutput();
716 $parserOutput->addLanguageLink( 'talk:Page' ); // T363538
719 $res = $this->doApiRequest( array_merge( [
720 'action' => 'parse',
721 'title' => 'Omelette',
722 'text' => '[[madeuplanguage:Omelette]]',
723 'prop' => 'langlinks',
724 ], $arr ) );
726 $langLinks = $res[0]['parse']['langlinks'];
728 $this->assertCount( 2, $langLinks );
729 $this->assertSame( 'madeuplanguage', $langLinks[0]['lang'] );
730 $this->assertSame( 'Omelette', $langLinks[0]['title'] );
731 $this->assertSame( 'https://example.com/wiki/Omelette', $langLinks[0]['url'] );
732 $this->assertSame( 'talk', $langLinks[1]['lang'] );
733 $this->assertSame( 'Page', $langLinks[1]['title'] );
734 $this->assertSame( 'https://talk.example.com/wiki/Page', $langLinks[1]['url'] );
735 $this->assertArrayNotHasKey( 'warnings', $res[0] );
738 public function testLangLinks() {
739 $this->setupInterwiki();
740 $this->doTestLangLinks();
743 public function testLangLinksWithSkin() {
744 $this->setupInterwiki();
745 $this->setupSkin();
746 $this->doTestLangLinks( [ 'useskin' => 'testing' ] );
749 public function testHeadItems() {
750 $res = $this->doApiRequest( [
751 'action' => 'parse',
752 'title' => __CLASS__,
753 'text' => '',
754 'prop' => 'headitems',
755 ] );
757 $this->assertSame( [], $res[0]['parse']['headitems'] );
758 $this->assertSame(
759 '"prop=headitems" is deprecated since MediaWiki 1.28. ' .
760 'Use "prop=headhtml" when creating new HTML documents, ' .
761 'or "prop=modules|jsconfigvars" when updating a document client-side.',
762 $res[0]['warnings']['parse']['warnings']
766 public function testHeadItemsWithSkin() {
767 $this->setupSkin();
769 $res = $this->doApiRequest( [
770 'action' => 'parse',
771 'title' => __CLASS__,
772 'text' => '',
773 'prop' => 'headitems',
774 'useskin' => 'testing',
775 ] );
777 $this->assertSame( [], $res[0]['parse']['headitems'] );
778 $this->assertSame(
779 '"prop=headitems" is deprecated since MediaWiki 1.28. ' .
780 'Use "prop=headhtml" when creating new HTML documents, ' .
781 'or "prop=modules|jsconfigvars" when updating a document client-side.',
782 $res[0]['warnings']['parse']['warnings']
786 public function testModules() {
787 $this->setTemporaryHook( 'ParserAfterParse',
788 static function ( $parser ) {
789 $parserOutput = $parser->getOutput();
790 $parserOutput->addModules( [ 'foo', 'bar' ] );
791 $parserOutput->addModuleStyles( [ 'aaa', 'zzz' ] );
792 $parserOutput->setJsConfigVar( 'x', 'y' );
793 $parserOutput->setJsConfigVar( 'z', -3 );
796 $res = $this->doApiRequest( [
797 'action' => 'parse',
798 'title' => __CLASS__,
799 'text' => 'Content',
800 'prop' => 'modules|jsconfigvars|encodedjsconfigvars',
801 ] );
803 $this->assertSame( [ 'foo', 'bar' ], $res[0]['parse']['modules'] );
804 $this->assertSame( [], $res[0]['parse']['modulescripts'] );
805 $this->assertSame( [ 'aaa', 'zzz' ], $res[0]['parse']['modulestyles'] );
806 $this->assertSame( [ 'x' => 'y', 'z' => -3 ], $res[0]['parse']['jsconfigvars'] );
807 $this->assertSame( '{"x":"y","z":-3}', $res[0]['parse']['encodedjsconfigvars'] );
808 $this->assertArrayNotHasKey( 'warnings', $res[0] );
811 public function testModulesWithSkin() {
812 $this->setupSkin();
814 $res = $this->doApiRequest( [
815 'action' => 'parse',
816 'pageid' => self::$pageId,
817 'useskin' => 'testing',
818 'prop' => 'modules',
819 ] );
820 $this->assertSame(
821 [ 'foo', 'bar', 'baz' ],
822 $res[0]['parse']['modules'],
823 'resp.parse.modules'
825 $this->assertSame(
827 $res[0]['parse']['modulescripts'],
828 'resp.parse.modulescripts'
830 $this->assertSame(
831 [ 'quux.styles' ],
832 $res[0]['parse']['modulestyles'],
833 'resp.parse.modulestyles'
835 $this->assertSame(
836 [ 'parse' =>
837 [ 'warnings' =>
838 'Property "modules" was set but not "jsconfigvars" or ' .
839 '"encodedjsconfigvars". Configuration variables are necessary for ' .
840 'proper module usage.'
843 $res[0]['warnings']
847 public function testIndicators() {
848 $res = $this->doApiRequest( [
849 'action' => 'parse',
850 'title' => __CLASS__,
851 'text' =>
852 '<indicator name="b">BBB!</indicator>Some text<indicator name="a">aaa</indicator>',
853 'prop' => 'indicators',
854 ] );
856 $this->assertSame(
857 // It seems we return in markup order and not display order
858 [ 'b' => 'BBB!', 'a' => 'aaa' ],
859 $res[0]['parse']['indicators']
861 $this->assertArrayNotHasKey( 'warnings', $res[0] );
864 public function testIndicatorsWithSkin() {
865 $this->setupSkin();
867 $res = $this->doApiRequest( [
868 'action' => 'parse',
869 'title' => __CLASS__,
870 'text' =>
871 '<indicator name="b">BBB!</indicator>Some text<indicator name="a">aaa</indicator>',
872 'prop' => 'indicators',
873 'useskin' => 'testing',
874 ] );
876 $this->assertSame(
877 // Now we return in display order rather than markup order
879 'a' => '<div class="mw-parser-output">aaa</div>',
880 'b' => '<div class="mw-parser-output">BBB!</div>',
882 $res[0]['parse']['indicators']
884 $this->assertArrayNotHasKey( 'warnings', $res[0] );
887 public function testIwlinks() {
888 $this->setupInterwiki();
890 $res = $this->doApiRequest( [
891 'action' => 'parse',
892 'title' => 'Omelette',
893 'text' => '[[:madeuplanguage:Omelette]][[madeuplanguage:Spaghetti]]',
894 'prop' => 'iwlinks',
895 ] );
897 $iwlinks = $res[0]['parse']['iwlinks'];
899 $this->assertCount( 1, $iwlinks );
900 $this->assertSame( 'madeuplanguage', $iwlinks[0]['prefix'] );
901 $this->assertSame( 'https://example.com/wiki/Omelette', $iwlinks[0]['url'] );
902 $this->assertSame( 'madeuplanguage:Omelette', $iwlinks[0]['title'] );
903 $this->assertArrayNotHasKey( 'warnings', $res[0] );
906 public function testLimitReports() {
907 $res = $this->doApiRequest( [
908 'action' => 'parse',
909 'pageid' => self::$pageId,
910 'prop' => 'limitreportdata|limitreporthtml',
911 ] );
913 // We don't bother testing the actual values here
914 $this->assertIsArray( $res[0]['parse']['limitreportdata'] );
915 $this->assertIsString( $res[0]['parse']['limitreporthtml'] );
916 $this->assertArrayNotHasKey( 'warnings', $res[0] );
919 public function testParseTreeNonWikitext() {
920 $this->expectApiErrorCode( 'notwikitext' );
922 $this->doApiRequest( [
923 'action' => 'parse',
924 'text' => '',
925 'contentmodel' => 'json',
926 'prop' => 'parsetree',
927 ] );
930 public function testParseTree() {
931 $res = $this->doApiRequest( [
932 'action' => 'parse',
933 'text' => "Some ''text'' is {{nice|to have|i=think}}",
934 'contentmodel' => 'wikitext',
935 'prop' => 'parsetree',
936 ] );
938 $this->assertEquals(
939 '<root>Some \'\'text\'\' is <template><title>nice</title>' .
940 '<part><name index="1"/><value>to have</value></part>' .
941 '<part><name>i</name><equals>=</equals><value>think</value></part>' .
942 '</template></root>',
943 $res[0]['parse']['parsetree']
945 $this->assertArrayNotHasKey( 'warnings', $res[0] );
948 public function testFormatCategories() {
949 $name = ucfirst( __FUNCTION__ );
951 $this->editPage( "Category:$name", 'Content' );
952 $this->editPage( 'Category:Hidden', '__HIDDENCAT__' );
954 $res = $this->doApiRequest( [
955 'action' => 'parse',
956 'title' => __CLASS__,
957 'text' => "[[Category:$name]][[Category:Foo|Sort me]][[Category:Hidden]]",
958 'prop' => 'categories',
959 ] );
961 $this->assertSame(
962 [ [ 'sortkey' => '', 'category' => $name ],
963 [ 'sortkey' => 'Sort me', 'category' => 'Foo', 'missing' => true ],
964 [ 'sortkey' => '', 'category' => 'Hidden', 'hidden' => true ] ],
965 $res[0]['parse']['categories']
967 $this->assertArrayNotHasKey( 'warnings', $res[0] );
970 public function testConcurrentLimitPageParse() {
971 $this->overrideConfigValue(
972 MainConfigNames::PoolCounterConf,
974 'ApiParser' => [
975 'class' => MockPoolCounterFailing::class,
980 try {
981 $this->doApiRequest( [
982 'action' => 'parse',
983 'page' => __CLASS__,
984 ] );
985 $this->fail( "API did not return an error when concurrency exceeded" );
986 } catch ( ApiUsageException $ex ) {
987 $this->assertApiErrorCode( 'concurrency-limit', $ex );
991 public function testConcurrentLimitContentParse() {
992 $this->overrideConfigValue(
993 MainConfigNames::PoolCounterConf,
995 'ApiParser' => [
996 'class' => MockPoolCounterFailing::class,
1001 try {
1002 $this->doApiRequest( [
1003 'action' => 'parse',
1004 'oldid' => self::$revIds['revdel'],
1005 ] );
1006 $this->fail( "API did not return an error when concurrency exceeded" );
1007 } catch ( ApiUsageException $ex ) {
1008 $this->assertApiErrorCode( 'concurrency-limit', $ex );
1012 public function testDisplayTitle() {
1013 $res = $this->doApiRequest( [
1014 'action' => 'parse',
1015 'title' => 'Art&copy',
1016 'text' => '{{DISPLAYTITLE:art&copy}}foo',
1017 'prop' => 'displaytitle',
1018 ] );
1020 $this->assertSame(
1021 'art&amp;copy',
1022 $res[0]['parse']['displaytitle']
1025 $res = $this->doApiRequest( [
1026 'action' => 'parse',
1027 'title' => 'Art&copy',
1028 'text' => 'foo',
1029 'prop' => 'displaytitle',
1030 ] );
1032 $this->assertSame(
1033 '<span class="mw-page-title-main">Art&amp;copy</span>',
1034 $res[0]['parse']['displaytitle']
1038 public function testIncompatFormat() {
1039 $this->expectApiErrorCode( 'badformat-generic' );
1041 $this->doApiRequest( [
1042 'action' => 'parse',
1043 'prop' => 'categories',
1044 'title' => __CLASS__,
1045 'text' => '',
1046 'contentformat' => 'application/json',
1047 ] );
1050 public function testIgnoreFormatUsingPage() {
1051 $res = $this->doApiRequest( [
1052 'action' => 'parse',
1053 'page' => __CLASS__,
1054 'prop' => 'wikitext',
1055 'contentformat' => 'text/plain',
1056 ] );
1057 $this->assertArrayHasKey( 'wikitext', $res[0]['parse'] );
1060 public function testShouldCastNumericImageLinksToString(): void {
1061 $res = $this->doApiRequest( [
1062 'action' => 'parse',
1063 'title' => __CLASS__,
1064 'prop' => 'images',
1065 'text' => '[[File:1]]',
1066 ] );
1067 $this->assertSame( [ '1' ], $res[0]['parse']['images'] );