Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
15.07% covered (danger)
15.07%
77 / 511
6.67% covered (danger)
6.67%
2 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
15.07% covered (danger)
15.07%
77 / 511
6.67% covered (danger)
6.67%
2 / 30
15647.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onRegistration
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
72
 getDataForDesktopArticleTargetInitModule
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 loadDiffModules
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 onDifferenceEngineViewHeader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onTextSlotDiffRendererTablePrefix
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 isSupportedEditPage
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
90
 isWikitextAvailable
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 deferredSetUserOption
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 onCustomEditor
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
72
 getEditPageEditor
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 getPreferredEditor
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
72
 getLastEditor
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 onSkinTemplateNavigation__Universal
0.00% covered (danger)
0.00%
0 / 113
0.00% covered (danger)
0.00%
0 / 1
1892
 onEditPage__showEditForm_fields
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onRecentChange_save
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 onSkinEditSectionLinks
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
420
 onOutputPageBodyAttributes
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
42
 onPreferencesFormPreSave
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 onChangeTagsListActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onListDefinedTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onMakeGlobalVariablesScript
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 onResourceLoaderGetConfigVars
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
1 / 1
5
 onResourceLoaderRegisterModules
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 onParserTestGlobals
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onRedirectSpecialArticleRedirectParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onBeforeInitialize
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onUserLoggedIn
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * VisualEditor extension hooks
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2011-2020 VisualEditor Team and others; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\VisualEditor;
12
13use MediaWiki\Actions\ActionEntryPoint;
14use MediaWiki\Auth\Hook\UserLoggedInHook;
15use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
16use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
17use MediaWiki\Config\Config;
18use MediaWiki\Context\IContextSource;
19use MediaWiki\Context\RequestContext;
20use MediaWiki\Deferred\DeferredUpdates;
21use MediaWiki\Diff\Hook\DifferenceEngineViewHeaderHook;
22use MediaWiki\Diff\Hook\TextSlotDiffRendererTablePrefixHook;
23use MediaWiki\EditPage\EditPage;
24use MediaWiki\Extension\VisualEditor\EditCheck\ApiEditCheckReferenceUrl;
25use MediaWiki\Extension\VisualEditor\Services\VisualEditorAvailabilityLookup;
26use MediaWiki\Hook\BeforeInitializeHook;
27use MediaWiki\Hook\CustomEditorHook;
28use MediaWiki\Hook\EditPage__showEditForm_fieldsHook;
29use MediaWiki\Hook\ParserTestGlobalsHook;
30use MediaWiki\Hook\RecentChange_saveHook;
31use MediaWiki\Hook\SkinEditSectionLinksHook;
32use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook;
33use MediaWiki\Html\Html;
34use MediaWiki\HTMLForm\HTMLForm;
35use MediaWiki\Language\Language;
36use MediaWiki\MediaWikiServices;
37use MediaWiki\Output\Hook\BeforePageDisplayHook;
38use MediaWiki\Output\Hook\MakeGlobalVariablesScriptHook;
39use MediaWiki\Output\Hook\OutputPageBodyAttributesHook;
40use MediaWiki\Output\OutputPage;
41use MediaWiki\Page\Article;
42use MediaWiki\Preferences\Hook\GetPreferencesHook;
43use MediaWiki\Preferences\Hook\PreferencesFormPreSaveHook;
44use MediaWiki\RecentChanges\RecentChange;
45use MediaWiki\Registration\ExtensionRegistry;
46use MediaWiki\Request\WebRequest;
47use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
48use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook;
49use MediaWiki\ResourceLoader\ResourceLoader;
50use MediaWiki\Skin\Skin;
51use MediaWiki\Skin\SkinTemplate;
52use MediaWiki\SpecialPage\Hook\RedirectSpecialArticleRedirectParamsHook;
53use MediaWiki\SpecialPage\SpecialPage;
54use MediaWiki\Title\Title;
55use MediaWiki\User\User;
56use MediaWiki\User\UserIdentity;
57use OOUI\ButtonGroupWidget;
58use OOUI\ButtonWidget;
59use TextSlotDiffRenderer;
60
61class Hooks implements
62    TextSlotDiffRendererTablePrefixHook,
63    BeforeInitializeHook,
64    BeforePageDisplayHook,
65    ChangeTagsListActiveHook,
66    CustomEditorHook,
67    DifferenceEngineViewHeaderHook,
68    EditPage__showEditForm_fieldsHook,
69    GetPreferencesHook,
70    ListDefinedTagsHook,
71    MakeGlobalVariablesScriptHook,
72    OutputPageBodyAttributesHook,
73    ParserTestGlobalsHook,
74    PreferencesFormPreSaveHook,
75    RecentChange_saveHook,
76    RedirectSpecialArticleRedirectParamsHook,
77    ResourceLoaderGetConfigVarsHook,
78    ResourceLoaderRegisterModulesHook,
79    SkinEditSectionLinksHook,
80    SkinTemplateNavigation__UniversalHook,
81    UserLoggedInHook
82{
83
84    // Known parameters that VE does not handle
85    // TODO: Other params too?
86    // Known-good parameters: edit, veaction, section, oldid, lintid, preload, preloadparams, editintro
87    // Partially-good: preloadtitle (source-mode only)
88    private const UNSUPPORTED_EDIT_PARAMS = [
89        'undo',
90        'undoafter',
91        // Only for WTE. This parameter is not supported right now, and NWE has a very different design
92        // for previews, so we might not want to support this at all.
93        'preview',
94    ];
95
96    private const TAGS = [
97        'visualeditor',
98        'visualeditor-wikitext',
99        // Edit check
100        'editcheck-references',
101        'editcheck-references-shown',
102        'editcheck-newcontent',
103        'editcheck-newreference',
104        'editcheck-tone',
105        'editcheck-tone-shown',
106        'editcheck-paste-shown',
107        'editsuggestion-seen',
108        'editsuggestion-used',
109        // No longer in active use:
110        'editcheck-references-activated',
111        'editcheck-reference-decline-common-knowledge',
112        'editcheck-reference-decline-irrelevant',
113        'editcheck-reference-decline-uncertain',
114        'editcheck-reference-decline-other',
115        'visualeditor-needcheck',
116        'visualeditor-switched'
117    ];
118
119    public function __construct(
120        private readonly ExtensionRegistry $extensionRegistry,
121        private readonly VisualEditorAvailabilityLookup $visualEditorAvailabilityLookup,
122    ) {
123    }
124
125    /**
126     * Initialise the 'VisualEditorAvailableNamespaces' setting, and add content
127     * namespaces to it. This will run after LocalSettings.php is processed.
128     * Also ensure Parsoid extension is loaded when necessary.
129     */
130    public static function onRegistration(): void {
131        global $wgVisualEditorAvailableNamespaces, $wgContentNamespaces;
132
133        foreach ( $wgContentNamespaces as $contentNamespace ) {
134            if ( !isset( $wgVisualEditorAvailableNamespaces[$contentNamespace] ) ) {
135                $wgVisualEditorAvailableNamespaces[$contentNamespace] = true;
136            }
137        }
138    }
139
140    /**
141     * Adds VisualEditor JS to the output.
142     *
143     * This is attached to the MediaWiki 'BeforePageDisplay' hook.
144     *
145     * @param OutputPage $output The page view.
146     * @param Skin $skin The skin that's going to build the UI.
147     */
148    public function onBeforePageDisplay( $output, $skin ): void {
149        $services = MediaWikiServices::getInstance();
150        $hookRunner = new VisualEditorHookRunner( $services->getHookContainer() );
151        if ( !$hookRunner->onVisualEditorBeforeEditor( $output, $skin ) ) {
152            $output->addJsConfigVars( 'wgVisualEditorDisabledByHook', true );
153            return;
154        }
155        if ( !(
156            $this->extensionRegistry->isLoaded( 'MobileFrontend' ) &&
157            $services->getService( 'MobileFrontend.Context' )
158                ->shouldDisplayMobileView()
159        ) ) {
160            $output->addModules( 'ext.visualEditor.desktopArticleTarget.init' );
161            $output->addModuleStyles( [ 'ext.visualEditor.desktopArticleTarget.noscript' ] );
162        } else {
163            $output->addModules( 'ext.visualEditor.targetLoader' );
164        }
165
166        // add scroll offset js variable to output
167        $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' );
168        $skinsToolbarScrollOffset = $veConfig->get( 'VisualEditorSkinToolbarScrollOffset' );
169        $toolbarScrollOffset = 0;
170        $skinName = $skin->getSkinName();
171        if ( isset( $skinsToolbarScrollOffset[$skinName] ) ) {
172            $toolbarScrollOffset = $skinsToolbarScrollOffset[$skinName];
173        }
174        // T220158: Don't add this unless it's non-default
175        // TODO: Move this to packageFiles as it's not relevant to the HTML request.
176        if ( $toolbarScrollOffset !== 0 ) {
177            $output->addJsConfigVars( 'wgVisualEditorToolbarScrollOffset', $toolbarScrollOffset );
178        }
179
180        $output->addJsConfigVars(
181            'wgEditSubmitButtonLabelPublish',
182            $veConfig->get( 'EditSubmitButtonLabelPublish' )
183        );
184
185        // TODO: Move to EditCheck
186        $isDisambiguation = $output->getTitle()->exists() &&
187            $services->getPageProps()->getProperties( $output->getTitle(), 'disambiguation' ) !== [];
188        $output->addJsConfigVars( 'wgVisualEditorPageIsDisambiguation', $isDisambiguation );
189
190        // Don't index VE edit pages (T319124)
191        if ( $output->getRequest()->getVal( 'veaction' ) ) {
192            $output->setRobotPolicy( 'noindex,nofollow' );
193        }
194    }
195
196    /**
197     * @internal For internal use in extension.json only.
198     */
199    public static function getDataForDesktopArticleTargetInitModule(): array {
200        return [
201            'unsupportedEditParams' => self::UNSUPPORTED_EDIT_PARAMS,
202        ];
203    }
204
205    /**
206     * Load modules required for a diff page
207     *
208     * @param OutputPage $output Output page
209     */
210    private function loadDiffModules( OutputPage $output ) {
211        $output->addModuleStyles( [
212            'ext.visualEditor.diffPage.init.styles',
213            'oojs-ui.styles.icons-accessibility',
214            'oojs-ui.styles.icons-editing-advanced'
215        ] );
216        $output->addModules( 'ext.visualEditor.diffPage.init' );
217        $output->enableOOUI();
218    }
219
220    /** @inheritDoc */
221    public function onDifferenceEngineViewHeader( $differenceEngine ) {
222        // T344596: Must load this module unconditionally. The TextSlotDiffRendererTablePrefix hook
223        // below doesn't run when the diff is e.g. a log entry with no change to the content.
224        $this->loadDiffModules( $differenceEngine->getContext()->getOutput() );
225    }
226
227    /**
228     * Handler for the DifferenceEngineViewHeader hook, to add visual diffs code as configured
229     *
230     * @param TextSlotDiffRenderer $textSlotDiffRenderer
231     * @param IContextSource $context
232     * @param string[] &$parts
233     * @return void
234     */
235    public function onTextSlotDiffRendererTablePrefix(
236        TextSlotDiffRenderer $textSlotDiffRenderer,
237        IContextSource $context,
238        array &$parts
239    ) {
240        $output = $context->getOutput();
241
242        // Return early if not viewing a diff of an allowed type.
243        if ( !$this->visualEditorAvailabilityLookup->isAllowedContentType( $textSlotDiffRenderer->getContentModel() )
244            || $output->getActionName() !== 'view'
245        ) {
246            return;
247        }
248
249        // onDifferenceEngineViewHeader may not run, so load modules here as well for styling (T361775)
250        $this->loadDiffModules( $output );
251
252        $parts['50_ve-init-mw-diffPage-diffMode'] = '<div class="ve-init-mw-diffPage-diffMode">' .
253            // Will be replaced by a ButtonSelectWidget in JS
254            new ButtonGroupWidget( [
255                'items' => [
256                    new ButtonWidget( [
257                        'data' => 'visual',
258                        'icon' => 'eye',
259                        'disabled' => true,
260                        'label' => $output->msg( 'visualeditor-savedialog-review-visual' )->plain()
261                    ] ),
262                    new ButtonWidget( [
263                        'data' => 'source',
264                        'icon' => 'wikiText',
265                        'active' => true,
266                        'label' => $output->msg( 'visualeditor-savedialog-review-wikitext' )->plain()
267                    ] )
268                ]
269            ] ) .
270            '</div>';
271    }
272
273    /**
274     * @param Title $title
275     * @param User $user
276     * @param WebRequest $req
277     * @return bool
278     */
279    private function isSupportedEditPage( Title $title, User $user, WebRequest $req ): bool {
280        if (
281            $req->getVal( 'action' ) !== 'edit' ||
282            !MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan( 'edit', $user, $title )
283        ) {
284            return false;
285        }
286
287        foreach ( self::UNSUPPORTED_EDIT_PARAMS as $param ) {
288            if ( $req->getVal( $param ) !== null ) {
289                return false;
290            }
291        }
292
293        switch ( self::getEditPageEditor( $user, $req ) ) {
294            case 'visualeditor':
295                return $this->visualEditorAvailabilityLookup->isAvailable( $title, $req, $user ) ||
296                    self::isWikitextAvailable( $title, $user );
297            case 'wikitext':
298            default:
299                return self::isWikitextAvailable( $title, $user );
300        }
301    }
302
303    /**
304     * @param Title $title
305     * @param UserIdentity $user
306     * @return bool
307     */
308    private static function isWikitextAvailable( Title $title, UserIdentity $user ): bool {
309        $services = MediaWikiServices::getInstance();
310        $userOptionsLookup = $services->getUserOptionsLookup();
311        return $userOptionsLookup->getOption( $user, 'visualeditor-newwikitext' ) &&
312            $title->getContentModel() === 'wikitext';
313    }
314
315    /**
316     * @param UserIdentity $user
317     * @param string $key
318     * @param string $value
319     */
320    private static function deferredSetUserOption( UserIdentity $user, string $key, string $value ): void {
321        DeferredUpdates::addCallableUpdate( static function () use ( $user, $key, $value ) {
322            $services = MediaWikiServices::getInstance();
323            if ( $services->getReadOnlyMode()->isReadOnly() ) {
324                return;
325            }
326            $userOptionsManager = $services->getUserOptionsManager();
327            $userOptionsManager->setOption( $user, $key, $value );
328            $userOptionsManager->saveOptions( $user );
329        } );
330    }
331
332    /**
333     * Decide whether to bother showing the wikitext editor at all.
334     * If not, we expect the VE initialisation JS to activate.
335     *
336     * @param Article $article The article being viewed.
337     * @param User $user The user-specific settings.
338     * @return bool Whether to show the wikitext editor or not.
339     */
340    public function onCustomEditor( $article, $user ) {
341        $req = $article->getContext()->getRequest();
342        $services = MediaWikiServices::getInstance();
343        $urlUtils = $services->getUrlUtils();
344        $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' );
345
346        if ( $this->extensionRegistry->isLoaded( 'MobileFrontend' ) ) {
347            // If mobilefrontend is involved it can make its own decisions about this
348            $mobFrontContext = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' );
349            if ( $mobFrontContext->shouldDisplayMobileView() ) {
350                return true;
351            }
352        }
353
354        if ( !$this->visualEditorAvailabilityLookup->isEnabledForUser( $user ) ) {
355            return true;
356        }
357
358        $title = $article->getTitle();
359
360        if ( $req->getVal( 'venoscript' ) ) {
361            $req->response()->setCookie( 'VEE', 'wikitext', 0, [ 'prefix' => '' ] );
362            if ( $user->isNamed() ) {
363                self::deferredSetUserOption( $user, 'visualeditor-editor', 'wikitext' );
364            }
365            return true;
366        }
367
368        if ( $this->isSupportedEditPage( $title, $user, $req ) ) {
369            $params = $req->getValues();
370            $params['venoscript'] = '1';
371            $url = wfScript() . '?' . wfArrayToCgi( $params );
372
373            $out = $article->getContext()->getOutput();
374            $titleMsg = $title->exists() ? 'editing' : 'creating';
375            $out->setPageTitleMsg( wfMessage( $titleMsg, $title->getPrefixedText() ) );
376            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Only null for invalid URL, shouldn't happen
377            $out->showPendingTakeover( $url, 'visualeditor-toload', $urlUtils->expand( $url ) );
378
379            $out->setRevisionId( $req->getInt( 'oldid', $article->getRevIdFetched() ) );
380            return false;
381        }
382        return true;
383    }
384
385    /**
386     * @param User $user
387     * @param WebRequest $req
388     * @return string 'wikitext' or 'visual'
389     */
390    private static function getEditPageEditor( User $user, WebRequest $req ): string {
391        $config = MediaWikiServices::getInstance()->getConfigFactory()
392            ->makeConfig( 'visualeditor' );
393        if ( $config->get( 'VisualEditorDisableForAnons' ) && !$user->isRegistered() ) {
394            return 'wikitext';
395        }
396        $isRedLink = $req->getBool( 'redlink' );
397        // On dual-edit-tab wikis, the edit page must mean the user wants wikitext,
398        // unless following a redlink
399        if ( !$config->get( 'VisualEditorUseSingleEditTab' ) && !$isRedLink ) {
400            return 'wikitext';
401        }
402        // Adding a new section is not supported in visual mode
403        if ( $req->getRawVal( 'section' ) === 'new' ) {
404            return 'wikitext';
405        }
406        // Force switched from VE
407        if ( $req->getVal( 'veswitched' ) !== null ) {
408            return 'wikitext';
409        }
410        return self::getPreferredEditor( $user, $req, !$isRedLink );
411    }
412
413    /**
414     * @param User $user
415     * @param WebRequest $req
416     * @param bool $useWikitextInMultiTab
417     * @return string 'wikitext' or 'visual'
418     */
419    public static function getPreferredEditor(
420        User $user, WebRequest $req, bool $useWikitextInMultiTab = false
421    ): string {
422        // VisualEditor shouldn't even call this method when it's disabled, but it is a public API for
423        // other extensions (e.g. DiscussionTools), and the editor preferences might have surprising
424        // values if the user has tried VisualEditor in the past and then disabled it. (T257234)
425        $services = MediaWikiServices::getInstance();
426        /** @var VisualEditorAvailabilityLookup $visualEditorAvailabilityLookup */
427        $visualEditorAvailabilityLookup = $services->get( VisualEditorAvailabilityLookup::SERVICE_NAME );
428        if ( !$visualEditorAvailabilityLookup->isEnabledForUser( $user ) ) {
429            return 'wikitext';
430        }
431
432        $userOptionsLookup = $services->getUserOptionsLookup();
433
434        switch ( $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) ) {
435            case 'prefer-ve':
436                return 'visualeditor';
437            case 'prefer-wt':
438                return 'wikitext';
439            case 'multi-tab':
440                // May have got here by switching from VE
441                // TODO: Make such an action explicitly request wikitext
442                // so we can use getLastEditor here instead.
443                return $useWikitextInMultiTab ?
444                    'wikitext' :
445                    self::getLastEditor( $user, $req );
446            case 'remember-last':
447            default:
448                return self::getLastEditor( $user, $req );
449        }
450    }
451
452    /**
453     * @param User $user
454     * @param WebRequest $req
455     * @return string
456     */
457    private static function getLastEditor( User $user, WebRequest $req ): string {
458        // This logic matches getLastEditor in:
459        // modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js
460        $editor = $req->getCookie( 'VEE', '' );
461        // Set editor to user's preference or site's default (ignore the cookie) if â€¦
462        if (
463            // â€¦ user is logged in,
464            $user->isNamed() ||
465            // â€¦ no cookie is set, or
466            !$editor ||
467            // value is invalid.
468            !( $editor === 'visualeditor' || $editor === 'wikitext' )
469        ) {
470            $services = MediaWikiServices::getInstance();
471            $userOptionsLookup = $services->getUserOptionsLookup();
472            $editor = $userOptionsLookup->getOption( $user, 'visualeditor-editor' );
473        }
474        return $editor;
475    }
476
477    /**
478     * Changes the Edit tab and adds the VisualEditor tab.
479     *
480     * This is attached to the MediaWiki 'SkinTemplateNavigation::Universal' hook.
481     *
482     * @param SkinTemplate $skin The skin template on which the UI is built.
483     * @param array &$links Navigation links.
484     */
485    public function onSkinTemplateNavigation__Universal( $skin, &$links ): void {
486        $services = MediaWikiServices::getInstance();
487        $userOptionsLookup = $services->getUserOptionsLookup();
488        $config = $services->getConfigFactory()
489            ->makeConfig( 'visualeditor' );
490
491        if (
492            $this->extensionRegistry->isLoaded( 'MobileFrontend' ) &&
493            $services->getService( 'MobileFrontend.Context' )->shouldDisplayMobileView()
494        ) {
495            return;
496        }
497
498        // Exit if there's no edit link for whatever reason (e.g. protected page)
499        if ( !isset( $links['views']['edit'] ) ) {
500            return;
501        }
502
503        $hookRunner = new VisualEditorHookRunner( $services->getHookContainer() );
504        if ( !$hookRunner->onVisualEditorBeforeEditor( $skin->getOutput(), $skin ) ) {
505            return;
506        }
507
508        $user = $skin->getUser();
509        if (
510            $config->get( 'VisualEditorUseSingleEditTab' ) &&
511            $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'prefer-wt'
512        ) {
513            return;
514        }
515
516        if (
517            $config->get( 'VisualEditorUseSingleEditTab' ) &&
518            wfTimestampNow() < $config->get( 'VisualEditorSingleEditTabSwitchTimeEnd' ) &&
519            $user->isNamed() &&
520            $this->visualEditorAvailabilityLookup->isEnabledForUser( $user ) &&
521            !$userOptionsLookup->getOption( $user, 'visualeditor-hidetabdialog' ) &&
522            $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last'
523        ) {
524            // Check if the user has made any edits before the SET switch time
525            $dbr = $services->getConnectionProvider()->getReplicaDatabase();
526            $revExists = $dbr->newSelectQueryBuilder()
527                ->from( 'revision' )
528                ->field( '1' )
529                ->where( [
530                    'rev_actor' => $user->getActorId(),
531                    $dbr->expr( 'rev_timestamp', '<', $dbr->timestamp(
532                        $config->get( 'VisualEditorSingleEditTabSwitchTime' )
533                    ) )
534                ] )
535                ->caller( __METHOD__ )
536                ->fetchField();
537            if ( $revExists ) {
538                $links['views']['edit']['class'] .= ' visualeditor-showtabdialog';
539            }
540        }
541
542        // Exit if the user doesn't have VE enabled
543        if (
544            !$this->visualEditorAvailabilityLookup->isEnabledForUser( $user ) ||
545            // T253941: This option does not actually disable the editor, only leaves the tabs/links unchanged
546            ( $config->get( 'VisualEditorDisableForAnons' ) && !$user->isRegistered() )
547        ) {
548            return;
549        }
550
551        $title = $skin->getRelevantTitle();
552        // Don't exit if this page isn't VE-enabled, since we should still
553        // change "Edit" to "Edit source".
554        $isAvailable = $this->visualEditorAvailabilityLookup->isAvailable( $title, $skin->getRequest(), $user );
555
556        $tabMessages = $config->get( 'VisualEditorTabMessages' );
557        // Rebuild the $links['views'] array and inject the VisualEditor tab before or after
558        // the edit tab as appropriate. We have to rebuild the array because PHP doesn't allow
559        // us to splice into the middle of an associative array.
560        $newViews = [];
561        $wikiPageFactory = $services->getWikiPageFactory();
562        if ( $title->inNamespace( NS_SPECIAL ) ) {
563            // @see https://phabricator.wikimedia.org/T376487
564            // The WikiPageFactory call would fatal if $title points to a special page, but sometimes,
565            // as unusual as it might be, a special page *can* call the relevant hooks to add an "edit"
566            // tab to itself. Luckily most special pages are smarter than that.
567            $isRemote = false;
568        } else {
569            $isRemote = !$wikiPageFactory->newFromTitle( $title )->isLocal();
570        }
571
572        $skinHasEditIcons = in_array(
573            $skin->getSkinName(),
574            $this->extensionRegistry->getAttribute( 'VisualEditorIconSkins' )
575        );
576
577        foreach ( $links['views'] as $action => $data ) {
578            if ( $action === 'edit' ) {
579                // Build the VisualEditor tab
580                $existing = $title->exists() || (
581                    $title->inNamespace( NS_MEDIAWIKI ) &&
582                    $title->getDefaultMessageText() !== false
583                );
584                $action = $existing ? 'edit' : 'create';
585                $veParams = $skin->editUrlOptions();
586                // Remove action=edit
587                unset( $veParams['action'] );
588                // Set veaction=edit
589                $veParams['veaction'] = 'edit';
590                $veTabMessage = $tabMessages[$action];
591                $veTabText = $veTabMessage === null ? ( $data['text'] ?? '' ) :
592                    $skin->msg( $veTabMessage )->text();
593                if ( $isRemote ) {
594                    // The following messages can be used here:
595                    // * tooltip-ca-ve-edit-local
596                    // * tooltip-ca-ve-create-local
597                    // The following messages can be generated upstream:
598                    // * accesskey-ca-ve-edit-local
599                    // * accesskey-ca-ve-create-local
600                    $veTooltip = 'ca-ve-' . $action . '-local';
601                } else {
602                    // The following messages can be used here:
603                    // * tooltip-ca-ve-edit
604                    // * tooltip-ca-ve-create
605                    // The following messages can be generated upstream:
606                    // * accesskey-ca-ve-edit
607                    // * accesskey-ca-ve-create
608                    $veTooltip = 'ca-ve-' . $action;
609                }
610                $veTab = [
611                    'href' => $title->getLocalURL( $veParams ),
612                    'text' => $veTabText,
613                    'single-id' => $veTooltip,
614                    'primary' => true,
615                    'icon' => $skinHasEditIcons ? 'edit' : null,
616                    'class' => '',
617                ];
618
619                // Alter the edit tab
620                $editTab = $data;
621                if ( $isRemote ) {
622                    // The following messages can be used here:
623                    // * visualeditor-ca-editlocaldescriptionsource
624                    // * visualeditor-ca-createlocaldescriptionsource
625                    $editTabMessage = $tabMessages[$action . 'localdescriptionsource'];
626                    // The following messages can be used here:
627                    // * tooltip-ca-editsource-local
628                    // * tooltip-ca-createsource-local
629                    // The following messages can be generated upstream:
630                    // * accesskey-ca-editsource-local
631                    // * accesskey-ca-createsource-local
632                    $editTabTooltip = 'ca-' . $action . 'source-local';
633                } else {
634                    // The following messages can be used here:
635                    // * visualeditor-ca-editsource
636                    // * visualeditor-ca-createsource
637                    $editTabMessage = $tabMessages[$action . 'source'];
638                    // The following messages can be used here:
639                    // * tooltip-ca-editsource
640                    // * tooltip-ca-createsource
641                    // The following messages can be generated upstream:
642                    // * accesskey-ca-editsource
643                    // * accesskey-ca-createsource
644                    $editTabTooltip = 'ca-' . $action . 'source';
645                }
646
647                if ( $editTabMessage !== null ) {
648                    $editTab['text'] = $skin->msg( $editTabMessage )->text();
649                    $editTab['single-id'] = $editTabTooltip;
650                }
651
652                $editor = self::getLastEditor( $user, $skin->getRequest() );
653                if (
654                    $isAvailable &&
655                    $config->get( 'VisualEditorUseSingleEditTab' ) &&
656                    (
657                        $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'prefer-ve' ||
658                        (
659                            $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last' &&
660                            $editor === 'visualeditor'
661                        )
662                    )
663                ) {
664                    $editTab['text'] = $veTabText;
665                    $newViews['edit'] = $editTab;
666                } elseif (
667                    $isAvailable &&
668                    (
669                        !$config->get( 'VisualEditorUseSingleEditTab' ) ||
670                        $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'multi-tab'
671                    )
672                ) {
673                    // Change icon
674                    $editTab['icon'] = $skinHasEditIcons ? 'wikiText' : null;
675                    // Inject the VE tab before or after the edit tab
676                    if ( $config->get( 'VisualEditorTabPosition' ) === 'before' ) {
677                        $editTab['class'] .= ' collapsible';
678                        $newViews['ve-edit'] = $veTab;
679                        $newViews['edit'] = $editTab;
680                    } else {
681                        $veTab['class'] .= ' collapsible';
682                        $newViews['edit'] = $editTab;
683                        $newViews['ve-edit'] = $veTab;
684                    }
685                } elseif (
686                    !$config->get( 'VisualEditorUseSingleEditTab' ) ||
687                    !$isAvailable ||
688                    $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'multi-tab' ||
689                    (
690                        $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last' &&
691                        $editor === 'wikitext'
692                    )
693                ) {
694                    // Don't add ve-edit, but do update the edit tab (e.g. "Edit source").
695                    $newViews['edit'] = $editTab;
696                } else {
697                    // This should not happen.
698                }
699            } else {
700                // Just pass through
701                $newViews[$action] = $data;
702            }
703        }
704        $links['views'] = $newViews;
705    }
706
707    /**
708     * Called when the normal wikitext editor is shown.
709     * Inserts a 'veswitched' hidden field if requested by the client
710     *
711     * @param EditPage $editPage The edit page view.
712     * @param OutputPage $output The page view.
713     */
714    public function onEditPage__showEditForm_fields( $editPage, $output ) {
715        $request = $output->getRequest();
716        if ( $request->getBool( 'veswitched' ) ) {
717            $output->addHTML( Html::hidden( 'veswitched', '1' ) );
718        }
719    }
720
721    /**
722     * Called when an edit is saved
723     * Adds 'visualeditor-switched' tag to the edit if requested
724     * Adds whatever tags from static::TAGS are present in the vetags parameter
725     *
726     * @param RecentChange $rc The new RC entry.
727     */
728    public function onRecentChange_save( $rc ) {
729        $request = RequestContext::getMain()->getRequest();
730        if ( $request->getBool( 'veswitched' ) && $rc->getAttribute( 'rc_this_oldid' ) ) {
731            $rc->addTags( 'visualeditor-switched' );
732        }
733
734        $tags = explode( ',', $request->getVal( 'vetags' ) ?? '' );
735        $tags = array_values( array_intersect( $tags, static::TAGS ) );
736        if ( $tags ) {
737            $rc->addTags( $tags );
738        }
739    }
740
741    /**
742     * Changes the section edit links to add a VE edit link.
743     *
744     * This is attached to the MediaWiki 'SkinEditSectionLinks' hook.
745     *
746     * @param Skin $skin Skin being used to render the UI
747     * @param Title $title Title being used for request
748     * @param string $section The name of the section being pointed to.
749     * @param string $tooltip The default tooltip.
750     * @param array &$result All link detail arrays.
751     * @phan-param array{editsection:array{text:string,targetTitle:Title,attribs:array,query:array}} $result
752     * @param Language $lang The user interface language.
753     */
754    public function onSkinEditSectionLinks( $skin, $title, $section,
755        $tooltip, &$result, $lang
756    ) {
757        $services = MediaWikiServices::getInstance();
758        $userOptionsLookup = $services->getUserOptionsLookup();
759        $config = $services->getConfigFactory()
760            ->makeConfig( 'visualeditor' );
761
762        // Exit if we're in parserTests
763        if ( isset( $GLOBALS[ 'wgVisualEditorInParserTests' ] ) ) {
764            return;
765        }
766
767        if (
768            $this->extensionRegistry->isLoaded( 'MobileFrontend' ) &&
769            $services->getService( 'MobileFrontend.Context' )->shouldDisplayMobileView()
770        ) {
771            return;
772        }
773
774        $user = $skin->getUser();
775        // Exit if the user doesn't have VE enabled
776        if (
777            !$this->visualEditorAvailabilityLookup->isEnabledForUser( $user ) ||
778            // T253941: This option does not actually disable the editor, only leaves the tabs/links unchanged
779            ( $config->get( 'VisualEditorDisableForAnons' ) && !$user->isRegistered() )
780        ) {
781            return;
782        }
783
784        // Exit if we're on a foreign file description page
785        if (
786            $title->inNamespace( NS_FILE ) &&
787            !$services->getWikiPageFactory()->newFromTitle( $title )->isLocal()
788        ) {
789            return;
790        }
791
792        $editor = self::getLastEditor( $user, $skin->getRequest() );
793        if (
794            !$config->get( 'VisualEditorUseSingleEditTab' ) ||
795            $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'multi-tab' ||
796            (
797                $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last' &&
798                $editor === 'wikitext'
799            )
800        ) {
801            // Don't add ve-edit, but do update the edit tab (e.g. "Edit source").
802            $tabMessages = $config->get( 'VisualEditorTabMessages' );
803            // The following messages can be used here:
804            // * visualeditor-ca-editsource-section
805            $sourceEditSection = $tabMessages['editsectionsource'];
806            $result['editsection']['text'] = $skin->msg( $sourceEditSection )->inLanguage( $lang )->text();
807            // The following messages can be used here:
808            // * visualeditor-ca-editsource-section-hint
809            $sourceEditSectionHint = $tabMessages['editsectionsourcehint'];
810            $result['editsection']['attribs']['title'] = $skin->msg( $sourceEditSectionHint )
811                ->plaintextParams( $tooltip )
812                ->inLanguage( $lang )->text();
813        }
814
815        // Exit if we're using the single edit tab.
816        if (
817            $config->get( 'VisualEditorUseSingleEditTab' ) &&
818            $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) !== 'multi-tab'
819        ) {
820            return;
821        }
822
823        $skinHasEditIcons = in_array(
824            $skin->getSkinName(),
825            $this->extensionRegistry->getAttribute( 'VisualEditorIconSkins' )
826        );
827
828        // add VE edit section in VE available namespaces
829        if ( $this->visualEditorAvailabilityLookup->isAvailable( $title, $skin->getRequest(), $user ) ) {
830            // The following messages can be used here:
831            // * editsection
832            $veEditSection = $tabMessages['editsection'];
833            // The following messages can be used here:
834            // * editsectionhint
835            $veEditSectionHint = $tabMessages['editsectionhint'];
836
837            $attribs = $result['editsection']['attribs'];
838            // class goes to SkinComponentLink which will accept a string or
839            // an array, and either might be provided at this point.
840            $class = $attribs['class'] ?? '';
841            if ( is_array( $class ) ) {
842                $class[] = 'mw-editsection-visualeditor';
843            } else {
844                $class .= ' mw-editsection-visualeditor';
845            }
846            $attribs['class'] = $class;
847            $attribs['title'] = $skin->msg( $veEditSectionHint )
848                ->plaintextParams( $tooltip )
849                ->inLanguage( $lang )->text();
850
851            $veLink = [
852                'text' => $skin->msg( $veEditSection )->inLanguage( $lang )->text(),
853                'icon' => $skinHasEditIcons ? 'edit' : null,
854                'targetTitle' => $title,
855                'attribs' => $attribs,
856                'query' => [ 'veaction' => 'edit', 'section' => $section ],
857                'options' => [ 'noclasses', 'known' ]
858            ];
859            // Change icon
860            $result['editsection']['icon'] = $skinHasEditIcons ? 'wikiText' : null;
861
862            $result['veeditsection'] = $veLink;
863            if ( $config->get( 'VisualEditorTabPosition' ) === 'before' ) {
864                krsort( $result );
865                // TODO: This will probably cause weird ordering if any other extensions added something
866                // already.
867                // ... wfArrayInsertBefore?
868            }
869        }
870    }
871
872    /**
873     * @param OutputPage $out
874     * @param Skin $sk
875     * @param string[] &$bodyAttrs
876     */
877    public function onOutputPageBodyAttributes( $out, $sk, &$bodyAttrs ): void {
878        $specialTitle = $sk->getTitle();
879
880        // HACK: Replace classes generated by Skin::getPageClasses as if an article title
881        // was passed in, instead of a special page.
882        if ( $specialTitle && $specialTitle->isSpecial( 'CollabPad' ) ) {
883            $articleTitle = Title::newFromText( 'DummyPage' );
884
885            $specialClasses = $sk->getPageClasses( $specialTitle );
886            $articleClasses = $sk->getPageClasses( $articleTitle );
887
888            $bodyAttrs['class'] = str_replace( $specialClasses, $articleClasses, $bodyAttrs['class'] );
889        }
890    }
891
892    /**
893     * Handler for the GetPreferences hook, to add and hide user preferences as configured
894     *
895     * @param User $user
896     * @param array &$preferences Their preferences object
897     */
898    public function onGetPreferences( $user, &$preferences ) {
899        $services = MediaWikiServices::getInstance();
900        $userOptionsLookup = $services->getUserOptionsLookup();
901        $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' );
902        $isBeta = $veConfig->get( 'VisualEditorEnableBetaFeature' );
903
904        // Use the old preference keys to avoid having to migrate data for now.
905        // (One day we might write and run a maintenance script to update the
906        // entries in the database and make this unnecessary.) (T344762)
907        if ( $isBeta ) {
908            $preferences['visualeditor-enable'] = [
909                'type' => 'toggle',
910                'label-message' => 'visualeditor-preference-visualeditor',
911                'section' => 'editing/editor',
912            ];
913        } else {
914            $preferences['visualeditor-betatempdisable'] = [
915                'invert' => true,
916                'type' => 'toggle',
917                'label-message' => 'visualeditor-preference-visualeditor',
918                'section' => 'editing/editor',
919                'default' => $userOptionsLookup->getOption( $user, 'visualeditor-betatempdisable' ) ||
920                    $userOptionsLookup->getOption( $user, 'visualeditor-autodisable' )
921            ];
922        }
923
924        if ( $veConfig->get( 'VisualEditorEnableWikitext' ) ) {
925            $preferences['visualeditor-newwikitext'] = [
926                'type' => 'toggle',
927                'label-message' => 'visualeditor-preference-newwikitexteditor-enable',
928                'help-message' => 'visualeditor-preference-newwikitexteditor-help',
929                'section' => 'editing/editor'
930            ];
931        }
932
933        // Config option for Single Edit Tab
934        if (
935            $veConfig->get( 'VisualEditorUseSingleEditTab' ) &&
936            $this->visualEditorAvailabilityLookup->isEnabledForUser( $user )
937        ) {
938            $preferences['visualeditor-tabs'] = [
939                'type' => 'select',
940                'label-message' => 'visualeditor-preference-tabs',
941                'section' => 'editing/editor',
942                'options-messages' => [
943                    'visualeditor-preference-tabs-remember-last' => 'remember-last',
944                    'visualeditor-preference-tabs-prefer-ve' => 'prefer-ve',
945                    'visualeditor-preference-tabs-prefer-wt' => 'prefer-wt',
946                    'visualeditor-preference-tabs-multi-tab' => 'multi-tab'
947                ]
948            ];
949        }
950
951        $api = [ 'type' => 'api' ];
952        // The "autodisable" preference records whether the user has explicitly opted out of VE.
953        // This is saved even when VE is off by default, which allows changing it to be on by default
954        // without affecting the users who opted out. There's also a maintenance script to silently
955        // opt-out existing users en masse before changing the default, thus only affecting new users.
956        // (This option is no longer set to 'true' anywhere, but we can still encounter old true
957        // values until they are migrated: T344760.)
958        $preferences['visualeditor-autodisable'] = $api;
959        // The diff mode is persisted for each editor mode separately,
960        // e.g. use visual diffs for visual mode only.
961        $preferences['visualeditor-diffmode-source'] = $api;
962        $preferences['visualeditor-diffmode-visual'] = $api;
963        $preferences['visualeditor-diffmode-historical'] = $api;
964        $preferences['visualeditor-editor'] = $api;
965        $preferences['visualeditor-hidebetawelcome'] = $api;
966        $preferences['visualeditor-hidetabdialog'] = $api;
967        $preferences['visualeditor-hidesourceswitchpopup'] = $api;
968        $preferences['visualeditor-hidevisualswitchpopup'] = $api;
969        $preferences['visualeditor-hideusered'] = $api;
970        $preferences['visualeditor-findAndReplace-diacritic'] = $api;
971        $preferences['visualeditor-findAndReplace-findText'] = $api;
972        $preferences['visualeditor-findAndReplace-replaceText'] = $api;
973        $preferences['visualeditor-findAndReplace-regex'] = $api;
974        $preferences['visualeditor-findAndReplace-matchCase'] = $api;
975        $preferences['visualeditor-findAndReplace-word'] = $api;
976        $preferences['visualeditor-symbolList-recentlyUsed-specialCharacters'] = $api;
977    }
978
979    /**
980     * Implements the PreferencesFormPreSave hook, to remove the 'autodisable' flag
981     * when the user it was set on explicitly enables VE.
982     *
983     * @param array $data User-submitted data
984     * @param HTMLForm $form A ContextSource
985     * @param User $user User with new preferences already set
986     * @param bool &$result Success or failure
987     * @param array $oldUserOptions
988     */
989    public function onPreferencesFormPreSave( $data, $form, $user, &$result, $oldUserOptions ) {
990        $services = MediaWikiServices::getInstance();
991        $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' );
992        $userOptionsManager = $services->getUserOptionsManager();
993        $isBeta = $veConfig->get( 'VisualEditorEnableBetaFeature' );
994
995        // The "autodisable" preference records whether the user has explicitly opted out of VE
996        // while it was in beta (which would otherwise not be saved, since it's the same as default).
997
998        if (
999            // When the user enables VE, clear the preference.
1000            $userOptionsManager->getOption( $user, 'visualeditor-autodisable' ) &&
1001            ( $isBeta ?
1002                $userOptionsManager->getOption( $user, 'visualeditor-enable' ) :
1003                !$userOptionsManager->getOption( $user, 'visualeditor-betatempdisable' ) )
1004        ) {
1005            $userOptionsManager->setOption( $user, 'visualeditor-autodisable', false );
1006        }
1007    }
1008
1009    /**
1010     * @param array &$tags
1011     */
1012    public function onChangeTagsListActive( &$tags ) {
1013        $this->onListDefinedTags( $tags );
1014    }
1015
1016    /**
1017     * Implements the ListDefinedTags and ChangeTagsListActive hooks, to
1018     * populate core Special:Tags with the change tags in use by VisualEditor.
1019     *
1020     * @param array &$tags Available change tags.
1021     */
1022    public function onListDefinedTags( &$tags ) {
1023        $tags = array_merge( $tags, static::TAGS );
1024    }
1025
1026    /**
1027     * Adds extra variables to the page config.
1028     *
1029     * @param array &$vars Global variables object
1030     * @param OutputPage $out The page view.
1031     */
1032    public function onMakeGlobalVariablesScript( &$vars, $out ): void {
1033        $pageLanguage = ApiVisualEditor::getPageLanguage( $out->getTitle() );
1034        $converter = MediaWikiServices::getInstance()->getLanguageConverterFactory()
1035            ->getLanguageConverter( $pageLanguage );
1036
1037        $fallbacks = $converter->getVariantFallbacks( $converter->getPreferredVariant() );
1038
1039        $vars['wgVisualEditor'] = [
1040            'pageLanguageCode' => $pageLanguage->getHtmlCode(),
1041            'pageLanguageDir' => $pageLanguage->getDir(),
1042            'pageVariantFallbacks' => $fallbacks,
1043        ];
1044    }
1045
1046    /**
1047     * Adds extra variables to the global config
1048     *
1049     * @param array &$vars Global variables object
1050     * @param string $skin
1051     * @param Config $config
1052     */
1053    public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void {
1054        $coreConfig = RequestContext::getMain()->getConfig();
1055        $services = MediaWikiServices::getInstance();
1056        $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' );
1057
1058        $availableNamespaces = $this->visualEditorAvailabilityLookup->getAvailableNamespaceIds();
1059        sort( $availableNamespaces );
1060
1061        $availableContentModels = array_filter(
1062            array_merge(
1063                $this->extensionRegistry->getAttribute( 'VisualEditorAvailableContentModels' ),
1064                $veConfig->get( 'VisualEditorAvailableContentModels' )
1065            )
1066        );
1067
1068        $namespacesWithSubpages = $coreConfig->get( 'NamespacesWithSubpages' );
1069        // Export as a list of namespaces where subpages are enabled instead of an object
1070        // mapping namespaces to if subpages are enabled or not, so filter out disabled
1071        // namespaces and then just use the keys. See T291729.
1072        $namespacesWithSubpages = array_filter( $namespacesWithSubpages );
1073        $namespacesWithSubpagesEnabled = array_keys( $namespacesWithSubpages );
1074        // $wgNamespacesWithSubpages can include namespaces that don't exist, no need
1075        // to include those in the JavaScript data. See T291727.
1076        // Run this filtering after the filter for subpages being enabled, to reduce
1077        // the number of calls needed to namespace info.
1078        $nsInfo = $services->getNamespaceInfo();
1079        $namespacesWithSubpagesEnabled = array_values( array_filter(
1080            $namespacesWithSubpagesEnabled,
1081            [ $nsInfo, 'exists' ]
1082        ) );
1083
1084        $defaultSortPrefix = $services->getMagicWordFactory()->get( 'defaultsort' )->getSynonym( 0 );
1085        // Sanitize trailing colon. /languages/messages/*.php are not consistent but the
1086        // presence or absence of a trailing colon in the message makes no difference.
1087        $defaultSortPrefix = preg_replace( '/:$/', '', $defaultSortPrefix );
1088
1089        $displayTitlePrefix = $services->getMagicWordFactory()->get( 'displaytitle' )->getSynonym( 0 );
1090        $displayTitlePrefix = preg_replace( '/:$/', '', $displayTitlePrefix );
1091
1092        $vars['wgVisualEditorConfig'] = [
1093            'usePageImages' => $this->extensionRegistry->isLoaded( 'PageImages' ),
1094            'usePageDescriptions' => $this->extensionRegistry->isLoaded( 'WikibaseClient' ),
1095            'namespaces' => $availableNamespaces,
1096            'contentModels' => $availableContentModels,
1097            'pluginModules' => array_merge(
1098                $this->extensionRegistry->getAttribute( 'VisualEditorPluginModules' ),
1099                // @todo deprecate the global setting
1100                $veConfig->get( 'VisualEditorPluginModules' )
1101            ),
1102            'thumbLimits' => $coreConfig->get( 'ThumbLimits' ),
1103            'galleryOptions' => $coreConfig->get( 'GalleryOptions' ),
1104            'editCheckReliabilityAvailable' => ApiEditCheckReferenceUrl::isAvailable(),
1105            'namespacesWithSubpages' => $namespacesWithSubpagesEnabled,
1106            'specialBooksources' => urldecode( SpecialPage::getTitleFor( 'Booksources' )->getPrefixedURL() ),
1107            'cirrusSearchLookup' => $this->extensionRegistry->isLoaded( 'CirrusSearch' ),
1108            'defaultSortPrefix' => $defaultSortPrefix,
1109            'displayTitlePrefix' => $displayTitlePrefix,
1110        ];
1111
1112        // VisualEditor config keys, automatically mapped to config vars:
1113        //   VisualEditorAllowExternalLinkPaste -> allowExternalLinkPaste
1114        $veConfigKeys = [
1115            'VisualEditorDisableForAnons',
1116            'VisualEditorEnableBetaFeature',
1117            'VisualEditorPreloadModules',
1118            'VisualEditorTabPosition',
1119            'VisualEditorTabMessages',
1120            'VisualEditorUseSingleEditTab',
1121            'VisualEditorEnableVisualSectionEditing',
1122            'VisualEditorShowBetaWelcome',
1123            'VisualEditorAllowExternalLinkPaste',
1124            'VisualEditorEnableHelpCompletion',
1125            'VisualEditorEnableWikitext',
1126            'VisualEditorRebaserURL',
1127            'VisualEditorFeedbackAPIURL',
1128            'VisualEditorSuggestionFeedbackAPIURL',
1129            'VisualEditorUseChangeTagging',
1130            'VisualEditorEditCheckTagging',
1131            'VisualEditorEditCheck',
1132            'VisualEditorEditCheckABTest',
1133            'VisualEditorEnableEditCheckExperimental',
1134            'VisualEditorEnableEditCheckSuggestionsBeta',
1135            'VisualEditorFeedbackTitle',
1136            'VisualEditorSourceFeedbackTitle',
1137            'VisualEditorSuggestionFeedbackTitle',
1138            'VisualEditorMobileInsertMenu',
1139        ];
1140
1141        foreach ( $veConfigKeys as $key ) {
1142            $jsKey = lcfirst( preg_replace( '/^VisualEditor/', '', $key ) );
1143            $value = $veConfig->get( $key );
1144            if ( $key === 'VisualEditorTabMessages' ) {
1145                $value = array_filter( $value );
1146            }
1147            $vars['wgVisualEditorConfig'][$jsKey] = $value;
1148        }
1149
1150        // This can be removed and the module added in TemplateData's extension.json
1151        // after the feature flag has been removed. T377976.
1152        if ( $this->extensionRegistry->isLoaded( 'TemplateData' )
1153            && $coreConfig->get( 'TemplateDataEnableDiscovery' )
1154        ) {
1155            $vars['wgVisualEditorConfig']['pluginModules'][] = 'ext.templateData.templateDiscovery';
1156        }
1157    }
1158
1159    /**
1160     * Conditionally register the jquery.uls.data and jquery.i18n modules, in case they've already
1161     * been registered by the UniversalLanguageSelector extension or the TemplateData extension.
1162     *
1163     * @param ResourceLoader $resourceLoader Client-side code and assets to be loaded.
1164     */
1165    public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void {
1166        $veResourceTemplate = [
1167            'localBasePath' => dirname( __DIR__ ),
1168            'remoteExtPath' => 'VisualEditor',
1169        ];
1170
1171        // Only register VisualEditor core's local version of jquery.uls.data if it hasn't been
1172        // installed locally already (presumably, by the UniversalLanguageSelector extension).
1173        if ( !$resourceLoader->isModuleRegistered( 'jquery.uls.data' ) ) {
1174            $resourceLoader->register( [
1175                'jquery.uls.data' => $veResourceTemplate + [
1176                    'scripts' => [
1177                        'lib/ve/lib/jquery.uls/src/jquery.uls.data.js',
1178                        'lib/ve/lib/jquery.uls/src/jquery.uls.data.utils.js',
1179                    ],
1180                ] ] );
1181        }
1182    }
1183
1184    /**
1185     * Ensures that we know whether we're running inside a parser test.
1186     *
1187     * @param array &$settings The settings with which MediaWiki is being run.
1188     */
1189    public function onParserTestGlobals( &$settings ) {
1190        $settings['wgVisualEditorInParserTests'] = true;
1191    }
1192
1193    /**
1194     * @param array &$redirectParams Parameters preserved on special page redirects
1195     *   to wiki pages
1196     */
1197    public function onRedirectSpecialArticleRedirectParams( &$redirectParams ) {
1198        $redirectParams[] = 'veaction';
1199    }
1200
1201    /**
1202     * If the user has specified that they want to edit the page with VE, suppress any redirect.
1203     *
1204     * @param Title $title Title being used for request
1205     * @param Article|null $article The page being viewed.
1206     * @param OutputPage $output The page view.
1207     * @param User $user The user-specific settings.
1208     * @param WebRequest $request
1209     * @param ActionEntryPoint $mediaWiki Helper class.
1210     */
1211    public function onBeforeInitialize(
1212        $title, $article, $output, $user, $request, $mediaWiki
1213    ) {
1214        if ( $request->getVal( 'veaction' ) ) {
1215            $request->setVal( 'redirect', 'no' );
1216        }
1217    }
1218
1219    /**
1220     * On login, if user has a VEE cookie, set their preference equal to it.
1221     *
1222     * @param User $user The user-specific settings.
1223     */
1224    public function onUserLoggedIn( $user ) {
1225        $cookie = RequestContext::getMain()->getRequest()->getCookie( 'VEE', '' );
1226        if ( $user->isNamed() && ( $cookie === 'visualeditor' || $cookie === 'wikitext' ) ) {
1227            self::deferredSetUserOption( $user, 'visualeditor-editor', $cookie );
1228        }
1229    }
1230}