API:
authorYuri Astrakhan <yurik@users.mediawiki.org>
Mon, 30 Jul 2007 08:09:15 +0000 (08:09 +0000)
committerYuri Astrakhan <yurik@users.mediawiki.org>
Mon, 30 Jul 2007 08:09:15 +0000 (08:09 +0000)
* 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.

RELEASE-NOTES
api.php
includes/AutoLoader.php
includes/api/ApiQuery.php
includes/api/ApiQueryAllLinks.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiQueryAllpages.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryRevisions.php
includes/api/ApiQuerySearch.php [new file with mode: 0644]

index 8e0f4cd..f7561f5 100644 (file)
@@ -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 981910d..fa85573 100644 (file)
--- 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.
index 0f7f11e..c5b933c 100644 (file)
@@ -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',
index bf513f3..f640ece 100644 (file)
@@ -67,6 +67,7 @@ class ApiQuery extends ApiBase {
                'imageusage' => 'ApiQueryBacklinks',
                'logevents' => 'ApiQueryLogEvents',
                'recentchanges' => 'ApiQueryRecentChanges',
+               'search' => 'ApiQuerySearch',
                'usercontribs' => 'ApiQueryContributions',
                'watchlist' => 'ApiQueryWatchlist',
                'exturlusage' => 'ApiQueryExtLinksUsage',
index a9a27ff..a0c8766 100644 (file)
@@ -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 (
index a9fcaf0..837123c 100644 (file)
@@ -5,7 +5,7 @@
  *
  * API for MediaWiki 1.8+
  *
- * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * Copyright (C) 2007 Yuri Astrakhan <Firstname><Lastname>@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.',
                );
        }
 
index 3ec357d..3719fe9 100644 (file)
@@ -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 (
index d9669b1..6079c8b 100644 (file)
@@ -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) {
index 2ebc98a..f4f92fc 100644 (file)
@@ -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;
index 776bb89..e0a2226 100644 (file)
@@ -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 (file)
index 0000000..0352a55
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+
+/*
+ * Created on July 30, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Yuri Astrakhan <Firstname><Lastname>@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$';
+       }
+}
+