wiki.techinc.nl/tests/phpunit/includes/parser/ParserMethodsTest.php
Brad Jorsch d65e96b763 Use new externallinks.el_index_60 field
This adds a method to LinkFilter to build the query conditions necessary
to properly use it, and adjusts code to use it.

This also takes the opportunity to clean up the calculation of el_index:
IPs are handled more sensibly and IDNs are canonicalized.

Also weird edge cases for invalid hosts like "http://.example.com" and
corresponding searches like "http://*..example.com" are now handled more
regularly instead of being treated as if the extra dot were omitted,
while explicit specification of the DNS root like "http://example.com./"
is canonicalized to the usual implicit specification.

Note that this patch will break link searches for links where the host
is an IP or IDN until refreshExternallinksIndex.php is run.

Bug: T59176
Bug: T130482
Change-Id: I84d224ef23de22dfe179009ec3a11fd0e4b5f56d
2018-11-12 22:33:18 +00:00

400 lines
12 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"&param[]=valüe',
'http://example.org/foo%20bar?param%5B%5D=%22value%22&param%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/?&=+;',
],
[
'IPv6 links aren\'t escaped',
'http://[::1]/foobar',
'http://[::1]/foobar',
],
[
'non-IPv6 links aren\'t unescaped',
'http://%5B::1%5D/foobar',
'http://%5B::1%5D/foobar',
],
];
}
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' );
}
}
public static function provideGuessSectionNameFromWikiText() {
return [
[ '1/2', 'html5', '#1/2' ],
[ '1/2', 'legacy', '#1.2F2' ],
];
}
/** @dataProvider provideGuessSectionNameFromWikiText */
public function testGuessSectionNameFromWikiText( $input, $mode, $expected ) {
$this->setMwGlobals( [ 'wgFragmentMode' => [ $mode ] ] );
global $wgParser;
$result = $wgParser->guessSectionNameFromWikiText( $input );
$this->assertEquals( $result, $expected );
}
// @todo Add tests for cleanSig() / cleanSigInSig(), getSection(),
// replaceSection(), getPreloadText()
}