Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.59% covered (warning)
85.59%
101 / 118
41.67% covered (danger)
41.67%
5 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiCoreThank
85.59% covered (warning)
85.59%
101 / 118
41.67% covered (danger)
41.67%
5 / 12
30.34
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 execute
90.48% covered (success)
90.48%
38 / 42
0.00% covered (danger)
0.00%
0 / 1
8.06
 userAlreadySentThanks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionFromId
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 getLogEntryFromId
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 getSourceFromParams
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getUserFromRevision
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getUserFromLog
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 sendThanks
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
2.01
 getAllowedParams
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Thanks\Api;
4
5use MediaWiki\Api\ApiBase;
6use MediaWiki\Api\ApiMain;
7use MediaWiki\Extension\Notifications\DiscussionParser;
8use MediaWiki\Extension\Thanks\Hooks;
9use MediaWiki\Extension\Thanks\Storage\Exceptions\InvalidLogType;
10use MediaWiki\Extension\Thanks\Storage\Exceptions\LogDeleted;
11use MediaWiki\Extension\Thanks\Storage\LogStore;
12use MediaWiki\Logging\DatabaseLogEntry;
13use MediaWiki\Logging\LogEntry;
14use MediaWiki\Notification\NotificationService;
15use MediaWiki\Notification\RecipientSet;
16use MediaWiki\Notification\Types\WikiNotification;
17use MediaWiki\Page\PageIdentity;
18use MediaWiki\Permissions\PermissionManager;
19use MediaWiki\Revision\RevisionRecord;
20use MediaWiki\Revision\RevisionStore;
21use MediaWiki\User\TempUser\TempUserDetailsLookup;
22use MediaWiki\User\User;
23use MediaWiki\User\UserFactory;
24use MediaWiki\User\UserIdentity;
25use Wikimedia\ParamValidator\ParamValidator;
26use Wikimedia\ParamValidator\TypeDef\IntegerDef;
27
28/**
29 * API module to send thanks notifications for revisions and log entries.
30 *
31 * @ingroup API
32 * @ingroup Extensions
33 */
34class ApiCoreThank extends ApiThank {
35
36    public function __construct(
37        ApiMain $main,
38        string $action,
39        PermissionManager $permissionManager,
40        LogStore $storage,
41        private readonly NotificationService $notifications,
42        protected readonly RevisionStore $revisionStore,
43        protected readonly UserFactory $userFactory,
44        TempUserDetailsLookup $tempUserDetailsLookup,
45    ) {
46        parent::__construct(
47            $main,
48            $action,
49            $permissionManager,
50            $storage,
51            $tempUserDetailsLookup
52        );
53    }
54
55    /**
56     * Perform the API request.
57     * @suppress PhanPossiblyUndeclaredVariable Phan get's confused by the badly arranged code
58     */
59    public function execute() {
60        // Initial setup.
61        $user = $this->getUser();
62        $this->dieOnBadUser( $user );
63        $this->dieOnUserBlockedFromThanks( $user );
64        $params = $this->extractRequestParams();
65        $revcreation = false;
66
67        $this->requireOnlyOneParameter( $params, 'rev', 'log' );
68
69        // Extract type and ID from the parameters.
70        if ( $params['rev'] !== null ) {
71            $type = 'rev';
72            $id = $params['rev'];
73        } elseif ( $params['log'] !== null ) {
74            $type = 'log';
75            $id = $params['log'];
76        } else {
77            ApiBase::dieDebug( __METHOD__, 'Unhandled parameter' );
78        }
79
80        // Determine thanks parameters.
81        if ( $type === 'log' ) {
82            $logEntry = $this->getLogEntryFromId( $id );
83            // If there's an associated revision, thank for that instead.
84            if ( $logEntry->getAssociatedRevId() ) {
85                $type = 'rev';
86                $id = $logEntry->getAssociatedRevId();
87            } else {
88                // If there's no associated revision, die if the user is sitewide blocked
89                $excerpt = '';
90                $page = $logEntry->getTarget();
91                $recipient = $this->getUserFromLog( $logEntry );
92            }
93        }
94        // Type may change from log to rev when associated revision exists
95        if ( $type === 'rev' ) {
96            $revision = $this->getRevisionFromId( $id );
97            $excerpt = DiscussionParser::getEditExcerpt( $revision, $this->getLanguage() );
98            $page = $revision->getPage();
99            $this->dieOnUserBlockedFromPage( $user, $page );
100
101            $recipient = $this->getUserFromRevision( $revision );
102
103            // If there is no parent revid of this revision, it's a page creation.
104            if ( !$this->revisionStore->getPreviousRevision( $revision ) ) {
105                $revcreation = true;
106            }
107        }
108
109        // Send thanks.
110        if ( $this->userAlreadySentThanks( $user, $type, $id ) ) {
111            $this->markResultSuccess( $recipient->getName() );
112        } else {
113            $this->dieOnBadRecipient( $user, $recipient );
114            $this->sendThanks(
115                $user,
116                $type,
117                $id,
118                $excerpt,
119                $recipient,
120                $this->getSourceFromParams( $params ),
121                $page,
122                $revcreation
123            );
124        }
125    }
126
127    /**
128     * Check the session data for an indication of whether this user has already sent this thanks.
129     * @param User $user The user being thanked.
130     * @param string $type Either 'rev' or 'log'.
131     * @param int $id The revision or log ID.
132     * @return bool
133     */
134    protected function userAlreadySentThanks( User $user, $type, $id ) {
135        return (bool)$user->getRequest()->getSessionData( Hooks::getSessionKey( $type, $id ) );
136    }
137
138    private function getRevisionFromId( int $revId ): RevisionRecord {
139        $revision = $this->revisionStore->getRevisionById( $revId );
140        // Revision ID 1 means an invalid argument was passed in.
141        // FIXME Get rid of this limitation! T344475
142        if ( !$revision || $revision->getId() === 1 ) {
143            $this->dieWithError( 'thanks-error-invalidrevision', 'invalidrevision' );
144        } elseif ( $revision->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
145            $this->dieWithError( 'thanks-error-revdeleted', 'revdeleted' );
146        }
147        return $revision;
148    }
149
150    /**
151     * Get the log entry from the ID.
152     * @param int $logId The log entry ID.
153     * @return DatabaseLogEntry
154     */
155    protected function getLogEntryFromId( $logId ): DatabaseLogEntry {
156        $logEntry = null;
157        try {
158            $logEntry = $this->storage->getLogEntryFromId( $logId );
159        } catch ( InvalidLogType $e ) {
160            $err = $this->msg( 'thanks-error-invalid-log-type', $e->getLogType() );
161            $this->dieWithError( $err, 'thanks-error-invalid-log-type' );
162        } catch ( LogDeleted ) {
163            $this->dieWithError( 'thanks-error-log-deleted', 'thanks-error-log-deleted' );
164        }
165
166        if ( !$logEntry ) {
167            $this->dieWithError( 'thanks-error-invalid-log-id', 'thanks-error-invalid-log-id' );
168        }
169        return $logEntry;
170    }
171
172    /**
173     * Set the source of the thanks, e.g. 'diff' or 'history'
174     * @param string[] $params Incoming API parameters, with a 'source' key.
175     * @return string The source, or 'undefined' if not provided.
176     */
177    private function getSourceFromParams( $params ) {
178        if ( $params['source'] ) {
179            return trim( $params['source'] );
180        } else {
181            return 'undefined';
182        }
183    }
184
185    /**
186     * @param RevisionRecord $revision
187     * @return User
188     */
189    private function getUserFromRevision( RevisionRecord $revision ) {
190        $recipient = $revision->getUser();
191        if ( !$recipient ) {
192            $this->dieWithError( 'thanks-error-invalidrecipient', 'invalidrecipient' );
193        }
194        return $this->userFactory->newFromUserIdentity( $recipient );
195    }
196
197    /**
198     * @param LogEntry $logEntry
199     * @return UserIdentity
200     */
201    private function getUserFromLog( LogEntry $logEntry ) {
202        $recipient = $logEntry->getPerformerIdentity();
203        return $this->userFactory->newFromUserIdentity( $recipient );
204    }
205
206    /**
207     * Create the thanks notification event, and log the thanks.
208     * @param User $user The thanks-sending user.
209     * @param string $type The thanks type ('rev' or 'log').
210     * @param int $id The log or revision ID.
211     * @param string $excerpt The excerpt to display as the thanks notification. This will only
212     * be used if it is not possible to retrieve the relevant excerpt at the time the
213     * notification is displayed (in order to account for changing visibility in the meantime).
214     * @param User $recipient The recipient of the thanks.
215     * @param string $source Where the thanks was given.
216     * @param PageIdentity $page The page for which thanks is given.
217     * @param bool $revcreation True if the linked revision is a page creation.
218     */
219    protected function sendThanks(
220        User $user, $type, $id, $excerpt, User $recipient, $source, PageIdentity $page, $revcreation
221    ) {
222        $uniqueId = $type . '-' . $id;
223        // Do one last check to make sure we haven't sent Thanks before
224        if ( $this->haveAlreadyThanked( $user, $uniqueId ) ) {
225            // Pretend the thanks were sent
226            $this->markResultSuccess( $recipient->getName() );
227            return;
228        }
229
230        // Create the notification
231        $this->notifications->notify(
232            new WikiNotification( 'edit-thank', $page, $user, [
233                $type . 'id' => $id,
234                'source' => $source,
235                'excerpt' => $excerpt,
236                'revcreation' => $revcreation,
237            ] ),
238            new RecipientSet( $recipient )
239        );
240
241        // And mark the thank in session for a cheaper check to prevent duplicates (T48690).
242        $user->getRequest()->setSessionData( Hooks::getSessionKey( $type, $id ), true );
243        // Set success message
244        $this->markResultSuccess( $recipient->getName() );
245        $this->logThanks( $user, $recipient, $uniqueId );
246    }
247
248    /** @inheritDoc */
249    public function getAllowedParams() {
250        return [
251            'rev' => [
252                ParamValidator::PARAM_TYPE => 'integer',
253                IntegerDef::PARAM_MIN => 1,
254                ParamValidator::PARAM_REQUIRED => false,
255            ],
256            'log' => [
257                ParamValidator::PARAM_TYPE => 'integer',
258                IntegerDef::PARAM_MIN => 1,
259                ParamValidator::PARAM_REQUIRED => false,
260            ],
261            'token' => [
262                ParamValidator::PARAM_TYPE => 'string',
263                ParamValidator::PARAM_REQUIRED => true,
264            ],
265            'source' => [
266                ParamValidator::PARAM_TYPE => 'string',
267                ParamValidator::PARAM_REQUIRED => false,
268            ],
269        ];
270    }
271
272    /** @inheritDoc */
273    public function getHelpUrls() {
274        return [
275            'https://www.mediawiki.org/wiki/Extension:Thanks#API_Documentation',
276        ];
277    }
278
279    /**
280     * @see ApiBase::getExamplesMessages()
281     * @return array
282     */
283    protected function getExamplesMessages() {
284        return [
285            'action=thank&revid=456&source=diff&token=123ABC'
286                => 'apihelp-thank-example-1',
287        ];
288    }
289}