Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 121
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 121
0.00% covered (danger)
0.00%
0 / 17
4032
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
 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 / 12
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 GenderCache;
13use IContextSource;
14use LogEventsList;
15use LogPage;
16use MediaWiki\Api\Hook\ApiMain__moduleManagerHook;
17use MediaWiki\Auth\Hook\LocalUserCreatedHook;
18use MediaWiki\Block\Hook\GetAllBlockActionsHook;
19use MediaWiki\Config\Config;
20use MediaWiki\Config\ConfigException;
21use MediaWiki\Diff\Hook\DifferenceEngineViewHeaderHook;
22use MediaWiki\Diff\Hook\DiffToolsHook;
23use MediaWiki\Extension\Thanks\Api\ApiFlowThank;
24use MediaWiki\Hook\BeforePageDisplayHook;
25use MediaWiki\Hook\GetLogTypesOnUserHook;
26use MediaWiki\Hook\HistoryToolsHook;
27use MediaWiki\Hook\LogEventsListLineEndingHook;
28use MediaWiki\Hook\PageHistoryBeforeListHook;
29use MediaWiki\Html\Html;
30use MediaWiki\Linker\LinkTarget;
31use MediaWiki\Output\OutputPage;
32use MediaWiki\Permissions\PermissionManager;
33use MediaWiki\Revision\RevisionLookup;
34use MediaWiki\Revision\RevisionRecord;
35use MediaWiki\SpecialPage\SpecialPage;
36use MediaWiki\Title\Title;
37use MediaWiki\User\Options\UserOptionsManager;
38use MediaWiki\User\User;
39use MediaWiki\User\UserFactory;
40use MediaWiki\User\UserIdentity;
41use RequestContext;
42use Skin;
43
44/**
45 * Hooks for Thanks extension
46 *
47 * @file
48 * @ingroup Extensions
49 */
50class 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}