Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 134
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 134
0.00% covered (danger)
0.00%
0 / 19
5112
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 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
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isUserBlockedFromThanks
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 canReceiveThanks
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 generateThankElement
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
56
 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
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 onGetLogTypesOnUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
132
1<?php
2
3// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
4
5namespace MediaWiki\Extension\Thanks;
6
7use ApiModuleManager;
8use Article;
9use DatabaseLogEntry;
10use DifferenceEngine;
11use ExtensionRegistry;
12use LogEventsList;
13use LogPage;
14use MediaWiki\Api\Hook\ApiMain__moduleManagerHook;
15use MediaWiki\Auth\Hook\LocalUserCreatedHook;
16use MediaWiki\Block\Hook\GetAllBlockActionsHook;
17use MediaWiki\Cache\GenderCache;
18use MediaWiki\Config\Config;
19use MediaWiki\Config\ConfigException;
20use MediaWiki\Context\IContextSource;
21use MediaWiki\Context\RequestContext;
22use MediaWiki\Diff\Hook\DifferenceEngineViewHeaderHook;
23use MediaWiki\Diff\Hook\DiffToolsHook;
24use MediaWiki\Extension\Thanks\Api\ApiFlowThank;
25use MediaWiki\Hook\ChangesListInitRowsHook;
26use MediaWiki\Hook\GetLogTypesOnUserHook;
27use MediaWiki\Hook\HistoryToolsHook;
28use MediaWiki\Hook\LogEventsListLineEndingHook;
29use MediaWiki\Hook\PageHistoryBeforeListHook;
30use MediaWiki\Hook\PageHistoryPager__doBatchLookupsHook;
31use MediaWiki\Html\Html;
32use MediaWiki\Linker\LinkTarget;
33use MediaWiki\Output\Hook\BeforePageDisplayHook;
34use MediaWiki\Output\OutputPage;
35use MediaWiki\Permissions\PermissionManager;
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                'data-' . $type . '-id' => $id,
279                'data-recipient-gender' => $this->genderCache->getGenderOf( $recipient->getName(), __METHOD__ ),
280            ],
281            wfMessage( 'thanks-thank', $sender->getName(), $recipient->getName() )->text()
282        );
283    }
284
285    /**
286     * @param OutputPage $outputPage The OutputPage to add the module to.
287     */
288    protected function addThanksModule( OutputPage $outputPage ) {
289        $confirmationRequired = $this->config->get( 'ThanksConfirmationRequired' );
290        $outputPage->addModules( [ 'ext.thanks.corethank' ] );
291        $outputPage->addJsConfigVars( 'thanks-confirmation-required', $confirmationRequired );
292    }
293
294    /**
295     * Handler for PageHistoryBeforeList hook.
296     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageHistoryBeforeList
297     *
298     * @param Article $page Not used
299     * @param IContextSource $context RequestContext object
300     */
301    public function onPageHistoryBeforeList( $page, $context ) {
302        if ( $context->getUser()->isRegistered() ) {
303            $this->addThanksModule( $context->getOutput() );
304        }
305    }
306
307    public function onPageHistoryPager__doBatchLookups( $pager, $result ) {
308        $userNames = [];
309        foreach ( $result as $row ) {
310            if ( $row->user_name !== null ) {
311                $userNames[] = $row->user_name;
312            }
313        }
314        if ( $userNames ) {
315            // Batch lookup for the use of GenderCache::getGenderOf in self::generateThankElement
316            $this->genderCache->doQuery( $userNames, __METHOD__ );
317        }
318    }
319
320    public function onChangesListInitRows( $changesList, $rows ) {
321        $userNames = [];
322        foreach ( $rows as $row ) {
323            if ( $row->rc_user_text !== null ) {
324                $userNames[] = $row->rc_user_text;
325            }
326        }
327        if ( $userNames ) {
328            // Batch lookup for the use of GenderCache::getGenderOf in self::generateThankElement
329            $this->genderCache->doQuery( $userNames, __METHOD__ );
330        }
331    }
332
333    /**
334     * Handler for DifferenceEngineViewHeader hook.
335     * @see https://www.mediawiki.org/wiki/Manual:Hooks/DifferenceEngineViewHeader
336     * @param DifferenceEngine $diff DifferenceEngine object that's calling.
337     */
338    public function onDifferenceEngineViewHeader( $diff ) {
339        if ( $diff->getUser()->isRegistered() ) {
340            $this->addThanksModule( $diff->getOutput() );
341        }
342    }
343
344    /**
345     * Handler for LocalUserCreated hook
346     * @see https://www.mediawiki.org/wiki/Manual:Hooks/LocalUserCreated
347     * @param User $user User object that was created.
348     * @param bool $autocreated True when account was auto-created
349     */
350    public function onLocalUserCreated( $user, $autocreated ) {
351        // New users get echo preferences set that are not the default settings for existing users.
352        // Specifically, new users are opted into email notifications for thanks.
353        if ( !$user->isTemp() && !$autocreated ) {
354            $this->userOptionsManager->setOption( $user, 'echo-subscriptions-email-edit-thank', true );
355        }
356    }
357
358    /**
359     * Handler for GetLogTypesOnUser.
360     * So users can just type in a username for target and it'll work.
361     * @link https://www.mediawiki.org/wiki/Manual:Hooks/GetLogTypesOnUser
362     * @param string[] &$types The list of log types, to add to.
363     */
364    public function onGetLogTypesOnUser( &$types ) {
365        $types[] = 'thanks';
366    }
367
368    public function onGetAllBlockActions( &$actions ) {
369        $actions[ 'thanks' ] = 100;
370    }
371
372    /**
373     * Handler for BeforePageDisplay.  Inserts javascript to enhance thank
374     * links from static urls to in-page dialogs along with reloading
375     * the previously thanked state.
376     * @link https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay
377     * @param OutputPage $out OutputPage object
378     * @param Skin $skin The skin in use.
379     */
380    public function onBeforePageDisplay( $out, $skin ): void {
381        $title = $out->getTitle();
382        // Add to Flow boards.
383        if ( $title instanceof Title && $title->hasContentModel( 'flow-board' ) ) {
384            $out->addModules( 'ext.thanks.flowthank' );
385        }
386        // Add to special pages where thank links appear
387        if (
388            $title->isSpecial( 'Log' ) ||
389            $title->isSpecial( 'Contributions' ) ||
390            $title->isSpecial( 'DeletedContributions' ) ||
391            $title->isSpecial( 'Recentchanges' ) ||
392            $title->isSpecial( 'Recentchangeslinked' ) ||
393            $title->isSpecial( 'Watchlist' )
394        ) {
395            $this->addThanksModule( $out );
396        }
397    }
398
399    /**
400     * Conditionally load API module 'flowthank' depending on whether or not
401     * Flow is installed.
402     *
403     * @param ApiModuleManager $moduleManager Module manager instance
404     */
405    public function onApiMain__moduleManager( $moduleManager ) {
406        if ( ExtensionRegistry::getInstance()->isLoaded( 'Flow' ) ) {
407            $moduleManager->addModule(
408                'flowthank',
409                'action',
410                [
411                    "class" => ApiFlowThank::class,
412                    "services" => [
413                        "PermissionManager",
414                        "ThanksLogStore",
415                        "UserFactory",
416                    ]
417                ]
418            );
419        }
420    }
421
422    /**
423     * Insert a 'thank' link into the log interface, if the user is allowed to thank.
424     *
425     * @link https://www.mediawiki.org/wiki/Manual:Hooks/LogEventsListLineEnding
426     * @param LogEventsList $page The log events list.
427     * @param string &$ret The line ending HTML, to modify.
428     * @param DatabaseLogEntry $entry The log entry.
429     * @param string[] &$classes CSS classes to add to the line.
430     * @param string[] &$attribs HTML attributes to add to the line.
431     * @throws ConfigException
432     */
433    public function onLogEventsListLineEnding(
434        $page, &$ret, $entry, &$classes, &$attribs
435    ) {
436        $user = $page->getUser();
437
438        // Don't thank if anonymous or blocked or if user is deleted from the log entry
439        if (
440            $user->isAnon()
441            || $entry->isDeleted( LogPage::DELETED_USER )
442            || $this->isUserBlockedFromTitle( $user, $entry->getTarget() )
443            || self::isUserBlockedFromThanks( $user )
444        ) {
445            return;
446        }
447
448        // Make sure this log type is allowed.
449        $allowedLogTypes = $this->config->get( 'ThanksAllowedLogTypes' );
450        if ( !in_array( $entry->getType(), $allowedLogTypes )
451            && !in_array( $entry->getType() . '/' . $entry->getSubtype(), $allowedLogTypes ) ) {
452            return;
453        }
454
455        // Don't thank if no recipient,
456        // or if recipient is the current user or unable to receive thanks.
457        // Don't check for deleted revision (this avoids extraneous queries from Special:Log).
458
459        $recipient = $entry->getPerformerIdentity();
460        if ( $recipient->getId() === $user->getId() ||
461            !self::canReceiveThanks( $this->config, $this->userFactory, $recipient )
462        ) {
463            return;
464        }
465
466        // Create thank link either for the revision (if there is an associated revision ID)
467        // or the log entry.
468        $type = $entry->getAssociatedRevId() ? 'revision' : 'log';
469        $id = $entry->getAssociatedRevId() ?: $entry->getId();
470        $thankLink = $this->generateThankElement( $id, $user, $recipient, $type );
471
472        // Add parentheses to match what's done with Thanks in revision lists and diff displays.
473        $ret .= ' ' . wfMessage( 'parentheses' )->rawParams( $thankLink )->escaped();
474    }
475}