wikimedia /
mediawiki
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * This is the MS SQL Server Native database abstraction layer. |
||
| 4 | * |
||
| 5 | * This program is free software; you can redistribute it and/or modify |
||
| 6 | * it under the terms of the GNU General Public License as published by |
||
| 7 | * the Free Software Foundation; either version 2 of the License, or |
||
| 8 | * (at your option) any later version. |
||
| 9 | * |
||
| 10 | * This program is distributed in the hope that it will be useful, |
||
| 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 13 | * GNU General Public License for more details. |
||
| 14 | * |
||
| 15 | * You should have received a copy of the GNU General Public License along |
||
| 16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
| 17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 18 | * http://www.gnu.org/copyleft/gpl.html |
||
| 19 | * |
||
| 20 | * @file |
||
| 21 | * @ingroup Database |
||
| 22 | * @author Joel Penner <a-joelpe at microsoft dot com> |
||
| 23 | * @author Chris Pucci <a-cpucci at microsoft dot com> |
||
| 24 | * @author Ryan Biesemeyer <v-ryanbi at microsoft dot com> |
||
| 25 | * @author Ryan Schmidt <skizzerz at gmail dot com> |
||
| 26 | */ |
||
| 27 | |||
| 28 | /** |
||
| 29 | * @ingroup Database |
||
| 30 | */ |
||
| 31 | class DatabaseMssql extends DatabaseBase { |
||
| 32 | protected $mInsertId = null; |
||
| 33 | protected $mLastResult = null; |
||
| 34 | protected $mAffectedRows = null; |
||
| 35 | protected $mSubqueryId = 0; |
||
| 36 | protected $mScrollableCursor = true; |
||
| 37 | protected $mPrepareStatements = true; |
||
| 38 | protected $mBinaryColumnCache = null; |
||
| 39 | protected $mBitColumnCache = null; |
||
| 40 | protected $mIgnoreDupKeyErrors = false; |
||
| 41 | protected $mIgnoreErrors = []; |
||
| 42 | |||
| 43 | protected $mPort; |
||
| 44 | |||
| 45 | public function implicitGroupby() { |
||
| 46 | return false; |
||
| 47 | } |
||
| 48 | |||
| 49 | public function implicitOrderby() { |
||
| 50 | return false; |
||
| 51 | } |
||
| 52 | |||
| 53 | public function unionSupportsOrderAndLimit() { |
||
| 54 | return false; |
||
| 55 | } |
||
| 56 | |||
| 57 | /** |
||
| 58 | * Usually aborts on failure |
||
| 59 | * @param string $server |
||
| 60 | * @param string $user |
||
| 61 | * @param string $password |
||
| 62 | * @param string $dbName |
||
| 63 | * @throws DBConnectionError |
||
| 64 | * @return bool|resource|null |
||
| 65 | */ |
||
| 66 | public function open( $server, $user, $password, $dbName ) { |
||
| 67 | # Test for driver support, to avoid suppressed fatal error |
||
| 68 | if ( !function_exists( 'sqlsrv_connect' ) ) { |
||
| 69 | throw new DBConnectionError( |
||
| 70 | $this, |
||
| 71 | "Microsoft SQL Server Native (sqlsrv) functions missing. |
||
| 72 | You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n" |
||
| 73 | ); |
||
| 74 | } |
||
| 75 | |||
| 76 | global $wgDBport, $wgDBWindowsAuthentication; |
||
| 77 | |||
| 78 | # e.g. the class is being loaded |
||
| 79 | if ( !strlen( $user ) ) { |
||
| 80 | return null; |
||
| 81 | } |
||
| 82 | |||
| 83 | $this->close(); |
||
| 84 | $this->mServer = $server; |
||
| 85 | $this->mPort = $wgDBport; |
||
| 86 | $this->mUser = $user; |
||
| 87 | $this->mPassword = $password; |
||
| 88 | $this->mDBname = $dbName; |
||
| 89 | |||
| 90 | $connectionInfo = []; |
||
| 91 | |||
| 92 | if ( $dbName ) { |
||
| 93 | $connectionInfo['Database'] = $dbName; |
||
| 94 | } |
||
| 95 | |||
| 96 | // Decide which auth scenerio to use |
||
| 97 | // if we are using Windows auth, don't add credentials to $connectionInfo |
||
| 98 | if ( !$wgDBWindowsAuthentication ) { |
||
| 99 | $connectionInfo['UID'] = $user; |
||
| 100 | $connectionInfo['PWD'] = $password; |
||
| 101 | } |
||
| 102 | |||
| 103 | MediaWiki\suppressWarnings(); |
||
| 104 | $this->mConn = sqlsrv_connect( $server, $connectionInfo ); |
||
| 105 | MediaWiki\restoreWarnings(); |
||
| 106 | |||
| 107 | if ( $this->mConn === false ) { |
||
| 108 | throw new DBConnectionError( $this, $this->lastError() ); |
||
| 109 | } |
||
| 110 | |||
| 111 | $this->mOpened = true; |
||
| 112 | |||
| 113 | return $this->mConn; |
||
| 114 | } |
||
| 115 | |||
| 116 | /** |
||
| 117 | * Closes a database connection, if it is open |
||
| 118 | * Returns success, true if already closed |
||
| 119 | * @return bool |
||
| 120 | */ |
||
| 121 | protected function closeConnection() { |
||
| 122 | return sqlsrv_close( $this->mConn ); |
||
| 123 | } |
||
| 124 | |||
| 125 | /** |
||
| 126 | * @param bool|MssqlResultWrapper|resource $result |
||
| 127 | * @return bool|MssqlResultWrapper |
||
| 128 | */ |
||
| 129 | protected function resultObject( $result ) { |
||
| 130 | if ( !$result ) { |
||
| 131 | return false; |
||
| 132 | } elseif ( $result instanceof MssqlResultWrapper ) { |
||
| 133 | return $result; |
||
| 134 | } elseif ( $result === true ) { |
||
| 135 | // Successful write query |
||
| 136 | return $result; |
||
| 137 | } else { |
||
| 138 | return new MssqlResultWrapper( $this, $result ); |
||
| 139 | } |
||
| 140 | } |
||
| 141 | |||
| 142 | /** |
||
| 143 | * @param string $sql |
||
| 144 | * @return bool|MssqlResult |
||
| 145 | * @throws DBUnexpectedError |
||
| 146 | */ |
||
| 147 | protected function doQuery( $sql ) { |
||
| 148 | if ( $this->getFlag( DBO_DEBUG ) ) { |
||
| 149 | wfDebug( "SQL: [$sql]\n" ); |
||
| 150 | } |
||
| 151 | $this->offset = 0; |
||
| 152 | |||
| 153 | // several extensions seem to think that all databases support limits |
||
| 154 | // via LIMIT N after the WHERE clause well, MSSQL uses SELECT TOP N, |
||
| 155 | // so to catch any of those extensions we'll do a quick check for a |
||
| 156 | // LIMIT clause and pass $sql through $this->LimitToTopN() which parses |
||
| 157 | // the limit clause and passes the result to $this->limitResult(); |
||
| 158 | if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) { |
||
| 159 | // massage LIMIT -> TopN |
||
| 160 | $sql = $this->LimitToTopN( $sql ); |
||
| 161 | } |
||
| 162 | |||
| 163 | // MSSQL doesn't have EXTRACT(epoch FROM XXX) |
||
| 164 | if ( preg_match( '#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) { |
||
| 165 | // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970 |
||
| 166 | $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql ); |
||
| 167 | } |
||
| 168 | |||
| 169 | // perform query |
||
| 170 | |||
| 171 | // SQLSRV_CURSOR_STATIC is slower than SQLSRV_CURSOR_CLIENT_BUFFERED (one of the two is |
||
| 172 | // needed if we want to be able to seek around the result set), however CLIENT_BUFFERED |
||
| 173 | // has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty |
||
| 174 | // strings make php throw a fatal error "Severe error translating Unicode" |
||
| 175 | if ( $this->mScrollableCursor ) { |
||
| 176 | $scrollArr = [ 'Scrollable' => SQLSRV_CURSOR_STATIC ]; |
||
| 177 | } else { |
||
| 178 | $scrollArr = []; |
||
| 179 | } |
||
| 180 | |||
| 181 | if ( $this->mPrepareStatements ) { |
||
| 182 | // we do prepare + execute so we can get its field metadata for later usage if desired |
||
| 183 | $stmt = sqlsrv_prepare( $this->mConn, $sql, [], $scrollArr ); |
||
| 184 | $success = sqlsrv_execute( $stmt ); |
||
| 185 | } else { |
||
| 186 | $stmt = sqlsrv_query( $this->mConn, $sql, [], $scrollArr ); |
||
| 187 | $success = (bool)$stmt; |
||
| 188 | } |
||
| 189 | |||
| 190 | // make a copy so that anything we add below does not get reflected in future queries |
||
| 191 | $ignoreErrors = $this->mIgnoreErrors; |
||
| 192 | |||
| 193 | if ( $this->mIgnoreDupKeyErrors ) { |
||
| 194 | // ignore duplicate key errors |
||
| 195 | // this emulates INSERT IGNORE in MySQL |
||
| 196 | $ignoreErrors[] = '2601'; // duplicate key error caused by unique index |
||
| 197 | $ignoreErrors[] = '2627'; // duplicate key error caused by primary key |
||
| 198 | $ignoreErrors[] = '3621'; // generic "the statement has been terminated" error |
||
| 199 | } |
||
| 200 | |||
| 201 | if ( $success === false ) { |
||
| 202 | $errors = sqlsrv_errors(); |
||
| 203 | $success = true; |
||
| 204 | |||
| 205 | foreach ( $errors as $err ) { |
||
| 206 | if ( !in_array( $err['code'], $ignoreErrors ) ) { |
||
| 207 | $success = false; |
||
| 208 | break; |
||
| 209 | } |
||
| 210 | } |
||
| 211 | |||
| 212 | if ( $success === false ) { |
||
| 213 | return false; |
||
| 214 | } |
||
| 215 | } |
||
| 216 | // remember number of rows affected |
||
| 217 | $this->mAffectedRows = sqlsrv_rows_affected( $stmt ); |
||
| 218 | |||
| 219 | return $stmt; |
||
| 220 | } |
||
| 221 | |||
| 222 | public function freeResult( $res ) { |
||
| 223 | if ( $res instanceof ResultWrapper ) { |
||
| 224 | $res = $res->result; |
||
| 225 | } |
||
| 226 | |||
| 227 | sqlsrv_free_stmt( $res ); |
||
| 228 | } |
||
| 229 | |||
| 230 | /** |
||
| 231 | * @param MssqlResultWrapper $res |
||
| 232 | * @return stdClass |
||
| 233 | */ |
||
| 234 | public function fetchObject( $res ) { |
||
| 235 | // $res is expected to be an instance of MssqlResultWrapper here |
||
| 236 | return $res->fetchObject(); |
||
| 237 | } |
||
| 238 | |||
| 239 | /** |
||
| 240 | * @param MssqlResultWrapper $res |
||
| 241 | * @return array |
||
| 242 | */ |
||
| 243 | public function fetchRow( $res ) { |
||
| 244 | return $res->fetchRow(); |
||
| 245 | } |
||
| 246 | |||
| 247 | /** |
||
| 248 | * @param mixed $res |
||
| 249 | * @return int |
||
| 250 | */ |
||
| 251 | public function numRows( $res ) { |
||
| 252 | if ( $res instanceof ResultWrapper ) { |
||
| 253 | $res = $res->result; |
||
| 254 | } |
||
| 255 | |||
| 256 | $ret = sqlsrv_num_rows( $res ); |
||
| 257 | |||
| 258 | if ( $ret === false ) { |
||
| 259 | // we cannot get an amount of rows from this cursor type |
||
| 260 | // has_rows returns bool true/false if the result has rows |
||
| 261 | $ret = (int)sqlsrv_has_rows( $res ); |
||
| 262 | } |
||
| 263 | |||
| 264 | return $ret; |
||
| 265 | } |
||
| 266 | |||
| 267 | /** |
||
| 268 | * @param mixed $res |
||
| 269 | * @return int |
||
| 270 | */ |
||
| 271 | public function numFields( $res ) { |
||
| 272 | if ( $res instanceof ResultWrapper ) { |
||
| 273 | $res = $res->result; |
||
| 274 | } |
||
| 275 | |||
| 276 | return sqlsrv_num_fields( $res ); |
||
| 277 | } |
||
| 278 | |||
| 279 | /** |
||
| 280 | * @param mixed $res |
||
| 281 | * @param int $n |
||
| 282 | * @return int |
||
| 283 | */ |
||
| 284 | public function fieldName( $res, $n ) { |
||
| 285 | if ( $res instanceof ResultWrapper ) { |
||
| 286 | $res = $res->result; |
||
| 287 | } |
||
| 288 | |||
| 289 | return sqlsrv_field_metadata( $res )[$n]['Name']; |
||
| 290 | } |
||
| 291 | |||
| 292 | /** |
||
| 293 | * This must be called after nextSequenceVal |
||
| 294 | * @return int|null |
||
| 295 | */ |
||
| 296 | public function insertId() { |
||
| 297 | return $this->mInsertId; |
||
| 298 | } |
||
| 299 | |||
| 300 | /** |
||
| 301 | * @param MssqlResultWrapper $res |
||
| 302 | * @param int $row |
||
| 303 | * @return bool |
||
| 304 | */ |
||
| 305 | public function dataSeek( $res, $row ) { |
||
| 306 | return $res->seek( $row ); |
||
| 307 | } |
||
| 308 | |||
| 309 | /** |
||
| 310 | * @return string |
||
| 311 | */ |
||
| 312 | public function lastError() { |
||
| 313 | $strRet = ''; |
||
| 314 | $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL ); |
||
| 315 | if ( $retErrors != null ) { |
||
| 316 | foreach ( $retErrors as $arrError ) { |
||
| 317 | $strRet .= $this->formatError( $arrError ) . "\n"; |
||
| 318 | } |
||
| 319 | } else { |
||
| 320 | $strRet = "No errors found"; |
||
| 321 | } |
||
| 322 | |||
| 323 | return $strRet; |
||
| 324 | } |
||
| 325 | |||
| 326 | /** |
||
| 327 | * @param array $err |
||
| 328 | * @return string |
||
| 329 | */ |
||
| 330 | private function formatError( $err ) { |
||
| 331 | return '[SQLSTATE ' . $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message']; |
||
| 332 | } |
||
| 333 | |||
| 334 | /** |
||
| 335 | * @return string |
||
| 336 | */ |
||
| 337 | public function lastErrno() { |
||
| 338 | $err = sqlsrv_errors( SQLSRV_ERR_ALL ); |
||
| 339 | if ( $err !== null && isset( $err[0] ) ) { |
||
| 340 | return $err[0]['code']; |
||
| 341 | } else { |
||
| 342 | return 0; |
||
| 343 | } |
||
| 344 | } |
||
| 345 | |||
| 346 | /** |
||
| 347 | * @return int |
||
| 348 | */ |
||
| 349 | public function affectedRows() { |
||
| 350 | return $this->mAffectedRows; |
||
| 351 | } |
||
| 352 | |||
| 353 | /** |
||
| 354 | * SELECT wrapper |
||
| 355 | * |
||
| 356 | * @param mixed $table Array or string, table name(s) (prefix auto-added) |
||
| 357 | * @param mixed $vars Array or string, field name(s) to be retrieved |
||
| 358 | * @param mixed $conds Array or string, condition(s) for WHERE |
||
| 359 | * @param string $fname Calling function name (use __METHOD__) for logs/profiling |
||
| 360 | * @param array $options Associative array of options (e.g. |
||
| 361 | * [ 'GROUP BY' => 'page_title' ]), see Database::makeSelectOptions |
||
| 362 | * code for list of supported stuff |
||
| 363 | * @param array $join_conds Associative array of table join conditions |
||
| 364 | * (optional) (e.g. [ 'page' => [ 'LEFT JOIN','page_latest=rev_id' ] ] |
||
| 365 | * @return mixed Database result resource (feed to Database::fetchObject |
||
| 366 | * or whatever), or false on failure |
||
| 367 | * @throws DBQueryError |
||
| 368 | * @throws DBUnexpectedError |
||
| 369 | * @throws Exception |
||
| 370 | */ |
||
| 371 | public function select( $table, $vars, $conds = '', $fname = __METHOD__, |
||
| 372 | $options = [], $join_conds = [] |
||
| 373 | ) { |
||
| 374 | $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); |
||
| 375 | if ( isset( $options['EXPLAIN'] ) ) { |
||
| 376 | try { |
||
| 377 | $this->mScrollableCursor = false; |
||
| 378 | $this->mPrepareStatements = false; |
||
| 379 | $this->query( "SET SHOWPLAN_ALL ON" ); |
||
| 380 | $ret = $this->query( $sql, $fname ); |
||
| 381 | $this->query( "SET SHOWPLAN_ALL OFF" ); |
||
| 382 | } catch ( DBQueryError $dqe ) { |
||
| 383 | if ( isset( $options['FOR COUNT'] ) ) { |
||
| 384 | // likely don't have privs for SHOWPLAN, so run a select count instead |
||
| 385 | $this->query( "SET SHOWPLAN_ALL OFF" ); |
||
| 386 | unset( $options['EXPLAIN'] ); |
||
| 387 | $ret = $this->select( |
||
| 388 | $table, |
||
| 389 | 'COUNT(*) AS EstimateRows', |
||
| 390 | $conds, |
||
| 391 | $fname, |
||
| 392 | $options, |
||
| 393 | $join_conds |
||
| 394 | ); |
||
| 395 | } else { |
||
| 396 | // someone actually wanted the query plan instead of an est row count |
||
| 397 | // let them know of the error |
||
| 398 | $this->mScrollableCursor = true; |
||
| 399 | $this->mPrepareStatements = true; |
||
| 400 | throw $dqe; |
||
| 401 | } |
||
| 402 | } |
||
| 403 | $this->mScrollableCursor = true; |
||
| 404 | $this->mPrepareStatements = true; |
||
| 405 | return $ret; |
||
| 406 | } |
||
| 407 | return $this->query( $sql, $fname ); |
||
| 408 | } |
||
| 409 | |||
| 410 | /** |
||
| 411 | * SELECT wrapper |
||
| 412 | * |
||
| 413 | * @param mixed $table Array or string, table name(s) (prefix auto-added) |
||
| 414 | * @param mixed $vars Array or string, field name(s) to be retrieved |
||
| 415 | * @param mixed $conds Array or string, condition(s) for WHERE |
||
| 416 | * @param string $fname Calling function name (use __METHOD__) for logs/profiling |
||
| 417 | * @param array $options Associative array of options (e.g. [ 'GROUP BY' => 'page_title' ]), |
||
| 418 | * see Database::makeSelectOptions code for list of supported stuff |
||
| 419 | * @param array $join_conds Associative array of table join conditions (optional) |
||
| 420 | * (e.g. [ 'page' => [ 'LEFT JOIN','page_latest=rev_id' ] ] |
||
| 421 | * @return string The SQL text |
||
| 422 | */ |
||
| 423 | public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, |
||
| 424 | $options = [], $join_conds = [] |
||
| 425 | ) { |
||
| 426 | if ( isset( $options['EXPLAIN'] ) ) { |
||
| 427 | unset( $options['EXPLAIN'] ); |
||
| 428 | } |
||
| 429 | |||
| 430 | $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); |
||
| 431 | |||
| 432 | // try to rewrite aggregations of bit columns (currently MAX and MIN) |
||
| 433 | if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) { |
||
| 434 | $bitColumns = []; |
||
| 435 | if ( is_array( $table ) ) { |
||
| 436 | foreach ( $table as $t ) { |
||
| 437 | $bitColumns += $this->getBitColumns( $this->tableName( $t ) ); |
||
| 438 | } |
||
| 439 | } else { |
||
| 440 | $bitColumns = $this->getBitColumns( $this->tableName( $table ) ); |
||
| 441 | } |
||
| 442 | |||
| 443 | foreach ( $bitColumns as $col => $info ) { |
||
| 444 | $replace = [ |
||
| 445 | "MAX({$col})" => "MAX(CAST({$col} AS tinyint))", |
||
| 446 | "MIN({$col})" => "MIN(CAST({$col} AS tinyint))", |
||
| 447 | ]; |
||
| 448 | $sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql ); |
||
| 449 | } |
||
| 450 | } |
||
| 451 | |||
| 452 | return $sql; |
||
| 453 | } |
||
| 454 | |||
| 455 | View Code Duplication | public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, |
|
| 456 | $fname = __METHOD__ |
||
| 457 | ) { |
||
| 458 | $this->mScrollableCursor = false; |
||
| 459 | try { |
||
| 460 | parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname ); |
||
| 461 | } catch ( Exception $e ) { |
||
| 462 | $this->mScrollableCursor = true; |
||
| 463 | throw $e; |
||
| 464 | } |
||
| 465 | $this->mScrollableCursor = true; |
||
| 466 | } |
||
| 467 | |||
| 468 | View Code Duplication | public function delete( $table, $conds, $fname = __METHOD__ ) { |
|
| 469 | $this->mScrollableCursor = false; |
||
| 470 | try { |
||
| 471 | parent::delete( $table, $conds, $fname ); |
||
| 472 | } catch ( Exception $e ) { |
||
| 473 | $this->mScrollableCursor = true; |
||
| 474 | throw $e; |
||
| 475 | } |
||
| 476 | $this->mScrollableCursor = true; |
||
| 477 | } |
||
| 478 | |||
| 479 | /** |
||
| 480 | * Estimate rows in dataset |
||
| 481 | * Returns estimated count, based on SHOWPLAN_ALL output |
||
| 482 | * This is not necessarily an accurate estimate, so use sparingly |
||
| 483 | * Returns -1 if count cannot be found |
||
| 484 | * Takes same arguments as Database::select() |
||
| 485 | * @param string $table |
||
| 486 | * @param string $vars |
||
| 487 | * @param string $conds |
||
| 488 | * @param string $fname |
||
| 489 | * @param array $options |
||
| 490 | * @return int |
||
| 491 | */ |
||
| 492 | View Code Duplication | public function estimateRowCount( $table, $vars = '*', $conds = '', |
|
| 493 | $fname = __METHOD__, $options = [] |
||
| 494 | ) { |
||
| 495 | // http://msdn2.microsoft.com/en-us/library/aa259203.aspx |
||
| 496 | $options['EXPLAIN'] = true; |
||
| 497 | $options['FOR COUNT'] = true; |
||
| 498 | $res = $this->select( $table, $vars, $conds, $fname, $options ); |
||
| 499 | |||
| 500 | $rows = -1; |
||
| 501 | if ( $res ) { |
||
| 502 | $row = $this->fetchRow( $res ); |
||
| 503 | |||
| 504 | if ( isset( $row['EstimateRows'] ) ) { |
||
| 505 | $rows = (int)$row['EstimateRows']; |
||
| 506 | } |
||
| 507 | } |
||
| 508 | |||
| 509 | return $rows; |
||
| 510 | } |
||
| 511 | |||
| 512 | /** |
||
| 513 | * Returns information about an index |
||
| 514 | * If errors are explicitly ignored, returns NULL on failure |
||
| 515 | * @param string $table |
||
| 516 | * @param string $index |
||
| 517 | * @param string $fname |
||
| 518 | * @return array|bool|null |
||
| 519 | */ |
||
| 520 | public function indexInfo( $table, $index, $fname = __METHOD__ ) { |
||
| 521 | # This does not return the same info as MYSQL would, but that's OK |
||
| 522 | # because MediaWiki never uses the returned value except to check for |
||
| 523 | # the existance of indexes. |
||
| 524 | $sql = "sp_helpindex '" . $this->tableName( $table ) . "'"; |
||
| 525 | $res = $this->query( $sql, $fname ); |
||
| 526 | |||
| 527 | if ( !$res ) { |
||
| 528 | return null; |
||
| 529 | } |
||
| 530 | |||
| 531 | $result = []; |
||
| 532 | foreach ( $res as $row ) { |
||
| 533 | if ( $row->index_name == $index ) { |
||
| 534 | $row->Non_unique = !stristr( $row->index_description, "unique" ); |
||
| 535 | $cols = explode( ", ", $row->index_keys ); |
||
| 536 | foreach ( $cols as $col ) { |
||
| 537 | $row->Column_name = trim( $col ); |
||
| 538 | $result[] = clone $row; |
||
| 539 | } |
||
| 540 | } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) { |
||
| 541 | $row->Non_unique = 0; |
||
| 542 | $cols = explode( ", ", $row->index_keys ); |
||
| 543 | foreach ( $cols as $col ) { |
||
| 544 | $row->Column_name = trim( $col ); |
||
| 545 | $result[] = clone $row; |
||
| 546 | } |
||
| 547 | } |
||
| 548 | } |
||
| 549 | |||
| 550 | return empty( $result ) ? false : $result; |
||
| 551 | } |
||
| 552 | |||
| 553 | /** |
||
| 554 | * INSERT wrapper, inserts an array into a table |
||
| 555 | * |
||
| 556 | * $arrToInsert may be a single associative array, or an array of these with numeric keys, for |
||
| 557 | * multi-row insert. |
||
| 558 | * |
||
| 559 | * Usually aborts on failure |
||
| 560 | * If errors are explicitly ignored, returns success |
||
| 561 | * @param string $table |
||
| 562 | * @param array $arrToInsert |
||
| 563 | * @param string $fname |
||
| 564 | * @param array $options |
||
| 565 | * @return bool |
||
| 566 | * @throws Exception |
||
| 567 | */ |
||
| 568 | public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = [] ) { |
||
| 569 | # No rows to insert, easy just return now |
||
| 570 | if ( !count( $arrToInsert ) ) { |
||
| 571 | return true; |
||
| 572 | } |
||
| 573 | |||
| 574 | if ( !is_array( $options ) ) { |
||
| 575 | $options = [ $options ]; |
||
| 576 | } |
||
| 577 | |||
| 578 | $table = $this->tableName( $table ); |
||
| 579 | |||
| 580 | View Code Duplication | if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row |
|
| 581 | $arrToInsert = [ 0 => $arrToInsert ]; // make everything multi row compatible |
||
| 582 | } |
||
| 583 | |||
| 584 | // We know the table we're inserting into, get its identity column |
||
| 585 | $identity = null; |
||
| 586 | // strip matching square brackets and the db/schema from table name |
||
| 587 | $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); |
||
| 588 | $tableRaw = array_pop( $tableRawArr ); |
||
| 589 | $res = $this->doQuery( |
||
| 590 | "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS " . |
||
| 591 | "WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" |
||
| 592 | ); |
||
| 593 | if ( $res && sqlsrv_has_rows( $res ) ) { |
||
| 594 | // There is an identity for this table. |
||
| 595 | $identityArr = sqlsrv_fetch_array( $res, SQLSRV_FETCH_ASSOC ); |
||
| 596 | $identity = array_pop( $identityArr ); |
||
| 597 | } |
||
| 598 | sqlsrv_free_stmt( $res ); |
||
| 599 | |||
| 600 | // Determine binary/varbinary fields so we can encode data as a hex string like 0xABCDEF |
||
| 601 | $binaryColumns = $this->getBinaryColumns( $table ); |
||
| 602 | |||
| 603 | // INSERT IGNORE is not supported by SQL Server |
||
| 604 | // remove IGNORE from options list and set ignore flag to true |
||
| 605 | if ( in_array( 'IGNORE', $options ) ) { |
||
| 606 | $options = array_diff( $options, [ 'IGNORE' ] ); |
||
| 607 | $this->mIgnoreDupKeyErrors = true; |
||
| 608 | } |
||
| 609 | |||
| 610 | foreach ( $arrToInsert as $a ) { |
||
| 611 | // start out with empty identity column, this is so we can return |
||
| 612 | // it as a result of the insert logic |
||
| 613 | $sqlPre = ''; |
||
| 614 | $sqlPost = ''; |
||
| 615 | $identityClause = ''; |
||
| 616 | |||
| 617 | // if we have an identity column |
||
| 618 | if ( $identity ) { |
||
| 619 | // iterate through |
||
| 620 | foreach ( $a as $k => $v ) { |
||
| 621 | if ( $k == $identity ) { |
||
| 622 | if ( !is_null( $v ) ) { |
||
| 623 | // there is a value being passed to us, |
||
| 624 | // we need to turn on and off inserted identity |
||
| 625 | $sqlPre = "SET IDENTITY_INSERT $table ON;"; |
||
| 626 | $sqlPost = ";SET IDENTITY_INSERT $table OFF;"; |
||
| 627 | } else { |
||
| 628 | // we can't insert NULL into an identity column, |
||
| 629 | // so remove the column from the insert. |
||
| 630 | unset( $a[$k] ); |
||
| 631 | } |
||
| 632 | } |
||
| 633 | } |
||
| 634 | |||
| 635 | // we want to output an identity column as result |
||
| 636 | $identityClause = "OUTPUT INSERTED.$identity "; |
||
| 637 | } |
||
| 638 | |||
| 639 | $keys = array_keys( $a ); |
||
| 640 | |||
| 641 | // Build the actual query |
||
| 642 | $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) . |
||
| 643 | " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES ("; |
||
| 644 | |||
| 645 | $first = true; |
||
| 646 | foreach ( $a as $key => $value ) { |
||
| 647 | if ( isset( $binaryColumns[$key] ) ) { |
||
| 648 | $value = new MssqlBlob( $value ); |
||
| 649 | } |
||
| 650 | if ( $first ) { |
||
| 651 | $first = false; |
||
| 652 | } else { |
||
| 653 | $sql .= ','; |
||
| 654 | } |
||
| 655 | if ( is_null( $value ) ) { |
||
| 656 | $sql .= 'null'; |
||
| 657 | } elseif ( is_array( $value ) || is_object( $value ) ) { |
||
| 658 | if ( is_object( $value ) && $value instanceof Blob ) { |
||
| 659 | $sql .= $this->addQuotes( $value ); |
||
| 660 | } else { |
||
| 661 | $sql .= $this->addQuotes( serialize( $value ) ); |
||
| 662 | } |
||
| 663 | } else { |
||
| 664 | $sql .= $this->addQuotes( $value ); |
||
| 665 | } |
||
| 666 | } |
||
| 667 | $sql .= ')' . $sqlPost; |
||
| 668 | |||
| 669 | // Run the query |
||
| 670 | $this->mScrollableCursor = false; |
||
| 671 | try { |
||
| 672 | $ret = $this->query( $sql ); |
||
| 673 | } catch ( Exception $e ) { |
||
| 674 | $this->mScrollableCursor = true; |
||
| 675 | $this->mIgnoreDupKeyErrors = false; |
||
| 676 | throw $e; |
||
| 677 | } |
||
| 678 | $this->mScrollableCursor = true; |
||
| 679 | |||
| 680 | if ( !is_null( $identity ) ) { |
||
| 681 | // then we want to get the identity column value we were assigned and save it off |
||
| 682 | $row = $ret->fetchObject(); |
||
| 683 | if ( is_object( $row ) ) { |
||
| 684 | $this->mInsertId = $row->$identity; |
||
| 685 | |||
| 686 | // it seems that mAffectedRows is -1 sometimes when OUTPUT INSERTED.identity is used |
||
| 687 | // if we got an identity back, we know for sure a row was affected, so adjust that here |
||
| 688 | if ( $this->mAffectedRows == -1 ) { |
||
| 689 | $this->mAffectedRows = 1; |
||
| 690 | } |
||
| 691 | } |
||
| 692 | } |
||
| 693 | } |
||
| 694 | $this->mIgnoreDupKeyErrors = false; |
||
| 695 | return $ret; |
||
|
0 ignored issues
–
show
|
|||
| 696 | } |
||
| 697 | |||
| 698 | /** |
||
| 699 | * INSERT SELECT wrapper |
||
| 700 | * $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ] |
||
| 701 | * Source items may be literals rather than field names, but strings should |
||
| 702 | * be quoted with Database::addQuotes(). |
||
| 703 | * @param string $destTable |
||
| 704 | * @param array|string $srcTable May be an array of tables. |
||
| 705 | * @param array $varMap |
||
| 706 | * @param array $conds May be "*" to copy the whole table. |
||
| 707 | * @param string $fname |
||
| 708 | * @param array $insertOptions |
||
| 709 | * @param array $selectOptions |
||
| 710 | * @return null|ResultWrapper |
||
| 711 | * @throws Exception |
||
| 712 | */ |
||
| 713 | public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, |
||
| 714 | $insertOptions = [], $selectOptions = [] |
||
| 715 | ) { |
||
| 716 | $this->mScrollableCursor = false; |
||
| 717 | try { |
||
| 718 | $ret = parent::nativeInsertSelect( |
||
| 719 | $destTable, |
||
| 720 | $srcTable, |
||
| 721 | $varMap, |
||
| 722 | $conds, |
||
| 723 | $fname, |
||
| 724 | $insertOptions, |
||
| 725 | $selectOptions |
||
| 726 | ); |
||
| 727 | } catch ( Exception $e ) { |
||
| 728 | $this->mScrollableCursor = true; |
||
| 729 | throw $e; |
||
| 730 | } |
||
| 731 | $this->mScrollableCursor = true; |
||
| 732 | |||
| 733 | return $ret; |
||
| 734 | } |
||
| 735 | |||
| 736 | /** |
||
| 737 | * UPDATE wrapper. Takes a condition array and a SET array. |
||
| 738 | * |
||
| 739 | * @param string $table Name of the table to UPDATE. This will be passed through |
||
| 740 | * Database::tableName(). |
||
| 741 | * |
||
| 742 | * @param array $values An array of values to SET. For each array element, |
||
| 743 | * the key gives the field name, and the value gives the data |
||
| 744 | * to set that field to. The data will be quoted by |
||
| 745 | * Database::addQuotes(). |
||
| 746 | * |
||
| 747 | * @param array $conds An array of conditions (WHERE). See |
||
| 748 | * Database::select() for the details of the format of |
||
| 749 | * condition arrays. Use '*' to update all rows. |
||
| 750 | * |
||
| 751 | * @param string $fname The function name of the caller (from __METHOD__), |
||
| 752 | * for logging and profiling. |
||
| 753 | * |
||
| 754 | * @param array $options An array of UPDATE options, can be: |
||
| 755 | * - IGNORE: Ignore unique key conflicts |
||
| 756 | * - LOW_PRIORITY: MySQL-specific, see MySQL manual. |
||
| 757 | * @return bool |
||
| 758 | * @throws DBUnexpectedError |
||
| 759 | * @throws Exception |
||
| 760 | */ |
||
| 761 | function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) { |
||
| 762 | $table = $this->tableName( $table ); |
||
| 763 | $binaryColumns = $this->getBinaryColumns( $table ); |
||
| 764 | |||
| 765 | $opts = $this->makeUpdateOptions( $options ); |
||
| 766 | $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns ); |
||
| 767 | |||
| 768 | if ( $conds !== [] && $conds !== '*' ) { |
||
| 769 | $sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns ); |
||
| 770 | } |
||
| 771 | |||
| 772 | $this->mScrollableCursor = false; |
||
| 773 | try { |
||
| 774 | $this->query( $sql ); |
||
| 775 | } catch ( Exception $e ) { |
||
| 776 | $this->mScrollableCursor = true; |
||
| 777 | throw $e; |
||
| 778 | } |
||
| 779 | $this->mScrollableCursor = true; |
||
| 780 | return true; |
||
| 781 | } |
||
| 782 | |||
| 783 | /** |
||
| 784 | * Makes an encoded list of strings from an array |
||
| 785 | * @param array $a Containing the data |
||
| 786 | * @param int $mode Constant |
||
| 787 | * - LIST_COMMA: comma separated, no field names |
||
| 788 | * - LIST_AND: ANDed WHERE clause (without the WHERE). See |
||
| 789 | * the documentation for $conds in Database::select(). |
||
| 790 | * - LIST_OR: ORed WHERE clause (without the WHERE) |
||
| 791 | * - LIST_SET: comma separated with field names, like a SET clause |
||
| 792 | * - LIST_NAMES: comma separated field names |
||
| 793 | * @param array $binaryColumns Contains a list of column names that are binary types |
||
| 794 | * This is a custom parameter only present for MS SQL. |
||
| 795 | * |
||
| 796 | * @throws DBUnexpectedError |
||
| 797 | * @return string |
||
| 798 | */ |
||
| 799 | public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = [] ) { |
||
| 800 | if ( !is_array( $a ) ) { |
||
| 801 | throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' ); |
||
| 802 | } |
||
| 803 | |||
| 804 | if ( $mode != LIST_NAMES ) { |
||
| 805 | // In MS SQL, values need to be specially encoded when they are |
||
| 806 | // inserted into binary fields. Perform this necessary encoding |
||
| 807 | // for the specified set of columns. |
||
| 808 | foreach ( array_keys( $a ) as $field ) { |
||
| 809 | if ( !isset( $binaryColumns[$field] ) ) { |
||
| 810 | continue; |
||
| 811 | } |
||
| 812 | |||
| 813 | if ( is_array( $a[$field] ) ) { |
||
| 814 | foreach ( $a[$field] as &$v ) { |
||
| 815 | $v = new MssqlBlob( $v ); |
||
| 816 | } |
||
| 817 | unset( $v ); |
||
| 818 | } else { |
||
| 819 | $a[$field] = new MssqlBlob( $a[$field] ); |
||
| 820 | } |
||
| 821 | } |
||
| 822 | } |
||
| 823 | |||
| 824 | return parent::makeList( $a, $mode ); |
||
| 825 | } |
||
| 826 | |||
| 827 | /** |
||
| 828 | * @param string $table |
||
| 829 | * @param string $field |
||
| 830 | * @return int Returns the size of a text field, or -1 for "unlimited" |
||
| 831 | */ |
||
| 832 | public function textFieldSize( $table, $field ) { |
||
| 833 | $table = $this->tableName( $table ); |
||
| 834 | $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns |
||
| 835 | WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'"; |
||
| 836 | $res = $this->query( $sql ); |
||
| 837 | $row = $this->fetchRow( $res ); |
||
| 838 | $size = -1; |
||
| 839 | if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) { |
||
| 840 | $size = $row['CHARACTER_MAXIMUM_LENGTH']; |
||
| 841 | } |
||
| 842 | |||
| 843 | return $size; |
||
| 844 | } |
||
| 845 | |||
| 846 | /** |
||
| 847 | * Construct a LIMIT query with optional offset |
||
| 848 | * This is used for query pages |
||
| 849 | * |
||
| 850 | * @param string $sql SQL query we will append the limit too |
||
| 851 | * @param int $limit The SQL limit |
||
| 852 | * @param bool|int $offset The SQL offset (default false) |
||
| 853 | * @return array|string |
||
| 854 | * @throws DBUnexpectedError |
||
| 855 | */ |
||
| 856 | public function limitResult( $sql, $limit, $offset = false ) { |
||
| 857 | if ( $offset === false || $offset == 0 ) { |
||
| 858 | if ( strpos( $sql, "SELECT" ) === false ) { |
||
| 859 | return "TOP {$limit} " . $sql; |
||
| 860 | } else { |
||
| 861 | return preg_replace( '/\bSELECT(\s+DISTINCT)?\b/Dsi', |
||
| 862 | 'SELECT$1 TOP ' . $limit, $sql, 1 ); |
||
| 863 | } |
||
| 864 | } else { |
||
| 865 | // This one is fun, we need to pull out the select list as well as any ORDER BY clause |
||
| 866 | $select = $orderby = []; |
||
| 867 | $s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select ); |
||
| 868 | $s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby ); |
||
| 869 | $overOrder = $postOrder = ''; |
||
|
0 ignored issues
–
show
$overOrder is not used, you could remove the assignment.
This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently. $myVar = 'Value';
$higher = false;
if (rand(1, 6) > 3) {
$higher = true;
} else {
$higher = false;
}
Both the Loading history...
|
|||
| 870 | $first = $offset + 1; |
||
| 871 | $last = $offset + $limit; |
||
| 872 | $sub1 = 'sub_' . $this->mSubqueryId; |
||
| 873 | $sub2 = 'sub_' . ( $this->mSubqueryId + 1 ); |
||
| 874 | $this->mSubqueryId += 2; |
||
| 875 | if ( !$s1 ) { |
||
| 876 | // wat |
||
| 877 | throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" ); |
||
| 878 | } |
||
| 879 | if ( !$s2 ) { |
||
| 880 | // no ORDER BY |
||
| 881 | $overOrder = 'ORDER BY (SELECT 1)'; |
||
| 882 | } else { |
||
| 883 | if ( !isset( $orderby[2] ) || !$orderby[2] ) { |
||
| 884 | // don't need to strip it out if we're using a FOR XML clause |
||
| 885 | $sql = str_replace( $orderby[1], '', $sql ); |
||
| 886 | } |
||
| 887 | $overOrder = $orderby[1]; |
||
| 888 | $postOrder = ' ' . $overOrder; |
||
| 889 | } |
||
| 890 | $sql = "SELECT {$select[1]} |
||
| 891 | FROM ( |
||
| 892 | SELECT ROW_NUMBER() OVER({$overOrder}) AS rowNumber, * |
||
| 893 | FROM ({$sql}) {$sub1} |
||
| 894 | ) {$sub2} |
||
| 895 | WHERE rowNumber BETWEEN {$first} AND {$last}{$postOrder}"; |
||
| 896 | |||
| 897 | return $sql; |
||
| 898 | } |
||
| 899 | } |
||
| 900 | |||
| 901 | /** |
||
| 902 | * If there is a limit clause, parse it, strip it, and pass the remaining |
||
| 903 | * SQL through limitResult() with the appropriate parameters. Not the |
||
| 904 | * prettiest solution, but better than building a whole new parser. This |
||
| 905 | * exists becase there are still too many extensions that don't use dynamic |
||
| 906 | * sql generation. |
||
| 907 | * |
||
| 908 | * @param string $sql |
||
| 909 | * @return array|mixed|string |
||
| 910 | */ |
||
| 911 | public function LimitToTopN( $sql ) { |
||
| 912 | // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset} |
||
| 913 | $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i'; |
||
| 914 | if ( preg_match( $pattern, $sql, $matches ) ) { |
||
| 915 | $row_count = $matches[4]; |
||
| 916 | $offset = $matches[3] ?: $matches[6] ?: false; |
||
| 917 | |||
| 918 | // strip the matching LIMIT clause out |
||
| 919 | $sql = str_replace( $matches[0], '', $sql ); |
||
| 920 | |||
| 921 | return $this->limitResult( $sql, $row_count, $offset ); |
||
| 922 | } |
||
| 923 | |||
| 924 | return $sql; |
||
| 925 | } |
||
| 926 | |||
| 927 | /** |
||
| 928 | * @return string Wikitext of a link to the server software's web site |
||
| 929 | */ |
||
| 930 | public function getSoftwareLink() { |
||
| 931 | return "[{{int:version-db-mssql-url}} MS SQL Server]"; |
||
| 932 | } |
||
| 933 | |||
| 934 | /** |
||
| 935 | * @return string Version information from the database |
||
| 936 | */ |
||
| 937 | public function getServerVersion() { |
||
| 938 | $server_info = sqlsrv_server_info( $this->mConn ); |
||
| 939 | $version = 'Error'; |
||
| 940 | if ( isset( $server_info['SQLServerVersion'] ) ) { |
||
| 941 | $version = $server_info['SQLServerVersion']; |
||
| 942 | } |
||
| 943 | |||
| 944 | return $version; |
||
| 945 | } |
||
| 946 | |||
| 947 | /** |
||
| 948 | * @param string $table |
||
| 949 | * @param string $fname |
||
| 950 | * @return bool |
||
| 951 | */ |
||
| 952 | public function tableExists( $table, $fname = __METHOD__ ) { |
||
| 953 | list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); |
||
| 954 | |||
| 955 | if ( $db !== false ) { |
||
| 956 | // remote database |
||
| 957 | wfDebug( "Attempting to call tableExists on a remote table" ); |
||
| 958 | return false; |
||
| 959 | } |
||
| 960 | |||
| 961 | if ( $schema === false ) { |
||
| 962 | global $wgDBmwschema; |
||
| 963 | $schema = $wgDBmwschema; |
||
| 964 | } |
||
| 965 | |||
| 966 | $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES |
||
| 967 | WHERE TABLE_TYPE = 'BASE TABLE' |
||
| 968 | AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" ); |
||
| 969 | |||
| 970 | if ( $res->numRows() ) { |
||
| 971 | return true; |
||
| 972 | } else { |
||
| 973 | return false; |
||
| 974 | } |
||
| 975 | } |
||
| 976 | |||
| 977 | /** |
||
| 978 | * Query whether a given column exists in the mediawiki schema |
||
| 979 | * @param string $table |
||
| 980 | * @param string $field |
||
| 981 | * @param string $fname |
||
| 982 | * @return bool |
||
| 983 | */ |
||
| 984 | public function fieldExists( $table, $field, $fname = __METHOD__ ) { |
||
| 985 | list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); |
||
| 986 | |||
| 987 | if ( $db !== false ) { |
||
| 988 | // remote database |
||
| 989 | wfDebug( "Attempting to call fieldExists on a remote table" ); |
||
| 990 | return false; |
||
| 991 | } |
||
| 992 | |||
| 993 | $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS |
||
| 994 | WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); |
||
| 995 | |||
| 996 | if ( $res->numRows() ) { |
||
| 997 | return true; |
||
| 998 | } else { |
||
| 999 | return false; |
||
| 1000 | } |
||
| 1001 | } |
||
| 1002 | |||
| 1003 | public function fieldInfo( $table, $field ) { |
||
| 1004 | list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); |
||
| 1005 | |||
| 1006 | if ( $db !== false ) { |
||
| 1007 | // remote database |
||
| 1008 | wfDebug( "Attempting to call fieldInfo on a remote table" ); |
||
| 1009 | return false; |
||
| 1010 | } |
||
| 1011 | |||
| 1012 | $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS |
||
| 1013 | WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); |
||
| 1014 | |||
| 1015 | $meta = $res->fetchRow(); |
||
| 1016 | if ( $meta ) { |
||
| 1017 | return new MssqlField( $meta ); |
||
| 1018 | } |
||
| 1019 | |||
| 1020 | return false; |
||
| 1021 | } |
||
| 1022 | |||
| 1023 | /** |
||
| 1024 | * Begin a transaction, committing any previously open transaction |
||
| 1025 | * @param string $fname |
||
| 1026 | */ |
||
| 1027 | protected function doBegin( $fname = __METHOD__ ) { |
||
| 1028 | sqlsrv_begin_transaction( $this->mConn ); |
||
| 1029 | $this->mTrxLevel = 1; |
||
| 1030 | } |
||
| 1031 | |||
| 1032 | /** |
||
| 1033 | * End a transaction |
||
| 1034 | * @param string $fname |
||
| 1035 | */ |
||
| 1036 | protected function doCommit( $fname = __METHOD__ ) { |
||
| 1037 | sqlsrv_commit( $this->mConn ); |
||
| 1038 | $this->mTrxLevel = 0; |
||
| 1039 | } |
||
| 1040 | |||
| 1041 | /** |
||
| 1042 | * Rollback a transaction. |
||
| 1043 | * No-op on non-transactional databases. |
||
| 1044 | * @param string $fname |
||
| 1045 | */ |
||
| 1046 | protected function doRollback( $fname = __METHOD__ ) { |
||
| 1047 | sqlsrv_rollback( $this->mConn ); |
||
| 1048 | $this->mTrxLevel = 0; |
||
| 1049 | } |
||
| 1050 | |||
| 1051 | /** |
||
| 1052 | * Escapes a identifier for use inm SQL. |
||
| 1053 | * Throws an exception if it is invalid. |
||
| 1054 | * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx |
||
| 1055 | * @param string $identifier |
||
| 1056 | * @throws InvalidArgumentException |
||
| 1057 | * @return string |
||
| 1058 | */ |
||
| 1059 | private function escapeIdentifier( $identifier ) { |
||
|
0 ignored issues
–
show
|
|||
| 1060 | if ( strlen( $identifier ) == 0 ) { |
||
| 1061 | throw new InvalidArgumentException( "An identifier must not be empty" ); |
||
| 1062 | } |
||
| 1063 | if ( strlen( $identifier ) > 128 ) { |
||
| 1064 | throw new InvalidArgumentException( "The identifier '$identifier' is too long (max. 128)" ); |
||
| 1065 | } |
||
| 1066 | if ( ( strpos( $identifier, '[' ) !== false ) |
||
| 1067 | || ( strpos( $identifier, ']' ) !== false ) |
||
| 1068 | ) { |
||
| 1069 | // It may be allowed if you quoted with double quotation marks, but |
||
| 1070 | // that would break if QUOTED_IDENTIFIER is OFF |
||
| 1071 | throw new InvalidArgumentException( "Square brackets are not allowed in '$identifier'" ); |
||
| 1072 | } |
||
| 1073 | |||
| 1074 | return "[$identifier]"; |
||
| 1075 | } |
||
| 1076 | |||
| 1077 | /** |
||
| 1078 | * @param string $s |
||
| 1079 | * @return string |
||
| 1080 | */ |
||
| 1081 | public function strencode( $s ) { |
||
| 1082 | // Should not be called by us |
||
| 1083 | |||
| 1084 | return str_replace( "'", "''", $s ); |
||
| 1085 | } |
||
| 1086 | |||
| 1087 | /** |
||
| 1088 | * @param string|int|null|bool|Blob $s |
||
| 1089 | * @return string|int |
||
| 1090 | */ |
||
| 1091 | public function addQuotes( $s ) { |
||
| 1092 | if ( $s instanceof MssqlBlob ) { |
||
| 1093 | return $s->fetch(); |
||
| 1094 | } elseif ( $s instanceof Blob ) { |
||
| 1095 | // this shouldn't really ever be called, but it's here if needed |
||
| 1096 | // (and will quite possibly make the SQL error out) |
||
| 1097 | $blob = new MssqlBlob( $s->fetch() ); |
||
| 1098 | return $blob->fetch(); |
||
| 1099 | } else { |
||
| 1100 | if ( is_bool( $s ) ) { |
||
| 1101 | $s = $s ? 1 : 0; |
||
| 1102 | } |
||
| 1103 | return parent::addQuotes( $s ); |
||
| 1104 | } |
||
| 1105 | } |
||
| 1106 | |||
| 1107 | /** |
||
| 1108 | * @param string $s |
||
| 1109 | * @return string |
||
| 1110 | */ |
||
| 1111 | public function addIdentifierQuotes( $s ) { |
||
| 1112 | // http://msdn.microsoft.com/en-us/library/aa223962.aspx |
||
| 1113 | return '[' . $s . ']'; |
||
| 1114 | } |
||
| 1115 | |||
| 1116 | /** |
||
| 1117 | * @param string $name |
||
| 1118 | * @return bool |
||
| 1119 | */ |
||
| 1120 | public function isQuotedIdentifier( $name ) { |
||
| 1121 | return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']'; |
||
| 1122 | } |
||
| 1123 | |||
| 1124 | /** |
||
| 1125 | * MS SQL supports more pattern operators than other databases (ex: [,],^) |
||
| 1126 | * |
||
| 1127 | * @param string $s |
||
| 1128 | * @return string |
||
| 1129 | */ |
||
| 1130 | protected function escapeLikeInternal( $s ) { |
||
| 1131 | return addcslashes( $s, '\%_[]^' ); |
||
| 1132 | } |
||
| 1133 | |||
| 1134 | /** |
||
| 1135 | * MS SQL requires specifying the escape character used in a LIKE query |
||
| 1136 | * or using Square brackets to surround characters that are to be escaped |
||
| 1137 | * https://msdn.microsoft.com/en-us/library/ms179859.aspx |
||
| 1138 | * Here we take the Specify-Escape-Character approach since it's less |
||
| 1139 | * invasive, renders a query that is closer to other DB's and better at |
||
| 1140 | * handling square bracket escaping |
||
| 1141 | * |
||
| 1142 | * @return string Fully built LIKE statement |
||
| 1143 | */ |
||
| 1144 | View Code Duplication | public function buildLike() { |
|
| 1145 | $params = func_get_args(); |
||
| 1146 | if ( count( $params ) > 0 && is_array( $params[0] ) ) { |
||
| 1147 | $params = $params[0]; |
||
| 1148 | } |
||
| 1149 | |||
| 1150 | return parent::buildLike( $params ) . " ESCAPE '\' "; |
||
| 1151 | } |
||
| 1152 | |||
| 1153 | /** |
||
| 1154 | * @param string $db |
||
| 1155 | * @return bool |
||
| 1156 | */ |
||
| 1157 | public function selectDB( $db ) { |
||
| 1158 | try { |
||
| 1159 | $this->mDBname = $db; |
||
| 1160 | $this->query( "USE $db" ); |
||
| 1161 | return true; |
||
| 1162 | } catch ( Exception $e ) { |
||
| 1163 | return false; |
||
| 1164 | } |
||
| 1165 | } |
||
| 1166 | |||
| 1167 | /** |
||
| 1168 | * @param array $options An associative array of options to be turned into |
||
| 1169 | * an SQL query, valid keys are listed in the function. |
||
| 1170 | * @return array |
||
| 1171 | */ |
||
| 1172 | public function makeSelectOptions( $options ) { |
||
| 1173 | $tailOpts = ''; |
||
| 1174 | $startOpts = ''; |
||
| 1175 | |||
| 1176 | $noKeyOptions = []; |
||
| 1177 | foreach ( $options as $key => $option ) { |
||
| 1178 | if ( is_numeric( $key ) ) { |
||
| 1179 | $noKeyOptions[$option] = true; |
||
| 1180 | } |
||
| 1181 | } |
||
| 1182 | |||
| 1183 | $tailOpts .= $this->makeGroupByWithHaving( $options ); |
||
| 1184 | |||
| 1185 | $tailOpts .= $this->makeOrderBy( $options ); |
||
| 1186 | |||
| 1187 | if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { |
||
| 1188 | $startOpts .= 'DISTINCT'; |
||
| 1189 | } |
||
| 1190 | |||
| 1191 | if ( isset( $noKeyOptions['FOR XML'] ) ) { |
||
| 1192 | // used in group concat field emulation |
||
| 1193 | $tailOpts .= " FOR XML PATH('')"; |
||
| 1194 | } |
||
| 1195 | |||
| 1196 | // we want this to be compatible with the output of parent::makeSelectOptions() |
||
| 1197 | return [ $startOpts, '', $tailOpts, '', '' ]; |
||
| 1198 | } |
||
| 1199 | |||
| 1200 | /** |
||
| 1201 | * Get the type of the DBMS, as it appears in $wgDBtype. |
||
| 1202 | * @return string |
||
| 1203 | */ |
||
| 1204 | public function getType() { |
||
| 1205 | return 'mssql'; |
||
| 1206 | } |
||
| 1207 | |||
| 1208 | /** |
||
| 1209 | * @param array $stringList |
||
| 1210 | * @return string |
||
| 1211 | */ |
||
| 1212 | public function buildConcat( $stringList ) { |
||
| 1213 | return implode( ' + ', $stringList ); |
||
| 1214 | } |
||
| 1215 | |||
| 1216 | /** |
||
| 1217 | * Build a GROUP_CONCAT or equivalent statement for a query. |
||
| 1218 | * MS SQL doesn't have GROUP_CONCAT so we emulate it with other stuff (and boy is it nasty) |
||
| 1219 | * |
||
| 1220 | * This is useful for combining a field for several rows into a single string. |
||
| 1221 | * NULL values will not appear in the output, duplicated values will appear, |
||
| 1222 | * and the resulting delimiter-separated values have no defined sort order. |
||
| 1223 | * Code using the results may need to use the PHP unique() or sort() methods. |
||
| 1224 | * |
||
| 1225 | * @param string $delim Glue to bind the results together |
||
| 1226 | * @param string|array $table Table name |
||
| 1227 | * @param string $field Field name |
||
| 1228 | * @param string|array $conds Conditions |
||
| 1229 | * @param string|array $join_conds Join conditions |
||
| 1230 | * @return string SQL text |
||
| 1231 | * @since 1.23 |
||
| 1232 | */ |
||
| 1233 | public function buildGroupConcatField( $delim, $table, $field, $conds = '', |
||
| 1234 | $join_conds = [] |
||
| 1235 | ) { |
||
| 1236 | $gcsq = 'gcsq_' . $this->mSubqueryId; |
||
| 1237 | $this->mSubqueryId++; |
||
| 1238 | |||
| 1239 | $delimLen = strlen( $delim ); |
||
| 1240 | $fld = "{$field} + {$this->addQuotes( $delim )}"; |
||
| 1241 | $sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM (" |
||
| 1242 | . $this->selectSQLText( $table, $fld, $conds, null, [ 'FOR XML' ], $join_conds ) |
||
| 1243 | . ") {$gcsq} ({$field}))"; |
||
| 1244 | |||
| 1245 | return $sql; |
||
| 1246 | } |
||
| 1247 | |||
| 1248 | /** |
||
| 1249 | * Returns an associative array for fields that are of type varbinary, binary, or image |
||
| 1250 | * $table can be either a raw table name or passed through tableName() first |
||
| 1251 | * @param string $table |
||
| 1252 | * @return array |
||
| 1253 | */ |
||
| 1254 | View Code Duplication | private function getBinaryColumns( $table ) { |
|
| 1255 | $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); |
||
| 1256 | $tableRaw = array_pop( $tableRawArr ); |
||
| 1257 | |||
| 1258 | if ( $this->mBinaryColumnCache === null ) { |
||
| 1259 | $this->populateColumnCaches(); |
||
| 1260 | } |
||
| 1261 | |||
| 1262 | return isset( $this->mBinaryColumnCache[$tableRaw] ) |
||
| 1263 | ? $this->mBinaryColumnCache[$tableRaw] |
||
| 1264 | : []; |
||
| 1265 | } |
||
| 1266 | |||
| 1267 | /** |
||
| 1268 | * @param string $table |
||
| 1269 | * @return array |
||
| 1270 | */ |
||
| 1271 | View Code Duplication | private function getBitColumns( $table ) { |
|
| 1272 | $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); |
||
| 1273 | $tableRaw = array_pop( $tableRawArr ); |
||
| 1274 | |||
| 1275 | if ( $this->mBitColumnCache === null ) { |
||
| 1276 | $this->populateColumnCaches(); |
||
| 1277 | } |
||
| 1278 | |||
| 1279 | return isset( $this->mBitColumnCache[$tableRaw] ) |
||
| 1280 | ? $this->mBitColumnCache[$tableRaw] |
||
| 1281 | : []; |
||
| 1282 | } |
||
| 1283 | |||
| 1284 | private function populateColumnCaches() { |
||
| 1285 | $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*', |
||
| 1286 | [ |
||
| 1287 | 'TABLE_CATALOG' => $this->mDBname, |
||
| 1288 | 'TABLE_SCHEMA' => $this->mSchema, |
||
| 1289 | 'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ] |
||
| 1290 | ] ); |
||
| 1291 | |||
| 1292 | $this->mBinaryColumnCache = []; |
||
| 1293 | $this->mBitColumnCache = []; |
||
| 1294 | foreach ( $res as $row ) { |
||
| 1295 | if ( $row->DATA_TYPE == 'bit' ) { |
||
| 1296 | $this->mBitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row; |
||
| 1297 | } else { |
||
| 1298 | $this->mBinaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row; |
||
| 1299 | } |
||
| 1300 | } |
||
| 1301 | } |
||
| 1302 | |||
| 1303 | /** |
||
| 1304 | * @param string $name |
||
| 1305 | * @param string $format |
||
| 1306 | * @return string |
||
| 1307 | */ |
||
| 1308 | function tableName( $name, $format = 'quoted' ) { |
||
| 1309 | # Replace reserved words with better ones |
||
| 1310 | switch ( $name ) { |
||
| 1311 | case 'user': |
||
| 1312 | return $this->realTableName( 'mwuser', $format ); |
||
| 1313 | default: |
||
| 1314 | return $this->realTableName( $name, $format ); |
||
| 1315 | } |
||
| 1316 | } |
||
| 1317 | |||
| 1318 | /** |
||
| 1319 | * call this instead of tableName() in the updater when renaming tables |
||
| 1320 | * @param string $name |
||
| 1321 | * @param string $format One of quoted, raw, or split |
||
| 1322 | * @return string |
||
| 1323 | */ |
||
| 1324 | function realTableName( $name, $format = 'quoted' ) { |
||
| 1325 | $table = parent::tableName( $name, $format ); |
||
|
0 ignored issues
–
show
It seems like you call parent on a different method (
tableName() instead of realTableName()). Are you sure this is correct? If so, you might want to change this to $this->tableName().
This check looks for a call to a parent method whose name is different than the method from which it is called. Consider the following code: class Daddy
{
protected function getFirstName()
{
return "Eidur";
}
protected function getSurName()
{
return "Gudjohnsen";
}
}
class Son
{
public function getFirstName()
{
return parent::getSurname();
}
}
The Loading history...
|
|||
| 1326 | if ( $format == 'split' ) { |
||
| 1327 | // Used internally, we want the schema split off from the table name and returned |
||
| 1328 | // as a list with 3 elements (database, schema, table) |
||
| 1329 | $table = explode( '.', $table ); |
||
| 1330 | while ( count( $table ) < 3 ) { |
||
| 1331 | array_unshift( $table, false ); |
||
| 1332 | } |
||
| 1333 | } |
||
| 1334 | return $table; |
||
| 1335 | } |
||
| 1336 | |||
| 1337 | /** |
||
| 1338 | * Delete a table |
||
| 1339 | * @param string $tableName |
||
| 1340 | * @param string $fName |
||
| 1341 | * @return bool|ResultWrapper |
||
| 1342 | * @since 1.18 |
||
| 1343 | */ |
||
| 1344 | View Code Duplication | public function dropTable( $tableName, $fName = __METHOD__ ) { |
|
| 1345 | if ( !$this->tableExists( $tableName, $fName ) ) { |
||
| 1346 | return false; |
||
| 1347 | } |
||
| 1348 | |||
| 1349 | // parent function incorrectly appends CASCADE, which we don't want |
||
| 1350 | $sql = "DROP TABLE " . $this->tableName( $tableName ); |
||
| 1351 | |||
| 1352 | return $this->query( $sql, $fName ); |
||
| 1353 | } |
||
| 1354 | |||
| 1355 | /** |
||
| 1356 | * Called in the installer and updater. |
||
| 1357 | * Probably doesn't need to be called anywhere else in the codebase. |
||
| 1358 | * @param bool|null $value |
||
| 1359 | * @return bool|null |
||
| 1360 | */ |
||
| 1361 | public function prepareStatements( $value = null ) { |
||
| 1362 | return wfSetVar( $this->mPrepareStatements, $value ); |
||
| 1363 | } |
||
| 1364 | |||
| 1365 | /** |
||
| 1366 | * Called in the installer and updater. |
||
| 1367 | * Probably doesn't need to be called anywhere else in the codebase. |
||
| 1368 | * @param bool|null $value |
||
| 1369 | * @return bool|null |
||
| 1370 | */ |
||
| 1371 | public function scrollableCursor( $value = null ) { |
||
| 1372 | return wfSetVar( $this->mScrollableCursor, $value ); |
||
| 1373 | } |
||
| 1374 | |||
| 1375 | /** |
||
| 1376 | * Called in the installer and updater. |
||
| 1377 | * Probably doesn't need to be called anywhere else in the codebase. |
||
| 1378 | * @param array|null $value |
||
| 1379 | * @return array|null |
||
| 1380 | */ |
||
| 1381 | public function ignoreErrors( array $value = null ) { |
||
| 1382 | return wfSetVar( $this->mIgnoreErrors, $value ); |
||
| 1383 | } |
||
| 1384 | } // end DatabaseMssql class |
||
| 1385 |
If you define a variable conditionally, it can happen that it is not defined for all execution paths.
Let’s take a look at an example:
In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.
Available Fixes
Check for existence of the variable explicitly:
Define a default value for the variable:
Add a value for the missing path: