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