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