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