-
Notifications
You must be signed in to change notification settings - Fork 8
/
FlaggableWikiPage.php
566 lines (537 loc) · 17.1 KB
/
FlaggableWikiPage.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
<?php
/**
* Class representing a MediaWiki article and history
*
* FlaggableWikiPage::getTitleInstance() is preferred over constructor calls
*/
class FlaggableWikiPage extends WikiPage {
/* Process cache variables */
protected $stable = 0;
protected $stableRev = null;
protected $revsArePending = null;
protected $pendingRevCount = null;
protected $pageConfig = null;
protected $syncedInTracking = null;
protected $file = null; // for file pages
/**
* Get a FlaggableWikiPage for a given title
* @param $title Title
* @return FlaggableWikiPage
*/
public static function getTitleInstance( Title $title ) {
// Check if there is already an instance on this title
if ( !isset( $title->flaggedRevsArticle ) ) {
$ctitle = clone $title; // avoid cycles
$title->flaggedRevsArticle = new self( $ctitle );
}
return $title->flaggedRevsArticle;
}
/**
* Transfer the prepared edit cache from a WikiPage object
*
* @param WikiPage $page
* @return void
*/
public function preloadPreparedEdit( WikiPage $page ) {
$this->mPreparedEdit = $page->mPreparedEdit;
}
/**
* Clear object process cache values
* @return void
*/
public function clear() {
$this->stable = 0;
$this->stableRev = null;
$this->revsArePending = null;
$this->pendingRevCount = null;
$this->pageConfig = null;
$this->syncedInTracking = null;
$this->file = null;
parent::clear(); // call super!
}
/**
* Get the current file version (null if this not a File page)
* @return File|null|bool false
*/
public function getFile() {
if ( $this->file === null && $this->mTitle->getNamespace() == NS_FILE ) {
$this->file = wfFindFile( $this->mTitle );
}
return $this->file;
}
/**
* Is the stable version shown by default for this page?
* @return bool
*/
public function isStableShownByDefault() {
if ( !$this->isReviewable() ) {
return false; // no stable versions can exist
}
$config = $this->getStabilitySettings(); // page configuration
return (bool)$config['override'];
}
/**
* Do edits have to be reviewed before being shown by default (going live)?
* @return bool
*/
public function editsRequireReview() {
return (
$this->isReviewable() && // reviewable page
$this->isStableShownByDefault() && // and stable versions override
$this->getStableRev() // and there is a stable version
);
}
/**
* Has data for this page been loaded?
* @return bool
*/
public function isDataLoaded() {
return $this->mDataLoaded;
}
/**
* Are edits to this page currently pending?
* @return bool
*/
public function revsArePending() {
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
return $this->revsArePending;
}
/**
* Get number of revs since the stable revision
* Note: slower than revsArePending()
* @param int $flags FR_MASTER (be sure to use loadFromDB( FR_MASTER ) if set)
* @return int
*/
public function getPendingRevCount( $flags = 0 ) {
global $wgMemc, $wgParserCacheExpireTime;
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
# Pending count deferred even after page data load
if ( $this->pendingRevCount !== null ) {
return $this->pendingRevCount; // use process cache
}
$srev = $this->getStableRev();
if ( !$srev ) {
return 0; // none
}
$count = null;
$sRevId = $srev->getRevId();
# Try the cache...
$key = wfMemcKey( 'flaggedrevs', 'countPending', $this->getId() );
if ( !( $flags & FR_MASTER ) ) {
$tuple = FlaggedRevs::getMemcValue( $wgMemc->get( $key ), $this );
# Items is cached and newer that page_touched...
if ( $tuple !== false ) {
# Confirm that cache value was made against the same stable rev Id.
# This avoids lengthy cache pollution if $sRevId is outdated.
list( $cRevId, $cPending ) = explode( '-', $tuple, 2 );
if ( $cRevId == $sRevId ) {
$count = (int)$cPending;
}
}
}
# Otherwise, fetch result from DB as needed...
if ( is_null( $count ) ) {
$db = ( $flags & FR_MASTER ) ?
wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
$srevTS = $db->timestamp( $srev->getRevTimestamp() );
$count = $db->selectField( 'revision', 'COUNT(*)',
array( 'rev_page' => $this->getId(),
'rev_timestamp > ' . $db->addQuotes( $srevTS ) ), // bug 15515
__METHOD__ );
# Save result to cache...
$data = FlaggedRevs::makeMemcObj( "{$sRevId}-{$count}" );
$wgMemc->set( $key, $data, $wgParserCacheExpireTime );
}
$this->pendingRevCount = $count;
return $this->pendingRevCount;
}
/**
* Checks if the stable version is synced with the current revision
* Note: slower than getPendingRevCount()
* @return bool
*/
public function stableVersionIsSynced() {
global $wgMemc, $wgParserCacheExpireTime;
$srev = $this->getStableRev();
if ( !$srev ) {
return true;
}
# Stable text revision must be the same as the current
if ( $this->revsArePending() ) {
return false;
# Stable file revision must be the same as the current
} elseif ( $this->mTitle->getNamespace() == NS_FILE ) {
$file = $this->getFile(); // current upload version
if ( $file && $file->getTimestamp() > $srev->getFileTimestamp() ) {
return false;
}
}
# If using the current version of includes, there is nothing else to check.
if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_CURRENT ) {
return true; // short-circuit
}
# Try the cache...
$key = wfMemcKey( 'flaggedrevs', 'includesSynced', $this->getId() );
$value = FlaggedRevs::getMemcValue( $wgMemc->get( $key ), $this );
if ( $value === "true" ) {
return true;
} elseif ( $value === "false" ) {
return false;
}
# Since the stable and current revisions have the same text and only outputs,
# the only other things to check for are template and file differences in the output.
# (a) Check if the current output has a newer template/file used
# (b) Check if the stable version has a file/template that was deleted
$synced = ( !$srev->findPendingTemplateChanges()
&& !$srev->findPendingFileChanges( 'noForeign' ) );
# Save to cache. This will be updated whenever the page is touched.
$data = FlaggedRevs::makeMemcObj( $synced ? "true" : "false" );
$wgMemc->set( $key, $data, $wgParserCacheExpireTime );
return $synced;
}
/**
* Are template/file changes and ONLY template/file changes pending?
* @return bool
*/
public function onlyTemplatesOrFilesPending() {
return ( !$this->revsArePending() && !$this->stableVersionIsSynced() );
}
/**
* Is this page less open than the site defaults?
* @return bool
*/
public function isPageLocked() {
return ( !FlaggedRevs::isStableShownByDefault() && $this->isStableShownByDefault() );
}
/**
* Is this page more open than the site defaults?
* @return bool
*/
public function isPageUnlocked() {
return ( FlaggedRevs::isStableShownByDefault() && !$this->isStableShownByDefault() );
}
/**
* Tags are only shown for unreviewed content and this page is not locked/unlocked?
* @return bool
*/
public function lowProfileUI() {
return FlaggedRevs::lowProfileUI() &&
FlaggedRevs::isStableShownByDefault() == $this->isStableShownByDefault();
}
/**
* Is this article reviewable?
* @return bool
*/
public function isReviewable() {
if ( !FlaggedRevs::inReviewNamespace( $this->mTitle ) ) {
return false;
}
# Check if flagging is disabled for this page via config
if ( FlaggedRevs::useOnlyIfProtected() ) {
$config = $this->getStabilitySettings(); // page configuration
return (bool)$config['override']; // stable is default or flagging disabled
}
return true;
}
/**
* Get the stable revision ID
* @return int
*/
public function getStable() {
if ( !FlaggedRevs::inReviewNamespace( $this->mTitle ) ) {
return 0; // short-circuit
}
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
return (int)$this->stable;
}
/**
* Get the stable revision
* @return mixed (FlaggedRevision/null)
*/
public function getStableRev() {
if ( !FlaggedRevs::inReviewNamespace( $this->mTitle ) ) {
return null; // short-circuit
}
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
# Stable rev deferred even after page data load
if ( $this->stableRev === null ) {
$srev = FlaggedRevision::newFromTitle( $this->mTitle, $this->stable );
$this->stableRev = $srev ? $srev : false; // cache negative hits too
}
return $this->stableRev ? $this->stableRev : null; // false => null
}
/**
* Get visiblity restrictions on page
* @return array (select,override)
*/
public function getStabilitySettings() {
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
return $this->pageConfig;
}
/*
* Get the fp_reviewed value for this page
* @return bool
*/
public function syncedInTracking() {
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
return $this->syncedInTracking;
}
/**
* Get the newest of the highest rated flagged revisions of this page
* Note: will not return deleted revisions
* @return int
*/
public function getBestFlaggedRevId() {
$dbr = wfGetDB( DB_SLAVE );
# Get the highest quality revision (not necessarily this one).
$oldid = $dbr->selectField( array( 'flaggedrevs', 'revision' ),
'fr_rev_id',
array(
'fr_page_id' => $this->getId(),
'rev_page = fr_page_id', // sanity
'rev_id = fr_rev_id',
$dbr->bitAnd( 'rev_deleted', Revision::DELETED_TEXT ) . ' = 0'
),
__METHOD__,
array(
'ORDER BY' => 'fr_quality DESC, fr_rev_timestamp DESC'
)
);
return (int)$oldid;
}
/**
* Updates the fp_reviewed field for this article
* @param bool $synced
*/
public function updateSyncStatus( $synced ) {
if ( !wfReadOnly() ) {
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'flaggedpages',
array( 'fp_reviewed' => $synced ? 1 : 0 ),
array( 'fp_page_id' => $this->getID() ),
__METHOD__
);
}
}
/**
* Fetch a page record with the given conditions
* @param $dbr Database object
* @param $conditions Array
* @param array $options
* @return mixed Database result resource, or false on failure
*/
protected function pageData( $dbr, $conditions, $options = array() ) {
$row = $dbr->selectRow(
array( 'page', 'flaggedpages', 'flaggedpage_config' ),
array_merge(
WikiPage::selectFields(),
FRPageConfig::selectFields(),
array( 'fp_pending_since', 'fp_stable', 'fp_reviewed' ) ),
$conditions,
__METHOD__,
$options,
array(
'flaggedpages' => array( 'LEFT JOIN', 'fp_page_id = page_id' ),
'flaggedpage_config' => array( 'LEFT JOIN', 'fpc_page_id = page_id' ) )
);
return $row;
}
/**
* Set the page field data loaded from some source
* @param \Database|string $data Database row object or "fromdb"
* @return void
*/
public function loadPageData( $data = 'fromdb' ) {
$this->mDataLoaded = true; // sanity
# Fetch data from DB as needed...
if ( $data === 'fromdb' || $data === 'fromdbmaster' ) {
$db = ( $data == 'fromdbmaster' )
? wfGetDB( DB_MASTER )
: wfGetDB( DB_SLAVE );
$data = $this->pageDataFromTitle( $db, $this->mTitle );
}
# Load in primary page data...
parent::loadPageData( $data /* Row obj */ );
# Load in FlaggedRevs page data...
$this->stable = 0; // 0 => "found nothing"
$this->stableRev = null; // defer this one...
$this->revsArePending = false; // false => "found nothing" or "none pending"
$this->pendingRevCount = null; // defer this one...
$this->pageConfig = FRPageConfig::getDefaultVisibilitySettings(); // default
$this->syncedInTracking = true; // false => "unreviewed" or "synced"
# Load in flaggedrevs Row data if the page exists...(sanity check NS)
if ( $data && FlaggedRevs::inReviewNamespace( $this->mTitle ) ) {
if ( $data->fpc_override !== null ) { // page config row found
$this->pageConfig = FRPageConfig::getVisibilitySettingsFromRow( $data );
}
if ( $data->fp_stable !== null ) { // stable rev found
$this->stable = (int)$data->fp_stable;
$this->revsArePending = ( $data->fp_pending_since !== null ); // revs await review
$this->syncedInTracking = (bool)$data->fp_reviewed;
}
}
}
/**
* Updates the flagging tracking tables for this page
* @param FlaggedRevision $srev The new stable version
* @param int|null $latest The latest rev ID (optional)
* @return bool Updates were done
*/
public function updateStableVersion( FlaggedRevision $srev, $latest = null ) {
$rev = $srev->getRevision();
if ( !$this->exists() || !$rev ) {
return false; // no bogus entries
}
# Get the latest revision ID if not set
if ( !$latest ) {
$latest = $this->mTitle->getLatestRevID( Title::GAID_FOR_UPDATE );
}
$dbw = wfGetDB( DB_MASTER );
# Get the highest quality revision (not necessarily this one)...
if ( $srev->getQuality() === FlaggedRevs::highestReviewTier() ) {
$maxQuality = $srev->getQuality(); // save a query
} else {
$maxQuality = $dbw->selectField( array( 'flaggedrevs', 'revision' ),
'fr_quality',
array( 'fr_page_id' => $this->getId(),
'rev_id = fr_rev_id',
'rev_page = fr_page_id',
$dbw->bitAnd( 'rev_deleted', Revision::DELETED_TEXT ) . ' = 0'
),
__METHOD__,
array( 'ORDER BY' => 'fr_quality DESC', 'LIMIT' => 1 )
);
$maxQuality = max( $maxQuality, $srev->getQuality() ); // sanity
}
# Get the timestamp of the first edit after the stable version (if any)...
$nextTimestamp = null;
if ( $rev->getId() != $latest ) {
$timestamp = $dbw->timestamp( $rev->getTimestamp() );
$nextEditTS = $dbw->selectField( 'revision',
'rev_timestamp',
array(
'rev_page' => $this->getId(),
"rev_timestamp > " . $dbw->addQuotes( $timestamp ) ),
__METHOD__,
array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
);
if ( $nextEditTS ) { // sanity check
$nextTimestamp = $nextEditTS;
}
}
# Get the new page sync status...
$synced = !(
$nextTimestamp !== null || // edits pending
$srev->findPendingTemplateChanges() || // template changes pending
$srev->findPendingFileChanges( 'noForeign' ) // file changes pending
);
# Alter table metadata
$dbw->replace( 'flaggedpages',
array( 'fp_page_id' ),
array(
'fp_page_id' => $this->getId(),
'fp_stable' => $rev->getId(),
'fp_reviewed' => $synced ? 1 : 0,
'fp_quality' => ( $maxQuality === false ) ? null : $maxQuality,
'fp_pending_since' => $dbw->timestampOrNull( $nextTimestamp )
),
__METHOD__
);
# Update pending edit tracking table
self::updatePendingList( $this->getId(), $latest );
return true;
}
/**
* Updates the flagging tracking tables for this page
* @return void
*/
public function clearStableVersion() {
if ( !$this->exists() ) {
return; // nothing to do
}
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'flaggedpages',
array( 'fp_page_id' => $this->getId() ), __METHOD__ );
$dbw->delete( 'flaggedpage_pending',
array( 'fpp_page_id' => $this->getId() ), __METHOD__ );
}
/**
* Updates the flaggedpage_pending table
* @param int $pageId Page ID
* @param int $latest Latest revision
*/
protected static function updatePendingList( $pageId, $latest ) {
$data = array();
# Get the highest tier used on this wiki
$level = FlaggedRevs::highestReviewTier();
$dbw = wfGetDB( DB_MASTER );
# Update pending times for each level, going from highest to lowest
$higherLevelId = 0;
$higherLevelTS = '';
while ( $level >= 0 ) {
# Get the latest revision of this level...
# Any revision of one tier is also a revision of lower tiers.
# Instead of doing fr_quality > X queries we do exact comparisons
# for better INDEX usage. However, in order to treat a rev as the
# latest tier X rev, we make sure it is newer than all tier (X+1) revs.
$row = $dbw->selectRow(
array( 'flaggedrevs', 'revision' ),
array( 'fr_rev_id', 'rev_timestamp' ),
array(
'fr_page_id' => $pageId,
'fr_quality' => $level, // this level
'fr_rev_timestamp > ' . $dbw->addQuotes( $higherLevelTS ),
'rev_id = fr_rev_id', // rev exists
'rev_page = fr_page_id', // sanity
$dbw->bitAnd( 'rev_deleted', Revision::DELETED_TEXT ) . ' = 0'
),
__METHOD__,
array( 'ORDER BY' => 'fr_rev_timestamp DESC', 'LIMIT' => 1 )
);
# If there is a revision of this level, track it...
# Revisions accepted to one tier count as accepted
# at the lower tiers (i.e. quality -> checked).
if ( $row ) {
$id = $row->fr_rev_id;
$ts = $row->rev_timestamp;
} else { // use previous rev of higher tier (if any)
$id = $higherLevelId;
$ts = $higherLevelTS;
}
# Get edits that actually are pending...
if ( $id && $latest > $id ) {
# Get the timestamp of the edit after this version (if any)
$nextTimestamp = $dbw->selectField( 'revision',
'rev_timestamp',
array( 'rev_page' => $pageId, "rev_timestamp > " . $dbw->addQuotes( $ts ) ),
__METHOD__,
array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
);
$data[] = array(
'fpp_page_id' => $pageId,
'fpp_quality' => $level,
'fpp_rev_id' => $id,
'fpp_pending_since' => $nextTimestamp
);
$higherLevelId = $id;
$higherLevelTS = $ts;
}
$level--;
}
# Clear any old junk, and insert new rows
$dbw->delete( 'flaggedpage_pending', array( 'fpp_page_id' => $pageId ), __METHOD__ );
$dbw->insert( 'flaggedpage_pending', $data, __METHOD__ );
}
}