API: Add license info to API help output
[lhc/web/wiklou.git] / includes / api / ApiBase.php
index 9cb895d..6e289dc 100644 (file)
@@ -32,9 +32,6 @@
  * Module parameters: Derived classes can define getAllowedParams() to specify
  *    which parameters to expect, how to parse and validate them.
  *
- * Profiling: various methods to allow keeping tabs on various tasks and their
- *    time costs
- *
  * Self-documentation: code to allow the API to document its own state
  *
  * @ingroup API
@@ -66,13 +63,11 @@ abstract class ApiBase extends ContextSource {
        const PARAM_RANGE_ENFORCE = 9;
        /// @since 1.25
        // Specify an alternative i18n message for this help parameter.
-       // Value can be a string key, an array giving key and parameters, or a
-       // Message object.
+       // Value is $msg for ApiBase::makeMessage()
        const PARAM_HELP_MSG = 10;
        /// @since 1.25
        // Specify additional i18n messages to append to the normal message. Value
-       // is an array of any of strings giving the message key, arrays giving key and
-       // parameters, or Message objects.
+       // is an array of $msg for ApiBase::makeMessage()
        const PARAM_HELP_MSG_APPEND = 11;
        /// @since 1.25
        // Specify additional information tags for the parameter. Value is an array
@@ -82,9 +77,14 @@ abstract class ApiBase extends ContextSource {
        // comma-joined list of values, $3 = module prefix.
        const PARAM_HELP_MSG_INFO = 12;
        /// @since 1.25
-       // When PARAM_DFLT is an array, this may be an array mapping those values
+       // When PARAM_TYPE is an array, this may be an array mapping those values
        // to page titles which will be linked in the help.
        const PARAM_VALUE_LINKS = 13;
+       /// @since 1.25
+       // When PARAM_TYPE is an array, this is an array mapping those values to
+       // $msg for ApiBase::makeMessage(). Any value not having a mapping will use
+       // apihelp-{$path}-paramvalue-{$param}-{$value} is used.
+       const PARAM_HELP_MSG_PER_VALUE = 14;
 
        const LIMIT_BIG1 = 500; // Fast query, std user limit
        const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit
@@ -98,12 +98,17 @@ abstract class ApiBase extends ContextSource {
         */
        const GET_VALUES_FOR_HELP = 1;
 
+       /** @var array Maps extension paths to info arrays */
+       private static $extensionInfo = null;
+
        /** @var ApiMain */
        private $mMainModule;
        /** @var string */
        private $mModuleName, $mModulePrefix;
        private $mSlaveDB = null;
        private $mParamCache = array();
+       /** @var array|null|bool */
+       private $mModuleSource = false;
 
        /**
         * @param ApiMain $mainModule
@@ -157,6 +162,9 @@ abstract class ApiBase extends ContextSource {
         * If the module may only be used with a certain format module,
         * it should override this method to return an instance of that formatter.
         * A value of null means the default format will be used.
+        * @note Do not use this just because you don't want to support non-json
+        * formats. This should be used only when there is a fundamental
+        * requirement for a specific format.
         * @return mixed Instance of a derived class of ApiFormatBase, or null
         */
        public function getCustomPrinter() {
@@ -377,6 +385,20 @@ abstract class ApiBase extends ContextSource {
                return $this->isMain() ? null : $this->getMain();
        }
 
+       /**
+        * Returns true if the current request breaks the same-origin policy.
+        *
+        * For example, json with callbacks.
+        *
+        * https://en.wikipedia.org/wiki/Same-origin_policy
+        *
+        * @since 1.25
+        * @return bool
+        */
+       public function lacksSameOriginSecurity() {
+               return $this->getMain()->getRequest()->getVal( 'callback' ) !== null;
+       }
+
        /**
         * Get the path to this module
         *
@@ -463,9 +485,7 @@ abstract class ApiBase extends ContextSource {
         */
        protected function getDB() {
                if ( !isset( $this->mSlaveDB ) ) {
-                       $this->profileDBIn();
                        $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' );
-                       $this->profileDBOut();
                }
 
                return $this->mSlaveDB;
@@ -1042,6 +1062,7 @@ abstract class ApiBase extends ContextSource {
         * @param string $token Supplied token
         * @param array $params All supplied parameters for the module
         * @return bool
+        * @throws MWException
         */
        final public function validateToken( $token, array $params ) {
                $tokenType = $this->needsToken();
@@ -1276,7 +1297,6 @@ abstract class ApiBase extends ContextSource {
         * @throws UsageException
         */
        public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
-               Profiler::instance()->close();
                throw new UsageException(
                        $description,
                        $this->encodeParamName( $errorCode ),
@@ -1291,6 +1311,7 @@ abstract class ApiBase extends ContextSource {
         * @since 1.23
         * @param Status $status
         * @return array Array of code and error string
+        * @throws MWException
         */
        public function getErrorFromStatus( $status ) {
                if ( $status->isGood() ) {
@@ -1932,6 +1953,21 @@ abstract class ApiBase extends ContextSource {
                throw new MWException( "Internal error in $method: $message" );
        }
 
+       /**
+        * Write logging information for API features to a debug log, for usage
+        * analysis.
+        * @param string $feature Feature being used.
+        */
+       protected function logFeatureUsage( $feature ) {
+               $request = $this->getRequest();
+               $s = '"' . addslashes( $feature ) . '"' .
+                       ' "' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . '"' .
+                       ' "' . $request->getIP() . '"' .
+                       ' "' . addslashes( $request->getHeader( 'Referer' ) ) . '"' .
+                       ' "' . addslashes( $this->getMain()->getUserAgent() ) . '"';
+               wfDebugLog( 'api-feature-usage', $s, 'private' );
+       }
+
        /**@}*/
 
        /************************************************************************//**
@@ -2018,6 +2054,10 @@ abstract class ApiBase extends ContextSource {
         * @return array Keys are parameter names, values are arrays of Message objects
         */
        public function getFinalParamDescription() {
+               $prefix = $this->getModulePrefix();
+               $name = $this->getModuleName();
+               $path = $this->getModulePath();
+
                $desc = $this->getParamDescription();
                Hooks::run( 'APIGetParamDescription', array( &$this, &$desc ) );
 
@@ -2048,35 +2088,61 @@ abstract class ApiBase extends ContextSource {
                        if ( isset( $settings[ApiBase::PARAM_HELP_MSG] ) ) {
                                $msg = $settings[ApiBase::PARAM_HELP_MSG];
                        } else {
-                               $msg = $this->msg( "apihelp-{$this->getModulePath()}-param-{$param}" );
+                               $msg = $this->msg( "apihelp-{$path}-param-{$param}" );
                                if ( !$msg->exists() ) {
                                        $msg = $this->msg( 'api-help-fallback-parameter', $d );
                                }
                        }
-                       $msg = ApiBase::makeMessage( $msg, $this->getContext(), array(
-                               $this->getModulePrefix(),
-                               $param,
-                               $this->getModuleName(),
-                               $this->getModulePath(),
-                       ) );
+                       $msg = ApiBase::makeMessage( $msg, $this->getContext(),
+                               array( $prefix, $param, $name, $path ) );
                        if ( !$msg ) {
                                $this->dieDebug( __METHOD__,
                                        'Value in ApiBase::PARAM_HELP_MSG is not valid' );
                        }
                        $msgs[$param] = array( $msg );
 
+                       if ( isset( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) {
+                               if ( !is_array( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) {
+                                       $this->dieDebug( __METHOD__,
+                                               'ApiBase::PARAM_HELP_MSG_PER_VALUE is not valid' );
+                               }
+                               if ( !is_array( $settings[ApiBase::PARAM_TYPE] ) ) {
+                                       $this->dieDebug( __METHOD__,
+                                               'ApiBase::PARAM_HELP_MSG_PER_VALUE may only be used when ' .
+                                               'ApiBase::PARAM_TYPE is an array' );
+                               }
+
+                               $valueMsgs = $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE];
+                               foreach ( $settings[ApiBase::PARAM_TYPE] as $value ) {
+                                       if ( isset( $valueMsgs[$value] ) ) {
+                                               $msg = $valueMsgs[$value];
+                                       } else {
+                                               $msg = "apihelp-{$path}-paramvalue-{$param}-{$value}";
+                                       }
+                                       $m = ApiBase::makeMessage( $msg, $this->getContext(),
+                                               array( $prefix, $param, $name, $path, $value ) );
+                                       if ( $m ) {
+                                               $m = new ApiHelpParamValueMessage(
+                                                       $value,
+                                                       array( $m->getKey(), 'api-help-param-no-description' ),
+                                                       $m->getParams()
+                                               );
+                                               $msgs[$param][] = $m->setContext( $this->getContext() );
+                                       } else {
+                                               $this->dieDebug( __METHOD__,
+                                                       "Value in ApiBase::PARAM_HELP_MSG_PER_VALUE for $value is not valid" );
+                                       }
+                               }
+                       }
+
                        if ( isset( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) {
                                if ( !is_array( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) {
                                        $this->dieDebug( __METHOD__,
                                                'Value for ApiBase::PARAM_HELP_MSG_APPEND is not an array' );
                                }
                                foreach ( $settings[ApiBase::PARAM_HELP_MSG_APPEND] as $m ) {
-                                       $m = ApiBase::makeMessage( $m, $this->getContext(), array(
-                                               $this->getModulePrefix(),
-                                               $param,
-                                               $this->getModuleName(),
-                                               $this->getModulePath(),
-                                       ) );
+                                       $m = ApiBase::makeMessage( $m, $this->getContext(),
+                                               array( $prefix, $param, $name, $path ) );
                                        if ( $m ) {
                                                $msgs[$param][] = $m;
                                        } else {
@@ -2124,166 +2190,102 @@ abstract class ApiBase extends ContextSource {
        }
 
        /**
-        * Called from ApiHelp before the pieces are joined together and returned.
+        * Returns information about the source of this module, if known
         *
-        * This exists mainly for ApiMain to add the Permissions and Credits
-        * sections. Other modules probably don't need it.
+        * Returned array is an array with the following keys:
+        * - path: Install path
+        * - name: Extension name, or "MediaWiki" for core
+        * - namemsg: (optional) i18n message key for a display name
+        * - license-name: (optional) Name of license
         *
-        * @param string[] &$help Array of help data
-        * @param array $options Options passed to ApiHelp::getHelp
-        */
-       public function modifyHelp( array &$help, array $options ) {
-       }
-
-       /**@}*/
-
-       /************************************************************************//**
-        * @name   Profiling
-        * @{
+        * @return array|null
         */
+       protected function getModuleSourceInfo() {
+               global $IP;
 
-       /**
-        * Profiling: total module execution time
-        */
-       private $mTimeIn = 0, $mModuleTime = 0;
-
-       /**
-        * Get the name of the module as shown in the profiler log
-        *
-        * @param DatabaseBase|bool $db
-        *
-        * @return string
-        */
-       public function getModuleProfileName( $db = false ) {
-               if ( $db ) {
-                       return 'API:' . $this->mModuleName . '-DB';
+               if ( $this->mModuleSource !== false ) {
+                       return $this->mModuleSource;
                }
 
-               return 'API:' . $this->mModuleName;
-       }
-
-       /**
-        * Start module profiling
-        */
-       public function profileIn() {
-               if ( $this->mTimeIn !== 0 ) {
-                       ApiBase::dieDebug( __METHOD__, 'Called twice without calling profileOut()' );
+               // First, try to find where the module comes from...
+               $rClass = new ReflectionClass( $this );
+               $path = $rClass->getFileName();
+               if ( !$path ) {
+                       // No path known?
+                       $this->mModuleSource = null;
+                       return null;
                }
-               $this->mTimeIn = microtime( true );
-               wfProfileIn( $this->getModuleProfileName() );
-       }
+               $path = realpath( $path ) ?: $path;
 
-       /**
-        * End module profiling
-        */
-       public function profileOut() {
-               if ( $this->mTimeIn === 0 ) {
-                       ApiBase::dieDebug( __METHOD__, 'Called without calling profileIn() first' );
-               }
-               if ( $this->mDBTimeIn !== 0 ) {
-                       ApiBase::dieDebug(
-                               __METHOD__,
-                               'Must be called after database profiling is done with profileDBOut()'
+               // Build map of extension directories to extension info
+               if ( self::$extensionInfo === null ) {
+                       self::$extensionInfo = array(
+                               realpath( __DIR__ ) ?: __DIR__ => array(
+                                       'path' => $IP,
+                                       'name' => 'MediaWiki',
+                                       'license-name' => 'GPL-2.0+',
+                               ),
+                               realpath( "$IP/extensions" ) ?: "$IP/extensions" => null,
                        );
-               }
-
-               $this->mModuleTime += microtime( true ) - $this->mTimeIn;
-               $this->mTimeIn = 0;
-               wfProfileOut( $this->getModuleProfileName() );
-       }
-
-       /**
-        * When modules crash, sometimes it is needed to do a profileOut() regardless
-        * of the profiling state the module was in. This method does such cleanup.
-        */
-       public function safeProfileOut() {
-               if ( $this->mTimeIn !== 0 ) {
-                       if ( $this->mDBTimeIn !== 0 ) {
-                               $this->profileDBOut();
-                       }
-                       $this->profileOut();
-               }
-       }
-
-       /**
-        * Total time the module was executed
-        * @return float
-        */
-       public function getProfileTime() {
-               if ( $this->mTimeIn !== 0 ) {
-                       ApiBase::dieDebug( __METHOD__, 'Called without calling profileOut() first' );
-               }
-
-               return $this->mModuleTime;
-       }
-
-       /**
-        * Profiling: database execution time
-        */
-       private $mDBTimeIn = 0, $mDBTime = 0;
-
-       /**
-        * Start module profiling
-        */
-       public function profileDBIn() {
-               if ( $this->mTimeIn === 0 ) {
-                       ApiBase::dieDebug(
-                               __METHOD__,
-                               'Must be called while profiling the entire module with profileIn()'
+                       $keep = array(
+                               'path' => null,
+                               'name' => null,
+                               'namemsg' => null,
+                               'license-name' => null,
                        );
-               }
-               if ( $this->mDBTimeIn !== 0 ) {
-                       ApiBase::dieDebug( __METHOD__, 'Called twice without calling profileDBOut()' );
-               }
-               $this->mDBTimeIn = microtime( true );
-               wfProfileIn( $this->getModuleProfileName( true ) );
-       }
+                       foreach ( $this->getConfig()->get( 'ExtensionCredits' ) as $group ) {
+                               foreach ( $group as $ext ) {
+                                       if ( !isset( $ext['path'] ) || !isset( $ext['name'] ) ) {
+                                               // This shouldn't happen, but does anyway.
+                                               continue;
+                                       }
 
-       /**
-        * End database profiling
-        */
-       public function profileDBOut() {
-               if ( $this->mTimeIn === 0 ) {
-                       ApiBase::dieDebug( __METHOD__, 'Must be called while profiling ' .
-                               'the entire module with profileIn()' );
-               }
-               if ( $this->mDBTimeIn === 0 ) {
-                       ApiBase::dieDebug( __METHOD__, 'Called without calling profileDBIn() first' );
+                                       $extpath = $ext['path'];
+                                       if ( !is_dir( $extpath ) ) {
+                                               $extpath = dirname( $extpath );
+                                       }
+                                       self::$extensionInfo[realpath( $extpath ) ?: $extpath] =
+                                               array_intersect_key( $ext, $keep );
+                               }
+                       }
+                       foreach ( ExtensionRegistry::getInstance()->getAllThings() as $ext ) {
+                               $extpath = $ext['path'];
+                               if ( !is_dir( $extpath ) ) {
+                                       $extpath = dirname( $extpath );
+                               }
+                               self::$extensionInfo[realpath( $extpath ) ?: $extpath] =
+                                       array_intersect_key( $ext, $keep );
+                       }
                }
 
-               $time = microtime( true ) - $this->mDBTimeIn;
-               $this->mDBTimeIn = 0;
+               // Now traverse parent directories until we find a match or run out of
+               // parents.
+               do {
+                       if ( array_key_exists( $path, self::$extensionInfo ) ) {
+                               // Found it!
+                               $this->mModuleSource = self::$extensionInfo[$path];
+                               return $this->mModuleSource;
+                       }
 
-               $this->mDBTime += $time;
-               $this->getMain()->mDBTime += $time;
-               wfProfileOut( $this->getModuleProfileName( true ) );
-       }
+                       $oldpath = $path;
+                       $path = dirname( $path );
+               } while ( $path !== $oldpath );
 
-       /**
-        * Total time the module used the database
-        * @return float
-        */
-       public function getProfileDBTime() {
-               if ( $this->mDBTimeIn !== 0 ) {
-                       ApiBase::dieDebug( __METHOD__, 'Called without calling profileDBOut() first' );
-               }
-
-               return $this->mDBTime;
+               // No idea what extension this might be.
+               $this->mModuleSource = null;
+               return null;
        }
 
        /**
-        * Write logging information for API features to a debug log, for usage
-        * analysis.
-        * @param string $feature Feature being used.
+        * Called from ApiHelp before the pieces are joined together and returned.
+        *
+        * This exists mainly for ApiMain to add the Permissions and Credits
+        * sections. Other modules probably don't need it.
+        *
+        * @param string[] &$help Array of help data
+        * @param array $options Options passed to ApiHelp::getHelp
         */
-       protected function logFeatureUsage( $feature ) {
-               $request = $this->getRequest();
-               $s = '"' . addslashes( $feature ) . '"' .
-                       ' "' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . '"' .
-                       ' "' . $request->getIP() . '"' .
-                       ' "' . addslashes( $request->getHeader( 'Referer' ) ) . '"' .
-                       ' "' . addslashes( $this->getMain()->getUserAgent() ) . '"';
-               wfDebugLog( 'api-feature-usage', $s, 'private' );
+       public function modifyHelp( array &$help, array $options ) {
        }
 
        /**@}*/
@@ -2738,6 +2740,71 @@ abstract class ApiBase extends ContextSource {
                return false;
        }
 
+       /**
+        * @deprecated since 1.25, always returns empty string
+        * @param DatabaseBase|bool $db
+        * @return string
+        */
+       public function getModuleProfileName( $db = false ) {
+               wfDeprecated( __METHOD__, '1.25' );
+               return '';
+       }
+
+       /**
+        * @deprecated since 1.25
+        */
+       public function profileIn() {
+               // No wfDeprecated() yet because extensions call this and might need to
+               // keep doing so for BC.
+       }
+
+       /**
+        * @deprecated since 1.25
+        */
+       public function profileOut() {
+               // No wfDeprecated() yet because extensions call this and might need to
+               // keep doing so for BC.
+       }
+
+       /**
+        * @deprecated since 1.25
+        */
+       public function safeProfileOut() {
+               wfDeprecated( __METHOD__, '1.25' );
+       }
+
+       /**
+        * @deprecated since 1.25, always returns 0
+        * @return float
+        */
+       public function getProfileTime() {
+               wfDeprecated( __METHOD__, '1.25' );
+               return 0;
+       }
+
+       /**
+        * @deprecated since 1.25
+        */
+       public function profileDBIn() {
+               wfDeprecated( __METHOD__, '1.25' );
+       }
+
+       /**
+        * @deprecated since 1.25
+        */
+       public function profileDBOut() {
+               wfDeprecated( __METHOD__, '1.25' );
+       }
+
+       /**
+        * @deprecated since 1.25, always returns 0
+        * @return float
+        */
+       public function getProfileDBTime() {
+               wfDeprecated( __METHOD__, '1.25' );
+               return 0;
+       }
+
        /**@}*/
 }