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