/** * Add unreviewed pages links */ public function addToCategoryView() { $reqUser = $this->getUser(); $this->load(); if (!$reqUser->isAllowed('review')) { return true; } if (!FlaggedRevs::useOnlyIfProtected()) { # Add links to lists of unreviewed pages and pending changes in this category $category = $this->article->getTitle()->getText(); $this->out->appendSubtitle(Html::rawElement('span', array('class' => 'plainlinks', 'id' => 'mw-fr-category-oldreviewed'), wfMsgExt('flaggedrevs-categoryview', 'parseinline', urlencode($category)))); } return true; }
/** * Purge expired restrictions from the flaggedpage_config table. * The stable version of pages may change and invalidation may be required. */ public static function purgeExpiredConfigurations() { if (wfReadOnly()) { return; } $dbw = wfGetDB(DB_MASTER); # Find pages with expired configs... $config = self::getDefaultVisibilitySettings(); // config is to be reset $encCutoff = $dbw->addQuotes($dbw->timestamp()); $ret = $dbw->select(array('flaggedpage_config', 'page'), array('fpc_page_id', 'page_namespace', 'page_title'), array('page_id = fpc_page_id', 'fpc_expiry < ' . $encCutoff), __METHOD__); # Figured out to do with each page... $pagesClearConfig = array(); $pagesClearTracking = $titlesClearTracking = array(); foreach ($ret as $row) { # If FlaggedRevs got "turned off" (in protection config) # for this page, then clear it from the tracking tables... if (FlaggedRevs::useOnlyIfProtected() && !$config['override']) { $pagesClearTracking[] = $row->fpc_page_id; // no stable version $titlesClearTracking[] = Title::newFromRow($row); // no stable version } $pagesClearConfig[] = $row->fpc_page_id; // page with expired config } # Clear the expired config for these pages... if (count($pagesClearConfig)) { $dbw->delete('flaggedpage_config', array('fpc_page_id' => $pagesClearConfig, 'fpc_expiry < ' . $encCutoff), __METHOD__); } # Clear the tracking rows and update page_touched for the # pages in $pagesClearConfig that do now have a stable version... if (count($pagesClearTracking)) { FlaggedRevs::clearTrackingRows($pagesClearTracking); $dbw->update('page', array('page_touched' => $dbw->timestamp()), array('page_id' => $pagesClearTracking), __METHOD__); } # Also, clear their squid caches and purge other pages that use this page. # NOTE: all of these updates are deferred via $wgDeferredUpdateList. foreach ($titlesClearTracking as $title) { FlaggedRevs::purgeSquid($title); if (FlaggedRevs::inclusionSetting() == FR_INCLUDES_STABLE) { FlaggedRevs::HTMLCacheUpdates($title); // purge pages that use this page } } }
/** * 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 a FlaggedRevision of the stable version of a title. * Skips tracking tables to figure out new stable version. * @param Title $title, page title * @param int $flags (FR_MASTER, FR_FOR_UPDATE) * @param array $config, optional page config (use to skip queries) * @param string $precedence (latest,quality,pristine) * @return FlaggedRevision|null (null on failure) */ public static function determineStable(Title $title, $flags = 0, $config = array(), $precedence = 'latest') { if (!FlaggedRevs::inReviewNamespace($title)) { return null; // short-circuit } $options = array(); # User master/slave as appropriate... if ($flags & FR_FOR_UPDATE || $flags & FR_MASTER) { $db = wfGetDB(DB_MASTER); if ($flags & FR_FOR_UPDATE) { $options[] = 'FOR UPDATE'; } $pageId = $title->getArticleID(Title::GAID_FOR_UPDATE); } else { $db = wfGetDB(DB_SLAVE); $pageId = $title->getArticleID(); } if (!$pageId) { return null; // short-circuit query } # Get visiblity settings to see if page is reviewable... if (FlaggedRevs::useOnlyIfProtected()) { if (empty($config)) { $config = FRPageConfig::getStabilitySettings($title, $flags); } if (!$config['override']) { return null; // page is not reviewable; no stable version } } $baseConds = array('fr_page_id' => $pageId, 'rev_id = fr_rev_id', 'rev_page = fr_page_id', $db->bitAnd('rev_deleted', Revision::DELETED_TEXT) . ' = 0'); $options['ORDER BY'] = 'fr_rev_timestamp DESC'; $row = null; if ($precedence !== 'latest') { # Look for the latest pristine revision... if (FlaggedRevs::pristineVersions()) { $prow = $db->selectRow(array('flaggedrevs', 'revision'), self::selectFields(), array_merge($baseConds, array('fr_quality' => FR_PRISTINE)), __METHOD__, $options); # Looks like a plausible revision $row = $prow ? $prow : $row; } if ($row && $precedence === 'pristine') { // we have what we want already # Look for the latest quality revision... } elseif (FlaggedRevs::qualityVersions()) { // If we found a pristine rev above, this one must be newer... $newerClause = $row ? array('fr_rev_timestamp > ' . $db->addQuotes($row->fr_rev_timestamp)) : array(); $qrow = $db->selectRow(array('flaggedrevs', 'revision'), self::selectFields(), array_merge($baseConds, array('fr_quality' => FR_QUALITY), $newerClause), __METHOD__, $options); $row = $qrow ? $qrow : $row; } } # Do we have one? If not, try the latest reviewed revision... if (!$row) { $row = $db->selectRow(array('flaggedrevs', 'revision'), self::selectFields(), $baseConds, __METHOD__, $options); if (!$row) { return null; } } $frev = new self($row); $frev->mTitle = $title; return $frev; }
/** * Get log params (associate array) from a stability config * @param array $config * @return array (associative) */ public static function stabilityLogParams(array $config) { $params = $config; if (!FlaggedRevs::useOnlyIfProtected()) { $params['precedence'] = 1; // b/c hack for presenting log params... } return $params; }
function __construct($form, $namespace, $level = -1, $category = '', $size = null, $watched = false, $stable = false) { $this->mForm = $form; # Must be a content page... $vnamespaces = FlaggedRevs::getReviewNamespaces(); if (is_null($namespace)) { $namespace = $vnamespaces; } else { $namespace = intval($namespace); } # Sanity check if (!in_array($namespace, $vnamespaces)) { $namespace = $vnamespaces; } $this->namespace = $namespace; # Sanity check level: 0 = checked; 1 = quality; 2 = pristine $this->level = $level >= 0 && $level <= 2 ? $level : -1; $this->category = $category ? str_replace(' ', '_', $category) : null; $this->size = $size !== null ? intval($size) : null; $this->watched = (bool) $watched; $this->stable = $stable && !FlaggedRevs::isStableShownByDefault() && !FlaggedRevs::useOnlyIfProtected(); parent::__construct(); # Don't get too expensive $this->mLimitsShown = array(20, 50, 100); $this->setLimit($this->mLimit); // apply max limit }
/** * Submit the form parameters for the page config to the DB. * * @return mixed (true on success, error string on failure) */ public function doSubmit() { # Double-check permissions if (!$this->isAllowed()) { return 'stablize_denied'; } # Parse and cleanup the expiry time given... $expiry = $this->getExpiry(); if ($expiry === false) { return 'stabilize_expiry_invalid'; } elseif ($expiry !== Block::infinity() && $expiry < wfTimestampNow()) { return 'stabilize_expiry_old'; } # Update the DB row with the new config... $changed = FRPageConfig::setStabilitySettings($this->page, $this->getNewConfig()); # Log if this actually changed anything... if ($changed) { $article = new FlaggableWikiPage($this->page); if (FlaggedRevs::useOnlyIfProtected()) { # Config may have changed to allow stable versions, so refresh # the tracking table to account for any hidden reviewed versions... $frev = FlaggedRevision::determineStable($this->page, FR_MASTER); if ($frev) { $article->updateStableVersion($frev); } else { $article->clearStableVersion(); } } # Update logs and make a null edit $nullRev = $this->updateLogsAndHistory($article); # Null edit may have been auto-reviewed already $frev = FlaggedRevision::newFromTitle($this->page, $nullRev->getId(), FR_MASTER); $updatesDone = (bool) $frev; // stableVersionUpdates() already called? # Check if this null edit is to be reviewed... if ($this->reviewThis && !$frev) { $flags = null; # Review this revision of the page... $ok = FlaggedRevs::autoReviewEdit($article, $this->user, $nullRev, $flags, true); if ($ok) { FlaggedRevs::markRevisionPatrolled($nullRev); // reviewed -> patrolled $updatesDone = true; // stableVersionUpdates() already called } } # Update page and tracking tables and clear cache. if (!$updatesDone) { FlaggedRevs::stableVersionUpdates($this->page); } } # Apply watchlist checkbox value (may be NULL) $this->updateWatchlist(); # Take this opportunity to purge out expired configurations FRPageConfig::purgeExpiredConfigurations(); return true; }
/** * Get edit review time statistics (as recent as possible) * @param $dbcache Database cache object * @param $users string "anons" or "users" * @return Array associative */ private static function getEditReviewTimes($dbCache, $users = 'anons') { $result = array('average' => 0, 'median' => 0, 'percTable' => array(), 'sampleSize' => 0, 'sampleStartTS' => null, 'sampleEndTS' => null); if (FlaggedRevs::useOnlyIfProtected()) { return $result; // disabled } $aveRT = $medianRT = 0; $rPerTable = array(); // review wait percentiles # Only go so far back...otherwise we will get garbage values due to # the fact that FlaggedRevs wasn't enabled until after a while. $dbr = wfGetDB(DB_SLAVE); $installedUnix = (int) $dbr->selectField('logging', 'UNIX_TIMESTAMP( MIN(log_timestamp) )', array('log_type' => 'review')); if (!$installedUnix) { $installedUnix = wfTimestamp(TS_UNIX); // now } $encInstalled = $dbr->addQuotes($dbr->timestamp($installedUnix)); # Skip the most recent recent revs as they are likely to just # be WHERE condition misses. This also gives us more data to use. # Lastly, we want to avoid bias that would make the time too low # since new revisions could not have "took a long time to sight". $worstLagTS = $dbr->timestamp(); // now $last = '0'; while (true) { // should almost always be ~1 pass # Get the page with the worst pending lag... $row = $dbr->selectRow(array('flaggedpage_pending', 'flaggedrevs'), array('fpp_page_id', 'fpp_rev_id', 'fpp_pending_since', 'fr_timestamp'), array('fpp_quality' => 0, 'fpp_pending_since > ' . $encInstalled, 'fr_page_id = fpp_page_id AND fr_rev_id = fpp_rev_id', 'fpp_pending_since > ' . $dbr->addQuotes($last)), __METHOD__, array('ORDER BY' => 'fpp_pending_since ASC', 'USE INDEX' => array('flaggedpage_pending' => 'fpp_quality_pending'))); if (!$row) { break; } # Find the newest revision at the time the page was reviewed, # this is the one that *should* have been reviewed. $idealRev = (int) $dbr->selectField('revision', 'rev_id', array('rev_page' => $row->fpp_page_id, 'rev_timestamp < ' . $dbr->addQuotes($row->fr_timestamp)), __METHOD__, array('ORDER BY' => 'rev_timestamp DESC', 'LIMIT' => 1)); if ($row->fpp_rev_id >= $idealRev) { $worstLagTS = $row->fpp_pending_since; break; // sane $worstLagTS found # Fudge factor to prevent deliberate reviewing of non-current revisions # from squeezing the range. Shouldn't effect anything otherwise. } else { $last = $row->fpp_pending_since; // next iteration } } # User condition (anons/users) if ($users === 'anons') { $userCondition = 'rev_user = 0'; } elseif ($users === 'users') { $userCondition = 'rev_user = 1'; } else { throw new MWException('Invalid $users param given.'); } # Avoid having to censor data # Note: if no edits pending, $worstLagTS is the cur time just before we checked # for the worst lag. Thus, new edits *right* after the check are properly excluded. $maxTSUnix = wfTimestamp(TS_UNIX, $worstLagTS) - 1; // all edits later reviewed $encMaxTS = $dbr->addQuotes($dbr->timestamp($maxTSUnix)); # Use a one week time range $days = 7; $minTSUnix = $maxTSUnix - $days * 86400; $encMinTS = $dbr->addQuotes($dbr->timestamp($minTSUnix)); # Approximate the number rows to scan $rows = $dbr->estimateRowCount('revision', '1', array($userCondition, "rev_timestamp BETWEEN {$encMinTS} AND {$encMaxTS}")); # If the range doesn't have many rows (like on small wikis), use 30 days if ($rows < 500) { $days = 30; $minTSUnix = $maxTSUnix - $days * 86400; $encMinTS = $dbr->addQuotes($dbr->timestamp($minTSUnix)); # Approximate rows to scan $rows = $dbr->estimateRowCount('revision', '1', array($userCondition, "rev_timestamp BETWEEN {$encMinTS} AND {$encMaxTS}")); # If the range doesn't have many rows (like on really tiny wikis), use 90 days if ($rows < 500) { $days = 90; $minTSUnix = $maxTSUnix - $days * 86400; } } $sampleSize = 1500; // sample size # Sanity check the starting timestamp $minTSUnix = max($minTSUnix, $installedUnix); $encMinTS = $dbr->addQuotes($dbr->timestamp($minTSUnix)); # Get timestamp boundaries $timeCondition = "rev_timestamp BETWEEN {$encMinTS} AND {$encMaxTS}"; # Get mod for edit spread $ecKey = wfMemcKey('flaggedrevs', 'anonEditCount', $days); $edits = (int) $dbCache->get($ecKey); if (!$edits) { $edits = (int) $dbr->selectField(array('page', 'revision'), 'COUNT(*)', array($userCondition, $timeCondition, 'page_id = rev_page', 'page_namespace' => FlaggedRevs::getReviewNamespaces())); $dbCache->set($ecKey, $edits, 14 * 24 * 3600); // cache for 2 weeks } $mod = max(floor($edits / $sampleSize), 1); # $mod >= 1 # For edits that started off pending, how long do they take to get reviewed? # Edits started off pending if made when a flagged rev of the page already existed. # Get the *first* reviewed rev *after* each edit and get the time difference. $res = $dbr->select(array('revision', 'p' => 'flaggedrevs', 'n' => 'flaggedrevs'), array('MIN(rev_timestamp) AS rt', 'MIN(n.fr_timestamp) AS nft', 'MAX(p.fr_rev_id)'), array($userCondition, $timeCondition, "(rev_id % {$mod}) = 0"), __METHOD__, array('GROUP BY' => array('rev_timestamp', 'rev_id'), 'USE INDEX' => array('revision' => 'user_timestamp'), 'STRAIGHT_JOIN'), array('p' => array('INNER JOIN', array('p.fr_page_id = rev_page', 'p.fr_rev_id < rev_id', 'p.fr_timestamp < rev_timestamp')), 'n' => array('INNER JOIN', array('n.fr_page_id = rev_page', 'n.fr_rev_id >= rev_id', 'n.fr_timestamp >= rev_timestamp')))); $secondsR = 0; // total wait seconds for edits later reviewed $secondsP = 0; // total wait seconds for edits still pending $aveRT = $medianRT = 0; $times = array(); if ($dbr->numRows($res)) { # Get the elapsed times revs were pending (flagged time - edit time) foreach ($res as $row) { $time = wfTimestamp(TS_UNIX, $row->nft) - wfTimestamp(TS_UNIX, $row->rt); $time = max($time, 0); // sanity $secondsR += $time; $times[] = $time; } $sampleSize = count($times); $aveRT = ($secondsR + $secondsP) / $sampleSize; // sample mean sort($times); // order smallest -> largest // Sample median $rank = round(count($times) / 2 + 0.5) - 1; $medianRT = $times[$rank]; // Make percentile tabulation data $doPercentiles = array(35, 45, 55, 65, 75, 85, 90, 95); foreach ($doPercentiles as $percentile) { $rank = round($percentile * count($times) / 100 + 0.5) - 1; $rPerTable[$percentile] = $times[$rank]; } $result['average'] = $aveRT; $result['median'] = $medianRT; $result['percTable'] = $rPerTable; $result['sampleSize'] = count($times); $result['sampleStartTS'] = $minTSUnix; $result['sampleEndTS'] = $maxTSUnix; } return $result; }
public function execute($par) { global $wgContLang, $wgFlaggedRevsStats, $wgFlaggedRevsProtection; $out = $this->getOutput(); $lang = $this->getLanguage(); $this->setHeaders(); $this->db = wfGetDB(DB_SLAVE); $this->maybeUpdate(); $ec = $this->getEditorCount(); $rc = $this->getReviewerCount(); $mt = $this->getMeanReviewWaitAnon(); $mdt = $this->getMedianReviewWaitAnon(); $pt = $this->getMeanPendingWait(); $pData = $this->getReviewPercentilesAnon(); $timestamp = $this->getLastUpdate(); $out->addWikiMsg('validationstatistics-users', $lang->formatnum($ec), $lang->formatnum($rc)); # Most of the output depends on background queries if (!$this->readyForQuery()) { return false; } # Is there a review time table available? if (count($pData)) { $headerRows = $dataRows = ''; foreach ($pData as $percentile => $perValue) { $headerRows .= "<th>P<sub>" . intval($percentile) . "</sub></th>"; $dataRows .= '<td>' . $lang->formatTimePeriod($perValue, 'avoidminutes') . '</td>'; } $css = 'wikitable flaggedrevs_stats_table'; $reviewChart = "<table class='{$css}' style='white-space: nowrap;'>\n"; $reviewChart .= "<tr align='center'>{$headerRows}</tr>\n"; $reviewChart .= "<tr align='center'>{$dataRows}</tr>\n"; $reviewChart .= "</table>\n"; } else { $reviewChart = ''; } if ($timestamp != '-') { # Show "last updated"... $out->addWikiMsg('validationstatistics-lastupdate', $lang->date($timestamp, true), $lang->time($timestamp, true)); } $out->addHtml('<hr/>'); # Show pending time stats... $out->addWikiMsg('validationstatistics-pndtime', $lang->formatTimePeriod($pt, 'avoidminutes')); # Show review time stats... if (!FlaggedRevs::useOnlyIfProtected()) { $out->addWikiMsg('validationstatistics-revtime', $lang->formatTimePeriod($mt, 'avoidminutes'), $lang->formatTimePeriod($mdt, 'avoidminutes'), $reviewChart); } # Show per-namespace stats table... $out->addWikiMsg('validationstatistics-table'); $out->addHTML(Xml::openElement('table', array('class' => 'wikitable flaggedrevs_stats_table'))); $out->addHTML("<tr>\n"); // Headings (for a positive grep result): // validationstatistics-ns, validationstatistics-total, validationstatistics-stable, // validationstatistics-latest, validationstatistics-synced, validationstatistics-old, // validationstatistics-unreviewed $msgs = array('ns', 'total', 'stable', 'latest', 'synced', 'old'); // our headings if (!$wgFlaggedRevsProtection) { $msgs[] = 'unreviewed'; } foreach ($msgs as $msg) { $out->addHTML('<th>' . $this->msg("validationstatistics-{$msg}")->parse() . '</th>'); } $out->addHTML("</tr>\n"); $namespaces = FlaggedRevs::getReviewNamespaces(); foreach ($namespaces as $namespace) { $total = $this->getTotalPages($namespace); $reviewed = $this->getReviewedPages($namespace); $synced = $this->getSyncedPages($namespace); if ($total === '-' || $reviewed === '-' || $synced === '-') { continue; // NS added to config recently? } $NsText = $wgContLang->getFormattedNsText($namespace); $NsText = $NsText ? $NsText : $this->msg('blanknamespace')->escaped(); $percRev = intval($total) == 0 ? '-' : $this->msg('parentheses', $this->msg('percent')->numParams(sprintf('%4.2f', 100 * intval($reviewed) / intval($total)))->escaped())->text(); $percLatest = intval($total) == 0 ? '-' : $this->msg('parentheses', $this->msg('percent')->numParams(sprintf('%4.2f', 100 * intval($synced) / intval($total)))->escaped())->text(); $percSynced = intval($reviewed) == 0 ? '-' : $this->msg('percent')->numParams(sprintf('%4.2f', 100 * intval($synced) / intval($reviewed)))->escaped(); $outdated = intval($reviewed) - intval($synced); $outdated = $lang->formatnum(max(0, $outdated)); // lag between queries $unreviewed = intval($total) - intval($reviewed); $unreviewed = $lang->formatnum(max(0, $unreviewed)); // lag between queries $out->addHTML("<tr align='center'>\n\t\t\t\t\t<td>" . htmlspecialchars($NsText) . "</td>\n\t\t\t\t\t<td>" . htmlspecialchars($lang->formatnum($total)) . "</td>\n\t\t\t\t\t<td>" . htmlspecialchars($lang->formatnum($reviewed) . $wgContLang->getDirMark()) . " <i>{$percRev}</i>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td>" . htmlspecialchars($lang->formatnum($synced) . $wgContLang->getDirMark()) . " <i>{$percLatest}</i>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td>" . $percSynced . "</td>\n\t\t\t\t\t<td>" . Linker::linkKnown(SpecialPage::getTitleFor('PendingChanges'), htmlspecialchars($outdated), array(), array('namespace' => $namespace)) . "</td>"); if (!$wgFlaggedRevsProtection) { $out->addHTML("\n\t\t\t\t\t<td>" . Linker::linkKnown(SpecialPage::getTitleFor('UnreviewedPages'), htmlspecialchars($unreviewed), array(), array('namespace' => $namespace)) . "</td>"); } $out->addHTML("\n\t\t\t\t</tr>"); } $out->addHTML(Xml::closeElement('table')); # Is there a top X user list? If so, then show it... $data = $this->getTopReviewers(); if (is_array($data) && count($data)) { $out->addWikiMsg('validationstatistics-utable', $lang->formatNum($wgFlaggedRevsStats['topReviewersCount']), $lang->formatNum($wgFlaggedRevsStats['topReviewersHours'])); $css = 'wikitable flaggedrevs_stats_table'; $reviewChart = "<table class='{$css}' style='white-space: nowrap;'>\n"; $reviewChart .= '<tr><th>' . $this->msg('validationstatistics-user')->escaped() . '</th><th>' . $this->msg('validationstatistics-reviews')->escaped() . '</th></tr>'; foreach ($data as $userId => $reviews) { $reviewChart .= '<tr><td>' . htmlspecialchars(User::whois($userId)) . '</td><td>' . $lang->formatNum($reviews) . '</td></tr>'; } $reviewChart .= "</table>\n"; $out->addHTML($reviewChart); } return true; }
public static function addToChangeListLine(&$list, &$articlelink, &$s, RecentChange &$rc) { global $wgUser; $title = $rc->getTitle(); // convenience if (!FlaggedRevs::inReviewNamespace($title) || empty($rc->mAttribs['rc_this_oldid']) || !array_key_exists('fp_stable', $rc->mAttribs)) { return true; // confirm that page is in reviewable namespace } $rlink = $css = ''; // page is not reviewed if ($rc->mAttribs['fp_stable'] == null) { // Is this a config were pages start off reviewable? // Hide notice from non-reviewers due to vandalism concerns (bug 24002). if (!FlaggedRevs::useOnlyIfProtected() && $wgUser->isAllowed('review')) { $rlink = wfMsgHtml('revreview-unreviewedpage'); $css = 'flaggedrevs-unreviewed'; } // page is reviewed and has pending edits (use timestamps; bug 15515) } elseif (isset($rc->mAttribs['fp_pending_since']) && $rc->mAttribs['rc_timestamp'] >= $rc->mAttribs['fp_pending_since']) { $rlink = $list->skin->link($title, wfMsgHtml('revreview-reviewlink'), array('title' => wfMsg('revreview-reviewlink-title')), array('oldid' => $rc->mAttribs['fp_stable'], 'diff' => 'cur') + FlaggedRevs::diffOnlyCGI()); $css = 'flaggedrevs-pending'; } if ($rlink != '') { $articlelink .= " <span class=\"mw-fr-reviewlink {$css}\">[{$rlink}]</span>"; } return true; }