Completed
Branch master (a465d1)
by
unknown
26:13
created

DatabaseMssql   D

Complexity

Total Complexity 197

Size/Duplication

Total Lines 1343
Duplicated Lines 7.07 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 197
lcom 1
cbo 10
dl 95
loc 1343
rs 4
c 1
b 1
f 0

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