API: Filter lists of IDs before sending them to the database
[lhc/web/wiklou.git] / includes / api / ApiBase.php
index c66e5d5..9ea8c6d 100644 (file)
@@ -21,6 +21,7 @@
  */
 
 use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\MediaWikiServices;
 
 /**
  * This abstract class implements many basic API functions, and is the base of
@@ -267,11 +268,14 @@ abstract class ApiBase extends ContextSource {
        /** @var array Maps extension paths to info arrays */
        private static $extensionInfo = null;
 
+       /** @var int[][][] Cache for self::filterIDs() */
+       private static $filterIDsCache = [];
+
        /** @var ApiMain */
        private $mMainModule;
        /** @var string */
        private $mModuleName, $mModulePrefix;
-       private $mSlaveDB = null;
+       private $mReplicaDB = null;
        private $mParamCache = [];
        /** @var array|null|bool */
        private $mModuleSource = false;
@@ -647,11 +651,11 @@ abstract class ApiBase extends ContextSource {
         * @return IDatabase
         */
        protected function getDB() {
-               if ( !isset( $this->mSlaveDB ) ) {
-                       $this->mSlaveDB = wfGetDB( DB_REPLICA, 'api' );
+               if ( !isset( $this->mReplicaDB ) ) {
+                       $this->mReplicaDB = wfGetDB( DB_REPLICA, 'api' );
                }
 
-               return $this->mSlaveDB;
+               return $this->mReplicaDB;
        }
 
        /**
@@ -1831,6 +1835,41 @@ abstract class ApiBase extends ContextSource {
                }
        }
 
+       /**
+        * Filter out-of-range values from a list of positive integer IDs
+        * @since 1.33
+        * @param array $fields Array of pairs of table and field to check
+        * @param (string|int)[] $ids IDs to filter. Strings in the array are
+        *  expected to be stringified ints.
+        * @return (string|int)[] Filtered IDs.
+        */
+       protected function filterIDs( $fields, array $ids ) {
+               $min = INF;
+               $max = 0;
+               foreach ( $fields as list( $table, $field ) ) {
+                       if ( isset( self::$filterIDsCache[$table][$field] ) ) {
+                               $row = self::$filterIDsCache[$table][$field];
+                       } else {
+                               $row = $this->getDB()->selectRow(
+                                       $table,
+                                       [
+                                               'min_id' => "MIN($field)",
+                                               'max_id' => "MAX($field)",
+                                       ],
+                                       null,
+                                       __METHOD__
+                               );
+                               self::$filterIDsCache[$table][$field] = $row;
+                       }
+                       $min = min( $min, $row->min_id );
+                       $max = max( $max, $row->max_id );
+               }
+               return array_filter( $ids, function ( $id ) use ( $min, $max ) {
+                       return ( is_int( $id ) && $id >= 0 || ctype_digit( $id ) )
+                               && $id >= $min && $id <= $max;
+               } );
+       }
+
        /**@}*/
 
        /************************************************************************//**
@@ -2069,11 +2108,41 @@ abstract class ApiBase extends ContextSource {
                foreach ( (array)$actions as $action ) {
                        $errors = array_merge( $errors, $title->getUserPermissionsErrors( $action, $user ) );
                }
+
                if ( $errors ) {
+                       // track block notices
+                       if ( $this->getConfig()->get( 'EnableBlockNoticeStats' ) ) {
+                               $this->trackBlockNotices( $errors );
+                       }
+
                        $this->dieStatus( $this->errorArrayToStatus( $errors, $user ) );
                }
        }
 
+       /**
+        * Keep track of errors messages resulting from a block
+        *
+        * @param array $errors
+        */
+       private function trackBlockNotices( array $errors ) {
+               $errorMessageKeys = [
+                       'blockedtext',
+                       'blockedtext-partial',
+                       'autoblockedtext',
+                       'systemblockedtext',
+               ];
+
+               $statsd = MediaWikiServices::getInstance()->getStatsdDataFactory();
+
+               foreach ( $errors as $error ) {
+                       if ( in_array( $error[0], $errorMessageKeys ) ) {
+                               $wiki = $this->getConfig()->get( 'DBname' );
+                               $statsd->increment( 'BlockNotices.' . $wiki . '.MediaWikiApi.returned' );
+                               break;
+                       }
+               }
+       }
+
        /**
         * Will only set a warning instead of failing if the global $wgDebugAPI
         * is set to true. Otherwise behaves exactly as self::dieWithError().