2017-02-20 04:33:24 +00:00
|
|
|
|
<?php
|
|
|
|
|
|
2022-02-06 14:39:49 +00:00
|
|
|
|
namespace MediaWiki\Extension\TemplateStyles;
|
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
/**
|
|
|
|
|
* @file
|
2018-03-02 23:43:40 +00:00
|
|
|
|
* @license GPL-2.0-or-later
|
2017-02-20 04:33:24 +00:00
|
|
|
|
*/
|
|
|
|
|
|
2022-02-06 14:39:49 +00:00
|
|
|
|
use CodeContentHandler;
|
|
|
|
|
use Content;
|
|
|
|
|
use CSSJanus;
|
2021-10-03 21:08:36 +00:00
|
|
|
|
use MediaWiki\Content\Renderer\ContentParseParams;
|
2021-10-27 12:44:52 +00:00
|
|
|
|
use MediaWiki\Content\ValidationParams;
|
2021-10-03 21:08:36 +00:00
|
|
|
|
use MediaWiki\MediaWikiServices;
|
2024-06-10 19:59:03 +00:00
|
|
|
|
use MediaWiki\Message\Message;
|
2024-01-04 20:53:17 +00:00
|
|
|
|
use MediaWiki\Parser\ParserOutput;
|
|
|
|
|
use MediaWiki\Status\Status;
|
2022-02-06 14:39:49 +00:00
|
|
|
|
use StatusValue;
|
2021-10-27 12:44:52 +00:00
|
|
|
|
use Wikimedia\CSS\Parser\Parser as CSSParser;
|
|
|
|
|
use Wikimedia\CSS\Util as CSSUtil;
|
2021-10-03 21:08:36 +00:00
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
/**
|
|
|
|
|
* Content handler for sanitized CSS
|
|
|
|
|
*/
|
|
|
|
|
class TemplateStylesContentHandler extends CodeContentHandler {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $modelId
|
|
|
|
|
*/
|
|
|
|
|
public function __construct( $modelId = 'sanitized-css' ) {
|
|
|
|
|
parent::__construct( $modelId, [ CONTENT_FORMAT_CSS ] );
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-27 12:44:52 +00:00
|
|
|
|
/**
|
|
|
|
|
* @inheritDoc
|
|
|
|
|
*/
|
|
|
|
|
public function validateSave(
|
|
|
|
|
Content $content,
|
|
|
|
|
ValidationParams $validationParams
|
|
|
|
|
) {
|
|
|
|
|
'@phan-var TemplateStylesContent $content';
|
|
|
|
|
return $this->sanitize( $content, [ 'novalue' => true, 'severity' => 'fatal' ] );
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-15 06:53:57 +00:00
|
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
2017-02-20 04:33:24 +00:00
|
|
|
|
protected function getContentClass() {
|
|
|
|
|
return TemplateStylesContent::class;
|
|
|
|
|
}
|
2021-10-03 21:08:36 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @inheritDoc
|
|
|
|
|
*/
|
|
|
|
|
protected function fillParserOutput(
|
|
|
|
|
Content $content,
|
|
|
|
|
ContentParseParams $cpoParams,
|
|
|
|
|
ParserOutput &$output
|
|
|
|
|
) {
|
|
|
|
|
'@phan-var TemplateStylesContent $content';
|
|
|
|
|
$services = MediaWikiServices::getInstance();
|
|
|
|
|
$page = $cpoParams->getPage();
|
|
|
|
|
$parserOptions = $cpoParams->getParserOptions();
|
|
|
|
|
|
|
|
|
|
// Inject our warnings into the resulting ParserOutput
|
|
|
|
|
parent::fillParserOutput( $content, $cpoParams, $output );
|
|
|
|
|
|
|
|
|
|
if ( $cpoParams->getGenerateHtml() ) {
|
|
|
|
|
$html = "";
|
|
|
|
|
$html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
|
|
|
|
|
$html .= htmlspecialchars( $content->getNativeData(), ENT_NOQUOTES );
|
|
|
|
|
$html .= "\n</pre>\n";
|
|
|
|
|
} else {
|
|
|
|
|
$html = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$output->clearWrapperDivClass();
|
2024-04-04 14:56:59 +00:00
|
|
|
|
$output->setRawText( $html );
|
2021-10-03 21:08:36 +00:00
|
|
|
|
|
2021-10-27 12:44:52 +00:00
|
|
|
|
$status = $this->sanitize( $content, [ 'novalue' => true, 'class' => $parserOptions->getWrapOutputClass() ] );
|
2021-10-03 21:08:36 +00:00
|
|
|
|
if ( $status->getErrors() ) {
|
|
|
|
|
foreach ( $status->getErrors() as $error ) {
|
|
|
|
|
$output->addWarningMsg( $error['message'], $error['params'] );
|
|
|
|
|
}
|
|
|
|
|
$services->getTrackingCategories()->addTrackingCategory(
|
|
|
|
|
$output,
|
|
|
|
|
'templatestyles-stylesheet-error-category',
|
|
|
|
|
$page
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-10-27 12:44:52 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle errors from the CSS parser and/or sanitizer
|
|
|
|
|
* @param StatusValue $status Object to add errors to
|
|
|
|
|
* @param array[] $errors Error array
|
|
|
|
|
* @param string $severity Whether to consider errors as 'warning' or 'fatal'
|
|
|
|
|
*/
|
|
|
|
|
protected static function processErrors( StatusValue $status, array $errors, $severity ) {
|
|
|
|
|
if ( $severity !== 'warning' && $severity !== 'fatal' ) {
|
|
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
|
throw new \InvalidArgumentException( 'Invalid $severity' );
|
|
|
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
|
}
|
|
|
|
|
foreach ( $errors as $error ) {
|
|
|
|
|
$error[0] = 'templatestyles-error-' . $error[0];
|
|
|
|
|
call_user_func_array( [ $status, $severity ], $error );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sanitize the content
|
|
|
|
|
* @param TemplateStylesContent $content
|
|
|
|
|
* @param array $options Options are:
|
|
|
|
|
* - class: (string) Class to prefix selectors with
|
|
|
|
|
* - extraWrapper: (string) Extra simple selector to prefix selectors with
|
|
|
|
|
* - flip: (bool) Have CSSJanus flip the stylesheet.
|
|
|
|
|
* - minify: (bool) Whether to minify. Default true.
|
|
|
|
|
* - novalue: (bool) Don't bother returning the actual stylesheet, just
|
|
|
|
|
* fill the Status with warnings.
|
|
|
|
|
* - severity: (string) Whether to consider errors as 'warning' or 'fatal'
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
public function sanitize( TemplateStylesContent $content, array $options = [] ) {
|
|
|
|
|
$options += [
|
|
|
|
|
'class' => false,
|
|
|
|
|
'extraWrapper' => null,
|
|
|
|
|
'flip' => false,
|
|
|
|
|
'minify' => true,
|
|
|
|
|
'novalue' => false,
|
|
|
|
|
'severity' => 'warning',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
|
|
|
|
|
$style = $content->getText();
|
2022-02-06 14:39:49 +00:00
|
|
|
|
$maxSize = Hooks::getConfig()->get( 'TemplateStylesMaxStylesheetSize' );
|
2021-10-27 12:44:52 +00:00
|
|
|
|
if ( $maxSize !== null && strlen( $style ) > $maxSize ) {
|
|
|
|
|
$status->fatal(
|
|
|
|
|
// Status::getWikiText() chokes on the Message::sizeParam if we
|
|
|
|
|
// don't wrap it in a Message ourself.
|
|
|
|
|
wfMessage( 'templatestyles-size-exceeded', $maxSize, Message::sizeParam( $maxSize ) )
|
|
|
|
|
);
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $options['flip'] ) {
|
|
|
|
|
$style = CSSJanus::transform( $style, true, false );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse it, and collect any errors
|
|
|
|
|
$cssParser = CSSParser::newFromString( $style );
|
|
|
|
|
$stylesheet = $cssParser->parseStylesheet();
|
|
|
|
|
self::processErrors( $status, $cssParser->getParseErrors(), $options['severity'] );
|
|
|
|
|
|
|
|
|
|
// Sanitize it, and collect any errors
|
2022-02-06 14:39:49 +00:00
|
|
|
|
$sanitizer = Hooks::getSanitizer(
|
2021-10-27 12:44:52 +00:00
|
|
|
|
$options['class'] ?: 'mw-parser-output', $options['extraWrapper']
|
|
|
|
|
);
|
2022-11-10 23:42:01 +00:00
|
|
|
|
// Just in case
|
|
|
|
|
$sanitizer->clearSanitizationErrors();
|
2021-10-27 12:44:52 +00:00
|
|
|
|
$stylesheet = $sanitizer->sanitize( $stylesheet );
|
|
|
|
|
self::processErrors( $status, $sanitizer->getSanitizationErrors(), $options['severity'] );
|
|
|
|
|
$sanitizer->clearSanitizationErrors();
|
|
|
|
|
|
|
|
|
|
// Stringify it while minifying
|
|
|
|
|
$value = CSSUtil::stringify( $stylesheet, [ 'minify' => $options['minify'] ] );
|
|
|
|
|
|
|
|
|
|
// Sanity check, don't allow "</style" if one somehow sneaks through the sanitizer
|
|
|
|
|
if ( preg_match( '!</style!i', $value ) ) {
|
|
|
|
|
$value = '';
|
|
|
|
|
$status->fatal( 'templatestyles-end-tag-injection' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !$options['novalue'] ) {
|
|
|
|
|
$status->value = $value;
|
|
|
|
|
|
|
|
|
|
// Sanity check, don't allow raw U+007F if one somehow sneaks through the sanitizer
|
|
|
|
|
$status->value = strtr( $status->value, [ "\x7f" => '<27>' ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|