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 $e ) { |
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 | } |