/** * 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; }
/** * (a) Stabilize inclusions in Parser output * (b) Load all of the "review time" versions of template/files from $frev * (c) Load their stable version counterparts (avoids DB hits) * Note: Used when calling FlaggedRevs::parseStableText(). * @param FlaggedRevision $frev * @return void */ public function stabilizeParserOutput(FlaggedRevision $frev) { $tStbVersions = $fStbVersions = array(); // stable versions $tRevVersions = $frev->getTemplateVersions(); $fRevVersions = $frev->getFileVersions(); # We can preload *most* of the stable version IDs the parser will need... if (FlaggedRevs::inclusionSetting() == FR_INCLUDES_STABLE) { $tStbVersions = $frev->getStableTemplateVersions(); $fStbVersions = $frev->getStableFileVersions(); } $this->setReviewedVersions($tRevVersions, $fRevVersions); $this->setStableVersionCache($tStbVersions, $fStbVersions); }
/** * 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 } } }
/** * Automatically review an revision and add a log entry in the review log. * * This is called during edit operations after the new revision is added * and the page tables updated, but before LinksUpdate is called. * * $auto is here for revisions checked off to be reviewed. Auto-review * triggers on edit, but we don't want those to count as just automatic. * This also makes it so the user's name shows up in the page history. * * If $flags is given, then they will be the review tags. If not, the one * from the stable version will be used or minimal tags if that's not possible. * If no appropriate tags can be found, then the review will abort. */ public static function autoReviewEdit(WikiPage $article, $user, Revision $rev, array $flags = null, $auto = true) { wfProfileIn(__METHOD__); $title = $article->getTitle(); // convenience # Get current stable version ID (for logging) $oldSv = FlaggedRevision::newFromStable($title, FR_MASTER); $oldSvId = $oldSv ? $oldSv->getRevId() : 0; # Set the auto-review tags from the prior stable version. # Normally, this should already be done and given here... if (!is_array($flags)) { if ($oldSv) { # Use the last stable version if $flags not given if ($user->isAllowed('bot')) { $flags = $oldSv->getTags(); // no change for bot edits } else { # Account for perms/tags... $flags = self::getAutoReviewTags($user, $oldSv->getTags()); } } else { // new page? $flags = self::quickTags(FR_CHECKED); // use minimal level } if (!is_array($flags)) { wfProfileOut(__METHOD__); return false; // can't auto-review this revision } } # Get review property flags $propFlags = $auto ? array('auto') : array(); # Note: this needs to match the prepareContentForEdit() call WikiPage::doEditContent. # This is for consistency and also to avoid triggering a second parse otherwise. $editInfo = $article->prepareContentForEdit($rev->getContent(), null, $user, $rev->getContentFormat()); $poutput = $editInfo->output; // revision HTML output # Get the "review time" versions of templates and files. # This tries to make sure each template/file version either came from the stable # version of that template/file or was a "review time" version used in the stable # version of this page. If a pending version of a template/file is currently vandalism, # we try to avoid storing its ID as the "review time" version so it won't show up when # someone views the page. If not possible, this stores the current template/file. if (FlaggedRevs::inclusionSetting() === FR_INCLUDES_CURRENT) { $tVersions = $poutput->getTemplateIds(); $fVersions = $poutput->getFileSearchOptions(); } else { $tVersions = $oldSv ? $oldSv->getTemplateVersions() : array(); $fVersions = $oldSv ? $oldSv->getFileVersions() : array(); foreach ($poutput->getTemplateIds() as $ns => $pages) { foreach ($pages as $dbKey => $revId) { if (!isset($tVersions[$ns][$dbKey])) { $srev = FlaggedRevision::newFromStable(Title::makeTitle($ns, $dbKey)); if ($srev) { // use stable $tVersions[$ns][$dbKey] = $srev->getRevId(); } else { // use current $tVersions[$ns][$dbKey] = $revId; } } } } foreach ($poutput->getFileSearchOptions() as $dbKey => $info) { if (!isset($fVersions[$dbKey])) { $srev = FlaggedRevision::newFromStable(Title::makeTitle(NS_FILE, $dbKey)); if ($srev && $srev->getFileTimestamp()) { // use stable $fVersions[$dbKey]['time'] = $srev->getFileTimestamp(); $fVersions[$dbKey]['sha1'] = $srev->getFileSha1(); } else { // use current $fVersions[$dbKey]['time'] = $info['time']; $fVersions[$dbKey]['sha1'] = $info['sha1']; } } } } # If this is an image page, get the corresponding file version info... $fileData = array('name' => null, 'timestamp' => null, 'sha1' => null); if ($title->getNamespace() == NS_FILE) { # We must use WikiFilePage process cache on upload or get bitten by slave lag $file = $article instanceof WikiFilePage || $article instanceof ImagePage ? $article->getFile() : wfFindFile($title, array('bypassCache' => true)); // skip cache; bug 31056 if (is_object($file) && $file->exists()) { $fileData['name'] = $title->getDBkey(); $fileData['timestamp'] = $file->getTimestamp(); $fileData['sha1'] = $file->getSha1(); } } # Our review entry $flaggedRevision = new FlaggedRevision(array('rev' => $rev, 'user_id' => $user->getId(), 'timestamp' => $rev->getTimestamp(), 'quality' => FlaggedRevs::getQualityTier($flags, 0), 'tags' => FlaggedRevision::flattenRevisionTags($flags), 'img_name' => $fileData['name'], 'img_timestamp' => $fileData['timestamp'], 'img_sha1' => $fileData['sha1'], 'templateVersions' => $tVersions, 'fileVersions' => $fVersions, 'flags' => implode(',', $propFlags))); $flaggedRevision->insert(); # Update the article review log FlaggedRevsLog::updateReviewLog($title, $flags, array(), '', $rev->getId(), $oldSvId, true, $auto); # Update page and tracking tables and clear cache FlaggedRevs::stableVersionUpdates($article); wfProfileOut(__METHOD__); return true; }
/** * Fetch pending file changes for this reviewed page * version against a list of current versions of files. * See findPendingFileChanges() for details. * * @param array $newFiles * @param bool|string $noForeign Using 'noForeign' skips foreign file updates (bug 15748) * @return array of (title, MW file timestamp in reviewed version, has stable rev) tuples */ public function findFileChanges(array $newFiles, $noForeign = false) { if (FlaggedRevs::inclusionSetting() == FR_INCLUDES_CURRENT) { return array(); // short-circuit } $fileChanges = array(); $rFiles = $this->getFileVersions(); $sFiles = $this->getStableFileVersions(); foreach ($newFiles as $dbKey => $sha1Time) { $reviewedTS = isset($rFiles[$dbKey]['time']) ? $rFiles[$dbKey]['time'] : null; $stableTS = isset($sFiles[$dbKey]['time']) ? $sFiles[$dbKey]['time'] : null; # Get file timestamp used in this FlaggedRevision when parsed $usedTS = self::fileTimestampUsed($stableTS, $reviewedTS); # Check for edits/creations/deletions... $title = Title::makeTitleSafe(NS_FILE, $dbKey); if (self::fileChanged($title, $usedTS, $noForeign)) { $fileChanges[] = array($title, $usedTS, (bool) $stableTS); } } return $fileChanges; }
/** * Update the page tables with a new stable version. * @param Title $title * @param FlaggedRevision|null $sv, the new stable version (optional) * @param FlaggedRevision|null $oldSv, the old stable version (optional) * @param Object editInfo Article edit info about the current revision (optional) * @return bool stable version text/file changed and FR_INCLUDES_STABLE */ public static function stableVersionUpdates(Title $title, $sv = null, $oldSv = null, $editInfo = null) { $changed = false; if ($oldSv === null) { // optional $oldSv = FlaggedRevision::newFromStable($title, FR_MASTER); } if ($sv === null) { // optional $sv = FlaggedRevision::determineStable($title, FR_MASTER); } $article = new FlaggableWikiPage($title); if (!$sv) { # Empty flaggedrevs data for this page if there is no stable version $article->clearStableVersion(); # Check if pages using this need to be refreshed... if (FlaggedRevs::inclusionSetting() == FR_INCLUDES_STABLE) { $changed = (bool) $oldSv; } } else { # Update flagged page related fields $article->updateStableVersion($sv, $editInfo ? $editInfo->revid : null); # Check if pages using this need to be invalidated/purged... if (FlaggedRevs::inclusionSetting() == FR_INCLUDES_STABLE) { $changed = !$oldSv || $sv->getRevId() != $oldSv->getRevId() || $sv->getFileTimestamp() != $oldSv->getFileTimestamp() || $sv->getFileSha1() != $oldSv->getFileSha1(); } # Update template/file version cache... if ($editInfo && $sv->getRevId() != $editInfo->revid) { FRInclusionCache::setRevIncludes($title, $editInfo->revid, $editInfo->output); } } # Lazily rebuild dependancies on next parse (we invalidate below) FlaggedRevs::clearStableOnlyDeps($title->getArticleID()); # Clear page cache $title->invalidateCache(); self::purgeSquid($title); return $changed; }
/** * Tag output function must be called by caller * Parser cache control deferred to caller * @param \FlaggedRevision|\stable $srev stable version * @param string $tag review box/bar info * @param string $prot protection notice * @return ParserOutput */ protected function showStableVersion(FlaggedRevision $srev, &$tag, $prot) { $reqUser = $this->getUser(); $this->load(); $flags = $srev->getTags(); $time = $this->getLanguage()->date($srev->getTimestamp(), true); # Set display revision ID $this->out->setRevisionId($srev->getRevId()); # Get quality level $quality = FlaggedRevs::isQuality($flags); $synced = $this->article->stableVersionIsSynced(); # Construct some tagging if (!$this->out->isPrintable() && !($this->article->lowProfileUI() && $synced)) { $revsSince = $this->article->getPendingRevCount(); // Simple icon-based UI if ($this->useSimpleUI()) { $icon = ''; # For protection based configs, show lock only if it's not redundant. if ($this->showRatingIcon()) { $icon = FlaggedRevsXML::stableStatusIcon($quality); } if (!$reqUser->getId()) { $msgHTML = ''; // Anons just see simple icons } else { $msg = $quality ? 'revreview-quick-quality' : 'revreview-quick-basic'; # Uses messages 'revreview-quick-quality-same', 'revreview-quick-basic-same' $msg = $synced ? "{$msg}-same" : $msg; $msgHTML = $this->msg($msg, $srev->getRevId())->numParams($revsSince)->parse(); } $msgHTML = $prot . $icon . $msgHTML; $tag = FlaggedRevsXML::prettyRatingBox($srev, $msgHTML, $revsSince, 'stable', $synced); // Standard UI } else { $icon = FlaggedRevsXML::stableStatusIcon($quality); $msg = $quality ? 'revreview-quality' : 'revreview-basic'; if ($synced) { # uses messages 'revreview-quality-same', 'revreview-basic-same' $msg .= '-same'; } elseif ($revsSince == 0) { # uses messages 'revreview-quality-i', 'revreview-basic-i' $msg .= '-i'; } $tag = $prot . $icon; $tag .= $this->msg($msg, $srev->getRevId(), $time)->numParams($revsSince)->parse(); if (!empty($flags)) { $tag .= FlaggedRevsXML::ratingToggle(); $tag .= "<div id='mw-fr-revisiondetails'>" . FlaggedRevsXML::addTagRatings($flags) . '</div>'; } } } # Get parsed stable version and output HTML $pOpts = $this->article->makeParserOptions($reqUser); if (!$this->article->getTitle()->quickUserCan('edit', $reqUser)) { $pOpts->setEditSection(false); } $parserCache = FRParserCacheStable::singleton(); $parserOut = $parserCache->get($this->article, $pOpts); # Do not use the parser cache if it lacks mImageTimeKeys and there is a # chance that a review form will be added to this page (which requires the versions). $canReview = $this->article->getTitle()->userCan('review'); if ($parserOut && (!$canReview || FlaggedRevs::parserOutputIsVersioned($parserOut))) { # Cache hit. Note that redirects are not cached. $this->out->addParserOutput($parserOut); } else { # Get the new stable parser output... if (FlaggedRevs::inclusionSetting() == FR_INCLUDES_CURRENT && $synced) { # We can try the current version cache, since they are the same revision $parserOut = ParserCache::singleton()->get($this->article, $pOpts); } else { $parserOut = false; } if (!$parserOut) { $parserOut = FlaggedRevs::parseStableRevision($srev, $pOpts); } if (!$parserOut) { // serious error $this->out->addWikiMsg('missing-article', $this->article->getTitle()->getPrefixedText(), $this->msg('missingarticle-rev', $srev->getRevId())->text()); return null; } # Update the stable version cache $parserCache->save($parserOut, $this->article, $pOpts); # Add the stable output to the page view $this->out->addParserOutput($parserOut); # Update the stable version dependancies FlaggedRevs::updateStableOnlyDeps($this->article, $parserOut); } # Update page sync status for tracking purposes. # NOTE: avoids master hits and doesn't have to be perfect for what it does if ($this->article->syncedInTracking() != $synced) { if (wfGetLB()->safeGetLag(wfGetDB(DB_SLAVE)) <= 5) { // avoid write-delay cycles $this->article->updateSyncStatus($synced); } } return $parserOut; }
/** * Select the desired images based on the selected stable version time/SHA-1 */ public static function parserFetchStableFile($parser, Title $title, &$options, &$query) { if (!$parser instanceof Parser) { return true; // nothing to do } $incManager = FRInclusionManager::singleton(); if (!$incManager->parserOutputIsStabilized()) { return true; // trigger for stable version parsing only } # Normalize NS_MEDIA to NS_FILE if ($title->getNamespace() == NS_MEDIA) { $title = Title::makeTitle(NS_FILE, $title->getDBkey()); $title->resetArticleId($title->getArticleID()); // avoid extra queries } # Check if this file is only on a foreign repo $file = wfFindFile($title); if ($file && !$file->isLocal()) { return true; // just use the current version (bug 41832) } $time = $sha1 = false; // unspecified (defaults to current version) # Check for the version of this file used when reviewed... list($maybeTS, $maybeSha1) = $incManager->getReviewedFileVersion($title); if ($maybeTS !== null) { $time = $maybeTS; // use if specified (even '0') $sha1 = $maybeSha1; } # Check for stable version of file if this feature is enabled... if (FlaggedRevs::inclusionSetting() == FR_INCLUDES_STABLE) { list($maybeTS, $maybeSha1) = $incManager->getStableFileVersion($title); # Take the newest of these two... if ($maybeTS && $maybeTS > $time) { $time = $maybeTS; $sha1 = $maybeSha1; } } # Tell Parser what file version to use if ($time === '0') { $options['broken'] = true; } elseif ($time !== false) { $options['time'] = $time; $options['sha1'] = $sha1; # Stabilize the file link if ($query != '') { $query .= '&'; } $query .= "filetimestamp=" . urlencode(wfTimestamp(TS_MW, $time)); } return true; }