Page MenuHomePhorge

No OneTemporary

diff --git a/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php b/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php
index 0f9201b02d..313ea5a3b0 100644
--- a/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php
+++ b/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php
@@ -1,405 +1,408 @@
<?php
abstract class AphrontBaseMySQLDatabaseConnection
extends AphrontDatabaseConnection {
private $configuration;
private $connection;
private $connectionPool = array();
private $lastResult;
private $nextError;
+ const CALLERROR_QUERY = 777777;
+ const CALLERROR_CONNECT = 777778;
+
abstract protected function connect();
abstract protected function rawQuery($raw_query);
abstract protected function rawQueries(array $raw_queries);
abstract protected function fetchAssoc($result);
abstract protected function getErrorCode($connection);
abstract protected function getErrorDescription($connection);
abstract protected function closeConnection();
abstract protected function freeResult($result);
public function __construct(array $configuration) {
$this->configuration = $configuration;
}
public function __clone() {
$this->establishConnection();
}
public function openConnection() {
$this->requireConnection();
}
public function close() {
if ($this->lastResult) {
$this->lastResult = null;
}
if ($this->connection) {
$this->closeConnection();
$this->connection = null;
}
}
public function escapeColumnName($name) {
return '`'.str_replace('`', '``', $name).'`';
}
public function escapeMultilineComment($comment) {
// These can either terminate a comment, confuse the hell out of the parser,
// make MySQL execute the comment as a query, or, in the case of semicolon,
// are quasi-dangerous because the semicolon could turn a broken query into
// a working query plus an ignored query.
static $map = array(
'--' => '(DOUBLEDASH)',
'*/' => '(STARSLASH)',
'//' => '(SLASHSLASH)',
'#' => '(HASH)',
'!' => '(BANG)',
';' => '(SEMICOLON)',
);
$comment = str_replace(
array_keys($map),
array_values($map),
$comment);
// For good measure, kill anything else that isn't a nice printable
// character.
$comment = preg_replace('/[^\x20-\x7F]+/', ' ', $comment);
return '/* '.$comment.' */';
}
public function escapeStringForLikeClause($value) {
$value = addcslashes($value, '\%_');
$value = $this->escapeUTF8String($value);
return $value;
}
protected function getConfiguration($key, $default = null) {
return idx($this->configuration, $key, $default);
}
private function establishConnection() {
$host = $this->getConfiguration('host');
$database = $this->getConfiguration('database');
$profiler = PhutilServiceProfiler::getInstance();
$call_id = $profiler->beginServiceCall(
array(
'type' => 'connect',
'host' => $host,
'database' => $database,
));
// If we receive these errors, we'll retry the connection up to the
// retry limit. For other errors, we'll fail immediately.
$retry_codes = array(
// "Connection Timeout"
2002 => true,
// "Unable to Connect"
2003 => true,
);
$max_retries = max(1, $this->getConfiguration('retries', 3));
for ($attempt = 1; $attempt <= $max_retries; $attempt++) {
try {
$conn = $this->connect();
$profiler->endServiceCall($call_id, array());
break;
} catch (AphrontQueryException $ex) {
$code = $ex->getCode();
if (($attempt < $max_retries) && isset($retry_codes[$code])) {
$message = pht(
'Retrying database connection to "%s" after connection '.
'failure (attempt %d; "%s"; error #%d): %s',
$host,
$attempt,
get_class($ex),
$code,
$ex->getMessage());
phlog($message);
} else {
$profiler->endServiceCall($call_id, array());
throw $ex;
}
}
}
$this->connection = $conn;
}
protected function requireConnection() {
if (!$this->connection) {
if ($this->connectionPool) {
$this->connection = array_pop($this->connectionPool);
} else {
$this->establishConnection();
}
}
return $this->connection;
}
protected function beginAsyncConnection() {
$connection = $this->requireConnection();
$this->connection = null;
return $connection;
}
protected function endAsyncConnection($connection) {
if ($this->connection) {
$this->connectionPool[] = $this->connection;
}
$this->connection = $connection;
}
public function selectAllResults() {
$result = array();
$res = $this->lastResult;
if ($res == null) {
throw new Exception(pht('No query result to fetch from!'));
}
while (($row = $this->fetchAssoc($res))) {
$result[] = $row;
}
return $result;
}
public function executeQuery(PhutilQueryString $query) {
$display_query = $query->getMaskedString();
$raw_query = $query->getUnmaskedString();
$this->lastResult = null;
$retries = max(1, $this->getConfiguration('retries', 3));
while ($retries--) {
try {
$this->requireConnection();
$is_write = $this->checkWrite($raw_query);
$profiler = PhutilServiceProfiler::getInstance();
$call_id = $profiler->beginServiceCall(
array(
'type' => 'query',
'config' => $this->configuration,
'query' => $display_query,
'write' => $is_write,
));
$result = $this->rawQuery($raw_query);
$profiler->endServiceCall($call_id, array());
if ($this->nextError) {
$result = null;
}
if ($result) {
$this->lastResult = $result;
break;
}
$this->throwQueryException($this->connection);
} catch (AphrontConnectionLostQueryException $ex) {
$can_retry = ($retries > 0);
if ($this->isInsideTransaction()) {
// Zero out the transaction state to prevent a second exception
// ("program exited with open transaction") from being thrown, since
// we're about to throw a more relevant/useful one instead.
$state = $this->getTransactionState();
while ($state->getDepth()) {
$state->decreaseDepth();
}
$can_retry = false;
}
if ($this->isHoldingAnyLock()) {
$this->forgetAllLocks();
$can_retry = false;
}
$this->close();
if (!$can_retry) {
throw $ex;
}
}
}
}
public function executeRawQueries(array $raw_queries) {
if (!$raw_queries) {
return array();
}
$is_write = false;
foreach ($raw_queries as $key => $raw_query) {
$is_write = $is_write || $this->checkWrite($raw_query);
$raw_queries[$key] = rtrim($raw_query, "\r\n\t ;");
}
$profiler = PhutilServiceProfiler::getInstance();
$call_id = $profiler->beginServiceCall(
array(
'type' => 'multi-query',
'config' => $this->configuration,
'queries' => $raw_queries,
'write' => $is_write,
));
$results = $this->rawQueries($raw_queries);
$profiler->endServiceCall($call_id, array());
return $results;
}
protected function processResult($result) {
if (!$result) {
try {
$this->throwQueryException($this->requireConnection());
} catch (Exception $ex) {
return $ex;
}
} else if (is_bool($result)) {
return $this->getAffectedRows();
}
$rows = array();
while (($row = $this->fetchAssoc($result))) {
$rows[] = $row;
}
$this->freeResult($result);
return $rows;
}
protected function checkWrite($raw_query) {
// NOTE: The opening "(" allows queries in the form of:
//
// (SELECT ...) UNION (SELECT ...)
$is_write = !preg_match('/^[(]*(SELECT|SHOW|EXPLAIN)\s/', $raw_query);
if ($is_write) {
if ($this->getReadOnly()) {
throw new Exception(
pht(
'Attempting to issue a write query on a read-only '.
'connection (to database "%s")!',
$this->getConfiguration('database')));
}
AphrontWriteGuard::willWrite();
return true;
}
return false;
}
protected function throwQueryException($connection) {
if ($this->nextError) {
$errno = $this->nextError;
$error = pht('Simulated error.');
$this->nextError = null;
} else {
$errno = $this->getErrorCode($connection);
$error = $this->getErrorDescription($connection);
}
$this->throwQueryCodeException($errno, $error);
}
private function throwCommonException($errno, $error) {
$message = pht('#%d: %s', $errno, $error);
switch ($errno) {
case 2013: // Connection Dropped
throw new AphrontConnectionLostQueryException($message);
case 2006: // Gone Away
$more = pht(
'This error may occur if your configured MySQL "wait_timeout" or '.
'"max_allowed_packet" values are too small. This may also indicate '.
'that something used the MySQL "KILL <process>" command to kill '.
'the connection running the query.');
throw new AphrontConnectionLostQueryException("{$message}\n\n{$more}");
case 1213: // Deadlock
throw new AphrontDeadlockQueryException($message);
case 1205: // Lock wait timeout exceeded
throw new AphrontLockTimeoutQueryException($message);
case 1062: // Duplicate Key
// NOTE: In some versions of MySQL we get a key name back here, but
// older versions just give us a key index ("key 2") so it's not
// portable to parse the key out of the error and attach it to the
// exception.
throw new AphrontDuplicateKeyQueryException($message);
case 1044: // Access denied to database
case 1142: // Access denied to table
case 1143: // Access denied to column
case 1227: // Access denied (e.g., no SUPER for SHOW SLAVE STATUS).
throw new AphrontAccessDeniedQueryException($message);
case 1045: // Access denied (auth)
throw new AphrontInvalidCredentialsQueryException($message);
case 1146: // No such table
case 1049: // No such database
case 1054: // Unknown column "..." in field list
throw new AphrontSchemaQueryException($message);
}
// TODO: 1064 is syntax error, and quite terrible in production.
return null;
}
protected function throwConnectionException($errno, $error, $user, $host) {
$this->throwCommonException($errno, $error);
$message = pht(
'Attempt to connect to %s@%s failed with error #%d: %s.',
$user,
$host,
$errno,
$error);
throw new AphrontConnectionQueryException($message, $errno);
}
protected function throwQueryCodeException($errno, $error) {
$this->throwCommonException($errno, $error);
$message = pht(
'#%d: %s',
$errno,
$error);
throw new AphrontQueryException($message, $errno);
}
/**
* Force the next query to fail with a simulated error. This should be used
* ONLY for unit tests.
*/
public function simulateErrorOnNextQuery($error) {
$this->nextError = $error;
return $this;
}
/**
* Check inserts for characters outside of the BMP. Even with the strictest
* settings, MySQL will silently truncate data when it encounters these, which
* can lead to data loss and security problems.
*/
protected function validateUTF8String($string) {
if (phutil_is_utf8($string)) {
return;
}
throw new AphrontCharacterSetQueryException(
pht(
'Attempting to construct a query using a non-utf8 string when '.
'utf8 is expected. Use the `%%B` conversion to escape binary '.
'strings data.'));
}
}
diff --git a/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php b/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php
index 7a4b5193d5..6a0bc759a7 100644
--- a/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php
+++ b/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php
@@ -1,244 +1,272 @@
<?php
/**
* @phutil-external-symbol class mysqli
*/
final class AphrontMySQLiDatabaseConnection
extends AphrontBaseMySQLDatabaseConnection {
private $connectionOpen = false;
public function escapeUTF8String($string) {
$this->validateUTF8String($string);
return $this->escapeBinaryString($string);
}
public function escapeBinaryString($string) {
return $this->requireConnection()->escape_string($string);
}
public function getInsertID() {
return $this->requireConnection()->insert_id;
}
public function getAffectedRows() {
return $this->requireConnection()->affected_rows;
}
protected function closeConnection() {
if ($this->connectionOpen) {
$this->requireConnection()->close();
$this->connectionOpen = false;
}
}
protected function connect() {
if (!class_exists('mysqli', false)) {
throw new Exception(pht(
'About to call new %s, but the PHP MySQLi extension is not available!',
'mysqli()'));
}
$user = $this->getConfiguration('user');
$host = $this->getConfiguration('host');
$port = $this->getConfiguration('port');
$database = $this->getConfiguration('database');
$pass = $this->getConfiguration('pass');
if ($pass instanceof PhutilOpaqueEnvelope) {
$pass = $pass->openEnvelope();
}
// If the host is "localhost", the port is ignored and mysqli attempts to
// connect over a socket.
if ($port) {
if ($host === 'localhost' || $host === null) {
$host = '127.0.0.1';
}
}
$conn = mysqli_init();
$timeout = $this->getConfiguration('timeout');
if ($timeout) {
$conn->options(MYSQLI_OPT_CONNECT_TIMEOUT, $timeout);
}
if ($this->getPersistent()) {
$host = 'p:'.$host;
}
- @$conn->real_connect(
+ $trap = new PhutilErrorTrap();
+
+ $ok = @$conn->real_connect(
$host,
$user,
$pass,
$database,
$port);
+ $call_error = $trap->getErrorsAsString();
+ $trap->destroy();
+
$errno = $conn->connect_errno;
if ($errno) {
$error = $conn->connect_error;
$this->throwConnectionException($errno, $error, $user, $host);
}
+ // See T13403. If the parameters to "real_connect()" are wrong, it may
+ // fail without setting an error code. In this case, raise a generic
+ // exception. (One way to reproduce this is to pass a string to the
+ // "port" parameter.)
+
+ if (!$ok) {
+ if (strlen($call_error)) {
+ $message = pht(
+ 'mysqli->real_connect() failed: %s',
+ $call_error);
+ } else {
+ $message = pht(
+ 'mysqli->real_connect() failed, but did not set an error code '.
+ 'or emit a message.');
+ }
+
+ $this->throwConnectionException(
+ self::CALLERROR_CONNECT,
+ $message,
+ $user,
+ $host);
+ }
+
// See T13238. Attempt to prevent "LOAD DATA LOCAL INFILE", which allows a
// malicious server to ask the client for any file. At time of writing,
// this option MUST be set after "real_connect()" on all PHP versions.
$conn->options(MYSQLI_OPT_LOCAL_INFILE, 0);
$this->connectionOpen = true;
$ok = @$conn->set_charset('utf8mb4');
if (!$ok) {
$ok = $conn->set_charset('binary');
}
return $conn;
}
protected function rawQuery($raw_query) {
$conn = $this->requireConnection();
$time_limit = $this->getQueryTimeout();
// If we have a query time limit, run this query synchronously but use
// the async API. This allows us to kill queries which take too long
// without requiring any configuration on the server side.
if ($time_limit && $this->supportsAsyncQueries()) {
$conn->query($raw_query, MYSQLI_ASYNC);
$read = array($conn);
$error = array($conn);
$reject = array($conn);
$result = mysqli::poll($read, $error, $reject, $time_limit);
if ($result === false) {
$this->closeConnection();
throw new Exception(
pht('Failed to poll mysqli connection!'));
} else if ($result === 0) {
$this->closeConnection();
throw new AphrontQueryTimeoutQueryException(
pht(
'Query timed out after %s second(s)!',
new PhutilNumber($time_limit)));
}
return @$conn->reap_async_query();
}
$trap = new PhutilErrorTrap();
$result = @$conn->query($raw_query);
$err = $trap->getErrorsAsString();
$trap->destroy();
// See T13238 and PHI1014. Sometimes, the call to "$conn->query()" may fail
// without setting an error code on the connection. One way to reproduce
// this is to use "LOAD DATA LOCAL INFILE" with "mysqli.allow_local_infile"
// disabled.
// If we have no result and no error code, raise a synthetic query error
// with whatever error message was raised as a local PHP warning.
if (!$result) {
$error_code = $this->getErrorCode($conn);
if (!$error_code) {
if (strlen($err)) {
$message = $err;
} else {
$message = pht(
'Call to "mysqli->query()" failed, but did not set an error '.
'code or emit an error message.');
}
- $this->throwQueryCodeException(777777, $message);
+ $this->throwQueryCodeException(self::CALLERROR_QUERY, $message);
}
}
return $result;
}
protected function rawQueries(array $raw_queries) {
$conn = $this->requireConnection();
$have_result = false;
$results = array();
foreach ($raw_queries as $key => $raw_query) {
if (!$have_result) {
// End line in front of semicolon to allow single line comments at the
// end of queries.
$have_result = $conn->multi_query(implode("\n;\n\n", $raw_queries));
} else {
$have_result = $conn->next_result();
}
array_shift($raw_queries);
$result = $conn->store_result();
if (!$result && !$this->getErrorCode($conn)) {
$result = true;
}
$results[$key] = $this->processResult($result);
}
if ($conn->more_results()) {
throw new Exception(
pht('There are some results left in the result set.'));
}
return $results;
}
protected function freeResult($result) {
$result->free_result();
}
protected function fetchAssoc($result) {
return $result->fetch_assoc();
}
protected function getErrorCode($connection) {
return $connection->errno;
}
protected function getErrorDescription($connection) {
return $connection->error;
}
public function supportsAsyncQueries() {
return defined('MYSQLI_ASYNC');
}
public function asyncQuery($raw_query) {
$this->checkWrite($raw_query);
$async = $this->beginAsyncConnection();
$async->query($raw_query, MYSQLI_ASYNC);
return $async;
}
public static function resolveAsyncQueries(array $conns, array $asyncs) {
assert_instances_of($conns, __CLASS__);
assert_instances_of($asyncs, 'mysqli');
$read = $error = $reject = array();
foreach ($asyncs as $async) {
$read[] = $error[] = $reject[] = $async;
}
if (!mysqli::poll($read, $error, $reject, 0)) {
return array();
}
$results = array();
foreach ($read as $async) {
$key = array_search($async, $asyncs, $strict = true);
$conn = $conns[$key];
$conn->endAsyncConnection($async);
$results[$key] = $conn->processResult($async->reap_async_query());
}
return $results;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 16:54 (2 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1126688
Default Alt Text
(20 KB)

Event Timeline