Correct check whether the e-mail field was filled in Special:Userlogin/signup
[lhc/web/wiklou.git] / includes / db / DatabaseMssql.php
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 */
26
27 /**
28 * @ingroup Database
29 */
30 class DatabaseMssql extends DatabaseBase {
31 var $mInsertId = NULL;
32 var $mLastResult = NULL;
33 var $mAffectedRows = NULL;
34
35 var $mPort;
36
37 function cascadingDeletes() {
38 return true;
39 }
40 function cleanupTriggers() {
41 return true;
42 }
43 function strictIPs() {
44 return true;
45 }
46 function realTimestamps() {
47 return true;
48 }
49 function implicitGroupby() {
50 return false;
51 }
52 function implicitOrderby() {
53 return false;
54 }
55 function functionalIndexes() {
56 return true;
57 }
58 function unionSupportsOrderAndLimit() {
59 return false;
60 }
61
62 /**
63 * Usually aborts on failure
64 * @param String $server
65 * @param String $user
66 * @param String $password
67 * @param String $dbName
68 * @throws DBConnectionError
69 * @return bool|DatabaseBase|null
70 */
71 function open( $server, $user, $password, $dbName ) {
72 # Test for driver support, to avoid suppressed fatal error
73 if ( !function_exists( 'sqlsrv_connect' ) ) {
74 throw new DBConnectionError( $this, "MS Sql Server Native (sqlsrv) functions missing. You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n" );
75 }
76
77 global $wgDBport;
78
79 if ( !strlen( $user ) ) { # e.g. the class is being loaded
80 return;
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 = array();
91
92 if( $dbName ) {
93 $connectionInfo['Database'] = $dbName;
94 }
95
96 // Start NT Auth Hack
97 // Quick and dirty work around to provide NT Auth designation support.
98 // Current solution requires installer to know to input 'ntauth' for both username and password
99 // to trigger connection via NT Auth. - ugly, ugly, ugly
100 // TO-DO: Make this better and add NT Auth choice to MW installer when SQL Server option is chosen.
101 $ntAuthUserTest = strtolower( $user );
102 $ntAuthPassTest = strtolower( $password );
103
104 // Decide which auth scenerio to use
105 if( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) {
106 // Don't add credentials to $connectionInfo
107 } else {
108 $connectionInfo['UID'] = $user;
109 $connectionInfo['PWD'] = $password;
110 }
111 // End NT Auth Hack
112
113 wfSuppressWarnings();
114 $this->mConn = sqlsrv_connect( $server, $connectionInfo );
115 wfRestoreWarnings();
116
117 if ( $this->mConn === false ) {
118 wfDebug( "DB connection error\n" );
119 wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
120 wfDebug( $this->lastError() . "\n" );
121 return false;
122 }
123
124 $this->mOpened = true;
125 return $this->mConn;
126 }
127
128 /**
129 * Closes a database connection, if it is open
130 * Returns success, true if already closed
131 * @return bool
132 */
133 protected function closeConnection() {
134 return sqlsrv_close( $this->mConn );
135 }
136
137 protected function doQuery( $sql ) {
138 wfDebug( "SQL: [$sql]\n" );
139 $this->offset = 0;
140
141 // several extensions seem to think that all databases support limits via LIMIT N after the WHERE clause
142 // well, MSSQL uses SELECT TOP N, so to catch any of those extensions we'll do a quick check for a LIMIT
143 // clause and pass $sql through $this->LimitToTopN() which parses the limit clause and passes the result to
144 // $this->limitResult();
145 if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
146 // massage LIMIT -> TopN
147 $sql = $this->LimitToTopN( $sql ) ;
148 }
149
150 // MSSQL doesn't have EXTRACT(epoch FROM XXX)
151 if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
152 // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
153 $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
154 }
155
156 // perform query
157 $stmt = sqlsrv_query( $this->mConn, $sql );
158 if ( $stmt == false ) {
159 $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
160 "Query: " . htmlentities( $sql ) . "\n" .
161 "Function: " . __METHOD__ . "\n";
162 // process each error (our driver will give us an array of errors unlike other providers)
163 foreach ( sqlsrv_errors() as $error ) {
164 $message .= $message . "ERROR[" . $error['code'] . "] " . $error['message'] . "\n";
165 }
166
167 throw new DBUnexpectedError( $this, $message );
168 }
169 // remember number of rows affected
170 $this->mAffectedRows = sqlsrv_rows_affected( $stmt );
171
172 // if it is a SELECT statement, or an insert with a request to output something we want to return a row.
173 if ( ( preg_match( '#\bSELECT\s#i', $sql ) ) ||
174 ( preg_match( '#\bINSERT\s#i', $sql ) && preg_match( '#\bOUTPUT\s+INSERTED\b#i', $sql ) ) ) {
175 // this is essentially a rowset, but Mediawiki calls these 'result'
176 // the rowset owns freeing the statement
177 $res = new MssqlResult( $stmt );
178 } else {
179 // otherwise we simply return it was successful, failure throws an exception
180 $res = true;
181 }
182 return $res;
183 }
184
185 function freeResult( $res ) {
186 if ( $res instanceof ResultWrapper ) {
187 $res = $res->result;
188 }
189 $res->free();
190 }
191
192 function fetchObject( $res ) {
193 if ( $res instanceof ResultWrapper ) {
194 $res = $res->result;
195 }
196 $row = $res->fetch( 'OBJECT' );
197 return $row;
198 }
199
200 function getErrors() {
201 $strRet = '';
202 $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
203 if ( $retErrors != null ) {
204 foreach ( $retErrors as $arrError ) {
205 $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n";
206 $strRet .= "Error Code: " . $arrError[ 'code'] . "\n";
207 $strRet .= "Message: " . $arrError[ 'message'] . "\n";
208 }
209 } else {
210 $strRet = "No errors found";
211 }
212 return $strRet;
213 }
214
215 function fetchRow( $res ) {
216 if ( $res instanceof ResultWrapper ) {
217 $res = $res->result;
218 }
219 $row = $res->fetch( SQLSRV_FETCH_BOTH );
220 return $row;
221 }
222
223 function numRows( $res ) {
224 if ( $res instanceof ResultWrapper ) {
225 $res = $res->result;
226 }
227 return ( $res ) ? $res->numrows() : 0;
228 }
229
230 function numFields( $res ) {
231 if ( $res instanceof ResultWrapper ) {
232 $res = $res->result;
233 }
234 return ( $res ) ? $res->numfields() : 0;
235 }
236
237 function fieldName( $res, $n ) {
238 if ( $res instanceof ResultWrapper ) {
239 $res = $res->result;
240 }
241 return ( $res ) ? $res->fieldname( $n ) : 0;
242 }
243
244 /**
245 * This must be called after nextSequenceVal
246 * @return null
247 */
248 function insertId() {
249 return $this->mInsertId;
250 }
251
252 function dataSeek( $res, $row ) {
253 if ( $res instanceof ResultWrapper ) {
254 $res = $res->result;
255 }
256 return ( $res ) ? $res->seek( $row ) : false;
257 }
258
259 function lastError() {
260 if ( $this->mConn ) {
261 return $this->getErrors();
262 } else {
263 return "No database connection";
264 }
265 }
266
267 function lastErrno() {
268 $err = sqlsrv_errors( SQLSRV_ERR_ALL );
269 if ( $err[0] ) {
270 return $err[0]['code'];
271 } else {
272 return 0;
273 }
274 }
275
276 function affectedRows() {
277 return $this->mAffectedRows;
278 }
279
280 /**
281 * SELECT wrapper
282 *
283 * @param $table Mixed: array or string, table name(s) (prefix auto-added)
284 * @param $vars Mixed: array or string, field name(s) to be retrieved
285 * @param $conds Mixed: array or string, condition(s) for WHERE
286 * @param $fname String: calling function name (use __METHOD__) for logs/profiling
287 * @param $options Array: associative array of options (e.g. array('GROUP BY' => 'page_title')),
288 * see Database::makeSelectOptions code for list of supported stuff
289 * @param $join_conds Array: Associative array of table join conditions (optional)
290 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
291 * @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure
292 */
293 function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() )
294 {
295 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
296 if ( isset( $options['EXPLAIN'] ) ) {
297 sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL ON;" );
298 $ret = $this->query( $sql, $fname );
299 sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL OFF;" );
300 return $ret;
301 }
302 return $this->query( $sql, $fname );
303 }
304
305 /**
306 * SELECT wrapper
307 *
308 * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
309 * @param $vars Mixed: Array or string, field name(s) to be retrieved
310 * @param $conds Mixed: Array or string, condition(s) for WHERE
311 * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
312 * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
313 * see Database::makeSelectOptions code for list of supported stuff
314 * @param $join_conds Array: Associative array of table join conditions (optional)
315 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
316 * @return string, the SQL text
317 */
318 function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) {
319 if ( isset( $options['EXPLAIN'] ) ) {
320 unset( $options['EXPLAIN'] );
321 }
322 return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
323 }
324
325 /**
326 * Estimate rows in dataset
327 * Returns estimated count, based on SHOWPLAN_ALL output
328 * This is not necessarily an accurate estimate, so use sparingly
329 * Returns -1 if count cannot be found
330 * Takes same arguments as Database::select()
331 * @return int
332 */
333 function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) {
334 $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
335 $res = $this->select( $table, $vars, $conds, $fname, $options );
336
337 $rows = -1;
338 if ( $res ) {
339 $row = $this->fetchRow( $res );
340 if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows'];
341 }
342 return $rows;
343 }
344
345 /**
346 * Returns information about an index
347 * If errors are explicitly ignored, returns NULL on failure
348 * @return array|bool|null
349 */
350 function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) {
351 # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
352 # returned value except to check for the existance of indexes.
353 $sql = "sp_helpindex '" . $table . "'";
354 $res = $this->query( $sql, $fname );
355 if ( !$res ) {
356 return NULL;
357 }
358
359 $result = array();
360 foreach ( $res as $row ) {
361 if ( $row->index_name == $index ) {
362 $row->Non_unique = !stristr( $row->index_description, "unique" );
363 $cols = explode( ", ", $row->index_keys );
364 foreach ( $cols as $col ) {
365 $row->Column_name = trim( $col );
366 $result[] = clone $row;
367 }
368 } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
369 $row->Non_unique = 0;
370 $cols = explode( ", ", $row->index_keys );
371 foreach ( $cols as $col ) {
372 $row->Column_name = trim( $col );
373 $result[] = clone $row;
374 }
375 }
376 }
377 return empty( $result ) ? false : $result;
378 }
379
380 /**
381 * INSERT wrapper, inserts an array into a table
382 *
383 * $arrToInsert may be a single associative array, or an array of these with numeric keys, for
384 * multi-row insert.
385 *
386 * Usually aborts on failure
387 * If errors are explicitly ignored, returns success
388 * @param String $table
389 * @param Array $arrToInsert
390 * @param string $fname
391 * @param array $options
392 * @throws DBQueryError
393 * @return bool
394 */
395 function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) {
396 # No rows to insert, easy just return now
397 if ( !count( $arrToInsert ) ) {
398 return true;
399 }
400
401 if ( !is_array( $options ) ) {
402 $options = array( $options );
403 }
404
405 $table = $this->tableName( $table );
406
407 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row
408 $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible
409 }
410
411 $allOk = true;
412
413 // We know the table we're inserting into, get its identity column
414 $identity = null;
415 $tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name
416 $res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" );
417 if( $res && $res->numrows() ) {
418 // There is an identity for this table.
419 $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) );
420 }
421 unset( $res );
422
423 foreach ( $arrToInsert as $a ) {
424 // start out with empty identity column, this is so we can return it as a result of the insert logic
425 $sqlPre = '';
426 $sqlPost = '';
427 $identityClause = '';
428
429 // if we have an identity column
430 if( $identity ) {
431 // iterate through
432 foreach ($a as $k => $v ) {
433 if ( $k == $identity ) {
434 if( !is_null($v) ) {
435 // there is a value being passed to us, we need to turn on and off inserted identity
436 $sqlPre = "SET IDENTITY_INSERT $table ON;" ;
437 $sqlPost = ";SET IDENTITY_INSERT $table OFF;";
438
439 } else {
440 // we can't insert NULL into an identity column, so remove the column from the insert.
441 unset( $a[$k] );
442 }
443 }
444 }
445 $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result
446 }
447
448 $keys = array_keys( $a );
449
450 // INSERT IGNORE is not supported by SQL Server
451 // remove IGNORE from options list and set ignore flag to true
452 $ignoreClause = false;
453 foreach ( $options as $k => $v ) {
454 if ( strtoupper( $v ) == "IGNORE" ) {
455 unset( $options[$k] );
456 $ignoreClause = true;
457 }
458 }
459
460 // translate MySQL INSERT IGNORE to something SQL Server can use
461 // example:
462 // MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
463 // MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
464 if ( $ignoreClause ) {
465 $prival = $a[$keys[0]];
466 $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')";
467 }
468
469 // Build the actual query
470 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
471 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
472
473 $first = true;
474 foreach ( $a as $value ) {
475 if ( $first ) {
476 $first = false;
477 } else {
478 $sql .= ',';
479 }
480 if ( is_string( $value ) ) {
481 $sql .= $this->addQuotes( $value );
482 } elseif ( is_null( $value ) ) {
483 $sql .= 'null';
484 } elseif ( is_array( $value ) || is_object( $value ) ) {
485 if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) {
486 $sql .= $this->addQuotes( $value );
487 } else {
488 $sql .= $this->addQuotes( serialize( $value ) );
489 }
490 } else {
491 $sql .= $value;
492 }
493 }
494 $sql .= ')' . $sqlPost;
495
496 // Run the query
497 $ret = sqlsrv_query( $this->mConn, $sql );
498
499 if ( $ret === false ) {
500 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
501 } elseif ( $ret != NULL ) {
502 // remember number of rows affected
503 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
504 if ( !is_null($identity) ) {
505 // then we want to get the identity column value we were assigned and save it off
506 $row = sqlsrv_fetch_object( $ret );
507 $this->mInsertId = $row->$identity;
508 }
509 sqlsrv_free_stmt( $ret );
510 continue;
511 }
512 $allOk = false;
513 }
514 return $allOk;
515 }
516
517 /**
518 * INSERT SELECT wrapper
519 * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
520 * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
521 * $conds may be "*" to copy the whole table
522 * srcTable may be an array of tables.
523 * @param string $destTable
524 * @param array|string $srcTable
525 * @param array $varMap
526 * @param array $conds
527 * @param string $fname
528 * @param array $insertOptions
529 * @param array $selectOptions
530 * @throws DBQueryError
531 * @return null|ResultWrapper
532 */
533 function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
534 $insertOptions = array(), $selectOptions = array() ) {
535 $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions );
536
537 if ( $ret === false ) {
538 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), /*$sql*/ '', $fname );
539 } elseif ( $ret != NULL ) {
540 // remember number of rows affected
541 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
542 return $ret;
543 }
544 return NULL;
545 }
546
547 /**
548 * Return the next in a sequence, save the value for retrieval via insertId()
549 * @return
550 */
551 function nextSequenceValue( $seqName ) {
552 if ( !$this->tableExists( 'sequence_' . $seqName ) ) {
553 sqlsrv_query( $this->mConn, "CREATE TABLE [sequence_$seqName] (id INT NOT NULL IDENTITY PRIMARY KEY, junk varchar(10) NULL)" );
554 }
555 sqlsrv_query( $this->mConn, "INSERT INTO [sequence_$seqName] (junk) VALUES ('')" );
556 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
557 $row = sqlsrv_fetch_array( $ret, SQLSRV_FETCH_ASSOC );// KEEP ASSOC THERE, weird weird bug dealing with the return value if you don't
558
559 sqlsrv_free_stmt( $ret );
560 $this->mInsertId = $row['id'];
561 return $row['id'];
562 }
563
564 /**
565 * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
566 * @return
567 */
568 function currentSequenceValue( $seqName ) {
569 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
570 if ( $ret !== false ) {
571 $row = sqlsrv_fetch_array( $ret );
572 sqlsrv_free_stmt( $ret );
573 return $row['id'];
574 } else {
575 return $this->nextSequenceValue( $seqName );
576 }
577 }
578
579 # Returns the size of a text field, or -1 for "unlimited"
580 function textFieldSize( $table, $field ) {
581 $table = $this->tableName( $table );
582 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
583 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
584 $res = $this->query( $sql );
585 $row = $this->fetchRow( $res );
586 $size = -1;
587 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) {
588 $size = $row['CHARACTER_MAXIMUM_LENGTH'];
589 }
590 return $size;
591 }
592
593 /**
594 * Construct a LIMIT query with optional offset
595 * This is used for query pages
596 * $sql string SQL query we will append the limit too
597 * $limit integer the SQL limit
598 * $offset integer the SQL offset (default false)
599 * @return mixed|string
600 */
601 function limitResult( $sql, $limit, $offset = false ) {
602 if ( $offset === false || $offset == 0 ) {
603 if ( strpos( $sql, "SELECT" ) === false ) {
604 return "TOP {$limit} " . $sql;
605 } else {
606 return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 );
607 }
608 } else {
609 $sql = '
610 SELECT * FROM (
611 SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM (
612 SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1
613 ) as sub2
614 ) AS sub3
615 WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit );
616 return $sql;
617 }
618 }
619
620 // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult()
621 // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser.
622 // This exists becase there are still too many extensions that don't use dynamic sql generation.
623 function LimitToTopN( $sql ) {
624 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
625 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
626 if ( preg_match( $pattern, $sql, $matches ) ) {
627 // row_count = $matches[4]
628 $row_count = $matches[4];
629 // offset = $matches[3] OR $matches[6]
630 $offset = $matches[3] or
631 $offset = $matches[6] or
632 $offset = false;
633
634 // strip the matching LIMIT clause out
635 $sql = str_replace( $matches[0], '', $sql );
636 return $this->limitResult( $sql, $row_count, $offset );
637 }
638 return $sql;
639 }
640
641 function timestamp( $ts = 0 ) {
642 return wfTimestamp( TS_ISO_8601, $ts );
643 }
644
645 /**
646 * @return string wikitext of a link to the server software's web site
647 */
648 public static function getSoftwareLink() {
649 return "[http://www.microsoft.com/sql/ MS SQL Server]";
650 }
651
652 /**
653 * @return string Version information from the database
654 */
655 function getServerVersion() {
656 $server_info = sqlsrv_server_info( $this->mConn );
657 $version = 'Error';
658 if ( isset( $server_info['SQLServerVersion'] ) ) {
659 $version = $server_info['SQLServerVersion'];
660 }
661 return $version;
662 }
663
664 function tableExists ( $table, $fname = __METHOD__, $schema = false ) {
665 $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables
666 WHERE table_type='BASE TABLE' AND table_name = '$table'" );
667 if ( $res === false ) {
668 print( "Error in tableExists query: " . $this->getErrors() );
669 return false;
670 }
671 if ( sqlsrv_fetch( $res ) ) {
672 return true;
673 } else {
674 return false;
675 }
676 }
677
678 /**
679 * Query whether a given column exists in the mediawiki schema
680 * @return bool
681 */
682 function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) {
683 $table = $this->tableName( $table );
684 $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns
685 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
686 if ( $res === false ) {
687 print( "Error in fieldExists query: " . $this->getErrors() );
688 return false;
689 }
690 if ( sqlsrv_fetch( $res ) ) {
691 return true;
692 } else {
693 return false;
694 }
695 }
696
697 function fieldInfo( $table, $field ) {
698 $table = $this->tableName( $table );
699 $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns
700 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
701 if ( $res === false ) {
702 print( "Error in fieldInfo query: " . $this->getErrors() );
703 return false;
704 }
705 $meta = $this->fetchRow( $res );
706 if ( $meta ) {
707 return new MssqlField( $meta );
708 }
709 return false;
710 }
711
712 /**
713 * Begin a transaction, committing any previously open transaction
714 */
715 protected function doBegin( $fname = 'DatabaseMssql::begin' ) {
716 sqlsrv_begin_transaction( $this->mConn );
717 $this->mTrxLevel = 1;
718 }
719
720 /**
721 * End a transaction
722 */
723 protected function doCommit( $fname = 'DatabaseMssql::commit' ) {
724 sqlsrv_commit( $this->mConn );
725 $this->mTrxLevel = 0;
726 }
727
728 /**
729 * Rollback a transaction.
730 * No-op on non-transactional databases.
731 */
732 protected function doRollback( $fname = 'DatabaseMssql::rollback' ) {
733 sqlsrv_rollback( $this->mConn );
734 $this->mTrxLevel = 0;
735 }
736
737 /**
738 * Escapes a identifier for use inm SQL.
739 * Throws an exception if it is invalid.
740 * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx
741 * @param $identifier
742 * @throws MWException
743 * @return string
744 */
745 private function escapeIdentifier( $identifier ) {
746 if ( strlen( $identifier ) == 0 ) {
747 throw new MWException( "An identifier must not be empty" );
748 }
749 if ( strlen( $identifier ) > 128 ) {
750 throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
751 }
752 if ( ( strpos( $identifier, '[' ) !== false ) || ( strpos( $identifier, ']' ) !== false ) ) {
753 // It may be allowed if you quoted with double quotation marks, but that would break if QUOTED_IDENTIFIER is OFF
754 throw new MWException( "You can't use square brackers in the identifier '$identifier'" );
755 }
756 return "[$identifier]";
757 }
758
759 /**
760 * Initial setup.
761 * Precondition: This object is connected as the superuser.
762 * Creates the database, schema, user and login.
763 */
764 function initial_setup( $dbName, $newUser, $loginPassword ) {
765 $dbName = $this->escapeIdentifier( $dbName );
766
767 // It is not clear what can be used as a login,
768 // From http://msdn.microsoft.com/en-us/library/ms173463.aspx
769 // a sysname may be the same as an identifier.
770 $newUser = $this->escapeIdentifier( $newUser );
771 $loginPassword = $this->addQuotes( $loginPassword );
772
773 $this->doQuery( "CREATE DATABASE $dbName;" );
774 $this->doQuery( "USE $dbName;" );
775 $this->doQuery( "CREATE SCHEMA $dbName;" );
776 $this->doQuery( "
777 CREATE
778 LOGIN $newUser
779 WITH
780 PASSWORD=$loginPassword
781 ;
782 " );
783 $this->doQuery( "
784 CREATE
785 USER $newUser
786 FOR
787 LOGIN $newUser
788 WITH
789 DEFAULT_SCHEMA=$dbName
790 ;
791 " );
792 $this->doQuery( "
793 GRANT
794 BACKUP DATABASE,
795 BACKUP LOG,
796 CREATE DEFAULT,
797 CREATE FUNCTION,
798 CREATE PROCEDURE,
799 CREATE RULE,
800 CREATE TABLE,
801 CREATE VIEW,
802 CREATE FULLTEXT CATALOG
803 ON
804 DATABASE::$dbName
805 TO $newUser
806 ;
807 " );
808 $this->doQuery( "
809 GRANT
810 CONTROL
811 ON
812 SCHEMA::$dbName
813 TO $newUser
814 ;
815 " );
816
817
818 }
819
820 function encodeBlob( $b ) {
821 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
822 return base64_encode( $b );
823 }
824
825 function decodeBlob( $b ) {
826 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
827 return base64_decode( $b );
828 }
829
830 /**
831 * @private
832 * @return string
833 */
834 function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
835 $ret = array();
836 $retJOIN = array();
837 $use_index_safe = is_array( $use_index ) ? $use_index : array();
838 $join_conds_safe = is_array( $join_conds ) ? $join_conds : array();
839 foreach ( $tables as $table ) {
840 // Is there a JOIN and INDEX clause for this table?
841 if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) {
842 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
843 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
844 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
845 $retJOIN[] = $tableClause;
846 // Is there an INDEX clause?
847 } elseif ( isset( $use_index_safe[$table] ) ) {
848 $tableClause = $this->tableName( $table );
849 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
850 $ret[] = $tableClause;
851 // Is there a JOIN clause?
852 } elseif ( isset( $join_conds_safe[$table] ) ) {
853 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
854 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
855 $retJOIN[] = $tableClause;
856 } else {
857 $tableClause = $this->tableName( $table );
858 $ret[] = $tableClause;
859 }
860 }
861 // We can't separate explicit JOIN clauses with ',', use ' ' for those
862 $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
863 $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
864 // Compile our final table clause
865 return implode( ' ', array( $straightJoins, $otherJoins ) );
866 }
867
868 function strencode( $s ) { # Should not be called by us
869 return str_replace( "'", "''", $s );
870 }
871
872 function addQuotes( $s ) {
873 if ( $s instanceof Blob ) {
874 return "'" . $s->fetch( $s ) . "'";
875 } else {
876 return parent::addQuotes( $s );
877 }
878 }
879
880 public function addIdentifierQuotes( $s ) {
881 // http://msdn.microsoft.com/en-us/library/aa223962.aspx
882 return '[' . $s . ']';
883 }
884
885 public function isQuotedIdentifier( $name ) {
886 return $name[0] == '[' && substr( $name, -1, 1 ) == ']';
887 }
888
889 function selectDB( $db ) {
890 return ( $this->query( "SET DATABASE $db" ) !== false );
891 }
892
893 /**
894 * @private
895 *
896 * @param $options Array: an associative array of options to be turned into
897 * an SQL query, valid keys are listed in the function.
898 * @return Array
899 */
900 function makeSelectOptions( $options ) {
901 $tailOpts = '';
902 $startOpts = '';
903
904 $noKeyOptions = array();
905 foreach ( $options as $key => $option ) {
906 if ( is_numeric( $key ) ) {
907 $noKeyOptions[$option] = true;
908 }
909 }
910
911 $tailOpts .= $this->makeGroupByWithHaving( $options );
912
913 $tailOpts .= $this->makeOrderBy( $options );
914
915 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) {
916 $startOpts .= 'DISTINCT';
917 }
918
919 // we want this to be compatible with the output of parent::makeSelectOptions()
920 return array( $startOpts, '', $tailOpts, '' );
921 }
922
923 /**
924 * Get the type of the DBMS, as it appears in $wgDBtype.
925 * @return string
926 */
927 function getType() {
928 return 'mssql';
929 }
930
931 function buildConcat( $stringList ) {
932 return implode( ' + ', $stringList );
933 }
934
935 public function getSearchEngine() {
936 return "SearchMssql";
937 }
938
939 /**
940 * Since MSSQL doesn't recognize the infinity keyword, set date manually.
941 * @todo Remove magic date
942 * @return string
943 */
944 public function getInfinity() {
945 return '3000-01-31 00:00:00.000';
946 }
947
948 } // end DatabaseMssql class
949
950 /**
951 * Utility class.
952 *
953 * @ingroup Database
954 */
955 class MssqlField implements Field {
956 private $name, $tablename, $default, $max_length, $nullable, $type;
957 function __construct ( $info ) {
958 $this->name = $info['COLUMN_NAME'];
959 $this->tablename = $info['TABLE_NAME'];
960 $this->default = $info['COLUMN_DEFAULT'];
961 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
962 $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' );
963 $this->type = $info['DATA_TYPE'];
964 }
965
966 function name() {
967 return $this->name;
968 }
969
970 function tableName() {
971 return $this->tableName;
972 }
973
974 function defaultValue() {
975 return $this->default;
976 }
977
978 function maxLength() {
979 return $this->max_length;
980 }
981
982 function isNullable() {
983 return $this->nullable;
984 }
985
986 function type() {
987 return $this->type;
988 }
989 }
990
991 /**
992 * The MSSQL PHP driver doesn't support sqlsrv_num_rows, so we recall all rows into an array and maintain our
993 * own cursor index into that array...This is similar to the way the Oracle driver handles this same issue
994 *
995 * @ingroup Database
996 */
997 class MssqlResult {
998
999 public function __construct( $queryresult = false ) {
1000 $this->mCursor = 0;
1001 $this->mRows = array();
1002 $this->mNumFields = sqlsrv_num_fields( $queryresult );
1003 $this->mFieldMeta = sqlsrv_field_metadata( $queryresult );
1004
1005 $rows = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC );
1006
1007 foreach( $rows as $row ) {
1008 if ( $row !== null ) {
1009 foreach ( $row as $k => $v ) {
1010 if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object
1011 $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" );
1012 }
1013 }
1014 $this->mRows[] = $row;// read results into memory, cursors are not supported
1015 }
1016 }
1017 $this->mRowCount = count( $this->mRows );
1018 sqlsrv_free_stmt( $queryresult );
1019 }
1020
1021 private function array_to_obj( $array, &$obj ) {
1022 foreach ( $array as $key => $value ) {
1023 if ( is_array( $value ) ) {
1024 $obj->$key = new stdClass();
1025 $this->array_to_obj( $value, $obj->$key );
1026 } else {
1027 if ( !empty( $key ) ) {
1028 $obj->$key = $value;
1029 }
1030 }
1031 }
1032 return $obj;
1033 }
1034
1035 public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) {
1036 if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) {
1037 return false;
1038 }
1039 $arrNum = array();
1040 if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) {
1041 foreach ( $this->mRows[$this->mCursor] as $value ) {
1042 $arrNum[] = $value;
1043 }
1044 }
1045 switch( $mode ) {
1046 case SQLSRV_FETCH_ASSOC:
1047 $ret = $this->mRows[$this->mCursor];
1048 break;
1049 case SQLSRV_FETCH_NUMERIC:
1050 $ret = $arrNum;
1051 break;
1052 case 'OBJECT':
1053 $o = new $object_class;
1054 $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o );
1055 break;
1056 case SQLSRV_FETCH_BOTH:
1057 default:
1058 $ret = $this->mRows[$this->mCursor] + $arrNum;
1059 break;
1060 }
1061
1062 $this->mCursor++;
1063 return $ret;
1064 }
1065
1066 public function get( $pos, $fld ) {
1067 return $this->mRows[$pos][$fld];
1068 }
1069
1070 public function numrows() {
1071 return $this->mRowCount;
1072 }
1073
1074 public function seek( $iRow ) {
1075 $this->mCursor = min( $iRow, $this->mRowCount );
1076 }
1077
1078 public function numfields() {
1079 return $this->mNumFields;
1080 }
1081
1082 public function fieldname( $nr ) {
1083 $arrKeys = array_keys( $this->mRows[0] );
1084 return $arrKeys[$nr];
1085 }
1086
1087 public function fieldtype( $nr ) {
1088 $i = 0;
1089 $intType = -1;
1090 foreach ( $this->mFieldMeta as $meta ) {
1091 if ( $nr == $i ) {
1092 $intType = $meta['Type'];
1093 break;
1094 }
1095 $i++;
1096 }
1097 // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table
1098 switch( $intType ) {
1099 case SQLSRV_SQLTYPE_BIGINT: $strType = 'bigint'; break;
1100 case SQLSRV_SQLTYPE_BINARY: $strType = 'binary'; break;
1101 case SQLSRV_SQLTYPE_BIT: $strType = 'bit'; break;
1102 case SQLSRV_SQLTYPE_CHAR: $strType = 'char'; break;
1103 case SQLSRV_SQLTYPE_DATETIME: $strType = 'datetime'; break;
1104 case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break;
1105 case SQLSRV_SQLTYPE_FLOAT: $strType = 'float'; break;
1106 case SQLSRV_SQLTYPE_IMAGE: $strType = 'image'; break;
1107 case SQLSRV_SQLTYPE_INT: $strType = 'int'; break;
1108 case SQLSRV_SQLTYPE_MONEY: $strType = 'money'; break;
1109 case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break;
1110 case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break;
1111 case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break;
1112 // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break;
1113 case SQLSRV_SQLTYPE_NTEXT: $strType = 'ntext'; break;
1114 case SQLSRV_SQLTYPE_REAL: $strType = 'real'; break;
1115 case SQLSRV_SQLTYPE_SMALLDATETIME: $strType = 'smalldatetime'; break;
1116 case SQLSRV_SQLTYPE_SMALLINT: $strType = 'smallint'; break;
1117 case SQLSRV_SQLTYPE_SMALLMONEY: $strType = 'smallmoney'; break;
1118 case SQLSRV_SQLTYPE_TEXT: $strType = 'text'; break;
1119 case SQLSRV_SQLTYPE_TIMESTAMP: $strType = 'timestamp'; break;
1120 case SQLSRV_SQLTYPE_TINYINT: $strType = 'tinyint'; break;
1121 case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break;
1122 case SQLSRV_SQLTYPE_UDT: $strType = 'UDT'; break;
1123 case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break;
1124 // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break;
1125 case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break;
1126 // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break;
1127 case SQLSRV_SQLTYPE_XML: $strType = 'xml'; break;
1128 default: $strType = $intType;
1129 }
1130 return $strType;
1131 }
1132
1133 public function free() {
1134 unset( $this->mRows );
1135 return;
1136 }
1137 }