Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ScribuntoContentHandler
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 11
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getContentClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isSupportedFormat
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 canBeUsedOn
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 supportsPreloadContent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateSave
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 validate
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 fillParserOutput
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
90
 highlight
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 makeRedirectContent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 supportsRedirects
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Scribunto;
4
5use CodeContentHandler;
6use Content;
7use ExtensionRegistry;
8use MediaWiki\Content\Renderer\ContentParseParams;
9use MediaWiki\Content\ValidationParams;
10use MediaWiki\Html\Html;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Page\PageIdentity;
13use MediaWiki\Parser\ParserOutput;
14use MediaWiki\Status\Status;
15use MediaWiki\SyntaxHighlight\SyntaxHighlight;
16use MediaWiki\Title\Title;
17use TextContent;
18
19/**
20 * Scribunto Content Handler
21 *
22 * @file
23 * @ingroup Extensions
24 * @ingroup Scribunto
25 *
26 * @author Brad Jorsch <bjorsch@wikimedia.org>
27 */
28
29class ScribuntoContentHandler extends CodeContentHandler {
30
31    /**
32     * @param string $modelId
33     * @param string[] $formats
34     */
35    public function __construct(
36        $modelId = CONTENT_MODEL_SCRIBUNTO, $formats = [ CONTENT_FORMAT_TEXT ]
37    ) {
38        parent::__construct( $modelId, $formats );
39    }
40
41    /**
42     * @return string Class name
43     */
44    protected function getContentClass() {
45        return ScribuntoContent::class;
46    }
47
48    /**
49     * @param string $format
50     * @return bool
51     */
52    public function isSupportedFormat( $format ) {
53        // An error in an earlier version of Scribunto means we might see this.
54        if ( $format === 'CONTENT_FORMAT_TEXT' ) {
55            $format = CONTENT_FORMAT_TEXT;
56        }
57        return parent::isSupportedFormat( $format );
58    }
59
60    /**
61     * Only allow this content handler to be used in the Module namespace
62     * @param Title $title
63     * @return bool
64     */
65    public function canBeUsedOn( Title $title ) {
66        if ( $title->getNamespace() !== NS_MODULE ) {
67            return false;
68        }
69
70        return parent::canBeUsedOn( $title );
71    }
72
73    /** @inheritDoc */
74    public function supportsPreloadContent(): bool {
75        return true;
76    }
77
78    /**
79     * @inheritDoc
80     */
81    public function validateSave(
82        Content $content,
83        ValidationParams $validationParams
84    ) {
85        '@phan-var ScribuntoContent $content';
86        return $this->validate( $content, $validationParams->getPageIdentity() );
87    }
88
89    /**
90     * Checks whether the script is valid
91     *
92     * @param TextContent $content
93     * @param PageIdentity $page
94     * @return Status
95     */
96    public function validate( TextContent $content, PageIdentity $page ) {
97        if ( !( $page instanceof Title ) ) {
98            $titleFactory = MediaWikiServices::getInstance()->getTitleFactory();
99            $page = $titleFactory->newFromPageIdentity( $page );
100        }
101
102        $engine = Scribunto::newDefaultEngine();
103        $engine->setTitle( $page );
104        return $engine->validate( $content->getText(), $page->getPrefixedDBkey() );
105    }
106
107    /**
108     * @inheritDoc
109     */
110    protected function fillParserOutput(
111        Content $content,
112        ContentParseParams $cpoParams,
113        ParserOutput &$parserOutput
114    ) {
115        '@phan-var ScribuntoContent $content';
116        $page = $cpoParams->getPage();
117        $title = Title::newFromPageReference( $page );
118        $parserOptions = $cpoParams->getParserOptions();
119        $revId = $cpoParams->getRevId();
120        $generateHtml = $cpoParams->getGenerateHtml();
121        $parser = MediaWikiServices::getInstance()->getParserFactory()->getInstance();
122        $sourceCode = $content->getText();
123        $docTitle = Scribunto::getDocPage( $title );
124        $docMsg = $docTitle ? wfMessage(
125            $docTitle->exists() ? 'scribunto-doc-page-show' : 'scribunto-doc-page-does-not-exist',
126            $docTitle->getPrefixedText()
127        )->inContentLanguage() : null;
128
129        // Accumulate the following output:
130        // - docs (if any)
131        // - validation error (if any)
132        // - highlighted source code
133        $html = '';
134
135        if ( $docMsg && !$docMsg->isDisabled() ) {
136            // In order to allow the doc page to categorize the Module page,
137            // we need access to the ParserOutput of the doc page.
138            // This is why we can't simply use $docMsg->parse().
139            //
140            // We also can't use use ParserOutput::getText and ParserOutput::collectMetadata
141            // to merge the result into $parserOutput, because doing so would remove the
142            // ability for Skin/OutputPage to (post-cache) decide on the ParserOutput::getText
143            // parameters edit section links, TOC, and user language etc.
144            //
145            // So instead, this uses the doc page's ParserOutput as the actual ParserOutput
146            // we return, and add the other stuff to it. This is the only way to leave
147            // skin-decisions undecided and in-tact.
148            if ( $parserOptions->getTargetLanguage() === null ) {
149                $parserOptions->setTargetLanguage( $docTitle->getPageLanguage() );
150            }
151            $parserOutput = $parser->parse( $docMsg->plain(), $page, $parserOptions, true, true, $revId );
152
153            // Code is displayed and syntax highlighted as LTR, but the
154            // documentation can be RTL on RTL-language wikis.
155            //
156            // As long as we leave the $parserOutput in-tact, it will preserve the appropiate
157            // lang, dir, and class attributes (mw-content-ltr or mw-content-rtl) as needed
158            // for correct styling and accessiblity of the documentation page content.
159            // These will be applied when OutputPage eventually calls ParserOutput::getText()
160            $html .= $parserOutput->getRawText();
161        } else {
162            $parserOutput = new ParserOutput();
163        }
164
165        if ( $docTitle ) {
166            // Mark the doc page as transcluded, so that edits to the doc page will
167            // purge this Module page.
168            $parserOutput->addTemplate( $docTitle, $docTitle->getArticleID(), $docTitle->getLatestRevID() );
169        }
170
171        // Validate the script, and include an error message and tracking
172        // category if it's invalid
173        $status = $this->validate( $content, $title );
174        if ( !$status->isOK() ) {
175            // FIXME: This uses a Status object, which in turn uses global RequestContext
176            // to localize the message. This would poison the ParserCache.
177            //
178            // But, this code is almost unreachable in practice because there has
179            // been no way to create a Module page with invalid content since 2014
180            // (we validate and abort on edit, undelete, content-model change etc.).
181            // See also T304381.
182            $html .= Html::rawElement( 'div', [ 'class' => 'errorbox' ],
183                $status->getHTML( 'scribunto-error-short', 'scribunto-error-long' )
184            );
185            $trackingCategories = MediaWikiServices::getInstance()->getTrackingCategories();
186            $trackingCategories->addTrackingCategory( $parserOutput, 'scribunto-module-with-errors-category', $page );
187        }
188
189        if ( !$generateHtml ) {
190            // The doc page and validation error produce metadata and must happen
191            // unconditionally. The next step (syntax highlight) can be skipped if
192            // we don't actually need the HTML.
193            $parserOutput->setRawText( '' );
194            return;
195        }
196
197        $engine = Scribunto::newDefaultEngine();
198        $engine->setTitle( $title );
199        $codeLang = $engine->getGeSHiLanguage();
200        $html .= $this->highlight( $sourceCode, $parserOutput, $codeLang );
201
202        $parserOutput->setRawText( $html );
203    }
204
205    /**
206     * Get syntax highlighted code and add metadata to output.
207     *
208     * If SyntaxHighlight is not possible, falls back to a `<pre>` element.
209     *
210     * @param string $source Source code
211     * @param ParserOutput $parserOutput
212     * @param string|false $codeLang
213     * @return string HTML
214     */
215    private function highlight( $source, ParserOutput $parserOutput, $codeLang ) {
216        $useGeSHi = MediaWikiServices::getInstance()->getMainConfig()->get( 'ScribuntoUseGeSHi' );
217        if (
218            $useGeSHi && $codeLang && ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' )
219        ) {
220            $status = SyntaxHighlight::highlight( $source, $codeLang, [ 'line' => true, 'linelinks' => 'L' ] );
221            if ( $status->isGood() ) {
222                // @todo replace addModuleStyles line with the appropriate call on
223                // SyntaxHighlight once one is created
224                $parserOutput->addModuleStyles( [ 'ext.pygments' ] );
225                $parserOutput->addModules( [ 'ext.pygments.linenumbers' ] );
226                return $status->getValue();
227            }
228        }
229
230        return Html::element( 'pre', [
231            // Same as CodeContentHandler
232            'lang' => 'en',
233            'dir' => 'ltr',
234            'class' => 'mw-code mw-script'
235        ], "\n$source\n" );
236    }
237
238    /**
239     * Create a redirect version of the content
240     *
241     * @param Title $target
242     * @param string $text
243     * @return ScribuntoContent
244     */
245    public function makeRedirectContent( Title $target, $text = '' ) {
246        return Scribunto::newDefaultEngine()->makeRedirectContent( $target, $text );
247    }
248
249    /**
250     * @return bool
251     */
252    public function supportsRedirects() {
253        return Scribunto::newDefaultEngine()->supportsRedirects();
254    }
255}