Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
75.00% |
282 / 376 |
|
48.84% |
21 / 43 |
CRAP | |
0.00% |
0 / 1 |
LuaEngine | |
75.20% |
282 / 375 |
|
48.84% |
21 / 43 |
335.00 | |
0.00% |
0 / 1 |
newAutodetectEngine | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
newInterpreter | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
newModule | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newLuaError | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
destroy | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
load | |
88.24% |
30 / 34 |
|
0.00% |
0 / 1 |
5.04 | |||
registerInterface | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getLuaLibDir | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
normalizeModuleFileName | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getPerformanceCharacteristics | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getInterpreter | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setupCurrentFrames | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
executeModule | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
2.15 | |||
executeFunctionChunk | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getLogBuffer | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
formatHtmlLogs | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
loadLibraryFromFile | |
81.82% |
18 / 22 |
|
0.00% |
0 / 1 |
7.29 | |||
getGeSHiLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCodeEditorLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
runConsole | |
91.89% |
34 / 37 |
|
0.00% |
0 / 1 |
5.01 | |||
checkType | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
checkString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
checkNumber | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
instantiatePHPLibrary | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
5.03 | |||
loadPHPLibrary | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
loadPackage | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
6.05 | |||
getFrameById | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
frameExists | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
newChildFrame | |
69.23% |
9 / 13 |
|
0.00% |
0 / 1 |
4.47 | |||
getFrameTitle | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setTTL | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getExpandedArgument | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
getAllExpandedArguments | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
expandTemplate | |
77.27% |
17 / 22 |
|
0.00% |
0 / 1 |
6.42 | |||
callParserFunction | |
66.67% |
30 / 45 |
|
0.00% |
0 / 1 |
21.26 | |||
preprocess | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
2.00 | |||
incrementExpensiveFunctionCount | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
addWarning | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
isSubsting | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doCachedExpansion | |
75.00% |
12 / 16 |
|
0.00% |
0 / 1 |
6.56 | |||
loadJsonData | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
updateRedirect | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getRedirectTarget | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
makeRedirectContent | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
supportsRedirects | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Scribunto\Engines\LuaCommon; |
4 | |
5 | use Exception; |
6 | use MediaWiki\Extension\Scribunto\Engines\LuaSandbox\LuaSandboxInterpreter; |
7 | use MediaWiki\Extension\Scribunto\Scribunto; |
8 | use MediaWiki\Extension\Scribunto\ScribuntoContent; |
9 | use MediaWiki\Extension\Scribunto\ScribuntoEngineBase; |
10 | use MediaWiki\Extension\Scribunto\ScribuntoException; |
11 | use MediaWiki\Html\Html; |
12 | use MediaWiki\Json\FormatJson; |
13 | use MediaWiki\MediaWikiServices; |
14 | use MediaWiki\Parser\Parser; |
15 | use MediaWiki\Parser\PPFrame; |
16 | use MediaWiki\Title\Title; |
17 | use RuntimeException; |
18 | use Wikimedia\ScopedCallback; |
19 | |
20 | abstract class LuaEngine extends ScribuntoEngineBase { |
21 | /** |
22 | * Libraries to load. See also the 'ScribuntoExternalLibraries' hook. |
23 | * @var array<string,class-string<LibraryBase>> Maps module names to PHP classes or definition arrays |
24 | */ |
25 | protected static $libraryClasses = [ |
26 | 'mw.site' => SiteLibrary::class, |
27 | 'mw.uri' => UriLibrary::class, |
28 | 'mw.ustring' => UstringLibrary::class, |
29 | 'mw.language' => LanguageLibrary::class, |
30 | 'mw.message' => MessageLibrary::class, |
31 | 'mw.title' => TitleLibrary::class, |
32 | 'mw.text' => TextLibrary::class, |
33 | 'mw.html' => HtmlLibrary::class, |
34 | 'mw.hash' => HashLibrary::class, |
35 | ]; |
36 | |
37 | /** |
38 | * Paths for modules that may be loaded from Lua. See also the |
39 | * 'ScribuntoExternalLibraryPaths' hook. |
40 | * @var string[] Paths |
41 | */ |
42 | protected static $libraryPaths = [ |
43 | '.', |
44 | 'luabit', |
45 | 'ustring', |
46 | ]; |
47 | |
48 | /** @var bool */ |
49 | protected $loaded = false; |
50 | |
51 | /** |
52 | * @var LuaInterpreter|null |
53 | */ |
54 | protected $interpreter; |
55 | |
56 | /** |
57 | * @var array |
58 | */ |
59 | protected $mw; |
60 | |
61 | /** |
62 | * @var array<string,?PPFrame> |
63 | */ |
64 | protected $currentFrames = []; |
65 | /** |
66 | * @var array<string,string>|null |
67 | */ |
68 | protected $expandCache = []; |
69 | /** |
70 | * @var array<string,array|class-string<LibraryBase>> |
71 | */ |
72 | protected $availableLibraries = []; |
73 | |
74 | private const MAX_EXPAND_CACHE_SIZE = 100; |
75 | |
76 | /** |
77 | * If luasandbox is installed and usable then use it, |
78 | * otherwise |
79 | * |
80 | * @param array $options |
81 | * @return LuaEngine |
82 | */ |
83 | public static function newAutodetectEngine( array $options ) { |
84 | $engineConf = MediaWikiServices::getInstance()->getMainConfig()->get( 'ScribuntoEngineConf' ); |
85 | $engine = 'luastandalone'; |
86 | try { |
87 | LuaSandboxInterpreter::checkLuaSandboxVersion(); |
88 | $engine = 'luasandbox'; |
89 | } catch ( LuaInterpreterNotFoundError | LuaInterpreterBadVersionError $e ) { |
90 | // pass |
91 | } |
92 | |
93 | unset( $options['factory'] ); |
94 | |
95 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
96 | return Scribunto::newEngine( $options + $engineConf[$engine] ); |
97 | } |
98 | |
99 | /** |
100 | * Create a new interpreter object |
101 | * @return LuaInterpreter |
102 | */ |
103 | abstract protected function newInterpreter(); |
104 | |
105 | /** |
106 | * @param string $text |
107 | * @param string|bool $chunkName |
108 | * @return LuaModule |
109 | */ |
110 | protected function newModule( $text, $chunkName ) { |
111 | return new LuaModule( $this, $text, $chunkName ); |
112 | } |
113 | |
114 | /** |
115 | * @param string $message |
116 | * @param array $params |
117 | * @return LuaError |
118 | */ |
119 | public function newLuaError( $message, $params = [] ) { |
120 | return new LuaError( $message, $this->getDefaultExceptionParams() + $params ); |
121 | } |
122 | |
123 | public function destroy() { |
124 | // Break reference cycles |
125 | $this->interpreter = null; |
126 | $this->mw = []; |
127 | $this->expandCache = null; |
128 | parent::destroy(); |
129 | } |
130 | |
131 | /** |
132 | * Initialise the interpreter and the base environment |
133 | */ |
134 | public function load() { |
135 | if ( $this->loaded ) { |
136 | return; |
137 | } |
138 | $this->loaded = true; |
139 | |
140 | try { |
141 | $this->interpreter = $this->newInterpreter(); |
142 | |
143 | $funcs = [ |
144 | 'loadPackage', |
145 | 'loadPHPLibrary', |
146 | 'frameExists', |
147 | 'newChildFrame', |
148 | 'getExpandedArgument', |
149 | 'getAllExpandedArguments', |
150 | 'expandTemplate', |
151 | 'callParserFunction', |
152 | 'preprocess', |
153 | 'incrementExpensiveFunctionCount', |
154 | 'isSubsting', |
155 | 'getFrameTitle', |
156 | 'setTTL', |
157 | 'addWarning', |
158 | 'loadJsonData', |
159 | ]; |
160 | |
161 | $lib = []; |
162 | foreach ( $funcs as $name ) { |
163 | $lib[$name] = [ $this, $name ]; |
164 | } |
165 | |
166 | $this->registerInterface( 'mwInit.lua', [] ); |
167 | $this->mw = $this->registerInterface( 'mw.lua', $lib, |
168 | [ 'allowEnvFuncs' => $this->options['allowEnvFuncs'] ] ); |
169 | |
170 | $this->availableLibraries = $this->getLibraries( 'lua', self::$libraryClasses ); |
171 | foreach ( $this->availableLibraries as $name => $def ) { |
172 | $this->instantiatePHPLibrary( $name, $def, false ); |
173 | } |
174 | } catch ( Exception $ex ) { |
175 | $this->loaded = false; |
176 | $this->interpreter = null; |
177 | throw $ex; |
178 | } |
179 | } |
180 | |
181 | /** |
182 | * Register a Lua Library |
183 | * |
184 | * This should be called from the library's PHP module's register() method. |
185 | * |
186 | * The value for $interfaceFuncs is used to populate the mw_interface |
187 | * global that is defined when the library's Lua module is loaded. Values |
188 | * must be PHP callables, which will be seen in Lua as functions. |
189 | * |
190 | * @param string $moduleFileName The path to the Lua portion of the library |
191 | * (absolute, or relative to $this->getLuaLibDir()) |
192 | * @param array<string,callable> $interfaceFuncs Populates mw_interface |
193 | * @param array $setupOptions Passed to the modules setupInterface() method. |
194 | * @return array Lua package |
195 | */ |
196 | public function registerInterface( $moduleFileName, $interfaceFuncs, $setupOptions = [] ) { |
197 | $this->interpreter->registerLibrary( 'mw_interface', $interfaceFuncs ); |
198 | $moduleFileName = $this->normalizeModuleFileName( $moduleFileName ); |
199 | $package = $this->loadLibraryFromFile( $moduleFileName ); |
200 | if ( !empty( $package['setupInterface'] ) ) { |
201 | $this->interpreter->callFunction( $package['setupInterface'], $setupOptions ); |
202 | } |
203 | return $package; |
204 | } |
205 | |
206 | /** |
207 | * Return the base path for Lua modules. |
208 | * @return string |
209 | */ |
210 | public function getLuaLibDir() { |
211 | return __DIR__ . '/lualib'; |
212 | } |
213 | |
214 | /** |
215 | * Normalize a lua module to its full path. If path does not look like an |
216 | * absolute path (i.e. begins with DIRECTORY_SEPARATOR or "X:"), prepend |
217 | * getLuaLibDir() |
218 | * |
219 | * @param string $fileName name of the lua module file |
220 | * @return string |
221 | */ |
222 | protected function normalizeModuleFileName( $fileName ) { |
223 | if ( !preg_match( '<^(?:[a-zA-Z]:)?' . preg_quote( DIRECTORY_SEPARATOR ) . '>', $fileName ) ) { |
224 | $fileName = "{$this->getLuaLibDir()}/{$fileName}"; |
225 | } |
226 | return $fileName; |
227 | } |
228 | |
229 | /** |
230 | * Get performance characteristics of the Lua engine/interpreter |
231 | * |
232 | * phpCallsRequireSerialization: boolean |
233 | * whether calls between PHP and Lua functions require (slow) |
234 | * serialization of parameters and return values |
235 | * |
236 | * @return array |
237 | */ |
238 | abstract public function getPerformanceCharacteristics(); |
239 | |
240 | /** |
241 | * Get the current interpreter object |
242 | * @return LuaInterpreter |
243 | */ |
244 | public function getInterpreter() { |
245 | $this->load(); |
246 | return $this->interpreter; |
247 | } |
248 | |
249 | /** |
250 | * Replaces the list of current frames, and return a ScopedCallback that |
251 | * will reset them when it goes out of scope. |
252 | * |
253 | * @param PPFrame|null $frame If null, an empty frame with no parent will be used |
254 | * @return ScopedCallback |
255 | */ |
256 | private function setupCurrentFrames( ?PPFrame $frame = null ) { |
257 | if ( !$frame ) { |
258 | $frame = $this->getParser()->getPreprocessor()->newFrame(); |
259 | } |
260 | |
261 | $oldFrames = $this->currentFrames; |
262 | $oldExpandCache = $this->expandCache; |
263 | $this->currentFrames = [ |
264 | 'current' => $frame, |
265 | 'parent' => $frame->parent ?? null, |
266 | ]; |
267 | $this->expandCache = []; |
268 | |
269 | return new ScopedCallback( function () use ( $oldFrames, $oldExpandCache ) { |
270 | $this->currentFrames = $oldFrames; |
271 | $this->expandCache = $oldExpandCache; |
272 | } ); |
273 | } |
274 | |
275 | /** |
276 | * Execute a module chunk in a new isolated environment, and return the specified function |
277 | * @param mixed $chunk As accepted by LuaInterpreter::callFunction() |
278 | * @param string $functionName |
279 | * @param PPFrame|null $frame |
280 | * @return mixed |
281 | * @throws ScribuntoException |
282 | */ |
283 | public function executeModule( $chunk, $functionName, $frame ) { |
284 | // $resetFrames is a ScopedCallback, so it has a purpose even though it appears unused. |
285 | $resetFrames = $this->setupCurrentFrames( $frame ); |
286 | |
287 | $retval = $this->getInterpreter()->callFunction( |
288 | $this->mw['executeModule'], $chunk, $functionName |
289 | ); |
290 | if ( !$retval[0] ) { |
291 | // If we get here, it means we asked for an element from the table the module returned, |
292 | // but it returned something other than a table. In this case, $retval[1] contains the type |
293 | // of what it did returned, instead of the value we asked for. |
294 | throw $this->newException( |
295 | 'scribunto-lua-notarrayreturn', [ 'args' => [ $retval[1] ] ] |
296 | ); |
297 | } |
298 | return $retval[1]; |
299 | } |
300 | |
301 | /** |
302 | * Execute a module function chunk |
303 | * @param mixed $chunk As accepted by LuaInterpreter::callFunction() |
304 | * @param PPFrame|null $frame |
305 | * @return array |
306 | */ |
307 | public function executeFunctionChunk( $chunk, $frame ) { |
308 | // $resetFrames is a ScopedCallback, so it has a purpose even though it appears unused. |
309 | $resetFrames = $this->setupCurrentFrames( $frame ); |
310 | |
311 | return $this->getInterpreter()->callFunction( |
312 | $this->mw['executeFunction'], |
313 | $chunk ); |
314 | } |
315 | |
316 | /** |
317 | * Get data logged by modules |
318 | * @return string Logged data |
319 | */ |
320 | protected function getLogBuffer() { |
321 | if ( !$this->loaded ) { |
322 | return ''; |
323 | } |
324 | try { |
325 | $log = $this->getInterpreter()->callFunction( $this->mw['getLogBuffer'] ); |
326 | return $log[0]; |
327 | } catch ( ScribuntoException $ex ) { |
328 | // Probably time expired, ignore it. |
329 | return ''; |
330 | } |
331 | } |
332 | |
333 | /** |
334 | * Format the logged data for HTML output |
335 | * @param string $logs Logged data |
336 | * @param bool $localize Whether to localize the message key |
337 | * @return string HTML |
338 | */ |
339 | protected function formatHtmlLogs( $logs, $localize ) { |
340 | $keyMsg = wfMessage( 'scribunto-limitreport-logs' ); |
341 | if ( !$localize ) { |
342 | $keyMsg->inLanguage( 'en' )->useDatabase( false ); |
343 | } |
344 | return Html::openElement( 'tr' ) . |
345 | Html::rawElement( 'th', [ 'colspan' => 2 ], $keyMsg->parse() ) . |
346 | Html::closeElement( 'tr' ) . |
347 | Html::openElement( 'tr' ) . |
348 | Html::openElement( 'td', [ 'colspan' => 2 ] ) . |
349 | Html::openElement( 'div', [ 'class' => 'mw-collapsible mw-collapsed' ] ) . |
350 | Html::element( 'pre', [ 'class' => 'scribunto-limitreport-logs' ], $logs ) . |
351 | Html::closeElement( 'div' ) . |
352 | Html::closeElement( 'td' ) . |
353 | Html::closeElement( 'tr' ); |
354 | } |
355 | |
356 | /** |
357 | * Load a library from the given file and execute it in the base environment. |
358 | * @param string $fileName File name/path to load |
359 | * @return array|null the export list, or null if there isn't one. |
360 | */ |
361 | protected function loadLibraryFromFile( $fileName ) { |
362 | static $cache = null; |
363 | |
364 | $objectcachefactory = MediaWikiServices::getInstance()->getObjectCacheFactory(); |
365 | |
366 | if ( !$cache ) { |
367 | $cache = $objectcachefactory->getLocalServerInstance( CACHE_HASH ); |
368 | } |
369 | |
370 | $mtime = filemtime( $fileName ); |
371 | if ( $mtime === false ) { |
372 | throw new RuntimeException( 'Lua file does not exist: ' . $fileName ); |
373 | } |
374 | |
375 | $cacheKey = $cache->makeGlobalKey( __CLASS__, $fileName ); |
376 | $fileData = $cache->get( $cacheKey ); |
377 | |
378 | $code = false; |
379 | if ( $fileData ) { |
380 | [ $code, $cachedMtime ] = $fileData; |
381 | if ( $cachedMtime < $mtime ) { |
382 | $code = false; |
383 | } |
384 | } |
385 | if ( !$code ) { |
386 | $code = file_get_contents( $fileName ); |
387 | if ( $code === false ) { |
388 | throw new RuntimeException( 'Lua file does not exist: ' . $fileName ); |
389 | } |
390 | $cache->set( $cacheKey, [ $code, $mtime ], 60 * 5 ); |
391 | } |
392 | |
393 | # Prepending an "@" to the chunk name makes Lua think it is a filename |
394 | $module = $this->getInterpreter()->loadString( $code, '@' . basename( $fileName ) ); |
395 | $ret = $this->getInterpreter()->callFunction( $module ); |
396 | return $ret[0] ?? null; |
397 | } |
398 | |
399 | /** @inheritDoc */ |
400 | public function getGeSHiLanguage() { |
401 | return 'lua'; |
402 | } |
403 | |
404 | /** @inheritDoc */ |
405 | public function getCodeEditorLanguage() { |
406 | return 'lua'; |
407 | } |
408 | |
409 | /** @inheritDoc */ |
410 | public function runConsole( array $params ) { |
411 | // $resetFrames is a ScopedCallback, so it has a purpose even though it appears unused. |
412 | $resetFrames = $this->setupCurrentFrames(); |
413 | |
414 | /** |
415 | * TODO: provide some means for giving correct line numbers for errors |
416 | * in console input, and for producing an informative error message |
417 | * if there is an error in prevQuestions. |
418 | * |
419 | * Maybe each console line could be evaluated as a different chunk, |
420 | * apparently that's what lua.c does. |
421 | */ |
422 | $code = "return function (__init, exe)\n" . |
423 | "if not exe then exe = function(...) return true, ... end end\n" . |
424 | "local p = select(2, exe(__init) )\n" . |
425 | "__init, exe = nil, nil\n" . |
426 | "local print = mw.log\n"; |
427 | foreach ( $params['prevQuestions'] as $q ) { |
428 | if ( substr( $q, 0, 1 ) === '=' ) { |
429 | $code .= "print(" . substr( $q, 1 ) . ")"; |
430 | } else { |
431 | $code .= $q; |
432 | } |
433 | $code .= "\n"; |
434 | } |
435 | $code .= "mw.clearLogBuffer()\n"; |
436 | if ( substr( $params['question'], 0, 1 ) === '=' ) { |
437 | // Treat a statement starting with "=" as a return statement, like in lua.c |
438 | $code .= "local ret = mw.allToString(" . substr( $params['question'], 1 ) . ")\n" . |
439 | "return ret, mw.getLogBuffer()\n"; |
440 | } else { |
441 | $code .= $params['question'] . "\n" . |
442 | "return nil, mw.getLogBuffer()\n"; |
443 | } |
444 | $code .= "end\n"; |
445 | |
446 | if ( $params['title']->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) { |
447 | $contentModule = $this->newModule( |
448 | $params['content'], $params['title']->getPrefixedDBkey() ); |
449 | $contentInit = $contentModule->getInitChunk(); |
450 | $contentExe = $this->mw['executeModule']; |
451 | } else { |
452 | $contentInit = $params['content']; |
453 | $contentExe = null; |
454 | } |
455 | |
456 | $consoleModule = $this->newModule( |
457 | $code, |
458 | wfMessage( 'scribunto-console-current-src' )->text() |
459 | ); |
460 | $consoleInit = $consoleModule->getInitChunk(); |
461 | $ret = $this->getInterpreter()->callFunction( $this->mw['executeModule'], $consoleInit, false ); |
462 | $func = $ret[1]; |
463 | $ret = $this->getInterpreter()->callFunction( $func, $contentInit, $contentExe ); |
464 | |
465 | return [ |
466 | 'return' => $ret[0] ?? null, |
467 | 'print' => $ret[1] ?? '', |
468 | ]; |
469 | } |
470 | |
471 | /** |
472 | * Workalike for luaL_checktype() |
473 | * |
474 | * @param string $funcName The Lua function name, for use in error messages |
475 | * @param array $args The argument array |
476 | * @param int $index0 The zero-based argument index |
477 | * @param string|string[] $type The allowed type names as given by gettype() |
478 | * @param string $msgType The type name used in the error message |
479 | * @throws LuaError |
480 | */ |
481 | public function checkType( $funcName, $args, $index0, $type, $msgType ) { |
482 | if ( !is_array( $type ) ) { |
483 | $type = [ $type ]; |
484 | } |
485 | if ( !isset( $args[$index0] ) || !in_array( gettype( $args[$index0] ), $type, true ) ) { |
486 | $index1 = $index0 + 1; |
487 | throw new LuaError( "bad argument #$index1 to '$funcName' ($msgType expected)" ); |
488 | } |
489 | } |
490 | |
491 | /** |
492 | * Workalike for luaL_checkstring() |
493 | * |
494 | * @param string $funcName The Lua function name, for use in error messages |
495 | * @param array $args The argument array |
496 | * @param int $index0 The zero-based argument index |
497 | */ |
498 | public function checkString( $funcName, $args, $index0 ) { |
499 | $this->checkType( $funcName, $args, $index0, 'string', 'string' ); |
500 | } |
501 | |
502 | /** |
503 | * Workalike for luaL_checknumber() |
504 | * |
505 | * @param string $funcName The Lua function name, for use in error messages |
506 | * @param array $args The argument array |
507 | * @param int $index0 The zero-based argument index |
508 | */ |
509 | public function checkNumber( $funcName, $args, $index0 ) { |
510 | $this->checkType( $funcName, $args, $index0, [ 'integer', 'double' ], 'number' ); |
511 | } |
512 | |
513 | /** |
514 | * Instantiate and register a library. |
515 | * @param string $name |
516 | * @param array|class-string<LibraryBase> $def |
517 | * @param bool $loadDeferred |
518 | * @return array|null |
519 | */ |
520 | private function instantiatePHPLibrary( $name, $def, $loadDeferred ) { |
521 | $def = $this->availableLibraries[$name]; |
522 | if ( is_string( $def ) ) { |
523 | /** @var LibraryBase $class */ |
524 | $class = new $def( $this ); |
525 | } else { |
526 | if ( !$loadDeferred && !empty( $def['deferLoad'] ) ) { |
527 | return null; |
528 | } |
529 | if ( isset( $def['class'] ) ) { |
530 | /** @var LibraryBase $class */ |
531 | $class = new $def['class']( $this ); |
532 | } else { |
533 | throw new RuntimeException( "No class for library \"$name\"" ); |
534 | } |
535 | } |
536 | return $class->register(); |
537 | } |
538 | |
539 | /** |
540 | * Handler for the loadPHPLibrary() callback. Register the specified |
541 | * library and return its function table. It's not necessary to cache the |
542 | * function table in the object instance, since there is caching in a |
543 | * wrapper on the Lua side. |
544 | * @internal |
545 | * @param string $name |
546 | * @return array |
547 | */ |
548 | public function loadPHPLibrary( $name ) { |
549 | $args = func_get_args(); |
550 | $this->checkString( 'loadPHPLibrary', $args, 0 ); |
551 | |
552 | $ret = null; |
553 | if ( isset( $this->availableLibraries[$name] ) ) { |
554 | $ret = $this->instantiatePHPLibrary( $name, $this->availableLibraries[$name], true ); |
555 | } |
556 | |
557 | return [ $ret ]; |
558 | } |
559 | |
560 | /** |
561 | * Handler for the loadPackage() callback. Load the specified |
562 | * module and return its chunk. It's not necessary to cache the resulting |
563 | * chunk in the object instance, since there is caching in a wrapper on the |
564 | * Lua side. |
565 | * @internal |
566 | * @param string $name |
567 | * @return array |
568 | */ |
569 | public function loadPackage( $name ) { |
570 | $args = func_get_args(); |
571 | $this->checkString( 'loadPackage', $args, 0 ); |
572 | |
573 | # This is what Lua does for its built-in loaders |
574 | $luaName = str_replace( '.', '/', $name ) . '.lua'; |
575 | $paths = $this->getLibraryPaths( 'lua', self::$libraryPaths ); |
576 | foreach ( $paths as $path ) { |
577 | $fileName = $this->normalizeModuleFileName( "$path/$luaName" ); |
578 | if ( !file_exists( $fileName ) ) { |
579 | continue; |
580 | } |
581 | $code = file_get_contents( $fileName ); |
582 | $init = $this->interpreter->loadString( $code, "@$luaName" ); |
583 | return [ $init ]; |
584 | } |
585 | |
586 | $title = Title::newFromText( $name ); |
587 | if ( !$title || !$title->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) { |
588 | return []; |
589 | } |
590 | |
591 | $module = $this->fetchModuleFromParser( $title ); |
592 | if ( $module ) { |
593 | // @phan-suppress-next-line PhanUndeclaredMethod |
594 | return [ $module->getInitChunk() ]; |
595 | } else { |
596 | return []; |
597 | } |
598 | } |
599 | |
600 | /** |
601 | * Helper function for the implementation of frame methods |
602 | * |
603 | * @param string $frameId |
604 | * @return PPFrame |
605 | * |
606 | * @throws LuaError |
607 | */ |
608 | protected function getFrameById( $frameId ) { |
609 | if ( $frameId === 'empty' ) { |
610 | return $this->getParser()->getPreprocessor()->newFrame(); |
611 | } elseif ( isset( $this->currentFrames[$frameId] ) ) { |
612 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive |
613 | return $this->currentFrames[$frameId]; |
614 | } else { |
615 | throw new LuaError( 'invalid frame ID' ); |
616 | } |
617 | } |
618 | |
619 | /** |
620 | * Handler for frameExists() |
621 | * |
622 | * @internal |
623 | * @param string $frameId |
624 | * @return array |
625 | */ |
626 | public function frameExists( $frameId ) { |
627 | return [ $frameId === 'empty' || isset( $this->currentFrames[$frameId] ) ]; |
628 | } |
629 | |
630 | /** |
631 | * Handler for newChildFrame() |
632 | * |
633 | * @internal |
634 | * @param string $frameId |
635 | * @param string $title |
636 | * @param array $args |
637 | * @return array |
638 | * @throws LuaError |
639 | */ |
640 | public function newChildFrame( $frameId, $title, array $args ) { |
641 | if ( count( $this->currentFrames ) > 100 ) { |
642 | throw new LuaError( 'newChild: too many frames' ); |
643 | } |
644 | |
645 | $frame = $this->getFrameById( $frameId ); |
646 | if ( $title === false ) { |
647 | $title = $frame->getTitle(); |
648 | } else { |
649 | $title = Title::newFromText( $title ); |
650 | if ( !$title ) { |
651 | throw new LuaError( 'newChild: invalid title' ); |
652 | } |
653 | } |
654 | $args = $this->getParser()->getPreprocessor()->newPartNodeArray( $args ); |
655 | $newFrame = $frame->newChild( $args, $title ); |
656 | $newFrameId = 'frame' . count( $this->currentFrames ); |
657 | $this->currentFrames[$newFrameId] = $newFrame; |
658 | return [ $newFrameId ]; |
659 | } |
660 | |
661 | /** |
662 | * Handler for getTitle() |
663 | * |
664 | * @internal |
665 | * @param string $frameId |
666 | * |
667 | * @return array |
668 | */ |
669 | public function getFrameTitle( $frameId ) { |
670 | $frame = $this->getFrameById( $frameId ); |
671 | return [ $frame->getTitle()->getPrefixedText() ]; |
672 | } |
673 | |
674 | /** |
675 | * Handler for setTTL() |
676 | * @internal |
677 | * @param int $ttl |
678 | */ |
679 | public function setTTL( $ttl ) { |
680 | $args = func_get_args(); |
681 | $this->checkNumber( 'setTTL', $args, 0 ); |
682 | |
683 | $frame = $this->getFrameById( 'current' ); |
684 | $frame->setTTL( $ttl ); |
685 | } |
686 | |
687 | /** |
688 | * Handler for getExpandedArgument() |
689 | * @internal |
690 | * @param string $frameId |
691 | * @param string $name |
692 | * @return array |
693 | */ |
694 | public function getExpandedArgument( $frameId, $name ) { |
695 | $args = func_get_args(); |
696 | $this->checkString( 'getExpandedArgument', $args, 0 ); |
697 | |
698 | $frame = $this->getFrameById( $frameId ); |
699 | $this->getInterpreter()->pauseUsageTimer(); |
700 | $result = $frame->getArgument( $name ); |
701 | if ( $result === false ) { |
702 | return []; |
703 | } else { |
704 | return [ $result ]; |
705 | } |
706 | } |
707 | |
708 | /** |
709 | * Handler for getAllExpandedArguments() |
710 | * @internal |
711 | * @param string $frameId |
712 | * @return array |
713 | */ |
714 | public function getAllExpandedArguments( $frameId ) { |
715 | $frame = $this->getFrameById( $frameId ); |
716 | $this->getInterpreter()->pauseUsageTimer(); |
717 | return [ $frame->getArguments() ]; |
718 | } |
719 | |
720 | /** |
721 | * Handler for expandTemplate() |
722 | * @internal |
723 | * @param string $frameId |
724 | * @param string $titleText |
725 | * @param array $args |
726 | * @return array |
727 | * @throws LuaError |
728 | */ |
729 | public function expandTemplate( $frameId, $titleText, $args ) { |
730 | $frame = $this->getFrameById( $frameId ); |
731 | $title = Title::newFromText( $titleText, NS_TEMPLATE ); |
732 | if ( !$title ) { |
733 | throw new LuaError( "expandTemplate: invalid title \"$titleText\"" ); |
734 | } |
735 | |
736 | if ( $frame->depth >= $this->parser->getOptions()->getMaxTemplateDepth() ) { |
737 | throw new LuaError( 'expandTemplate: template depth limit exceeded' ); |
738 | } |
739 | if ( MediaWikiServices::getInstance()->getNamespaceInfo()->isNonincludable( $title->getNamespace() ) ) { |
740 | throw new LuaError( 'expandTemplate: template inclusion denied' ); |
741 | } |
742 | |
743 | [ $dom, $finalTitle ] = $this->parser->getTemplateDom( $title ); |
744 | if ( $dom === false ) { |
745 | throw new LuaError( "expandTemplate: template \"$titleText\" does not exist" ); |
746 | } |
747 | if ( !$frame->loopCheck( $finalTitle ) ) { |
748 | throw new LuaError( 'expandTemplate: template loop detected' ); |
749 | } |
750 | |
751 | $fargs = $this->getParser()->getPreprocessor()->newPartNodeArray( $args ); |
752 | $newFrame = $frame->newChild( $fargs, $finalTitle ); |
753 | $text = $this->doCachedExpansion( $newFrame, $dom, |
754 | [ |
755 | 'frameId' => $frameId, |
756 | 'template' => $finalTitle->getPrefixedDBkey(), |
757 | 'args' => $args |
758 | ] ); |
759 | return [ $text ]; |
760 | } |
761 | |
762 | /** |
763 | * Handler for callParserFunction() |
764 | * @internal |
765 | * @param string $frameId |
766 | * @param string $function |
767 | * @param array $args |
768 | * @throws LuaError |
769 | * @return array |
770 | * @suppress PhanImpossibleCondition |
771 | */ |
772 | public function callParserFunction( $frameId, $function, $args ) { |
773 | $frame = $this->getFrameById( $frameId ); |
774 | |
775 | # Make zero-based, without screwing up named args |
776 | $args = array_merge( [], $args ); |
777 | |
778 | # Sort, since we can't rely on the order coming in from Lua |
779 | uksort( $args, static function ( $a, $b ) { |
780 | if ( is_int( $a ) !== is_int( $b ) ) { |
781 | return is_int( $a ) ? -1 : 1; |
782 | } |
783 | if ( is_int( $a ) ) { |
784 | return $a - $b; |
785 | } |
786 | return strcmp( $a, $b ); |
787 | } ); |
788 | |
789 | # Be user-friendly |
790 | $colonPos = strpos( $function, ':' ); |
791 | if ( $colonPos !== false ) { |
792 | array_unshift( $args, trim( substr( $function, $colonPos + 1 ) ) ); |
793 | $function = substr( $function, 0, $colonPos ); |
794 | } |
795 | if ( !isset( $args[0] ) ) { |
796 | # It's impossible to call a parser function from wikitext without |
797 | # supplying an arg 0. Insist that one be provided via Lua, too. |
798 | throw new LuaError( 'callParserFunction: At least one unnamed parameter ' . |
799 | '(the parameter that comes after the colon in wikitext) ' . |
800 | 'must be provided' |
801 | ); |
802 | } |
803 | |
804 | $result = $this->parser->callParserFunction( $frame, $function, $args ); |
805 | if ( !$result['found'] ) { |
806 | throw new LuaError( "callParserFunction: function \"$function\" was not found" ); |
807 | } |
808 | |
809 | # Set defaults for various flags |
810 | $result += [ |
811 | 'nowiki' => false, |
812 | 'isChildObj' => false, |
813 | 'isLocalObj' => false, |
814 | 'isHTML' => false, |
815 | 'title' => false, |
816 | ]; |
817 | |
818 | $text = $result['text']; |
819 | if ( $result['isChildObj'] ) { |
820 | $fargs = $this->getParser()->getPreprocessor()->newPartNodeArray( $args ); |
821 | $newFrame = $frame->newChild( $fargs, $result['title'] ); |
822 | if ( $result['nowiki'] ) { |
823 | $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG ); |
824 | } else { |
825 | $text = $newFrame->expand( $text ); |
826 | } |
827 | } |
828 | if ( $result['isLocalObj'] && $result['nowiki'] ) { |
829 | $text = $frame->expand( $text, PPFrame::RECOVER_ORIG ); |
830 | $result['isLocalObj'] = false; |
831 | } |
832 | |
833 | # Replace raw HTML by a placeholder |
834 | if ( $result['isHTML'] ) { |
835 | $text = $this->parser->insertStripItem( $text ); |
836 | } elseif ( $result['nowiki'] ) { |
837 | # Escape nowiki-style return values |
838 | $text = wfEscapeWikiText( $text ); |
839 | } |
840 | |
841 | if ( $result['isLocalObj'] ) { |
842 | $text = $frame->expand( $text ); |
843 | } |
844 | |
845 | return [ "$text" ]; |
846 | } |
847 | |
848 | /** |
849 | * Handler for preprocess() |
850 | * @internal |
851 | * @param string $frameId |
852 | * @param string $text |
853 | * @return array |
854 | * @throws LuaError |
855 | */ |
856 | public function preprocess( $frameId, $text ) { |
857 | $args = func_get_args(); |
858 | $this->checkString( 'preprocess', $args, 0 ); |
859 | |
860 | $frame = $this->getFrameById( $frameId ); |
861 | |
862 | if ( !$frame ) { |
863 | throw new LuaError( 'attempt to call mw.preprocess with no frame' ); |
864 | } |
865 | |
866 | // Don't count the time for expanding all the frame arguments against |
867 | // the Lua time limit. |
868 | $this->getInterpreter()->pauseUsageTimer(); |
869 | $frame->getArguments(); |
870 | $this->getInterpreter()->unpauseUsageTimer(); |
871 | |
872 | $text = $this->doCachedExpansion( $frame, $text, |
873 | [ |
874 | 'frameId' => $frameId, |
875 | 'inputText' => $text |
876 | ] ); |
877 | return [ $text ]; |
878 | } |
879 | |
880 | /** |
881 | * Increment the expensive function count, and throw if limit exceeded |
882 | * |
883 | * @internal |
884 | * @throws LuaError |
885 | * @return null |
886 | */ |
887 | public function incrementExpensiveFunctionCount() { |
888 | if ( !$this->getParser()->incrementExpensiveFunctionCount() ) { |
889 | throw new LuaError( "too many expensive function calls" ); |
890 | } |
891 | return null; |
892 | } |
893 | |
894 | /** |
895 | * Adds a warning to be displayed upon preview |
896 | * |
897 | * @internal |
898 | * @param string $text wikitext |
899 | */ |
900 | public function addWarning( $text ) { |
901 | $args = func_get_args(); |
902 | $this->checkString( 'addWarning', $args, 0 ); |
903 | |
904 | // Message localization has to happen on the Lua side |
905 | $this->getParser()->getOutput()->addWarningMsg( |
906 | 'scribunto-lua-warning', |
907 | $text |
908 | ); |
909 | } |
910 | |
911 | /** |
912 | * Return whether the parser is currently substing |
913 | * |
914 | * @internal |
915 | * @return array |
916 | */ |
917 | public function isSubsting() { |
918 | // See Parser::braceSubstitution, OT_WIKI is the switch |
919 | return [ $this->getParser()->getOutputType() === Parser::OT_WIKI ]; |
920 | } |
921 | |
922 | /** |
923 | * @param PPFrame $frame |
924 | * @param string|array $input |
925 | * @param mixed $cacheKey |
926 | * @return string |
927 | */ |
928 | private function doCachedExpansion( $frame, $input, $cacheKey ) { |
929 | $hash = md5( serialize( $cacheKey ) ); |
930 | if ( isset( $this->expandCache[$hash] ) ) { |
931 | return $this->expandCache[$hash]; |
932 | } |
933 | |
934 | if ( is_scalar( $input ) ) { |
935 | $input = str_replace( [ "\r\n", "\r" ], "\n", $input ); |
936 | $dom = $this->parser->getPreprocessor()->preprocessToObj( |
937 | $input, $frame->depth ? Parser::PTD_FOR_INCLUSION : 0 ); |
938 | } else { |
939 | $dom = $input; |
940 | } |
941 | $ret = $frame->expand( $dom ); |
942 | if ( !$frame->isVolatile() ) { |
943 | if ( count( $this->expandCache ) > self::MAX_EXPAND_CACHE_SIZE ) { |
944 | reset( $this->expandCache ); |
945 | $oldHash = key( $this->expandCache ); |
946 | unset( $this->expandCache[$oldHash] ); |
947 | } |
948 | $this->expandCache[$hash] = $ret; |
949 | } |
950 | return $ret; |
951 | } |
952 | |
953 | /** |
954 | * Implements mw.loadJsonData() |
955 | * |
956 | * @param string $title Title text, type-checked in Lua |
957 | * @return string[] |
958 | */ |
959 | public function loadJsonData( $title ) { |
960 | $this->incrementExpensiveFunctionCount(); |
961 | |
962 | $titleObj = Title::newFromText( $title ); |
963 | if ( !$titleObj || !$titleObj->exists() || !$titleObj->hasContentModel( CONTENT_MODEL_JSON ) ) { |
964 | throw new LuaError( |
965 | "bad argument #1 to 'mw.loadJsonData' ('$title' is not a valid JSON page)" |
966 | ); |
967 | } |
968 | |
969 | $parser = $this->getParser(); |
970 | [ $text, $finalTitle ] = $parser->fetchTemplateAndTitle( $titleObj ); |
971 | |
972 | $json = FormatJson::decode( $text, true ); |
973 | if ( is_array( $json ) ) { |
974 | $json = TextLibrary::reindexArrays( $json, false ); |
975 | } |
976 | // We'll throw an error for non-tables on the Lua side |
977 | |
978 | return [ $json ]; |
979 | } |
980 | |
981 | /** |
982 | * @see Content::updateRedirect |
983 | * |
984 | * @param ScribuntoContent $content |
985 | * @param Title $target |
986 | * @return ScribuntoContent |
987 | */ |
988 | public function updateRedirect( ScribuntoContent $content, Title $target ): ScribuntoContent { |
989 | if ( !$content->isRedirect() ) { |
990 | return $content; |
991 | } |
992 | return $this->makeRedirectContent( $target ); |
993 | } |
994 | |
995 | /** |
996 | * @see Content::getRedirectTarget |
997 | * |
998 | * @param ScribuntoContent $content |
999 | * @return Title|null |
1000 | */ |
1001 | public function getRedirectTarget( ScribuntoContent $content ) { |
1002 | $text = $content->getText(); |
1003 | preg_match( '/^return require \[\[(.*?)\]\]/', $text, $matches ); |
1004 | if ( isset( $matches[1] ) ) { |
1005 | $title = Title::newFromText( $matches[1] ); |
1006 | // Can only redirect to other Scribunto pages |
1007 | if ( $title && $title->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) { |
1008 | // Have a title, check that the current content equals what |
1009 | // the redirect content should be |
1010 | if ( $content->equals( $this->makeRedirectContent( $title ) ) ) { |
1011 | return $title; |
1012 | } |
1013 | } |
1014 | } |
1015 | return null; |
1016 | } |
1017 | |
1018 | /** |
1019 | * @see ContentHandler::makeRedirectContent |
1020 | * @param Title $destination |
1021 | * @param string $text |
1022 | * @return ScribuntoContent |
1023 | */ |
1024 | public function makeRedirectContent( Title $destination, $text = '' ) { |
1025 | $targetPage = $destination->getPrefixedText(); |
1026 | $redirectText = "return require [[$targetPage]]"; |
1027 | return new ScribuntoContent( $redirectText ); |
1028 | } |
1029 | |
1030 | /** |
1031 | * @see ContentHandler::supportsRedirects |
1032 | * @return bool true |
1033 | */ |
1034 | public function supportsRedirects(): bool { |
1035 | return true; |
1036 | } |
1037 | } |
1038 | |
1039 | class_alias( LuaEngine::class, 'Scribunto_LuaEngine' ); |