During development a lot of classes were placed in MediaWiki\Storage\. The precedent set would mean that every class relating to something stored in a database table, plus all related value classes and such, would go into that namespace. Let's put them into MediaWiki\Revision\ instead. Then future classes related to the 'page' table can go into MediaWiki\Page\, future classes related to the 'user' table can go into MediaWiki\User\, and so on. Note I didn't move DerivedPageDataUpdater, PageUpdateException, PageUpdater, or RevisionSlotsUpdate in this patch. If these are kept long-term, they probably belong in MediaWiki\Page\ or MediaWiki\Edit\ instead. Bug: T204158 Change-Id: I16bea8927566a3c73c07e4f4afb3537e05aa04a5
375 lines
11 KiB
PHP
375 lines
11 KiB
PHP
<?php
|
|
use MediaWiki\Revision\MutableRevisionRecord;
|
|
use MediaWiki\Revision\RevisionStore;
|
|
use MediaWiki\Revision\SlotRecord;
|
|
use MediaWiki\User\UserIdentityValue;
|
|
|
|
/**
|
|
* @group Database
|
|
* @covers Parser
|
|
* @covers BlockLevelPass
|
|
*/
|
|
class ParserMethodsTest extends MediaWikiLangTestCase {
|
|
|
|
public static function providePreSaveTransform() {
|
|
return [
|
|
[ 'hello this is ~~~',
|
|
"hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
|
|
],
|
|
[ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
|
|
'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider providePreSaveTransform
|
|
*/
|
|
public function testPreSaveTransform( $text, $expected ) {
|
|
global $wgParser;
|
|
|
|
$title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
|
|
$user = new User();
|
|
$user->setName( "127.0.0.1" );
|
|
$popts = ParserOptions::newFromUser( $user );
|
|
$text = $wgParser->preSaveTransform( $text, $title, $user, $popts );
|
|
|
|
$this->assertEquals( $expected, $text );
|
|
}
|
|
|
|
public static function provideStripOuterParagraph() {
|
|
// This mimics the most common use case (stripping paragraphs generated by the parser).
|
|
$message = new RawMessage( "Message text." );
|
|
|
|
return [
|
|
[
|
|
"<p>Text.</p>",
|
|
"Text.",
|
|
],
|
|
[
|
|
"<p class='foo'>Text.</p>",
|
|
"<p class='foo'>Text.</p>",
|
|
],
|
|
[
|
|
"<p>Text.\n</p>\n",
|
|
"Text.",
|
|
],
|
|
[
|
|
"<p>Text.</p><p>More text.</p>",
|
|
"<p>Text.</p><p>More text.</p>",
|
|
],
|
|
[
|
|
$message->parse(),
|
|
"Message text.",
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideStripOuterParagraph
|
|
*/
|
|
public function testStripOuterParagraph( $text, $expected ) {
|
|
$this->assertEquals( $expected, Parser::stripOuterParagraph( $text ) );
|
|
}
|
|
|
|
/**
|
|
* @expectedException MWException
|
|
* @expectedExceptionMessage Parser state cleared while parsing.
|
|
* Did you call Parser::parse recursively?
|
|
*/
|
|
public function testRecursiveParse() {
|
|
global $wgParser;
|
|
$title = Title::newFromText( 'foo' );
|
|
$po = new ParserOptions;
|
|
$wgParser->setHook( 'recursivecallparser', [ $this, 'helperParserFunc' ] );
|
|
$wgParser->parse( '<recursivecallparser>baz</recursivecallparser>', $title, $po );
|
|
}
|
|
|
|
public function helperParserFunc( $input, $args, $parser ) {
|
|
$title = Title::newFromText( 'foo' );
|
|
$po = new ParserOptions;
|
|
$parser->parse( $input, $title, $po );
|
|
return 'bar';
|
|
}
|
|
|
|
public function testCallParserFunction() {
|
|
global $wgParser;
|
|
|
|
// Normal parses test passing PPNodes. Test passing an array.
|
|
$title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
|
|
$wgParser->startExternalParse( $title, new ParserOptions(), Parser::OT_HTML );
|
|
$frame = $wgParser->getPreprocessor()->newFrame();
|
|
$ret = $wgParser->callParserFunction( $frame, '#tag',
|
|
[ 'pre', 'foo', 'style' => 'margin-left: 1.6em' ]
|
|
);
|
|
$ret['text'] = $wgParser->mStripState->unstripBoth( $ret['text'] );
|
|
$this->assertSame( [
|
|
'found' => true,
|
|
'text' => '<pre style="margin-left: 1.6em">foo</pre>',
|
|
], $ret, 'callParserFunction works for {{#tag:pre|foo|style=margin-left: 1.6em}}' );
|
|
}
|
|
|
|
/**
|
|
* @covers Parser
|
|
* @covers ParserOutput::getSections
|
|
*/
|
|
public function testGetSections() {
|
|
global $wgParser;
|
|
|
|
$title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
|
|
$out = $wgParser->parse( "==foo==\n<h2>bar</h2>\n==baz==\n", $title, new ParserOptions() );
|
|
$this->assertSame( [
|
|
[
|
|
'toclevel' => 1,
|
|
'level' => '2',
|
|
'line' => 'foo',
|
|
'number' => '1',
|
|
'index' => '1',
|
|
'fromtitle' => $title->getPrefixedDBkey(),
|
|
'byteoffset' => 0,
|
|
'anchor' => 'foo',
|
|
],
|
|
[
|
|
'toclevel' => 1,
|
|
'level' => '2',
|
|
'line' => 'bar',
|
|
'number' => '2',
|
|
'index' => '',
|
|
'fromtitle' => false,
|
|
'byteoffset' => null,
|
|
'anchor' => 'bar',
|
|
],
|
|
[
|
|
'toclevel' => 1,
|
|
'level' => '2',
|
|
'line' => 'baz',
|
|
'number' => '3',
|
|
'index' => '2',
|
|
'fromtitle' => $title->getPrefixedDBkey(),
|
|
'byteoffset' => 21,
|
|
'anchor' => 'baz',
|
|
],
|
|
], $out->getSections(), 'getSections() with proper value when <h2> is used' );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideNormalizeLinkUrl
|
|
*/
|
|
public function testNormalizeLinkUrl( $explanation, $url, $expected ) {
|
|
$this->assertEquals( $expected, Parser::normalizeLinkUrl( $url ), $explanation );
|
|
}
|
|
|
|
public static function provideNormalizeLinkUrl() {
|
|
return [
|
|
[
|
|
'Escaping of unsafe characters',
|
|
'http://example.org/foo bar?param[]="value"¶m[]=valüe',
|
|
'http://example.org/foo%20bar?param%5B%5D=%22value%22¶m%5B%5D=val%C3%BCe',
|
|
],
|
|
[
|
|
'Case normalization of percent-encoded characters',
|
|
'http://example.org/%ab%cD%Ef%FF',
|
|
'http://example.org/%AB%CD%EF%FF',
|
|
],
|
|
[
|
|
'Unescaping of safe characters',
|
|
'http://example.org/%3C%66%6f%6F%3E?%3C%66%6f%6F%3E#%3C%66%6f%6F%3E',
|
|
'http://example.org/%3Cfoo%3E?%3Cfoo%3E#%3Cfoo%3E',
|
|
],
|
|
[
|
|
'Context-sensitive replacement of sometimes-safe characters',
|
|
'http://example.org/%23%2F%3F%26%3D%2B%3B?%23%2F%3F%26%3D%2B%3B#%23%2F%3F%26%3D%2B%3B',
|
|
'http://example.org/%23%2F%3F&=+;?%23/?%26%3D%2B%3B#%23/?&=+;',
|
|
],
|
|
];
|
|
}
|
|
|
|
public function testWrapOutput() {
|
|
global $wgParser;
|
|
$title = Title::newFromText( 'foo' );
|
|
$po = new ParserOptions();
|
|
$wgParser->parse( 'Hello World', $title, $po );
|
|
$text = $wgParser->getOutput()->getText();
|
|
|
|
$this->assertContains( 'Hello World', $text );
|
|
$this->assertContains( '<div', $text );
|
|
$this->assertContains( 'class="mw-parser-output"', $text );
|
|
}
|
|
|
|
/**
|
|
* @param string $name
|
|
* @return Title
|
|
*/
|
|
private function getMockTitle( $name ) {
|
|
$title = $this->getMock( Title::class );
|
|
$title->method( 'getPrefixedDBkey' )->willReturn( $name );
|
|
$title->method( 'getPrefixedText' )->willReturn( $name );
|
|
$title->method( 'getDBkey' )->willReturn( $name );
|
|
$title->method( 'getText' )->willReturn( $name );
|
|
$title->method( 'getNamespace' )->willReturn( 0 );
|
|
$title->method( 'getPageLanguage' )->willReturn( Language::factory( 'en' ) );
|
|
|
|
return $title;
|
|
}
|
|
|
|
public function provideRevisionAccess() {
|
|
$title = $this->getMockTitle( 'ParserRevisionAccessTest' );
|
|
|
|
$frank = $this->getMockBuilder( User::class )
|
|
->disableOriginalConstructor()
|
|
->getMock();
|
|
|
|
$frank->method( 'getName' )->willReturn( 'Frank' );
|
|
|
|
$text = '* user:{{REVISIONUSER}};id:{{REVISIONID}};time:{{REVISIONTIMESTAMP}};';
|
|
$po = new ParserOptions( $frank );
|
|
|
|
yield 'current' => [ $text, $po, 0, 'user:CurrentAuthor;id:200;time:20160606000000;' ];
|
|
yield 'current with ID' => [ $text, $po, 200, 'user:CurrentAuthor;id:200;time:20160606000000;' ];
|
|
|
|
$text = '* user:{{REVISIONUSER}};id:{{REVISIONID}};time:{{REVISIONTIMESTAMP}};';
|
|
$po = new ParserOptions( $frank );
|
|
|
|
yield 'old' => [ $text, $po, 100, 'user:OldAuthor;id:100;time:20140404000000;' ];
|
|
|
|
$oldRevision = new MutableRevisionRecord( $title );
|
|
$oldRevision->setId( 100 );
|
|
$oldRevision->setUser( new UserIdentityValue( 7, 'FauxAuthor', 0 ) );
|
|
$oldRevision->setTimestamp( '20141111111111' );
|
|
$oldRevision->setContent( SlotRecord::MAIN, new WikitextContent( 'FAUX' ) );
|
|
|
|
$po = new ParserOptions( $frank );
|
|
$po->setCurrentRevisionCallback( function () use ( $oldRevision ) {
|
|
return new Revision( $oldRevision );
|
|
} );
|
|
|
|
yield 'old with override' => [ $text, $po, 100, 'user:FauxAuthor;id:100;time:20141111111111;' ];
|
|
|
|
$text = '* user:{{REVISIONUSER}};user-subst:{{subst:REVISIONUSER}};';
|
|
|
|
$po = new ParserOptions( $frank );
|
|
$po->setIsPreview( true );
|
|
|
|
yield 'preview without override, using context' => [
|
|
$text,
|
|
$po,
|
|
null,
|
|
'user:Frank;',
|
|
'user-subst:Frank;',
|
|
];
|
|
|
|
$text = '* user:{{REVISIONUSER}};time:{{REVISIONTIMESTAMP}};'
|
|
. 'user-subst:{{subst:REVISIONUSER}};time-subst:{{subst:REVISIONTIMESTAMP}};';
|
|
|
|
$newRevision = new MutableRevisionRecord( $title );
|
|
$newRevision->setUser( new UserIdentityValue( 9, 'NewAuthor', 0 ) );
|
|
$newRevision->setTimestamp( '20180808000000' );
|
|
$newRevision->setContent( SlotRecord::MAIN, new WikitextContent( 'NEW' ) );
|
|
|
|
$po = new ParserOptions( $frank );
|
|
$po->setIsPreview( true );
|
|
$po->setCurrentRevisionCallback( function () use ( $newRevision ) {
|
|
return new Revision( $newRevision );
|
|
} );
|
|
|
|
yield 'preview' => [
|
|
$text,
|
|
$po,
|
|
null,
|
|
'user:NewAuthor;time:20180808000000;',
|
|
'user-subst:NewAuthor;time-subst:20180808000000;',
|
|
];
|
|
|
|
$po = new ParserOptions( $frank );
|
|
$po->setCurrentRevisionCallback( function () use ( $newRevision ) {
|
|
return new Revision( $newRevision );
|
|
} );
|
|
|
|
yield 'pre-save' => [
|
|
$text,
|
|
$po,
|
|
null,
|
|
'user:NewAuthor;time:20180808000000;',
|
|
'user-subst:NewAuthor;time-subst:20180808000000;',
|
|
];
|
|
|
|
$text = "(ONE)<includeonly>(TWO)</includeonly>"
|
|
. "<noinclude>#{{:ParserRevisionAccessTest}}#</noinclude>";
|
|
|
|
$newRevision = new MutableRevisionRecord( $title );
|
|
$newRevision->setUser( new UserIdentityValue( 9, 'NewAuthor', 0 ) );
|
|
$newRevision->setTimestamp( '20180808000000' );
|
|
$newRevision->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
|
|
|
|
$po = new ParserOptions( $frank );
|
|
$po->setIsPreview( true );
|
|
$po->setCurrentRevisionCallback( function () use ( $newRevision ) {
|
|
return new Revision( $newRevision );
|
|
} );
|
|
|
|
yield 'preview with self-transclude' => [ $text, $po, null, '(ONE)#(ONE)(TWO)#' ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideRevisionAccess
|
|
*/
|
|
public function testRevisionAccess(
|
|
$text,
|
|
ParserOptions $po,
|
|
$revId,
|
|
$expectedInHtml,
|
|
$expectedInPst = null
|
|
) {
|
|
global $wgParser;
|
|
|
|
$title = $this->getMockTitle( 'ParserRevisionAccessTest' );
|
|
|
|
$po->enableLimitReport( false );
|
|
|
|
$oldRevision = new MutableRevisionRecord( $title );
|
|
$oldRevision->setId( 100 );
|
|
$oldRevision->setUser( new UserIdentityValue( 7, 'OldAuthor', 0 ) );
|
|
$oldRevision->setTimestamp( '20140404000000' );
|
|
$oldRevision->setContent( SlotRecord::MAIN, new WikitextContent( 'OLD' ) );
|
|
|
|
$currentRevision = new MutableRevisionRecord( $title );
|
|
$currentRevision->setId( 200 );
|
|
$currentRevision->setUser( new UserIdentityValue( 9, 'CurrentAuthor', 0 ) );
|
|
$currentRevision->setTimestamp( '20160606000000' );
|
|
$currentRevision->setContent( SlotRecord::MAIN, new WikitextContent( 'CURRENT' ) );
|
|
|
|
$revisionStore = $this->getMockBuilder( RevisionStore::class )
|
|
->disableOriginalConstructor()
|
|
->getMock();
|
|
|
|
$revisionStore
|
|
->method( 'getKnownCurrentRevision' )
|
|
->willReturnMap( [
|
|
[ $title, 100, $oldRevision ],
|
|
[ $title, 200, $currentRevision ],
|
|
[ $title, 0, $currentRevision ],
|
|
] );
|
|
|
|
$revisionStore
|
|
->method( 'getRevisionById' )
|
|
->willReturnMap( [
|
|
[ 100, 0, $oldRevision ],
|
|
[ 200, 0, $currentRevision ],
|
|
] );
|
|
|
|
$this->setService( 'RevisionStore', $revisionStore );
|
|
|
|
$wgParser->parse( $text, $title, $po, true, true, $revId );
|
|
$html = $wgParser->getOutput()->getText();
|
|
|
|
$this->assertContains( $expectedInHtml, $html, 'In HTML' );
|
|
|
|
if ( $expectedInPst !== null ) {
|
|
$pst = $wgParser->preSaveTransform( $text, $title, $po->getUser(), $po );
|
|
$this->assertContains( $expectedInPst, $pst, 'After Pre-Safe Transform' );
|
|
}
|
|
}
|
|
|
|
// @todo Add tests for cleanSig() / cleanSigInSig(), getSection(),
|
|
// replaceSection(), getPreloadText()
|
|
}
|