Move web application classes into "phabricator/"
Summary: Ref T13395. Companion change to D20773. Test Plan: See D20773. Maniphest Tasks: T13395 Differential Revision: https://secure.phabricator.com/D20774
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
<?php
|
||||
|
||||
abstract class AphrontBaseMySQLDatabaseConnection
|
||||
extends AphrontDatabaseConnection {
|
||||
|
||||
private $configuration;
|
||||
private $connection;
|
||||
private $connectionPool = array();
|
||||
private $lastResult;
|
||||
|
||||
private $nextError;
|
||||
|
||||
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.'));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user