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