Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
15.74% |
37 / 235 |
|
37.04% |
10 / 27 |
CRAP | |
0.00% |
0 / 1 |
| FlaggedRevision | |
15.74% |
37 / 235 |
|
37.04% |
10 / 27 |
2361.19 | |
0.00% |
0 / 1 |
| newFromRow | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
| __construct | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
3.18 | |||
| newFromTitle | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
42 | |||
| newFromStable | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
56 | |||
| getStableRevId | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| determineStable | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
| insert | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
20 | |||
| delete | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| getQueryInfo | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
| getRevId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getPage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getTitle | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| getTimestamp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getRevTimestamp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getRevisionRecord | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getRevText | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
4.12 | |||
| getTags | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getTag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| userCanSetTag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getStableTemplateVersions | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 | |||
| findPendingTemplateChanges | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
6 | |||
| approveRevertedTagUpdate | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| revIsFlagged | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
| getDefaultTags | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| getDefaultTag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
| expandRevisionTags | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
| flattenRevisionTags | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| 1 | <?php |
| 2 | |
| 3 | use MediaWiki\Content\TextContent; |
| 4 | use MediaWiki\Logger\LoggerFactory; |
| 5 | use MediaWiki\MediaWikiServices; |
| 6 | use MediaWiki\Revision\RevisionAccessException; |
| 7 | use MediaWiki\Revision\RevisionRecord; |
| 8 | use MediaWiki\Revision\SlotRecord; |
| 9 | use MediaWiki\Title\Title; |
| 10 | use MediaWiki\User\UserIdentity; |
| 11 | use Wikimedia\Rdbms\IDBAccessObject; |
| 12 | use Wikimedia\Rdbms\SelectQueryBuilder; |
| 13 | |
| 14 | /** |
| 15 | * Class representing a stable version of a MediaWiki revision |
| 16 | * |
| 17 | * This contains a page revision |
| 18 | */ |
| 19 | class FlaggedRevision { |
| 20 | |
| 21 | /** @var RevisionRecord base revision */ |
| 22 | private $mRevRecord; |
| 23 | |
| 24 | /* Flagging metadata */ |
| 25 | /** @var mixed review timestamp */ |
| 26 | private $mTimestamp; |
| 27 | /** @var array<string,int> Review tags */ |
| 28 | private array $mTags; |
| 29 | /** @var string[] flags (for auto-review ect...) */ |
| 30 | private $mFlags; |
| 31 | /** @var int reviewing user */ |
| 32 | private $mUser; |
| 33 | |
| 34 | /* Redundant fields for lazy-loading */ |
| 35 | /** @var Title|null */ |
| 36 | private $mTitle; |
| 37 | /** @var array|null stable versions of template version used */ |
| 38 | private $mStableTemplates; |
| 39 | |
| 40 | /** |
| 41 | * @param stdClass $row DB row |
| 42 | * @param Title $title |
| 43 | * @param int $flags One of the IDBAccessObject::READ_… constants |
| 44 | * @return self |
| 45 | */ |
| 46 | private static function newFromRow( stdClass $row, Title $title, $flags ) { |
| 47 | # Base Revision object |
| 48 | $revFactory = MediaWikiServices::getInstance()->getRevisionFactory(); |
| 49 | $revRecord = $revFactory->newRevisionFromRow( $row, $flags, $title ); |
| 50 | $frev = new self( [ |
| 51 | 'timestamp' => $row->fr_timestamp, |
| 52 | 'tags' => $row->fr_tags, |
| 53 | 'flags' => $row->fr_flags, |
| 54 | 'user_id' => $row->fr_user, |
| 55 | 'revrecord' => $revRecord, |
| 56 | ] ); |
| 57 | $frev->mTitle = $title; |
| 58 | return $frev; |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * @param array $row |
| 63 | */ |
| 64 | public function __construct( array $row ) { |
| 65 | if ( !is_array( $row['tags'] ) ) { |
| 66 | $row['tags'] = self::expandRevisionTags( $row['tags'] ); |
| 67 | } |
| 68 | |
| 69 | $this->mTimestamp = $row['timestamp']; |
| 70 | $this->mTags = array_merge( self::getDefaultTags(), $row['tags'] ); |
| 71 | $this->mFlags = explode( ',', $row['flags'] ); |
| 72 | $this->mUser = intval( $row['user_id'] ); |
| 73 | # Base Revision object |
| 74 | $this->mRevRecord = $row['revrecord']; |
| 75 | if ( !( $this->mRevRecord instanceof RevisionRecord ) ) { |
| 76 | throw new InvalidArgumentException( |
| 77 | 'FlaggedRevision constructor passed invalid RevisionRecord object.' |
| 78 | ); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Get a FlaggedRevision for a title and rev ID. |
| 84 | * Note: will return NULL if the revision is deleted. |
| 85 | * @param Title $title |
| 86 | * @param int $revId |
| 87 | * @param int $flags One of the IDBAccessObject::READ_… constants |
| 88 | * @return self|null (null on failure) |
| 89 | */ |
| 90 | public static function newFromTitle( Title $title, $revId, $flags = 0 ) { |
| 91 | if ( !$revId || !FlaggedRevs::inReviewNamespace( $title ) ) { |
| 92 | return null; // short-circuit |
| 93 | } |
| 94 | # User primary/replica as appropriate... |
| 95 | $pageId = $title->getArticleID( $flags ); |
| 96 | if ( !$pageId ) { |
| 97 | return null; // short-circuit query |
| 98 | } |
| 99 | if ( $flags & IDBAccessObject::READ_LATEST ) { |
| 100 | $db = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
| 101 | } else { |
| 102 | $db = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
| 103 | } |
| 104 | # Skip deleted revisions |
| 105 | $frQuery = self::getQueryInfo(); |
| 106 | $row = $db->newSelectQueryBuilder() |
| 107 | ->tables( $frQuery['tables'] ) |
| 108 | ->fields( $frQuery['fields'] ) |
| 109 | ->where( [ |
| 110 | 'fr_page_id' => $pageId, |
| 111 | 'fr_rev_id' => $revId, |
| 112 | $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0' |
| 113 | ] ) |
| 114 | ->joinConds( $frQuery['joins'] ) |
| 115 | ->caller( __METHOD__ ) |
| 116 | ->fetchRow(); |
| 117 | # Sorted from highest to lowest, so just take the first one if any |
| 118 | return $row ? self::newFromRow( $row, $title, $flags ) : null; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Get a FlaggedRevision of the stable version of a title. |
| 123 | * Note: will return NULL if the revision is deleted, though this |
| 124 | * should never happen as fp_stable is updated as revs are deleted. |
| 125 | * @param Title $title page title |
| 126 | * @param int $flags One of the IDBAccessObject::READ_… constants |
| 127 | * @return self|null (null on failure) |
| 128 | */ |
| 129 | public static function newFromStable( Title $title, $flags = 0 ) { |
| 130 | if ( !FlaggedRevs::inReviewNamespace( $title ) ) { |
| 131 | return null; // short-circuit |
| 132 | } |
| 133 | # User primary/replica as appropriate... |
| 134 | $pageId = $title->getArticleID( $flags ); |
| 135 | if ( !$pageId ) { |
| 136 | return null; // short-circuit query |
| 137 | } |
| 138 | if ( $flags & IDBAccessObject::READ_LATEST ) { |
| 139 | $db = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
| 140 | } else { |
| 141 | $db = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
| 142 | } |
| 143 | # Check tracking tables |
| 144 | $frQuery = self::getQueryInfo(); |
| 145 | $row = $db->newSelectQueryBuilder() |
| 146 | ->tables( $frQuery['tables'] ) |
| 147 | ->fields( $frQuery['fields'] ) |
| 148 | ->select( [ 'fr_page_id' ] ) |
| 149 | ->join( 'flaggedpages', null, 'fr_rev_id = fp_stable' ) |
| 150 | ->where( [ |
| 151 | 'fp_page_id' => $pageId, |
| 152 | $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0', // sanity |
| 153 | ] ) |
| 154 | ->joinConds( $frQuery['joins'] ) |
| 155 | ->caller( __METHOD__ ) |
| 156 | ->fetchRow(); |
| 157 | if ( $row ) { |
| 158 | if ( (int)$row->rev_page !== $pageId || (int)$row->fr_page_id !== $pageId ) { |
| 159 | // Warn about inconsistent flaggedpages rows, see T246720 |
| 160 | $logger = LoggerFactory::getInstance( 'FlaggedRevisions' ); |
| 161 | $logger->warning( 'Found revision with mismatching page ID! ', [ |
| 162 | 'fp_page_id' => $pageId, |
| 163 | 'fr_page_id' => $row->fr_page_id, |
| 164 | 'rev_page' => $row->rev_page, |
| 165 | 'rev_id' => $row->rev_id, |
| 166 | 'trace' => wfBacktrace() |
| 167 | ] ); |
| 168 | |
| 169 | // TODO: Can we make this self-healing somehow? We shouldn't return a FlaggedRevision |
| 170 | // here that belongs to a different page. Can we find the correct revision for |
| 171 | // the given page ID in flaggedrevs? Can we rely on fr_page_id, or is that |
| 172 | // going to be wrong as well? |
| 173 | return null; |
| 174 | } |
| 175 | |
| 176 | return self::newFromRow( $row, $title, $flags ); |
| 177 | } |
| 178 | return null; |
| 179 | } |
| 180 | |
| 181 | /** |
| 182 | * Get the ID of the stable version of a title. |
| 183 | * @param Title $title page title |
| 184 | * @return int (0 on failure) |
| 185 | */ |
| 186 | public static function getStableRevId( Title $title ) { |
| 187 | $srev = self::newFromStable( $title ); |
| 188 | return $srev ? $srev->getRevId() : 0; |
| 189 | } |
| 190 | |
| 191 | /** |
| 192 | * Get a FlaggedRevision of the stable version of a title. |
| 193 | * Skips tracking tables to figure out new stable version. |
| 194 | * @param Title $title page title |
| 195 | * @return self|null (null on failure) |
| 196 | */ |
| 197 | public static function determineStable( Title $title ) { |
| 198 | if ( !FlaggedRevs::inReviewNamespace( $title ) ) { |
| 199 | return null; // short-circuit |
| 200 | } |
| 201 | |
| 202 | $db = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
| 203 | $pageId = $title->getArticleID( IDBAccessObject::READ_LATEST ); |
| 204 | if ( !$pageId ) { |
| 205 | return null; // short-circuit query |
| 206 | } |
| 207 | # Get visibility settings to see if page is reviewable... |
| 208 | if ( FlaggedRevs::useOnlyIfProtected() ) { |
| 209 | $config = FRPageConfig::getStabilitySettings( $title, IDBAccessObject::READ_LATEST ); |
| 210 | if ( !$config['override'] ) { |
| 211 | return null; // page is not reviewable; no stable version |
| 212 | } |
| 213 | } |
| 214 | $baseConds = [ |
| 215 | 'fr_page_id' => $pageId, |
| 216 | 'rev_id = fr_rev_id', |
| 217 | 'rev_page = fr_page_id', // sanity |
| 218 | $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0' |
| 219 | ]; |
| 220 | |
| 221 | $frQuery = self::getQueryInfo(); |
| 222 | $row = $db->newSelectQueryBuilder() |
| 223 | ->tables( $frQuery['tables'] ) |
| 224 | ->fields( $frQuery['fields'] ) |
| 225 | ->where( $baseConds ) |
| 226 | ->orderBy( [ 'fr_rev_timestamp', 'fr_rev_id' ], SelectQueryBuilder::SORT_DESC ) |
| 227 | ->joinConds( $frQuery['joins'] ) |
| 228 | ->caller( __METHOD__ ) |
| 229 | ->fetchRow(); |
| 230 | return $row ? self::newFromRow( $row, $title, IDBAccessObject::READ_LATEST ) : null; |
| 231 | } |
| 232 | |
| 233 | /** |
| 234 | * Insert a FlaggedRevision object into the database |
| 235 | * |
| 236 | * @return true|string true on success, error string on failure |
| 237 | */ |
| 238 | public function insert() { |
| 239 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
| 240 | |
| 241 | # Set any flagged revision flags |
| 242 | $this->mFlags = array_merge( $this->mFlags, [ 'dynamic' ] ); // legacy |
| 243 | # Sanity check for partial revisions |
| 244 | if ( !$this->getPage() ) { |
| 245 | return 'no page id'; |
| 246 | } elseif ( !$this->getRevId() ) { |
| 247 | return 'no revision id'; |
| 248 | } |
| 249 | # Our new review entry |
| 250 | $revRow = [ |
| 251 | 'fr_page_id' => $this->getPage(), |
| 252 | 'fr_rev_id' => $this->getRevId(), |
| 253 | 'fr_rev_timestamp' => $dbw->timestamp( $this->getRevTimestamp() ), |
| 254 | 'fr_user' => $this->mUser, |
| 255 | 'fr_timestamp' => $dbw->timestamp( $this->mTimestamp ), |
| 256 | 'fr_quality' => FR_CHECKED, |
| 257 | 'fr_tags' => self::flattenRevisionTags( $this->mTags ), |
| 258 | 'fr_flags' => implode( ',', $this->mFlags ), |
| 259 | ]; |
| 260 | # Update the main flagged revisions table... |
| 261 | $dbw->newInsertQueryBuilder()->insertInto( 'flaggedrevs' ) |
| 262 | ->ignore() |
| 263 | ->row( $revRow ) |
| 264 | ->caller( __METHOD__ ) |
| 265 | ->execute(); |
| 266 | if ( !$dbw->affectedRows() ) { |
| 267 | return 'duplicate review'; |
| 268 | } |
| 269 | return true; |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * Remove a FlaggedRevision object from the database |
| 274 | */ |
| 275 | public function delete() { |
| 276 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
| 277 | |
| 278 | # Delete from flaggedrevs table |
| 279 | $dbw->newDeleteQueryBuilder() |
| 280 | ->deleteFrom( 'flaggedrevs' ) |
| 281 | ->where( [ 'fr_rev_id' => $this->getRevId() ] ) |
| 282 | ->caller( __METHOD__ ) |
| 283 | ->execute(); |
| 284 | } |
| 285 | |
| 286 | /** |
| 287 | * Get query info for FlaggedRevision DB row (flaggedrevs/revision tables) |
| 288 | * @return array |
| 289 | */ |
| 290 | private static function getQueryInfo() { |
| 291 | $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo(); |
| 292 | return [ |
| 293 | 'tables' => array_merge( [ 'flaggedrevs' ], $revQuery['tables'] ), |
| 294 | 'fields' => array_merge( $revQuery['fields'], [ |
| 295 | 'fr_rev_id', 'fr_page_id', 'fr_rev_timestamp', |
| 296 | 'fr_user', 'fr_timestamp', 'fr_tags', 'fr_flags' |
| 297 | ] ), |
| 298 | 'joins' => [ |
| 299 | 'revision' => [ 'JOIN', [ |
| 300 | 'rev_id = fr_rev_id', |
| 301 | 'rev_page = fr_page_id', // sanity |
| 302 | ] ], |
| 303 | ] + $revQuery['joins'], |
| 304 | ]; |
| 305 | } |
| 306 | |
| 307 | /** |
| 308 | * @return int revision record's ID |
| 309 | */ |
| 310 | public function getRevId() { |
| 311 | return $this->mRevRecord->getId(); |
| 312 | } |
| 313 | |
| 314 | /** |
| 315 | * @return int page ID |
| 316 | */ |
| 317 | private function getPage() { |
| 318 | return $this->mRevRecord->getPageId(); |
| 319 | } |
| 320 | |
| 321 | /** |
| 322 | * @return Title |
| 323 | */ |
| 324 | public function getTitle() { |
| 325 | if ( $this->mTitle === null ) { |
| 326 | $linkTarget = $this->mRevRecord->getPageAsLinkTarget(); |
| 327 | $this->mTitle = Title::newFromLinkTarget( $linkTarget ); |
| 328 | } |
| 329 | return $this->mTitle; |
| 330 | } |
| 331 | |
| 332 | /** |
| 333 | * Get timestamp of review |
| 334 | * @return string revision timestamp in MW format |
| 335 | */ |
| 336 | public function getTimestamp() { |
| 337 | return wfTimestamp( TS_MW, $this->mTimestamp ); |
| 338 | } |
| 339 | |
| 340 | /** |
| 341 | * Get timestamp of the corresponding revision |
| 342 | * Note: here for convenience |
| 343 | * @return string revision timestamp in MW format |
| 344 | */ |
| 345 | public function getRevTimestamp() { |
| 346 | return $this->mRevRecord->getTimestamp(); |
| 347 | } |
| 348 | |
| 349 | /** |
| 350 | * Get the corresponding revision record |
| 351 | * @return RevisionRecord |
| 352 | */ |
| 353 | public function getRevisionRecord() { |
| 354 | return $this->mRevRecord; |
| 355 | } |
| 356 | |
| 357 | /** |
| 358 | * Get text of the corresponding revision |
| 359 | * Note: here for convenience |
| 360 | * @return string|null Revision text, if available |
| 361 | */ |
| 362 | public function getRevText() { |
| 363 | try { |
| 364 | $content = $this->mRevRecord->getContent( SlotRecord::MAIN ); |
| 365 | } catch ( RevisionAccessException ) { |
| 366 | return ''; |
| 367 | } |
| 368 | return ( $content instanceof TextContent ) ? $content->getText() : null; |
| 369 | } |
| 370 | |
| 371 | /** |
| 372 | * Get tags (levels) of all tiers this revision has. |
| 373 | * Use getTag() instead unless you really need other tiers set on |
| 374 | * historical revisions (these tiers are no longer supported, cannot |
| 375 | * be set by users anymore). |
| 376 | * @return array<string,int> tag metadata |
| 377 | */ |
| 378 | public function getTags(): array { |
| 379 | return $this->mTags; |
| 380 | } |
| 381 | |
| 382 | /** |
| 383 | * Get the tag (level) of the page in the default tier. |
| 384 | * This is always defined (possibly zero) unless in protection mode. |
| 385 | */ |
| 386 | public function getTag(): ?int { |
| 387 | return $this->mTags[FlaggedRevs::getTagName()] ?? null; |
| 388 | } |
| 389 | |
| 390 | /** |
| 391 | * Whether the given user can set the tag in the default tier. |
| 392 | * Always returns true in protection mode if the user has review right. |
| 393 | */ |
| 394 | public function userCanSetTag( UserIdentity $user ): bool { |
| 395 | return FlaggedRevs::userCanSetTag( $user, $this->getTag() ); |
| 396 | } |
| 397 | |
| 398 | /** |
| 399 | * Get the current stable version of the templates |
| 400 | * @return int[][] template versions (ns -> dbKey -> rev Id) |
| 401 | * Note: 0 used for template rev Id if it doesn't exist |
| 402 | */ |
| 403 | public function getStableTemplateVersions() { |
| 404 | if ( $this->mStableTemplates == null ) { |
| 405 | $this->mStableTemplates = []; |
| 406 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
| 407 | |
| 408 | $linksMigration = MediaWikiServices::getInstance()->getLinksMigration(); |
| 409 | [ $nsField, $titleField ] = $linksMigration->getTitleFields( 'templatelinks' ); |
| 410 | $queryInfo = $linksMigration->getQueryInfo( 'templatelinks' ); |
| 411 | $res = $dbr->newSelectQueryBuilder() |
| 412 | ->select( [ 'page_namespace', 'page_title', 'fp_stable' ] ) |
| 413 | ->tables( $queryInfo['tables'] ) |
| 414 | ->leftJoin( 'page', null, [ "page_namespace = $nsField", "page_title = $titleField" ] ) |
| 415 | ->leftJoin( 'flaggedpages', null, 'fp_page_id = page_id' ) |
| 416 | ->where( [ |
| 417 | 'tl_from' => $this->getPage(), |
| 418 | # Only get templates with stable or "review time" versions. |
| 419 | $dbr->expr( 'fp_stable', '!=', null ), |
| 420 | ] ) // current version templates |
| 421 | ->joinConds( $queryInfo['joins'] ) |
| 422 | ->caller( __METHOD__ ) |
| 423 | ->fetchResultSet(); |
| 424 | foreach ( $res as $row ) { |
| 425 | $revId = (int)$row->fp_stable; // 0 => none |
| 426 | $this->mStableTemplates[$row->page_namespace][$row->page_title] = $revId; |
| 427 | } |
| 428 | } |
| 429 | return $this->mStableTemplates; |
| 430 | } |
| 431 | |
| 432 | /** |
| 433 | * Fetch pending template changes for this reviewed page version. |
| 434 | * For each template, the "version used" (for stable parsing) is: |
| 435 | * (a) (the latest rev) if FR_INCLUDES_CURRENT. Might be non-existing. |
| 436 | * (b) newest( stable rev, rev at time of review ) if FR_INCLUDES_STABLE |
| 437 | * |
| 438 | * @return bool |
| 439 | */ |
| 440 | public function findPendingTemplateChanges() { |
| 441 | if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_CURRENT ) { |
| 442 | return false; // short-circuit |
| 443 | } |
| 444 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
| 445 | |
| 446 | $linksMigration = MediaWikiServices::getInstance()->getLinksMigration(); |
| 447 | [ $nsField, $titleField ] = $linksMigration->getTitleFields( 'templatelinks' ); |
| 448 | $queryInfo = $linksMigration->getQueryInfo( 'templatelinks' ); |
| 449 | $ret = $dbr->newSelectQueryBuilder() |
| 450 | ->select( [ $nsField, $titleField ] ) |
| 451 | ->tables( $queryInfo['tables'] ) |
| 452 | ->leftJoin( 'page', null, [ "page_namespace = $nsField", "page_title = $titleField" ] ) |
| 453 | ->join( 'flaggedpages', null, 'fp_page_id = page_id' ) |
| 454 | ->where( [ |
| 455 | 'tl_from' => $this->getPage(), |
| 456 | # Only get templates with stable or "review time" versions. |
| 457 | $dbr->expr( 'fp_pending_since', '!=', null )->or( 'fp_stable', '=', null ), |
| 458 | ] ) // current version templates |
| 459 | ->joinConds( $queryInfo['joins'] ) |
| 460 | ->caller( __METHOD__ ) |
| 461 | ->fetchResultSet(); |
| 462 | return (bool)$ret->count(); |
| 463 | } |
| 464 | |
| 465 | /** |
| 466 | * Notify the reverted tag subsystem that the edit was reviewed. |
| 467 | */ |
| 468 | public function approveRevertedTagUpdate() { |
| 469 | $rtuManager = MediaWikiServices::getInstance()->getRevertedTagUpdateManager(); |
| 470 | $rtuManager->approveRevertedTagForRevision( $this->getRevId() ); |
| 471 | } |
| 472 | |
| 473 | /** |
| 474 | * @param int $rev_id |
| 475 | * @param int $flags One of the IDBAccessObject::READ_… constants |
| 476 | * @return bool |
| 477 | */ |
| 478 | public static function revIsFlagged( int $rev_id, int $flags = 0 ): bool { |
| 479 | if ( $flags & IDBAccessObject::READ_LATEST ) { |
| 480 | $db = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
| 481 | } else { |
| 482 | $db = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
| 483 | } |
| 484 | return (bool)$db->newSelectQueryBuilder() |
| 485 | ->select( '1' ) |
| 486 | ->from( 'flaggedrevs' ) |
| 487 | ->where( [ 'fr_rev_id' => $rev_id ] ) |
| 488 | ->caller( __METHOD__ ) |
| 489 | ->fetchField(); |
| 490 | } |
| 491 | |
| 492 | /** |
| 493 | * @return array<string,int> |
| 494 | */ |
| 495 | public static function getDefaultTags(): array { |
| 496 | return FlaggedRevs::useOnlyIfProtected() ? [] : [ FlaggedRevs::getTagName() => 0 ]; |
| 497 | } |
| 498 | |
| 499 | public static function getDefaultTag(): ?int { |
| 500 | return FlaggedRevs::useOnlyIfProtected() ? null : 0; |
| 501 | } |
| 502 | |
| 503 | /** |
| 504 | * @param string $tags |
| 505 | * @return array<string,int> |
| 506 | */ |
| 507 | public static function expandRevisionTags( string $tags ): array { |
| 508 | $flags = []; |
| 509 | $max = FlaggedRevs::getMaxLevel(); |
| 510 | $tags = str_replace( '\n', "\n", $tags ); // B/C, old broken rows |
| 511 | // Tag string format is <tag:val\ntag:val\n...> |
| 512 | $tags = explode( "\n", $tags ); |
| 513 | foreach ( $tags as $tuple ) { |
| 514 | $set = explode( ':', $tuple, 2 ); |
| 515 | // Skip broken and old serializations that end with \n, which shows up as [ "" ] here |
| 516 | if ( count( $set ) == 2 ) { |
| 517 | [ $tag, $value ] = $set; |
| 518 | $flags[$tag] = min( max( 0, (int)$value ), $max ); |
| 519 | } |
| 520 | } |
| 521 | return $flags; |
| 522 | } |
| 523 | |
| 524 | /** |
| 525 | * @param array<string,int> $tags |
| 526 | * @return string |
| 527 | */ |
| 528 | public static function flattenRevisionTags( array $tags ): string { |
| 529 | $flags = ''; |
| 530 | foreach ( $tags as $tag => $value ) { |
| 531 | if ( $flags ) { |
| 532 | $flags .= "\n"; |
| 533 | } |
| 534 | $flags .= $tag . ':' . (int)$value; |
| 535 | } |
| 536 | return $flags; |
| 537 | } |
| 538 | } |