Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/db/DatabaseMssql.php (4 issues)

Upgrade to new PHP Analysis Engine

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
The variable $ret does not seem to be defined for all execution paths leading up to this point.

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:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

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

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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 $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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
This method is not used, and could be removed.
Loading history...
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
Comprehensibility Bug introduced by
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 getFirstName() method in the Son calls the wrong method in the parent class.

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