Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.08% covered (warning)
54.08%
179 / 331
35.71% covered (danger)
35.71%
5 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiVisualEditor
54.08% covered (warning)
54.08%
179 / 331
35.71% covered (danger)
35.71%
5 / 14
604.29
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
52.56% covered (warning)
52.56%
123 / 234
0.00% covered (danger)
0.00%
0 / 1
403.79
 makeSafeHtmlForNfc
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isAllowedNamespace
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getAvailableNamespaceIds
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 isAllowedContentType
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 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 MediaWiki\Api\ApiBase;
14use MediaWiki\Api\ApiBlockInfoTrait;
15use MediaWiki\Api\ApiMain;
16use MediaWiki\Api\ApiResult;
17use MediaWiki\Config\Config;
18use MediaWiki\Content\ContentHandler;
19use MediaWiki\Content\Transform\ContentTransformer;
20use MediaWiki\Content\WikitextContent;
21use MediaWiki\Context\DerivativeContext;
22use MediaWiki\Context\RequestContext;
23use MediaWiki\EditPage\EditPage;
24use MediaWiki\EditPage\IntroMessageBuilder;
25use MediaWiki\EditPage\PreloadedContentBuilder;
26use MediaWiki\EditPage\TextboxBuilder;
27use MediaWiki\Language\RawMessage;
28use MediaWiki\Logger\LoggerFactory;
29use MediaWiki\MediaWikiServices;
30use MediaWiki\Page\Article;
31use MediaWiki\Page\PageReference;
32use MediaWiki\Page\WikiPageFactory;
33use MediaWiki\Permissions\PermissionManager;
34use MediaWiki\Registration\ExtensionRegistry;
35use MediaWiki\Request\DerivativeRequest;
36use MediaWiki\Revision\RevisionLookup;
37use MediaWiki\SpecialPage\SpecialPageFactory;
38use MediaWiki\Title\Title;
39use MediaWiki\User\Options\UserOptionsLookup;
40use MediaWiki\User\TempUser\TempUserCreator;
41use MediaWiki\User\User;
42use MediaWiki\User\UserFactory;
43use MediaWiki\User\UserIdentity;
44use MediaWiki\Watchlist\WatchlistManager;
45use MessageLocalizer;
46use Wikimedia\Assert\Assert;
47use Wikimedia\ParamValidator\ParamValidator;
48use Wikimedia\Stats\StatsFactory;
49
50class ApiVisualEditor extends ApiBase {
51    use ApiBlockInfoTrait;
52    use ApiParsoidTrait;
53
54    private RevisionLookup $revisionLookup;
55    private TempUserCreator $tempUserCreator;
56    private UserFactory $userFactory;
57    private UserOptionsLookup $userOptionsLookup;
58    private WatchlistManager $watchlistManager;
59    private ContentTransformer $contentTransformer;
60    private WikiPageFactory $wikiPageFactory;
61    private IntroMessageBuilder $introMessageBuilder;
62    private PreloadedContentBuilder $preloadedContentBuilder;
63    private SpecialPageFactory $specialPageFactory;
64    private VisualEditorParsoidClientFactory $parsoidClientFactory;
65
66    public function __construct(
67        ApiMain $main,
68        string $name,
69        RevisionLookup $revisionLookup,
70        TempUserCreator $tempUserCreator,
71        UserFactory $userFactory,
72        UserOptionsLookup $userOptionsLookup,
73        WatchlistManager $watchlistManager,
74        ContentTransformer $contentTransformer,
75        StatsFactory $statsFactory,
76        WikiPageFactory $wikiPageFactory,
77        IntroMessageBuilder $introMessageBuilder,
78        PreloadedContentBuilder $preloadedContentBuilder,
79        SpecialPageFactory $specialPageFactory,
80        VisualEditorParsoidClientFactory $parsoidClientFactory
81    ) {
82        parent::__construct( $main, $name );
83        $this->setLogger( LoggerFactory::getInstance( 'VisualEditor' ) );
84        $this->setStatsFactory( $statsFactory );
85        $this->revisionLookup = $revisionLookup;
86        $this->tempUserCreator = $tempUserCreator;
87        $this->userFactory = $userFactory;
88        $this->userOptionsLookup = $userOptionsLookup;
89        $this->watchlistManager = $watchlistManager;
90        $this->contentTransformer = $contentTransformer;
91        $this->wikiPageFactory = $wikiPageFactory;
92        $this->introMessageBuilder = $introMessageBuilder;
93        $this->preloadedContentBuilder = $preloadedContentBuilder;
94        $this->specialPageFactory = $specialPageFactory;
95        $this->parsoidClientFactory = $parsoidClientFactory;
96    }
97
98    /**
99     * @inheritDoc
100     */
101    protected function getParsoidClient(): ParsoidClient {
102        return $this->parsoidClientFactory->createParsoidClient(
103            $this->getRequest()->getHeader( 'Cookie' )
104        );
105    }
106
107    /**
108     * @see EditPage::getUserForPermissions
109     */
110    private function getUserForPermissions(): User {
111        $user = $this->getUser();
112        if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
113            return $this->userFactory->newUnsavedTempUser(
114                $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
115            );
116        }
117        return $user;
118    }
119
120    /**
121     * @see ApiParse::getUserForPreview
122     */
123    private function getUserForPreview(): UserIdentity {
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::getEditPermissionStatus()
327                // TODO: Use API
328                // action=query&prop=info&intestactions=edit&intestactionsdetail=full&errorformat=html&errorsuselocal=1
329                $status = $permissionManager->getPermissionStatus(
330                    'edit', $this->getUserForPermissions(), $title, PermissionManager::RIGOR_FULL );
331                if ( !$status->isGood() ) {
332                    // Show generic permission errors, including page protection, user blocks, etc.
333                    $notice = $this->getOutput()->formatPermissionStatus( $status, '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                        $params['paction'] === 'wikitext' ? 'veaction=editsource' : 'veaction=edit',
356                        false,
357                        $section
358                    );
359                }
360
361                // Will be false e.g. if user is blocked or page is protected
362                $canEdit = $status->isGood();
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 #getPermissionStatus 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                // phpcs:disable MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment
439                /** @phpcs-require-sorted-array */
440                $result = [
441                    // --------------------------------------------------------------------------------
442                    // This should match ArticleTarget#getWikitextDataPromiseForDoc and ArticleTarget#storeDocState
443                    // --------------------------------------------------------------------------------
444                    'basetimestamp' => $baseTimestamp,
445                    'blockinfo' => $blockinfo, // only used by MobileFrontend EditorGateway
446                    'canEdit' => $canEdit,
447                    'checkboxesDef' => $checkboxesDef,
448                    'checkboxesMessages' => $checkboxesMessages,
449                    // 'content' => ..., // optional, see below
450                    'copyrightWarning' => $copyrightWarning,
451                    // 'etag' => ..., // optional, see below
452                    'notices' => $notices,
453                    'oldid' => $oldid,
454                    // 'preloaded' => ..., // optional, see below
455                    'protectedClasses' => implode( ' ', $protectedClasses ),
456                    'result' => 'success', // probably unused?
457                    'starttimestamp' => wfTimestampNow(),
458                    'wouldautocreate' => $wouldautocreate,
459                ];
460                // phpcs:enable
461                if ( isset( $restbaseHeaders['etag'] ) ) {
462                    $result['etag'] = $restbaseHeaders['etag'];
463                }
464                if ( isset( $params['badetag'] ) ) {
465                    $badetag = $params['badetag'];
466                    $goodetag = $result['etag'] ?? '';
467                    $this->getLogger()->info(
468                        __METHOD__ . ": Client reported bad ETag: {badetag}, expected: {goodetag}",
469                        [
470                            'badetag' => $badetag,
471                            'goodetag' => $goodetag,
472                        ]
473                    );
474                }
475
476                if ( isset( $content ) ) {
477                    Assert::postcondition( is_string( $content ), 'Content expected' );
478                    $result['content'] = $content;
479                    $result['preloaded'] = $preloaded;
480                }
481                break;
482
483            case 'templatesused':
484                // HACK: Build a fake EditPage so we can get checkboxes from it
485                // Deliberately omitting ,0 so oldid comes from request
486                $article = new Article( $title );
487                $editPage = new EditPage( $article );
488                $result = $editPage->makeTemplatesOnThisPageList( $editPage->getTemplates() );
489                break;
490
491            case 'parsefragment':
492                $wikitext = $params['wikitext'];
493                if ( $wikitext === null ) {
494                    $this->dieWithError( [ 'apierror-missingparam', 'wikitext' ] );
495                }
496                if ( $params['pst'] ) {
497                    $wikitext = $this->pstWikitext( $title, $wikitext );
498                }
499                $content = $this->transformWikitext(
500                    $title, $wikitext, true
501                )['body'];
502                Assert::postcondition( is_string( $content ), 'Content expected' );
503                $result = [
504                    'result' => 'success',
505                    'content' => $content
506                ];
507                break;
508        }
509
510        if (
511            is_array( $result ) &&
512            isset( $result['content'] ) &&
513            is_string( $result['content'] )
514        ) {
515            // Protect content from being corrupted by conversion to Unicode NFC.
516            // Without this, MediaWiki::Api::ApiResult::addValue can break html tags.
517            // See T382756
518            $result['content'] = $this->makeSafeHtmlForNfc( $result['content'] );
519        }
520
521        $this->getResult()->addValue( null, $this->getModuleName(), $result );
522    }
523
524    /**
525     * Protect html-like content from being corrupted by conversion to Unicode NFC.
526     *
527     * Encodes U+0338 COMBINING LONG SOLIDUS OVERLAY as an html numeric character reference.
528     * Otherwise, conversion to Unicode NFC can break html tags by converting
529     * '>' + U+0338 to U+226F (NOT GREATER THAN), and
530     * '<' + U+0338 to U+226E (NOT LESS THAN)
531     *
532     * Note we cannot just search for those two combinations, because sequences of combining
533     * characters can get reordered, e.g. '>' + U+0339 + U+0338 will become U+226F + U+0339.
534     * See https://unicode.org/reports/tr15/
535     *
536     * @param string $html
537     * @return string
538     */
539    public static function makeSafeHtmlForNfc( string $html ) {
540        $html = str_replace( "\u{0338}", '&#x338;', $html );
541        return $html;
542    }
543
544    /**
545     * Check if the configured allowed namespaces include the specified namespace
546     *
547     * @deprecated Since 1.45. Use {@link VisualEditorAvailabilityLookup::isAllowedNamespace} instead
548     * @param Config $config
549     * @param int $namespaceId Namespace ID
550     * @return bool
551     */
552    public static function isAllowedNamespace( Config $config, int $namespaceId ): bool {
553        wfDeprecated( __METHOD__, '1.45' );
554        return in_array( $namespaceId, self::getAvailableNamespaceIds( $config ), true );
555    }
556
557    /**
558     * Get a list of allowed namespace IDs
559     *
560     * @deprecated Since 1.45. Use {@link VisualEditorAvailabilityLookup::getAvailableNamespaceIds} instead
561     * @param Config $config
562     * @return int[]
563     */
564    public static function getAvailableNamespaceIds( Config $config ): array {
565        wfDeprecated( __METHOD__, '1.45' );
566        $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
567        $configuredNamespaces = array_replace(
568            ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorAvailableNamespaces' ),
569            $config->get( 'VisualEditorAvailableNamespaces' )
570        );
571        $normalized = [];
572        foreach ( $configuredNamespaces as $id => $enabled ) {
573            // Convert canonical namespace names to IDs
574            $id = $namespaceInfo->getCanonicalIndex( strtolower( $id ) ) ?? $id;
575            $normalized[$id] = $enabled && $namespaceInfo->exists( $id );
576        }
577        ksort( $normalized );
578        return array_keys( array_filter( $normalized ) );
579    }
580
581    /**
582     * Check if the configured allowed content models include the specified content model
583     *
584     * @deprecated Since 1.45. Use {@link VisualEditorAvailabilityLookup::isAllowedContentType} instead
585     * @param Config $config
586     * @param string $contentModel Content model ID
587     * @return bool
588     */
589    public static function isAllowedContentType( Config $config, string $contentModel ): bool {
590        wfDeprecated( __METHOD__, '1.45' );
591        $availableContentModels = array_merge(
592            ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorAvailableContentModels' ),
593            $config->get( 'VisualEditorAvailableContentModels' )
594        );
595        return (bool)( $availableContentModels[$contentModel] ?? false );
596    }
597
598    /**
599     * @inheritDoc
600     */
601    public function getAllowedParams() {
602        return [
603            'page' => [
604                ParamValidator::PARAM_REQUIRED => true,
605            ],
606            'badetag' => null,
607            'format' => [
608                ParamValidator::PARAM_DEFAULT => 'jsonfm',
609                ParamValidator::PARAM_TYPE => [ 'json', 'jsonfm' ],
610            ],
611            'paction' => [
612                ParamValidator::PARAM_REQUIRED => true,
613                ParamValidator::PARAM_TYPE => [
614                    'parse',
615                    'metadata',
616                    'templatesused',
617                    'wikitext',
618                    'parsefragment',
619                ],
620            ],
621            'wikitext' => [
622                ParamValidator::PARAM_TYPE => 'text',
623                ParamValidator::PARAM_DEFAULT => null,
624            ],
625            'section' => null,
626            'stash' => false,
627            'oldid' => [
628                ParamValidator::PARAM_TYPE => 'integer',
629            ],
630            'editintro' => null,
631            'pst' => false,
632            'preload' => null,
633            'preloadparams' => [
634                ParamValidator::PARAM_ISMULTI => true,
635            ],
636        ];
637    }
638
639    /**
640     * @inheritDoc
641     */
642    public function needsToken() {
643        return false;
644    }
645
646    /**
647     * @inheritDoc
648     */
649    public function isInternal() {
650        return true;
651    }
652
653    /**
654     * @inheritDoc
655     */
656    public function isWriteMode() {
657        return false;
658    }
659}