Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.00% covered (warning)
68.00%
102 / 150
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiPageTriageAction
68.00% covered (warning)
68.00%
102 / 150
50.00% covered (danger)
50.00%
5 / 10
78.47
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 execute
67.74% covered (warning)
67.74%
21 / 31
0.00% covered (danger)
0.00%
0 / 1
8.64
 canPerformReviewAction
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
9.16
 markAsReviewed
67.65% covered (warning)
67.65%
23 / 34
0.00% covered (danger)
0.00%
0 / 1
8.66
 enqueue
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
 logAction
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
2.02
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
1
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\PageTriage\Api;
4
5use ApiBase;
6use ApiMain;
7use Article;
8use ChangeTags;
9use Language;
10use ManualLogEntry;
11use MediaWiki\Deferred\DeferredUpdates;
12use MediaWiki\Extension\PageTriage\ArticleCompile\ArticleCompileProcessor;
13use MediaWiki\Extension\PageTriage\ArticleMetadata;
14use MediaWiki\Extension\PageTriage\PageTriage;
15use MediaWiki\Extension\PageTriage\PageTriageUtil;
16use MediaWiki\Extension\PageTriage\QueueRecord;
17use MediaWiki\Revision\RevisionRecord;
18use MediaWiki\Revision\RevisionStore;
19use Wikimedia\ParamValidator\ParamValidator;
20
21class ApiPageTriageAction extends ApiBase {
22
23    private RevisionStore $revStore;
24    private Language $contLang;
25
26    /**
27     * @param ApiMain $queryModule
28     * @param string $moduleName
29     * @param RevisionStore $revStore
30     * @param Language $contLang
31     */
32    public function __construct(
33        ApiMain $queryModule,
34        $moduleName,
35        RevisionStore $revStore,
36        Language $contLang
37    ) {
38        parent::__construct( $queryModule, $moduleName );
39        $this->revStore = $revStore;
40        $this->contLang = $contLang;
41    }
42
43    public function execute() {
44        $params = $this->extractRequestParams();
45        $this->requireOnlyOneParameter( $params, 'reviewed', 'enqueue' );
46
47        $article = Article::newFromID( $params['pageid'] );
48        if ( !$article ) {
49            $this->dieWithError( 'apierror-missingtitle', 'bad-page' );
50        }
51
52        if ( $this->getUser()->pingLimiter( 'pagetriage-mark-action' ) ) {
53            $this->dieWithError( 'apierror-ratelimited' );
54        }
55
56        $logEntryTags = [];
57        if ( $params['tags'] ) {
58            $tagStatus = ChangeTags::canAddTagsAccompanyingChange(
59                $params['tags'],
60                $this->getUser()
61            );
62            if ( !$tagStatus->isOK() ) {
63                $this->dieStatus( $tagStatus );
64            }
65            $logEntryTags = $params['tags'];
66        }
67        $logEntryTags[] = 'pagetriage';
68
69        $note = $params['note'];
70
71        if ( isset( $params['reviewed'] ) ) {
72            if ( !$this->canPerformReviewAction( (int)$params['reviewed'], $article ) ) {
73                $this->dieWithError( [ 'apierror-permissiondenied', $this->msg( 'action-patrol' ) ] );
74            }
75
76            $result = $this->markAsReviewed(
77                $article,
78                $params['reviewed'],
79                $note,
80                $params['skipnotif'],
81                $logEntryTags
82            );
83        } else {
84            $this->checkTitleUserPermissions( $article->getTitle(), 'patrol' );
85
86            $result = $this->enqueue( $article, $note, $logEntryTags );
87        }
88
89        $this->getResult()->addValue( null, $this->getModuleName(), $result );
90    }
91
92    /**
93     * Check if the user is allowed to perform the action they are supposed to
94     * perform.
95     * @param int $attemptedReviewAction This will be 0 when attempting to unreview
96     * and 1 when attempting to review corresponding to the QueueRecord::... values
97     * that will be set in the database.
98     * @param Article $article Article on which this action is to be performed
99     * @return bool
100     */
101    private function canPerformReviewAction( int $attemptedReviewAction, Article $article ): bool {
102        $isPatroller = $this->getAuthority()->isAllowed( 'patrol' );
103        $isAutopatrolled = $this->getAuthority()->isAllowed( 'autopatrol' );
104
105        if ( $isPatroller && $isAutopatrolled ) {
106            return true;
107        }
108
109        $pageCreator = $this->revStore->getFirstRevision(
110            $article->getPage() )->getUser( RevisionRecord::RAW );
111        $isPageCreator = $this->getUser()->equals( $pageCreator );
112
113        $attemptingToReview = $attemptedReviewAction === QueueRecord::REVIEW_STATUS_REVIEWED;
114
115        if ( $attemptingToReview ) {
116
117            // T314245 - do not allow someone to mark their own articles as reviewed
118            // when not being autopatrolled
119            if ( !$isPageCreator && $isPatroller ) {
120                return true;
121            }
122        } else {
123            // attempting to unreview a page
124            if ( $isPatroller ) {
125                return true;
126            }
127
128            // T351954 - Allow autopatrolled users to unreview their own
129            // articles
130            if ( $isPageCreator && $isAutopatrolled ) {
131                return true;
132            }
133        }
134
135        return false;
136    }
137
138    /**
139     * @param Article $article
140     * @param string $reviewedStatus
141     * @param string $note
142     * @param bool $skipNotif
143     * @param array $tags
144     * @return array Result for API
145     */
146    private function markAsReviewed( Article $article, $reviewedStatus, $note, $skipNotif, $tags ) {
147        if (
148            !ArticleMetadata::validatePageIds(
149                [ $article->getPage()->getId() ],
150                DB_REPLICA
151            )
152        ) {
153            $this->dieWithError( 'apierror-bad-pagetriage-page' );
154        }
155
156        $pageTriage = new PageTriage( $article->getPage()->getId() );
157        $statusChanged = $pageTriage->setTriageStatus( (int)$reviewedStatus, $this->getUser() );
158
159        // no notification or log entry if page status didn't change
160        if ( $statusChanged ) {
161            // notification on mark as reviewed
162            if ( !$skipNotif && $reviewedStatus ) {
163                PageTriageUtil::createNotificationEvent(
164                    $article->getTitle(),
165                    $this->getUser(),
166                    'pagetriage-mark-as-reviewed',
167                    [
168                        'note' => $note,
169                    ]
170                );
171            }
172
173            $reviewLogEntryType = 'reviewed';
174
175            if ( !$reviewedStatus ) {
176                $reviewLogEntryType = 'unreviewed';
177            }
178
179            if ( $article->getTitle()->isRedirect() ) {
180                $reviewLogEntryType .= '-redirect';
181            } else {
182                $reviewLogEntryType .= '-article';
183            }
184
185            // The following messages will be used by this log entry
186            // * logentry-pagetriage-curation-reviewed-redirect
187            // * logentry-pagetriage-curation-reviewed-article
188            // * logentry-pagetriage-curation-unreviewed-redirect
189            // * logentry-pagetriage-curation-unreviewed-article
190            $this->logAction(
191                $article,
192                $reviewLogEntryType,
193                $note,
194                $tags
195            );
196            return [ 'result' => 'success' ];
197        } else {
198            return [
199                'result' => 'done',
200                'pagetriage_unchanged_status' => $article->getPage()->getId(),
201            ];
202        }
203    }
204
205    /**
206     * @param Article $article
207     * @param string $note
208     * @param array $tags
209     * @return array Result for API
210     */
211    private function enqueue( Article $article, $note, $tags ) {
212        $title = $article->getTitle();
213        if ( $title->isMainPage() ) {
214            $this->dieWithError( 'apierror-bad-pagetriage-enqueue-mainpage' );
215        }
216        if ( !in_array( $title->getNamespace(), PageTriageUtil::getNamespaces() ) ) {
217            $this->dieWithError( 'apierror-bad-pagetriage-enqueue-invalidnamespace' );
218        }
219
220        $articleId = $article->getPage()->getId();
221        if ( ArticleMetadata::validatePageIds( [ $articleId ], DB_REPLICA ) ) {
222            $this->dieWithError( 'apierror-bad-pagetriage-enqueue-alreadyqueued' );
223        }
224
225        $pt = new PageTriage( $articleId );
226        $pt->addToPageTriageQueue();
227
228        DeferredUpdates::addCallableUpdate( static function () use ( $articleId ) {
229            // Validate the page ID from DB_PRIMARY, compile metadata from DB_PRIMARY and return.
230            $acp = ArticleCompileProcessor::newFromPageId(
231                [ $articleId ],
232                false,
233                DB_PRIMARY
234            );
235            if ( $acp ) {
236                $acp->compileMetadata();
237            }
238        } );
239
240        // The following messages will be used by this log entry
241        // * logentry-pagetriage-curation-reviewed-redirect
242        // * logentry-pagetriage-curation-reviewed-article
243        $reviewLogEntryType = 'unreviewed-' . ( $title->isRedirect() ? 'redirect' : 'article' );
244
245        $this->logAction( $article, 'enqueue', $note, $tags );
246        $this->logAction( $article, $reviewLogEntryType, $note, $tags );
247
248        return [ 'result' => 'success' ];
249    }
250
251    /**
252     * Logs triage action
253     *
254     * @param Article $article
255     * @param string $subtype
256     * @param string $note
257     * @param array $tags
258     */
259    private function logAction( Article $article, $subtype, $note, $tags ) {
260        $logEntry = new ManualLogEntry(
261            'pagetriage-curation',
262            $subtype
263        );
264        $logEntry->setPerformer( $this->getUser() );
265        $logEntry->setTarget( $article->getTitle() );
266        if ( $note ) {
267            $note = $this->contLang->truncateForDatabase( $note, 150 );
268            $logEntry->setComment( $note );
269        }
270        $logEntry->addTags( $tags );
271        $logEntry->publish( $logEntry->insert() );
272    }
273
274    public function needsToken() {
275        return 'csrf';
276    }
277
278    /**
279     * @inheritDoc
280     */
281    public function getAllowedParams() {
282        return [
283            'pageid' => [
284                ParamValidator::PARAM_REQUIRED => true,
285                ParamValidator::PARAM_TYPE => 'integer'
286            ],
287            'reviewed' => [
288                ParamValidator::PARAM_REQUIRED => false,
289                ParamValidator::PARAM_TYPE => [
290                    // reviewed
291                    '1',
292                    // unreviewed
293                    '0',
294                ],
295            ],
296            'enqueue' => [
297                ParamValidator::PARAM_REQUIRED => false,
298                ParamValidator::PARAM_TYPE => 'boolean',
299            ],
300            'token' => [
301                ParamValidator::PARAM_REQUIRED => true,
302            ],
303            'note' => null,
304            'skipnotif' => [
305                ParamValidator::PARAM_REQUIRED => false,
306                ParamValidator::PARAM_TYPE => 'boolean'
307            ],
308            'tags' => [
309                ParamValidator::PARAM_TYPE => 'tags',
310                ParamValidator::PARAM_ISMULTI => true,
311            ],
312        ];
313    }
314
315    /** @inheritDoc */
316    public function mustBePosted() {
317        return true;
318    }
319
320    /** @inheritDoc */
321    public function isWriteMode() {
322        return true;
323    }
324}