Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
7.35% |
10 / 136 |
|
12.50% |
2 / 16 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
7.35% |
10 / 136 |
|
12.50% |
2 / 16 |
2119.40 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onRegistration | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onSoftwareInfo | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
onParserFirstCallInit | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onParserClearState | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onParserCloned | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
invokeHook | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
132 | |||
reportTiming | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
132 | |||
onContentHandlerDefaultModelFor | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
onParserLimitReportPrepare | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onParserLimitReportFormat | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onEditPage__showStandardInputs_options | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onEditPage__showReadOnlyForm_initial | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onEditPageBeforeEditButtons | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onEditFilterMergedContent | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
onArticleViewHeader | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * Wikitext scripting infrastructure for MediaWiki: hooks. |
4 | * Copyright (C) 2009-2012 Victor Vasiliev <vasilvv@gmail.com> |
5 | * https://www.mediawiki.org/ |
6 | * |
7 | * This program is free software; you can redistribute it and/or modify |
8 | * it under the terms of the GNU General Public License as published by |
9 | * the Free Software Foundation; either version 2 of the License, or |
10 | * (at your option) any later version. |
11 | * |
12 | * This program is distributed in the hope that it will be useful, |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | * GNU General Public License for more details. |
16 | * |
17 | * You should have received a copy of the GNU General Public License along |
18 | * with this program; if not, write to the Free Software Foundation, Inc., |
19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
20 | * http://www.gnu.org/copyleft/gpl.html |
21 | */ |
22 | |
23 | // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
24 | |
25 | namespace MediaWiki\Extension\Scribunto; |
26 | |
27 | use Article; |
28 | use Content; |
29 | use EmptyBagOStuff; |
30 | use IContextSource; |
31 | use MediaWiki\Config\Config; |
32 | use MediaWiki\EditPage\EditPage; |
33 | use MediaWiki\Hook\EditFilterMergedContentHook; |
34 | use MediaWiki\Hook\EditPage__showReadOnlyForm_initialHook; |
35 | use MediaWiki\Hook\EditPage__showStandardInputs_optionsHook; |
36 | use MediaWiki\Hook\EditPageBeforeEditButtonsHook; |
37 | use MediaWiki\Hook\ParserClearStateHook; |
38 | use MediaWiki\Hook\ParserClonedHook; |
39 | use MediaWiki\Hook\ParserFirstCallInitHook; |
40 | use MediaWiki\Hook\ParserLimitReportFormatHook; |
41 | use MediaWiki\Hook\ParserLimitReportPrepareHook; |
42 | use MediaWiki\Hook\SoftwareInfoHook; |
43 | use MediaWiki\Html\Html; |
44 | use MediaWiki\MediaWikiServices; |
45 | use MediaWiki\Output\OutputPage; |
46 | use MediaWiki\Page\Hook\ArticleViewHeaderHook; |
47 | use MediaWiki\Parser\ParserOutput; |
48 | use MediaWiki\Revision\Hook\ContentHandlerDefaultModelForHook; |
49 | use MediaWiki\Status\Status; |
50 | use MediaWiki\Title\Title; |
51 | use MediaWiki\User\User; |
52 | use MediaWiki\WikiMap\WikiMap; |
53 | use ObjectCache; |
54 | use Parser; |
55 | use PPFrame; |
56 | use UtfNormal\Validator; |
57 | use Wikimedia\PSquare; |
58 | use Xml; |
59 | |
60 | /** |
61 | * Hooks for the Scribunto extension. |
62 | */ |
63 | class Hooks implements |
64 | SoftwareInfoHook, |
65 | ParserFirstCallInitHook, |
66 | ParserLimitReportPrepareHook, |
67 | ParserLimitReportFormatHook, |
68 | ParserClearStateHook, |
69 | ParserClonedHook, |
70 | EditPage__showStandardInputs_optionsHook, |
71 | EditPage__showReadOnlyForm_initialHook, |
72 | EditPageBeforeEditButtonsHook, |
73 | EditFilterMergedContentHook, |
74 | ArticleViewHeaderHook, |
75 | ContentHandlerDefaultModelForHook |
76 | { |
77 | private Config $config; |
78 | |
79 | public function __construct( |
80 | Config $config |
81 | ) { |
82 | $this->config = $config; |
83 | } |
84 | |
85 | /** |
86 | * Define content handler constant upon extension registration |
87 | */ |
88 | public static function onRegistration() { |
89 | define( 'CONTENT_MODEL_SCRIBUNTO', 'Scribunto' ); |
90 | } |
91 | |
92 | /** |
93 | * Get software information for Special:Version |
94 | * |
95 | * @param array &$software |
96 | * @return bool |
97 | */ |
98 | public function onSoftwareInfo( &$software ) { |
99 | $engine = Scribunto::newDefaultEngine(); |
100 | $engine->setTitle( Title::makeTitle( NS_SPECIAL, 'Version' ) ); |
101 | $engine->getSoftwareInfo( $software ); |
102 | return true; |
103 | } |
104 | |
105 | /** |
106 | * Register parser hooks. |
107 | * |
108 | * @param Parser $parser |
109 | * @return bool |
110 | */ |
111 | public function onParserFirstCallInit( $parser ) { |
112 | $parser->setFunctionHook( 'invoke', [ $this, 'invokeHook' ], Parser::SFH_OBJECT_ARGS ); |
113 | return true; |
114 | } |
115 | |
116 | /** |
117 | * Called when the interpreter is to be reset. |
118 | * |
119 | * @param Parser $parser |
120 | * @return bool |
121 | */ |
122 | public function onParserClearState( $parser ) { |
123 | Scribunto::resetParserEngine( $parser ); |
124 | return true; |
125 | } |
126 | |
127 | /** |
128 | * Called when the parser is cloned |
129 | * |
130 | * @param Parser $parser |
131 | * @return bool |
132 | */ |
133 | public function onParserCloned( $parser ) { |
134 | $parser->scribunto_engine = null; |
135 | return true; |
136 | } |
137 | |
138 | /** |
139 | * Hook function for {{#invoke:module|func}} |
140 | * |
141 | * @param Parser $parser |
142 | * @param PPFrame $frame |
143 | * @param array $args |
144 | * @return string |
145 | */ |
146 | public function invokeHook( Parser $parser, PPFrame $frame, array $args ) { |
147 | try { |
148 | if ( count( $args ) < 2 ) { |
149 | throw new ScribuntoException( 'scribunto-common-nofunction' ); |
150 | } |
151 | $moduleName = trim( $frame->expand( $args[0] ) ); |
152 | $engine = Scribunto::getParserEngine( $parser ); |
153 | |
154 | $title = Title::makeTitleSafe( NS_MODULE, $moduleName ); |
155 | if ( !$title || !$title->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) { |
156 | throw new ScribuntoException( 'scribunto-common-nosuchmodule', |
157 | [ 'args' => [ $moduleName ] ] ); |
158 | } |
159 | $module = $engine->fetchModuleFromParser( $title ); |
160 | if ( !$module ) { |
161 | throw new ScribuntoException( 'scribunto-common-nosuchmodule', |
162 | [ 'args' => [ $moduleName ] ] ); |
163 | } |
164 | $functionName = trim( $frame->expand( $args[1] ) ); |
165 | |
166 | $bits = $args[1]->splitArg(); |
167 | unset( $args[0] ); |
168 | unset( $args[1] ); |
169 | |
170 | // If $bits['index'] is empty, then the function name was parsed as a |
171 | // key=value pair (because of an equals sign in it), and since it didn't |
172 | // have an index, we don't need the index offset. |
173 | $childFrame = $frame->newChild( $args, $title, $bits['index'] === '' ? 0 : 1 ); |
174 | |
175 | if ( $this->config->get( 'ScribuntoGatherFunctionStats' ) ) { |
176 | $u0 = $engine->getResourceUsage( $engine::CPU_SECONDS ); |
177 | $result = $module->invoke( $functionName, $childFrame ); |
178 | $u1 = $engine->getResourceUsage( $engine::CPU_SECONDS ); |
179 | |
180 | if ( $u1 > $u0 ) { |
181 | $timingMs = (int)( 1000 * ( $u1 - $u0 ) ); |
182 | // Since the overhead of stats is worst when #invoke |
183 | // calls are very short, don't process measurements <= 20ms. |
184 | if ( $timingMs > 20 ) { |
185 | $this->reportTiming( $moduleName, $functionName, $timingMs ); |
186 | } |
187 | } |
188 | } else { |
189 | $result = $module->invoke( $functionName, $childFrame ); |
190 | } |
191 | |
192 | return Validator::cleanUp( strval( $result ) ); |
193 | } catch ( ScribuntoException $e ) { |
194 | $trace = $e->getScriptTraceHtml( [ 'msgOptions' => [ 'content' ] ] ); |
195 | $html = Html::element( 'p', [], $e->getMessage() ); |
196 | if ( $trace !== false ) { |
197 | $html .= Html::element( 'p', |
198 | [], |
199 | wfMessage( 'scribunto-common-backtrace' )->inContentLanguage()->text() |
200 | ) . $trace; |
201 | } else { |
202 | $html .= Html::element( 'p', |
203 | [], |
204 | wfMessage( 'scribunto-common-no-details' )->inContentLanguage()->text() |
205 | ); |
206 | } |
207 | |
208 | // Index this error by a uniq ID so that we are independent of |
209 | // page parse order. (T300979) |
210 | // (The only way this will conflict is if two exceptions have |
211 | // exactly the same backtrace, in which case we really only need |
212 | // one copy of the backtrace!) |
213 | $uuid = substr( sha1( $html ), -8 ); |
214 | $parserOutput = $parser->getOutput(); |
215 | $parserOutput->appendExtensionData( 'ScribuntoErrors', $uuid ); |
216 | $parserOutput->setExtensionData( "ScribuntoErrors-$uuid", $html ); |
217 | |
218 | $parserOutput->appendJsConfigVar( 'ScribuntoErrors', $uuid ); |
219 | $parserOutput->setJsConfigVar( "ScribuntoErrors-$uuid", $html ); |
220 | |
221 | // These methods are idempotent; doesn't hurt to call them every |
222 | // time. |
223 | $parser->addTrackingCategory( 'scribunto-common-error-category' ); |
224 | $parserOutput->addModules( [ 'ext.scribunto.errors' ] ); |
225 | |
226 | $id = "mw-scribunto-error-$uuid"; |
227 | $parserError = htmlspecialchars( $e->getMessage() ); |
228 | |
229 | // #iferror-compatible error element |
230 | return "<strong class=\"error\"><span class=\"scribunto-error\" id=\"$id\">" . |
231 | $parserError . "</span></strong>"; |
232 | } |
233 | } |
234 | |
235 | /** |
236 | * Record stats on slow function calls. |
237 | * |
238 | * @param string $moduleName |
239 | * @param string $functionName |
240 | * @param int $timing Function execution time in milliseconds. |
241 | */ |
242 | private function reportTiming( $moduleName, $functionName, $timing ) { |
243 | if ( !$this->config->get( 'ScribuntoGatherFunctionStats' ) ) { |
244 | return; |
245 | } |
246 | |
247 | $threshold = $this->config->get( 'ScribuntoSlowFunctionThreshold' ); |
248 | if ( !( is_float( $threshold ) && $threshold > 0 && $threshold < 1 ) ) { |
249 | return; |
250 | } |
251 | |
252 | static $cache; |
253 | if ( !$cache ) { |
254 | $cache = ObjectCache::getLocalServerInstance( CACHE_NONE ); |
255 | |
256 | } |
257 | |
258 | // To control the sampling rate, we keep a compact histogram of |
259 | // observations in APC, and extract the Nth percentile (specified |
260 | // via $wgScribuntoSlowFunctionThreshold; defaults to 0.90). |
261 | // We need a non-empty local server cache for that (e.g. php-apcu). |
262 | if ( $cache instanceof EmptyBagOStuff ) { |
263 | return; |
264 | } |
265 | |
266 | $cacheVersion = '3'; |
267 | $key = $cache->makeGlobalKey( 'scribunto-stats', $cacheVersion, (string)$threshold ); |
268 | |
269 | // This is a classic "read-update-write" critical section with no |
270 | // mutual exclusion, but the only consequence is that some samples |
271 | // will be dropped. We only need enough samples to estimate the |
272 | // shape of the data, so that's fine. |
273 | $ps = $cache->get( $key ) ?: new PSquare( $threshold ); |
274 | $ps->addObservation( $timing ); |
275 | $cache->set( $key, $ps, 60 ); |
276 | |
277 | if ( $ps->getCount() < 1000 || $timing < $ps->getValue() ) { |
278 | return; |
279 | } |
280 | |
281 | static $stats; |
282 | if ( !$stats ) { |
283 | $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); |
284 | } |
285 | |
286 | $metricKey = sprintf( 'scribunto.traces.%s__%s__%s', WikiMap::getCurrentWikiId(), $moduleName, $functionName ); |
287 | $stats->timing( $metricKey, $timing ); |
288 | } |
289 | |
290 | /** |
291 | * Set the Scribunto content handler for modules |
292 | * |
293 | * @param Title $title |
294 | * @param string &$model |
295 | * @return bool |
296 | */ |
297 | public function onContentHandlerDefaultModelFor( $title, &$model ) { |
298 | if ( $model === 'sanitized-css' ) { |
299 | // Let TemplateStyles override Scribunto |
300 | return true; |
301 | } |
302 | if ( $title->getNamespace() === NS_MODULE ) { |
303 | if ( str_ends_with( $title->getText(), '.json' ) ) { |
304 | $model = CONTENT_MODEL_JSON; |
305 | } elseif ( !Scribunto::isDocPage( $title ) ) { |
306 | $model = CONTENT_MODEL_SCRIBUNTO; |
307 | } |
308 | return true; |
309 | } |
310 | return true; |
311 | } |
312 | |
313 | /** |
314 | * Adds report of number of evaluations by the single wikitext page. |
315 | * |
316 | * @param Parser $parser |
317 | * @param ParserOutput $parserOutput |
318 | * @return bool |
319 | */ |
320 | public function onParserLimitReportPrepare( $parser, $parserOutput ) { |
321 | if ( Scribunto::isParserEnginePresent( $parser ) ) { |
322 | $engine = Scribunto::getParserEngine( $parser ); |
323 | $engine->reportLimitData( $parserOutput ); |
324 | } |
325 | return true; |
326 | } |
327 | |
328 | /** |
329 | * Formats the limit report data |
330 | * |
331 | * @param string $key |
332 | * @param mixed &$value |
333 | * @param string &$report |
334 | * @param bool $isHTML |
335 | * @param bool $localize |
336 | * @return bool |
337 | */ |
338 | public function onParserLimitReportFormat( $key, &$value, &$report, $isHTML, $localize ) { |
339 | $engine = Scribunto::newDefaultEngine(); |
340 | return $engine->formatLimitData( $key, $value, $report, $isHTML, $localize ); |
341 | } |
342 | |
343 | /** |
344 | * EditPage::showStandardInputs:options hook |
345 | * |
346 | * @param EditPage $editor |
347 | * @param OutputPage $output |
348 | * @param int &$tab Current tabindex |
349 | * @return bool |
350 | */ |
351 | public function onEditPage__showStandardInputs_options( $editor, $output, &$tab ) { |
352 | if ( $editor->getTitle()->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) { |
353 | $output->addModules( 'ext.scribunto.edit' ); |
354 | $editor->editFormTextAfterTools .= '<div id="mw-scribunto-console"></div>'; |
355 | } |
356 | return true; |
357 | } |
358 | |
359 | /** |
360 | * EditPage::showReadOnlyForm:initial hook |
361 | * |
362 | * @param EditPage $editor |
363 | * @param OutputPage $output |
364 | * @return bool |
365 | */ |
366 | public function onEditPage__showReadOnlyForm_initial( $editor, $output ) { |
367 | if ( $editor->getTitle()->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) { |
368 | $output->addModules( 'ext.scribunto.edit' ); |
369 | $editor->editFormTextAfterContent .= '<div id="mw-scribunto-console"></div>'; |
370 | } |
371 | return true; |
372 | } |
373 | |
374 | /** |
375 | * EditPageBeforeEditButtons hook |
376 | * |
377 | * @param EditPage $editor |
378 | * @param array &$buttons Button array |
379 | * @param int &$tabindex Current tabindex |
380 | * @return bool |
381 | */ |
382 | public function onEditPageBeforeEditButtons( $editor, &$buttons, &$tabindex ) { |
383 | if ( $editor->getTitle()->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) { |
384 | unset( $buttons['preview'] ); |
385 | } |
386 | return true; |
387 | } |
388 | |
389 | /** |
390 | * @param IContextSource $context |
391 | * @param Content $content |
392 | * @param Status $status |
393 | * @param string $summary |
394 | * @param User $user |
395 | * @param bool $minoredit |
396 | * @return bool |
397 | */ |
398 | public function onEditFilterMergedContent( IContextSource $context, Content $content, |
399 | Status $status, $summary, User $user, $minoredit |
400 | ) { |
401 | $title = $context->getTitle(); |
402 | |
403 | if ( !$content instanceof ScribuntoContent ) { |
404 | return true; |
405 | } |
406 | $contentHandlerFactory = MediaWikiServices::getInstance()->getContentHandlerFactory(); |
407 | $contentHandler = $contentHandlerFactory->getContentHandler( $content->getModel() ); |
408 | |
409 | '@phan-var ScribuntoContentHandler $contentHandler'; |
410 | $validateStatus = $contentHandler->validate( $content, $title ); |
411 | if ( $validateStatus->isOK() ) { |
412 | return true; |
413 | } |
414 | |
415 | $status->merge( $validateStatus ); |
416 | |
417 | if ( isset( $validateStatus->value->params['module'] ) ) { |
418 | $module = $validateStatus->value->params['module']; |
419 | $line = $validateStatus->value->params['line']; |
420 | if ( $module === $title->getPrefixedDBkey() && preg_match( '/^\d+$/', $line ) ) { |
421 | $out = $context->getOutput(); |
422 | $out->addInlineScript( 'window.location.hash = ' . Xml::encodeJsVar( "#mw-ce-l$line" ) ); |
423 | } |
424 | } |
425 | if ( !$status->isOK() ) { |
426 | // @todo Remove this line after this extension do not support mediawiki version 1.36 and before |
427 | $status->value = EditPage::AS_HOOK_ERROR_EXPECTED; |
428 | return false; |
429 | } |
430 | |
431 | return true; |
432 | } |
433 | |
434 | /** |
435 | * @param Article $article |
436 | * @param bool|ParserOutput|null &$outputDone |
437 | * @param bool &$pcache |
438 | * @return bool |
439 | */ |
440 | public function onArticleViewHeader( $article, &$outputDone, &$pcache ) { |
441 | $title = $article->getTitle(); |
442 | if ( Scribunto::isDocPage( $title, $forModule ) ) { |
443 | $article->getContext()->getOutput()->addHTML( |
444 | wfMessage( 'scribunto-doc-page-header', $forModule->getPrefixedText() )->parseAsBlock() |
445 | ); |
446 | } |
447 | return true; |
448 | } |
449 | } |