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