예제 #1
0
 /**
  * @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);
     }
 }
예제 #3
0
 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');
     }
 }
예제 #5
0
 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);
     }
 }