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