mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/TemplateStyles
synced 2024-11-15 03:35:47 +00:00
b301a30abf
wikimedia/css-sanitizer provides a real CSS parser, which should be safer than poking at things with regular expressions. Instead of the strange hybrid model that tried to both process inline CSS and save CSS when the template is saved, it now looks for <templatestyles src="Title" /> during the parse to do all the transclusion of styles. The output method is "<style> tags in the body", pending someone implementing T160563. It now also registers a "sanitized-css" content model, which should pick up the CSS syntax highlighting and will validate the submitted CSS on submit and prevent a save if it's not valid. This patch also takes advantage of LGPL-2.x § 3 to relicense the extension as GPL-2.0+, although at this point none of the LGPL code remains anyway. Bug: T133408 Bug: T136054 Bug: T135788 Bug: T135789 Change-Id: I993e6f18d32a43aac8398743133d227b05133bbd Depends-On: If4eb5bf71f94fa366ec4eddb6964e8f4df6b824a
254 lines
7.2 KiB
PHP
254 lines
7.2 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @group TemplateStyles
|
|
*/
|
|
class TemplateStylesContentTest extends TextContentTest {
|
|
|
|
protected function setUp() {
|
|
parent::setUp();
|
|
|
|
$this->setMwGlobals( [
|
|
'wgTextModelsToParse' => [
|
|
'sanitized-css',
|
|
],
|
|
'wgTemplateStylesMaxStylesheetSize' => 1024000,
|
|
] );
|
|
}
|
|
|
|
public function newContent( $text ) {
|
|
return new TemplateStylesContent( $text );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideSanitize
|
|
* @param string $text Input text
|
|
* @param array $options
|
|
* @param Status $expect
|
|
*/
|
|
public function testSanitize( $text, $options, $expect ) {
|
|
$this->assertEquals( $expect, $this->newContent( $text )->sanitize( $options ) );
|
|
}
|
|
|
|
public static function provideSanitize() {
|
|
$status1 = Status::newGood( '.mw-parser-output .foo{}' );
|
|
$status1->warning( 'templatestyles-error-bad-value-for-property', 1, 15, 'color' );
|
|
|
|
return [
|
|
'flip' => [
|
|
'.foo { margin-left: 10px; /*@noflip*/ padding-left: 1em; }',
|
|
[ 'flip' => true ],
|
|
Status::newGood( '.mw-parser-output .foo{margin-right:10px;padding-left:1em}' )
|
|
],
|
|
'no minify' => [
|
|
'.foo { margin-left: 10px }',
|
|
[ 'minify' => false ],
|
|
Status::newGood( '.mw-parser-output .foo { margin-left: 10px ; }' )
|
|
],
|
|
'With warnings' => [
|
|
'.foo { color: bogus; }',
|
|
[],
|
|
$status1
|
|
],
|
|
'With warnings, fatal and no value' => [
|
|
'.foo { bogus: bogus; }',
|
|
[ 'severity' => 'fatal', 'novalue' => true ],
|
|
Status::newFatal( 'templatestyles-error-unrecognized-property', 1, 8 ),
|
|
],
|
|
'With overridden class prefix' => [
|
|
'.foo { margin-left: 10px }',
|
|
[ 'class' => 'foo bar', 'minify' => false ],
|
|
Status::newGood( '.foo\ bar .foo { margin-left: 10px ; }' )
|
|
],
|
|
'With boolean false as a class prefix' => [
|
|
'.foo { margin-left: 10px }',
|
|
[ 'class' => false, 'minify' => false ],
|
|
Status::newGood( '.mw-parser-output .foo { margin-left: 10px ; }' )
|
|
],
|
|
'Escaping U+007F' => [
|
|
".foo\\\x7f { content: '\x7f'; }",
|
|
[],
|
|
Status::newGood(
|
|
'.mw-parser-output .foo\\7f {content:"\\7f "}'
|
|
)
|
|
],
|
|
'@font-face prefixing' => [
|
|
'@font-face { font-family: nope; }',
|
|
[ 'severity' => 'fatal', 'novalue' => true ],
|
|
Status::newFatal( 'templatestyles-error-bad-value-for-property', 1, 27, 'font-family' ),
|
|
],
|
|
];
|
|
}
|
|
|
|
public function testSizeLimit() {
|
|
$this->setMwGlobals( [
|
|
'wgTemplateStylesMaxStylesheetSize' => 10,
|
|
] );
|
|
|
|
$this->assertEquals(
|
|
Status::newGood( '.mw-parser-output .foobar{}' ),
|
|
$this->newContent( '.foobar {}' )->sanitize()
|
|
);
|
|
$this->assertEquals(
|
|
Status::newFatal( wfMessage( 'templatestyles-size-exceeded', 10, Message::sizeParam( 10 ) ) ),
|
|
$this->newContent( '.foobar2 {}' )->sanitize()
|
|
);
|
|
|
|
$this->setMwGlobals( [
|
|
'wgTemplateStylesMaxStylesheetSize' => null,
|
|
] );
|
|
$long = str_repeat( 'X', 102400 );
|
|
$this->assertEquals(
|
|
Status::newGood( ".mw-parser-output .{$long}{}" ),
|
|
$this->newContent( ".{$long} {}" )->sanitize()
|
|
);
|
|
}
|
|
|
|
public function testPrepareSave() {
|
|
$this->assertEquals(
|
|
$this->newContent( '.foo { bogus: bogus; }' )->prepareSave(
|
|
WikiPage::factory( Title::newFromText( 'Template:Test/styles.css' ) ),
|
|
0,
|
|
123,
|
|
new User
|
|
),
|
|
Status::newFatal( 'templatestyles-error-unrecognized-property', 1, 8 )
|
|
);
|
|
}
|
|
|
|
public static function dataGetParserOutput() {
|
|
return [
|
|
[
|
|
'Template:Test/styles.css',
|
|
'sanitized-css',
|
|
".hello { content: 'world'; color: bogus; }\n\n<ok>\n",
|
|
// @codingStandardsIgnoreStart Generic.Files.LineLength
|
|
"<pre class=\"mw-code mw-css\" dir=\"ltr\">\n.hello { content: 'world'; color: bogus; }\n\n<ok>\n\n</pre>",
|
|
// @codingStandardsIgnoreEnd
|
|
[
|
|
'Warnings' => [
|
|
'Unexpected end of stylesheet in rule at line 4 character 1.',
|
|
'Invalid or unsupported value for property <code>color</code> at line 1 character 35.',
|
|
]
|
|
]
|
|
],
|
|
[
|
|
'Template:Test/styles.css',
|
|
'sanitized-css',
|
|
"/* hello [[world]] */\n",
|
|
"<pre class=\"mw-code mw-css\" dir=\"ltr\">\n/* hello [[world]] */\n\n</pre>",
|
|
[
|
|
'Links' => [
|
|
[ 'World' => 0 ]
|
|
]
|
|
]
|
|
],
|
|
];
|
|
}
|
|
|
|
public static function dataPreSaveTransform() {
|
|
return [
|
|
[
|
|
'hello this is ~~~',
|
|
'hello this is ~~~',
|
|
],
|
|
[
|
|
'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
|
|
'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
|
|
],
|
|
[
|
|
" Foo \n ",
|
|
" Foo",
|
|
],
|
|
];
|
|
}
|
|
|
|
public static function dataPreloadTransform() {
|
|
return [
|
|
[
|
|
'hello this is ~~~',
|
|
'hello this is ~~~',
|
|
],
|
|
[
|
|
'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
|
|
'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
|
|
],
|
|
];
|
|
}
|
|
|
|
public function testGetModel() {
|
|
$content = $this->newContent( 'hello world.' );
|
|
|
|
$this->assertEquals( 'sanitized-css', $content->getModel() );
|
|
}
|
|
|
|
public function testGetContentHandler() {
|
|
$content = $this->newContent( 'hello world.' );
|
|
|
|
$this->assertEquals( 'sanitized-css', $content->getContentHandler()->getModelID() );
|
|
}
|
|
|
|
/**
|
|
* Redirects aren't supported
|
|
*/
|
|
public static function provideUpdateRedirect() {
|
|
// @codingStandardsIgnoreStart Generic.Files.LineLength
|
|
return [
|
|
[
|
|
'#REDIRECT [[Someplace]]',
|
|
'#REDIRECT [[Someplace]]',
|
|
],
|
|
|
|
// The style supported by CssContent
|
|
[
|
|
'/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);',
|
|
'/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);',
|
|
],
|
|
];
|
|
// @codingStandardsIgnoreEnd
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideGetRedirectTarget
|
|
*/
|
|
public function testGetRedirectTarget( $title, $text ) {
|
|
$this->setMwGlobals( [
|
|
'wgServer' => '//example.org',
|
|
'wgScriptPath' => '/w',
|
|
'wgScript' => '/w/index.php',
|
|
] );
|
|
$content = $this->newContent( $text );
|
|
$target = $content->getRedirectTarget();
|
|
$this->assertEquals( $title, $target ? $target->getPrefixedText() : null );
|
|
}
|
|
|
|
public static function provideGetRedirectTarget() {
|
|
// @codingStandardsIgnoreStart Generic.Files.LineLength
|
|
return [
|
|
[ null, "/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);" ],
|
|
[ null, "/* #REDIRECT */@import url(//example.org/w/index.php?title=User:FooBar/common.css&action=raw&ctype=text/css);" ],
|
|
[ null, "/* #REDIRECT */@import url(//example.org/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ],
|
|
[ null, "@import url(//example.org/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ],
|
|
[ null, "/* #REDIRECT */@import url(//example.com/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ],
|
|
];
|
|
// @codingStandardsIgnoreEnd
|
|
}
|
|
|
|
public static function dataEquals() {
|
|
return [
|
|
[ new TemplateStylesContent( 'hallo' ), null, false ],
|
|
[ new TemplateStylesContent( 'hallo' ), new TemplateStylesContent( 'hallo' ), true ],
|
|
[ new TemplateStylesContent( 'hallo' ), new CssContent( 'hallo' ), false ],
|
|
[ new TemplateStylesContent( 'hallo' ), new WikitextContent( 'hallo' ), false ],
|
|
[ new TemplateStylesContent( 'hallo' ), new TemplateStylesContent( 'HALLO' ), false ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider dataEquals
|
|
*/
|
|
public function testEquals( Content $a, Content $b = null, $equal = false ) {
|
|
$this->assertEquals( $equal, $a->equals( $b ) );
|
|
}
|
|
}
|