/**
* Compatibility functions
*
- * We support PHP 5.3.2 and up.
+ * We support PHP 5.3.3 and up.
* Re-implementations of newer functions or functions in non-standard
* PHP extensions may be included here.
*/
}
// hash_equals function only exists in PHP >= 5.6.0
+// http://php.net/hash_equals
if ( !function_exists( 'hash_equals' ) ) {
/**
- * Check whether a user-provided string is equal to a fixed-length secret without
- * revealing bytes of the secret through timing differences.
+ * Check whether a user-provided string is equal to a fixed-length secret string
+ * without revealing bytes of the secret string through timing differences.
*
- * This timing guarantee -- that a partial match takes the same time as a complete
- * mismatch -- is why this function is used in some security-sensitive parts of the code.
- * For example, it shouldn't be possible to guess an HMAC signature one byte at a time.
+ * The usual way to compare strings (PHP's === operator or the underlying memcmp()
+ * function in C) is to compare corresponding bytes and stop at the first difference,
+ * which would take longer for a partial match than for a complete mismatch. This
+ * is not secure when one of the strings (e.g. an HMAC or token) must remain secret
+ * and the other may come from an attacker. Statistical analysis of timing measurements
+ * over many requests may allow the attacker to guess the string's bytes one at a time
+ * (and check his guesses) even if the timing differences are extremely small.
+ *
+ * When making such a security-sensitive comparison, it is essential that the sequence
+ * in which instructions are executed and memory locations are accessed not depend on
+ * the secret string's value. HOWEVER, for simplicity, we do not attempt to minimize
+ * the inevitable leakage of the string's length. That is generally known anyway as
+ * a chararacteristic of the hash function used to compute the secret value.
*
* Longer explanation: http://www.emerose.com/timing-attacks-explained
*
* @codeCoverageIgnore
- * @param string $known_string Fixed-length secret to compare against
+ * @param string $known_string Fixed-length secret string to compare against
* @param string $user_string User-provided string
* @return bool True if the strings are the same, false otherwise
*/
return false;
}
- // Note that we do one thing PHP doesn't: try to avoid leaking information about
- // relative lengths of $known_string and $user_string, and of multiple $known_strings.
- // However, lengths may still inevitably leak through, for example, CPU cache misses.
$known_string_len = strlen( $known_string );
- $user_string_len = strlen( $user_string );
- $result = $known_string_len ^ $user_string_len;
- for ( $i = 0; $i < $user_string_len; $i++ ) {
- $result |= ord( $known_string[$i % $known_string_len] ) ^ ord( $user_string[$i] );
+ if ( $known_string_len !== strlen( $user_string ) ) {
+ return false;
+ }
+
+ $result = 0;
+ for ( $i = 0; $i < $known_string_len; $i++ ) {
+ $result |= ord( $known_string[$i] ) ^ ord( $user_string[$i] );
}
return ( $result === 0 );
* - false: same as 'log'
*/
function wfDebug( $text, $dest = 'all' ) {
- global $wgDebugLogFile, $wgDebugRawPage, $wgDebugLogPrefix;
+ global $wgDebugRawPage, $wgDebugLogPrefix;
if ( !$wgDebugRawPage && wfIsDebugRawPage() ) {
return;
$timer = wfDebugTimer();
if ( $timer !== '' ) {
+ // Prepend elapsed request time and real memory usage to each line
$text = preg_replace( '/[^\n]/', $timer . '\0', $text, 1 );
}
MWDebug::debugMsg( $text );
}
- if ( $wgDebugLogFile != '' ) {
- # Strip unprintables; they can switch terminal modes when binary data
- # gets dumped, which is pretty annoying.
- $text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $text );
- $text = $wgDebugLogPrefix . $text;
- wfErrorLog( $text, $wgDebugLogFile );
+ $ctx = array();
+ if ( $wgDebugLogPrefix !== '' ) {
+ $ctx['prefix'] = $wgDebugLogPrefix;
}
+
+ $logger = MWLogger::getInstance( 'wfDebug' );
+ $logger->debug( rtrim( $text, "\n" ), $ctx );
}
/**
function wfDebugLog( $logGroup, $text, $dest = 'all' ) {
global $wgDebugLogGroups;
- $text = trim( $text ) . "\n";
-
// Turn $dest into a string if it's a boolean (for b/c)
if ( $dest === true ) {
$dest = 'all';
$dest = 'private';
}
- if ( !isset( $wgDebugLogGroups[$logGroup] ) ) {
- if ( $dest !== 'private' ) {
- wfDebug( "[$logGroup] $text", $dest );
- }
- return;
- }
+ $text = trim( $text );
if ( $dest === 'all' ) {
- MWDebug::debugMsg( "[$logGroup] $text" );
+ MWDebug::debugMsg( "[{$logGroup}] {$text}\n" );
}
- $logConfig = $wgDebugLogGroups[$logGroup];
- if ( $logConfig === false ) {
- return;
- }
- if ( is_array( $logConfig ) ) {
- if ( isset( $logConfig['sample'] ) && mt_rand( 1, $logConfig['sample'] ) !== 1 ) {
- return;
- }
- $destination = $logConfig['destination'];
- } else {
- $destination = strval( $logConfig );
- }
-
- $time = wfTimestamp( TS_DB );
- $wiki = wfWikiID();
- $host = wfHostname();
- wfErrorLog( "$time $host $wiki: $text", $destination );
+ $logger = MWLogger::getInstance( $logGroup );
+ $logger->debug( $text, array(
+ 'private' => ( $dest === 'private' ),
+ ) );
}
/**
* @param string $text Database error message.
*/
function wfLogDBError( $text ) {
- global $wgDBerrorLog, $wgDBerrorLogTZ;
- static $logDBErrorTimeZoneObject = null;
-
- if ( $wgDBerrorLog ) {
- $host = wfHostname();
- $wiki = wfWikiID();
-
- if ( $wgDBerrorLogTZ && !$logDBErrorTimeZoneObject ) {
- $logDBErrorTimeZoneObject = new DateTimeZone( $wgDBerrorLogTZ );
- }
-
- // Workaround for https://bugs.php.net/bug.php?id=52063
- // Can be removed when min PHP > 5.3.2
- if ( $logDBErrorTimeZoneObject === null ) {
- $d = date_create( "now" );
- } else {
- $d = date_create( "now", $logDBErrorTimeZoneObject );
- }
-
- $date = $d->format( 'D M j G:i:s T Y' );
-
- $text = "$date\t$host\t$wiki\t" . trim( $text ) . "\n";
- wfErrorLog( $text, $wgDBerrorLog );
- }
+ $logger = MWLogger::getInstance( 'wfLogDBError' );
+ $logger->error( trim( $text ) );
}
/**
* @throws MWException
*/
function wfErrorLog( $text, $file ) {
- if ( substr( $file, 0, 4 ) == 'udp:' ) {
- # Needs the sockets extension
- if ( preg_match( '!^(tcp|udp):(?://)?\[([0-9a-fA-F:]+)\]:(\d+)(?:/(.*))?$!', $file, $m ) ) {
- // IPv6 bracketed host
- $host = $m[2];
- $port = intval( $m[3] );
- $prefix = isset( $m[4] ) ? $m[4] : false;
- $domain = AF_INET6;
- } elseif ( preg_match( '!^(tcp|udp):(?://)?([a-zA-Z0-9.-]+):(\d+)(?:/(.*))?$!', $file, $m ) ) {
- $host = $m[2];
- if ( !IP::isIPv4( $host ) ) {
- $host = gethostbyname( $host );
- }
- $port = intval( $m[3] );
- $prefix = isset( $m[4] ) ? $m[4] : false;
- $domain = AF_INET;
- } else {
- throw new MWException( __METHOD__ . ': Invalid UDP specification' );
- }
-
- // Clean it up for the multiplexer
- if ( strval( $prefix ) !== '' ) {
- $text = preg_replace( '/^/m', $prefix . ' ', $text );
-
- // Limit to 64KB
- if ( strlen( $text ) > 65506 ) {
- $text = substr( $text, 0, 65506 );
- }
-
- if ( substr( $text, -1 ) != "\n" ) {
- $text .= "\n";
- }
- } elseif ( strlen( $text ) > 65507 ) {
- $text = substr( $text, 0, 65507 );
- }
-
- $sock = socket_create( $domain, SOCK_DGRAM, SOL_UDP );
- if ( !$sock ) {
- return;
- }
-
- socket_sendto( $sock, $text, strlen( $text ), 0, $host, $port );
- socket_close( $sock );
- } else {
- wfSuppressWarnings();
- $exists = file_exists( $file );
- $size = $exists ? filesize( $file ) : false;
- if ( !$exists || ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) ) {
- file_put_contents( $file, $text, FILE_APPEND );
- }
- wfRestoreWarnings();
- }
+ $logger = MWLogger::getInstance( 'wfErrorLog' );
+ $logger->info( trim( $text ), array(
+ 'destination' => $file,
+ ) );
}
/**
* function, as all the arguments to wfShellExec can become unwieldy.
*
* @note This also includes errors from limit.sh, e.g. if $wgMaxShellFileSize is exceeded.
- * @param string $cmd Command line, properly escaped for shell.
+ * @param string|string[] $cmd If string, a properly shell-escaped command line,
+ * or an array of unescaped arguments, in which case each value will be escaped
+ * Example: [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'"
* @param null|mixed &$retval Optional, will receive the program's exit code.
* (non-zero is usually failure)
* @param array $environ Optional environment variables which should be
}
/**
- * Modern version of wfWaitForSlaves(). Instead of looking at replication lag
- * and waiting for it to go down, this waits for the slaves to catch up to the
- * master position. Use this when updating very large numbers of rows, as
- * in maintenance scripts, to avoid causing too much lag. Of course, this is
- * a no-op if there are no slaves.
+ * Waits for the slaves to catch up to the master position
+ *
+ * Use this when updating very large numbers of rows, as in maintenance scripts,
+ * to avoid causing too much lag. Of course, this is a no-op if there are no slaves.
+ *
+ * By default this waits on the main DB cluster of the current wiki.
+ * If $cluster is set to "*" it will wait on all DB clusters, including
+ * external ones. If the lag being waiting on is caused by the code that
+ * does this check, it makes since to use $ifWritesSince, particularly if
+ * cluster is "*", to avoid excess overhead.
+ *
+ * Never call this function after a big DB write that is still in a transaction.
+ * This only makes sense after the possible lag inducing changes were committed.
*
* @param float|null $ifWritesSince Only wait if writes were done since this UNIX timestamp
* @param string|bool $wiki Wiki identifier accepted by wfGetLB
* @param string|bool $cluster Cluster name accepted by LBFactory. Default: false.
+ * @param int|null $timeout Max wait time. Default: 1 day (cli), ~10 seconds (web)
* @return bool Success (able to connect and no timeouts reached)
*/
-function wfWaitForSlaves( $ifWritesSince = false, $wiki = false, $cluster = false ) {
+function wfWaitForSlaves(
+ $ifWritesSince = null, $wiki = false, $cluster = false, $timeout = null
+) {
// B/C: first argument used to be "max seconds of lag"; ignore such values
- $ifWritesSince = ( $ifWritesSince > 1e9 ) ? $ifWritesSince : false;
+ $ifWritesSince = ( $ifWritesSince > 1e9 ) ? $ifWritesSince : null;
- if ( $cluster !== false ) {
- $lb = wfGetLBFactory()->getExternalLB( $cluster );
- } else {
- $lb = wfGetLB( $wiki );
+ if ( $timeout === null ) {
+ $timeout = ( PHP_SAPI === 'cli' ) ? 86400 : 10;
}
- // bug 27975 - Don't try to wait for slaves if there are none
- // Prevents permission error when getting master position
- if ( $lb->getServerCount() > 1 ) {
- if ( $ifWritesSince && !$lb->hasMasterConnection() ) {
- return true; // assume no writes done
- }
- $dbw = $lb->getConnection( DB_MASTER, array(), $wiki );
- if ( $ifWritesSince && $dbw->lastDoneWrites() < $ifWritesSince ) {
- return true; // no writes since the last wait
+ // Figure out which clusters need to be checked
+ $lbs = array();
+ if ( $cluster === '*' ) {
+ wfGetLBFactory()->forEachLB( function ( LoadBalancer $lb ) use ( &$lbs ) {
+ $lbs[] = $lb;
+ } );
+ } elseif ( $cluster !== false ) {
+ $lbs[] = wfGetLBFactory()->getExternalLB( $cluster );
+ } else {
+ $lbs[] = wfGetLB( $wiki );
+ }
+
+ // Get all the master positions of applicable DBs right now.
+ // This can be faster since waiting on one cluster reduces the
+ // time needed to wait on the next clusters.
+ $masterPositions = array_fill( 0, count( $lbs ), false );
+ foreach ( $lbs as $i => $lb ) {
+ // bug 27975 - Don't try to wait for slaves if there are none
+ // Prevents permission error when getting master position
+ if ( $lb->getServerCount() > 1 ) {
+ if ( $ifWritesSince && !$lb->hasMasterConnection() ) {
+ continue; // assume no writes done
+ }
+ // Use the empty string to not trigger selectDB() since the connection
+ // may have been to a server that does not have a DB for the current wiki.
+ $dbw = $lb->getConnection( DB_MASTER, array(), '' );
+ if ( $ifWritesSince && $dbw->lastDoneWrites() < $ifWritesSince ) {
+ continue; // no writes since the last wait
+ }
+ $masterPositions[$i] = $dbw->getMasterPos();
}
- $pos = $dbw->getMasterPos();
- // The DBMS may not support getMasterPos() or the whole
- // load balancer might be fake (e.g. $wgAllDBsAreLocalhost).
- if ( $pos !== false ) {
- return $lb->waitForAll( $pos, PHP_SAPI === 'cli' ? 86400 : null );
+ }
+
+ $ok = true;
+ foreach ( $lbs as $i => $lb ) {
+ if ( $masterPositions[$i] ) {
+ // The DBMS may not support getMasterPos() or the whole
+ // load balancer might be fake (e.g. $wgAllDBsAreLocalhost).
+ $ok = $lb->waitForAll( $masterPositions[$i], $timeout ) && $ok;
}
}
- return true;
+ return $ok;
}
/**