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