Extensions can use the ArticleParserOptions hook to customize the parser options used for article read views, and Visual Editor uses ApiParse to regenerate the article read view HTML after edit. In order for VE to remain consistent with the original parse options, provide a `usearticle` option to the parse API to allow it to invoke the ArticleParserOptions hook. Bug: T373212 Change-Id: I906affc8d2335b7bb48dff3a4b061fcf86b71cee
1049 lines
29 KiB
PHP
1049 lines
29 KiB
PHP
<?php
|
|
|
|
/**
|
|
* ApiParse check functions
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
*/
|
|
|
|
namespace MediaWiki\Tests\Api;
|
|
|
|
use ApiUsageException;
|
|
use MediaWiki\Context\RequestContext;
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\Revision\RevisionRecord;
|
|
use MediaWiki\Tests\Unit\DummyServicesTrait;
|
|
use MediaWiki\Title\TitleValue;
|
|
use MockPoolCounterFailing;
|
|
use SkinFactory;
|
|
use SkinFallback;
|
|
|
|
/**
|
|
* @group API
|
|
* @group Database
|
|
* @group medium
|
|
*
|
|
* @covers \ApiParse
|
|
*/
|
|
class ApiParseTest extends ApiTestCase {
|
|
use DummyServicesTrait;
|
|
|
|
protected static $pageId;
|
|
protected static $revIds = [];
|
|
|
|
public function addDBDataOnce() {
|
|
$page = $this->getServiceContainer()->getWikiPageFactory()
|
|
->newFromLinkTarget( new TitleValue( NS_MAIN, __CLASS__ ) );
|
|
$status = $this->editPage( $page, 'Test for revdel' );
|
|
self::$pageId = $status->getNewRevision()->getPageId();
|
|
self::$revIds['revdel'] = $status->getNewRevision()->getId();
|
|
|
|
$status = $this->editPage( $page, 'Test for suppressed' );
|
|
self::$revIds['suppressed'] = $status->getNewRevision()->getId();
|
|
|
|
$status = $this->editPage( $page, 'Test for oldid' );
|
|
self::$revIds['oldid'] = $status->getNewRevision()->getId();
|
|
|
|
$status = $this->editPage( $page, 'Test for latest' );
|
|
self::$revIds['latest'] = $status->getNewRevision()->getId();
|
|
|
|
// Set a user for modifying the visibility, this is needed because
|
|
// setVisibility generates a log, which cannot be an anonymous user actor
|
|
// when temporary accounts are enabled.
|
|
RequestContext::getMain()->setUser( $this->getTestUser()->getUser() );
|
|
$this->revisionDelete( self::$revIds['revdel'] );
|
|
$this->revisionDelete(
|
|
self::$revIds['suppressed'],
|
|
[
|
|
RevisionRecord::DELETED_TEXT => 1,
|
|
RevisionRecord::DELETED_RESTRICTED => 1
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Assert that the given result of calling $this->doApiRequest() with
|
|
* action=parse resulted in $html, accounting for the boilerplate that the
|
|
* parser adds around the parsed page. Also asserts that warnings match
|
|
* the provided $warning.
|
|
*
|
|
* @param string $expected Expected HTML
|
|
* @param array $res Returned from doApiRequest()
|
|
* @param string|null $warnings Exact value of expected warnings, null for
|
|
* no warnings
|
|
*/
|
|
protected function assertParsedTo( $expected, array $res, $warnings = null ) {
|
|
$this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertSame' ] );
|
|
}
|
|
|
|
/**
|
|
* Same as above, but asserts that the HTML matches a regexp instead of a
|
|
* literal string match.
|
|
*
|
|
* @param string $expected Expected HTML
|
|
* @param array $res Returned from doApiRequest()
|
|
* @param string|null $warnings Exact value of expected warnings, null for
|
|
* no warnings
|
|
*/
|
|
protected function assertParsedToRegExp( $expected, array $res, $warnings = null ) {
|
|
$this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertMatchesRegularExpression' ] );
|
|
}
|
|
|
|
private function doAssertParsedTo( $expected, array $res, $warnings, callable $callback ) {
|
|
$html = $res[0]['parse']['text'];
|
|
|
|
$expectedStart = '<div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr"';
|
|
$this->assertSame( $expectedStart, substr( $html, 0, strlen( $expectedStart ) ) );
|
|
|
|
$html = substr( $html, strlen( $expectedStart ) );
|
|
|
|
# Parsoid-based transformations may add ID and data-mw-parsoid-version
|
|
# attributes to the wrapper div
|
|
$possibleIdAttr = '/^( (id|data-mw[^=]*)="[^"]+")*>/';
|
|
$html = preg_replace( $possibleIdAttr, '', $html );
|
|
|
|
$possibleParserCache = '/\n<!-- Saved in (?>parser cache|RevisionOutputCache) (?>.*?\n -->)\n/';
|
|
$html = preg_replace( $possibleParserCache, '', $html );
|
|
|
|
if ( $res[1]->getBool( 'disablelimitreport' ) ) {
|
|
$expectedEnd = "</div>";
|
|
$this->assertSame( $expectedEnd, substr( $html, -strlen( $expectedEnd ) ) );
|
|
|
|
$unexpectedEnd = '#<!-- \nNewPP limit report|' .
|
|
'<!--\nTransclusion expansion time report#';
|
|
$this->assertDoesNotMatchRegularExpression( $unexpectedEnd, $html );
|
|
|
|
$html = substr( $html, 0, strlen( $html ) - strlen( $expectedEnd ) );
|
|
} else {
|
|
$expectedEnd = '#\n<!-- \nNewPP limit report\n(?>.+?\n-->)\n' .
|
|
'<!--\nTransclusion expansion time report \(%,ms,calls,template\)\n(?>.*?\n-->)\n' .
|
|
'</div>$#s';
|
|
$this->assertMatchesRegularExpression( $expectedEnd, $html );
|
|
|
|
$html = preg_replace( $expectedEnd, '', $html );
|
|
}
|
|
|
|
$callback( $expected, $html );
|
|
|
|
if ( $warnings === null ) {
|
|
$this->assertCount( 1, $res[0] );
|
|
} else {
|
|
$this->assertCount( 2, $res[0] );
|
|
$this->assertSame( [ 'warnings' => $warnings ], $res[0]['warnings']['parse'] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up an interwiki entry for testing.
|
|
*/
|
|
protected function setupInterwiki() {
|
|
$this->getDb()->newInsertQueryBuilder()
|
|
->insertInto( 'interwiki' )
|
|
->ignore()
|
|
->row( [
|
|
'iw_prefix' => 'madeuplanguage',
|
|
'iw_url' => "https://example.com/wiki/$1",
|
|
'iw_api' => '',
|
|
'iw_wikiid' => '',
|
|
'iw_local' => false,
|
|
] )
|
|
->caller( __METHOD__ )
|
|
->execute();
|
|
|
|
$this->overrideConfigValue(
|
|
MainConfigNames::ExtraInterlanguageLinkPrefixes,
|
|
[ 'madeuplanguage' ]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set up a skin for testing.
|
|
*
|
|
* @todo Should this code be in MediaWikiIntegrationTestCase or something?
|
|
*/
|
|
protected function setupSkin() {
|
|
$factory = new SkinFactory( $this->getDummyObjectFactory(), [] );
|
|
$factory->register( 'testing', 'Testing', function () {
|
|
$skin = $this->getMockBuilder( SkinFallback::class )
|
|
->onlyMethods( [ 'getDefaultModules' ] )
|
|
->getMock();
|
|
$skin->expects( $this->once() )->method( 'getDefaultModules' )
|
|
->willReturn( [
|
|
'styles' => [ 'core' => [ 'quux.styles' ] ],
|
|
'core' => [ 'foo', 'bar' ],
|
|
'content' => [ 'baz' ]
|
|
] );
|
|
return $skin;
|
|
} );
|
|
$this->setService( 'SkinFactory', $factory );
|
|
}
|
|
|
|
public function testParseByName() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => __CLASS__,
|
|
] );
|
|
$this->assertParsedTo( "<p>Test for latest\n</p>", $res );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => __CLASS__,
|
|
'disablelimitreport' => 1,
|
|
] );
|
|
$this->assertParsedTo( "<p>Test for latest\n</p>", $res );
|
|
}
|
|
|
|
public function testParseById() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'pageid' => self::$pageId,
|
|
] );
|
|
$this->assertParsedTo( "<p>Test for latest\n</p>", $res );
|
|
}
|
|
|
|
public function testParseByOldId() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'oldid' => self::$revIds['oldid'],
|
|
] );
|
|
$this->assertParsedTo( "<p>Test for oldid\n</p>", $res );
|
|
$this->assertArrayNotHasKey( 'textdeleted', $res[0]['parse'] );
|
|
$this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] );
|
|
}
|
|
|
|
public function testRevDel() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'oldid' => self::$revIds['revdel'],
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>Test for revdel\n</p>", $res );
|
|
$this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] );
|
|
$this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] );
|
|
}
|
|
|
|
public function testRevDelNoPermission() {
|
|
$this->expectApiErrorCode( 'permissiondenied' );
|
|
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'oldid' => self::$revIds['revdel'],
|
|
], null, null, static::getTestUser()->getAuthority() );
|
|
}
|
|
|
|
public function testSuppressed() {
|
|
$this->setGroupPermissions( 'sysop', 'viewsuppressed', true );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'oldid' => self::$revIds['suppressed']
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>Test for suppressed\n</p>", $res );
|
|
$this->assertArrayHasKey( 'textsuppressed', $res[0]['parse'] );
|
|
$this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] );
|
|
}
|
|
|
|
public function testNonexistentPage() {
|
|
try {
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => 'DoesNotExist',
|
|
] );
|
|
|
|
$this->fail( "API did not return an error when parsing a nonexistent page" );
|
|
} catch ( ApiUsageException $ex ) {
|
|
$this->assertApiErrorCode( 'missingtitle', $ex );
|
|
}
|
|
}
|
|
|
|
public function testTitleProvided() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => 'Some interesting page',
|
|
'text' => '{{PAGENAME}} has attracted my attention',
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>Some interesting page has attracted my attention\n</p>", $res );
|
|
}
|
|
|
|
public function testSection() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( $name,
|
|
"Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => $name,
|
|
'section' => 1,
|
|
] );
|
|
|
|
$this->assertParsedToRegExp( '!<h2[^>]*>.*Section 1.*</h2>.*\n<p>Content 1\n</p>!', $res );
|
|
}
|
|
|
|
public function testInvalidSection() {
|
|
$this->expectApiErrorCode( 'invalidsection' );
|
|
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'section' => 'T-new',
|
|
] );
|
|
}
|
|
|
|
public function testSectionNoContent() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$status = $this->editPage( $name,
|
|
"Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" );
|
|
|
|
$this->expectApiErrorCode( 'missingcontent-pageid' );
|
|
|
|
$this->getDb()->newDeleteQueryBuilder()
|
|
->deleteFrom( 'revision' )
|
|
->where( [ 'rev_id' => $status->getNewRevision()->getId() ] )
|
|
->caller( __METHOD__ )
|
|
->execute();
|
|
|
|
// Ignore warning from WikiPage::getContentModel
|
|
@$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => $name,
|
|
'section' => 1,
|
|
] );
|
|
}
|
|
|
|
public function testNewSectionWithPage() {
|
|
$this->expectApiErrorCode( 'invalidparammix' );
|
|
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => __CLASS__,
|
|
'section' => 'new',
|
|
] );
|
|
}
|
|
|
|
public function testNonexistentOldId() {
|
|
$this->expectApiErrorCode( 'nosuchrevid' );
|
|
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'oldid' => pow( 2, 31 ) - 1,
|
|
] );
|
|
}
|
|
|
|
public function testUnfollowedRedirect() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( $name, "#REDIRECT [[$name 2]]" );
|
|
$this->editPage( "$name 2", "Some ''text''" );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => $name,
|
|
] );
|
|
|
|
// Can't use assertParsedTo because the parser output is different for
|
|
// redirects
|
|
$this->assertMatchesRegularExpression( "/Redirect to:.*$name 2/", $res[0]['parse']['text'] );
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testFollowedRedirect() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( $name, "#REDIRECT [[$name 2]]" );
|
|
$this->editPage( "$name 2", "Some ''text''" );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => $name,
|
|
'redirects' => true,
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
|
|
}
|
|
|
|
public function testFollowedRedirectById() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$id = $this->editPage( $name, "#REDIRECT [[$name 2]]" )
|
|
->getNewRevision()->getPageId();
|
|
$this->editPage( "$name 2", "Some ''text''" );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'pageid' => $id,
|
|
'redirects' => true,
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
|
|
}
|
|
|
|
public function testNonRedirectOk() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( $name, "Some ''text''" );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => $name,
|
|
'redirects' => true,
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
|
|
}
|
|
|
|
public function testNonRedirectByIdOk() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$id = $this->editPage( $name, "Some ''text''" )->getNewRevision()->getPageId();
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'pageid' => $id,
|
|
'redirects' => true,
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res );
|
|
}
|
|
|
|
public function testInvalidTitle() {
|
|
$this->expectApiErrorCode( 'invalidtitle' );
|
|
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => '|',
|
|
] );
|
|
}
|
|
|
|
public function testTitleWithNonexistentRevId() {
|
|
$this->expectApiErrorCode( 'nosuchrevid' );
|
|
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'revid' => pow( 2, 31 ) - 1,
|
|
] );
|
|
}
|
|
|
|
public function testTitleWithNonMatchingRevId() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => $name,
|
|
'revid' => self::$revIds['latest'],
|
|
'text' => 'Some text',
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>Some text\n</p>", $res,
|
|
'r' . self::$revIds['latest'] . " is not a revision of $name." );
|
|
}
|
|
|
|
public function testRevId() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'revid' => self::$revIds['latest'],
|
|
'text' => 'My revid is {{REVISIONID}}!',
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>My revid is " . self::$revIds['latest'] . "!\n</p>", $res );
|
|
}
|
|
|
|
public function testTitleNoText() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => 'Special:AllPages',
|
|
] );
|
|
|
|
$this->assertParsedTo( '', $res,
|
|
'"title" used without "text", and parsed page properties were requested. ' .
|
|
'Did you mean to use "page" instead of "title"?' );
|
|
}
|
|
|
|
public function testRevidNoText() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'revid' => self::$revIds['latest'],
|
|
] );
|
|
|
|
$this->assertParsedTo( '', $res,
|
|
'"revid" used without "text", and parsed page properties were requested. ' .
|
|
'Did you mean to use "oldid" instead of "revid"?' );
|
|
}
|
|
|
|
public function testTextNoContentModel() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'text' => "Some ''text''",
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res,
|
|
'No "title" or "contentmodel" was given, assuming wikitext.' );
|
|
}
|
|
|
|
public function testSerializationError() {
|
|
$this->expectApiErrorCode( 'parseerror' );
|
|
|
|
$this->mergeMwGlobalArrayValue( 'wgContentHandlers',
|
|
[ 'testing-serialize-error' => 'DummySerializeErrorContentHandler' ] );
|
|
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'text' => "Some ''text''",
|
|
'contentmodel' => 'testing-serialize-error',
|
|
] );
|
|
}
|
|
|
|
public function testNewSection() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'section' => 'new',
|
|
'sectiontitle' => 'Title',
|
|
'text' => 'Content',
|
|
] );
|
|
|
|
$this->assertParsedToRegExp( '!<h2[^>]*>.*Title.*</h2>.*\n<p>Content\n</p>!', $res );
|
|
}
|
|
|
|
public function testExistingSection() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'section' => 1,
|
|
'text' => "Intro\n\n== Section 1 ==\n\nContent\n\n== Section 2 ==\n\nMore content",
|
|
] );
|
|
|
|
$this->assertParsedToRegExp( '!<h2[^>]*>.*Section 1.*</h2>.*\n<p>Content\n</p>!', $res );
|
|
}
|
|
|
|
public function testNoPst() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( "Template:$name", "Template ''text''" );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'text' => "{{subst:$name}}",
|
|
'contentmodel' => 'wikitext',
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>{{subst:$name}}\n</p>", $res );
|
|
}
|
|
|
|
public function testPst() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( "Template:$name", "Template ''text''" );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'pst' => '',
|
|
'text' => "{{subst:$name}}",
|
|
'contentmodel' => 'wikitext',
|
|
'prop' => 'text|wikitext',
|
|
] );
|
|
|
|
$this->assertParsedTo( "<p>Template <i>text</i>\n</p>", $res );
|
|
$this->assertSame( "{{subst:$name}}", $res[0]['parse']['wikitext'] );
|
|
}
|
|
|
|
public function testOnlyPst() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( "Template:$name", "Template ''text''" );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'onlypst' => '',
|
|
'text' => "{{subst:$name}}",
|
|
'contentmodel' => 'wikitext',
|
|
'prop' => 'text|wikitext',
|
|
'summary' => 'Summary',
|
|
] );
|
|
|
|
$this->assertSame(
|
|
[ 'parse' => [
|
|
'text' => "Template ''text''",
|
|
'wikitext' => "{{subst:$name}}",
|
|
'parsedsummary' => 'Summary',
|
|
] ],
|
|
$res[0]
|
|
);
|
|
}
|
|
|
|
/** @dataProvider providerTestParsoid */
|
|
public function testParsoid( $parsoid, $existing, $expected ) {
|
|
# For simplicity, ensure that [[Foo]] isn't a redlink.
|
|
$this->editPage( "Foo", __FUNCTION__ );
|
|
$res = $this->doApiRequest( [
|
|
# check that we're using the contents of 'text' not the contents of
|
|
# [[<title>]] by using pre-existing title __CLASS__ sometimes
|
|
'title' => $existing ? __CLASS__ : 'Bar',
|
|
'action' => 'parse',
|
|
'text' => "[[Foo]]",
|
|
'contentmodel' => 'wikitext',
|
|
'parsoid' => $parsoid ?: null,
|
|
'disablelimitreport' => true,
|
|
] );
|
|
|
|
$this->assertParsedToRegexp( $expected, $res );
|
|
}
|
|
|
|
public static function providerTestParsoid() {
|
|
// Legacy parses, with and without pre-existing content.
|
|
$expected = '!^<p><a href="[^"]*" title="Foo">Foo</a>\n</p>$!';
|
|
yield [ false, false, $expected ];
|
|
yield [ false, true, $expected ];
|
|
// Parsoid parses, with and without pre-existing content.
|
|
$expected = '!^<section[^>]*><p[^>]*><a rel="mw:WikiLink" href="[^"]*Foo" title="Foo"[^>]*>Foo</a></p></section>!';
|
|
yield [ true, false, $expected ];
|
|
yield [ true, true, $expected ];
|
|
}
|
|
|
|
/** @dataProvider providerTestParsoid */
|
|
public function testUseArticle( $parsoid, $existing, $expected ) {
|
|
# For simplicity, ensure that [[Foo]] isn't a redlink.
|
|
$this->editPage( "Foo", __FUNCTION__ );
|
|
# Use an ArticleParserOptions hook to set the useParsoid option
|
|
$this->setTemporaryHook( 'ArticleParserOptions',
|
|
static function ( $unused, $po ) use ( $parsoid ) {
|
|
if ( $parsoid ) {
|
|
$po->setUseParsoid();
|
|
}
|
|
}
|
|
);
|
|
|
|
$res = $this->doApiRequest( [
|
|
# check that we're using the contents of 'text' not the contents of
|
|
# [[<title>]] by using pre-existing title __CLASS__ sometimes
|
|
'title' => $existing ? __CLASS__ : 'Bar',
|
|
'action' => 'parse',
|
|
'text' => "[[Foo]]",
|
|
'contentmodel' => 'wikitext',
|
|
'usearticle' => true,
|
|
# Note that we're not passing the 'parsoid' parameter here.
|
|
'disablelimitreport' => true,
|
|
] );
|
|
|
|
$this->assertParsedToRegexp( $expected, $res );
|
|
}
|
|
|
|
public function testHeadHtml() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => __CLASS__,
|
|
'prop' => 'headhtml',
|
|
] );
|
|
|
|
// Just do a rough check
|
|
$this->assertMatchesRegularExpression( '#<!DOCTYPE.*<html.*<head.*</head>.*<body#s',
|
|
$res[0]['parse']['headhtml'] );
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testCategoriesHtml() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( $name, "[[Category:$name]]" );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => $name,
|
|
'prop' => 'categorieshtml',
|
|
] );
|
|
|
|
$this->assertMatchesRegularExpression( "#Category.*Category:$name.*$name#",
|
|
$res[0]['parse']['categorieshtml'] );
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testEffectiveLangLinks() {
|
|
$hookRan = false;
|
|
$this->setTemporaryHook( 'LanguageLinks',
|
|
static function () use ( &$hookRan ) {
|
|
$hookRan = true;
|
|
}
|
|
);
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'text' => '[[zh:' . __CLASS__ . ']]',
|
|
'effectivelanglinks' => '',
|
|
] );
|
|
|
|
$this->assertTrue( $hookRan );
|
|
$this->assertSame( 'The parameter "effectivelanglinks" has been deprecated.',
|
|
$res[0]['warnings']['parse']['warnings'] );
|
|
}
|
|
|
|
/**
|
|
* @param array $arr Extra params to add to API request
|
|
*/
|
|
private function doTestLangLinks( array $arr = [] ) {
|
|
$res = $this->doApiRequest( array_merge( [
|
|
'action' => 'parse',
|
|
'title' => 'Omelette',
|
|
'text' => '[[madeuplanguage:Omelette]]',
|
|
'prop' => 'langlinks',
|
|
], $arr ) );
|
|
|
|
$langLinks = $res[0]['parse']['langlinks'];
|
|
|
|
$this->assertCount( 1, $langLinks );
|
|
$this->assertSame( 'madeuplanguage', $langLinks[0]['lang'] );
|
|
$this->assertSame( 'Omelette', $langLinks[0]['title'] );
|
|
$this->assertSame( 'https://example.com/wiki/Omelette', $langLinks[0]['url'] );
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testLangLinks() {
|
|
$this->setupInterwiki();
|
|
$this->doTestLangLinks();
|
|
}
|
|
|
|
public function testLangLinksWithSkin() {
|
|
$this->setupInterwiki();
|
|
$this->setupSkin();
|
|
$this->doTestLangLinks( [ 'useskin' => 'testing' ] );
|
|
}
|
|
|
|
public function testHeadItems() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'text' => '',
|
|
'prop' => 'headitems',
|
|
] );
|
|
|
|
$this->assertSame( [], $res[0]['parse']['headitems'] );
|
|
$this->assertSame(
|
|
'"prop=headitems" is deprecated since MediaWiki 1.28. ' .
|
|
'Use "prop=headhtml" when creating new HTML documents, ' .
|
|
'or "prop=modules|jsconfigvars" when updating a document client-side.',
|
|
$res[0]['warnings']['parse']['warnings']
|
|
);
|
|
}
|
|
|
|
public function testHeadItemsWithSkin() {
|
|
$this->setupSkin();
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'text' => '',
|
|
'prop' => 'headitems',
|
|
'useskin' => 'testing',
|
|
] );
|
|
|
|
$this->assertSame( [], $res[0]['parse']['headitems'] );
|
|
$this->assertSame(
|
|
'"prop=headitems" is deprecated since MediaWiki 1.28. ' .
|
|
'Use "prop=headhtml" when creating new HTML documents, ' .
|
|
'or "prop=modules|jsconfigvars" when updating a document client-side.',
|
|
$res[0]['warnings']['parse']['warnings']
|
|
);
|
|
}
|
|
|
|
public function testModules() {
|
|
$this->setTemporaryHook( 'ParserAfterParse',
|
|
static function ( $parser ) {
|
|
$parserOutput = $parser->getOutput();
|
|
$parserOutput->addModules( [ 'foo', 'bar' ] );
|
|
$parserOutput->addModuleStyles( [ 'aaa', 'zzz' ] );
|
|
$parserOutput->setJsConfigVar( 'x', 'y' );
|
|
$parserOutput->setJsConfigVar( 'z', -3 );
|
|
}
|
|
);
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'text' => 'Content',
|
|
'prop' => 'modules|jsconfigvars|encodedjsconfigvars',
|
|
] );
|
|
|
|
$this->assertSame( [ 'foo', 'bar' ], $res[0]['parse']['modules'] );
|
|
$this->assertSame( [], $res[0]['parse']['modulescripts'] );
|
|
$this->assertSame( [ 'aaa', 'zzz' ], $res[0]['parse']['modulestyles'] );
|
|
$this->assertSame( [ 'x' => 'y', 'z' => -3 ], $res[0]['parse']['jsconfigvars'] );
|
|
$this->assertSame( '{"x":"y","z":-3}', $res[0]['parse']['encodedjsconfigvars'] );
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testModulesWithSkin() {
|
|
$this->setupSkin();
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'pageid' => self::$pageId,
|
|
'useskin' => 'testing',
|
|
'prop' => 'modules',
|
|
] );
|
|
$this->assertSame(
|
|
[ 'foo', 'bar', 'baz' ],
|
|
$res[0]['parse']['modules'],
|
|
'resp.parse.modules'
|
|
);
|
|
$this->assertSame(
|
|
[],
|
|
$res[0]['parse']['modulescripts'],
|
|
'resp.parse.modulescripts'
|
|
);
|
|
$this->assertSame(
|
|
[ 'quux.styles' ],
|
|
$res[0]['parse']['modulestyles'],
|
|
'resp.parse.modulestyles'
|
|
);
|
|
$this->assertSame(
|
|
[ 'parse' =>
|
|
[ 'warnings' =>
|
|
'Property "modules" was set but not "jsconfigvars" or ' .
|
|
'"encodedjsconfigvars". Configuration variables are necessary for ' .
|
|
'proper module usage.'
|
|
]
|
|
],
|
|
$res[0]['warnings']
|
|
);
|
|
}
|
|
|
|
public function testIndicators() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'text' =>
|
|
'<indicator name="b">BBB!</indicator>Some text<indicator name="a">aaa</indicator>',
|
|
'prop' => 'indicators',
|
|
] );
|
|
|
|
$this->assertSame(
|
|
// It seems we return in markup order and not display order
|
|
[ 'b' => 'BBB!', 'a' => 'aaa' ],
|
|
$res[0]['parse']['indicators']
|
|
);
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testIndicatorsWithSkin() {
|
|
$this->setupSkin();
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'text' =>
|
|
'<indicator name="b">BBB!</indicator>Some text<indicator name="a">aaa</indicator>',
|
|
'prop' => 'indicators',
|
|
'useskin' => 'testing',
|
|
] );
|
|
|
|
$this->assertSame(
|
|
// Now we return in display order rather than markup order
|
|
[
|
|
'a' => '<div class="mw-parser-output">aaa</div>',
|
|
'b' => '<div class="mw-parser-output">BBB!</div>',
|
|
],
|
|
$res[0]['parse']['indicators']
|
|
);
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testIwlinks() {
|
|
$this->setupInterwiki();
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => 'Omelette',
|
|
'text' => '[[:madeuplanguage:Omelette]][[madeuplanguage:Spaghetti]]',
|
|
'prop' => 'iwlinks',
|
|
] );
|
|
|
|
$iwlinks = $res[0]['parse']['iwlinks'];
|
|
|
|
$this->assertCount( 1, $iwlinks );
|
|
$this->assertSame( 'madeuplanguage', $iwlinks[0]['prefix'] );
|
|
$this->assertSame( 'https://example.com/wiki/Omelette', $iwlinks[0]['url'] );
|
|
$this->assertSame( 'madeuplanguage:Omelette', $iwlinks[0]['title'] );
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testLimitReports() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'pageid' => self::$pageId,
|
|
'prop' => 'limitreportdata|limitreporthtml',
|
|
] );
|
|
|
|
// We don't bother testing the actual values here
|
|
$this->assertIsArray( $res[0]['parse']['limitreportdata'] );
|
|
$this->assertIsString( $res[0]['parse']['limitreporthtml'] );
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testParseTreeNonWikitext() {
|
|
$this->expectApiErrorCode( 'notwikitext' );
|
|
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'text' => '',
|
|
'contentmodel' => 'json',
|
|
'prop' => 'parsetree',
|
|
] );
|
|
}
|
|
|
|
public function testParseTree() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'text' => "Some ''text'' is {{nice|to have|i=think}}",
|
|
'contentmodel' => 'wikitext',
|
|
'prop' => 'parsetree',
|
|
] );
|
|
|
|
$this->assertEquals(
|
|
'<root>Some \'\'text\'\' is <template><title>nice</title>' .
|
|
'<part><name index="1"/><value>to have</value></part>' .
|
|
'<part><name>i</name><equals>=</equals><value>think</value></part>' .
|
|
'</template></root>',
|
|
$res[0]['parse']['parsetree']
|
|
);
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testFormatCategories() {
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( "Category:$name", 'Content' );
|
|
$this->editPage( 'Category:Hidden', '__HIDDENCAT__' );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'text' => "[[Category:$name]][[Category:Foo|Sort me]][[Category:Hidden]]",
|
|
'prop' => 'categories',
|
|
] );
|
|
|
|
$this->assertSame(
|
|
[ [ 'sortkey' => '', 'category' => $name ],
|
|
[ 'sortkey' => 'Sort me', 'category' => 'Foo', 'missing' => true ],
|
|
[ 'sortkey' => '', 'category' => 'Hidden', 'hidden' => true ] ],
|
|
$res[0]['parse']['categories']
|
|
);
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
public function testConcurrentLimitPageParse() {
|
|
$this->overrideConfigValue(
|
|
MainConfigNames::PoolCounterConf,
|
|
[
|
|
'ApiParser' => [
|
|
'class' => MockPoolCounterFailing::class,
|
|
]
|
|
]
|
|
);
|
|
|
|
try {
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => __CLASS__,
|
|
] );
|
|
$this->fail( "API did not return an error when concurrency exceeded" );
|
|
} catch ( ApiUsageException $ex ) {
|
|
$this->assertApiErrorCode( 'concurrency-limit', $ex );
|
|
}
|
|
}
|
|
|
|
public function testConcurrentLimitContentParse() {
|
|
$this->overrideConfigValue(
|
|
MainConfigNames::PoolCounterConf,
|
|
[
|
|
'ApiParser' => [
|
|
'class' => MockPoolCounterFailing::class,
|
|
]
|
|
]
|
|
);
|
|
|
|
try {
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'oldid' => self::$revIds['revdel'],
|
|
] );
|
|
$this->fail( "API did not return an error when concurrency exceeded" );
|
|
} catch ( ApiUsageException $ex ) {
|
|
$this->assertApiErrorCode( 'concurrency-limit', $ex );
|
|
}
|
|
}
|
|
|
|
public function testDisplayTitle() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => 'Art©',
|
|
'text' => '{{DISPLAYTITLE:art©}}foo',
|
|
'prop' => 'displaytitle',
|
|
] );
|
|
|
|
$this->assertSame(
|
|
'art&copy',
|
|
$res[0]['parse']['displaytitle']
|
|
);
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => 'Art©',
|
|
'text' => 'foo',
|
|
'prop' => 'displaytitle',
|
|
] );
|
|
|
|
$this->assertSame(
|
|
'<span class="mw-page-title-main">Art&copy</span>',
|
|
$res[0]['parse']['displaytitle']
|
|
);
|
|
}
|
|
|
|
public function testIncompatFormat() {
|
|
$this->expectApiErrorCode( 'badformat-generic' );
|
|
|
|
$this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'prop' => 'categories',
|
|
'title' => __CLASS__,
|
|
'text' => '',
|
|
'contentformat' => 'application/json',
|
|
] );
|
|
}
|
|
|
|
public function testIgnoreFormatUsingPage() {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'page' => __CLASS__,
|
|
'prop' => 'wikitext',
|
|
'contentformat' => 'text/plain',
|
|
] );
|
|
$this->assertArrayHasKey( 'wikitext', $res[0]['parse'] );
|
|
}
|
|
|
|
public function testShouldCastNumericImageLinksToString(): void {
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'parse',
|
|
'title' => __CLASS__,
|
|
'prop' => 'images',
|
|
'text' => '[[File:1]]',
|
|
] );
|
|
$this->assertSame( [ '1' ], $res[0]['parse']['images'] );
|
|
}
|
|
}
|