2017-02-20 04:33:24 +00:00
< ? php
2024-10-20 11:21:02 +00:00
use MediaWiki\Content\ContentHandler ;
2024-06-10 19:59:03 +00:00
use MediaWiki\Context\RequestContext ;
2022-02-06 14:39:49 +00:00
use MediaWiki\Extension\TemplateStyles\Hooks as TemplateStylesHooks ;
use MediaWiki\Extension\TemplateStyles\TemplateStylesContent ;
2024-07-29 11:02:33 +00:00
use MediaWiki\MainConfigNames ;
2024-10-20 11:21:02 +00:00
use MediaWiki\Parser\ParserOptions ;
use MediaWiki\Registration\ExtensionRegistry ;
2020-04-17 16:59:59 +00:00
use MediaWiki\Revision\MutableRevisionRecord ;
use MediaWiki\Revision\SlotRecord ;
2023-08-19 04:19:46 +00:00
use MediaWiki\Title\Title ;
2020-09-15 17:34:01 +00:00
use Wikimedia\CSS\Parser\Parser as CSSParser ;
2020-04-16 18:38:36 +00:00
2017-02-20 04:33:24 +00:00
/**
* @ group TemplateStyles
* @ group Database
2022-02-06 14:39:49 +00:00
* @ covers \MediaWiki\Extension\TemplateStyles\Hooks
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 );
2021-12-16 20:54:21 +00:00
$page = $this -> getServiceContainer () -> getWikiPageFactory () -> newFromTitle ( $title );
2017-02-20 04:33:24 +00:00
$user = static :: getTestSysop () -> getUser ();
2021-06-24 05:58:50 +00:00
$status = $page -> doUserEditContent ( $content , $user , 'Test for TemplateStyles' );
2020-05-08 00:16:42 +00:00
if ( ! $status -> isOk () ) {
$this -> fail ( " Failed to create $title : " . $status -> getWikiText ( false , false , 'en' ) );
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' );
}
2020-09-15 17:34:01 +00:00
public function testGetSanitizerNonLinearWrapper () {
$sanitizer = TemplateStylesHooks :: getSanitizer ( 'foo' , 'div[data]' );
$sanitizer -> sanitize ( CSSParser :: newFromString ( '.not-empty { }' ) -> parseStylesheet () );
$this -> assertSame ( [], $sanitizer -> getSanitizationErrors () );
}
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 ) {
2024-07-29 11:02:33 +00:00
$this -> overrideConfigValues ( [
MainConfigNames :: TextModelsToParse => $textModelsToParse ,
'TemplateStylesAutoParseContent' => $autoParseContent ,
2017-02-20 04:33:24 +00:00
] );
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 ) {
2024-07-29 11:02:33 +00:00
$this -> overrideConfigValues ( [
'TemplateStylesNamespaces' => [
2018-10-18 13:32:49 +00:00
10 => true ,
2 => false ,
3000 => true ,
3002 => true ,
3006 => false ,
],
2024-07-29 11:02:33 +00:00
MainConfigNames :: NamespacesWithSubpages => [
2018-10-18 13:32:49 +00:00
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' ;
2023-09-24 20:12:07 +00:00
$ret = ( new TemplateStylesHooks ) -> onContentHandlerDefaultModelFor (
2017-02-20 04:33:24 +00:00
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
];
}
/**
* 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 = []
) {
2024-07-29 11:02:33 +00:00
$this -> overrideConfigValues ( $globals + [
MainConfigNames :: ScriptPath => '' ,
MainConfigNames :: Script => '/index.php' ,
MainConfigNames :: ArticlePath => '/wiki/$1' ,
2017-02-20 04:33:24 +00:00
] );
2020-04-17 16:59:59 +00:00
$oldCurrentRevisionRecordCallback = $popt -> setCurrentRevisionRecordCallback (
2021-05-03 10:21:33 +00:00
static function ( Title $title , $parser = null ) use ( & $oldCurrentRevisionRecordCallback ) {
2017-02-20 04:33:24 +00:00
if ( $title -> getPrefixedText () === 'Template:Test replacement' ) {
$user = RequestContext :: getMain () -> getUser ();
2020-04-17 16:59:59 +00:00
$revRecord = new MutableRevisionRecord ( $title );
$revRecord -> setUser ( $user );
$revRecord -> setContent (
SlotRecord :: MAIN ,
new TemplateStylesContent ( '.baz { color:orange; bogus:bogus; }' )
);
$revRecord -> setParentId ( $title -> getLatestRevID () );
return $revRecord ;
2017-02-20 04:33:24 +00:00
}
2020-04-17 16:59:59 +00:00
return call_user_func ( $oldCurrentRevisionRecordCallback , $title , $parser );
2017-02-20 04:33:24 +00:00
}
);
2024-07-29 10:54:16 +00:00
$services = $this -> getServiceContainer ();
2020-04-16 18:38:36 +00:00
$parser = $services -> getParserFactory () -> create ();
2017-02-20 04:33:24 +00:00
$parser -> firstCallInit ();
2021-02-18 22:23:22 +00:00
if ( ! in_array ( 'templatestyles' , $parser -> getTags (), true ) ) {
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 );
2021-05-03 10:21:33 +00:00
$expect = preg_replace_callback ( '/\{\{REV:(.*?)\}\}/' , static function ( $m ) {
2017-11-24 19:20:47 +00:00
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 () );
2024-04-29 17:36:03 +00:00
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
@ $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 />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><p><strong class= \" error \" >TemplateStyles' <code>src</code> attribute must not be empty.</strong> \n </p></div> " ,
2017-02-20 04:33:24 +00:00
],
'Tag with invalid src' => [
2017-12-22 18:43:15 +00:00
$popt , [],
2017-02-20 04:33:24 +00:00
'<templatestyles src="Test<>" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><p><strong class= \" error \" >Invalid title for TemplateStyles' <code>src</code> attribute.</strong> \n </p></div> " ,
2017-02-20 04:33:24 +00:00
],
'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" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><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
],
'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" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><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
],
'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" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><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> " ,
2017-02-20 04:33:24 +00:00
],
'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" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><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> " ,
2017-02-20 04:33:24 +00:00
],
'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" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><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
],
2018-02-06 02:55:05 +00:00
'Disabled' => [
$popt , [],
'<templatestyles src="TemplateStyles test/styles1.css" />' ,
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ></div> " ,
2024-07-29 11:02:33 +00:00
[ 'TemplateStylesDisable' => true ],
2018-02-06 02:55:05 +00:00
],
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" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><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
],
2018-08-24 19:07:10 +00:00
'Hoistable selectors are hoisted' => [
$popt , [],
'<templatestyles src="TemplateStyles test/styles3.css" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><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
],
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" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
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-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" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2019-04-12 21:58:20 +00:00
" <style data-mw-deduplicate= \" TemplateStyles:r { { REV:styles1.css}} \" >.mw-parser-output .foo { color:blue}</style> " ,
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 " />
' ),
2021-03-26 23:59:00 +00:00
// phpcs:disable Generic.Files.LineLength
2017-02-20 04:33:24 +00:00
trim ( '
2023-11-06 19:43:49 +00:00
< div class = " mw-content-ltr templatestyles-test " lang = " en " dir = " ltr " >< style data - mw - deduplicate = " TemplateStyles:r { { REV:styles1.css}}/templatestyles-test " >. templatestyles - test . foo { color : blue } </ style >
2023-05-21 16:07:53 +00:00
< link rel = " mw-deduplicated-inline-style " href = " mw-data:TemplateStyles:r { { REV:styles1.css}}/templatestyles-test " >
2017-11-24 19:20:47 +00:00
< style data - mw - deduplicate = " TemplateStyles:r { { REV:styles2.css}}/templatestyles-test " >. templatestyles - test . bar { color : green } </ style >
2023-05-21 16:07:53 +00:00
< link rel = " mw-deduplicated-inline-style " href = " mw-data:TemplateStyles:r { { REV:styles1.css}}/templatestyles-test " >
< 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
' ),
2021-03-26 23:59:00 +00:00
// phpcs:enable
2018-10-17 17:28:43 +00:00
],
'Wrapper parameter' => [
$popt2 , [],
'<templatestyles src="TemplateStyles test/styles1.css" wrapper=".foobar" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr mw-parser-output \" lang= \" en \" dir= \" ltr \" ><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
],
'Invalid wrapper parameter' => [
$popt , [],
'<templatestyles src="TemplateStyles test/styles1.css" wrapper=".foo .bar" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><p><strong class= \" error \" >Invalid value for TemplateStyles' <code>wrapper</code> attribute.</strong> \n </p></div> " ,
2018-10-17 17:28:43 +00:00
],
'Invalid wrapper parameter (2)' => [
$popt , [],
'<templatestyles src="TemplateStyles test/styles1.css" wrapper=".foo/*" />' ,
2021-03-26 23:59:00 +00:00
// phpcs:ignore Generic.Files.LineLength
2023-11-06 19:43:49 +00:00
" <div class= \" mw-content-ltr templatestyles-test \" lang= \" en \" dir= \" ltr \" ><p><strong class= \" error \" >Invalid value for TemplateStyles' <code>wrapper</code> attribute.</strong> \n </p></div> " ,
2018-10-17 17:28:43 +00:00
],
'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 " />
' ),
2021-03-26 23:59:00 +00:00
// phpcs:disable Generic.Files.LineLength
2018-10-17 17:28:43 +00:00
trim ( '
2023-11-06 19:43:49 +00:00
< div class = " mw-content-ltr mw-parser-output " lang = " en " dir = " ltr " >< style data - mw - deduplicate = " TemplateStyles:r { { REV:styles1.css}} " >. mw - parser - output . foo { color : blue } </ style >
2023-05-21 16:07:53 +00:00
< link rel = " mw-deduplicated-inline-style " href = " mw-data:TemplateStyles:r { { REV:styles1.css}} " >
2018-10-17 17:28:43 +00:00
< 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 >
2023-05-21 16:07:53 +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
' ),
2021-03-26 23:59:00 +00:00
// phpcs:enable
2017-02-20 04:33:24 +00:00
],
];
}
}