Path: blob/master/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php
12242 views
<?php12/**3* @phutil-external-symbol class mysqli4*/5final class AphrontMySQLiDatabaseConnection6extends AphrontBaseMySQLDatabaseConnection {78private $connectionOpen = false;910public function escapeUTF8String($string) {11$this->validateUTF8String($string);12return $this->escapeBinaryString($string);13}1415public function escapeBinaryString($string) {16return $this->requireConnection()->escape_string($string);17}1819public function getInsertID() {20return $this->requireConnection()->insert_id;21}2223public function getAffectedRows() {24return $this->requireConnection()->affected_rows;25}2627protected function closeConnection() {28if ($this->connectionOpen) {29$this->requireConnection()->close();30$this->connectionOpen = false;31}32}3334protected function connect() {35if (!class_exists('mysqli', false)) {36throw new Exception(pht(37'About to call new %s, but the PHP MySQLi extension is not available!',38'mysqli()'));39}4041$user = $this->getConfiguration('user');42$host = $this->getConfiguration('host');43$port = $this->getConfiguration('port');44$database = $this->getConfiguration('database');4546$pass = $this->getConfiguration('pass');47if ($pass instanceof PhutilOpaqueEnvelope) {48$pass = $pass->openEnvelope();49}5051// If the host is "localhost", the port is ignored and mysqli attempts to52// connect over a socket.53if ($port) {54if ($host === 'localhost' || $host === null) {55$host = '127.0.0.1';56}57}5859// See T13588. In PHP 8.1, the default "report mode" for MySQLi has60// changed, which causes MySQLi to raise exceptions. Disable exceptions61// to align behavior with older default behavior under MySQLi, which62// this code expects. Plausibly, this code could be updated to use63// MySQLi exceptions to handle errors under a wider range of PHP versions.64mysqli_report(MYSQLI_REPORT_OFF);6566$conn = mysqli_init();6768$timeout = $this->getConfiguration('timeout');69if ($timeout) {70$conn->options(MYSQLI_OPT_CONNECT_TIMEOUT, $timeout);71}7273if ($this->getPersistent()) {74$host = 'p:'.$host;75}7677$trap = new PhutilErrorTrap();7879$ok = @$conn->real_connect(80$host,81$user,82$pass,83$database,84$port);8586$call_error = $trap->getErrorsAsString();87$trap->destroy();8889$errno = $conn->connect_errno;90if ($errno) {91$error = $conn->connect_error;92$this->throwConnectionException($errno, $error, $user, $host);93}9495// See T13403. If the parameters to "real_connect()" are wrong, it may96// fail without setting an error code. In this case, raise a generic97// exception. (One way to reproduce this is to pass a string to the98// "port" parameter.)99100if (!$ok) {101if (strlen($call_error)) {102$message = pht(103'mysqli->real_connect() failed: %s',104$call_error);105} else {106$message = pht(107'mysqli->real_connect() failed, but did not set an error code '.108'or emit a message.');109}110111$this->throwConnectionException(112self::CALLERROR_CONNECT,113$message,114$user,115$host);116}117118// See T13238. Attempt to prevent "LOAD DATA LOCAL INFILE", which allows a119// malicious server to ask the client for any file. At time of writing,120// this option MUST be set after "real_connect()" on all PHP versions.121$conn->options(MYSQLI_OPT_LOCAL_INFILE, 0);122123$this->connectionOpen = true;124125$ok = @$conn->set_charset('utf8mb4');126if (!$ok) {127$ok = $conn->set_charset('binary');128}129130return $conn;131}132133protected function rawQuery($raw_query) {134$conn = $this->requireConnection();135$time_limit = $this->getQueryTimeout();136137// If we have a query time limit, run this query synchronously but use138// the async API. This allows us to kill queries which take too long139// without requiring any configuration on the server side.140if ($time_limit && $this->supportsAsyncQueries()) {141$conn->query($raw_query, MYSQLI_ASYNC);142143$read = array($conn);144$error = array($conn);145$reject = array($conn);146147$result = mysqli::poll($read, $error, $reject, $time_limit);148149if ($result === false) {150$this->closeConnection();151throw new Exception(152pht('Failed to poll mysqli connection!'));153} else if ($result === 0) {154$this->closeConnection();155throw new AphrontQueryTimeoutQueryException(156pht(157'Query timed out after %s second(s)!',158new PhutilNumber($time_limit)));159}160161return @$conn->reap_async_query();162}163164$trap = new PhutilErrorTrap();165166$result = @$conn->query($raw_query);167168$err = $trap->getErrorsAsString();169$trap->destroy();170171// See T13238 and PHI1014. Sometimes, the call to "$conn->query()" may fail172// without setting an error code on the connection. One way to reproduce173// this is to use "LOAD DATA LOCAL INFILE" with "mysqli.allow_local_infile"174// disabled.175176// If we have no result and no error code, raise a synthetic query error177// with whatever error message was raised as a local PHP warning.178179if (!$result) {180$error_code = $this->getErrorCode($conn);181if (!$error_code) {182if (strlen($err)) {183$message = $err;184} else {185$message = pht(186'Call to "mysqli->query()" failed, but did not set an error '.187'code or emit an error message.');188}189$this->throwQueryCodeException(self::CALLERROR_QUERY, $message);190}191}192193return $result;194}195196protected function rawQueries(array $raw_queries) {197$conn = $this->requireConnection();198199$have_result = false;200$results = array();201202foreach ($raw_queries as $key => $raw_query) {203if (!$have_result) {204// End line in front of semicolon to allow single line comments at the205// end of queries.206$have_result = $conn->multi_query(implode("\n;\n\n", $raw_queries));207} else {208$have_result = $conn->next_result();209}210211array_shift($raw_queries);212213$result = $conn->store_result();214if (!$result && !$this->getErrorCode($conn)) {215$result = true;216}217$results[$key] = $this->processResult($result);218}219220if ($conn->more_results()) {221throw new Exception(222pht('There are some results left in the result set.'));223}224225return $results;226}227228protected function freeResult($result) {229$result->free_result();230}231232protected function fetchAssoc($result) {233return $result->fetch_assoc();234}235236protected function getErrorCode($connection) {237return $connection->errno;238}239240protected function getErrorDescription($connection) {241return $connection->error;242}243244public function supportsAsyncQueries() {245return defined('MYSQLI_ASYNC');246}247248public function asyncQuery($raw_query) {249$this->checkWrite($raw_query);250$async = $this->beginAsyncConnection();251$async->query($raw_query, MYSQLI_ASYNC);252return $async;253}254255public static function resolveAsyncQueries(array $conns, array $asyncs) {256assert_instances_of($conns, __CLASS__);257assert_instances_of($asyncs, 'mysqli');258259$read = $error = $reject = array();260foreach ($asyncs as $async) {261$read[] = $error[] = $reject[] = $async;262}263264if (!mysqli::poll($read, $error, $reject, 0)) {265return array();266}267268$results = array();269foreach ($read as $async) {270$key = array_search($async, $asyncs, $strict = true);271$conn = $conns[$key];272$conn->endAsyncConnection($async);273$results[$key] = $conn->processResult($async->reap_async_query());274}275return $results;276}277278}279280281