Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
27.09% |
185 / 683 |
|
10.53% |
4 / 38 |
CRAP | |
0.00% |
0 / 1 |
| FlaggedRevsHooks | |
27.09% |
185 / 683 |
|
10.53% |
4 / 38 |
19849.15 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| onRegistration | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
2 | |||
| onRevisionUndeleted | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| onArticleMergeComplete | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
6 | |||
| onPageMoveComplete | |
48.15% |
13 / 27 |
|
0.00% |
0 / 1 |
13.83 | |||
| onRevisionDataUpdates | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| onArticleDeleteComplete | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onArticleUndelete | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| onArticleRevisionVisibilitySet | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| onParserFirstCallInit | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
| onParserGetVariableValueSwitch | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
| onGetMagicVariableIDs | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| parserPendingChangeLevel | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
| onGetUserPermissionsErrors | |
33.33% |
11 / 33 |
|
0.00% |
0 / 1 |
138.52 | |||
| maybeMakeEditReviewed | |
88.57% |
62 / 70 |
|
0.00% |
0 / 1 |
34.63 | |||
| editCheckReview | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
| isSelfRevertToStable | |
52.94% |
18 / 34 |
|
0.00% |
0 / 1 |
7.61 | |||
| maybeNullEditReview | |
56.10% |
23 / 41 |
|
0.00% |
0 / 1 |
41.45 | |||
| onRecentChange_save | |
87.50% |
14 / 16 |
|
0.00% |
0 / 1 |
5.05 | |||
| maybeIncrementReverts | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
7.04 | |||
| getQueryData | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
20 | |||
| editSpacingCheck | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
210 | |||
| reviewedEditsCheck | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
56 | |||
| wasPreviouslyBlocked | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
| recentEditCount | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
| recentContentEditCount | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
12 | |||
| onUserGetRights | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
4.12 | |||
| onRevisionFromEditComplete | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| onPageSaveComplete | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
7 | |||
| onUserRequirementsCondition | |
0.00% |
0 / 112 |
|
0.00% |
0 / 1 |
1260 | |||
| onUserLoadAfterLoadFromSession | |
16.67% |
1 / 6 |
|
0.00% |
0 / 1 |
13.26 | |||
| onWikiExporter__dumpStableQuery | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
| onBeforeCreateEchoEvent | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| onUserMergeAccountFields | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onMergeAccountFromTo | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| onDeleteAccount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onBeforeRevertedTagUpdate | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
2.00 | |||
| onUserRequirementsConditionDisplay | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
132 | |||
| 1 | <?php |
| 2 | |
| 3 | use MediaWiki\Config\Config; |
| 4 | use MediaWiki\Context\IContextSource; |
| 5 | use MediaWiki\Deferred\DeferredUpdates; |
| 6 | use MediaWiki\Export\Hook\WikiExporter__dumpStableQueryHook; |
| 7 | use MediaWiki\Extension\Notifications\AttributeManager; |
| 8 | use MediaWiki\Extension\Notifications\UserLocator; |
| 9 | use MediaWiki\Hook\ArticleMergeCompleteHook; |
| 10 | use MediaWiki\Hook\PageMoveCompleteHook; |
| 11 | use MediaWiki\MediaWikiServices; |
| 12 | use MediaWiki\Page\Hook\ArticleDeleteCompleteHook; |
| 13 | use MediaWiki\Page\Hook\ArticleUndeleteHook; |
| 14 | use MediaWiki\Page\Hook\RevisionFromEditCompleteHook; |
| 15 | use MediaWiki\Page\Hook\RevisionUndeletedHook; |
| 16 | use MediaWiki\Page\WikiPage; |
| 17 | use MediaWiki\Parser\Hook\GetMagicVariableIDsHook; |
| 18 | use MediaWiki\Parser\Hook\ParserFirstCallInitHook; |
| 19 | use MediaWiki\Parser\Hook\ParserGetVariableValueSwitchHook; |
| 20 | use MediaWiki\Parser\Parser; |
| 21 | use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook; |
| 22 | use MediaWiki\Permissions\Hook\UserGetRightsHook; |
| 23 | use MediaWiki\Permissions\PermissionManager; |
| 24 | use MediaWiki\Permissions\RestrictionStore; |
| 25 | use MediaWiki\RecentChanges\Hook\RecentChange_saveHook; |
| 26 | use MediaWiki\Revision\RevisionLookup; |
| 27 | use MediaWiki\Revision\RevisionRecord; |
| 28 | use MediaWiki\RevisionDelete\Hook\ArticleRevisionVisibilitySetHook; |
| 29 | use MediaWiki\Storage\EditResult; |
| 30 | use MediaWiki\Storage\Hook\BeforeRevertedTagUpdateHook; |
| 31 | use MediaWiki\Storage\Hook\PageSaveCompleteHook; |
| 32 | use MediaWiki\Storage\Hook\RevisionDataUpdatesHook; |
| 33 | use MediaWiki\Title\Title; |
| 34 | use MediaWiki\User\ActorMigration; |
| 35 | use MediaWiki\User\Hook\UserLoadAfterLoadFromSessionHook; |
| 36 | use MediaWiki\User\Hook\UserRequirementsConditionDisplayHook; |
| 37 | use MediaWiki\User\Hook\UserRequirementsConditionHook; |
| 38 | use MediaWiki\User\User; |
| 39 | use MediaWiki\User\UserFactory; |
| 40 | use MediaWiki\User\UserIdentity; |
| 41 | use MediaWiki\User\UserIdentityUtils; |
| 42 | use MediaWiki\User\UserNameUtils; |
| 43 | use MediaWiki\Utils\MWCryptRand; |
| 44 | use Wikimedia\Message\MessageSpecifier; |
| 45 | use Wikimedia\Message\MessageValue; |
| 46 | use Wikimedia\ObjectCache\WANObjectCache; |
| 47 | use Wikimedia\Rdbms\Database; |
| 48 | use Wikimedia\Rdbms\IConnectionProvider; |
| 49 | use Wikimedia\Rdbms\IDBAccessObject; |
| 50 | use Wikimedia\Rdbms\IReadableDatabase; |
| 51 | use Wikimedia\Rdbms\RawSQLValue; |
| 52 | use Wikimedia\Rdbms\SelectQueryBuilder; |
| 53 | |
| 54 | /** |
| 55 | * Class containing hooked functions for a FlaggedRevs environment |
| 56 | */ |
| 57 | class FlaggedRevsHooks implements |
| 58 | ArticleDeleteCompleteHook, |
| 59 | ArticleMergeCompleteHook, |
| 60 | ArticleRevisionVisibilitySetHook, |
| 61 | ArticleUndeleteHook, |
| 62 | BeforeRevertedTagUpdateHook, |
| 63 | getUserPermissionsErrorsHook, |
| 64 | GetMagicVariableIDsHook, |
| 65 | RevisionFromEditCompleteHook, |
| 66 | PageSaveCompleteHook, |
| 67 | PageMoveCompleteHook, |
| 68 | ParserFirstCallInitHook, |
| 69 | ParserGetVariableValueSwitchHook, |
| 70 | RecentChange_saveHook, |
| 71 | RevisionDataUpdatesHook, |
| 72 | RevisionUndeletedHook, |
| 73 | UserGetRightsHook, |
| 74 | UserLoadAfterLoadFromSessionHook, |
| 75 | UserRequirementsConditionHook, |
| 76 | UserRequirementsConditionDisplayHook, |
| 77 | WikiExporter__dumpStableQueryHook |
| 78 | { |
| 79 | |
| 80 | public function __construct( |
| 81 | private readonly Config $config, |
| 82 | private readonly IConnectionProvider $connectionProvider, |
| 83 | private readonly PermissionManager $permissionManager, |
| 84 | private readonly RestrictionStore $restrictionStore, |
| 85 | private readonly RevisionLookup $revisionLookup, |
| 86 | private readonly UserNameUtils $userNameUtils, |
| 87 | private readonly UserIdentityUtils $userIdentityUtils, |
| 88 | private readonly UserFactory $userFactory, |
| 89 | private readonly WANObjectCache $cache, |
| 90 | ) { |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * @see https://www.mediawiki.org/wiki/Manual:Extension_registration#Customizing_registration |
| 95 | */ |
| 96 | public static function onRegistration() { |
| 97 | # Review tier constants... |
| 98 | define( 'FR_CHECKED', 0 ); // "basic"/"checked" |
| 99 | |
| 100 | # Inclusion (templates) settings |
| 101 | define( 'FR_INCLUDES_CURRENT', 0 ); |
| 102 | define( 'FR_INCLUDES_STABLE', 2 ); |
| 103 | |
| 104 | # Autoreview settings for priviledged users |
| 105 | define( 'FR_AUTOREVIEW_NONE', 0 ); |
| 106 | define( 'FR_AUTOREVIEW_CHANGES', 1 ); |
| 107 | define( 'FR_AUTOREVIEW_CREATION', 2 ); |
| 108 | define( 'FR_AUTOREVIEW_CREATION_AND_CHANGES', FR_AUTOREVIEW_CHANGES | FR_AUTOREVIEW_CREATION ); |
| 109 | |
| 110 | # User preference for when page views use stable or current page versions |
| 111 | define( 'FR_SHOW_STABLE_DEFAULT', 0 ); // page config default; b/c with "false" |
| 112 | define( 'FR_SHOW_STABLE_ALWAYS', 1 ); // stable version (current version if none) |
| 113 | define( 'FR_SHOW_STABLE_NEVER', 2 ); // current version |
| 114 | |
| 115 | # Autopromote conds (F=70,R=82) |
| 116 | # @TODO: move these 6 to core |
| 117 | define( 'APCOND_FR_EDITSUMMARYCOUNT', 70821 ); |
| 118 | define( 'APCOND_FR_NEVERBLOCKED', 70822 ); |
| 119 | define( 'APCOND_FR_NEVERBOCKED', 70822 ); // b/c |
| 120 | define( 'APCOND_FR_UNIQUEPAGECOUNT', 70823 ); |
| 121 | define( 'APCOND_FR_CONTENTEDITCOUNT', 70824 ); |
| 122 | define( 'APCOND_FR_USERPAGEBYTES', 70825 ); |
| 123 | define( 'APCOND_FR_EDITCOUNT', 70826 ); |
| 124 | |
| 125 | define( 'APCOND_FR_EDITSPACING', 70827 ); |
| 126 | define( 'APCOND_FR_CHECKEDEDITCOUNT', 70828 ); |
| 127 | define( 'APCOND_FR_MAXREVERTEDEDITRATIO', 70829 ); |
| 128 | define( 'APCOND_FR_NEVERDEMOTED', 70830 ); |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * @inheritDoc |
| 133 | * |
| 134 | * Update flaggedrevs table on revision restore |
| 135 | */ |
| 136 | public function onRevisionUndeleted( $revision, $oldPageID ) { |
| 137 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
| 138 | |
| 139 | # Some revisions may have had null rev_id values stored when deleted. |
| 140 | # This hook is called after insertOn() however, in which case it is set |
| 141 | # as a new one. |
| 142 | $dbw->newUpdateQueryBuilder() |
| 143 | ->update( 'flaggedrevs' ) |
| 144 | ->set( [ 'fr_page_id' => $revision->getPageId() ] ) |
| 145 | ->where( [ 'fr_page_id' => $oldPageID, 'fr_rev_id' => $revision->getId() ] ) |
| 146 | ->caller( __METHOD__ ) |
| 147 | ->execute(); |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * @inheritDoc |
| 152 | */ |
| 153 | public function onArticleMergeComplete( $sourceTitle, $destTitle ) { |
| 154 | $oldPageID = $sourceTitle->getArticleID(); |
| 155 | $newPageID = $destTitle->getArticleID(); |
| 156 | # Get flagged revisions from old page id that point to destination page |
| 157 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
| 158 | |
| 159 | $revIDs = $dbw->newSelectQueryBuilder() |
| 160 | ->select( 'fr_rev_id' ) |
| 161 | ->from( 'flaggedrevs' ) |
| 162 | ->join( 'revision', null, 'fr_rev_id = rev_id' ) |
| 163 | ->where( [ |
| 164 | 'fr_page_id' => $oldPageID, |
| 165 | 'rev_page' => $newPageID |
| 166 | ] ) |
| 167 | ->caller( __METHOD__ ) |
| 168 | ->fetchFieldValues(); |
| 169 | # Update these rows |
| 170 | if ( $revIDs ) { |
| 171 | $dbw->newUpdateQueryBuilder() |
| 172 | ->update( 'flaggedrevs' ) |
| 173 | ->set( [ 'fr_page_id' => $newPageID ] ) |
| 174 | ->where( [ 'fr_page_id' => $oldPageID, 'fr_rev_id' => $revIDs ] ) |
| 175 | ->caller( __METHOD__ ) |
| 176 | ->execute(); |
| 177 | } |
| 178 | # Update pages...stable versions possibly lost to another page |
| 179 | FlaggedRevs::stableVersionUpdates( $sourceTitle ); |
| 180 | FlaggedRevs::updateHtmlCaches( $sourceTitle ); |
| 181 | FlaggedRevs::stableVersionUpdates( $destTitle ); |
| 182 | FlaggedRevs::updateHtmlCaches( $destTitle ); |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * @inheritDoc |
| 187 | * (a) Update flaggedrevs page/tracking tables |
| 188 | * (b) Autoreview pages moved into reviewable namespaces (bug 19379) |
| 189 | */ |
| 190 | public function onPageMoveComplete( |
| 191 | $oLinkTarget, |
| 192 | $nLinkTarget, |
| 193 | $userIdentity, |
| 194 | $pageId, |
| 195 | $redirid, |
| 196 | $reason, |
| 197 | $revision |
| 198 | ) { |
| 199 | $ntitle = Title::newFromLinkTarget( $nLinkTarget ); |
| 200 | $otitle = Title::newFromLinkTarget( $oLinkTarget ); |
| 201 | if ( FlaggedRevs::inReviewNamespace( $ntitle ) ) { |
| 202 | $user = User::newFromIdentity( $userIdentity ); |
| 203 | |
| 204 | if ( FlaggedRevs::inReviewNamespace( $otitle ) ) { |
| 205 | $fa = FlaggableWikiPage::getTitleInstance( $ntitle ); |
| 206 | $fa->loadPageData( IDBAccessObject::READ_LATEST ); |
| 207 | $config = $fa->getStabilitySettings(); |
| 208 | // Insert a stable log entry if page doesn't have default wiki settings |
| 209 | if ( !FRPageConfig::configIsReset( $config ) ) { |
| 210 | FlaggedRevsLog::updateStabilityLogOnMove( $ntitle, $otitle, $reason, $user ); |
| 211 | } |
| 212 | } elseif ( FlaggedRevs::autoReviewNewPages() ) { |
| 213 | $fa = FlaggableWikiPage::getTitleInstance( $ntitle ); |
| 214 | $fa->loadPageData( IDBAccessObject::READ_LATEST ); |
| 215 | // Re-validate NS/config (new title may not be reviewable) |
| 216 | if ( $fa->isReviewable() && |
| 217 | $this->permissionManager->userCan( 'autoreview', $user, $ntitle ) |
| 218 | ) { |
| 219 | // Auto-review such edits like new pages... |
| 220 | FlaggedRevs::autoReviewEdit( |
| 221 | $fa, |
| 222 | $user, |
| 223 | $revision, |
| 224 | null, |
| 225 | true, |
| 226 | true // approve the reverted tag update |
| 227 | ); |
| 228 | } |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | # Update page and tracking tables and clear cache |
| 233 | FlaggedRevs::stableVersionUpdates( $otitle ); |
| 234 | FlaggedRevs::updateHtmlCaches( $otitle ); |
| 235 | FlaggedRevs::stableVersionUpdates( $ntitle ); |
| 236 | FlaggedRevs::updateHtmlCaches( $ntitle ); |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * @inheritDoc |
| 241 | * (a) Update flaggedrevs page/tracking tables |
| 242 | * (b) Pages with stable versions that use this page will be purged |
| 243 | * Note: pages with current versions that use this page should already be purged |
| 244 | */ |
| 245 | public function onRevisionDataUpdates( |
| 246 | $title, $renderedRevision, &$updates |
| 247 | ) { |
| 248 | $updates[] = new FRStableVersionUpdate( $title, $renderedRevision ); |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * @inheritDoc |
| 253 | * (a) Update flaggedrevs page/tracking tables |
| 254 | * (b) Pages with stable versions that use this page will be purged |
| 255 | * Note: pages with current versions that use this page should already be purged |
| 256 | */ |
| 257 | public function onArticleDeleteComplete( $wikiPage, $user, $reason, $id, $content, $logEntry, $count ) { |
| 258 | FlaggedRevs::clearTrackingRows( $id ); |
| 259 | } |
| 260 | |
| 261 | /** |
| 262 | * @inheritDoc |
| 263 | * (a) Update flaggedrevs page/tracking tables |
| 264 | * (b) Pages with stable versions that use this page will be purged |
| 265 | * Note: pages with current versions that use this page should already be purged |
| 266 | */ |
| 267 | public function onArticleUndelete( $title, $create, $comment, $oldPageId, $restoredPages ) { |
| 268 | FlaggedRevs::stableVersionUpdates( $title ); |
| 269 | FlaggedRevs::updateHtmlCaches( $title ); |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * @inheritDoc |
| 274 | * Update flaggedrevs page/tracking tables |
| 275 | */ |
| 276 | public function onArticleRevisionVisibilitySet( $title, $ids, $visibilityChangeMap ) { |
| 277 | $changed = FlaggedRevs::stableVersionUpdates( $title ); |
| 278 | if ( $changed ) { |
| 279 | FlaggedRevs::updateHtmlCaches( $title ); |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | /** |
| 284 | * @inheritDoc |
| 285 | */ |
| 286 | public function onParserFirstCallInit( $parser ) { |
| 287 | global $wgFlaggedRevsProtection; |
| 288 | |
| 289 | if ( !$wgFlaggedRevsProtection ) { |
| 290 | return; |
| 291 | } |
| 292 | |
| 293 | $parser->setFunctionHook( 'pendingchangelevel', |
| 294 | [ self::class, 'parserPendingChangeLevel' ], Parser::SFH_NO_HASH ); |
| 295 | } |
| 296 | |
| 297 | /** |
| 298 | * @inheritDoc |
| 299 | */ |
| 300 | public function onParserGetVariableValueSwitch( $parser, &$cache, $word, &$ret, $frame ) { |
| 301 | global $wgFlaggedRevsProtection; |
| 302 | if ( $wgFlaggedRevsProtection && $word === 'pendingchangelevel' ) { |
| 303 | $ret = self::parserPendingChangeLevel( $parser ); |
| 304 | $cache[$word] = $ret; |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | /** |
| 309 | * @inheritDoc |
| 310 | */ |
| 311 | public function onGetMagicVariableIDs( &$variableIDs ): void { |
| 312 | global $wgFlaggedRevsProtection; |
| 313 | |
| 314 | if ( !$wgFlaggedRevsProtection ) { |
| 315 | return; |
| 316 | } |
| 317 | |
| 318 | $variableIDs[] = 'pendingchangelevel'; |
| 319 | } |
| 320 | |
| 321 | /** |
| 322 | * @see https://www.mediawiki.org/wiki/Manual:Parser_functions#The_setFunctionHook_hook |
| 323 | * |
| 324 | * @param Parser $parser |
| 325 | * @param string $page |
| 326 | * @return string |
| 327 | */ |
| 328 | public static function parserPendingChangeLevel( Parser $parser, $page = '' ) { |
| 329 | $title = Title::newFromText( $page ); |
| 330 | if ( !( $title instanceof Title ) ) { |
| 331 | $title = $parser->getTitle(); |
| 332 | } |
| 333 | if ( !FlaggedRevs::inReviewNamespace( $title ) ) { |
| 334 | return ''; |
| 335 | } |
| 336 | $page = FlaggableWikiPage::getTitleInstance( $title ); |
| 337 | if ( !$page->isDataLoaded() && !$parser->incrementExpensiveFunctionCount() ) { |
| 338 | return ''; |
| 339 | } |
| 340 | $config = $page->getStabilitySettings(); |
| 341 | return $config['autoreview']; |
| 342 | } |
| 343 | |
| 344 | /** |
| 345 | * @inheritDoc |
| 346 | * Check page move and patrol permissions for FlaggedRevs |
| 347 | */ |
| 348 | public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) { |
| 349 | if ( $result === false ) { |
| 350 | return true; // nothing to do |
| 351 | } |
| 352 | # Don't let users vandalize pages by moving them... |
| 353 | if ( $action === 'move' ) { |
| 354 | if ( !FlaggedRevs::inReviewNamespace( $title ) || !$title->exists() ) { |
| 355 | return true; // extra short-circuit |
| 356 | } |
| 357 | $flaggedArticle = FlaggableWikiPage::getTitleInstance( $title ); |
| 358 | # If the draft shows by default anyway, nothing to do... |
| 359 | if ( !$flaggedArticle->isStableShownByDefault() ) { |
| 360 | return true; |
| 361 | } |
| 362 | $frev = $flaggedArticle->getStableRev(); |
| 363 | if ( $frev && !$this->permissionManager->userHasRight( $user, 'review' ) && |
| 364 | !$this->permissionManager->userHasRight( $user, 'movestable' ) |
| 365 | ) { |
| 366 | # Allow for only editors/reviewers to move this page |
| 367 | $result = false; |
| 368 | return false; |
| 369 | } |
| 370 | # Enforce autoreview/review restrictions |
| 371 | } elseif ( $action === 'autoreview' || $action === 'review' ) { |
| 372 | # Get autoreview restriction settings... |
| 373 | $fa = FlaggableWikiPage::getTitleInstance( $title ); |
| 374 | $config = $fa->getStabilitySettings(); |
| 375 | # Convert Sysop -> protect |
| 376 | $right = $config['autoreview']; |
| 377 | if ( $right === 'sysop' ) { |
| 378 | // Backwards compatibility, rewrite sysop -> editprotected |
| 379 | $right = 'editprotected'; |
| 380 | } |
| 381 | if ( $right === 'autoconfirmed' ) { |
| 382 | // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected |
| 383 | $right = 'editsemiprotected'; |
| 384 | } |
| 385 | # Check if the user has the required right, if any |
| 386 | if ( $right != '' && !$this->permissionManager->userHasRight( $user, $right ) ) { |
| 387 | $result = false; |
| 388 | return false; |
| 389 | } |
| 390 | # Respect page protection to handle cases of "review wars". |
| 391 | # If a page is restricted from editing such that a user cannot |
| 392 | # edit it, then said user should not be able to review it. |
| 393 | foreach ( $this->restrictionStore->getRestrictions( $title, 'edit' ) as $right ) { |
| 394 | if ( $right === 'sysop' ) { |
| 395 | // Backwards compatibility, rewrite sysop -> editprotected |
| 396 | $right = 'editprotected'; |
| 397 | } |
| 398 | if ( $right === 'autoconfirmed' ) { |
| 399 | // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected |
| 400 | $right = 'editsemiprotected'; |
| 401 | } |
| 402 | if ( $right != '' && !$this->permissionManager->userHasRight( $user, $right ) ) { |
| 403 | $result = false; |
| 404 | return false; |
| 405 | } |
| 406 | } |
| 407 | } |
| 408 | return true; |
| 409 | } |
| 410 | |
| 411 | /** |
| 412 | * When an edit is made by a user, review it if either: |
| 413 | * (a) The user can 'autoreview' and the edit's base revision was checked |
| 414 | * (b) The edit is a self-revert to the stable version (by anyone) |
| 415 | * (c) The user can 'autoreview' new pages and this edit is a new page |
| 416 | * (d) The user can 'review' and the "review pending edits" checkbox was checked |
| 417 | * |
| 418 | * Note: This hook handler is triggered in a variety of places, not just |
| 419 | * during edits. Example include null revision creation, imports, |
| 420 | * and page moves. |
| 421 | * |
| 422 | * Note: RC items not inserted yet, RecentChange_save hook does rc_patrolled bit... |
| 423 | * @param WikiPage $wikiPage |
| 424 | * @param RevisionRecord $revRecord |
| 425 | * @param int|false $baseRevId |
| 426 | * @param UserIdentity $user |
| 427 | */ |
| 428 | public static function maybeMakeEditReviewed( |
| 429 | WikiPage $wikiPage, RevisionRecord $revRecord, $baseRevId, UserIdentity $user |
| 430 | ) { |
| 431 | $request = RequestContext::getMain()->getRequest(); |
| 432 | |
| 433 | $title = $wikiPage->getTitle(); // convenience |
| 434 | # Edit must be non-null, to a reviewable page, with $user set |
| 435 | $fa = FlaggableWikiPage::getTitleInstance( $title ); |
| 436 | $fa->loadPageData( IDBAccessObject::READ_LATEST ); |
| 437 | if ( !$fa->isReviewable() ) { |
| 438 | return; |
| 439 | } |
| 440 | |
| 441 | $user = User::newFromIdentity( $user ); |
| 442 | |
| 443 | $fa->preloadPreparedEdit( $wikiPage ); // avoid double parse |
| 444 | $title->resetArticleID( $revRecord->getPageId() ); // Avoid extra DB hit and lag issues |
| 445 | # Get what was just the current revision ID |
| 446 | $prevRevId = $revRecord->getParentId(); |
| 447 | # Get edit timestamp. Existance already validated by \MediaWiki\EditPage\EditPage |
| 448 | $editTimestamp = $request->getVal( 'wpEdittime' ); |
| 449 | $pm = MediaWikiServices::getInstance()->getPermissionManager(); |
| 450 | # Is the page manually checked off to be reviewed? |
| 451 | if ( $editTimestamp |
| 452 | && $request->getCheck( 'wpReviewEdit' ) |
| 453 | && $pm->userCan( 'review', $user, $title ) |
| 454 | && self::editCheckReview( $fa, $revRecord, $user, $editTimestamp ) |
| 455 | ) { |
| 456 | // Reviewed... done! |
| 457 | return; |
| 458 | } |
| 459 | # All cases below require auto-review of edits to be enabled |
| 460 | if ( !FlaggedRevs::autoReviewEnabled() ) { |
| 461 | // Short-circuit |
| 462 | return; |
| 463 | } |
| 464 | # If a $baseRevId is passed in, the edit is using an old revision's text |
| 465 | $isOldRevCopy = (bool)$baseRevId; // null edit or rollback |
| 466 | # Get the revision ID the incoming one was based off... |
| 467 | $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup(); |
| 468 | if ( !$baseRevId && $prevRevId ) { |
| 469 | $prevTimestamp = $revisionLookup->getTimestampFromId( |
| 470 | $prevRevId, |
| 471 | IDBAccessObject::READ_LATEST |
| 472 | ); |
| 473 | # The user just made an edit. The one before that should have |
| 474 | # been the current version. If not reflected in wpEdittime, an |
| 475 | # edit may have been auto-merged in between, in that case, discard |
| 476 | # the baseRevId given from the client. |
| 477 | if ( $editTimestamp && $prevTimestamp === $editTimestamp ) { |
| 478 | $baseRevId = $request->getInt( 'baseRevId' ); |
| 479 | } |
| 480 | # If baseRevId not given, assume the previous revision ID (for bots). |
| 481 | # For auto-merges, this also occurs since the given ID is ignored. |
| 482 | if ( !$baseRevId ) { |
| 483 | $baseRevId = $prevRevId; |
| 484 | } |
| 485 | } |
| 486 | $frev = null; // flagged rev this edit was based on |
| 487 | $flags = null; // review flags (null => default flags) |
| 488 | $srev = $fa->getStableRev(); |
| 489 | # Case A: this user can auto-review edits. Check if either: |
| 490 | # (a) this new revision creates a new page and new page autoreview is enabled |
| 491 | # (b) this new revision is based on an old, reviewed, revision |
| 492 | if ( $pm->userCan( 'autoreview', $user, $title ) ) { |
| 493 | # For rollback/null edits, use the previous ID as the alternate base ID. |
| 494 | # Otherwise, use the 'altBaseRevId' parameter passed in by the request. |
| 495 | $altBaseRevId = $isOldRevCopy ? $prevRevId : $request->getInt( 'altBaseRevId' ); |
| 496 | if ( !$prevRevId ) { // New pages |
| 497 | $reviewableNewPage = FlaggedRevs::autoReviewNewPages(); |
| 498 | $reviewableChange = false; |
| 499 | } else { // Edits to existing pages |
| 500 | $reviewableNewPage = false; // had previous rev |
| 501 | # If a edit was automatically merged, do not trust 'baseRevId' (bug 33481). |
| 502 | # Do this by verifying the user-provided edittime against the prior revision. |
| 503 | $prevRevTimestamp = $revisionLookup->getTimestampFromId( |
| 504 | $prevRevId, |
| 505 | IDBAccessObject::READ_LATEST |
| 506 | ); |
| 507 | if ( $editTimestamp && $editTimestamp !== $prevRevTimestamp ) { |
| 508 | $baseRevId = $prevRevId; |
| 509 | $altBaseRevId = 0; |
| 510 | } |
| 511 | # Check if the base revision was reviewed... |
| 512 | if ( FlaggedRevs::autoReviewEdits() ) { |
| 513 | $frev = FlaggedRevision::newFromTitle( $title, $baseRevId, IDBAccessObject::READ_LATEST ); |
| 514 | if ( !$frev && $altBaseRevId ) { |
| 515 | $frev = FlaggedRevision::newFromTitle( $title, $altBaseRevId, IDBAccessObject::READ_LATEST ); |
| 516 | } |
| 517 | } |
| 518 | $reviewableChange = $frev || |
| 519 | # Bug 57073: If a user with autoreview returns the page to its last stable |
| 520 | # version, it should be marked stable, regardless of the method used to do so. |
| 521 | ( $srev && $revRecord->getSha1() === $srev->getRevisionRecord()->getSha1() ); |
| 522 | } |
| 523 | # Is this an edit directly to a reviewed version or a new page? |
| 524 | if ( $reviewableNewPage || $reviewableChange ) { |
| 525 | if ( $isOldRevCopy && $frev ) { |
| 526 | $flags = $frev->getTags(); // null edits & rollbacks keep previous tags |
| 527 | } |
| 528 | # Review this revision of the page... |
| 529 | FlaggedRevs::autoReviewEdit( $fa, $user, $revRecord, $flags ); |
| 530 | } |
| 531 | # Case B: the user cannot autoreview edits. Check if either: |
| 532 | # (a) this is a rollback to the stable version |
| 533 | # (b) this is a self-reversion to the stable version |
| 534 | # These are subcases of making a new revision based on an old, reviewed, revision. |
| 535 | } elseif ( FlaggedRevs::autoReviewEdits() && $srev ) { |
| 536 | # Check for rollbacks... |
| 537 | $reviewableChange = ( |
| 538 | $isOldRevCopy && // rollback or null edit |
| 539 | $baseRevId != $prevRevId && // not a null edit |
| 540 | $baseRevId == $srev->getRevId() && // restored stable rev |
| 541 | $pm->userCan( 'autoreviewrestore', $user, $title ) |
| 542 | ); |
| 543 | # Check for self-reversions (checks text hashes)... |
| 544 | if ( !$reviewableChange ) { |
| 545 | $reviewableChange = self::isSelfRevertToStable( $revRecord, $srev, $baseRevId, $user ); |
| 546 | } |
| 547 | # Is this a rollback or self-reversion to the stable rev? |
| 548 | if ( $reviewableChange ) { |
| 549 | $flags = $srev->getTags(); // use old tags |
| 550 | # Review this revision of the page... |
| 551 | FlaggedRevs::autoReviewEdit( $fa, $user, $revRecord, $flags ); |
| 552 | } |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | /** |
| 557 | * Review $rev if $editTimestamp matches the previous revision's timestamp. |
| 558 | * Otherwise, review the revision that has $editTimestamp as its timestamp value. |
| 559 | * @param WikiPage $wikiPage |
| 560 | * @param RevisionRecord $revRecord |
| 561 | * @param User $user |
| 562 | * @param string $editTimestamp |
| 563 | * @return bool |
| 564 | */ |
| 565 | private static function editCheckReview( |
| 566 | WikiPage $wikiPage, $revRecord, $user, $editTimestamp |
| 567 | ) { |
| 568 | $prevTimestamp = null; |
| 569 | $prevRevId = $revRecord->getParentId(); // id for revision before $revRecord |
| 570 | # Check wpEdittime against the former current rev for verification |
| 571 | if ( $prevRevId ) { |
| 572 | $prevTimestamp = MediaWikiServices::getInstance() |
| 573 | ->getRevisionLookup() |
| 574 | ->getTimestampFromId( $prevRevId, IDBAccessObject::READ_LATEST ); |
| 575 | } |
| 576 | # Was $revRecord an edit to an existing page? |
| 577 | if ( $prevTimestamp && $prevRevId ) { |
| 578 | # Check wpEdittime against the former current revision's time. |
| 579 | # If an edit was auto-merged in between, then the new revision |
| 580 | # has content different than what the user expected. However, if |
| 581 | # the auto-merged edit was reviewed, then assume that it's OK. |
| 582 | if ( $editTimestamp != $prevTimestamp |
| 583 | && !FlaggedRevision::revIsFlagged( $prevRevId, IDBAccessObject::READ_LATEST ) |
| 584 | ) { |
| 585 | return false; // not flagged? |
| 586 | } |
| 587 | } |
| 588 | $flags = null; |
| 589 | # Review this revision of the page... |
| 590 | return FlaggedRevs::autoReviewEdit( $wikiPage, $user, $revRecord, $flags, false /* manual */ ); |
| 591 | } |
| 592 | |
| 593 | /** |
| 594 | * Check if a user reverted himself to the stable version |
| 595 | * @param RevisionRecord $revRecord |
| 596 | * @param FlaggedRevision $srev |
| 597 | * @param int $baseRevId |
| 598 | * @param User $user |
| 599 | * @return bool |
| 600 | */ |
| 601 | private static function isSelfRevertToStable( |
| 602 | RevisionRecord $revRecord, |
| 603 | $srev, |
| 604 | $baseRevId, |
| 605 | $user |
| 606 | ) { |
| 607 | if ( !$srev || $baseRevId != $srev->getRevId() ) { |
| 608 | return false; // user reports they are not the same |
| 609 | } |
| 610 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
| 611 | |
| 612 | # Such a revert requires 1+ revs between it and the stable |
| 613 | $revWhere = ActorMigration::newMigration()->getWhere( $dbw, 'rev_user', $user ); |
| 614 | $revertedRevs = (bool)$dbw->newSelectQueryBuilder() |
| 615 | ->select( '1' ) |
| 616 | ->from( 'revision' ) |
| 617 | ->tables( $revWhere['tables'] ) |
| 618 | ->where( [ |
| 619 | 'rev_page' => $revRecord->getPageId(), |
| 620 | $dbw->expr( 'rev_id', '>', intval( $baseRevId ) ), // stable rev |
| 621 | $dbw->expr( 'rev_id', '<', intval( $revRecord->getId() ) ), // this rev |
| 622 | $revWhere['conds'] |
| 623 | ] ) |
| 624 | ->joinConds( $revWhere['joins'] ) |
| 625 | ->caller( __METHOD__ ) |
| 626 | ->fetchField(); |
| 627 | if ( !$revertedRevs ) { |
| 628 | return false; // can't be a revert |
| 629 | } |
| 630 | # Check that this user is ONLY reverting his/herself. |
| 631 | $otherUsers = (bool)$dbw->newSelectQueryBuilder() |
| 632 | ->select( '1' ) |
| 633 | ->from( 'revision' ) |
| 634 | ->tables( $revWhere['tables'] ) |
| 635 | ->where( [ |
| 636 | 'rev_page' => $revRecord->getPageId(), |
| 637 | $dbw->expr( 'rev_id', '>', intval( $baseRevId ) ), |
| 638 | 'NOT( ' . $revWhere['conds'] . ' )' |
| 639 | ] ) |
| 640 | ->joinConds( $revWhere['joins'] ) |
| 641 | ->caller( __METHOD__ ) |
| 642 | ->fetchField(); |
| 643 | if ( $otherUsers ) { |
| 644 | return false; // only looking for self-reverts |
| 645 | } |
| 646 | # Confirm the text because we can't trust this user. |
| 647 | return ( $revRecord->getSha1() === $srev->getRevisionRecord()->getSha1() ); |
| 648 | } |
| 649 | |
| 650 | /** |
| 651 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete |
| 652 | * |
| 653 | * When an user makes a null-edit we sometimes want to review it... |
| 654 | * (a) Null undo or rollback |
| 655 | * (b) Null edit with review box checked |
| 656 | * Note: called after edit ops are finished |
| 657 | * |
| 658 | * @param WikiPage $wikiPage |
| 659 | * @param UserIdentity $userIdentity |
| 660 | * @param string $summary |
| 661 | * @param int $flags |
| 662 | * @param RevisionRecord $revisionRecord |
| 663 | * @param EditResult $editResult |
| 664 | */ |
| 665 | public static function maybeNullEditReview( |
| 666 | WikiPage $wikiPage, |
| 667 | UserIdentity $userIdentity, |
| 668 | string $summary, |
| 669 | int $flags, |
| 670 | RevisionRecord $revisionRecord, |
| 671 | EditResult $editResult |
| 672 | ) { |
| 673 | if ( !$editResult->isNullEdit() ) { |
| 674 | // Not a null edit |
| 675 | return; |
| 676 | } |
| 677 | $request = RequestContext::getMain()->getRequest(); |
| 678 | |
| 679 | $baseId = $editResult->getOriginalRevisionId(); |
| 680 | |
| 681 | # Rollback/undo or box checked |
| 682 | $reviewEdit = $request->getCheck( 'wpReviewEdit' ); |
| 683 | if ( !$baseId && !$reviewEdit ) { |
| 684 | // Short-circuit |
| 685 | return; |
| 686 | } |
| 687 | |
| 688 | $title = $wikiPage->getTitle(); // convenience |
| 689 | $fa = FlaggableWikiPage::getTitleInstance( $title ); |
| 690 | $fa->loadPageData( IDBAccessObject::READ_LATEST ); |
| 691 | if ( !$fa->isReviewable() ) { |
| 692 | // Page is not reviewable |
| 693 | return; |
| 694 | } |
| 695 | # Get the current revision ID |
| 696 | $revLookup = MediaWikiServices::getInstance()->getRevisionLookup(); |
| 697 | $revRecord = $revLookup->getRevisionByTitle( $title, 0, IDBAccessObject::READ_LATEST ); |
| 698 | if ( !$revRecord ) { |
| 699 | return; |
| 700 | } |
| 701 | |
| 702 | $flags = null; |
| 703 | $user = User::newFromIdentity( $userIdentity ); |
| 704 | # Is this a rollback/undo that didn't change anything? |
| 705 | if ( $baseId > 0 ) { |
| 706 | $frev = FlaggedRevision::newFromTitle( $title, $baseId ); // base rev of null edit |
| 707 | $pRevRecord = $revLookup->getRevisionById( $revRecord->getParentId() ); // current rev parent |
| 708 | $revIsNull = ( $pRevRecord && $pRevRecord->hasSameContent( $revRecord ) ); |
| 709 | # Was the edit that we tried to revert to reviewed? |
| 710 | # We avoid auto-reviewing null edits to avoid confusion (bug 28476). |
| 711 | if ( $frev && !$revIsNull ) { |
| 712 | # Review this revision of the page... |
| 713 | $ok = FlaggedRevs::autoReviewEdit( $wikiPage, $user, $revRecord, $flags ); |
| 714 | if ( $ok ) { |
| 715 | FlaggedRevs::markRevisionPatrolled( $revRecord ); // reviewed -> patrolled |
| 716 | return; |
| 717 | } |
| 718 | } |
| 719 | } |
| 720 | # Get edit timestamp, it must exist. |
| 721 | $editTimestamp = $request->getVal( 'wpEdittime' ); |
| 722 | # Is the page checked off to be reviewed? |
| 723 | if ( $editTimestamp && $reviewEdit && MediaWikiServices::getInstance() |
| 724 | ->getPermissionManager()->userCan( 'review', $user, $title ) |
| 725 | ) { |
| 726 | # Check wpEdittime against current revision's time. |
| 727 | # If an edit was auto-merged in between, review only up to what |
| 728 | # was the current rev when this user started editing the page. |
| 729 | if ( $revRecord->getTimestamp() != $editTimestamp ) { |
| 730 | $revRecord = $revLookup->getRevisionByTimestamp( |
| 731 | $title, |
| 732 | $editTimestamp, |
| 733 | IDBAccessObject::READ_LATEST |
| 734 | ); |
| 735 | if ( !$revRecord ) { |
| 736 | // Deleted? |
| 737 | return; |
| 738 | } |
| 739 | } |
| 740 | # Review this revision of the page... |
| 741 | $ok = FlaggedRevs::autoReviewEdit( $wikiPage, $user, $revRecord, $flags, false /* manual */ ); |
| 742 | if ( $ok ) { |
| 743 | FlaggedRevs::markRevisionPatrolled( $revRecord ); // reviewed -> patrolled |
| 744 | } |
| 745 | } |
| 746 | } |
| 747 | |
| 748 | /** |
| 749 | * @inheritDoc |
| 750 | * Mark auto-reviewed edits as patrolled |
| 751 | */ |
| 752 | public function onRecentChange_save( $rc ) { |
| 753 | if ( !$rc->getAttribute( 'rc_this_oldid' ) ) { |
| 754 | return; |
| 755 | } |
| 756 | // don't autopatrol autoreviewed edits when using pending changes, |
| 757 | // otherwise edits by autoreviewed users on pending changes protected pages would be |
| 758 | // autopatrolled and could not be checked through RC patrol as on regular pages |
| 759 | if ( FlaggedRevs::useOnlyIfProtected() ) { |
| 760 | return; |
| 761 | } |
| 762 | $fa = FlaggableWikiPage::getTitleInstance( $rc->getTitle() ); |
| 763 | $fa->loadPageData( IDBAccessObject::READ_LATEST ); |
| 764 | // Is the page reviewable? |
| 765 | if ( $fa->isReviewable() ) { |
| 766 | $revId = $rc->getAttribute( 'rc_this_oldid' ); |
| 767 | // If the edit we just made was reviewed, then it's the stable rev |
| 768 | $frev = FlaggedRevision::newFromTitle( $rc->getTitle(), $revId, IDBAccessObject::READ_LATEST ); |
| 769 | // Reviewed => patrolled |
| 770 | if ( $frev ) { |
| 771 | DeferredUpdates::addCallableUpdate( static function () use ( $rc, $frev ) { |
| 772 | RevisionReviewForm::updateRecentChanges( $rc, 'patrol', $frev ); |
| 773 | } ); |
| 774 | $rcAttribs = $rc->getAttributes(); |
| 775 | $rcAttribs['rc_patrolled'] = 1; // make sure irc/email notifs know status |
| 776 | $rc->setAttribs( $rcAttribs ); |
| 777 | } |
| 778 | } |
| 779 | } |
| 780 | |
| 781 | private function maybeIncrementReverts( |
| 782 | WikiPage $wikiPage, RevisionRecord $revRecord, EditResult $editResult, UserIdentity $user |
| 783 | ) { |
| 784 | $undid = $editResult->getOldestRevertedRevisionId(); |
| 785 | |
| 786 | # Was this an edit by an auto-sighter that undid another edit? |
| 787 | if ( !( $undid && $this->permissionManager->userHasRight( $user, 'autoreview' ) ) ) { |
| 788 | return; |
| 789 | } |
| 790 | |
| 791 | // Note: $rev->getTitle() might be undefined (no rev id?) |
| 792 | $badRevRecord = $this->revisionLookup->getRevisionByTitle( $wikiPage->getTitle(), $undid ); |
| 793 | if ( !( $badRevRecord && $badRevRecord->getUser( RevisionRecord::RAW ) ) ) { |
| 794 | return; |
| 795 | } |
| 796 | |
| 797 | $revRecordUser = $revRecord->getUser( RevisionRecord::RAW ); |
| 798 | $badRevRecordUser = $badRevRecord->getUser( RevisionRecord::RAW ); |
| 799 | if ( $this->userIdentityUtils->isNamed( $badRevRecordUser ) |
| 800 | && !$badRevRecordUser->equals( $revRecordUser ) // no self-reverts |
| 801 | ) { |
| 802 | FRUserCounters::incCount( $badRevRecordUser->getId(), 'revertedEdits' ); |
| 803 | } |
| 804 | } |
| 805 | |
| 806 | /** |
| 807 | * Get query data for making efficient queries based on rev_user and |
| 808 | * rev_timestamp in an actor table world. |
| 809 | * @param IReadableDatabase $dbr |
| 810 | * @param UserIdentity $user |
| 811 | * @return array[] |
| 812 | */ |
| 813 | private static function getQueryData( IReadableDatabase $dbr, $user ) { |
| 814 | $revWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $user ); |
| 815 | $queryData = []; |
| 816 | foreach ( $revWhere['orconds'] as $key => $cond ) { |
| 817 | if ( $key === 'actor' ) { |
| 818 | $data = [ |
| 819 | 'tables' => [ 'revision' ] + $revWhere['tables'], |
| 820 | 'tsField' => 'revactor_timestamp', |
| 821 | 'cond' => $cond, |
| 822 | 'joins' => $revWhere['joins'], |
| 823 | 'useIndex' => [ 'temp_rev_user' => 'actor_timestamp' ], |
| 824 | ]; |
| 825 | $data['joins']['temp_rev_user'][0] = 'JOIN'; |
| 826 | } elseif ( $key === 'username' ) { |
| 827 | // Ignore this, shouldn't happen |
| 828 | continue; |
| 829 | } else { // future migration from revision_actor_temp to rev_actor |
| 830 | $data = [ |
| 831 | 'tables' => [ 'revision' ], |
| 832 | 'tsField' => 'rev_timestamp', |
| 833 | 'cond' => $cond, |
| 834 | 'joins' => [], |
| 835 | 'useIndex' => [ 'revision' => 'rev_actor_timestamp' ], |
| 836 | ]; |
| 837 | } |
| 838 | $queryData[] = $data; |
| 839 | } |
| 840 | return $queryData; |
| 841 | } |
| 842 | |
| 843 | /** |
| 844 | * Check if a user meets the edit spacing requirements. |
| 845 | * If the user does not, return a *lower bound* number of seconds |
| 846 | * that must elapse for it to be possible for the user to meet them. |
| 847 | * @param UserIdentity $user |
| 848 | * @param int $spacingReq days apart (of edit points) |
| 849 | * @param int $pointsReq number of edit points |
| 850 | * @return true|int True if passed, int seconds on failure |
| 851 | */ |
| 852 | private static function editSpacingCheck( UserIdentity $user, $spacingReq, $pointsReq ) { |
| 853 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
| 854 | |
| 855 | $queryData = self::getQueryData( $dbr, $user ); |
| 856 | |
| 857 | $benchmarks = 0; // actual edit points |
| 858 | # Convert days to seconds... |
| 859 | $spacingReq = $spacingReq * 24 * 3600; |
| 860 | # Check the oldest edit |
| 861 | $lower = false; |
| 862 | foreach ( $queryData as $data ) { |
| 863 | $ts = $dbr->newSelectQueryBuilder() |
| 864 | ->tables( $data['tables'] ) |
| 865 | ->field( $data['tsField'] ) |
| 866 | ->where( $data['cond'] ) |
| 867 | ->orderby( $data['tsField'] ) |
| 868 | ->useIndex( $data['useIndex'] ) |
| 869 | ->joinConds( $data['joins'] ) |
| 870 | ->caller( __METHOD__ ) |
| 871 | ->fetchField(); |
| 872 | $lower = $lower && $ts ? min( $lower, $ts ) : ( $lower ?: $ts ); |
| 873 | } |
| 874 | # Recursively check for an edit $spacingReq seconds later, until we are done. |
| 875 | if ( $lower ) { |
| 876 | $benchmarks++; // the first edit above counts |
| 877 | while ( $lower && $benchmarks < $pointsReq ) { |
| 878 | $next = (int)wfTimestamp( TS_UNIX, $lower ) + $spacingReq; |
| 879 | $lower = false; |
| 880 | foreach ( $queryData as $data ) { |
| 881 | $ts = $dbr->newSelectQueryBuilder() |
| 882 | ->tables( $data['tables'] ) |
| 883 | ->field( $data['tsField'] ) |
| 884 | ->where( $data['cond'] ) |
| 885 | ->andWhere( $dbr->expr( $data['tsField'], '>', $dbr->timestamp( $next ) ) ) |
| 886 | ->orderBy( $data['tsField'] ) |
| 887 | ->useIndex( $data['useIndex'] ) |
| 888 | ->joinConds( $data['joins'] ) |
| 889 | ->caller( __METHOD__ ) |
| 890 | ->fetchField(); |
| 891 | $lower = $lower && $ts ? min( $lower, $ts ) : ( $lower ?: $ts ); |
| 892 | } |
| 893 | if ( $lower !== false ) { |
| 894 | $benchmarks++; |
| 895 | } |
| 896 | } |
| 897 | } |
| 898 | if ( $benchmarks >= $pointsReq ) { |
| 899 | return true; |
| 900 | } else { |
| 901 | // Does not add time for the last required edit point; it could be a |
| 902 | // fraction of $spacingReq depending on the last actual edit point time. |
| 903 | return ( $spacingReq * ( $pointsReq - $benchmarks - 1 ) ); |
| 904 | } |
| 905 | } |
| 906 | |
| 907 | /** |
| 908 | * Check if a user has enough implicitly reviewed edits (before stable version) |
| 909 | * @param User $user |
| 910 | * @param int $editsReq |
| 911 | * @param int $seconds |
| 912 | * @return bool |
| 913 | */ |
| 914 | private static function reviewedEditsCheck( User $user, $editsReq, $seconds = 0 ) { |
| 915 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
| 916 | |
| 917 | $queryData = self::getQueryData( $dbr, $user ); |
| 918 | // Get cutoff timestamp (excludes edits that are too recent) |
| 919 | foreach ( $queryData as $k => $data ) { |
| 920 | $queryData[$k]['conds'] = [ |
| 921 | $data['cond'], |
| 922 | $dbr->expr( $data['tsField'], '<', $dbr->timestamp( time() - $seconds ) ), |
| 923 | ]; |
| 924 | } |
| 925 | // Get the lower cutoff to avoid scanning over many rows. |
| 926 | // Users with many revisions will only have the last 10k inspected. |
| 927 | $lowCutoff = false; |
| 928 | if ( $user->getEditCount() > 10000 ) { |
| 929 | foreach ( $queryData as $data ) { |
| 930 | $lowCutoff = max( $lowCutoff, $dbr->newSelectQueryBuilder() |
| 931 | ->tables( $data['tables'] ) |
| 932 | ->field( $data['tsField'] ) |
| 933 | ->where( $data['conds'] ) |
| 934 | ->orderBy( $data['tsField'], SelectQueryBuilder::SORT_DESC ) |
| 935 | ->offset( 9999 ) |
| 936 | ->joinConds( $data['joins'] ) |
| 937 | ->caller( __METHOD__ ) |
| 938 | ->fetchField() |
| 939 | ); |
| 940 | } |
| 941 | } |
| 942 | $lowCutoff = $lowCutoff ?: 1; // default to UNIX 1970 |
| 943 | // Get revs from pages that have a reviewed rev of equal or higher timestamp |
| 944 | $ct = 0; |
| 945 | foreach ( $queryData as $data ) { |
| 946 | if ( $ct >= $editsReq ) { |
| 947 | break; |
| 948 | } |
| 949 | $res = $dbr->newSelectQueryBuilder() |
| 950 | ->select( '1' ) |
| 951 | ->tables( $data['tables'] ) |
| 952 | ->join( 'flaggedpages', null, 'fp_page_id = rev_page' ) |
| 953 | ->where( $data['conds'] ) |
| 954 | ->andWhere( [ |
| 955 | // bug 15515 |
| 956 | $dbr->expr( 'fp_pending_since', '=', null ) |
| 957 | ->or( 'fp_pending_since', '>', new RawSQLValue( $data['tsField'] ) ), |
| 958 | // Avoid too much scanning |
| 959 | $dbr->expr( $data['tsField'], '>', $dbr->timestamp( $lowCutoff ) ) |
| 960 | ] ) |
| 961 | ->limit( $editsReq - $ct ) |
| 962 | ->joinConds( $data['joins'] ) |
| 963 | ->caller( __METHOD__ ) |
| 964 | ->fetchResultSet(); |
| 965 | $ct += $res->numRows(); |
| 966 | } |
| 967 | return ( $ct >= $editsReq ); |
| 968 | } |
| 969 | |
| 970 | /** |
| 971 | * Checks if $user was previously blocked since $cutoff_unixtime |
| 972 | * @param User $user |
| 973 | * @param IReadableDatabase $db |
| 974 | * @param int $cutoff_unixtime = 0 |
| 975 | * @return bool |
| 976 | */ |
| 977 | private static function wasPreviouslyBlocked( |
| 978 | User $user, |
| 979 | IReadableDatabase $db, |
| 980 | $cutoff_unixtime = 0 |
| 981 | ) { |
| 982 | $conds = [ |
| 983 | 'log_namespace' => NS_USER, |
| 984 | 'log_title' => $user->getUserPage()->getDBkey(), |
| 985 | 'log_type' => 'block', |
| 986 | 'log_action' => 'block' |
| 987 | ]; |
| 988 | if ( $cutoff_unixtime > 0 ) { |
| 989 | # Hint to improve NS,title,timestamp INDEX use |
| 990 | $conds[] = $db->expr( 'log_timestamp', '>=', $db->timestamp( $cutoff_unixtime ) ); |
| 991 | } |
| 992 | return (bool)$db->newSelectQueryBuilder() |
| 993 | ->select( '1' ) |
| 994 | ->from( 'logging' ) |
| 995 | ->where( $conds ) |
| 996 | ->caller( __METHOD__ ) |
| 997 | ->fetchField(); |
| 998 | } |
| 999 | |
| 1000 | /** |
| 1001 | * @param int $userId |
| 1002 | * @param int $seconds |
| 1003 | * @param int $limit |
| 1004 | * @return int |
| 1005 | */ |
| 1006 | private static function recentEditCount( $userId, $seconds, $limit ) { |
| 1007 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
| 1008 | |
| 1009 | $queryData = self::getQueryData( $dbr, User::newFromId( $userId ) ); |
| 1010 | # Get cutoff timestamp (edits that are too recent) |
| 1011 | $cutoff = $dbr->timestamp( time() - $seconds ); |
| 1012 | # Check all recent edits... |
| 1013 | $ct = 0; |
| 1014 | foreach ( $queryData as $data ) { |
| 1015 | if ( $ct > $limit ) { |
| 1016 | break; |
| 1017 | } |
| 1018 | $res = $dbr->newSelectQueryBuilder() |
| 1019 | ->select( '1' ) |
| 1020 | ->tables( $data['tables'] ) |
| 1021 | ->where( $data['cond'] ) |
| 1022 | ->andWhere( $dbr->expr( $data['tsField'], '>', $cutoff ) ) |
| 1023 | ->limit( $limit + 1 - $ct ) |
| 1024 | ->joinConds( $data['joins'] ) |
| 1025 | ->caller( __METHOD__ ) |
| 1026 | ->fetchResultSet(); |
| 1027 | $ct += $res->numRows(); |
| 1028 | } |
| 1029 | return $ct; |
| 1030 | } |
| 1031 | |
| 1032 | /** |
| 1033 | * @param int $userId |
| 1034 | * @param int $seconds |
| 1035 | * @param int $limit |
| 1036 | * @return int |
| 1037 | */ |
| 1038 | private static function recentContentEditCount( $userId, $seconds, $limit ) { |
| 1039 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
| 1040 | |
| 1041 | $queryData = self::getQueryData( $dbr, User::newFromId( $userId ) ); |
| 1042 | # Get cutoff timestamp (edits that are too recent) |
| 1043 | $cutoff = $dbr->timestamp( time() - $seconds ); |
| 1044 | # Check all recent content edits... |
| 1045 | $ct = 0; |
| 1046 | $contentNamespaces = MediaWikiServices::getInstance() |
| 1047 | ->getNamespaceInfo() |
| 1048 | ->getContentNamespaces(); |
| 1049 | foreach ( $queryData as $data ) { |
| 1050 | if ( $ct > $limit ) { |
| 1051 | break; |
| 1052 | } |
| 1053 | $res = $dbr->newSelectQueryBuilder() |
| 1054 | ->select( '1' ) |
| 1055 | ->tables( $data['tables'] ) |
| 1056 | ->join( 'page', null, 'rev_page = page_id' ) |
| 1057 | ->where( [ |
| 1058 | $data['cond'], |
| 1059 | $dbr->expr( $data['tsField'], '>', $cutoff ), |
| 1060 | 'page_namespace' => $contentNamespaces |
| 1061 | ] ) |
| 1062 | ->limit( $limit + 1 - $ct ) |
| 1063 | ->useIndex( $data['useIndex'] ) |
| 1064 | ->joinConds( $data['joins'] ) |
| 1065 | ->caller( __METHOD__ ) |
| 1066 | ->fetchResultSet(); |
| 1067 | $ct += $res->numRows(); |
| 1068 | } |
| 1069 | return $ct; |
| 1070 | } |
| 1071 | |
| 1072 | /** |
| 1073 | * @inheritDoc |
| 1074 | * Grant 'autoreview' rights to users with the 'bot' right |
| 1075 | */ |
| 1076 | public function onUserGetRights( $user, &$rights ) { |
| 1077 | # Make sure bots always have the 'autoreview' right |
| 1078 | if ( in_array( 'bot', $rights ) && !in_array( 'autoreview', $rights ) ) { |
| 1079 | $rights[] = 'autoreview'; |
| 1080 | } |
| 1081 | } |
| 1082 | |
| 1083 | /** |
| 1084 | * @inheritDoc |
| 1085 | * Mark the edit as autoreviewed if needed. |
| 1086 | * This must happen in this hook, and not in onPageSaveComplete(), for two reasons: |
| 1087 | * - onBeforeRevertedTagUpdate() implementation relies on it happening first (T361918) |
| 1088 | * - It must also be done for null revisions created during some actions (T361940, T361960) |
| 1089 | */ |
| 1090 | public function onRevisionFromEditComplete( |
| 1091 | $wikiPage, $revRecord, $baseRevId, $user, &$tags |
| 1092 | ) { |
| 1093 | self::maybeMakeEditReviewed( $wikiPage, $revRecord, $baseRevId, $user ); |
| 1094 | } |
| 1095 | |
| 1096 | /** |
| 1097 | * @inheritDoc |
| 1098 | * Callback that autopromotes user according to the setting in |
| 1099 | * $wgFlaggedRevsAutopromote. This also handles user stats tallies. |
| 1100 | */ |
| 1101 | public function onPageSaveComplete( |
| 1102 | $wikiPage, |
| 1103 | $userIdentity, |
| 1104 | $summary, |
| 1105 | $flags, |
| 1106 | $revisionRecord, |
| 1107 | $editResult |
| 1108 | ) { |
| 1109 | self::maybeIncrementReverts( $wikiPage, $revisionRecord, $editResult, $userIdentity ); |
| 1110 | |
| 1111 | self::maybeNullEditReview( $wikiPage, $userIdentity, $summary, $flags, $revisionRecord, $editResult ); |
| 1112 | |
| 1113 | # Ignore null edits, edits by IP users or temporary users, and MW role account edits |
| 1114 | if ( $editResult->isNullEdit() || |
| 1115 | !$this->userIdentityUtils->isNamed( $userIdentity ) || |
| 1116 | !$this->userNameUtils->isUsable( $userIdentity->getName() ) |
| 1117 | ) { |
| 1118 | return; |
| 1119 | } |
| 1120 | |
| 1121 | # No sense in running counters if nothing uses them |
| 1122 | if ( !( $this->config->get( 'FlaggedRevsAutopromote' ) || $this->config->get( 'FlaggedRevsAutoconfirm' ) ) ) { |
| 1123 | return; |
| 1124 | } |
| 1125 | |
| 1126 | $userId = $userIdentity->getId(); |
| 1127 | DeferredUpdates::addCallableUpdate( static function () use ( $userId, $wikiPage, $summary ) { |
| 1128 | $p = FRUserCounters::getUserParams( $userId, IDBAccessObject::READ_EXCLUSIVE ); |
| 1129 | $changed = FRUserCounters::updateUserParams( $p, $wikiPage->getTitle(), $summary ); |
| 1130 | if ( $changed ) { |
| 1131 | FRUserCounters::saveUserParams( $userId, $p ); // save any updates |
| 1132 | } |
| 1133 | } ); |
| 1134 | } |
| 1135 | |
| 1136 | /** |
| 1137 | * @inheritDoc |
| 1138 | * Check a user requirements condition that is defined by FlaggedRevs |
| 1139 | * |
| 1140 | * Note: some unobtrusive caching is used to avoid DB hits. |
| 1141 | */ |
| 1142 | public function onUserRequirementsCondition( |
| 1143 | string|int $cond, |
| 1144 | array $params, |
| 1145 | UserIdentity $user, |
| 1146 | bool $isPerformingRequest, |
| 1147 | ?bool &$result |
| 1148 | ): void { |
| 1149 | if ( |
| 1150 | $user->getWikiId() !== UserIdentity::LOCAL |
| 1151 | && is_int( $cond ) |
| 1152 | && $cond >= APCOND_FR_EDITSUMMARYCOUNT |
| 1153 | && $cond <= APCOND_FR_NEVERDEMOTED |
| 1154 | ) { |
| 1155 | // The code below may be unprepared for checking interwiki users |
| 1156 | // Even if the conditions handlers are updated to evaluate them in the context of a proper wiki, |
| 1157 | // we can't assume all wikis in the cluster have FR enabled (as in Wikimedia, for example) |
| 1158 | $result = false; |
| 1159 | return; |
| 1160 | } |
| 1161 | switch ( $cond ) { |
| 1162 | case APCOND_FR_EDITSUMMARYCOUNT: |
| 1163 | $p = FRUserCounters::getParams( $user ); |
| 1164 | $result = ( $p && $p['editComments'] >= $params[0] ); |
| 1165 | break; |
| 1166 | case APCOND_FR_NEVERBLOCKED: |
| 1167 | $userObject = $this->userFactory->newFromUserIdentity( $user ); |
| 1168 | if ( $userObject->getBlock() ) { |
| 1169 | $result = false; // failed |
| 1170 | } else { |
| 1171 | // See T262970 for an explanation of this |
| 1172 | $hasPriorBlock = $this->cache->getWithSetCallback( |
| 1173 | $this->cache->makeKey( 'flaggedrevs-autopromote-notblocked', $user->getId() ), |
| 1174 | $this->cache::TTL_SECOND, |
| 1175 | function ( $oldValue, &$ttl, array &$setOpts, $oldAsOf ) use ( $userObject ) { |
| 1176 | // Once the user is blocked once, this condition will always |
| 1177 | // fail. To avoid running queries again, if the old cached value |
| 1178 | // is `priorBlock`, just return that immediately. |
| 1179 | if ( $oldValue === 'priorBlock' ) { |
| 1180 | return 'priorBlock'; |
| 1181 | } |
| 1182 | |
| 1183 | // Since the user had no block prior to the last time |
| 1184 | // the value was cached, we only need to check for |
| 1185 | // blocks since then. If there was no prior cached |
| 1186 | // value, check for all time (since time 0). |
| 1187 | // The time of the last check is itself the cached |
| 1188 | // value. |
| 1189 | $startingTimestamp = is_int( $oldValue ) ? $oldValue : 0; |
| 1190 | |
| 1191 | // If the user still hasn't been blocked, we will |
| 1192 | // update the cached value to be the current timestamp |
| 1193 | $newTimestamp = time(); |
| 1194 | |
| 1195 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
| 1196 | |
| 1197 | $setOpts += Database::getCacheSetOptions( $dbr ); |
| 1198 | |
| 1199 | $hasPriorBlock = self::wasPreviouslyBlocked( |
| 1200 | $userObject, |
| 1201 | $dbr, |
| 1202 | $startingTimestamp |
| 1203 | ); |
| 1204 | if ( $hasPriorBlock ) { |
| 1205 | // Store 'priorBlock' so that we can |
| 1206 | // skip everything in the future |
| 1207 | return 'priorBlock'; |
| 1208 | } |
| 1209 | |
| 1210 | // Store the current time, so that future |
| 1211 | // checks don't query everything |
| 1212 | return $newTimestamp; |
| 1213 | }, |
| 1214 | [ 'staleTTL' => $this->cache::TTL_WEEK ] |
| 1215 | ); |
| 1216 | $result = ( $hasPriorBlock !== 'priorBlock' ); |
| 1217 | } |
| 1218 | break; |
| 1219 | case APCOND_FR_UNIQUEPAGECOUNT: |
| 1220 | $p = FRUserCounters::getParams( $user ); |
| 1221 | $result = ( $p && count( $p['uniqueContentPages'] ) >= $params[0] ); |
| 1222 | break; |
| 1223 | case APCOND_FR_EDITSPACING: |
| 1224 | $key = $this->cache->makeKey( |
| 1225 | 'flaggedrevs-autopromote-editspacing', |
| 1226 | $user->getId(), |
| 1227 | $params[0], |
| 1228 | $params[1] |
| 1229 | ); |
| 1230 | $val = $this->cache->get( $key ); |
| 1231 | if ( $val === 'true' ) { |
| 1232 | $result = true; // passed |
| 1233 | } elseif ( $val === 'false' ) { |
| 1234 | $result = false; // failed |
| 1235 | } else { |
| 1236 | # Hit the DB only if the result is not cached... |
| 1237 | $pass = self::editSpacingCheck( $user, $params[0], $params[1] ); |
| 1238 | # Make a key to store the results |
| 1239 | if ( $pass === true ) { |
| 1240 | $this->cache->set( $key, 'true', 2 * $this->cache::TTL_WEEK ); |
| 1241 | } else { |
| 1242 | $this->cache->set( $key, 'false', $pass /* wait time */ ); |
| 1243 | } |
| 1244 | $result = ( $pass === true ); |
| 1245 | } |
| 1246 | break; |
| 1247 | case APCOND_FR_EDITCOUNT: |
| 1248 | # $maxNew is the *most* edits that can be too recent |
| 1249 | $userObject = $this->userFactory->newFromUserIdentity( $user ); |
| 1250 | $maxNew = $userObject->getEditCount() - $params[0]; |
| 1251 | if ( $maxNew < 0 ) { |
| 1252 | $result = false; // doesn't meet count even *with* recent edits |
| 1253 | } elseif ( $params[1] <= 0 ) { |
| 1254 | $result = true; // passed; we aren't excluding any recent edits |
| 1255 | } else { |
| 1256 | # Check all recent edits... |
| 1257 | $n = self::recentEditCount( $user->getId(), $params[1], $maxNew ); |
| 1258 | $result = ( $n <= $maxNew ); |
| 1259 | } |
| 1260 | break; |
| 1261 | case APCOND_FR_CONTENTEDITCOUNT: |
| 1262 | $p = FRUserCounters::getParams( $user ); |
| 1263 | if ( !$p ) { |
| 1264 | $result = false; |
| 1265 | } else { |
| 1266 | # $maxNew is the *most* edits that can be too recent |
| 1267 | $maxNew = $p['totalContentEdits'] - $params[0]; |
| 1268 | if ( $maxNew < 0 ) { |
| 1269 | $result = false; // doesn't meet count even *with* recent edits |
| 1270 | } elseif ( $params[1] <= 0 ) { |
| 1271 | $result = true; // passed; we aren't excluding any recent edits |
| 1272 | } else { |
| 1273 | # Check all recent content edits... |
| 1274 | $n = self::recentContentEditCount( $user->getId(), $params[1], $maxNew ); |
| 1275 | $result = ( $n <= $maxNew ); |
| 1276 | } |
| 1277 | } |
| 1278 | break; |
| 1279 | case APCOND_FR_CHECKEDEDITCOUNT: |
| 1280 | $key = $this->cache->makeKey( |
| 1281 | 'flaggedrevs-autopromote-reviewededits', |
| 1282 | $user->getId(), |
| 1283 | $params[0], |
| 1284 | $params[1] |
| 1285 | ); |
| 1286 | $val = $this->cache->get( $key ); |
| 1287 | if ( $val === 'true' ) { |
| 1288 | $result = true; // passed |
| 1289 | } elseif ( $val === 'false' ) { |
| 1290 | $result = false; // failed |
| 1291 | } else { |
| 1292 | # Hit the DB only if the result is not cached... |
| 1293 | $userObject = $this->userFactory->newFromUserIdentity( $user ); |
| 1294 | $result = self::reviewedEditsCheck( $userObject, $params[0], $params[1] ); |
| 1295 | if ( $result ) { |
| 1296 | $this->cache->set( $key, 'true', $this->cache::TTL_WEEK ); |
| 1297 | } else { |
| 1298 | $this->cache->set( $key, 'false', $this->cache::TTL_HOUR ); // briefly cache |
| 1299 | } |
| 1300 | } |
| 1301 | break; |
| 1302 | case APCOND_FR_USERPAGEBYTES: |
| 1303 | $userObject = $this->userFactory->newFromUserIdentity( $user ); |
| 1304 | $result = ( !$params[0] || $userObject->getUserPage()->getLength() >= $params[0] ); |
| 1305 | break; |
| 1306 | case APCOND_FR_MAXREVERTEDEDITRATIO: |
| 1307 | $userObject = $this->userFactory->newFromUserIdentity( $user ); |
| 1308 | $p = FRUserCounters::getParams( $user ); |
| 1309 | $result = ( $p && $params[0] * $userObject->getEditCount() >= $p['revertedEdits'] ); |
| 1310 | break; |
| 1311 | case APCOND_FR_NEVERDEMOTED: // b/c |
| 1312 | $p = FRUserCounters::getParams( $user ); |
| 1313 | $result = $p !== null && !( $p['demoted'] ?? false ); |
| 1314 | break; |
| 1315 | } |
| 1316 | } |
| 1317 | |
| 1318 | /** |
| 1319 | * @inheritDoc |
| 1320 | * Set session key. |
| 1321 | */ |
| 1322 | public function onUserLoadAfterLoadFromSession( $user ) { |
| 1323 | if ( $user->isRegistered() && $this->permissionManager->userHasRight( $user, 'review' ) |
| 1324 | ) { |
| 1325 | $request = RequestContext::getMain()->getRequest(); |
| 1326 | $key = $request->getSessionData( 'wsFlaggedRevsKey' ); |
| 1327 | if ( $key === null ) { // should catch login |
| 1328 | $key = MWCryptRand::generateHex( 32 ); |
| 1329 | // Temporary secret key attached to this session |
| 1330 | $request->setSessionData( 'wsFlaggedRevsKey', $key ); |
| 1331 | } |
| 1332 | } |
| 1333 | } |
| 1334 | |
| 1335 | /** |
| 1336 | * @inheritDoc |
| 1337 | */ |
| 1338 | public function onWikiExporter__dumpStableQuery( &$tables, &$opts, &$join ) { |
| 1339 | $namespaces = FlaggedRevs::getReviewNamespaces(); |
| 1340 | if ( $namespaces ) { |
| 1341 | $tables[] = 'flaggedpages'; |
| 1342 | $opts['ORDER BY'] = 'fp_page_id ASC'; |
| 1343 | $opts['USE INDEX'] = [ 'flaggedpages' => 'PRIMARY' ]; |
| 1344 | $join['page'] = [ 'INNER JOIN', |
| 1345 | [ 'page_id = fp_page_id', 'page_namespace' => $namespaces ] |
| 1346 | ]; |
| 1347 | $join['revision'] = [ 'INNER JOIN', |
| 1348 | [ 'rev_page = fp_page_id', 'rev_id = fp_stable' ] ]; |
| 1349 | } |
| 1350 | } |
| 1351 | |
| 1352 | /** |
| 1353 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeCreateEchoEvent |
| 1354 | * |
| 1355 | * This should go once we can remove all Echo-specific code for reverts, |
| 1356 | * see: T153570 |
| 1357 | * |
| 1358 | * @inheritDoc |
| 1359 | */ |
| 1360 | public static function onBeforeCreateEchoEvent( |
| 1361 | array &$notifications, |
| 1362 | array &$notificationCategories, |
| 1363 | array &$notificationIcons |
| 1364 | ) { |
| 1365 | // Override default handlers |
| 1366 | // FlaggedRevs uses a different 'extra' property to pass multiple reverted users |
| 1367 | $notifications['reverted'][AttributeManager::ATTR_LOCATORS][] = [ |
| 1368 | [ UserLocator::class, 'locateFromEventExtra' ], |
| 1369 | [ 'reverted-users-ids' ] |
| 1370 | ]; |
| 1371 | } |
| 1372 | |
| 1373 | /** |
| 1374 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/UserMergeAccountFields |
| 1375 | * |
| 1376 | * @param array &$updateFields |
| 1377 | */ |
| 1378 | public static function onUserMergeAccountFields( array &$updateFields ) { |
| 1379 | $updateFields[] = [ 'flaggedrevs', 'fr_user' ]; |
| 1380 | } |
| 1381 | |
| 1382 | /** |
| 1383 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/MergeAccountFromTo |
| 1384 | * |
| 1385 | * @param User $oldUser |
| 1386 | * @param User $newUser |
| 1387 | */ |
| 1388 | public static function onMergeAccountFromTo( User $oldUser, User $newUser ) { |
| 1389 | if ( $newUser->isRegistered() ) { |
| 1390 | FRUserCounters::mergeUserParams( $oldUser, $newUser ); |
| 1391 | } |
| 1392 | } |
| 1393 | |
| 1394 | /** |
| 1395 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/DeleteAccount |
| 1396 | * |
| 1397 | * @param User $oldUser |
| 1398 | */ |
| 1399 | public static function onDeleteAccount( User $oldUser ) { |
| 1400 | FRUserCounters::deleteUserParams( $oldUser ); |
| 1401 | } |
| 1402 | |
| 1403 | /** |
| 1404 | * @inheritDoc |
| 1405 | * As the hook is called after saving the edit (in a deferred update), we have already |
| 1406 | * figured out whether the edit should be autoreviewed or not (see: maybeMakeEditReviewed |
| 1407 | * method). This hook just checks whether the edit is marked as reviewed or not. |
| 1408 | */ |
| 1409 | public function onBeforeRevertedTagUpdate( |
| 1410 | $wikiPage, |
| 1411 | $user, |
| 1412 | $summary, |
| 1413 | $flags, |
| 1414 | $revisionRecord, |
| 1415 | $editResult, |
| 1416 | &$approved |
| 1417 | ): void { |
| 1418 | $title = $wikiPage->getTitle(); |
| 1419 | $fPage = FlaggableWikiPage::getTitleInstance( $title ); |
| 1420 | $fPage->loadPageData( IDBAccessObject::READ_LATEST ); |
| 1421 | if ( !$fPage->isReviewable() ) { |
| 1422 | // The page is not reviewable |
| 1423 | return; |
| 1424 | } |
| 1425 | |
| 1426 | // Check if the revision was approved |
| 1427 | $flaggedRev = FlaggedRevision::newFromTitle( |
| 1428 | $wikiPage->getTitle(), |
| 1429 | $revisionRecord->getId(), |
| 1430 | IDBAccessObject::READ_LATEST |
| 1431 | ); |
| 1432 | // FlaggedRevision object exists if and only if for each of the defined review tags, |
| 1433 | // the edit has at least a "minimum" review level. |
| 1434 | $approved = $flaggedRev !== null; |
| 1435 | } |
| 1436 | |
| 1437 | /** @inheritDoc */ |
| 1438 | public function onUserRequirementsConditionDisplay( |
| 1439 | string|int $type, |
| 1440 | array $args, |
| 1441 | IContextSource $context, |
| 1442 | ?MessageSpecifier &$message |
| 1443 | ): void { |
| 1444 | $msgKey = match ( $type ) { |
| 1445 | APCOND_FR_EDITSUMMARYCOUNT => 'listgrouprights-restrictedgroups-cond-fr-editsummarycount', |
| 1446 | APCOND_FR_NEVERBLOCKED => 'listgrouprights-restrictedgroups-cond-fr-neverblocked', |
| 1447 | APCOND_FR_UNIQUEPAGECOUNT => 'listgrouprights-restrictedgroups-cond-fr-uniquepagecount', |
| 1448 | APCOND_FR_CONTENTEDITCOUNT => 'listgrouprights-restrictedgroups-cond-fr-contenteditcount', |
| 1449 | APCOND_FR_USERPAGEBYTES => 'listgrouprights-restrictedgroups-cond-fr-userpagebytes', |
| 1450 | APCOND_FR_EDITCOUNT => 'listgrouprights-restrictedgroups-cond-fr-editcount', |
| 1451 | APCOND_FR_EDITSPACING => 'listgrouprights-restrictedgroups-cond-fr-editspacing', |
| 1452 | APCOND_FR_CHECKEDEDITCOUNT => 'listgrouprights-restrictedgroups-cond-fr-checkededitcount', |
| 1453 | APCOND_FR_MAXREVERTEDEDITRATIO => 'listgrouprights-restrictedgroups-cond-fr-maxrevertededitratio', |
| 1454 | APCOND_FR_NEVERDEMOTED => 'listgrouprights-restrictedgroups-cond-fr-neverdemoted', |
| 1455 | default => null, |
| 1456 | }; |
| 1457 | |
| 1458 | if ( $msgKey === null ) { |
| 1459 | return; |
| 1460 | } |
| 1461 | |
| 1462 | $message = MessageValue::new( $msgKey ); |
| 1463 | switch ( $type ) { |
| 1464 | case APCOND_FR_CONTENTEDITCOUNT: |
| 1465 | case APCOND_FR_EDITCOUNT: |
| 1466 | case APCOND_FR_CHECKEDEDITCOUNT: |
| 1467 | $message->numParams( $args[0] )->longDurationParams( $args[1] ); |
| 1468 | break; |
| 1469 | case APCOND_FR_EDITSPACING: |
| 1470 | $message->numParams( $args[1] )->longDurationParams( $args[0] * 86400 ); |
| 1471 | break; |
| 1472 | case APCOND_FR_EDITSUMMARYCOUNT: |
| 1473 | case APCOND_FR_UNIQUEPAGECOUNT: |
| 1474 | case APCOND_FR_USERPAGEBYTES: |
| 1475 | $message->numParams( $args[0] ); |
| 1476 | break; |
| 1477 | case APCOND_FR_MAXREVERTEDEDITRATIO: |
| 1478 | $message->numParams( $args[0] * 100 ); |
| 1479 | break; |
| 1480 | default: |
| 1481 | $message->params( ...$args ); |
| 1482 | break; |
| 1483 | } |
| 1484 | } |
| 1485 | } |