/** * @param IDatabase $db * @return mixed */ public function doQuery($db) { $ids = array_map('intval', $this->ids); $live = $db->select(array('revision', 'page', 'user'), array_merge(Revision::selectFields(), Revision::selectUserFields()), array('rev_page' => $this->title->getArticleID(), 'rev_id' => $ids), __METHOD__, array('ORDER BY' => 'rev_id DESC'), array('page' => Revision::pageJoinCond(), 'user' => Revision::userJoinCond())); if ($live->numRows() >= count($ids)) { // All requested revisions are live, keeps things simple! return $live; } // Check if any requested revisions are available fully deleted. $archived = $db->select(array('archive'), Revision::selectArchiveFields(), array('ar_rev_id' => $ids), __METHOD__, array('ORDER BY' => 'ar_rev_id DESC')); if ($archived->numRows() == 0) { return $live; } elseif ($live->numRows() == 0) { return $archived; } else { // Combine the two! Whee $rows = array(); foreach ($live as $row) { $rows[$row->rev_id] = $row; } foreach ($archived as $row) { $rows[$row->ar_rev_id] = $row; } krsort($rows); return new FakeResultWrapper(array_values($rows)); } }
/** * @param IDatabase $db * @return mixed */ public function doQuery($db) { $ids = array_map('intval', $this->ids); $queryInfo = ['tables' => ['revision', 'user'], 'fields' => array_merge(Revision::selectFields(), Revision::selectUserFields()), 'conds' => ['rev_page' => $this->title->getArticleID(), 'rev_id' => $ids], 'options' => ['ORDER BY' => 'rev_id DESC'], 'join_conds' => ['page' => Revision::pageJoinCond(), 'user' => Revision::userJoinCond()]]; ChangeTags::modifyDisplayQuery($queryInfo['tables'], $queryInfo['fields'], $queryInfo['conds'], $queryInfo['join_conds'], $queryInfo['options'], ''); $live = $db->select($queryInfo['tables'], $queryInfo['fields'], $queryInfo['conds'], __METHOD__, $queryInfo['options'], $queryInfo['join_conds']); if ($live->numRows() >= count($ids)) { // All requested revisions are live, keeps things simple! return $live; } $archiveQueryInfo = ['tables' => ['archive'], 'fields' => Revision::selectArchiveFields(), 'conds' => ['ar_rev_id' => $ids], 'options' => ['ORDER BY' => 'ar_rev_id DESC'], 'join_conds' => []]; ChangeTags::modifyDisplayQuery($archiveQueryInfo['tables'], $archiveQueryInfo['fields'], $archiveQueryInfo['conds'], $archiveQueryInfo['join_conds'], $archiveQueryInfo['options'], ''); // Check if any requested revisions are available fully deleted. $archived = $db->select($archiveQueryInfo['tables'], $archiveQueryInfo['fields'], $archiveQueryInfo['conds'], __METHOD__, $archiveQueryInfo['options'], $archiveQueryInfo['join_conds']); if ($archived->numRows() == 0) { return $live; } elseif ($live->numRows() == 0) { return $archived; } else { // Combine the two! Whee $rows = []; foreach ($live as $row) { $rows[$row->rev_id] = $row; } foreach ($archived as $row) { $rows[$row->ar_rev_id] = $row; } krsort($rows); return new FakeResultWrapper(array_values($rows)); } }
/** * @param IDatabase $db * @return mixed */ public function doQuery($db) { $timestamps = array(); foreach ($this->ids as $id) { $timestamps[] = $db->timestamp($id); } return $db->select('archive', Revision::selectArchiveFields(), array('ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey(), 'ar_timestamp' => $timestamps), __METHOD__, array('ORDER BY' => 'ar_timestamp DESC')); }
/** * @param IDatabase $db * @return mixed */ public function doQuery($db) { $timestamps = []; foreach ($this->ids as $id) { $timestamps[] = $db->timestamp($id); } $tables = ['archive']; $fields = Revision::selectArchiveFields(); $conds = ['ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey(), 'ar_timestamp' => $timestamps]; $join_conds = []; $options = ['ORDER BY' => 'ar_timestamp DESC']; ChangeTags::modifyDisplayQuery($tables, $fields, $conds, $join_conds, $options, ''); return $db->select($tables, $fields, $conds, __METHOD__, $options, $join_conds); }
public function doDBUpdates() { $db = $this->getDB(DB_MASTER); if (!$db->tableExists('revision')) { $this->error("revision table does not exist", true); } elseif (!$db->tableExists('archive')) { $this->error("archive table does not exist", true); } elseif (!$db->fieldExists('revision', 'rev_len', __METHOD__)) { $this->output("rev_len column does not exist\n\n", true); return false; } $this->output("Populating rev_len column\n"); $rev = $this->doLenUpdates('revision', 'rev_id', 'rev', Revision::selectFields()); $this->output("Populating ar_len column\n"); $ar = $this->doLenUpdates('archive', 'ar_id', 'ar', Revision::selectArchiveFields()); $this->output("rev_len and ar_len population complete [{$rev} revision rows, {$ar} archive rows].\n"); return true; }
/** * @param ApiPageSet $resultPageSet * @return void */ protected function run(ApiPageSet $resultPageSet = null) { $user = $this->getUser(); // Before doing anything at all, let's check permissions if (!$user->isAllowed('deletedhistory')) { $this->dieUsage('You don\'t have permission to view deleted revision information', 'permissiondenied'); } $db = $this->getDB(); $params = $this->extractRequestParams(false); $result = $this->getResult(); // If the user wants no namespaces, they get no pages. if ($params['namespace'] === []) { if ($resultPageSet === null) { $result->addValue('query', $this->getModuleName(), []); } return; } // This module operates in two modes: // 'user': List deleted revs by a certain user // 'all': List all deleted revs in NS $mode = 'all'; if (!is_null($params['user'])) { $mode = 'user'; } if ($mode == 'user') { foreach (['from', 'to', 'prefix', 'excludeuser'] as $param) { if (!is_null($params[$param])) { $p = $this->getModulePrefix(); $this->dieUsage("The '{$p}{$param}' parameter cannot be used with '{$p}user'", 'badparams'); } } } else { foreach (['start', 'end'] as $param) { if (!is_null($params[$param])) { $p = $this->getModulePrefix(); $this->dieUsage("The '{$p}{$param}' parameter may only be used with '{$p}user'", 'badparams'); } } } // If we're generating titles only, we can use DISTINCT for a better // query. But we can't do that in 'user' mode (wrong index), and we can // only do it when sorting ASC (because MySQL apparently can't use an // index backwards for grouping even though it can for ORDER BY, WTF?) $dir = $params['dir']; $optimizeGenerateTitles = false; if ($mode === 'all' && $params['generatetitles'] && $resultPageSet !== null) { if ($dir === 'newer') { $optimizeGenerateTitles = true; } else { $p = $this->getModulePrefix(); $this->setWarning("For better performance when generating titles, set {$p}dir=newer"); } } $this->addTables('archive'); if ($resultPageSet === null) { $this->parseParameters($params); $this->addFields(Revision::selectArchiveFields()); $this->addFields(['ar_title', 'ar_namespace']); } else { $this->limit = $this->getParameter('limit') ?: 10; $this->addFields(['ar_title', 'ar_namespace']); if ($optimizeGenerateTitles) { $this->addOption('DISTINCT'); } else { $this->addFields(['ar_timestamp', 'ar_rev_id', 'ar_id']); } } if ($this->fld_tags) { $this->addTables('tag_summary'); $this->addJoinConds(['tag_summary' => ['LEFT JOIN', ['ar_rev_id=ts_rev_id']]]); $this->addFields('ts_tags'); } if (!is_null($params['tag'])) { $this->addTables('change_tag'); $this->addJoinConds(['change_tag' => ['INNER JOIN', ['ar_rev_id=ct_rev_id']]]); $this->addWhereFld('ct_tag', $params['tag']); } if ($this->fetchContent) { // Modern MediaWiki has the content for deleted revs in the 'text' // table using fields old_text and old_flags. But revisions deleted // pre-1.5 store the content in the 'archive' table directly using // fields ar_text and ar_flags, and no corresponding 'text' row. So // we have to LEFT JOIN and fetch all four fields. $this->addTables('text'); $this->addJoinConds(['text' => ['LEFT JOIN', ['ar_text_id=old_id']]]); $this->addFields(['ar_text', 'ar_flags', 'old_text', 'old_flags']); // This also means stricter restrictions if (!$user->isAllowedAny('undelete', 'deletedtext')) { $this->dieUsage('You don\'t have permission to view deleted revision content', 'permissiondenied'); } } $miser_ns = null; if ($mode == 'all') { if ($params['namespace'] !== null) { $namespaces = $params['namespace']; } else { $namespaces = MWNamespace::getValidNamespaces(); } $this->addWhereFld('ar_namespace', $namespaces); // For from/to/prefix, we have to consider the potential // transformations of the title in all specified namespaces. // Generally there will be only one transformation, but wikis with // some namespaces case-sensitive could have two. if ($params['from'] !== null || $params['to'] !== null) { $isDirNewer = $dir === 'newer'; $after = $isDirNewer ? '>=' : '<='; $before = $isDirNewer ? '<=' : '>='; $where = []; foreach ($namespaces as $ns) { $w = []; if ($params['from'] !== null) { $w[] = 'ar_title' . $after . $db->addQuotes($this->titlePartToKey($params['from'], $ns)); } if ($params['to'] !== null) { $w[] = 'ar_title' . $before . $db->addQuotes($this->titlePartToKey($params['to'], $ns)); } $w = $db->makeList($w, LIST_AND); $where[$w][] = $ns; } if (count($where) == 1) { $where = key($where); $this->addWhere($where); } else { $where2 = []; foreach ($where as $w => $ns) { $where2[] = $db->makeList([$w, 'ar_namespace' => $ns], LIST_AND); } $this->addWhere($db->makeList($where2, LIST_OR)); } } if (isset($params['prefix'])) { $where = []; foreach ($namespaces as $ns) { $w = 'ar_title' . $db->buildLike($this->titlePartToKey($params['prefix'], $ns), $db->anyString()); $where[$w][] = $ns; } if (count($where) == 1) { $where = key($where); $this->addWhere($where); } else { $where2 = []; foreach ($where as $w => $ns) { $where2[] = $db->makeList([$w, 'ar_namespace' => $ns], LIST_AND); } $this->addWhere($db->makeList($where2, LIST_OR)); } } } else { if ($this->getConfig()->get('MiserMode')) { $miser_ns = $params['namespace']; } else { $this->addWhereFld('ar_namespace', $params['namespace']); } $this->addTimestampWhereRange('ar_timestamp', $dir, $params['start'], $params['end']); } if (!is_null($params['user'])) { $this->addWhereFld('ar_user_text', $params['user']); } elseif (!is_null($params['excludeuser'])) { $this->addWhere('ar_user_text != ' . $db->addQuotes($params['excludeuser'])); } if (!is_null($params['user']) || !is_null($params['excludeuser'])) { // Paranoia: avoid brute force searches (bug 17342) // (shouldn't be able to get here without 'deletedhistory', but // check it again just in case) if (!$user->isAllowed('deletedhistory')) { $bitmask = Revision::DELETED_USER; } elseif (!$user->isAllowedAny('suppressrevision', 'viewsuppressed')) { $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; } else { $bitmask = 0; } if ($bitmask) { $this->addWhere($db->bitAnd('ar_deleted', $bitmask) . " != {$bitmask}"); } } if (!is_null($params['continue'])) { $cont = explode('|', $params['continue']); $op = $dir == 'newer' ? '>' : '<'; if ($optimizeGenerateTitles) { $this->dieContinueUsageIf(count($cont) != 2); $ns = intval($cont[0]); $this->dieContinueUsageIf(strval($ns) !== $cont[0]); $title = $db->addQuotes($cont[1]); $this->addWhere("ar_namespace {$op} {$ns} OR " . "(ar_namespace = {$ns} AND ar_title {$op}= {$title})"); } elseif ($mode == 'all') { $this->dieContinueUsageIf(count($cont) != 4); $ns = intval($cont[0]); $this->dieContinueUsageIf(strval($ns) !== $cont[0]); $title = $db->addQuotes($cont[1]); $ts = $db->addQuotes($db->timestamp($cont[2])); $ar_id = (int) $cont[3]; $this->dieContinueUsageIf(strval($ar_id) !== $cont[3]); $this->addWhere("ar_namespace {$op} {$ns} OR " . "(ar_namespace = {$ns} AND " . "(ar_title {$op} {$title} OR " . "(ar_title = {$title} AND " . "(ar_timestamp {$op} {$ts} OR " . "(ar_timestamp = {$ts} AND " . "ar_id {$op}= {$ar_id})))))"); } else { $this->dieContinueUsageIf(count($cont) != 2); $ts = $db->addQuotes($db->timestamp($cont[0])); $ar_id = (int) $cont[1]; $this->dieContinueUsageIf(strval($ar_id) !== $cont[1]); $this->addWhere("ar_timestamp {$op} {$ts} OR " . "(ar_timestamp = {$ts} AND " . "ar_id {$op}= {$ar_id})"); } } $this->addOption('LIMIT', $this->limit + 1); $sort = $dir == 'newer' ? '' : ' DESC'; $orderby = []; if ($optimizeGenerateTitles) { // Targeting index name_title_timestamp if ($params['namespace'] === null || count(array_unique($params['namespace'])) > 1) { $orderby[] = "ar_namespace {$sort}"; } $orderby[] = "ar_title {$sort}"; } elseif ($mode == 'all') { // Targeting index name_title_timestamp if ($params['namespace'] === null || count(array_unique($params['namespace'])) > 1) { $orderby[] = "ar_namespace {$sort}"; } $orderby[] = "ar_title {$sort}"; $orderby[] = "ar_timestamp {$sort}"; $orderby[] = "ar_id {$sort}"; } else { // Targeting index usertext_timestamp // 'user' is always constant. $orderby[] = "ar_timestamp {$sort}"; $orderby[] = "ar_id {$sort}"; } $this->addOption('ORDER BY', $orderby); $res = $this->select(__METHOD__); $pageMap = []; // Maps ns&title to array index $count = 0; $nextIndex = 0; $generated = []; foreach ($res as $row) { if (++$count > $this->limit) { // We've had enough if ($optimizeGenerateTitles) { $this->setContinueEnumParameter('continue', "{$row->ar_namespace}|{$row->ar_title}"); } elseif ($mode == 'all') { $this->setContinueEnumParameter('continue', "{$row->ar_namespace}|{$row->ar_title}|{$row->ar_timestamp}|{$row->ar_id}"); } else { $this->setContinueEnumParameter('continue', "{$row->ar_timestamp}|{$row->ar_id}"); } break; } // Miser mode namespace check if ($miser_ns !== null && !in_array($row->ar_namespace, $miser_ns)) { continue; } if ($resultPageSet !== null) { if ($params['generatetitles']) { $key = "{$row->ar_namespace}:{$row->ar_title}"; if (!isset($generated[$key])) { $generated[$key] = Title::makeTitle($row->ar_namespace, $row->ar_title); } } else { $generated[] = $row->ar_rev_id; } } else { $revision = Revision::newFromArchiveRow($row); $rev = $this->extractRevisionInfo($revision, $row); if (!isset($pageMap[$row->ar_namespace][$row->ar_title])) { $index = $nextIndex++; $pageMap[$row->ar_namespace][$row->ar_title] = $index; $title = $revision->getTitle(); $a = ['pageid' => $title->getArticleID(), 'revisions' => [$rev]]; ApiResult::setIndexedTagName($a['revisions'], 'rev'); ApiQueryBase::addTitleInfo($a, $title); $fit = $result->addValue(['query', $this->getModuleName()], $index, $a); } else { $index = $pageMap[$row->ar_namespace][$row->ar_title]; $fit = $result->addValue(['query', $this->getModuleName(), $index, 'revisions'], null, $rev); } if (!$fit) { if ($mode == 'all') { $this->setContinueEnumParameter('continue', "{$row->ar_namespace}|{$row->ar_title}|{$row->ar_timestamp}|{$row->ar_id}"); } else { $this->setContinueEnumParameter('continue', "{$row->ar_timestamp}|{$row->ar_id}"); } break; } } } if ($resultPageSet !== null) { if ($params['generatetitles']) { $resultPageSet->populateFromTitles($generated); } else { $resultPageSet->populateFromRevisionIDs($generated); } } else { $result->addIndexedTagName(['query', $this->getModuleName()], 'page'); } }
protected function run(ApiPageSet $resultPageSet = null) { $user = $this->getUser(); // Before doing anything at all, let's check permissions if (!$user->isAllowed('deletedhistory')) { $this->dieUsage('You don\'t have permission to view deleted revision information', 'permissiondenied'); } $result = $this->getResult(); $pageSet = $this->getPageSet(); $pageMap = $pageSet->getGoodAndMissingTitlesByNamespace(); $pageCount = count($pageSet->getGoodAndMissingTitles()); $revCount = $pageSet->getRevisionCount(); if ($revCount === 0 && $pageCount === 0) { // Nothing to do return; } if ($revCount !== 0 && count($pageSet->getDeletedRevisionIDs()) === 0) { // Nothing to do, revisions were supplied but none are deleted return; } $params = $this->extractRequestParams(false); $db = $this->getDB(); if (!is_null($params['user']) && !is_null($params['excludeuser'])) { $this->dieUsage('user and excludeuser cannot be used together', 'badparams'); } $this->addTables('archive'); if ($resultPageSet === null) { $this->parseParameters($params); $this->addFields(Revision::selectArchiveFields()); $this->addFields(array('ar_title', 'ar_namespace')); } else { $this->limit = $this->getParameter('limit') ?: 10; $this->addFields(array('ar_title', 'ar_namespace', 'ar_timestamp', 'ar_rev_id', 'ar_id')); } if ($this->fld_tags) { $this->addTables('tag_summary'); $this->addJoinConds(array('tag_summary' => array('LEFT JOIN', array('ar_rev_id=ts_rev_id')))); $this->addFields('ts_tags'); } if (!is_null($params['tag'])) { $this->addTables('change_tag'); $this->addJoinConds(array('change_tag' => array('INNER JOIN', array('ar_rev_id=ct_rev_id')))); $this->addWhereFld('ct_tag', $params['tag']); } if ($this->fetchContent) { // Modern MediaWiki has the content for deleted revs in the 'text' // table using fields old_text and old_flags. But revisions deleted // pre-1.5 store the content in the 'archive' table directly using // fields ar_text and ar_flags, and no corresponding 'text' row. So // we have to LEFT JOIN and fetch all four fields. $this->addTables('text'); $this->addJoinConds(array('text' => array('LEFT JOIN', array('ar_text_id=old_id')))); $this->addFields(array('ar_text', 'ar_flags', 'old_text', 'old_flags')); // This also means stricter restrictions if (!$user->isAllowedAny('undelete', 'deletedtext')) { $this->dieUsage('You don\'t have permission to view deleted revision content', 'permissiondenied'); } } $dir = $params['dir']; if ($revCount !== 0) { $this->addWhere(array('ar_rev_id' => array_keys($pageSet->getDeletedRevisionIDs()))); } else { // We need a custom WHERE clause that matches all titles. $lb = new LinkBatch($pageSet->getGoodAndMissingTitles()); $where = $lb->constructSet('ar', $db); $this->addWhere($where); } if (!is_null($params['user'])) { $this->addWhereFld('ar_user_text', $params['user']); } elseif (!is_null($params['excludeuser'])) { $this->addWhere('ar_user_text != ' . $db->addQuotes($params['excludeuser'])); } if (!is_null($params['user']) || !is_null($params['excludeuser'])) { // Paranoia: avoid brute force searches (bug 17342) // (shouldn't be able to get here without 'deletedhistory', but // check it again just in case) if (!$user->isAllowed('deletedhistory')) { $bitmask = Revision::DELETED_USER; } elseif (!$user->isAllowedAny('suppressrevision', 'viewsuppressed')) { $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; } else { $bitmask = 0; } if ($bitmask) { $this->addWhere($db->bitAnd('ar_deleted', $bitmask) . " != {$bitmask}"); } } if (!is_null($params['continue'])) { $cont = explode('|', $params['continue']); $op = $dir == 'newer' ? '>' : '<'; if ($revCount !== 0) { $this->dieContinueUsageIf(count($cont) != 2); $rev = intval($cont[0]); $this->dieContinueUsageIf(strval($rev) !== $cont[0]); $ar_id = (int) $cont[1]; $this->dieContinueUsageIf(strval($ar_id) !== $cont[1]); $this->addWhere("ar_rev_id {$op} {$rev} OR " . "(ar_rev_id = {$rev} AND " . "ar_id {$op}= {$ar_id})"); } else { $this->dieContinueUsageIf(count($cont) != 4); $ns = intval($cont[0]); $this->dieContinueUsageIf(strval($ns) !== $cont[0]); $title = $db->addQuotes($cont[1]); $ts = $db->addQuotes($db->timestamp($cont[2])); $ar_id = (int) $cont[3]; $this->dieContinueUsageIf(strval($ar_id) !== $cont[3]); $this->addWhere("ar_namespace {$op} {$ns} OR " . "(ar_namespace = {$ns} AND " . "(ar_title {$op} {$title} OR " . "(ar_title = {$title} AND " . "(ar_timestamp {$op} {$ts} OR " . "(ar_timestamp = {$ts} AND " . "ar_id {$op}= {$ar_id})))))"); } } $this->addOption('LIMIT', $this->limit + 1); if ($revCount !== 0) { // Sort by ar_rev_id when querying by ar_rev_id $this->addWhereRange('ar_rev_id', $dir, null, null); } else { // Sort by ns and title in the same order as timestamp for efficiency // But only when not already unique in the query if (count($pageMap) > 1) { $this->addWhereRange('ar_namespace', $dir, null, null); } $oneTitle = key(reset($pageMap)); foreach ($pageMap as $pages) { if (count($pages) > 1 || key($pages) !== $oneTitle) { $this->addWhereRange('ar_title', $dir, null, null); break; } } $this->addTimestampWhereRange('ar_timestamp', $dir, $params['start'], $params['end']); } // Include in ORDER BY for uniqueness $this->addWhereRange('ar_id', $dir, null, null); $res = $this->select(__METHOD__); $count = 0; $generated = array(); foreach ($res as $row) { if (++$count > $this->limit) { // We've had enough $this->setContinueEnumParameter('continue', $revCount ? "{$row->ar_rev_id}|{$row->ar_id}" : "{$row->ar_namespace}|{$row->ar_title}|{$row->ar_timestamp}|{$row->ar_id}"); break; } if ($resultPageSet !== null) { $generated[] = $row->ar_rev_id; } else { if (!isset($pageMap[$row->ar_namespace][$row->ar_title])) { // Was it converted? $title = Title::makeTitle($row->ar_namespace, $row->ar_title); $converted = $pageSet->getConvertedTitles(); if ($title && isset($converted[$title->getPrefixedText()])) { $title = Title::newFromText($converted[$title->getPrefixedText()]); if ($title && isset($pageMap[$title->getNamespace()][$title->getDBkey()])) { $pageMap[$row->ar_namespace][$row->ar_title] = $pageMap[$title->getNamespace()][$title->getDBkey()]; } } } if (!isset($pageMap[$row->ar_namespace][$row->ar_title])) { ApiBase::dieDebug("Found row in archive (ar_id={$row->ar_id}) that didn't " . "get processed by ApiPageSet"); } $fit = $this->addPageSubItem($pageMap[$row->ar_namespace][$row->ar_title], $this->extractRevisionInfo(Revision::newFromArchiveRow($row), $row), 'rev'); if (!$fit) { $this->setContinueEnumParameter('continue', $revCount ? "{$row->ar_rev_id}|{$row->ar_id}" : "{$row->ar_namespace}|{$row->ar_title}|{$row->ar_timestamp}|{$row->ar_id}"); break; } } } if ($resultPageSet !== null) { $resultPageSet->populateFromRevisionIDs($generated); } }