1 | <?php |
||||
2 | namespace Elgg; |
||||
3 | |||||
4 | use Doctrine\DBAL\DriverManager; |
||||
5 | use Doctrine\DBAL\Connection; |
||||
6 | use Doctrine\DBAL\Driver\Statement; |
||||
7 | use Doctrine\DBAL\Driver\ServerInfoAwareConnection; |
||||
8 | use Doctrine\DBAL\Query\QueryBuilder; |
||||
9 | use Elgg\Database\DbConfig as DbConfig; |
||||
10 | |||||
11 | /** |
||||
12 | * The Elgg database |
||||
13 | * |
||||
14 | * @access private |
||||
15 | * @internal Use the public API functions in engine/lib/database.php |
||||
16 | * |
||||
17 | * @property-read string $prefix Elgg table prefix (read only) |
||||
18 | */ |
||||
19 | class Database { |
||||
20 | use Profilable; |
||||
21 | use Loggable; |
||||
22 | |||||
23 | const DELAYED_QUERY = 'q'; |
||||
24 | const DELAYED_TYPE = 't'; |
||||
25 | const DELAYED_HANDLER = 'h'; |
||||
26 | const DELAYED_PARAMS = 'p'; |
||||
27 | |||||
28 | /** |
||||
29 | * @var string $table_prefix Prefix for database tables |
||||
30 | */ |
||||
31 | private $table_prefix; |
||||
32 | |||||
33 | /** |
||||
34 | * @var Connection[] |
||||
35 | */ |
||||
36 | private $connections = []; |
||||
37 | |||||
38 | /** |
||||
39 | * @var int $query_count The number of queries made |
||||
40 | */ |
||||
41 | private $query_count = 0; |
||||
42 | |||||
43 | /** |
||||
44 | * Query cache for select queries. |
||||
45 | * |
||||
46 | * Queries and their results are stored in this cache as: |
||||
47 | * <code> |
||||
48 | * $DB_QUERY_CACHE[query hash] => array(result1, result2, ... resultN) |
||||
49 | * </code> |
||||
50 | * @see \Elgg\Database::getResults() for details on the hash. |
||||
51 | * |
||||
52 | * @var \Elgg\Cache\LRUCache $query_cache The cache |
||||
53 | */ |
||||
54 | private $query_cache = null; |
||||
55 | |||||
56 | /** |
||||
57 | * @var int $query_cache_size The number of queries to cache |
||||
58 | */ |
||||
59 | private $query_cache_size = 50; |
||||
60 | |||||
61 | /** |
||||
62 | * Queries are saved as an array with the DELAYED_* constants as keys. |
||||
63 | * |
||||
64 | * @see registerDelayedQuery() |
||||
65 | * |
||||
66 | * @var array $delayed_queries Queries to be run during shutdown |
||||
67 | */ |
||||
68 | private $delayed_queries = []; |
||||
69 | |||||
70 | /** |
||||
71 | * @var \Elgg\Database\DbConfig $config Database configuration |
||||
72 | */ |
||||
73 | private $config; |
||||
74 | |||||
75 | /** |
||||
76 | * Constructor |
||||
77 | * |
||||
78 | * @param DbConfig $config DB configuration |
||||
79 | */ |
||||
80 | 4417 | public function __construct(DbConfig $config) { |
|||
81 | 4417 | $this->resetConnections($config); |
|||
82 | 4417 | } |
|||
83 | |||||
84 | /** |
||||
85 | * Reset the connections with new credentials |
||||
86 | * |
||||
87 | * @param DbConfig $config DB config |
||||
88 | * |
||||
89 | * @return void |
||||
90 | */ |
||||
91 | 4417 | public function resetConnections(DbConfig $config) { |
|||
92 | 4417 | $this->connections = []; |
|||
93 | 4417 | $this->config = $config; |
|||
94 | 4417 | $this->table_prefix = $config->getTablePrefix(); |
|||
95 | 4417 | $this->enableQueryCache(); |
|||
96 | 4417 | } |
|||
97 | |||||
98 | /** |
||||
99 | * Gets (if required, also creates) a DB connection. |
||||
100 | * |
||||
101 | * @param string $type The type of link we want: "read", "write" or "readwrite". |
||||
102 | * |
||||
103 | * @return Connection |
||||
104 | * @throws \DatabaseException |
||||
105 | * @access private |
||||
106 | */ |
||||
107 | 601 | public function getConnection($type) { |
|||
108 | 601 | if (isset($this->connections[$type])) { |
|||
109 | 18 | return $this->connections[$type]; |
|||
110 | 601 | } else if (isset($this->connections['readwrite'])) { |
|||
111 | 601 | return $this->connections['readwrite']; |
|||
112 | } else { |
||||
113 | 18 | $this->setupConnections(); |
|||
114 | 18 | return $this->getConnection($type); |
|||
115 | } |
||||
116 | } |
||||
117 | |||||
118 | /** |
||||
119 | * Establish database connections |
||||
120 | * |
||||
121 | * If the configuration has been set up for multiple read/write databases, set those |
||||
122 | * links up separately; otherwise just create the one database link. |
||||
123 | * |
||||
124 | * @return void |
||||
125 | * @throws \DatabaseException |
||||
126 | * @access private |
||||
127 | */ |
||||
128 | 18 | public function setupConnections() { |
|||
129 | 18 | if ($this->config->isDatabaseSplit()) { |
|||
130 | $this->connect('read'); |
||||
131 | $this->connect('write'); |
||||
132 | } else { |
||||
133 | 18 | $this->connect('readwrite'); |
|||
134 | } |
||||
135 | 18 | } |
|||
136 | |||||
137 | /** |
||||
138 | * Establish a connection to the database server |
||||
139 | * |
||||
140 | * Connect to the database server and use the Elgg database for a particular database link |
||||
141 | * |
||||
142 | * @param string $type The type of database connection. "read", "write", or "readwrite". |
||||
143 | * |
||||
144 | * @return void |
||||
145 | * @throws \DatabaseException |
||||
146 | * @access private |
||||
147 | */ |
||||
148 | 18 | public function connect($type = "readwrite") { |
|||
149 | 18 | $conf = $this->config->getConnectionConfig($type); |
|||
150 | |||||
151 | $params = [ |
||||
152 | 18 | 'dbname' => $conf['database'], |
|||
153 | 18 | 'user' => $conf['user'], |
|||
154 | 18 | 'password' => $conf['password'], |
|||
155 | 18 | 'host' => $conf['host'], |
|||
156 | 18 | 'charset' => $conf['encoding'], |
|||
157 | 18 | 'driver' => 'pdo_mysql', |
|||
158 | ]; |
||||
159 | |||||
160 | try { |
||||
161 | 18 | $this->connections[$type] = DriverManager::getConnection($params); |
|||
162 | 18 | $this->connections[$type]->setFetchMode(\PDO::FETCH_OBJ); |
|||
163 | |||||
164 | // https://github.com/Elgg/Elgg/issues/8121 |
||||
165 | 18 | $sub_query = "SELECT REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '')"; |
|||
166 | 18 | $this->connections[$type]->exec("SET SESSION sql_mode=($sub_query);"); |
|||
167 | } catch (\Exception $e) { |
||||
168 | // http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html |
||||
169 | if ($e->getCode() == 1102 || $e->getCode() == 1049) { |
||||
170 | $msg = "Elgg couldn't select the database '{$conf['database']}'. " |
||||
171 | . "Please check that the database is created and you have access to it."; |
||||
172 | } else { |
||||
173 | $msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file."; |
||||
174 | } |
||||
175 | throw new \DatabaseException($msg); |
||||
176 | } |
||||
177 | 18 | } |
|||
178 | |||||
179 | /** |
||||
180 | * Retrieve rows from the database. |
||||
181 | * |
||||
182 | * Queries are executed with {@link \Elgg\Database::executeQuery()} and results |
||||
183 | * are retrieved with {@link \PDO::fetchObject()}. If a callback |
||||
184 | * function $callback is defined, each row will be passed as a single |
||||
185 | * argument to $callback. If no callback function is defined, the |
||||
186 | * entire result set is returned as an array. |
||||
187 | * |
||||
188 | * @param QueryBuilder|string $query The query being passed. |
||||
189 | * @param callable $callback Optionally, the name of a function to call back to on each row |
||||
190 | * @param array $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve'] |
||||
191 | * |
||||
192 | * @return array An array of database result objects or callback function results. If the query |
||||
193 | * returned nothing, an empty array. |
||||
194 | * @throws \DatabaseException |
||||
195 | */ |
||||
196 | 1211 | public function getData($query, $callback = null, array $params = []) { |
|||
197 | 1211 | return $this->getResults($query, $callback, false, $params); |
|||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
198 | } |
||||
199 | |||||
200 | /** |
||||
201 | * Retrieve a single row from the database. |
||||
202 | * |
||||
203 | * Similar to {@link \Elgg\Database::getData()} but returns only the first row |
||||
204 | * matched. If a callback function $callback is specified, the row will be passed |
||||
205 | * as the only argument to $callback. |
||||
206 | * |
||||
207 | * @param QueryBuilder|string $query The query to execute. |
||||
208 | * @param callable $callback A callback function to apply to the row |
||||
209 | * @param array $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve'] |
||||
210 | * |
||||
211 | * @return mixed A single database result object or the result of the callback function. |
||||
212 | * @throws \DatabaseException |
||||
213 | */ |
||||
214 | 682 | public function getDataRow($query, $callback = null, array $params = []) { |
|||
215 | 682 | return $this->getResults($query, $callback, true, $params); |
|||
0 ignored issues
–
show
It seems like
$callback can also be of type callable ; however, parameter $callback of Elgg\Database::getResults() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
216 | } |
||||
217 | |||||
218 | /** |
||||
219 | * Insert a row into the database. |
||||
220 | * |
||||
221 | * @note Altering the DB invalidates all queries in the query cache. |
||||
222 | * |
||||
223 | * @param QueryBuilder|string $query The query to execute. |
||||
224 | * @param array $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve'] |
||||
225 | * |
||||
226 | * @return int|false The database id of the inserted row if a AUTO_INCREMENT field is |
||||
227 | * defined, 0 if not, and false on failure. |
||||
228 | * @throws \DatabaseException |
||||
229 | */ |
||||
230 | 1077 | public function insertData($query, array $params = []) { |
|||
231 | |||||
232 | 1077 | if ($query instanceof QueryBuilder) { |
|||
233 | 1044 | $params = $query->getParameters(); |
|||
234 | 1044 | $query = $query->getSQL(); |
|||
235 | } |
||||
236 | |||||
237 | 1077 | if ($this->logger) { |
|||
238 | 1077 | $this->logger->info("DB insert query $query (params: " . print_r($params, true) . ")"); |
|||
239 | } |
||||
240 | |||||
241 | 1077 | $connection = $this->getConnection('write'); |
|||
242 | |||||
243 | 1077 | $this->invalidateQueryCache(); |
|||
244 | |||||
245 | 1077 | $this->executeQuery($query, $connection, $params); |
|||
246 | 1076 | return (int) $connection->lastInsertId(); |
|||
247 | } |
||||
248 | |||||
249 | /** |
||||
250 | * Update the database. |
||||
251 | * |
||||
252 | * @note Altering the DB invalidates all queries in the query cache. |
||||
253 | * |
||||
254 | * @note WARNING! update_data() has the 2nd and 3rd arguments reversed. |
||||
255 | * |
||||
256 | * @param QueryBuilder|string $query The query to run. |
||||
257 | * @param bool $get_num_rows Return the number of rows affected (default: false). |
||||
258 | * @param array $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve'] |
||||
259 | * |
||||
260 | * @return bool|int |
||||
261 | * @throws \DatabaseException |
||||
262 | */ |
||||
263 | 316 | public function updateData($query, $get_num_rows = false, array $params = []) { |
|||
264 | |||||
265 | 316 | if ($query instanceof QueryBuilder) { |
|||
266 | 212 | $params = $query->getParameters(); |
|||
267 | 212 | $query = $query->getSQL(); |
|||
268 | } |
||||
269 | |||||
270 | 316 | if ($this->logger) { |
|||
271 | 316 | $this->logger->info("DB update query $query (params: " . print_r($params, true) . ")"); |
|||
272 | } |
||||
273 | |||||
274 | 316 | $this->invalidateQueryCache(); |
|||
275 | |||||
276 | 316 | $stmt = $this->executeQuery($query, $this->getConnection('write'), $params); |
|||
277 | 315 | if ($get_num_rows) { |
|||
278 | 128 | return $stmt->rowCount(); |
|||
279 | } else { |
||||
280 | 228 | return true; |
|||
281 | } |
||||
282 | } |
||||
283 | |||||
284 | /** |
||||
285 | * Delete data from the database |
||||
286 | * |
||||
287 | * @note Altering the DB invalidates all queries in query cache. |
||||
288 | * |
||||
289 | * @param QueryBuilder|string $query The SQL query to run |
||||
290 | * @param array $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve'] |
||||
291 | * |
||||
292 | * @return int The number of affected rows |
||||
293 | * @throws \DatabaseException |
||||
294 | */ |
||||
295 | 245 | public function deleteData($query, array $params = []) { |
|||
296 | |||||
297 | 245 | if ($query instanceof QueryBuilder) { |
|||
298 | 228 | $params = $query->getParameters(); |
|||
299 | 228 | $query = $query->getSQL(); |
|||
300 | } |
||||
301 | |||||
302 | 245 | if ($this->logger) { |
|||
303 | 245 | $this->logger->info("DB delete query $query (params: " . print_r($params, true) . ")"); |
|||
304 | } |
||||
305 | |||||
306 | 245 | $connection = $this->getConnection('write'); |
|||
307 | |||||
308 | 245 | $this->invalidateQueryCache(); |
|||
309 | |||||
310 | 245 | $stmt = $this->executeQuery("$query", $connection, $params); |
|||
311 | 244 | return (int) $stmt->rowCount(); |
|||
312 | } |
||||
313 | |||||
314 | /** |
||||
315 | * Get a string that uniquely identifies a callback during the current request. |
||||
316 | * |
||||
317 | * This is used to cache queries whose results were transformed by the callback. If the callback involves |
||||
318 | * object method calls of the same class, different instances will return different values. |
||||
319 | * |
||||
320 | * @param callable $callback The callable value to fingerprint |
||||
321 | * |
||||
322 | * @return string A string that is unique for each callable passed in |
||||
323 | * @since 1.9.4 |
||||
324 | * @access private |
||||
325 | */ |
||||
326 | 1154 | protected function fingerprintCallback($callback) { |
|||
327 | 1154 | if (is_string($callback)) { |
|||
328 | 1151 | return $callback; |
|||
329 | } |
||||
330 | 1058 | if (is_object($callback)) { |
|||
331 | 1037 | return spl_object_hash($callback) . "::__invoke"; |
|||
332 | } |
||||
333 | 220 | if (is_array($callback)) { |
|||
334 | 220 | if (is_string($callback[0])) { |
|||
335 | 1 | return "{$callback[0]}::{$callback[1]}"; |
|||
336 | } |
||||
337 | 220 | return spl_object_hash($callback[0]) . "::{$callback[1]}"; |
|||
338 | } |
||||
339 | // this should not happen |
||||
340 | return ""; |
||||
341 | } |
||||
342 | |||||
343 | /** |
||||
344 | * Handles queries that return results, running the results through a |
||||
345 | * an optional callback function. This is for R queries (from CRUD). |
||||
346 | * |
||||
347 | * @param QueryBuilder|string $query The select query to execute |
||||
348 | * @param string $callback An optional callback function to run on each row |
||||
349 | * @param bool $single Return only a single result? |
||||
350 | * @param array $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve'] |
||||
351 | * |
||||
352 | * @return array An array of database result objects or callback function results. If the query |
||||
353 | * returned nothing, an empty array. |
||||
354 | * @throws \DatabaseException |
||||
355 | */ |
||||
356 | 1276 | protected function getResults($query, $callback = null, $single = false, array $params = []) { |
|||
357 | |||||
358 | 1276 | if ($query instanceof QueryBuilder) { |
|||
359 | 1242 | $params = $query->getParameters(); |
|||
360 | 1242 | $sql = $query->getSQL(); |
|||
361 | } else { |
||||
362 | 383 | $sql = $query; |
|||
363 | } |
||||
364 | |||||
365 | // Since we want to cache results of running the callback, we need to |
||||
366 | // namespace the query with the callback and single result request. |
||||
367 | // https://github.com/elgg/elgg/issues/4049 |
||||
368 | 1276 | $query_id = (int) $single . $sql . '|'; |
|||
369 | 1276 | if ($params) { |
|||
0 ignored issues
–
show
The expression
$params of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using
Loading history...
|
|||||
370 | 1272 | $query_id .= serialize($params) . '|'; |
|||
371 | } |
||||
372 | |||||
373 | 1276 | if ($callback) { |
|||
374 | 1154 | if (!is_callable($callback)) { |
|||
375 | 1 | throw new \RuntimeException('$callback must be a callable function. Given ' |
|||
376 | 1 | . _elgg_services()->handlers->describeCallable($callback)); |
|||
377 | } |
||||
378 | 1153 | $query_id .= $this->fingerprintCallback($callback); |
|||
379 | } |
||||
380 | |||||
381 | 1275 | if ($this->logger) { |
|||
382 | 1274 | $this->logger->info("DB select query $sql (params: " . print_r($params, true) . ")"); |
|||
383 | } |
||||
384 | |||||
385 | // MD5 yields smaller mem usage for cache and cleaner logs |
||||
386 | 1275 | $hash = md5($query_id); |
|||
387 | |||||
388 | // Is cached? |
||||
389 | 1275 | if ($this->query_cache) { |
|||
390 | 1275 | if (isset($this->query_cache[$hash])) { |
|||
391 | 620 | if ($this->logger) { |
|||
392 | 620 | $this->logger->info("DB query results returned from cache (hash: $hash)"); |
|||
393 | } |
||||
394 | 620 | return $this->query_cache[$hash]; |
|||
395 | } |
||||
396 | } |
||||
397 | |||||
398 | 1210 | $return = []; |
|||
399 | |||||
400 | 1210 | if ($query instanceof QueryBuilder) { |
|||
401 | 1197 | $stmt = $query->execute(); |
|||
402 | } else { |
||||
403 | 361 | $stmt = $this->executeQuery($sql, $this->getConnection('read'), $params); |
|||
404 | } |
||||
405 | |||||
406 | 1210 | while ($row = $stmt->fetch()) { |
|||
407 | 649 | if ($callback) { |
|||
408 | 450 | $row = call_user_func($callback, $row); |
|||
409 | } |
||||
410 | |||||
411 | 649 | if ($single) { |
|||
412 | 573 | $return = $row; |
|||
413 | 573 | break; |
|||
414 | } else { |
||||
415 | 588 | $return[] = $row; |
|||
416 | } |
||||
417 | } |
||||
418 | |||||
419 | // Cache result |
||||
420 | 1210 | if ($this->query_cache) { |
|||
421 | 1210 | $this->query_cache[$hash] = $return; |
|||
422 | 1210 | if ($this->logger) { |
|||
423 | 1209 | $this->logger->info("DB query results cached (hash: $hash)"); |
|||
424 | } |
||||
425 | } |
||||
426 | |||||
427 | 1210 | return $return; |
|||
428 | } |
||||
429 | |||||
430 | /** |
||||
431 | * Execute a query. |
||||
432 | * |
||||
433 | * $query is executed via {@link Connection::query}. If there is an SQL error, |
||||
434 | * a {@link DatabaseException} is thrown. |
||||
435 | * |
||||
436 | * @param QueryBuilder|string $query The query |
||||
437 | * @param Connection $connection The DB connection |
||||
438 | * @param array $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve'] |
||||
439 | * |
||||
440 | * @return Statement The result of the query |
||||
441 | * @throws \DatabaseException |
||||
442 | */ |
||||
443 | 1102 | protected function executeQuery($query, Connection $connection, array $params = []) { |
|||
444 | 1102 | if ($query == null) { |
|||
445 | throw new \DatabaseException("Query cannot be null"); |
||||
446 | } |
||||
447 | |||||
448 | 1102 | if ($query instanceof QueryBuilder) { |
|||
449 | $params = $query->getParameters(); |
||||
450 | $query = $query->getSQL(); |
||||
451 | } |
||||
452 | |||||
453 | 1102 | $this->query_count++; |
|||
454 | |||||
455 | 1102 | if ($this->timer) { |
|||
456 | $timer_key = preg_replace('~\\s+~', ' ', trim($query . '|' . serialize($params))); |
||||
457 | $this->timer->begin(['SQL', $timer_key]); |
||||
458 | } |
||||
459 | |||||
460 | try { |
||||
461 | 1102 | if ($params) { |
|||
0 ignored issues
–
show
The expression
$params of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using
Loading history...
|
|||||
462 | 1092 | $value = $connection->executeQuery($query, $params); |
|||
463 | } else { |
||||
464 | // faster |
||||
465 | 1102 | $value = $connection->query($query); |
|||
466 | } |
||||
467 | 3 | } catch (\Exception $e) { |
|||
468 | 3 | throw new \DatabaseException($e->getMessage() . "\n\n" |
|||
469 | 3 | . "QUERY: $query \n\n" |
|||
470 | 3 | . "PARAMS: " . print_r($params, true)); |
|||
471 | } |
||||
472 | |||||
473 | 1099 | if ($this->timer) { |
|||
474 | $this->timer->end(['SQL', $timer_key]); |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
475 | } |
||||
476 | |||||
477 | 1099 | return $value; |
|||
478 | } |
||||
479 | |||||
480 | /** |
||||
481 | * Runs a full database script from disk. |
||||
482 | * |
||||
483 | * The file specified should be a standard SQL file as created by |
||||
484 | * mysqldump or similar. Statements must be terminated with ; |
||||
485 | * and a newline character (\n or \r\n). |
||||
486 | * |
||||
487 | * The special string 'prefix_' is replaced with the database prefix |
||||
488 | * as defined in {@link $this->tablePrefix}. |
||||
489 | * |
||||
490 | * @warning Only single line comments are supported. A comment |
||||
491 | * must start with '-- ' or '# ', where the comment sign is at the |
||||
492 | * very beginning of each line. |
||||
493 | * |
||||
494 | * @warning Errors do not halt execution of the script. If a line |
||||
495 | * generates an error, the error message is saved and the |
||||
496 | * next line is executed. After the file is run, any errors |
||||
497 | * are displayed as a {@link DatabaseException} |
||||
498 | * |
||||
499 | * @param string $scriptlocation The full path to the script |
||||
500 | * |
||||
501 | * @return void |
||||
502 | * @throws \DatabaseException |
||||
503 | * @access private |
||||
504 | */ |
||||
505 | 5 | public function runSqlScript($scriptlocation) { |
|||
506 | 5 | $script = file_get_contents($scriptlocation); |
|||
507 | 5 | if ($script) { |
|||
508 | 5 | $errors = []; |
|||
509 | |||||
510 | // Remove MySQL '-- ' and '# ' style comments |
||||
511 | 5 | $script = preg_replace('/^(?:--|#) .*$/m', '', $script); |
|||
512 | |||||
513 | // Statements must end with ; and a newline |
||||
514 | 5 | $sql_statements = preg_split('/;[\n\r]+/', "$script\n"); |
|||
515 | |||||
516 | 5 | foreach ($sql_statements as $statement) { |
|||
517 | 5 | $statement = trim($statement); |
|||
518 | 5 | $statement = str_replace("prefix_", $this->table_prefix, $statement); |
|||
519 | 5 | if (!empty($statement)) { |
|||
520 | try { |
||||
521 | 5 | $this->updateData($statement); |
|||
522 | } catch (\DatabaseException $e) { |
||||
523 | 5 | $errors[] = $e->getMessage(); |
|||
524 | } |
||||
525 | } |
||||
526 | } |
||||
527 | 5 | if (!empty($errors)) { |
|||
528 | $errortxt = ""; |
||||
529 | foreach ($errors as $error) { |
||||
530 | $errortxt .= " {$error};"; |
||||
531 | } |
||||
532 | |||||
533 | $msg = "There were a number of issues: " . $errortxt; |
||||
534 | 5 | throw new \DatabaseException($msg); |
|||
535 | } |
||||
536 | } else { |
||||
537 | $msg = "Elgg couldn't find the requested database script at " . $scriptlocation . "."; |
||||
538 | throw new \DatabaseException($msg); |
||||
539 | } |
||||
540 | 5 | } |
|||
541 | |||||
542 | /** |
||||
543 | * Queue a query for execution upon shutdown. |
||||
544 | * |
||||
545 | * You can specify a callback if you care about the result. This function will always |
||||
546 | * be passed a \Doctrine\DBAL\Driver\Statement. |
||||
547 | * |
||||
548 | * @param string $query The query to execute |
||||
549 | * @param string $type The query type ('read' or 'write') |
||||
550 | * @param callable $callback A callback function to pass the results array to |
||||
551 | * @param array $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve'] |
||||
552 | * |
||||
553 | * @return boolean Whether registering was successful. |
||||
554 | * @access private |
||||
555 | */ |
||||
556 | 427 | public function registerDelayedQuery($query, $type, $callback = null, array $params = []) { |
|||
557 | 427 | if ($type != 'read' && $type != 'write') { |
|||
558 | return false; |
||||
559 | } |
||||
560 | |||||
561 | 427 | $this->delayed_queries[] = [ |
|||
562 | 427 | self::DELAYED_QUERY => $query, |
|||
563 | 427 | self::DELAYED_TYPE => $type, |
|||
564 | 427 | self::DELAYED_HANDLER => $callback, |
|||
565 | 427 | self::DELAYED_PARAMS => $params, |
|||
566 | ]; |
||||
567 | |||||
568 | 427 | return true; |
|||
569 | } |
||||
570 | |||||
571 | /** |
||||
572 | * Trigger all queries that were registered as "delayed" queries. This is |
||||
573 | * called by the system automatically on shutdown. |
||||
574 | * |
||||
575 | * @return void |
||||
576 | * @access private |
||||
577 | * @todo make protected once this class is part of public API |
||||
578 | */ |
||||
579 | 3 | public function executeDelayedQueries() { |
|||
580 | |||||
581 | 3 | foreach ($this->delayed_queries as $set) { |
|||
582 | 3 | $query = $set[self::DELAYED_QUERY]; |
|||
583 | 3 | $type = $set[self::DELAYED_TYPE]; |
|||
584 | 3 | $handler = $set[self::DELAYED_HANDLER]; |
|||
585 | 3 | $params = $set[self::DELAYED_PARAMS]; |
|||
586 | |||||
587 | try { |
||||
588 | 3 | $stmt = $this->executeQuery($query, $this->getConnection($type), $params); |
|||
589 | |||||
590 | 3 | if (is_callable($handler)) { |
|||
591 | 3 | call_user_func($handler, $stmt); |
|||
592 | } |
||||
593 | } catch (\Exception $e) { |
||||
594 | if ($this->logger) { |
||||
595 | // Suppress all exceptions since page already sent to requestor |
||||
596 | 3 | $this->logger->error($e); |
|||
597 | } |
||||
598 | } |
||||
599 | } |
||||
600 | |||||
601 | 3 | $this->delayed_queries = []; |
|||
602 | 3 | } |
|||
603 | |||||
604 | /** |
||||
605 | * Enable the query cache |
||||
606 | * |
||||
607 | * This does not take precedence over the \Elgg\Database\Config setting. |
||||
608 | * |
||||
609 | * @return void |
||||
610 | * @access private |
||||
611 | */ |
||||
612 | 4665 | public function enableQueryCache() { |
|||
613 | 4665 | if ($this->config->isQueryCacheEnabled() && $this->query_cache === null) { |
|||
614 | // @todo if we keep this cache, expose the size as a config parameter |
||||
615 | 4665 | $this->query_cache = new \Elgg\Cache\LRUCache($this->query_cache_size); |
|||
616 | } |
||||
617 | 4665 | } |
|||
618 | |||||
619 | /** |
||||
620 | * Disable the query cache |
||||
621 | * |
||||
622 | * This is useful for special scripts that pull large amounts of data back |
||||
623 | * in single queries. |
||||
624 | * |
||||
625 | * @return void |
||||
626 | * @access private |
||||
627 | */ |
||||
628 | 259 | public function disableQueryCache() { |
|||
629 | 259 | $this->query_cache = null; |
|||
630 | 259 | } |
|||
631 | |||||
632 | /** |
||||
633 | * Invalidate the query cache |
||||
634 | * |
||||
635 | * @return void |
||||
636 | */ |
||||
637 | 1085 | protected function invalidateQueryCache() { |
|||
638 | 1085 | if ($this->query_cache) { |
|||
639 | 1085 | $this->query_cache->clear(); |
|||
640 | 1085 | if ($this->logger) { |
|||
641 | 1085 | $this->logger->info("Query cache invalidated"); |
|||
642 | } |
||||
643 | } |
||||
644 | 1085 | } |
|||
645 | |||||
646 | /** |
||||
647 | * Get the number of queries made to the database |
||||
648 | * |
||||
649 | * @return int |
||||
650 | * @access private |
||||
651 | */ |
||||
652 | 1 | public function getQueryCount() { |
|||
653 | 1 | return $this->query_count; |
|||
654 | } |
||||
655 | |||||
656 | /** |
||||
657 | * Sanitizes an integer value for use in a query |
||||
658 | * |
||||
659 | * @param int $value Value to sanitize |
||||
660 | * @param bool $signed Whether negative values are allowed (default: true) |
||||
661 | * @return int |
||||
662 | * @deprecated Use query parameters where possible |
||||
663 | */ |
||||
664 | 1 | public function sanitizeInt($value, $signed = true) { |
|||
665 | 1 | $value = (int) $value; |
|||
666 | |||||
667 | 1 | if ($signed === false) { |
|||
668 | 1 | if ($value < 0) { |
|||
669 | $value = 0; |
||||
670 | } |
||||
671 | } |
||||
672 | |||||
673 | 1 | return $value; |
|||
674 | } |
||||
675 | |||||
676 | /** |
||||
677 | * Sanitizes a string for use in a query |
||||
678 | * |
||||
679 | * @param string $value Value to escape |
||||
680 | * @return string |
||||
681 | * @throws \DatabaseException |
||||
682 | * @deprecated Use query parameters where possible |
||||
683 | */ |
||||
684 | 15 | public function sanitizeString($value) { |
|||
685 | 15 | if (is_array($value)) { |
|||
686 | 1 | throw new \DatabaseException(__METHOD__ . '() and serialize_string() cannot accept arrays.'); |
|||
687 | } |
||||
688 | 14 | $quoted = $this->getConnection('read')->quote($value); |
|||
689 | 14 | if ($quoted[0] !== "'" || substr($quoted, -1) !== "'") { |
|||
690 | throw new \DatabaseException("PDO::quote did not return surrounding single quotes."); |
||||
691 | } |
||||
692 | 14 | return substr($quoted, 1, -1); |
|||
693 | } |
||||
694 | |||||
695 | /** |
||||
696 | * Get the server version number |
||||
697 | * |
||||
698 | * @param string $type Connection type (Config constants, e.g. Config::READ_WRITE) |
||||
699 | * |
||||
700 | * @return string Empty if version cannot be determined |
||||
701 | * @access private |
||||
702 | */ |
||||
703 | 5 | public function getServerVersion($type) { |
|||
704 | 5 | $driver = $this->getConnection($type)->getWrappedConnection(); |
|||
705 | 5 | if ($driver instanceof ServerInfoAwareConnection) { |
|||
706 | 5 | return $driver->getServerVersion(); |
|||
707 | } |
||||
708 | |||||
709 | return null; |
||||
710 | } |
||||
711 | |||||
712 | /** |
||||
713 | * Handle magic property reads |
||||
714 | * |
||||
715 | * @param string $name Property name |
||||
716 | * @return mixed |
||||
717 | */ |
||||
718 | 5233 | public function __get($name) { |
|||
719 | 5233 | if ($name === 'prefix') { |
|||
720 | 5233 | return $this->table_prefix; |
|||
721 | } |
||||
722 | |||||
723 | throw new \RuntimeException("Cannot read property '$name'"); |
||||
724 | } |
||||
725 | |||||
726 | /** |
||||
727 | * Handle magic property writes |
||||
728 | * |
||||
729 | * @param string $name Property name |
||||
730 | * @param mixed $value Value |
||||
731 | * @return void |
||||
732 | */ |
||||
733 | public function __set($name, $value) { |
||||
734 | throw new \RuntimeException("Cannot write property '$name'"); |
||||
735 | } |
||||
736 | } |
||||
737 |