Add the other existing $skin.css/.js to the message files too to be consistent
[lhc/web/wiklou.git] / includes / Database.php
1 <?php
2 /**
3 * @defgroup Database Database
4 *
5 * @file
6 * @ingroup Database
7 * This file deals with MySQL interface functions
8 * and query specifics/optimisations
9 */
10
11 /** Number of times to re-try an operation in case of deadlock */
12 define( 'DEADLOCK_TRIES', 4 );
13 /** Minimum time to wait before retry, in microseconds */
14 define( 'DEADLOCK_DELAY_MIN', 500000 );
15 /** Maximum time to wait before retry */
16 define( 'DEADLOCK_DELAY_MAX', 1500000 );
17
18 /**
19 * Database abstraction object
20 * @ingroup Database
21 */
22 class Database {
23
24 #------------------------------------------------------------------------------
25 # Variables
26 #------------------------------------------------------------------------------
27
28 protected $mLastQuery = '';
29 protected $mPHPError = false;
30
31 protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
32 protected $mOut, $mOpened = false;
33
34 protected $mFailFunction;
35 protected $mTablePrefix;
36 protected $mFlags;
37 protected $mTrxLevel = 0;
38 protected $mErrorCount = 0;
39 protected $mLBInfo = array();
40 protected $mFakeSlaveLag = null, $mFakeMaster = false;
41
42 #------------------------------------------------------------------------------
43 # Accessors
44 #------------------------------------------------------------------------------
45 # These optionally set a variable and return the previous state
46
47 /**
48 * Fail function, takes a Database as a parameter
49 * Set to false for default, 1 for ignore errors
50 */
51 function failFunction( $function = NULL ) {
52 return wfSetVar( $this->mFailFunction, $function );
53 }
54
55 /**
56 * Output page, used for reporting errors
57 * FALSE means discard output
58 */
59 function setOutputPage( $out ) {
60 $this->mOut = $out;
61 }
62
63 /**
64 * Boolean, controls output of large amounts of debug information
65 */
66 function debug( $debug = NULL ) {
67 return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
68 }
69
70 /**
71 * Turns buffering of SQL result sets on (true) or off (false).
72 * Default is "on" and it should not be changed without good reasons.
73 */
74 function bufferResults( $buffer = NULL ) {
75 if ( is_null( $buffer ) ) {
76 return !(bool)( $this->mFlags & DBO_NOBUFFER );
77 } else {
78 return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
79 }
80 }
81
82 /**
83 * Turns on (false) or off (true) the automatic generation and sending
84 * of a "we're sorry, but there has been a database error" page on
85 * database errors. Default is on (false). When turned off, the
86 * code should use lastErrno() and lastError() to handle the
87 * situation as appropriate.
88 */
89 function ignoreErrors( $ignoreErrors = NULL ) {
90 return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
91 }
92
93 /**
94 * The current depth of nested transactions
95 * @param $level Integer: , default NULL.
96 */
97 function trxLevel( $level = NULL ) {
98 return wfSetVar( $this->mTrxLevel, $level );
99 }
100
101 /**
102 * Number of errors logged, only useful when errors are ignored
103 */
104 function errorCount( $count = NULL ) {
105 return wfSetVar( $this->mErrorCount, $count );
106 }
107
108 function tablePrefix( $prefix = null ) {
109 return wfSetVar( $this->mTablePrefix, $prefix );
110 }
111
112 /**
113 * Properties passed down from the server info array of the load balancer
114 */
115 function getLBInfo( $name = NULL ) {
116 if ( is_null( $name ) ) {
117 return $this->mLBInfo;
118 } else {
119 if ( array_key_exists( $name, $this->mLBInfo ) ) {
120 return $this->mLBInfo[$name];
121 } else {
122 return NULL;
123 }
124 }
125 }
126
127 function setLBInfo( $name, $value = NULL ) {
128 if ( is_null( $value ) ) {
129 $this->mLBInfo = $name;
130 } else {
131 $this->mLBInfo[$name] = $value;
132 }
133 }
134
135 /**
136 * Set lag time in seconds for a fake slave
137 */
138 function setFakeSlaveLag( $lag ) {
139 $this->mFakeSlaveLag = $lag;
140 }
141
142 /**
143 * Make this connection a fake master
144 */
145 function setFakeMaster( $enabled = true ) {
146 $this->mFakeMaster = $enabled;
147 }
148
149 /**
150 * Returns true if this database supports (and uses) cascading deletes
151 */
152 function cascadingDeletes() {
153 return false;
154 }
155
156 /**
157 * Returns true if this database supports (and uses) triggers (e.g. on the page table)
158 */
159 function cleanupTriggers() {
160 return false;
161 }
162
163 /**
164 * Returns true if this database is strict about what can be put into an IP field.
165 * Specifically, it uses a NULL value instead of an empty string.
166 */
167 function strictIPs() {
168 return false;
169 }
170
171 /**
172 * Returns true if this database uses timestamps rather than integers
173 */
174 function realTimestamps() {
175 return false;
176 }
177
178 /**
179 * Returns true if this database does an implicit sort when doing GROUP BY
180 */
181 function implicitGroupby() {
182 return true;
183 }
184
185 /**
186 * Returns true if this database does an implicit order by when the column has an index
187 * For example: SELECT page_title FROM page LIMIT 1
188 */
189 function implicitOrderby() {
190 return true;
191 }
192
193 /**
194 * Returns true if this database can do a native search on IP columns
195 * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
196 */
197 function searchableIPs() {
198 return false;
199 }
200
201 /**
202 * Returns true if this database can use functional indexes
203 */
204 function functionalIndexes() {
205 return false;
206 }
207
208 /**#@+
209 * Get function
210 */
211 function lastQuery() { return $this->mLastQuery; }
212 function isOpen() { return $this->mOpened; }
213 /**#@-*/
214
215 function setFlag( $flag ) {
216 $this->mFlags |= $flag;
217 }
218
219 function clearFlag( $flag ) {
220 $this->mFlags &= ~$flag;
221 }
222
223 function getFlag( $flag ) {
224 return !!($this->mFlags & $flag);
225 }
226
227 /**
228 * General read-only accessor
229 */
230 function getProperty( $name ) {
231 return $this->$name;
232 }
233
234 function getWikiID() {
235 if( $this->mTablePrefix ) {
236 return "{$this->mDBname}-{$this->mTablePrefix}";
237 } else {
238 return $this->mDBname;
239 }
240 }
241
242 #------------------------------------------------------------------------------
243 # Other functions
244 #------------------------------------------------------------------------------
245
246 /**@{{
247 * Constructor.
248 * @param string $server database server host
249 * @param string $user database user name
250 * @param string $password database user password
251 * @param string $dbname database name
252 * @param failFunction
253 * @param $flags
254 * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
255 */
256 function __construct( $server = false, $user = false, $password = false, $dbName = false,
257 $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
258
259 global $wgOut, $wgDBprefix, $wgCommandLineMode;
260 # Can't get a reference if it hasn't been set yet
261 if ( !isset( $wgOut ) ) {
262 $wgOut = NULL;
263 }
264 $this->mOut =& $wgOut;
265
266 $this->mFailFunction = $failFunction;
267 $this->mFlags = $flags;
268
269 if ( $this->mFlags & DBO_DEFAULT ) {
270 if ( $wgCommandLineMode ) {
271 $this->mFlags &= ~DBO_TRX;
272 } else {
273 $this->mFlags |= DBO_TRX;
274 }
275 }
276
277 /*
278 // Faster read-only access
279 if ( wfReadOnly() ) {
280 $this->mFlags |= DBO_PERSISTENT;
281 $this->mFlags &= ~DBO_TRX;
282 }*/
283
284 /** Get the default table prefix*/
285 if ( $tablePrefix == 'get from global' ) {
286 $this->mTablePrefix = $wgDBprefix;
287 } else {
288 $this->mTablePrefix = $tablePrefix;
289 }
290
291 if ( $server ) {
292 $this->open( $server, $user, $password, $dbName );
293 }
294 }
295
296 /**
297 * @static
298 * @param failFunction
299 * @param $flags
300 */
301 static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
302 {
303 return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
304 }
305
306 /**
307 * Usually aborts on failure
308 * If the failFunction is set to a non-zero integer, returns success
309 */
310 function open( $server, $user, $password, $dbName ) {
311 global $wguname, $wgAllDBsAreLocalhost;
312 wfProfileIn( __METHOD__ );
313
314 # Test for missing mysql.so
315 # First try to load it
316 if (!@extension_loaded('mysql')) {
317 @dl('mysql.so');
318 }
319
320 # Fail now
321 # Otherwise we get a suppressed fatal error, which is very hard to track down
322 if ( !function_exists( 'mysql_connect' ) ) {
323 throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
324 }
325
326 # Debugging hack -- fake cluster
327 if ( $wgAllDBsAreLocalhost ) {
328 $realServer = 'localhost';
329 } else {
330 $realServer = $server;
331 }
332 $this->close();
333 $this->mServer = $server;
334 $this->mUser = $user;
335 $this->mPassword = $password;
336 $this->mDBname = $dbName;
337
338 $success = false;
339
340 wfProfileIn("dbconnect-$server");
341
342 # Try to connect up to three times
343 # The kernel's default SYN retransmission period is far too slow for us,
344 # so we use a short timeout plus a manual retry.
345 $this->mConn = false;
346 $max = 3;
347 $this->installErrorHandler();
348 for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
349 if ( $i > 1 ) {
350 usleep( 1000 );
351 }
352 if ( $this->mFlags & DBO_PERSISTENT ) {
353 $this->mConn = mysql_pconnect( $realServer, $user, $password );
354 } else {
355 # Create a new connection...
356 $this->mConn = mysql_connect( $realServer, $user, $password, true );
357 }
358 if ($this->mConn === false) {
359 #$iplus = $i + 1;
360 #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
361 }
362 }
363 $phpError = $this->restoreErrorHandler();
364
365 wfProfileOut("dbconnect-$server");
366
367 if ( $dbName != '' ) {
368 if ( $this->mConn !== false ) {
369 $success = @/**/mysql_select_db( $dbName, $this->mConn );
370 if ( !$success ) {
371 $error = "Error selecting database $dbName on server {$this->mServer} " .
372 "from client host {$wguname['nodename']}\n";
373 wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
374 wfDebug( $error );
375 }
376 } else {
377 wfDebug( "DB connection error\n" );
378 wfDebug( "Server: $server, User: $user, Password: " .
379 substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
380 $success = false;
381 }
382 } else {
383 # Delay USE query
384 $success = (bool)$this->mConn;
385 }
386
387 if ( $success ) {
388 $version = $this->getServerVersion();
389 if ( version_compare( $version, '4.1' ) >= 0 ) {
390 // Tell the server we're communicating with it in UTF-8.
391 // This may engage various charset conversions.
392 global $wgDBmysql5;
393 if( $wgDBmysql5 ) {
394 $this->query( 'SET NAMES utf8', __METHOD__ );
395 }
396 // Turn off strict mode
397 $this->query( "SET sql_mode = ''", __METHOD__ );
398 }
399
400 // Turn off strict mode if it is on
401 } else {
402 $this->reportConnectionError( $phpError );
403 }
404
405 $this->mOpened = $success;
406 wfProfileOut( __METHOD__ );
407 return $success;
408 }
409 /**@}}*/
410
411 protected function installErrorHandler() {
412 $this->mPHPError = false;
413 set_error_handler( array( $this, 'connectionErrorHandler' ) );
414 }
415
416 protected function restoreErrorHandler() {
417 restore_error_handler();
418 return $this->mPHPError;
419 }
420
421 protected function connectionErrorHandler( $errno, $errstr ) {
422 $this->mPHPError = $errstr;
423 }
424
425 /**
426 * Closes a database connection.
427 * if it is open : commits any open transactions
428 *
429 * @return bool operation success. true if already closed.
430 */
431 function close()
432 {
433 $this->mOpened = false;
434 if ( $this->mConn ) {
435 if ( $this->trxLevel() ) {
436 $this->immediateCommit();
437 }
438 return mysql_close( $this->mConn );
439 } else {
440 return true;
441 }
442 }
443
444 /**
445 * @param string $error fallback error message, used if none is given by MySQL
446 */
447 function reportConnectionError( $error = 'Unknown error' ) {
448 $myError = $this->lastError();
449 if ( $myError ) {
450 $error = $myError;
451 }
452
453 if ( $this->mFailFunction ) {
454 # Legacy error handling method
455 if ( !is_int( $this->mFailFunction ) ) {
456 $ff = $this->mFailFunction;
457 $ff( $this, $error );
458 }
459 } else {
460 # New method
461 wfLogDBError( "Connection error: $error\n" );
462 throw new DBConnectionError( $this, $error );
463 }
464 }
465
466 /**
467 * Usually aborts on failure. If errors are explicitly ignored, returns success.
468 *
469 * @param $sql String: SQL query
470 * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST
471 * comment (you can use __METHOD__ or add some extra info)
472 * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors...
473 * maybe best to catch the exception instead?
474 * @return true for a successful write query, ResultWrapper object for a successful read query,
475 * or false on failure if $tempIgnore set
476 * @throws DBQueryError Thrown when the database returns an error of any kind
477 */
478 public function query( $sql, $fname = '', $tempIgnore = false ) {
479 global $wgProfiler;
480
481 $isMaster = !is_null( $this->getLBInfo( 'master' ) );
482 if ( isset( $wgProfiler ) ) {
483 # generalizeSQL will probably cut down the query to reasonable
484 # logging size most of the time. The substr is really just a sanity check.
485
486 # Who's been wasting my precious column space? -- TS
487 #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
488
489 if ( $isMaster ) {
490 $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
491 $totalProf = 'Database::query-master';
492 } else {
493 $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
494 $totalProf = 'Database::query';
495 }
496 wfProfileIn( $totalProf );
497 wfProfileIn( $queryProf );
498 }
499
500 $this->mLastQuery = $sql;
501
502 # Add a comment for easy SHOW PROCESSLIST interpretation
503 #if ( $fname ) {
504 global $wgUser;
505 if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
506 $userName = $wgUser->getName();
507 if ( mb_strlen( $userName ) > 15 ) {
508 $userName = mb_substr( $userName, 0, 15 ) . '...';
509 }
510 $userName = str_replace( '/', '', $userName );
511 } else {
512 $userName = '';
513 }
514 $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
515 #} else {
516 # $commentedSql = $sql;
517 #}
518
519 # If DBO_TRX is set, start a transaction
520 if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
521 $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
522 // avoid establishing transactions for SHOW and SET statements too -
523 // that would delay transaction initializations to once connection
524 // is really used by application
525 $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
526 if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0)
527 $this->begin();
528 }
529
530 if ( $this->debug() ) {
531 $sqlx = substr( $commentedSql, 0, 500 );
532 $sqlx = strtr( $sqlx, "\t\n", ' ' );
533 if ( $isMaster ) {
534 wfDebug( "SQL-master: $sqlx\n" );
535 } else {
536 wfDebug( "SQL: $sqlx\n" );
537 }
538 }
539
540 # Do the query and handle errors
541 $ret = $this->doQuery( $commentedSql );
542
543 # Try reconnecting if the connection was lost
544 if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) {
545 # Transaction is gone, like it or not
546 $this->mTrxLevel = 0;
547 wfDebug( "Connection lost, reconnecting...\n" );
548 if ( $this->ping() ) {
549 wfDebug( "Reconnected\n" );
550 $sqlx = substr( $commentedSql, 0, 500 );
551 $sqlx = strtr( $sqlx, "\t\n", ' ' );
552 global $wgRequestTime;
553 $elapsed = round( microtime(true) - $wgRequestTime, 3 );
554 wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
555 $ret = $this->doQuery( $commentedSql );
556 } else {
557 wfDebug( "Failed\n" );
558 }
559 }
560
561 if ( false === $ret ) {
562 $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
563 }
564
565 if ( isset( $wgProfiler ) ) {
566 wfProfileOut( $queryProf );
567 wfProfileOut( $totalProf );
568 }
569 return $this->resultObject( $ret );
570 }
571
572 /**
573 * The DBMS-dependent part of query()
574 * @param $sql String: SQL query.
575 * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
576 * @access private
577 */
578 /*private*/ function doQuery( $sql ) {
579 if( $this->bufferResults() ) {
580 $ret = mysql_query( $sql, $this->mConn );
581 } else {
582 $ret = mysql_unbuffered_query( $sql, $this->mConn );
583 }
584 return $ret;
585 }
586
587 /**
588 * @param $error
589 * @param $errno
590 * @param $sql
591 * @param string $fname
592 * @param bool $tempIgnore
593 */
594 function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
595 global $wgCommandLineMode;
596 # Ignore errors during error handling to avoid infinite recursion
597 $ignore = $this->ignoreErrors( true );
598 ++$this->mErrorCount;
599
600 if( $ignore || $tempIgnore ) {
601 wfDebug("SQL ERROR (ignored): $error\n");
602 $this->ignoreErrors( $ignore );
603 } else {
604 $sql1line = str_replace( "\n", "\\n", $sql );
605 wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
606 wfDebug("SQL ERROR: " . $error . "\n");
607 throw new DBQueryError( $this, $error, $errno, $sql, $fname );
608 }
609 }
610
611
612 /**
613 * Intended to be compatible with the PEAR::DB wrapper functions.
614 * http://pear.php.net/manual/en/package.database.db.intro-execute.php
615 *
616 * ? = scalar value, quoted as necessary
617 * ! = raw SQL bit (a function for instance)
618 * & = filename; reads the file and inserts as a blob
619 * (we don't use this though...)
620 */
621 function prepare( $sql, $func = 'Database::prepare' ) {
622 /* MySQL doesn't support prepared statements (yet), so just
623 pack up the query for reference. We'll manually replace
624 the bits later. */
625 return array( 'query' => $sql, 'func' => $func );
626 }
627
628 function freePrepared( $prepared ) {
629 /* No-op for MySQL */
630 }
631
632 /**
633 * Execute a prepared query with the various arguments
634 * @param string $prepared the prepared sql
635 * @param mixed $args Either an array here, or put scalars as varargs
636 */
637 function execute( $prepared, $args = null ) {
638 if( !is_array( $args ) ) {
639 # Pull the var args
640 $args = func_get_args();
641 array_shift( $args );
642 }
643 $sql = $this->fillPrepared( $prepared['query'], $args );
644 return $this->query( $sql, $prepared['func'] );
645 }
646
647 /**
648 * Prepare & execute an SQL statement, quoting and inserting arguments
649 * in the appropriate places.
650 * @param string $query
651 * @param string $args ...
652 */
653 function safeQuery( $query, $args = null ) {
654 $prepared = $this->prepare( $query, 'Database::safeQuery' );
655 if( !is_array( $args ) ) {
656 # Pull the var args
657 $args = func_get_args();
658 array_shift( $args );
659 }
660 $retval = $this->execute( $prepared, $args );
661 $this->freePrepared( $prepared );
662 return $retval;
663 }
664
665 /**
666 * For faking prepared SQL statements on DBs that don't support
667 * it directly.
668 * @param string $preparedSql - a 'preparable' SQL statement
669 * @param array $args - array of arguments to fill it with
670 * @return string executable SQL
671 */
672 function fillPrepared( $preparedQuery, $args ) {
673 reset( $args );
674 $this->preparedArgs =& $args;
675 return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
676 array( &$this, 'fillPreparedArg' ), $preparedQuery );
677 }
678
679 /**
680 * preg_callback func for fillPrepared()
681 * The arguments should be in $this->preparedArgs and must not be touched
682 * while we're doing this.
683 *
684 * @param array $matches
685 * @return string
686 * @private
687 */
688 function fillPreparedArg( $matches ) {
689 switch( $matches[1] ) {
690 case '\\?': return '?';
691 case '\\!': return '!';
692 case '\\&': return '&';
693 }
694 list( /* $n */ , $arg ) = each( $this->preparedArgs );
695 switch( $matches[1] ) {
696 case '?': return $this->addQuotes( $arg );
697 case '!': return $arg;
698 case '&':
699 # return $this->addQuotes( file_get_contents( $arg ) );
700 throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
701 default:
702 throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
703 }
704 }
705
706 /**#@+
707 * @param mixed $res A SQL result
708 */
709 /**
710 * Free a result object
711 */
712 function freeResult( $res ) {
713 if ( $res instanceof ResultWrapper ) {
714 $res = $res->result;
715 }
716 if ( !@/**/mysql_free_result( $res ) ) {
717 throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
718 }
719 }
720
721 /**
722 * Fetch the next row from the given result object, in object form.
723 * Fields can be retrieved with $row->fieldname, with fields acting like
724 * member variables.
725 *
726 * @param $res SQL result object as returned from Database::query(), etc.
727 * @return MySQL row object
728 * @throws DBUnexpectedError Thrown if the database returns an error
729 */
730 function fetchObject( $res ) {
731 if ( $res instanceof ResultWrapper ) {
732 $res = $res->result;
733 }
734 @/**/$row = mysql_fetch_object( $res );
735 if( $this->lastErrno() ) {
736 throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
737 }
738 return $row;
739 }
740
741 /**
742 * Fetch the next row from the given result object, in associative array
743 * form. Fields are retrieved with $row['fieldname'].
744 *
745 * @param $res SQL result object as returned from Database::query(), etc.
746 * @return MySQL row object
747 * @throws DBUnexpectedError Thrown if the database returns an error
748 */
749 function fetchRow( $res ) {
750 if ( $res instanceof ResultWrapper ) {
751 $res = $res->result;
752 }
753 @/**/$row = mysql_fetch_array( $res );
754 if ( $this->lastErrno() ) {
755 throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
756 }
757 return $row;
758 }
759
760 /**
761 * Get the number of rows in a result object
762 */
763 function numRows( $res ) {
764 if ( $res instanceof ResultWrapper ) {
765 $res = $res->result;
766 }
767 @/**/$n = mysql_num_rows( $res );
768 if( $this->lastErrno() ) {
769 throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
770 }
771 return $n;
772 }
773
774 /**
775 * Get the number of fields in a result object
776 * See documentation for mysql_num_fields()
777 */
778 function numFields( $res ) {
779 if ( $res instanceof ResultWrapper ) {
780 $res = $res->result;
781 }
782 return mysql_num_fields( $res );
783 }
784
785 /**
786 * Get a field name in a result object
787 * See documentation for mysql_field_name():
788 * http://www.php.net/mysql_field_name
789 */
790 function fieldName( $res, $n ) {
791 if ( $res instanceof ResultWrapper ) {
792 $res = $res->result;
793 }
794 return mysql_field_name( $res, $n );
795 }
796
797 /**
798 * Get the inserted value of an auto-increment row
799 *
800 * The value inserted should be fetched from nextSequenceValue()
801 *
802 * Example:
803 * $id = $dbw->nextSequenceValue('page_page_id_seq');
804 * $dbw->insert('page',array('page_id' => $id));
805 * $id = $dbw->insertId();
806 */
807 function insertId() { return mysql_insert_id( $this->mConn ); }
808
809 /**
810 * Change the position of the cursor in a result object
811 * See mysql_data_seek()
812 */
813 function dataSeek( $res, $row ) {
814 if ( $res instanceof ResultWrapper ) {
815 $res = $res->result;
816 }
817 return mysql_data_seek( $res, $row );
818 }
819
820 /**
821 * Get the last error number
822 * See mysql_errno()
823 */
824 function lastErrno() {
825 if ( $this->mConn ) {
826 return mysql_errno( $this->mConn );
827 } else {
828 return mysql_errno();
829 }
830 }
831
832 /**
833 * Get a description of the last error
834 * See mysql_error() for more details
835 */
836 function lastError() {
837 if ( $this->mConn ) {
838 # Even if it's non-zero, it can still be invalid
839 wfSuppressWarnings();
840 $error = mysql_error( $this->mConn );
841 if ( !$error ) {
842 $error = mysql_error();
843 }
844 wfRestoreWarnings();
845 } else {
846 $error = mysql_error();
847 }
848 if( $error ) {
849 $error .= ' (' . $this->mServer . ')';
850 }
851 return $error;
852 }
853 /**
854 * Get the number of rows affected by the last write query
855 * See mysql_affected_rows() for more details
856 */
857 function affectedRows() { return mysql_affected_rows( $this->mConn ); }
858 /**#@-*/ // end of template : @param $result
859
860 /**
861 * Simple UPDATE wrapper
862 * Usually aborts on failure
863 * If errors are explicitly ignored, returns success
864 *
865 * This function exists for historical reasons, Database::update() has a more standard
866 * calling convention and feature set
867 */
868 function set( $table, $var, $value, $cond, $fname = 'Database::set' )
869 {
870 $table = $this->tableName( $table );
871 $sql = "UPDATE $table SET $var = '" .
872 $this->strencode( $value ) . "' WHERE ($cond)";
873 return (bool)$this->query( $sql, $fname );
874 }
875
876 /**
877 * Simple SELECT wrapper, returns a single field, input must be encoded
878 * Usually aborts on failure
879 * If errors are explicitly ignored, returns FALSE on failure
880 */
881 function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
882 if ( !is_array( $options ) ) {
883 $options = array( $options );
884 }
885 $options['LIMIT'] = 1;
886
887 $res = $this->select( $table, $var, $cond, $fname, $options );
888 if ( $res === false || !$this->numRows( $res ) ) {
889 return false;
890 }
891 $row = $this->fetchRow( $res );
892 if ( $row !== false ) {
893 $this->freeResult( $res );
894 return $row[0];
895 } else {
896 return false;
897 }
898 }
899
900 /**
901 * Returns an optional USE INDEX clause to go after the table, and a
902 * string to go at the end of the query
903 *
904 * @private
905 *
906 * @param array $options an associative array of options to be turned into
907 * an SQL query, valid keys are listed in the function.
908 * @return array
909 */
910 function makeSelectOptions( $options ) {
911 $preLimitTail = $postLimitTail = '';
912 $startOpts = '';
913
914 $noKeyOptions = array();
915 foreach ( $options as $key => $option ) {
916 if ( is_numeric( $key ) ) {
917 $noKeyOptions[$option] = true;
918 }
919 }
920
921 if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
922 if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
923 if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
924
925 //if (isset($options['LIMIT'])) {
926 // $tailOpts .= $this->limitResult('', $options['LIMIT'],
927 // isset($options['OFFSET']) ? $options['OFFSET']
928 // : false);
929 //}
930
931 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
932 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
933 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
934
935 # Various MySQL extensions
936 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
937 if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
938 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
939 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
940 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
941 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
942 if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
943 if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
944
945 if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
946 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
947 } else {
948 $useIndex = '';
949 }
950
951 return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
952 }
953
954 /**
955 * SELECT wrapper
956 *
957 * @param mixed $table Array or string, table name(s) (prefix auto-added)
958 * @param mixed $vars Array or string, field name(s) to be retrieved
959 * @param mixed $conds Array or string, condition(s) for WHERE
960 * @param string $fname Calling function name (use __METHOD__) for logs/profiling
961 * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
962 * see Database::makeSelectOptions code for list of supported stuff
963 * @param array $join_conds Associative array of table join conditions (optional)
964 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
965 * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
966 */
967 function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
968 {
969 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
970 return $this->query( $sql, $fname );
971 }
972
973 /**
974 * SELECT wrapper
975 *
976 * @param mixed $table Array or string, table name(s) (prefix auto-added)
977 * @param mixed $vars Array or string, field name(s) to be retrieved
978 * @param mixed $conds Array or string, condition(s) for WHERE
979 * @param string $fname Calling function name (use __METHOD__) for logs/profiling
980 * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
981 * see Database::makeSelectOptions code for list of supported stuff
982 * @param array $join_conds Associative array of table join conditions (optional)
983 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
984 * @return string, the SQL text
985 */
986 function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
987 if( is_array( $vars ) ) {
988 $vars = implode( ',', $vars );
989 }
990 if( !is_array( $options ) ) {
991 $options = array( $options );
992 }
993 if( is_array( $table ) ) {
994 if ( !empty($join_conds) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) )
995 $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds );
996 else
997 $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
998 } elseif ($table!='') {
999 if ($table{0}==' ') {
1000 $from = ' FROM ' . $table;
1001 } else {
1002 $from = ' FROM ' . $this->tableName( $table );
1003 }
1004 } else {
1005 $from = '';
1006 }
1007
1008 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
1009
1010 if( !empty( $conds ) ) {
1011 if ( is_array( $conds ) ) {
1012 $conds = $this->makeList( $conds, LIST_AND );
1013 }
1014 $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
1015 } else {
1016 $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
1017 }
1018
1019 if (isset($options['LIMIT']))
1020 $sql = $this->limitResult($sql, $options['LIMIT'],
1021 isset($options['OFFSET']) ? $options['OFFSET'] : false);
1022 $sql = "$sql $postLimitTail";
1023
1024 if (isset($options['EXPLAIN'])) {
1025 $sql = 'EXPLAIN ' . $sql;
1026 }
1027 return $sql;
1028 }
1029
1030 /**
1031 * Single row SELECT wrapper
1032 * Aborts or returns FALSE on error
1033 *
1034 * $vars: the selected variables
1035 * $conds: a condition map, terms are ANDed together.
1036 * Items with numeric keys are taken to be literal conditions
1037 * Takes an array of selected variables, and a condition map, which is ANDed
1038 * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
1039 * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
1040 * $obj- >page_id is the ID of the Astronomy article
1041 *
1042 * @todo migrate documentation to phpdocumentor format
1043 */
1044 function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
1045 $options['LIMIT'] = 1;
1046 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1047 if ( $res === false )
1048 return false;
1049 if ( !$this->numRows($res) ) {
1050 $this->freeResult($res);
1051 return false;
1052 }
1053 $obj = $this->fetchObject( $res );
1054 $this->freeResult( $res );
1055 return $obj;
1056
1057 }
1058
1059 /**
1060 * Estimate rows in dataset
1061 * Returns estimated count, based on EXPLAIN output
1062 * Takes same arguments as Database::select()
1063 */
1064
1065 function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
1066 $options['EXPLAIN']=true;
1067 $res = $this->select ($table, $vars, $conds, $fname, $options );
1068 if ( $res === false )
1069 return false;
1070 if (!$this->numRows($res)) {
1071 $this->freeResult($res);
1072 return 0;
1073 }
1074
1075 $rows=1;
1076
1077 while( $plan = $this->fetchObject( $res ) ) {
1078 $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
1079 }
1080
1081 $this->freeResult($res);
1082 return $rows;
1083 }
1084
1085
1086 /**
1087 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1088 * It's only slightly flawed. Don't use for anything important.
1089 *
1090 * @param string $sql A SQL Query
1091 * @static
1092 */
1093 static function generalizeSQL( $sql ) {
1094 # This does the same as the regexp below would do, but in such a way
1095 # as to avoid crashing php on some large strings.
1096 # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
1097
1098 $sql = str_replace ( "\\\\", '', $sql);
1099 $sql = str_replace ( "\\'", '', $sql);
1100 $sql = str_replace ( "\\\"", '', $sql);
1101 $sql = preg_replace ("/'.*'/s", "'X'", $sql);
1102 $sql = preg_replace ('/".*"/s', "'X'", $sql);
1103
1104 # All newlines, tabs, etc replaced by single space
1105 $sql = preg_replace ( '/\s+/', ' ', $sql);
1106
1107 # All numbers => N
1108 $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
1109
1110 return $sql;
1111 }
1112
1113 /**
1114 * Determines whether a field exists in a table
1115 * Usually aborts on failure
1116 * If errors are explicitly ignored, returns NULL on failure
1117 */
1118 function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
1119 $table = $this->tableName( $table );
1120 $res = $this->query( 'DESCRIBE '.$table, $fname );
1121 if ( !$res ) {
1122 return NULL;
1123 }
1124
1125 $found = false;
1126
1127 while ( $row = $this->fetchObject( $res ) ) {
1128 if ( $row->Field == $field ) {
1129 $found = true;
1130 break;
1131 }
1132 }
1133 return $found;
1134 }
1135
1136 /**
1137 * Determines whether an index exists
1138 * Usually aborts on failure
1139 * If errors are explicitly ignored, returns NULL on failure
1140 */
1141 function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
1142 $info = $this->indexInfo( $table, $index, $fname );
1143 if ( is_null( $info ) ) {
1144 return NULL;
1145 } else {
1146 return $info !== false;
1147 }
1148 }
1149
1150
1151 /**
1152 * Get information about an index into an object
1153 * Returns false if the index does not exist
1154 */
1155 function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
1156 # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
1157 # SHOW INDEX should work for 3.x and up:
1158 # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
1159 $table = $this->tableName( $table );
1160 $sql = 'SHOW INDEX FROM '.$table;
1161 $res = $this->query( $sql, $fname );
1162 if ( !$res ) {
1163 return NULL;
1164 }
1165
1166 $result = array();
1167 while ( $row = $this->fetchObject( $res ) ) {
1168 if ( $row->Key_name == $index ) {
1169 $result[] = $row;
1170 }
1171 }
1172 $this->freeResult($res);
1173
1174 return empty($result) ? false : $result;
1175 }
1176
1177 /**
1178 * Query whether a given table exists
1179 */
1180 function tableExists( $table ) {
1181 $table = $this->tableName( $table );
1182 $old = $this->ignoreErrors( true );
1183 $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
1184 $this->ignoreErrors( $old );
1185 if( $res ) {
1186 $this->freeResult( $res );
1187 return true;
1188 } else {
1189 return false;
1190 }
1191 }
1192
1193 /**
1194 * mysql_fetch_field() wrapper
1195 * Returns false if the field doesn't exist
1196 *
1197 * @param $table
1198 * @param $field
1199 */
1200 function fieldInfo( $table, $field ) {
1201 $table = $this->tableName( $table );
1202 $res = $this->query( "SELECT * FROM $table LIMIT 1" );
1203 $n = mysql_num_fields( $res->result );
1204 for( $i = 0; $i < $n; $i++ ) {
1205 $meta = mysql_fetch_field( $res->result, $i );
1206 if( $field == $meta->name ) {
1207 return new MySQLField($meta);
1208 }
1209 }
1210 return false;
1211 }
1212
1213 /**
1214 * mysql_field_type() wrapper
1215 */
1216 function fieldType( $res, $index ) {
1217 if ( $res instanceof ResultWrapper ) {
1218 $res = $res->result;
1219 }
1220 return mysql_field_type( $res, $index );
1221 }
1222
1223 /**
1224 * Determines if a given index is unique
1225 */
1226 function indexUnique( $table, $index ) {
1227 $indexInfo = $this->indexInfo( $table, $index );
1228 if ( !$indexInfo ) {
1229 return NULL;
1230 }
1231 return !$indexInfo[0]->Non_unique;
1232 }
1233
1234 /**
1235 * INSERT wrapper, inserts an array into a table
1236 *
1237 * $a may be a single associative array, or an array of these with numeric keys, for
1238 * multi-row insert.
1239 *
1240 * Usually aborts on failure
1241 * If errors are explicitly ignored, returns success
1242 */
1243 function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
1244 # No rows to insert, easy just return now
1245 if ( !count( $a ) ) {
1246 return true;
1247 }
1248
1249 $table = $this->tableName( $table );
1250 if ( !is_array( $options ) ) {
1251 $options = array( $options );
1252 }
1253 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1254 $multi = true;
1255 $keys = array_keys( $a[0] );
1256 } else {
1257 $multi = false;
1258 $keys = array_keys( $a );
1259 }
1260
1261 $sql = 'INSERT ' . implode( ' ', $options ) .
1262 " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1263
1264 if ( $multi ) {
1265 $first = true;
1266 foreach ( $a as $row ) {
1267 if ( $first ) {
1268 $first = false;
1269 } else {
1270 $sql .= ',';
1271 }
1272 $sql .= '(' . $this->makeList( $row ) . ')';
1273 }
1274 } else {
1275 $sql .= '(' . $this->makeList( $a ) . ')';
1276 }
1277 return (bool)$this->query( $sql, $fname );
1278 }
1279
1280 /**
1281 * Make UPDATE options for the Database::update function
1282 *
1283 * @private
1284 * @param array $options The options passed to Database::update
1285 * @return string
1286 */
1287 function makeUpdateOptions( $options ) {
1288 if( !is_array( $options ) ) {
1289 $options = array( $options );
1290 }
1291 $opts = array();
1292 if ( in_array( 'LOW_PRIORITY', $options ) )
1293 $opts[] = $this->lowPriorityOption();
1294 if ( in_array( 'IGNORE', $options ) )
1295 $opts[] = 'IGNORE';
1296 return implode(' ', $opts);
1297 }
1298
1299 /**
1300 * UPDATE wrapper, takes a condition array and a SET array
1301 *
1302 * @param string $table The table to UPDATE
1303 * @param array $values An array of values to SET
1304 * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
1305 * @param string $fname The Class::Function calling this function
1306 * (for the log)
1307 * @param array $options An array of UPDATE options, can be one or
1308 * more of IGNORE, LOW_PRIORITY
1309 * @return bool
1310 */
1311 function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
1312 $table = $this->tableName( $table );
1313 $opts = $this->makeUpdateOptions( $options );
1314 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
1315 if ( $conds != '*' ) {
1316 $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
1317 }
1318 return $this->query( $sql, $fname );
1319 }
1320
1321 /**
1322 * Makes an encoded list of strings from an array
1323 * $mode:
1324 * LIST_COMMA - comma separated, no field names
1325 * LIST_AND - ANDed WHERE clause (without the WHERE)
1326 * LIST_OR - ORed WHERE clause (without the WHERE)
1327 * LIST_SET - comma separated with field names, like a SET clause
1328 * LIST_NAMES - comma separated field names
1329 */
1330 function makeList( $a, $mode = LIST_COMMA ) {
1331 if ( !is_array( $a ) ) {
1332 throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
1333 }
1334
1335 $first = true;
1336 $list = '';
1337 foreach ( $a as $field => $value ) {
1338 if ( !$first ) {
1339 if ( $mode == LIST_AND ) {
1340 $list .= ' AND ';
1341 } elseif($mode == LIST_OR) {
1342 $list .= ' OR ';
1343 } else {
1344 $list .= ',';
1345 }
1346 } else {
1347 $first = false;
1348 }
1349 if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
1350 $list .= "($value)";
1351 } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
1352 $list .= "$value";
1353 } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
1354 if( count( $value ) == 0 ) {
1355 throw new MWException( __METHOD__.': empty input' );
1356 } elseif( count( $value ) == 1 ) {
1357 // Special-case single values, as IN isn't terribly efficient
1358 // Don't necessarily assume the single key is 0; we don't
1359 // enforce linear numeric ordering on other arrays here.
1360 $value = array_values( $value );
1361 $list .= $field." = ".$this->addQuotes( $value[0] );
1362 } else {
1363 $list .= $field." IN (".$this->makeList($value).") ";
1364 }
1365 } elseif( is_null($value) ) {
1366 if ( $mode == LIST_AND || $mode == LIST_OR ) {
1367 $list .= "$field IS ";
1368 } elseif ( $mode == LIST_SET ) {
1369 $list .= "$field = ";
1370 }
1371 $list .= 'NULL';
1372 } else {
1373 if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
1374 $list .= "$field = ";
1375 }
1376 $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
1377 }
1378 }
1379 return $list;
1380 }
1381
1382 /**
1383 * Change the current database
1384 */
1385 function selectDB( $db ) {
1386 $this->mDBname = $db;
1387 return mysql_select_db( $db, $this->mConn );
1388 }
1389
1390 /**
1391 * Get the current DB name
1392 */
1393 function getDBname() {
1394 return $this->mDBname;
1395 }
1396
1397 /**
1398 * Get the server hostname or IP address
1399 */
1400 function getServer() {
1401 return $this->mServer;
1402 }
1403
1404 /**
1405 * Format a table name ready for use in constructing an SQL query
1406 *
1407 * This does two important things: it quotes the table names to clean them up,
1408 * and it adds a table prefix if only given a table name with no quotes.
1409 *
1410 * All functions of this object which require a table name call this function
1411 * themselves. Pass the canonical name to such functions. This is only needed
1412 * when calling query() directly.
1413 *
1414 * @param string $name database table name
1415 * @return string full database name
1416 */
1417 function tableName( $name ) {
1418 global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
1419 # Skip the entire process when we have a string quoted on both ends.
1420 # Note that we check the end so that we will still quote any use of
1421 # use of `database`.table. But won't break things if someone wants
1422 # to query a database table with a dot in the name.
1423 if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name;
1424
1425 # Lets test for any bits of text that should never show up in a table
1426 # name. Basically anything like JOIN or ON which are actually part of
1427 # SQL queries, but may end up inside of the table value to combine
1428 # sql. Such as how the API is doing.
1429 # Note that we use a whitespace test rather than a \b test to avoid
1430 # any remote case where a word like on may be inside of a table name
1431 # surrounded by symbols which may be considered word breaks.
1432 if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name;
1433
1434 # Split database and table into proper variables.
1435 # We reverse the explode so that database.table and table both output
1436 # the correct table.
1437 $dbDetails = array_reverse( explode( '.', $name, 2 ) );
1438 if( isset( $dbDetails[1] ) ) @list( $table, $database ) = $dbDetails;
1439 else @list( $table ) = $dbDetails;
1440 $prefix = $this->mTablePrefix; # Default prefix
1441
1442 # A database name has been specified in input. Quote the table name
1443 # because we don't want any prefixes added.
1444 if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" );
1445
1446 # Note that we use the long format because php will complain in in_array if
1447 # the input is not an array, and will complain in is_array if it is not set.
1448 if( !isset( $database ) # Don't use shared database if pre selected.
1449 && isset( $wgSharedDB ) # We have a shared database
1450 && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`'
1451 && isset( $wgSharedTables )
1452 && is_array( $wgSharedTables )
1453 && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
1454 $database = $wgSharedDB;
1455 $prefix = isset( $wgSharedprefix ) ? $wgSharedprefix : $prefix;
1456 }
1457
1458 # Quote the $database and $table and apply the prefix if not quoted.
1459 if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" );
1460 $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" );
1461
1462 # Merge our database and table into our final table name.
1463 $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" );
1464
1465 # We're finished, return.
1466 return $tableName;
1467 }
1468
1469 /**
1470 * Fetch a number of table names into an array
1471 * This is handy when you need to construct SQL for joins
1472 *
1473 * Example:
1474 * extract($dbr->tableNames('user','watchlist'));
1475 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1476 * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1477 */
1478 public function tableNames() {
1479 $inArray = func_get_args();
1480 $retVal = array();
1481 foreach ( $inArray as $name ) {
1482 $retVal[$name] = $this->tableName( $name );
1483 }
1484 return $retVal;
1485 }
1486
1487 /**
1488 * Fetch a number of table names into an zero-indexed numerical array
1489 * This is handy when you need to construct SQL for joins
1490 *
1491 * Example:
1492 * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
1493 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1494 * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1495 */
1496 public function tableNamesN() {
1497 $inArray = func_get_args();
1498 $retVal = array();
1499 foreach ( $inArray as $name ) {
1500 $retVal[] = $this->tableName( $name );
1501 }
1502 return $retVal;
1503 }
1504
1505 /**
1506 * @private
1507 */
1508 function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
1509 $ret = array();
1510 $retJOIN = array();
1511 $use_index_safe = is_array($use_index) ? $use_index : array();
1512 $join_conds_safe = is_array($join_conds) ? $join_conds : array();
1513 foreach ( $tables as $table ) {
1514 // Is there a JOIN and INDEX clause for this table?
1515 if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
1516 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
1517 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
1518 $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
1519 $retJOIN[] = $tableClause;
1520 // Is there an INDEX clause?
1521 } else if ( isset($use_index_safe[$table]) ) {
1522 $tableClause = $this->tableName( $table );
1523 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
1524 $ret[] = $tableClause;
1525 // Is there a JOIN clause?
1526 } else if ( isset($join_conds_safe[$table]) ) {
1527 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
1528 $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
1529 $retJOIN[] = $tableClause;
1530 } else {
1531 $tableClause = $this->tableName( $table );
1532 $ret[] = $tableClause;
1533 }
1534 }
1535 // We can't separate explicit JOIN clauses with ',', use ' ' for those
1536 $straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
1537 $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
1538 // Compile our final table clause
1539 return implode(' ',array($straightJoins,$otherJoins) );
1540 }
1541
1542 /**
1543 * Wrapper for addslashes()
1544 * @param string $s String to be slashed.
1545 * @return string slashed string.
1546 */
1547 function strencode( $s ) {
1548 return mysql_real_escape_string( $s, $this->mConn );
1549 }
1550
1551 /**
1552 * If it's a string, adds quotes and backslashes
1553 * Otherwise returns as-is
1554 */
1555 function addQuotes( $s ) {
1556 if ( is_null( $s ) ) {
1557 return 'NULL';
1558 } else {
1559 # This will also quote numeric values. This should be harmless,
1560 # and protects against weird problems that occur when they really
1561 # _are_ strings such as article titles and string->number->string
1562 # conversion is not 1:1.
1563 return "'" . $this->strencode( $s ) . "'";
1564 }
1565 }
1566
1567 /**
1568 * Escape string for safe LIKE usage
1569 */
1570 function escapeLike( $s ) {
1571 $s=$this->strencode( $s );
1572 $s=str_replace(array('%','_'),array('\%','\_'),$s);
1573 return $s;
1574 }
1575
1576 /**
1577 * Returns an appropriately quoted sequence value for inserting a new row.
1578 * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
1579 * subclass will return an integer, and save the value for insertId()
1580 */
1581 function nextSequenceValue( $seqName ) {
1582 return NULL;
1583 }
1584
1585 /**
1586 * USE INDEX clause
1587 * PostgreSQL doesn't have them and returns ""
1588 */
1589 function useIndexClause( $index ) {
1590 return "FORCE INDEX ($index)";
1591 }
1592
1593 /**
1594 * REPLACE query wrapper
1595 * PostgreSQL simulates this with a DELETE followed by INSERT
1596 * $row is the row to insert, an associative array
1597 * $uniqueIndexes is an array of indexes. Each element may be either a
1598 * field name or an array of field names
1599 *
1600 * It may be more efficient to leave off unique indexes which are unlikely to collide.
1601 * However if you do this, you run the risk of encountering errors which wouldn't have
1602 * occurred in MySQL
1603 *
1604 * @todo migrate comment to phodocumentor format
1605 */
1606 function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
1607 $table = $this->tableName( $table );
1608
1609 # Single row case
1610 if ( !is_array( reset( $rows ) ) ) {
1611 $rows = array( $rows );
1612 }
1613
1614 $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
1615 $first = true;
1616 foreach ( $rows as $row ) {
1617 if ( $first ) {
1618 $first = false;
1619 } else {
1620 $sql .= ',';
1621 }
1622 $sql .= '(' . $this->makeList( $row ) . ')';
1623 }
1624 return $this->query( $sql, $fname );
1625 }
1626
1627 /**
1628 * DELETE where the condition is a join
1629 * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
1630 *
1631 * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
1632 * join condition matches, set $conds='*'
1633 *
1634 * DO NOT put the join condition in $conds
1635 *
1636 * @param string $delTable The table to delete from.
1637 * @param string $joinTable The other table.
1638 * @param string $delVar The variable to join on, in the first table.
1639 * @param string $joinVar The variable to join on, in the second table.
1640 * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
1641 */
1642 function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
1643 if ( !$conds ) {
1644 throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
1645 }
1646
1647 $delTable = $this->tableName( $delTable );
1648 $joinTable = $this->tableName( $joinTable );
1649 $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
1650 if ( $conds != '*' ) {
1651 $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
1652 }
1653
1654 return $this->query( $sql, $fname );
1655 }
1656
1657 /**
1658 * Returns the size of a text field, or -1 for "unlimited"
1659 */
1660 function textFieldSize( $table, $field ) {
1661 $table = $this->tableName( $table );
1662 $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
1663 $res = $this->query( $sql, 'Database::textFieldSize' );
1664 $row = $this->fetchObject( $res );
1665 $this->freeResult( $res );
1666
1667 $m = array();
1668 if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
1669 $size = $m[1];
1670 } else {
1671 $size = -1;
1672 }
1673 return $size;
1674 }
1675
1676 /**
1677 * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
1678 */
1679 function lowPriorityOption() {
1680 return 'LOW_PRIORITY';
1681 }
1682
1683 /**
1684 * DELETE query wrapper
1685 *
1686 * Use $conds == "*" to delete all rows
1687 */
1688 function delete( $table, $conds, $fname = 'Database::delete' ) {
1689 if ( !$conds ) {
1690 throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
1691 }
1692 $table = $this->tableName( $table );
1693 $sql = "DELETE FROM $table";
1694 if ( $conds != '*' ) {
1695 $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
1696 }
1697 return $this->query( $sql, $fname );
1698 }
1699
1700 /**
1701 * INSERT SELECT wrapper
1702 * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
1703 * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
1704 * $conds may be "*" to copy the whole table
1705 * srcTable may be an array of tables.
1706 */
1707 function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
1708 $insertOptions = array(), $selectOptions = array() )
1709 {
1710 $destTable = $this->tableName( $destTable );
1711 if ( is_array( $insertOptions ) ) {
1712 $insertOptions = implode( ' ', $insertOptions );
1713 }
1714 if( !is_array( $selectOptions ) ) {
1715 $selectOptions = array( $selectOptions );
1716 }
1717 list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
1718 if( is_array( $srcTable ) ) {
1719 $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
1720 } else {
1721 $srcTable = $this->tableName( $srcTable );
1722 }
1723 $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
1724 " SELECT $startOpts " . implode( ',', $varMap ) .
1725 " FROM $srcTable $useIndex ";
1726 if ( $conds != '*' ) {
1727 $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
1728 }
1729 $sql .= " $tailOpts";
1730 return $this->query( $sql, $fname );
1731 }
1732
1733 /**
1734 * Construct a LIMIT query with optional offset
1735 * This is used for query pages
1736 * $sql string SQL query we will append the limit too
1737 * $limit integer the SQL limit
1738 * $offset integer the SQL offset (default false)
1739 */
1740 function limitResult($sql, $limit, $offset=false) {
1741 if( !is_numeric($limit) ) {
1742 throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
1743 }
1744 return "$sql LIMIT "
1745 . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
1746 . "{$limit} ";
1747 }
1748 function limitResultForUpdate($sql, $num) {
1749 return $this->limitResult($sql, $num, 0);
1750 }
1751
1752 /**
1753 * Returns an SQL expression for a simple conditional.
1754 * Uses IF on MySQL.
1755 *
1756 * @param string $cond SQL expression which will result in a boolean value
1757 * @param string $trueVal SQL expression to return if true
1758 * @param string $falseVal SQL expression to return if false
1759 * @return string SQL fragment
1760 */
1761 function conditional( $cond, $trueVal, $falseVal ) {
1762 return " IF($cond, $trueVal, $falseVal) ";
1763 }
1764
1765 /**
1766 * Returns a comand for str_replace function in SQL query.
1767 * Uses REPLACE() in MySQL
1768 *
1769 * @param string $orig String or column to modify
1770 * @param string $old String or column to seek
1771 * @param string $new String or column to replace with
1772 */
1773 function strreplace( $orig, $old, $new ) {
1774 return "REPLACE({$orig}, {$old}, {$new})";
1775 }
1776
1777 /**
1778 * Determines if the last failure was due to a deadlock
1779 */
1780 function wasDeadlock() {
1781 return $this->lastErrno() == 1213;
1782 }
1783
1784 /**
1785 * Perform a deadlock-prone transaction.
1786 *
1787 * This function invokes a callback function to perform a set of write
1788 * queries. If a deadlock occurs during the processing, the transaction
1789 * will be rolled back and the callback function will be called again.
1790 *
1791 * Usage:
1792 * $dbw->deadlockLoop( callback, ... );
1793 *
1794 * Extra arguments are passed through to the specified callback function.
1795 *
1796 * Returns whatever the callback function returned on its successful,
1797 * iteration, or false on error, for example if the retry limit was
1798 * reached.
1799 */
1800 function deadlockLoop() {
1801 $myFname = 'Database::deadlockLoop';
1802
1803 $this->begin();
1804 $args = func_get_args();
1805 $function = array_shift( $args );
1806 $oldIgnore = $this->ignoreErrors( true );
1807 $tries = DEADLOCK_TRIES;
1808 if ( is_array( $function ) ) {
1809 $fname = $function[0];
1810 } else {
1811 $fname = $function;
1812 }
1813 do {
1814 $retVal = call_user_func_array( $function, $args );
1815 $error = $this->lastError();
1816 $errno = $this->lastErrno();
1817 $sql = $this->lastQuery();
1818
1819 if ( $errno ) {
1820 if ( $this->wasDeadlock() ) {
1821 # Retry
1822 usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
1823 } else {
1824 $this->reportQueryError( $error, $errno, $sql, $fname );
1825 }
1826 }
1827 } while( $this->wasDeadlock() && --$tries > 0 );
1828 $this->ignoreErrors( $oldIgnore );
1829 if ( $tries <= 0 ) {
1830 $this->query( 'ROLLBACK', $myFname );
1831 $this->reportQueryError( $error, $errno, $sql, $fname );
1832 return false;
1833 } else {
1834 $this->query( 'COMMIT', $myFname );
1835 return $retVal;
1836 }
1837 }
1838
1839 /**
1840 * Do a SELECT MASTER_POS_WAIT()
1841 *
1842 * @param string $file the binlog file
1843 * @param string $pos the binlog position
1844 * @param integer $timeout the maximum number of seconds to wait for synchronisation
1845 */
1846 function masterPosWait( MySQLMasterPos $pos, $timeout ) {
1847 $fname = 'Database::masterPosWait';
1848 wfProfileIn( $fname );
1849
1850 # Commit any open transactions
1851 if ( $this->mTrxLevel ) {
1852 $this->immediateCommit();
1853 }
1854
1855 if ( !is_null( $this->mFakeSlaveLag ) ) {
1856 $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 );
1857 if ( $wait > $timeout * 1e6 ) {
1858 wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" );
1859 wfProfileOut( $fname );
1860 return -1;
1861 } elseif ( $wait > 0 ) {
1862 wfDebug( "Fake slave waiting $wait us\n" );
1863 usleep( $wait );
1864 wfProfileOut( $fname );
1865 return 1;
1866 } else {
1867 wfDebug( "Fake slave up to date ($wait us)\n" );
1868 wfProfileOut( $fname );
1869 return 0;
1870 }
1871 }
1872
1873 # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
1874 $encFile = $this->addQuotes( $pos->file );
1875 $encPos = intval( $pos->pos );
1876 $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
1877 $res = $this->doQuery( $sql );
1878 if ( $res && $row = $this->fetchRow( $res ) ) {
1879 $this->freeResult( $res );
1880 wfProfileOut( $fname );
1881 return $row[0];
1882 } else {
1883 wfProfileOut( $fname );
1884 return false;
1885 }
1886 }
1887
1888 /**
1889 * Get the position of the master from SHOW SLAVE STATUS
1890 */
1891 function getSlavePos() {
1892 if ( !is_null( $this->mFakeSlaveLag ) ) {
1893 $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag );
1894 wfDebug( __METHOD__.": fake slave pos = $pos\n" );
1895 return $pos;
1896 }
1897 $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
1898 $row = $this->fetchObject( $res );
1899 if ( $row ) {
1900 return new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos );
1901 } else {
1902 return false;
1903 }
1904 }
1905
1906 /**
1907 * Get the position of the master from SHOW MASTER STATUS
1908 */
1909 function getMasterPos() {
1910 if ( $this->mFakeMaster ) {
1911 return new MySQLMasterPos( 'fake', microtime( true ) );
1912 }
1913 $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
1914 $row = $this->fetchObject( $res );
1915 if ( $row ) {
1916 return new MySQLMasterPos( $row->File, $row->Position );
1917 } else {
1918 return false;
1919 }
1920 }
1921
1922 /**
1923 * Begin a transaction, committing any previously open transaction
1924 */
1925 function begin( $fname = 'Database::begin' ) {
1926 $this->query( 'BEGIN', $fname );
1927 $this->mTrxLevel = 1;
1928 }
1929
1930 /**
1931 * End a transaction
1932 */
1933 function commit( $fname = 'Database::commit' ) {
1934 $this->query( 'COMMIT', $fname );
1935 $this->mTrxLevel = 0;
1936 }
1937
1938 /**
1939 * Rollback a transaction.
1940 * No-op on non-transactional databases.
1941 */
1942 function rollback( $fname = 'Database::rollback' ) {
1943 $this->query( 'ROLLBACK', $fname, true );
1944 $this->mTrxLevel = 0;
1945 }
1946
1947 /**
1948 * Begin a transaction, committing any previously open transaction
1949 * @deprecated use begin()
1950 */
1951 function immediateBegin( $fname = 'Database::immediateBegin' ) {
1952 $this->begin();
1953 }
1954
1955 /**
1956 * Commit transaction, if one is open
1957 * @deprecated use commit()
1958 */
1959 function immediateCommit( $fname = 'Database::immediateCommit' ) {
1960 $this->commit();
1961 }
1962
1963 /**
1964 * Return MW-style timestamp used for MySQL schema
1965 */
1966 function timestamp( $ts=0 ) {
1967 return wfTimestamp(TS_MW,$ts);
1968 }
1969
1970 /**
1971 * Local database timestamp format or null
1972 */
1973 function timestampOrNull( $ts = null ) {
1974 if( is_null( $ts ) ) {
1975 return null;
1976 } else {
1977 return $this->timestamp( $ts );
1978 }
1979 }
1980
1981 /**
1982 * @todo document
1983 */
1984 function resultObject( $result ) {
1985 if( empty( $result ) ) {
1986 return false;
1987 } elseif ( $result instanceof ResultWrapper ) {
1988 return $result;
1989 } elseif ( $result === true ) {
1990 // Successful write query
1991 return $result;
1992 } else {
1993 return new ResultWrapper( $this, $result );
1994 }
1995 }
1996
1997 /**
1998 * Return aggregated value alias
1999 */
2000 function aggregateValue ($valuedata,$valuename='value') {
2001 return $valuename;
2002 }
2003
2004 /**
2005 * @return string wikitext of a link to the server software's web site
2006 */
2007 function getSoftwareLink() {
2008 return "[http://www.mysql.com/ MySQL]";
2009 }
2010
2011 /**
2012 * @return string Version information from the database
2013 */
2014 function getServerVersion() {
2015 return mysql_get_server_info( $this->mConn );
2016 }
2017
2018 /**
2019 * Ping the server and try to reconnect if it there is no connection
2020 */
2021 function ping() {
2022 if( !function_exists( 'mysql_ping' ) ) {
2023 wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
2024 return true;
2025 }
2026 $ping = mysql_ping( $this->mConn );
2027 if ( $ping ) {
2028 return true;
2029 }
2030
2031 // Need to reconnect manually in MySQL client 5.0.13+
2032 if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
2033 mysql_close( $this->mConn );
2034 $this->mOpened = false;
2035 $this->mConn = false;
2036 $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
2037 return true;
2038 }
2039 return false;
2040 }
2041
2042 /**
2043 * Get slave lag.
2044 * At the moment, this will only work if the DB user has the PROCESS privilege
2045 */
2046 function getLag() {
2047 if ( !is_null( $this->mFakeSlaveLag ) ) {
2048 wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
2049 return $this->mFakeSlaveLag;
2050 }
2051 $res = $this->query( 'SHOW PROCESSLIST' );
2052 # Find slave SQL thread
2053 while ( $row = $this->fetchObject( $res ) ) {
2054 /* This should work for most situations - when default db
2055 * for thread is not specified, it had no events executed,
2056 * and therefore it doesn't know yet how lagged it is.
2057 *
2058 * Relay log I/O thread does not select databases.
2059 */
2060 if ( $row->User == 'system user' &&
2061 $row->State != 'Waiting for master to send event' &&
2062 $row->State != 'Connecting to master' &&
2063 $row->State != 'Queueing master event to the relay log' &&
2064 $row->State != 'Waiting for master update' &&
2065 $row->State != 'Requesting binlog dump'
2066 ) {
2067 # This is it, return the time (except -ve)
2068 if ( $row->Time > 0x7fffffff ) {
2069 return false;
2070 } else {
2071 return $row->Time;
2072 }
2073 }
2074 }
2075 return false;
2076 }
2077
2078 /**
2079 * Get status information from SHOW STATUS in an associative array
2080 */
2081 function getStatus($which="%") {
2082 $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
2083 $status = array();
2084 while ( $row = $this->fetchObject( $res ) ) {
2085 $status[$row->Variable_name] = $row->Value;
2086 }
2087 return $status;
2088 }
2089
2090 /**
2091 * Return the maximum number of items allowed in a list, or 0 for unlimited.
2092 */
2093 function maxListLen() {
2094 return 0;
2095 }
2096
2097 function encodeBlob($b) {
2098 return $b;
2099 }
2100
2101 function decodeBlob($b) {
2102 return $b;
2103 }
2104
2105 /**
2106 * Override database's default connection timeout.
2107 * May be useful for very long batch queries such as
2108 * full-wiki dumps, where a single query reads out
2109 * over hours or days.
2110 * @param int $timeout in seconds
2111 */
2112 public function setTimeout( $timeout ) {
2113 $this->query( "SET net_read_timeout=$timeout" );
2114 $this->query( "SET net_write_timeout=$timeout" );
2115 }
2116
2117 /**
2118 * Read and execute SQL commands from a file.
2119 * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
2120 * @param string $filename File name to open
2121 * @param callback $lineCallback Optional function called before reading each line
2122 * @param callback $resultCallback Optional function called for each MySQL result
2123 */
2124 function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
2125 $fp = fopen( $filename, 'r' );
2126 if ( false === $fp ) {
2127 throw new MWException( "Could not open \"{$filename}\".\n" );
2128 }
2129 $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
2130 fclose( $fp );
2131 return $error;
2132 }
2133
2134 /**
2135 * Read and execute commands from an open file handle
2136 * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
2137 * @param string $fp File handle
2138 * @param callback $lineCallback Optional function called before reading each line
2139 * @param callback $resultCallback Optional function called for each MySQL result
2140 */
2141 function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
2142 $cmd = "";
2143 $done = false;
2144 $dollarquote = false;
2145
2146 while ( ! feof( $fp ) ) {
2147 if ( $lineCallback ) {
2148 call_user_func( $lineCallback );
2149 }
2150 $line = trim( fgets( $fp, 1024 ) );
2151 $sl = strlen( $line ) - 1;
2152
2153 if ( $sl < 0 ) { continue; }
2154 if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
2155
2156 ## Allow dollar quoting for function declarations
2157 if (substr($line,0,4) == '$mw$') {
2158 if ($dollarquote) {
2159 $dollarquote = false;
2160 $done = true;
2161 }
2162 else {
2163 $dollarquote = true;
2164 }
2165 }
2166 else if (!$dollarquote) {
2167 if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
2168 $done = true;
2169 $line = substr( $line, 0, $sl );
2170 }
2171 }
2172
2173 if ( '' != $cmd ) { $cmd .= ' '; }
2174 $cmd .= "$line\n";
2175
2176 if ( $done ) {
2177 $cmd = str_replace(';;', ";", $cmd);
2178 $cmd = $this->replaceVars( $cmd );
2179 $res = $this->query( $cmd, __METHOD__ );
2180 if ( $resultCallback ) {
2181 call_user_func( $resultCallback, $res );
2182 }
2183
2184 if ( false === $res ) {
2185 $err = $this->lastError();
2186 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
2187 }
2188
2189 $cmd = '';
2190 $done = false;
2191 }
2192 }
2193 return true;
2194 }
2195
2196
2197 /**
2198 * Replace variables in sourced SQL
2199 */
2200 protected function replaceVars( $ins ) {
2201 $varnames = array(
2202 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
2203 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
2204 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
2205 );
2206
2207 // Ordinary variables
2208 foreach ( $varnames as $var ) {
2209 if( isset( $GLOBALS[$var] ) ) {
2210 $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
2211 $ins = str_replace( '{$' . $var . '}', $val, $ins );
2212 $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
2213 $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
2214 }
2215 }
2216
2217 // Table prefixes
2218 $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-zA-Z_0-9]*)/',
2219 array( &$this, 'tableNameCallback' ), $ins );
2220 return $ins;
2221 }
2222
2223 /**
2224 * Table name callback
2225 * @private
2226 */
2227 protected function tableNameCallback( $matches ) {
2228 return $this->tableName( $matches[1] );
2229 }
2230
2231 /*
2232 * Build a concatenation list to feed into a SQL query
2233 */
2234 function buildConcat( $stringList ) {
2235 return 'CONCAT(' . implode( ',', $stringList ) . ')';
2236 }
2237
2238 /**
2239 * Acquire a lock
2240 *
2241 * Abstracted from Filestore::lock() so child classes can implement for
2242 * their own needs.
2243 *
2244 * @param string $lockName Name of lock to aquire
2245 * @param string $method Name of method calling us
2246 * @return bool
2247 */
2248 public function lock( $lockName, $method ) {
2249 $lockName = $this->addQuotes( $lockName );
2250 $result = $this->query( "SELECT GET_LOCK($lockName, 5) AS lockstatus", $method );
2251 $row = $this->fetchObject( $result );
2252 $this->freeResult( $result );
2253
2254 if( $row->lockstatus == 1 ) {
2255 return true;
2256 } else {
2257 wfDebug( __METHOD__." failed to acquire lock\n" );
2258 return false;
2259 }
2260 }
2261 /**
2262 * Release a lock.
2263 *
2264 * @todo fixme - Figure out a way to return a bool
2265 * based on successful lock release.
2266 *
2267 * @param string $lockName Name of lock to release
2268 * @param string $method Name of method calling us
2269 */
2270 public function unlock( $lockName, $method ) {
2271 $lockName = $this->addQuotes( $lockName );
2272 $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method );
2273 $this->freeResult( $result );
2274 }
2275 }
2276
2277 /**
2278 * Database abstraction object for mySQL
2279 * Inherit all methods and properties of Database::Database()
2280 *
2281 * @ingroup Database
2282 * @see Database
2283 */
2284 class DatabaseMysql extends Database {
2285 # Inherit all
2286 }
2287
2288 /******************************************************************************
2289 * Utility classes
2290 *****************************************************************************/
2291
2292 /**
2293 * Utility class.
2294 * @ingroup Database
2295 */
2296 class DBObject {
2297 public $mData;
2298
2299 function DBObject($data) {
2300 $this->mData = $data;
2301 }
2302
2303 function isLOB() {
2304 return false;
2305 }
2306
2307 function data() {
2308 return $this->mData;
2309 }
2310 }
2311
2312 /**
2313 * Utility class
2314 * @ingroup Database
2315 *
2316 * This allows us to distinguish a blob from a normal string and an array of strings
2317 */
2318 class Blob {
2319 private $mData;
2320 function __construct($data) {
2321 $this->mData = $data;
2322 }
2323 function fetch() {
2324 return $this->mData;
2325 }
2326 }
2327
2328 /**
2329 * Utility class.
2330 * @ingroup Database
2331 */
2332 class MySQLField {
2333 private $name, $tablename, $default, $max_length, $nullable,
2334 $is_pk, $is_unique, $is_multiple, $is_key, $type;
2335 function __construct ($info) {
2336 $this->name = $info->name;
2337 $this->tablename = $info->table;
2338 $this->default = $info->def;
2339 $this->max_length = $info->max_length;
2340 $this->nullable = !$info->not_null;
2341 $this->is_pk = $info->primary_key;
2342 $this->is_unique = $info->unique_key;
2343 $this->is_multiple = $info->multiple_key;
2344 $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
2345 $this->type = $info->type;
2346 }
2347
2348 function name() {
2349 return $this->name;
2350 }
2351
2352 function tableName() {
2353 return $this->tableName;
2354 }
2355
2356 function defaultValue() {
2357 return $this->default;
2358 }
2359
2360 function maxLength() {
2361 return $this->max_length;
2362 }
2363
2364 function nullable() {
2365 return $this->nullable;
2366 }
2367
2368 function isKey() {
2369 return $this->is_key;
2370 }
2371
2372 function isMultipleKey() {
2373 return $this->is_multiple;
2374 }
2375
2376 function type() {
2377 return $this->type;
2378 }
2379 }
2380
2381 /******************************************************************************
2382 * Error classes
2383 *****************************************************************************/
2384
2385 /**
2386 * Database error base class
2387 * @ingroup Database
2388 */
2389 class DBError extends MWException {
2390 public $db;
2391
2392 /**
2393 * Construct a database error
2394 * @param Database $db The database object which threw the error
2395 * @param string $error A simple error message to be used for debugging
2396 */
2397 function __construct( Database &$db, $error ) {
2398 $this->db =& $db;
2399 parent::__construct( $error );
2400 }
2401 }
2402
2403 /**
2404 * @ingroup Database
2405 */
2406 class DBConnectionError extends DBError {
2407 public $error;
2408
2409 function __construct( Database &$db, $error = 'unknown error' ) {
2410 $msg = 'DB connection error';
2411 if ( trim( $error ) != '' ) {
2412 $msg .= ": $error";
2413 }
2414 $this->error = $error;
2415 parent::__construct( $db, $msg );
2416 }
2417
2418 function useOutputPage() {
2419 // Not likely to work
2420 return false;
2421 }
2422
2423 function useMessageCache() {
2424 // Not likely to work
2425 return false;
2426 }
2427
2428 function getText() {
2429 return $this->getMessage() . "\n";
2430 }
2431
2432 function getLogMessage() {
2433 # Don't send to the exception log
2434 return false;
2435 }
2436
2437 function getPageTitle() {
2438 global $wgSitename;
2439 return "$wgSitename has a problem";
2440 }
2441
2442 function getHTML() {
2443 global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
2444 global $wgSitename, $wgServer, $wgMessageCache;
2445
2446 # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
2447 # Hard coding strings instead.
2448
2449 $noconnect = "<p><strong>Sorry! This site is experiencing technical difficulties.</strong></p><p>Try waiting a few minutes and reloading.</p><p><small>(Can't contact the database server: $1)</small></p>";
2450 $mainpage = 'Main Page';
2451 $searchdisabled = <<<EOT
2452 <p style="margin: 1.5em 2em 1em">$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
2453 <span style="font-size: 89%; display: block; margin-left: .2em">Note that their indexes of $wgSitename content may be out of date.</span></p>',
2454 EOT;
2455
2456 $googlesearch = "
2457 <!-- SiteSearch Google -->
2458 <FORM method=GET action=\"http://www.google.com/search\">
2459 <TABLE bgcolor=\"#FFFFFF\"><tr><td>
2460 <A HREF=\"http://www.google.com/\">
2461 <IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
2462 border=\"0\" ALT=\"Google\"></A>
2463 </td>
2464 <td>
2465 <INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
2466 <INPUT type=submit name=btnG VALUE=\"Google Search\">
2467 <font size=-1>
2468 <input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
2469 <input type='hidden' name='ie' value='$2'>
2470 <input type='hidden' name='oe' value='$2'>
2471 </font>
2472 </td></tr></TABLE>
2473 </FORM>
2474 <!-- SiteSearch Google -->";
2475 $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
2476
2477 # No database access
2478 if ( is_object( $wgMessageCache ) ) {
2479 $wgMessageCache->disable();
2480 }
2481
2482 if ( trim( $this->error ) == '' ) {
2483 $this->error = $this->db->getProperty('mServer');
2484 }
2485
2486 $text = str_replace( '$1', $this->error, $noconnect );
2487 $text .= wfGetSiteNotice();
2488
2489 if($wgUseFileCache) {
2490 if($wgTitle) {
2491 $t =& $wgTitle;
2492 } else {
2493 if($title) {
2494 $t = Title::newFromURL( $title );
2495 } elseif (@/**/$_REQUEST['search']) {
2496 $search = $_REQUEST['search'];
2497 return $searchdisabled .
2498 str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ),
2499 $wgInputEncoding ), $googlesearch );
2500 } else {
2501 $t = Title::newFromText( $mainpage );
2502 }
2503 }
2504
2505 $cache = new HTMLFileCache( $t );
2506 if( $cache->isFileCached() ) {
2507 // @todo, FIXME: $msg is not defined on the next line.
2508 $msg = '<p style="color: red"><b>'.$msg."<br />\n" .
2509 $cachederror . "</b></p>\n";
2510
2511 $tag = '<div id="article">';
2512 $text = str_replace(
2513 $tag,
2514 $tag . $msg,
2515 $cache->fetchPageText() );
2516 }
2517 }
2518
2519 return $text;
2520 }
2521 }
2522
2523 /**
2524 * @ingroup Database
2525 */
2526 class DBQueryError extends DBError {
2527 public $error, $errno, $sql, $fname;
2528
2529 function __construct( Database &$db, $error, $errno, $sql, $fname ) {
2530 $message = "A database error has occurred\n" .
2531 "Query: $sql\n" .
2532 "Function: $fname\n" .
2533 "Error: $errno $error\n";
2534
2535 parent::__construct( $db, $message );
2536 $this->error = $error;
2537 $this->errno = $errno;
2538 $this->sql = $sql;
2539 $this->fname = $fname;
2540 }
2541
2542 function getText() {
2543 if ( $this->useMessageCache() ) {
2544 return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
2545 htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
2546 } else {
2547 return $this->getMessage();
2548 }
2549 }
2550
2551 function getSQL() {
2552 global $wgShowSQLErrors;
2553 if( !$wgShowSQLErrors ) {
2554 return $this->msg( 'sqlhidden', 'SQL hidden' );
2555 } else {
2556 return $this->sql;
2557 }
2558 }
2559
2560 function getLogMessage() {
2561 # Don't send to the exception log
2562 return false;
2563 }
2564
2565 function getPageTitle() {
2566 return $this->msg( 'databaseerror', 'Database error' );
2567 }
2568
2569 function getHTML() {
2570 if ( $this->useMessageCache() ) {
2571 return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
2572 htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
2573 } else {
2574 return nl2br( htmlspecialchars( $this->getMessage() ) );
2575 }
2576 }
2577 }
2578
2579 /**
2580 * @ingroup Database
2581 */
2582 class DBUnexpectedError extends DBError {}
2583
2584
2585 /**
2586 * Result wrapper for grabbing data queried by someone else
2587 * @ingroup Database
2588 */
2589 class ResultWrapper implements Iterator {
2590 var $db, $result, $pos = 0, $currentRow = null;
2591
2592 /**
2593 * Create a new result object from a result resource and a Database object
2594 */
2595 function ResultWrapper( $database, $result ) {
2596 $this->db = $database;
2597 if ( $result instanceof ResultWrapper ) {
2598 $this->result = $result->result;
2599 } else {
2600 $this->result = $result;
2601 }
2602 }
2603
2604 /**
2605 * Get the number of rows in a result object
2606 */
2607 function numRows() {
2608 return $this->db->numRows( $this->result );
2609 }
2610
2611 /**
2612 * Fetch the next row from the given result object, in object form.
2613 * Fields can be retrieved with $row->fieldname, with fields acting like
2614 * member variables.
2615 *
2616 * @param $res SQL result object as returned from Database::query(), etc.
2617 * @return MySQL row object
2618 * @throws DBUnexpectedError Thrown if the database returns an error
2619 */
2620 function fetchObject() {
2621 return $this->db->fetchObject( $this->result );
2622 }
2623
2624 /**
2625 * Fetch the next row from the given result object, in associative array
2626 * form. Fields are retrieved with $row['fieldname'].
2627 *
2628 * @param $res SQL result object as returned from Database::query(), etc.
2629 * @return MySQL row object
2630 * @throws DBUnexpectedError Thrown if the database returns an error
2631 */
2632 function fetchRow() {
2633 return $this->db->fetchRow( $this->result );
2634 }
2635
2636 /**
2637 * Free a result object
2638 */
2639 function free() {
2640 $this->db->freeResult( $this->result );
2641 unset( $this->result );
2642 unset( $this->db );
2643 }
2644
2645 /**
2646 * Change the position of the cursor in a result object
2647 * See mysql_data_seek()
2648 */
2649 function seek( $row ) {
2650 $this->db->dataSeek( $this->result, $row );
2651 }
2652
2653 /*********************
2654 * Iterator functions
2655 * Note that using these in combination with the non-iterator functions
2656 * above may cause rows to be skipped or repeated.
2657 */
2658
2659 function rewind() {
2660 if ($this->numRows()) {
2661 $this->db->dataSeek($this->result, 0);
2662 }
2663 $this->pos = 0;
2664 $this->currentRow = null;
2665 }
2666
2667 function current() {
2668 if ( is_null( $this->currentRow ) ) {
2669 $this->next();
2670 }
2671 return $this->currentRow;
2672 }
2673
2674 function key() {
2675 return $this->pos;
2676 }
2677
2678 function next() {
2679 $this->pos++;
2680 $this->currentRow = $this->fetchObject();
2681 return $this->currentRow;
2682 }
2683
2684 function valid() {
2685 return $this->current() !== false;
2686 }
2687 }
2688
2689 class MySQLMasterPos {
2690 var $file, $pos;
2691
2692 function __construct( $file, $pos ) {
2693 $this->file = $file;
2694 $this->pos = $pos;
2695 }
2696
2697 function __toString() {
2698 return "{$this->file}/{$this->pos}";
2699 }
2700 }