From: Yuri Astrakhan Date: Mon, 30 Jul 2007 08:09:15 +0000 (+0000) Subject: API: X-Git-Tag: 1.31.0-rc.0~51930 X-Git-Url: http://git.cyclocoop.org/fichier?a=commitdiff_plain;h=ce91d949f78fe99126c161dd999e4ee398c1973c;p=lhc%2Fweb%2Fwiklou.git API: * Added full text search in titles and content (list=search) * (bug 10684) Expanded list=allusers functionality * Possible breaking change: prop=revisions no longer includes pageid for rvprop=ids * Bug fix: proper search escaping for SQL LIKE queries. --- diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 8e0f4cd8b8..f7561f5330 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -380,6 +380,9 @@ Full API documentation is available at http://www.mediawiki.org/wiki/API * Added external url search within wiki pages (list=exturlusage) * Added link enumeration (list=alllinks) * Added registered users enumeration (list=allusers) +* Added full text search in titles and content (list=search) +* (bug 10684) Expanded list=allusers functionality +* Possible breaking change: prop=revisions no longer includes pageid for rvprop=ids == Maintenance script changes since 1.10 == diff --git a/api.php b/api.php index 981910d8e9..fa85573df5 100644 --- a/api.php +++ b/api.php @@ -50,7 +50,7 @@ if (!$wgEnableAPI) { */ $processor = new ApiMain($wgRequest, $wgEnableWriteAPI); -// Generate the output. +// Process data & print results $processor->execute(); // Log what the user did, for book-keeping purposes. diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 0f7f11e0e4..c5b933c9fc 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -328,6 +328,7 @@ function __autoload($className) { 'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php', 'ApiQueryRecentChanges'=> 'includes/api/ApiQueryRecentChanges.php', 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', + 'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php', 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', 'ApiResult' => 'includes/api/ApiResult.php', diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index bf513f3346..f640ece518 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -67,6 +67,7 @@ class ApiQuery extends ApiBase { 'imageusage' => 'ApiQueryBacklinks', 'logevents' => 'ApiQueryLogEvents', 'recentchanges' => 'ApiQueryRecentChanges', + 'search' => 'ApiQuerySearch', 'usercontribs' => 'ApiQueryContributions', 'watchlist' => 'ApiQueryWatchlist', 'exturlusage' => 'ApiQueryExtLinksUsage', diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index a9a27ff410..a0c8766e9a 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -70,7 +70,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { if (!is_null($params['from'])) $this->addWhere('pl_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from']))); if (isset ($params['prefix'])) - $this->addWhere("pl_title LIKE '" . $db->strencode(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); + $this->addWhere("pl_title LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); if (is_null($resultPageSet)) { $this->addFields(array ( diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index a9fcaf0c25..837123c116 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan @gmail.com + * Copyright (C) 2007 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -47,42 +47,107 @@ class ApiQueryAllUsers extends ApiQueryBase { if (!is_null($prop)) { $prop = array_flip($prop); $fld_editcount = isset($prop['editcount']); + $fld_groups = isset($prop['groups']); } else { - $fld_editcount = false; + $fld_editcount = $fld_groups = false; } - $this->addTables('user'); + $limit = $params['limit']; + $tables = $db->tableName('user'); if (!is_null($params['from'])) $this->addWhere('user_name>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from']))); + if (isset($params['prefix'])) + $this->addWhere("user_name LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); + + if (!is_null($params['group'])) { + // Filter only users that belong to a given group + $tblName = $db->tableName('user_groups'); + $tables = "$tables INNER JOIN $tblName ug1 ON ug1.ug_user=user_id"; + $this->addWhereFld('ug1.ug_group', $params['group']); + } + + if ($fld_groups) { + // Show the groups the given users belong to + // request more than needed to avoid not getting all rows that belong to one user + $groupCount = count(User::getAllGroups()); + $sqlLimit = $limit+$groupCount+1; + + $tblName = $db->tableName('user_groups'); + $tables = "$tables LEFT JOIN $tblName ug2 ON ug2.ug_user=user_id"; + $this->addFields('ug2.ug_group ug_group2'); + } else { + $sqlLimit = $limit+1; + } + + $this->addOption('LIMIT', $sqlLimit); + $this->addTables($tables); + $this->addFields('user_name'); $this->addFieldsIf('user_editcount', $fld_editcount); - $limit = $params['limit']; - $this->addOption('LIMIT', $limit+1); $this->addOption('ORDER BY', 'user_name'); $res = $this->select(__METHOD__); $data = array (); $count = 0; - while ($row = $db->fetchObject($res)) { - if (++ $count > $limit) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->user_name)); - break; + $lastUserData = false; + $lastUser = false; + $result = $this->getResult(); + + // + // This loop keeps track of the last entry. + // For each new row, if the new row is for different user then the last, the last entry is added to results. + // Otherwise, the group of the new row is appended to the last entry. + // The setContinue... is more complex because of this, and takes into account the higher sql limit + // to make sure all rows that belong to the same user are received. + // + while (true) { + + $row = $db->fetchObject($res); + $count++; + + if (!$row || $lastUser != $row->user_name) { + // Save the last pass's user data + if (is_array($lastUserData)) + $data[] = $lastUserData; + + // No more rows left + if (!$row) + break; + + if ($count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->user_name)); + break; + } + + // Record new user's data + $lastUser = $row->user_name; + $lastUserData = array( 'name' => $lastUser ); + if ($fld_editcount) + $lastUserData['editcount'] = intval($row->user_editcount); + } - - $vals = array( 'name' => $row->user_name ); - if ($fld_editcount) { - $vals['editcount'] = intval($row->user_editcount); + + if ($sqlLimit == $count) { + // BUG! database contains group name that User::getAllGroups() does not return + // TODO: should handle this more gracefully + ApiBase :: dieDebug(__METHOD__, + 'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function'); + } + + // Add user's group info + if ($fld_groups && !is_null($row->ug_group2)) { + $lastUserData['groups'][] = $row->ug_group2; + $result->setIndexedTagName($lastUserData['groups'], 'g'); } - $data[] = $vals; } + $db->freeResult($res); - $result = $this->getResult(); $result->setIndexedTagName($data, 'u'); $result->addValue('query', $this->getModuleName(), $data); } @@ -90,10 +155,15 @@ class ApiQueryAllUsers extends ApiQueryBase { protected function getAllowedParams() { return array ( 'from' => null, + 'prefix' => null, + 'group' => array( + ApiBase :: PARAM_TYPE => User::getAllGroups() + ), 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array ( - 'editcount' + 'editcount', + 'groups', ) ), 'limit' => array ( @@ -109,8 +179,12 @@ class ApiQueryAllUsers extends ApiQueryBase { protected function getParamDescription() { return array ( 'from' => 'The user name to start enumerating from.', - 'prop' => 'What pieces of information to include', - 'limit' => 'How many total user names to return.' + 'prefix' => 'Search for all page titles that begin with this value.', + 'group' => 'Limit users to a given group name', + 'prop' => array( + 'What pieces of information to include.', + '`groups` property uses more server resources and may return fewer results than the limit.'), + 'limit' => 'How many total user names to return.', ); } diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php index 3ec357defa..3719fe9e10 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllpages.php @@ -63,7 +63,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { if (!is_null($params['from'])) $this->addWhere('page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from']))); if (isset ($params['prefix'])) - $this->addWhere("page_title LIKE '" . $db->strencode(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); + $this->addWhere("page_title LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); if (is_null($resultPageSet)) { $this->addFields(array ( diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index d9669b19bd..6079c8b015 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -52,11 +52,16 @@ abstract class ApiQueryBase extends ApiBase { $this->options = array (); } - protected function addTables($value) { - if (is_array($value)) - $this->tables = array_merge($this->tables, $value); - else - $this->tables[] = $value; + protected function addTables($tables, $alias = null) { + if (is_array($tables)) { + if (!is_null($alias)) + ApiBase :: dieDebug(__METHOD__, 'Multiple table aliases not supported'); + $this->tables = array_merge($this->tables, $tables); + } else { + if (!is_null($alias)) + $tables = $this->getDB()->tableName($tables) . ' ' . $alias; + $this->tables[] = $tables; + } } protected function addFields($value) { diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index 2ebc98aa90..f4f92fc0e3 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -71,8 +71,11 @@ class ApiQueryImageInfo extends ApiQueryBase { if ($fld_timestamp) $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $line->img_timestamp); - if ($fld_user) + if ($fld_user) { $vals['user'] = $line->img_user_text; + if(!$line->img_user) + $vals['anon'] = ''; + } if ($fld_size) { $vals['size'] = $line->img_size; $vals['width'] = $line->img_width; diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 776bb89811..e0a222604b 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -219,7 +219,6 @@ class ApiQueryRevisions extends ApiQueryBase { if ($this->fld_ids) { $vals['revid'] = intval($row->rev_id); - $vals['pageid'] = intval($row->rev_page); // $vals['oldid'] = intval($row->rev_text_id); // todo: should this be exposed? } diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php new file mode 100644 index 0000000000..0352a55d18 --- /dev/null +++ b/includes/api/ApiQuerySearch.php @@ -0,0 +1,151 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * Query module to perform full text search within wiki titles and content + * + * @addtogroup API + */ +class ApiQuerySearch extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'sr'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + + $params = $this->extractRequestParams(); + + $limit = $params['limit']; + $query = $params['search']; + if (is_null($query) || empty($query)) + $this->dieUsage("empty search string is not allowed", 'param-search'); + + $search = SearchEngine::create(); + $search->setLimitOffset( $limit+1, $params['offset'] ); + $search->setNamespaces( $params['namespace'] ); + $search->showRedirects = $params['redirects']; + + if ($params['what'] == 'text') + $matches = $search->searchText( $query ); + else + $matches = $search->searchTitle( $query ); + + $data = array (); + $count = 0; + while( $result = $matches->next() ) { + 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; + } + + $title = $result->getTitle(); + if (is_null($resultPageSet)) { + $data[] = array( + 'ns' => intval($title->getNamespace()), + 'title' => $title->getPrefixedText()); + } else { + $data[] = $title; + } + } + + if (is_null($resultPageSet)) { + $result = $this->getResult(); + $result->setIndexedTagName($data, 'p'); + $result->addValue('query', $this->getModuleName(), $data); + } else { + $resultPageSet->populateFromTitles($data); + } + } + + protected function getAllowedParams() { + return array ( + 'search' => null, + 'namespace' => array ( + ApiBase :: PARAM_DFLT => 0, + ApiBase :: PARAM_TYPE => 'namespace', + ApiBase :: PARAM_ISMULTI => true, + ), + 'what' => array ( + ApiBase :: PARAM_DFLT => 'title', + ApiBase :: PARAM_TYPE => array ( + 'title', + 'text', + ) + ), + 'redirects' => false, + 'offset' => 0, + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ) + ); + } + + protected function getParamDescription() { + return array ( + 'search' => 'Search for all page titles (or content) that has this value.', + 'namespace' => 'The namespace(s) to enumerate.', + 'what' => 'Search inside the text or titles.', + 'redirects' => 'Include redirect pages in the search.', + 'offset' => 'Use this value to continue paging (return by query)', + 'limit' => 'How many total pages to return.' + ); + } + + protected function getDescription() { + return 'Perform a full text search'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=search&srsearch=meaning', + 'api.php?action=query&list=search&srwhat=text&srsearch=meaning', + 'api.php?action=query&generator=search&gsrsearch=meaning&prop=info', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} +