API: HTMLize and internationalize the help, add Special:ApiHelp
[mediawiki.git] / includes / api / ApiMain.php
blob836853d925282fbdfa91f23c71a04911ba07ba2a
1 <?php
2 /**
5 * Created on Sep 4, 2006
7 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 * http://www.gnu.org/copyleft/gpl.html
24 * @file
25 * @defgroup API API
28 /**
29 * This is the main API class, used for both external and internal processing.
30 * When executed, it will create the requested formatter object,
31 * instantiate and execute an object associated with the needed action,
32 * and use formatter to print results.
33 * In case of an exception, an error message will be printed using the same formatter.
35 * To use API from another application, run it using FauxRequest object, in which
36 * case any internal exceptions will not be handled but passed up to the caller.
37 * After successful execution, use getResult() for the resulting data.
39 * @ingroup API
41 class ApiMain extends ApiBase {
42 /**
43 * When no format parameter is given, this format will be used
45 const API_DEFAULT_FORMAT = 'xmlfm';
47 /**
48 * List of available modules: action name => module class
50 private static $Modules = array(
51 'login' => 'ApiLogin',
52 'logout' => 'ApiLogout',
53 'createaccount' => 'ApiCreateAccount',
54 'query' => 'ApiQuery',
55 'expandtemplates' => 'ApiExpandTemplates',
56 'parse' => 'ApiParse',
57 'opensearch' => 'ApiOpenSearch',
58 'feedcontributions' => 'ApiFeedContributions',
59 'feedrecentchanges' => 'ApiFeedRecentChanges',
60 'feedwatchlist' => 'ApiFeedWatchlist',
61 'help' => 'ApiHelp',
62 'paraminfo' => 'ApiParamInfo',
63 'rsd' => 'ApiRsd',
64 'compare' => 'ApiComparePages',
65 'tokens' => 'ApiTokens',
67 // Write modules
68 'purge' => 'ApiPurge',
69 'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
70 'rollback' => 'ApiRollback',
71 'delete' => 'ApiDelete',
72 'undelete' => 'ApiUndelete',
73 'protect' => 'ApiProtect',
74 'block' => 'ApiBlock',
75 'unblock' => 'ApiUnblock',
76 'move' => 'ApiMove',
77 'edit' => 'ApiEditPage',
78 'upload' => 'ApiUpload',
79 'filerevert' => 'ApiFileRevert',
80 'emailuser' => 'ApiEmailUser',
81 'watch' => 'ApiWatch',
82 'patrol' => 'ApiPatrol',
83 'import' => 'ApiImport',
84 'clearhasmsg' => 'ApiClearHasMsg',
85 'userrights' => 'ApiUserrights',
86 'options' => 'ApiOptions',
87 'imagerotate' => 'ApiImageRotate',
88 'revisiondelete' => 'ApiRevisionDelete',
91 /**
92 * List of available formats: format name => format class
94 private static $Formats = array(
95 'json' => 'ApiFormatJson',
96 'jsonfm' => 'ApiFormatJson',
97 'php' => 'ApiFormatPhp',
98 'phpfm' => 'ApiFormatPhp',
99 'wddx' => 'ApiFormatWddx',
100 'wddxfm' => 'ApiFormatWddx',
101 'xml' => 'ApiFormatXml',
102 'xmlfm' => 'ApiFormatXml',
103 'yaml' => 'ApiFormatYaml',
104 'yamlfm' => 'ApiFormatYaml',
105 'rawfm' => 'ApiFormatJson',
106 'txt' => 'ApiFormatTxt',
107 'txtfm' => 'ApiFormatTxt',
108 'dbg' => 'ApiFormatDbg',
109 'dbgfm' => 'ApiFormatDbg',
110 'dump' => 'ApiFormatDump',
111 'dumpfm' => 'ApiFormatDump',
112 'none' => 'ApiFormatNone',
115 // @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line
117 * List of user roles that are specifically relevant to the API.
118 * array( 'right' => array ( 'msg' => 'Some message with a $1',
119 * 'params' => array ( $someVarToSubst ) ),
120 * );
122 private static $mRights = array(
123 'writeapi' => array(
124 'msg' => 'right-writeapi',
125 'params' => array()
127 'apihighlimits' => array(
128 'msg' => 'api-help-right-apihighlimits',
129 'params' => array( ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 )
132 // @codingStandardsIgnoreEnd
135 * @var ApiFormatBase
137 private $mPrinter;
139 private $mModuleMgr, $mResult;
140 private $mAction;
141 private $mEnableWrite;
142 private $mInternalMode, $mSquidMaxage, $mModule;
144 private $mCacheMode = 'private';
145 private $mCacheControl = array();
146 private $mParamsUsed = array();
149 * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
151 * @param IContextSource|WebRequest $context If this is an instance of
152 * FauxRequest, errors are thrown and no printing occurs
153 * @param bool $enableWrite Should be set to true if the api may modify data
155 public function __construct( $context = null, $enableWrite = false ) {
156 if ( $context === null ) {
157 $context = RequestContext::getMain();
158 } elseif ( $context instanceof WebRequest ) {
159 // BC for pre-1.19
160 $request = $context;
161 $context = RequestContext::getMain();
163 // We set a derivative context so we can change stuff later
164 $this->setContext( new DerivativeContext( $context ) );
166 if ( isset( $request ) ) {
167 $this->getContext()->setRequest( $request );
170 $this->mInternalMode = ( $this->getRequest() instanceof FauxRequest );
172 // Special handling for the main module: $parent === $this
173 parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
175 if ( !$this->mInternalMode ) {
176 // Impose module restrictions.
177 // If the current user cannot read,
178 // Remove all modules other than login
179 global $wgUser;
181 if ( $this->getVal( 'callback' ) !== null ) {
182 // JSON callback allows cross-site reads.
183 // For safety, strip user credentials.
184 wfDebug( "API: stripping user credentials for JSON callback\n" );
185 $wgUser = new User();
186 $this->getContext()->setUser( $wgUser );
190 $uselang = $this->getParameter( 'uselang' );
191 if ( $uselang === 'user' ) {
192 $uselang = $this->getUser()->getOption( 'language' );
193 $uselang = RequestContext::sanitizeLangCode( $uselang );
194 wfRunHooks( 'UserGetLanguageObject', array( $this->getUser(), &$uselang, $this ) );
196 $code = RequestContext::sanitizeLangCode( $uselang );
197 $this->getContext()->setLanguage( $code );
198 if ( !$this->mInternalMode ) {
199 global $wgLang;
200 $wgLang = RequestContext::getMain()->getLanguage();
203 $config = $this->getConfig();
204 $this->mModuleMgr = new ApiModuleManager( $this );
205 $this->mModuleMgr->addModules( self::$Modules, 'action' );
206 $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
207 $this->mModuleMgr->addModules( self::$Formats, 'format' );
208 $this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
210 $this->mResult = new ApiResult( $this );
211 $this->mEnableWrite = $enableWrite;
213 $this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling()
214 $this->mCommit = false;
218 * Return true if the API was started by other PHP code using FauxRequest
219 * @return bool
221 public function isInternalMode() {
222 return $this->mInternalMode;
226 * Get the ApiResult object associated with current request
228 * @return ApiResult
230 public function getResult() {
231 return $this->mResult;
235 * Get the API module object. Only works after executeAction()
237 * @return ApiBase
239 public function getModule() {
240 return $this->mModule;
244 * Get the result formatter object. Only works after setupExecuteAction()
246 * @return ApiFormatBase
248 public function getPrinter() {
249 return $this->mPrinter;
253 * Set how long the response should be cached.
255 * @param int $maxage
257 public function setCacheMaxAge( $maxage ) {
258 $this->setCacheControl( array(
259 'max-age' => $maxage,
260 's-maxage' => $maxage
261 ) );
265 * Set the type of caching headers which will be sent.
267 * @param string $mode One of:
268 * - 'public': Cache this object in public caches, if the maxage or smaxage
269 * parameter is set, or if setCacheMaxAge() was called. If a maximum age is
270 * not provided by any of these means, the object will be private.
271 * - 'private': Cache this object only in private client-side caches.
272 * - 'anon-public-user-private': Make this object cacheable for logged-out
273 * users, but private for logged-in users. IMPORTANT: If this is set, it must be
274 * set consistently for a given URL, it cannot be set differently depending on
275 * things like the contents of the database, or whether the user is logged in.
277 * If the wiki does not allow anonymous users to read it, the mode set here
278 * will be ignored, and private caching headers will always be sent. In other words,
279 * the "public" mode is equivalent to saying that the data sent is as public as a page
280 * view.
282 * For user-dependent data, the private mode should generally be used. The
283 * anon-public-user-private mode should only be used where there is a particularly
284 * good performance reason for caching the anonymous response, but where the
285 * response to logged-in users may differ, or may contain private data.
287 * If this function is never called, then the default will be the private mode.
289 public function setCacheMode( $mode ) {
290 if ( !in_array( $mode, array( 'private', 'public', 'anon-public-user-private' ) ) ) {
291 wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
293 // Ignore for forwards-compatibility
294 return;
297 if ( !User::isEveryoneAllowed( 'read' ) ) {
298 // Private wiki, only private headers
299 if ( $mode !== 'private' ) {
300 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
302 return;
306 if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
307 // User language is used for i18n, so we don't want to publicly
308 // cache. Anons are ok, because if they have non-default language
309 // then there's an appropriate Vary header set by whatever set
310 // their non-default language.
311 wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
312 "'anon-public-user-private' due to uselang=user\n" );
313 $mode = 'anon-public-user-private';
316 wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
317 $this->mCacheMode = $mode;
321 * Set directives (key/value pairs) for the Cache-Control header.
322 * Boolean values will be formatted as such, by including or omitting
323 * without an equals sign.
325 * Cache control values set here will only be used if the cache mode is not
326 * private, see setCacheMode().
328 * @param array $directives
330 public function setCacheControl( $directives ) {
331 $this->mCacheControl = $directives + $this->mCacheControl;
335 * Create an instance of an output formatter by its name
337 * @param string $format
339 * @return ApiFormatBase
341 public function createPrinterByName( $format ) {
342 $printer = $this->mModuleMgr->getModule( $format, 'format' );
343 if ( $printer === null ) {
344 $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
347 return $printer;
351 * Execute api request. Any errors will be handled if the API was called by the remote client.
353 public function execute() {
354 $this->profileIn();
355 if ( $this->mInternalMode ) {
356 $this->executeAction();
357 } else {
358 $this->executeActionWithErrorHandling();
361 $this->profileOut();
365 * Execute an action, and in case of an error, erase whatever partial results
366 * have been accumulated, and replace it with an error message and a help screen.
368 protected function executeActionWithErrorHandling() {
369 // Verify the CORS header before executing the action
370 if ( !$this->handleCORS() ) {
371 // handleCORS() has sent a 403, abort
372 return;
375 // Exit here if the request method was OPTIONS
376 // (assume there will be a followup GET or POST)
377 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
378 return;
381 // In case an error occurs during data output,
382 // clear the output buffer and print just the error information
383 ob_start();
385 $t = microtime( true );
386 try {
387 $this->executeAction();
388 } catch ( Exception $e ) {
389 $this->handleException( $e );
392 // Log the request whether or not there was an error
393 $this->logRequest( microtime( true ) - $t );
395 // Send cache headers after any code which might generate an error, to
396 // avoid sending public cache headers for errors.
397 $this->sendCacheHeaders();
399 if ( $this->mPrinter->getIsHtml() && !$this->mPrinter->isDisabled() ) {
400 echo wfReportTime();
403 ob_end_flush();
407 * Handle an exception as an API response
409 * @since 1.23
410 * @param Exception $e
412 protected function handleException( Exception $e ) {
413 // Bug 63145: Rollback any open database transactions
414 if ( !( $e instanceof UsageException ) ) {
415 // UsageExceptions are intentional, so don't rollback if that's the case
416 MWExceptionHandler::rollbackMasterChangesAndLog( $e );
419 // Allow extra cleanup and logging
420 wfRunHooks( 'ApiMain::onException', array( $this, $e ) );
422 // Log it
423 if ( !( $e instanceof UsageException ) ) {
424 MWExceptionHandler::logException( $e );
427 // Handle any kind of exception by outputting properly formatted error message.
428 // If this fails, an unhandled exception should be thrown so that global error
429 // handler will process and log it.
431 $errCode = $this->substituteResultWithError( $e );
433 // Error results should not be cached
434 $this->setCacheMode( 'private' );
436 $response = $this->getRequest()->response();
437 $headerStr = 'MediaWiki-API-Error: ' . $errCode;
438 if ( $e->getCode() === 0 ) {
439 $response->header( $headerStr );
440 } else {
441 $response->header( $headerStr, true, $e->getCode() );
444 // Reset and print just the error message
445 ob_clean();
447 // If the error occurred during printing, do a printer->profileOut()
448 $this->mPrinter->safeProfileOut();
449 $this->printResult( true );
453 * Handle an exception from the ApiBeforeMain hook.
455 * This tries to print the exception as an API response, to be more
456 * friendly to clients. If it fails, it will rethrow the exception.
458 * @since 1.23
459 * @param Exception $e
461 public static function handleApiBeforeMainException( Exception $e ) {
462 ob_start();
464 try {
465 $main = new self( RequestContext::getMain(), false );
466 $main->handleException( $e );
467 } catch ( Exception $e2 ) {
468 // Nope, even that didn't work. Punt.
469 throw $e;
472 // Log the request and reset cache headers
473 $main->logRequest( 0 );
474 $main->sendCacheHeaders();
476 ob_end_flush();
480 * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
482 * If no origin parameter is present, nothing happens.
483 * If an origin parameter is present but doesn't match the Origin header, a 403 status code
484 * is set and false is returned.
485 * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
486 * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
487 * headers are set.
489 * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
491 protected function handleCORS() {
492 $originParam = $this->getParameter( 'origin' ); // defaults to null
493 if ( $originParam === null ) {
494 // No origin parameter, nothing to do
495 return true;
498 $request = $this->getRequest();
499 $response = $request->response();
500 // Origin: header is a space-separated list of origins, check all of them
501 $originHeader = $request->getHeader( 'Origin' );
502 if ( $originHeader === false ) {
503 $origins = array();
504 } else {
505 $origins = explode( ' ', $originHeader );
508 if ( !in_array( $originParam, $origins ) ) {
509 // origin parameter set but incorrect
510 // Send a 403 response
511 $message = HttpStatus::getMessage( 403 );
512 $response->header( "HTTP/1.1 403 $message", true, 403 );
513 $response->header( 'Cache-Control: no-cache' );
514 echo "'origin' parameter does not match Origin header\n";
516 return false;
519 $config = $this->getConfig();
520 $matchOrigin = self::matchOrigin(
521 $originParam,
522 $config->get( 'CrossSiteAJAXdomains' ),
523 $config->get( 'CrossSiteAJAXdomainExceptions' )
526 if ( $matchOrigin ) {
527 $response->header( "Access-Control-Allow-Origin: $originParam" );
528 $response->header( 'Access-Control-Allow-Credentials: true' );
529 $this->getOutput()->addVaryHeader( 'Origin' );
532 return true;
536 * Attempt to match an Origin header against a set of rules and a set of exceptions
537 * @param string $value Origin header
538 * @param array $rules Set of wildcard rules
539 * @param array $exceptions Set of wildcard rules
540 * @return bool True if $value matches a rule in $rules and doesn't match
541 * any rules in $exceptions, false otherwise
543 protected static function matchOrigin( $value, $rules, $exceptions ) {
544 foreach ( $rules as $rule ) {
545 if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
546 // Rule matches, check exceptions
547 foreach ( $exceptions as $exc ) {
548 if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
549 return false;
553 return true;
557 return false;
561 * Helper function to convert wildcard string into a regex
562 * '*' => '.*?'
563 * '?' => '.'
565 * @param string $wildcard String with wildcards
566 * @return string Regular expression
568 protected static function wildcardToRegex( $wildcard ) {
569 $wildcard = preg_quote( $wildcard, '/' );
570 $wildcard = str_replace(
571 array( '\*', '\?' ),
572 array( '.*?', '.' ),
573 $wildcard
576 return "/https?:\/\/$wildcard/";
579 protected function sendCacheHeaders() {
580 $response = $this->getRequest()->response();
581 $out = $this->getOutput();
583 $config = $this->getConfig();
585 if ( $config->get( 'VaryOnXFP' ) ) {
586 $out->addVaryHeader( 'X-Forwarded-Proto' );
589 if ( $this->mCacheMode == 'private' ) {
590 $response->header( 'Cache-Control: private' );
591 return;
594 $useXVO = $config->get( 'UseXVO' );
595 if ( $this->mCacheMode == 'anon-public-user-private' ) {
596 $out->addVaryHeader( 'Cookie' );
597 $response->header( $out->getVaryHeader() );
598 if ( $useXVO ) {
599 $response->header( $out->getXVO() );
600 if ( $out->haveCacheVaryCookies() ) {
601 // Logged in, mark this request private
602 $response->header( 'Cache-Control: private' );
603 return;
605 // Logged out, send normal public headers below
606 } elseif ( session_id() != '' ) {
607 // Logged in or otherwise has session (e.g. anonymous users who have edited)
608 // Mark request private
609 $response->header( 'Cache-Control: private' );
611 return;
612 } // else no XVO and anonymous, send public headers below
615 // Send public headers
616 $response->header( $out->getVaryHeader() );
617 if ( $useXVO ) {
618 $response->header( $out->getXVO() );
621 // If nobody called setCacheMaxAge(), use the (s)maxage parameters
622 if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
623 $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
625 if ( !isset( $this->mCacheControl['max-age'] ) ) {
626 $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
629 if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
630 // Public cache not requested
631 // Sending a Vary header in this case is harmless, and protects us
632 // against conditional calls of setCacheMaxAge().
633 $response->header( 'Cache-Control: private' );
635 return;
638 $this->mCacheControl['public'] = true;
640 // Send an Expires header
641 $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
642 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
643 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
645 // Construct the Cache-Control header
646 $ccHeader = '';
647 $separator = '';
648 foreach ( $this->mCacheControl as $name => $value ) {
649 if ( is_bool( $value ) ) {
650 if ( $value ) {
651 $ccHeader .= $separator . $name;
652 $separator = ', ';
654 } else {
655 $ccHeader .= $separator . "$name=$value";
656 $separator = ', ';
660 $response->header( "Cache-Control: $ccHeader" );
664 * Replace the result data with the information about an exception.
665 * Returns the error code
666 * @param Exception $e
667 * @return string
669 protected function substituteResultWithError( $e ) {
670 $result = $this->getResult();
672 // Printer may not be initialized if the extractRequestParams() fails for the main module
673 if ( !isset( $this->mPrinter ) ) {
674 // The printer has not been created yet. Try to manually get formatter value.
675 $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
676 if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
677 $value = self::API_DEFAULT_FORMAT;
680 $this->mPrinter = $this->createPrinterByName( $value );
683 // Printer may not be able to handle errors. This is particularly
684 // likely if the module returns something for getCustomPrinter().
685 if ( !$this->mPrinter->canPrintErrors() ) {
686 $this->mPrinter->safeProfileOut();
687 $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
690 // Update raw mode flag for the selected printer.
691 $result->setRawMode( $this->mPrinter->getNeedsRawData() );
693 $config = $this->getConfig();
695 if ( $e instanceof UsageException ) {
696 // User entered incorrect parameters - generate error response
697 $errMessage = $e->getMessageArray();
698 $link = wfExpandUrl( wfScript( 'api' ) );
699 ApiResult::setContent( $errMessage, "See $link for API usage" );
700 } else {
701 // Something is seriously wrong
702 if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
703 $info = 'Database query error';
704 } else {
705 $info = "Exception Caught: {$e->getMessage()}";
708 $errMessage = array(
709 'code' => 'internal_api_error_' . get_class( $e ),
710 'info' => $info,
712 ApiResult::setContent(
713 $errMessage,
714 $config->get( 'ShowExceptionDetails' ) ? "\n\n{$e->getTraceAsString()}\n\n" : ''
718 // Remember all the warnings to re-add them later
719 $oldResult = $result->getData();
720 $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null;
722 $result->reset();
723 // Re-add the id
724 $requestid = $this->getParameter( 'requestid' );
725 if ( !is_null( $requestid ) ) {
726 $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
728 if ( $config->get( 'ShowHostnames' ) ) {
729 // servedby is especially useful when debugging errors
730 $result->addValue( null, 'servedby', wfHostName(), ApiResult::NO_SIZE_CHECK );
732 if ( $warnings !== null ) {
733 $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
736 $result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK );
738 return $errMessage['code'];
742 * Set up for the execution.
743 * @return array
745 protected function setupExecuteAction() {
746 // First add the id to the top element
747 $result = $this->getResult();
748 $requestid = $this->getParameter( 'requestid' );
749 if ( !is_null( $requestid ) ) {
750 $result->addValue( null, 'requestid', $requestid );
753 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
754 $servedby = $this->getParameter( 'servedby' );
755 if ( $servedby ) {
756 $result->addValue( null, 'servedby', wfHostName() );
760 if ( $this->getParameter( 'curtimestamp' ) ) {
761 $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ),
762 ApiResult::NO_SIZE_CHECK );
765 $params = $this->extractRequestParams();
767 $this->mAction = $params['action'];
769 if ( !is_string( $this->mAction ) ) {
770 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
773 return $params;
777 * Set up the module for response
778 * @return ApiBase The module that will handle this action
780 protected function setupModule() {
781 // Instantiate the module requested by the user
782 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
783 if ( $module === null ) {
784 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
786 $moduleParams = $module->extractRequestParams();
788 // Check token, if necessary
789 if ( $module->needsToken() === true ) {
790 throw new MWException(
791 "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
792 "See documentation for ApiBase::needsToken for details."
795 if ( $module->needsToken() ) {
796 if ( !$module->mustBePosted() ) {
797 throw new MWException(
798 "Module '{$module->getModuleName()}' must require POST to use tokens."
802 if ( !isset( $moduleParams['token'] ) ) {
803 $this->dieUsageMsg( array( 'missingparam', 'token' ) );
806 if ( !$this->getConfig()->get( 'DebugAPI' ) &&
807 array_key_exists(
808 $module->encodeParamName( 'token' ),
809 $this->getRequest()->getQueryValues()
812 $this->dieUsage(
813 "The '{$module->encodeParamName( 'token' )}' parameter was found in the query string, but must be in the POST body",
814 'mustposttoken'
818 if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
819 $this->dieUsageMsg( 'sessionfailure' );
823 return $module;
827 * Check the max lag if necessary
828 * @param ApiBase $module Api module being used
829 * @param array $params Array an array containing the request parameters.
830 * @return bool True on success, false should exit immediately
832 protected function checkMaxLag( $module, $params ) {
833 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
834 // Check for maxlag
835 $maxLag = $params['maxlag'];
836 list( $host, $lag ) = wfGetLB()->getMaxLag();
837 if ( $lag > $maxLag ) {
838 $response = $this->getRequest()->response();
840 $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
841 $response->header( 'X-Database-Lag: ' . intval( $lag ) );
843 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
844 $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
847 $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
851 return true;
855 * Check for sufficient permissions to execute
856 * @param ApiBase $module An Api module
858 protected function checkExecutePermissions( $module ) {
859 $user = $this->getUser();
860 if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
861 !$user->isAllowed( 'read' )
863 $this->dieUsageMsg( 'readrequired' );
865 if ( $module->isWriteMode() ) {
866 if ( !$this->mEnableWrite ) {
867 $this->dieUsageMsg( 'writedisabled' );
869 if ( !$user->isAllowed( 'writeapi' ) ) {
870 $this->dieUsageMsg( 'writerequired' );
872 if ( wfReadOnly() ) {
873 $this->dieReadOnly();
877 // Allow extensions to stop execution for arbitrary reasons.
878 $message = false;
879 if ( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
880 $this->dieUsageMsg( $message );
885 * Check asserts of the user's rights
886 * @param array $params
888 protected function checkAsserts( $params ) {
889 if ( isset( $params['assert'] ) ) {
890 $user = $this->getUser();
891 switch ( $params['assert'] ) {
892 case 'user':
893 if ( $user->isAnon() ) {
894 $this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' );
896 break;
897 case 'bot':
898 if ( !$user->isAllowed( 'bot' ) ) {
899 $this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' );
901 break;
907 * Check POST for external response and setup result printer
908 * @param ApiBase $module An Api module
909 * @param array $params An array with the request parameters
911 protected function setupExternalResponse( $module, $params ) {
912 if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) {
913 // Module requires POST. GET request might still be allowed
914 // if $wgDebugApi is true, otherwise fail.
915 $this->dieUsageMsgOrDebug( array( 'mustbeposted', $this->mAction ) );
918 // See if custom printer is used
919 $this->mPrinter = $module->getCustomPrinter();
920 if ( is_null( $this->mPrinter ) ) {
921 // Create an appropriate printer
922 $this->mPrinter = $this->createPrinterByName( $params['format'] );
925 if ( $this->mPrinter->getNeedsRawData() ) {
926 $this->getResult()->setRawMode();
931 * Execute the actual module, without any error handling
933 protected function executeAction() {
934 $params = $this->setupExecuteAction();
935 $module = $this->setupModule();
936 $this->mModule = $module;
938 $this->checkExecutePermissions( $module );
940 if ( !$this->checkMaxLag( $module, $params ) ) {
941 return;
944 if ( !$this->mInternalMode ) {
945 $this->setupExternalResponse( $module, $params );
948 $this->checkAsserts( $params );
950 // Execute
951 $module->profileIn();
952 $module->execute();
953 wfRunHooks( 'APIAfterExecute', array( &$module ) );
954 $module->profileOut();
956 $this->reportUnusedParams();
958 if ( !$this->mInternalMode ) {
959 //append Debug information
960 MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
962 // Print result data
963 $this->printResult( false );
968 * Log the preceding request
969 * @param int $time Time in seconds
971 protected function logRequest( $time ) {
972 $request = $this->getRequest();
973 $milliseconds = $time === null ? '?' : round( $time * 1000 );
974 $s = 'API' .
975 ' ' . $request->getMethod() .
976 ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
977 ' ' . $request->getIP() .
978 ' T=' . $milliseconds . 'ms';
979 foreach ( $this->getParamsUsed() as $name ) {
980 $value = $request->getVal( $name );
981 if ( $value === null ) {
982 continue;
984 $s .= ' ' . $name . '=';
985 if ( strlen( $value ) > 256 ) {
986 $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) );
987 $s .= $encValue . '[...]';
988 } else {
989 $s .= $this->encodeRequestLogValue( $value );
992 $s .= "\n";
993 wfDebugLog( 'api', $s, 'private' );
997 * Encode a value in a format suitable for a space-separated log line.
998 * @param string $s
999 * @return string
1001 protected function encodeRequestLogValue( $s ) {
1002 static $table;
1003 if ( !$table ) {
1004 $chars = ';@$!*(),/:';
1005 $numChars = strlen( $chars );
1006 for ( $i = 0; $i < $numChars; $i++ ) {
1007 $table[rawurlencode( $chars[$i] )] = $chars[$i];
1011 return strtr( rawurlencode( $s ), $table );
1015 * Get the request parameters used in the course of the preceding execute() request
1016 * @return array
1018 protected function getParamsUsed() {
1019 return array_keys( $this->mParamsUsed );
1023 * Get a request value, and register the fact that it was used, for logging.
1024 * @param string $name
1025 * @param mixed $default
1026 * @return mixed
1028 public function getVal( $name, $default = null ) {
1029 $this->mParamsUsed[$name] = true;
1031 $ret = $this->getRequest()->getVal( $name );
1032 if ( $ret === null ) {
1033 if ( $this->getRequest()->getArray( $name ) !== null ) {
1034 // See bug 10262 for why we don't just join( '|', ... ) the
1035 // array.
1036 $this->setWarning(
1037 "Parameter '$name' uses unsupported PHP array syntax"
1040 $ret = $default;
1042 return $ret;
1046 * Get a boolean request value, and register the fact that the parameter
1047 * was used, for logging.
1048 * @param string $name
1049 * @return bool
1051 public function getCheck( $name ) {
1052 return $this->getVal( $name, null ) !== null;
1056 * Get a request upload, and register the fact that it was used, for logging.
1058 * @since 1.21
1059 * @param string $name Parameter name
1060 * @return WebRequestUpload
1062 public function getUpload( $name ) {
1063 $this->mParamsUsed[$name] = true;
1065 return $this->getRequest()->getUpload( $name );
1069 * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
1070 * for example in case of spelling mistakes or a missing 'g' prefix for generators.
1072 protected function reportUnusedParams() {
1073 $paramsUsed = $this->getParamsUsed();
1074 $allParams = $this->getRequest()->getValueNames();
1076 if ( !$this->mInternalMode ) {
1077 // Printer has not yet executed; don't warn that its parameters are unused
1078 $printerParams = array_map(
1079 array( $this->mPrinter, 'encodeParamName' ),
1080 array_keys( $this->mPrinter->getFinalParams() ?: array() )
1082 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1083 } else {
1084 $unusedParams = array_diff( $allParams, $paramsUsed );
1087 if ( count( $unusedParams ) ) {
1088 $s = count( $unusedParams ) > 1 ? 's' : '';
1089 $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
1094 * Print results using the current printer
1096 * @param bool $isError
1098 protected function printResult( $isError ) {
1099 if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
1100 $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
1103 $this->getResult()->cleanUpUTF8();
1104 $printer = $this->mPrinter;
1105 $printer->profileIn();
1107 $printer->initPrinter( false );
1109 $printer->execute();
1110 $printer->closePrinter();
1111 $printer->profileOut();
1115 * @return bool
1117 public function isReadMode() {
1118 return false;
1122 * See ApiBase for description.
1124 * @return array
1126 public function getAllowedParams() {
1127 global $wgContLang;
1129 return array(
1130 'action' => array(
1131 ApiBase::PARAM_DFLT => 'help',
1132 ApiBase::PARAM_TYPE => 'submodule',
1134 'format' => array(
1135 ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT,
1136 ApiBase::PARAM_TYPE => 'submodule',
1138 'maxlag' => array(
1139 ApiBase::PARAM_TYPE => 'integer'
1141 'smaxage' => array(
1142 ApiBase::PARAM_TYPE => 'integer',
1143 ApiBase::PARAM_DFLT => 0
1145 'maxage' => array(
1146 ApiBase::PARAM_TYPE => 'integer',
1147 ApiBase::PARAM_DFLT => 0
1149 'assert' => array(
1150 ApiBase::PARAM_TYPE => array( 'user', 'bot' )
1152 'requestid' => null,
1153 'servedby' => false,
1154 'curtimestamp' => false,
1155 'origin' => null,
1156 'uselang' => array(
1157 ApiBase::PARAM_DFLT => $wgContLang->getCode(),
1162 /** @see ApiBase::getExamplesMessages() */
1163 public function getExamplesMessages() {
1164 return array(
1165 'action=help' => 'apihelp-help-example-main',
1166 'action=help&recursivesubmodules=1' => 'apihelp-help-example-recursive',
1170 public function modifyHelp( array &$help, array $options ) {
1171 // Wish PHP had an "array_insert_before". Instead, we have to manually
1172 // reindex the array to get 'permissions' in the right place.
1173 $oldHelp = $help;
1174 $help = array();
1175 foreach ( $oldHelp as $k => $v ) {
1176 if ( $k === 'submodules' ) {
1177 $help['permissions'] = '';
1179 $help[$k] = $v;
1181 $help['credits'] = '';
1183 // Fill 'permissions'
1184 $help['permissions'] .= Html::openElement( 'div',
1185 array( 'class' => 'apihelp-block apihelp-permissions' ) );
1186 $m = $this->msg( 'api-help-permissions' );
1187 if ( !$m->isDisabled() ) {
1188 $help['permissions'] .= Html::rawElement( 'div', array( 'class' => 'apihelp-block-head' ),
1189 $m->numParams( count( self::$mRights ) )->parse()
1192 $help['permissions'] .= Html::openElement( 'dl' );
1193 foreach ( self::$mRights as $right => $rightMsg ) {
1194 $help['permissions'] .= Html::element( 'dt', null, $right );
1196 $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
1197 $help['permissions'] .= Html::rawElement( 'dd', null, $rightMsg );
1199 $groups = array_map( function ( $group ) {
1200 return $group == '*' ? 'all' : $group;
1201 }, User::getGroupsWithPermission( $right ) );
1203 $help['permissions'] .= Html::rawElement( 'dd', null,
1204 $this->msg( 'api-help-permissions-granted-to' )
1205 ->numParams( count( $groups ) )
1206 ->params( $this->getLanguage()->commaList( $groups ) )
1207 ->parse()
1210 $help['permissions'] .= Html::closeElement( 'dl' );
1211 $help['permissions'] .= Html::closeElement( 'div' );
1213 // Fill 'credits', if applicable
1214 if ( empty( $options['nolead'] ) ) {
1215 $help['credits'] .= Html::element( 'h' . min( 6, $options['headerlevel'] + 1 ),
1216 array( 'id' => '+credits', 'class' => 'apihelp-header' ),
1217 $this->msg( 'api-credits-header' )->parse()
1219 $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
1223 private $mCanApiHighLimits = null;
1226 * Check whether the current user is allowed to use high limits
1227 * @return bool
1229 public function canApiHighLimits() {
1230 if ( !isset( $this->mCanApiHighLimits ) ) {
1231 $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
1234 return $this->mCanApiHighLimits;
1238 * Overrides to return this instance's module manager.
1239 * @return ApiModuleManager
1241 public function getModuleManager() {
1242 return $this->mModuleMgr;
1245 /************************************************************************//**
1246 * @name Deprecated
1247 * @{
1251 * @deprecated since 1.25
1252 * @return array
1254 public function getParamDescription() {
1255 return array(
1256 'format' => 'The format of the output',
1257 'action' => 'What action you would like to perform. See below for module help',
1258 'maxlag' => array(
1259 'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.',
1260 'To save actions causing any more site replication lag, this parameter can make the client',
1261 'wait until the replication lag is less than the specified value.',
1262 'In case of a replag error, error code "maxlag" is returned, with the message like',
1263 '"Waiting for $host: $lag seconds lagged\n".',
1264 'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information',
1266 'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached',
1267 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
1268 'assert' => 'Verify the user is logged in if set to "user", or has the bot userright if "bot"',
1269 'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
1270 'servedby' => 'Include the hostname that served the request in the ' .
1271 'results. Unconditionally shown on error',
1272 'curtimestamp' => 'Include the current timestamp in the result.',
1273 'origin' => array(
1274 'When accessing the API using a cross-domain AJAX request (CORS), set this to the',
1275 'originating domain. This must be included in any pre-flight request, and',
1276 'therefore must be part of the request URI (not the POST body). This must match',
1277 'one of the origins in the Origin: header exactly, so it has to be set to ',
1278 'something like http://en.wikipedia.org or https://meta.wikimedia.org . If this',
1279 'parameter does not match the Origin: header, a 403 response will be returned. If',
1280 'this parameter matches the Origin: header and the origin is whitelisted, an',
1281 'Access-Control-Allow-Origin header will be set.',
1287 * @deprecated since 1.25
1288 * @return array
1290 public function getDescription() {
1291 return array(
1294 '**********************************************************************************************',
1295 '** **',
1296 '** This is an auto-generated MediaWiki API documentation page **',
1297 '** **',
1298 '** Documentation and Examples: **',
1299 '** https://www.mediawiki.org/wiki/API **',
1300 '** **',
1301 '**********************************************************************************************',
1303 'Status: All features shown on this page should be working, but the API',
1304 ' is still in active development, and may change at any time.',
1305 ' Make sure to monitor our mailing list for any updates.',
1307 'Erroneous requests: When erroneous requests are sent to the API, a HTTP header will be sent',
1308 ' with the key "MediaWiki-API-Error" and then both the value of the',
1309 ' header and the error code sent back will be set to the same value.',
1311 ' In the case of an invalid action being passed, these will have a value',
1312 ' of "unknown_action".',
1314 ' For more information see https://www.mediawiki.org' .
1315 '/wiki/API:Errors_and_warnings',
1317 'Documentation: https://www.mediawiki.org/wiki/API:Main_page',
1318 'FAQ https://www.mediawiki.org/wiki/API:FAQ',
1319 'Mailing list: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api',
1320 'Api Announcements: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce',
1321 'Bugs & Requests: https://bugzilla.wikimedia.org/buglist.cgi?component=API&' .
1322 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts',
1332 * Sets whether the pretty-printer should format *bold* and $italics$
1334 * @deprecated since 1.25
1335 * @param bool $help
1337 public function setHelp( $help = true ) {
1338 wfDeprecated( __METHOD__, '1.25' );
1339 $this->mPrinter->setHelp( $help );
1343 * Override the parent to generate help messages for all available modules.
1345 * @deprecated since 1.25
1346 * @return string
1348 public function makeHelpMsg() {
1349 wfDeprecated( __METHOD__, '1.25' );
1350 global $wgMemc;
1351 $this->setHelp();
1352 // Get help text from cache if present
1353 $key = wfMemcKey( 'apihelp', $this->getModuleName(),
1354 str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) );
1356 $cacheHelpTimeout = $this->getConfig()->get( 'APICacheHelpTimeout' );
1357 if ( $cacheHelpTimeout > 0 ) {
1358 $cached = $wgMemc->get( $key );
1359 if ( $cached ) {
1360 return $cached;
1363 $retval = $this->reallyMakeHelpMsg();
1364 if ( $cacheHelpTimeout > 0 ) {
1365 $wgMemc->set( $key, $retval, $cacheHelpTimeout );
1368 return $retval;
1372 * @deprecated since 1.25
1373 * @return mixed|string
1375 public function reallyMakeHelpMsg() {
1376 wfDeprecated( __METHOD__, '1.25' );
1377 $this->setHelp();
1379 // Use parent to make default message for the main module
1380 $msg = parent::makeHelpMsg();
1382 $astriks = str_repeat( '*** ', 14 );
1383 $msg .= "\n\n$astriks Modules $astriks\n\n";
1385 foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) {
1386 $module = $this->mModuleMgr->getModule( $name );
1387 $msg .= self::makeHelpMsgHeader( $module, 'action' );
1389 $msg2 = $module->makeHelpMsg();
1390 if ( $msg2 !== false ) {
1391 $msg .= $msg2;
1393 $msg .= "\n";
1396 $msg .= "\n$astriks Permissions $astriks\n\n";
1397 foreach ( self::$mRights as $right => $rightMsg ) {
1398 $rightsMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )
1399 ->useDatabase( false )
1400 ->inLanguage( 'en' )
1401 ->text();
1402 $groups = User::getGroupsWithPermission( $right );
1403 $msg .= "* " . $right . " *\n $rightsMsg" .
1404 "\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
1407 $msg .= "\n$astriks Formats $astriks\n\n";
1408 foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) {
1409 $module = $this->mModuleMgr->getModule( $name );
1410 $msg .= self::makeHelpMsgHeader( $module, 'format' );
1411 $msg2 = $module->makeHelpMsg();
1412 if ( $msg2 !== false ) {
1413 $msg .= $msg2;
1415 $msg .= "\n";
1418 $credits = $this->msg( 'api-credits' )->useDatabase( 'false' )->inLanguage( 'en' )->text();
1419 $credits = str_replace( "\n", "\n ", $credits );
1420 $msg .= "\n*** Credits: ***\n $credits\n";
1422 return $msg;
1426 * @deprecated since 1.25
1427 * @param ApiBase $module
1428 * @param string $paramName What type of request is this? e.g. action,
1429 * query, list, prop, meta, format
1430 * @return string
1432 public static function makeHelpMsgHeader( $module, $paramName ) {
1433 wfDeprecated( __METHOD__, '1.25' );
1434 $modulePrefix = $module->getModulePrefix();
1435 if ( strval( $modulePrefix ) !== '' ) {
1436 $modulePrefix = "($modulePrefix) ";
1439 return "* $paramName={$module->getModuleName()} $modulePrefix*";
1443 * Check whether the user wants us to show version information in the API help
1444 * @return bool
1445 * @deprecated since 1.21, always returns false
1447 public function getShowVersions() {
1448 wfDeprecated( __METHOD__, '1.21' );
1450 return false;
1454 * Add or overwrite a module in this ApiMain instance. Intended for use by extending
1455 * classes who wish to add their own modules to their lexicon or override the
1456 * behavior of inherent ones.
1458 * @deprecated since 1.21, Use getModuleManager()->addModule() instead.
1459 * @param string $name The identifier for this module.
1460 * @param ApiBase $class The class where this module is implemented.
1462 protected function addModule( $name, $class ) {
1463 $this->getModuleManager()->addModule( $name, 'action', $class );
1467 * Add or overwrite an output format for this ApiMain. Intended for use by extending
1468 * classes who wish to add to or modify current formatters.
1470 * @deprecated since 1.21, Use getModuleManager()->addModule() instead.
1471 * @param string $name The identifier for this format.
1472 * @param ApiFormatBase $class The class implementing this format.
1474 protected function addFormat( $name, $class ) {
1475 $this->getModuleManager()->addModule( $name, 'format', $class );
1479 * Get the array mapping module names to class names
1480 * @deprecated since 1.21, Use getModuleManager()'s methods instead.
1481 * @return array
1483 function getModules() {
1484 return $this->getModuleManager()->getNamesWithClasses( 'action' );
1488 * Returns the list of supported formats in form ( 'format' => 'ClassName' )
1490 * @since 1.18
1491 * @deprecated since 1.21, Use getModuleManager()'s methods instead.
1492 * @return array
1494 public function getFormats() {
1495 return $this->getModuleManager()->getNamesWithClasses( 'format' );
1498 /**@}*/
1503 * This exception will be thrown when dieUsage is called to stop module execution.
1505 * @ingroup API
1507 class UsageException extends MWException {
1509 private $mCodestr;
1512 * @var null|array
1514 private $mExtraData;
1517 * @param string $message
1518 * @param string $codestr
1519 * @param int $code
1520 * @param array|null $extradata
1522 public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
1523 parent::__construct( $message, $code );
1524 $this->mCodestr = $codestr;
1525 $this->mExtraData = $extradata;
1529 * @return string
1531 public function getCodeString() {
1532 return $this->mCodestr;
1536 * @return array
1538 public function getMessageArray() {
1539 $result = array(
1540 'code' => $this->mCodestr,
1541 'info' => $this->getMessage()
1543 if ( is_array( $this->mExtraData ) ) {
1544 $result = array_merge( $result, $this->mExtraData );
1547 return $result;
1551 * @return string
1553 public function __toString() {
1554 return "{$this->getCodeString()}: {$this->getMessage()}";
1559 * For really cool vim folding this needs to be at the end:
1560 * vim: foldmarker=@{,@} foldmethod=marker