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