Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.01% covered (warning)
59.01%
190 / 322
53.85% covered (warning)
53.85%
7 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiVisualEditor
59.01% covered (warning)
59.01%
190 / 322
53.85% covered (warning)
53.85%
7 / 13
396.98
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getParsoidClient
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getUserForPermissions
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
2.50
 getUserForPreview
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 pstWikitext
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 execute
51.74% covered (warning)
51.74%
119 / 230
0.00% covered (danger)
0.00%
0 / 1
368.75
 isAllowedNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAvailableNamespaceIds
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 isAllowedContentType
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
1
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Parsoid/RESTBase+MediaWiki API wrapper.
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2011-2021 VisualEditor Team and others; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\VisualEditor;
12
13use ApiBase;
14use ApiBlockInfoTrait;
15use ApiMain;
16use ApiResult;
17use Article;
18use Config;
19use ContentHandler;
20use DerivativeContext;
21use ExtensionRegistry;
22use IBufferingStatsdDataFactory;
23use MediaWiki\Content\Transform\ContentTransformer;
24use MediaWiki\EditPage\EditPage;
25use MediaWiki\EditPage\IntroMessageBuilder;
26use MediaWiki\EditPage\PreloadedContentBuilder;
27use MediaWiki\EditPage\TextboxBuilder;
28use MediaWiki\Language\RawMessage;
29use MediaWiki\Logger\LoggerFactory;
30use MediaWiki\MediaWikiServices;
31use MediaWiki\Page\PageReference;
32use MediaWiki\Page\WikiPageFactory;
33use MediaWiki\Request\DerivativeRequest;
34use MediaWiki\Revision\RevisionLookup;
35use MediaWiki\SpecialPage\SpecialPageFactory;
36use MediaWiki\Title\Title;
37use MediaWiki\User\Options\UserOptionsLookup;
38use MediaWiki\User\TempUser\TempUserCreator;
39use MediaWiki\User\UserFactory;
40use MediaWiki\Watchlist\WatchlistManager;
41use MessageLocalizer;
42use RequestContext;
43use User;
44use Wikimedia\Assert\Assert;
45use Wikimedia\ParamValidator\ParamValidator;
46use WikitextContent;
47
48class ApiVisualEditor extends ApiBase {
49    use ApiBlockInfoTrait;
50    use ApiParsoidTrait;
51
52    private RevisionLookup $revisionLookup;
53    private TempUserCreator $tempUserCreator;
54    private UserFactory $userFactory;
55    private UserOptionsLookup $userOptionsLookup;
56    private WatchlistManager $watchlistManager;
57    private ContentTransformer $contentTransformer;
58    private WikiPageFactory $wikiPageFactory;
59    private IntroMessageBuilder $introMessageBuilder;
60    private PreloadedContentBuilder $preloadedContentBuilder;
61    private SpecialPageFactory $specialPageFactory;
62    private VisualEditorParsoidClientFactory $parsoidClientFactory;
63
64    public function __construct(
65        ApiMain $main,
66        string $name,
67        RevisionLookup $revisionLookup,
68        TempUserCreator $tempUserCreator,
69        UserFactory $userFactory,
70        UserOptionsLookup $userOptionsLookup,
71        WatchlistManager $watchlistManager,
72        ContentTransformer $contentTransformer,
73        IBufferingStatsdDataFactory $statsdDataFactory,
74        WikiPageFactory $wikiPageFactory,
75        IntroMessageBuilder $introMessageBuilder,
76        PreloadedContentBuilder $preloadedContentBuilder,
77        SpecialPageFactory $specialPageFactory,
78        VisualEditorParsoidClientFactory $parsoidClientFactory
79    ) {
80        parent::__construct( $main, $name );
81        $this->setLogger( LoggerFactory::getInstance( 'VisualEditor' ) );
82        $this->setStats( $statsdDataFactory );
83        $this->revisionLookup = $revisionLookup;
84        $this->tempUserCreator = $tempUserCreator;
85        $this->userFactory = $userFactory;
86        $this->userOptionsLookup = $userOptionsLookup;
87        $this->watchlistManager = $watchlistManager;
88        $this->contentTransformer = $contentTransformer;
89        $this->wikiPageFactory = $wikiPageFactory;
90        $this->introMessageBuilder = $introMessageBuilder;
91        $this->preloadedContentBuilder = $preloadedContentBuilder;
92        $this->specialPageFactory = $specialPageFactory;
93        $this->parsoidClientFactory = $parsoidClientFactory;
94    }
95
96    /**
97     * @inheritDoc
98     */
99    protected function getParsoidClient(): ParsoidClient {
100        return $this->parsoidClientFactory->createParsoidClient(
101            $this->getRequest()->getHeader( 'Cookie' )
102        );
103    }
104
105    /**
106     * @see EditPage::getUserForPermissions
107     * @return User
108     */
109    private function getUserForPermissions() {
110        $user = $this->getUser();
111        if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
112            return $this->userFactory->newUnsavedTempUser(
113                $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
114            );
115        }
116        return $user;
117    }
118
119    /**
120     * @see ApiParse::getUserForPreview
121     * @return User
122     */
123    private function getUserForPreview() {
124        $user = $this->getUser();
125        if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
126            return $this->userFactory->newUnsavedTempUser(
127                $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
128            );
129        }
130        return $user;
131    }
132
133    /**
134     * Run wikitext through the parser's Pre-Save-Transform
135     *
136     * @param Title $title The title of the page to use as the parsing context
137     * @param string $wikitext The wikitext to transform
138     * @return string The transformed wikitext
139     */
140    protected function pstWikitext( Title $title, $wikitext ) {
141        $content = ContentHandler::makeContent( $wikitext, $title, CONTENT_MODEL_WIKITEXT );
142        return $this->contentTransformer->preSaveTransform(
143            $content,
144            $title,
145            $this->getUserForPreview(),
146            $this->wikiPageFactory->newFromTitle( $title )->makeParserOptions( $this->getContext() )
147        )
148        ->serialize( 'text/x-wiki' );
149    }
150
151    /**
152     * @inheritDoc
153     * @suppress PhanPossiblyUndeclaredVariable False positives
154     */
155    public function execute() {
156        $user = $this->getUser();
157        $params = $this->extractRequestParams();
158        $permissionManager = $this->getPermissionManager();
159
160        $title = Title::newFromText( $params['page'] );
161        if ( $title && $title->isSpecialPage() ) {
162            // Convert Special:CollabPad/MyPage to MyPage so we can parsefragment properly
163            [ $special, $subPage ] = $this->specialPageFactory->resolveAlias( $title->getDBkey() );
164            if ( $special === 'CollabPad' ) {
165                $title = Title::newFromText( $subPage );
166            }
167        }
168        if ( !$title ) {
169            $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['page'] ) ] );
170        }
171        if ( !$title->canExist() ) {
172            $this->dieWithError( 'apierror-pagecannotexist' );
173        }
174
175        wfDebugLog( 'visualeditor', "called on '$title' with paction: '{$params['paction']}'" );
176        switch ( $params['paction'] ) {
177            case 'parse':
178            case 'wikitext':
179            case 'metadata':
180                // Dirty hack to provide the correct context for FlaggedRevs when it generates edit notices
181                // and save dialog checkboxes. (T307852)
182                // FIXME Don't write to globals! Eww.
183                RequestContext::getMain()->setTitle( $title );
184
185                $preloaded = false;
186                $restbaseHeaders = null;
187
188                $section = $params['section'] ?? null;
189
190                // Get information about current revision
191                if ( $title->exists() ) {
192                    $latestRevision = $this->revisionLookup->getRevisionByTitle( $title );
193                    if ( !$latestRevision ) {
194                        $this->dieWithError(
195                            [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ],
196                            'nosuchrevid'
197                        );
198                    }
199                    if ( isset( $params['oldid'] ) ) {
200                        $revision = $this->revisionLookup->getRevisionById( $params['oldid'] );
201                        if ( !$revision ) {
202                            $this->dieWithError( [ 'apierror-nosuchrevid', $params['oldid'] ] );
203                        }
204                    } else {
205                        $revision = $latestRevision;
206                    }
207
208                    $baseTimestamp = $latestRevision->getTimestamp();
209                    $oldid = $revision->getId();
210
211                    // If requested, request HTML from Parsoid/RESTBase
212                    if ( $params['paction'] === 'parse' ) {
213                        $wikitext = $params['wikitext'] ?? null;
214                        if ( $wikitext !== null ) {
215                            $stash = $params['stash'];
216                            if ( $params['pst'] ) {
217                                $wikitext = $this->pstWikitext( $title, $wikitext );
218                            }
219                            if ( $section !== null ) {
220                                $sectionContent = new WikitextContent( $wikitext );
221                                $page = $this->wikiPageFactory->newFromTitle( $title );
222                                $newSectionContent = $page->replaceSectionAtRev(
223                                    $section, $sectionContent, '', $oldid
224                                );
225                                '@phan-var WikitextContent $newSectionContent';
226                                $wikitext = $newSectionContent->getText();
227                            }
228                            $response = $this->transformWikitext(
229                                $title, $wikitext, false, $oldid, $stash
230                            );
231                        } else {
232                            $response = $this->requestRestbasePageHtml( $revision );
233                        }
234                        $content = $response['body'];
235                        $restbaseHeaders = $response['headers'];
236                    } elseif ( $params['paction'] === 'wikitext' ) {
237                        $apiParams = [
238                            'action' => 'query',
239                            'revids' => $oldid,
240                            'prop' => 'revisions',
241                            'rvprop' => 'content|ids'
242                        ];
243
244                        $apiParams['rvsection'] = $section;
245
246                        $context = new DerivativeContext( $this->getContext() );
247                        $context->setRequest(
248                            new DerivativeRequest(
249                                $context->getRequest(),
250                                $apiParams,
251                                /* was posted? */ true
252                            )
253                        );
254                        $api = new ApiMain(
255                            $context,
256                            /* enable write? */ true
257                        );
258                        $api->execute();
259                        $result = $api->getResult()->getResultData();
260                        $pid = $title->getArticleID();
261                        $content = false;
262                        if ( isset( $result['query']['pages'][$pid]['revisions'] ) ) {
263                            foreach ( $result['query']['pages'][$pid]['revisions'] as $revArr ) {
264                                // Check 'revisions' is an array (T193718)
265                                if ( is_array( $revArr ) && $revArr['revid'] === $oldid ) {
266                                    $content = $revArr['content'];
267                                }
268                            }
269                        }
270                    }
271                } else {
272                    $revision = null;
273                }
274
275                // Use $title as the context page in every processed message (T300184)
276                $localizerWithTitle = new class( $this, $title ) implements MessageLocalizer {
277                    private MessageLocalizer $base;
278                    private PageReference $page;
279
280                    public function __construct( MessageLocalizer $base, PageReference $page ) {
281                        $this->base = $base;
282                        $this->page = $page;
283                    }
284
285                    /**
286                     * @inheritDoc
287                     */
288                    public function msg( $key, ...$params ) {
289                        return $this->base->msg( $key, ...$params )->page( $this->page );
290                    }
291                };
292
293                if ( !$title->exists() || $section === 'new' ) {
294                    if ( isset( $params['wikitext'] ) ) {
295                        $content = $params['wikitext'];
296                        if ( $params['pst'] ) {
297                            $content = $this->pstWikitext( $title, $content );
298                        }
299                    } else {
300                        $contentObj = $this->preloadedContentBuilder->getPreloadedContent(
301                            $title->toPageIdentity(),
302                            $user,
303                            $params['preload'],
304                            $params['preloadparams'] ?? [],
305                            $section
306                        );
307                        $dfltContent = $section === 'new' ? null :
308                            $this->preloadedContentBuilder->getDefaultContent( $title->toPageIdentity() );
309                        $preloaded = $dfltContent ? !$contentObj->equals( $dfltContent ) : !$contentObj->isEmpty();
310                        $content = $contentObj->serialize();
311                    }
312
313                    if ( $content !== '' && $params['paction'] !== 'wikitext' ) {
314                        $response = $this->transformWikitext( $title, $content, false, null, true );
315                        $content = $response['body'];
316                        $restbaseHeaders = $response['headers'];
317                    }
318                    $baseTimestamp = wfTimestampNow();
319                    $oldid = 0;
320                }
321
322                // Look at protection status to set up notices + surface class(es)
323                $builder = new TextboxBuilder();
324                $protectedClasses = $builder->getTextboxProtectionCSSClasses( $title );
325
326                // Simplified EditPage::getEditPermissionErrors()
327                // TODO: Use API
328                // action=query&prop=info&intestactions=edit&intestactionsdetail=full&errorformat=html&errorsuselocal=1
329                $permErrors = $permissionManager->getPermissionErrors(
330                    'edit', $this->getUserForPermissions(), $title, 'full' );
331                if ( $permErrors ) {
332                    // Show generic permission errors, including page protection, user blocks, etc.
333                    $notice = $this->getOutput()->formatPermissionsErrorMessage( $permErrors, 'edit' );
334                    // That method returns wikitext (eww), hack to get it parsed:
335                    $notice = ( new RawMessage( '$1', [ $notice ] ) )->page( $title )->parseAsBlock();
336                    // Invent a message key 'permissions-error' to store in $notices
337                    // (This probably shouldn't use the notices system…)
338                    $notices = [ 'permissions-error' => $notice ];
339                } else {
340                    $notices = $this->introMessageBuilder->getIntroMessages(
341                        IntroMessageBuilder::LESS_FRAMES,
342                        [
343                            // This message was not shown by VisualEditor before it was switched to use
344                            // IntroMessageBuilder, and it may be unexpected to display it now, so skip it.
345                            'editpage-head-copy-warn',
346                            // This message was not shown by VisualEditor previously, and on many Wikipedias it's
347                            // technically non-empty but hidden with CSS, and not a real edit notice (T337633).
348                            'editnotice-notext',
349                        ],
350                        $localizerWithTitle,
351                        $title->toPageIdentity(),
352                        $revision,
353                        $user,
354                        $params['editintro'],
355                        null,
356                        false,
357                        $section
358                    );
359                }
360
361                // Will be false e.g. if user is blocked or page is protected
362                $canEdit = !$permErrors;
363
364                $blockinfo = null;
365                // Blocked user notice
366                if ( $permissionManager->isBlockedFrom( $user, $title, true ) ) {
367                    $block = $user->getBlock();
368                    if ( $block ) {
369                        // Already added to $notices via #getPermissionErrors above.
370                        // Add block info for MobileFrontend:
371                        $blockinfo = $this->getBlockDetails( $block );
372                    }
373                }
374
375                // HACK: Build a fake EditPage so we can get checkboxes from it
376                // Deliberately omitting ,0 so oldid comes from request
377                $article = new Article( $title );
378                $editPage = new EditPage( $article );
379                $req = $this->getRequest();
380                $req->setVal( 'format', $editPage->contentFormat );
381                // By reference for some reason (T54466)
382                $editPage->importFormData( $req );
383                $states = [
384                    'minor' => $this->userOptionsLookup->getOption( $user, 'minordefault' ) && $title->exists(),
385                    'watch' => $this->userOptionsLookup->getOption( $user, 'watchdefault' ) ||
386                        ( $this->userOptionsLookup->getOption( $user, 'watchcreations' ) && !$title->exists() ) ||
387                        $this->watchlistManager->isWatched( $user, $title ),
388                ];
389                $checkboxesDef = $editPage->getCheckboxesDefinition( $states );
390                $checkboxesMessagesList = [];
391                foreach ( $checkboxesDef as &$options ) {
392                    if ( isset( $options['tooltip'] ) ) {
393                        $checkboxesMessagesList[] = "accesskey-{$options['tooltip']}";
394                        $checkboxesMessagesList[] = "tooltip-{$options['tooltip']}";
395                    }
396                    if ( isset( $options['title-message'] ) ) {
397                        $checkboxesMessagesList[] = $options['title-message'];
398                        if ( !is_string( $options['title-message'] ) ) {
399                            // Extract only the key. Any parameters are included in the fake message definition
400                            // passed via $checkboxesMessages. (This changes $checkboxesDef by reference.)
401                            $options['title-message'] = $this->msg( $options['title-message'] )->getKey();
402                        }
403                    }
404                    $checkboxesMessagesList[] = $options['label-message'];
405                    if ( !is_string( $options['label-message'] ) ) {
406                        // Extract only the key. Any parameters are included in the fake message definition
407                        // passed via $checkboxesMessages. (This changes $checkboxesDef by reference.)
408                        $options['label-message'] = $this->msg( $options['label-message'] )->getKey();
409                    }
410                }
411                $checkboxesMessages = [];
412                foreach ( $checkboxesMessagesList as $messageSpecifier ) {
413                    // $messageSpecifier may be a string or a Message object
414                    $message = $this->msg( $messageSpecifier );
415                    $checkboxesMessages[ $message->getKey() ] = $message->plain();
416                }
417
418                foreach ( $checkboxesDef as &$value ) {
419                    // Don't convert the boolean to empty string with formatversion=1
420                    $value[ApiResult::META_BC_BOOLS] = [ 'default' ];
421                }
422
423                $copyrightWarning = EditPage::getCopyrightWarning(
424                    $title,
425                    'parse',
426                    $this
427                );
428
429                // Copied from EditPage::maybeActivateTempUserCreate
430                // Used by code in MobileFrontend and DiscussionTools.
431                // TODO Make them use API
432                // action=query&prop=info&intestactions=edit&intestactionsautocreate=1
433                $wouldautocreate =
434                    !$user->isRegistered()
435                        && $this->tempUserCreator->isAutoCreateAction( 'edit' )
436                        && $permissionManager->userHasRight( $user, 'createaccount' );
437
438                $result = [
439                    'result' => 'success',
440                    'notices' => $notices,
441                    'copyrightWarning' => $copyrightWarning,
442                    'checkboxesDef' => $checkboxesDef,
443                    'checkboxesMessages' => $checkboxesMessages,
444                    'protectedClasses' => implode( ' ', $protectedClasses ),
445                    'basetimestamp' => $baseTimestamp,
446                    'starttimestamp' => wfTimestampNow(),
447                    'oldid' => $oldid,
448                    'blockinfo' => $blockinfo,
449                    'wouldautocreate' => $wouldautocreate,
450                    'canEdit' => $canEdit,
451                ];
452                if ( isset( $restbaseHeaders['etag'] ) ) {
453                    $result['etag'] = $restbaseHeaders['etag'];
454                }
455                if ( isset( $params['badetag'] ) ) {
456                    $badetag = $params['badetag'];
457                    $goodetag = $result['etag'] ?? '';
458                    $this->getLogger()->info(
459                        __METHOD__ . ": Client reported bad ETag: {badetag}, expected: {goodetag}",
460                        [
461                            'badetag' => $badetag,
462                            'goodetag' => $goodetag,
463                        ]
464                    );
465                }
466
467                if ( isset( $content ) ) {
468                    Assert::postcondition( is_string( $content ), 'Content expected' );
469                    $result['content'] = $content;
470                    $result['preloaded'] = $preloaded;
471                }
472                break;
473
474            case 'templatesused':
475                // HACK: Build a fake EditPage so we can get checkboxes from it
476                // Deliberately omitting ,0 so oldid comes from request
477                $article = new Article( $title );
478                $editPage = new EditPage( $article );
479                $result = $editPage->makeTemplatesOnThisPageList( $editPage->getTemplates() );
480                break;
481
482            case 'parsefragment':
483                $wikitext = $params['wikitext'];
484                if ( $wikitext === null ) {
485                    $this->dieWithError( [ 'apierror-missingparam', 'wikitext' ] );
486                }
487                if ( $params['pst'] ) {
488                    $wikitext = $this->pstWikitext( $title, $wikitext );
489                }
490                $content = $this->transformWikitext(
491                    $title, $wikitext, true
492                )['body'];
493                Assert::postcondition( is_string( $content ), 'Content expected' );
494                $result = [
495                    'result' => 'success',
496                    'content' => $content
497                ];
498                break;
499        }
500
501        $this->getResult()->addValue( null, $this->getModuleName(), $result );
502    }
503
504    /**
505     * Check if the configured allowed namespaces include the specified namespace
506     *
507     * @param Config $config
508     * @param int $namespaceId Namespace ID
509     * @return bool
510     */
511    public static function isAllowedNamespace( Config $config, $namespaceId ) {
512        return in_array( $namespaceId, self::getAvailableNamespaceIds( $config ), true );
513    }
514
515    /**
516     * Get a list of allowed namespace IDs
517     *
518     * @param Config $config
519     * @return int[]
520     */
521    public static function getAvailableNamespaceIds( Config $config ) {
522        $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
523        $configuredNamespaces = array_replace(
524            ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorAvailableNamespaces' ),
525            $config->get( 'VisualEditorAvailableNamespaces' )
526        );
527        $normalized = [];
528        foreach ( $configuredNamespaces as $id => $enabled ) {
529            // Convert canonical namespace names to IDs
530            $id = $namespaceInfo->getCanonicalIndex( strtolower( $id ) ) ?? $id;
531            $normalized[$id] = $enabled && $namespaceInfo->exists( $id );
532        }
533        ksort( $normalized );
534        return array_keys( array_filter( $normalized ) );
535    }
536
537    /**
538     * Check if the configured allowed content models include the specified content model
539     *
540     * @param Config $config
541     * @param string $contentModel Content model ID
542     * @return bool
543     */
544    public static function isAllowedContentType( Config $config, $contentModel ) {
545        $availableContentModels = array_merge(
546            ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorAvailableContentModels' ),
547            $config->get( 'VisualEditorAvailableContentModels' )
548        );
549        return (bool)( $availableContentModels[$contentModel] ?? false );
550    }
551
552    /**
553     * @inheritDoc
554     */
555    public function getAllowedParams() {
556        return [
557            'page' => [
558                ParamValidator::PARAM_REQUIRED => true,
559            ],
560            'badetag' => null,
561            'format' => [
562                ParamValidator::PARAM_DEFAULT => 'jsonfm',
563                ParamValidator::PARAM_TYPE => [ 'json', 'jsonfm' ],
564            ],
565            'paction' => [
566                ParamValidator::PARAM_REQUIRED => true,
567                ParamValidator::PARAM_TYPE => [
568                    'parse',
569                    'metadata',
570                    'templatesused',
571                    'wikitext',
572                    'parsefragment',
573                ],
574            ],
575            'wikitext' => [
576                ParamValidator::PARAM_TYPE => 'text',
577                ParamValidator::PARAM_DEFAULT => null,
578            ],
579            'section' => null,
580            'stash' => false,
581            'oldid' => [
582                ParamValidator::PARAM_TYPE => 'integer',
583            ],
584            'editintro' => null,
585            'pst' => false,
586            'preload' => null,
587            'preloadparams' => [
588                ParamValidator::PARAM_ISMULTI => true,
589            ],
590        ];
591    }
592
593    /**
594     * @inheritDoc
595     */
596    public function needsToken() {
597        return false;
598    }
599
600    /**
601     * @inheritDoc
602     */
603    public function isInternal() {
604        return true;
605    }
606
607    /**
608     * @inheritDoc
609     */
610    public function isWriteMode() {
611        return false;
612    }
613}