Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
13.45% |
92 / 684 |
|
0.00% |
0 / 35 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
13.45% |
92 / 684 |
|
0.00% |
0 / 35 |
22133.90 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
2 | |||
onUserGetDefaultOptions | |
79.59% |
39 / 49 |
|
0.00% |
0 / 1 |
8.54 | |||
initEchoExtension | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
56 | |||
onResourceLoaderRegisterModules | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
onGetPreferences | |
0.00% |
0 / 133 |
|
0.00% |
0 / 1 |
702 | |||
onPreferencesGetIcon | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isEmailChangeAllowed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onPageSaveComplete | |
50.85% |
30 / 59 |
|
0.00% |
0 / 1 |
33.07 | |||
getEditCount | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
onLocalUserCreated | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
onUserGroupsChanged | |
60.53% |
23 / 38 |
|
0.00% |
0 / 1 |
13.98 | |||
onLinksUpdateComplete | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
210 | |||
onBeforePageDisplay | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
processMarkAsRead | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
210 | |||
shouldDisplayTalkAlert | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
onSkinTemplateNavigation__Universal | |
0.00% |
0 / 102 |
|
0.00% |
0 / 1 |
462 | |||
onAbortTalkPageEmailNotification | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onSendWatchlistEmailNotification | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
90 | |||
onOutputPageCheckLastModified | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
onGetNewMessagesAlert | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
onRollbackComplete | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
onUserSaveSettings | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getVirtualUserOptions | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
onLoadUserOptions | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
onSaveUserOptions | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
mapToInt | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
onUserClearNewTalkNotification | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onEmailUserComplete | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
onLoginFormValidErrorMessages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConfigVars | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
onArticleDeleteComplete | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onArticleUndelete | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
onSpecialMuteModifyFormFields | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
onRecentChange_save | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
onApiMain__ModuleManager | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Notifications; |
4 | |
5 | use ApiModuleManager; |
6 | use Content; |
7 | use EchoAttributeManager; |
8 | use EchoUserLocator; |
9 | use EmailNotification; |
10 | use ExtensionRegistry; |
11 | use HTMLCheckMatrix; |
12 | use IBufferingStatsdDataFactory; |
13 | use Language; |
14 | use LogEntry; |
15 | use LogicException; |
16 | use MailAddress; |
17 | use MediaWiki\Api\Hook\ApiMain__moduleManagerHook; |
18 | use MediaWiki\Auth\AuthManager; |
19 | use MediaWiki\Auth\Hook\LocalUserCreatedHook; |
20 | use MediaWiki\Config\Config; |
21 | use MediaWiki\DAO\WikiAwareEntity; |
22 | use MediaWiki\Deferred\DeferredUpdates; |
23 | use MediaWiki\Deferred\LinksUpdate\LinksUpdate; |
24 | use MediaWiki\Extension\Notifications\Controller\ModerationController; |
25 | use MediaWiki\Extension\Notifications\Controller\NotificationController; |
26 | use MediaWiki\Extension\Notifications\Formatters\EchoEventPresentationModel; |
27 | use MediaWiki\Extension\Notifications\Hooks\HookRunner; |
28 | use MediaWiki\Extension\Notifications\Mapper\EventMapper; |
29 | use MediaWiki\Extension\Notifications\Mapper\NotificationMapper; |
30 | use MediaWiki\Extension\Notifications\Model\Event; |
31 | use MediaWiki\Extension\Notifications\Model\Notification; |
32 | use MediaWiki\Extension\Notifications\Push\Api\ApiEchoPushSubscriptions; |
33 | use MediaWiki\Hook\AbortTalkPageEmailNotificationHook; |
34 | use MediaWiki\Hook\BeforePageDisplayHook; |
35 | use MediaWiki\Hook\EmailUserCompleteHook; |
36 | use MediaWiki\Hook\GetNewMessagesAlertHook; |
37 | use MediaWiki\Hook\LinksUpdateCompleteHook; |
38 | use MediaWiki\Hook\LoginFormValidErrorMessagesHook; |
39 | use MediaWiki\Hook\OutputPageCheckLastModifiedHook; |
40 | use MediaWiki\Hook\PreferencesGetIconHook; |
41 | use MediaWiki\Hook\RecentChange_saveHook; |
42 | use MediaWiki\Hook\SendWatchlistEmailNotificationHook; |
43 | use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook; |
44 | use MediaWiki\Hook\SpecialMuteModifyFormFieldsHook; |
45 | use MediaWiki\HookContainer\HookContainer; |
46 | use MediaWiki\Linker\LinkRenderer; |
47 | use MediaWiki\Logger\LoggerFactory; |
48 | use MediaWiki\MainConfigNames; |
49 | use MediaWiki\MediaWikiServices; |
50 | use MediaWiki\Output\OutputPage; |
51 | use MediaWiki\Page\Hook\ArticleDeleteCompleteHook; |
52 | use MediaWiki\Page\Hook\ArticleUndeleteHook; |
53 | use MediaWiki\Page\Hook\RollbackCompleteHook; |
54 | use MediaWiki\Permissions\PermissionManager; |
55 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
56 | use MediaWiki\Preferences\MultiTitleFilter; |
57 | use MediaWiki\Preferences\MultiUsernameFilter; |
58 | use MediaWiki\Request\WebRequest; |
59 | use MediaWiki\ResourceLoader as RL; |
60 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook; |
61 | use MediaWiki\ResourceLoader\ResourceLoader; |
62 | use MediaWiki\Revision\RevisionRecord; |
63 | use MediaWiki\Revision\RevisionStore; |
64 | use MediaWiki\SpecialPage\SpecialPage; |
65 | use MediaWiki\Storage\EditResult; |
66 | use MediaWiki\Storage\Hook\PageSaveCompleteHook; |
67 | use MediaWiki\Title\NamespaceInfo; |
68 | use MediaWiki\Title\Title; |
69 | use MediaWiki\User\CentralId\CentralIdLookup; |
70 | use MediaWiki\User\Hook\UserClearNewTalkNotificationHook; |
71 | use MediaWiki\User\Hook\UserGetDefaultOptionsHook; |
72 | use MediaWiki\User\Hook\UserGroupsChangedHook; |
73 | use MediaWiki\User\Hook\UserSaveSettingsHook; |
74 | use MediaWiki\User\Options\Hook\LoadUserOptionsHook; |
75 | use MediaWiki\User\Options\Hook\SaveUserOptionsHook; |
76 | use MediaWiki\User\Options\UserOptionsManager; |
77 | use MediaWiki\User\TalkPageNotificationManager; |
78 | use MediaWiki\User\User; |
79 | use MediaWiki\User\UserEditTracker; |
80 | use MediaWiki\User\UserFactory; |
81 | use MediaWiki\User\UserIdentity; |
82 | use MediaWiki\WikiMap\WikiMap; |
83 | use RecentChange; |
84 | use Skin; |
85 | use SkinTemplate; |
86 | use WikiPage; |
87 | |
88 | class Hooks implements |
89 | AbortTalkPageEmailNotificationHook, |
90 | ApiMain__moduleManagerHook, |
91 | ArticleDeleteCompleteHook, |
92 | ArticleUndeleteHook, |
93 | BeforePageDisplayHook, |
94 | EmailUserCompleteHook, |
95 | GetNewMessagesAlertHook, |
96 | GetPreferencesHook, |
97 | LinksUpdateCompleteHook, |
98 | LoadUserOptionsHook, |
99 | LocalUserCreatedHook, |
100 | LoginFormValidErrorMessagesHook, |
101 | OutputPageCheckLastModifiedHook, |
102 | PageSaveCompleteHook, |
103 | PreferencesGetIconHook, |
104 | RecentChange_saveHook, |
105 | ResourceLoaderRegisterModulesHook, |
106 | RollbackCompleteHook, |
107 | SaveUserOptionsHook, |
108 | SendWatchlistEmailNotificationHook, |
109 | SkinTemplateNavigation__UniversalHook, |
110 | UserClearNewTalkNotificationHook, |
111 | UserGetDefaultOptionsHook, |
112 | UserGroupsChangedHook, |
113 | UserSaveSettingsHook, |
114 | SpecialMuteModifyFormFieldsHook |
115 | { |
116 | private AuthManager $authManager; |
117 | private CentralIdLookup $centralIdLookup; |
118 | private Config $config; |
119 | private EchoAttributeManager $attributeManager; |
120 | private HookContainer $hookContainer; |
121 | private Language $contentLanguage; |
122 | private LinkRenderer $linkRenderer; |
123 | private NamespaceInfo $namespaceInfo; |
124 | private PermissionManager $permissionManager; |
125 | private RevisionStore $revisionStore; |
126 | private IBufferingStatsdDataFactory $statsdDataFactory; |
127 | private TalkPageNotificationManager $talkPageNotificationManager; |
128 | private UserEditTracker $userEditTracker; |
129 | private UserFactory $userFactory; |
130 | private UserOptionsManager $userOptionsManager; |
131 | |
132 | private static array $revertedRevIds = []; |
133 | |
134 | public function __construct( |
135 | AuthManager $authManager, |
136 | CentralIdLookup $centralIdLookup, |
137 | Config $config, |
138 | EchoAttributeManager $attributeManager, |
139 | HookContainer $hookContainer, |
140 | Language $contentLanguage, |
141 | LinkRenderer $linkRenderer, |
142 | NamespaceInfo $namespaceInfo, |
143 | PermissionManager $permissionManager, |
144 | RevisionStore $revisionStore, |
145 | IBufferingStatsdDataFactory $statsdDataFactory, |
146 | TalkPageNotificationManager $talkPageNotificationManager, |
147 | UserEditTracker $userEditTracker, |
148 | UserFactory $userFactory, |
149 | UserOptionsManager $userOptionsManager |
150 | ) { |
151 | $this->authManager = $authManager; |
152 | $this->centralIdLookup = $centralIdLookup; |
153 | $this->config = $config; |
154 | $this->attributeManager = $attributeManager; |
155 | $this->hookContainer = $hookContainer; |
156 | $this->contentLanguage = $contentLanguage; |
157 | $this->linkRenderer = $linkRenderer; |
158 | $this->namespaceInfo = $namespaceInfo; |
159 | $this->permissionManager = $permissionManager; |
160 | $this->revisionStore = $revisionStore; |
161 | $this->statsdDataFactory = $statsdDataFactory; |
162 | $this->talkPageNotificationManager = $talkPageNotificationManager; |
163 | $this->userEditTracker = $userEditTracker; |
164 | $this->userFactory = $userFactory; |
165 | $this->userOptionsManager = $userOptionsManager; |
166 | } |
167 | |
168 | /** |
169 | * @param array &$defaults |
170 | */ |
171 | public function onUserGetDefaultOptions( &$defaults ) { |
172 | if ( $this->config->get( MainConfigNames::AllowHTMLEmail ) ) { |
173 | $defaults['echo-email-format'] = 'html'; |
174 | } else { |
175 | $defaults['echo-email-format'] = 'plain-text'; |
176 | } |
177 | |
178 | $presets = [ |
179 | // Set all of the events to notify by web but not email by default |
180 | // (won't affect events that don't email) |
181 | 'default' => [ |
182 | 'email' => false, |
183 | 'web' => true, |
184 | ], |
185 | // most settings default to web on, email off, but override these |
186 | 'system' => [ |
187 | 'email' => true, |
188 | ], |
189 | 'user-rights' => [ |
190 | 'email' => true, |
191 | ], |
192 | 'article-linked' => [ |
193 | 'web' => false, |
194 | ], |
195 | 'mention-failure' => [ |
196 | 'web' => false, |
197 | ], |
198 | 'mention-success' => [ |
199 | 'web' => false, |
200 | ], |
201 | 'watchlist' => [ |
202 | 'web' => false, |
203 | ], |
204 | 'minor-watchlist' => [ |
205 | 'web' => false, |
206 | ], |
207 | ]; |
208 | |
209 | $echoPushEnabled = $this->config->get( ConfigNames::EnablePush ); |
210 | if ( $echoPushEnabled ) { |
211 | $presets['default']['push'] = true; |
212 | $presets['article-linked']['push'] = false; |
213 | $presets['mention-failure']['push'] = false; |
214 | $presets['mention-success']['push'] = false; |
215 | $presets['watchlist']['push'] = false; |
216 | $presets['minor-watchlist']['push'] = false; |
217 | } |
218 | |
219 | foreach ( $this->config->get( ConfigNames::NotificationCategories ) as $category => $categoryData ) { |
220 | if ( !isset( $defaults["echo-subscriptions-email-{$category}"] ) ) { |
221 | $defaults["echo-subscriptions-email-{$category}"] = $presets[$category]['email'] |
222 | ?? $presets['default']['email']; |
223 | } |
224 | if ( !isset( $defaults["echo-subscriptions-web-{$category}"] ) ) { |
225 | $defaults["echo-subscriptions-web-{$category}"] = $presets[$category]['web'] |
226 | ?? $presets['default']['web']; |
227 | } |
228 | if ( $echoPushEnabled && !isset( $defaults["echo-subscriptions-push-{$category}"] ) ) { |
229 | $defaults["echo-subscriptions-push-{$category}"] = $presets[$category]['push'] |
230 | // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset |
231 | ?? $presets['default']['push']; |
232 | } |
233 | } |
234 | } |
235 | |
236 | /** |
237 | * Initialize Echo extension with necessary data, this function is invoked |
238 | * from $wgExtensionFunctions |
239 | */ |
240 | public static function initEchoExtension() { |
241 | global $wgEchoNotifications, $wgEchoNotificationCategories, $wgEchoNotificationIcons, |
242 | $wgEchoMentionStatusNotifications, $wgAllowArticleReminderNotification, $wgAPIModules, |
243 | $wgEchoWatchlistNotifications, $wgEchoSeenTimeCacheType, $wgMainStash, $wgEnableEmail, |
244 | $wgEnableUserEmail; |
245 | |
246 | // allow extensions to define their own event |
247 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onBeforeCreateEchoEvent( |
248 | $wgEchoNotifications, $wgEchoNotificationCategories, $wgEchoNotificationIcons ); |
249 | |
250 | // Only allow mention status notifications when enabled |
251 | if ( !$wgEchoMentionStatusNotifications ) { |
252 | unset( $wgEchoNotificationCategories['mention-failure'] ); |
253 | unset( $wgEchoNotificationCategories['mention-success'] ); |
254 | } |
255 | |
256 | // Only allow article reminder notifications when enabled |
257 | if ( !$wgAllowArticleReminderNotification ) { |
258 | unset( $wgEchoNotificationCategories['article-reminder'] ); |
259 | unset( $wgAPIModules['echoarticlereminder'] ); |
260 | } |
261 | |
262 | // Only allow watchlist notifications when enabled |
263 | if ( !$wgEchoWatchlistNotifications ) { |
264 | unset( $wgEchoNotificationCategories['watchlist'] ); |
265 | unset( $wgEchoNotificationCategories['minor-watchlist'] ); |
266 | } |
267 | |
268 | // Only allow user email notifications when enabled |
269 | if ( !$wgEnableEmail || !$wgEnableUserEmail ) { |
270 | unset( $wgEchoNotificationCategories['emailuser'] ); |
271 | } |
272 | |
273 | // Default $wgEchoSeenTimeCacheType to $wgMainStash |
274 | if ( $wgEchoSeenTimeCacheType === null ) { |
275 | $wgEchoSeenTimeCacheType = $wgMainStash; |
276 | } |
277 | } |
278 | |
279 | /** |
280 | * Handler for ResourceLoaderRegisterModules hook |
281 | * @param ResourceLoader $resourceLoader |
282 | */ |
283 | public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void { |
284 | $resourceLoader->register( 'ext.echo.emailicons', [ |
285 | 'class' => ResourceLoaderEchoImageModule::class, |
286 | 'icons' => $this->config->get( ConfigNames::NotificationIcons ), |
287 | 'selector' => '.mw-echo-icon-{name}', |
288 | 'localBasePath' => $this->config->get( MainConfigNames::ExtensionDirectory ), |
289 | 'remoteExtPath' => 'Echo/modules' |
290 | ] ); |
291 | $resourceLoader->register( 'ext.echo.secondaryicons', [ |
292 | 'class' => ResourceLoaderEchoImageModule::class, |
293 | 'icons' => $this->config->get( ConfigNames::SecondaryIcons ), |
294 | 'selector' => '.mw-echo-icon-{name}', |
295 | 'localBasePath' => $this->config->get( MainConfigNames::ExtensionDirectory ), |
296 | 'remoteExtPath' => 'Echo/modules' |
297 | ] ); |
298 | } |
299 | |
300 | /** |
301 | * Handler for GetPreferences hook. |
302 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences |
303 | * |
304 | * @param User $user User to get preferences for |
305 | * @param array &$preferences Preferences array |
306 | */ |
307 | public function onGetPreferences( $user, &$preferences ) { |
308 | // The following messages are generated upstrem: |
309 | // * prefs-echo |
310 | // * prefs-description-echo |
311 | |
312 | // Show email frequency options |
313 | $freqOptions = [ |
314 | 'echo-pref-email-frequency-never' => EmailFrequency::NEVER, |
315 | 'echo-pref-email-frequency-immediately' => EmailFrequency::IMMEDIATELY, |
316 | ]; |
317 | // Only show digest options if email batch is enabled |
318 | if ( $this->config->get( ConfigNames::EnableEmailBatch ) ) { |
319 | $freqOptions += [ |
320 | 'echo-pref-email-frequency-daily' => EmailFrequency::DAILY_DIGEST, |
321 | 'echo-pref-email-frequency-weekly' => EmailFrequency::WEEKLY_DIGEST, |
322 | ]; |
323 | } |
324 | $preferences['echo-email-frequency'] = [ |
325 | 'type' => 'select', |
326 | 'label-message' => 'echo-pref-send-me', |
327 | // The following message is generated upstrem: |
328 | // * prefs-emailsettings |
329 | 'section' => 'echo/emailsettings', |
330 | 'options-messages' => $freqOptions |
331 | ]; |
332 | |
333 | $preferences['echo-dont-email-read-notifications'] = [ |
334 | 'type' => 'toggle', |
335 | 'label-message' => 'echo-pref-dont-email-read-notifications', |
336 | // The following message is generated upstrem: |
337 | // * prefs-emailsettings |
338 | 'section' => 'echo/emailsettings', |
339 | 'hide-if' => [ 'OR', [ '===', 'echo-email-frequency', '-1' ], [ '===', 'echo-email-frequency', '0' ] ] |
340 | ]; |
341 | |
342 | // Display information about the user's currently set email address |
343 | $prefsTitle = SpecialPage::getTitleFor( 'Preferences', false, 'mw-prefsection-echo' ); |
344 | $link = $this->linkRenderer->makeLink( |
345 | SpecialPage::getTitleFor( 'ChangeEmail' ), |
346 | wfMessage( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(), |
347 | [], |
348 | [ 'returnto' => $prefsTitle->getFullText() ] |
349 | ); |
350 | $emailAddress = $user->getEmail() && $this->permissionManager->userHasRight( $user, 'viewmyprivateinfo' ) |
351 | ? htmlspecialchars( $user->getEmail() ) : ''; |
352 | if ( $this->permissionManager->userHasRight( $user, 'editmyprivateinfo' ) && $this->isEmailChangeAllowed() ) { |
353 | if ( $emailAddress === '' ) { |
354 | $emailAddress .= $link; |
355 | } else { |
356 | $emailAddress .= wfMessage( 'word-separator' )->escaped() |
357 | . wfMessage( 'parentheses' )->rawParams( $link )->escaped(); |
358 | } |
359 | } |
360 | $preferences['echo-emailaddress'] = [ |
361 | 'type' => 'info', |
362 | 'raw' => true, |
363 | 'default' => $emailAddress, |
364 | 'label-message' => 'echo-pref-send-to', |
365 | // The following message is generated upstrem: |
366 | // * prefs-emailsettings |
367 | 'section' => 'echo/emailsettings' |
368 | ]; |
369 | |
370 | // Only show this option if html email is allowed, otherwise it is always plain text format |
371 | if ( $this->config->get( MainConfigNames::AllowHTMLEmail ) ) { |
372 | // Email format |
373 | $preferences['echo-email-format'] = [ |
374 | 'type' => 'select', |
375 | 'label-message' => 'echo-pref-email-format', |
376 | // The following message is generated upstrem: |
377 | // * prefs-emailsettings |
378 | 'section' => 'echo/emailsettings', |
379 | 'options-messages' => [ |
380 | 'echo-pref-email-format-html' => EmailFormat::HTML, |
381 | 'echo-pref-email-format-plain-text' => EmailFormat::PLAIN_TEXT, |
382 | ] |
383 | ]; |
384 | } |
385 | |
386 | // Sort notification categories by priority |
387 | $categoriesAndPriorities = []; |
388 | foreach ( $this->attributeManager->getInternalCategoryNames() as $category ) { |
389 | // See if the category should be hidden from preferences. |
390 | if ( !$this->attributeManager->isCategoryDisplayedInPreferences( $category ) ) { |
391 | continue; |
392 | } |
393 | |
394 | // See if user is eligible to receive this notification (per user group restrictions) |
395 | if ( $this->attributeManager->getCategoryEligibility( $user, $category ) ) { |
396 | $categoriesAndPriorities[$category] = $this->attributeManager->getCategoryPriority( $category ); |
397 | } |
398 | } |
399 | asort( $categoriesAndPriorities ); |
400 | $validSortedCategories = array_keys( $categoriesAndPriorities ); |
401 | |
402 | // Show subscription options. IMPORTANT: 'echo-subscriptions-email-edit-user-talk', |
403 | // 'echo-subscriptions-email-watchlist', and 'echo-subscriptions-email-minor-watchlist' are |
404 | // virtual options, their values are saved to existing notification options 'enotifusertalkpages', |
405 | // 'enotifwatchlistpages', and 'enotifminoredits', see onLoadUserOptions() and onSaveUserOptions() |
406 | // for more information on how it is handled. Doing it in this way, we can avoid keeping running |
407 | // massive data migration script to keep these two options synced when echo is enabled on |
408 | // new wikis or Echo is disabled and re-enabled for some reason. We can update the name |
409 | // if Echo is ever merged to core |
410 | |
411 | // Build the columns (notify types) |
412 | $columns = []; |
413 | foreach ( $this->config->get( ConfigNames::Notifiers ) as $notifierType => $notifierData ) { |
414 | // The following messages are generated here |
415 | // * echo-pref-web |
416 | // * echo-pref-email |
417 | // * echo-pref-push |
418 | $formatMessage = wfMessage( 'echo-pref-' . $notifierType )->escaped(); |
419 | $columns[$formatMessage] = $notifierType; |
420 | } |
421 | |
422 | // Build the rows (notification categories) |
423 | $rows = []; |
424 | $tooltips = []; |
425 | $notificationCategories = $this->config->get( ConfigNames::NotificationCategories ); |
426 | foreach ( $validSortedCategories as $category ) { |
427 | $categoryMessage = wfMessage( 'echo-category-title-' . $category )->numParams( 1 )->escaped(); |
428 | $rows[$categoryMessage] = $category; |
429 | if ( isset( $notificationCategories[$category]['tooltip'] ) ) { |
430 | $tooltips[$categoryMessage] = wfMessage( $notificationCategories[$category]['tooltip'] )->text(); |
431 | } |
432 | } |
433 | |
434 | // Figure out the individual exceptions in the matrix and make them disabled |
435 | $forceOptionsOff = $forceOptionsOn = []; |
436 | foreach ( $this->config->get( ConfigNames::Notifiers ) as $notifierType => $notifierData ) { |
437 | foreach ( $validSortedCategories as $category ) { |
438 | // See if this notify type is non-dismissable |
439 | if ( !$this->attributeManager->isNotifyTypeDismissableForCategory( $category, $notifierType ) ) { |
440 | $forceOptionsOn[] = "$notifierType-$category"; |
441 | } |
442 | |
443 | if ( !$this->attributeManager->isNotifyTypeAvailableForCategory( $category, $notifierType ) ) { |
444 | $forceOptionsOff[] = "$notifierType-$category"; |
445 | } |
446 | } |
447 | } |
448 | |
449 | $invalid = array_intersect( $forceOptionsOff, $forceOptionsOn ); |
450 | if ( $invalid ) { |
451 | throw new LogicException( sprintf( |
452 | 'The following notifications are both forced and removed: %s', |
453 | implode( ', ', $invalid ) |
454 | ) ); |
455 | } |
456 | $preferences['echo-subscriptions'] = [ |
457 | 'class' => HTMLCheckMatrix::class, |
458 | // The following message is generated upstrem: |
459 | // * prefs-echosubscriptions |
460 | 'section' => 'echo/echosubscriptions', |
461 | 'rows' => $rows, |
462 | 'columns' => $columns, |
463 | 'prefix' => 'echo-subscriptions-', |
464 | 'force-options-off' => $forceOptionsOff, |
465 | 'force-options-on' => $forceOptionsOn, |
466 | 'tooltips' => $tooltips, |
467 | ]; |
468 | |
469 | if ( $this->config->get( ConfigNames::CrossWikiNotifications ) ) { |
470 | $preferences['echo-cross-wiki-notifications'] = [ |
471 | 'type' => 'toggle', |
472 | 'label-message' => 'echo-pref-cross-wiki-notifications', |
473 | // The following message is generated upstrem: |
474 | // * prefs-echocrosswiki |
475 | 'section' => 'echo/echocrosswiki' |
476 | ]; |
477 | } |
478 | |
479 | if ( $this->config->get( ConfigNames::PollForUpdates ) ) { |
480 | $preferences['echo-show-poll-updates'] = [ |
481 | 'type' => 'toggle', |
482 | 'label-message' => 'echo-pref-show-poll-updates', |
483 | 'help-message' => 'echo-pref-show-poll-updates-help', |
484 | // The following message is generated upstrem: |
485 | // * prefs-echopollupdates |
486 | 'section' => 'echo/echopollupdates' |
487 | ]; |
488 | } |
489 | |
490 | // If we're using Echo to handle user talk page post or watchlist notifications, |
491 | // hide the old (non-Echo) preferences for them. If Echo is moved to core |
492 | // we'll want to remove the old user options entirely. For now, though, |
493 | // we need to keep it defined in case Echo is ever uninstalled. |
494 | // Otherwise, that preference could be lost entirely. This hiding logic |
495 | // is not abstracted since there are only three preferences in core |
496 | // that are potentially made obsolete by Echo. |
497 | $notifications = $this->config->get( ConfigNames::Notifications ); |
498 | if ( isset( $notifications['edit-user-talk'] ) ) { |
499 | $preferences['enotifusertalkpages']['type'] = 'hidden'; |
500 | unset( $preferences['enotifusertalkpages']['section'] ); |
501 | } |
502 | if ( $this->config->get( ConfigNames::WatchlistNotifications ) && |
503 | isset( $notifications['watchlist-change'] ) |
504 | ) { |
505 | $preferences['enotifwatchlistpages']['type'] = 'hidden'; |
506 | unset( $preferences['enotifusertalkpages']['section'] ); |
507 | $preferences['enotifminoredits']['type'] = 'hidden'; |
508 | unset( $preferences['enotifminoredits']['section'] ); |
509 | } |
510 | |
511 | if ( $this->config->get( ConfigNames::PerUserBlacklist ) ) { |
512 | $preferences['echo-notifications-blacklist'] = [ |
513 | 'type' => 'usersmultiselect', |
514 | 'label-message' => 'echo-pref-notifications-blacklist', |
515 | // The following message is generated upstrem: |
516 | // * prefs-blocknotificationslist |
517 | 'section' => 'echo/blocknotificationslist', |
518 | 'filter' => MultiUsernameFilter::class, |
519 | ]; |
520 | $preferences['echo-notifications-page-linked-title-muted-list'] = [ |
521 | 'type' => 'titlesmultiselect', |
522 | 'label-message' => 'echo-pref-notifications-page-linked-title-muted-list', |
523 | // The following message is generated upstrem: |
524 | // * prefs-mutedpageslist |
525 | 'section' => 'echo/mutedpageslist', |
526 | 'showMissing' => false, |
527 | 'excludeDynamicNamespaces' => true, |
528 | 'filter' => new MultiTitleFilter() |
529 | ]; |
530 | } |
531 | } |
532 | |
533 | /** |
534 | * Add icon for Special:Preferences mobile layout |
535 | * |
536 | * @param array &$iconNames Array of icon names for their respective sections. |
537 | */ |
538 | public function onPreferencesGetIcon( &$iconNames ) { |
539 | $iconNames[ 'echo' ] = 'bell'; |
540 | } |
541 | |
542 | /** |
543 | * Test whether email address change is supposed to be allowed |
544 | * @return bool |
545 | */ |
546 | private function isEmailChangeAllowed() { |
547 | return $this->authManager->allowsPropertyChange( 'emailaddress' ); |
548 | } |
549 | |
550 | /** |
551 | * Handler for PageSaveComplete hook |
552 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete |
553 | * |
554 | * @param WikiPage $wikiPage modified WikiPage |
555 | * @param UserIdentity $userIdentity User who edited |
556 | * @param string $summary Edit summary |
557 | * @param int $flags Edit flags |
558 | * @param RevisionRecord $revisionRecord RevisionRecord for the revision that was created |
559 | * @param EditResult $editResult |
560 | */ |
561 | public function onPageSaveComplete( |
562 | $wikiPage, |
563 | $userIdentity, |
564 | $summary, |
565 | $flags, |
566 | $revisionRecord, |
567 | $editResult |
568 | ) { |
569 | if ( $editResult->isNullEdit() ) { |
570 | return; |
571 | } |
572 | |
573 | $title = $wikiPage->getTitle(); |
574 | $isRevert = $editResult->getRevertMethod() === EditResult::REVERT_UNDO || |
575 | $editResult->getRevertMethod() === EditResult::REVERT_ROLLBACK; |
576 | |
577 | // Save the revert status for the LinksUpdateComplete hook |
578 | if ( $isRevert ) { |
579 | self::$revertedRevIds[$revisionRecord->getId()] = true; |
580 | } |
581 | |
582 | // Try to do this after the HTTP response |
583 | DeferredUpdates::addCallableUpdate( static function () use ( $revisionRecord, $isRevert ) { |
584 | DiscussionParser::generateEventsForRevision( $revisionRecord, $isRevert ); |
585 | } ); |
586 | |
587 | // If the user is not an IP and this is not a null edit, |
588 | // test for them reaching a congratulatory threshold |
589 | $thresholds = [ 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000 ]; |
590 | if ( $userIdentity->isRegistered() ) { |
591 | $thresholdCount = $this->getEditCount( $userIdentity ); |
592 | if ( in_array( $thresholdCount, $thresholds ) ) { |
593 | DeferredUpdates::addCallableUpdate( static function () use ( |
594 | $revisionRecord, $userIdentity, $title, $thresholdCount |
595 | ) { |
596 | $notificationMapper = new NotificationMapper(); |
597 | $notifications = $notificationMapper->fetchByUser( $userIdentity, 10, null, [ 'thank-you-edit' ] ); |
598 | /** @var Notification $notification */ |
599 | foreach ( $notifications as $notification ) { |
600 | if ( $notification->getEvent()->getExtraParam( 'editCount' ) === $thresholdCount ) { |
601 | LoggerFactory::getInstance( 'Echo' )->debug( |
602 | '{user} (id: {id}) has already been thanked for their {count} edit', |
603 | [ |
604 | 'user' => $userIdentity->getName(), |
605 | 'id' => $userIdentity->getId(), |
606 | 'count' => $thresholdCount, |
607 | ] |
608 | ); |
609 | return; |
610 | } |
611 | } |
612 | |
613 | Event::create( [ |
614 | 'type' => 'thank-you-edit', |
615 | 'title' => $title, |
616 | 'agent' => $userIdentity, |
617 | // Edit threshold notifications are sent to the agent |
618 | 'extra' => [ |
619 | 'editCount' => $thresholdCount, |
620 | 'revid' => $revisionRecord->getId(), |
621 | ] |
622 | ] ); |
623 | } ); |
624 | } |
625 | } |
626 | |
627 | // Handle the case of someone undoing an edit, either through the |
628 | // 'undo' link in the article history or via the API. |
629 | // Reverts through the 'rollback' link (EditResult::REVERT_ROLLBACK) |
630 | // are handled in ::onRollbackComplete(). |
631 | if ( $editResult->getRevertMethod() === EditResult::REVERT_UNDO ) { |
632 | $undidRevId = $editResult->getUndidRevId(); |
633 | $undidRevision = $this->revisionStore->getRevisionById( $undidRevId ); |
634 | if ( |
635 | $undidRevision && |
636 | Title::newFromLinkTarget( $undidRevision->getPageAsLinkTarget() )->equals( $title ) |
637 | ) { |
638 | $revertedUser = $undidRevision->getUser(); |
639 | // No notifications for anonymous users |
640 | if ( $revertedUser && $revertedUser->getId() ) { |
641 | Event::create( [ |
642 | 'type' => 'reverted', |
643 | 'title' => $title, |
644 | 'extra' => [ |
645 | 'revid' => $revisionRecord->getId(), |
646 | 'reverted-user-id' => $revertedUser->getId(), |
647 | 'reverted-revision-id' => $undidRevId, |
648 | 'method' => 'undo', |
649 | 'summary' => $summary, |
650 | ], |
651 | 'agent' => $userIdentity, |
652 | ] ); |
653 | } |
654 | } |
655 | } |
656 | } |
657 | |
658 | /** |
659 | * @param UserIdentity $user |
660 | * @return int |
661 | */ |
662 | private function getEditCount( UserIdentity $user ) { |
663 | $editCount = $this->userEditTracker->getUserEditCount( $user ) ?: 0; |
664 | // When this code runs from a maintenance script or unit tests |
665 | // the deferred update incrementing edit count runs right away |
666 | // so the edit count is right. Otherwise it lags by one. |
667 | if ( wfIsCLI() ) { |
668 | return $editCount; |
669 | } |
670 | return $editCount + 1; |
671 | } |
672 | |
673 | /** |
674 | * Handler for LocalUserCreated hook. |
675 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/LocalUserCreated |
676 | * @param User $user User object that was created. |
677 | * @param bool $autocreated True when account was auto-created |
678 | */ |
679 | public function onLocalUserCreated( $user, $autocreated ) { |
680 | if ( !$autocreated ) { |
681 | Event::create( [ |
682 | 'type' => 'welcome', |
683 | 'agent' => $user, |
684 | ] ); |
685 | } |
686 | |
687 | $seenTime = SeenTime::newFromUser( $user ); |
688 | |
689 | // Set seen time to UNIX epoch, so initially all notifications are unseen. |
690 | $seenTime->setTime( wfTimestamp( TS_MW, 1 ), 'all' ); |
691 | } |
692 | |
693 | /** |
694 | * Handler for UserGroupsChanged hook. |
695 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/UserGroupsChanged |
696 | * |
697 | * @param UserIdentity $userId user that was changed |
698 | * @param string[] $add strings corresponding to groups added |
699 | * @param string[] $remove strings corresponding to groups removed |
700 | * @param User|bool $performer |
701 | * @param string|bool $reason Reason given by the user changing the rights |
702 | * @param array $oldUGMs |
703 | * @param array $newUGMs |
704 | */ |
705 | public function onUserGroupsChanged( $userId, $add, $remove, $performer, $reason, $oldUGMs, $newUGMs ) { |
706 | if ( !$performer ) { |
707 | // TODO: Implement support for autopromotion |
708 | return; |
709 | } |
710 | |
711 | if ( $userId->getWikiId() !== WikiAwareEntity::LOCAL ) { |
712 | // TODO: Support external users |
713 | return; |
714 | } |
715 | |
716 | $user = $this->userFactory->newFromUserIdentity( $userId ); |
717 | |
718 | if ( $user->equals( $performer ) ) { |
719 | // Don't notify for self changes |
720 | return; |
721 | } |
722 | |
723 | // If any old groups are in $add, those groups are having their expiry |
724 | // changed, not actually being added |
725 | $expiryChanged = []; |
726 | $reallyAdded = []; |
727 | foreach ( $add as $group ) { |
728 | if ( isset( $oldUGMs[$group] ) ) { |
729 | $expiryChanged[] = $group; |
730 | } else { |
731 | $reallyAdded[] = $group; |
732 | } |
733 | } |
734 | |
735 | if ( $expiryChanged ) { |
736 | // use a separate notification for these, so the notification text doesn't |
737 | // get too long |
738 | Event::create( |
739 | [ |
740 | 'type' => 'user-rights', |
741 | 'extra' => [ |
742 | 'user' => $user->getId(), |
743 | 'expiry-changed' => $expiryChanged, |
744 | 'reason' => $reason, |
745 | ], |
746 | 'agent' => $performer, |
747 | ] |
748 | ); |
749 | } |
750 | |
751 | if ( $reallyAdded || $remove ) { |
752 | Event::create( |
753 | [ |
754 | 'type' => 'user-rights', |
755 | 'extra' => [ |
756 | 'user' => $user->getId(), |
757 | 'add' => $reallyAdded, |
758 | 'remove' => $remove, |
759 | 'reason' => $reason, |
760 | ], |
761 | 'agent' => $performer, |
762 | ] |
763 | ); |
764 | } |
765 | } |
766 | |
767 | /** |
768 | * Handler for LinksUpdateComplete hook. |
769 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/LinksUpdateComplete |
770 | * @param LinksUpdate $linksUpdate |
771 | * @param mixed $ticket |
772 | */ |
773 | public function onLinksUpdateComplete( $linksUpdate, $ticket ) { |
774 | // Rollback or undo should not trigger link notification |
775 | if ( $linksUpdate->getRevisionRecord() ) { |
776 | $revId = $linksUpdate->getRevisionRecord()->getId(); |
777 | if ( isset( self::$revertedRevIds[$revId] ) ) { |
778 | return; |
779 | } |
780 | } |
781 | |
782 | // Handle only |
783 | // 1. content namespace pages && |
784 | // 2. non-transcluding pages && |
785 | // 3. non-redirect pages |
786 | if ( !$this->namespaceInfo->isContent( $linksUpdate->getTitle()->getNamespace() ) |
787 | || !$linksUpdate->isRecursive() || $linksUpdate->getTitle()->isRedirect() |
788 | ) { |
789 | return; |
790 | } |
791 | |
792 | $revRecord = $linksUpdate->getRevisionRecord(); |
793 | $revid = $revRecord ? $revRecord->getId() : null; |
794 | $user = $revRecord ? $revRecord->getUser() : null; |
795 | |
796 | // link notification is boundless as you can include infinite number of links in a page |
797 | // db insert is expensive, limit it to a reasonable amount, we can increase this limit |
798 | // once the storage is on Redis |
799 | $max = 10; |
800 | // Only create notifications for links to content namespace pages |
801 | // @Todo - use one big insert instead of individual insert inside foreach loop |
802 | foreach ( $linksUpdate->getAddedLinks() as $title ) { |
803 | if ( $this->namespaceInfo->isContent( $title->getNamespace() ) ) { |
804 | if ( $title->isRedirect() ) { |
805 | continue; |
806 | } |
807 | |
808 | $linkFromPageId = $linksUpdate->getTitle()->getArticleID(); |
809 | // T318523: Don't send page-linked notifications for pages created by bot users. |
810 | $articleAuthor = EchoUserLocator::getArticleAuthorByArticleId( $title->getArticleID() ); |
811 | if ( $articleAuthor && $articleAuthor->isBot() ) { |
812 | continue; |
813 | } |
814 | Event::create( [ |
815 | 'type' => 'page-linked', |
816 | 'title' => $title, |
817 | 'agent' => $user, |
818 | 'extra' => [ |
819 | 'target-page' => $linkFromPageId, |
820 | 'link-from-page-id' => $linkFromPageId, |
821 | 'revid' => $revid, |
822 | ] |
823 | ] ); |
824 | $max--; |
825 | } |
826 | if ( $max < 0 ) { |
827 | break; |
828 | } |
829 | } |
830 | } |
831 | |
832 | /** |
833 | * Handler for BeforePageDisplay hook. |
834 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay |
835 | * @param OutputPage $out |
836 | * @param Skin $skin Skin being used. |
837 | */ |
838 | public function onBeforePageDisplay( $out, $skin ): void { |
839 | $user = $out->getUser(); |
840 | |
841 | if ( !$user->isRegistered() ) { |
842 | if ( ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) ) { |
843 | $out->addModules( [ 'ext.echo.centralauth' ] ); |
844 | } |
845 | return; |
846 | } |
847 | |
848 | if ( $this->shouldDisplayTalkAlert( $user, $out->getTitle() ) ) { |
849 | // Load the module for the Orange alert |
850 | $out->addModuleStyles( 'ext.echo.styles.alert' ); |
851 | } |
852 | |
853 | // Load the module for the Notifications flyout |
854 | $out->addModules( [ 'ext.echo.init' ] ); |
855 | // Load the styles for the Notifications badge |
856 | $out->addModuleStyles( [ |
857 | 'ext.echo.styles.badge', |
858 | 'oojs-ui.styles.icons-alerts' |
859 | ] ); |
860 | } |
861 | |
862 | private function processMarkAsRead( User $user, WebRequest $request, Title $title ) { |
863 | $subtractions = [ |
864 | AttributeManager::ALERT => 0, |
865 | AttributeManager::MESSAGE => 0 |
866 | ]; |
867 | |
868 | // Attempt to mark a notification as read when visiting a page |
869 | $eventIds = []; |
870 | if ( $title->getArticleID() ) { |
871 | $eventMapper = new EventMapper(); |
872 | $events = $eventMapper->fetchUnreadByUserAndPage( $user, $title->getArticleID() ); |
873 | |
874 | foreach ( $events as $event ) { |
875 | $subtractions[$event->getSection()]++; |
876 | $eventIds[] = $event->getId(); |
877 | } |
878 | } |
879 | |
880 | // Attempt to mark as read the event IDs in the ?markasread= parameter, if present |
881 | $markAsReadIds = array_filter( explode( '|', $request->getText( 'markasread' ) ) ); |
882 | $markAsReadWiki = $request->getText( 'markasreadwiki', WikiMap::getCurrentWikiId() ); |
883 | $markAsReadLocal = !$this->config->get( ConfigNames::CrossWikiNotifications ) || |
884 | $markAsReadWiki === WikiMap::getCurrentWikiId(); |
885 | if ( $markAsReadIds ) { |
886 | if ( $markAsReadLocal ) { |
887 | // gather the IDs that we didn't already find with target_pages |
888 | $eventsToMarkAsRead = []; |
889 | foreach ( $markAsReadIds as $markAsReadId ) { |
890 | $markAsReadId = intval( $markAsReadId ); |
891 | if ( $markAsReadId !== 0 && !in_array( $markAsReadId, $eventIds ) ) { |
892 | $eventsToMarkAsRead[] = $markAsReadId; |
893 | } |
894 | } |
895 | |
896 | if ( $eventsToMarkAsRead ) { |
897 | // fetch the notifications to adjust the counters |
898 | $notifMapper = new NotificationMapper(); |
899 | $notifs = $notifMapper->fetchByUserEvents( $user, $eventsToMarkAsRead ); |
900 | |
901 | foreach ( $notifs as $notif ) { |
902 | if ( !$notif->getReadTimestamp() ) { |
903 | $subtractions[$notif->getEvent()->getSection()]++; |
904 | $eventIds[] = intval( $notif->getEvent()->getId() ); |
905 | } |
906 | } |
907 | } |
908 | } else { |
909 | $markAsReadIds = array_map( 'intval', $markAsReadIds ); |
910 | // Look up the notifications on the foreign wiki |
911 | $notifUser = NotifUser::newFromUser( $user ); |
912 | $notifInfo = $notifUser->getForeignNotificationInfo( $markAsReadIds, $markAsReadWiki, $request ); |
913 | foreach ( $notifInfo as $id => $info ) { |
914 | $subtractions[$info['section']]++; |
915 | } |
916 | |
917 | // Schedule a deferred update to mark these notifications as read on the foreign wiki |
918 | DeferredUpdates::addCallableUpdate( |
919 | static function () use ( $user, $markAsReadIds, $markAsReadWiki, $request ) { |
920 | $notifUser = NotifUser::newFromUser( $user ); |
921 | $notifUser->markReadForeign( $markAsReadIds, $markAsReadWiki, $request ); |
922 | } |
923 | ); |
924 | } |
925 | } |
926 | |
927 | // Schedule a deferred update to mark local target_page and ?markasread= notifications as read |
928 | if ( $eventIds ) { |
929 | DeferredUpdates::addCallableUpdate( static function () use ( $user, $eventIds ) { |
930 | $notifUser = NotifUser::newFromUser( $user ); |
931 | $notifUser->markRead( $eventIds ); |
932 | } ); |
933 | } |
934 | |
935 | return $subtractions; |
936 | } |
937 | |
938 | /** |
939 | * Determine if a talk page alert should be displayed. |
940 | * We need to check: |
941 | * - User actually has new messages |
942 | * - User is not viewing their user talk page, as user_newtalk will not have been cleared yet. |
943 | * (bug T107655). |
944 | * |
945 | * @param User $user |
946 | * @param Title $title |
947 | * @return bool |
948 | */ |
949 | private function shouldDisplayTalkAlert( $user, $title ) { |
950 | $userHasNewMessages = $this->talkPageNotificationManager->userHasNewMessages( $user ); |
951 | |
952 | return $userHasNewMessages && !$user->getTalkPage()->equals( $title ); |
953 | } |
954 | |
955 | /** |
956 | * Handler for SkinTemplateNavigation::Universal hook. |
957 | * Adds "Notifications" items to the notifications content navigation. |
958 | * SkinTemplate automatically merges these into the personal tools for older skins. |
959 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation::Universal |
960 | * @param SkinTemplate $skinTemplate |
961 | * @param array &$links Array of URLs to append to. |
962 | */ |
963 | public function onSkinTemplateNavigation__Universal( $skinTemplate, &$links ): void { |
964 | $user = $skinTemplate->getUser(); |
965 | if ( !$user->isRegistered() ) { |
966 | return; |
967 | } |
968 | |
969 | $title = $skinTemplate->getTitle(); |
970 | $out = $skinTemplate->getOutput(); |
971 | |
972 | $subtractions = $this->processMarkAsRead( $user, $out->getRequest(), $title ); |
973 | |
974 | // Add a "My notifications" item to personal URLs |
975 | $notifUser = NotifUser::newFromUser( $user ); |
976 | $msgCount = $notifUser->getMessageCount() - $subtractions[AttributeManager::MESSAGE]; |
977 | $alertCount = $notifUser->getAlertCount() - $subtractions[AttributeManager::ALERT]; |
978 | // But make sure we never show a negative number (T130853) |
979 | $msgCount = max( 0, $msgCount ); |
980 | $alertCount = max( 0, $alertCount ); |
981 | |
982 | $msgNotificationTimestamp = $notifUser->getLastUnreadMessageTime(); |
983 | $alertNotificationTimestamp = $notifUser->getLastUnreadAlertTime(); |
984 | |
985 | $seenTime = SeenTime::newFromUser( $user ); |
986 | if ( $title->isSpecial( 'Notifications' ) ) { |
987 | // If this is the Special:Notifications page, seenTime to now |
988 | $seenTime->setTime( wfTimestamp( TS_MW ), AttributeManager::ALL ); |
989 | } |
990 | $seenAlertTime = $seenTime->getTime( 'alert', TS_ISO_8601 ); |
991 | $seenMsgTime = $seenTime->getTime( 'message', TS_ISO_8601 ); |
992 | |
993 | $out->addJsConfigVars( 'wgEchoSeenTime', [ |
994 | 'alert' => $seenAlertTime, |
995 | 'notice' => $seenMsgTime, |
996 | ] ); |
997 | |
998 | $msgFormattedCount = NotificationController::formatNotificationCount( $msgCount ); |
999 | $alertFormattedCount = NotificationController::formatNotificationCount( $alertCount ); |
1000 | |
1001 | $url = SpecialPage::getTitleFor( 'Notifications' )->getLocalURL(); |
1002 | |
1003 | $skinName = strtolower( $skinTemplate->getSkinName() ); |
1004 | $isMinervaSkin = $skinName === 'minerva'; |
1005 | // HACK: inverted icons only work in the "MediaWiki" OOUI theme |
1006 | // Avoid flashes in skins that don't use it (T111821) |
1007 | $out::setupOOUI( $skinName, $out->getLanguage()->getDir() ); |
1008 | $bellIconClass = $isMinervaSkin ? 'oo-ui-icon-bellOutline' : 'oo-ui-icon-bell'; |
1009 | |
1010 | $msgLinkClasses = [ "mw-echo-notifications-badge", "mw-echo-notification-badge-nojs", "oo-ui-icon-tray" ]; |
1011 | $alertLinkClasses = [ "mw-echo-notifications-badge", "mw-echo-notification-badge-nojs", $bellIconClass ]; |
1012 | |
1013 | $hasUnseen = false; |
1014 | if ( |
1015 | // no unread notifications |
1016 | $msgCount !== 0 && |
1017 | // should already always be false if count === 0 |
1018 | $msgNotificationTimestamp !== false && |
1019 | // there are no unseen notifications |
1020 | ( $seenMsgTime === null || |
1021 | $seenMsgTime < $msgNotificationTimestamp->getTimestamp( TS_ISO_8601 ) ) |
1022 | ) { |
1023 | $msgLinkClasses[] = 'mw-echo-unseen-notifications'; |
1024 | $hasUnseen = true; |
1025 | } elseif ( $msgCount === 0 ) { |
1026 | $msgLinkClasses[] = 'mw-echo-notifications-badge-all-read'; |
1027 | } |
1028 | |
1029 | if ( $msgCount > NotifUser::MAX_BADGE_COUNT ) { |
1030 | $msgLinkClasses[] = 'mw-echo-notifications-badge-long-label'; |
1031 | } |
1032 | |
1033 | if ( |
1034 | // no unread notifications |
1035 | $alertCount !== 0 && |
1036 | // should already always be false if count === 0 |
1037 | $alertNotificationTimestamp !== false && |
1038 | // all notifications have already been seen |
1039 | ( $seenAlertTime === null || |
1040 | $seenAlertTime < $alertNotificationTimestamp->getTimestamp( TS_ISO_8601 ) ) |
1041 | ) { |
1042 | $alertLinkClasses[] = 'mw-echo-unseen-notifications'; |
1043 | $hasUnseen = true; |
1044 | } elseif ( $alertCount === 0 ) { |
1045 | $alertLinkClasses[] = 'mw-echo-notifications-badge-all-read'; |
1046 | } |
1047 | |
1048 | if ( $alertCount > NotifUser::MAX_BADGE_COUNT ) { |
1049 | $alertLinkClasses[] = 'mw-echo-notifications-badge-long-label'; |
1050 | } |
1051 | |
1052 | $mytalk = $links['user-menu']['mytalk'] ?? false; |
1053 | if ( |
1054 | $mytalk && |
1055 | $this->shouldDisplayTalkAlert( $user, $title ) && |
1056 | ( new HookRunner( $this->hookContainer ) )->onBeforeDisplayOrangeAlert( $user, $title ) |
1057 | ) { |
1058 | // Create new talk alert inheriting from the talk link data. |
1059 | $links['notifications']['talk-alert'] = array_merge( |
1060 | $links['user-menu']['mytalk'], |
1061 | [ |
1062 | // Hardcode id, which is needed to dismiss the talk alert notification |
1063 | 'id' => 'pt-talk-alert', |
1064 | // If Vector hook ran anicon will have been copied to the link class. |
1065 | // We must reset it. |
1066 | 'link-class' => [], |
1067 | 'text' => $skinTemplate->msg( 'echo-new-messages' )->text(), |
1068 | 'class' => [ 'mw-echo-alert' ], |
1069 | // unset icon |
1070 | 'icon' => null, |
1071 | ] |
1072 | ); |
1073 | |
1074 | // If there's exactly one new user talk message, then link directly to it from the alert. |
1075 | $notificationMapper = new NotificationMapper(); |
1076 | $notifications = $notificationMapper->fetchUnreadByUser( $user, 2, null, [ 'edit-user-talk' ] ); |
1077 | if ( count( $notifications ) === 1 ) { |
1078 | $presModel = EchoEventPresentationModel::factory( |
1079 | current( $notifications )->getEvent(), |
1080 | $out->getLanguage(), |
1081 | $user |
1082 | ); |
1083 | $links['notifications']['talk-alert']['href'] = $presModel->getPrimaryLink()['url']; |
1084 | } |
1085 | } |
1086 | |
1087 | $links['notifications']['notifications-alert'] = [ |
1088 | 'href' => $url, |
1089 | 'text' => $skinTemplate->msg( 'echo-notification-alert', $alertCount )->text(), |
1090 | 'active' => ( $url == $title->getLocalURL() ), |
1091 | 'link-class' => $alertLinkClasses, |
1092 | 'icon' => 'bell', |
1093 | 'data' => [ |
1094 | 'event-name' => 'ui.notifications', |
1095 | 'counter-num' => $alertCount, |
1096 | 'counter-text' => $alertFormattedCount, |
1097 | ], |
1098 | // This item used to be part of personal tools, and much CSS relies on it using this id. |
1099 | 'id' => 'pt-notifications-alert', |
1100 | ]; |
1101 | |
1102 | $links['notifications']['notifications-notice'] = [ |
1103 | 'href' => $url, |
1104 | 'text' => $skinTemplate->msg( 'echo-notification-notice', $msgCount )->text(), |
1105 | 'active' => ( $url == $title->getLocalURL() ), |
1106 | 'link-class' => $msgLinkClasses, |
1107 | 'icon' => 'tray', |
1108 | 'data' => [ |
1109 | 'counter-num' => $msgCount, |
1110 | 'counter-text' => $msgFormattedCount, |
1111 | ], |
1112 | // This item used to be part of personal tools, and much CSS relies on it using this id. |
1113 | 'id' => 'pt-notifications-notice', |
1114 | ]; |
1115 | |
1116 | if ( $hasUnseen ) { |
1117 | // Record that the user is going to see an indicator that they have unseen notifications |
1118 | // This is part of tracking how likely users are to click a badge with unseen notifications. |
1119 | // The other part is the 'echo.unseen.click' counter, see ext.echo.init.js. |
1120 | $this->statsdDataFactory->increment( 'echo.unseen' ); |
1121 | } |
1122 | } |
1123 | |
1124 | /** |
1125 | * Handler for AbortTalkPageEmailNotification hook. |
1126 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/AbortTalkPageEmailNotification |
1127 | * @param User $targetUser |
1128 | * @param Title $title |
1129 | * @return bool |
1130 | */ |
1131 | public function onAbortTalkPageEmailNotification( $targetUser, $title ) { |
1132 | // Send legacy talk page email notification if |
1133 | // 1. echo is disabled for them or |
1134 | // 2. echo talk page notification is disabled |
1135 | if ( !isset( $this->config->get( ConfigNames::Notifications )['edit-user-talk'] ) ) { |
1136 | // Legacy talk page email notification |
1137 | return true; |
1138 | } |
1139 | |
1140 | // Echo talk page email notification |
1141 | return false; |
1142 | } |
1143 | |
1144 | /** |
1145 | * Handler for AbortWatchlistEmailNotification hook. |
1146 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/AbortWatchlistEmailNotification |
1147 | * @param User $targetUser |
1148 | * @param Title $title |
1149 | * @param EmailNotification $emailNotification The email notification object that sends non-echo notifications |
1150 | * @return bool |
1151 | */ |
1152 | public function onSendWatchlistEmailNotification( $targetUser, $title, $emailNotification ) { |
1153 | if ( $this->config->get( ConfigNames::WatchlistNotifications ) && |
1154 | isset( $this->config->get( ConfigNames::Notifications )["watchlist-change"] ) |
1155 | ) { |
1156 | // Let echo handle watchlist notifications entirely |
1157 | return false; |
1158 | } |
1159 | $eventName = false; |
1160 | // The edit-user-talk and edit-user-page events effectively duplicate watchlist notifications. |
1161 | // If we are sending Echo notification emails, suppress the watchlist notifications. |
1162 | if ( $title->inNamespace( NS_USER_TALK ) && $targetUser->getTalkPage()->equals( $title ) ) { |
1163 | $eventName = 'edit-user-talk'; |
1164 | } elseif ( $title->inNamespace( NS_USER ) && $targetUser->getUserPage()->equals( $title ) ) { |
1165 | $eventName = 'edit-user-page'; |
1166 | } |
1167 | |
1168 | if ( $eventName !== false ) { |
1169 | $events = $this->attributeManager->getUserEnabledEvents( $targetUser, 'email' ); |
1170 | if ( in_array( $eventName, $events ) ) { |
1171 | // Do not send watchlist email notification, the user will receive an Echo notification |
1172 | return false; |
1173 | } |
1174 | } |
1175 | |
1176 | // Proceed to send watchlist email notification |
1177 | return true; |
1178 | } |
1179 | |
1180 | /** |
1181 | * @param array &$modifiedTimes |
1182 | * @param OutputPage $out |
1183 | */ |
1184 | public function onOutputPageCheckLastModified( &$modifiedTimes, $out ) { |
1185 | $req = $out->getRequest(); |
1186 | if ( $req->getRawVal( 'action' ) === 'raw' || $req->getRawVal( 'action' ) === 'render' ) { |
1187 | // Optimisation: Avoid expensive SeenTime compute on non-skin responses (T279213) |
1188 | return; |
1189 | } |
1190 | |
1191 | $user = $out->getUser(); |
1192 | if ( $user->isRegistered() ) { |
1193 | $notifUser = NotifUser::newFromUser( $user ); |
1194 | $lastUpdate = $notifUser->getGlobalUpdateTime(); |
1195 | if ( $lastUpdate !== false ) { |
1196 | $modifiedTimes['notifications-global'] = $lastUpdate; |
1197 | } |
1198 | |
1199 | $modifiedTimes['notifications-seen-alert'] = SeenTime::newFromUser( $user )->getTime( 'alert' ); |
1200 | $modifiedTimes['notifications-seen-message'] = SeenTime::newFromUser( $user )->getTime( 'message' ); |
1201 | } |
1202 | } |
1203 | |
1204 | /** |
1205 | * Handler for GetNewMessagesAlert hook. |
1206 | * We're using the GetNewMessagesAlert hook instead of the |
1207 | * ArticleEditUpdateNewTalk hook since we still want the user_newtalk data |
1208 | * to be updated and available to client-side tools and the API. |
1209 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetNewMessagesAlert |
1210 | * @param string &$newMessagesAlert An alert that the user has new messages |
1211 | * or an empty string if the user does not (empty by default) |
1212 | * @param array $newtalks This will be empty if the user has no new messages |
1213 | * or an Array containing links and revisions if there are new messages |
1214 | * @param User $user The user who is loading the page |
1215 | * @param OutputPage $out |
1216 | * @return bool Should return false to prevent the new messages alert (OBOD) |
1217 | * or true to allow the new messages alert |
1218 | */ |
1219 | public function onGetNewMessagesAlert( &$newMessagesAlert, $newtalks, $user, $out ) { |
1220 | // If the user has the notifications flyout turned on and is receiving |
1221 | // notifications for talk page messages, disable the new messages alert. |
1222 | if ( $user->isRegistered() |
1223 | && isset( $this->config->get( ConfigNames::Notifications )['edit-user-talk'] ) |
1224 | && ( new HookRunner( $this->hookContainer ) )->onEchoCanAbortNewMessagesAlert() |
1225 | ) { |
1226 | // hide new messages alert |
1227 | return false; |
1228 | } else { |
1229 | // show new messages alert |
1230 | return true; |
1231 | } |
1232 | } |
1233 | |
1234 | /** |
1235 | * Handler for RollbackComplete hook. |
1236 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/RollbackComplete |
1237 | * |
1238 | * @param WikiPage $wikiPage The article that was edited |
1239 | * @param UserIdentity $agent The user who did the rollback |
1240 | * @param RevisionRecord $newRevision The revision the page was reverted back to |
1241 | * @param RevisionRecord $oldRevision The revision of the top edit that was reverted |
1242 | */ |
1243 | public function onRollbackComplete( |
1244 | $wikiPage, |
1245 | $agent, |
1246 | $newRevision, |
1247 | $oldRevision |
1248 | ) { |
1249 | $revertedUser = $oldRevision->getUser(); |
1250 | $latestRevision = $wikiPage->getRevisionRecord(); |
1251 | |
1252 | if ( |
1253 | $revertedUser && |
1254 | // No notifications for anonymous users |
1255 | $revertedUser->isRegistered() && |
1256 | // No notifications for null rollbacks |
1257 | !$oldRevision->hasSameContent( $newRevision ) |
1258 | ) { |
1259 | Event::create( [ |
1260 | 'type' => 'reverted', |
1261 | 'title' => $wikiPage->getTitle(), |
1262 | 'extra' => [ |
1263 | 'revid' => $latestRevision->getId(), |
1264 | 'reverted-user-id' => $revertedUser->getId(), |
1265 | 'reverted-revision-id' => $oldRevision->getId(), |
1266 | 'method' => 'rollback', |
1267 | ], |
1268 | 'agent' => $agent, |
1269 | ] ); |
1270 | } |
1271 | } |
1272 | |
1273 | /** |
1274 | * Handler for UserSaveSettings hook. |
1275 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/UserSaveSettings |
1276 | * @param User $user whose settings were saved |
1277 | */ |
1278 | public function onUserSaveSettings( $user ) { |
1279 | // Extensions like AbuseFilter might create an account, but |
1280 | // the tables we need might not exist. Bug 57335 |
1281 | if ( !defined( 'MW_UPDATER' ) ) { |
1282 | // Reset the notification count since it may have changed due to user |
1283 | // option changes. This covers both explicit changes in the preferences |
1284 | // and changes made through the options API (since both call this hook). |
1285 | DeferredUpdates::addCallableUpdate( static function () use ( $user ) { |
1286 | if ( !$user->isRegistered() ) { |
1287 | // It's possible the user account was deleted before the deferred |
1288 | // update runs (T318081) |
1289 | return; |
1290 | } |
1291 | NotifUser::newFromUser( $user )->resetNotificationCount(); |
1292 | } ); |
1293 | } |
1294 | } |
1295 | |
1296 | /** |
1297 | * Some of Echo's subscription user preferences are mapped to existing user preferences defined in |
1298 | * core MediaWiki. This returns the map of Echo preference names to core preference names. |
1299 | * |
1300 | * @return array |
1301 | */ |
1302 | public static function getVirtualUserOptions() { |
1303 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
1304 | $options = []; |
1305 | $options['echo-subscriptions-email-edit-user-talk'] = 'enotifusertalkpages'; |
1306 | if ( $config->get( ConfigNames::WatchlistNotifications ) ) { |
1307 | $options['echo-subscriptions-email-watchlist'] = 'enotifwatchlistpages'; |
1308 | $options['echo-subscriptions-email-minor-watchlist'] = 'enotifminoredits'; |
1309 | } |
1310 | return $options; |
1311 | } |
1312 | |
1313 | /** |
1314 | * Handler for LoadUserOptions hook. |
1315 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/LoadUserOptions |
1316 | * @param UserIdentity $user User whose options were loaded |
1317 | * @param array &$options Options can be modified |
1318 | */ |
1319 | public function onLoadUserOptions( UserIdentity $user, &$options ): void { |
1320 | foreach ( self::getVirtualUserOptions() as $echoPref => $mwPref ) { |
1321 | // Use the existing core option's value for the Echo option |
1322 | if ( isset( $options[ $mwPref ] ) ) { |
1323 | $options[ $echoPref ] = $options[ $mwPref ]; |
1324 | } |
1325 | } |
1326 | } |
1327 | |
1328 | /** |
1329 | * Handler for SaveUserOptions hook. |
1330 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/SaveUserOptions |
1331 | * @param UserIdentity $user User whose options are being saved |
1332 | * @param array &$modifiedOptions Options can be modified |
1333 | * @param array $originalOptions |
1334 | */ |
1335 | public function onSaveUserOptions( UserIdentity $user, array &$modifiedOptions, array $originalOptions ) { |
1336 | foreach ( self::getVirtualUserOptions() as $echoPref => $mwPref ) { |
1337 | // Save virtual option values in corresponding real option values |
1338 | if ( isset( $modifiedOptions[ $echoPref ] ) ) { |
1339 | $modifiedOptions[ $mwPref ] = $modifiedOptions[ $echoPref ]; |
1340 | unset( $modifiedOptions[ $echoPref ] ); |
1341 | } |
1342 | } |
1343 | } |
1344 | |
1345 | /** |
1346 | * Convert all values in an array to integers and filter out zeroes. |
1347 | * |
1348 | * @param array $numbers |
1349 | * |
1350 | * @return int[] |
1351 | */ |
1352 | protected static function mapToInt( array $numbers ) { |
1353 | $data = []; |
1354 | |
1355 | foreach ( $numbers as $value ) { |
1356 | $int = intval( $value ); |
1357 | if ( $int === 0 ) { |
1358 | continue; |
1359 | } |
1360 | $data[] = $int; |
1361 | } |
1362 | |
1363 | return $data; |
1364 | } |
1365 | |
1366 | /** |
1367 | * Handler for UserClearNewTalkNotification hook. |
1368 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/UserClearNewTalkNotification |
1369 | * @param UserIdentity $user User whose talk page notification should be marked as read |
1370 | * @param int $oldid |
1371 | */ |
1372 | public function onUserClearNewTalkNotification( $user, $oldid ) { |
1373 | if ( $user->isRegistered() ) { |
1374 | DeferredUpdates::addCallableUpdate( static function () use ( $user ) { |
1375 | NotifUser::newFromUser( $user )->clearUserTalkNotifications(); |
1376 | } ); |
1377 | } |
1378 | } |
1379 | |
1380 | /** |
1381 | * Handler for EmailUserComplete hook. |
1382 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/EmailUserComplete |
1383 | * @param MailAddress $address Adress of receiving user |
1384 | * @param MailAddress $from Adress of sending user |
1385 | * @param string $subject Subject of the mail |
1386 | * @param string $text Text of the mail |
1387 | */ |
1388 | public function onEmailUserComplete( $address, $from, $subject, $text ) { |
1389 | if ( $from->name === $address->name ) { |
1390 | // nothing to notify |
1391 | return; |
1392 | } |
1393 | $userTo = User::newFromName( $address->name ); |
1394 | $userFrom = User::newFromName( $from->name ); |
1395 | |
1396 | $autoSubject = wfMessage( 'defemailsubject', $from->name )->inContentLanguage()->text(); |
1397 | if ( $subject === $autoSubject ) { |
1398 | $autoFooter = "\n\n-- \n" . wfMessage( 'emailuserfooter', $from->name, $address->name ) |
1399 | ->inContentLanguage()->text(); |
1400 | $textWithoutFooter = preg_replace( '/' . preg_quote( $autoFooter, '/' ) . '$/', '', $text ); |
1401 | $preview = $this->contentLanguage->truncateForVisual( $textWithoutFooter, 125 ); |
1402 | } else { |
1403 | $preview = $subject; |
1404 | } |
1405 | |
1406 | Event::create( [ |
1407 | 'type' => 'emailuser', |
1408 | 'extra' => [ |
1409 | 'to-user-id' => $userTo->getId(), |
1410 | 'preview' => $preview, |
1411 | ], |
1412 | 'agent' => $userFrom, |
1413 | ] ); |
1414 | } |
1415 | |
1416 | /** |
1417 | * Sets custom login message for redirect from notification page |
1418 | * |
1419 | * @param array &$messages |
1420 | */ |
1421 | public function onLoginFormValidErrorMessages( array &$messages ) { |
1422 | $messages[] = 'echo-notification-loginrequired'; |
1423 | } |
1424 | |
1425 | public static function getConfigVars( RL\Context $context, Config $config ) { |
1426 | return [ |
1427 | 'EchoMaxNotificationCount' => NotifUser::MAX_BADGE_COUNT, |
1428 | 'EchoPollForUpdates' => $config->get( ConfigNames::PollForUpdates ) |
1429 | ]; |
1430 | } |
1431 | |
1432 | /** |
1433 | * @param WikiPage $article |
1434 | * @param User $user |
1435 | * @param string $reason |
1436 | * @param int $articleId |
1437 | * @param Content|null $content |
1438 | * @param LogEntry $logEntry |
1439 | * @param int $archivedRevisionCount |
1440 | */ |
1441 | public function onArticleDeleteComplete( |
1442 | $article, |
1443 | $user, |
1444 | $reason, |
1445 | $articleId, |
1446 | $content, |
1447 | $logEntry, |
1448 | $archivedRevisionCount |
1449 | ) { |
1450 | DeferredUpdates::addCallableUpdate( static function () use ( $articleId ) { |
1451 | $eventMapper = new EventMapper(); |
1452 | $eventIds = $eventMapper->fetchIdsByPage( $articleId ); |
1453 | ModerationController::moderate( $eventIds, true ); |
1454 | } ); |
1455 | } |
1456 | |
1457 | /** |
1458 | * @param Title $title |
1459 | * @param bool $create |
1460 | * @param string $comment |
1461 | * @param int $oldPageId |
1462 | * @param array $restoredPages |
1463 | */ |
1464 | public function onArticleUndelete( $title, $create, $comment, $oldPageId, $restoredPages ) { |
1465 | if ( $create ) { |
1466 | DeferredUpdates::addCallableUpdate( static function () use ( $oldPageId ) { |
1467 | $eventMapper = new EventMapper(); |
1468 | $eventIds = $eventMapper->fetchIdsByPage( $oldPageId ); |
1469 | ModerationController::moderate( $eventIds, false ); |
1470 | } ); |
1471 | } |
1472 | } |
1473 | |
1474 | /** |
1475 | * Handler for SpecialMuteModifyFormFields hook |
1476 | * |
1477 | * @param UserIdentity|null $target |
1478 | * @param User $user |
1479 | * @param array &$fields |
1480 | */ |
1481 | public function onSpecialMuteModifyFormFields( $target, $user, &$fields ) { |
1482 | $echoPerUserBlacklist = $this->config->get( ConfigNames::PerUserBlacklist ); |
1483 | if ( $echoPerUserBlacklist ) { |
1484 | $id = $target ? $this->centralIdLookup->centralIdFromLocalUser( $target ) : 0; |
1485 | $list = MultiUsernameFilter::splitIds( |
1486 | $this->userOptionsManager->getOption( $user, 'echo-notifications-blacklist' ) |
1487 | ); |
1488 | $fields[ 'echo-notifications-blacklist'] = [ |
1489 | 'type' => 'check', |
1490 | 'label-message' => [ |
1491 | 'echo-specialmute-label-mute-notifications', |
1492 | $target ? $target->getName() : '' |
1493 | ], |
1494 | 'default' => in_array( $id, $list, true ), |
1495 | ]; |
1496 | } |
1497 | } |
1498 | |
1499 | /** |
1500 | * @param RecentChange $change |
1501 | * @return bool|void |
1502 | */ |
1503 | public function onRecentChange_save( $change ) { |
1504 | if ( !$this->config->get( 'EchoWatchlistNotifications' ) ) { |
1505 | return; |
1506 | } |
1507 | if ( $change->getAttribute( 'rc_minor' ) ) { |
1508 | $type = 'minor-watchlist-change'; |
1509 | } else { |
1510 | $type = 'watchlist-change'; |
1511 | } |
1512 | Event::create( [ |
1513 | 'type' => $type, |
1514 | 'title' => $change->getTitle(), |
1515 | 'extra' => [ |
1516 | 'page_title' => $change->getPage()->getDBkey(), |
1517 | 'page_namespace' => $change->getPage()->getNamespace(), |
1518 | 'revid' => $change->getAttribute( "rc_this_oldid" ), |
1519 | 'logid' => $change->getAttribute( "rc_logid" ), |
1520 | 'status' => $change->mExtra["pageStatus"], |
1521 | 'timestamp' => $change->getAttribute( "rc_timestamp" ), |
1522 | 'emailonce' => $this->config->get( ConfigNames::WatchlistEmailOncePerPage ), |
1523 | ], |
1524 | 'agent' => $change->getPerformerIdentity(), |
1525 | ] ); |
1526 | } |
1527 | |
1528 | /** |
1529 | * Hook handler for ApiMain::moduleManager. |
1530 | * Used here to put the echopushsubscriptions API module behind our push feature flag. |
1531 | * TODO: Register this the usual way in extension.json when we don't need the feature flag |
1532 | * anymore. |
1533 | * @param ApiModuleManager $moduleManager |
1534 | */ |
1535 | public function onApiMain__ModuleManager( $moduleManager ) { |
1536 | $pushEnabled = $this->config->get( 'EchoEnablePush' ); |
1537 | if ( $pushEnabled ) { |
1538 | $moduleManager->addModule( |
1539 | 'echopushsubscriptions', |
1540 | 'action', |
1541 | ApiEchoPushSubscriptions::class |
1542 | ); |
1543 | } |
1544 | } |
1545 | |
1546 | } |