Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
11.28% covered (danger)
11.28%
59 / 523
3.12% covered (danger)
3.12%
1 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
11.28% covered (danger)
11.28%
59 / 523
3.12% covered (danger)
3.12%
1 / 32
18945.77
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 / 27
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 MediaWiki\Actions\ActionEntryPoint;
15use MediaWiki\Auth\Hook\UserLoggedInHook;
16use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
17use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
18use MediaWiki\Config\Config;
19use MediaWiki\Context\IContextSource;
20use MediaWiki\Context\RequestContext;
21use MediaWiki\Deferred\DeferredUpdates;
22use MediaWiki\Diff\Hook\DifferenceEngineViewHeaderHook;
23use MediaWiki\Diff\Hook\TextSlotDiffRendererTablePrefixHook;
24use MediaWiki\EditPage\EditPage;
25use MediaWiki\Extension\VisualEditor\EditCheck\ApiEditCheckReferenceUrl;
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\Preferences\Hook\GetPreferencesHook;
42use MediaWiki\Preferences\Hook\PreferencesFormPreSaveHook;
43use MediaWiki\Registration\ExtensionRegistry;
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        $urlUtils = $services->getUrlUtils();
381        $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' );
382
383        if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) ) {
384            // If mobilefrontend is involved it can make its own decisions about this
385            $mobFrontContext = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' );
386            if ( $mobFrontContext->shouldDisplayMobileView() ) {
387                return true;
388            }
389        }
390
391        if ( !self::enabledForUser( $user ) ) {
392            return true;
393        }
394
395        $title = $article->getTitle();
396
397        if ( $req->getVal( 'venoscript' ) ) {
398            $req->response()->setCookie( 'VEE', 'wikitext', 0, [ 'prefix' => '' ] );
399            if ( $user->isNamed() ) {
400                self::deferredSetUserOption( $user, 'visualeditor-editor', 'wikitext' );
401            }
402            return true;
403        }
404
405        if ( self::isSupportedEditPage( $title, $user, $req ) ) {
406            $params = $req->getValues();
407            $params['venoscript'] = '1';
408            $url = wfScript() . '?' . wfArrayToCgi( $params );
409
410            $out = $article->getContext()->getOutput();
411            $titleMsg = $title->exists() ? 'editing' : 'creating';
412            $out->setPageTitleMsg( wfMessage( $titleMsg, $title->getPrefixedText() ) );
413            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Only null for invalid URL, shouldn't happen
414            $out->showPendingTakeover( $url, 'visualeditor-toload', $urlUtils->expand( $url ) );
415
416            $out->setRevisionId( $req->getInt( 'oldid', $article->getRevIdFetched() ) );
417            return false;
418        }
419        return true;
420    }
421
422    /**
423     * @param User $user
424     * @param WebRequest $req
425     * @return string 'wikitext' or 'visual'
426     */
427    private static function getEditPageEditor( User $user, WebRequest $req ): string {
428        $config = MediaWikiServices::getInstance()->getConfigFactory()
429            ->makeConfig( 'visualeditor' );
430        if ( $config->get( 'VisualEditorDisableForAnons' ) && !$user->isRegistered() ) {
431            return 'wikitext';
432        }
433        $isRedLink = $req->getBool( 'redlink' );
434        // On dual-edit-tab wikis, the edit page must mean the user wants wikitext,
435        // unless following a redlink
436        if ( !$config->get( 'VisualEditorUseSingleEditTab' ) && !$isRedLink ) {
437            return 'wikitext';
438        }
439        return self::getPreferredEditor( $user, $req, !$isRedLink );
440    }
441
442    /**
443     * @param User $user
444     * @param WebRequest $req
445     * @param bool $useWikitextInMultiTab
446     * @return string 'wikitext' or 'visual'
447     */
448    public static function getPreferredEditor(
449        User $user, WebRequest $req, bool $useWikitextInMultiTab = false
450    ): string {
451        // VisualEditor shouldn't even call this method when it's disabled, but it is a public API for
452        // other extensions (e.g. DiscussionTools), and the editor preferences might have surprising
453        // values if the user has tried VisualEditor in the past and then disabled it. (T257234)
454        if ( !self::enabledForUser( $user ) ) {
455            return 'wikitext';
456        }
457
458        $services = MediaWikiServices::getInstance();
459        $userOptionsLookup = $services->getUserOptionsLookup();
460
461        switch ( $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) ) {
462            case 'prefer-ve':
463                return 'visualeditor';
464            case 'prefer-wt':
465                return 'wikitext';
466            case 'multi-tab':
467                // May have got here by switching from VE
468                // TODO: Make such an action explicitly request wikitext
469                // so we can use getLastEditor here instead.
470                return $useWikitextInMultiTab ?
471                    'wikitext' :
472                    self::getLastEditor( $user, $req );
473            case 'remember-last':
474            default:
475                return self::getLastEditor( $user, $req );
476        }
477    }
478
479    /**
480     * @param User $user
481     * @param WebRequest $req
482     * @return string
483     */
484    private static function getLastEditor( User $user, WebRequest $req ): string {
485        // This logic matches getLastEditor in:
486        // modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js
487        $editor = $req->getCookie( 'VEE', '' );
488        // Set editor to user's preference or site's default (ignore the cookie) if â€¦
489        if (
490            // â€¦ user is logged in,
491            $user->isNamed() ||
492            // â€¦ no cookie is set, or
493            !$editor ||
494            // value is invalid.
495            !( $editor === 'visualeditor' || $editor === 'wikitext' )
496        ) {
497            $services = MediaWikiServices::getInstance();
498            $userOptionsLookup = $services->getUserOptionsLookup();
499            $editor = $userOptionsLookup->getOption( $user, 'visualeditor-editor' );
500        }
501        return $editor;
502    }
503
504    /**
505     * Changes the Edit tab and adds the VisualEditor tab.
506     *
507     * This is attached to the MediaWiki 'SkinTemplateNavigation::Universal' hook.
508     *
509     * @param SkinTemplate $skin The skin template on which the UI is built.
510     * @param array &$links Navigation links.
511     */
512    public function onSkinTemplateNavigation__Universal( $skin, &$links ): void {
513        $services = MediaWikiServices::getInstance();
514        $userOptionsLookup = $services->getUserOptionsLookup();
515        $config = $services->getConfigFactory()
516            ->makeConfig( 'visualeditor' );
517
518        self::onSkinTemplateNavigationSpecialPage( $skin, $links );
519
520        if (
521            ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) &&
522            $services->getService( 'MobileFrontend.Context' )->shouldDisplayMobileView()
523        ) {
524            return;
525        }
526
527        // Exit if there's no edit link for whatever reason (e.g. protected page)
528        if ( !isset( $links['views']['edit'] ) ) {
529            return;
530        }
531
532        $hookRunner = new VisualEditorHookRunner( $services->getHookContainer() );
533        if ( !$hookRunner->onVisualEditorBeforeEditor( $skin->getOutput(), $skin ) ) {
534            return;
535        }
536
537        $user = $skin->getUser();
538        if (
539            $config->get( 'VisualEditorUseSingleEditTab' ) &&
540            $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'prefer-wt'
541        ) {
542            return;
543        }
544
545        if (
546            $config->get( 'VisualEditorUseSingleEditTab' ) &&
547            wfTimestampNow() < $config->get( 'VisualEditorSingleEditTabSwitchTimeEnd' ) &&
548            $user->isNamed() &&
549            self::enabledForUser( $user ) &&
550            !$userOptionsLookup->getOption( $user, 'visualeditor-hidetabdialog' ) &&
551            $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last'
552        ) {
553            // Check if the user has made any edits before the SET switch time
554            $dbr = $services->getConnectionProvider()->getReplicaDatabase();
555            $revExists = $dbr->newSelectQueryBuilder()
556                ->from( 'revision' )
557                ->field( '1' )
558                ->where( [
559                    'rev_actor' => $user->getActorId(),
560                    $dbr->expr( 'rev_timestamp', '<', $dbr->timestamp(
561                        $config->get( 'VisualEditorSingleEditTabSwitchTime' )
562                    ) )
563                ] )
564                ->caller( __METHOD__ )
565                ->fetchField();
566            if ( $revExists ) {
567                $links['views']['edit']['class'] .= ' visualeditor-showtabdialog';
568            }
569        }
570
571        // Exit if the user doesn't have VE enabled
572        if (
573            !self::enabledForUser( $user ) ||
574            // T253941: This option does not actually disable the editor, only leaves the tabs/links unchanged
575            ( $config->get( 'VisualEditorDisableForAnons' ) && !$user->isRegistered() )
576        ) {
577            return;
578        }
579
580        $title = $skin->getRelevantTitle();
581        // Don't exit if this page isn't VE-enabled, since we should still
582        // change "Edit" to "Edit source".
583        $isAvailable = self::isVisualAvailable( $title, $skin->getRequest(), $user );
584
585        $tabMessages = $config->get( 'VisualEditorTabMessages' );
586        // Rebuild the $links['views'] array and inject the VisualEditor tab before or after
587        // the edit tab as appropriate. We have to rebuild the array because PHP doesn't allow
588        // us to splice into the middle of an associative array.
589        $newViews = [];
590        $wikiPageFactory = $services->getWikiPageFactory();
591        $isRemote = !$wikiPageFactory->newFromTitle( $title )->isLocal();
592
593        $skinHasEditIcons = in_array(
594            $skin->getSkinName(),
595            ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorIconSkins' )
596        );
597
598        foreach ( $links['views'] as $action => $data ) {
599            if ( $action === 'edit' ) {
600                // Build the VisualEditor tab
601                $existing = $title->exists() || (
602                    $title->inNamespace( NS_MEDIAWIKI ) &&
603                    $title->getDefaultMessageText() !== false
604                );
605                $action = $existing ? 'edit' : 'create';
606                $veParams = $skin->editUrlOptions();
607                // Remove action=edit
608                unset( $veParams['action'] );
609                // Set veaction=edit
610                $veParams['veaction'] = 'edit';
611                $veTabMessage = $tabMessages[$action];
612                $veTabText = $veTabMessage === null ? $data['text'] :
613                    $skin->msg( $veTabMessage )->text();
614                if ( $isRemote ) {
615                    // The following messages can be used here:
616                    // * tooltip-ca-ve-edit-local
617                    // * tooltip-ca-ve-create-local
618                    // The following messages can be generated upstream:
619                    // * accesskey-ca-ve-edit-local
620                    // * accesskey-ca-ve-create-local
621                    $veTooltip = 'ca-ve-' . $action . '-local';
622                } else {
623                    // The following messages can be used here:
624                    // * tooltip-ca-ve-edit
625                    // * tooltip-ca-ve-create
626                    // The following messages can be generated upstream:
627                    // * accesskey-ca-ve-edit
628                    // * accesskey-ca-ve-create
629                    $veTooltip = 'ca-ve-' . $action;
630                }
631                $veTab = [
632                    'href' => $title->getLocalURL( $veParams ),
633                    'text' => $veTabText,
634                    'single-id' => $veTooltip,
635                    'primary' => true,
636                    'icon' => $skinHasEditIcons ? 'edit' : null,
637                    'class' => '',
638                ];
639
640                // Alter the edit tab
641                $editTab = $data;
642                if ( $isRemote ) {
643                    // The following messages can be used here:
644                    // * visualeditor-ca-editlocaldescriptionsource
645                    // * visualeditor-ca-createlocaldescriptionsource
646                    $editTabMessage = $tabMessages[$action . 'localdescriptionsource'];
647                    // The following messages can be used here:
648                    // * tooltip-ca-editsource-local
649                    // * tooltip-ca-createsource-local
650                    // The following messages can be generated upstream:
651                    // * accesskey-ca-editsource-local
652                    // * accesskey-ca-createsource-local
653                    $editTabTooltip = 'ca-' . $action . 'source-local';
654                } else {
655                    // The following messages can be used here:
656                    // * visualeditor-ca-editsource
657                    // * visualeditor-ca-createsource
658                    $editTabMessage = $tabMessages[$action . 'source'];
659                    // The following messages can be used here:
660                    // * tooltip-ca-editsource
661                    // * tooltip-ca-createsource
662                    // The following messages can be generated upstream:
663                    // * accesskey-ca-editsource
664                    // * accesskey-ca-createsource
665                    $editTabTooltip = 'ca-' . $action . 'source';
666                }
667
668                if ( $editTabMessage !== null ) {
669                    $editTab['text'] = $skin->msg( $editTabMessage )->text();
670                    $editTab['single-id'] = $editTabTooltip;
671                }
672
673                $editor = self::getLastEditor( $user, $skin->getRequest() );
674                if (
675                    $isAvailable &&
676                    $config->get( 'VisualEditorUseSingleEditTab' ) &&
677                    (
678                        $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'prefer-ve' ||
679                        (
680                            $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last' &&
681                            $editor === 'visualeditor'
682                        )
683                    )
684                ) {
685                    $editTab['text'] = $veTabText;
686                    $newViews['edit'] = $editTab;
687                } elseif (
688                    $isAvailable &&
689                    (
690                        !$config->get( 'VisualEditorUseSingleEditTab' ) ||
691                        $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'multi-tab'
692                    )
693                ) {
694                    // Change icon
695                    $editTab['icon'] = $skinHasEditIcons ? 'wikiText' : null;
696                    // Inject the VE tab before or after the edit tab
697                    if ( $config->get( 'VisualEditorTabPosition' ) === 'before' ) {
698                        // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
699                        $editTab['class'] .= ' collapsible';
700                        $newViews['ve-edit'] = $veTab;
701                        $newViews['edit'] = $editTab;
702                    } else {
703                        $veTab['class'] .= ' collapsible';
704                        $newViews['edit'] = $editTab;
705                        $newViews['ve-edit'] = $veTab;
706                    }
707                } elseif (
708                    !$config->get( 'VisualEditorUseSingleEditTab' ) ||
709                    !$isAvailable ||
710                    $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'multi-tab' ||
711                    (
712                        $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last' &&
713                        $editor === 'wikitext'
714                    )
715                ) {
716                    // Don't add ve-edit, but do update the edit tab (e.g. "Edit source").
717                    $newViews['edit'] = $editTab;
718                } else {
719                    // This should not happen.
720                }
721            } else {
722                // Just pass through
723                $newViews[$action] = $data;
724            }
725        }
726        $links['views'] = $newViews;
727    }
728
729    /**
730     * @param SkinTemplate $skin The skin template on which the UI is built.
731     * @param array &$links Navigation links.
732     */
733    private static function onSkinTemplateNavigationSpecialPage( SkinTemplate $skin, array &$links ) {
734        $title = $skin->getTitle();
735        if ( !$title || !$title->isSpecialPage() ) {
736            return;
737        }
738        [ $special, $subPage ] = MediaWikiServices::getInstance()->getSpecialPageFactory()
739            ->resolveAlias( $title->getDBkey() );
740        if ( $special !== 'CollabPad' ) {
741            return;
742        }
743        $links['namespaces']['special']['text'] = $skin->msg( 'collabpad' )->text();
744        $subPageTitle = Title::newFromText( $subPage );
745        if ( $subPageTitle ) {
746            $links['namespaces']['special']['href'] = SpecialPage::getTitleFor( $special )->getLocalURL();
747            $links['namespaces']['special']['class'] = '';
748
749            $links['namespaces']['pad']['text'] = $subPageTitle->getPrefixedText();
750            $links['namespaces']['pad']['href'] = '';
751            $links['namespaces']['pad']['class'] = 'selected';
752        }
753    }
754
755    /**
756     * Called when the normal wikitext editor is shown.
757     * Inserts a 'veswitched' hidden field if requested by the client
758     *
759     * @param EditPage $editPage The edit page view.
760     * @param OutputPage $output The page view.
761     */
762    public function onEditPage__showEditForm_fields( $editPage, $output ) {
763        $request = $output->getRequest();
764        if ( $request->getBool( 'veswitched' ) ) {
765            $output->addHTML( Html::hidden( 'veswitched', '1' ) );
766        }
767    }
768
769    /**
770     * Called when an edit is saved
771     * Adds 'visualeditor-switched' tag to the edit if requested
772     * Adds whatever tags from static::TAGS are present in the vetags parameter
773     *
774     * @param RecentChange $rc The new RC entry.
775     */
776    public function onRecentChange_Save( $rc ) {
777        $request = RequestContext::getMain()->getRequest();
778        if ( $request->getBool( 'veswitched' ) && $rc->getAttribute( 'rc_this_oldid' ) ) {
779            $rc->addTags( 'visualeditor-switched' );
780        }
781
782        $tags = explode( ',', $request->getVal( 'vetags' ) ?? '' );
783        $tags = array_values( array_intersect( $tags, static::TAGS ) );
784        if ( $tags ) {
785            $rc->addTags( $tags );
786        }
787    }
788
789    /**
790     * Changes the section edit links to add a VE edit link.
791     *
792     * This is attached to the MediaWiki 'SkinEditSectionLinks' hook.
793     *
794     * @param Skin $skin Skin being used to render the UI
795     * @param Title $title Title being used for request
796     * @param string $section The name of the section being pointed to.
797     * @param string $tooltip The default tooltip.
798     * @param array &$result All link detail arrays.
799     * @phan-param array{editsection:array{text:string,targetTitle:Title,attribs:array,query:array}} $result
800     * @param Language $lang The user interface language.
801     */
802    public function onSkinEditSectionLinks( $skin, $title, $section,
803        $tooltip, &$result, $lang
804    ) {
805        $services = MediaWikiServices::getInstance();
806        $userOptionsLookup = $services->getUserOptionsLookup();
807        $config = $services->getConfigFactory()
808            ->makeConfig( 'visualeditor' );
809
810        // Exit if we're in parserTests
811        if ( isset( $GLOBALS[ 'wgVisualEditorInParserTests' ] ) ) {
812            return;
813        }
814
815        if (
816            ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) &&
817            $services->getService( 'MobileFrontend.Context' )->shouldDisplayMobileView()
818        ) {
819            return;
820        }
821
822        $user = $skin->getUser();
823        // Exit if the user doesn't have VE enabled
824        if (
825            !self::enabledForUser( $user ) ||
826            // T253941: This option does not actually disable the editor, only leaves the tabs/links unchanged
827            ( $config->get( 'VisualEditorDisableForAnons' ) && !$user->isRegistered() )
828        ) {
829            return;
830        }
831
832        // Exit if we're on a foreign file description page
833        if (
834            $title->inNamespace( NS_FILE ) &&
835            !$services->getWikiPageFactory()->newFromTitle( $title )->isLocal()
836        ) {
837            return;
838        }
839
840        $editor = self::getLastEditor( $user, $skin->getRequest() );
841        if (
842            !$config->get( 'VisualEditorUseSingleEditTab' ) ||
843            $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'multi-tab' ||
844            (
845                $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last' &&
846                $editor === 'wikitext'
847            )
848        ) {
849            // Don't add ve-edit, but do update the edit tab (e.g. "Edit source").
850            $tabMessages = $config->get( 'VisualEditorTabMessages' );
851            // The following messages can be used here:
852            // * visualeditor-ca-editsource-section
853            $sourceEditSection = $tabMessages['editsectionsource'];
854            $result['editsection']['text'] = $skin->msg( $sourceEditSection )->inLanguage( $lang )->text();
855            // The following messages can be used here:
856            // * visualeditor-ca-editsource-section-hint
857            $sourceEditSectionHint = $tabMessages['editsectionsourcehint'];
858            $result['editsection']['attribs']['title'] = $skin->msg( $sourceEditSectionHint )
859                ->plaintextParams( $tooltip )
860                ->inLanguage( $lang )->text();
861        }
862
863        // Exit if we're using the single edit tab.
864        if (
865            $config->get( 'VisualEditorUseSingleEditTab' ) &&
866            $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) !== 'multi-tab'
867        ) {
868            return;
869        }
870
871        $skinHasEditIcons = in_array(
872            $skin->getSkinName(),
873            ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorIconSkins' )
874        );
875
876        // add VE edit section in VE available namespaces
877        if ( self::isVisualAvailable( $title, $skin->getRequest(), $user ) ) {
878            // The following messages can be used here:
879            // * editsection
880            $veEditSection = $tabMessages['editsection'];
881            // The following messages can be used here:
882            // * editsectionhint
883            $veEditSectionHint = $tabMessages['editsectionhint'];
884
885            $attribs = $result['editsection']['attribs'];
886            // class goes to SkinComponentLink which will accept a string or
887            // an array, and either might be provided at this point.
888            $class = $attribs['class'] ?? '';
889            if ( is_array( $class ) ) {
890                $class[] = 'mw-editsection-visualeditor';
891            } else {
892                $class .= ' mw-editsection-visualeditor';
893            }
894            $attribs['class'] = $class;
895            $attribs['title'] = $skin->msg( $veEditSectionHint )
896                ->plaintextParams( $tooltip )
897                ->inLanguage( $lang )->text();
898
899            $veLink = [
900                'text' => $skin->msg( $veEditSection )->inLanguage( $lang )->text(),
901                'icon' => $skinHasEditIcons ? 'edit' : null,
902                'targetTitle' => $title,
903                'attribs' => $attribs,
904                'query' => [ 'veaction' => 'edit', 'section' => $section ],
905                'options' => [ 'noclasses', 'known' ]
906            ];
907            // Change icon
908            $result['editsection']['icon'] = $skinHasEditIcons ? 'wikiText' : null;
909
910            $result['veeditsection'] = $veLink;
911            if ( $config->get( 'VisualEditorTabPosition' ) === 'before' ) {
912                krsort( $result );
913                // TODO: This will probably cause weird ordering if any other extensions added something
914                // already.
915                // ... wfArrayInsertBefore?
916            }
917        }
918    }
919
920    /**
921     * @param OutputPage $out
922     * @param Skin $sk
923     * @param string[] &$bodyAttrs
924     */
925    public function onOutputPageBodyAttributes( $out, $sk, &$bodyAttrs ): void {
926        $specialTitle = $sk->getTitle();
927
928        // HACK: Replace classes generated by Skin::getPageClasses as if an article title
929        // was passed in, instead of a special page.
930        if ( $specialTitle && $specialTitle->isSpecial( 'CollabPad' ) ) {
931            $articleTitle = Title::newFromText( 'DummyPage' );
932
933            $specialClasses = $sk->getPageClasses( $specialTitle );
934            $articleClasses = $sk->getPageClasses( $articleTitle );
935
936            $bodyAttrs['class'] = str_replace( $specialClasses, $articleClasses, $bodyAttrs['class'] );
937        }
938    }
939
940    /**
941     * Handler for the GetPreferences hook, to add and hide user preferences as configured
942     *
943     * @param User $user
944     * @param array &$preferences Their preferences object
945     */
946    public function onGetPreferences( $user, &$preferences ) {
947        $services = MediaWikiServices::getInstance();
948        $userOptionsLookup = $services->getUserOptionsLookup();
949        $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' );
950        $isBeta = $veConfig->get( 'VisualEditorEnableBetaFeature' );
951
952        // Use the old preference keys to avoid having to migrate data for now.
953        // (One day we might write and run a maintenance script to update the
954        // entries in the database and make this unnecessary.) (T344762)
955        if ( $isBeta ) {
956            $preferences['visualeditor-enable'] = [
957                'type' => 'toggle',
958                'label-message' => 'visualeditor-preference-visualeditor',
959                'section' => 'editing/editor',
960            ];
961        } else {
962            $preferences['visualeditor-betatempdisable'] = [
963                'invert' => true,
964                'type' => 'toggle',
965                'label-message' => 'visualeditor-preference-visualeditor',
966                'section' => 'editing/editor',
967                'default' => $userOptionsLookup->getOption( $user, 'visualeditor-betatempdisable' ) ||
968                    $userOptionsLookup->getOption( $user, 'visualeditor-autodisable' )
969            ];
970        }
971
972        if ( $veConfig->get( 'VisualEditorEnableWikitext' ) ) {
973            $preferences['visualeditor-newwikitext'] = [
974                'type' => 'toggle',
975                'label-message' => 'visualeditor-preference-newwikitexteditor-enable',
976                'help-message' => 'visualeditor-preference-newwikitexteditor-help',
977                'section' => 'editing/editor'
978            ];
979        }
980
981        // Config option for Single Edit Tab
982        if (
983            $veConfig->get( 'VisualEditorUseSingleEditTab' ) &&
984            self::enabledForUser( $user )
985        ) {
986            $preferences['visualeditor-tabs'] = [
987                'type' => 'select',
988                'label-message' => 'visualeditor-preference-tabs',
989                'section' => 'editing/editor',
990                'options-messages' => [
991                    'visualeditor-preference-tabs-remember-last' => 'remember-last',
992                    'visualeditor-preference-tabs-prefer-ve' => 'prefer-ve',
993                    'visualeditor-preference-tabs-prefer-wt' => 'prefer-wt',
994                    'visualeditor-preference-tabs-multi-tab' => 'multi-tab'
995                ]
996            ];
997        }
998
999        $api = [ 'type' => 'api' ];
1000        // The "autodisable" preference records whether the user has explicitly opted out of VE.
1001        // This is saved even when VE is off by default, which allows changing it to be on by default
1002        // without affecting the users who opted out. There's also a maintenance script to silently
1003        // opt-out existing users en masse before changing the default, thus only affecting new users.
1004        // (This option is no longer set to 'true' anywhere, but we can still encounter old true
1005        // values until they are migrated: T344760.)
1006        $preferences['visualeditor-autodisable'] = $api;
1007        // The diff mode is persisted for each editor mode separately,
1008        // e.g. use visual diffs for visual mode only.
1009        $preferences['visualeditor-diffmode-source'] = $api;
1010        $preferences['visualeditor-diffmode-visual'] = $api;
1011        $preferences['visualeditor-diffmode-historical'] = $api;
1012        $preferences['visualeditor-editor'] = $api;
1013        $preferences['visualeditor-hidebetawelcome'] = $api;
1014        $preferences['visualeditor-hidetabdialog'] = $api;
1015        $preferences['visualeditor-hidesourceswitchpopup'] = $api;
1016        $preferences['visualeditor-hidevisualswitchpopup'] = $api;
1017        $preferences['visualeditor-hideusered'] = $api;
1018        $preferences['visualeditor-findAndReplace-diacritic'] = $api;
1019        $preferences['visualeditor-findAndReplace-findText'] = $api;
1020        $preferences['visualeditor-findAndReplace-replaceText'] = $api;
1021        $preferences['visualeditor-findAndReplace-regex'] = $api;
1022        $preferences['visualeditor-findAndReplace-matchCase'] = $api;
1023        $preferences['visualeditor-findAndReplace-word'] = $api;
1024    }
1025
1026    /**
1027     * Implements the PreferencesFormPreSave hook, to remove the 'autodisable' flag
1028     * when the user it was set on explicitly enables VE.
1029     *
1030     * @param array $data User-submitted data
1031     * @param HTMLForm $form A ContextSource
1032     * @param User $user User with new preferences already set
1033     * @param bool &$result Success or failure
1034     * @param array $oldUserOptions
1035     */
1036    public function onPreferencesFormPreSave( $data, $form, $user, &$result, $oldUserOptions ) {
1037        $services = MediaWikiServices::getInstance();
1038        $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' );
1039        $userOptionsManager = $services->getUserOptionsManager();
1040        $isBeta = $veConfig->get( 'VisualEditorEnableBetaFeature' );
1041
1042        // The "autodisable" preference records whether the user has explicitly opted out of VE
1043        // while it was in beta (which would otherwise not be saved, since it's the same as default).
1044
1045        if (
1046            // When the user enables VE, clear the preference.
1047            $userOptionsManager->getOption( $user, 'visualeditor-autodisable' ) &&
1048            ( $isBeta ?
1049                $userOptionsManager->getOption( $user, 'visualeditor-enable' ) :
1050                !$userOptionsManager->getOption( $user, 'visualeditor-betatempdisable' ) )
1051        ) {
1052            $userOptionsManager->setOption( $user, 'visualeditor-autodisable', false );
1053        }
1054    }
1055
1056    /**
1057     * @param array &$tags
1058     */
1059    public function onChangeTagsListActive( &$tags ) {
1060        $this->onListDefinedTags( $tags );
1061    }
1062
1063    /**
1064     * Implements the ListDefinedTags and ChangeTagsListActive hooks, to
1065     * populate core Special:Tags with the change tags in use by VisualEditor.
1066     *
1067     * @param array &$tags Available change tags.
1068     */
1069    public function onListDefinedTags( &$tags ) {
1070        $tags = array_merge( $tags, static::TAGS );
1071    }
1072
1073    /**
1074     * Adds extra variables to the page config.
1075     *
1076     * @param array &$vars Global variables object
1077     * @param OutputPage $out The page view.
1078     */
1079    public function onMakeGlobalVariablesScript( &$vars, $out ): void {
1080        $pageLanguage = ApiVisualEditor::getPageLanguage( $out->getTitle() );
1081        $converter = MediaWikiServices::getInstance()->getLanguageConverterFactory()
1082            ->getLanguageConverter( $pageLanguage );
1083
1084        $fallbacks = $converter->getVariantFallbacks( $converter->getPreferredVariant() );
1085
1086        $vars['wgVisualEditor'] = [
1087            'pageLanguageCode' => $pageLanguage->getHtmlCode(),
1088            'pageLanguageDir' => $pageLanguage->getDir(),
1089            'pageVariantFallbacks' => $fallbacks,
1090        ];
1091    }
1092
1093    /**
1094     * Adds extra variables to the global config
1095     *
1096     * @param array &$vars Global variables object
1097     * @param string $skin
1098     * @param Config $config
1099     */
1100    public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void {
1101        $coreConfig = RequestContext::getMain()->getConfig();
1102        $services = MediaWikiServices::getInstance();
1103        $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' );
1104        $extensionRegistry = ExtensionRegistry::getInstance();
1105        $availableNamespaces = ApiVisualEditor::getAvailableNamespaceIds( $veConfig );
1106        $availableContentModels = array_filter(
1107            array_merge(
1108                $extensionRegistry->getAttribute( 'VisualEditorAvailableContentModels' ),
1109                $veConfig->get( 'VisualEditorAvailableContentModels' )
1110            )
1111        );
1112
1113        $namespacesWithSubpages = $coreConfig->get( 'NamespacesWithSubpages' );
1114        // Export as a list of namespaces where subpages are enabled instead of an object
1115        // mapping namespaces to if subpages are enabled or not, so filter out disabled
1116        // namespaces and then just use the keys. See T291729.
1117        $namespacesWithSubpages = array_filter( $namespacesWithSubpages );
1118        $namespacesWithSubpagesEnabled = array_keys( $namespacesWithSubpages );
1119        // $wgNamespacesWithSubpages can include namespaces that don't exist, no need
1120        // to include those in the JavaScript data. See T291727.
1121        // Run this filtering after the filter for subpages being enabled, to reduce
1122        // the number of calls needed to namespace info.
1123        $nsInfo = $services->getNamespaceInfo();
1124        $namespacesWithSubpagesEnabled = array_values( array_filter(
1125            $namespacesWithSubpagesEnabled,
1126            [ $nsInfo, 'exists' ]
1127        ) );
1128
1129        $defaultSortPrefix = $services->getMagicWordFactory()->get( 'defaultsort' )->getSynonym( 0 );
1130        // Sanitize trailing colon. /languages/messages/*.php are not consistent but the
1131        // presence or absence of a trailing colon in the message makes no difference.
1132        $defaultSortPrefix = preg_replace( '/:$/', '', $defaultSortPrefix );
1133
1134        $vars['wgVisualEditorConfig'] = [
1135            'usePageImages' => $extensionRegistry->isLoaded( 'PageImages' ),
1136            'usePageDescriptions' => $extensionRegistry->isLoaded( 'WikibaseClient' ),
1137            'isBeta' => $veConfig->get( 'VisualEditorEnableBetaFeature' ),
1138            'disableForAnons' => $veConfig->get( 'VisualEditorDisableForAnons' ),
1139            'preloadModules' => $veConfig->get( 'VisualEditorPreloadModules' ),
1140            'namespaces' => $availableNamespaces,
1141            'contentModels' => $availableContentModels,
1142            'pluginModules' => array_merge(
1143                $extensionRegistry->getAttribute( 'VisualEditorPluginModules' ),
1144                // @todo deprecate the global setting
1145                $veConfig->get( 'VisualEditorPluginModules' )
1146            ),
1147            'thumbLimits' => $coreConfig->get( 'ThumbLimits' ),
1148            'galleryOptions' => $coreConfig->get( 'GalleryOptions' ),
1149            'tabPosition' => $veConfig->get( 'VisualEditorTabPosition' ),
1150            'tabMessages' => array_filter( $veConfig->get( 'VisualEditorTabMessages' ) ),
1151            'singleEditTab' => $veConfig->get( 'VisualEditorUseSingleEditTab' ),
1152            'enableVisualSectionEditing' => $veConfig->get( 'VisualEditorEnableVisualSectionEditing' ),
1153            'showBetaWelcome' => $veConfig->get( 'VisualEditorShowBetaWelcome' ),
1154            'allowExternalLinkPaste' => $veConfig->get( 'VisualEditorAllowExternalLinkPaste' ),
1155            'enableHelpCompletion' => $veConfig->get( 'VisualEditorEnableHelpCompletion' ),
1156            'enableTocWidget' => $veConfig->get( 'VisualEditorEnableTocWidget' ),
1157            'enableWikitext' => $veConfig->get( 'VisualEditorEnableWikitext' ),
1158            'useChangeTagging' => $veConfig->get( 'VisualEditorUseChangeTagging' ),
1159            'editCheckTagging' => $veConfig->get( 'VisualEditorEditCheckTagging' ),
1160            'editCheck' => $veConfig->get( 'VisualEditorEditCheck' ),
1161            'editCheckABTest' => $veConfig->get( 'VisualEditorEditCheckABTest' ),
1162            'editCheckReliabilityAvailable' => ApiEditCheckReferenceUrl::isAvailable(),
1163            'namespacesWithSubpages' => $namespacesWithSubpagesEnabled,
1164            'specialBooksources' => urldecode( SpecialPage::getTitleFor( 'Booksources' )->getPrefixedURL() ),
1165            'rebaserUrl' => $coreConfig->get( 'VisualEditorRebaserURL' ),
1166            'feedbackApiUrl' => $veConfig->get( 'VisualEditorFeedbackAPIURL' ),
1167            'feedbackTitle' => $veConfig->get( 'VisualEditorFeedbackTitle' ),
1168            'sourceFeedbackTitle' => $veConfig->get( 'VisualEditorSourceFeedbackTitle' ),
1169            // TODO: Remove when all usages in .js files are removed
1170            'transclusionDialogNewSidebar' => true,
1171            'cirrusSearchLookup' => $extensionRegistry->isLoaded( 'CirrusSearch' ),
1172            'defaultSortPrefix' => $defaultSortPrefix,
1173        ];
1174    }
1175
1176    /**
1177     * Conditionally register the jquery.uls.data and jquery.i18n modules, in case they've already
1178     * been registered by the UniversalLanguageSelector extension or the TemplateData extension.
1179     *
1180     * @param ResourceLoader $resourceLoader Client-side code and assets to be loaded.
1181     */
1182    public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void {
1183        $veResourceTemplate = [
1184            'localBasePath' => dirname( __DIR__ ),
1185            'remoteExtPath' => 'VisualEditor',
1186        ];
1187
1188        // Only register VisualEditor core's local version of jquery.uls.data if it hasn't been
1189        // installed locally already (presumably, by the UniversalLanguageSelector extension).
1190        if ( !$resourceLoader->isModuleRegistered( 'jquery.uls.data' ) ) {
1191            $resourceLoader->register( [
1192                'jquery.uls.data' => $veResourceTemplate + [
1193                    'scripts' => [
1194                        'lib/ve/lib/jquery.uls/src/jquery.uls.data.js',
1195                        'lib/ve/lib/jquery.uls/src/jquery.uls.data.utils.js',
1196                    ],
1197                ] ] );
1198        }
1199    }
1200
1201    /**
1202     * Ensures that we know whether we're running inside a parser test.
1203     *
1204     * @param array &$settings The settings with which MediaWiki is being run.
1205     */
1206    public function onParserTestGlobals( &$settings ) {
1207        $settings['wgVisualEditorInParserTests'] = true;
1208    }
1209
1210    /**
1211     * @param array &$redirectParams Parameters preserved on special page redirects
1212     *   to wiki pages
1213     */
1214    public function onRedirectSpecialArticleRedirectParams( &$redirectParams ) {
1215        $redirectParams[] = 'veaction';
1216    }
1217
1218    /**
1219     * If the user has specified that they want to edit the page with VE, suppress any redirect.
1220     *
1221     * @param Title $title Title being used for request
1222     * @param Article|null $article The page being viewed.
1223     * @param OutputPage $output The page view.
1224     * @param User $user The user-specific settings.
1225     * @param WebRequest $request
1226     * @param ActionEntryPoint $mediaWiki Helper class.
1227     */
1228    public function onBeforeInitialize(
1229        $title, $article, $output, $user, $request, $mediaWiki
1230    ) {
1231        if ( $request->getVal( 'veaction' ) ) {
1232            $request->setVal( 'redirect', 'no' );
1233        }
1234    }
1235
1236    /**
1237     * On login, if user has a VEE cookie, set their preference equal to it.
1238     *
1239     * @param User $user The user-specific settings.
1240     */
1241    public function onUserLoggedIn( $user ) {
1242        $cookie = RequestContext::getMain()->getRequest()->getCookie( 'VEE', '' );
1243        if ( $user->isNamed() && ( $cookie === 'visualeditor' || $cookie === 'wikitext' ) ) {
1244            self::deferredSetUserOption( $user, 'visualeditor-editor', $cookie );
1245        }
1246    }
1247}