]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/api/ApiQueryBase.php
MediaWiki 1.17.4
[autoinstalls/mediawiki.git] / includes / api / ApiQueryBase.php
1 <?php
2 /**
3  * API for MediaWiki 1.8+
4  *
5  * Created on Sep 7, 2006
6  *
7  * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License along
20  * with this program; if not, write to the Free Software Foundation, Inc.,
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22  * http://www.gnu.org/copyleft/gpl.html
23  *
24  * @file
25  */
26
27 if ( !defined( 'MEDIAWIKI' ) ) {
28         // Eclipse helper - will be ignored in production
29         require_once( 'ApiBase.php' );
30 }
31
32 /**
33  * This is a base class for all Query modules.
34  * It provides some common functionality such as constructing various SQL
35  * queries.
36  *
37  * @ingroup API
38  */
39 abstract class ApiQueryBase extends ApiBase {
40
41         private $mQueryModule, $mDb, $tables, $where, $fields, $options, $join_conds;
42
43         public function __construct( ApiBase $query, $moduleName, $paramPrefix = '' ) {
44                 parent::__construct( $query->getMain(), $moduleName, $paramPrefix );
45                 $this->mQueryModule = $query;
46                 $this->mDb = null;
47                 $this->resetQueryParams();
48         }
49
50         /**
51          * Get the cache mode for the data generated by this module. Override
52          * this in the module subclass. For possible return values and other
53          * details about cache modes, see ApiMain::setCacheMode()
54          *
55          * Public caching will only be allowed if *all* the modules that supply
56          * data for a given request return a cache mode of public.
57          */
58         public function getCacheMode( $params ) {
59                 return 'private';
60         }
61
62         /**
63          * Blank the internal arrays with query parameters
64          */
65         protected function resetQueryParams() {
66                 $this->tables = array();
67                 $this->where = array();
68                 $this->fields = array();
69                 $this->options = array();
70                 $this->join_conds = array();
71         }
72
73         /**
74          * Add a set of tables to the internal array
75          * @param $tables mixed Table name or array of table names
76          * @param $alias mixed Table alias, or null for no alias. Cannot be
77          *  used with multiple tables
78          */
79         protected function addTables( $tables, $alias = null ) {
80                 if ( is_array( $tables ) ) {
81                         if ( !is_null( $alias ) ) {
82                                 ApiBase::dieDebug( __METHOD__, 'Multiple table aliases not supported' );
83                         }
84                         $this->tables = array_merge( $this->tables, $tables );
85                 } else {
86                         if ( !is_null( $alias ) ) {
87                                 $tables = $this->getAliasedName( $tables, $alias );
88                         }
89                         $this->tables[] = $tables;
90                 }
91         }
92
93         /**
94          * Get the SQL for a table name with alias
95          * @param $table string Table name
96          * @param $alias string Alias
97          * @return string SQL
98          */
99         protected function getAliasedName( $table, $alias ) {
100                 return $this->getDB()->tableName( $table ) . ' ' . $alias;
101         }
102
103         /**
104          * Add a set of JOIN conditions to the internal array
105          *
106          * JOIN conditions are formatted as array( tablename => array(jointype,
107          * conditions) e.g. array('page' => array('LEFT JOIN',
108          * 'page_id=rev_page')) . conditions may be a string or an
109          * addWhere()-style array
110          * @param $join_conds array JOIN conditions
111          */
112         protected function addJoinConds( $join_conds ) {
113                 if ( !is_array( $join_conds ) ) {
114                         ApiBase::dieDebug( __METHOD__, 'Join conditions have to be arrays' );
115                 }
116                 $this->join_conds = array_merge( $this->join_conds, $join_conds );
117         }
118
119         /**
120          * Add a set of fields to select to the internal array
121          * @param $value mixed Field name or array of field names
122          */
123         protected function addFields( $value ) {
124                 if ( is_array( $value ) ) {
125                         $this->fields = array_merge( $this->fields, $value );
126                 } else {
127                         $this->fields[] = $value;
128                 }
129         }
130
131         /**
132          * Same as addFields(), but add the fields only if a condition is met
133          * @param $value mixed See addFields()
134          * @param $condition bool If false, do nothing
135          * @return bool $condition
136          */
137         protected function addFieldsIf( $value, $condition ) {
138                 if ( $condition ) {
139                         $this->addFields( $value );
140                         return true;
141                 }
142                 return false;
143         }
144
145         /**
146          * Add a set of WHERE clauses to the internal array.
147          * Clauses can be formatted as 'foo=bar' or array('foo' => 'bar'),
148          * the latter only works if the value is a constant (i.e. not another field)
149          *
150          * If $value is an empty array, this function does nothing.
151          *
152          * For example, array('foo=bar', 'baz' => 3, 'bla' => 'foo') translates
153          * to "foo=bar AND baz='3' AND bla='foo'"
154          * @param $value mixed String or array
155          */
156         protected function addWhere( $value ) {
157                 if ( is_array( $value ) ) {
158                         // Sanity check: don't insert empty arrays,
159                         // Database::makeList() chokes on them
160                         if ( count( $value ) ) {
161                                 $this->where = array_merge( $this->where, $value );
162                         }
163                 } else {
164                         $this->where[] = $value;
165                 }
166         }
167
168         /**
169          * Same as addWhere(), but add the WHERE clauses only if a condition is met
170          * @param $value mixed See addWhere()
171          * @param $condition bool If false, do nothing
172          * @return bool $condition
173          */
174         protected function addWhereIf( $value, $condition ) {
175                 if ( $condition ) {
176                         $this->addWhere( $value );
177                         return true;
178                 }
179                 return false;
180         }
181
182         /**
183          * Equivalent to addWhere(array($field => $value))
184          * @param $field string Field name
185          * @param $value string Value; ignored if null or empty array;
186          */
187         protected function addWhereFld( $field, $value ) {
188                 // Use count() to its full documented capabilities to simultaneously
189                 // test for null, empty array or empty countable object
190                 if ( count( $value ) ) {
191                         $this->where[$field] = $value;
192                 }
193         }
194
195         /**
196          * Add a WHERE clause corresponding to a range, and an ORDER BY
197          * clause to sort in the right direction
198          * @param $field string Field name
199          * @param $dir string If 'newer', sort in ascending order, otherwise
200          *  sort in descending order
201          * @param $start string Value to start the list at. If $dir == 'newer'
202          *  this is the lower boundary, otherwise it's the upper boundary
203          * @param $end string Value to end the list at. If $dir == 'newer' this
204          *  is the upper boundary, otherwise it's the lower boundary
205          * @param $sort bool If false, don't add an ORDER BY clause
206          */
207         protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) {
208                 $isDirNewer = ( $dir === 'newer' );
209                 $after = ( $isDirNewer ? '>=' : '<=' );
210                 $before = ( $isDirNewer ? '<=' : '>=' );
211                 $db = $this->getDB();
212
213                 if ( !is_null( $start ) ) {
214                         $this->addWhere( $field . $after . $db->addQuotes( $start ) );
215                 }
216
217                 if ( !is_null( $end ) ) {
218                         $this->addWhere( $field . $before . $db->addQuotes( $end ) );
219                 }
220
221                 if ( $sort ) {
222                         $order = $field . ( $isDirNewer ? '' : ' DESC' );
223                         if ( !isset( $this->options['ORDER BY'] ) ) {
224                                 $this->addOption( 'ORDER BY', $order );
225                         } else {
226                                 $this->addOption( 'ORDER BY', $this->options['ORDER BY'] . ', ' . $order );
227                         }
228                 }
229         }
230
231         /**
232          * Add an option such as LIMIT or USE INDEX. If an option was set
233          * before, the old value will be overwritten
234          * @param $name string Option name
235          * @param $value string Option value
236          */
237         protected function addOption( $name, $value = null ) {
238                 if ( is_null( $value ) ) {
239                         $this->options[] = $name;
240                 } else {
241                         $this->options[$name] = $value;
242                 }
243         }
244
245         /**
246          * Execute a SELECT query based on the values in the internal arrays
247          * @param $method string Function the query should be attributed to.
248          *  You should usually use __METHOD__ here
249          * @param $extraQuery array Query data to add but not store in the object
250          *  Format is array( 'tables' => ..., 'fields' => ..., 'where' => ..., 'options' => ..., 'join_conds' => ... )
251          * @return ResultWrapper
252          */
253         protected function select( $method, $extraQuery = array() ) {
254
255                 $tables = array_merge( $this->tables, isset( $extraQuery['tables'] ) ? (array)$extraQuery['tables'] : array() );
256                 $fields = array_merge( $this->fields, isset( $extraQuery['fields'] ) ? (array)$extraQuery['fields'] : array() );
257                 $where = array_merge( $this->where, isset( $extraQuery['where'] ) ? (array)$extraQuery['where'] : array() );
258                 $options = array_merge( $this->options, isset( $extraQuery['options'] ) ? (array)$extraQuery['options'] : array() );
259                 $join_conds = array_merge( $this->join_conds, isset( $extraQuery['join_conds'] ) ? (array)$extraQuery['join_conds'] : array() );
260
261                 // getDB has its own profileDBIn/Out calls
262                 $db = $this->getDB();
263
264                 $this->profileDBIn();
265                 $res = $db->select( $tables, $fields, $where, $method, $options, $join_conds );
266                 $this->profileDBOut();
267
268                 return $res;
269         }
270
271         /**
272          * Estimate the row count for the SELECT query that would be run if we
273          * called select() right now, and check if it's acceptable.
274          * @return bool true if acceptable, false otherwise
275          */
276         protected function checkRowCount() {
277                 $db = $this->getDB();
278                 $this->profileDBIn();
279                 $rowcount = $db->estimateRowCount( $this->tables, $this->fields, $this->where, __METHOD__, $this->options );
280                 $this->profileDBOut();
281
282                 global $wgAPIMaxDBRows;
283                 if ( $rowcount > $wgAPIMaxDBRows ) {
284                         return false;
285                 }
286                 return true;
287         }
288
289         /**
290          * Add information (title and namespace) about a Title object to a
291          * result array
292          * @param $arr array Result array à la ApiResult
293          * @param $title Title
294          * @param $prefix string Module prefix
295          */
296         public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
297                 $arr[$prefix . 'ns'] = intval( $title->getNamespace() );
298                 $arr[$prefix . 'title'] = $title->getPrefixedText();
299         }
300
301         /**
302          * Override this method to request extra fields from the pageSet
303          * using $pageSet->requestField('fieldName')
304          * @param $pageSet ApiPageSet
305          */
306         public function requestExtraData( $pageSet ) {
307         }
308
309         /**
310          * Get the main Query module
311          * @return ApiQuery
312          */
313         public function getQuery() {
314                 return $this->mQueryModule;
315         }
316
317         /**
318          * Add a sub-element under the page element with the given page ID
319          * @param $pageId int Page ID
320          * @param $data array Data array à la ApiResult
321          * @return bool Whether the element fit in the result
322          */
323         protected function addPageSubItems( $pageId, $data ) {
324                 $result = $this->getResult();
325                 $result->setIndexedTagName( $data, $this->getModulePrefix() );
326                 return $result->addValue( array( 'query', 'pages', intval( $pageId ) ),
327                         $this->getModuleName(),
328                         $data );
329         }
330
331         /**
332          * Same as addPageSubItems(), but one element of $data at a time
333          * @param $pageId int Page ID
334          * @param $item array Data array à la ApiResult
335          * @param $elemname string XML element name. If null, getModuleName()
336          *  is used
337          * @return bool Whether the element fit in the result
338          */
339         protected function addPageSubItem( $pageId, $item, $elemname = null ) {
340                 if ( is_null( $elemname ) ) {
341                         $elemname = $this->getModulePrefix();
342                 }
343                 $result = $this->getResult();
344                 $fit = $result->addValue( array( 'query', 'pages', $pageId,
345                                          $this->getModuleName() ), null, $item );
346                 if ( !$fit ) {
347                         return false;
348                 }
349                 $result->setIndexedTagName_internal( array( 'query', 'pages', $pageId,
350                                 $this->getModuleName() ), $elemname );
351                 return true;
352         }
353
354         /**
355          * Set a query-continue value
356          * @param $paramName string Parameter name
357          * @param $paramValue string Parameter value
358          */
359         protected function setContinueEnumParameter( $paramName, $paramValue ) {
360                 $paramName = $this->encodeParamName( $paramName );
361                 $msg = array( $paramName => $paramValue );
362                 $this->getResult()->disableSizeCheck();
363                 $this->getResult()->addValue( 'query-continue', $this->getModuleName(), $msg );
364                 $this->getResult()->enableSizeCheck();
365         }
366
367         /**
368          * Get the Query database connection (read-only)
369          * @return Database
370          */
371         protected function getDB() {
372                 if ( is_null( $this->mDb ) ) {
373                         $apiQuery = $this->getQuery();
374                         $this->mDb = $apiQuery->getDB();
375                 }
376                 return $this->mDb;
377         }
378
379         /**
380          * Selects the query database connection with the given name.
381          * See ApiQuery::getNamedDB() for more information
382          * @param $name string Name to assign to the database connection
383          * @param $db int One of the DB_* constants
384          * @param $groups array Query groups
385          * @return Database
386          */
387         public function selectNamedDB( $name, $db, $groups ) {
388                 $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups );
389         }
390
391         /**
392          * Get the PageSet object to work on
393          * @return ApiPageSet
394          */
395         protected function getPageSet() {
396                 return $this->getQuery()->getPageSet();
397         }
398
399         /**
400          * Convert a title to a DB key
401          * @param $title string Page title with spaces
402          * @return string Page title with underscores
403          */
404         public function titleToKey( $title ) {
405                 // Don't throw an error if we got an empty string
406                 if ( trim( $title ) == '' ) {
407                         return '';
408                 }
409                 $t = Title::newFromText( $title );
410                 if ( !$t ) {
411                         $this->dieUsageMsg( array( 'invalidtitle', $title ) );
412                 }
413                 return $t->getPrefixedDbKey();
414         }
415
416         /**
417          * The inverse of titleToKey()
418          * @param $key string Page title with underscores
419          * @return string Page title with spaces
420          */
421         public function keyToTitle( $key ) {
422                 // Don't throw an error if we got an empty string
423                 if ( trim( $key ) == '' ) {
424                         return '';
425                 }
426                 $t = Title::newFromDbKey( $key );
427                 // This really shouldn't happen but we gotta check anyway
428                 if ( !$t ) {
429                         $this->dieUsageMsg( array( 'invalidtitle', $key ) );
430                 }
431                 return $t->getPrefixedText();
432         }
433
434         /**
435          * An alternative to titleToKey() that doesn't trim trailing spaces
436          * @param $titlePart string Title part with spaces
437          * @return string Title part with underscores
438          */
439         public function titlePartToKey( $titlePart ) {
440                 return substr( $this->titleToKey( $titlePart . 'x' ), 0, - 1 );
441         }
442
443         /**
444          * An alternative to keyToTitle() that doesn't trim trailing spaces
445          * @param $keyPart string Key part with spaces
446          * @return string Key part with underscores
447          */
448         public function keyPartToTitle( $keyPart ) {
449                 return substr( $this->keyToTitle( $keyPart . 'x' ), 0, - 1 );
450         }
451
452         /**
453          * Filters hidden users (where the user doesn't have the right to view them)
454          * Also adds relevant block information
455          *
456          * @param bool $showBlockInfo
457          * @return void
458          */
459         public function showHiddenUsersAddBlockInfo( $showBlockInfo ) {
460                 global $wgUser;
461                 $userCanViewHiddenUsers = $wgUser->isAllowed( 'hideuser' );
462
463                 if ( $showBlockInfo || !$userCanViewHiddenUsers ) {
464                         $this->addTables( 'ipblocks' );
465                         $this->addJoinConds( array(
466                                 'ipblocks' => array( 'LEFT JOIN', 'ipb_user=user_id' ),
467                         ) );
468
469                         $this->addFields( 'ipb_deleted' );
470
471                         if ( $showBlockInfo ) {
472                                 $this->addFields( array( 'ipb_reason', 'ipb_by_text', 'ipb_expiry' ) );
473                         }
474
475                         // Don't show hidden names
476                         if ( !$userCanViewHiddenUsers ) {
477                                 $this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' );
478                         }
479                 }
480         }
481
482         public function getPossibleErrors() {
483                 return array_merge( parent::getPossibleErrors(), array(
484                         array( 'invalidtitle', 'title' ),
485                         array( 'invalidtitle', 'key' ),
486                 ) );
487         }
488
489         /**
490          * Get version string for use in the API help output
491          * @return string
492          */
493         public static function getBaseVersion() {
494                 return __CLASS__ . ': $Id$';
495         }
496 }
497
498 /**
499  * @ingroup API
500  */
501 abstract class ApiQueryGeneratorBase extends ApiQueryBase {
502
503         private $mIsGenerator;
504
505         public function __construct( $query, $moduleName, $paramPrefix = '' ) {
506                 parent::__construct( $query, $moduleName, $paramPrefix );
507                 $this->mIsGenerator = false;
508         }
509
510         /**
511          * Switch this module to generator mode. By default, generator mode is
512          * switched off and the module acts like a normal query module.
513          */
514         public function setGeneratorMode() {
515                 $this->mIsGenerator = true;
516         }
517
518         /**
519          * Overrides base class to prepend 'g' to every generator parameter
520          * @param $paramName string Parameter name
521          * @return string Prefixed parameter name
522          */
523         public function encodeParamName( $paramName ) {
524                 if ( $this->mIsGenerator ) {
525                         return 'g' . parent::encodeParamName( $paramName );
526                 } else {
527                         return parent::encodeParamName( $paramName );
528                 }
529         }
530
531         /**
532          * Execute this module as a generator
533          * @param $resultPageSet ApiPageSet: All output should be appended to
534          *  this object
535          */
536         public abstract function executeGenerator( $resultPageSet );
537 }