Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php
12242 views
1
<?php
2
3
/**
4
* @phutil-external-symbol class mysqli
5
*/
6
final 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
// See T13588. In PHP 8.1, the default "report mode" for MySQLi has
61
// changed, which causes MySQLi to raise exceptions. Disable exceptions
62
// to align behavior with older default behavior under MySQLi, which
63
// this code expects. Plausibly, this code could be updated to use
64
// MySQLi exceptions to handle errors under a wider range of PHP versions.
65
mysqli_report(MYSQLI_REPORT_OFF);
66
67
$conn = mysqli_init();
68
69
$timeout = $this->getConfiguration('timeout');
70
if ($timeout) {
71
$conn->options(MYSQLI_OPT_CONNECT_TIMEOUT, $timeout);
72
}
73
74
if ($this->getPersistent()) {
75
$host = 'p:'.$host;
76
}
77
78
$trap = new PhutilErrorTrap();
79
80
$ok = @$conn->real_connect(
81
$host,
82
$user,
83
$pass,
84
$database,
85
$port);
86
87
$call_error = $trap->getErrorsAsString();
88
$trap->destroy();
89
90
$errno = $conn->connect_errno;
91
if ($errno) {
92
$error = $conn->connect_error;
93
$this->throwConnectionException($errno, $error, $user, $host);
94
}
95
96
// See T13403. If the parameters to "real_connect()" are wrong, it may
97
// fail without setting an error code. In this case, raise a generic
98
// exception. (One way to reproduce this is to pass a string to the
99
// "port" parameter.)
100
101
if (!$ok) {
102
if (strlen($call_error)) {
103
$message = pht(
104
'mysqli->real_connect() failed: %s',
105
$call_error);
106
} else {
107
$message = pht(
108
'mysqli->real_connect() failed, but did not set an error code '.
109
'or emit a message.');
110
}
111
112
$this->throwConnectionException(
113
self::CALLERROR_CONNECT,
114
$message,
115
$user,
116
$host);
117
}
118
119
// See T13238. Attempt to prevent "LOAD DATA LOCAL INFILE", which allows a
120
// malicious server to ask the client for any file. At time of writing,
121
// this option MUST be set after "real_connect()" on all PHP versions.
122
$conn->options(MYSQLI_OPT_LOCAL_INFILE, 0);
123
124
$this->connectionOpen = true;
125
126
$ok = @$conn->set_charset('utf8mb4');
127
if (!$ok) {
128
$ok = $conn->set_charset('binary');
129
}
130
131
return $conn;
132
}
133
134
protected function rawQuery($raw_query) {
135
$conn = $this->requireConnection();
136
$time_limit = $this->getQueryTimeout();
137
138
// If we have a query time limit, run this query synchronously but use
139
// the async API. This allows us to kill queries which take too long
140
// without requiring any configuration on the server side.
141
if ($time_limit && $this->supportsAsyncQueries()) {
142
$conn->query($raw_query, MYSQLI_ASYNC);
143
144
$read = array($conn);
145
$error = array($conn);
146
$reject = array($conn);
147
148
$result = mysqli::poll($read, $error, $reject, $time_limit);
149
150
if ($result === false) {
151
$this->closeConnection();
152
throw new Exception(
153
pht('Failed to poll mysqli connection!'));
154
} else if ($result === 0) {
155
$this->closeConnection();
156
throw new AphrontQueryTimeoutQueryException(
157
pht(
158
'Query timed out after %s second(s)!',
159
new PhutilNumber($time_limit)));
160
}
161
162
return @$conn->reap_async_query();
163
}
164
165
$trap = new PhutilErrorTrap();
166
167
$result = @$conn->query($raw_query);
168
169
$err = $trap->getErrorsAsString();
170
$trap->destroy();
171
172
// See T13238 and PHI1014. Sometimes, the call to "$conn->query()" may fail
173
// without setting an error code on the connection. One way to reproduce
174
// this is to use "LOAD DATA LOCAL INFILE" with "mysqli.allow_local_infile"
175
// disabled.
176
177
// If we have no result and no error code, raise a synthetic query error
178
// with whatever error message was raised as a local PHP warning.
179
180
if (!$result) {
181
$error_code = $this->getErrorCode($conn);
182
if (!$error_code) {
183
if (strlen($err)) {
184
$message = $err;
185
} else {
186
$message = pht(
187
'Call to "mysqli->query()" failed, but did not set an error '.
188
'code or emit an error message.');
189
}
190
$this->throwQueryCodeException(self::CALLERROR_QUERY, $message);
191
}
192
}
193
194
return $result;
195
}
196
197
protected function rawQueries(array $raw_queries) {
198
$conn = $this->requireConnection();
199
200
$have_result = false;
201
$results = array();
202
203
foreach ($raw_queries as $key => $raw_query) {
204
if (!$have_result) {
205
// End line in front of semicolon to allow single line comments at the
206
// end of queries.
207
$have_result = $conn->multi_query(implode("\n;\n\n", $raw_queries));
208
} else {
209
$have_result = $conn->next_result();
210
}
211
212
array_shift($raw_queries);
213
214
$result = $conn->store_result();
215
if (!$result && !$this->getErrorCode($conn)) {
216
$result = true;
217
}
218
$results[$key] = $this->processResult($result);
219
}
220
221
if ($conn->more_results()) {
222
throw new Exception(
223
pht('There are some results left in the result set.'));
224
}
225
226
return $results;
227
}
228
229
protected function freeResult($result) {
230
$result->free_result();
231
}
232
233
protected function fetchAssoc($result) {
234
return $result->fetch_assoc();
235
}
236
237
protected function getErrorCode($connection) {
238
return $connection->errno;
239
}
240
241
protected function getErrorDescription($connection) {
242
return $connection->error;
243
}
244
245
public function supportsAsyncQueries() {
246
return defined('MYSQLI_ASYNC');
247
}
248
249
public function asyncQuery($raw_query) {
250
$this->checkWrite($raw_query);
251
$async = $this->beginAsyncConnection();
252
$async->query($raw_query, MYSQLI_ASYNC);
253
return $async;
254
}
255
256
public static function resolveAsyncQueries(array $conns, array $asyncs) {
257
assert_instances_of($conns, __CLASS__);
258
assert_instances_of($asyncs, 'mysqli');
259
260
$read = $error = $reject = array();
261
foreach ($asyncs as $async) {
262
$read[] = $error[] = $reject[] = $async;
263
}
264
265
if (!mysqli::poll($read, $error, $reject, 0)) {
266
return array();
267
}
268
269
$results = array();
270
foreach ($read as $async) {
271
$key = array_search($async, $asyncs, $strict = true);
272
$conn = $conns[$key];
273
$conn->endAsyncConnection($async);
274
$results[$key] = $conn->processResult($async->reap_async_query());
275
}
276
return $results;
277
}
278
279
}
280
281