Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiScribuntoConsole
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 7
420
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
156
 runConsole
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 newSession
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 needsToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Scribunto;
4
5use ApiBase;
6use ApiMain;
7use MediaWiki\Html\Html;
8use MediaWiki\Title\Title;
9use ObjectCache;
10use Parser;
11use ParserFactory;
12use ParserOptions;
13use Wikimedia\ParamValidator\ParamValidator;
14
15/**
16 * API module for serving debug console requests on the edit page
17 */
18class ApiScribuntoConsole extends ApiBase {
19    private const SC_MAX_SIZE = 500000;
20    private const SC_SESSION_EXPIRY = 3600;
21    private ParserFactory $parserFactory;
22
23    /**
24     * @param ApiMain $main
25     * @param string $action
26     * @param ParserFactory $parserFactory
27     */
28    public function __construct(
29        ApiMain $main,
30        $action,
31        ParserFactory $parserFactory
32    ) {
33        parent::__construct( $main, $action );
34        $this->parserFactory = $parserFactory;
35    }
36
37    /**
38     * @suppress PhanTypePossiblyInvalidDimOffset
39     */
40    public function execute() {
41        $params = $this->extractRequestParams();
42
43        $title = Title::newFromText( $params['title'] );
44        if ( !$title ) {
45            $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
46        }
47
48        if ( $params['session'] ) {
49            $sessionId = $params['session'];
50        } else {
51            $sessionId = mt_rand( 0, 0x7fffffff );
52        }
53
54        $cache = ObjectCache::getInstance( CACHE_ANYTHING );
55        $sessionKey = $cache->makeKey( 'scribunto-console', $this->getUser()->getId(), $sessionId );
56        $session = null;
57        $sessionIsNew = false;
58        if ( $params['session'] ) {
59            $session = $cache->get( $sessionKey );
60        }
61        if ( !isset( $session['version'] ) ) {
62            $session = $this->newSession();
63            $sessionIsNew = true;
64        }
65
66        // Create a variable holding the session which will be stored if there
67        // are no errors. If there are errors, we don't want to store the current
68        // question to the state builder array, since that will cause subsequent
69        // requests to fail.
70        $newSession = $session;
71
72        if ( !empty( $params['clear'] ) ) {
73            $newSession['size'] -= strlen( implode( '', $newSession['questions'] ) );
74            $newSession['questions'] = [];
75            $session['questions'] = [];
76        }
77        if ( strlen( $params['question'] ) ) {
78            $newSession['size'] += strlen( $params['question'] );
79            $newSession['questions'][] = $params['question'];
80        }
81        if ( $params['content'] ) {
82            $newSession['size'] += strlen( $params['content'] ) - strlen( $newSession['content'] );
83            $newSession['content'] = $params['content'];
84        }
85
86        if ( $newSession['size'] > self::SC_MAX_SIZE ) {
87            $this->dieWithError( 'scribunto-console-too-large' );
88        }
89        $result = $this->runConsole( [
90            'title' => $title,
91            'content' => $newSession['content'],
92            'prevQuestions' => $session['questions'],
93            'question' => $params['question'],
94        ] );
95
96        if ( $result['type'] === 'error' ) {
97            // Restore the questions array
98            $newSession['questions'] = $session['questions'];
99        }
100        $cache->set( $sessionKey, $newSession, self::SC_SESSION_EXPIRY );
101        $result['session'] = $sessionId;
102        $result['sessionSize'] = $newSession['size'];
103        $result['sessionMaxSize'] = self::SC_MAX_SIZE;
104        if ( $sessionIsNew ) {
105            $result['sessionIsNew'] = '';
106        }
107        foreach ( $result as $key => $value ) {
108            $this->getResult()->addValue( null, $key, $value );
109        }
110    }
111
112    /**
113     * Execute the console
114     * @param array $params
115     *  - 'title': (Title) Module being processed
116     *  - 'content': (string) New module text
117     *  - 'prevQuestions': (string[]) Previous values for 'question' in this session.
118     *  - 'question': (string) Lua code to run.
119     * @return array Result data
120     */
121    protected function runConsole( array $params ) {
122        $parser = $this->parserFactory->getInstance();
123        $options = new ParserOptions( $this->getUser() );
124        $parser->startExternalParse( $params['title'], $options, Parser::OT_HTML, true );
125        $engine = Scribunto::getParserEngine( $parser );
126        try {
127            $result = $engine->runConsole( $params );
128        } catch ( ScribuntoException $e ) {
129            $trace = $e->getScriptTraceHtml();
130            $message = $e->getMessage();
131            $html = Html::element( 'p', [], $message );
132            if ( $trace !== false ) {
133                $html .= Html::element( 'p',
134                    [],
135                    $this->msg( 'scribunto-common-backtrace' )->inContentLanguage()->text()
136                ) . $trace;
137            }
138
139            return [
140                'type' => 'error',
141                'html' => $html,
142                'message' => $message,
143                'messagename' => $e->getMessageName() ];
144        }
145        return [
146            'type' => 'normal',
147            'print' => strval( $result['print'] ),
148            'return' => strval( $result['return'] )
149        ];
150    }
151
152    /**
153     * @return array
154     */
155    protected function newSession() {
156        return [
157            'content' => '',
158            'questions' => [],
159            'size' => 0,
160            'version' => 1,
161        ];
162    }
163
164    /** @inheritDoc */
165    public function needsToken() {
166        return 'csrf';
167    }
168
169    /** @inheritDoc */
170    public function isInternal() {
171        return true;
172    }
173
174    /** @inheritDoc */
175    public function getAllowedParams() {
176        return [
177            'title' => [
178                ParamValidator::PARAM_TYPE => 'string',
179            ],
180            'content' => [
181                ParamValidator::PARAM_TYPE => 'text'
182            ],
183            'session' => [
184                ParamValidator::PARAM_TYPE => 'integer',
185            ],
186            'question' => [
187                ParamValidator::PARAM_TYPE => 'text',
188                ParamValidator::PARAM_REQUIRED => true,
189            ],
190            'clear' => [
191                ParamValidator::PARAM_TYPE => 'boolean',
192            ],
193        ];
194    }
195}