Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php
12242 views
1
<?php
2
3
abstract 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 SLAVE 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
}
426
427