Path: blob/master/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php
12242 views
<?php12abstract class AphrontBaseMySQLDatabaseConnection3extends AphrontDatabaseConnection {45private $configuration;6private $connection;7private $connectionPool = array();8private $lastResult;910private $nextError;1112const CALLERROR_QUERY = 777777;13const CALLERROR_CONNECT = 777778;1415abstract protected function connect();16abstract protected function rawQuery($raw_query);17abstract protected function rawQueries(array $raw_queries);18abstract protected function fetchAssoc($result);19abstract protected function getErrorCode($connection);20abstract protected function getErrorDescription($connection);21abstract protected function closeConnection();22abstract protected function freeResult($result);2324public function __construct(array $configuration) {25$this->configuration = $configuration;26}2728public function __clone() {29$this->establishConnection();30}3132public function openConnection() {33$this->requireConnection();34}3536public function close() {37if ($this->lastResult) {38$this->lastResult = null;39}40if ($this->connection) {41$this->closeConnection();42$this->connection = null;43}44}4546public function escapeColumnName($name) {47return '`'.str_replace('`', '``', $name).'`';48}495051public function escapeMultilineComment($comment) {52// These can either terminate a comment, confuse the hell out of the parser,53// make MySQL execute the comment as a query, or, in the case of semicolon,54// are quasi-dangerous because the semicolon could turn a broken query into55// a working query plus an ignored query.5657static $map = array(58'--' => '(DOUBLEDASH)',59'*/' => '(STARSLASH)',60'//' => '(SLASHSLASH)',61'#' => '(HASH)',62'!' => '(BANG)',63';' => '(SEMICOLON)',64);6566$comment = str_replace(67array_keys($map),68array_values($map),69$comment);7071// For good measure, kill anything else that isn't a nice printable72// character.73$comment = preg_replace('/[^\x20-\x7F]+/', ' ', $comment);7475return '/* '.$comment.' */';76}7778public function escapeStringForLikeClause($value) {79$value = phutil_string_cast($value);80$value = addcslashes($value, '\%_');81$value = $this->escapeUTF8String($value);82return $value;83}8485protected function getConfiguration($key, $default = null) {86return idx($this->configuration, $key, $default);87}8889private function establishConnection() {90$host = $this->getConfiguration('host');91$database = $this->getConfiguration('database');9293$profiler = PhutilServiceProfiler::getInstance();94$call_id = $profiler->beginServiceCall(95array(96'type' => 'connect',97'host' => $host,98'database' => $database,99));100101// If we receive these errors, we'll retry the connection up to the102// retry limit. For other errors, we'll fail immediately.103$retry_codes = array(104// "Connection Timeout"1052002 => true,106107// "Unable to Connect"1082003 => true,109);110111$max_retries = max(1, $this->getConfiguration('retries', 3));112for ($attempt = 1; $attempt <= $max_retries; $attempt++) {113try {114$conn = $this->connect();115$profiler->endServiceCall($call_id, array());116break;117} catch (AphrontQueryException $ex) {118$code = $ex->getCode();119if (($attempt < $max_retries) && isset($retry_codes[$code])) {120$message = pht(121'Retrying database connection to "%s" after connection '.122'failure (attempt %d; "%s"; error #%d): %s',123$host,124$attempt,125get_class($ex),126$code,127$ex->getMessage());128129// See T13403. If we're silenced with the "@" operator, don't log130// this connection attempt. This keeps things quiet if we're131// running a setup workflow like "bin/config" and expect that the132// database credentials will often be incorrect.133134if (error_reporting()) {135phlog($message);136}137} else {138$profiler->endServiceCall($call_id, array());139throw $ex;140}141}142}143144$this->connection = $conn;145}146147protected function requireConnection() {148if (!$this->connection) {149if ($this->connectionPool) {150$this->connection = array_pop($this->connectionPool);151} else {152$this->establishConnection();153}154}155return $this->connection;156}157158protected function beginAsyncConnection() {159$connection = $this->requireConnection();160$this->connection = null;161return $connection;162}163164protected function endAsyncConnection($connection) {165if ($this->connection) {166$this->connectionPool[] = $this->connection;167}168$this->connection = $connection;169}170171public function selectAllResults() {172$result = array();173$res = $this->lastResult;174if ($res == null) {175throw new Exception(pht('No query result to fetch from!'));176}177while (($row = $this->fetchAssoc($res))) {178$result[] = $row;179}180return $result;181}182183public function executeQuery(PhutilQueryString $query) {184$display_query = $query->getMaskedString();185$raw_query = $query->getUnmaskedString();186187$this->lastResult = null;188$retries = max(1, $this->getConfiguration('retries', 3));189while ($retries--) {190try {191$this->requireConnection();192$is_write = $this->checkWrite($raw_query);193194$profiler = PhutilServiceProfiler::getInstance();195$call_id = $profiler->beginServiceCall(196array(197'type' => 'query',198'config' => $this->configuration,199'query' => $display_query,200'write' => $is_write,201));202203$result = $this->rawQuery($raw_query);204205$profiler->endServiceCall($call_id, array());206207if ($this->nextError) {208$result = null;209}210211if ($result) {212$this->lastResult = $result;213break;214}215216$this->throwQueryException($this->connection);217} catch (AphrontConnectionLostQueryException $ex) {218$can_retry = ($retries > 0);219220if ($this->isInsideTransaction()) {221// Zero out the transaction state to prevent a second exception222// ("program exited with open transaction") from being thrown, since223// we're about to throw a more relevant/useful one instead.224$state = $this->getTransactionState();225while ($state->getDepth()) {226$state->decreaseDepth();227}228229$can_retry = false;230}231232if ($this->isHoldingAnyLock()) {233$this->forgetAllLocks();234$can_retry = false;235}236237$this->close();238239if (!$can_retry) {240throw $ex;241}242}243}244}245246public function executeRawQueries(array $raw_queries) {247if (!$raw_queries) {248return array();249}250251$is_write = false;252foreach ($raw_queries as $key => $raw_query) {253$is_write = $is_write || $this->checkWrite($raw_query);254$raw_queries[$key] = rtrim($raw_query, "\r\n\t ;");255}256257$profiler = PhutilServiceProfiler::getInstance();258$call_id = $profiler->beginServiceCall(259array(260'type' => 'multi-query',261'config' => $this->configuration,262'queries' => $raw_queries,263'write' => $is_write,264));265266$results = $this->rawQueries($raw_queries);267268$profiler->endServiceCall($call_id, array());269270return $results;271}272273protected function processResult($result) {274if (!$result) {275try {276$this->throwQueryException($this->requireConnection());277} catch (Exception $ex) {278return $ex;279}280} else if (is_bool($result)) {281return $this->getAffectedRows();282}283$rows = array();284while (($row = $this->fetchAssoc($result))) {285$rows[] = $row;286}287$this->freeResult($result);288return $rows;289}290291protected function checkWrite($raw_query) {292// NOTE: The opening "(" allows queries in the form of:293//294// (SELECT ...) UNION (SELECT ...)295$is_write = !preg_match('/^[(]*(SELECT|SHOW|EXPLAIN)\s/', $raw_query);296if ($is_write) {297if ($this->getReadOnly()) {298throw new Exception(299pht(300'Attempting to issue a write query on a read-only '.301'connection (to database "%s")!',302$this->getConfiguration('database')));303}304AphrontWriteGuard::willWrite();305return true;306}307308return false;309}310311protected function throwQueryException($connection) {312if ($this->nextError) {313$errno = $this->nextError;314$error = pht('Simulated error.');315$this->nextError = null;316} else {317$errno = $this->getErrorCode($connection);318$error = $this->getErrorDescription($connection);319}320$this->throwQueryCodeException($errno, $error);321}322323private function throwCommonException($errno, $error) {324$message = pht('#%d: %s', $errno, $error);325326switch ($errno) {327case 2013: // Connection Dropped328throw new AphrontConnectionLostQueryException($message);329case 2006: // Gone Away330$more = pht(331'This error may occur if your configured MySQL "wait_timeout" or '.332'"max_allowed_packet" values are too small. This may also indicate '.333'that something used the MySQL "KILL <process>" command to kill '.334'the connection running the query.');335throw new AphrontConnectionLostQueryException("{$message}\n\n{$more}");336case 1213: // Deadlock337throw new AphrontDeadlockQueryException($message);338case 1205: // Lock wait timeout exceeded339throw new AphrontLockTimeoutQueryException($message);340case 1062: // Duplicate Key341// NOTE: In some versions of MySQL we get a key name back here, but342// older versions just give us a key index ("key 2") so it's not343// portable to parse the key out of the error and attach it to the344// exception.345throw new AphrontDuplicateKeyQueryException($message);346case 1044: // Access denied to database347case 1142: // Access denied to table348case 1143: // Access denied to column349case 1227: // Access denied (e.g., no SUPER for SHOW SLAVE STATUS).350351// See T13622. Try to help users figure out that this is a GRANT352// problem.353354$more = pht(355'This error usually indicates that you need to "GRANT" the '.356'MySQL user additional permissions. See "GRANT" in the MySQL '.357'manual for help.');358359throw new AphrontAccessDeniedQueryException("{$message}\n\n{$more}");360case 1045: // Access denied (auth)361throw new AphrontInvalidCredentialsQueryException($message);362case 1146: // No such table363case 1049: // No such database364case 1054: // Unknown column "..." in field list365throw new AphrontSchemaQueryException($message);366}367368// TODO: 1064 is syntax error, and quite terrible in production.369370return null;371}372373protected function throwConnectionException($errno, $error, $user, $host) {374$this->throwCommonException($errno, $error);375376$message = pht(377'Attempt to connect to %s@%s failed with error #%d: %s.',378$user,379$host,380$errno,381$error);382383throw new AphrontConnectionQueryException($message, $errno);384}385386387protected function throwQueryCodeException($errno, $error) {388$this->throwCommonException($errno, $error);389390$message = pht(391'#%d: %s',392$errno,393$error);394395throw new AphrontQueryException($message, $errno);396}397398/**399* Force the next query to fail with a simulated error. This should be used400* ONLY for unit tests.401*/402public function simulateErrorOnNextQuery($error) {403$this->nextError = $error;404return $this;405}406407/**408* Check inserts for characters outside of the BMP. Even with the strictest409* settings, MySQL will silently truncate data when it encounters these, which410* can lead to data loss and security problems.411*/412protected function validateUTF8String($string) {413if (phutil_is_utf8($string)) {414return;415}416417throw new AphrontCharacterSetQueryException(418pht(419'Attempting to construct a query using a non-utf8 string when '.420'utf8 is expected. Use the `%%B` conversion to escape binary '.421'strings data.'));422}423424}425426427