Elgg  Version 7.1
Database.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
5 use Doctrine\DBAL\Connection;
6 use Doctrine\DBAL\Driver\Exception\NoIdentityValue;
7 use Doctrine\DBAL\DriverManager;
8 use Doctrine\DBAL\Exception\DriverException;
9 use Doctrine\DBAL\Result;
16 use Elgg\Traits\Loggable;
17 use Psr\Log\LogLevel;
18 
26 class Database {
27 
28  use Profilable;
29  use Loggable;
30 
31  const DELAYED_QUERY = 'q';
32  const DELAYED_HANDLER = 'h';
33 
37  protected $table_prefix;
38 
42  protected array $connections = [];
43 
47  protected int $query_count = 0;
48 
56  protected array $delayed_queries = [];
57 
61  protected $db_config;
62 
70  public function __construct(DbConfig $db_config, protected QueryCache $query_cache, protected Config $config) {
71  $this->resetConnections($db_config);
72  }
73 
81  public function resetConnections(DbConfig $config): void {
82  $this->closeConnections();
83 
84  $this->db_config = $config;
85  $this->table_prefix = $config->getTablePrefix();
86  $this->query_cache->enable();
87  $this->query_cache->clear();
88  }
89 
98  public function closeConnections(): void {
99  foreach ($this->connections as $connection) {
100  $connection->close();
101  }
102 
103  $this->connections = [];
104  }
105 
113  public function getConnection(string $type): Connection {
114  if (isset($this->connections[$type])) {
115  // type is configured
116  return $this->connections[$type];
117  } elseif (isset($this->connections[DbConfig::READ_WRITE])) {
118  // fallback, for request of read/write but no split db
119  return $this->connections[DbConfig::READ_WRITE];
120  } elseif (isset($this->connections[DbConfig::READ])) {
121  // split db configured, readwrite requested
122  return $this->connections[DbConfig::READ];
123  }
124 
125  $this->setupConnections();
126 
127  return $this->getConnection($type);
128  }
129 
138  public function setupConnections(): void {
139  if ($this->db_config->isDatabaseSplit()) {
140  $this->connect(DbConfig::READ);
141  $this->connect(DbConfig::WRITE);
142  } else {
143  $this->connect(DbConfig::READ_WRITE);
144  }
145  }
146 
157  public function connect(string $type = DbConfig::READ_WRITE): void {
158  $conf = $this->db_config->getConnectionConfig($type);
159 
160  $params = [
161  'dbname' => $conf['database'],
162  'user' => $conf['user'],
163  'password' => $conf['password'],
164  'host' => $conf['host'],
165  'port' => $conf['port'],
166  'charset' => $conf['encoding'],
167  'driver' => 'pdo_mysql',
168  ];
169 
170  try {
171  $this->connections[$type] = DriverManager::getConnection($params);
172 
173  // https://github.com/Elgg/Elgg/issues/8121
174  $sub_query = "SELECT REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '')";
175  $this->connections[$type]->executeStatement("SET SESSION sql_mode=($sub_query);");
176  } catch (\Exception $e) {
177  // http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html
178  $this->log(LogLevel::ERROR, $e);
179 
180  if ($e->getCode() == 1102 || $e->getCode() == 1049) {
181  $msg = "Elgg couldn't select the database '{$conf['database']}'. Please check that the database is created and you have access to it.";
182  } else {
183  $msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file.";
184  }
185 
186  throw new DatabaseException($msg);
187  }
188  }
189 
205  public function getData(QueryBuilder $query, $callback = null) {
206  return $this->getResults($query, $callback, false);
207  }
208 
221  public function getDataRow(QueryBuilder $query, $callback = null) {
222  return $this->getResults($query, $callback, true);
223  }
224 
234  public function insertData(QueryBuilder $query): int {
235 
236  $params = $query->getParameters();
237  $sql = $query->getSQL();
238 
239  $this->getLogger()->info("DB insert query {$sql} (params: " . print_r($params, true) . ')');
240 
241  $this->executeQuery($query);
242 
243  try {
244  return (int) $query->getConnection()->lastInsertId();
245  } catch (DriverException $e) {
246  if ($e->getPrevious() instanceof NoIdentityValue) {
247  return 0;
248  }
249 
250  throw $e;
251  }
252  }
253 
264  public function updateData(QueryBuilder $query, bool $get_num_rows = false): bool|int {
265  $params = $query->getParameters();
266  $sql = $query->getSQL();
267 
268  $this->getLogger()->info("DB update query {$sql} (params: " . print_r($params, true) . ')');
269 
270  $result = $this->executeQuery($query);
271  if (!$get_num_rows) {
272  return true;
273  }
274 
275  return ($result instanceof Result) ? (int) $result->rowCount() : $result;
276  }
277 
287  public function deleteData(QueryBuilder $query): int {
288  $params = $query->getParameters();
289  $sql = $query->getSQL();
290 
291  $this->getLogger()->info("DB delete query {$sql} (params: " . print_r($params, true) . ')');
292 
293  $result = $this->executeQuery($query);
294  return ($result instanceof Result) ? (int) $result->rowCount() : $result;
295  }
296 
308  protected function fingerprintCallback($callback): string {
309  if (is_string($callback)) {
310  return $callback;
311  }
312 
313  if (is_object($callback)) {
314  return spl_object_hash($callback) . '::__invoke';
315  }
316 
317  if (is_array($callback)) {
318  if (is_string($callback[0])) {
319  return "{$callback[0]}::{$callback[1]}";
320  }
321 
322  return spl_object_hash($callback[0]) . "::{$callback[1]}";
323  }
324 
325  // this should not happen
326  return '';
327  }
328 
341  protected function getResults(QueryBuilder $query, $callback = null, bool $single = false) {
342  $params = $query->getParameters();
343  $sql = $query->getSQL();
344 
345  // Since we want to cache results of running the callback, we need to
346  // namespace the query with the callback and single result request.
347  // https://github.com/elgg/elgg/issues/4049
348  $extras = (int) $single . '|';
349  if ($callback) {
350  if (!is_callable($callback)) {
351  throw new RuntimeException('$callback must be a callable function. Given ' . _elgg_services()->handlers->describeCallable($callback));
352  }
353 
354  $extras .= $this->fingerprintCallback($callback);
355  }
356 
357  $hash = $this->getCacheHash($sql, $params, $extras);
358 
359  $cached_results = $this->query_cache->load($hash);
360  if (isset($cached_results)) {
361  return $cached_results;
362  }
363 
364  $this->getLogger()->info("DB select query {$sql} (params: " . print_r($params, true) . ')');
365 
366  $return = [];
367 
368  $stmt = $this->executeQuery($query);
369 
370  while ($row = $stmt->fetchAssociative()) {
371  $row_obj = (object) $row;
372  if ($callback) {
373  $row_obj = call_user_func($callback, $row_obj);
374  }
375 
376  if ($single) {
377  $return = $row_obj;
378  break;
379  } else {
380  $return[] = $row_obj;
381  }
382  }
383 
384  $this->query_cache->save($hash, $return);
385 
386  return $return;
387  }
388 
400  protected function executeQuery(QueryBuilder $query) {
401 
402  try {
403  $result = $this->trackQuery($query, function() use ($query) {
404  if ($query instanceof \Elgg\Database\Select) {
405  return $query->executeQuery();
406  } else {
407  return $query->executeStatement();
408  }
409  });
410  } catch (\Exception $e) {
411  $ex = new DatabaseException($e->getMessage(), 0, $e);
412  $ex->setParameters($query->getParameters());
413  $ex->setQuery($query->getSQL());
414 
415  throw $ex;
416  }
417 
418  return $result;
419  }
420 
431  protected function getCacheHash(string $sql, array $params = [], string $extras = ''): string {
432  $query_id = $sql . '|';
433  if (!empty($params)) {
434  $query_id .= serialize($params) . '|';
435  }
436 
437  $query_id .= $extras;
438 
439  // MD5 yields smaller mem usage for cache
440  return md5($query_id);
441  }
442 
451  public function trackQuery(QueryBuilder $query, callable $callback) {
452  $params = $query->getParameters();
453  $sql = $query->getSQL();
454 
455  if ($this->config->db_enable_query_logging) {
456  $this->getLogger()->notice($sql, ['params' => $params]);
457  }
458 
459  $this->query_count++;
460 
461  $timer_key = preg_replace('~\\s+~', ' ', trim($sql . '|' . serialize($params)));
462  $this->beginTimer(['SQL', $timer_key]);
463 
464  $stop_timer = function() use ($timer_key) {
465  $this->endTimer(['SQL', $timer_key]);
466  };
467 
468  try {
469  $result = $callback();
470  } catch (\Exception $e) {
471  $stop_timer();
472 
473  throw $e;
474  }
475 
476  $stop_timer();
477 
478  return $result;
479  }
480 
492  public function registerDelayedQuery(QueryBuilder $query, $callback = null): void {
493  if (Application::isCli() && !$this->config->testing_mode) {
494  // during CLI execute delayed queries immediately (unless in testing mode, during PHPUnit)
495  // this should prevent OOM during long-running jobs
496  // @see Database::executeDelayedQueries()
497  try {
498  $stmt = $this->executeQuery($query);
499 
500  if (is_callable($callback)) {
501  call_user_func($callback, $stmt);
502  }
503  } catch (\Throwable $t) {
504  // Suppress all exceptions to not allow the application to crash
505  $this->getLogger()->error($t);
506  }
507 
508  return;
509  }
510 
511  $this->delayed_queries[] = [
512  self::DELAYED_QUERY => $query,
513  self::DELAYED_HANDLER => $callback,
514  ];
515  }
516 
523  public function executeDelayedQueries(): void {
524 
525  foreach ($this->delayed_queries as $set) {
526  $query = $set[self::DELAYED_QUERY];
527  $handler = $set[self::DELAYED_HANDLER];
528 
529  try {
530  $stmt = $this->executeQuery($query);
531 
532  if (is_callable($handler)) {
533  call_user_func($handler, $stmt);
534  }
535  } catch (\Throwable $t) {
536  // Suppress all exceptions since page already sent to requestor
537  $this->getLogger()->error($t);
538  }
539  }
540 
541  $this->delayed_queries = [];
542  }
543 
549  public function getQueryCount(): int {
550  return $this->query_count;
551  }
552 
560  public function getServerVersion(string $type = DbConfig::READ): string {
561  return $this->getConnection($type)->getServerVersion();
562  }
563 
571  public function isMariaDB(string $type = DbConfig::READ): bool {
572  return $this->getConnection($type)->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MariaDBPlatform;
573  }
574 
584  public function isMySQL(string $type = DbConfig::READ): bool {
585  return $this->getConnection($type)->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MySQLPlatform;
586  }
587 
596  public function __get($name) {
597  if ($name === 'prefix') {
598  return $this->table_prefix;
599  }
600 
601  throw new RuntimeException("Cannot read property '{$name}'");
602  }
603 
613  public function __set($name, $value): void {
614  throw new RuntimeException("Cannot write property '{$name}'");
615  }
616 }
if(! $user||! $user->canDelete()) $name
Definition: delete.php:22
$type
Definition: delete.php:21
$params
Saves global plugin settings.
Definition: save.php:13
$handler
Definition: add.php:7
return[ 'admin/delete_admin_notices'=>['access'=> 'admin'], 'admin/menu/save'=>['access'=> 'admin'], 'admin/plugins/activate'=>['access'=> 'admin'], 'admin/plugins/activate_all'=>['access'=> 'admin'], 'admin/plugins/deactivate'=>['access'=> 'admin'], 'admin/plugins/deactivate_all'=>['access'=> 'admin'], 'admin/plugins/set_priority'=>['access'=> 'admin'], 'admin/security/security_txt'=>['access'=> 'admin'], 'admin/security/settings'=>['access'=> 'admin'], 'admin/security/regenerate_site_secret'=>['access'=> 'admin'], 'admin/site/cache/clear'=>['access'=> 'admin'], 'admin/site/cache/invalidate'=>['access'=> 'admin'], 'admin/site/icons'=>['access'=> 'admin'], 'admin/site/set_maintenance_mode'=>['access'=> 'admin'], 'admin/site/set_robots'=>['access'=> 'admin'], 'admin/site/theme'=>['access'=> 'admin'], 'admin/site/unlock_upgrade'=>['access'=> 'admin'], 'admin/site/settings'=>['access'=> 'admin'], 'admin/upgrade'=>['access'=> 'admin'], 'admin/upgrade/reset'=>['access'=> 'admin'], 'admin/user/ban'=>['access'=> 'admin'], 'admin/user/bulk/ban'=>['access'=> 'admin'], 'admin/user/bulk/delete'=>['access'=> 'admin'], 'admin/user/bulk/unban'=>['access'=> 'admin'], 'admin/user/bulk/validate'=>['access'=> 'admin'], 'admin/user/change_email'=>['access'=> 'admin'], 'admin/user/delete'=>['access'=> 'admin'], 'admin/user/login_as'=>['access'=> 'admin'], 'admin/user/logout_as'=>[], 'admin/user/makeadmin'=>['access'=> 'admin'], 'admin/user/resetpassword'=>['access'=> 'admin'], 'admin/user/removeadmin'=>['access'=> 'admin'], 'admin/user/unban'=>['access'=> 'admin'], 'admin/user/validate'=>['access'=> 'admin'], 'annotation/delete'=>[], 'avatar/upload'=>[], 'comment/save'=>[], 'diagnostics/download'=>['access'=> 'admin', 'controller'=> \Elgg\Diagnostics\DownloadController::class,], 'entity/chooserestoredestination'=>[], 'entity/delete'=>[], 'entity/mute'=>[], 'entity/restore'=>[], 'entity/subscribe'=>[], 'entity/trash'=>[], 'entity/unmute'=>[], 'entity/unsubscribe'=>[], 'login'=>['access'=> 'logged_out'], 'logout'=>[], 'notifications/mute'=>['access'=> 'public'], 'plugins/settings/remove'=>['access'=> 'admin'], 'plugins/settings/save'=>['access'=> 'admin'], 'plugins/usersettings/save'=>[], 'register'=>['access'=> 'logged_out', 'middleware'=>[\Elgg\Router\Middleware\RegistrationAllowedGatekeeper::class,],], 'river/delete'=>[], 'settings/notifications'=>[], 'settings/notifications/subscriptions'=>[], 'user/changepassword'=>['access'=> 'public'], 'user/requestnewpassword'=>['access'=> 'public'], 'useradd'=>['access'=> 'admin'], 'usersettings/save'=>[], 'widgets/add'=>[], 'widgets/delete'=>[], 'widgets/move'=>[], 'widgets/save'=>[],]
Definition: actions.php:76
foreach( $paths as $path)
Definition: autoloader.php:12
$query
Load, boot, and implement a front controller for an Elgg application.
Definition: Application.php:48
Volatile cache for select queries.
Definition: QueryCache.php:12
Database configuration service.
Definition: DbConfig.php:13
Database abstraction query builder.
The Elgg database.
Definition: Database.php:26
isMySQL(string $type=DbConfig::READ)
Is the database MySQL.
Definition: Database.php:584
insertData(QueryBuilder $query)
Insert a row into the database.
Definition: Database.php:234
getData(QueryBuilder $query, $callback=null)
Retrieve rows from the database.
Definition: Database.php:205
getServerVersion(string $type=DbConfig::READ)
Get the server version number.
Definition: Database.php:560
executeQuery(QueryBuilder $query)
Execute a query.
Definition: Database.php:400
getResults(QueryBuilder $query, $callback=null, bool $single=false)
Handles queries that return results, running the results through an optional callback function.
Definition: Database.php:341
getCacheHash(string $sql, array $params=[], string $extras='')
Returns a hashed key for storage in the cache.
Definition: Database.php:431
isMariaDB(string $type=DbConfig::READ)
Is the database MariaDB.
Definition: Database.php:571
registerDelayedQuery(QueryBuilder $query, $callback=null)
Queue a query for execution upon shutdown.
Definition: Database.php:492
__set($name, $value)
Handle magic property writes.
Definition: Database.php:613
trackQuery(QueryBuilder $query, callable $callback)
Tracks the query count and timers for a given query.
Definition: Database.php:451
getQueryCount()
Get the number of queries made to the database.
Definition: Database.php:549
getConnection(string $type)
Gets (if required, also creates) a DB connection.
Definition: Database.php:113
deleteData(QueryBuilder $query)
Delete data from the database.
Definition: Database.php:287
setupConnections()
Establish database connections.
Definition: Database.php:138
resetConnections(DbConfig $config)
Reset the connections with new credentials.
Definition: Database.php:81
connect(string $type=DbConfig::READ_WRITE)
Establish a connection to the database server.
Definition: Database.php:157
getDataRow(QueryBuilder $query, $callback=null)
Retrieve a single row from the database.
Definition: Database.php:221
closeConnections()
Close all database connections.
Definition: Database.php:98
fingerprintCallback($callback)
Get a string that uniquely identifies a callback during the current request.
Definition: Database.php:308
__get($name)
Handle magic property reads.
Definition: Database.php:596
updateData(QueryBuilder $query, bool $get_num_rows=false)
Update the database.
Definition: Database.php:264
executeDelayedQueries()
Trigger all queries that were registered as "delayed" queries.
Definition: Database.php:523
__construct(DbConfig $db_config, protected QueryCache $query_cache, protected Config $config)
Constructor.
Definition: Database.php:70
A generic parent class for database exceptions.
Exception thrown if an error which can only be found on runtime occurs.
Result of a single BatchUpgrade run.
Definition: Result.php:10
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
if($item instanceof \ElggEntity) elseif($item instanceof \ElggRiverItem) elseif($item instanceof \ElggRelationship) elseif(is_callable([ $item, 'getType']))
Definition: item.php:48
_elgg_services()
Get the global service provider.
Definition: elgglib.php:347
$value
Definition: generic.php:51
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:59
trait Profilable
Make an object accept a timer.
Definition: Profilable.php:12
beginTimer(array $keys)
Start the timer (when enabled)
Definition: Profilable.php:43
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10