/** * @param ApiPageSet $resultPageSet * @return void */ protected function run(ApiPageSet $resultPageSet = null) { $db = $this->getDB(); $params = $this->extractRequestParams(false); $result = $this->getResult(); $this->requireMaxOneParameter($params, 'user', 'excludeuser'); // Namespace check is likely to be desired, but can't be done // efficiently in SQL. $miser_ns = null; $needPageTable = false; if ($params['namespace'] !== null) { $params['namespace'] = array_unique($params['namespace']); sort($params['namespace']); if ($params['namespace'] != MWNamespace::getValidNamespaces()) { $needPageTable = true; if ($this->getConfig()->get('MiserMode')) { $miser_ns = $params['namespace']; } else { $this->addWhere(array('page_namespace' => $params['namespace'])); } } } $this->addTables('revision'); if ($resultPageSet === null) { $this->parseParameters($params); $this->addTables('page'); $this->addJoinConds(array('page' => array('INNER JOIN', array('rev_page = page_id')))); $this->addFields(Revision::selectFields()); $this->addFields(Revision::selectPageFields()); // Review this depeneding on the outcome of T113901 $this->addOption('STRAIGHT_JOIN'); } else { $this->limit = $this->getParameter('limit') ?: 10; $this->addFields(array('rev_timestamp', 'rev_id')); if ($params['generatetitles']) { $this->addFields(array('rev_page')); } if ($needPageTable) { $this->addTables('page'); $this->addJoinConds(array('page' => array('INNER JOIN', array('rev_page = page_id')))); $this->addFieldsIf(array('page_namespace'), (bool) $miser_ns); // Review this depeneding on the outcome of T113901 $this->addOption('STRAIGHT_JOIN'); } } if ($this->fld_tags) { $this->addTables('tag_summary'); $this->addJoinConds(array('tag_summary' => array('LEFT JOIN', array('rev_id=ts_rev_id')))); $this->addFields('ts_tags'); } if ($this->fetchContent) { $this->addTables('text'); $this->addJoinConds(array('text' => array('INNER JOIN', array('rev_text_id=old_id')))); $this->addFields('old_id'); $this->addFields(Revision::selectTextFields()); } if ($params['user'] !== null) { $id = User::idFromName($params['user']); if ($id) { $this->addWhereFld('rev_user', $id); } else { $this->addWhereFld('rev_user_text', $params['user']); } } elseif ($params['excludeuser'] !== null) { $id = User::idFromName($params['excludeuser']); if ($id) { $this->addWhere('rev_user != ' . $id); } else { $this->addWhere('rev_user_text != ' . $db->addQuotes($params['excludeuser'])); } } if ($params['user'] !== null || $params['excludeuser'] !== null) { // Paranoia: avoid brute force searches (bug 17342) if (!$this->getUser()->isAllowed('deletedhistory')) { $bitmask = Revision::DELETED_USER; } elseif (!$this->getUser()->isAllowedAny('suppressrevision', 'viewsuppressed')) { $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; } else { $bitmask = 0; } if ($bitmask) { $this->addWhere($db->bitAnd('rev_deleted', $bitmask) . " != {$bitmask}"); } } $dir = $params['dir']; if ($params['continue'] !== null) { $op = $dir == 'newer' ? '>' : '<'; $cont = explode('|', $params['continue']); $this->dieContinueUsageIf(count($cont) != 2); $ts = $db->addQuotes($db->timestamp($cont[0])); $rev_id = (int) $cont[1]; $this->dieContinueUsageIf(strval($rev_id) !== $cont[1]); $this->addWhere("rev_timestamp {$op} {$ts} OR " . "(rev_timestamp = {$ts} AND " . "rev_id {$op}= {$rev_id})"); } $this->addOption('LIMIT', $this->limit + 1); $sort = $dir == 'newer' ? '' : ' DESC'; $orderby = array(); // Targeting index rev_timestamp, user_timestamp, or usertext_timestamp // But 'user' is always constant for the latter two, so it doesn't matter here. $orderby[] = "rev_timestamp {$sort}"; $orderby[] = "rev_id {$sort}"; $this->addOption('ORDER BY', $orderby); $res = $this->select(__METHOD__); $pageMap = array(); // Maps rev_page to array index $count = 0; $nextIndex = 0; $generated = array(); foreach ($res as $row) { if (++$count > $this->limit) { // We've had enough $this->setContinueEnumParameter('continue', "{$row->rev_timestamp}|{$row->rev_id}"); break; } // Miser mode namespace check if ($miser_ns !== null && !in_array($row->page_namespace, $miser_ns)) { continue; } if ($resultPageSet !== null) { if ($params['generatetitles']) { $generated[$row->rev_page] = $row->rev_page; } else { $generated[] = $row->rev_id; } } else { $revision = Revision::newFromRow($row); $rev = $this->extractRevisionInfo($revision, $row); if (!isset($pageMap[$row->rev_page])) { $index = $nextIndex++; $pageMap[$row->rev_page] = $index; $title = $revision->getTitle(); $a = array('pageid' => $title->getArticleID(), 'revisions' => array($rev)); ApiResult::setIndexedTagName($a['revisions'], 'rev'); ApiQueryBase::addTitleInfo($a, $title); $fit = $result->addValue(array('query', $this->getModuleName()), $index, $a); } else { $index = $pageMap[$row->rev_page]; $fit = $result->addValue(array('query', $this->getModuleName(), $index, 'revisions'), null, $rev); } if (!$fit) { $this->setContinueEnumParameter('continue', "{$row->rev_timestamp}|{$row->rev_id}"); break; } } } if ($resultPageSet !== null) { if ($params['generatetitles']) { $resultPageSet->populateFromPageIDs($generated); } else { $resultPageSet->populateFromRevisionIDs($generated); } } else { $result->addIndexedTagName(array('query', $this->getModuleName()), 'page'); } }
protected function run(ApiPageSet $resultPageSet = null) { $params = $this->extractRequestParams(false); // If any of those parameters are used, work in 'enumeration' mode. // Enum mode can only be used when exactly one page is provided. // Enumerating revisions on multiple pages make it extremely // difficult to manage continuations and require additional SQL indexes $enumRevMode = !is_null($params['user']) || !is_null($params['excludeuser']) || !is_null($params['limit']) || !is_null($params['startid']) || !is_null($params['endid']) || $params['dir'] === 'newer' || !is_null($params['start']) || !is_null($params['end']); $pageSet = $this->getPageSet(); $pageCount = $pageSet->getGoodTitleCount(); $revCount = $pageSet->getRevisionCount(); // Optimization -- nothing to do if ($revCount === 0 && $pageCount === 0) { // Nothing to do return; } if ($revCount > 0 && count($pageSet->getLiveRevisionIDs()) === 0) { // We're in revisions mode but all given revisions are deleted return; } if ($revCount > 0 && $enumRevMode) { $this->dieUsage('The revids= parameter may not be used with the list options ' . '(limit, startid, endid, dirNewer, start, end).', 'revids'); } if ($pageCount > 1 && $enumRevMode) { $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, ' . 'but the limit, startid, endid, dirNewer, user, excludeuser, start ' . 'and end parameters may only be used on a single page.', 'multpages'); } // In non-enum mode, rvlimit can't be directly used. Use the maximum // allowed value. if (!$enumRevMode) { $this->setParsedLimit = false; $params['limit'] = 'max'; } $db = $this->getDB(); $this->addTables(array('revision', 'page')); $this->addJoinConds(array('page' => array('INNER JOIN', array('page_id = rev_page')))); if ($resultPageSet === null) { $this->parseParameters($params); $this->token = $params['token']; $this->addFields(Revision::selectFields()); if ($this->token !== null || $pageCount > 0) { $this->addFields(Revision::selectPageFields()); } } else { $this->limit = $this->getParameter('limit') ?: 10; $this->addFields(array('rev_id', 'rev_page')); } if ($this->fld_tags) { $this->addTables('tag_summary'); $this->addJoinConds(array('tag_summary' => array('LEFT JOIN', array('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('rev_id=ct_rev_id')))); $this->addWhereFld('ct_tag', $params['tag']); } if ($this->fetchContent) { // For each page we will request, the user must have read rights for that page $user = $this->getUser(); /** @var $title Title */ foreach ($pageSet->getGoodTitles() as $title) { if (!$title->userCan('read', $user)) { $this->dieUsage('The current user is not allowed to read ' . $title->getPrefixedText(), 'accessdenied'); } } $this->addTables('text'); $this->addJoinConds(array('text' => array('INNER JOIN', array('rev_text_id=old_id')))); $this->addFields('old_id'); $this->addFields(Revision::selectTextFields()); } // add user name, if needed if ($this->fld_user) { $this->addTables('user'); $this->addJoinConds(array('user' => Revision::userJoinCond())); $this->addFields(Revision::selectUserFields()); } if ($enumRevMode) { // This is mostly to prevent parameter errors (and optimize SQL?) if (!is_null($params['startid']) && !is_null($params['start'])) { $this->dieUsage('start and startid cannot be used together', 'badparams'); } if (!is_null($params['endid']) && !is_null($params['end'])) { $this->dieUsage('end and endid cannot be used together', 'badparams'); } if (!is_null($params['user']) && !is_null($params['excludeuser'])) { $this->dieUsage('user and excludeuser cannot be used together', 'badparams'); } // Continuing effectively uses startid. But we can't use rvstartid // directly, because there is no way to tell the client to ''not'' // send rvstart if it sent it in the original query. So instead we // send the continuation startid as rvcontinue, and ignore both // rvstart and rvstartid when that is supplied. if (!is_null($params['continue'])) { $params['startid'] = $params['continue']; $params['start'] = null; } // This code makes an assumption that sorting by rev_id and rev_timestamp produces // the same result. This way users may request revisions starting at a given time, // but to page through results use the rev_id returned after each page. // Switching to rev_id removes the potential problem of having more than // one row with the same timestamp for the same page. // The order needs to be the same as start parameter to avoid SQL filesort. if (is_null($params['startid']) && is_null($params['endid'])) { $this->addTimestampWhereRange('rev_timestamp', $params['dir'], $params['start'], $params['end']); } else { $this->addWhereRange('rev_id', $params['dir'], $params['startid'], $params['endid']); // One of start and end can be set // If neither is set, this does nothing $this->addTimestampWhereRange('rev_timestamp', $params['dir'], $params['start'], $params['end'], false); } // There is only one ID, use it $ids = array_keys($pageSet->getGoodTitles()); $this->addWhereFld('rev_page', reset($ids)); if (!is_null($params['user'])) { $this->addWhereFld('rev_user_text', $params['user']); } elseif (!is_null($params['excludeuser'])) { $this->addWhere('rev_user_text != ' . $db->addQuotes($params['excludeuser'])); } if (!is_null($params['user']) || !is_null($params['excludeuser'])) { // Paranoia: avoid brute force searches (bug 17342) if (!$this->getUser()->isAllowed('deletedhistory')) { $bitmask = Revision::DELETED_USER; } elseif (!$this->getUser()->isAllowedAny('suppressrevision', 'viewsuppressed')) { $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; } else { $bitmask = 0; } if ($bitmask) { $this->addWhere($db->bitAnd('rev_deleted', $bitmask) . " != {$bitmask}"); } } } elseif ($revCount > 0) { $revs = $pageSet->getLiveRevisionIDs(); // Get all revision IDs $this->addWhereFld('rev_id', array_keys($revs)); if (!is_null($params['continue'])) { $this->addWhere('rev_id >= ' . intval($params['continue'])); } $this->addOption('ORDER BY', 'rev_id'); } elseif ($pageCount > 0) { $titles = $pageSet->getGoodTitles(); // When working in multi-page non-enumeration mode, // limit to the latest revision only $this->addWhere('page_latest=rev_id'); // Get all page IDs $this->addWhereFld('page_id', array_keys($titles)); // Every time someone relies on equality propagation, god kills a kitten :) $this->addWhereFld('rev_page', array_keys($titles)); if (!is_null($params['continue'])) { $cont = explode('|', $params['continue']); $this->dieContinueUsageIf(count($cont) != 2); $pageid = intval($cont[0]); $revid = intval($cont[1]); $this->addWhere("rev_page > {$pageid} OR " . "(rev_page = {$pageid} AND " . "rev_id >= {$revid})"); } $this->addOption('ORDER BY', array('rev_page', 'rev_id')); } else { ApiBase::dieDebug(__METHOD__, 'param validation?'); } $this->addOption('LIMIT', $this->limit + 1); $count = 0; $generated = array(); $res = $this->select(__METHOD__); foreach ($res as $row) { if (++$count > $this->limit) { // We've reached the one extra which shows that there are // additional pages to be had. Stop here... if ($enumRevMode) { $this->setContinueEnumParameter('continue', intval($row->rev_id)); } elseif ($revCount > 0) { $this->setContinueEnumParameter('continue', intval($row->rev_id)); } else { $this->setContinueEnumParameter('continue', intval($row->rev_page) . '|' . intval($row->rev_id)); } break; } if ($resultPageSet !== null) { $generated[] = $row->rev_id; } else { $revision = new Revision($row); $rev = $this->extractRevisionInfo($revision, $row); if ($this->token !== null) { $title = $revision->getTitle(); $tokenFunctions = $this->getTokenFunctions(); foreach ($this->token as $t) { $val = call_user_func($tokenFunctions[$t], $title->getArticleID(), $title, $revision); if ($val === false) { $this->setWarning("Action '{$t}' is not allowed for the current user"); } else { $rev[$t . 'token'] = $val; } } } $fit = $this->addPageSubItem($row->rev_page, $rev, 'rev'); if (!$fit) { if ($enumRevMode) { $this->setContinueEnumParameter('continue', intval($row->rev_id)); } elseif ($revCount > 0) { $this->setContinueEnumParameter('continue', intval($row->rev_id)); } else { $this->setContinueEnumParameter('continue', intval($row->rev_page) . '|' . intval($row->rev_id)); } break; } } } if ($resultPageSet !== null) { $resultPageSet->populateFromRevisionIDs($generated); } }
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); } }
/** * @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) { $params = $this->extractRequestParams(false); // If any of those parameters are used, work in 'enumeration' mode. // Enum mode can only be used when exactly one page is provided. // Enumerating revisions on multiple pages make it extremely // difficult to manage continuations and require additional SQL indexes $enumRevMode = $params['user'] !== null || $params['excludeuser'] !== null || $params['limit'] !== null || $params['startid'] !== null || $params['endid'] !== null || $params['dir'] === 'newer' || $params['start'] !== null || $params['end'] !== null; $pageSet = $this->getPageSet(); $pageCount = $pageSet->getGoodTitleCount(); $revCount = $pageSet->getRevisionCount(); // Optimization -- nothing to do if ($revCount === 0 && $pageCount === 0) { // Nothing to do return; } if ($revCount > 0 && count($pageSet->getLiveRevisionIDs()) === 0) { // We're in revisions mode but all given revisions are deleted return; } if ($revCount > 0 && $enumRevMode) { $this->dieUsage('The revids= parameter may not be used with the list options ' . '(limit, startid, endid, dirNewer, start, end).', 'revids'); } if ($pageCount > 1 && $enumRevMode) { $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, ' . 'but the limit, startid, endid, dirNewer, user, excludeuser, start ' . 'and end parameters may only be used on a single page.', 'multpages'); } // In non-enum mode, rvlimit can't be directly used. Use the maximum // allowed value. if (!$enumRevMode) { $this->setParsedLimit = false; $params['limit'] = 'max'; } $db = $this->getDB(); $this->addTables(['revision', 'page']); $this->addJoinConds(['page' => ['INNER JOIN', ['page_id = rev_page']]]); if ($resultPageSet === null) { $this->parseParameters($params); $this->token = $params['token']; $this->addFields(Revision::selectFields()); if ($this->token !== null || $pageCount > 0) { $this->addFields(Revision::selectPageFields()); } } else { $this->limit = $this->getParameter('limit') ?: 10; $this->addFields(['rev_id', 'rev_timestamp', 'rev_page']); } if ($this->fld_tags) { $this->addTables('tag_summary'); $this->addJoinConds(['tag_summary' => ['LEFT JOIN', ['rev_id=ts_rev_id']]]); $this->addFields('ts_tags'); } if ($params['tag'] !== null) { $this->addTables('change_tag'); $this->addJoinConds(['change_tag' => ['INNER JOIN', ['rev_id=ct_rev_id']]]); $this->addWhereFld('ct_tag', $params['tag']); } if ($this->fetchContent) { // For each page we will request, the user must have read rights for that page $user = $this->getUser(); /** @var $title Title */ foreach ($pageSet->getGoodTitles() as $title) { if (!$title->userCan('read', $user)) { $this->dieUsage('The current user is not allowed to read ' . $title->getPrefixedText(), 'accessdenied'); } } $this->addTables('text'); $this->addJoinConds(['text' => ['INNER JOIN', ['rev_text_id=old_id']]]); $this->addFields('old_id'); $this->addFields(Revision::selectTextFields()); } // add user name, if needed if ($this->fld_user) { $this->addTables('user'); $this->addJoinConds(['user' => Revision::userJoinCond()]); $this->addFields(Revision::selectUserFields()); } if ($enumRevMode) { // Indexes targeted: // page_timestamp if we don't have rvuser // page_user_timestamp if we have a logged-in rvuser // page_timestamp or usertext_timestamp if we have an IP rvuser // This is mostly to prevent parameter errors (and optimize SQL?) if ($params['startid'] !== null && $params['start'] !== null) { $this->dieUsage('start and startid cannot be used together', 'badparams'); } if ($params['endid'] !== null && $params['end'] !== null) { $this->dieUsage('end and endid cannot be used together', 'badparams'); } if ($params['user'] !== null && $params['excludeuser'] !== null) { $this->dieUsage('user and excludeuser cannot be used together', 'badparams'); } if ($params['continue'] !== null) { $cont = explode('|', $params['continue']); $this->dieContinueUsageIf(count($cont) != 2); $op = $params['dir'] === 'newer' ? '>' : '<'; $continueTimestamp = $db->addQuotes($db->timestamp($cont[0])); $continueId = (int) $cont[1]; $this->dieContinueUsageIf($continueId != $cont[1]); $this->addWhere("rev_timestamp {$op} {$continueTimestamp} OR " . "(rev_timestamp = {$continueTimestamp} AND " . "rev_id {$op}= {$continueId})"); } $this->addTimestampWhereRange('rev_timestamp', $params['dir'], $params['start'], $params['end']); $this->addWhereRange('rev_id', $params['dir'], $params['startid'], $params['endid']); // There is only one ID, use it $ids = array_keys($pageSet->getGoodTitles()); $this->addWhereFld('rev_page', reset($ids)); if ($params['user'] !== null) { $user = User::newFromName($params['user']); if ($user && $user->getId() > 0) { $this->addWhereFld('rev_user', $user->getId()); } else { $this->addWhereFld('rev_user_text', $params['user']); } } elseif ($params['excludeuser'] !== null) { $user = User::newFromName($params['excludeuser']); if ($user && $user->getId() > 0) { $this->addWhere('rev_user != ' . $user->getId()); } else { $this->addWhere('rev_user_text != ' . $db->addQuotes($params['excludeuser'])); } } if ($params['user'] !== null || $params['excludeuser'] !== null) { // Paranoia: avoid brute force searches (bug 17342) if (!$this->getUser()->isAllowed('deletedhistory')) { $bitmask = Revision::DELETED_USER; } elseif (!$this->getUser()->isAllowedAny('suppressrevision', 'viewsuppressed')) { $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; } else { $bitmask = 0; } if ($bitmask) { $this->addWhere($db->bitAnd('rev_deleted', $bitmask) . " != {$bitmask}"); } } } elseif ($revCount > 0) { // Always targets the PRIMARY index $revs = $pageSet->getLiveRevisionIDs(); // Get all revision IDs $this->addWhereFld('rev_id', array_keys($revs)); if ($params['continue'] !== null) { $this->addWhere('rev_id >= ' . intval($params['continue'])); } $this->addOption('ORDER BY', 'rev_id'); } elseif ($pageCount > 0) { // Always targets the rev_page_id index $titles = $pageSet->getGoodTitles(); // When working in multi-page non-enumeration mode, // limit to the latest revision only $this->addWhere('page_latest=rev_id'); // Get all page IDs $this->addWhereFld('page_id', array_keys($titles)); // Every time someone relies on equality propagation, god kills a kitten :) $this->addWhereFld('rev_page', array_keys($titles)); if ($params['continue'] !== null) { $cont = explode('|', $params['continue']); $this->dieContinueUsageIf(count($cont) != 2); $pageid = intval($cont[0]); $revid = intval($cont[1]); $this->addWhere("rev_page > {$pageid} OR " . "(rev_page = {$pageid} AND " . "rev_id >= {$revid})"); } $this->addOption('ORDER BY', ['rev_page', 'rev_id']); } else { ApiBase::dieDebug(__METHOD__, 'param validation?'); } $this->addOption('LIMIT', $this->limit + 1); $count = 0; $generated = []; $hookData = []; $res = $this->select(__METHOD__, [], $hookData); foreach ($res as $row) { if (++$count > $this->limit) { // We've reached the one extra which shows that there are // additional pages to be had. Stop here... if ($enumRevMode) { $this->setContinueEnumParameter('continue', $row->rev_timestamp . '|' . intval($row->rev_id)); } elseif ($revCount > 0) { $this->setContinueEnumParameter('continue', intval($row->rev_id)); } else { $this->setContinueEnumParameter('continue', intval($row->rev_page) . '|' . intval($row->rev_id)); } break; } if ($resultPageSet !== null) { $generated[] = $row->rev_id; } else { $revision = new Revision($row); $rev = $this->extractRevisionInfo($revision, $row); if ($this->token !== null) { $title = $revision->getTitle(); $tokenFunctions = $this->getTokenFunctions(); foreach ($this->token as $t) { $val = call_user_func($tokenFunctions[$t], $title->getArticleID(), $title, $revision); if ($val === false) { $this->setWarning("Action '{$t}' is not allowed for the current user"); } else { $rev[$t . 'token'] = $val; } } } $fit = $this->processRow($row, $rev, $hookData) && $this->addPageSubItem($row->rev_page, $rev, 'rev'); if (!$fit) { if ($enumRevMode) { $this->setContinueEnumParameter('continue', $row->rev_timestamp . '|' . intval($row->rev_id)); } elseif ($revCount > 0) { $this->setContinueEnumParameter('continue', intval($row->rev_id)); } else { $this->setContinueEnumParameter('continue', intval($row->rev_page) . '|' . intval($row->rev_id)); } break; } } } if ($resultPageSet !== null) { $resultPageSet->populateFromRevisionIDs($generated); } }