@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 277 lines 7.9 kB view raw
1<?php 2 3/** 4 * @phutil-external-symbol class mysqli 5 */ 6final class AphrontMySQLiDatabaseConnection 7 extends AphrontBaseMySQLDatabaseConnection { 8 9 private $connectionOpen = false; 10 11 public function escapeUTF8String($string) { 12 $this->validateUTF8String($string); 13 return $this->escapeBinaryString($string); 14 } 15 16 public function escapeBinaryString($string) { 17 return $this->requireConnection()->escape_string($string); 18 } 19 20 public function getInsertID() { 21 return $this->requireConnection()->insert_id; 22 } 23 24 public function getAffectedRows() { 25 return $this->requireConnection()->affected_rows; 26 } 27 28 protected function closeConnection() { 29 if ($this->connectionOpen) { 30 $this->requireConnection()->close(); 31 $this->connectionOpen = false; 32 } 33 } 34 35 protected function connect() { 36 if (!class_exists('mysqli', false)) { 37 throw new Exception(pht( 38 'About to call new %s, but the PHP MySQLi extension is not available!', 39 'mysqli()')); 40 } 41 42 $user = $this->getConfiguration('user'); 43 $host = $this->getConfiguration('host'); 44 $port = $this->getConfiguration('port'); 45 $database = $this->getConfiguration('database'); 46 47 $pass = $this->getConfiguration('pass'); 48 if ($pass instanceof PhutilOpaqueEnvelope) { 49 $pass = $pass->openEnvelope(); 50 } 51 52 // If the host is "localhost", the port is ignored and mysqli attempts to 53 // connect over a socket. 54 if ($port) { 55 if ($host === 'localhost' || $host === null) { 56 $host = '127.0.0.1'; 57 } 58 } 59 60 // In PHP 8.1, the default "report mode" for MySQLi has changed, which 61 // causes MySQLi to raise exceptions. Disable exceptions to align behavior 62 // with older default behavior under MySQLi, which this code expects. 63 // https://www.php.net/manual/mysqli-driver.report-mode.php 64 // https://www.php.net/manual/migration81.incompatible.php#migration81.incompatible.mysqli 65 // TODO: Plausibly, this code could be updated to use MySQLi exceptions 66 // to handle errors. See https://we.phorge.it/T16341 67 mysqli_report(MYSQLI_REPORT_OFF); 68 69 $conn = mysqli_init(); 70 71 $timeout = $this->getConfiguration('timeout'); 72 if ($timeout) { 73 $conn->options(MYSQLI_OPT_CONNECT_TIMEOUT, $timeout); 74 } 75 76 if ($this->getPersistent()) { 77 $host = 'p:'.$host; 78 } 79 80 $trap = new PhutilErrorTrap(); 81 82 $ok = @$conn->real_connect( 83 $host, 84 $user, 85 $pass, 86 $database, 87 $port); 88 89 $call_error = $trap->getErrorsAsString(); 90 $trap->destroy(); 91 92 $errno = $conn->connect_errno; 93 if ($errno) { 94 $error = $conn->connect_error; 95 $this->throwConnectionException($errno, $error, $user, $host); 96 } 97 98 // See T13403. If the parameters to "real_connect()" are wrong, it may 99 // fail without setting an error code. In this case, raise a generic 100 // exception. (One way to reproduce this is to pass a string to the 101 // "port" parameter.) 102 103 if (!$ok) { 104 if (strlen($call_error)) { 105 $message = pht( 106 'mysqli->real_connect() failed: %s', 107 $call_error); 108 } else { 109 $message = pht( 110 'mysqli->real_connect() failed, but did not set an error code '. 111 'or emit a message.'); 112 } 113 114 $this->throwConnectionException( 115 self::CALLERROR_CONNECT, 116 $message, 117 $user, 118 $host); 119 } 120 121 // See T13238. Attempt to prevent "LOAD DATA LOCAL INFILE", which allows a 122 // malicious server to ask the client for any file. At time of writing, 123 // this option MUST be set after "real_connect()" on all PHP versions. 124 $conn->options(MYSQLI_OPT_LOCAL_INFILE, 0); 125 126 $this->connectionOpen = true; 127 128 $ok = @$conn->set_charset('utf8mb4'); 129 if (!$ok) { 130 $ok = $conn->set_charset('binary'); 131 } 132 133 return $conn; 134 } 135 136 protected function rawQuery($raw_query) { 137 $conn = $this->requireConnection(); 138 $time_limit = $this->getQueryTimeout(); 139 140 // If we have a query time limit, run this query synchronously but use 141 // the async API. This allows us to kill queries which take too long 142 // without requiring any configuration on the server side. 143 if ($time_limit) { 144 $conn->query($raw_query, MYSQLI_ASYNC); 145 146 $read = array($conn); 147 $error = array($conn); 148 $reject = array($conn); 149 150 $result = mysqli::poll($read, $error, $reject, $time_limit); 151 152 if ($result === false) { 153 $this->closeConnection(); 154 throw new Exception( 155 pht('Failed to poll mysqli connection!')); 156 } else if ($result === 0) { 157 $this->closeConnection(); 158 throw new AphrontQueryTimeoutQueryException( 159 pht( 160 'Query timed out after %s second(s)!', 161 new PhutilNumber($time_limit))); 162 } 163 164 return @$conn->reap_async_query(); 165 } 166 167 $trap = new PhutilErrorTrap(); 168 169 $result = @$conn->query($raw_query); 170 171 $err = $trap->getErrorsAsString(); 172 $trap->destroy(); 173 174 // See T13238 and PHI1014. Sometimes, the call to "$conn->query()" may fail 175 // without setting an error code on the connection. One way to reproduce 176 // this is to use "LOAD DATA LOCAL INFILE" with "mysqli.allow_local_infile" 177 // disabled. 178 179 // If we have no result and no error code, raise a synthetic query error 180 // with whatever error message was raised as a local PHP warning. 181 182 if (!$result) { 183 $error_code = $this->getErrorCode($conn); 184 if (!$error_code) { 185 if (strlen($err)) { 186 $message = $err; 187 } else { 188 $message = pht( 189 'Call to "mysqli->query()" failed, but did not set an error '. 190 'code or emit an error message.'); 191 } 192 $this->throwQueryCodeException(self::CALLERROR_QUERY, $message); 193 } 194 } 195 196 return $result; 197 } 198 199 protected function rawQueries(array $raw_queries) { 200 $conn = $this->requireConnection(); 201 202 $have_result = false; 203 $results = array(); 204 205 foreach ($raw_queries as $key => $raw_query) { 206 if (!$have_result) { 207 // End line in front of semicolon to allow single line comments at the 208 // end of queries. 209 $have_result = $conn->multi_query(implode("\n;\n\n", $raw_queries)); 210 } else { 211 $have_result = $conn->next_result(); 212 } 213 214 array_shift($raw_queries); 215 216 $result = $conn->store_result(); 217 if (!$result && !$this->getErrorCode($conn)) { 218 $result = true; 219 } 220 $results[$key] = $this->processResult($result); 221 } 222 223 if ($conn->more_results()) { 224 throw new Exception( 225 pht('There are some results left in the result set.')); 226 } 227 228 return $results; 229 } 230 231 protected function freeResult($result) { 232 $result->free_result(); 233 } 234 235 protected function fetchAssoc($result) { 236 return $result->fetch_assoc(); 237 } 238 239 protected function getErrorCode($connection) { 240 return $connection->errno; 241 } 242 243 protected function getErrorDescription($connection) { 244 return $connection->error; 245 } 246 247 public function asyncQuery($raw_query) { 248 $this->checkWrite($raw_query); 249 $async = $this->beginAsyncConnection(); 250 $async->query($raw_query, MYSQLI_ASYNC); 251 return $async; 252 } 253 254 public static function resolveAsyncQueries(array $conns, array $asyncs) { 255 assert_instances_of($conns, self::class); 256 assert_instances_of($asyncs, 'mysqli'); 257 258 $read = $error = $reject = array(); 259 foreach ($asyncs as $async) { 260 $read[] = $error[] = $reject[] = $async; 261 } 262 263 if (!mysqli::poll($read, $error, $reject, 0)) { 264 return array(); 265 } 266 267 $results = array(); 268 foreach ($read as $async) { 269 $key = array_search($async, $asyncs, $strict = true); 270 $conn = $conns[$key]; 271 $conn->endAsyncConnection($async); 272 $results[$key] = $conn->processResult($async->reap_async_query()); 273 } 274 return $results; 275 } 276 277}