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