Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 129
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExtensionHandler
0.00% covered (danger)
0.00%
0 / 129
0.00% covered (danger)
0.00%
0 / 6
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 normalizeExtOptions
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 onExtension
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
182
 onDocumentFragment
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
72
 onTag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 stripAnnotations
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html\TT;
5
6use Wikimedia\Assert\Assert;
7use Wikimedia\Assert\UnreachableException;
8use Wikimedia\Parsoid\Config\SiteConfig;
9use Wikimedia\Parsoid\DOM\DocumentFragment;
10use Wikimedia\Parsoid\Ext\ExtensionError;
11use Wikimedia\Parsoid\Ext\ExtensionTag;
12use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
13use Wikimedia\Parsoid\NodeData\DataMw;
14use Wikimedia\Parsoid\Tokens\Token;
15use Wikimedia\Parsoid\Utils\DOMDataUtils;
16use Wikimedia\Parsoid\Utils\DOMUtils;
17use Wikimedia\Parsoid\Utils\PipelineUtils;
18use Wikimedia\Parsoid\Utils\TokenUtils;
19use Wikimedia\Parsoid\Utils\Utils;
20use Wikimedia\Parsoid\Utils\WTUtils;
21use Wikimedia\Parsoid\Wt2Html\TokenTransformManager;
22
23class ExtensionHandler extends TokenHandler {
24
25    public function __construct( TokenTransformManager $manager, array $options ) {
26        parent::__construct( $manager, $options );
27    }
28
29    private static function normalizeExtOptions( array $options ): array {
30        // Mimics Sanitizer::decodeTagAttributes from the PHP parser
31        //
32        // Extension options should always be interpreted as plain text. The
33        // tokenizer parses them to tokens in case they are for an HTML tag,
34        // but here we use the text source instead.
35        $n = count( $options );
36        for ( $i = 0; $i < $n; $i++ ) {
37            $o = $options[$i];
38            // Use the source if present. If not use the value, but ensure it's a
39            // string, as it can be a token stream if the parser has recognized it
40            // as a directive.
41            $v = $o->vsrc ?? TokenUtils::tokensToString( $o->v, false, [ 'includeEntities' => true ] );
42            // Normalize whitespace in extension attribute values
43            // FIXME: If the option is parsed as wikitext, this normalization
44            // can mess with src offsets.
45            $o->v = trim( preg_replace( '/[\t\r\n ]+/', ' ', $v ) );
46            // Decode character references
47            $o->v = Utils::decodeWtEntities( $o->v );
48        }
49        return $options;
50    }
51
52    private function onExtension( Token $token ): TokenHandlerResult {
53        $env = $this->env;
54        $siteConfig = $env->getSiteConfig();
55        $pageConfig = $env->getPageConfig();
56        $extensionName = $token->getAttributeV( 'name' );
57        $extConfig = $env->getSiteConfig()->getExtTagConfig( $extensionName );
58
59        $metrics = $siteConfig->metrics();
60        if ( $metrics ) {
61            // Track uses of extensions
62            $wiki = $siteConfig->iwp();
63            $ns = $env->getContextTitle()->getNamespace();
64            if ( $ns === 0 ) {
65                // Article space
66                $nsName = 'main';
67            } elseif ( $siteConfig->namespaceIsTalk( $ns ) ) {
68                // Any talk namespace
69                $nsName = 'talk';
70            } else {
71                // Everything else
72                $nsName = "ns-$ns";
73            }
74            $metrics->increment( "extension.{$wiki}.{$nsName}.{$extensionName}" );
75        }
76
77        $nativeExt = $siteConfig->getExtTagImpl( $extensionName );
78        $cachedExpansion = $env->extensionCache[$token->dataParsoid->src] ?? null;
79
80        $options = $token->getAttributeV( 'options' );
81        $token->setAttribute( 'options', self::normalizeExtOptions( $options ) );
82
83        // Call after normalizing extension options, since that can affect the result
84        $dataMw = Utils::getExtArgInfo( $token );
85
86        if ( $nativeExt !== null ) {
87            $extArgs = $token->getAttributeV( 'options' );
88            $extApi = new ParsoidExtensionAPI( $env, [
89                'wt2html' => [
90                    'frame' => $this->manager->getFrame(),
91                    'parseOpts' => $this->options,
92                    'extTag' => new ExtensionTag( $token ),
93                ],
94            ] );
95            try {
96                $extSrc = $dataMw->body->extsrc ?? '';
97                if ( !( $extConfig['options']['hasWikitextInput'] ?? true ) ) {
98                    $extSrc = $this->stripAnnotations( $extSrc, $env->getSiteConfig() );
99                }
100                $domFragment = $nativeExt->sourceToDom(
101                    $extApi, $extSrc ?? '', $extArgs
102                );
103                $errors = $extApi->getErrors();
104                if ( $extConfig['options']['wt2html']['customizesDataMw'] ?? false ) {
105                    $firstNode = $domFragment->firstChild;
106                    DOMUtils::assertElt( $firstNode );
107                    $dataMw = DOMDataUtils::getDataMw( $firstNode );
108                }
109            } catch ( ExtensionError $e ) {
110                $domFragment = WTUtils::createInterfaceI18nFragment(
111                    $env->topLevelDoc, $e->err['key'], $e->err['params'] ?? null
112                );
113                $errors = [ $e->err ];
114                // FIXME: Should we include any errors collected
115                // from $extApi->getErrors() here?  Also, what's the correct $dataMw
116                // to apply in this case?
117            }
118            if ( $domFragment !== false ) {
119                if ( $domFragment !== null ) {
120                    // Turn this document fragment into a token
121                    $toks = $this->onDocumentFragment(
122                        $token, $domFragment, $dataMw, $errors
123                    );
124                    return new TokenHandlerResult( $toks );
125                } else {
126                    // The extension dropped this instance completely (!!)
127                    // Should be a rarity and presumably the extension
128                    // knows what it is doing. Ex: nested refs are dropped
129                    // in some scenarios.
130                    return new TokenHandlerResult( [] );
131                }
132            }
133            // Fall through: this extension is electing not to use
134            // a custom sourceToDom method (by returning false from
135            // sourceToDom).
136        }
137
138        if ( $cachedExpansion ) {
139            // WARNING: THIS HAS BEEN UNUSED SINCE 2015, SEE T98995.
140            // THIS CODE WAS WRITTEN BUT APPARENTLY NEVER TESTED.
141            // NO WARRANTY.  MAY HALT AND CATCH ON FIRE.
142            throw new UnreachableException( 'Should not be here!' );
143            /*
144            $toks = PipelineUtils::encapsulateExpansionHTML(
145                $env, $token, $cachedExpansion, [ 'fromCache' => true ]
146            );
147            */
148        } else {
149            $start = microtime( true );
150            $domFragment = PipelineUtils::fetchHTML( $env, $token->getAttributeV( 'source' ) );
151            if ( $env->profiling() ) {
152                $profile = $env->getCurrentProfile();
153                $profile->bumpMWTime( "Extension", 1000 * ( microtime( true ) - $start ), "api" );
154                $profile->bumpCount( "Extension" );
155            }
156            if ( !$domFragment ) {
157                $domFragment = DOMUtils::parseHTMLToFragment( $env->topLevelDoc, '' );
158            }
159            $toks = $this->onDocumentFragment( $token, $domFragment, $dataMw, [] );
160        }
161        return new TokenHandlerResult( $toks );
162    }
163
164    /**
165     * DOMFragment-based encapsulation
166     *
167     * @param Token $extToken
168     * @param DocumentFragment $domFragment
169     * @param DataMw $dataMw
170     * @param array $errors
171     * @return array
172     */
173    private function onDocumentFragment(
174        Token $extToken, DocumentFragment $domFragment, DataMw $dataMw,
175        array $errors
176    ): array {
177        $env = $this->env;
178        $extensionName = $extToken->getAttributeV( 'name' );
179
180        if ( $env->hasDumpFlag( 'extoutput' ) ) {
181            $logger = $env->getSiteConfig()->getLogger();
182            $logger->warning( str_repeat( '=', 80 ) );
183            $logger->warning(
184                'EXTENSION INPUT: ' . $extToken->getAttributeV( 'source' )
185            );
186            $logger->warning( str_repeat( '=', 80 ) );
187            $logger->warning( "EXTENSION OUTPUT:\n" );
188            $logger->warning(
189                DOMUtils::getFragmentInnerHTML( $domFragment )
190            );
191            $logger->warning( str_repeat( '-', 80 ) );
192        }
193
194        $opts = [
195            'setDSR' => true,
196            'wrapperName' => $extensionName,
197        ];
198
199        // Check if the tag wants its DOM fragment not to be unpacked.
200        // The default setting is to unpack the content DOM fragment automatically.
201        $extConfig = $env->getSiteConfig()->getExtTagConfig( $extensionName );
202        if ( isset( $extConfig['options']['wt2html'] ) ) {
203            $opts += $extConfig['options']['wt2html'];
204        }
205
206        // This special case is only because, from the beginning, Parsoid has
207        // treated <nowiki>s as core functionality with lean markup (no about,
208        // no data-mw, custom typeof).
209        //
210        // We'll keep this hardcoded to avoid exposing the functionality to
211        // other native extensions until it's needed.
212        if ( $extensionName !== 'nowiki' ) {
213            if ( !$domFragment->hasChildNodes() ) {
214                // RT extensions expanding to nothing.
215                $domFragment->appendChild(
216                    $domFragment->ownerDocument->createElement( 'link' )
217                );
218            }
219
220            // Wrap the top-level nodes so that we have a firstNode element
221            // to annotate with the typeof and to apply about ids.
222            PipelineUtils::addSpanWrappers( $domFragment->childNodes );
223
224            // Now get the firstNode
225            $firstNode = $domFragment->firstChild;
226
227            DOMUtils::assertElt( $firstNode );
228
229            // Adds the wrapper attributes to the first element
230            DOMUtils::addTypeOf( $firstNode, "mw:Extension/{$extensionName}" );
231
232            // FIXME: What happens if $firstNode is template generated, since
233            // they have higher precedence?  These questions and more in T214241
234            Assert::invariant(
235                !DOMUtils::hasTypeOf( $firstNode, 'mw:Transclusion' ),
236                'First node of extension content is transcluded.'
237            );
238
239            if ( count( $errors ) > 0 ) {
240                DOMUtils::addTypeOf( $firstNode, 'mw:Error' );
241                $dataMw->errors = is_array( $dataMw->errors ?? null ) ?
242                    array_merge( $dataMw->errors, $errors ) : $errors;
243            }
244
245            // Set data-mw
246            // FIXME: Similar to T214241, we're clobbering $firstNode
247            DOMDataUtils::setDataMw( $firstNode, $dataMw );
248
249            // Add about to all wrapper tokens.
250            $about = $env->newAboutId();
251            $n = $firstNode;
252            while ( $n ) {
253                $n->setAttribute( 'about', $about );
254                $n = $n->nextSibling;
255            }
256
257            // Update data-parsoid
258            $dp = DOMDataUtils::getDataParsoid( $firstNode );
259            $dp->tsr = clone $extToken->dataParsoid->tsr;
260            $dp->src = $extToken->dataParsoid->src;
261            DOMDataUtils::setDataParsoid( $firstNode, $dp );
262        }
263
264        return PipelineUtils::tunnelDOMThroughTokens(
265            $env, $extToken, $domFragment, $opts
266        );
267    }
268
269    /**
270     * @inheritDoc
271     */
272    public function onTag( Token $token ): ?TokenHandlerResult {
273        return $token->getName() === 'extension' ? $this->onExtension( $token ) : null;
274    }
275
276    private function stripAnnotations( string $s, SiteConfig $siteConfig ): string {
277        $annotationStrippers = $siteConfig->getAnnotationStrippers();
278
279        $res = $s;
280        foreach ( $annotationStrippers as $annotationStripper ) {
281            $res = $annotationStripper->stripAnnotations( $s );
282        }
283        return $res;
284    }
285}