Rename JsonUnserial… to JsonDeserial…
[mediawiki.git] / tests / phpunit / includes / api / ApiParseTest.php
blob99aaf45a7df05784dde1687b4def3d68009d3856
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 ApiUsageException;
27 use MediaWiki\MainConfigNames;
28 use MediaWiki\Revision\RevisionRecord;
29 use MediaWiki\Tests\Unit\DummyServicesTrait;
30 use MediaWiki\Title\TitleValue;
31 use MockPoolCounterFailing;
32 use SkinFactory;
33 use SkinFallback;
35 /**
36 * @group API
37 * @group Database
38 * @group medium
40 * @covers \ApiParse
42 class ApiParseTest extends ApiTestCase {
43 use DummyServicesTrait;
45 protected static $pageId;
46 protected static $revIds = [];
48 public function addDBDataOnce() {
49 $page = $this->getServiceContainer()->getWikiPageFactory()
50 ->newFromLinkTarget( new TitleValue( NS_MAIN, __CLASS__ ) );
51 $status = $this->editPage( $page, 'Test for revdel' );
52 self::$pageId = $status->getNewRevision()->getPageId();
53 self::$revIds['revdel'] = $status->getNewRevision()->getId();
55 $status = $this->editPage( $page, 'Test for suppressed' );
56 self::$revIds['suppressed'] = $status->getNewRevision()->getId();
58 $status = $this->editPage( $page, 'Test for oldid' );
59 self::$revIds['oldid'] = $status->getNewRevision()->getId();
61 $status = $this->editPage( $page, 'Test for latest' );
62 self::$revIds['latest'] = $status->getNewRevision()->getId();
64 $this->revisionDelete( self::$revIds['revdel'] );
65 $this->revisionDelete(
66 self::$revIds['suppressed'],
68 RevisionRecord::DELETED_TEXT => 1,
69 RevisionRecord::DELETED_RESTRICTED => 1
74 /**
75 * Assert that the given result of calling $this->doApiRequest() with
76 * action=parse resulted in $html, accounting for the boilerplate that the
77 * parser adds around the parsed page. Also asserts that warnings match
78 * the provided $warning.
80 * @param string $expected Expected HTML
81 * @param array $res Returned from doApiRequest()
82 * @param string|null $warnings Exact value of expected warnings, null for
83 * no warnings
85 protected function assertParsedTo( $expected, array $res, $warnings = null ) {
86 $this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertSame' ] );
89 /**
90 * Same as above, but asserts that the HTML matches a regexp instead of a
91 * literal string match.
93 * @param string $expected Expected HTML
94 * @param array $res Returned from doApiRequest()
95 * @param string|null $warnings Exact value of expected warnings, null for
96 * no warnings
98 protected function assertParsedToRegExp( $expected, array $res, $warnings = null ) {
99 $this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertMatchesRegularExpression' ] );
102 private function doAssertParsedTo( $expected, array $res, $warnings, callable $callback ) {
103 $html = $res[0]['parse']['text'];
105 $expectedStart = '<div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr"';
106 $this->assertSame( $expectedStart, substr( $html, 0, strlen( $expectedStart ) ) );
108 $html = substr( $html, strlen( $expectedStart ) );
110 # Parsoid-based transformations may add ID and data-mw-parsoid-version
111 # attributes to the wrapper div
112 $possibleIdAttr = '/^( (id|data-mw[^=]*)="[^"]+")*>/';
113 $html = preg_replace( $possibleIdAttr, '', $html );
115 $possibleParserCache = '/\n<!-- Saved in (?>parser cache|RevisionOutputCache) (?>.*?\n -->)\n/';
116 $html = preg_replace( $possibleParserCache, '', $html );
118 if ( $res[1]->getBool( 'disablelimitreport' ) ) {
119 $expectedEnd = "</div>";
120 $this->assertSame( $expectedEnd, substr( $html, -strlen( $expectedEnd ) ) );
122 $unexpectedEnd = '#<!-- \nNewPP limit report|' .
123 '<!--\nTransclusion expansion time report#';
124 $this->assertDoesNotMatchRegularExpression( $unexpectedEnd, $html );
126 $html = substr( $html, 0, strlen( $html ) - strlen( $expectedEnd ) );
127 } else {
128 $expectedEnd = '#\n<!-- \nNewPP limit report\n(?>.+?\n-->)\n' .
129 '<!--\nTransclusion expansion time report \(%,ms,calls,template\)\n(?>.*?\n-->)\n' .
130 '</div>$#s';
131 $this->assertMatchesRegularExpression( $expectedEnd, $html );
133 $html = preg_replace( $expectedEnd, '', $html );
136 $callback( $expected, $html );
138 if ( $warnings === null ) {
139 $this->assertCount( 1, $res[0] );
140 } else {
141 $this->assertCount( 2, $res[0] );
142 $this->assertSame( [ 'warnings' => $warnings ], $res[0]['warnings']['parse'] );
147 * Set up an interwiki entry for testing.
149 protected function setupInterwiki() {
150 $this->getDb()->newInsertQueryBuilder()
151 ->insertInto( 'interwiki' )
152 ->ignore()
153 ->row( [
154 'iw_prefix' => 'madeuplanguage',
155 'iw_url' => "https://example.com/wiki/$1",
156 'iw_api' => '',
157 'iw_wikiid' => '',
158 'iw_local' => false,
160 ->caller( __METHOD__ )
161 ->execute();
163 $this->overrideConfigValue(
164 MainConfigNames::ExtraInterlanguageLinkPrefixes,
165 [ 'madeuplanguage' ]
170 * Set up a skin for testing.
172 * @todo Should this code be in MediaWikiIntegrationTestCase or something?
174 protected function setupSkin() {
175 $factory = new SkinFactory( $this->getDummyObjectFactory(), [] );
176 $factory->register( 'testing', 'Testing', function () {
177 $skin = $this->getMockBuilder( SkinFallback::class )
178 ->onlyMethods( [ 'getDefaultModules' ] )
179 ->getMock();
180 $skin->expects( $this->once() )->method( 'getDefaultModules' )
181 ->willReturn( [
182 'styles' => [ 'core' => [ 'quux.styles' ] ],
183 'core' => [ 'foo', 'bar' ],
184 'content' => [ 'baz' ]
185 ] );
186 return $skin;
187 } );
188 $this->setService( 'SkinFactory', $factory );
191 public function testParseByName() {
192 $res = $this->doApiRequest( [
193 'action' => 'parse',
194 'page' => __CLASS__,
195 ] );
196 $this->assertParsedTo( "<p>Test for latest\n</p>", $res );
198 $res = $this->doApiRequest( [
199 'action' => 'parse',
200 'page' => __CLASS__,
201 'disablelimitreport' => 1,
202 ] );
203 $this->assertParsedTo( "<p>Test for latest\n</p>", $res );
206 public function testParseById() {
207 $res = $this->doApiRequest( [
208 'action' => 'parse',
209 'pageid' => self::$pageId,
210 ] );
211 $this->assertParsedTo( "<p>Test for latest\n</p>", $res );
214 public function testParseByOldId() {
215 $res = $this->doApiRequest( [
216 'action' => 'parse',
217 'oldid' => self::$revIds['oldid'],
218 ] );
219 $this->assertParsedTo( "<p>Test for oldid\n</p>", $res );
220 $this->assertArrayNotHasKey( 'textdeleted', $res[0]['parse'] );
221 $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] );
224 public function testRevDel() {
225 $res = $this->doApiRequest( [
226 'action' => 'parse',
227 'oldid' => self::$revIds['revdel'],
228 ] );
230 $this->assertParsedTo( "<p>Test for revdel\n</p>", $res );
231 $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] );
232 $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] );
235 public function testRevDelNoPermission() {
236 $this->expectApiErrorCode( 'permissiondenied' );
238 $this->doApiRequest( [
239 'action' => 'parse',
240 'oldid' => self::$revIds['revdel'],
241 ], null, null, static::getTestUser()->getAuthority() );
244 public function testSuppressed() {
245 $this->setGroupPermissions( 'sysop', 'viewsuppressed', true );
247 $res = $this->doApiRequest( [
248 'action' => 'parse',
249 'oldid' => self::$revIds['suppressed']
250 ] );
252 $this->assertParsedTo( "<p>Test for suppressed\n</p>", $res );
253 $this->assertArrayHasKey( 'textsuppressed', $res[0]['parse'] );
254 $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] );
257 public function testNonexistentPage() {
258 try {
259 $this->doApiRequest( [
260 'action' => 'parse',
261 'page' => 'DoesNotExist',
262 ] );
264 $this->fail( "API did not return an error when parsing a nonexistent page" );
265 } catch ( ApiUsageException $ex ) {
266 $this->assertApiErrorCode( 'missingtitle', $ex );
270 public function testTitleProvided() {
271 $res = $this->doApiRequest( [
272 'action' => 'parse',
273 'title' => 'Some interesting page',
274 'text' => '{{PAGENAME}} has attracted my attention',
275 ] );
277 $this->assertParsedTo( "<p>Some interesting page has attracted my attention\n</p>", $res );
280 public function testSection() {
281 $name = ucfirst( __FUNCTION__ );
283 $this->editPage( $name,
284 "Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" );
286 $res = $this->doApiRequest( [
287 'action' => 'parse',
288 'page' => $name,
289 'section' => 1,
290 ] );
292 $this->assertParsedToRegExp( '!<h2[^>]*>.*Section 1.*</h2>.*\n<p>Content 1\n</p>!', $res );
295 public function testInvalidSection() {
296 $this->expectApiErrorCode( 'invalidsection' );
298 $this->doApiRequest( [
299 'action' => 'parse',
300 'section' => 'T-new',
301 ] );
304 public function testSectionNoContent() {
305 $name = ucfirst( __FUNCTION__ );
307 $status = $this->editPage( $name,
308 "Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" );
310 $this->expectApiErrorCode( 'missingcontent-pageid' );
312 $this->db->newDeleteQueryBuilder()
313 ->deleteFrom( 'revision' )
314 ->where( [ 'rev_id' => $status->getNewRevision()->getId() ] )
315 ->caller( __METHOD__ )
316 ->execute();
318 // Ignore warning from WikiPage::getContentModel
319 @$this->doApiRequest( [
320 'action' => 'parse',
321 'page' => $name,
322 'section' => 1,
323 ] );
326 public function testNewSectionWithPage() {
327 $this->expectApiErrorCode( 'invalidparammix' );
329 $this->doApiRequest( [
330 'action' => 'parse',
331 'page' => __CLASS__,
332 'section' => 'new',
333 ] );
336 public function testNonexistentOldId() {
337 $this->expectApiErrorCode( 'nosuchrevid' );
339 $this->doApiRequest( [
340 'action' => 'parse',
341 'oldid' => pow( 2, 31 ) - 1,
342 ] );
345 public function testUnfollowedRedirect() {
346 $name = ucfirst( __FUNCTION__ );
348 $this->editPage( $name, "#REDIRECT [[$name 2]]" );
349 $this->editPage( "$name 2", "Some ''text''" );
351 $res = $this->doApiRequest( [
352 'action' => 'parse',
353 'page' => $name,
354 ] );
356 // Can't use assertParsedTo because the parser output is different for
357 // redirects
358 $this->assertMatchesRegularExpression( "/Redirect to:.*$name 2/", $res[0]['parse']['text'] );
359 $this->assertArrayNotHasKey( 'warnings', $res[0] );
362 public function testFollowedRedirect() {
363 $name = ucfirst( __FUNCTION__ );
365 $this->editPage( $name, "#REDIRECT [[$name 2]]" );
366 $this->editPage( "$name 2", "Some ''text''" );
368 $res = $this->doApiRequest( [
369 'action' => 'parse',
370 'page' => $name,
371 'redirects' => true,
372 ] );
374 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
377 public function testFollowedRedirectById() {
378 $name = ucfirst( __FUNCTION__ );
380 $id = $this->editPage( $name, "#REDIRECT [[$name 2]]" )
381 ->getNewRevision()->getPageId();
382 $this->editPage( "$name 2", "Some ''text''" );
384 $res = $this->doApiRequest( [
385 'action' => 'parse',
386 'pageid' => $id,
387 'redirects' => true,
388 ] );
390 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
393 public function testNonRedirectOk() {
394 $name = ucfirst( __FUNCTION__ );
396 $this->editPage( $name, "Some ''text''" );
398 $res = $this->doApiRequest( [
399 'action' => 'parse',
400 'page' => $name,
401 'redirects' => true,
402 ] );
404 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
407 public function testNonRedirectByIdOk() {
408 $name = ucfirst( __FUNCTION__ );
410 $id = $this->editPage( $name, "Some ''text''" )->getNewRevision()->getPageId();
412 $res = $this->doApiRequest( [
413 'action' => 'parse',
414 'pageid' => $id,
415 'redirects' => true,
416 ] );
418 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
421 public function testInvalidTitle() {
422 $this->expectApiErrorCode( 'invalidtitle' );
424 $this->doApiRequest( [
425 'action' => 'parse',
426 'title' => '|',
427 ] );
430 public function testTitleWithNonexistentRevId() {
431 $this->expectApiErrorCode( 'nosuchrevid' );
433 $this->doApiRequest( [
434 'action' => 'parse',
435 'title' => __CLASS__,
436 'revid' => pow( 2, 31 ) - 1,
437 ] );
440 public function testTitleWithNonMatchingRevId() {
441 $name = ucfirst( __FUNCTION__ );
443 $res = $this->doApiRequest( [
444 'action' => 'parse',
445 'title' => $name,
446 'revid' => self::$revIds['latest'],
447 'text' => 'Some text',
448 ] );
450 $this->assertParsedTo( "<p>Some text\n</p>", $res,
451 'r' . self::$revIds['latest'] . " is not a revision of $name." );
454 public function testRevId() {
455 $res = $this->doApiRequest( [
456 'action' => 'parse',
457 'revid' => self::$revIds['latest'],
458 'text' => 'My revid is {{REVISIONID}}!',
459 ] );
461 $this->assertParsedTo( "<p>My revid is " . self::$revIds['latest'] . "!\n</p>", $res );
464 public function testTitleNoText() {
465 $res = $this->doApiRequest( [
466 'action' => 'parse',
467 'title' => 'Special:AllPages',
468 ] );
470 $this->assertParsedTo( '', $res,
471 '"title" used without "text", and parsed page properties were requested. ' .
472 'Did you mean to use "page" instead of "title"?' );
475 public function testRevidNoText() {
476 $res = $this->doApiRequest( [
477 'action' => 'parse',
478 'revid' => self::$revIds['latest'],
479 ] );
481 $this->assertParsedTo( '', $res,
482 '"revid" used without "text", and parsed page properties were requested. ' .
483 'Did you mean to use "oldid" instead of "revid"?' );
486 public function testTextNoContentModel() {
487 $res = $this->doApiRequest( [
488 'action' => 'parse',
489 'text' => "Some ''text''",
490 ] );
492 $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res,
493 'No "title" or "contentmodel" was given, assuming wikitext.' );
496 public function testSerializationError() {
497 $this->expectApiErrorCode( 'parseerror' );
499 $this->mergeMwGlobalArrayValue( 'wgContentHandlers',
500 [ 'testing-serialize-error' => 'DummySerializeErrorContentHandler' ] );
502 $this->doApiRequest( [
503 'action' => 'parse',
504 'text' => "Some ''text''",
505 'contentmodel' => 'testing-serialize-error',
506 ] );
509 public function testNewSection() {
510 $res = $this->doApiRequest( [
511 'action' => 'parse',
512 'title' => __CLASS__,
513 'section' => 'new',
514 'sectiontitle' => 'Title',
515 'text' => 'Content',
516 ] );
518 $this->assertParsedToRegExp( '!<h2[^>]*>.*Title.*</h2>.*\n<p>Content\n</p>!', $res );
521 public function testExistingSection() {
522 $res = $this->doApiRequest( [
523 'action' => 'parse',
524 'title' => __CLASS__,
525 'section' => 1,
526 'text' => "Intro\n\n== Section 1 ==\n\nContent\n\n== Section 2 ==\n\nMore content",
527 ] );
529 $this->assertParsedToRegExp( '!<h2[^>]*>.*Section 1.*</h2>.*\n<p>Content\n</p>!', $res );
532 public function testNoPst() {
533 $name = ucfirst( __FUNCTION__ );
535 $this->editPage( "Template:$name", "Template ''text''" );
537 $res = $this->doApiRequest( [
538 'action' => 'parse',
539 'text' => "{{subst:$name}}",
540 'contentmodel' => 'wikitext',
541 ] );
543 $this->assertParsedTo( "<p>{{subst:$name}}\n</p>", $res );
546 public function testPst() {
547 $name = ucfirst( __FUNCTION__ );
549 $this->editPage( "Template:$name", "Template ''text''" );
551 $res = $this->doApiRequest( [
552 'action' => 'parse',
553 'pst' => '',
554 'text' => "{{subst:$name}}",
555 'contentmodel' => 'wikitext',
556 'prop' => 'text|wikitext',
557 ] );
559 $this->assertParsedTo( "<p>Template <i>text</i>\n</p>", $res );
560 $this->assertSame( "{{subst:$name}}", $res[0]['parse']['wikitext'] );
563 public function testOnlyPst() {
564 $name = ucfirst( __FUNCTION__ );
566 $this->editPage( "Template:$name", "Template ''text''" );
568 $res = $this->doApiRequest( [
569 'action' => 'parse',
570 'onlypst' => '',
571 'text' => "{{subst:$name}}",
572 'contentmodel' => 'wikitext',
573 'prop' => 'text|wikitext',
574 'summary' => 'Summary',
575 ] );
577 $this->assertSame(
578 [ 'parse' => [
579 'text' => "Template ''text''",
580 'wikitext' => "{{subst:$name}}",
581 'parsedsummary' => 'Summary',
582 ] ],
583 $res[0]
587 /** @dataProvider providerTestParsoid */
588 public function testParsoid( $parsoid, $existing, $expected ) {
589 # For simplicity, ensure that [[Foo]] isn't a redlink.
590 $this->editPage( "Foo", __FUNCTION__ );
591 $res = $this->doApiRequest( [
592 # check that we're using the contents of 'text' not the contents of
593 # [[<title>]] by using pre-existing title __CLASS__ sometimes
594 'title' => $existing ? __CLASS__ : 'Bar',
595 'action' => 'parse',
596 'text' => "[[Foo]]",
597 'contentmodel' => 'wikitext',
598 'parsoid' => $parsoid ?: null,
599 'disablelimitreport' => true,
600 ] );
602 $this->assertParsedToRegexp( $expected, $res );
605 public static function providerTestParsoid() {
606 // Legacy parses, with and without pre-existing content.
607 $expected = '!^<p><a href="[^"]*" title="Foo">Foo</a>\n</p>$!';
608 yield [ false, false, $expected ];
609 yield [ false, true, $expected ];
610 // Parsoid parses, with and without pre-existing content.
611 $expected = '!^<section[^>]*><p[^>]*><a rel="mw:WikiLink" href="[^"]*Foo" title="Foo"[^>]*>Foo</a></p></section>!';
612 yield [ true, false, $expected ];
613 yield [ true, true, $expected ];
616 public function testHeadHtml() {
617 $res = $this->doApiRequest( [
618 'action' => 'parse',
619 'page' => __CLASS__,
620 'prop' => 'headhtml',
621 ] );
623 // Just do a rough check
624 $this->assertMatchesRegularExpression( '#<!DOCTYPE.*<html.*<head.*</head>.*<body#s',
625 $res[0]['parse']['headhtml'] );
626 $this->assertArrayNotHasKey( 'warnings', $res[0] );
629 public function testCategoriesHtml() {
630 $name = ucfirst( __FUNCTION__ );
632 $this->editPage( $name, "[[Category:$name]]" );
634 $res = $this->doApiRequest( [
635 'action' => 'parse',
636 'page' => $name,
637 'prop' => 'categorieshtml',
638 ] );
640 $this->assertMatchesRegularExpression( "#Category.*Category:$name.*$name#",
641 $res[0]['parse']['categorieshtml'] );
642 $this->assertArrayNotHasKey( 'warnings', $res[0] );
645 public function testEffectiveLangLinks() {
646 $hookRan = false;
647 $this->setTemporaryHook( 'LanguageLinks',
648 static function () use ( &$hookRan ) {
649 $hookRan = true;
653 $res = $this->doApiRequest( [
654 'action' => 'parse',
655 'title' => __CLASS__,
656 'text' => '[[zh:' . __CLASS__ . ']]',
657 'effectivelanglinks' => '',
658 ] );
660 $this->assertTrue( $hookRan );
661 $this->assertSame( 'The parameter "effectivelanglinks" has been deprecated.',
662 $res[0]['warnings']['parse']['warnings'] );
666 * @param array $arr Extra params to add to API request
668 private function doTestLangLinks( array $arr = [] ) {
669 $res = $this->doApiRequest( array_merge( [
670 'action' => 'parse',
671 'title' => 'Omelette',
672 'text' => '[[madeuplanguage:Omelette]]',
673 'prop' => 'langlinks',
674 ], $arr ) );
676 $langLinks = $res[0]['parse']['langlinks'];
678 $this->assertCount( 1, $langLinks );
679 $this->assertSame( 'madeuplanguage', $langLinks[0]['lang'] );
680 $this->assertSame( 'Omelette', $langLinks[0]['title'] );
681 $this->assertSame( 'https://example.com/wiki/Omelette', $langLinks[0]['url'] );
682 $this->assertArrayNotHasKey( 'warnings', $res[0] );
685 public function testLangLinks() {
686 $this->setupInterwiki();
687 $this->doTestLangLinks();
690 public function testLangLinksWithSkin() {
691 $this->setupInterwiki();
692 $this->setupSkin();
693 $this->doTestLangLinks( [ 'useskin' => 'testing' ] );
696 public function testHeadItems() {
697 $res = $this->doApiRequest( [
698 'action' => 'parse',
699 'title' => __CLASS__,
700 'text' => '',
701 'prop' => 'headitems',
702 ] );
704 $this->assertSame( [], $res[0]['parse']['headitems'] );
705 $this->assertSame(
706 '"prop=headitems" is deprecated since MediaWiki 1.28. ' .
707 'Use "prop=headhtml" when creating new HTML documents, ' .
708 'or "prop=modules|jsconfigvars" when updating a document client-side.',
709 $res[0]['warnings']['parse']['warnings']
713 public function testHeadItemsWithSkin() {
714 $this->setupSkin();
716 $res = $this->doApiRequest( [
717 'action' => 'parse',
718 'title' => __CLASS__,
719 'text' => '',
720 'prop' => 'headitems',
721 'useskin' => 'testing',
722 ] );
724 $this->assertSame( [], $res[0]['parse']['headitems'] );
725 $this->assertSame(
726 '"prop=headitems" is deprecated since MediaWiki 1.28. ' .
727 'Use "prop=headhtml" when creating new HTML documents, ' .
728 'or "prop=modules|jsconfigvars" when updating a document client-side.',
729 $res[0]['warnings']['parse']['warnings']
733 public function testModules() {
734 $this->setTemporaryHook( 'ParserAfterParse',
735 static function ( $parser ) {
736 $output = $parser->getOutput();
737 $output->addModules( [ 'foo', 'bar' ] );
738 $output->addModuleStyles( [ 'aaa', 'zzz' ] );
739 $output->addJsConfigVars( [ 'x' => 'y', 'z' => -3 ] );
742 $res = $this->doApiRequest( [
743 'action' => 'parse',
744 'title' => __CLASS__,
745 'text' => 'Content',
746 'prop' => 'modules|jsconfigvars|encodedjsconfigvars',
747 ] );
749 $this->assertSame( [ 'foo', 'bar' ], $res[0]['parse']['modules'] );
750 $this->assertSame( [], $res[0]['parse']['modulescripts'] );
751 $this->assertSame( [ 'aaa', 'zzz' ], $res[0]['parse']['modulestyles'] );
752 $this->assertSame( [ 'x' => 'y', 'z' => -3 ], $res[0]['parse']['jsconfigvars'] );
753 $this->assertSame( '{"x":"y","z":-3}', $res[0]['parse']['encodedjsconfigvars'] );
754 $this->assertArrayNotHasKey( 'warnings', $res[0] );
757 public function testModulesWithSkin() {
758 $this->setupSkin();
760 $res = $this->doApiRequest( [
761 'action' => 'parse',
762 'pageid' => self::$pageId,
763 'useskin' => 'testing',
764 'prop' => 'modules',
765 ] );
766 $this->assertSame(
767 [ 'foo', 'bar', 'baz' ],
768 $res[0]['parse']['modules'],
769 'resp.parse.modules'
771 $this->assertSame(
773 $res[0]['parse']['modulescripts'],
774 'resp.parse.modulescripts'
776 $this->assertSame(
777 [ 'quux.styles' ],
778 $res[0]['parse']['modulestyles'],
779 'resp.parse.modulestyles'
781 $this->assertSame(
782 [ 'parse' =>
783 [ 'warnings' =>
784 'Property "modules" was set but not "jsconfigvars" or ' .
785 '"encodedjsconfigvars". Configuration variables are necessary for ' .
786 'proper module usage.'
789 $res[0]['warnings']
793 public function testIndicators() {
794 $res = $this->doApiRequest( [
795 'action' => 'parse',
796 'title' => __CLASS__,
797 'text' =>
798 '<indicator name="b">BBB!</indicator>Some text<indicator name="a">aaa</indicator>',
799 'prop' => 'indicators',
800 ] );
802 $this->assertSame(
803 // It seems we return in markup order and not display order
804 [ 'b' => 'BBB!', 'a' => 'aaa' ],
805 $res[0]['parse']['indicators']
807 $this->assertArrayNotHasKey( 'warnings', $res[0] );
810 public function testIndicatorsWithSkin() {
811 $this->setupSkin();
813 $res = $this->doApiRequest( [
814 'action' => 'parse',
815 'title' => __CLASS__,
816 'text' =>
817 '<indicator name="b">BBB!</indicator>Some text<indicator name="a">aaa</indicator>',
818 'prop' => 'indicators',
819 'useskin' => 'testing',
820 ] );
822 $this->assertSame(
823 // Now we return in display order rather than markup order
825 'a' => '<div class="mw-parser-output">aaa</div>',
826 'b' => '<div class="mw-parser-output">BBB!</div>',
828 $res[0]['parse']['indicators']
830 $this->assertArrayNotHasKey( 'warnings', $res[0] );
833 public function testIwlinks() {
834 $this->setupInterwiki();
836 $res = $this->doApiRequest( [
837 'action' => 'parse',
838 'title' => 'Omelette',
839 'text' => '[[:madeuplanguage:Omelette]][[madeuplanguage:Spaghetti]]',
840 'prop' => 'iwlinks',
841 ] );
843 $iwlinks = $res[0]['parse']['iwlinks'];
845 $this->assertCount( 1, $iwlinks );
846 $this->assertSame( 'madeuplanguage', $iwlinks[0]['prefix'] );
847 $this->assertSame( 'https://example.com/wiki/Omelette', $iwlinks[0]['url'] );
848 $this->assertSame( 'madeuplanguage:Omelette', $iwlinks[0]['title'] );
849 $this->assertArrayNotHasKey( 'warnings', $res[0] );
852 public function testLimitReports() {
853 $res = $this->doApiRequest( [
854 'action' => 'parse',
855 'pageid' => self::$pageId,
856 'prop' => 'limitreportdata|limitreporthtml',
857 ] );
859 // We don't bother testing the actual values here
860 $this->assertIsArray( $res[0]['parse']['limitreportdata'] );
861 $this->assertIsString( $res[0]['parse']['limitreporthtml'] );
862 $this->assertArrayNotHasKey( 'warnings', $res[0] );
865 public function testParseTreeNonWikitext() {
866 $this->expectApiErrorCode( 'notwikitext' );
868 $this->doApiRequest( [
869 'action' => 'parse',
870 'text' => '',
871 'contentmodel' => 'json',
872 'prop' => 'parsetree',
873 ] );
876 public function testParseTree() {
877 $res = $this->doApiRequest( [
878 'action' => 'parse',
879 'text' => "Some ''text'' is {{nice|to have|i=think}}",
880 'contentmodel' => 'wikitext',
881 'prop' => 'parsetree',
882 ] );
884 $this->assertEquals(
885 '<root>Some \'\'text\'\' is <template><title>nice</title>' .
886 '<part><name index="1"/><value>to have</value></part>' .
887 '<part><name>i</name><equals>=</equals><value>think</value></part>' .
888 '</template></root>',
889 $res[0]['parse']['parsetree']
891 $this->assertArrayNotHasKey( 'warnings', $res[0] );
894 public function testFormatCategories() {
895 $name = ucfirst( __FUNCTION__ );
897 $this->editPage( "Category:$name", 'Content' );
898 $this->editPage( 'Category:Hidden', '__HIDDENCAT__' );
900 $res = $this->doApiRequest( [
901 'action' => 'parse',
902 'title' => __CLASS__,
903 'text' => "[[Category:$name]][[Category:Foo|Sort me]][[Category:Hidden]]",
904 'prop' => 'categories',
905 ] );
907 $this->assertSame(
908 [ [ 'sortkey' => '', 'category' => $name ],
909 [ 'sortkey' => 'Sort me', 'category' => 'Foo', 'missing' => true ],
910 [ 'sortkey' => '', 'category' => 'Hidden', 'hidden' => true ] ],
911 $res[0]['parse']['categories']
913 $this->assertArrayNotHasKey( 'warnings', $res[0] );
916 public function testConcurrentLimitPageParse() {
917 $this->overrideConfigValue(
918 MainConfigNames::PoolCounterConf,
920 'ApiParser' => [
921 'class' => MockPoolCounterFailing::class,
926 try {
927 $this->doApiRequest( [
928 'action' => 'parse',
929 'page' => __CLASS__,
930 ] );
931 $this->fail( "API did not return an error when concurrency exceeded" );
932 } catch ( ApiUsageException $ex ) {
933 $this->assertApiErrorCode( 'concurrency-limit', $ex );
937 public function testConcurrentLimitContentParse() {
938 $this->overrideConfigValue(
939 MainConfigNames::PoolCounterConf,
941 'ApiParser' => [
942 'class' => MockPoolCounterFailing::class,
947 try {
948 $this->doApiRequest( [
949 'action' => 'parse',
950 'oldid' => self::$revIds['revdel'],
951 ] );
952 $this->fail( "API did not return an error when concurrency exceeded" );
953 } catch ( ApiUsageException $ex ) {
954 $this->assertApiErrorCode( 'concurrency-limit', $ex );
958 public function testDisplayTitle() {
959 $res = $this->doApiRequest( [
960 'action' => 'parse',
961 'title' => 'Art&copy',
962 'text' => '{{DISPLAYTITLE:art&copy}}foo',
963 'prop' => 'displaytitle',
964 ] );
966 $this->assertSame(
967 'art&amp;copy',
968 $res[0]['parse']['displaytitle']
971 $res = $this->doApiRequest( [
972 'action' => 'parse',
973 'title' => 'Art&copy',
974 'text' => 'foo',
975 'prop' => 'displaytitle',
976 ] );
978 $this->assertSame(
979 '<span class="mw-page-title-main">Art&amp;copy</span>',
980 $res[0]['parse']['displaytitle']
984 public function testIncompatFormat() {
985 $this->expectApiErrorCode( 'badformat-generic' );
987 $this->doApiRequest( [
988 'action' => 'parse',
989 'prop' => 'categories',
990 'title' => __CLASS__,
991 'text' => '',
992 'contentformat' => 'application/json',
993 ] );
996 public function testIgnoreFormatUsingPage() {
997 $res = $this->doApiRequest( [
998 'action' => 'parse',
999 'page' => __CLASS__,
1000 'prop' => 'wikitext',
1001 'contentformat' => 'text/plain',
1002 ] );
1003 $this->assertArrayHasKey( 'wikitext', $res[0]['parse'] );
1006 public function testShouldCastNumericImageLinksToString(): void {
1007 $res = $this->doApiRequest( [
1008 'action' => 'parse',
1009 'title' => __CLASS__,
1010 'prop' => 'images',
1011 'text' => '[[File:1]]',
1012 ] );
1013 $this->assertSame( [ '1' ], $res[0]['parse']['images'] );