Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
20.00% |
51 / 255 |
|
48.57% |
17 / 35 |
CRAP | |
0.00% |
0 / 1 |
FlaggedRevs | |
20.00% |
51 / 255 |
|
48.57% |
17 / 35 |
7005.47 | |
0.00% |
0 / 1 |
binaryFlagging | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getTagName | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
autoReviewEdits | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
autoReviewNewPages | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
autoReviewEnabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
maxAutoReviewLevel | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isStableShownByDefault | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
useOnlyIfProtected | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
inclusionSetting | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
useProtectionLevels | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getRestrictionLevels | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
getMaxLevel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
valueIsValid | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
tagIsValid | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
userCanSetValue | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
72 | |||
userCanSetTag | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
56 | |||
userCanSetAutoreviewLevel | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
72 | |||
getParserCacheInstance | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
parseStableRevisionPooled | |
83.33% |
20 / 24 |
|
0.00% |
0 / 1 |
3.04 | |||
parseStableRevision | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
272 | |||
stableVersionUpdates | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
156 | |||
clearTrackingRows | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
clearStableOnlyDeps | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
purgeMediaWikiHtmlCdn | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
updateHtmlCaches | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
extraHTMLCacheUpdate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
markRevisionPatrolled | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
quickTags | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
quickTag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getAutoReviewTags | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
getReviewNamespaces | |
37.50% |
3 / 8 |
|
0.00% |
0 / 1 |
11.10 | |||
getFirstReviewNamespace | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isReviewNamespace | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
inReviewNamespace | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
autoReviewEdit | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
132 |
1 | <?php |
2 | |
3 | use MediaWiki\Config\ConfigException; |
4 | use MediaWiki\Deferred\DeferredUpdates; |
5 | use MediaWiki\MediaWikiServices; |
6 | use MediaWiki\Page\PageIdentity; |
7 | use MediaWiki\Page\PageReference; |
8 | use MediaWiki\Parser\Parser; |
9 | use MediaWiki\Parser\ParserOptions; |
10 | use MediaWiki\Parser\ParserOutput; |
11 | use MediaWiki\PoolCounter\PoolCounterWorkViaCallback; |
12 | use MediaWiki\Revision\RenderedRevision; |
13 | use MediaWiki\Revision\RevisionRecord; |
14 | use MediaWiki\Revision\SlotRecord; |
15 | use MediaWiki\Status\Status; |
16 | use MediaWiki\Title\Title; |
17 | use MediaWiki\User\User; |
18 | use MediaWiki\User\UserIdentity; |
19 | use Wikimedia\Rdbms\IDBAccessObject; |
20 | |
21 | /** |
22 | * Class containing utility functions for a FlaggedRevs environment |
23 | * |
24 | * Class is lazily-initialized, calling load() as needed |
25 | */ |
26 | class FlaggedRevs { |
27 | /** |
28 | * The name of the ParserCache to use for stable revisions caching. |
29 | * |
30 | * @note This name is used as a part of the ParserCache key, so |
31 | * changing it will invalidate the parser cache for stable revisions. |
32 | * |
33 | * TODO: Extract constant to FlaggedRevsParserCache |
34 | * |
35 | * @deprecated 1.39 |
36 | */ |
37 | public const PARSER_CACHE_NAME = 'stable-pcache'; |
38 | public const PARSOID_PARSER_CACHE_NAME = 'stable-parsoid-pcache'; |
39 | |
40 | # ################ Basic config accessors ################# |
41 | |
42 | /** |
43 | * Is there only one tag and it has only one level? |
44 | * @return bool |
45 | */ |
46 | public static function binaryFlagging() { |
47 | return self::useOnlyIfProtected() || self::getMaxLevel() <= 1; |
48 | } |
49 | |
50 | /** |
51 | * Get the supported dimension name. |
52 | * @return string |
53 | */ |
54 | public static function getTagName(): string { |
55 | global $wgFlaggedRevsTags; |
56 | if ( count( $wgFlaggedRevsTags ) !== 1 ) { |
57 | throw new ConfigException( 'FlaggedRevs given invalid tag name! We only support one dimension now.' ); |
58 | } |
59 | return array_keys( $wgFlaggedRevsTags )[0]; |
60 | } |
61 | |
62 | /** |
63 | * Allow auto-review edits directly to the stable version by reviewers? |
64 | * @return bool |
65 | */ |
66 | public static function autoReviewEdits() { |
67 | global $wgFlaggedRevsAutoReview; |
68 | return (bool)( $wgFlaggedRevsAutoReview & FR_AUTOREVIEW_CHANGES ); |
69 | } |
70 | |
71 | /** |
72 | * Auto-review new pages with the minimal level? |
73 | * @return bool |
74 | */ |
75 | public static function autoReviewNewPages() { |
76 | global $wgFlaggedRevsAutoReview; |
77 | return (bool)( $wgFlaggedRevsAutoReview & FR_AUTOREVIEW_CREATION ); |
78 | } |
79 | |
80 | /** |
81 | * Auto-review of new pages or edits to pages enabled? |
82 | * @return bool |
83 | */ |
84 | public static function autoReviewEnabled() { |
85 | return self::autoReviewEdits() || self::autoReviewNewPages(); |
86 | } |
87 | |
88 | /** |
89 | * Get the maximum level that can be autoreviewed |
90 | * @return int |
91 | */ |
92 | private static function maxAutoReviewLevel() { |
93 | global $wgFlaggedRevsTagsAuto; |
94 | if ( !self::autoReviewEnabled() ) { |
95 | return 0; // shouldn't happen |
96 | } |
97 | // B/C (before $wgFlaggedRevsTagsAuto) |
98 | return (int)( $wgFlaggedRevsTagsAuto[self::getTagName()] ?? 1 ); |
99 | } |
100 | |
101 | /** |
102 | * Is a "stable version" used as the default display |
103 | * version for all pages in reviewable namespaces? |
104 | * @return bool |
105 | */ |
106 | public static function isStableShownByDefault() { |
107 | global $wgFlaggedRevsOverride; |
108 | if ( self::useOnlyIfProtected() ) { |
109 | return false; // must be configured per-page |
110 | } |
111 | return (bool)$wgFlaggedRevsOverride; |
112 | } |
113 | |
114 | /** |
115 | * Are pages reviewable only if they have been manually |
116 | * configured by an admin to use a "stable version" as the default? |
117 | * @return bool |
118 | */ |
119 | public static function useOnlyIfProtected() { |
120 | global $wgFlaggedRevsProtection; |
121 | return (bool)$wgFlaggedRevsProtection; |
122 | } |
123 | |
124 | /** |
125 | * @return int |
126 | */ |
127 | public static function inclusionSetting() { |
128 | global $wgFlaggedRevsHandleIncludes; |
129 | return $wgFlaggedRevsHandleIncludes; |
130 | } |
131 | |
132 | /** |
133 | * Are there site defined protection levels for review |
134 | * @return bool |
135 | */ |
136 | public static function useProtectionLevels(): bool { |
137 | return self::useOnlyIfProtected() && self::getRestrictionLevels(); |
138 | } |
139 | |
140 | /** |
141 | * Get the autoreview restriction levels available |
142 | * @return string[] Value from $wgFlaggedRevsRestrictionLevels |
143 | */ |
144 | public static function getRestrictionLevels(): array { |
145 | global $wgFlaggedRevsRestrictionLevels; |
146 | if ( in_array( '', $wgFlaggedRevsRestrictionLevels ) ) { |
147 | throw new ConfigException( 'Invalid empty value in $wgFlaggedRevsRestrictionLevels' ); |
148 | } |
149 | return $wgFlaggedRevsRestrictionLevels; |
150 | } |
151 | |
152 | /** |
153 | * @return int Number of levels, excluding "0" level |
154 | */ |
155 | public static function getMaxLevel() { |
156 | global $wgFlaggedRevsTags; |
157 | return reset( $wgFlaggedRevsTags )['levels']; |
158 | } |
159 | |
160 | # ################ Permission functions ################# |
161 | |
162 | /** Check if the tag has a valid value */ |
163 | private static function valueIsValid( int $value ): bool { |
164 | return $value >= 0 && $value <= self::getMaxLevel(); |
165 | } |
166 | |
167 | /** |
168 | * Check if we’re in protection mode or the tag has a valid value |
169 | */ |
170 | public static function tagIsValid( ?int $tag ): bool { |
171 | return self::useOnlyIfProtected() || ( $tag !== null && self::valueIsValid( $tag ) ); |
172 | } |
173 | |
174 | /** |
175 | * Returns true if a user can set $value |
176 | */ |
177 | public static function userCanSetValue( UserIdentity $user, int $value ): bool { |
178 | global $wgFlaggedRevsTagsRestrictions; |
179 | |
180 | $pm = MediaWikiServices::getInstance()->getPermissionManager(); |
181 | # Sanity check tag and value |
182 | if ( !self::valueIsValid( $value ) ) { |
183 | return false; // flag range is invalid |
184 | } |
185 | $restrictions = $wgFlaggedRevsTagsRestrictions[self::getTagName()] ?? []; |
186 | # No restrictions -> full access |
187 | # Validators always have full access |
188 | if ( !$restrictions || $pm->userHasRight( $user, 'validate' ) ) { |
189 | return true; |
190 | } |
191 | # Check if this user has any right that lets him/her set |
192 | # up to this particular value |
193 | foreach ( $restrictions as $right => $level ) { |
194 | if ( $value <= $level && $level > 0 && $pm->userHasRight( $user, $right ) ) { |
195 | return true; |
196 | } |
197 | } |
198 | return false; |
199 | } |
200 | |
201 | /** |
202 | * Returns true if a user can set $tag for a revision via review. |
203 | * Requires the same for $oldTag if given. |
204 | * @param UserIdentity $user |
205 | * @param int|null $tag suggested tag |
206 | * @param int|null $oldTag pre-existing tag |
207 | */ |
208 | public static function userCanSetTag( UserIdentity $user, ?int $tag, ?int $oldTag = null ): bool { |
209 | if ( !MediaWikiServices::getInstance()->getPermissionManager() |
210 | ->userHasRight( $user, 'review' ) |
211 | ) { |
212 | return false; // User is not able to review pages |
213 | } |
214 | if ( self::useOnlyIfProtected() ) { |
215 | return true; |
216 | } |
217 | |
218 | if ( $tag === null ) { |
219 | return false; // unspecified |
220 | } elseif ( !self::userCanSetValue( $user, $tag ) ) { |
221 | return false; // user cannot set proposed flag |
222 | } elseif ( $oldTag !== null && !self::userCanSetValue( $user, $oldTag ) ) { |
223 | return false; // user cannot change old flag |
224 | } |
225 | return true; |
226 | } |
227 | |
228 | /** |
229 | * Check if a user can set the autoreview restiction level to $right |
230 | * @param User $user |
231 | * @param string $right the level |
232 | * @return bool |
233 | */ |
234 | public static function userCanSetAutoreviewLevel( $user, $right ) { |
235 | if ( $right == '' ) { |
236 | return true; // no restrictions (none) |
237 | } |
238 | if ( !in_array( $right, self::getRestrictionLevels() ) ) { |
239 | return false; // invalid restriction level |
240 | } |
241 | $pm = MediaWikiServices::getInstance()->getPermissionManager(); |
242 | # Don't let them choose levels above their own rights |
243 | if ( $right == 'sysop' ) { |
244 | // special case, rewrite sysop to editprotected |
245 | if ( !$pm->userHasRight( $user, 'editprotected' ) ) { |
246 | return false; |
247 | } |
248 | } elseif ( $right == 'autoconfirmed' ) { |
249 | // special case, rewrite autoconfirmed to editsemiprotected |
250 | if ( !$pm->userHasRight( $user, 'editsemiprotected' ) ) { |
251 | return false; |
252 | } |
253 | } elseif ( !$pm->userHasRight( $user, $right ) ) { |
254 | return false; |
255 | } |
256 | return true; |
257 | } |
258 | |
259 | # ################ Parsing functions ################# |
260 | |
261 | /** |
262 | * @param ParserOptions $pOpts |
263 | * @return FlaggedRevsParserCache |
264 | */ |
265 | public static function getParserCacheInstance( ParserOptions $pOpts ): FlaggedRevsParserCache { |
266 | $cacheName = $pOpts->getUseParsoid() ? 'FlaggedRevsParsoidParserCache' : 'FlaggedRevsParserCache'; |
267 | /** @var FlaggedRevsParserCache $cache */ |
268 | $cache = MediaWikiServices::getInstance()->getService( $cacheName ); |
269 | return $cache; |
270 | } |
271 | |
272 | /** |
273 | * Get the HTML output of a revision, using PoolCounter in the process |
274 | * |
275 | * @param FlaggedRevision $frev |
276 | * @param ParserOptions $pOpts |
277 | * @return Status Fatal if the pool is full. Otherwise good with an optional ParserOutput, or |
278 | * null if the revision is missing. |
279 | */ |
280 | public static function parseStableRevisionPooled( |
281 | FlaggedRevision $frev, ParserOptions $pOpts |
282 | ) { |
283 | $services = MediaWikiServices::getInstance(); |
284 | $page = $services->getWikiPageFactory()->newFromTitle( $frev->getTitle() ); |
285 | $stableParserCache = self::getParserCacheInstance( $pOpts ); |
286 | $keyPrefix = $stableParserCache->makeKey( $page, $pOpts ); |
287 | |
288 | $work = new PoolCounterWorkViaCallback( |
289 | 'ArticleView', // use standard parse PoolCounter config |
290 | $keyPrefix . ':revid:' . $frev->getRevId(), |
291 | [ |
292 | 'doWork' => function () use ( $frev, $pOpts ) { |
293 | return Status::newGood( self::parseStableRevision( $frev, $pOpts ) ); |
294 | }, |
295 | 'doCachedWork' => static function () use ( $page, $pOpts, $stableParserCache ) { |
296 | // Use new cache value from other thread |
297 | return Status::newGood( $stableParserCache->get( $page, $pOpts ) ?: null ); |
298 | }, |
299 | 'fallback' => static function () use ( $page, $pOpts, $stableParserCache ) { |
300 | // Use stale cache if possible |
301 | $parserOutput = $stableParserCache->getDirty( $page, $pOpts ); |
302 | // The fallback wasn't able to prevent the error situation, return false to |
303 | // continue the original error handling |
304 | return $parserOutput ? Status::newGood( $parserOutput ) : false; |
305 | }, |
306 | 'error' => static function ( Status $status ) { |
307 | return $status; |
308 | }, |
309 | ] |
310 | ); |
311 | |
312 | return $work->execute(); |
313 | } |
314 | |
315 | /** |
316 | * Get the HTML output of a revision. |
317 | * @param FlaggedRevision $frev |
318 | * @param ParserOptions $pOpts |
319 | * @return ParserOutput|null |
320 | */ |
321 | public static function parseStableRevision( FlaggedRevision $frev, ParserOptions $pOpts ) { |
322 | # Notify Parser if includes should be stabilized |
323 | $resetManager = false; |
324 | $incManager = FRInclusionManager::singleton(); |
325 | if ( $frev->getRevId() && self::inclusionSetting() != FR_INCLUDES_CURRENT ) { |
326 | # Use FRInclusionManager to do the template version query |
327 | # up front unless the versions are already specified there... |
328 | if ( !$incManager->parserOutputIsStabilized() ) { |
329 | $incManager->stabilizeParserOutput( $frev ); |
330 | $resetManager = true; // need to reset when done |
331 | } |
332 | } |
333 | # Parse the new body |
334 | $content = $frev->getRevisionRecord()->getContent( SlotRecord::MAIN ); |
335 | if ( $content === null ) { |
336 | return null; // missing revision |
337 | } |
338 | |
339 | // Make this parse use reviewed/stable versions of templates |
340 | $oldCurrentRevisionRecordCallback = $pOpts->setCurrentRevisionRecordCallback( |
341 | function ( $title, $parser = null ) use ( &$oldCurrentRevisionRecordCallback, $incManager ) { |
342 | if ( !( $parser instanceof Parser ) ) { |
343 | // nothing to do |
344 | return call_user_func( $oldCurrentRevisionRecordCallback, $title, $parser ); |
345 | } |
346 | if ( $title->getNamespace() < 0 || $title->getNamespace() === NS_MEDIAWIKI ) { |
347 | // nothing to do (bug 29579 for NS_MEDIAWIKI) |
348 | return call_user_func( $oldCurrentRevisionRecordCallback, $title, $parser ); |
349 | } |
350 | if ( !$incManager->parserOutputIsStabilized() ) { |
351 | // nothing to do |
352 | return call_user_func( $oldCurrentRevisionRecordCallback, $title, $parser ); |
353 | } |
354 | $id = false; // current version |
355 | # Check for the version of this template used when reviewed... |
356 | $maybeId = $incManager->getReviewedTemplateVersion( $title ); |
357 | if ( $maybeId !== null ) { |
358 | $id = (int)$maybeId; // use if specified (even 0) |
359 | } |
360 | # Check for stable version of template if this feature is enabled... |
361 | if ( self::inclusionSetting() == FR_INCLUDES_STABLE ) { |
362 | $maybeId = $incManager->getStableTemplateVersion( $title ); |
363 | # Take the newest of these two... |
364 | if ( $maybeId && $maybeId > $id ) { |
365 | $id = (int)$maybeId; |
366 | } |
367 | } |
368 | # Found a reviewed/stable revision |
369 | if ( $id !== false ) { |
370 | # If $id is zero, don't bother loading it (page does not exist) |
371 | if ( $id === 0 ) { |
372 | return null; |
373 | } |
374 | return MediaWikiServices::getInstance() |
375 | ->getRevisionLookup() |
376 | ->getRevisionById( $id ); |
377 | } |
378 | # Otherwise, fall back to default behavior (load latest revision) |
379 | return call_user_func( $oldCurrentRevisionRecordCallback, $title, $parser ); |
380 | } |
381 | ); |
382 | $contentRenderer = MediaWikiServices::getInstance()->getContentRenderer(); |
383 | $parserOut = $contentRenderer->getParserOutput( |
384 | $content, $frev->getTitle(), $frev->getRevisionRecord(), $pOpts ); |
385 | # Stable parse done! |
386 | if ( $resetManager ) { |
387 | $incManager->clear(); // reset the FRInclusionManager as needed |
388 | } |
389 | $pOpts->setCurrentRevisionRecordCallback( $oldCurrentRevisionRecordCallback ); |
390 | return $parserOut; |
391 | } |
392 | |
393 | # ################ Tracking/cache update update functions ################# |
394 | |
395 | /** |
396 | * Update the page tables with a new stable version. |
397 | * @param FlaggableWikiPage|PageIdentity $page |
398 | * @param FlaggedRevision|null $sv the new stable version (optional) |
399 | * @param FlaggedRevision|null $oldSv the old stable version (optional) |
400 | * @param RenderedRevision|null $renderedRevision (optional) |
401 | * @return bool stable version text changed and FR_INCLUDES_STABLE |
402 | */ |
403 | public static function stableVersionUpdates( |
404 | object $page, $sv = null, $oldSv = null, $renderedRevision = null |
405 | ) { |
406 | if ( $page instanceof FlaggableWikiPage ) { |
407 | $article = $page; |
408 | } elseif ( $page instanceof PageIdentity ) { |
409 | $article = FlaggableWikiPage::getTitleInstance( $page ); |
410 | } else { |
411 | throw new InvalidArgumentException( "First argument should be a PageIdentity." ); |
412 | } |
413 | if ( !$article->isReviewable() ) { |
414 | return false; |
415 | } |
416 | $title = $article->getTitle(); |
417 | |
418 | $changed = false; |
419 | if ( $oldSv === null ) { // optional |
420 | $oldSv = FlaggedRevision::newFromStable( $title, IDBAccessObject::READ_LATEST ); |
421 | } |
422 | if ( $sv === null ) { // optional |
423 | $sv = FlaggedRevision::determineStable( $title ); |
424 | } |
425 | |
426 | if ( !$sv ) { |
427 | # Empty flaggedrevs data for this page if there is no stable version |
428 | $article->clearStableVersion(); |
429 | # Check if pages using this need to be refreshed... |
430 | if ( self::inclusionSetting() == FR_INCLUDES_STABLE ) { |
431 | $changed = (bool)$oldSv; |
432 | } |
433 | } else { |
434 | if ( $renderedRevision ) { |
435 | $renderedId = $renderedRevision->getRevision()->getId(); |
436 | } else { |
437 | $renderedId = null; |
438 | } |
439 | |
440 | # Update flagged page related fields |
441 | $article->updateStableVersion( $sv, $renderedId ); |
442 | # Check if pages using this need to be invalidated/purged... |
443 | if ( self::inclusionSetting() == FR_INCLUDES_STABLE ) { |
444 | $changed = ( |
445 | !$oldSv || |
446 | $sv->getRevId() != $oldSv->getRevId() |
447 | ); |
448 | } |
449 | } |
450 | # Lazily rebuild dependencies on next parse (we invalidate below) |
451 | self::clearStableOnlyDeps( $title->getArticleID() ); |
452 | # Clear page cache unless this is hooked via RevisionDataUpdates, in |
453 | # which case these updates will happen already with tuned timestamps |
454 | if ( !$renderedRevision ) { |
455 | $title->invalidateCache(); |
456 | self::purgeMediaWikiHtmlCdn( $title ); |
457 | } |
458 | |
459 | return $changed; |
460 | } |
461 | |
462 | /** |
463 | * Clear FlaggedRevs tracking tables for this page |
464 | * @param int|int[] $pageId (int or array) |
465 | */ |
466 | public static function clearTrackingRows( $pageId ) { |
467 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
468 | |
469 | $dbw->newDeleteQueryBuilder() |
470 | ->deleteFrom( 'flaggedpages' ) |
471 | ->where( [ 'fp_page_id' => $pageId ] ) |
472 | ->caller( __METHOD__ ) |
473 | ->execute(); |
474 | $dbw->newDeleteQueryBuilder() |
475 | ->deleteFrom( 'flaggedrevs_tracking' ) |
476 | ->where( [ 'ftr_from' => $pageId ] ) |
477 | ->caller( __METHOD__ ) |
478 | ->execute(); |
479 | } |
480 | |
481 | /** |
482 | * Clear tracking table of stable-only links for this page |
483 | * @param int|int[] $pageId (int or array) |
484 | */ |
485 | public static function clearStableOnlyDeps( $pageId ) { |
486 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
487 | |
488 | $dbw->newDeleteQueryBuilder() |
489 | ->deleteFrom( 'flaggedrevs_tracking' ) |
490 | ->where( [ 'ftr_from' => $pageId ] ) |
491 | ->caller( __METHOD__ ) |
492 | ->execute(); |
493 | } |
494 | |
495 | /** |
496 | * Updates MediaWiki's HTML cache for a Title. Defers till after main commit(). |
497 | * |
498 | * @param Title $title |
499 | */ |
500 | public static function purgeMediaWikiHtmlCdn( Title $title ) { |
501 | DeferredUpdates::addCallableUpdate( static function () use ( $title ) { |
502 | $htmlCache = MediaWikiServices::getInstance()->getHtmlCacheUpdater(); |
503 | $htmlCache->purgeTitleUrls( $title, $htmlCache::PURGE_INTENT_TXROUND_REFLECTED ); |
504 | } ); |
505 | } |
506 | |
507 | /** |
508 | * Do cache updates for when the stable version of a page changed. |
509 | * Invalidates/purges pages that include the given page. |
510 | * @param Title $title |
511 | */ |
512 | public static function updateHtmlCaches( Title $title ) { |
513 | $jobs = []; |
514 | $jobs[] = HTMLCacheUpdateJob::newForBacklinks( $title, 'templatelinks' ); |
515 | MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs ); |
516 | |
517 | DeferredUpdates::addUpdate( new FRExtraCacheUpdate( $title ) ); |
518 | } |
519 | |
520 | /** |
521 | * Invalidates/purges pages where only stable version includes this page. |
522 | * @param Title $title |
523 | */ |
524 | public static function extraHTMLCacheUpdate( Title $title ) { |
525 | DeferredUpdates::addUpdate( new FRExtraCacheUpdate( $title ) ); |
526 | } |
527 | |
528 | # ################ Revision functions ################# |
529 | |
530 | /** |
531 | * Mark a revision as patrolled if needed |
532 | * @param RevisionRecord $revRecord |
533 | */ |
534 | public static function markRevisionPatrolled( RevisionRecord $revRecord ) { |
535 | $rcid = MediaWikiServices::getInstance() |
536 | ->getRevisionStore() |
537 | ->getRcIdIfUnpatrolled( $revRecord ); |
538 | # Make sure it is now marked patrolled... |
539 | if ( $rcid ) { |
540 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
541 | |
542 | $dbw->newUpdateQueryBuilder() |
543 | ->update( 'recentchanges' ) |
544 | ->set( [ 'rc_patrolled' => 1 ] ) |
545 | ->where( [ 'rc_id' => $rcid ] ) |
546 | ->caller( __METHOD__ ) |
547 | ->execute(); |
548 | } |
549 | } |
550 | |
551 | # ################ Other utility functions ################# |
552 | |
553 | /** |
554 | * Get minimum level tags for a tier |
555 | * @deprecated Use quickTag() instead. |
556 | * @return array<string,int> |
557 | */ |
558 | public static function quickTags() { |
559 | return self::useOnlyIfProtected() ? |
560 | [] : |
561 | [ self::getTagName() => 1 ]; |
562 | } |
563 | |
564 | /** |
565 | * Get minimum level tag for the default tier, |
566 | * or `null` if FlaggedRevs is used in protection mode |
567 | */ |
568 | public static function quickTag(): ?int { |
569 | return self::useOnlyIfProtected() ? null : 1; |
570 | } |
571 | |
572 | /** |
573 | * Get minimum tags that are closest to $oldFlags |
574 | * given the site, page, and user rights limitations. |
575 | * @param User $user |
576 | * @param array<string,int> $oldFlags previous stable rev flags |
577 | * @return array<string,int>|null |
578 | */ |
579 | private static function getAutoReviewTags( $user, array $oldFlags ) { |
580 | if ( !self::autoReviewEdits() ) { |
581 | return null; // shouldn't happen |
582 | } |
583 | if ( self::useOnlyIfProtected() ) { |
584 | return []; |
585 | } |
586 | $tag = self::getTagName(); |
587 | # Try to keep this tag val the same as the stable rev's |
588 | $val = $oldFlags[$tag] ?? 1; |
589 | $val = min( $val, self::maxAutoReviewLevel() ); |
590 | # Dial down the level to one the user has permission to set |
591 | while ( !self::userCanSetValue( $user, $val ) ) { |
592 | $val--; |
593 | if ( $val <= 0 ) { |
594 | return null; // all tags vals must be > 0 |
595 | } |
596 | } |
597 | return [ $tag => $val ]; |
598 | } |
599 | |
600 | /** |
601 | * Get the list of reviewable namespaces |
602 | * @return int[] Value from $wgFlaggedRevsNamespaces |
603 | */ |
604 | public static function getReviewNamespaces(): array { |
605 | global $wgFlaggedRevsNamespaces; |
606 | static $validated = false; |
607 | if ( !$validated ) { |
608 | $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); |
609 | foreach ( $wgFlaggedRevsNamespaces as $ns ) { |
610 | if ( $ns === NS_MEDIAWIKI || $namespaceInfo->isTalk( $ns ) ) { |
611 | throw new ConfigException( 'Invalid talk or project namespace in $wgFlaggedRevsNamespaces' ); |
612 | } |
613 | } |
614 | $validated = true; |
615 | } |
616 | return $wgFlaggedRevsNamespaces; |
617 | } |
618 | |
619 | public static function getFirstReviewNamespace(): int { |
620 | return self::getReviewNamespaces()[0] ?? NS_MAIN; |
621 | } |
622 | |
623 | public static function isReviewNamespace( int $ns ): bool { |
624 | return in_array( $ns, self::getReviewNamespaces() ); |
625 | } |
626 | |
627 | public static function inReviewNamespace( PageReference $page ): bool { |
628 | $ns = $page->getNamespace(); |
629 | if ( $ns === NS_MEDIA ) { |
630 | $ns = NS_FILE; |
631 | } |
632 | return self::isReviewNamespace( $ns ); |
633 | } |
634 | |
635 | # ################ Auto-review function ################# |
636 | |
637 | /** |
638 | * Automatically review an revision and add a log entry in the review log. |
639 | * |
640 | * This is called during edit operations after the new revision is added |
641 | * and the page tables updated, but before LinksUpdate is called. |
642 | * |
643 | * $auto is here for revisions checked off to be reviewed. Auto-review |
644 | * triggers on edit, but we don't want those to count as just automatic. |
645 | * This also makes it so the user's name shows up in the page history. |
646 | * |
647 | * If $flags is given, then they will be the review tags. If not, the one |
648 | * from the stable version will be used or minimal tags if that's not possible. |
649 | * If no appropriate tags can be found, then the review will abort. |
650 | * @param WikiPage $article |
651 | * @param User $user |
652 | * @param RevisionRecord $revRecord |
653 | * @param int[]|null $flags |
654 | * @param bool $auto |
655 | * @param bool $approveRevertedTagUpdate Whether to notify the reverted tag |
656 | * subsystem that the edit was reviewed. Should be false when autoreviewing |
657 | * during page creation, true otherwise. Default is false. |
658 | * @return bool |
659 | */ |
660 | public static function autoReviewEdit( |
661 | WikiPage $article, |
662 | $user, |
663 | RevisionRecord $revRecord, |
664 | ?array $flags = null, |
665 | $auto = true, |
666 | $approveRevertedTagUpdate = false |
667 | ) { |
668 | $title = $article->getTitle(); // convenience |
669 | # Get current stable version ID (for logging) |
670 | $oldSv = FlaggedRevision::newFromStable( $title, IDBAccessObject::READ_LATEST ); |
671 | $oldSvId = $oldSv ? $oldSv->getRevId() : 0; |
672 | |
673 | if ( self::useOnlyIfProtected() ) { |
674 | $flags = []; |
675 | } else { |
676 | # Set the auto-review tags from the prior stable version. |
677 | # Normally, this should already be done and given here... |
678 | if ( !is_array( $flags ) ) { |
679 | if ( $oldSv ) { |
680 | # Use the last stable version if $flags not given |
681 | if ( MediaWikiServices::getInstance()->getPermissionManager() |
682 | ->userHasRight( $user, 'bot' ) |
683 | ) { |
684 | $flags = $oldSv->getTags(); // no change for bot edits |
685 | } else { |
686 | # Account for perms/tags... |
687 | $flags = self::getAutoReviewTags( $user, $oldSv->getTags() ); |
688 | } |
689 | } else { // new page? |
690 | $flags = self::quickTags(); |
691 | } |
692 | if ( !is_array( $flags ) ) { |
693 | return false; // can't auto-review this revision |
694 | } |
695 | } |
696 | } |
697 | |
698 | # Our review entry |
699 | $flaggedRevision = new FlaggedRevision( [ |
700 | 'revrecord' => $revRecord, |
701 | 'user_id' => $user->getId(), |
702 | 'timestamp' => $revRecord->getTimestamp(), // same as edit time |
703 | 'tags' => $flags, |
704 | 'flags' => $auto ? 'auto' : '', |
705 | ] ); |
706 | |
707 | // Insert the flagged revision |
708 | $success = $flaggedRevision->insert(); |
709 | if ( $success !== true ) { |
710 | return false; |
711 | } |
712 | |
713 | if ( $approveRevertedTagUpdate ) { |
714 | $flaggedRevision->approveRevertedTagUpdate(); |
715 | } |
716 | |
717 | # Update the article review log |
718 | if ( !$auto ) { |
719 | FlaggedRevsLog::updateReviewLog( $title, |
720 | $flags, '', $revRecord->getId(), $oldSvId, true, $user ); |
721 | } |
722 | |
723 | # Update page and tracking tables and clear cache |
724 | self::stableVersionUpdates( $article ); |
725 | |
726 | return true; |
727 | } |
728 | |
729 | } |