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