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