Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
83.33% covered (warning)
83.33%
5 / 6
CRAP
64.29% covered (warning)
64.29%
36 / 56
TemplateStylesContentHandler
0.00% covered (danger)
0.00%
0 / 1
83.33% covered (warning)
83.33%
5 / 6
32.76
64.29% covered (warning)
64.29%
36 / 56
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 validateSave
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getContentClass
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 fillParserOutput
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 20
 processErrors
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
5 / 5
 sanitize
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
27 / 27
<?php
namespace MediaWiki\Extension\TemplateStyles;
/**
 * @file
 * @license GPL-2.0-or-later
 */
use CodeContentHandler;
use Content;
use CSSJanus;
use MediaWiki\Content\Renderer\ContentParseParams;
use MediaWiki\Content\ValidationParams;
use MediaWiki\MediaWikiServices;
use Message;
use ParserOutput;
use Status;
use StatusValue;
use Wikimedia\CSS\Parser\Parser as CSSParser;
use Wikimedia\CSS\Util as CSSUtil;
/**
 * Content handler for sanitized CSS
 */
class TemplateStylesContentHandler extends CodeContentHandler {
    /**
     * @param string $modelId
     */
    public function __construct( $modelId = 'sanitized-css' ) {
        parent::__construct( $modelId, [ CONTENT_FORMAT_CSS ] );
    }
    /**
     * @inheritDoc
     */
    public function validateSave(
        Content $content,
        ValidationParams $validationParams
    ) {
        '@phan-var TemplateStylesContent $content';
        return $this->sanitize( $content, [ 'novalue' => true, 'severity' => 'fatal' ] );
    }
    /**
     * @return string
     */
    protected function getContentClass() {
        return TemplateStylesContent::class;
    }
    /**
     * @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();
        $output->setText( $html );
        $status = $this->sanitize( $content, [ 'novalue' => true, 'class' => $parserOptions->getWrapOutputClass() ] );
        if ( $status->getErrors() ) {
            foreach ( $status->getErrors() as $error ) {
                $output->addWarningMsg( $error['message'], $error['params'] );
            }
            $services->getTrackingCategories()->addTrackingCategory(
                $output,
                'templatestyles-stylesheet-error-category',
                $page
            );
        }
    }
    /**
     * 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();
        $maxSize = Hooks::getConfig()->get( 'TemplateStylesMaxStylesheetSize' );
        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
        $sanitizer = Hooks::getSanitizer(
            $options['class'] ?: 'mw-parser-output', $options['extraWrapper']
        );
        $sanitizer->clearSanitizationErrors(); // Just in case
        $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" => '�' ] );
        }
        return $status;
    }
}