Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 129 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
ExtensionHandler | |
0.00% |
0 / 129 |
|
0.00% |
0 / 6 |
812 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
normalizeExtOptions | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
onExtension | |
0.00% |
0 / 64 |
|
0.00% |
0 / 1 |
182 | |||
onDocumentFragment | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
72 | |||
onTag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
stripAnnotations | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Wt2Html\TT; |
5 | |
6 | use Wikimedia\Assert\Assert; |
7 | use Wikimedia\Assert\UnreachableException; |
8 | use Wikimedia\Parsoid\Config\SiteConfig; |
9 | use Wikimedia\Parsoid\DOM\DocumentFragment; |
10 | use Wikimedia\Parsoid\Ext\ExtensionError; |
11 | use Wikimedia\Parsoid\Ext\ExtensionTag; |
12 | use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; |
13 | use Wikimedia\Parsoid\NodeData\DataMw; |
14 | use Wikimedia\Parsoid\Tokens\Token; |
15 | use Wikimedia\Parsoid\Utils\DOMDataUtils; |
16 | use Wikimedia\Parsoid\Utils\DOMUtils; |
17 | use Wikimedia\Parsoid\Utils\PipelineUtils; |
18 | use Wikimedia\Parsoid\Utils\TokenUtils; |
19 | use Wikimedia\Parsoid\Utils\Utils; |
20 | use Wikimedia\Parsoid\Utils\WTUtils; |
21 | use Wikimedia\Parsoid\Wt2Html\TokenTransformManager; |
22 | |
23 | class 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 | } |