Completed
Branch master (62f6c6)
by
unknown
21:31
created

DatabaseMssql   D

Complexity

Total Complexity 196

Size/Duplication

Total Lines 1383
Duplicated Lines 7.59 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
dl 105
loc 1383
rs 4.4102
c 0
b 0
f 0
wmc 196
lcom 1
cbo 10

67 Methods

Rating   Name   Duplication   Size   Complexity  
A cascadingDeletes() 0 3 1
A cleanupTriggers() 0 3 1
A strictIPs() 0 3 1
A realTimestamps() 0 3 1
A implicitGroupby() 0 3 1
A implicitOrderby() 0 3 1
A functionalIndexes() 0 3 1
A unionSupportsOrderAndLimit() 0 3 1
B open() 0 49 6
A closeConnection() 0 3 1
A resultObject() 0 12 4
F doQuery() 0 74 11
A freeResult() 0 7 2
A fetchObject() 0 4 1
A fetchRow() 0 3 1
A numRows() 0 15 3
A numFields() 0 7 2
A fieldName() 0 7 2
A insertId() 0 3 1
A dataSeek() 0 3 1
A lastError() 0 13 3
A formatError() 0 3 1
A lastErrno() 0 8 3
A affectedRows() 0 3 1
B select() 0 38 4
C selectSQLText() 0 31 7
A deleteJoin() 12 12 2
A delete() 10 10 2
A estimateRowCount() 19 19 3
D indexInfo() 0 32 9
F insert() 3 129 25
A insertSelect() 0 22 2
A update() 3 21 4
C makeList() 0 28 7
A textFieldSize() 0 13 2
C limitResult() 0 44 8
A LimitToTopN() 0 15 4
A getSoftwareLink() 0 3 1
A getServerVersion() 0 9 2
B tableExists() 0 24 4
A fieldExists() 0 18 3
A fieldInfo() 0 19 3
A doBegin() 0 4 1
A doCommit() 0 4 1
A doRollback() 0 4 1
B escapeIdentifier() 7 17 5
A strencode() 0 5 1
B addQuotes() 0 15 5
A addIdentifierQuotes() 0 4 1
A isQuotedIdentifier() 0 3 3
A escapeLikeInternal() 0 3 1
A buildLike() 8 8 3
A selectDB() 0 9 2
B makeSelectOptions() 0 27 6
A getType() 0 3 1
A buildConcat() 0 3 1
A buildGroupConcatField() 0 14 1
A getSearchEngine() 0 3 1
A getBinaryColumns() 12 12 3
A getBitColumns() 12 12 3
A populateColumnCaches() 0 18 3
A tableName() 9 9 2
A realTableName() 0 12 3
A dropTable() 10 10 2
A prepareStatements() 0 3 1
A scrollableCursor() 0 3 1
A ignoreErrors() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DatabaseMssql often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DatabaseMssql, and based on these observations, apply Extract Interface, too.

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 Database {
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 cascadingDeletes() {
46
		return true;
47
	}
48
49
	public function cleanupTriggers() {
50
		return false;
51
	}
52
53
	public function strictIPs() {
54
		return false;
55
	}
56
57
	public function realTimestamps() {
58
		return false;
59
	}
60
61
	public function implicitGroupby() {
62
		return false;
63
	}
64
65
	public function implicitOrderby() {
66
		return false;
67
	}
68
69
	public function functionalIndexes() {
70
		return true;
71
	}
72
73
	public function unionSupportsOrderAndLimit() {
74
		return false;
75
	}
76
77
	/**
78
	 * Usually aborts on failure
79
	 * @param string $server
80
	 * @param string $user
81
	 * @param string $password
82
	 * @param string $dbName
83
	 * @throws DBConnectionError
84
	 * @return bool|DatabaseBase|null
85
	 */
86
	public function open( $server, $user, $password, $dbName ) {
87
		# Test for driver support, to avoid suppressed fatal error
88
		if ( !function_exists( 'sqlsrv_connect' ) ) {
89
			throw new DBConnectionError(
90
				$this,
91
				"Microsoft SQL Server Native (sqlsrv) functions missing.
92
				You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n"
93
			);
94
		}
95
96
		global $wgDBport, $wgDBWindowsAuthentication;
97
98
		# e.g. the class is being loaded
99
		if ( !strlen( $user ) ) {
100
			return null;
101
		}
102
103
		$this->close();
104
		$this->mServer = $server;
105
		$this->mPort = $wgDBport;
106
		$this->mUser = $user;
107
		$this->mPassword = $password;
108
		$this->mDBname = $dbName;
109
110
		$connectionInfo = [];
111
112
		if ( $dbName ) {
113
			$connectionInfo['Database'] = $dbName;
114
		}
115
116
		// Decide which auth scenerio to use
117
		// if we are using Windows auth, don't add credentials to $connectionInfo
118
		if ( !$wgDBWindowsAuthentication ) {
119
			$connectionInfo['UID'] = $user;
120
			$connectionInfo['PWD'] = $password;
121
		}
122
123
		MediaWiki\suppressWarnings();
124
		$this->mConn = sqlsrv_connect( $server, $connectionInfo );
125
		MediaWiki\restoreWarnings();
126
127
		if ( $this->mConn === false ) {
128
			throw new DBConnectionError( $this, $this->lastError() );
129
		}
130
131
		$this->mOpened = true;
132
133
		return $this->mConn;
134
	}
135
136
	/**
137
	 * Closes a database connection, if it is open
138
	 * Returns success, true if already closed
139
	 * @return bool
140
	 */
141
	protected function closeConnection() {
142
		return sqlsrv_close( $this->mConn );
143
	}
144
145
	/**
146
	 * @param bool|MssqlResultWrapper|resource $result
147
	 * @return bool|MssqlResultWrapper
148
	 */
149
	protected function resultObject( $result ) {
150
		if ( !$result ) {
151
			return false;
152
		} elseif ( $result instanceof MssqlResultWrapper ) {
153
			return $result;
154
		} elseif ( $result === true ) {
155
			// Successful write query
156
			return $result;
157
		} else {
158
			return new MssqlResultWrapper( $this, $result );
159
		}
160
	}
161
162
	/**
163
	 * @param string $sql
164
	 * @return bool|MssqlResult
165
	 * @throws DBUnexpectedError
166
	 */
167
	protected function doQuery( $sql ) {
168
		if ( $this->debug() ) {
169
			wfDebug( "SQL: [$sql]\n" );
170
		}
171
		$this->offset = 0;
0 ignored issues
show
Bug introduced by
The property offset does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
172
173
		// several extensions seem to think that all databases support limits
174
		// via LIMIT N after the WHERE clause well, MSSQL uses SELECT TOP N,
175
		// so to catch any of those extensions we'll do a quick check for a
176
		// LIMIT clause and pass $sql through $this->LimitToTopN() which parses
177
		// the limit clause and passes the result to $this->limitResult();
178
		if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
179
			// massage LIMIT -> TopN
180
			$sql = $this->LimitToTopN( $sql );
181
		}
182
183
		// MSSQL doesn't have EXTRACT(epoch FROM XXX)
184
		if ( preg_match( '#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
185
			// This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
186
			$sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
187
		}
188
189
		// perform query
190
191
		// SQLSRV_CURSOR_STATIC is slower than SQLSRV_CURSOR_CLIENT_BUFFERED (one of the two is
192
		// needed if we want to be able to seek around the result set), however CLIENT_BUFFERED
193
		// has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty
194
		// strings make php throw a fatal error "Severe error translating Unicode"
195
		if ( $this->mScrollableCursor ) {
196
			$scrollArr = [ 'Scrollable' => SQLSRV_CURSOR_STATIC ];
197
		} else {
198
			$scrollArr = [];
199
		}
200
201
		if ( $this->mPrepareStatements ) {
202
			// we do prepare + execute so we can get its field metadata for later usage if desired
203
			$stmt = sqlsrv_prepare( $this->mConn, $sql, [], $scrollArr );
204
			$success = sqlsrv_execute( $stmt );
205
		} else {
206
			$stmt = sqlsrv_query( $this->mConn, $sql, [], $scrollArr );
207
			$success = (bool)$stmt;
208
		}
209
210
		// make a copy so that anything we add below does not get reflected in future queries
211
		$ignoreErrors = $this->mIgnoreErrors;
212
213
		if ( $this->mIgnoreDupKeyErrors ) {
214
			// ignore duplicate key errors
215
			// this emulates INSERT IGNORE in MySQL
216
			$ignoreErrors[] = '2601'; // duplicate key error caused by unique index
217
			$ignoreErrors[] = '2627'; // duplicate key error caused by primary key
218
			$ignoreErrors[] = '3621'; // generic "the statement has been terminated" error
219
		}
220
221
		if ( $success === false ) {
222
			$errors = sqlsrv_errors();
223
			$success = true;
224
225
			foreach ( $errors as $err ) {
226
				if ( !in_array( $err['code'], $ignoreErrors ) ) {
227
					$success = false;
228
					break;
229
				}
230
			}
231
232
			if ( $success === false ) {
233
				return false;
234
			}
235
		}
236
		// remember number of rows affected
237
		$this->mAffectedRows = sqlsrv_rows_affected( $stmt );
238
239
		return $stmt;
240
	}
241
242
	public function freeResult( $res ) {
243
		if ( $res instanceof ResultWrapper ) {
244
			$res = $res->result;
245
		}
246
247
		sqlsrv_free_stmt( $res );
248
	}
249
250
	/**
251
	 * @param MssqlResultWrapper $res
252
	 * @return stdClass
253
	 */
254
	public function fetchObject( $res ) {
255
		// $res is expected to be an instance of MssqlResultWrapper here
256
		return $res->fetchObject();
257
	}
258
259
	/**
260
	 * @param MssqlResultWrapper $res
261
	 * @return array
262
	 */
263
	public function fetchRow( $res ) {
264
		return $res->fetchRow();
265
	}
266
267
	/**
268
	 * @param mixed $res
269
	 * @return int
270
	 */
271
	public function numRows( $res ) {
272
		if ( $res instanceof ResultWrapper ) {
273
			$res = $res->result;
274
		}
275
276
		$ret = sqlsrv_num_rows( $res );
277
278
		if ( $ret === false ) {
279
			// we cannot get an amount of rows from this cursor type
280
			// has_rows returns bool true/false if the result has rows
281
			$ret = (int)sqlsrv_has_rows( $res );
282
		}
283
284
		return $ret;
285
	}
286
287
	/**
288
	 * @param mixed $res
289
	 * @return int
290
	 */
291
	public function numFields( $res ) {
292
		if ( $res instanceof ResultWrapper ) {
293
			$res = $res->result;
294
		}
295
296
		return sqlsrv_num_fields( $res );
297
	}
298
299
	/**
300
	 * @param mixed $res
301
	 * @param int $n
302
	 * @return int
303
	 */
304
	public function fieldName( $res, $n ) {
305
		if ( $res instanceof ResultWrapper ) {
306
			$res = $res->result;
307
		}
308
309
		return sqlsrv_field_metadata( $res )[$n]['Name'];
310
	}
311
312
	/**
313
	 * This must be called after nextSequenceVal
314
	 * @return int|null
315
	 */
316
	public function insertId() {
317
		return $this->mInsertId;
318
	}
319
320
	/**
321
	 * @param MssqlResultWrapper $res
322
	 * @param int $row
323
	 * @return bool
324
	 */
325
	public function dataSeek( $res, $row ) {
326
		return $res->seek( $row );
327
	}
328
329
	/**
330
	 * @return string
331
	 */
332
	public function lastError() {
333
		$strRet = '';
334
		$retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
335
		if ( $retErrors != null ) {
336
			foreach ( $retErrors as $arrError ) {
337
				$strRet .= $this->formatError( $arrError ) . "\n";
338
			}
339
		} else {
340
			$strRet = "No errors found";
341
		}
342
343
		return $strRet;
344
	}
345
346
	/**
347
	 * @param array $err
348
	 * @return string
349
	 */
350
	private function formatError( $err ) {
351
		return '[SQLSTATE ' . $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message'];
352
	}
353
354
	/**
355
	 * @return string
356
	 */
357
	public function lastErrno() {
358
		$err = sqlsrv_errors( SQLSRV_ERR_ALL );
359
		if ( $err !== null && isset( $err[0] ) ) {
360
			return $err[0]['code'];
361
		} else {
362
			return 0;
363
		}
364
	}
365
366
	/**
367
	 * @return int
368
	 */
369
	public function affectedRows() {
370
		return $this->mAffectedRows;
371
	}
372
373
	/**
374
	 * SELECT wrapper
375
	 *
376
	 * @param mixed $table Array or string, table name(s) (prefix auto-added)
377
	 * @param mixed $vars Array or string, field name(s) to be retrieved
378
	 * @param mixed $conds Array or string, condition(s) for WHERE
379
	 * @param string $fname Calling function name (use __METHOD__) for logs/profiling
380
	 * @param array $options Associative array of options (e.g.
381
	 *   array('GROUP BY' => 'page_title')), see Database::makeSelectOptions
382
	 *   code for list of supported stuff
383
	 * @param array $join_conds Associative array of table join conditions
384
	 *   (optional) (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
385
	 * @return mixed Database result resource (feed to Database::fetchObject
386
	 *   or whatever), or false on failure
387
	 * @throws DBQueryError
388
	 * @throws DBUnexpectedError
389
	 * @throws Exception
390
	 */
391
	public function select( $table, $vars, $conds = '', $fname = __METHOD__,
392
		$options = [], $join_conds = []
393
	) {
394
		$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
395
		if ( isset( $options['EXPLAIN'] ) ) {
396
			try {
397
				$this->mScrollableCursor = false;
398
				$this->mPrepareStatements = false;
399
				$this->query( "SET SHOWPLAN_ALL ON" );
400
				$ret = $this->query( $sql, $fname );
401
				$this->query( "SET SHOWPLAN_ALL OFF" );
402
			} catch ( DBQueryError $dqe ) {
403
				if ( isset( $options['FOR COUNT'] ) ) {
404
					// likely don't have privs for SHOWPLAN, so run a select count instead
405
					$this->query( "SET SHOWPLAN_ALL OFF" );
406
					unset( $options['EXPLAIN'] );
407
					$ret = $this->select(
408
						$table,
409
						'COUNT(*) AS EstimateRows',
410
						$conds,
411
						$fname,
412
						$options,
413
						$join_conds
414
					);
415
				} else {
416
					// someone actually wanted the query plan instead of an est row count
417
					// let them know of the error
418
					$this->mScrollableCursor = true;
419
					$this->mPrepareStatements = true;
420
					throw $dqe;
421
				}
422
			}
423
			$this->mScrollableCursor = true;
424
			$this->mPrepareStatements = true;
425
			return $ret;
426
		}
427
		return $this->query( $sql, $fname );
428
	}
429
430
	/**
431
	 * SELECT wrapper
432
	 *
433
	 * @param mixed $table Array or string, table name(s) (prefix auto-added)
434
	 * @param mixed $vars Array or string, field name(s) to be retrieved
435
	 * @param mixed $conds Array or string, condition(s) for WHERE
436
	 * @param string $fname Calling function name (use __METHOD__) for logs/profiling
437
	 * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
438
	 *   see Database::makeSelectOptions code for list of supported stuff
439
	 * @param array $join_conds Associative array of table join conditions (optional)
440
	 *    (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
441
	 * @return string The SQL text
442
	 */
443
	public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
444
		$options = [], $join_conds = []
445
	) {
446
		if ( isset( $options['EXPLAIN'] ) ) {
447
			unset( $options['EXPLAIN'] );
448
		}
449
450
		$sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
451
452
		// try to rewrite aggregations of bit columns (currently MAX and MIN)
453
		if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) {
454
			$bitColumns = [];
455
			if ( is_array( $table ) ) {
456
				foreach ( $table as $t ) {
457
					$bitColumns += $this->getBitColumns( $this->tableName( $t ) );
0 ignored issues
show
Bug introduced by
It seems like $this->tableName($t) targeting DatabaseMssql::tableName() can also be of type array; however, DatabaseMssql::getBitColumns() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
458
				}
459
			} else {
460
				$bitColumns = $this->getBitColumns( $this->tableName( $table ) );
0 ignored issues
show
Bug introduced by
It seems like $this->tableName($table) targeting DatabaseMssql::tableName() can also be of type array; however, DatabaseMssql::getBitColumns() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
461
			}
462
463
			foreach ( $bitColumns as $col => $info ) {
464
				$replace = [
465
					"MAX({$col})" => "MAX(CAST({$col} AS tinyint))",
466
					"MIN({$col})" => "MIN(CAST({$col} AS tinyint))",
467
				];
468
				$sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql );
469
			}
470
		}
471
472
		return $sql;
473
	}
474
475 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
476
		$fname = __METHOD__
477
	) {
478
		$this->mScrollableCursor = false;
479
		try {
480
			parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname );
481
		} catch ( Exception $e ) {
482
			$this->mScrollableCursor = true;
483
			throw $e;
484
		}
485
		$this->mScrollableCursor = true;
486
	}
487
488 View Code Duplication
	public function delete( $table, $conds, $fname = __METHOD__ ) {
489
		$this->mScrollableCursor = false;
490
		try {
491
			parent::delete( $table, $conds, $fname );
492
		} catch ( Exception $e ) {
493
			$this->mScrollableCursor = true;
494
			throw $e;
495
		}
496
		$this->mScrollableCursor = true;
497
	}
498
499
	/**
500
	 * Estimate rows in dataset
501
	 * Returns estimated count, based on SHOWPLAN_ALL output
502
	 * This is not necessarily an accurate estimate, so use sparingly
503
	 * Returns -1 if count cannot be found
504
	 * Takes same arguments as Database::select()
505
	 * @param string $table
506
	 * @param string $vars
507
	 * @param string $conds
508
	 * @param string $fname
509
	 * @param array $options
510
	 * @return int
511
	 */
512 View Code Duplication
	public function estimateRowCount( $table, $vars = '*', $conds = '',
513
		$fname = __METHOD__, $options = []
514
	) {
515
		// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
516
		$options['EXPLAIN'] = true;
517
		$options['FOR COUNT'] = true;
518
		$res = $this->select( $table, $vars, $conds, $fname, $options );
519
520
		$rows = -1;
521
		if ( $res ) {
522
			$row = $this->fetchRow( $res );
523
524
			if ( isset( $row['EstimateRows'] ) ) {
525
				$rows = (int)$row['EstimateRows'];
526
			}
527
		}
528
529
		return $rows;
530
	}
531
532
	/**
533
	 * Returns information about an index
534
	 * If errors are explicitly ignored, returns NULL on failure
535
	 * @param string $table
536
	 * @param string $index
537
	 * @param string $fname
538
	 * @return array|bool|null
539
	 */
540
	public function indexInfo( $table, $index, $fname = __METHOD__ ) {
541
		# This does not return the same info as MYSQL would, but that's OK
542
		# because MediaWiki never uses the returned value except to check for
543
		# the existance of indexes.
544
		$sql = "sp_helpindex '" . $this->tableName( $table ) . "'";
545
		$res = $this->query( $sql, $fname );
546
547
		if ( !$res ) {
548
			return null;
549
		}
550
551
		$result = [];
552
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
553
			if ( $row->index_name == $index ) {
554
				$row->Non_unique = !stristr( $row->index_description, "unique" );
555
				$cols = explode( ", ", $row->index_keys );
556
				foreach ( $cols as $col ) {
557
					$row->Column_name = trim( $col );
558
					$result[] = clone $row;
559
				}
560
			} elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
561
				$row->Non_unique = 0;
562
				$cols = explode( ", ", $row->index_keys );
563
				foreach ( $cols as $col ) {
564
					$row->Column_name = trim( $col );
565
					$result[] = clone $row;
566
				}
567
			}
568
		}
569
570
		return empty( $result ) ? false : $result;
571
	}
572
573
	/**
574
	 * INSERT wrapper, inserts an array into a table
575
	 *
576
	 * $arrToInsert may be a single associative array, or an array of these with numeric keys, for
577
	 * multi-row insert.
578
	 *
579
	 * Usually aborts on failure
580
	 * If errors are explicitly ignored, returns success
581
	 * @param string $table
582
	 * @param array $arrToInsert
583
	 * @param string $fname
584
	 * @param array $options
585
	 * @return bool
586
	 * @throws Exception
587
	 */
588
	public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = [] ) {
589
		# No rows to insert, easy just return now
590
		if ( !count( $arrToInsert ) ) {
591
			return true;
592
		}
593
594
		if ( !is_array( $options ) ) {
595
			$options = [ $options ];
596
		}
597
598
		$table = $this->tableName( $table );
599
600 View Code Duplication
		if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row
601
			$arrToInsert = [ 0 => $arrToInsert ]; // make everything multi row compatible
602
		}
603
604
		// We know the table we're inserting into, get its identity column
605
		$identity = null;
606
		// strip matching square brackets and the db/schema from table name
607
		$tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
608
		$tableRaw = array_pop( $tableRawArr );
609
		$res = $this->doQuery(
610
			"SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS " .
611
				"WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'"
612
		);
613
		if ( $res && sqlsrv_has_rows( $res ) ) {
614
			// There is an identity for this table.
615
			$identityArr = sqlsrv_fetch_array( $res, SQLSRV_FETCH_ASSOC );
616
			$identity = array_pop( $identityArr );
617
		}
618
		sqlsrv_free_stmt( $res );
619
620
		// Determine binary/varbinary fields so we can encode data as a hex string like 0xABCDEF
621
		$binaryColumns = $this->getBinaryColumns( $table );
0 ignored issues
show
Bug introduced by
It seems like $table defined by $this->tableName($table) on line 598 can also be of type array; however, DatabaseMssql::getBinaryColumns() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
622
623
		// INSERT IGNORE is not supported by SQL Server
624
		// remove IGNORE from options list and set ignore flag to true
625
		if ( in_array( 'IGNORE', $options ) ) {
626
			$options = array_diff( $options, [ 'IGNORE' ] );
627
			$this->mIgnoreDupKeyErrors = true;
628
		}
629
630
		foreach ( $arrToInsert as $a ) {
631
			// start out with empty identity column, this is so we can return
632
			// it as a result of the insert logic
633
			$sqlPre = '';
634
			$sqlPost = '';
635
			$identityClause = '';
636
637
			// if we have an identity column
638
			if ( $identity ) {
639
				// iterate through
640
				foreach ( $a as $k => $v ) {
641
					if ( $k == $identity ) {
642
						if ( !is_null( $v ) ) {
643
							// there is a value being passed to us,
644
							// we need to turn on and off inserted identity
645
							$sqlPre = "SET IDENTITY_INSERT $table ON;";
646
							$sqlPost = ";SET IDENTITY_INSERT $table OFF;";
647
						} else {
648
							// we can't insert NULL into an identity column,
649
							// so remove the column from the insert.
650
							unset( $a[$k] );
651
						}
652
					}
653
				}
654
655
				// we want to output an identity column as result
656
				$identityClause = "OUTPUT INSERTED.$identity ";
657
			}
658
659
			$keys = array_keys( $a );
660
661
			// Build the actual query
662
			$sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
663
				" INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
664
665
			$first = true;
666
			foreach ( $a as $key => $value ) {
667
				if ( isset( $binaryColumns[$key] ) ) {
668
					$value = new MssqlBlob( $value );
669
				}
670
				if ( $first ) {
671
					$first = false;
672
				} else {
673
					$sql .= ',';
674
				}
675
				if ( is_null( $value ) ) {
676
					$sql .= 'null';
677
				} elseif ( is_array( $value ) || is_object( $value ) ) {
678
					if ( is_object( $value ) && $value instanceof Blob ) {
679
						$sql .= $this->addQuotes( $value );
680
					} else {
681
						$sql .= $this->addQuotes( serialize( $value ) );
682
					}
683
				} else {
684
					$sql .= $this->addQuotes( $value );
685
				}
686
			}
687
			$sql .= ')' . $sqlPost;
688
689
			// Run the query
690
			$this->mScrollableCursor = false;
691
			try {
692
				$ret = $this->query( $sql );
693
			} catch ( Exception $e ) {
694
				$this->mScrollableCursor = true;
695
				$this->mIgnoreDupKeyErrors = false;
696
				throw $e;
697
			}
698
			$this->mScrollableCursor = true;
699
700
			if ( !is_null( $identity ) ) {
701
				// then we want to get the identity column value we were assigned and save it off
702
				$row = $ret->fetchObject();
703
				if ( is_object( $row ) ) {
704
					$this->mInsertId = $row->$identity;
705
706
					// it seems that mAffectedRows is -1 sometimes when OUTPUT INSERTED.identity is used
707
					// if we got an identity back, we know for sure a row was affected, so adjust that here
708
					if ( $this->mAffectedRows == -1 ) {
709
						$this->mAffectedRows = 1;
710
					}
711
				}
712
			}
713
		}
714
		$this->mIgnoreDupKeyErrors = false;
715
		return $ret;
0 ignored issues
show
Bug introduced by
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...
716
	}
717
718
	/**
719
	 * INSERT SELECT wrapper
720
	 * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
721
	 * Source items may be literals rather than field names, but strings should
722
	 * be quoted with Database::addQuotes().
723
	 * @param string $destTable
724
	 * @param array|string $srcTable May be an array of tables.
725
	 * @param array $varMap
726
	 * @param array $conds May be "*" to copy the whole table.
727
	 * @param string $fname
728
	 * @param array $insertOptions
729
	 * @param array $selectOptions
730
	 * @return null|ResultWrapper
731
	 * @throws Exception
732
	 */
733
	public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
734
		$insertOptions = [], $selectOptions = []
735
	) {
736
		$this->mScrollableCursor = false;
737
		try {
738
			$ret = parent::insertSelect(
739
				$destTable,
740
				$srcTable,
741
				$varMap,
742
				$conds,
743
				$fname,
744
				$insertOptions,
745
				$selectOptions
746
			);
747
		} catch ( Exception $e ) {
748
			$this->mScrollableCursor = true;
749
			throw $e;
750
		}
751
		$this->mScrollableCursor = true;
752
753
		return $ret;
754
	}
755
756
	/**
757
	 * UPDATE wrapper. Takes a condition array and a SET array.
758
	 *
759
	 * @param string $table Name of the table to UPDATE. This will be passed through
760
	 *                DatabaseBase::tableName().
761
	 *
762
	 * @param array $values An array of values to SET. For each array element,
763
	 *                the key gives the field name, and the value gives the data
764
	 *                to set that field to. The data will be quoted by
765
	 *                DatabaseBase::addQuotes().
766
	 *
767
	 * @param array $conds An array of conditions (WHERE). See
768
	 *                DatabaseBase::select() for the details of the format of
769
	 *                condition arrays. Use '*' to update all rows.
770
	 *
771
	 * @param string $fname The function name of the caller (from __METHOD__),
772
	 *                for logging and profiling.
773
	 *
774
	 * @param array $options An array of UPDATE options, can be:
775
	 *                   - IGNORE: Ignore unique key conflicts
776
	 *                   - LOW_PRIORITY: MySQL-specific, see MySQL manual.
777
	 * @return bool
778
	 * @throws DBUnexpectedError
779
	 * @throws Exception
780
	 * @throws MWException
781
	 */
782
	function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
783
		$table = $this->tableName( $table );
784
		$binaryColumns = $this->getBinaryColumns( $table );
0 ignored issues
show
Bug introduced by
It seems like $table defined by $this->tableName($table) on line 783 can also be of type array; however, DatabaseMssql::getBinaryColumns() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
785
786
		$opts = $this->makeUpdateOptions( $options );
787
		$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns );
788
789 View Code Duplication
		if ( $conds !== [] && $conds !== '*' ) {
790
			$sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns );
791
		}
792
793
		$this->mScrollableCursor = false;
794
		try {
795
			$ret = $this->query( $sql );
0 ignored issues
show
Unused Code introduced by
$ret 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...
796
		} catch ( Exception $e ) {
797
			$this->mScrollableCursor = true;
798
			throw $e;
799
		}
800
		$this->mScrollableCursor = true;
801
		return true;
802
	}
803
804
	/**
805
	 * Makes an encoded list of strings from an array
806
	 * @param array $a Containing the data
807
	 * @param int $mode Constant
808
	 *      - LIST_COMMA:          comma separated, no field names
809
	 *      - LIST_AND:            ANDed WHERE clause (without the WHERE). See
810
	 *        the documentation for $conds in DatabaseBase::select().
811
	 *      - LIST_OR:             ORed WHERE clause (without the WHERE)
812
	 *      - LIST_SET:            comma separated with field names, like a SET clause
813
	 *      - LIST_NAMES:          comma separated field names
814
	 * @param array $binaryColumns Contains a list of column names that are binary types
815
	 *      This is a custom parameter only present for MS SQL.
816
	 *
817
	 * @throws MWException|DBUnexpectedError
818
	 * @return string
819
	 */
820
	public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = [] ) {
821
		if ( !is_array( $a ) ) {
822
			throw new DBUnexpectedError( $this,
823
				'DatabaseBase::makeList called with incorrect parameters' );
824
		}
825
826
		if ( $mode != LIST_NAMES ) {
827
			// In MS SQL, values need to be specially encoded when they are
828
			// inserted into binary fields. Perform this necessary encoding
829
			// for the specified set of columns.
830
			foreach ( array_keys( $a ) as $field ) {
831
				if ( !isset( $binaryColumns[$field] ) ) {
832
					continue;
833
				}
834
835
				if ( is_array( $a[$field] ) ) {
836
					foreach ( $a[$field] as &$v ) {
837
						$v = new MssqlBlob( $v );
838
					}
839
					unset( $v );
840
				} else {
841
					$a[$field] = new MssqlBlob( $a[$field] );
842
				}
843
			}
844
		}
845
846
		return parent::makeList( $a, $mode );
847
	}
848
849
	/**
850
	 * @param string $table
851
	 * @param string $field
852
	 * @return int Returns the size of a text field, or -1 for "unlimited"
853
	 */
854
	public function textFieldSize( $table, $field ) {
855
		$table = $this->tableName( $table );
856
		$sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
857
			WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
858
		$res = $this->query( $sql );
859
		$row = $this->fetchRow( $res );
860
		$size = -1;
861
		if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) {
862
			$size = $row['CHARACTER_MAXIMUM_LENGTH'];
863
		}
864
865
		return $size;
866
	}
867
868
	/**
869
	 * Construct a LIMIT query with optional offset
870
	 * This is used for query pages
871
	 *
872
	 * @param string $sql SQL query we will append the limit too
873
	 * @param int $limit The SQL limit
874
	 * @param bool|int $offset The SQL offset (default false)
875
	 * @return array|string
876
	 * @throws DBUnexpectedError
877
	 */
878
	public function limitResult( $sql, $limit, $offset = false ) {
879
		if ( $offset === false || $offset == 0 ) {
880
			if ( strpos( $sql, "SELECT" ) === false ) {
881
				return "TOP {$limit} " . $sql;
882
			} else {
883
				return preg_replace( '/\bSELECT(\s+DISTINCT)?\b/Dsi',
884
					'SELECT$1 TOP ' . $limit, $sql, 1 );
885
			}
886
		} else {
887
			// This one is fun, we need to pull out the select list as well as any ORDER BY clause
888
			$select = $orderby = [];
889
			$s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select );
890
			$s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby );
891
			$overOrder = $postOrder = '';
0 ignored issues
show
Unused Code introduced by
$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...
892
			$first = $offset + 1;
893
			$last = $offset + $limit;
894
			$sub1 = 'sub_' . $this->mSubqueryId;
895
			$sub2 = 'sub_' . ( $this->mSubqueryId + 1 );
896
			$this->mSubqueryId += 2;
897
			if ( !$s1 ) {
898
				// wat
899
				throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" );
900
			}
901
			if ( !$s2 ) {
902
				// no ORDER BY
903
				$overOrder = 'ORDER BY (SELECT 1)';
904
			} else {
905
				if ( !isset( $orderby[2] ) || !$orderby[2] ) {
906
					// don't need to strip it out if we're using a FOR XML clause
907
					$sql = str_replace( $orderby[1], '', $sql );
908
				}
909
				$overOrder = $orderby[1];
910
				$postOrder = ' ' . $overOrder;
911
			}
912
			$sql = "SELECT {$select[1]}
913
					FROM (
914
						SELECT ROW_NUMBER() OVER({$overOrder}) AS rowNumber, *
915
						FROM ({$sql}) {$sub1}
916
					) {$sub2}
917
					WHERE rowNumber BETWEEN {$first} AND {$last}{$postOrder}";
918
919
			return $sql;
920
		}
921
	}
922
923
	/**
924
	 * If there is a limit clause, parse it, strip it, and pass the remaining
925
	 * SQL through limitResult() with the appropriate parameters. Not the
926
	 * prettiest solution, but better than building a whole new parser. This
927
	 * exists becase there are still too many extensions that don't use dynamic
928
	 * sql generation.
929
	 *
930
	 * @param string $sql
931
	 * @return array|mixed|string
932
	 */
933
	public function LimitToTopN( $sql ) {
934
		// Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
935
		$pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
936
		if ( preg_match( $pattern, $sql, $matches ) ) {
937
			$row_count = $matches[4];
938
			$offset = $matches[3] ?: $matches[6] ?: false;
939
940
			// strip the matching LIMIT clause out
941
			$sql = str_replace( $matches[0], '', $sql );
942
943
			return $this->limitResult( $sql, $row_count, $offset );
0 ignored issues
show
Bug introduced by
It seems like $offset defined by $matches[3] ?: $matches[6] ?: false on line 938 can also be of type string; however, DatabaseMssql::limitResult() does only seem to accept boolean|integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
944
		}
945
946
		return $sql;
947
	}
948
949
	/**
950
	 * @return string Wikitext of a link to the server software's web site
951
	 */
952
	public function getSoftwareLink() {
953
		return "[{{int:version-db-mssql-url}} MS SQL Server]";
954
	}
955
956
	/**
957
	 * @return string Version information from the database
958
	 */
959
	public function getServerVersion() {
960
		$server_info = sqlsrv_server_info( $this->mConn );
961
		$version = 'Error';
962
		if ( isset( $server_info['SQLServerVersion'] ) ) {
963
			$version = $server_info['SQLServerVersion'];
964
		}
965
966
		return $version;
967
	}
968
969
	/**
970
	 * @param string $table
971
	 * @param string $fname
972
	 * @return bool
973
	 */
974
	public function tableExists( $table, $fname = __METHOD__ ) {
975
		list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
976
977
		if ( $db !== false ) {
978
			// remote database
979
			wfDebug( "Attempting to call tableExists on a remote table" );
980
			return false;
981
		}
982
983
		if ( $schema === false ) {
984
			global $wgDBmwschema;
985
			$schema = $wgDBmwschema;
986
		}
987
988
		$res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES
989
			WHERE TABLE_TYPE = 'BASE TABLE'
990
			AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" );
991
992
		if ( $res->numRows() ) {
993
			return true;
994
		} else {
995
			return false;
996
		}
997
	}
998
999
	/**
1000
	 * Query whether a given column exists in the mediawiki schema
1001
	 * @param string $table
1002
	 * @param string $field
1003
	 * @param string $fname
1004
	 * @return bool
1005
	 */
1006
	public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1007
		list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
1008
1009
		if ( $db !== false ) {
1010
			// remote database
1011
			wfDebug( "Attempting to call fieldExists on a remote table" );
1012
			return false;
1013
		}
1014
1015
		$res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
1016
			WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
1017
1018
		if ( $res->numRows() ) {
1019
			return true;
1020
		} else {
1021
			return false;
1022
		}
1023
	}
1024
1025
	public function fieldInfo( $table, $field ) {
1026
		list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
1027
1028
		if ( $db !== false ) {
1029
			// remote database
1030
			wfDebug( "Attempting to call fieldInfo on a remote table" );
1031
			return false;
1032
		}
1033
1034
		$res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS
1035
			WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
1036
1037
		$meta = $res->fetchRow();
1038
		if ( $meta ) {
1039
			return new MssqlField( $meta );
1040
		}
1041
1042
		return false;
1043
	}
1044
1045
	/**
1046
	 * Begin a transaction, committing any previously open transaction
1047
	 * @param string $fname
1048
	 */
1049
	protected function doBegin( $fname = __METHOD__ ) {
1050
		sqlsrv_begin_transaction( $this->mConn );
1051
		$this->mTrxLevel = 1;
1052
	}
1053
1054
	/**
1055
	 * End a transaction
1056
	 * @param string $fname
1057
	 */
1058
	protected function doCommit( $fname = __METHOD__ ) {
1059
		sqlsrv_commit( $this->mConn );
1060
		$this->mTrxLevel = 0;
1061
	}
1062
1063
	/**
1064
	 * Rollback a transaction.
1065
	 * No-op on non-transactional databases.
1066
	 * @param string $fname
1067
	 */
1068
	protected function doRollback( $fname = __METHOD__ ) {
1069
		sqlsrv_rollback( $this->mConn );
1070
		$this->mTrxLevel = 0;
1071
	}
1072
1073
	/**
1074
	 * Escapes a identifier for use inm SQL.
1075
	 * Throws an exception if it is invalid.
1076
	 * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx
1077
	 * @param string $identifier
1078
	 * @throws MWException
1079
	 * @return string
1080
	 */
1081
	private function escapeIdentifier( $identifier ) {
1082
		if ( strlen( $identifier ) == 0 ) {
1083
			throw new MWException( "An identifier must not be empty" );
1084
		}
1085
		if ( strlen( $identifier ) > 128 ) {
1086
			throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
1087
		}
1088 View Code Duplication
		if ( ( strpos( $identifier, '[' ) !== false )
1089
			|| ( strpos( $identifier, ']' ) !== false )
1090
		) {
1091
			// It may be allowed if you quoted with double quotation marks, but
1092
			// that would break if QUOTED_IDENTIFIER is OFF
1093
			throw new MWException( "Square brackets are not allowed in '$identifier'" );
1094
		}
1095
1096
		return "[$identifier]";
1097
	}
1098
1099
	/**
1100
	 * @param string $s
1101
	 * @return string
1102
	 */
1103
	public function strencode( $s ) {
1104
		// Should not be called by us
1105
1106
		return str_replace( "'", "''", $s );
1107
	}
1108
1109
	/**
1110
	 * @param string|Blob $s
1111
	 * @return string
1112
	 */
1113
	public function addQuotes( $s ) {
1114
		if ( $s instanceof MssqlBlob ) {
1115
			return $s->fetch();
1116
		} elseif ( $s instanceof Blob ) {
1117
			// this shouldn't really ever be called, but it's here if needed
1118
			// (and will quite possibly make the SQL error out)
1119
			$blob = new MssqlBlob( $s->fetch() );
1120
			return $blob->fetch();
1121
		} else {
1122
			if ( is_bool( $s ) ) {
1123
				$s = $s ? 1 : 0;
1124
			}
1125
			return parent::addQuotes( $s );
1126
		}
1127
	}
1128
1129
	/**
1130
	 * @param string $s
1131
	 * @return string
1132
	 */
1133
	public function addIdentifierQuotes( $s ) {
1134
		// http://msdn.microsoft.com/en-us/library/aa223962.aspx
1135
		return '[' . $s . ']';
1136
	}
1137
1138
	/**
1139
	 * @param string $name
1140
	 * @return bool
1141
	 */
1142
	public function isQuotedIdentifier( $name ) {
1143
		return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']';
1144
	}
1145
1146
	/**
1147
	 * MS SQL supports more pattern operators than other databases (ex: [,],^)
1148
	 *
1149
	 * @param string $s
1150
	 * @return string
1151
	 */
1152
	protected function escapeLikeInternal( $s ) {
1153
		return addcslashes( $s, '\%_[]^' );
1154
	}
1155
1156
	/**
1157
	 * MS SQL requires specifying the escape character used in a LIKE query
1158
	 * or using Square brackets to surround characters that are to be escaped
1159
	 * http://msdn.microsoft.com/en-us/library/ms179859.aspx
1160
	 * Here we take the Specify-Escape-Character approach since it's less
1161
	 * invasive, renders a query that is closer to other DB's and better at
1162
	 * handling square bracket escaping
1163
	 *
1164
	 * @return string Fully built LIKE statement
1165
	 */
1166 View Code Duplication
	public function buildLike() {
1167
		$params = func_get_args();
1168
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
1169
			$params = $params[0];
1170
		}
1171
1172
		return parent::buildLike( $params ) . " ESCAPE '\' ";
1173
	}
1174
1175
	/**
1176
	 * @param string $db
1177
	 * @return bool
1178
	 */
1179
	public function selectDB( $db ) {
1180
		try {
1181
			$this->mDBname = $db;
1182
			$this->query( "USE $db" );
1183
			return true;
1184
		} catch ( Exception $e ) {
1185
			return false;
1186
		}
1187
	}
1188
1189
	/**
1190
	 * @param array $options An associative array of options to be turned into
1191
	 *   an SQL query, valid keys are listed in the function.
1192
	 * @return array
1193
	 */
1194
	public function makeSelectOptions( $options ) {
1195
		$tailOpts = '';
1196
		$startOpts = '';
1197
1198
		$noKeyOptions = [];
1199
		foreach ( $options as $key => $option ) {
1200
			if ( is_numeric( $key ) ) {
1201
				$noKeyOptions[$option] = true;
1202
			}
1203
		}
1204
1205
		$tailOpts .= $this->makeGroupByWithHaving( $options );
1206
1207
		$tailOpts .= $this->makeOrderBy( $options );
1208
1209
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1210
			$startOpts .= 'DISTINCT';
1211
		}
1212
1213
		if ( isset( $noKeyOptions['FOR XML'] ) ) {
1214
			// used in group concat field emulation
1215
			$tailOpts .= " FOR XML PATH('')";
1216
		}
1217
1218
		// we want this to be compatible with the output of parent::makeSelectOptions()
1219
		return [ $startOpts, '', $tailOpts, '' ];
1220
	}
1221
1222
	/**
1223
	 * Get the type of the DBMS, as it appears in $wgDBtype.
1224
	 * @return string
1225
	 */
1226
	public function getType() {
1227
		return 'mssql';
1228
	}
1229
1230
	/**
1231
	 * @param array $stringList
1232
	 * @return string
1233
	 */
1234
	public function buildConcat( $stringList ) {
1235
		return implode( ' + ', $stringList );
1236
	}
1237
1238
	/**
1239
	 * Build a GROUP_CONCAT or equivalent statement for a query.
1240
	 * MS SQL doesn't have GROUP_CONCAT so we emulate it with other stuff (and boy is it nasty)
1241
	 *
1242
	 * This is useful for combining a field for several rows into a single string.
1243
	 * NULL values will not appear in the output, duplicated values will appear,
1244
	 * and the resulting delimiter-separated values have no defined sort order.
1245
	 * Code using the results may need to use the PHP unique() or sort() methods.
1246
	 *
1247
	 * @param string $delim Glue to bind the results together
1248
	 * @param string|array $table Table name
1249
	 * @param string $field Field name
1250
	 * @param string|array $conds Conditions
1251
	 * @param string|array $join_conds Join conditions
1252
	 * @return string SQL text
1253
	 * @since 1.23
1254
	 */
1255
	public function buildGroupConcatField( $delim, $table, $field, $conds = '',
1256
		$join_conds = []
1257
	) {
1258
		$gcsq = 'gcsq_' . $this->mSubqueryId;
1259
		$this->mSubqueryId++;
1260
1261
		$delimLen = strlen( $delim );
1262
		$fld = "{$field} + {$this->addQuotes( $delim )}";
1263
		$sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM ("
1264
			. $this->selectSQLText( $table, $fld, $conds, null, [ 'FOR XML' ], $join_conds )
0 ignored issues
show
Bug introduced by
It seems like $join_conds defined by parameter $join_conds on line 1256 can also be of type string; however, DatabaseMssql::selectSQLText() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1265
			. ") {$gcsq} ({$field}))";
1266
1267
		return $sql;
1268
	}
1269
1270
	/**
1271
	 * @return string
1272
	 */
1273
	public function getSearchEngine() {
1274
		return "SearchMssql";
1275
	}
1276
1277
	/**
1278
	 * Returns an associative array for fields that are of type varbinary, binary, or image
1279
	 * $table can be either a raw table name or passed through tableName() first
1280
	 * @param string $table
1281
	 * @return array
1282
	 */
1283 View Code Duplication
	private function getBinaryColumns( $table ) {
1284
		$tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
1285
		$tableRaw = array_pop( $tableRawArr );
1286
1287
		if ( $this->mBinaryColumnCache === null ) {
1288
			$this->populateColumnCaches();
1289
		}
1290
1291
		return isset( $this->mBinaryColumnCache[$tableRaw] )
1292
			? $this->mBinaryColumnCache[$tableRaw]
1293
			: [];
1294
	}
1295
1296
	/**
1297
	 * @param string $table
1298
	 * @return array
1299
	 */
1300 View Code Duplication
	private function getBitColumns( $table ) {
1301
		$tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
1302
		$tableRaw = array_pop( $tableRawArr );
1303
1304
		if ( $this->mBitColumnCache === null ) {
1305
			$this->populateColumnCaches();
1306
		}
1307
1308
		return isset( $this->mBitColumnCache[$tableRaw] )
1309
			? $this->mBitColumnCache[$tableRaw]
1310
			: [];
1311
	}
1312
1313
	private function populateColumnCaches() {
1314
		$res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*',
1315
			[
1316
				'TABLE_CATALOG' => $this->mDBname,
1317
				'TABLE_SCHEMA' => $this->mSchema,
1318
				'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ]
1319
			] );
1320
1321
		$this->mBinaryColumnCache = [];
1322
		$this->mBitColumnCache = [];
1323
		foreach ( $res as $row ) {
1324
			if ( $row->DATA_TYPE == 'bit' ) {
1325
				$this->mBitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
1326
			} else {
1327
				$this->mBinaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
1328
			}
1329
		}
1330
	}
1331
1332
	/**
1333
	 * @param string $name
1334
	 * @param string $format
1335
	 * @return string
1336
	 */
1337 View Code Duplication
	function tableName( $name, $format = 'quoted' ) {
1338
		# Replace reserved words with better ones
1339
		switch ( $name ) {
1340
			case 'user':
1341
				return $this->realTableName( 'mwuser', $format );
1342
			default:
1343
				return $this->realTableName( $name, $format );
1344
		}
1345
	}
1346
1347
	/**
1348
	 * call this instead of tableName() in the updater when renaming tables
1349
	 * @param string $name
1350
	 * @param string $format One of quoted, raw, or split
1351
	 * @return string
1352
	 */
1353
	function realTableName( $name, $format = 'quoted' ) {
1354
		$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...
1355
		if ( $format == 'split' ) {
1356
			// Used internally, we want the schema split off from the table name and returned
1357
			// as a list with 3 elements (database, schema, table)
1358
			$table = explode( '.', $table );
1359
			while ( count( $table ) < 3 ) {
1360
				array_unshift( $table, false );
1361
			}
1362
		}
1363
		return $table;
1364
	}
1365
1366
	/**
1367
	 * Delete a table
1368
	 * @param string $tableName
1369
	 * @param string $fName
1370
	 * @return bool|ResultWrapper
1371
	 * @since 1.18
1372
	 */
1373 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
1374
		if ( !$this->tableExists( $tableName, $fName ) ) {
1375
			return false;
1376
		}
1377
1378
		// parent function incorrectly appends CASCADE, which we don't want
1379
		$sql = "DROP TABLE " . $this->tableName( $tableName );
1380
1381
		return $this->query( $sql, $fName );
1382
	}
1383
1384
	/**
1385
	 * Called in the installer and updater.
1386
	 * Probably doesn't need to be called anywhere else in the codebase.
1387
	 * @param bool|null $value
1388
	 * @return bool|null
1389
	 */
1390
	public function prepareStatements( $value = null ) {
1391
		return wfSetVar( $this->mPrepareStatements, $value );
1392
	}
1393
1394
	/**
1395
	 * Called in the installer and updater.
1396
	 * Probably doesn't need to be called anywhere else in the codebase.
1397
	 * @param bool|null $value
1398
	 * @return bool|null
1399
	 */
1400
	public function scrollableCursor( $value = null ) {
1401
		return wfSetVar( $this->mScrollableCursor, $value );
1402
	}
1403
1404
	/**
1405
	 * Called in the installer and updater.
1406
	 * Probably doesn't need to be called anywhere else in the codebase.
1407
	 * @param array|null $value
1408
	 * @return array|null
1409
	 */
1410
	public function ignoreErrors( array $value = null ) {
1411
		return wfSetVar( $this->mIgnoreErrors, $value );
1412
	}
1413
} // end DatabaseMssql class
1414
1415
/**
1416
 * Utility class.
1417
 *
1418
 * @ingroup Database
1419
 */
1420
class MssqlField implements Field {
1421
	private $name, $tableName, $default, $max_length, $nullable, $type;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
1422
1423
	function __construct( $info ) {
1424
		$this->name = $info['COLUMN_NAME'];
1425
		$this->tableName = $info['TABLE_NAME'];
1426
		$this->default = $info['COLUMN_DEFAULT'];
1427
		$this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
1428
		$this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' );
1429
		$this->type = $info['DATA_TYPE'];
1430
	}
1431
1432
	function name() {
1433
		return $this->name;
1434
	}
1435
1436
	function tableName() {
1437
		return $this->tableName;
1438
	}
1439
1440
	function defaultValue() {
1441
		return $this->default;
1442
	}
1443
1444
	function maxLength() {
1445
		return $this->max_length;
1446
	}
1447
1448
	function isNullable() {
1449
		return $this->nullable;
1450
	}
1451
1452
	function type() {
1453
		return $this->type;
1454
	}
1455
}
1456
1457
class MssqlBlob extends Blob {
1458
	public function __construct( $data ) {
1459
		if ( $data instanceof MssqlBlob ) {
1460
			return $data;
1461
		} elseif ( $data instanceof Blob ) {
1462
			$this->mData = $data->fetch();
1463
		} elseif ( is_array( $data ) && is_object( $data ) ) {
1464
			$this->mData = serialize( $data );
1465
		} else {
1466
			$this->mData = $data;
1467
		}
1468
	}
1469
1470
	/**
1471
	 * Returns an unquoted hex representation of a binary string
1472
	 * for insertion into varbinary-type fields
1473
	 * @return string
1474
	 */
1475
	public function fetch() {
1476
		if ( $this->mData === null ) {
1477
			return 'null';
1478
		}
1479
1480
		$ret = '0x';
1481
		$dataLength = strlen( $this->mData );
1482
		for ( $i = 0; $i < $dataLength; $i++ ) {
1483
			$ret .= bin2hex( pack( 'C', ord( $this->mData[$i] ) ) );
1484
		}
1485
1486
		return $ret;
1487
	}
1488
}
1489
1490
class MssqlResultWrapper extends ResultWrapper {
1491
	private $mSeekTo = null;
1492
1493
	/**
1494
	 * @return stdClass|bool
1495
	 */
1496 View Code Duplication
	public function fetchObject() {
1497
		$res = $this->result;
1498
1499
		if ( $this->mSeekTo !== null ) {
1500
			$result = sqlsrv_fetch_object( $res, 'stdClass', [],
1501
				SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo );
1502
			$this->mSeekTo = null;
1503
		} else {
1504
			$result = sqlsrv_fetch_object( $res );
1505
		}
1506
1507
		// MediaWiki expects us to return boolean false when there are no more rows instead of null
1508
		if ( $result === null ) {
1509
			return false;
1510
		}
1511
1512
		return $result;
1513
	}
1514
1515
	/**
1516
	 * @return array|bool
1517
	 */
1518 View Code Duplication
	public function fetchRow() {
1519
		$res = $this->result;
1520
1521
		if ( $this->mSeekTo !== null ) {
1522
			$result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH,
1523
				SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo );
1524
			$this->mSeekTo = null;
1525
		} else {
1526
			$result = sqlsrv_fetch_array( $res );
1527
		}
1528
1529
		// MediaWiki expects us to return boolean false when there are no more rows instead of null
1530
		if ( $result === null ) {
1531
			return false;
1532
		}
1533
1534
		return $result;
1535
	}
1536
1537
	/**
1538
	 * @param int $row
1539
	 * @return bool
1540
	 */
1541
	public function seek( $row ) {
1542
		$res = $this->result;
1543
1544
		// check bounds
1545
		$numRows = $this->db->numRows( $res );
1546
		$row = intval( $row );
1547
1548
		if ( $numRows === 0 ) {
1549
			return false;
1550
		} elseif ( $row < 0 || $row > $numRows - 1 ) {
1551
			return false;
1552
		}
1553
1554
		// Unlike MySQL, the seek actually happens on the next access
1555
		$this->mSeekTo = $row;
1556
		return true;
1557
	}
1558
}
1559