]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/api/ApiBase.php
MediaWiki 1.15.3
[autoinstalls/mediawiki.git] / includes / api / ApiBase.php
1 <?php
2
3 /*
4  * Created on Sep 5, 2006
5  *
6  * API for MediaWiki 1.8+
7  *
8  * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License along
21  * with this program; if not, write to the Free Software Foundation, Inc.,
22  * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23  * http://www.gnu.org/copyleft/gpl.html
24  */
25
26 /**
27  * This abstract class implements many basic API functions, and is the base of
28  * all API classes.
29  * The class functions are divided into several areas of functionality:
30  *
31  * Module parameters: Derived classes can define getAllowedParams() to specify
32  *      which parameters to expect,h ow to parse and validate them.
33  *
34  * Profiling: various methods to allow keeping tabs on various tasks and their
35  *      time costs
36  *
37  * Self-documentation: code to allow the API to document its own state
38  *
39  * @ingroup API
40  */
41 abstract class ApiBase {
42
43         // These constants allow modules to specify exactly how to treat incoming parameters.
44
45         const PARAM_DFLT = 0; // Default value of the parameter
46         const PARAM_ISMULTI = 1; // Boolean, do we accept more than one item for this parameter (e.g.: titles)?
47         const PARAM_TYPE = 2; // Can be either a string type (e.g.: 'integer') or an array of allowed values
48         const PARAM_MAX = 3; // Max value allowed for a parameter. Only applies if TYPE='integer'
49         const PARAM_MAX2 = 4; // Max value allowed for a parameter for bots and sysops. Only applies if TYPE='integer'
50         const PARAM_MIN = 5; // Lowest value allowed for a parameter. Only applies if TYPE='integer'
51         const PARAM_ALLOW_DUPLICATES = 6; // Boolean, do we allow the same value to be set more than once when ISMULTI=true
52
53         const LIMIT_BIG1 = 500; // Fast query, std user limit
54         const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit
55         const LIMIT_SML1 = 50; // Slow query, std user limit
56         const LIMIT_SML2 = 500; // Slow query, bot/sysop limit
57
58         private $mMainModule, $mModuleName, $mModulePrefix;
59
60         /**
61          * Constructor
62          * @param $mainModule ApiMain object
63          * @param $moduleName string Name of this module
64          * @param $modulePrefix string Prefix to use for parameter names
65          */
66         public function __construct($mainModule, $moduleName, $modulePrefix = '') {
67                 $this->mMainModule = $mainModule;
68                 $this->mModuleName = $moduleName;
69                 $this->mModulePrefix = $modulePrefix;
70         }
71
72         /*****************************************************************************
73          * ABSTRACT METHODS                                                          *
74          *****************************************************************************/
75
76         /**
77          * Evaluates the parameters, performs the requested query, and sets up
78          * the result. Concrete implementations of ApiBase must override this
79          * method to provide whatever functionality their module offers.
80          * Implementations must not produce any output on their own and are not
81          * expected to handle any errors.
82          *
83          * The execute() method will be invoked directly by ApiMain immediately
84          * before the result of the module is output. Aside from the
85          * constructor, implementations should assume that no other methods
86          * will be called externally on the module before the result is
87          * processed.
88          *
89          * The result data should be stored in the ApiResult object available
90          * through getResult().
91          */
92         public abstract function execute();
93
94         /**
95          * Returns a string that identifies the version of the extending class.
96          * Typically includes the class name, the svn revision, timestamp, and
97          * last author. Usually done with SVN's Id keyword
98          * @return string
99          */
100         public abstract function getVersion();
101
102         /**
103          * Get the name of the module being executed by this instance
104          * @return string
105          */
106         public function getModuleName() {
107                 return $this->mModuleName;
108         }
109
110         /**
111          * Get parameter prefix (usually two letters or an empty string).
112          * @return string
113          */
114         public function getModulePrefix() {
115                 return $this->mModulePrefix;
116         }
117
118         /**
119          * Get the name of the module as shown in the profiler log
120          * @return string
121          */
122         public function getModuleProfileName($db = false) {
123                 if ($db)
124                         return 'API:' . $this->mModuleName . '-DB';
125                 else
126                         return 'API:' . $this->mModuleName;
127         }
128
129         /**
130          * Get the main module
131          * @return ApiMain object
132          */
133         public function getMain() {
134                 return $this->mMainModule;
135         }
136
137         /**
138          * Returns true if this module is the main module ($this === $this->mMainModule),
139          * false otherwise.
140          * @return bool
141          */
142         public function isMain() {
143                 return $this === $this->mMainModule;
144         }
145
146         /**
147          * Get the result object
148          * @return ApiResult
149          */
150         public function getResult() {
151                 // Main module has getResult() method overriden
152                 // Safety - avoid infinite loop:
153                 if ($this->isMain())
154                         ApiBase :: dieDebug(__METHOD__, 'base method was called on main module. ');
155                 return $this->getMain()->getResult();
156         }
157
158         /**
159          * Get the result data array (read-only)
160          * @return array
161          */
162         public function getResultData() {
163                 return $this->getResult()->getData();
164         }
165
166         /**
167          * Set warning section for this module. Users should monitor this
168          * section to notice any changes in API. Multiple calls to this
169          * function will result in the warning messages being separated by
170          * newlines
171          * @param $warning string Warning message
172          */
173         public function setWarning($warning) {
174                 $data = $this->getResult()->getData();
175                 if(isset($data['warnings'][$this->getModuleName()]))
176                 {
177                         # Don't add duplicate warnings
178                         $warn_regex = preg_quote($warning, '/');
179                         if(preg_match("/{$warn_regex}(\\n|$)/", $data['warnings'][$this->getModuleName()]['*']))
180                                 return;
181                         $oldwarning = $data['warnings'][$this->getModuleName()]['*'];
182                         # If there is a warning already, append it to the existing one
183                         $warning = "$oldwarning\n$warning";
184                         $this->getResult()->unsetValue('warnings', $this->getModuleName());
185                 }
186                 $msg = array();
187                 ApiResult :: setContent($msg, $warning);
188                 $this->getResult()->disableSizeCheck();
189                 $this->getResult()->addValue('warnings', $this->getModuleName(), $msg);
190                 $this->getResult()->enableSizeCheck();
191         }
192
193         /**
194          * If the module may only be used with a certain format module,
195          * it should override this method to return an instance of that formatter.
196          * A value of null means the default format will be used.
197          * @return mixed instance of a derived class of ApiFormatBase, or null
198          */
199         public function getCustomPrinter() {
200                 return null;
201         }
202
203         /**
204          * Generates help message for this module, or false if there is no description
205          * @return mixed string or false
206          */
207         public function makeHelpMsg() {
208
209                 static $lnPrfx = "\n  ";
210
211                 $msg = $this->getDescription();
212
213                 if ($msg !== false) {
214
215                         if (!is_array($msg))
216                                 $msg = array (
217                                         $msg
218                                 );
219                         $msg = $lnPrfx . implode($lnPrfx, $msg) . "\n";
220
221                         if ($this->isReadMode())
222                                 $msg .= "\nThis module requires read rights.";
223                         if ($this->isWriteMode())
224                                 $msg .= "\nThis module requires write rights.";
225                         if ($this->mustBePosted())
226                                 $msg .= "\nThis module only accepts POST requests.";
227                         if ($this->isReadMode() || $this->isWriteMode() ||
228                                         $this->mustBePosted())
229                                 $msg .= "\n";
230
231                         // Parameters
232                         $paramsMsg = $this->makeHelpMsgParameters();
233                         if ($paramsMsg !== false) {
234                                 $msg .= "Parameters:\n$paramsMsg";
235                         }
236
237                         // Examples
238                         $examples = $this->getExamples();
239                         if ($examples !== false) {
240                                 if (!is_array($examples))
241                                         $examples = array (
242                                                 $examples
243                                         );
244                                 $msg .= 'Example' . (count($examples) > 1 ? 's' : '') . ":\n  ";
245                                 $msg .= implode($lnPrfx, $examples) . "\n";
246                         }
247
248                         if ($this->getMain()->getShowVersions()) {
249                                 $versions = $this->getVersion();
250                                 $pattern = '/(\$.*) ([0-9a-z_]+\.php) (.*\$)/i';
251                                 $replacement = '\\0' . "\n    " . 'http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/api/\\2';
252
253                                 if (is_array($versions)) {
254                                         foreach ($versions as &$v)
255                                                 $v = preg_replace($pattern, $replacement, $v);
256                                         $versions = implode("\n  ", $versions);
257                                 }
258                                 else
259                                         $versions = preg_replace($pattern, $replacement, $versions);
260
261                                 $msg .= "Version:\n  $versions\n";
262                         }
263                 }
264
265                 return $msg;
266         }
267
268         /**
269          * Generates the parameter descriptions for this module, to be displayed in the
270          * module's help.
271          * @return string
272          */
273         public function makeHelpMsgParameters() {
274                 $params = $this->getFinalParams();
275                 if ($params !== false) {
276
277                         $paramsDescription = $this->getFinalParamDescription();
278                         $msg = '';
279                         $paramPrefix = "\n" . str_repeat(' ', 19);
280                         foreach ($params as $paramName => $paramSettings) {
281                                 $desc = isset ($paramsDescription[$paramName]) ? $paramsDescription[$paramName] : '';
282                                 if (is_array($desc))
283                                         $desc = implode($paramPrefix, $desc);
284
285                                 $type = isset($paramSettings[self :: PARAM_TYPE])? $paramSettings[self :: PARAM_TYPE] : null;
286                                 if (isset ($type)) {
287                                         if (isset ($paramSettings[self :: PARAM_ISMULTI]))
288                                                 $prompt = 'Values (separate with \'|\'): ';
289                                         else
290                                                 $prompt = 'One value: ';
291
292                                         if (is_array($type)) {
293                                                 $choices = array();
294                                                 $nothingPrompt = false;
295                                                 foreach ($type as $t)
296                                                         if ($t === '')
297                                                                 $nothingPrompt = 'Can be empty, or ';
298                                                         else
299                                                                 $choices[] =  $t;
300                                                 $desc .= $paramPrefix . $nothingPrompt . $prompt . implode(', ', $choices);
301                                         } else {
302                                                 switch ($type) {
303                                                         case 'namespace':
304                                                                 // Special handling because namespaces are type-limited, yet they are not given
305                                                                 $desc .= $paramPrefix . $prompt . implode(', ', ApiBase :: getValidNamespaces());
306                                                                 break;
307                                                         case 'limit':
308                                                                 $desc .= $paramPrefix . "No more than {$paramSettings[self :: PARAM_MAX]} ({$paramSettings[self :: PARAM_MAX2]} for bots) allowed.";
309                                                                 break;
310                                                         case 'integer':
311                                                                 $hasMin = isset($paramSettings[self :: PARAM_MIN]);
312                                                                 $hasMax = isset($paramSettings[self :: PARAM_MAX]);
313                                                                 if ($hasMin || $hasMax) {
314                                                                         if (!$hasMax)
315                                                                                 $intRangeStr = "The value must be no less than {$paramSettings[self :: PARAM_MIN]}";
316                                                                         elseif (!$hasMin)
317                                                                                 $intRangeStr = "The value must be no more than {$paramSettings[self :: PARAM_MAX]}";
318                                                                         else
319                                                                                 $intRangeStr = "The value must be between {$paramSettings[self :: PARAM_MIN]} and {$paramSettings[self :: PARAM_MAX]}";
320
321                                                                         $desc .= $paramPrefix . $intRangeStr;
322                                                                 }
323                                                                 break;
324                                                 }
325                                         }
326                                 }
327
328                                 $default = is_array($paramSettings) ? (isset ($paramSettings[self :: PARAM_DFLT]) ? $paramSettings[self :: PARAM_DFLT] : null) : $paramSettings;
329                                 if (!is_null($default) && $default !== false)
330                                         $desc .= $paramPrefix . "Default: $default";
331
332                                 $msg .= sprintf("  %-14s - %s\n", $this->encodeParamName($paramName), $desc);
333                         }
334                         return $msg;
335
336                 } else
337                         return false;
338         }
339
340         /**
341          * Returns the description string for this module
342          * @return mixed string or array of strings
343          */
344         protected function getDescription() {
345                 return false;
346         }
347
348         /**
349          * Returns usage examples for this module. Return null if no examples are available.
350          * @return mixed string or array of strings
351          */
352         protected function getExamples() {
353                 return false;
354         }
355
356         /**
357          * Returns an array of allowed parameters (parameter name) => (default
358          * value) or (parameter name) => (array with PARAM_* constants as keys)
359          * Don't call this function directly: use getFinalParams() to allow
360          * hooks to modify parameters as needed.
361          * @return array
362          */
363         protected function getAllowedParams() {
364                 return false;
365         }
366
367         /**
368          * Returns an array of parameter descriptions.
369          * Don't call this functon directly: use getFinalParamDescription() to
370          * allow hooks to modify descriptions as needed.
371          * @return array
372          */
373         protected function getParamDescription() {
374                 return false;
375         }
376         
377         /**
378          * Get final list of parameters, after hooks have had a chance to
379          * tweak it as needed.
380          * @return array
381          */
382         public function getFinalParams() {
383                 $params = $this->getAllowedParams();
384                 wfRunHooks('APIGetAllowedParams', array(&$this, &$params));
385                 return $params;
386         }
387
388         /**
389          * Get final description, after hooks have had a chance to tweak it as
390          * needed.
391          * @return array
392          */
393         public function getFinalParamDescription() {
394                 $desc = $this->getParamDescription();
395                 wfRunHooks('APIGetParamDescription', array(&$this, &$desc));
396                 return $desc;
397         }
398
399         /**
400          * This method mangles parameter name based on the prefix supplied to the constructor.
401          * Override this method to change parameter name during runtime
402          * @param $paramName string Parameter name
403          * @return string Prefixed parameter name
404          */
405         public function encodeParamName($paramName) {
406                 return $this->mModulePrefix . $paramName;
407         }
408
409         /**
410         * Using getAllowedParams(), this function makes an array of the values
411         * provided by the user, with key being the name of the variable, and
412         * value - validated value from user or default. limit=max will not be
413         * parsed if $parseMaxLimit is set to false; use this when the max
414         * limit is not definitive yet, e.g. when getting revisions.
415         * @param $parseMaxLimit bool
416         * @return array
417         */
418         public function extractRequestParams($parseMaxLimit = true) {
419                 $params = $this->getFinalParams();
420                 $results = array ();
421
422                 foreach ($params as $paramName => $paramSettings)
423                         $results[$paramName] = $this->getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit);
424
425                 return $results;
426         }
427
428         /**
429          * Get a value for the given parameter
430          * @param $paramName string Parameter name
431          * @param $parseMaxLimit bool see extractRequestParams()
432          * @return mixed Parameter value
433          */
434         protected function getParameter($paramName, $parseMaxLimit = true) {
435                 $params = $this->getFinalParams();
436                 $paramSettings = $params[$paramName];
437                 return $this->getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit);
438         }
439         
440         /**
441          * Die if none or more than one of a certain set of parameters is set
442          * @param $params array of parameter names
443          */
444         public function requireOnlyOneParameter($params) {
445                 $required = func_get_args();
446                 array_shift($required);
447                 
448                 $intersection = array_intersect(array_keys(array_filter($params,
449                                 create_function('$x', 'return !is_null($x);')
450                         )), $required);
451                 if (count($intersection) > 1) {
452                         $this->dieUsage('The parameters '.implode(', ', $intersection).' can not be used together', 'invalidparammix');
453                 } elseif (count($intersection) == 0) {
454                         $this->dieUsage('One of the parameters '.implode(', ', $required).' is required', 'missingparam');
455                 }
456         }
457
458         /**
459          * Returns an array of the namespaces (by integer id) that exist on the
460          * wiki. Used primarily in help documentation.
461          * @return array
462          */
463         public static function getValidNamespaces() {
464                 static $mValidNamespaces = null;
465                 if (is_null($mValidNamespaces)) {
466
467                         global $wgContLang;
468                         $mValidNamespaces = array ();
469                         foreach (array_keys($wgContLang->getNamespaces()) as $ns) {
470                                 if ($ns >= 0)
471                                         $mValidNamespaces[] = $ns;
472                         }
473                 }
474                 return $mValidNamespaces;
475         }
476
477         /**
478          * Using the settings determine the value for the given parameter
479          *
480          * @param $paramName String: parameter name
481          * @param $paramSettings Mixed: default value or an array of settings
482          *  using PARAM_* constants.
483          * @param $parseMaxLimit Boolean: parse limit when max is given?
484          * @return mixed Parameter value
485          */
486         protected function getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit) {
487
488                 // Some classes may decide to change parameter names
489                 $encParamName = $this->encodeParamName($paramName);
490
491                 if (!is_array($paramSettings)) {
492                         $default = $paramSettings;
493                         $multi = false;
494                         $type = gettype($paramSettings);
495                         $dupes = false;
496                 } else {
497                         $default = isset ($paramSettings[self :: PARAM_DFLT]) ? $paramSettings[self :: PARAM_DFLT] : null;
498                         $multi = isset ($paramSettings[self :: PARAM_ISMULTI]) ? $paramSettings[self :: PARAM_ISMULTI] : false;
499                         $type = isset ($paramSettings[self :: PARAM_TYPE]) ? $paramSettings[self :: PARAM_TYPE] : null;
500                         $dupes = isset ($paramSettings[self:: PARAM_ALLOW_DUPLICATES]) ? $paramSettings[self :: PARAM_ALLOW_DUPLICATES] : false;
501
502                         // When type is not given, and no choices, the type is the same as $default
503                         if (!isset ($type)) {
504                                 if (isset ($default))
505                                         $type = gettype($default);
506                                 else
507                                         $type = 'NULL'; // allow everything
508                         }
509                 }
510
511                 if ($type == 'boolean') {
512                         if (isset ($default) && $default !== false) {
513                                 // Having a default value of anything other than 'false' is pointless
514                                 ApiBase :: dieDebug(__METHOD__, "Boolean param $encParamName's default is set to '$default'");
515                         }
516
517                         $value = $this->getMain()->getRequest()->getCheck($encParamName);
518                 } else {
519                         $value = $this->getMain()->getRequest()->getVal($encParamName, $default);
520
521                         if (isset ($value) && $type == 'namespace')
522                                 $type = ApiBase :: getValidNamespaces();
523                 }
524
525                 if (isset ($value) && ($multi || is_array($type)))
526                         $value = $this->parseMultiValue($encParamName, $value, $multi, is_array($type) ? $type : null);
527
528                 // More validation only when choices were not given
529                 // choices were validated in parseMultiValue()
530                 if (isset ($value)) {
531                         if (!is_array($type)) {
532                                 switch ($type) {
533                                         case 'NULL' : // nothing to do
534                                                 break;
535                                         case 'string' : // nothing to do
536                                                 break;
537                                         case 'integer' : // Force everything using intval() and optionally validate limits
538
539                                                 $value = is_array($value) ? array_map('intval', $value) : intval($value);
540                                                 $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : null;
541                                                 $max = isset ($paramSettings[self :: PARAM_MAX]) ? $paramSettings[self :: PARAM_MAX] : null;
542
543                                                 if (!is_null($min) || !is_null($max)) {
544                                                         $values = is_array($value) ? $value : array($value);
545                                                         foreach ($values as $v) {
546                                                                 $this->validateLimit($paramName, $v, $min, $max);
547                                                         }
548                                                 }
549                                                 break;
550                                         case 'limit' :
551                                                 if (!isset ($paramSettings[self :: PARAM_MAX]) || !isset ($paramSettings[self :: PARAM_MAX2]))
552                                                         ApiBase :: dieDebug(__METHOD__, "MAX1 or MAX2 are not defined for the limit $encParamName");
553                                                 if ($multi)
554                                                         ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName");
555                                                 $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : 0;
556                                                 if( $value == 'max' ) {
557                                                         if( $parseMaxLimit ) {
558                                                                 $value = $this->getMain()->canApiHighLimits() ? $paramSettings[self :: PARAM_MAX2] : $paramSettings[self :: PARAM_MAX];
559                                                                 $this->getResult()->addValue( 'limits', $this->getModuleName(), $value );
560                                                                 $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]);
561                                                         }
562                                                 }
563                                                 else {
564                                                         $value = intval($value);
565                                                         $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]);
566                                                 }
567                                                 break;
568                                         case 'boolean' :
569                                                 if ($multi)
570                                                         ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName");
571                                                 break;
572                                         case 'timestamp' :
573                                                 if ($multi)
574                                                         ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName");
575                                                 $value = wfTimestamp(TS_UNIX, $value);
576                                                 if ($value === 0)
577                                                         $this->dieUsage("Invalid value '$value' for timestamp parameter $encParamName", "badtimestamp_{$encParamName}");
578                                                 $value = wfTimestamp(TS_MW, $value);
579                                                 break;
580                                         case 'user' :
581                                                 $title = Title::makeTitleSafe( NS_USER, $value );
582                                                 if ( is_null( $title ) )
583                                                         $this->dieUsage("Invalid value for user parameter $encParamName", "baduser_{$encParamName}");
584                                                 $value = $title->getText();
585                                                 break;
586                                         default :
587                                                 ApiBase :: dieDebug(__METHOD__, "Param $encParamName's type is unknown - $type");
588                                 }
589                         }
590
591                         // Throw out duplicates if requested
592                         if (is_array($value) && !$dupes)
593                                 $value = array_unique($value);
594                 }
595
596                 return $value;
597         }
598
599         /**
600         * Return an array of values that were given in a 'a|b|c' notation,
601         * after it optionally validates them against the list allowed values.
602         *
603         * @param $valueName string The name of the parameter (for error
604         *  reporting)
605         * @param $value mixed The value being parsed
606         * @param $allowMultiple bool Can $value contain more than one value
607         *  separated by '|'?
608         * @param $allowedValues mixed An array of values to check against. If
609         *  null, all values are accepted.
610         * @return mixed (allowMultiple ? an_array_of_values : a_single_value)
611         */
612         protected function parseMultiValue($valueName, $value, $allowMultiple, $allowedValues) {
613                 if( trim($value) === "" && $allowMultiple)
614                         return array();
615                 $sizeLimit = $this->mMainModule->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1;
616                 $valuesList = explode('|', $value, $sizeLimit + 1);
617                 if( self::truncateArray($valuesList, $sizeLimit) ) {
618                         $this->setWarning("Too many values supplied for parameter '$valueName': the limit is $sizeLimit");
619                 }
620                 if (!$allowMultiple && count($valuesList) != 1) {
621                         $possibleValues = is_array($allowedValues) ? "of '" . implode("', '", $allowedValues) . "'" : '';
622                         $this->dieUsage("Only one $possibleValues is allowed for parameter '$valueName'", "multival_$valueName");
623                 }
624                 if (is_array($allowedValues)) {
625                         # Check for unknown values
626                         $unknown = array_diff($valuesList, $allowedValues);
627                         if(count($unknown))
628                         {
629                                 if($allowMultiple)
630                                 {
631                                         $s = count($unknown) > 1 ? "s" : "";
632                                         $vals = implode(", ", $unknown); 
633                                         $this->setWarning("Unrecognized value$s for parameter '$valueName': $vals");
634                                 }
635                                 else
636                                         $this->dieUsage("Unrecognized value for parameter '$valueName': {$valuesList[0]}", "unknown_$valueName");
637                         }
638                         # Now throw them out
639                         $valuesList = array_intersect($valuesList, $allowedValues);
640                 }
641
642                 return $allowMultiple ? $valuesList : $valuesList[0];
643         }
644
645         /**
646          * Validate the value against the minimum and user/bot maximum limits.
647          * Prints usage info on failure.
648          * @param $paramName string Parameter name
649          * @param $value int Parameter value
650          * @param $min int Minimum value
651          * @param $max int Maximum value for users
652          * @param $botMax int Maximum value for sysops/bots
653          */
654         function validateLimit($paramName, $value, $min, $max, $botMax = null) {
655                 if (!is_null($min) && $value < $min) {
656                         $this->dieUsage($this->encodeParamName($paramName) . " may not be less than $min (set to $value)", $paramName);
657                 }
658
659                 // Minimum is always validated, whereas maximum is checked only if not running in internal call mode
660                 if ($this->getMain()->isInternalMode())
661                         return;
662
663                 // Optimization: do not check user's bot status unless really needed -- skips db query
664                 // assumes $botMax >= $max
665                 if (!is_null($max) && $value > $max) {
666                         if (!is_null($botMax) && $this->getMain()->canApiHighLimits()) {
667                                 if ($value > $botMax) {
668                                         $this->dieUsage($this->encodeParamName($paramName) . " may not be over $botMax (set to $value) for bots or sysops", $paramName);
669                                 }
670                         } else {
671                                 $this->dieUsage($this->encodeParamName($paramName) . " may not be over $max (set to $value) for users", $paramName);
672                         }
673                 }
674         }
675         
676         /**
677          * Truncate an array to a certain length.
678          * @param $arr array Array to truncate
679          * @param $limit int Maximum length
680          * @return bool True if the array was truncated, false otherwise
681          */
682         public static function truncateArray(&$arr, $limit)
683         {
684                 $modified = false;
685                 while(count($arr) > $limit)
686                 {
687                         $junk = array_pop($arr);
688                         $modified = true;
689                 }
690                 return $modified;
691         }
692
693         /**
694          * Call the main module's error handler
695          * @param $description string Error text
696          * @param $errorCode string Error code
697          * @param $httpRespCode int HTTP response code
698          */
699         public function dieUsage($description, $errorCode, $httpRespCode = 0) {
700                 wfProfileClose();
701                 throw new UsageException($description, $this->encodeParamName($errorCode), $httpRespCode);
702         }
703
704         /**
705          * Array that maps message keys to error messages. $1 and friends are replaced.
706          */
707         public static $messageMap = array(
708                 // This one MUST be present, or dieUsageMsg() will recurse infinitely
709                 'unknownerror' => array('code' => 'unknownerror', 'info' => "Unknown error: ``\$1''"),
710                 'unknownerror-nocode' => array('code' => 'unknownerror', 'info' => 'Unknown error'),
711
712                 // Messages from Title::getUserPermissionsErrors()
713                 'ns-specialprotected' => array('code' => 'unsupportednamespace', 'info' => "Pages in the Special namespace can't be edited"),
714                 'protectedinterface' => array('code' => 'protectednamespace-interface', 'info' => "You're not allowed to edit interface messages"),
715                 'namespaceprotected' => array('code' => 'protectednamespace', 'info' => "You're not allowed to edit pages in the ``\$1'' namespace"),
716                 'customcssjsprotected' => array('code' => 'customcssjsprotected', 'info' => "You're not allowed to edit custom CSS and JavaScript pages"),
717                 'cascadeprotected' => array('code' => 'cascadeprotected', 'info' =>"The page you're trying to edit is protected because it's included in a cascade-protected page"),
718                 'protectedpagetext' => array('code' => 'protectedpage', 'info' => "The ``\$1'' right is required to edit this page"),
719                 'protect-cantedit' => array('code' => 'cantedit', 'info' => "You can't protect this page because you can't edit it"),
720                 'badaccess-group0' => array('code' => 'permissiondenied', 'info' => "Permission denied"), // Generic permission denied message
721                 'badaccess-groups' => array('code' => 'permissiondenied', 'info' => "Permission denied"),
722                 'titleprotected' => array('code' => 'protectedtitle', 'info' => "This title has been protected from creation"),
723                 'nocreate-loggedin' => array('code' => 'cantcreate', 'info' => "You don't have permission to create new pages"),
724                 'nocreatetext' => array('code' => 'cantcreate-anon', 'info' => "Anonymous users can't create new pages"),
725                 'movenologintext' => array('code' => 'cantmove-anon', 'info' => "Anonymous users can't move pages"),
726                 'movenotallowed' => array('code' => 'cantmove', 'info' => "You don't have permission to move pages"),
727                 'confirmedittext' => array('code' => 'confirmemail', 'info' => "You must confirm your e-mail address before you can edit"),
728                 'blockedtext' => array('code' => 'blocked', 'info' => "You have been blocked from editing"),
729                 'autoblockedtext' => array('code' => 'autoblocked', 'info' => "Your IP address has been blocked automatically, because it was used by a blocked user"),
730
731                 // Miscellaneous interface messages
732                 'actionthrottledtext' => array('code' => 'ratelimited', 'info' => "You've exceeded your rate limit. Please wait some time and try again"),
733                 'alreadyrolled' => array('code' => 'alreadyrolled', 'info' => "The page you tried to rollback was already rolled back"),
734                 'cantrollback' => array('code' => 'onlyauthor', 'info' => "The page you tried to rollback only has one author"),
735                 'readonlytext' => array('code' => 'readonly', 'info' => "The wiki is currently in read-only mode"),
736                 'sessionfailure' => array('code' => 'badtoken', 'info' => "Invalid token"),
737                 'cannotdelete' => array('code' => 'cantdelete', 'info' => "Couldn't delete ``\$1''. Maybe it was deleted already by someone else"),
738                 'notanarticle' => array('code' => 'missingtitle', 'info' => "The page you requested doesn't exist"),
739                 'selfmove' => array('code' => 'selfmove', 'info' => "Can't move a page to itself"),
740                 'immobile_namespace' => array('code' => 'immobilenamespace', 'info' => "You tried to move pages from or to a namespace that is protected from moving"),
741                 'articleexists' => array('code' => 'articleexists', 'info' => "The destination article already exists and is not a redirect to the source article"),
742                 'protectedpage' => array('code' => 'protectedpage', 'info' => "You don't have permission to perform this move"),
743                 'hookaborted' => array('code' => 'hookaborted', 'info' => "The modification you tried to make was aborted by an extension hook"),
744                 'cantmove-titleprotected' => array('code' => 'protectedtitle', 'info' => "The destination article has been protected from creation"),
745                 'imagenocrossnamespace' => array('code' => 'nonfilenamespace', 'info' => "Can't move a file to a non-file namespace"),
746                 'imagetypemismatch' => array('code' => 'filetypemismatch', 'info' => "The new file extension doesn't match its type"),
747                 // 'badarticleerror' => shouldn't happen
748                 // 'badtitletext' => shouldn't happen
749                 'ip_range_invalid' => array('code' => 'invalidrange', 'info' => "Invalid IP range"),
750                 'range_block_disabled' => array('code' => 'rangedisabled', 'info' => "Blocking IP ranges has been disabled"),
751                 'nosuchusershort' => array('code' => 'nosuchuser', 'info' => "The user you specified doesn't exist"),
752                 'badipaddress' => array('code' => 'invalidip', 'info' => "Invalid IP address specified"),
753                 'ipb_expiry_invalid' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time"),
754                 'ipb_already_blocked' => array('code' => 'alreadyblocked', 'info' => "The user you tried to block was already blocked"),
755                 'ipb_blocked_as_range' => array('code' => 'blockedasrange', 'info' => "IP address ``\$1'' was blocked as part of range ``\$2''. You can't unblock the IP invidually, but you can unblock the range as a whole."),
756                 'ipb_cant_unblock' => array('code' => 'cantunblock', 'info' => "The block you specified was not found. It may have been unblocked already"),
757                 'mailnologin' => array('code' => 'cantsend', 'info' => "You're not logged in or you don't have a confirmed e-mail address, so you can't send e-mail"),
758                 'usermaildisabled' => array('code' => 'usermaildisabled', 'info' => "User email has been disabled"),
759                 'blockedemailuser' => array('code' => 'blockedfrommail', 'info' => "You have been blocked from sending e-mail"),
760                 'notarget' => array('code' => 'notarget', 'info' => "You have not specified a valid target for this action"),
761                 'noemail' => array('code' => 'noemail', 'info' => "The user has not specified a valid e-mail address, or has chosen not to receive e-mail from other users"),
762                 'rcpatroldisabled' => array('code' => 'patroldisabled', 'info' => "Patrolling is disabled on this wiki"),
763                 'markedaspatrollederror-noautopatrol' => array('code' => 'noautopatrol', 'info' => "You don't have permission to patrol your own changes"),
764                 'delete-toobig' => array('code' => 'bigdelete', 'info' => "You can't delete this page because it has more than \$1 revisions"),
765                 'movenotallowedfile' => array('code' => 'cantmovefile', 'info' => "You don't have permission to move files"),
766
767                 // API-specific messages
768                 'readrequired' => array('code' => 'readapidenied', 'info' => "You need read permission to use this module"),
769                 'writedisabled' => array('code' => 'noapiwrite', 'info' => "Editing of this wiki through the API is disabled. Make sure the \$wgEnableWriteAPI=true; statement is included in the wiki's LocalSettings.php file"),
770                 'writerequired' => array('code' => 'writeapidenied', 'info' => "You're not allowed to edit this wiki through the API"),
771                 'missingparam' => array('code' => 'no$1', 'info' => "The \$1 parameter must be set"),
772                 'invalidtitle' => array('code' => 'invalidtitle', 'info' => "Bad title ``\$1''"),
773                 'nosuchpageid' => array('code' => 'nosuchpageid', 'info' => "There is no page with ID \$1"),
774                 'nosuchrevid' => array('code' => 'nosuchrevid', 'info' => "There is no revision with ID \$1"),
775                 'invaliduser' => array('code' => 'invaliduser', 'info' => "Invalid username ``\$1''"),
776                 'invalidexpiry' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time ``\$1''"),
777                 'pastexpiry' => array('code' => 'pastexpiry', 'info' => "Expiry time ``\$1'' is in the past"),
778                 'create-titleexists' => array('code' => 'create-titleexists', 'info' => "Existing titles can't be protected with 'create'"),
779                 'missingtitle-createonly' => array('code' => 'missingtitle-createonly', 'info' => "Missing titles can only be protected with 'create'"),
780                 'cantblock' => array('code' => 'cantblock', 'info' => "You don't have permission to block users"),
781                 'canthide' => array('code' => 'canthide', 'info' => "You don't have permission to hide user names from the block log"),
782                 'cantblock-email' => array('code' => 'cantblock-email', 'info' => "You don't have permission to block users from sending e-mail through the wiki"),
783                 'unblock-notarget' => array('code' => 'notarget', 'info' => "Either the id or the user parameter must be set"),
784                 'unblock-idanduser' => array('code' => 'idanduser', 'info' => "The id and user parameters can't be used together"),
785                 'cantunblock' => array('code' => 'permissiondenied', 'info' => "You don't have permission to unblock users"),
786                 'cannotundelete' => array('code' => 'cantundelete', 'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already"),
787                 'permdenied-undelete' => array('code' => 'permissiondenied', 'info' => "You don't have permission to restore deleted revisions"),
788                 'createonly-exists' => array('code' => 'articleexists', 'info' => "The article you tried to create has been created already"),
789                 'nocreate-missing' => array('code' => 'missingtitle', 'info' => "The article you tried to edit doesn't exist"),
790                 'nosuchrcid' => array('code' => 'nosuchrcid', 'info' => "There is no change with rcid ``\$1''"),
791                 'cantpurge' => array('code' => 'cantpurge', 'info' => "Only users with the 'purge' right can purge pages via the API"),
792                 'protect-invalidaction' => array('code' => 'protect-invalidaction', 'info' => "Invalid protection type ``\$1''"),
793                 'protect-invalidlevel' => array('code' => 'protect-invalidlevel', 'info' => "Invalid protection level ``\$1''"),
794                 'toofewexpiries' => array('code' => 'toofewexpiries', 'info' => "\$1 expiry timestamps were provided where \$2 were needed"),
795                 'cantimport' => array('code' => 'cantimport', 'info' => "You don't have permission to import pages"),
796                 'cantimport-upload' => array('code' => 'cantimport-upload', 'info' => "You don't have permission to import uploaded pages"),
797                 'importnofile' => array('code' => 'nofile', 'info' => "You didn't upload a file"),
798                 'importuploaderrorsize' => array('code' => 'filetoobig', 'info' => 'The file you uploaded is bigger than the maximum upload size'),
799                 'importuploaderrorpartial' => array('code' => 'partialupload', 'info' => 'The file was only partially uploaded'),
800                 'importuploaderrortemp' => array('code' => 'notempdir', 'info' => 'The temporary upload directory is missing'),
801                 'importcantopen' => array('code' => 'cantopenfile', 'info' => "Couldn't open the uploaded file"),
802                 'import-noarticle' => array('code' => 'badinterwiki', 'info' => 'Invalid interwiki title specified'),
803                 'importbadinterwiki' => array('code' => 'badinterwiki', 'info' => 'Invalid interwiki title specified'),
804                 'import-unknownerror' => array('code' => 'import-unknownerror', 'info' => "Unknown error on import: ``\$1''"),
805
806                 // ApiEditPage messages
807                 'noimageredirect-anon' => array('code' => 'noimageredirect-anon', 'info' => "Anonymous users can't create image redirects"),
808                 'noimageredirect-logged' => array('code' => 'noimageredirect', 'info' => "You don't have permission to create image redirects"),
809                 'spamdetected' => array('code' => 'spamdetected', 'info' => "Your edit was refused because it contained a spam fragment: ``\$1''"),
810                 'filtered' => array('code' => 'filtered', 'info' => "The filter callback function refused your edit"),
811                 'contenttoobig' => array('code' => 'contenttoobig', 'info' => "The content you supplied exceeds the article size limit of \$1 kilobytes"),
812                 'noedit-anon' => array('code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages"),
813                 'noedit' => array('code' => 'noedit', 'info' => "You don't have permission to edit pages"),
814                 'wasdeleted' => array('code' => 'pagedeleted', 'info' => "The page has been deleted since you fetched its timestamp"),
815                 'blankpage' => array('code' => 'emptypage', 'info' => "Creating new, empty pages is not allowed"),
816                 'editconflict' => array('code' => 'editconflict', 'info' => "Edit conflict detected"),
817                 'hashcheckfailed' => array('code' => 'badmd5', 'info' => "The supplied MD5 hash was incorrect"),
818                 'missingtext' => array('code' => 'notext', 'info' => "One of the text, appendtext, prependtext and undo parameters must be set"),
819                 'emptynewsection' => array('code' => 'emptynewsection', 'info' => 'Creating empty new sections is not possible.'),
820                 'revwrongpage' => array('code' => 'revwrongpage', 'info' => "r\$1 is not a revision of ``\$2''"),
821                 'undo-failure' => array('code' => 'undofailure', 'info' => 'Undo failed due to conflicting intermediate edits'),
822         );
823
824         /**
825          * Output the error message related to a certain array
826          * @param $error array Element of a getUserPermissionsErrors()-style array
827          */
828         public function dieUsageMsg($error) {
829                 $parsed = $this->parseMsg($error);
830                 $this->dieUsage($parsed['info'], $parsed['code']);
831         }
832         
833         /**
834          * Return the error message related to a certain array
835          * @param $error array Element of a getUserPermissionsErrors()-style array
836          * @return array('code' => code, 'info' => info)
837          */
838         public function parseMsg($error) {
839                 $key = array_shift($error);
840                 if(isset(self::$messageMap[$key]))
841                         return array(   'code' =>
842                                 wfMsgReplaceArgs(self::$messageMap[$key]['code'], $error),
843                                         'info' =>
844                                 wfMsgReplaceArgs(self::$messageMap[$key]['info'], $error)
845                         );
846                 // If the key isn't present, throw an "unknown error"
847                 return $this->parseMsg(array('unknownerror', $key));
848         }
849
850         /**
851          * Internal code errors should be reported with this method
852          * @param $method string Method or function name
853          * @param $message string Error message
854          */
855         protected static function dieDebug($method, $message) {
856                 wfDebugDieBacktrace("Internal error in $method: $message");
857         }
858
859         /**
860          * Indicates if this module needs maxlag to be checked
861          * @return bool
862          */
863         public function shouldCheckMaxlag() {
864                 return true;
865         }
866
867         /**
868          * Indicates whether this module requires read rights
869          * @return bool
870          */
871         public function isReadMode() {
872                 return true;
873         }
874         /**
875          * Indicates whether this module requires write mode
876          * @return bool
877          */
878         public function isWriteMode() {
879                 return false;
880         }
881
882         /**
883          * Indicates whether this module must be called with a POST request
884          * @return bool
885          */
886         public function mustBePosted() {
887                 return false;
888         }
889
890
891         /**
892          * Profiling: total module execution time
893          */
894         private $mTimeIn = 0, $mModuleTime = 0;
895
896         /**
897          * Start module profiling
898          */
899         public function profileIn() {
900                 if ($this->mTimeIn !== 0)
901                         ApiBase :: dieDebug(__METHOD__, 'called twice without calling profileOut()');
902                 $this->mTimeIn = microtime(true);
903                 wfProfileIn($this->getModuleProfileName());
904         }
905
906         /**
907          * End module profiling
908          */
909         public function profileOut() {
910                 if ($this->mTimeIn === 0)
911                         ApiBase :: dieDebug(__METHOD__, 'called without calling profileIn() first');
912                 if ($this->mDBTimeIn !== 0)
913                         ApiBase :: dieDebug(__METHOD__, 'must be called after database profiling is done with profileDBOut()');
914
915                 $this->mModuleTime += microtime(true) - $this->mTimeIn;
916                 $this->mTimeIn = 0;
917                 wfProfileOut($this->getModuleProfileName());
918         }
919
920         /**
921          * When modules crash, sometimes it is needed to do a profileOut() regardless
922          * of the profiling state the module was in. This method does such cleanup.
923          */
924         public function safeProfileOut() {
925                 if ($this->mTimeIn !== 0) {
926                         if ($this->mDBTimeIn !== 0)
927                                 $this->profileDBOut();
928                         $this->profileOut();
929                 }
930         }
931
932         /**
933          * Total time the module was executed
934          * @return float
935          */
936         public function getProfileTime() {
937                 if ($this->mTimeIn !== 0)
938                         ApiBase :: dieDebug(__METHOD__, 'called without calling profileOut() first');
939                 return $this->mModuleTime;
940         }
941
942         /**
943          * Profiling: database execution time
944          */
945         private $mDBTimeIn = 0, $mDBTime = 0;
946
947         /**
948          * Start module profiling
949          */
950         public function profileDBIn() {
951                 if ($this->mTimeIn === 0)
952                         ApiBase :: dieDebug(__METHOD__, 'must be called while profiling the entire module with profileIn()');
953                 if ($this->mDBTimeIn !== 0)
954                         ApiBase :: dieDebug(__METHOD__, 'called twice without calling profileDBOut()');
955                 $this->mDBTimeIn = microtime(true);
956                 wfProfileIn($this->getModuleProfileName(true));
957         }
958
959         /**
960          * End database profiling
961          */
962         public function profileDBOut() {
963                 if ($this->mTimeIn === 0)
964                         ApiBase :: dieDebug(__METHOD__, 'must be called while profiling the entire module with profileIn()');
965                 if ($this->mDBTimeIn === 0)
966                         ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBIn() first');
967
968                 $time = microtime(true) - $this->mDBTimeIn;
969                 $this->mDBTimeIn = 0;
970
971                 $this->mDBTime += $time;
972                 $this->getMain()->mDBTime += $time;
973                 wfProfileOut($this->getModuleProfileName(true));
974         }
975
976         /**
977          * Total time the module used the database
978          * @return float
979          */
980         public function getProfileDBTime() {
981                 if ($this->mDBTimeIn !== 0)
982                         ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBOut() first');
983                 return $this->mDBTime;
984         }
985
986         /**
987          * Debugging function that prints a value and an optional backtrace
988          * @param $value mixed Value to print
989          * @param $name string Description of the printed value
990          * @param $backtrace bool If true, print a backtrace
991          */
992         public static function debugPrint($value, $name = 'unknown', $backtrace = false) {
993                 print "\n\n<pre><b>Debugging value '$name':</b>\n\n";
994                 var_export($value);
995                 if ($backtrace)
996                         print "\n" . wfBacktrace();
997                 print "\n</pre>\n";
998         }
999
1000
1001         /**
1002          * Returns a string that identifies the version of this class.
1003          * @return string
1004          */
1005         public static function getBaseVersion() {
1006                 return __CLASS__ . ': $Id: ApiBase.php 50217 2009-05-05 13:12:16Z tstarling $';
1007         }
1008 }