/** * @param ApiPageSet $resultPageSet */ private function run($resultPageSet = null) { $params = $this->extractRequestParams(); $search = $params['search']; $limit = $params['limit']; $namespaces = $params['namespace']; $searcher = new TitlePrefixSearch(); $titles = $searcher->searchWithVariants($search, $limit, $namespaces); if ($resultPageSet) { $resultPageSet->populateFromTitles($titles); } else { $result = $this->getResult(); foreach ($titles as $title) { if (!$limit--) { break; } $vals = array('ns' => intval($title->getNamespace()), 'title' => $title->getPrefixedText()); if ($title->isSpecialPage()) { $vals['special'] = ''; } else { $vals['pageid'] = intval($title->getArticleId()); } $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals); if (!$fit) { break; } } $result->setIndexedTagName_internal(array('query', $this->getModuleName()), $this->getModulePrefix()); } }
/** * @param ApiPageSet $resultPageSet */ private function run($resultPageSet = null) { $params = $this->extractRequestParams(); $search = $params['search']; $limit = $params['limit']; $namespaces = $params['namespace']; $offset = $params['offset']; $searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); $searchEngine->setLimitOffset($limit + 1, $offset); $searchEngine->setNamespaces($namespaces); $titles = $searchEngine->extractTitles($searchEngine->completionSearchWithVariants($search)); if ($resultPageSet) { $resultPageSet->setRedirectMergePolicy(function (array $current, array $new) { if (!isset($current['index']) || $new['index'] < $current['index']) { $current['index'] = $new['index']; } return $current; }); if (count($titles) > $limit) { $this->setContinueEnumParameter('offset', $offset + $params['limit']); array_pop($titles); } $resultPageSet->populateFromTitles($titles); foreach ($titles as $index => $title) { $resultPageSet->setGeneratorData($title, ['index' => $index + $offset + 1]); } } else { $result = $this->getResult(); $count = 0; foreach ($titles as $title) { if (++$count > $limit) { $this->setContinueEnumParameter('offset', $offset + $params['limit']); break; } $vals = ['ns' => intval($title->getNamespace()), 'title' => $title->getPrefixedText()]; if ($title->isSpecialPage()) { $vals['special'] = true; } else { $vals['pageid'] = intval($title->getArticleID()); } $fit = $result->addValue(['query', $this->getModuleName()], null, $vals); if (!$fit) { $this->setContinueEnumParameter('offset', $offset + $count - 1); break; } } $result->addIndexedTagName(['query', $this->getModuleName()], $this->getModulePrefix()); } }
protected function createPageSetWithRedirect() { $target = Title::makeTitle(NS_MAIN, 'UTRedirectTarget'); $sourceA = Title::makeTitle(NS_MAIN, 'UTRedirectSourceA'); $sourceB = Title::makeTitle(NS_MAIN, 'UTRedirectSourceB'); self::editPage('UTRedirectTarget', 'api page set test'); self::editPage('UTRedirectSourceA', '#REDIRECT [[UTRedirectTarget]]'); self::editPage('UTRedirectSourceB', '#REDIRECT [[UTRedirectTarget]]'); $request = new FauxRequest(['redirects' => 1]); $context = new RequestContext(); $context->setRequest($request); $main = new ApiMain($context); $pageSet = new ApiPageSet($main); $pageSet->setGeneratorData($sourceA, ['index' => 1]); $pageSet->setGeneratorData($sourceB, ['index' => 3]); $pageSet->populateFromTitles([$sourceA, $sourceB]); return [$target, $pageSet]; }
/** * @param ApiPageSet $resultPageSet */ private function run($resultPageSet = null) { $params = $this->extractRequestParams(); $search = $params['search']; $limit = $params['limit']; $namespaces = $params['namespace']; $offset = $params['offset']; $searcher = new TitlePrefixSearch(); $titles = $searcher->searchWithVariants($search, $limit + 1, $namespaces, $offset); if ($resultPageSet) { if (count($titles) > $limit) { $this->setContinueEnumParameter('offset', $offset + $params['limit']); array_pop($titles); } $resultPageSet->populateFromTitles($titles); foreach ($titles as $index => $title) { $resultPageSet->setGeneratorData($title, array('index' => $index + $offset + 1)); } } else { $result = $this->getResult(); $count = 0; foreach ($titles as $title) { if (++$count > $limit) { $this->setContinueEnumParameter('offset', $offset + $params['limit']); break; } $vals = array('ns' => intval($title->getNamespace()), 'title' => $title->getPrefixedText()); if ($title->isSpecialPage()) { $vals['special'] = true; } else { $vals['pageid'] = intval($title->getArticleId()); } $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals); if (!$fit) { $this->setContinueEnumParameter('offset', $offset + $count - 1); break; } } $result->addIndexedTagName(array('query', $this->getModuleName()), $this->getModulePrefix()); } }
/** * @param ApiPageSet $resultPageSet * @return void */ private function run($resultPageSet = null) { global $wgContLang; $params = $this->extractRequestParams(); // Extract parameters $limit = $params['limit']; $query = $params['search']; $what = $params['what']; $interwiki = $params['interwiki']; $searchInfo = array_flip($params['info']); $prop = array_flip($params['prop']); // Deprecated parameters if (isset($prop['hasrelated'])) { $this->logFeatureUsage('action=search&srprop=hasrelated'); $this->setWarning('srprop=hasrelated has been deprecated'); } if (isset($prop['score'])) { $this->logFeatureUsage('action=search&srprop=score'); $this->setWarning('srprop=score has been deprecated'); } // Create search engine instance and set options $search = isset($params['backend']) && $params['backend'] != self::BACKEND_NULL_PARAM ? SearchEngine::create($params['backend']) : SearchEngine::create(); $search->setLimitOffset($limit + 1, $params['offset']); $search->setNamespaces($params['namespace']); $search->setFeatureData('rewrite', (bool) $params['enablerewrites']); $query = $search->transformSearchTerm($query); $query = $search->replacePrefixes($query); // Perform the actual search if ($what == 'text') { $matches = $search->searchText($query); } elseif ($what == 'title') { $matches = $search->searchTitle($query); } elseif ($what == 'nearmatch') { // near matches must receive the user input as provided, otherwise // the near matches within namespaces are lost. $matches = SearchEngine::getNearMatchResultSet($params['search']); } else { // We default to title searches; this is a terrible legacy // of the way we initially set up the MySQL fulltext-based // search engine with separate title and text fields. // In the future, the default should be for a combined index. $what = 'title'; $matches = $search->searchTitle($query); // Not all search engines support a separate title search, // for instance the Lucene-based engine we use on Wikipedia. // In this case, fall back to full-text search (which will // include titles in it!) if (is_null($matches)) { $what = 'text'; $matches = $search->searchText($query); } } if (is_null($matches)) { $this->dieUsage("{$what} search is disabled", "search-{$what}-disabled"); } elseif ($matches instanceof Status && !$matches->isGood()) { $this->dieUsage($matches->getWikiText(), 'search-error'); } if ($resultPageSet === null) { $apiResult = $this->getResult(); // Add search meta data to result if (isset($searchInfo['totalhits'])) { $totalhits = $matches->getTotalHits(); if ($totalhits !== null) { $apiResult->addValue(array('query', 'searchinfo'), 'totalhits', $totalhits); } } if (isset($searchInfo['suggestion']) && $matches->hasSuggestion()) { $apiResult->addValue(array('query', 'searchinfo'), 'suggestion', $matches->getSuggestionQuery()); $apiResult->addValue(array('query', 'searchinfo'), 'suggestionsnippet', $matches->getSuggestionSnippet()); } if (isset($searchInfo['rewrittenquery']) && $matches->hasRewrittenQuery()) { $apiResult->addValue(array('query', 'searchinfo'), 'rewrittenquery', $matches->getQueryAfterRewrite()); $apiResult->addValue(array('query', 'searchinfo'), 'rewrittenquerysnippet', $matches->getQueryAfterRewriteSnippet()); } } // Add the search results to the result $terms = $wgContLang->convertForSearchResult($matches->termMatches()); $titles = array(); $count = 0; $result = $matches->next(); while ($result) { if (++$count > $limit) { // We've reached the one extra which shows that there are // additional items to be had. Stop here... $this->setContinueEnumParameter('offset', $params['offset'] + $params['limit']); break; } // Silently skip broken and missing titles if ($result->isBrokenTitle() || $result->isMissingRevision()) { $result = $matches->next(); continue; } $title = $result->getTitle(); if ($resultPageSet === null) { $vals = array(); ApiQueryBase::addTitleInfo($vals, $title); if (isset($prop['snippet'])) { $vals['snippet'] = $result->getTextSnippet($terms); } if (isset($prop['size'])) { $vals['size'] = $result->getByteSize(); } if (isset($prop['wordcount'])) { $vals['wordcount'] = $result->getWordCount(); } if (isset($prop['timestamp'])) { $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $result->getTimestamp()); } if (isset($prop['titlesnippet'])) { $vals['titlesnippet'] = $result->getTitleSnippet(); } if (isset($prop['categorysnippet'])) { $vals['categorysnippet'] = $result->getCategorySnippet(); } if (!is_null($result->getRedirectTitle())) { if (isset($prop['redirecttitle'])) { $vals['redirecttitle'] = $result->getRedirectTitle()->getPrefixedText(); } if (isset($prop['redirectsnippet'])) { $vals['redirectsnippet'] = $result->getRedirectSnippet(); } } if (!is_null($result->getSectionTitle())) { if (isset($prop['sectiontitle'])) { $vals['sectiontitle'] = $result->getSectionTitle()->getFragment(); } if (isset($prop['sectionsnippet'])) { $vals['sectionsnippet'] = $result->getSectionSnippet(); } } if (isset($prop['isfilematch'])) { $vals['isfilematch'] = $result->isFileMatch(); } // Add item to results and see whether it fits $fit = $apiResult->addValue(array('query', $this->getModuleName()), null, $vals); if (!$fit) { $this->setContinueEnumParameter('offset', $params['offset'] + $count - 1); break; } } else { $titles[] = $title; } $result = $matches->next(); } $hasInterwikiResults = false; $totalhits = null; if ($interwiki && $resultPageSet === null && $matches->hasInterwikiResults()) { foreach ($matches->getInterwikiResults() as $matches) { $matches = $matches->getInterwikiResults(); $hasInterwikiResults = true; // Include number of results if requested if ($resultPageSet === null && isset($searchInfo['totalhits'])) { $totalhits += $matches->getTotalHits(); } $result = $matches->next(); while ($result) { $title = $result->getTitle(); if ($resultPageSet === null) { $vals = array('namespace' => $result->getInterwikiNamespaceText(), 'title' => $title->getText(), 'url' => $title->getFullUrl()); // Add item to results and see whether it fits $fit = $apiResult->addValue(array('query', 'interwiki' . $this->getModuleName(), $result->getInterwikiPrefix()), null, $vals); if (!$fit) { // We hit the limit. We can't really provide any meaningful // pagination info so just bail out break; } } else { $titles[] = $title; } $result = $matches->next(); } } if ($totalhits !== null) { $apiResult->addValue(array('query', 'interwikisearchinfo'), 'totalhits', $totalhits); } } if ($resultPageSet === null) { $apiResult->addIndexedTagName(array('query', $this->getModuleName()), 'p'); if ($hasInterwikiResults) { $apiResult->addIndexedTagName(array('query', 'interwiki' . $this->getModuleName()), 'p'); } } else { $resultPageSet->populateFromTitles($titles); $offset = $params['offset'] + 1; foreach ($titles as $index => $title) { $resultPageSet->setGeneratorData($title, array('index' => $index + $offset)); } } }
/** * @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'); } }