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