Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
40.74% |
55 / 135 |
|
21.05% |
4 / 19 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
40.74% |
55 / 135 |
|
21.05% |
4 / 19 |
1120.02 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
onHistoryTools | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onDiffTools | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
insertThankLink | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
90 | |||
isUserBlockedFromTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isUserBlockedFromThanks | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
canReceiveThanks | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
5.58 | |||
generateThankElement | |
75.86% |
22 / 29 |
|
0.00% |
0 / 1 |
7.69 | |||
addThanksModule | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
onPageHistoryBeforeList | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
onPageHistoryPager__doBatchLookups | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
onChangesListInitRows | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
onDifferenceEngineViewHeader | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
onLocalUserCreated | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
4.12 | |||
onGetLogTypesOnUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onGetAllBlockActions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onBeforePageDisplay | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
90 | |||
onApiMain__moduleManager | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
onLogEventsListLineEnding | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
11.02 |
1 | <?php |
2 | |
3 | // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
4 | |
5 | namespace MediaWiki\Extension\Thanks; |
6 | |
7 | use Article; |
8 | use DatabaseLogEntry; |
9 | use DifferenceEngine; |
10 | use LogEventsList; |
11 | use LogPage; |
12 | use MediaWiki\Api\ApiModuleManager; |
13 | use MediaWiki\Api\Hook\ApiMain__moduleManagerHook; |
14 | use MediaWiki\Auth\Hook\LocalUserCreatedHook; |
15 | use MediaWiki\Block\Hook\GetAllBlockActionsHook; |
16 | use MediaWiki\Cache\GenderCache; |
17 | use MediaWiki\Config\Config; |
18 | use MediaWiki\Config\ConfigException; |
19 | use MediaWiki\Context\IContextSource; |
20 | use MediaWiki\Context\RequestContext; |
21 | use MediaWiki\Diff\Hook\DifferenceEngineViewHeaderHook; |
22 | use MediaWiki\Diff\Hook\DiffToolsHook; |
23 | use MediaWiki\Extension\Thanks\Api\ApiFlowThank; |
24 | use MediaWiki\Hook\ChangesListInitRowsHook; |
25 | use MediaWiki\Hook\GetLogTypesOnUserHook; |
26 | use MediaWiki\Hook\HistoryToolsHook; |
27 | use MediaWiki\Hook\LogEventsListLineEndingHook; |
28 | use MediaWiki\Hook\PageHistoryBeforeListHook; |
29 | use MediaWiki\Hook\PageHistoryPager__doBatchLookupsHook; |
30 | use MediaWiki\Html\Html; |
31 | use MediaWiki\Linker\LinkTarget; |
32 | use MediaWiki\Output\Hook\BeforePageDisplayHook; |
33 | use MediaWiki\Output\OutputPage; |
34 | use MediaWiki\Permissions\PermissionManager; |
35 | use MediaWiki\Registration\ExtensionRegistry; |
36 | use MediaWiki\Revision\RevisionLookup; |
37 | use MediaWiki\Revision\RevisionRecord; |
38 | use MediaWiki\SpecialPage\SpecialPage; |
39 | use MediaWiki\Title\Title; |
40 | use MediaWiki\User\Options\UserOptionsManager; |
41 | use MediaWiki\User\User; |
42 | use MediaWiki\User\UserFactory; |
43 | use MediaWiki\User\UserIdentity; |
44 | use Skin; |
45 | |
46 | /** |
47 | * Hooks for Thanks extension |
48 | * |
49 | * @file |
50 | * @ingroup Extensions |
51 | */ |
52 | class Hooks implements |
53 | ApiMain__moduleManagerHook, |
54 | BeforePageDisplayHook, |
55 | DiffToolsHook, |
56 | DifferenceEngineViewHeaderHook, |
57 | GetAllBlockActionsHook, |
58 | GetLogTypesOnUserHook, |
59 | HistoryToolsHook, |
60 | LocalUserCreatedHook, |
61 | LogEventsListLineEndingHook, |
62 | PageHistoryBeforeListHook, |
63 | PageHistoryPager__doBatchLookupsHook, |
64 | ChangesListInitRowsHook |
65 | { |
66 | private Config $config; |
67 | private GenderCache $genderCache; |
68 | private PermissionManager $permissionManager; |
69 | private RevisionLookup $revisionLookup; |
70 | private UserFactory $userFactory; |
71 | private UserOptionsManager $userOptionsManager; |
72 | |
73 | public function __construct( |
74 | Config $config, |
75 | GenderCache $genderCache, |
76 | PermissionManager $permissionManager, |
77 | RevisionLookup $revisionLookup, |
78 | UserFactory $userFactory, |
79 | UserOptionsManager $userOptionsManager |
80 | ) { |
81 | $this->config = $config; |
82 | $this->genderCache = $genderCache; |
83 | $this->permissionManager = $permissionManager; |
84 | $this->revisionLookup = $revisionLookup; |
85 | $this->userFactory = $userFactory; |
86 | $this->userOptionsManager = $userOptionsManager; |
87 | } |
88 | |
89 | /** |
90 | * Handler for the HistoryTools hook |
91 | * |
92 | * @param RevisionRecord $revisionRecord |
93 | * @param array &$links |
94 | * @param RevisionRecord|null $oldRevisionRecord |
95 | * @param UserIdentity $userIdentity |
96 | */ |
97 | public function onHistoryTools( |
98 | $revisionRecord, |
99 | &$links, |
100 | $oldRevisionRecord, |
101 | $userIdentity |
102 | ) { |
103 | $this->insertThankLink( $revisionRecord, |
104 | $links, $userIdentity ); |
105 | } |
106 | |
107 | /** |
108 | * Handler for the DiffTools hook |
109 | * |
110 | * @param RevisionRecord $revisionRecord |
111 | * @param array &$links |
112 | * @param RevisionRecord|null $oldRevisionRecord |
113 | * @param UserIdentity $userIdentity |
114 | */ |
115 | public function onDiffTools( |
116 | $revisionRecord, |
117 | &$links, |
118 | $oldRevisionRecord, |
119 | $userIdentity |
120 | ) { |
121 | // Don't allow thanking for a diff that includes multiple revisions |
122 | // This does a query that is too expensive for history rows (T284274) |
123 | $previous = $this->revisionLookup->getPreviousRevision( $revisionRecord ); |
124 | if ( $oldRevisionRecord && $previous && |
125 | $previous->getId() !== $oldRevisionRecord->getId() |
126 | ) { |
127 | return; |
128 | } |
129 | |
130 | $this->insertThankLink( $revisionRecord, |
131 | $links, $userIdentity, true ); |
132 | } |
133 | |
134 | /** |
135 | * Insert a 'thank' link into revision interface, if the user is allowed to thank. |
136 | * |
137 | * @param RevisionRecord $revisionRecord RevisionRecord object to add the thank link for |
138 | * @param array &$links Links to add to the revision interface |
139 | * @param UserIdentity $userIdentity The user performing the thanks. |
140 | * @param bool $isPrimaryButton whether the link/button should be progressive |
141 | */ |
142 | private function insertThankLink( |
143 | RevisionRecord $revisionRecord, |
144 | array &$links, |
145 | UserIdentity $userIdentity, |
146 | bool $isPrimaryButton = false |
147 | ) { |
148 | $recipient = $revisionRecord->getUser(); |
149 | if ( $recipient === null ) { |
150 | // Cannot see the user |
151 | return; |
152 | } |
153 | |
154 | $user = $this->userFactory->newFromUserIdentity( $userIdentity ); |
155 | |
156 | // Don't let users thank themselves. |
157 | // Exclude anonymous users. |
158 | // Exclude temp users (T345679) |
159 | // Exclude users who are blocked. |
160 | // Check whether bots are allowed to receive thanks. |
161 | // Don't allow thanking for a diff that includes multiple revisions |
162 | // Check whether we have a revision id to link to |
163 | if ( $user->isNamed() |
164 | && !$userIdentity->equals( $recipient ) |
165 | && !$this->isUserBlockedFromTitle( $user, $revisionRecord->getPageAsLinkTarget() ) |
166 | && !self::isUserBlockedFromThanks( $user ) |
167 | && self::canReceiveThanks( $this->config, $this->userFactory, $recipient ) |
168 | && !$revisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) |
169 | && $revisionRecord->getId() !== 0 |
170 | ) { |
171 | $links[] = $this->generateThankElement( |
172 | $revisionRecord->getId(), |
173 | $user, |
174 | $recipient, |
175 | 'revision', |
176 | $isPrimaryButton |
177 | ); |
178 | } |
179 | } |
180 | |
181 | /** |
182 | * Check whether the user is blocked from the title associated with the revision. |
183 | * |
184 | * This queries the replicas for a block; if 'no block' is incorrectly reported, it |
185 | * will be caught by ApiThank::dieOnUserBlockedFromTitle when the user attempts to thank. |
186 | * |
187 | * @param User $user |
188 | * @param LinkTarget $title |
189 | * @return bool |
190 | */ |
191 | private function isUserBlockedFromTitle( User $user, LinkTarget $title ) { |
192 | return $this->permissionManager->isBlockedFrom( $user, $title, true ); |
193 | } |
194 | |
195 | /** |
196 | * Check whether the user is blocked from giving thanks. |
197 | * |
198 | * @param User $user |
199 | * @return bool |
200 | */ |
201 | private static function isUserBlockedFromThanks( User $user ) { |
202 | $block = $user->getBlock(); |
203 | return $block && ( $block->isSitewide() || $block->appliesToRight( 'thanks' ) ); |
204 | } |
205 | |
206 | /** |
207 | * Check whether a user is allowed to receive thanks or not |
208 | * |
209 | * @param Config $config |
210 | * @param UserFactory $userFactory |
211 | * @param UserIdentity $user Recipient |
212 | * @return bool true if allowed, false if not |
213 | */ |
214 | public static function canReceiveThanks( |
215 | Config $config, |
216 | UserFactory $userFactory, |
217 | UserIdentity $user |
218 | ) { |
219 | $legacyUser = $userFactory->newFromUserIdentity( $user ); |
220 | if ( !$user->isRegistered() || $legacyUser->isSystemUser() ) { |
221 | return false; |
222 | } |
223 | |
224 | if ( !$config->get( 'ThanksSendToBots' ) && |
225 | $legacyUser->isBot() |
226 | ) { |
227 | return false; |
228 | } |
229 | |
230 | return true; |
231 | } |
232 | |
233 | /** |
234 | * Helper for self::insertThankLink |
235 | * Creates either a thank link or thanked span based on users session |
236 | * @param int $id Revision or log ID to generate the thank element for. |
237 | * @param User $sender User who sends thanks notification. |
238 | * @param UserIdentity $recipient User who receives thanks notification. |
239 | * @param string $type Either 'revision' or 'log'. |
240 | * @param bool $isPrimaryButton whether the link/button should be progressive |
241 | * @return string |
242 | */ |
243 | protected function generateThankElement( |
244 | $id, User $sender, UserIdentity $recipient, $type = 'revision', |
245 | bool $isPrimaryButton = false |
246 | ) { |
247 | $useCodex = RequestContext::getMain()->getSkin()->getSkinName() === 'minerva'; |
248 | // Check if the user has already thanked for this revision or log entry. |
249 | // Session keys are backwards-compatible, and are also used in the ApiCoreThank class. |
250 | $sessionKey = ( $type === 'revision' ) ? $id : $type . $id; |
251 | $class = $useCodex ? 'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled' : ''; |
252 | if ( $isPrimaryButton && $useCodex ) { |
253 | $class .= ' cdx-button--weight-primary cdx-button--action-progressive'; |
254 | } |
255 | if ( $sender->getRequest()->getSessionData( "thanks-thanked-$sessionKey" ) ) { |
256 | $class .= ' mw-thanks-thanked'; |
257 | |
258 | return Html::element( |
259 | 'span', |
260 | [ 'class' => $class ], |
261 | wfMessage( 'thanks-thanked', $sender->getName(), $recipient->getName() )->text() |
262 | ); |
263 | } |
264 | |
265 | // Add 'thank' link |
266 | $tooltip = wfMessage( 'thanks-thank-tooltip' ) |
267 | ->params( $sender->getName(), $recipient->getName() ) |
268 | ->text(); |
269 | |
270 | $class .= ' mw-thanks-thank-link'; |
271 | $subpage = ( $type === 'revision' ) ? '' : 'Log/'; |
272 | return Html::element( |
273 | 'a', |
274 | [ |
275 | 'class' => $class, |
276 | 'href' => SpecialPage::getTitleFor( 'Thanks', $subpage . $id )->getFullURL(), |
277 | 'title' => $tooltip, |
278 | 'role' => 'button', |
279 | 'data-' . $type . '-id' => $id, |
280 | 'data-recipient-gender' => $this->genderCache->getGenderOf( $recipient->getName(), __METHOD__ ), |
281 | ], |
282 | wfMessage( 'thanks-thank', $sender->getName(), $recipient->getName() )->text() |
283 | ); |
284 | } |
285 | |
286 | /** |
287 | * @param OutputPage $outputPage The OutputPage to add the module to. |
288 | */ |
289 | protected function addThanksModule( OutputPage $outputPage ) { |
290 | $confirmationRequired = $this->config->get( 'ThanksConfirmationRequired' ); |
291 | $outputPage->addModules( [ 'ext.thanks.corethank' ] ); |
292 | $outputPage->addJsConfigVars( 'thanks-confirmation-required', $confirmationRequired ); |
293 | } |
294 | |
295 | /** |
296 | * Handler for PageHistoryBeforeList hook. |
297 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageHistoryBeforeList |
298 | * |
299 | * @param Article $page Not used |
300 | * @param IContextSource $context RequestContext object |
301 | */ |
302 | public function onPageHistoryBeforeList( $page, $context ) { |
303 | if ( $context->getUser()->isRegistered() ) { |
304 | $this->addThanksModule( $context->getOutput() ); |
305 | } |
306 | } |
307 | |
308 | public function onPageHistoryPager__doBatchLookups( $pager, $result ) { |
309 | $userNames = []; |
310 | foreach ( $result as $row ) { |
311 | if ( $row->user_name !== null ) { |
312 | $userNames[] = $row->user_name; |
313 | } |
314 | } |
315 | if ( $userNames ) { |
316 | // Batch lookup for the use of GenderCache::getGenderOf in self::generateThankElement |
317 | $this->genderCache->doQuery( $userNames, __METHOD__ ); |
318 | } |
319 | } |
320 | |
321 | public function onChangesListInitRows( $changesList, $rows ) { |
322 | $userNames = []; |
323 | foreach ( $rows as $row ) { |
324 | if ( $row->rc_user_text !== null ) { |
325 | $userNames[] = $row->rc_user_text; |
326 | } |
327 | } |
328 | if ( $userNames ) { |
329 | // Batch lookup for the use of GenderCache::getGenderOf in self::generateThankElement |
330 | $this->genderCache->doQuery( $userNames, __METHOD__ ); |
331 | } |
332 | } |
333 | |
334 | /** |
335 | * Handler for DifferenceEngineViewHeader hook. |
336 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/DifferenceEngineViewHeader |
337 | * @param DifferenceEngine $diff DifferenceEngine object that's calling. |
338 | */ |
339 | public function onDifferenceEngineViewHeader( $diff ) { |
340 | if ( $diff->getUser()->isRegistered() ) { |
341 | $this->addThanksModule( $diff->getOutput() ); |
342 | } |
343 | } |
344 | |
345 | /** |
346 | * Handler for LocalUserCreated hook |
347 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/LocalUserCreated |
348 | * @param User $user User object that was created. |
349 | * @param bool $autocreated True when account was auto-created |
350 | */ |
351 | public function onLocalUserCreated( $user, $autocreated ) { |
352 | // New users get echo preferences set that are not the default settings for existing users. |
353 | // Specifically, new users are opted into email notifications for thanks. |
354 | if ( !$user->isTemp() && !$autocreated ) { |
355 | $this->userOptionsManager->setOption( $user, 'echo-subscriptions-email-edit-thank', true ); |
356 | } |
357 | } |
358 | |
359 | /** |
360 | * Handler for GetLogTypesOnUser. |
361 | * So users can just type in a username for target and it'll work. |
362 | * @link https://www.mediawiki.org/wiki/Manual:Hooks/GetLogTypesOnUser |
363 | * @param string[] &$types The list of log types, to add to. |
364 | */ |
365 | public function onGetLogTypesOnUser( &$types ) { |
366 | $types[] = 'thanks'; |
367 | } |
368 | |
369 | public function onGetAllBlockActions( &$actions ) { |
370 | $actions[ 'thanks' ] = 100; |
371 | } |
372 | |
373 | /** |
374 | * Handler for BeforePageDisplay. Inserts javascript to enhance thank |
375 | * links from static urls to in-page dialogs along with reloading |
376 | * the previously thanked state. |
377 | * @link https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay |
378 | * @param OutputPage $out OutputPage object |
379 | * @param Skin $skin The skin in use. |
380 | */ |
381 | public function onBeforePageDisplay( $out, $skin ): void { |
382 | $title = $out->getTitle(); |
383 | // Add to Flow boards. |
384 | if ( $title instanceof Title && $title->hasContentModel( 'flow-board' ) ) { |
385 | $out->addModules( 'ext.thanks.flowthank' ); |
386 | } |
387 | // Add to special pages where thank links appear |
388 | if ( |
389 | $title->isSpecial( 'Log' ) || |
390 | $title->isSpecial( 'Contributions' ) || |
391 | $title->isSpecial( 'DeletedContributions' ) || |
392 | $title->isSpecial( 'Recentchanges' ) || |
393 | $title->isSpecial( 'Recentchangeslinked' ) || |
394 | $title->isSpecial( 'Watchlist' ) |
395 | ) { |
396 | $this->addThanksModule( $out ); |
397 | } |
398 | } |
399 | |
400 | /** |
401 | * Conditionally load API module 'flowthank' depending on whether or not |
402 | * Flow is installed. |
403 | * |
404 | * @param ApiModuleManager $moduleManager Module manager instance |
405 | */ |
406 | public function onApiMain__moduleManager( $moduleManager ) { |
407 | if ( ExtensionRegistry::getInstance()->isLoaded( 'Flow' ) ) { |
408 | $moduleManager->addModule( |
409 | 'flowthank', |
410 | 'action', |
411 | [ |
412 | "class" => ApiFlowThank::class, |
413 | "services" => [ |
414 | "PermissionManager", |
415 | "ThanksLogStore", |
416 | "UserFactory", |
417 | ] |
418 | ] |
419 | ); |
420 | } |
421 | } |
422 | |
423 | /** |
424 | * Insert a 'thank' link into the log interface, if the user is allowed to thank. |
425 | * |
426 | * @link https://www.mediawiki.org/wiki/Manual:Hooks/LogEventsListLineEnding |
427 | * @param LogEventsList $page The log events list. |
428 | * @param string &$ret The line ending HTML, to modify. |
429 | * @param DatabaseLogEntry $entry The log entry. |
430 | * @param string[] &$classes CSS classes to add to the line. |
431 | * @param string[] &$attribs HTML attributes to add to the line. |
432 | * @throws ConfigException |
433 | */ |
434 | public function onLogEventsListLineEnding( |
435 | $page, &$ret, $entry, &$classes, &$attribs |
436 | ) { |
437 | $user = $page->getUser(); |
438 | |
439 | // Don't provide thanks link if not named, blocked or if user is deleted from the log entry |
440 | if ( |
441 | !$user->isNamed() |
442 | || $entry->isDeleted( LogPage::DELETED_USER ) |
443 | || $this->isUserBlockedFromTitle( $user, $entry->getTarget() ) |
444 | || self::isUserBlockedFromThanks( $user ) |
445 | ) { |
446 | return; |
447 | } |
448 | |
449 | // Make sure this log type is allowed. |
450 | $allowedLogTypes = $this->config->get( 'ThanksAllowedLogTypes' ); |
451 | if ( !in_array( $entry->getType(), $allowedLogTypes ) |
452 | && !in_array( $entry->getType() . '/' . $entry->getSubtype(), $allowedLogTypes ) ) { |
453 | return; |
454 | } |
455 | |
456 | // Don't thank if no recipient, |
457 | // or if recipient is the current user or unable to receive thanks. |
458 | // Don't check for deleted revision (this avoids extraneous queries from Special:Log). |
459 | |
460 | $recipient = $entry->getPerformerIdentity(); |
461 | if ( $recipient->getId() === $user->getId() || |
462 | !self::canReceiveThanks( $this->config, $this->userFactory, $recipient ) |
463 | ) { |
464 | return; |
465 | } |
466 | |
467 | // Create thank link either for the revision (if there is an associated revision ID) |
468 | // or the log entry. |
469 | $type = $entry->getAssociatedRevId() ? 'revision' : 'log'; |
470 | $id = $entry->getAssociatedRevId() ?: $entry->getId(); |
471 | $thankLink = $this->generateThankElement( $id, $user, $recipient, $type ); |
472 | |
473 | // Add parentheses to match what's done with Thanks in revision lists and diff displays. |
474 | $ret .= ' ' . wfMessage( 'parentheses' )->rawParams( $thankLink )->escaped(); |
475 | } |
476 | } |