Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
21.89% |
65 / 297 |
|
5.00% |
1 / 20 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
21.89% |
65 / 297 |
|
5.00% |
1 / 20 |
4038.09 | |
0.00% |
0 / 1 |
onBeforePageDisplay | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
addSXPublishingFollowupModule | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
addMobileNewByTranslationInvitation | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
56 | |||
isMobileView | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
isSXEnabled | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
isPotentialTranslator | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
56 | |||
addModules | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
306 | |||
onGetBetaFeaturePreferences | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
onSpecialContributionsBeforeMainOutput | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
onResourceLoaderRegisterModules | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
onListDefinedTags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onChangeTagsListActive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
registerTags | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
onEditPage__showEditForm_initial | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
210 | |||
onSaveUserOptions | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
56 | |||
onBeforeCreateEchoEvent | |
100.00% |
59 / 59 |
|
100.00% |
1 / 1 |
1 | |||
onEchoGetBundleRules | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
onGetPreferences | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
devModeCallback | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onContributeCards | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | /** |
4 | * Hooks for ContentTranslation extension. |
5 | * |
6 | * @copyright See AUTHORS.txt |
7 | * @license GPL-2.0-or-later |
8 | */ |
9 | |
10 | namespace ContentTranslation; |
11 | |
12 | // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
13 | |
14 | use ContentTranslation\Service\TranslatorService; |
15 | use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook; |
16 | use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook; |
17 | use MediaWiki\Config\Config; |
18 | use MediaWiki\Context\RequestContext; |
19 | use MediaWiki\EditPage\EditPage; |
20 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
21 | use MediaWiki\Extension\Notifications\AttributeManager; |
22 | use MediaWiki\Extension\Notifications\Model\Event; |
23 | use MediaWiki\Extension\Notifications\UserLocator; |
24 | use MediaWiki\Hook\EditPage__showEditForm_initialHook; |
25 | use MediaWiki\Hook\SpecialContributionsBeforeMainOutputHook; |
26 | use MediaWiki\MediaWikiServices; |
27 | use MediaWiki\Output\Hook\BeforePageDisplayHook; |
28 | use MediaWiki\Output\OutputPage; |
29 | use MediaWiki\Permissions\PermissionManager; |
30 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
31 | use MediaWiki\Registration\ExtensionRegistry; |
32 | use MediaWiki\ResourceLoader\Context as ResourceLoaderContext; |
33 | use MediaWiki\ResourceLoader\FilePath as ResourceLoaderFilePath; |
34 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook; |
35 | use MediaWiki\ResourceLoader\ResourceLoader; |
36 | use MediaWiki\Skin\Skin; |
37 | use MediaWiki\SpecialPage\SpecialPage; |
38 | use MediaWiki\Specials\Contribute\Card\ContributeCard; |
39 | use MediaWiki\Specials\Contribute\Card\ContributeCardActionLink; |
40 | use MediaWiki\Specials\Contribute\ContributeFactory; |
41 | use MediaWiki\Specials\Contribute\Hook\ContributeCardsHook; |
42 | use MediaWiki\User\Options\Hook\SaveUserOptionsHook; |
43 | use MediaWiki\User\User; |
44 | use MediaWiki\User\UserIdentity; |
45 | use MediaWiki\WikiMap\WikiMap; |
46 | use MobileContext; |
47 | |
48 | class Hooks implements |
49 | BeforePageDisplayHook, |
50 | GetPreferencesHook, |
51 | ResourceLoaderRegisterModulesHook, |
52 | SpecialContributionsBeforeMainOutputHook, |
53 | ListDefinedTagsHook, |
54 | ChangeTagsListActiveHook, |
55 | SaveUserOptionsHook, |
56 | EditPage__showEditForm_initialHook, |
57 | ContributeCardsHook |
58 | { |
59 | /** |
60 | * @param OutputPage $out |
61 | * @param Skin $skin |
62 | */ |
63 | public function onBeforePageDisplay( $out, $skin ): void { |
64 | self::addModules( $out, $skin ); |
65 | self::addSXPublishingFollowupModule( $out, $skin ); |
66 | self::addMobileNewByTranslationInvitation( $out, $skin ); |
67 | } |
68 | |
69 | /** |
70 | * Add 'sx.publishing.followup' module when output page is an article page |
71 | * or a page in user namespace(sandbox), and "sx-published-section" query |
72 | * params exists. This is the case for redirections to target article page |
73 | * after Section Translation successful publishing |
74 | * @param OutputPage $out |
75 | * @param Skin $skin |
76 | */ |
77 | public static function addSXPublishingFollowupModule( OutputPage $out, Skin $skin ): void { |
78 | $sxPublishedQueryParam = $out->getRequest()->getVal( "sx-published-section" ); |
79 | $isContentPage = $out->getTitle()->isContentPage(); |
80 | $isSandboxPage = $out->getTitle()->inNamespace( NS_USER ); |
81 | if ( ( $isContentPage || $isSandboxPage ) && $sxPublishedQueryParam !== null ) { |
82 | $out->addModules( 'sx.publishing.followup' ); |
83 | } |
84 | } |
85 | |
86 | public static function addMobileNewByTranslationInvitation( OutputPage $out, Skin $skin ): void { |
87 | // This entrypoint should only be enabled for mobile web version |
88 | if ( !self::isMobileView() ) { |
89 | return; |
90 | } |
91 | |
92 | if ( !self::isSXEnabled() ) { |
93 | return; |
94 | } |
95 | |
96 | // This entrypoint should only be enabled for logged-in users or wikis that |
97 | // have section translation enabled for anonymous users |
98 | $user = $out->getUser(); |
99 | $isSxEnabledForAnon = $out->getConfig()->get( 'ContentTranslationEnableAnonSectionTranslation' ); |
100 | if ( !$user->isNamed() && !$isSxEnabledForAnon ) { |
101 | return; |
102 | } |
103 | |
104 | $isValidContext = !$out->getTitle()->exists() && $out->getTitle()->inNamespace( NS_MAIN ); |
105 | |
106 | if ( !$isValidContext ) { |
107 | return; |
108 | } |
109 | |
110 | $out->addModules( 'ext.cx.entrypoints.newbytranslation.mobile' ); |
111 | } |
112 | |
113 | /** |
114 | * Check whether the current context is in a mobile interface |
115 | * |
116 | * @return bool |
117 | */ |
118 | private static function isMobileView() { |
119 | $isMobileView = false; |
120 | |
121 | if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) ) { |
122 | /** @var MobileContext $mobileContext */ |
123 | $mobileContext = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
124 | $isMobileView = $mobileContext->shouldDisplayMobileView(); |
125 | } |
126 | return $isMobileView; |
127 | } |
128 | |
129 | /** |
130 | * Check whether SectionTranslation is enabled in current wiki |
131 | * |
132 | * @return bool |
133 | */ |
134 | private static function isSXEnabled() { |
135 | $out = RequestContext::getMain()->getOutput(); |
136 | $currentLanguageCode = SiteMapper::getCurrentLanguageCode(); |
137 | $enabledLanguages = $out->getConfig()->get( 'SectionTranslationTargetLanguages' ); |
138 | return is_array( $enabledLanguages ) && in_array( $currentLanguageCode, $enabledLanguages ); |
139 | } |
140 | |
141 | /** |
142 | * Check whether the current user is a potential translator |
143 | * |
144 | * @param User $user |
145 | * @return bool |
146 | */ |
147 | private static function isPotentialTranslator( User $user ) { |
148 | /** @var TranslatorService $translatorService */ |
149 | $translatorService = MediaWikiServices::getInstance()->get( 'ContentTranslation.TranslatorService' ); |
150 | |
151 | if ( $translatorService->isTranslator( $user ) ) { |
152 | // Already a translator |
153 | return true; |
154 | } |
155 | |
156 | if ( ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) ) { |
157 | $centralUser = CentralAuthUser::getInstance( $user ); |
158 | |
159 | // Check if the user has edited in more than one wiki. |
160 | $editedWikiCount = 0; |
161 | $attachedAccounts = $centralUser->queryAttached(); |
162 | foreach ( $attachedAccounts as $wikiId => $account ) { |
163 | $wikiRef = WikiMap::getWiki( $wikiId ); // Get WikiReference instance |
164 | $url = ''; |
165 | if ( $wikiRef ) { |
166 | $url = $wikiRef->getCanonicalServer(); |
167 | } |
168 | if ( |
169 | // Ignore non-wikipedia wikis such as commons, mediawiki, meta etc |
170 | // url property example "https://commons.wikimedia.org", |
171 | strpos( $url, 'wikipedia' ) !== false && |
172 | intval( $account['editCount'] ?? 0 ) > 0 |
173 | ) { |
174 | $editedWikiCount++; |
175 | } |
176 | } |
177 | |
178 | return $editedWikiCount > 1; |
179 | } |
180 | |
181 | return false; |
182 | } |
183 | |
184 | /** |
185 | * Hook: BeforePageDisplay |
186 | * @param OutputPage $out |
187 | * @param Skin $skin |
188 | */ |
189 | public static function addModules( OutputPage $out, Skin $skin ) { |
190 | global $wgContentTranslationAsBetaFeature, $wgContentTranslationCampaigns; |
191 | |
192 | $title = $out->getTitle(); |
193 | $user = $out->getUser(); |
194 | |
195 | /** @var PreferenceHelper $preferenceHelper */ |
196 | $preferenceHelper = MediaWikiServices::getInstance()->getService( 'ContentTranslation.PreferenceHelper' ); |
197 | if ( $preferenceHelper->isCXEntrypointDisabled( $user ) ) { |
198 | return; |
199 | } |
200 | |
201 | $out->addModules( 'ext.cx.eventlogging.campaigns' ); |
202 | |
203 | if ( !$title || |
204 | $title->isSpecial( 'ContentTranslation' ) |
205 | ) { |
206 | // Entry point modules need not be shown in CX special pages |
207 | return; |
208 | } |
209 | |
210 | $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); |
211 | |
212 | // Load the new article campaign for VisualEditor if it's relevant. |
213 | // Done separately from loading the newarticle campaign for the |
214 | // wiki syntax editor because of the different actions with which |
215 | // the editing page is loaded. |
216 | if ( !$preferenceHelper->isEnabledForUser( $user ) ) { |
217 | if ( |
218 | !$title->exists() && |
219 | $wgContentTranslationCampaigns['newarticle'] && |
220 | !$out->getRequest()->getCookie( 'cx_campaign_newarticle_hide', '' ) && |
221 | $title->inNamespace( NS_MAIN ) && |
222 | $user->isRegistered() && |
223 | $permissionManager->userCan( 'edit', $user, $title, PermissionManager::RIGOR_QUICK ) |
224 | ) { |
225 | $out->addModules( 'ext.cx.entrypoints.newarticle.veloader' ); |
226 | } |
227 | |
228 | return; |
229 | } |
230 | |
231 | if ( $title->inNamespace( NS_MAIN ) && |
232 | $out->getActionName() === 'view' && |
233 | $title->exists() && |
234 | in_array( $skin->getSkinName(), [ 'vector', 'vector-2022' ] ) |
235 | ) { |
236 | $out->addJsConfigVars( [ |
237 | 'wgContentTranslationAsBetaFeature' => |
238 | $wgContentTranslationAsBetaFeature, |
239 | ] ); |
240 | $out->addModules( 'ext.cx.interlanguagelink.init' ); |
241 | } |
242 | |
243 | // Add translations entry for the personal menu |
244 | if ( !self::isMobileView() && in_array( $skin->getSkinName(), [ 'vector-2022' ] ) ) { |
245 | $out->addModules( 'ext.cx.entrypoints.contributionsmenu' ); |
246 | } |
247 | } |
248 | |
249 | /** |
250 | * Hook: GetBetaFeaturePreferences |
251 | * @param User $user |
252 | * @param array[] &$prefs |
253 | */ |
254 | public static function onGetBetaFeaturePreferences( User $user, array &$prefs ) { |
255 | global $wgExtensionAssetsPath, $wgContentTranslationAsBetaFeature; |
256 | |
257 | if ( !$wgContentTranslationAsBetaFeature ) { |
258 | return; |
259 | } |
260 | |
261 | $imageDir = "$wgExtensionAssetsPath/ContentTranslation/images"; |
262 | |
263 | $prefs['cx'] = [ |
264 | 'label-message' => 'cx-beta', |
265 | 'desc-message' => 'cx-beta-desc', |
266 | 'screenshot' => [ |
267 | 'ltr' => "$imageDir/cx-icon-ltr.svg", |
268 | 'rtl' => "$imageDir/cx-icon-rtl.svg", |
269 | ], |
270 | 'info-link' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Content_translation', |
271 | 'discussion-link' => 'https://www.mediawiki.org/wiki/Talk:Content_translation', |
272 | 'requirements' => [ |
273 | 'javascript' => true, |
274 | ] |
275 | ]; |
276 | } |
277 | |
278 | /** |
279 | * Hook: SpecialContributionsBeforeMainOutput |
280 | * @param int $id |
281 | * @param UserIdentity $user |
282 | * @param SpecialPage $page |
283 | */ |
284 | public function onSpecialContributionsBeforeMainOutput( $id, $user, $page ) { |
285 | $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user ); |
286 | if ( !$user->isNamed() ) { |
287 | return; |
288 | } |
289 | |
290 | /** @var PreferenceHelper $preferenceHelper */ |
291 | $preferenceHelper = MediaWikiServices::getInstance()->getService( 'ContentTranslation.PreferenceHelper' ); |
292 | |
293 | if ( $user->getId() !== $page->getUser()->getId() || !$preferenceHelper->isEnabledForUser( $user ) ) { |
294 | return; |
295 | } |
296 | |
297 | if ( $preferenceHelper->isCXEntrypointDisabled( $user ) ) { |
298 | return; |
299 | } |
300 | |
301 | if ( self::isMobileView() ) { |
302 | // Contribution buttons should be shown only in desktop |
303 | return; |
304 | } |
305 | |
306 | $modules = [ 'ext.cx.eventlogging.campaigns' ]; |
307 | |
308 | $isSpecialContributeEnabled = ContributeFactory::isEnabledOnCurrentSkin( |
309 | $page->getSkin(), |
310 | $page->getConfig()->get( 'SpecialContributeSkinsEnabled' ) |
311 | ); |
312 | |
313 | if ( !$isSpecialContributeEnabled ) { |
314 | $modules[] = 'ext.cx.contributions'; |
315 | } |
316 | $page->getOutput()->addModules( $modules ); |
317 | } |
318 | |
319 | /** |
320 | * Hook: ResourceLoaderRegisterModules |
321 | * |
322 | * @param ResourceLoader $resourceLoader Client-side code and assets to be loaded. |
323 | */ |
324 | public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void { |
325 | $cxResourceTemplate = [ |
326 | 'localBasePath' => dirname( __DIR__ ), |
327 | 'remoteExtPath' => 'ContentTranslation', |
328 | ]; |
329 | |
330 | $externalMessages = []; |
331 | $extReg = ExtensionRegistry::getInstance(); |
332 | if ( $extReg->isLoaded( 'ConfirmEdit' ) ) { |
333 | $externalMessages[] = 'captcha-create'; |
334 | $externalMessages[] = 'captcha-label'; |
335 | |
336 | if ( $extReg->isLoaded( 'QuestyCaptcha' ) ) { |
337 | $externalMessages[] = 'questycaptcha-create'; |
338 | } |
339 | |
340 | if ( $extReg->isLoaded( 'FancyCaptcha' ) ) { |
341 | $externalMessages[] = 'fancycaptcha-create'; |
342 | $externalMessages[] = 'fancycaptcha-reload-text'; |
343 | } |
344 | } |
345 | |
346 | $resourceLoader->register( [ |
347 | 'mw.cx.externalmessages' => $cxResourceTemplate + [ |
348 | 'messages' => $externalMessages, |
349 | ] |
350 | ] ); |
351 | } |
352 | |
353 | /** |
354 | * Hooks: ListDefinedTags |
355 | * Define the content translation change tag |
356 | * @param array &$tags |
357 | */ |
358 | public function onListDefinedTags( &$tags ) { |
359 | self::registerTags( $tags ); |
360 | } |
361 | |
362 | /** |
363 | * Hooks: ChangeTagsListActive |
364 | * Mart the content translation change tag as active |
365 | * @param array &$tags |
366 | */ |
367 | public function onChangeTagsListActive( &$tags ) { |
368 | self::registerTags( $tags ); |
369 | } |
370 | |
371 | public static function registerTags( array &$tags ) { |
372 | global $wgContentTranslationCampaigns; |
373 | $tags[] = 'contenttranslation'; |
374 | $tags[] = 'contenttranslation-v2'; // CX2 distinct tag. Used since 2018-09 |
375 | $tags[] = 'sectiontranslation'; |
376 | $tags[] = 'contenttranslation-high-unmodified-mt-text'; |
377 | foreach ( $wgContentTranslationCampaigns as $tagName => $tag ) { |
378 | if ( isset( $tag['edittag'] ) ) { |
379 | $tags[] = $tag['edittag']; |
380 | } |
381 | } |
382 | } |
383 | |
384 | /** |
385 | * Hook: EditPage::showEditForm:initial |
386 | * @param EditPage $newPage |
387 | * @param OutputPage $out |
388 | */ |
389 | public function onEditPage__showEditForm_initial( $newPage, $out ) { |
390 | global $wgContentTranslationAsBetaFeature, $wgContentTranslationCampaigns; |
391 | |
392 | $user = $out->getUser(); |
393 | /** @var PreferenceHelper $preferenceHelper */ |
394 | $preferenceHelper = MediaWikiServices::getInstance()->getService( 'ContentTranslation.PreferenceHelper' ); |
395 | if ( $preferenceHelper->isCXEntrypointDisabled( $user ) ) { |
396 | return; |
397 | } |
398 | |
399 | $isValidEditContext = $user->isRegistered() && |
400 | !$newPage->getTitle()->exists() && |
401 | $newPage->getTitle()->inNamespace( NS_MAIN ); |
402 | |
403 | if ( !$isValidEditContext ) { |
404 | return; |
405 | } |
406 | |
407 | $veConfig = MediaWikiServices::getInstance()->getConfigFactory() |
408 | ->makeConfig( 'visualeditor' ); |
409 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
410 | if ( $veConfig->get( 'VisualEditorShowBetaWelcome' ) && |
411 | !$userOptionsLookup->getOption( $user, 'visualeditor-hidebetawelcome' ) |
412 | ) { |
413 | // VisualEditorShowBetaWelcome is enabled and user has not |
414 | // seen the visualeditor yet. So when edit page is loaded |
415 | // VE user guiding dialogs will appear. We don't want to mix |
416 | // that with our invitations. |
417 | return; |
418 | } |
419 | |
420 | if ( $wgContentTranslationAsBetaFeature === false && |
421 | // CX is enabled for everybody. Not a beta feature. |
422 | self::isPotentialTranslator( $user ) |
423 | ) { |
424 | $modules = [ 'ext.cx.eventlogging.campaigns' ]; |
425 | |
426 | // "firsttime" property is true the first time the edit form is rendered, |
427 | // and it's false after re-rendering with preview, diff, save prompts, etc. |
428 | // Here we only want to display the invitation when not in "preview" or "diff" mode. |
429 | if ( $newPage->firsttime ) { |
430 | $modules[] = 'ext.cx.entrypoints.newbytranslation'; |
431 | } |
432 | |
433 | $out->addModules( $modules ); |
434 | $invitationShown = $preferenceHelper->getGlobalPreference( |
435 | $user, |
436 | 'cx_campaign_newarticle_shown' |
437 | ); |
438 | /** @var TranslatorService $translatorService */ |
439 | $translatorService = MediaWikiServices::getInstance()->get( 'ContentTranslation.TranslatorService' ); |
440 | $existingTranslator = $translatorService->isTranslator( $user ); |
441 | $out->addJsConfigVars( [ |
442 | 'wgContentTranslationNewByTranslationShown' => $invitationShown, |
443 | 'wgContentTranslationExistingTranslator' => $existingTranslator, |
444 | ] ); |
445 | return; |
446 | } |
447 | |
448 | if ( $wgContentTranslationAsBetaFeature && |
449 | // CX is a beta feature |
450 | !$preferenceHelper->isBetaFeatureEnabled( $user ) && |
451 | $wgContentTranslationCampaigns['newarticle'] && |
452 | // The below cookie reading does not use default cookie prefix for historical reasons |
453 | !$out->getRequest()->getCookie( 'cx_campaign_newarticle_hide', '' ) |
454 | ) { |
455 | // CX is a beta feature in this wiki and user has not enabled it. |
456 | $out->addModules( [ |
457 | 'ext.cx.entrypoints.newarticle', |
458 | 'ext.cx.eventlogging.campaigns' |
459 | ] ); |
460 | } |
461 | } |
462 | |
463 | /** |
464 | * Hook: User::SaveUserOptions |
465 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/SaveUserOptions |
466 | * |
467 | * @param UserIdentity $user |
468 | * @param array &$modifiedOptions |
469 | * @param array $originalOptions |
470 | */ |
471 | public function onSaveUserOptions( UserIdentity $user, array &$modifiedOptions, array $originalOptions ) { |
472 | $out = RequestContext::getMain()->getOutput(); |
473 | |
474 | $mergedOptions = array_merge( $originalOptions, $modifiedOptions ); |
475 | |
476 | if ( !isset( $mergedOptions['cx'] ) || $mergedOptions['cx'] !== 1 ) { |
477 | // Not using ContentTranslation; bail. |
478 | return; |
479 | } |
480 | |
481 | if ( isset( $mergedOptions['cx-know'] ) ) { |
482 | // The auto-open contribution menu has already been shown; bail. |
483 | return; |
484 | } |
485 | |
486 | $title = $out->getTitle(); |
487 | if ( $title && $title->isSpecial( 'ContentTranslation' ) ) { |
488 | // Don't show the menu on Special:ContentTranslation. |
489 | return; |
490 | } |
491 | |
492 | // Show the auto-open contribution menu and set the cx-know preference |
493 | // as true to prevent it from being automatically shown in the future. |
494 | if ( !self::isMobileView() ) { |
495 | $out->addModules( [ |
496 | 'ext.cx.betafeature.init', |
497 | 'ext.cx.entrypoints.contributionsmenu', |
498 | ] ); |
499 | } |
500 | $modifiedOptions['cx-know'] = true; |
501 | } |
502 | |
503 | /** |
504 | * Add notification events to Echo |
505 | * |
506 | * @param array &$notifications array of Echo notifications |
507 | * @param array &$notificationCategories array of Echo notification categories |
508 | * @param array &$icons array of icon details |
509 | */ |
510 | public static function onBeforeCreateEchoEvent( |
511 | &$notifications, |
512 | &$notificationCategories, |
513 | &$icons |
514 | ) { |
515 | $notificationCategories['cx'] = [ |
516 | 'priority' => 3, |
517 | 'tooltip' => 'echo-pref-tooltip-cx', |
518 | ]; |
519 | |
520 | $userLocator = [ |
521 | AttributeManager::ATTR_LOCATORS => [ |
522 | [ |
523 | [ UserLocator::class, 'locateFromEventExtra' ], |
524 | [ 'recipient' ] |
525 | ], |
526 | ], |
527 | ]; |
528 | |
529 | $notifications['cx-first-translation'] = [ |
530 | 'category' => 'cx', |
531 | 'group' => 'positive', |
532 | 'section' => 'message', |
533 | 'presentation-model' => EchoNotificationPresentationModel::class, |
534 | ] + $userLocator; |
535 | |
536 | $notifications['cx-tenth-translation'] = [ |
537 | 'category' => 'cx', |
538 | 'group' => 'positive', |
539 | 'section' => 'message', |
540 | 'presentation-model' => EchoNotificationPresentationModel::class, |
541 | ] + $userLocator; |
542 | |
543 | $notifications['cx-hundredth-translation'] = [ |
544 | 'category' => 'cx', |
545 | 'group' => 'positive', |
546 | 'section' => 'message', |
547 | 'presentation-model' => EchoNotificationPresentationModel::class, |
548 | ] + $userLocator; |
549 | |
550 | $notifications['cx-suggestions-available'] = [ |
551 | 'category' => 'cx', |
552 | 'group' => 'positive', |
553 | 'section' => 'message', |
554 | 'presentation-model' => EchoNotificationPresentationModel::class, |
555 | ] + $userLocator; |
556 | |
557 | $notifications['cx-deleted-draft'] = [ |
558 | 'category' => 'cx', |
559 | 'group' => 'negative', |
560 | 'section' => 'message', |
561 | 'presentation-model' => DraftNotificationPresentationModel::class, |
562 | 'bundle' => [ 'web' => true, 'expandable' => true ] |
563 | ] + $userLocator; |
564 | |
565 | $notifications['cx-continue-translation'] = [ |
566 | 'category' => 'cx', |
567 | 'group' => 'positive', |
568 | 'section' => 'message', |
569 | 'presentation-model' => DraftNotificationPresentationModel::class, |
570 | 'bundle' => [ 'web' => true, 'expandable' => true ] |
571 | ] + $userLocator; |
572 | |
573 | $icons['cx'] = [ |
574 | 'path' => 'ContentTranslation/images/cx-notification-green.svg', |
575 | ]; |
576 | $icons['cx-blue'] = [ |
577 | 'path' => 'ContentTranslation/images/cx-notification-blue.svg' |
578 | ]; |
579 | $icons['outdated'] = [ |
580 | 'path' => 'ContentTranslation/images/cx-notification-gray.svg' |
581 | ]; |
582 | } |
583 | |
584 | /** |
585 | * Set bundle for message |
586 | * |
587 | * @param Event $event |
588 | * @param string &$bundleString |
589 | */ |
590 | public static function onEchoGetBundleRules( $event, &$bundleString ) { |
591 | $recipient = $event->getExtraParam( 'recipient' ); |
592 | if ( !$recipient ) { |
593 | return; |
594 | } |
595 | |
596 | if ( $event->getType() === 'cx-deleted-draft' ) { |
597 | $bundleString = 'cx-deleted-draft-' . $recipient; |
598 | } |
599 | |
600 | if ( $event->getType() === 'cx-continue-translation' ) { |
601 | $bundleString = 'cx-continue-translation-' . $recipient; |
602 | } |
603 | } |
604 | |
605 | /** |
606 | * Hook: Preferences::GetPreferences |
607 | * @param User $user |
608 | * @param array &$preferences |
609 | */ |
610 | public function onGetPreferences( $user, &$preferences ) { |
611 | global $wgContentTranslationAsBetaFeature; |
612 | |
613 | if ( $wgContentTranslationAsBetaFeature === false ) { |
614 | $preferences['cx-enable-entrypoints'] = [ |
615 | 'type' => 'check', |
616 | 'section' => 'rendering/languages', |
617 | 'label-message' => [ |
618 | 'cx-preference-enable-entrypoints', |
619 | 'mediawikiwiki:Special:MyLanguage/Help:Content_translation/Starting' |
620 | ] |
621 | ]; |
622 | } |
623 | |
624 | $preferences['cx-entrypoint-fd-status'] = [ |
625 | 'type' => 'api', |
626 | ]; |
627 | $preferences['cx_campaign_newarticle_shown'] = [ |
628 | 'type' => 'api', |
629 | ]; |
630 | |
631 | $preferences['cx-dashboard'] = [ |
632 | 'type' => 'api', |
633 | ]; |
634 | } |
635 | |
636 | /** |
637 | * Integrate Vite's HMR based development workflow if enabled by configuration. |
638 | * |
639 | * @param ResourceLoaderContext $context |
640 | * @param Config $config |
641 | * @param array $paths |
642 | * @return ResourceLoaderFilePath |
643 | */ |
644 | public static function devModeCallback( ResourceLoaderContext $context, Config $config, array $paths ) { |
645 | [ $buildPath, $devPath ] = $paths; |
646 | $file = $buildPath; |
647 | if ( $config->get( 'ContentTranslationDevMode' ) ) { |
648 | $file = $devPath; |
649 | } |
650 | return new ResourceLoaderFilePath( $file ); |
651 | } |
652 | |
653 | /** |
654 | * Add a persistent contribution entry point for creating translations |
655 | * Hook: ContributeCards |
656 | * @param array &$cards List of contribute cards data |
657 | */ |
658 | public function onContributeCards( array &$cards ): void { |
659 | $context = RequestContext::getMain(); |
660 | |
661 | if ( self::isMobileView() && !self::isSXEnabled() ) { |
662 | // This entrypoint should only be enabled for wikis that have SectionTranslation enabled |
663 | return; |
664 | } |
665 | |
666 | $cards[] = ( new ContributeCard( |
667 | $context->msg( 'cx-contributecard-entrypoint-title' )->text(), |
668 | $context->msg( 'cx-contributecard-entrypoint-desc' )->text(), |
669 | 'language', // icon |
670 | new ContributeCardActionLink( |
671 | // The CX beta feature is automatically enabled, when a valid campaign param exists. |
672 | // This enablement is done by a call to "SpecialContentTranslation::enableCXBetaFeature" method |
673 | SpecialPage::getTitleFor( 'ContentTranslation' ) |
674 | ->getLocalUrl( [ 'campaign' => 'specialcontribute' ] ), |
675 | $context->msg( 'cx-contributecard-entrypoint-cta' )->text(), |
676 | ) |
677 | ) )->toArray(); |
678 | // 'language' icon is in oojs-ui.styles.icons-editing-advanced RL module. Load that. |
679 | $out = $context->getOutput(); |
680 | $out->addModuleStyles( [ 'oojs-ui.styles.icons-editing-advanced' ] ); |
681 | } |
682 | |
683 | } |