00001 <?php
00012 define( 'DEADLOCK_TRIES', 4 );
00014 define( 'DEADLOCK_DELAY_MIN', 500000 );
00016 define( 'DEADLOCK_DELAY_MAX', 1500000 );
00017
00022 class Database {
00023
00024 #------------------------------------------------------------------------------
00025 # Variables
00026 #------------------------------------------------------------------------------
00027
00028 protected $mLastQuery = '';
00029 protected $mDoneWrites = false;
00030 protected $mPHPError = false;
00031
00032 protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
00033 protected $mOpened = false;
00034
00035 protected $mFailFunction;
00036 protected $mTablePrefix;
00037 protected $mFlags;
00038 protected $mTrxLevel = 0;
00039 protected $mErrorCount = 0;
00040 protected $mLBInfo = array();
00041 protected $mFakeSlaveLag = null, $mFakeMaster = false;
00042
00043 #------------------------------------------------------------------------------
00044 # Accessors
00045 #------------------------------------------------------------------------------
00046 # These optionally set a variable and return the previous state
00047
00052 function failFunction( $function = NULL ) {
00053 return wfSetVar( $this->mFailFunction, $function );
00054 }
00055
00060 function setOutputPage( $out ) {
00061 wfDeprecated( __METHOD__ );
00062 }
00063
00067 function debug( $debug = NULL ) {
00068 return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
00069 }
00070
00075 function bufferResults( $buffer = NULL ) {
00076 if ( is_null( $buffer ) ) {
00077 return !(bool)( $this->mFlags & DBO_NOBUFFER );
00078 } else {
00079 return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
00080 }
00081 }
00082
00090 function ignoreErrors( $ignoreErrors = NULL ) {
00091 return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
00092 }
00093
00098 function trxLevel( $level = NULL ) {
00099 return wfSetVar( $this->mTrxLevel, $level );
00100 }
00101
00105 function errorCount( $count = NULL ) {
00106 return wfSetVar( $this->mErrorCount, $count );
00107 }
00108
00109 function tablePrefix( $prefix = null ) {
00110 return wfSetVar( $this->mTablePrefix, $prefix );
00111 }
00112
00116 function getLBInfo( $name = NULL ) {
00117 if ( is_null( $name ) ) {
00118 return $this->mLBInfo;
00119 } else {
00120 if ( array_key_exists( $name, $this->mLBInfo ) ) {
00121 return $this->mLBInfo[$name];
00122 } else {
00123 return NULL;
00124 }
00125 }
00126 }
00127
00128 function setLBInfo( $name, $value = NULL ) {
00129 if ( is_null( $value ) ) {
00130 $this->mLBInfo = $name;
00131 } else {
00132 $this->mLBInfo[$name] = $value;
00133 }
00134 }
00135
00139 function setFakeSlaveLag( $lag ) {
00140 $this->mFakeSlaveLag = $lag;
00141 }
00142
00146 function setFakeMaster( $enabled = true ) {
00147 $this->mFakeMaster = $enabled;
00148 }
00149
00153 function cascadingDeletes() {
00154 return false;
00155 }
00156
00160 function cleanupTriggers() {
00161 return false;
00162 }
00163
00168 function strictIPs() {
00169 return false;
00170 }
00171
00175 function realTimestamps() {
00176 return false;
00177 }
00178
00182 function implicitGroupby() {
00183 return true;
00184 }
00185
00190 function implicitOrderby() {
00191 return true;
00192 }
00193
00198 function searchableIPs() {
00199 return false;
00200 }
00201
00205 function functionalIndexes() {
00206 return false;
00207 }
00208
00213 function lastQuery() { return $this->mLastQuery; }
00214
00215
00220 function doneWrites() { return $this->mDoneWrites; }
00221
00226 function isOpen() { return $this->mOpened; }
00227
00228 function setFlag( $flag ) {
00229 $this->mFlags |= $flag;
00230 }
00231
00232 function clearFlag( $flag ) {
00233 $this->mFlags &= ~$flag;
00234 }
00235
00236 function getFlag( $flag ) {
00237 return !!($this->mFlags & $flag);
00238 }
00239
00243 function getProperty( $name ) {
00244 return $this->$name;
00245 }
00246
00247 function getWikiID() {
00248 if( $this->mTablePrefix ) {
00249 return "{$this->mDBname}-{$this->mTablePrefix}";
00250 } else {
00251 return $this->mDBname;
00252 }
00253 }
00254
00255 #------------------------------------------------------------------------------
00256 # Other functions
00257 #------------------------------------------------------------------------------
00258
00269 function __construct( $server = false, $user = false, $password = false, $dbName = false,
00270 $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
00271
00272 global $wgOut, $wgDBprefix, $wgCommandLineMode;
00273 # Can't get a reference if it hasn't been set yet
00274 if ( !isset( $wgOut ) ) {
00275 $wgOut = NULL;
00276 }
00277
00278 $this->mFailFunction = $failFunction;
00279 $this->mFlags = $flags;
00280
00281 if ( $this->mFlags & DBO_DEFAULT ) {
00282 if ( $wgCommandLineMode ) {
00283 $this->mFlags &= ~DBO_TRX;
00284 } else {
00285 $this->mFlags |= DBO_TRX;
00286 }
00287 }
00288
00289
00290
00291
00292
00293
00294
00295
00297 if ( $tablePrefix == 'get from global' ) {
00298 $this->mTablePrefix = $wgDBprefix;
00299 } else {
00300 $this->mTablePrefix = $tablePrefix;
00301 }
00302
00303 if ( $server ) {
00304 $this->open( $server, $user, $password, $dbName );
00305 }
00306 }
00307
00317 static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
00318 {
00319 return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
00320 }
00321
00330 function open( $server, $user, $password, $dbName ) {
00331 global $wgAllDBsAreLocalhost;
00332 wfProfileIn( __METHOD__ );
00333
00334 # Test for missing mysql.so
00335 # First try to load it
00336 if (!@extension_loaded('mysql')) {
00337 @dl('mysql.so');
00338 }
00339
00340 # Fail now
00341 # Otherwise we get a suppressed fatal error, which is very hard to track down
00342 if ( !function_exists( 'mysql_connect' ) ) {
00343 throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
00344 }
00345
00346 # Debugging hack -- fake cluster
00347 if ( $wgAllDBsAreLocalhost ) {
00348 $realServer = 'localhost';
00349 } else {
00350 $realServer = $server;
00351 }
00352 $this->close();
00353 $this->mServer = $server;
00354 $this->mUser = $user;
00355 $this->mPassword = $password;
00356 $this->mDBname = $dbName;
00357
00358 $success = false;
00359
00360 wfProfileIn("dbconnect-$server");
00361
00362 # The kernel's default SYN retransmission period is far too slow for us,
00363 # so we use a short timeout plus a manual retry. Retrying means that a small
00364 # but finite rate of SYN packet loss won't cause user-visible errors.
00365 $this->mConn = false;
00366 if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
00367 $numAttempts = 2;
00368 } else {
00369 $numAttempts = 1;
00370 }
00371 $this->installErrorHandler();
00372 for ( $i = 0; $i < $numAttempts && !$this->mConn; $i++ ) {
00373 if ( $i > 1 ) {
00374 usleep( 1000 );
00375 }
00376 if ( $this->mFlags & DBO_PERSISTENT ) {
00377 $this->mConn = mysql_pconnect( $realServer, $user, $password );
00378 } else {
00379 # Create a new connection...
00380 $this->mConn = mysql_connect( $realServer, $user, $password, true );
00381 }
00382 if ($this->mConn === false) {
00383 #$iplus = $i + 1;
00384 #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
00385 }
00386 }
00387 $phpError = $this->restoreErrorHandler();
00388 # Always log connection errors
00389 if ( !$this->mConn ) {
00390 $error = $this->lastError();
00391 if ( !$error ) {
00392 $error = $phpError;
00393 }
00394 wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
00395 wfDebug( "DB connection error\n" );
00396 wfDebug( "Server: $server, User: $user, Password: " .
00397 substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
00398 $success = false;
00399 }
00400
00401 wfProfileOut("dbconnect-$server");
00402
00403 if ( $dbName != '' && $this->mConn !== false ) {
00404 $success = @mysql_select_db( $dbName, $this->mConn );
00405 if ( !$success ) {
00406 $error = "Error selecting database $dbName on server {$this->mServer} " .
00407 "from client host " . wfHostname() . "\n";
00408 wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
00409 wfDebug( $error );
00410 }
00411 } else {
00412 # Delay USE query
00413 $success = (bool)$this->mConn;
00414 }
00415
00416 if ( $success ) {
00417 $version = $this->getServerVersion();
00418 if ( version_compare( $version, '4.1' ) >= 0 ) {
00419
00420
00421 global $wgDBmysql5;
00422 if( $wgDBmysql5 ) {
00423 $this->query( 'SET NAMES utf8', __METHOD__ );
00424 }
00425
00426 $this->query( "SET sql_mode = ''", __METHOD__ );
00427 }
00428
00429
00430 } else {
00431 $this->reportConnectionError( $phpError );
00432 }
00433
00434 $this->mOpened = $success;
00435 wfProfileOut( __METHOD__ );
00436 return $success;
00437 }
00438
00439 protected function installErrorHandler() {
00440 $this->mPHPError = false;
00441 $this->htmlErrors = ini_set( 'html_errors', '0' );
00442 set_error_handler( array( $this, 'connectionErrorHandler' ) );
00443 }
00444
00445 protected function restoreErrorHandler() {
00446 restore_error_handler();
00447 if ( $this->htmlErrors !== false ) {
00448 ini_set( 'html_errors', $this->htmlErrors );
00449 }
00450 if ( $this->mPHPError ) {
00451 $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
00452 $error = preg_replace( '!^.*?:(.*)$!', '$1', $error );
00453 return $error;
00454 } else {
00455 return false;
00456 }
00457 }
00458
00459 protected function connectionErrorHandler( $errno, $errstr ) {
00460 $this->mPHPError = $errstr;
00461 }
00462
00469 function close()
00470 {
00471 $this->mOpened = false;
00472 if ( $this->mConn ) {
00473 if ( $this->trxLevel() ) {
00474 $this->immediateCommit();
00475 }
00476 return mysql_close( $this->mConn );
00477 } else {
00478 return true;
00479 }
00480 }
00481
00485 function reportConnectionError( $error = 'Unknown error' ) {
00486 $myError = $this->lastError();
00487 if ( $myError ) {
00488 $error = $myError;
00489 }
00490
00491 if ( $this->mFailFunction ) {
00492 # Legacy error handling method
00493 if ( !is_int( $this->mFailFunction ) ) {
00494 $ff = $this->mFailFunction;
00495 $ff( $this, $error );
00496 }
00497 } else {
00498 # New method
00499 throw new DBConnectionError( $this, $error );
00500 }
00501 }
00502
00507 function isWriteQuery( $sql ) {
00508 return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW)\b/i', $sql );
00509 }
00510
00523 public function query( $sql, $fname = '', $tempIgnore = false ) {
00524 global $wgProfiler;
00525
00526 $isMaster = !is_null( $this->getLBInfo( 'master' ) );
00527 if ( isset( $wgProfiler ) ) {
00528 # generalizeSQL will probably cut down the query to reasonable
00529 # logging size most of the time. The substr is really just a sanity check.
00530
00531 # Who's been wasting my precious column space? -- TS
00532 #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
00533
00534 if ( $isMaster ) {
00535 $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
00536 $totalProf = 'Database::query-master';
00537 } else {
00538 $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
00539 $totalProf = 'Database::query';
00540 }
00541 wfProfileIn( $totalProf );
00542 wfProfileIn( $queryProf );
00543 }
00544
00545 $this->mLastQuery = $sql;
00546 if ( !$this->mDoneWrites && $this->isWriteQuery( $sql ) ) {
00547
00548 wfDebug( __METHOD__.": Writes done: $sql\n" );
00549 $this->mDoneWrites = true;
00550 }
00551
00552 # Add a comment for easy SHOW PROCESSLIST interpretation
00553 #if ( $fname ) {
00554 global $wgUser;
00555 if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
00556 $userName = $wgUser->getName();
00557 if ( mb_strlen( $userName ) > 15 ) {
00558 $userName = mb_substr( $userName, 0, 15 ) . '...';
00559 }
00560 $userName = str_replace( '/', '', $userName );
00561 } else {
00562 $userName = '';
00563 }
00564 $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
00565 #} else {
00566 # $commentedSql = $sql;
00567 #}
00568
00569 # If DBO_TRX is set, start a transaction
00570 if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
00571 $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
00572
00573
00574
00575 $sqlstart = substr($sql,0,10);
00576 if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0)
00577 $this->begin();
00578 }
00579
00580 if ( $this->debug() ) {
00581 $sqlx = substr( $commentedSql, 0, 500 );
00582 $sqlx = strtr( $sqlx, "\t\n", ' ' );
00583 if ( $isMaster ) {
00584 wfDebug( "SQL-master: $sqlx\n" );
00585 } else {
00586 wfDebug( "SQL: $sqlx\n" );
00587 }
00588 }
00589
00590 if ( istainted( $sql ) & TC_MYSQL ) {
00591 throw new MWException( 'Tainted query found' );
00592 }
00593
00594 # Do the query and handle errors
00595 $ret = $this->doQuery( $commentedSql );
00596
00597 # Try reconnecting if the connection was lost
00598 if ( false === $ret && $this->wasErrorReissuable() ) {
00599 # Transaction is gone, like it or not
00600 $this->mTrxLevel = 0;
00601 wfDebug( "Connection lost, reconnecting...\n" );
00602 if ( $this->ping() ) {
00603 wfDebug( "Reconnected\n" );
00604 $sqlx = substr( $commentedSql, 0, 500 );
00605 $sqlx = strtr( $sqlx, "\t\n", ' ' );
00606 global $wgRequestTime;
00607 $elapsed = round( microtime(true) - $wgRequestTime, 3 );
00608 wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
00609 $ret = $this->doQuery( $commentedSql );
00610 } else {
00611 wfDebug( "Failed\n" );
00612 }
00613 }
00614
00615 if ( false === $ret ) {
00616 $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
00617 }
00618
00619 if ( isset( $wgProfiler ) ) {
00620 wfProfileOut( $queryProf );
00621 wfProfileOut( $totalProf );
00622 }
00623 return $this->resultObject( $ret );
00624 }
00625
00632 function doQuery( $sql ) {
00633 if( $this->bufferResults() ) {
00634 $ret = mysql_query( $sql, $this->mConn );
00635 } else {
00636 $ret = mysql_unbuffered_query( $sql, $this->mConn );
00637 }
00638 return $ret;
00639 }
00640
00648 function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
00649 global $wgCommandLineMode;
00650 # Ignore errors during error handling to avoid infinite recursion
00651 $ignore = $this->ignoreErrors( true );
00652 ++$this->mErrorCount;
00653
00654 if( $ignore || $tempIgnore ) {
00655 wfDebug("SQL ERROR (ignored): $error\n");
00656 $this->ignoreErrors( $ignore );
00657 } else {
00658 $sql1line = str_replace( "\n", "\\n", $sql );
00659 wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
00660 wfDebug("SQL ERROR: " . $error . "\n");
00661 throw new DBQueryError( $this, $error, $errno, $sql, $fname );
00662 }
00663 }
00664
00665
00675 function prepare( $sql, $func = 'Database::prepare' ) {
00676
00677
00678
00679 return array( 'query' => $sql, 'func' => $func );
00680 }
00681
00682 function freePrepared( $prepared ) {
00683
00684 }
00685
00691 function execute( $prepared, $args = null ) {
00692 if( !is_array( $args ) ) {
00693 # Pull the var args
00694 $args = func_get_args();
00695 array_shift( $args );
00696 }
00697 $sql = $this->fillPrepared( $prepared['query'], $args );
00698 return $this->query( $sql, $prepared['func'] );
00699 }
00700
00707 function safeQuery( $query, $args = null ) {
00708 $prepared = $this->prepare( $query, 'Database::safeQuery' );
00709 if( !is_array( $args ) ) {
00710 # Pull the var args
00711 $args = func_get_args();
00712 array_shift( $args );
00713 }
00714 $retval = $this->execute( $prepared, $args );
00715 $this->freePrepared( $prepared );
00716 return $retval;
00717 }
00718
00726 function fillPrepared( $preparedQuery, $args ) {
00727 reset( $args );
00728 $this->preparedArgs =& $args;
00729 return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
00730 array( &$this, 'fillPreparedArg' ), $preparedQuery );
00731 }
00732
00742 function fillPreparedArg( $matches ) {
00743 switch( $matches[1] ) {
00744 case '\\?': return '?';
00745 case '\\!': return '!';
00746 case '\\&': return '&';
00747 }
00748 list( , $arg ) = each( $this->preparedArgs );
00749 switch( $matches[1] ) {
00750 case '?': return $this->addQuotes( $arg );
00751 case '!': return $arg;
00752 case '&':
00753 # return $this->addQuotes( file_get_contents( $arg ) );
00754 throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
00755 default:
00756 throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
00757 }
00758 }
00759
00764 function freeResult( $res ) {
00765 if ( $res instanceof ResultWrapper ) {
00766 $res = $res->result;
00767 }
00768 if ( !@mysql_free_result( $res ) ) {
00769 throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
00770 }
00771 }
00772
00782 function fetchObject( $res ) {
00783 if ( $res instanceof ResultWrapper ) {
00784 $res = $res->result;
00785 }
00786 @$row = mysql_fetch_object( $res );
00787 if( $this->lastErrno() ) {
00788 throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
00789 }
00790 return $row;
00791 }
00792
00801 function fetchRow( $res ) {
00802 if ( $res instanceof ResultWrapper ) {
00803 $res = $res->result;
00804 }
00805 @$row = mysql_fetch_array( $res );
00806 if ( $this->lastErrno() ) {
00807 throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
00808 }
00809 return $row;
00810 }
00811
00816 function numRows( $res ) {
00817 if ( $res instanceof ResultWrapper ) {
00818 $res = $res->result;
00819 }
00820 @$n = mysql_num_rows( $res );
00821 if( $this->lastErrno() ) {
00822 throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
00823 }
00824 return $n;
00825 }
00826
00832 function numFields( $res ) {
00833 if ( $res instanceof ResultWrapper ) {
00834 $res = $res->result;
00835 }
00836 return mysql_num_fields( $res );
00837 }
00838
00846 function fieldName( $res, $n ) {
00847 if ( $res instanceof ResultWrapper ) {
00848 $res = $res->result;
00849 }
00850 return mysql_field_name( $res, $n );
00851 }
00852
00863 function insertId() { return mysql_insert_id( $this->mConn ); }
00864
00871 function dataSeek( $res, $row ) {
00872 if ( $res instanceof ResultWrapper ) {
00873 $res = $res->result;
00874 }
00875 return mysql_data_seek( $res, $row );
00876 }
00877
00882 function lastErrno() {
00883 if ( $this->mConn ) {
00884 return mysql_errno( $this->mConn );
00885 } else {
00886 return mysql_errno();
00887 }
00888 }
00889
00894 function lastError() {
00895 if ( $this->mConn ) {
00896 # Even if it's non-zero, it can still be invalid
00897 wfSuppressWarnings();
00898 $error = mysql_error( $this->mConn );
00899 if ( !$error ) {
00900 $error = mysql_error();
00901 }
00902 wfRestoreWarnings();
00903 } else {
00904 $error = mysql_error();
00905 }
00906 if( $error ) {
00907 $error .= ' (' . $this->mServer . ')';
00908 }
00909 return $error;
00910 }
00915 function affectedRows() { return mysql_affected_rows( $this->mConn ); }
00916
00925 function set( $table, $var, $value, $cond, $fname = 'Database::set' ) {
00926 $table = $this->tableName( $table );
00927 $sql = "UPDATE $table SET $var = '" .
00928 $this->strencode( $value ) . "' WHERE ($cond)";
00929 return (bool)$this->query( $sql, $fname );
00930 }
00931
00937 function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
00938 if ( !is_array( $options ) ) {
00939 $options = array( $options );
00940 }
00941 $options['LIMIT'] = 1;
00942
00943 $res = $this->select( $table, $var, $cond, $fname, $options );
00944 if ( $res === false || !$this->numRows( $res ) ) {
00945 return false;
00946 }
00947 $row = $this->fetchRow( $res );
00948 if ( $row !== false ) {
00949 $this->freeResult( $res );
00950 return reset( $row );
00951 } else {
00952 return false;
00953 }
00954 }
00955
00966 function makeSelectOptions( $options ) {
00967 $preLimitTail = $postLimitTail = '';
00968 $startOpts = '';
00969
00970 $noKeyOptions = array();
00971 foreach ( $options as $key => $option ) {
00972 if ( is_numeric( $key ) ) {
00973 $noKeyOptions[$option] = true;
00974 }
00975 }
00976
00977 if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
00978 if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
00979 if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
00980
00981
00982
00983
00984
00985
00986
00987 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
00988 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
00989 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
00990
00991 # Various MySQL extensions
00992 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
00993 if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
00994 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
00995 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
00996 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
00997 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
00998 if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
00999 if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
01000
01001 if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
01002 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
01003 } else {
01004 $useIndex = '';
01005 }
01006
01007 return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
01008 }
01009
01023 function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
01024 {
01025 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
01026 return $this->query( $sql, $fname );
01027 }
01028
01042 function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
01043 if( is_array( $vars ) ) {
01044 $vars = implode( ',', $vars );
01045 }
01046 if( !is_array( $options ) ) {
01047 $options = array( $options );
01048 }
01049 if( is_array( $table ) ) {
01050 if ( !empty($join_conds) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) )
01051 $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds );
01052 else
01053 $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
01054 } elseif ($table!='') {
01055 if ($table{0}==' ') {
01056 $from = ' FROM ' . $table;
01057 } else {
01058 $from = ' FROM ' . $this->tableName( $table );
01059 }
01060 } else {
01061 $from = '';
01062 }
01063
01064 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
01065
01066 if( !empty( $conds ) ) {
01067 if ( is_array( $conds ) ) {
01068 $conds = $this->makeList( $conds, LIST_AND );
01069 }
01070 $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
01071 } else {
01072 $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
01073 }
01074
01075 if (isset($options['LIMIT']))
01076 $sql = $this->limitResult($sql, $options['LIMIT'],
01077 isset($options['OFFSET']) ? $options['OFFSET'] : false);
01078 $sql = "$sql $postLimitTail";
01079
01080 if (isset($options['EXPLAIN'])) {
01081 $sql = 'EXPLAIN ' . $sql;
01082 }
01083 return $sql;
01084 }
01085
01104 function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
01105 $options['LIMIT'] = 1;
01106 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
01107 if ( $res === false )
01108 return false;
01109 if ( !$this->numRows($res) ) {
01110 $this->freeResult($res);
01111 return false;
01112 }
01113 $obj = $this->fetchObject( $res );
01114 $this->freeResult( $res );
01115 return $obj;
01116
01117 }
01118
01125 function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
01126 $options['EXPLAIN']=true;
01127 $res = $this->select ($table, $vars, $conds, $fname, $options );
01128 if ( $res === false )
01129 return false;
01130 if (!$this->numRows($res)) {
01131 $this->freeResult($res);
01132 return 0;
01133 }
01134
01135 $rows=1;
01136
01137 while( $plan = $this->fetchObject( $res ) ) {
01138 $rows *= ($plan->rows > 0)?$plan->rows:1;
01139 }
01140
01141 $this->freeResult($res);
01142 return $rows;
01143 }
01144
01145
01152 static function generalizeSQL( $sql ) {
01153 # This does the same as the regexp below would do, but in such a way
01154 # as to avoid crashing php on some large strings.
01155 # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
01156
01157 $sql = str_replace ( "\\\\", '', $sql);
01158 $sql = str_replace ( "\\'", '', $sql);
01159 $sql = str_replace ( "\\\"", '', $sql);
01160 $sql = preg_replace ("/'.*'/s", "'X'", $sql);
01161 $sql = preg_replace ('/".*"/s', "'X'", $sql);
01162
01163 # All newlines, tabs, etc replaced by single space
01164 $sql = preg_replace ( '/\s+/', ' ', $sql);
01165
01166 # All numbers => N
01167 $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
01168
01169 return $sql;
01170 }
01171
01177 function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
01178 $table = $this->tableName( $table );
01179 $res = $this->query( 'DESCRIBE '.$table, $fname );
01180 if ( !$res ) {
01181 return NULL;
01182 }
01183
01184 $found = false;
01185
01186 while ( $row = $this->fetchObject( $res ) ) {
01187 if ( $row->Field == $field ) {
01188 $found = true;
01189 break;
01190 }
01191 }
01192 return $found;
01193 }
01194
01200 function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
01201 $info = $this->indexInfo( $table, $index, $fname );
01202 if ( is_null( $info ) ) {
01203 return NULL;
01204 } else {
01205 return $info !== false;
01206 }
01207 }
01208
01209
01214 function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
01215 # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
01216 # SHOW INDEX should work for 3.x and up:
01217 # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
01218 $table = $this->tableName( $table );
01219 $index = $this->indexName( $index );
01220 $sql = 'SHOW INDEX FROM '.$table;
01221 $res = $this->query( $sql, $fname );
01222 if ( !$res ) {
01223 return NULL;
01224 }
01225
01226 $result = array();
01227 while ( $row = $this->fetchObject( $res ) ) {
01228 if ( $row->Key_name == $index ) {
01229 $result[] = $row;
01230 }
01231 }
01232 $this->freeResult($res);
01233
01234 return empty($result) ? false : $result;
01235 }
01236
01240 function tableExists( $table ) {
01241 $table = $this->tableName( $table );
01242 $old = $this->ignoreErrors( true );
01243 $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
01244 $this->ignoreErrors( $old );
01245 if( $res ) {
01246 $this->freeResult( $res );
01247 return true;
01248 } else {
01249 return false;
01250 }
01251 }
01252
01260 function fieldInfo( $table, $field ) {
01261 $table = $this->tableName( $table );
01262 $res = $this->query( "SELECT * FROM $table LIMIT 1" );
01263 $n = mysql_num_fields( $res->result );
01264 for( $i = 0; $i < $n; $i++ ) {
01265 $meta = mysql_fetch_field( $res->result, $i );
01266 if( $field == $meta->name ) {
01267 return new MySQLField($meta);
01268 }
01269 }
01270 return false;
01271 }
01272
01276 function fieldType( $res, $index ) {
01277 if ( $res instanceof ResultWrapper ) {
01278 $res = $res->result;
01279 }
01280 return mysql_field_type( $res, $index );
01281 }
01282
01286 function indexUnique( $table, $index ) {
01287 $indexInfo = $this->indexInfo( $table, $index );
01288 if ( !$indexInfo ) {
01289 return NULL;
01290 }
01291 return !$indexInfo[0]->Non_unique;
01292 }
01293
01303 function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
01304 # No rows to insert, easy just return now
01305 if ( !count( $a ) ) {
01306 return true;
01307 }
01308
01309 $table = $this->tableName( $table );
01310 if ( !is_array( $options ) ) {
01311 $options = array( $options );
01312 }
01313 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
01314 $multi = true;
01315 $keys = array_keys( $a[0] );
01316 } else {
01317 $multi = false;
01318 $keys = array_keys( $a );
01319 }
01320
01321 $sql = 'INSERT ' . implode( ' ', $options ) .
01322 " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
01323
01324 if ( $multi ) {
01325 $first = true;
01326 foreach ( $a as $row ) {
01327 if ( $first ) {
01328 $first = false;
01329 } else {
01330 $sql .= ',';
01331 }
01332 $sql .= '(' . $this->makeList( $row ) . ')';
01333 }
01334 } else {
01335 $sql .= '(' . $this->makeList( $a ) . ')';
01336 }
01337 return (bool)$this->query( $sql, $fname );
01338 }
01339
01347 function makeUpdateOptions( $options ) {
01348 if( !is_array( $options ) ) {
01349 $options = array( $options );
01350 }
01351 $opts = array();
01352 if ( in_array( 'LOW_PRIORITY', $options ) )
01353 $opts[] = $this->lowPriorityOption();
01354 if ( in_array( 'IGNORE', $options ) )
01355 $opts[] = 'IGNORE';
01356 return implode(' ', $opts);
01357 }
01358
01371 function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
01372 $table = $this->tableName( $table );
01373 $opts = $this->makeUpdateOptions( $options );
01374 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
01375 if ( $conds != '*' ) {
01376 $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
01377 }
01378 return $this->query( $sql, $fname );
01379 }
01380
01390 function makeList( $a, $mode = LIST_COMMA ) {
01391 if ( !is_array( $a ) ) {
01392 throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
01393 }
01394
01395 $first = true;
01396 $list = '';
01397 foreach ( $a as $field => $value ) {
01398 if ( !$first ) {
01399 if ( $mode == LIST_AND ) {
01400 $list .= ' AND ';
01401 } elseif($mode == LIST_OR) {
01402 $list .= ' OR ';
01403 } else {
01404 $list .= ',';
01405 }
01406 } else {
01407 $first = false;
01408 }
01409 if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
01410 $list .= "($value)";
01411 } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
01412 $list .= "$value";
01413 } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
01414 if( count( $value ) == 0 ) {
01415 throw new MWException( __METHOD__.': empty input' );
01416 } elseif( count( $value ) == 1 ) {
01417
01418
01419
01420 $value = array_values( $value );
01421 $list .= $field." = ".$this->addQuotes( $value[0] );
01422 } else {
01423 $list .= $field." IN (".$this->makeList($value).") ";
01424 }
01425 } elseif( $value === null ) {
01426 if ( $mode == LIST_AND || $mode == LIST_OR ) {
01427 $list .= "$field IS ";
01428 } elseif ( $mode == LIST_SET ) {
01429 $list .= "$field = ";
01430 }
01431 $list .= 'NULL';
01432 } else {
01433 if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
01434 $list .= "$field = ";
01435 }
01436 $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
01437 }
01438 }
01439 return $list;
01440 }
01441
01445 function selectDB( $db ) {
01446 $this->mDBname = $db;
01447 return mysql_select_db( $db, $this->mConn );
01448 }
01449
01453 function getDBname() {
01454 return $this->mDBname;
01455 }
01456
01460 function getServer() {
01461 return $this->mServer;
01462 }
01463
01477 function tableName( $name ) {
01478 global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
01479 # Skip the entire process when we have a string quoted on both ends.
01480 # Note that we check the end so that we will still quote any use of
01481 # use of `database`.table. But won't break things if someone wants
01482 # to query a database table with a dot in the name.
01483 if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name;
01484
01485 # Lets test for any bits of text that should never show up in a table
01486 # name. Basically anything like JOIN or ON which are actually part of
01487 # SQL queries, but may end up inside of the table value to combine
01488 # sql. Such as how the API is doing.
01489 # Note that we use a whitespace test rather than a \b test to avoid
01490 # any remote case where a word like on may be inside of a table name
01491 # surrounded by symbols which may be considered word breaks.
01492 if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name;
01493
01494 # Split database and table into proper variables.
01495 # We reverse the explode so that database.table and table both output
01496 # the correct table.
01497 $dbDetails = array_reverse( explode( '.', $name, 2 ) );
01498 if( isset( $dbDetails[1] ) ) @list( $table, $database ) = $dbDetails;
01499 else @list( $table ) = $dbDetails;
01500 $prefix = $this->mTablePrefix; # Default prefix
01501
01502 # A database name has been specified in input. Quote the table name
01503 # because we don't want any prefixes added.
01504 if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" );
01505
01506 # Note that we use the long format because php will complain in in_array if
01507 # the input is not an array, and will complain in is_array if it is not set.
01508 if( !isset( $database ) # Don't use shared database if pre selected.
01509 && isset( $wgSharedDB ) # We have a shared database
01510 && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`'
01511 && isset( $wgSharedTables )
01512 && is_array( $wgSharedTables )
01513 && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
01514 $database = $wgSharedDB;
01515 $prefix = isset( $wgSharedPrefix ) ? $wgSharedPrefix : $prefix;
01516 }
01517
01518 # Quote the $database and $table and apply the prefix if not quoted.
01519 if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" );
01520 $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" );
01521
01522 # Merge our database and table into our final table name.
01523 $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" );
01524
01525 # We're finished, return.
01526 return $tableName;
01527 }
01528
01538 public function tableNames() {
01539 $inArray = func_get_args();
01540 $retVal = array();
01541 foreach ( $inArray as $name ) {
01542 $retVal[$name] = $this->tableName( $name );
01543 }
01544 return $retVal;
01545 }
01546
01556 public function tableNamesN() {
01557 $inArray = func_get_args();
01558 $retVal = array();
01559 foreach ( $inArray as $name ) {
01560 $retVal[] = $this->tableName( $name );
01561 }
01562 return $retVal;
01563 }
01564
01568 function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
01569 $ret = array();
01570 $retJOIN = array();
01571 $use_index_safe = is_array($use_index) ? $use_index : array();
01572 $join_conds_safe = is_array($join_conds) ? $join_conds : array();
01573 foreach ( $tables as $table ) {
01574
01575 if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
01576 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
01577 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
01578 $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
01579 $retJOIN[] = $tableClause;
01580
01581 } else if ( isset($use_index_safe[$table]) ) {
01582 $tableClause = $this->tableName( $table );
01583 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
01584 $ret[] = $tableClause;
01585
01586 } else if ( isset($join_conds_safe[$table]) ) {
01587 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
01588 $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
01589 $retJOIN[] = $tableClause;
01590 } else {
01591 $tableClause = $this->tableName( $table );
01592 $ret[] = $tableClause;
01593 }
01594 }
01595
01596 $straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
01597 $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
01598
01599 return implode(' ',array($straightJoins,$otherJoins) );
01600 }
01601
01605 function indexName( $index ) {
01606
01607 $renamed = array(
01608 'ar_usertext_timestamp' => 'usertext_timestamp',
01609 'un_user_id' => 'user_id',
01610 'un_user_ip' => 'user_ip',
01611 );
01612 if( isset( $renamed[$index] ) ) {
01613 return $renamed[$index];
01614 } else {
01615 return $index;
01616 }
01617 }
01618
01624 function strencode( $s ) {
01625 return mysql_real_escape_string( $s, $this->mConn );
01626 }
01627
01632 function addQuotes( $s ) {
01633 if ( $s === null ) {
01634 return 'NULL';
01635 } else {
01636 # This will also quote numeric values. This should be harmless,
01637 # and protects against weird problems that occur when they really
01638 # _are_ strings such as article titles and string->number->string
01639 # conversion is not 1:1.
01640 return "'" . $this->strencode( $s ) . "'";
01641 }
01642 }
01643
01647 function escapeLike( $s ) {
01648 $s=str_replace('\\','\\\\',$s);
01649 $s=$this->strencode( $s );
01650 $s=str_replace(array('%','_'),array('\%','\_'),$s);
01651 return $s;
01652 }
01653
01659 function nextSequenceValue( $seqName ) {
01660 return NULL;
01661 }
01662
01667 function useIndexClause( $index ) {
01668 return "FORCE INDEX (" . $this->indexName( $index ) . ")";
01669 }
01670
01684 function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
01685 $table = $this->tableName( $table );
01686
01687 # Single row case
01688 if ( !is_array( reset( $rows ) ) ) {
01689 $rows = array( $rows );
01690 }
01691
01692 $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
01693 $first = true;
01694 foreach ( $rows as $row ) {
01695 if ( $first ) {
01696 $first = false;
01697 } else {
01698 $sql .= ',';
01699 }
01700 $sql .= '(' . $this->makeList( $row ) . ')';
01701 }
01702 return $this->query( $sql, $fname );
01703 }
01704
01721 function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
01722 if ( !$conds ) {
01723 throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
01724 }
01725
01726 $delTable = $this->tableName( $delTable );
01727 $joinTable = $this->tableName( $joinTable );
01728 $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
01729 if ( $conds != '*' ) {
01730 $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
01731 }
01732
01733 return $this->query( $sql, $fname );
01734 }
01735
01739 function textFieldSize( $table, $field ) {
01740 $table = $this->tableName( $table );
01741 $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
01742 $res = $this->query( $sql, 'Database::textFieldSize' );
01743 $row = $this->fetchObject( $res );
01744 $this->freeResult( $res );
01745
01746 $m = array();
01747 if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
01748 $size = $m[1];
01749 } else {
01750 $size = -1;
01751 }
01752 return $size;
01753 }
01754
01758 function lowPriorityOption() {
01759 return 'LOW_PRIORITY';
01760 }
01761
01767 function delete( $table, $conds, $fname = 'Database::delete' ) {
01768 if ( !$conds ) {
01769 throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
01770 }
01771 $table = $this->tableName( $table );
01772 $sql = "DELETE FROM $table";
01773 if ( $conds != '*' ) {
01774 $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
01775 }
01776 return $this->query( $sql, $fname );
01777 }
01778
01786 function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
01787 $insertOptions = array(), $selectOptions = array() )
01788 {
01789 $destTable = $this->tableName( $destTable );
01790 if ( is_array( $insertOptions ) ) {
01791 $insertOptions = implode( ' ', $insertOptions );
01792 }
01793 if( !is_array( $selectOptions ) ) {
01794 $selectOptions = array( $selectOptions );
01795 }
01796 list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
01797 if( is_array( $srcTable ) ) {
01798 $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
01799 } else {
01800 $srcTable = $this->tableName( $srcTable );
01801 }
01802 $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
01803 " SELECT $startOpts " . implode( ',', $varMap ) .
01804 " FROM $srcTable $useIndex ";
01805 if ( $conds != '*' ) {
01806 $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
01807 }
01808 $sql .= " $tailOpts";
01809 return $this->query( $sql, $fname );
01810 }
01811
01819 function limitResult($sql, $limit, $offset=false) {
01820 if( !is_numeric($limit) ) {
01821 throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
01822 }
01823 return "$sql LIMIT "
01824 . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
01825 . "{$limit} ";
01826 }
01827 function limitResultForUpdate($sql, $num) {
01828 return $this->limitResult($sql, $num, 0);
01829 }
01830
01840 function conditional( $cond, $trueVal, $falseVal ) {
01841 return " IF($cond, $trueVal, $falseVal) ";
01842 }
01843
01852 function strreplace( $orig, $old, $new ) {
01853 return "REPLACE({$orig}, {$old}, {$new})";
01854 }
01855
01859 function wasDeadlock() {
01860 return $this->lastErrno() == 1213;
01861 }
01862
01867 function wasErrorReissuable() {
01868 return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
01869 }
01870
01887 function deadlockLoop() {
01888 $myFname = 'Database::deadlockLoop';
01889
01890 $this->begin();
01891 $args = func_get_args();
01892 $function = array_shift( $args );
01893 $oldIgnore = $this->ignoreErrors( true );
01894 $tries = DEADLOCK_TRIES;
01895 if ( is_array( $function ) ) {
01896 $fname = $function[0];
01897 } else {
01898 $fname = $function;
01899 }
01900 do {
01901 $retVal = call_user_func_array( $function, $args );
01902 $error = $this->lastError();
01903 $errno = $this->lastErrno();
01904 $sql = $this->lastQuery();
01905
01906 if ( $errno ) {
01907 if ( $this->wasDeadlock() ) {
01908 # Retry
01909 usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
01910 } else {
01911 $this->reportQueryError( $error, $errno, $sql, $fname );
01912 }
01913 }
01914 } while( $this->wasDeadlock() && --$tries > 0 );
01915 $this->ignoreErrors( $oldIgnore );
01916 if ( $tries <= 0 ) {
01917 $this->query( 'ROLLBACK', $myFname );
01918 $this->reportQueryError( $error, $errno, $sql, $fname );
01919 return false;
01920 } else {
01921 $this->query( 'COMMIT', $myFname );
01922 return $retVal;
01923 }
01924 }
01925
01932 function masterPosWait( MySQLMasterPos $pos, $timeout ) {
01933 $fname = 'Database::masterPosWait';
01934 wfProfileIn( $fname );
01935
01936 # Commit any open transactions
01937 if ( $this->mTrxLevel ) {
01938 $this->immediateCommit();
01939 }
01940
01941 if ( !is_null( $this->mFakeSlaveLag ) ) {
01942 $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 );
01943 if ( $wait > $timeout * 1e6 ) {
01944 wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" );
01945 wfProfileOut( $fname );
01946 return -1;
01947 } elseif ( $wait > 0 ) {
01948 wfDebug( "Fake slave waiting $wait us\n" );
01949 usleep( $wait );
01950 wfProfileOut( $fname );
01951 return 1;
01952 } else {
01953 wfDebug( "Fake slave up to date ($wait us)\n" );
01954 wfProfileOut( $fname );
01955 return 0;
01956 }
01957 }
01958
01959 # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
01960 $encFile = $this->addQuotes( $pos->file );
01961 $encPos = intval( $pos->pos );
01962 $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
01963 $res = $this->doQuery( $sql );
01964 if ( $res && $row = $this->fetchRow( $res ) ) {
01965 $this->freeResult( $res );
01966 wfProfileOut( $fname );
01967 return $row[0];
01968 } else {
01969 wfProfileOut( $fname );
01970 return false;
01971 }
01972 }
01973
01977 function getSlavePos() {
01978 if ( !is_null( $this->mFakeSlaveLag ) ) {
01979 $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag );
01980 wfDebug( __METHOD__.": fake slave pos = $pos\n" );
01981 return $pos;
01982 }
01983 $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
01984 $row = $this->fetchObject( $res );
01985 if ( $row ) {
01986 $pos = isset($row->Exec_master_log_pos) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos;
01987 return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos );
01988 } else {
01989 return false;
01990 }
01991 }
01992
01996 function getMasterPos() {
01997 if ( $this->mFakeMaster ) {
01998 return new MySQLMasterPos( 'fake', microtime( true ) );
01999 }
02000 $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
02001 $row = $this->fetchObject( $res );
02002 if ( $row ) {
02003 return new MySQLMasterPos( $row->File, $row->Position );
02004 } else {
02005 return false;
02006 }
02007 }
02008
02012 function begin( $fname = 'Database::begin' ) {
02013 $this->query( 'BEGIN', $fname );
02014 $this->mTrxLevel = 1;
02015 }
02016
02020 function commit( $fname = 'Database::commit' ) {
02021 $this->query( 'COMMIT', $fname );
02022 $this->mTrxLevel = 0;
02023 }
02024
02029 function rollback( $fname = 'Database::rollback' ) {
02030 $this->query( 'ROLLBACK', $fname, true );
02031 $this->mTrxLevel = 0;
02032 }
02033
02038 function immediateBegin( $fname = 'Database::immediateBegin' ) {
02039 $this->begin();
02040 }
02041
02046 function immediateCommit( $fname = 'Database::immediateCommit' ) {
02047 $this->commit();
02048 }
02049
02053 function timestamp( $ts=0 ) {
02054 return wfTimestamp(TS_MW,$ts);
02055 }
02056
02060 function timestampOrNull( $ts = null ) {
02061 if( is_null( $ts ) ) {
02062 return null;
02063 } else {
02064 return $this->timestamp( $ts );
02065 }
02066 }
02067
02071 function resultObject( $result ) {
02072 if( empty( $result ) ) {
02073 return false;
02074 } elseif ( $result instanceof ResultWrapper ) {
02075 return $result;
02076 } elseif ( $result === true ) {
02077
02078 return $result;
02079 } else {
02080 return new ResultWrapper( $this, $result );
02081 }
02082 }
02083
02087 function aggregateValue ($valuedata,$valuename='value') {
02088 return $valuename;
02089 }
02090
02094 function getSoftwareLink() {
02095 return "[http://www.mysql.com/ MySQL]";
02096 }
02097
02101 function getServerVersion() {
02102 return mysql_get_server_info( $this->mConn );
02103 }
02104
02108 function ping() {
02109 if( !function_exists( 'mysql_ping' ) ) {
02110 wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
02111 return true;
02112 }
02113 $ping = mysql_ping( $this->mConn );
02114 if ( $ping ) {
02115 return true;
02116 }
02117
02118
02119 if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
02120 mysql_close( $this->mConn );
02121 $this->mOpened = false;
02122 $this->mConn = false;
02123 $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
02124 return true;
02125 }
02126 return false;
02127 }
02128
02133 function getLag() {
02134 if ( !is_null( $this->mFakeSlaveLag ) ) {
02135 wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
02136 return $this->mFakeSlaveLag;
02137 }
02138 $res = $this->query( 'SHOW PROCESSLIST' );
02139 # Find slave SQL thread
02140 while ( $row = $this->fetchObject( $res ) ) {
02141
02142
02143
02144
02145
02146
02147 if ( $row->User == 'system user' &&
02148 $row->State != 'Waiting for master to send event' &&
02149 $row->State != 'Connecting to master' &&
02150 $row->State != 'Queueing master event to the relay log' &&
02151 $row->State != 'Waiting for master update' &&
02152 $row->State != 'Requesting binlog dump'
02153 ) {
02154 # This is it, return the time (except -ve)
02155 if ( $row->Time > 0x7fffffff ) {
02156 return false;
02157 } else {
02158 return $row->Time;
02159 }
02160 }
02161 }
02162 return false;
02163 }
02164
02168 function getStatus($which="%") {
02169 $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
02170 $status = array();
02171 while ( $row = $this->fetchObject( $res ) ) {
02172 $status[$row->Variable_name] = $row->Value;
02173 }
02174 return $status;
02175 }
02176
02180 function maxListLen() {
02181 return 0;
02182 }
02183
02184 function encodeBlob($b) {
02185 return $b;
02186 }
02187
02188 function decodeBlob($b) {
02189 return $b;
02190 }
02191
02199 public function setTimeout( $timeout ) {
02200 $this->query( "SET net_read_timeout=$timeout" );
02201 $this->query( "SET net_write_timeout=$timeout" );
02202 }
02203
02211 function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
02212 $fp = fopen( $filename, 'r' );
02213 if ( false === $fp ) {
02214 throw new MWException( "Could not open \"{$filename}\".\n" );
02215 }
02216 $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
02217 fclose( $fp );
02218 return $error;
02219 }
02220
02228 function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
02229 $cmd = "";
02230 $done = false;
02231 $dollarquote = false;
02232
02233 while ( ! feof( $fp ) ) {
02234 if ( $lineCallback ) {
02235 call_user_func( $lineCallback );
02236 }
02237 $line = trim( fgets( $fp, 1024 ) );
02238 $sl = strlen( $line ) - 1;
02239
02240 if ( $sl < 0 ) { continue; }
02241 if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
02242
02243 ## Allow dollar quoting for function declarations
02244 if (substr($line,0,4) == '$mw$') {
02245 if ($dollarquote) {
02246 $dollarquote = false;
02247 $done = true;
02248 }
02249 else {
02250 $dollarquote = true;
02251 }
02252 }
02253 else if (!$dollarquote) {
02254 if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
02255 $done = true;
02256 $line = substr( $line, 0, $sl );
02257 }
02258 }
02259
02260 if ( '' != $cmd ) { $cmd .= ' '; }
02261 $cmd .= "$line\n";
02262
02263 if ( $done ) {
02264 $cmd = str_replace(';;', ";", $cmd);
02265 $cmd = $this->replaceVars( $cmd );
02266 $res = $this->query( $cmd, __METHOD__ );
02267 if ( $resultCallback ) {
02268 call_user_func( $resultCallback, $res, $this );
02269 }
02270
02271 if ( false === $res ) {
02272 $err = $this->lastError();
02273 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
02274 }
02275
02276 $cmd = '';
02277 $done = false;
02278 }
02279 }
02280 return true;
02281 }
02282
02283
02287 protected function replaceVars( $ins ) {
02288 $varnames = array(
02289 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
02290 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
02291 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
02292 );
02293
02294
02295 foreach ( $varnames as $var ) {
02296 if( isset( $GLOBALS[$var] ) ) {
02297 $val = addslashes( $GLOBALS[$var] );
02298 $ins = str_replace( '{$' . $var . '}', $val, $ins );
02299 $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
02300 $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
02301 }
02302 }
02303
02304
02305 $ins = preg_replace_callback( '!/\*(?:\$wgDBprefix|_)\*/([a-zA-Z_0-9]*)!',
02306 array( $this, 'tableNameCallback' ), $ins );
02307
02308
02309 $ins = preg_replace_callback( '!/\*i\*/([a-zA-Z_0-9]*)!',
02310 array( $this, 'indexNameCallback' ), $ins );
02311 return $ins;
02312 }
02313
02318 protected function tableNameCallback( $matches ) {
02319 return $this->tableName( $matches[1] );
02320 }
02321
02325 protected function indexNameCallback( $matches ) {
02326 return $this->indexName( $matches[1] );
02327 }
02328
02329
02330
02331
02332 function buildConcat( $stringList ) {
02333 return 'CONCAT(' . implode( ',', $stringList ) . ')';
02334 }
02335
02346 public function lock( $lockName, $method ) {
02347 $lockName = $this->addQuotes( $lockName );
02348 $result = $this->query( "SELECT GET_LOCK($lockName, 5) AS lockstatus", $method );
02349 $row = $this->fetchObject( $result );
02350 $this->freeResult( $result );
02351
02352 if( $row->lockstatus == 1 ) {
02353 return true;
02354 } else {
02355 wfDebug( __METHOD__." failed to acquire lock\n" );
02356 return false;
02357 }
02358 }
02368 public function unlock( $lockName, $method ) {
02369 $lockName = $this->addQuotes( $lockName );
02370 $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method );
02371 $this->freeResult( $result );
02372 }
02373
02380 public function getSearchEngine() {
02381 return "SearchMySQL";
02382 }
02383 }
02384
02392 class DatabaseMysql extends Database {
02393 # Inherit all
02394 }
02395
02396
02397
02398
02399
02404 class DBObject {
02405 public $mData;
02406
02407 function DBObject($data) {
02408 $this->mData = $data;
02409 }
02410
02411 function isLOB() {
02412 return false;
02413 }
02414
02415 function data() {
02416 return $this->mData;
02417 }
02418 }
02419
02426 class Blob {
02427 private $mData;
02428 function __construct($data) {
02429 $this->mData = $data;
02430 }
02431 function fetch() {
02432 return $this->mData;
02433 }
02434 }
02435
02440 class MySQLField {
02441 private $name, $tablename, $default, $max_length, $nullable,
02442 $is_pk, $is_unique, $is_multiple, $is_key, $type;
02443 function __construct ($info) {
02444 $this->name = $info->name;
02445 $this->tablename = $info->table;
02446 $this->default = $info->def;
02447 $this->max_length = $info->max_length;
02448 $this->nullable = !$info->not_null;
02449 $this->is_pk = $info->primary_key;
02450 $this->is_unique = $info->unique_key;
02451 $this->is_multiple = $info->multiple_key;
02452 $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
02453 $this->type = $info->type;
02454 }
02455
02456 function name() {
02457 return $this->name;
02458 }
02459
02460 function tableName() {
02461 return $this->tableName;
02462 }
02463
02464 function defaultValue() {
02465 return $this->default;
02466 }
02467
02468 function maxLength() {
02469 return $this->max_length;
02470 }
02471
02472 function nullable() {
02473 return $this->nullable;
02474 }
02475
02476 function isKey() {
02477 return $this->is_key;
02478 }
02479
02480 function isMultipleKey() {
02481 return $this->is_multiple;
02482 }
02483
02484 function type() {
02485 return $this->type;
02486 }
02487 }
02488
02489
02490
02491
02492
02497 class DBError extends MWException {
02498 public $db;
02499
02505 function __construct( Database &$db, $error ) {
02506 $this->db =& $db;
02507 parent::__construct( $error );
02508 }
02509 }
02510
02514 class DBConnectionError extends DBError {
02515 public $error;
02516
02517 function __construct( Database &$db, $error = 'unknown error' ) {
02518 $msg = 'DB connection error';
02519 if ( trim( $error ) != '' ) {
02520 $msg .= ": $error";
02521 }
02522 $this->error = $error;
02523 parent::__construct( $db, $msg );
02524 }
02525
02526 function useOutputPage() {
02527
02528 return false;
02529 }
02530
02531 function useMessageCache() {
02532
02533 return false;
02534 }
02535
02536 function getText() {
02537 return $this->getMessage() . "\n";
02538 }
02539
02540 function getLogMessage() {
02541 # Don't send to the exception log
02542 return false;
02543 }
02544
02545 function getPageTitle() {
02546 global $wgSitename, $wgLang;
02547 $header = "$wgSitename has a problem";
02548 if ( $wgLang instanceof Language ) {
02549 $header = htmlspecialchars( $wgLang->getMessage( 'dberr-header' ) );
02550 }
02551
02552 return $header;
02553 }
02554
02555 function getHTML() {
02556 global $wgLang, $wgMessageCache, $wgUseFileCache;
02557
02558 $sorry = 'Sorry! This site is experiencing technical difficulties.';
02559 $again = 'Try waiting a few minutes and reloading.';
02560 $info = '(Can\'t contact the database server: $1)';
02561
02562 if ( $wgLang instanceof Language ) {
02563 $sorry = htmlspecialchars( $wgLang->getMessage( 'dberr-problems' ) );
02564 $again = htmlspecialchars( $wgLang->getMessage( 'dberr-again' ) );
02565 $info = htmlspecialchars( $wgLang->getMessage( 'dberr-info' ) );
02566 }
02567
02568 # No database access
02569 if ( is_object( $wgMessageCache ) ) {
02570 $wgMessageCache->disable();
02571 }
02572
02573 if ( trim( $this->error ) == '' ) {
02574 $this->error = $this->db->getProperty('mServer');
02575 }
02576
02577 $noconnect = "<p><strong>$sorry</strong><br />$again</p><p><small>$info</small></p>";
02578 $text = str_replace( '$1', $this->error, $noconnect );
02579
02580
02581
02582
02583
02584
02585
02586
02587 $extra = $this->searchForm();
02588
02589 if( $wgUseFileCache ) {
02590 $cache = $this->fileCachedPage();
02591 # Cached version on file system?
02592 if( $cache !== null ) {
02593 # Hack: extend the body for error messages
02594 $cache = str_replace( array('</html>','</body>'), '', $cache );
02595 # Add cache notice...
02596 $cachederror = "This is a cached copy of the requested page, and may not be up to date. ";
02597 # Localize it if possible...
02598 if( $wgLang instanceof Language ) {
02599 $cachederror = htmlspecialchars( $wgLang->getMessage( 'dberr-cachederror' ) );
02600 }
02601 $warning = "<div style='color:red;font-size:150%;font-weight:bold;'>$cachederror</div>";
02602 # Output cached page with notices on bottom and re-close body
02603 return "{$cache}{$warning}<hr />$text<hr />$extra</body></html>";
02604 }
02605 }
02606 # Headers needed here - output is just the error message
02607 return $this->htmlHeader()."$text<hr />$extra".$this->htmlFooter();
02608 }
02609
02610 function searchForm() {
02611 global $wgSitename, $wgServer, $wgLang, $wgInputEncoding;
02612 $usegoogle = "You can try searching via Google in the meantime.";
02613 $outofdate = "Note that their indexes of our content may be out of date.";
02614 $googlesearch = "Search";
02615
02616 if ( $wgLang instanceof Language ) {
02617 $usegoogle = htmlspecialchars( $wgLang->getMessage( 'dberr-usegoogle' ) );
02618 $outofdate = htmlspecialchars( $wgLang->getMessage( 'dberr-outofdate' ) );
02619 $googlesearch = htmlspecialchars( $wgLang->getMessage( 'searchbutton' ) );
02620 }
02621
02622 $search = htmlspecialchars(@$_REQUEST['search']);
02623
02624 $trygoogle = <<<EOT
02625 <div style="margin: 1.5em">$usegoogle<br />
02626 <small>$outofdate</small></div>
02627 <!-- SiteSearch Google -->
02628 <form method="get" action="http://www.google.com/search" id="googlesearch">
02629 <input type="hidden" name="domains" value="$wgServer" />
02630 <input type="hidden" name="num" value="50" />
02631 <input type="hidden" name="ie" value="$wgInputEncoding" />
02632 <input type="hidden" name="oe" value="$wgInputEncoding" />
02633
02634 <img src="http://www.google.com/logos/Logo_40wht.gif" alt="" style="float:left; margin-left: 1.5em; margin-right: 1.5em;" />
02635
02636 <input type="text" name="q" size="31" maxlength="255" value="$search" />
02637 <input type="submit" name="btnG" value="$googlesearch" />
02638 <div>
02639 <input type="radio" name="sitesearch" id="gwiki" value="$wgServer" checked="checked" /><label for="gwiki">$wgSitename</label>
02640 <input type="radio" name="sitesearch" id="gWWW" value="" /><label for="gWWW">WWW</label>
02641 </div>
02642 </form>
02643 <!-- SiteSearch Google -->
02644 EOT;
02645 return $trygoogle;
02646 }
02647
02648 function fileCachedPage() {
02649 global $wgTitle, $title, $wgLang, $wgOut;
02650 if( $wgOut->isDisabled() ) return;
02651 $mainpage = 'Main Page';
02652 if ( $wgLang instanceof Language ) {
02653 $mainpage = htmlspecialchars( $wgLang->getMessage( 'mainpage' ) );
02654 }
02655
02656 if($wgTitle) {
02657 $t =& $wgTitle;
02658 } elseif($title) {
02659 $t = Title::newFromURL( $title );
02660 } else {
02661 $t = Title::newFromText( $mainpage );
02662 }
02663
02664 $cache = new HTMLFileCache( $t );
02665 if( $cache->isFileCached() ) {
02666 return $cache->fetchPageText();
02667 } else {
02668 return '';
02669 }
02670 }
02671
02672 function htmlBodyOnly() {
02673 return true;
02674 }
02675
02676 }
02677
02681 class DBQueryError extends DBError {
02682 public $error, $errno, $sql, $fname;
02683
02684 function __construct( Database &$db, $error, $errno, $sql, $fname ) {
02685 $message = "A database error has occurred\n" .
02686 "Query: $sql\n" .
02687 "Function: $fname\n" .
02688 "Error: $errno $error\n";
02689
02690 parent::__construct( $db, $message );
02691 $this->error = $error;
02692 $this->errno = $errno;
02693 $this->sql = $sql;
02694 $this->fname = $fname;
02695 }
02696
02697 function getText() {
02698 if ( $this->useMessageCache() ) {
02699 return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
02700 htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
02701 } else {
02702 return $this->getMessage();
02703 }
02704 }
02705
02706 function getSQL() {
02707 global $wgShowSQLErrors;
02708 if( !$wgShowSQLErrors ) {
02709 return $this->msg( 'sqlhidden', 'SQL hidden' );
02710 } else {
02711 return $this->sql;
02712 }
02713 }
02714
02715 function getLogMessage() {
02716 # Don't send to the exception log
02717 return false;
02718 }
02719
02720 function getPageTitle() {
02721 return $this->msg( 'databaseerror', 'Database error' );
02722 }
02723
02724 function getHTML() {
02725 if ( $this->useMessageCache() ) {
02726 return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
02727 htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
02728 } else {
02729 return nl2br( htmlspecialchars( $this->getMessage() ) );
02730 }
02731 }
02732 }
02733
02737 class DBUnexpectedError extends DBError {}
02738
02739
02744 class ResultWrapper implements Iterator {
02745 var $db, $result, $pos = 0, $currentRow = null;
02746
02750 function ResultWrapper( $database, $result ) {
02751 $this->db = $database;
02752 if ( $result instanceof ResultWrapper ) {
02753 $this->result = $result->result;
02754 } else {
02755 $this->result = $result;
02756 }
02757 }
02758
02762 function numRows() {
02763 return $this->db->numRows( $this );
02764 }
02765
02775 function fetchObject() {
02776 return $this->db->fetchObject( $this );
02777 }
02778
02787 function fetchRow() {
02788 return $this->db->fetchRow( $this );
02789 }
02790
02794 function free() {
02795 $this->db->freeResult( $this );
02796 unset( $this->result );
02797 unset( $this->db );
02798 }
02799
02804 function seek( $row ) {
02805 $this->db->dataSeek( $this, $row );
02806 }
02807
02808
02809
02810
02811
02812
02813
02814 function rewind() {
02815 if ($this->numRows()) {
02816 $this->db->dataSeek($this, 0);
02817 }
02818 $this->pos = 0;
02819 $this->currentRow = null;
02820 }
02821
02822 function current() {
02823 if ( is_null( $this->currentRow ) ) {
02824 $this->next();
02825 }
02826 return $this->currentRow;
02827 }
02828
02829 function key() {
02830 return $this->pos;
02831 }
02832
02833 function next() {
02834 $this->pos++;
02835 $this->currentRow = $this->fetchObject();
02836 return $this->currentRow;
02837 }
02838
02839 function valid() {
02840 return $this->current() !== false;
02841 }
02842 }
02843
02844 class MySQLMasterPos {
02845 var $file, $pos;
02846
02847 function __construct( $file, $pos ) {
02848 $this->file = $file;
02849 $this->pos = $pos;
02850 }
02851
02852 function __toString() {
02853 return "{$this->file}/{$this->pos}";
02854 }
02855 }