@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 425 lines 12 kB view raw
1<?php 2 3abstract class AphrontBaseMySQLDatabaseConnection 4 extends AphrontDatabaseConnection { 5 6 private $configuration; 7 private $connection; 8 private $connectionPool = array(); 9 private $lastResult; 10 11 private $nextError; 12 13 const CALLERROR_QUERY = 777777; 14 const CALLERROR_CONNECT = 777778; 15 16 abstract protected function connect(); 17 abstract protected function rawQuery($raw_query); 18 abstract protected function rawQueries(array $raw_queries); 19 abstract protected function fetchAssoc($result); 20 abstract protected function getErrorCode($connection); 21 abstract protected function getErrorDescription($connection); 22 abstract protected function closeConnection(); 23 abstract protected function freeResult($result); 24 25 public function __construct(array $configuration) { 26 $this->configuration = $configuration; 27 } 28 29 public function __clone() { 30 $this->establishConnection(); 31 } 32 33 public function openConnection() { 34 $this->requireConnection(); 35 } 36 37 public function close() { 38 if ($this->lastResult) { 39 $this->lastResult = null; 40 } 41 if ($this->connection) { 42 $this->closeConnection(); 43 $this->connection = null; 44 } 45 } 46 47 public function escapeColumnName($name) { 48 return '`'.str_replace('`', '``', $name).'`'; 49 } 50 51 52 public function escapeMultilineComment($comment) { 53 // These can either terminate a comment, confuse the hell out of the parser, 54 // make MySQL execute the comment as a query, or, in the case of semicolon, 55 // are quasi-dangerous because the semicolon could turn a broken query into 56 // a working query plus an ignored query. 57 58 static $map = array( 59 '--' => '(DOUBLEDASH)', 60 '*/' => '(STARSLASH)', 61 '//' => '(SLASHSLASH)', 62 '#' => '(HASH)', 63 '!' => '(BANG)', 64 ';' => '(SEMICOLON)', 65 ); 66 67 $comment = str_replace( 68 array_keys($map), 69 array_values($map), 70 $comment); 71 72 // For good measure, kill anything else that isn't a nice printable 73 // character. 74 $comment = preg_replace('/[^\x20-\x7F]+/', ' ', $comment); 75 76 return '/* '.$comment.' */'; 77 } 78 79 public function escapeStringForLikeClause($value) { 80 $value = phutil_string_cast($value); 81 $value = addcslashes($value, '\%_'); 82 $value = $this->escapeUTF8String($value); 83 return $value; 84 } 85 86 protected function getConfiguration($key, $default = null) { 87 return idx($this->configuration, $key, $default); 88 } 89 90 private function establishConnection() { 91 $host = $this->getConfiguration('host'); 92 $database = $this->getConfiguration('database'); 93 94 $profiler = PhutilServiceProfiler::getInstance(); 95 $call_id = $profiler->beginServiceCall( 96 array( 97 'type' => 'connect', 98 'host' => $host, 99 'database' => $database, 100 )); 101 102 // If we receive these errors, we'll retry the connection up to the 103 // retry limit. For other errors, we'll fail immediately. 104 $retry_codes = array( 105 // "Connection Timeout" 106 2002 => true, 107 108 // "Unable to Connect" 109 2003 => true, 110 ); 111 112 $max_retries = max(1, $this->getConfiguration('retries', 3)); 113 for ($attempt = 1; $attempt <= $max_retries; $attempt++) { 114 try { 115 $conn = $this->connect(); 116 $profiler->endServiceCall($call_id, array()); 117 break; 118 } catch (AphrontQueryException $ex) { 119 $code = $ex->getCode(); 120 if (($attempt < $max_retries) && isset($retry_codes[$code])) { 121 $message = pht( 122 'Retrying database connection to "%s" after connection '. 123 'failure (attempt %d; "%s"; error #%d): %s', 124 $host, 125 $attempt, 126 get_class($ex), 127 $code, 128 $ex->getMessage()); 129 130 // See T13403. If we're silenced with the "@" operator, don't log 131 // this connection attempt. This keeps things quiet if we're 132 // running a setup workflow like "bin/config" and expect that the 133 // database credentials will often be incorrect. 134 135 if (error_reporting()) { 136 phlog($message); 137 } 138 } else { 139 $profiler->endServiceCall($call_id, array()); 140 throw $ex; 141 } 142 } 143 } 144 145 $this->connection = $conn; 146 } 147 148 protected function requireConnection() { 149 if (!$this->connection) { 150 if ($this->connectionPool) { 151 $this->connection = array_pop($this->connectionPool); 152 } else { 153 $this->establishConnection(); 154 } 155 } 156 return $this->connection; 157 } 158 159 protected function beginAsyncConnection() { 160 $connection = $this->requireConnection(); 161 $this->connection = null; 162 return $connection; 163 } 164 165 protected function endAsyncConnection($connection) { 166 if ($this->connection) { 167 $this->connectionPool[] = $this->connection; 168 } 169 $this->connection = $connection; 170 } 171 172 public function selectAllResults() { 173 $result = array(); 174 $res = $this->lastResult; 175 if ($res == null) { 176 throw new Exception(pht('No query result to fetch from!')); 177 } 178 while (($row = $this->fetchAssoc($res))) { 179 $result[] = $row; 180 } 181 return $result; 182 } 183 184 public function executeQuery(PhutilQueryString $query) { 185 $display_query = $query->getMaskedString(); 186 $raw_query = $query->getUnmaskedString(); 187 188 $this->lastResult = null; 189 $retries = max(1, $this->getConfiguration('retries', 3)); 190 while ($retries--) { 191 try { 192 $this->requireConnection(); 193 $is_write = $this->checkWrite($raw_query); 194 195 $profiler = PhutilServiceProfiler::getInstance(); 196 $call_id = $profiler->beginServiceCall( 197 array( 198 'type' => 'query', 199 'config' => $this->configuration, 200 'query' => $display_query, 201 'write' => $is_write, 202 )); 203 204 $result = $this->rawQuery($raw_query); 205 206 $profiler->endServiceCall($call_id, array()); 207 208 if ($this->nextError) { 209 $result = null; 210 } 211 212 if ($result) { 213 $this->lastResult = $result; 214 break; 215 } 216 217 $this->throwQueryException($this->connection); 218 } catch (AphrontConnectionLostQueryException $ex) { 219 $can_retry = ($retries > 0); 220 221 if ($this->isInsideTransaction()) { 222 // Zero out the transaction state to prevent a second exception 223 // ("program exited with open transaction") from being thrown, since 224 // we're about to throw a more relevant/useful one instead. 225 $state = $this->getTransactionState(); 226 while ($state->getDepth()) { 227 $state->decreaseDepth(); 228 } 229 230 $can_retry = false; 231 } 232 233 if ($this->isHoldingAnyLock()) { 234 $this->forgetAllLocks(); 235 $can_retry = false; 236 } 237 238 $this->close(); 239 240 if (!$can_retry) { 241 throw $ex; 242 } 243 } 244 } 245 } 246 247 public function executeRawQueries(array $raw_queries) { 248 if (!$raw_queries) { 249 return array(); 250 } 251 252 $is_write = false; 253 foreach ($raw_queries as $key => $raw_query) { 254 $is_write = $is_write || $this->checkWrite($raw_query); 255 $raw_queries[$key] = rtrim($raw_query, "\r\n\t ;"); 256 } 257 258 $profiler = PhutilServiceProfiler::getInstance(); 259 $call_id = $profiler->beginServiceCall( 260 array( 261 'type' => 'multi-query', 262 'config' => $this->configuration, 263 'queries' => $raw_queries, 264 'write' => $is_write, 265 )); 266 267 $results = $this->rawQueries($raw_queries); 268 269 $profiler->endServiceCall($call_id, array()); 270 271 return $results; 272 } 273 274 protected function processResult($result) { 275 if (!$result) { 276 try { 277 $this->throwQueryException($this->requireConnection()); 278 } catch (Exception $ex) { 279 return $ex; 280 } 281 } else if (is_bool($result)) { 282 return $this->getAffectedRows(); 283 } 284 $rows = array(); 285 while (($row = $this->fetchAssoc($result))) { 286 $rows[] = $row; 287 } 288 $this->freeResult($result); 289 return $rows; 290 } 291 292 protected function checkWrite($raw_query) { 293 // NOTE: The opening "(" allows queries in the form of: 294 // 295 // (SELECT ...) UNION (SELECT ...) 296 $is_write = !preg_match('/^[(]*(SELECT|SHOW|EXPLAIN)\s/', $raw_query); 297 if ($is_write) { 298 if ($this->getReadOnly()) { 299 throw new Exception( 300 pht( 301 'Attempting to issue a write query on a read-only '. 302 'connection (to database "%s")!', 303 $this->getConfiguration('database'))); 304 } 305 AphrontWriteGuard::willWrite(); 306 return true; 307 } 308 309 return false; 310 } 311 312 protected function throwQueryException($connection) { 313 if ($this->nextError) { 314 $errno = $this->nextError; 315 $error = pht('Simulated error.'); 316 $this->nextError = null; 317 } else { 318 $errno = $this->getErrorCode($connection); 319 $error = $this->getErrorDescription($connection); 320 } 321 $this->throwQueryCodeException($errno, $error); 322 } 323 324 private function throwCommonException($errno, $error) { 325 $message = pht('#%d: %s', $errno, $error); 326 327 switch ($errno) { 328 case 2013: // Connection Dropped 329 throw new AphrontConnectionLostQueryException($message); 330 case 2006: // Gone Away 331 $more = pht( 332 'This error may occur if your configured MySQL "wait_timeout" or '. 333 '"max_allowed_packet" values are too small. This may also indicate '. 334 'that something used the MySQL "KILL <process>" command to kill '. 335 'the connection running the query.'); 336 throw new AphrontConnectionLostQueryException("{$message}\n\n{$more}"); 337 case 1213: // Deadlock 338 throw new AphrontDeadlockQueryException($message); 339 case 1205: // Lock wait timeout exceeded 340 throw new AphrontLockTimeoutQueryException($message); 341 case 1062: // Duplicate Key 342 // NOTE: In some versions of MySQL we get a key name back here, but 343 // older versions just give us a key index ("key 2") so it's not 344 // portable to parse the key out of the error and attach it to the 345 // exception. 346 throw new AphrontDuplicateKeyQueryException($message); 347 case 1044: // Access denied to database 348 case 1142: // Access denied to table 349 case 1143: // Access denied to column 350 case 1227: // Access denied (e.g., no SUPER for SHOW REPLICA STATUS). 351 352 // See T13622. Try to help users figure out that this is a GRANT 353 // problem. 354 355 $more = pht( 356 'This error usually indicates that you need to "GRANT" the '. 357 'MySQL user additional permissions. See "GRANT" in the MySQL '. 358 'manual for help.'); 359 360 throw new AphrontAccessDeniedQueryException("{$message}\n\n{$more}"); 361 case 1045: // Access denied (auth) 362 throw new AphrontInvalidCredentialsQueryException($message); 363 case 1146: // No such table 364 case 1049: // No such database 365 case 1054: // Unknown column "..." in field list 366 throw new AphrontSchemaQueryException($message); 367 } 368 369 // TODO: 1064 is syntax error, and quite terrible in production. 370 371 return null; 372 } 373 374 protected function throwConnectionException($errno, $error, $user, $host) { 375 $this->throwCommonException($errno, $error); 376 377 $message = pht( 378 'Attempt to connect to %s@%s failed with error #%d: %s.', 379 $user, 380 $host, 381 $errno, 382 $error); 383 384 throw new AphrontConnectionQueryException($message, $errno); 385 } 386 387 388 protected function throwQueryCodeException($errno, $error) { 389 $this->throwCommonException($errno, $error); 390 391 $message = pht( 392 '#%d: %s', 393 $errno, 394 $error); 395 396 throw new AphrontQueryException($message, $errno); 397 } 398 399 /** 400 * Force the next query to fail with a simulated error. This should be used 401 * ONLY for unit tests. 402 */ 403 public function simulateErrorOnNextQuery($error) { 404 $this->nextError = $error; 405 return $this; 406 } 407 408 /** 409 * Check inserts for characters outside of the BMP. Even with the strictest 410 * settings, MySQL will silently truncate data when it encounters these, which 411 * can lead to data loss and security problems. 412 */ 413 protected function validateUTF8String($string) { 414 if (phutil_is_utf8($string)) { 415 return; 416 } 417 418 throw new AphrontCharacterSetQueryException( 419 pht( 420 'Attempting to construct a query using a non-utf8 string when '. 421 'utf8 is expected. Use the `%%B` conversion to escape binary '. 422 'strings data.')); 423 } 424 425}