getServiceContainer()->getWikiPageFactory()->newFromTitle( $title ); $user = static::getTestSysop()->getUser(); $status = $page->doUserEditContent( $content, $user, 'Test for TemplateStyles' ); if ( !$status->isOk() ) { $this->fail( "Failed to create $title: " . $status->getWikiText( false, false, 'en' ) ); } } 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' ); $this->addPage( 'styles3.css', 'html.no-js body.skin-minerva .bar { color: yellow; }', 'sanitized-css' ); } public function testGetSanitizerInvalidWrapper() { $this->expectException( InvalidArgumentException::class ); $this->expectExceptionMessage( 'Invalid value for $extraWrapper: .foo>.bar' ); TemplateStylesHooks::getSanitizer( 'foo', '.foo>.bar' ); } public function testGetSanitizerNonLinearWrapper() { $sanitizer = TemplateStylesHooks::getSanitizer( 'foo', 'div[data]' ); $sanitizer->sanitize( CSSParser::newFromString( '.not-empty { }' )->parseStylesheet() ); $this->assertSame( [], $sanitizer->getSanitizationErrors() ); } /** * @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( [ 'wgTemplateStylesNamespaces' => [ 10 => true, 2 => false, 3000 => true, 3002 => true, 3006 => false, ], 'wgNamespacesWithSubpages' => [ 10 => true, 2 => true, 3000 => true, 3002 => false, 3004 => true, 3006 => true ], ] ); $reset = ExtensionRegistry::getInstance()->setAttributeForTest( 'TemplateStylesNamespaces', [ 3004, 3006 ] ); $model = 'unchanged'; $ret = ( new 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 ], [ 3004, 'Test/test.css', true ], [ 3006, 'Test/test.css', false ], ]; } /** * 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 */ public function testTag( ParserOptions $popt, $getTextOptions, $wikitext, $expect, $globals = [] ) { $this->setMwGlobals( $globals + [ 'wgScriptPath' => '', 'wgScript' => '/index.php', 'wgArticlePath' => '/wiki/$1', ] ); $oldCurrentRevisionRecordCallback = $popt->setCurrentRevisionRecordCallback( static function ( Title $title, $parser = null ) use ( &$oldCurrentRevisionRecordCallback ) { if ( $title->getPrefixedText() === 'Template:Test replacement' ) { $user = RequestContext::getMain()->getUser(); $revRecord = new MutableRevisionRecord( $title ); $revRecord->setUser( $user ); $revRecord->setContent( SlotRecord::MAIN, new TemplateStylesContent( '.baz { color:orange; bogus:bogus; }' ) ); $revRecord->setParentId( $title->getLatestRevID() ); return $revRecord; } return call_user_func( $oldCurrentRevisionRecordCallback, $title, $parser ); } ); $services = MediaWikiServices::getInstance(); $parser = $services->getParserFactory()->create(); $parser->firstCallInit(); if ( !in_array( 'templatestyles', $parser->getTags(), true ) ) { throw new Exception( 'templatestyles tag hook is not in the parser' ); } $out = $parser->parse( $wikitext, Title::newFromText( 'Test' ), $popt ); $expect = preg_replace_callback( '/\{\{REV:(.*?)\}\}/', static function ( $m ) { return Title::newFromText( 'Template:TemplateStyles test/' . $m[1] )->getLatestRevID(); }, $expect ); $this->assertEquals( $expect, $out->getText( $getTextOptions ) ); } public static function provideTag() { $popt = ParserOptions::newFromContext( RequestContext::getMain() ); $popt->setWrapOutputClass( 'templatestyles-test' ); $popt2 = ParserOptions::newFromContext( RequestContext::getMain() ); $popt3 = ParserOptions::newFromContext( RequestContext::getMain() ); AtEase::quietCall( [ $popt3, 'setWrapOutputClass' ], false ); return [ 'Tag without src' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "

TemplateStyles' src attribute must not be empty.\n

", ], 'Tag with invalid src' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "

Invalid title for TemplateStyles' src attribute.\n

", ], 'Tag with valid but nonexistent title' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "

Page Template:ThisDoes'''Not'''Exist has no content.\n

", ], 'Tag with valid but nonexistent title, main namespace' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "

Page ThisDoes'''Not'''Exist has no content.\n

", ], 'Tag with wikitext page' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "

Page Template:TemplateStyles test/wikitext must have content model \"Sanitized CSS\" for TemplateStyles (current model is \"wikitext\").\n

", ], 'Tag with CSS (not sanitized-css) page' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "

Page Template:TemplateStyles test/nonsanitized.css must have content model \"Sanitized CSS\" for TemplateStyles (current model is \"CSS\").\n

", ], 'Working tag' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "
", ], 'Disabled' => [ $popt, [], '', "
", [ 'wgTemplateStylesDisable' => true ], ], 'Replaced content (which includes sanitization errors)' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "
", ], 'Hoistable selectors are hoisted' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "
", ], 'Still prefixed despite no wrapping class' => [ $popt2, [ 'unwrap' => true ], '', // phpcs:ignore Generic.Files.LineLength "", ], 'Still prefixed despite deprecated no wrapping class' => [ $popt3, [], '', // phpcs:ignore Generic.Files.LineLength "", ], 'Deduplicated tags' => [ $popt, [], trim( ' ' ), // phpcs:disable Generic.Files.LineLength trim( '
' ), // phpcs:enable ], 'Wrapper parameter' => [ $popt2, [], '', // phpcs:ignore Generic.Files.LineLength "
", ], 'Invalid wrapper parameter' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "

Invalid value for TemplateStyles' wrapper attribute.\n

", ], 'Invalid wrapper parameter (2)' => [ $popt, [], '', // phpcs:ignore Generic.Files.LineLength "

Invalid value for TemplateStyles' wrapper attribute.\n

", ], 'Wrapper parameter and proper deduplication' => [ $popt2, [], trim( ' ' ), // phpcs:disable Generic.Files.LineLength trim( '
' ), // phpcs:enable ], ]; } }