getHookContainer() ); if ( !$hookRunner->onVisualEditorBeforeEditor( $output, $skin ) ) { $output->addJsConfigVars( 'wgVisualEditorDisabledByHook', true ); return; } if ( !( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) && $services->getService( 'MobileFrontend.Context' ) ->shouldDisplayMobileView() ) ) { $output->addModules( [ 'ext.visualEditor.desktopArticleTarget.init', 'ext.visualEditor.targetLoader' ] ); $output->addModuleStyles( [ 'ext.visualEditor.desktopArticleTarget.noscript' ] ); } if ( $services->getUserOptionsLookup()->getOption( $skin->getUser(), 'visualeditor-collab' ) || // Joining a collab session $output->getRequest()->getVal( 'collabSession' ) ) { $output->addModules( 'ext.visualEditor.collab' ); } // add scroll offset js variable to output $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' ); $skinsToolbarScrollOffset = $veConfig->get( 'VisualEditorSkinToolbarScrollOffset' ); $toolbarScrollOffset = 0; $skinName = $skin->getSkinName(); if ( isset( $skinsToolbarScrollOffset[$skinName] ) ) { $toolbarScrollOffset = $skinsToolbarScrollOffset[$skinName]; } // T220158: Don't add this unless it's non-default // TODO: Move this to packageFiles as it's not relevant to the HTML request. if ( $toolbarScrollOffset !== 0 ) { $output->addJsConfigVars( 'wgVisualEditorToolbarScrollOffset', $toolbarScrollOffset ); } $output->addJsConfigVars( 'wgEditSubmitButtonLabelPublish', $veConfig->get( 'EditSubmitButtonLabelPublish' ) ); // Don't index VE edit pages (T319124) if ( $output->getRequest()->getVal( 'veaction' ) ) { $output->setRobotPolicy( 'noindex,nofollow' ); } } /** * @internal For internal use in extension.json only. */ public static function getDataForDesktopArticleTargetInitModule(): array { return [ 'unsupportedEditParams' => self::UNSUPPORTED_EDIT_PARAMS, ]; } /** * Load modules required for a diff page * * @param OutputPage $output Output page */ private function loadDiffModules( OutputPage $output ) { $output->addModuleStyles( [ 'ext.visualEditor.diffPage.init.styles', 'oojs-ui.styles.icons-accessibility', 'oojs-ui.styles.icons-editing-advanced' ] ); $output->addModules( 'ext.visualEditor.diffPage.init' ); $output->enableOOUI(); } /** @inheritDoc */ public function onDifferenceEngineViewHeader( $differenceEngine ) { // T344596: Must load this module unconditionally. The TextSlotDiffRendererTablePrefix hook // below doesn't run when the diff is e.g. a log entry with no change to the content. $this->loadDiffModules( $differenceEngine->getContext()->getOutput() ); } /** * Handler for the DifferenceEngineViewHeader hook, to add visual diffs code as configured * * @param TextSlotDiffRenderer $textSlotDiffRenderer * @param IContextSource $context * @param string[] &$parts * @return void */ public function onTextSlotDiffRendererTablePrefix( TextSlotDiffRenderer $textSlotDiffRenderer, IContextSource $context, array &$parts ) { $services = MediaWikiServices::getInstance(); $veConfig = $services->getConfigFactory() ->makeConfig( 'visualeditor' ); $output = $context->getOutput(); // Return early if not viewing a diff of an allowed type. if ( !ApiVisualEditor::isAllowedContentType( $veConfig, $textSlotDiffRenderer->getContentModel() ) || $output->getActionName() !== 'view' ) { return; } // onDifferenceEngineViewHeader may not run, so load modules here as well for styling (T361775) $this->loadDiffModules( $output ); $parts['50_ve-init-mw-diffPage-diffMode'] = '
' . // Will be replaced by a ButtonSelectWidget in JS new ButtonGroupWidget( [ 'items' => [ new ButtonWidget( [ 'data' => 'visual', 'icon' => 'eye', 'disabled' => true, 'label' => $output->msg( 'visualeditor-savedialog-review-visual' )->plain() ] ), new ButtonWidget( [ 'data' => 'source', 'icon' => 'wikiText', 'active' => true, 'label' => $output->msg( 'visualeditor-savedialog-review-wikitext' )->plain() ] ) ] ] ) . '
'; } /** * @param Title $title * @param User $user * @param WebRequest $req * @return bool */ private static function isSupportedEditPage( Title $title, User $user, WebRequest $req ): bool { if ( $req->getVal( 'action' ) !== 'edit' || !MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan( 'edit', $user, $title ) ) { return false; } foreach ( self::UNSUPPORTED_EDIT_PARAMS as $param ) { if ( $req->getVal( $param ) !== null ) { return false; } } switch ( self::getEditPageEditor( $user, $req ) ) { case 'visualeditor': return self::isVisualAvailable( $title, $req, $user ) || self::isWikitextAvailable( $title, $user ); case 'wikitext': default: return self::isWikitextAvailable( $title, $user ); } } /** * @param UserIdentity $user * @return bool */ private static function enabledForUser( UserIdentity $user ): bool { $services = MediaWikiServices::getInstance(); $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' ); $userOptionsLookup = $services->getUserOptionsLookup(); $isBeta = $veConfig->get( 'VisualEditorEnableBetaFeature' ); return ( $isBeta ? $userOptionsLookup->getOption( $user, 'visualeditor-enable' ) : !$userOptionsLookup->getOption( $user, 'visualeditor-betatempdisable' ) ) && !$userOptionsLookup->getOption( $user, 'visualeditor-autodisable' ); } /** * @param Title $title * @param WebRequest $req * @param UserIdentity $user * @return bool */ private static function isVisualAvailable( Title $title, WebRequest $req, UserIdentity $user ): bool { $veConfig = MediaWikiServices::getInstance()->getConfigFactory() ->makeConfig( 'visualeditor' ); return ( // If forced by the URL parameter, skip the namespace check (T221892) and preference check ( $req->getVal( 'veaction' ) === 'edit' || ( // Only in enabled namespaces ApiVisualEditor::isAllowedNamespace( $veConfig, $title->getNamespace() ) && // Enabled per user preferences self::enabledForUser( $user ) ) ) && // Only for pages with a supported content model ApiVisualEditor::isAllowedContentType( $veConfig, $title->getContentModel() ) ); } /** * @param Title $title * @param UserIdentity $user * @return bool */ private static function isWikitextAvailable( Title $title, UserIdentity $user ): bool { $services = MediaWikiServices::getInstance(); $userOptionsLookup = $services->getUserOptionsLookup(); return $userOptionsLookup->getOption( $user, 'visualeditor-newwikitext' ) && $title->getContentModel() === 'wikitext'; } /** * @param UserIdentity $user * @param string $key * @param string $value */ private static function deferredSetUserOption( UserIdentity $user, string $key, string $value ): void { DeferredUpdates::addCallableUpdate( static function () use ( $user, $key, $value ) { $services = MediaWikiServices::getInstance(); if ( $services->getReadOnlyMode()->isReadOnly() ) { return; } $userOptionsManager = $services->getUserOptionsManager(); $userOptionsManager->setOption( $user, $key, $value ); $userOptionsManager->saveOptions( $user ); } ); } /** * Decide whether to bother showing the wikitext editor at all. * If not, we expect the VE initialisation JS to activate. * * @param Article $article The article being viewed. * @param User $user The user-specific settings. * @return bool Whether to show the wikitext editor or not. */ public function onCustomEditor( $article, $user ) { $req = $article->getContext()->getRequest(); $services = MediaWikiServices::getInstance(); $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' ); if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) ) { // If mobilefrontend is involved it can make its own decisions about this $mobFrontContext = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); if ( $mobFrontContext->shouldDisplayMobileView() ) { return true; } } if ( !self::enabledForUser( $user ) ) { return true; } $title = $article->getTitle(); if ( $req->getVal( 'venoscript' ) ) { $req->response()->setCookie( 'VEE', 'wikitext', 0, [ 'prefix' => '' ] ); if ( $user->isNamed() ) { self::deferredSetUserOption( $user, 'visualeditor-editor', 'wikitext' ); } return true; } if ( self::isSupportedEditPage( $title, $user, $req ) ) { $params = $req->getValues(); $params['venoscript'] = '1'; $url = wfScript() . '?' . wfArrayToCgi( $params ); $out = $article->getContext()->getOutput(); $titleMsg = $title->exists() ? 'editing' : 'creating'; $out->setPageTitleMsg( wfMessage( $titleMsg, $title->getPrefixedText() ) ); $out->showPendingTakeover( $url, 'visualeditor-toload', wfGetUrlUtils()->expand( $url ) ); $out->setRevisionId( $req->getInt( 'oldid', $article->getRevIdFetched() ) ); return false; } return true; } /** * @param User $user * @param WebRequest $req * @return string 'wikitext' or 'visual' */ private static function getEditPageEditor( User $user, WebRequest $req ): string { $config = MediaWikiServices::getInstance()->getConfigFactory() ->makeConfig( 'visualeditor' ); if ( $config->get( 'VisualEditorDisableForAnons' ) && !$user->isRegistered() ) { return 'wikitext'; } $isRedLink = $req->getBool( 'redlink' ); // On dual-edit-tab wikis, the edit page must mean the user wants wikitext, // unless following a redlink if ( !$config->get( 'VisualEditorUseSingleEditTab' ) && !$isRedLink ) { return 'wikitext'; } return self::getPreferredEditor( $user, $req, !$isRedLink ); } /** * @param User $user * @param WebRequest $req * @param bool $useWikitextInMultiTab * @return string 'wikitext' or 'visual' */ public static function getPreferredEditor( User $user, WebRequest $req, bool $useWikitextInMultiTab = false ): string { // VisualEditor shouldn't even call this method when it's disabled, but it is a public API for // other extensions (e.g. DiscussionTools), and the editor preferences might have surprising // values if the user has tried VisualEditor in the past and then disabled it. (T257234) if ( !self::enabledForUser( $user ) ) { return 'wikitext'; } $services = MediaWikiServices::getInstance(); $userOptionsLookup = $services->getUserOptionsLookup(); switch ( $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) ) { case 'prefer-ve': return 'visualeditor'; case 'prefer-wt': return 'wikitext'; case 'multi-tab': // May have got here by switching from VE // TODO: Make such an action explicitly request wikitext // so we can use getLastEditor here instead. return $useWikitextInMultiTab ? 'wikitext' : self::getLastEditor( $user, $req ); case 'remember-last': default: return self::getLastEditor( $user, $req ); } } /** * @param User $user * @param WebRequest $req * @return string */ private static function getLastEditor( User $user, WebRequest $req ): string { // This logic matches getLastEditor in: // modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js $editor = $req->getCookie( 'VEE', '' ); // Set editor to user's preference or site's default (ignore the cookie) if … if ( // … user is logged in, $user->isNamed() || // … no cookie is set, or !$editor || // value is invalid. !( $editor === 'visualeditor' || $editor === 'wikitext' ) ) { $services = MediaWikiServices::getInstance(); $userOptionsLookup = $services->getUserOptionsLookup(); $editor = $userOptionsLookup->getOption( $user, 'visualeditor-editor' ); } return $editor; } /** * Changes the Edit tab and adds the VisualEditor tab. * * This is attached to the MediaWiki 'SkinTemplateNavigation::Universal' hook. * * @param SkinTemplate $skin The skin template on which the UI is built. * @param array &$links Navigation links. */ public function onSkinTemplateNavigation__Universal( $skin, &$links ): void { $services = MediaWikiServices::getInstance(); $userOptionsLookup = $services->getUserOptionsLookup(); $config = $services->getConfigFactory() ->makeConfig( 'visualeditor' ); self::onSkinTemplateNavigationSpecialPage( $skin, $links ); if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) && $services->getService( 'MobileFrontend.Context' )->shouldDisplayMobileView() ) { return; } // Exit if there's no edit link for whatever reason (e.g. protected page) if ( !isset( $links['views']['edit'] ) ) { return; } $hookRunner = new VisualEditorHookRunner( $services->getHookContainer() ); if ( !$hookRunner->onVisualEditorBeforeEditor( $skin->getOutput(), $skin ) ) { return; } $user = $skin->getUser(); if ( $config->get( 'VisualEditorUseSingleEditTab' ) && $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'prefer-wt' ) { return; } if ( $config->get( 'VisualEditorUseSingleEditTab' ) && wfTimestampNow() < $config->get( 'VisualEditorSingleEditTabSwitchTimeEnd' ) && $user->isNamed() && self::enabledForUser( $user ) && !$userOptionsLookup->getOption( $user, 'visualeditor-hidetabdialog' ) && $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last' ) { // Check if the user has made any edits before the SET switch time $dbr = $services->getConnectionProvider()->getReplicaDatabase(); $revExists = $dbr->newSelectQueryBuilder() ->from( 'revision' ) ->field( '1' ) ->where( [ 'rev_actor' => $user->getActorId(), $dbr->expr( 'rev_timestamp', '<', $dbr->timestamp( $config->get( 'VisualEditorSingleEditTabSwitchTime' ) ) ) ] ) ->caller( __METHOD__ ) ->fetchField(); if ( $revExists ) { $links['views']['edit']['class'] .= ' visualeditor-showtabdialog'; } } // Exit if the user doesn't have VE enabled if ( !self::enabledForUser( $user ) || // T253941: This option does not actually disable the editor, only leaves the tabs/links unchanged ( $config->get( 'VisualEditorDisableForAnons' ) && !$user->isRegistered() ) ) { return; } $title = $skin->getRelevantTitle(); // Don't exit if this page isn't VE-enabled, since we should still // change "Edit" to "Edit source". $isAvailable = self::isVisualAvailable( $title, $skin->getRequest(), $user ); $tabMessages = $config->get( 'VisualEditorTabMessages' ); // Rebuild the $links['views'] array and inject the VisualEditor tab before or after // the edit tab as appropriate. We have to rebuild the array because PHP doesn't allow // us to splice into the middle of an associative array. $newViews = []; $wikiPageFactory = $services->getWikiPageFactory(); $isRemote = !$wikiPageFactory->newFromTitle( $title )->isLocal(); $skinHasEditIcons = in_array( $skin->getSkinName(), ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorIconSkins' ) ); foreach ( $links['views'] as $action => $data ) { if ( $action === 'edit' ) { // Build the VisualEditor tab $existing = $title->exists() || ( $title->inNamespace( NS_MEDIAWIKI ) && $title->getDefaultMessageText() !== false ); $action = $existing ? 'edit' : 'create'; $veParams = $skin->editUrlOptions(); // Remove action=edit unset( $veParams['action'] ); // Set veaction=edit $veParams['veaction'] = 'edit'; $veTabMessage = $tabMessages[$action]; $veTabText = $veTabMessage === null ? $data['text'] : $skin->msg( $veTabMessage )->text(); if ( $isRemote ) { // The following messages can be used here: // * tooltip-ca-ve-edit-local // * tooltip-ca-ve-create-local // The following messages can be generated upstream: // * accesskey-ca-ve-edit-local // * accesskey-ca-ve-create-local $veTooltip = 'ca-ve-' . $action . '-local'; } else { // The following messages can be used here: // * tooltip-ca-ve-edit // * tooltip-ca-ve-create // The following messages can be generated upstream: // * accesskey-ca-ve-edit // * accesskey-ca-ve-create $veTooltip = 'ca-ve-' . $action; } $veTab = [ 'href' => $title->getLocalURL( $veParams ), 'text' => $veTabText, 'single-id' => $veTooltip, 'primary' => true, 'icon' => $skinHasEditIcons ? 'edit' : null, 'class' => '', ]; // Alter the edit tab $editTab = $data; if ( $isRemote ) { // The following messages can be used here: // * visualeditor-ca-editlocaldescriptionsource // * visualeditor-ca-createlocaldescriptionsource $editTabMessage = $tabMessages[$action . 'localdescriptionsource']; // The following messages can be used here: // * tooltip-ca-editsource-local // * tooltip-ca-createsource-local // The following messages can be generated upstream: // * accesskey-ca-editsource-local // * accesskey-ca-createsource-local $editTabTooltip = 'ca-' . $action . 'source-local'; } else { // The following messages can be used here: // * visualeditor-ca-editsource // * visualeditor-ca-createsource $editTabMessage = $tabMessages[$action . 'source']; // The following messages can be used here: // * tooltip-ca-editsource // * tooltip-ca-createsource // The following messages can be generated upstream: // * accesskey-ca-editsource // * accesskey-ca-createsource $editTabTooltip = 'ca-' . $action . 'source'; } if ( $editTabMessage !== null ) { $editTab['text'] = $skin->msg( $editTabMessage )->text(); $editTab['single-id'] = $editTabTooltip; } $editor = self::getLastEditor( $user, $skin->getRequest() ); if ( $isAvailable && $config->get( 'VisualEditorUseSingleEditTab' ) && ( $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'prefer-ve' || ( $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last' && $editor === 'visualeditor' ) ) ) { $editTab['text'] = $veTabText; $newViews['edit'] = $editTab; } elseif ( $isAvailable && ( !$config->get( 'VisualEditorUseSingleEditTab' ) || $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'multi-tab' ) ) { // Change icon $editTab['icon'] = $skinHasEditIcons ? 'wikiText' : null; // Inject the VE tab before or after the edit tab if ( $config->get( 'VisualEditorTabPosition' ) === 'before' ) { // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset $editTab['class'] .= ' collapsible'; $newViews['ve-edit'] = $veTab; $newViews['edit'] = $editTab; } else { $veTab['class'] .= ' collapsible'; $newViews['edit'] = $editTab; $newViews['ve-edit'] = $veTab; } } elseif ( !$config->get( 'VisualEditorUseSingleEditTab' ) || !$isAvailable || $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'multi-tab' || ( $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last' && $editor === 'wikitext' ) ) { // Don't add ve-edit, but do update the edit tab (e.g. "Edit source"). $newViews['edit'] = $editTab; } else { // This should not happen. } } else { // Just pass through $newViews[$action] = $data; } } $links['views'] = $newViews; } /** * @param SkinTemplate $skin The skin template on which the UI is built. * @param array &$links Navigation links. */ private static function onSkinTemplateNavigationSpecialPage( SkinTemplate $skin, array &$links ) { $title = $skin->getTitle(); if ( !$title || !$title->isSpecialPage() ) { return; } [ $special, $subPage ] = MediaWikiServices::getInstance()->getSpecialPageFactory() ->resolveAlias( $title->getDBkey() ); if ( $special !== 'CollabPad' ) { return; } $links['namespaces']['special']['text'] = $skin->msg( 'collabpad' )->text(); $subPageTitle = Title::newFromText( $subPage ); if ( $subPageTitle ) { $links['namespaces']['special']['href'] = SpecialPage::getTitleFor( $special )->getLocalURL(); $links['namespaces']['special']['class'] = ''; $links['namespaces']['pad']['text'] = $subPageTitle->getPrefixedText(); $links['namespaces']['pad']['href'] = ''; $links['namespaces']['pad']['class'] = 'selected'; } } /** * Called when the normal wikitext editor is shown. * Inserts a 'veswitched' hidden field if requested by the client * * @param EditPage $editPage The edit page view. * @param OutputPage $output The page view. */ public function onEditPage__showEditForm_fields( $editPage, $output ) { $request = $output->getRequest(); if ( $request->getBool( 'veswitched' ) ) { $output->addHTML( Html::hidden( 'veswitched', '1' ) ); } } /** * Called when an edit is saved * Adds 'visualeditor-switched' tag to the edit if requested * Adds whatever tags from static::TAGS are present in the vetags parameter * * @param RecentChange $rc The new RC entry. */ public function onRecentChange_Save( $rc ) { $request = RequestContext::getMain()->getRequest(); if ( $request->getBool( 'veswitched' ) && $rc->getAttribute( 'rc_this_oldid' ) ) { $rc->addTags( 'visualeditor-switched' ); } $tags = explode( ',', $request->getVal( 'vetags' ) ?? '' ); $tags = array_values( array_intersect( $tags, static::TAGS ) ); if ( $tags ) { $rc->addTags( $tags ); } } /** * Changes the section edit links to add a VE edit link. * * This is attached to the MediaWiki 'SkinEditSectionLinks' hook. * * @param Skin $skin Skin being used to render the UI * @param Title $title Title being used for request * @param string $section The name of the section being pointed to. * @param string $tooltip The default tooltip. * @param array &$result All link detail arrays. * @phan-param array{editsection:array{text:string,targetTitle:Title,attribs:array,query:array}} $result * @param Language $lang The user interface language. */ public function onSkinEditSectionLinks( $skin, $title, $section, $tooltip, &$result, $lang ) { $services = MediaWikiServices::getInstance(); $userOptionsLookup = $services->getUserOptionsLookup(); $config = $services->getConfigFactory() ->makeConfig( 'visualeditor' ); // Exit if we're in parserTests if ( isset( $GLOBALS[ 'wgVisualEditorInParserTests' ] ) ) { return; } if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) && $services->getService( 'MobileFrontend.Context' )->shouldDisplayMobileView() ) { return; } $user = $skin->getUser(); // Exit if the user doesn't have VE enabled if ( !self::enabledForUser( $user ) || // T253941: This option does not actually disable the editor, only leaves the tabs/links unchanged ( $config->get( 'VisualEditorDisableForAnons' ) && !$user->isRegistered() ) ) { return; } // Exit if we're on a foreign file description page if ( $title->inNamespace( NS_FILE ) && !$services->getWikiPageFactory()->newFromTitle( $title )->isLocal() ) { return; } $editor = self::getLastEditor( $user, $skin->getRequest() ); if ( !$config->get( 'VisualEditorUseSingleEditTab' ) || $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'multi-tab' || ( $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) === 'remember-last' && $editor === 'wikitext' ) ) { // Don't add ve-edit, but do update the edit tab (e.g. "Edit source"). $tabMessages = $config->get( 'VisualEditorTabMessages' ); // The following messages can be used here: // * visualeditor-ca-editsource-section $sourceEditSection = $tabMessages['editsectionsource']; $result['editsection']['text'] = $skin->msg( $sourceEditSection )->inLanguage( $lang )->text(); // The following messages can be used here: // * visualeditor-ca-editsource-section-hint $sourceEditSectionHint = $tabMessages['editsectionsourcehint']; $result['editsection']['attribs']['title'] = $skin->msg( $sourceEditSectionHint ) ->plaintextParams( $tooltip ) ->inLanguage( $lang )->text(); } // Exit if we're using the single edit tab. if ( $config->get( 'VisualEditorUseSingleEditTab' ) && $userOptionsLookup->getOption( $user, 'visualeditor-tabs' ) !== 'multi-tab' ) { return; } $skinHasEditIcons = in_array( $skin->getSkinName(), ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorIconSkins' ) ); // add VE edit section in VE available namespaces if ( self::isVisualAvailable( $title, $skin->getRequest(), $user ) ) { // The following messages can be used here: // * editsection $veEditSection = $tabMessages['editsection']; // The following messages can be used here: // * editsectionhint $veEditSectionHint = $tabMessages['editsectionhint']; $attribs = $result['editsection']['attribs']; // class goes to SkinComponentLink which will accept a string or // an array, and either might be provided at this point. $class = $attribs['class'] ?? ''; if ( is_array( $class ) ) { $class[] = 'mw-editsection-visualeditor'; } else { $class .= ' mw-editsection-visualeditor'; } $attribs['class'] = $class; $attribs['title'] = $skin->msg( $veEditSectionHint ) ->plaintextParams( $tooltip ) ->inLanguage( $lang )->text(); $veLink = [ 'text' => $skin->msg( $veEditSection )->inLanguage( $lang )->text(), 'icon' => $skinHasEditIcons ? 'edit' : null, 'targetTitle' => $title, 'attribs' => $attribs, 'query' => [ 'veaction' => 'edit', 'section' => $section ], 'options' => [ 'noclasses', 'known' ] ]; // Change icon $result['editsection']['icon'] = $skinHasEditIcons ? 'wikiText' : null; $result['veeditsection'] = $veLink; if ( $config->get( 'VisualEditorTabPosition' ) === 'before' ) { krsort( $result ); // TODO: This will probably cause weird ordering if any other extensions added something // already. // ... wfArrayInsertBefore? } } } /** * @param OutputPage $out * @param Skin $sk * @param string[] &$bodyAttrs */ public function onOutputPageBodyAttributes( $out, $sk, &$bodyAttrs ): void { $specialTitle = $sk->getTitle(); // HACK: Replace classes generated by Skin::getPageClasses as if an article title // was passed in, instead of a special page. if ( $specialTitle && $specialTitle->isSpecial( 'CollabPad' ) ) { $articleTitle = Title::newFromText( 'DummyPage' ); $specialClasses = $sk->getPageClasses( $specialTitle ); $articleClasses = $sk->getPageClasses( $articleTitle ); $bodyAttrs['class'] = str_replace( $specialClasses, $articleClasses, $bodyAttrs['class'] ); } } /** * Handler for the GetPreferences hook, to add and hide user preferences as configured * * @param User $user * @param array &$preferences Their preferences object */ public function onGetPreferences( $user, &$preferences ) { $services = MediaWikiServices::getInstance(); $userOptionsLookup = $services->getUserOptionsLookup(); $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' ); $isBeta = $veConfig->get( 'VisualEditorEnableBetaFeature' ); // Use the old preference keys to avoid having to migrate data for now. // (One day we might write and run a maintenance script to update the // entries in the database and make this unnecessary.) (T344762) if ( $isBeta ) { $preferences['visualeditor-enable'] = [ 'type' => 'toggle', 'label-message' => 'visualeditor-preference-visualeditor', 'section' => 'editing/editor', ]; } else { $preferences['visualeditor-betatempdisable'] = [ 'invert' => true, 'type' => 'toggle', 'label-message' => 'visualeditor-preference-visualeditor', 'section' => 'editing/editor', 'default' => $userOptionsLookup->getOption( $user, 'visualeditor-betatempdisable' ) || $userOptionsLookup->getOption( $user, 'visualeditor-autodisable' ) ]; } if ( $veConfig->get( 'VisualEditorEnableWikitext' ) ) { $preferences['visualeditor-newwikitext'] = [ 'type' => 'toggle', 'label-message' => 'visualeditor-preference-newwikitexteditor-enable', 'help-message' => 'visualeditor-preference-newwikitexteditor-help', 'section' => 'editing/editor' ]; } // Config option for Single Edit Tab if ( $veConfig->get( 'VisualEditorUseSingleEditTab' ) && self::enabledForUser( $user ) ) { $preferences['visualeditor-tabs'] = [ 'type' => 'select', 'label-message' => 'visualeditor-preference-tabs', 'section' => 'editing/editor', 'options-messages' => [ 'visualeditor-preference-tabs-remember-last' => 'remember-last', 'visualeditor-preference-tabs-prefer-ve' => 'prefer-ve', 'visualeditor-preference-tabs-prefer-wt' => 'prefer-wt', 'visualeditor-preference-tabs-multi-tab' => 'multi-tab' ] ]; } $api = [ 'type' => 'api' ]; // The "autodisable" preference records whether the user has explicitly opted out of VE. // This is saved even when VE is off by default, which allows changing it to be on by default // without affecting the users who opted out. There's also a maintenance script to silently // opt-out existing users en masse before changing the default, thus only affecting new users. // (This option is no longer set to 'true' anywhere, but we can still encounter old true // values until they are migrated: T344760.) $preferences['visualeditor-autodisable'] = $api; // The diff mode is persisted for each editor mode separately, // e.g. use visual diffs for visual mode only. $preferences['visualeditor-diffmode-source'] = $api; $preferences['visualeditor-diffmode-visual'] = $api; $preferences['visualeditor-diffmode-historical'] = $api; $preferences['visualeditor-editor'] = $api; $preferences['visualeditor-hidebetawelcome'] = $api; $preferences['visualeditor-hidetabdialog'] = $api; $preferences['visualeditor-hidesourceswitchpopup'] = $api; $preferences['visualeditor-hidevisualswitchpopup'] = $api; $preferences['visualeditor-hideusered'] = $api; $preferences['visualeditor-findAndReplace-diacritic'] = $api; $preferences['visualeditor-findAndReplace-findText'] = $api; $preferences['visualeditor-findAndReplace-replaceText'] = $api; $preferences['visualeditor-findAndReplace-regex'] = $api; $preferences['visualeditor-findAndReplace-matchCase'] = $api; $preferences['visualeditor-findAndReplace-word'] = $api; } /** * Implements the PreferencesFormPreSave hook, to remove the 'autodisable' flag * when the user it was set on explicitly enables VE. * * @param array $data User-submitted data * @param HTMLForm $form A ContextSource * @param User $user User with new preferences already set * @param bool &$result Success or failure * @param array $oldUserOptions */ public function onPreferencesFormPreSave( $data, $form, $user, &$result, $oldUserOptions ) { $services = MediaWikiServices::getInstance(); $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' ); $userOptionsManager = $services->getUserOptionsManager(); $isBeta = $veConfig->get( 'VisualEditorEnableBetaFeature' ); // The "autodisable" preference records whether the user has explicitly opted out of VE // while it was in beta (which would otherwise not be saved, since it's the same as default). if ( // When the user enables VE, clear the preference. $userOptionsManager->getOption( $user, 'visualeditor-autodisable' ) && ( $isBeta ? $userOptionsManager->getOption( $user, 'visualeditor-enable' ) : !$userOptionsManager->getOption( $user, 'visualeditor-betatempdisable' ) ) ) { $userOptionsManager->setOption( $user, 'visualeditor-autodisable', false ); } } /** * @param array &$tags */ public function onChangeTagsListActive( &$tags ) { $this->onListDefinedTags( $tags ); } /** * Implements the ListDefinedTags and ChangeTagsListActive hooks, to * populate core Special:Tags with the change tags in use by VisualEditor. * * @param array &$tags Available change tags. */ public function onListDefinedTags( &$tags ) { $tags = array_merge( $tags, static::TAGS ); } /** * Adds extra variables to the page config. * * @param array &$vars Global variables object * @param OutputPage $out The page view. */ public function onMakeGlobalVariablesScript( &$vars, $out ): void { $pageLanguage = ApiVisualEditor::getPageLanguage( $out->getTitle() ); $converter = MediaWikiServices::getInstance()->getLanguageConverterFactory() ->getLanguageConverter( $pageLanguage ); $fallbacks = $converter->getVariantFallbacks( $converter->getPreferredVariant() ); $vars['wgVisualEditor'] = [ 'pageLanguageCode' => $pageLanguage->getHtmlCode(), 'pageLanguageDir' => $pageLanguage->getDir(), 'pageVariantFallbacks' => $fallbacks, ]; } /** * Adds extra variables to the global config * * @param array &$vars Global variables object * @param string $skin * @param Config $config */ public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void { $coreConfig = RequestContext::getMain()->getConfig(); $services = MediaWikiServices::getInstance(); $veConfig = $services->getConfigFactory()->makeConfig( 'visualeditor' ); $extensionRegistry = ExtensionRegistry::getInstance(); $availableNamespaces = ApiVisualEditor::getAvailableNamespaceIds( $veConfig ); $availableContentModels = array_filter( array_merge( $extensionRegistry->getAttribute( 'VisualEditorAvailableContentModels' ), $veConfig->get( 'VisualEditorAvailableContentModels' ) ) ); $namespacesWithSubpages = $coreConfig->get( 'NamespacesWithSubpages' ); // Export as a list of namespaces where subpages are enabled instead of an object // mapping namespaces to if subpages are enabled or not, so filter out disabled // namespaces and then just use the keys. See T291729. $namespacesWithSubpages = array_filter( $namespacesWithSubpages ); $namespacesWithSubpagesEnabled = array_keys( $namespacesWithSubpages ); // $wgNamespacesWithSubpages can include namespaces that don't exist, no need // to include those in the JavaScript data. See T291727. // Run this filtering after the filter for subpages being enabled, to reduce // the number of calls needed to namespace info. $nsInfo = $services->getNamespaceInfo(); $namespacesWithSubpagesEnabled = array_values( array_filter( $namespacesWithSubpagesEnabled, [ $nsInfo, 'exists' ] ) ); $defaultSortPrefix = $services->getMagicWordFactory()->get( 'defaultsort' )->getSynonym( 0 ); // Sanitize trailing colon. /languages/messages/*.php are not consistent but the // presence or absence of a trailing colon in the message makes no difference. $defaultSortPrefix = preg_replace( '/:$/', '', $defaultSortPrefix ); $vars['wgVisualEditorConfig'] = [ 'usePageImages' => $extensionRegistry->isLoaded( 'PageImages' ), 'usePageDescriptions' => $extensionRegistry->isLoaded( 'WikibaseClient' ), 'isBeta' => $veConfig->get( 'VisualEditorEnableBetaFeature' ), 'disableForAnons' => $veConfig->get( 'VisualEditorDisableForAnons' ), 'preloadModules' => $veConfig->get( 'VisualEditorPreloadModules' ), 'namespaces' => $availableNamespaces, 'contentModels' => $availableContentModels, 'pluginModules' => array_merge( $extensionRegistry->getAttribute( 'VisualEditorPluginModules' ), // @todo deprecate the global setting $veConfig->get( 'VisualEditorPluginModules' ) ), 'thumbLimits' => $coreConfig->get( 'ThumbLimits' ), 'galleryOptions' => $coreConfig->get( 'GalleryOptions' ), 'tabPosition' => $veConfig->get( 'VisualEditorTabPosition' ), 'tabMessages' => array_filter( $veConfig->get( 'VisualEditorTabMessages' ) ), 'singleEditTab' => $veConfig->get( 'VisualEditorUseSingleEditTab' ), 'enableVisualSectionEditing' => $veConfig->get( 'VisualEditorEnableVisualSectionEditing' ), 'showBetaWelcome' => $veConfig->get( 'VisualEditorShowBetaWelcome' ), 'allowExternalLinkPaste' => $veConfig->get( 'VisualEditorAllowExternalLinkPaste' ), 'enableHelpCompletion' => $veConfig->get( 'VisualEditorEnableHelpCompletion' ), 'enableTocWidget' => $veConfig->get( 'VisualEditorEnableTocWidget' ), 'enableWikitext' => $veConfig->get( 'VisualEditorEnableWikitext' ), 'useChangeTagging' => $veConfig->get( 'VisualEditorUseChangeTagging' ), 'editCheckTagging' => $veConfig->get( 'VisualEditorEditCheckTagging' ), 'editCheck' => $veConfig->get( 'VisualEditorEditCheck' ), 'editCheckABTest' => $veConfig->get( 'VisualEditorEditCheckABTest' ), 'editCheckReliabilityAvailable' => ApiEditCheckReferenceUrl::isAvailable(), 'namespacesWithSubpages' => $namespacesWithSubpagesEnabled, 'specialBooksources' => urldecode( SpecialPage::getTitleFor( 'Booksources' )->getPrefixedURL() ), 'rebaserUrl' => $coreConfig->get( 'VisualEditorRebaserURL' ), 'feedbackApiUrl' => $veConfig->get( 'VisualEditorFeedbackAPIURL' ), 'feedbackTitle' => $veConfig->get( 'VisualEditorFeedbackTitle' ), 'sourceFeedbackTitle' => $veConfig->get( 'VisualEditorSourceFeedbackTitle' ), // TODO: Remove when all usages in .js files are removed 'transclusionDialogNewSidebar' => true, 'cirrusSearchLookup' => $extensionRegistry->isLoaded( 'CirrusSearch' ), 'defaultSortPrefix' => $defaultSortPrefix, ]; } /** * Conditionally register the jquery.uls.data and jquery.i18n modules, in case they've already * been registered by the UniversalLanguageSelector extension or the TemplateData extension. * * @param ResourceLoader $resourceLoader Client-side code and assets to be loaded. */ public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void { $veResourceTemplate = [ 'localBasePath' => dirname( __DIR__ ), 'remoteExtPath' => 'VisualEditor', ]; // Only register VisualEditor core's local version of jquery.uls.data if it hasn't been // installed locally already (presumably, by the UniversalLanguageSelector extension). if ( !$resourceLoader->isModuleRegistered( 'jquery.uls.data' ) ) { $resourceLoader->register( [ 'jquery.uls.data' => $veResourceTemplate + [ 'scripts' => [ 'lib/ve/lib/jquery.uls/src/jquery.uls.data.js', 'lib/ve/lib/jquery.uls/src/jquery.uls.data.utils.js', ], ] ] ); } } /** * Ensures that we know whether we're running inside a parser test. * * @param array &$settings The settings with which MediaWiki is being run. */ public function onParserTestGlobals( &$settings ) { $settings['wgVisualEditorInParserTests'] = true; } /** * @param array &$redirectParams Parameters preserved on special page redirects * to wiki pages */ public function onRedirectSpecialArticleRedirectParams( &$redirectParams ) { $redirectParams[] = 'veaction'; } /** * If the user has specified that they want to edit the page with VE, suppress any redirect. * * @param Title $title Title being used for request * @param Article|null $article The page being viewed. * @param OutputPage $output The page view. * @param User $user The user-specific settings. * @param WebRequest $request * @param ActionEntryPoint $mediaWiki Helper class. */ public function onBeforeInitialize( $title, $article, $output, $user, $request, $mediaWiki ) { if ( $request->getVal( 'veaction' ) ) { $request->setVal( 'redirect', 'no' ); } } /** * On login, if user has a VEE cookie, set their preference equal to it. * * @param User $user The user-specific settings. */ public function onUserLoggedIn( $user ) { $cookie = RequestContext::getMain()->getRequest()->getCookie( 'VEE', '' ); if ( $user->isNamed() && ( $cookie === 'visualeditor' || $cookie === 'wikitext' ) ) { self::deferredSetUserOption( $user, 'visualeditor-editor', $cookie ); } } }