2017-02-20 04:33:24 +00:00
< ? php
2020-04-16 18:38:36 +00:00
use MediaWiki\MediaWikiServices ;
2020-04-20 00:22:56 +00:00
use MediaWiki\Revision\SlotRecord ;
2020-04-16 18:38:36 +00:00
2017-02-20 04:33:24 +00:00
/**
* @ group TemplateStyles
* @ group Database
2018-02-06 02:19:30 +00:00
* @ covers TemplateStylesHooks
2017-02-20 04:33:24 +00:00
*/
class TemplateStylesHooksTest extends MediaWikiLangTestCase {
protected function addPage ( $page , $text , $model ) {
$title = Title :: newFromText ( 'Template:TemplateStyles test/' . $page );
$content = ContentHandler :: makeContent ( $text , $title , $model );
2020-04-20 00:22:56 +00:00
$wikipage = WikiPage :: factory ( $title );
2017-02-20 04:33:24 +00:00
$user = static :: getTestSysop () -> getUser ();
2020-04-20 00:22:56 +00:00
$summary = CommentStoreComment :: newUnsavedComment ( 'Test for TemplateStyles' );
$updater = $wikipage -> newPageUpdater ( $user );
$updater -> setContent (
SlotRecord :: MAIN ,
$content
);
$updater -> saveRevision ( $summary );
if ( ! $updater -> wasSuccessful () ) {
$this -> fail ( " Failed to create $title . " );
2017-02-20 04:33:24 +00:00
}
}
public function addDBDataOnce () {
$this -> addPage ( 'wikitext' , '.foo { color: red; }' , CONTENT_MODEL_WIKITEXT );
$this -> addPage ( 'nonsanitized.css' , '.foo { color: red; }' , CONTENT_MODEL_CSS );
$this -> addPage ( 'styles1.css' , '.foo { color: blue; }' , 'sanitized-css' );
$this -> addPage ( 'styles2.css' , '.bar { color: green; }' , 'sanitized-css' );
2018-08-24 19:07:10 +00:00
$this -> addPage (
'styles3.css' , 'html.no-js body.skin-minerva .bar { color: yellow; }' , 'sanitized-css'
);
2017-02-20 04:33:24 +00:00
}
2018-10-17 17:28:43 +00:00
public function testGetSanitizerInvalidWrapper () {
2019-10-12 02:39:25 +00:00
$this -> expectException ( InvalidArgumentException :: class );
$this -> expectExceptionMessage ( 'Invalid value for $extraWrapper: .foo>.bar' );
2018-10-17 17:28:43 +00:00
TemplateStylesHooks :: getSanitizer ( 'foo' , '.foo>.bar' );
}
2017-02-20 04:33:24 +00:00
/**
* @ dataProvider provideOnRegistration
* @ param array $textModelsToParse
* @ param bool $autoParseContent
* @ param array $expect
*/
public function testOnRegistration ( $textModelsToParse , $autoParseContent , $expect ) {
$this -> setMwGlobals ( [
'wgTextModelsToParse' => $textModelsToParse ,
'wgTemplateStylesAutoParseContent' => $autoParseContent ,
] );
global $wgTextModelsToParse ;
TemplateStylesHooks :: onRegistration ();
$this -> assertSame ( $expect , $wgTextModelsToParse );
}
public static function provideOnRegistration () {
return [
[
[ CONTENT_MODEL_WIKITEXT ],
true ,
[ CONTENT_MODEL_WIKITEXT ]
],
[
[ CONTENT_MODEL_WIKITEXT , CONTENT_MODEL_CSS ],
true ,
[ CONTENT_MODEL_WIKITEXT , CONTENT_MODEL_CSS , 'sanitized-css' ],
],
[
[ CONTENT_MODEL_WIKITEXT , CONTENT_MODEL_CSS ],
false ,
[ CONTENT_MODEL_WIKITEXT , CONTENT_MODEL_CSS ],
],
];
}
/**
* @ dataProvider provideOnContentHandlerDefaultModelFor
*/
public function testOnContentHandlerDefaultModelFor ( $ns , $title , $expect ) {
$this -> setMwGlobals ( [
2018-10-18 13:32:49 +00:00
'wgTemplateStylesNamespaces' => [
10 => true ,
2 => false ,
3000 => true ,
3002 => true ,
3006 => false ,
],
'wgNamespacesWithSubpages' => [
10 => true ,
2 => true ,
3000 => true ,
3002 => false ,
3004 => true ,
3006 => true
],
2017-02-20 04:33:24 +00:00
] );
2018-10-18 13:32:49 +00:00
$reset = ExtensionRegistry :: getInstance () -> setAttributeForTest (
'TemplateStylesNamespaces' , [ 3004 , 3006 ]
);
2017-02-20 04:33:24 +00:00
$model = 'unchanged' ;
$ret = TemplateStylesHooks :: onContentHandlerDefaultModelFor (
Title :: makeTitle ( $ns , $title ), $model
);
$this -> assertSame ( ! $expect , $ret );
$this -> assertSame ( $expect ? 'sanitized-css' : 'unchanged' , $model );
}
public static function provideOnContentHandlerDefaultModelFor () {
return [
[ 10 , 'Test/test.css' , true ],
[ 10 , 'Test.css' , false ],
[ 10 , 'Test/test.xss' , false ],
[ 10 , 'Test/test.CSS' , false ],
[ 3000 , 'Test/test.css' , true ],
[ 3002 , 'Test/test.css' , false ],
[ 2 , 'Test/test.css' , false ],
2018-10-18 13:32:49 +00:00
[ 3004 , 'Test/test.css' , true ],
[ 3006 , 'Test/test.css' , false ],
2017-02-20 04:33:24 +00:00
];
}
2018-02-06 02:55:05 +00:00
/**
* @ dataProvider provideOnCodeEditorGetPageLanguage
*/
public function testOnCodeEditorGetPageLanguage ( $useCodeEditor , $model , $expect ) {
$this -> setMwGlobals ( [
'wgTemplateStylesUseCodeEditor' => $useCodeEditor ,
] );
$title = Title :: makeTitle ( NS_TEMPLATE , 'Test.css' );
$lang = 'unchanged' ;
$ret = TemplateStylesHooks :: onCodeEditorGetPageLanguage (
$title , $lang , $model , 'text/x-whatever'
);
$this -> assertSame ( ! $expect , $ret );
$this -> assertSame ( $expect ? 'css' : 'unchanged' , $lang );
}
public static function provideOnCodeEditorGetPageLanguage () {
return [
[ true , 'wikitext' , false ],
[ true , 'css' , false ],
[ true , 'sanitized-css' , true ],
[ false , 'sanitized-css' , false ],
];
}
2017-02-20 04:33:24 +00:00
/**
* Unfortunately we can ' t just use a parserTests . txt file because our
* tag ' s output depends on the revision IDs of the input pages .
* @ dataProvider provideTag
*/
2018-02-06 02:55:05 +00:00
public function testTag (
ParserOptions $popt , $getTextOptions , $wikitext , $expect , $globals = []
) {
$this -> setMwGlobals ( $globals + [
2017-02-20 04:33:24 +00:00
'wgScriptPath' => '' ,
'wgScript' => '/index.php' ,
'wgArticlePath' => '/wiki/$1' ,
] );
$oldCurrentRevisionCallback = $popt -> setCurrentRevisionCallback (
2017-11-14 21:51:17 +00:00
function ( Title $title , $parser = false ) use ( & $oldCurrentRevisionCallback ) {
2017-02-20 04:33:24 +00:00
if ( $title -> getPrefixedText () === 'Template:Test replacement' ) {
$user = RequestContext :: getMain () -> getUser ();
return new Revision ( [
'page' => $title -> getArticleID (),
'user_text' => $user -> getName (),
'user' => $user -> getId (),
2017-11-14 21:51:17 +00:00
'parent_id' => $title -> getLatestRevID (),
2017-02-20 04:33:24 +00:00
'title' => $title ,
'content' => new TemplateStylesContent ( '.baz { color:orange; bogus:bogus; }' )
] );
}
return call_user_func ( $oldCurrentRevisionCallback , $title , $parser );
}
);
2020-04-16 18:38:36 +00:00
$services = MediaWikiServices :: getInstance ();
$parser = $services -> getParserFactory () -> create ();
2017-02-20 04:33:24 +00:00
$parser -> firstCallInit ();
if ( ! isset ( $parser -> mTagHooks [ 'templatestyles' ] ) ) {
2017-11-14 21:51:50 +00:00
throw new Exception ( 'templatestyles tag hook is not in the parser' );
2017-02-20 04:33:24 +00:00
}
$out = $parser -> parse ( $wikitext , Title :: newFromText ( 'Test' ), $popt );
$parser -> mPreprocessor = null ; # Break the Parser <-> Preprocessor cycle
2017-11-24 19:20:47 +00:00
$expect = preg_replace_callback ( '/\{\{REV:(.*?)\}\}/' , function ( $m ) {
return Title :: newFromText ( 'Template:TemplateStyles test/' . $m [ 1 ] ) -> getLatestRevID ();
}, $expect );
2017-12-22 18:43:15 +00:00
$this -> assertEquals ( $expect , $out -> getText ( $getTextOptions ) );
2017-02-20 04:33:24 +00:00
}
public static function provideTag () {
$popt = ParserOptions :: newFromContext ( RequestContext :: getMain () );
$popt -> setWrapOutputClass ( 'templatestyles-test' );
$popt2 = ParserOptions :: newFromContext ( RequestContext :: getMain () );
2018-02-06 02:55:05 +00:00
$popt3 = ParserOptions :: newFromContext ( RequestContext :: getMain () );
2018-02-11 06:03:39 +00:00
Wikimedia\quietCall ( [ $popt3 , 'setWrapOutputClass' ], false );
2018-02-06 02:55:05 +00:00
2017-02-20 04:33:24 +00:00
return [
'Tag without src' => [
2017-12-22 18:43:15 +00:00
$popt , [],
2017-02-20 04:33:24 +00:00
'<templatestyles />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
" <div class= \" templatestyles-test \" ><p><strong class= \" error \" >TemplateStyles' <code>src</code> attribute must not be empty.</strong> \n </p></div> " ,
// @codingStandardsIgnoreEnd
],
'Tag with invalid src' => [
2017-12-22 18:43:15 +00:00
$popt , [],
2017-02-20 04:33:24 +00:00
'<templatestyles src="Test<>" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
" <div class= \" templatestyles-test \" ><p><strong class= \" error \" >Invalid title for TemplateStyles' <code>src</code> attribute.</strong> \n </p></div> " ,
// @codingStandardsIgnoreEnd
],
'Tag with valid but nonexistent title' => [
2017-12-22 18:43:15 +00:00
$popt , [],
2017-02-20 04:33:24 +00:00
'<templatestyles src="ThisDoes\'\'\'Not\'\'\'Exist" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
2017-11-14 21:48:20 +00:00
" <div class= \" templatestyles-test \" ><p><strong class= \" error \" >Page <a href= \" /index.php?title=Template:ThisDoes%27%27%27Not%27%27%27Exist&action=edit&redlink=1 \" class= \" new \" title= \" Template:ThisDoes'''Not'''Exist (page does not exist) \" >Template:ThisDoes'''Not'''Exist</a> has no content.</strong> \n </p></div> " ,
2017-02-20 04:33:24 +00:00
// @codingStandardsIgnoreEnd
],
'Tag with valid but nonexistent title, main namespace' => [
2017-12-22 18:43:15 +00:00
$popt , [],
2017-02-20 04:33:24 +00:00
'<templatestyles src=":ThisDoes\'\'\'Not\'\'\'Exist" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
2017-11-14 21:48:20 +00:00
" <div class= \" templatestyles-test \" ><p><strong class= \" error \" >Page <a href= \" /index.php?title=ThisDoes%27%27%27Not%27%27%27Exist&action=edit&redlink=1 \" class= \" new \" title= \" ThisDoes'''Not'''Exist (page does not exist) \" >ThisDoes'''Not'''Exist</a> has no content.</strong> \n </p></div> " ,
2017-02-20 04:33:24 +00:00
// @codingStandardsIgnoreEnd
],
'Tag with wikitext page' => [
2017-12-22 18:43:15 +00:00
$popt , [],
2017-02-20 04:33:24 +00:00
'<templatestyles src="TemplateStyles test/wikitext" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
" <div class= \" templatestyles-test \" ><p><strong class= \" error \" >Page <a href= \" /wiki/Template:TemplateStyles_test/wikitext \" title= \" Template:TemplateStyles test/wikitext \" >Template:TemplateStyles test/wikitext</a> must have content model \" Sanitized CSS \" for TemplateStyles (current model is \" wikitext \" ).</strong> \n </p></div> " ,
// @codingStandardsIgnoreEnd
],
'Tag with CSS (not sanitized-css) page' => [
2017-12-22 18:43:15 +00:00
$popt , [],
2017-02-20 04:33:24 +00:00
'<templatestyles src="TemplateStyles test/nonsanitized.css" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
" <div class= \" templatestyles-test \" ><p><strong class= \" error \" >Page <a href= \" /wiki/Template:TemplateStyles_test/nonsanitized.css \" title= \" Template:TemplateStyles test/nonsanitized.css \" >Template:TemplateStyles test/nonsanitized.css</a> must have content model \" Sanitized CSS \" for TemplateStyles (current model is \" CSS \" ).</strong> \n </p></div> " ,
// @codingStandardsIgnoreEnd
],
'Working tag' => [
2017-12-22 18:43:15 +00:00
$popt , [],
2017-02-20 04:33:24 +00:00
'<templatestyles src="TemplateStyles test/styles1.css" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
2019-04-12 21:58:20 +00:00
" <div class= \" templatestyles-test \" ><style data-mw-deduplicate= \" TemplateStyles:r { { REV:styles1.css}}/templatestyles-test \" >.templatestyles-test .foo { color:blue}</style></div> " ,
2017-02-20 04:33:24 +00:00
// @codingStandardsIgnoreEnd
],
2018-02-06 02:55:05 +00:00
'Disabled' => [
$popt , [],
'<templatestyles src="TemplateStyles test/styles1.css" />' ,
" <div class= \" templatestyles-test \" ></div> " ,
[ 'wgTemplateStylesDisable' => true ],
],
2017-02-20 04:33:24 +00:00
'Replaced content (which includes sanitization errors)' => [
2017-12-22 18:43:15 +00:00
$popt , [],
2017-02-20 04:33:24 +00:00
'<templatestyles src="Test replacement" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
2019-04-12 21:58:20 +00:00
" <div class= \" templatestyles-test \" ><style data-mw-deduplicate= \" TemplateStyles:8fd14043c1cce91e8b9d1487a9d17d8d9ae43890/templatestyles-test \" >/* \n Errors processing stylesheet [[:Template:Test replacement]] (rev ): \n • Unrecognized or unsupported property at line 1 character 22. \n */ \n .templatestyles-test .baz { color:orange}</style></div> " ,
2017-02-20 04:33:24 +00:00
// @codingStandardsIgnoreEnd
],
2018-08-24 19:07:10 +00:00
'Hoistable selectors are hoisted' => [
$popt , [],
'<templatestyles src="TemplateStyles test/styles3.css" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
2019-04-12 21:58:20 +00:00
" <div class= \" templatestyles-test \" ><style data-mw-deduplicate= \" TemplateStyles:r { { REV:styles3.css}}/templatestyles-test \" >html.no-js body.skin-minerva .templatestyles-test .bar { color:yellow}</style></div> " ,
2018-08-24 19:07:10 +00:00
// @codingStandardsIgnoreEnd
],
2018-10-17 17:28:43 +00:00
'Still prefixed despite no wrapping class' => [
2017-12-22 18:43:15 +00:00
$popt2 , [ 'unwrap' => true ],
2017-02-20 04:33:24 +00:00
'<templatestyles src="TemplateStyles test/styles1.css" />' ,
2017-11-24 19:20:47 +00:00
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
2019-04-12 21:58:20 +00:00
" <style data-mw-deduplicate= \" TemplateStyles:r { { REV:styles1.css}} \" >.mw-parser-output .foo { color:blue}</style> " ,
2017-11-24 19:20:47 +00:00
// @codingStandardsIgnoreEnd
2017-02-20 04:33:24 +00:00
],
2018-10-17 17:28:43 +00:00
'Still prefixed despite deprecated no wrapping class' => [
2018-02-06 02:55:05 +00:00
$popt3 , [],
'<templatestyles src="TemplateStyles test/styles1.css" />' ,
2017-11-24 19:20:47 +00:00
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
2019-04-12 21:58:20 +00:00
" <style data-mw-deduplicate= \" TemplateStyles:r { { REV:styles1.css}} \" >.mw-parser-output .foo { color:blue}</style> " ,
2017-11-24 19:20:47 +00:00
// @codingStandardsIgnoreEnd
2018-02-06 02:55:05 +00:00
],
2017-11-24 19:20:47 +00:00
'Deduplicated tags' => [
2017-12-22 18:43:15 +00:00
$popt , [],
2017-02-20 04:33:24 +00:00
trim ( '
< templatestyles src = " TemplateStyles test/styles1.css " />
< templatestyles src = " TemplateStyles test/styles1.css " />
< templatestyles src = " TemplateStyles test/styles2.css " />
< templatestyles src = " TemplateStyles test/styles1.css " />
< templatestyles src = " TemplateStyles test/styles2.css " />
' ),
2017-11-24 19:20:47 +00:00
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
2017-02-20 04:33:24 +00:00
trim ( '
2018-03-03 23:24:21 +00:00
< div class = " templatestyles-test " >< style data - mw - deduplicate = " TemplateStyles:r { { REV:styles1.css}}/templatestyles-test " >. templatestyles - test . foo { color : blue } </ style >
2017-11-24 19:20:47 +00:00
< link rel = " mw-deduplicated-inline-style " href = " mw-data:TemplateStyles:r { { REV:styles1.css}}/templatestyles-test " />
< style data - mw - deduplicate = " TemplateStyles:r { { REV:styles2.css}}/templatestyles-test " >. templatestyles - test . bar { color : green } </ style >
< link rel = " mw-deduplicated-inline-style " href = " mw-data:TemplateStyles:r { { REV:styles1.css}}/templatestyles-test " />
2019-04-12 21:58:20 +00:00
< link rel = " mw-deduplicated-inline-style " href = " mw-data:TemplateStyles:r { { REV:styles2.css}}/templatestyles-test " /></ div >
2018-10-17 17:28:43 +00:00
' ),
// @codingStandardsIgnoreEnd
],
'Wrapper parameter' => [
$popt2 , [],
'<templatestyles src="TemplateStyles test/styles1.css" wrapper=".foobar" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
2019-04-12 21:58:20 +00:00
" <div class= \" mw-parser-output \" ><style data-mw-deduplicate= \" TemplateStyles:r { { REV:styles1.css}}/mw-parser-output/.foobar \" >.mw-parser-output .foobar .foo { color:blue}</style></div> " ,
2018-10-17 17:28:43 +00:00
// @codingStandardsIgnoreEnd
],
'Invalid wrapper parameter' => [
$popt , [],
'<templatestyles src="TemplateStyles test/styles1.css" wrapper=".foo .bar" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
" <div class= \" templatestyles-test \" ><p><strong class= \" error \" >Invalid value for TemplateStyles' <code>wrapper</code> attribute.</strong> \n </p></div> " ,
// @codingStandardsIgnoreEnd
],
'Invalid wrapper parameter (2)' => [
$popt , [],
'<templatestyles src="TemplateStyles test/styles1.css" wrapper=".foo/*" />' ,
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
" <div class= \" templatestyles-test \" ><p><strong class= \" error \" >Invalid value for TemplateStyles' <code>wrapper</code> attribute.</strong> \n </p></div> " ,
// @codingStandardsIgnoreEnd
],
'Wrapper parameter and proper deduplication' => [
$popt2 , [],
trim ( '
< templatestyles src = " TemplateStyles test/styles1.css " />
< templatestyles src = " TemplateStyles test/styles1.css " wrapper = " " />
< templatestyles src = " TemplateStyles test/styles1.css " wrapper = " .foobar " />
< templatestyles src = " TemplateStyles test/styles1.css " wrapper = " .foobaz " />
< templatestyles src = " TemplateStyles test/styles1.css " wrapper = " .foobar " />
' ),
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
trim ( '
< div class = " mw-parser-output " >< style data - mw - deduplicate = " TemplateStyles:r { { REV:styles1.css}} " >. mw - parser - output . foo { color : blue } </ style >
< link rel = " mw-deduplicated-inline-style " href = " mw-data:TemplateStyles:r { { REV:styles1.css}} " />
< style data - mw - deduplicate = " TemplateStyles:r { { REV:styles1.css}}/mw-parser-output/.foobar " >. mw - parser - output . foobar . foo { color : blue } </ style >
< style data - mw - deduplicate = " TemplateStyles:r { { REV:styles1.css}}/mw-parser-output/.foobaz " >. mw - parser - output . foobaz . foo { color : blue } </ style >
2019-04-12 21:58:20 +00:00
< link rel = " mw-deduplicated-inline-style " href = " mw-data:TemplateStyles:r { { REV:styles1.css}}/mw-parser-output/.foobar " /></ div >
2017-02-20 04:33:24 +00:00
' ),
2017-11-24 19:20:47 +00:00
// @codingStandardsIgnoreEnd
2017-02-20 04:33:24 +00:00
],
];
}
}