Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Hooks
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
4 / 4
19
100.00% covered (success)
100.00%
1 / 1
 showError
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 onOutputPageParserOutput
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 onParserFirstCallInit
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 createTagHandler
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
13
1<?php
2/**
3 * Copyright (C) 2021 Brandon Fowler
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20namespace MediaWiki\Extension\UseResource;
21
22use ContentHandler;
23use CssContent;
24use Html;
25use JavaScriptContent;
26use MediaWiki\Hook\OutputPageParserOutputHook;
27use MediaWiki\Hook\ParserFirstCallInitHook;
28use MediaWiki\ResourceLoader\ResourceLoader;
29use MediaWiki\Revision\SlotRecord;
30use OutputPage;
31use Parser;
32use ParserOutput;
33use TextContent;
34use Title;
35
36/**
37 * Hook handlers for the UseResource extension
38 */
39class Hooks implements OutputPageParserOutputHook, ParserFirstCallInitHook {
40    /**
41     * Show an error message
42     * @param Parser $parser Current Parser object
43     * @param string $key The key of the message
44     * @param mixed ...$args Message parameters
45     * @return string HTML
46     */
47    private static function showError( $parser, $key, ...$args ) {
48        $parser->addTrackingCategory( 'useresource-error-category' );
49        return Html::rawElement(
50            'strong',
51            [ 'class' => 'error' ],
52            wfMessage( $key, ...$args )->inContentLanguage()->parse()
53        );
54    }
55
56    /**
57     * Handler for the OutputPageParserOutput hook
58     * @param OutputPage $out
59     * @param ParserOutput $parserOutput
60     */
61    public function onOutputPageParserOutput( $out, $parserOutput ): void {
62        $data = $parserOutput->getExtensionData( 'useresource' );
63
64        if ( !$data ) {
65            return;
66        }
67
68        if ( !empty( $data['js'] ) ) {
69            $out->getResourceLoader()->register( 'ext.useresource', [
70                'class' => Module::class,
71                'code' => $data['js']
72            ] );
73            $out->addModules( 'ext.useresource' );
74        }
75
76        if ( !empty( $data['css'] ) ) {
77            $out->addHeadItem( 'useresource', '<style>' . $data['css'] . '</style>' );
78        }
79    }
80
81    /**
82     * Handler for the ParserFirstCallInit hook
83     * @param Parser $parser
84     */
85    public function onParserFirstCallInit( $parser ) {
86        $parser->setHook( 'usescript', self::createTagHandler(
87            'js', JavaScriptContent::class, CONTENT_MODEL_JAVASCRIPT, [ '!function(){', '}();' ]
88        ) );
89
90        $parser->setHook( 'usestyle', self::createTagHandler(
91            'css', CssContent::class, CONTENT_MODEL_CSS, []
92        ) );
93    }
94
95    /**
96     * Create a tag handler function
97     * @param string $type The language of the content
98     * @param string $class The required content class, must be a child of TextContent
99     * @param string $model The model id
100     * @param string[] $wrap Two element array with text to wrap around the code
101     * @return callable A callback compatible with Parser::setHook
102     */
103    public static function createTagHandler( $type, $class, $model, $wrap ) {
104        return function ( $input, array $args, Parser $parser, $frame ) use ( $type, $class, $model, $wrap ) {
105            if ( !isset( $args['src'] ) || trim( $args['src'] ) === '' ) {
106                return self::showError( $parser, 'useresource-empty-src' );
107            }
108
109            $title = Title::newFromText( $args['src'], NS_MEDIAWIKI );
110
111            if ( !$title ) {
112                return self::showError( $parser, 'useresource-invalid-title' );
113            }
114
115            if ( !$title->inNamespace( NS_MEDIAWIKI ) ) {
116                return self::showError(
117                    $parser,
118                    'useresource-invalid-namespace',
119                    $title->getFullText(),
120                    $parser->getContentLanguage()->getFormattedNsText( NS_MEDIAWIKI )
121                );
122            }
123
124            $revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
125            $articleID = $title->getArticleID();
126            $content = $revRecord ? $revRecord->getContent( SlotRecord::MAIN, $revRecord::RAW ) : null;
127
128            // Register as a template so the page is re-parsed when the script is edited
129            $parser->getOutput()->addTemplate( $title, $articleID, $revRecord ? $revRecord->getId() : null );
130
131            if ( !$content ) {
132                return self::showError( $parser, 'useresource-no-content', $title->getFullText() );
133            }
134
135            if ( !is_a( $content, $class ) || !$content instanceof TextContent ) {
136                return self::showError(
137                    $parser,
138                    'useresource-invalid-content',
139                    $title->getFullText(),
140                    ContentHandler::getLocalizedName( $model ),
141                    ContentHandler::getLocalizedName( $content->getModel() )
142                );
143            }
144
145            $data = $parser->getOutput()->getExtensionData( 'useresource' ) ?? [ 'pages' => [] ];
146
147            if ( in_array( $articleID, $data['pages'] ) ) {
148                return '';
149            }
150
151            $data['pages'][] = $articleID;
152            $text = $content->getText();
153
154            if ( $text ) {
155                $text = ResourceLoader::filter( 'minify-' . $type, $text );
156                $data[$type] = ( $data[$type] ?? '' ) . ( $wrap ? $wrap[0] . $text . $wrap[1] : $text );
157            }
158
159            $parser->getOutput()->setExtensionData( 'useresource', $data );
160
161            return '';
162        };
163    }
164}