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