Merge "Fix escaping for MSSQL LIKE queries."
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 25 Apr 2016 05:01:51 +0000 (05:01 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 25 Apr 2016 05:01:51 +0000 (05:01 +0000)
1  2 
includes/db/DatabaseMssql.php

@@@ -28,7 -28,7 +28,7 @@@
  /**
   * @ingroup Database
   */
 -class DatabaseMssql extends DatabaseBase {
 +class DatabaseMssql extends Database {
        protected $mInsertId = null;
        protected $mLastResult = null;
        protected $mAffectedRows = null;
                $this->mPassword = $password;
                $this->mDBname = $dbName;
  
 -              $connectionInfo = array();
 +              $connectionInfo = [];
  
                if ( $dbName ) {
                        $connectionInfo['Database'] = $dbName;
                        $connectionInfo['PWD'] = $password;
                }
  
 -              wfSuppressWarnings();
 +              MediaWiki\suppressWarnings();
                $this->mConn = sqlsrv_connect( $server, $connectionInfo );
 -              wfRestoreWarnings();
 +              MediaWiki\restoreWarnings();
  
                if ( $this->mConn === false ) {
                        throw new DBConnectionError( $this, $this->lastError() );
         * @param bool|MssqlResultWrapper|resource $result
         * @return bool|MssqlResultWrapper
         */
 -      public function resultObject( $result ) {
 -              if ( empty( $result ) ) {
 +      protected function resultObject( $result ) {
 +              if ( !$result ) {
                        return false;
                } elseif ( $result instanceof MssqlResultWrapper ) {
                        return $result;
                // has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty
                // strings make php throw a fatal error "Severe error translating Unicode"
                if ( $this->mScrollableCursor ) {
 -                      $scrollArr = array( 'Scrollable' => SQLSRV_CURSOR_STATIC );
 +                      $scrollArr = [ 'Scrollable' => SQLSRV_CURSOR_STATIC ];
                } else {
 -                      $scrollArr = array();
 +                      $scrollArr = [];
                }
  
                if ( $this->mPrepareStatements ) {
                        // we do prepare + execute so we can get its field metadata for later usage if desired
 -                      $stmt = sqlsrv_prepare( $this->mConn, $sql, array(), $scrollArr );
 +                      $stmt = sqlsrv_prepare( $this->mConn, $sql, [], $scrollArr );
                        $success = sqlsrv_execute( $stmt );
                } else {
 -                      $stmt = sqlsrv_query( $this->mConn, $sql, array(), $scrollArr );
 +                      $stmt = sqlsrv_query( $this->mConn, $sql, [], $scrollArr );
                        $success = (bool)$stmt;
                }
  
                        $res = $res->result;
                }
  
 -              $metadata = sqlsrv_field_metadata( $res );
 -              return $metadata[$n]['Name'];
 +              return sqlsrv_field_metadata( $res )[$n]['Name'];
        }
  
        /**
         *   (optional) (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
         * @return mixed Database result resource (feed to Database::fetchObject
         *   or whatever), or false on failure
 +       * @throws DBQueryError
 +       * @throws DBUnexpectedError
 +       * @throws Exception
         */
        public function select( $table, $vars, $conds = '', $fname = __METHOD__,
 -              $options = array(), $join_conds = array()
 +              $options = [], $join_conds = []
        ) {
                $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
                if ( isset( $options['EXPLAIN'] ) ) {
         * @return string The SQL text
         */
        public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
 -              $options = array(), $join_conds = array()
 +              $options = [], $join_conds = []
        ) {
                if ( isset( $options['EXPLAIN'] ) ) {
                        unset( $options['EXPLAIN'] );
  
                // try to rewrite aggregations of bit columns (currently MAX and MIN)
                if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) {
 -                      $bitColumns = array();
 +                      $bitColumns = [];
                        if ( is_array( $table ) ) {
                                foreach ( $table as $t ) {
                                        $bitColumns += $this->getBitColumns( $this->tableName( $t ) );
                        }
  
                        foreach ( $bitColumns as $col => $info ) {
 -                              $replace = array(
 +                              $replace = [
                                        "MAX({$col})" => "MAX(CAST({$col} AS tinyint))",
                                        "MIN({$col})" => "MIN(CAST({$col} AS tinyint))",
 -                              );
 +                              ];
                                $sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql );
                        }
                }
         * @return int
         */
        public function estimateRowCount( $table, $vars = '*', $conds = '',
 -              $fname = __METHOD__, $options = array()
 +              $fname = __METHOD__, $options = []
        ) {
                // http://msdn2.microsoft.com/en-us/library/aa259203.aspx
                $options['EXPLAIN'] = true;
                        $row = $this->fetchRow( $res );
  
                        if ( isset( $row['EstimateRows'] ) ) {
 -                              $rows = $row['EstimateRows'];
 +                              $rows = (int)$row['EstimateRows'];
                        }
                }
  
                        return null;
                }
  
 -              $result = array();
 +              $result = [];
                foreach ( $res as $row ) {
                        if ( $row->index_name == $index ) {
                                $row->Non_unique = !stristr( $row->index_description, "unique" );
         * @param array $arrToInsert
         * @param string $fname
         * @param array $options
 -       * @throws DBQueryError
         * @return bool
 +       * @throws Exception
         */
 -      public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) {
 +      public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = [] ) {
                # No rows to insert, easy just return now
                if ( !count( $arrToInsert ) ) {
                        return true;
                }
  
                if ( !is_array( $options ) ) {
 -                      $options = array( $options );
 +                      $options = [ $options ];
                }
  
                $table = $this->tableName( $table );
  
                if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row
 -                      $arrToInsert = array( 0 => $arrToInsert ); // make everything multi row compatible
 +                      $arrToInsert = [ 0 => $arrToInsert ]; // make everything multi row compatible
                }
  
                // We know the table we're inserting into, get its identity column
                // INSERT IGNORE is not supported by SQL Server
                // remove IGNORE from options list and set ignore flag to true
                if ( in_array( 'IGNORE', $options ) ) {
 -                      $options = array_diff( $options, array( 'IGNORE' ) );
 +                      $options = array_diff( $options, [ 'IGNORE' ] );
                        $this->mIgnoreDupKeyErrors = true;
                }
  
                        if ( !is_null( $identity ) ) {
                                // then we want to get the identity column value we were assigned and save it off
                                $row = $ret->fetchObject();
 -                              if( is_object( $row ) ){
 +                              if ( is_object( $row ) ) {
                                        $this->mInsertId = $row->$identity;
                                }
                        }
         * @param string $fname
         * @param array $insertOptions
         * @param array $selectOptions
 -       * @throws DBQueryError
         * @return null|ResultWrapper
 +       * @throws Exception
         */
        public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
 -              $insertOptions = array(), $selectOptions = array()
 +              $insertOptions = [], $selectOptions = []
        ) {
                $this->mScrollableCursor = false;
                try {
         *                   - IGNORE: Ignore unique key conflicts
         *                   - LOW_PRIORITY: MySQL-specific, see MySQL manual.
         * @return bool
 +       * @throws DBUnexpectedError
 +       * @throws Exception
 +       * @throws MWException
         */
 -      function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) {
 +      function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
                $table = $this->tableName( $table );
                $binaryColumns = $this->getBinaryColumns( $table );
  
                $opts = $this->makeUpdateOptions( $options );
                $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns );
  
 -              if ( $conds !== array() && $conds !== '*' ) {
 +              if ( $conds !== [] && $conds !== '*' ) {
                        $sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns );
                }
  
         * @throws MWException|DBUnexpectedError
         * @return string
         */
 -      public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = array() ) {
 +      public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = [] ) {
                if ( !is_array( $a ) ) {
                        throw new DBUnexpectedError( $this,
                                'DatabaseBase::makeList called with incorrect parameters' );
                }
  
 -              $first = true;
 -              $list = '';
 +              if ( $mode != LIST_NAMES ) {
 +                      // In MS SQL, values need to be specially encoded when they are
 +                      // inserted into binary fields. Perform this necessary encoding
 +                      // for the specified set of columns.
 +                      foreach ( array_keys( $a ) as $field ) {
 +                              if ( !isset( $binaryColumns[$field] ) ) {
 +                                      continue;
 +                              }
  
 -              foreach ( $a as $field => $value ) {
 -                      if ( $mode != LIST_NAMES && isset( $binaryColumns[$field] ) ) {
 -                              if ( is_array( $value ) ) {
 -                                      foreach ( $value as &$v ) {
 +                              if ( is_array( $a[$field] ) ) {
 +                                      foreach ( $a[$field] as &$v ) {
                                                $v = new MssqlBlob( $v );
                                        }
 +                                      unset( $v );
                                } else {
 -                                      $value = new MssqlBlob( $value );
 +                                      $a[$field] = new MssqlBlob( $a[$field] );
                                }
                        }
 -
 -                      if ( !$first ) {
 -                              if ( $mode == LIST_AND ) {
 -                                      $list .= ' AND ';
 -                              } elseif ( $mode == LIST_OR ) {
 -                                      $list .= ' OR ';
 -                              } else {
 -                                      $list .= ',';
 -                              }
 -                      } else {
 -                              $first = false;
 -                      }
 -
 -                      if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
 -                              $list .= "($value)";
 -                      } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
 -                              $list .= "$value";
 -                      } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
 -                              if ( count( $value ) == 0 ) {
 -                                      throw new MWException( __METHOD__ . ": empty input for field $field" );
 -                              } elseif ( count( $value ) == 1 ) {
 -                                      // Special-case single values, as IN isn't terribly efficient
 -                                      // Don't necessarily assume the single key is 0; we don't
 -                                      // enforce linear numeric ordering on other arrays here.
 -                                      $value = array_values( $value );
 -                                      $list .= $field . " = " . $this->addQuotes( $value[0] );
 -                              } else {
 -                                      $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
 -                              }
 -                      } elseif ( $value === null ) {
 -                              if ( $mode == LIST_AND || $mode == LIST_OR ) {
 -                                      $list .= "$field IS ";
 -                              } elseif ( $mode == LIST_SET ) {
 -                                      $list .= "$field = ";
 -                              }
 -                              $list .= 'NULL';
 -                      } else {
 -                              if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
 -                                      $list .= "$field = ";
 -                              }
 -                              $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
 -                      }
                }
  
 -              return $list;
 +              return parent::makeList( $a, $mode );
        }
  
        /**
         * @param int $limit The SQL limit
         * @param bool|int $offset The SQL offset (default false)
         * @return array|string
 +       * @throws DBUnexpectedError
         */
        public function limitResult( $sql, $limit, $offset = false ) {
                if ( $offset === false || $offset == 0 ) {
                        }
                } else {
                        // This one is fun, we need to pull out the select list as well as any ORDER BY clause
 -                      $select = $orderby = array();
 +                      $select = $orderby = [];
                        $s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select );
                        $s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby );
                        $overOrder = $postOrder = '';
                // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
                $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
                if ( preg_match( $pattern, $sql, $matches ) ) {
 -                      // row_count = $matches[4]
                        $row_count = $matches[4];
 -                      // offset = $matches[3] OR $matches[6]
 -                      $offset = $matches[3] or
 -                      $offset = $matches[6] or
 -                      $offset = false;
 +                      $offset = $matches[3] ?: $matches[6] ?: false;
  
                        // strip the matching LIMIT clause out
                        $sql = str_replace( $matches[0], '', $sql );
         * @param string $s
         * @return string
         */
 -      public function strencode( $s ) { # Should not be called by us
 +      public function strencode( $s ) {
 +              // Should not be called by us
 +
                return str_replace( "'", "''", $s );
        }
  
        /**
 -       * @param string $s
 +       * @param string|Blob $s
         * @return string
         */
        public function addQuotes( $s ) {
                return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']';
        }
  
+       /**
+        * MS SQL supports more pattern operators than other databases (ex: [,],^)
+        *
+        * @param string $s
+        * @return string
+        */
+       protected function escapeLikeInternal( $s ) {
+               return addcslashes( $s, '\%_[]^' );
+       }
+       /**
+        * MS SQL requires specifying the escape character used in a LIKE query
+        * or using Square brackets to surround characters that are to be escaped
+        * http://msdn.microsoft.com/en-us/library/ms179859.aspx
+        * Here we take the Specify-Escape-Character approach since it's less
+        * invasive, renders a query that is closer to other DB's and better at
+        * handling square bracket escaping
+        *
+        * @return string Fully built LIKE statement
+        */
+       public function buildLike() {
+               $params = func_get_args();
+               if ( count( $params ) > 0 && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+               return parent::buildLike( $params ) . " ESCAPE '\' ";
+       }
        /**
         * @param string $db
         * @return bool
                $tailOpts = '';
                $startOpts = '';
  
 -              $noKeyOptions = array();
 +              $noKeyOptions = [];
                foreach ( $options as $key => $option ) {
                        if ( is_numeric( $key ) ) {
                                $noKeyOptions[$option] = true;
                }
  
                // we want this to be compatible with the output of parent::makeSelectOptions()
 -              return array( $startOpts, '', $tailOpts, '' );
 +              return [ $startOpts, '', $tailOpts, '' ];
        }
  
        /**
         * @since 1.23
         */
        public function buildGroupConcatField( $delim, $table, $field, $conds = '',
 -              $join_conds = array()
 +              $join_conds = []
        ) {
                $gcsq = 'gcsq_' . $this->mSubqueryId;
                $this->mSubqueryId++;
                $delimLen = strlen( $delim );
                $fld = "{$field} + {$this->addQuotes( $delim )}";
                $sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM ("
 -                      . $this->selectSQLText( $table, $fld, $conds, null, array( 'FOR XML' ), $join_conds )
 +                      . $this->selectSQLText( $table, $fld, $conds, null, [ 'FOR XML' ], $join_conds )
                        . ") {$gcsq} ({$field}))";
  
                return $sql;
  
                return isset( $this->mBinaryColumnCache[$tableRaw] )
                        ? $this->mBinaryColumnCache[$tableRaw]
 -                      : array();
 +                      : [];
        }
  
        /**
  
                return isset( $this->mBitColumnCache[$tableRaw] )
                        ? $this->mBitColumnCache[$tableRaw]
 -                      : array();
 +                      : [];
        }
  
        private function populateColumnCaches() {
                $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*',
 -                      array(
 +                      [
                                'TABLE_CATALOG' => $this->mDBname,
                                'TABLE_SCHEMA' => $this->mSchema,
 -                              'DATA_TYPE' => array( 'varbinary', 'binary', 'image', 'bit' )
 -                      ) );
 +                              'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ]
 +                      ] );
  
 -              $this->mBinaryColumnCache = array();
 -              $this->mBitColumnCache = array();
 +              $this->mBinaryColumnCache = [];
 +              $this->mBitColumnCache = [];
                foreach ( $res as $row ) {
                        if ( $row->DATA_TYPE == 'bit' ) {
                                $this->mBitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
@@@ -1428,7 -1490,7 +1457,7 @@@ class MssqlResultWrapper extends Result
                $res = $this->result;
  
                if ( $this->mSeekTo !== null ) {
 -                      $result = sqlsrv_fetch_object( $res, 'stdClass', array(),
 +                      $result = sqlsrv_fetch_object( $res, 'stdClass', [],
                                SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo );
                        $this->mSeekTo = null;
                } else {