Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.74% covered (danger)
40.74%
55 / 135
21.05% covered (danger)
21.05%
4 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
40.74% covered (danger)
40.74%
55 / 135
21.05% covered (danger)
21.05%
4 / 19
1120.02
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 onHistoryTools
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onDiffTools
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 insertThankLink
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
90
 isUserBlockedFromTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isUserBlockedFromThanks
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 canReceiveThanks
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
5.58
 generateThankElement
75.86% covered (warning)
75.86%
22 / 29
0.00% covered (danger)
0.00%
0 / 1
7.69
 addThanksModule
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 onPageHistoryBeforeList
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onPageHistoryPager__doBatchLookups
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 onChangesListInitRows
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 onDifferenceEngineViewHeader
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onLocalUserCreated
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
4.12
 onGetLogTypesOnUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onGetAllBlockActions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
90
 onApiMain__moduleManager
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 onLogEventsListLineEnding
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
11.02
1<?php
2
3// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
4
5namespace MediaWiki\Extension\Thanks;
6
7use Article;
8use DatabaseLogEntry;
9use DifferenceEngine;
10use LogEventsList;
11use LogPage;
12use MediaWiki\Api\ApiModuleManager;
13use MediaWiki\Api\Hook\ApiMain__moduleManagerHook;
14use MediaWiki\Auth\Hook\LocalUserCreatedHook;
15use MediaWiki\Block\Hook\GetAllBlockActionsHook;
16use MediaWiki\Cache\GenderCache;
17use MediaWiki\Config\Config;
18use MediaWiki\Config\ConfigException;
19use MediaWiki\Context\IContextSource;
20use MediaWiki\Context\RequestContext;
21use MediaWiki\Diff\Hook\DifferenceEngineViewHeaderHook;
22use MediaWiki\Diff\Hook\DiffToolsHook;
23use MediaWiki\Extension\Thanks\Api\ApiFlowThank;
24use MediaWiki\Hook\ChangesListInitRowsHook;
25use MediaWiki\Hook\GetLogTypesOnUserHook;
26use MediaWiki\Hook\HistoryToolsHook;
27use MediaWiki\Hook\LogEventsListLineEndingHook;
28use MediaWiki\Hook\PageHistoryBeforeListHook;
29use MediaWiki\Hook\PageHistoryPager__doBatchLookupsHook;
30use MediaWiki\Html\Html;
31use MediaWiki\Linker\LinkTarget;
32use MediaWiki\Output\Hook\BeforePageDisplayHook;
33use MediaWiki\Output\OutputPage;
34use MediaWiki\Permissions\PermissionManager;
35use MediaWiki\Registration\ExtensionRegistry;
36use MediaWiki\Revision\RevisionLookup;
37use MediaWiki\Revision\RevisionRecord;
38use MediaWiki\SpecialPage\SpecialPage;
39use MediaWiki\Title\Title;
40use MediaWiki\User\Options\UserOptionsManager;
41use MediaWiki\User\User;
42use MediaWiki\User\UserFactory;
43use MediaWiki\User\UserIdentity;
44use Skin;
45
46/**
47 * Hooks for Thanks extension
48 *
49 * @file
50 * @ingroup Extensions
51 */
52class 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}