Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
25.22% covered (danger)
25.22%
29 / 115
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParsoidParser
25.22% covered (danger)
25.22%
29 / 115
0.00% covered (danger)
0.00%
0 / 6
169.98
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 setParsoidRenderID
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 genParserOutput
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
30
 parse
93.55% covered (success)
93.55%
29 / 31
0.00% covered (danger)
0.00%
0 / 1
7.01
 parseFakeRevision
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 makeLimitReport
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Parser\Parsoid;
4
5use MediaWiki\Languages\LanguageConverterFactory;
6use MediaWiki\MediaWikiServices;
7use MediaWiki\Page\PageReference;
8use MediaWiki\Parser\ParserOutput;
9use MediaWiki\Parser\Parsoid\Config\PageConfigFactory;
10use MediaWiki\Revision\MutableRevisionRecord;
11use MediaWiki\Revision\RevisionRecord;
12use MediaWiki\Revision\SlotRecord;
13use MediaWiki\Title\Title;
14use ParserFactory;
15use ParserOptions;
16use Wikimedia\Assert\Assert;
17use Wikimedia\Parsoid\Config\PageConfig;
18use Wikimedia\Parsoid\Parsoid;
19use Wikimedia\UUID\GlobalIdGenerator;
20use WikitextContent;
21
22/**
23 * Parser implementation which uses Parsoid.
24 *
25 * Currently incomplete; see T236809 for the long-term plan.
26 *
27 * @since 1.41
28 * @unstable since 1.41; see T236809 for plan.
29 */
30class ParsoidParser /* eventually this will extend \Parser */ {
31    /**
32     * @unstable
33     * This should not be used widely right now since this may go away.
34     * This is being added to support DiscussionTools with Parsoid HTML
35     * and after initial exploration, this may be implemented differently.
36     */
37    public const PARSOID_TITLE_KEY = "parsoid:title-dbkey";
38    private Parsoid $parsoid;
39    private PageConfigFactory $pageConfigFactory;
40    private LanguageConverterFactory $languageConverterFactory;
41    private ParserFactory $legacyParserFactory;
42    private GlobalIdGenerator $globalIdGenerator;
43
44    /**
45     * @param Parsoid $parsoid
46     * @param PageConfigFactory $pageConfigFactory
47     * @param LanguageConverterFactory $languageConverterFactory
48     * @param ParserFactory $legacyParserFactory
49     * @param GlobalIdGenerator $globalIdGenerator
50     */
51    public function __construct(
52        Parsoid $parsoid,
53        PageConfigFactory $pageConfigFactory,
54        LanguageConverterFactory $languageConverterFactory,
55        ParserFactory $legacyParserFactory,
56        GlobalIdGenerator $globalIdGenerator
57    ) {
58        $this->parsoid = $parsoid;
59        $this->pageConfigFactory = $pageConfigFactory;
60        $this->languageConverterFactory = $languageConverterFactory;
61        $this->legacyParserFactory = $legacyParserFactory;
62        $this->globalIdGenerator = $globalIdGenerator;
63    }
64
65    /**
66     * API users expect a ParsoidRenderID value set in the parser output's extension data.
67     * @param PageConfig $pageConfig
68     * @param ParserOutput $parserOutput
69     */
70    private function setParsoidRenderID( PageConfig $pageConfig, ParserOutput $parserOutput ): void {
71        $parserOutput->setRenderId( $this->globalIdGenerator->newUUIDv1() );
72        $parserOutput->setCacheRevisionId( $pageConfig->getRevisionId() );
73        $parserOutput->setRevisionTimestamp( $pageConfig->getRevisionTimestamp() );
74        $parserOutput->setCacheTime( wfTimestampNow() );
75    }
76
77    /**
78     * Internal helper to avoid code deuplication across two methods
79     *
80     * @param PageConfig $pageConfig
81     * @param ParserOptions $options
82     * @return ParserOutput
83     */
84    private function genParserOutput(
85        PageConfig $pageConfig, ParserOptions $options
86    ): ParserOutput {
87        $parserOutput = new ParserOutput();
88
89        // Parsoid itself does not vary output by parser options right now.
90        // But, ensure that any option use by extensions, parser functions,
91        // recursive parses, or (in the unlikely future scenario) Parsoid itself
92        // are recorded as used.
93        $options->registerWatcher( [ $parserOutput, 'recordOption' ] );
94
95        // The enable/disable logic here matches that in Parser::internalParseHalfParsed(),
96        // although __NOCONTENTCONVERT__ is handled internal to Parsoid.
97        //
98        // T349137: It might be preferable to handle __NOCONTENTCONVERT__ here rather than
99        // by inspecting the DOM inside Parsoid. That will come in a separate patch.
100        $htmlVariantLanguage = null;
101        if ( !( $options->getDisableContentConversion() || $options->getInterfaceMessage() ) ) {
102            // NOTES (some of these are TODOs for read views integration)
103            // 1. This html variant conversion is a pre-cache transform. HtmlOutputRendererHelper
104            //    has another variant conversion that is a post-cache transform based on the
105            //    'Accept-Language' header. If that header is set, there is really no reason to
106            //    do this conversion here. So, eventually, we are likely to either not pass in
107            //    the htmlVariantLanguage option below OR disable language conversion from the
108            //    wt2html path in Parsoid and this and the Accept-Language variant conversion
109            //    both would have to be handled as post-cache transforms.
110            //
111            // 2. Parser.php calls convert() which computes a preferred variant from the
112            //    target language. But, we cannot do that unconditionally here because REST API
113            //    requests specify the exact variant via the 'Content-Language' header.
114            //
115            //    For Parsoid page views, either the callers will have to compute the
116            //    preferred variant and set it in ParserOptions OR the REST API will have
117            //    to set some other flag indicating that the preferred variant should not
118            //    be computed. For now, I am adding a temporary hack, but this should be
119            //    replaced with something more sensible (T267067).
120            //
121            // 3. Additionally, Parsoid's callers will have to set targetLanguage in ParserOptions
122            //    to mimic the logic in Parser.php (missing right now).
123            $langCode = $pageConfig->getPageLanguageBcp47();
124            if ( $options->getRenderReason() === 'page-view' ) { // TEMPORARY HACK
125                $langFactory = MediaWikiServices::getInstance()->getLanguageFactory();
126                $lang = $langFactory->getLanguage( $langCode );
127                $langConv = $this->languageConverterFactory->getLanguageConverter( $lang );
128                $htmlVariantLanguage = $langFactory->getLanguage( $langConv->getPreferredVariant() );
129            } else {
130                $htmlVariantLanguage = $langCode;
131            }
132        }
133
134        $defaultOptions = [
135            'pageBundle' => true,
136            'wrapSections' => true,
137            'logLinterData' => true,
138            'body_only' => false,
139            'htmlVariantLanguage' => $htmlVariantLanguage,
140            'offsetType' => 'byte',
141            'outputContentVersion' => Parsoid::defaultHTMLVersion()
142        ];
143
144        $parserOutput->resetParseStartTime();
145
146        // This can throw ClientError or ResourceLimitExceededException.
147        // Callers are responsible for figuring out how to handle them.
148        $pageBundle = $this->parsoid->wikitext2html(
149            $pageConfig,
150            $defaultOptions,
151            $headers,
152            $parserOutput );
153
154        $parserOutput = PageBundleParserOutputConverter::parserOutputFromPageBundle( $pageBundle, $parserOutput );
155
156        // Record the page title in dbkey form so that post-cache transforms
157        // have access to the title.
158        $parserOutput->setExtensionData(
159            self::PARSOID_TITLE_KEY,
160            Title::newFromLinkTarget( $pageConfig->getLinkTarget() )->getPrefixedDBkey()
161        );
162
163        // Register a watcher again because the $parserOuptut arg
164        // and $parserOutput return value above are different objects!
165        $options->registerWatcher( [ $parserOutput, 'recordOption' ] );
166
167        $revId = $pageConfig->getRevisionId();
168        if ( $revId !== null ) {
169            // T350538: This shouldn't be necessary so long as ContentRenderer
170            // is involved in the call chain somewhere, and should be turned
171            // into an assertion (and ::setParsoidRenderID() removed).
172            $this->setParsoidRenderID( $pageConfig, $parserOutput );
173        }
174
175        $parserOutput->setFromParserOptions( $options );
176
177        $parserOutput->recordTimeProfile();
178        $this->makeLimitReport( $options, $parserOutput );
179
180        // Add Parsoid skinning module
181        $parserOutput->addModuleStyles( [ 'mediawiki.skinning.content.parsoid' ] );
182
183        // Record Parsoid version in extension data; this allows
184        // us to use the onRejectParserCacheValue hook to selectively
185        // expire "bad" generated content in the event of a rollback.
186        $parserOutput->setExtensionData(
187            'core:parsoid-version', Parsoid::version()
188        );
189        $parserOutput->setExtensionData(
190            'core:html-version', Parsoid::defaultHTMLVersion()
191        );
192
193        return $parserOutput;
194    }
195
196    /**
197     * Convert wikitext to HTML
198     * Do not call this function recursively.
199     *
200     * @param string $text Text we want to parse
201     * @param-taint $text escapes_htmlnoent
202     * @param PageReference $page
203     * @param ParserOptions $options
204     * @param bool $linestart
205     * @param bool $clearState
206     * @param int|null $revId ID of the revision being rendered. This is used to render
207     *  REVISION* magic words. 0 means that any current revision will be used. Null means
208     *  that {{REVISIONID}}/{{REVISIONUSER}} will be empty and {{REVISIONTIMESTAMP}} will
209     *  use the current timestamp.
210     * @return ParserOutput
211     * @return-taint escaped
212     * @unstable since 1.41
213     */
214    public function parse(
215        string $text, PageReference $page, ParserOptions $options,
216        bool $linestart = true, bool $clearState = true, ?int $revId = null
217    ): ParserOutput {
218        Assert::invariant( $linestart, '$linestart=false is not yet supported' );
219        Assert::invariant( $clearState, '$clearState=false is not yet supported' );
220        $title = Title::newFromPageReference( $page );
221        $lang = $options->getTargetLanguage();
222        if ( $lang === null && $options->getInterfaceMessage() ) {
223            $lang = $options->getUserLangObj();
224        }
225        $pageConfig = $revId === null ? null : $this->pageConfigFactory->create(
226            $title,
227            $options->getUserIdentity(),
228            $revId,
229            null, // unused
230            $lang // defaults to title page language if null
231        );
232        if ( !( $pageConfig && $pageConfig->getPageMainContent() === $text ) ) {
233            // This is a bit awkward! But we really need to parse $text, which
234            // may or may not correspond to the $revId provided!
235            // T332928 suggests one solution: splitting the "have revid"
236            // callers from the "bare text, no associated revision" callers.
237            $revisionRecord = new MutableRevisionRecord( $title );
238            if ( $revId !== null ) {
239                $revisionRecord->setId( $revId );
240            }
241            $revisionRecord->setSlot(
242                SlotRecord::newUnsaved(
243                    SlotRecord::MAIN,
244                    new WikitextContent( $text )
245                )
246            );
247            $pageConfig = $this->pageConfigFactory->create(
248                $title,
249                $options->getUserIdentity(),
250                $revisionRecord,
251                null, // unused
252                $lang // defaults to title page language if null
253            );
254        }
255
256        return $this->genParserOutput( $pageConfig, $options );
257    }
258
259    /**
260     * @internal
261     *
262     * Convert custom wikitext (stored in main slot of the $fakeRev arg) to HTML.
263     * Callers are expected NOT to stuff the result into ParserCache.
264     *
265     * @param RevisionRecord $fakeRev Revision to parse
266     * @param PageReference $page
267     * @param ParserOptions $options
268     * @return ParserOutput
269     * @unstable since 1.41
270     */
271    public function parseFakeRevision(
272        RevisionRecord $fakeRev, PageReference $page, ParserOptions $options
273    ): ParserOutput {
274        $title = Title::newFromPageReference( $page );
275        $lang = $options->getTargetLanguage();
276        if ( $lang === null && $options->getInterfaceMessage() ) {
277            $lang = $options->getUserLangObj();
278        }
279        $pageConfig = $this->pageConfigFactory->create(
280            $title,
281            $options->getUserIdentity(),
282            $fakeRev,
283            null, // unused
284            $lang // defaults to title page language if null
285        );
286
287        return $this->genParserOutput( $pageConfig, $options );
288    }
289
290    /**
291     * Set the limit report data in the current ParserOutput.
292     * This is ported from Parser::makeLimitReport() and should eventually
293     * use the method from the superclass directly.
294     */
295    protected function makeLimitReport(
296        ParserOptions $parserOptions, ParserOutput $parserOutput
297    ) {
298        $maxIncludeSize = $parserOptions->getMaxIncludeSize();
299
300        $cpuTime = $parserOutput->getTimeProfile( 'cpu' );
301        if ( $cpuTime !== null ) {
302            $parserOutput->setLimitReportData( 'limitreport-cputime',
303                sprintf( "%.3f", $cpuTime )
304            );
305        }
306
307        $wallTime = $parserOutput->getTimeProfile( 'wall' );
308        $parserOutput->setLimitReportData( 'limitreport-walltime',
309            sprintf( "%.3f", $wallTime )
310        );
311
312        $parserOutput->setLimitReportData( 'limitreport-timingprofile', [ 'not yet supported' ] );
313
314        // Add other cache related metadata
315        $parserOutput->setLimitReportData( 'cachereport-timestamp',
316            $parserOutput->getCacheTime() );
317        $parserOutput->setLimitReportData( 'cachereport-ttl',
318            $parserOutput->getCacheExpiry() );
319        $parserOutput->setLimitReportData( 'cachereport-transientcontent',
320            $parserOutput->hasReducedExpiry() );
321    }
322
323}