Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
52.00% |
130 / 250 |
|
33.33% |
10 / 30 |
CRAP | |
0.00% |
0 / 1 |
RevisionReviewForm | |
52.00% |
130 / 250 |
|
33.33% |
10 / 30 |
1276.27 | |
0.00% |
0 / 1 |
initialize | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
2.50 | |||
getTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setAction | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLastChangeTime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getNewLastChangeTime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRefId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setRefId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOldId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setOldId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setTemplateParams | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setValidatedParams | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getComment | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setComment | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setDim | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setTag | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getTags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
setSessionKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
bypassValidationKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doCheckTargetGiven | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
doBuildOnReady | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doCheckTarget | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
doCheckParameters | |
60.71% |
17 / 28 |
|
0.00% |
0 / 1 |
28.64 | |||
isAllowed | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getAction | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doSubmit | |
83.78% |
93 / 111 |
|
0.00% |
0 / 1 |
40.22 | |||
approveRevision | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
56 | |||
unapproveRevision | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
validationKey | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
updateRecentChanges | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
110 |
1 | <?php |
2 | |
3 | use MediaWiki\Extension\Notifications\Model\Event; |
4 | use MediaWiki\MediaWikiServices; |
5 | use MediaWiki\Registration\ExtensionRegistry; |
6 | use MediaWiki\Revision\RevisionRecord; |
7 | use MediaWiki\Revision\SlotRecord; |
8 | use MediaWiki\Title\Title; |
9 | use Wikimedia\Rdbms\IDBAccessObject; |
10 | |
11 | /** |
12 | * Class containing revision review form business logic |
13 | */ |
14 | class RevisionReviewForm extends FRGenericSubmitForm { |
15 | |
16 | public const ACTION_APPROVE = 'approve'; |
17 | public const ACTION_UNAPPROVE = 'unapprove'; |
18 | public const ACTION_REJECT = 'reject'; |
19 | |
20 | /** @var Title|null Target title object */ |
21 | private $title = null; |
22 | /** @var FlaggableWikiPage|null Target page object */ |
23 | private $page = null; |
24 | /** @var string|null One of the self::ACTION_… constants */ |
25 | private $action = null; |
26 | /** @var int ID being reviewed (last "bad" ID for rejection) */ |
27 | private $oldid = 0; |
28 | /** @var int Old, "last good", ID (used for rejection) */ |
29 | private $refid = 0; |
30 | /** @var string Included template versions (flat string) */ |
31 | private $templateParams = ''; |
32 | /** @var string Parameter key */ |
33 | private $validatedParams = ''; |
34 | /** @var string Review comments */ |
35 | private $comment = ''; |
36 | /** Review tag (for approval) */ |
37 | private ?int $tag = null; |
38 | /** @var string|null Conflict handling */ |
39 | private $lastChangeTime = null; |
40 | /** @var string|null Conflict handling */ |
41 | private $newLastChangeTime = null; |
42 | |
43 | /** @var FlaggedRevision|null Prior FlaggedRevision for Rev with ID $oldid */ |
44 | private $oldFrev = null; |
45 | |
46 | /** @var string User session key */ |
47 | private $sessionKey = ''; |
48 | /** @var bool Skip validatedParams check */ |
49 | private $skipValidationKey = false; |
50 | |
51 | protected function initialize() { |
52 | if ( FlaggedRevs::useOnlyIfProtected() ) { |
53 | $this->tag = 0; // default to "inadequate" |
54 | } |
55 | } |
56 | |
57 | /** |
58 | * @return Title|null |
59 | */ |
60 | public function getTitle() { |
61 | return $this->title; |
62 | } |
63 | |
64 | /** |
65 | * @param Title $value |
66 | */ |
67 | public function setTitle( Title $value ) { |
68 | $this->trySet( $this->title, $value ); |
69 | } |
70 | |
71 | /** |
72 | * @param string $action One of the self::ACTION… constants |
73 | */ |
74 | public function setAction( string $action ) { |
75 | $this->trySet( $this->action, $action ); |
76 | } |
77 | |
78 | /** |
79 | * @param string|null $value |
80 | */ |
81 | public function setLastChangeTime( $value ) { |
82 | $this->trySet( $this->lastChangeTime, $value ); |
83 | } |
84 | |
85 | /** |
86 | * @return string|null |
87 | */ |
88 | public function getNewLastChangeTime() { |
89 | return $this->newLastChangeTime; |
90 | } |
91 | |
92 | /** |
93 | * @return int |
94 | */ |
95 | public function getRefId() { |
96 | return $this->refid; |
97 | } |
98 | |
99 | /** |
100 | * @param int $value |
101 | */ |
102 | public function setRefId( $value ) { |
103 | $this->trySet( $this->refid, (int)$value ); |
104 | } |
105 | |
106 | /** |
107 | * @return int |
108 | */ |
109 | public function getOldId() { |
110 | return $this->oldid; |
111 | } |
112 | |
113 | /** |
114 | * @param int $value |
115 | */ |
116 | public function setOldId( $value ) { |
117 | $this->trySet( $this->oldid, (int)$value ); |
118 | } |
119 | |
120 | /** |
121 | * @param string $value |
122 | */ |
123 | public function setTemplateParams( $value ) { |
124 | $this->trySet( $this->templateParams, $value ); |
125 | } |
126 | |
127 | /** |
128 | * @param string $value |
129 | */ |
130 | public function setValidatedParams( $value ) { |
131 | $this->trySet( $this->validatedParams, $value ); |
132 | } |
133 | |
134 | /** |
135 | * @return string |
136 | */ |
137 | public function getComment() { |
138 | return $this->comment; |
139 | } |
140 | |
141 | /** |
142 | * @param string $value |
143 | */ |
144 | public function setComment( $value ) { |
145 | $this->trySet( $this->comment, $value ); |
146 | } |
147 | |
148 | /** |
149 | * @param int $value |
150 | * @deprecated Use setTag() instead. |
151 | */ |
152 | public function setDim( $value ) { |
153 | $this->setTag( (int)$value ); |
154 | } |
155 | |
156 | public function setTag( ?int $value ): void { |
157 | if ( !FlaggedRevs::useOnlyIfProtected() ) { |
158 | $this->trySet( $this->tag, $value ); |
159 | } |
160 | } |
161 | |
162 | /** |
163 | * Get tags array, for usage with code that expects an array of tags |
164 | * rather than a single tag. |
165 | * @return array<string,int> |
166 | */ |
167 | private function getTags(): array { |
168 | return $this->tag !== null ? [ FlaggedRevs::getTagName() => $this->tag ] : []; |
169 | } |
170 | |
171 | /** |
172 | * @param string $sessionId |
173 | */ |
174 | public function setSessionKey( $sessionId ) { |
175 | $this->sessionKey = $sessionId; |
176 | } |
177 | |
178 | public function bypassValidationKey() { |
179 | $this->skipValidationKey = true; |
180 | } |
181 | |
182 | /** |
183 | * Check that a target is given (e.g. from GET/POST request) |
184 | * @return true|string true on success, error string on failure |
185 | */ |
186 | protected function doCheckTargetGiven() { |
187 | if ( $this->title === null ) { |
188 | return 'review_page_invalid'; |
189 | } |
190 | return true; |
191 | } |
192 | |
193 | /** |
194 | * Load any objects after ready() called |
195 | */ |
196 | protected function doBuildOnReady() { |
197 | $this->page = FlaggableWikiPage::getTitleInstance( $this->title ); |
198 | } |
199 | |
200 | /** |
201 | * Check that the target is valid (e.g. from GET/POST request) |
202 | * @param int $flags FOR_SUBMISSION (set on submit) |
203 | * @return true|string true on success, error string on failure |
204 | */ |
205 | protected function doCheckTarget( $flags = 0 ) { |
206 | $flgs = ( $flags & self::FOR_SUBMISSION ) ? IDBAccessObject::READ_LATEST : 0; |
207 | if ( !$this->title->getArticleID( $flgs ) ) { |
208 | return 'review_page_notexists'; |
209 | } |
210 | if ( !$this->page->isReviewable() ) { |
211 | return 'review_page_unreviewable'; |
212 | } |
213 | return true; |
214 | } |
215 | |
216 | /** |
217 | * Validate and clean up parameters (e.g. from POST request). |
218 | * @return true|string true on success, error string on failure |
219 | */ |
220 | protected function doCheckParameters() { |
221 | $action = $this->getAction(); |
222 | if ( $action === null ) { |
223 | return 'review_param_missing'; // no action specified (approve, reject, de-approve) |
224 | } elseif ( !$this->oldid ) { |
225 | return 'review_no_oldid'; // no revision target |
226 | } |
227 | # Get the revision's current flags (if any) |
228 | $this->oldFrev = FlaggedRevision::newFromTitle( $this->title, $this->oldid, IDBAccessObject::READ_LATEST ); |
229 | $oldTag = $this->oldFrev ? $this->oldFrev->getTag() : FlaggedRevision::getDefaultTag(); |
230 | # Set initial value for newLastChangeTime (if unchanged on submit) |
231 | $this->newLastChangeTime = $this->lastChangeTime; |
232 | # Fill in implicit tag for binary flag case |
233 | if ( FlaggedRevs::binaryFlagging() ) { |
234 | if ( $this->action === self::ACTION_APPROVE ) { |
235 | $this->tag = 1; |
236 | } elseif ( $this->action === self::ACTION_UNAPPROVE ) { |
237 | $this->tag = 0; |
238 | } |
239 | } |
240 | if ( $action === self::ACTION_APPROVE ) { |
241 | # The tag should not be zero |
242 | if ( $this->tag === 0 ) { |
243 | return 'review_too_low'; |
244 | } |
245 | # Special token to discourage fiddling with templates... |
246 | if ( !$this->skipValidationKey ) { |
247 | $k = self::validationKey( $this->oldid, $this->sessionKey ); |
248 | if ( $this->validatedParams !== $k ) { |
249 | return 'review_bad_key'; |
250 | } |
251 | } |
252 | # Sanity check tag |
253 | if ( !FlaggedRevs::tagIsValid( $this->tag ) ) { |
254 | return 'review_bad_tags'; |
255 | } |
256 | # Check permissions with tag |
257 | if ( !FlaggedRevs::userCanSetTag( $this->user, $this->tag, $oldTag ) ) { |
258 | return 'review_denied'; |
259 | } |
260 | } elseif ( $action === self::ACTION_UNAPPROVE ) { |
261 | # Check permissions with old tag |
262 | if ( !FlaggedRevs::userCanSetTag( $this->user, $oldTag ) ) { |
263 | return 'review_denied'; |
264 | } |
265 | } |
266 | return true; |
267 | } |
268 | |
269 | /** |
270 | * @return bool |
271 | */ |
272 | private function isAllowed() { |
273 | // Basic permission check |
274 | return ( $this->title && MediaWikiServices::getInstance()->getPermissionManager() |
275 | ->userCan( 'review', $this->user, $this->title ) ); |
276 | } |
277 | |
278 | /** |
279 | * Get the action this submission is requesting |
280 | * @return string|null (approve,unapprove,reject) |
281 | */ |
282 | public function getAction(): ?string { |
283 | return $this->action; |
284 | } |
285 | |
286 | /** |
287 | * Submit the form parameters for the page config to the DB. |
288 | * |
289 | * @return true|string true on success, error string on failure |
290 | */ |
291 | protected function doSubmit() { |
292 | # Double-check permissions |
293 | if ( !$this->isAllowed() ) { |
294 | return 'review_denied'; |
295 | } |
296 | $user = $this->user; |
297 | # We can only approve actual revisions... |
298 | $services = MediaWikiServices::getInstance(); |
299 | $revStore = $services->getRevisionStore(); |
300 | if ( $this->getAction() === self::ACTION_APPROVE ) { |
301 | $revRecord = $revStore->getRevisionByTitle( $this->title, $this->oldid ); |
302 | # Check for archived/deleted revisions... |
303 | if ( !$revRecord || $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) { |
304 | return 'review_bad_oldid'; |
305 | } |
306 | # Check for review conflicts... |
307 | if ( $this->lastChangeTime !== null ) { // API uses null |
308 | $lastChange = $this->oldFrev ? $this->oldFrev->getTimestamp() : ''; |
309 | if ( $lastChange !== $this->lastChangeTime ) { |
310 | return 'review_conflict_oldid'; |
311 | } |
312 | } |
313 | $this->approveRevision( $revRecord, $this->oldFrev ); |
314 | $status = true; |
315 | # We can only unapprove approved revisions... |
316 | } elseif ( $this->getAction() === self::ACTION_UNAPPROVE ) { |
317 | # Check for review conflicts... |
318 | if ( $this->lastChangeTime !== null ) { // API uses null |
319 | $lastChange = $this->oldFrev ? $this->oldFrev->getTimestamp() : ''; |
320 | if ( $lastChange !== $this->lastChangeTime ) { |
321 | return 'review_conflict_oldid'; |
322 | } |
323 | } |
324 | # Check if we can find this flagged rev... |
325 | if ( !$this->oldFrev ) { |
326 | return 'review_not_flagged'; |
327 | } |
328 | $this->unapproveRevision( $this->oldFrev ); |
329 | $status = true; |
330 | } elseif ( $this->getAction() === self::ACTION_REJECT ) { |
331 | $newRevRecord = $revStore->getRevisionByTitle( $this->title, $this->oldid ); |
332 | $oldRevRecord = $revStore->getRevisionByTitle( $this->title, $this->refid ); |
333 | # Do not mess with archived/deleted revisions |
334 | if ( !$oldRevRecord || |
335 | $oldRevRecord->isDeleted( RevisionRecord::DELETED_TEXT ) || |
336 | !$newRevRecord || |
337 | $newRevRecord->isDeleted( RevisionRecord::DELETED_TEXT ) |
338 | ) { |
339 | return 'review_bad_oldid'; |
340 | } |
341 | # Check that the revs are in order |
342 | if ( $oldRevRecord->getTimestamp() > $newRevRecord->getTimestamp() ) { |
343 | return 'review_cannot_undo'; |
344 | } |
345 | # Make sure we are only rejecting pending changes |
346 | $srev = FlaggedRevision::newFromStable( $this->title, IDBAccessObject::READ_LATEST ); |
347 | if ( $srev && $oldRevRecord->getTimestamp() < $srev->getRevTimestamp() ) { |
348 | return 'review_cannot_reject'; // not really a use case |
349 | } |
350 | $article = $services->getWikiPageFactory()->newFromTitle( $this->title ); |
351 | # Get text with changes after $oldRev up to and including $newRev removed |
352 | if ( WikiPage::hasDifferencesOutsideMainSlot( $newRevRecord, $oldRevRecord ) ) { |
353 | return 'review_cannot_undo'; |
354 | } |
355 | $undoHandler = $services->getContentHandlerFactory() |
356 | ->getContentHandler( |
357 | $newRevRecord->getSlot( SlotRecord::MAIN )->getModel() |
358 | ); |
359 | $currentContent = $article->getRevisionRecord()->getContent( SlotRecord::MAIN ); |
360 | $undoContent = $newRevRecord->getContent( SlotRecord::MAIN ); |
361 | $undoAfterContent = $oldRevRecord->getContent( SlotRecord::MAIN ); |
362 | if ( !$currentContent || !$undoContent || !$undoAfterContent ) { |
363 | return 'review_cannot_undo'; |
364 | } |
365 | $new_content = $undoHandler->getUndoContent( |
366 | $currentContent, |
367 | $undoContent, |
368 | $undoAfterContent, |
369 | $newRevRecord->isCurrent() |
370 | ); |
371 | if ( $new_content === false ) { |
372 | return 'review_cannot_undo'; |
373 | } |
374 | |
375 | $baseRevId = $newRevRecord->isCurrent() ? $oldRevRecord->getId() : 0; |
376 | |
377 | $comment = $this->getComment(); |
378 | |
379 | // Actually make the edit... |
380 | // Note: this should be changed to use the $undidRevId parameter so that the |
381 | // edit is properly marked as an undo. Do this only after T153570 is merged |
382 | // into Echo, otherwise we would get duplicate revert notifications. |
383 | $editStatus = $article->doUserEditContent( |
384 | $new_content, |
385 | $user, |
386 | $comment, |
387 | 0, // flags |
388 | $baseRevId |
389 | ); |
390 | |
391 | $status = $editStatus->isOK() ? true : 'review_cannot_undo'; |
392 | |
393 | // Notify Echo about the revert. |
394 | // This is due to the lack of appropriate EditResult handling in Echo, in the |
395 | // future, when T153570 is merged, this entire code block should be removed. |
396 | if ( $status === true && |
397 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
398 | $editStatus->value['revision-record'] && |
399 | ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) |
400 | ) { |
401 | $affectedRevisions = []; // revid -> userid |
402 | $revQuery = $revStore->getQueryInfo(); |
403 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
404 | |
405 | $revisions = $dbr->newSelectQueryBuilder() |
406 | ->select( [ 'rev_id', 'rev_user' => $revQuery['fields']['rev_user'] ] ) |
407 | ->tables( $revQuery['tables'] ) |
408 | ->where( [ |
409 | $dbr->expr( 'rev_id', '<=', $newRevRecord->getId() ), |
410 | $dbr->expr( 'rev_timestamp', '<=', $dbr->timestamp( $newRevRecord->getTimestamp() ) ), |
411 | $dbr->expr( 'rev_id', '>', $oldRevRecord->getId() ), |
412 | $dbr->expr( 'rev_timestamp', '>', $dbr->timestamp( $oldRevRecord->getTimestamp() ) ), |
413 | 'rev_page' => $article->getId(), |
414 | ] ) |
415 | ->joinConds( $revQuery['joins'] ) |
416 | ->caller( __METHOD__ ) |
417 | ->fetchResultSet(); |
418 | foreach ( $revisions as $row ) { |
419 | $affectedRevisions[$row->rev_id] = $row->rev_user; |
420 | } |
421 | |
422 | Event::create( [ |
423 | 'type' => 'reverted', |
424 | 'title' => $this->title, |
425 | 'extra' => [ |
426 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
427 | 'revid' => $editStatus->value['revision-record']->getId(), |
428 | 'reverted-users-ids' => array_values( $affectedRevisions ), |
429 | 'reverted-revision-ids' => array_keys( $affectedRevisions ), |
430 | 'method' => 'flaggedrevs-reject', |
431 | ], |
432 | 'agent' => $user, |
433 | ] ); |
434 | } |
435 | } else { |
436 | return 'review_param_missing'; |
437 | } |
438 | # Watch page if set to do so |
439 | if ( $status === true ) { |
440 | $userOptionsLookup = $services->getUserOptionsLookup(); |
441 | $watchlistManager = $services->getWatchlistManager(); |
442 | if ( $userOptionsLookup->getOption( $user, 'flaggedrevswatch' ) && |
443 | !$watchlistManager->isWatched( $user, $this->title ) ) { |
444 | $watchlistManager->addWatch( $user, $this->title ); |
445 | } |
446 | } |
447 | |
448 | ( new FlaggedRevsHookRunner( $services->getHookContainer() ) )->onFlaggedRevsRevisionReviewFormAfterDoSubmit( |
449 | $this, |
450 | $status |
451 | ); |
452 | |
453 | return $status; |
454 | } |
455 | |
456 | /** |
457 | * Adds or updates the flagged revision table for this page/id set |
458 | * @param RevisionRecord $revRecord The revision to be accepted |
459 | * @param FlaggedRevision|null $oldFrev Currently accepted version of $rev or null |
460 | */ |
461 | private function approveRevision( |
462 | RevisionRecord $revRecord, |
463 | ?FlaggedRevision $oldFrev = null |
464 | ) { |
465 | # Revision rating flags |
466 | $flags = $this->getTags(); |
467 | # Get current stable version ID (for logging) |
468 | $oldSv = FlaggedRevision::newFromStable( $this->title, IDBAccessObject::READ_LATEST ); |
469 | |
470 | # Is this a duplicate review? |
471 | if ( $oldFrev && $oldFrev->getTags() == $flags ) { |
472 | return; // don't record if the same |
473 | } |
474 | |
475 | # The new review entry... |
476 | $flaggedRevision = new FlaggedRevision( [ |
477 | 'revrecord' => $revRecord, |
478 | 'user_id' => $this->user->getId(), |
479 | 'timestamp' => wfTimestampNow(), |
480 | 'tags' => $flags, |
481 | 'flags' => '' |
482 | ] ); |
483 | # Delete the old review entry if it exists... |
484 | if ( $oldFrev ) { |
485 | $oldFrev->delete(); |
486 | } |
487 | # Insert the new review entry... |
488 | $status = $flaggedRevision->insert(); |
489 | if ( $status !== true ) { |
490 | throw new UnexpectedValueException( |
491 | 'Flagged revision with ID ' . |
492 | (string)$revRecord->getId() . |
493 | ' exists with unexpected fr_page_id, error: ' . $status |
494 | ); |
495 | } |
496 | |
497 | $flaggedRevision->approveRevertedTagUpdate(); |
498 | |
499 | # Update the article review log... |
500 | $oldSvId = $oldSv ? $oldSv->getRevId() : 0; |
501 | FlaggedRevsLog::updateReviewLog( $this->title, $this->getTags(), |
502 | $this->comment, $this->oldid, $oldSvId, true, $this->user ); |
503 | |
504 | # Get the new stable version as of now |
505 | $sv = FlaggedRevision::determineStable( $this->title ); |
506 | # Update recent changes... |
507 | self::updateRecentChanges( $revRecord, 'patrol', $sv ); |
508 | # Update page and tracking tables and clear cache |
509 | $changed = FlaggedRevs::stableVersionUpdates( $this->title, $sv, $oldSv ); |
510 | if ( $changed ) { |
511 | FlaggedRevs::updateHtmlCaches( $this->title ); // purge pages that use this page |
512 | } |
513 | |
514 | # Caller may want to get the change time |
515 | $this->newLastChangeTime = $flaggedRevision->getTimestamp(); |
516 | } |
517 | |
518 | /** |
519 | * @param FlaggedRevision $frev |
520 | * Removes flagged revision data for this page/id set |
521 | */ |
522 | private function unapproveRevision( FlaggedRevision $frev ) { |
523 | # Get current stable version ID (for logging) |
524 | $oldSv = FlaggedRevision::newFromStable( $this->title, IDBAccessObject::READ_LATEST ); |
525 | |
526 | # Delete from flaggedrevs table |
527 | $frev->delete(); |
528 | |
529 | # Get the new stable version as of now |
530 | $sv = FlaggedRevision::determineStable( $this->title ); |
531 | |
532 | # Update the article review log |
533 | $svId = $sv ? $sv->getRevId() : 0; |
534 | FlaggedRevsLog::updateReviewLog( $this->title, $this->getTags(), |
535 | $this->comment, $this->oldid, $svId, false, $this->user ); |
536 | |
537 | # Update recent changes |
538 | self::updateRecentChanges( $frev->getRevisionRecord(), 'unpatrol', $sv ); |
539 | # Update page and tracking tables and clear cache |
540 | $changed = FlaggedRevs::stableVersionUpdates( $this->title, $sv, $oldSv ); |
541 | if ( $changed ) { |
542 | FlaggedRevs::updateHtmlCaches( $this->title ); // purge pages that use this page |
543 | } |
544 | |
545 | # Caller may want to get the change time |
546 | $this->newLastChangeTime = ''; |
547 | } |
548 | |
549 | /** |
550 | * Get a validation key from template versioning metadata |
551 | * @param int $revisionId |
552 | * @param string $sessKey Session key |
553 | * @return string |
554 | */ |
555 | public static function validationKey( $revisionId, $sessKey ) { |
556 | global $wgSecretKey; |
557 | $key = md5( $wgSecretKey ); |
558 | $keyBits = $key[3] . $key[9] . $key[13] . $key[19] . $key[26]; |
559 | return md5( $revisionId . $sessKey . $keyBits ); |
560 | } |
561 | |
562 | /** |
563 | * Update rc_patrolled fields in recent changes after (un)accepting a rev. |
564 | * This maintains the patrolled <=> reviewed relationship for reviewable namespaces. |
565 | * |
566 | * RecentChange should only be passed in when an RC item is saved. |
567 | * |
568 | * @param RevisionRecord|RecentChange $rev |
569 | * @param string $patrol "patrol" or "unpatrol" |
570 | * @param FlaggedRevision|null $srev The new stable version |
571 | * @return void |
572 | */ |
573 | public static function updateRecentChanges( $rev, $patrol, $srev ) { |
574 | if ( $rev instanceof RecentChange ) { |
575 | $pageId = $rev->getAttribute( 'rc_cur_id' ); |
576 | } else { |
577 | $pageId = $rev->getPageId(); |
578 | } |
579 | $sTimestamp = $srev ? $srev->getRevTimestamp() : null; |
580 | |
581 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
582 | |
583 | $limit = 100; // sanity limit to avoid replica lag (most useful when FR is first enabled) |
584 | $conds = [ 'rc_cur_id' => $pageId ]; |
585 | |
586 | $newPatrolState = null; // set rc_patrolled to this value |
587 | # If we accepted this rev, then mark prior revs as patrolled... |
588 | if ( $patrol === 'patrol' ) { |
589 | if ( $sTimestamp ) { // sanity check; should always be set |
590 | $conds[] = $dbw->expr( 'rc_timestamp', '<=', $dbw->timestamp( $sTimestamp ) ); |
591 | $newPatrolState = 1; |
592 | } |
593 | # If we un-accepted this rev, then mark now-pending revs as unpatrolled... |
594 | } elseif ( $patrol === 'unpatrol' ) { |
595 | if ( $sTimestamp ) { |
596 | $conds[] = $dbw->expr( 'rc_timestamp', '>', $dbw->timestamp( $sTimestamp ) ); |
597 | } |
598 | $newPatrolState = 0; |
599 | } |
600 | |
601 | if ( $newPatrolState === null ) { |
602 | return; // leave alone |
603 | } |
604 | |
605 | // Only update rows that need it |
606 | $conds['rc_patrolled'] = $newPatrolState ? 0 : 1; |
607 | // SELECT and update by PK to avoid lag |
608 | $rcIds = $dbw->newSelectQueryBuilder() |
609 | ->select( 'rc_id' ) |
610 | ->from( 'recentchanges' ) |
611 | ->where( $conds ) |
612 | ->limit( $limit ) |
613 | ->caller( __METHOD__ ) |
614 | ->fetchFieldValues(); |
615 | if ( $rcIds ) { |
616 | $dbw->newUpdateQueryBuilder() |
617 | ->update( 'recentchanges' ) |
618 | ->set( [ 'rc_patrolled' => $newPatrolState ] ) |
619 | ->where( [ 'rc_id' => $rcIds ] ) |
620 | ->caller( __METHOD__ ) |
621 | ->execute(); |
622 | } |
623 | } |
624 | } |