Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
68.00% |
102 / 150 |
|
50.00% |
5 / 10 |
CRAP | |
0.00% |
0 / 1 |
ApiPageTriageAction | |
68.00% |
102 / 150 |
|
50.00% |
5 / 10 |
78.47 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
67.74% |
21 / 31 |
|
0.00% |
0 / 1 |
8.64 | |||
canPerformReviewAction | |
87.50% |
14 / 16 |
|
0.00% |
0 / 1 |
9.16 | |||
markAsReviewed | |
67.65% |
23 / 34 |
|
0.00% |
0 / 1 |
8.66 | |||
enqueue | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
42 | |||
logAction | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
2.02 | |||
needsToken | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedParams | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
1 | |||
mustBePosted | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isWriteMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\PageTriage\Api; |
4 | |
5 | use ApiBase; |
6 | use ApiMain; |
7 | use Article; |
8 | use ChangeTags; |
9 | use Language; |
10 | use ManualLogEntry; |
11 | use MediaWiki\Deferred\DeferredUpdates; |
12 | use MediaWiki\Extension\PageTriage\ArticleCompile\ArticleCompileProcessor; |
13 | use MediaWiki\Extension\PageTriage\ArticleMetadata; |
14 | use MediaWiki\Extension\PageTriage\PageTriage; |
15 | use MediaWiki\Extension\PageTriage\PageTriageUtil; |
16 | use MediaWiki\Extension\PageTriage\QueueRecord; |
17 | use MediaWiki\Revision\RevisionRecord; |
18 | use MediaWiki\Revision\RevisionStore; |
19 | use Wikimedia\ParamValidator\ParamValidator; |
20 | |
21 | class 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 | } |