Elgg  Version 6.3
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->query_cache->clear();
242 
243  $this->executeQuery($query);
244 
245  try {
246  return (int) $query->getConnection()->lastInsertId();
247  } catch (DriverException $e) {
248  if ($e->getPrevious() instanceof NoIdentityValue) {
249  return 0;
250  }
251 
252  throw $e;
253  }
254  }
255 
266  public function updateData(QueryBuilder $query, bool $get_num_rows = false): bool|int {
267  $params = $query->getParameters();
268  $sql = $query->getSQL();
269 
270  $this->getLogger()->info("DB update query {$sql} (params: " . print_r($params, true) . ')');
271 
272  $this->query_cache->clear();
273 
274  $result = $this->executeQuery($query);
275  if (!$get_num_rows) {
276  return true;
277  }
278 
279  return ($result instanceof Result) ? (int) $result->rowCount() : $result;
280  }
281 
291  public function deleteData(QueryBuilder $query): int {
292  $params = $query->getParameters();
293  $sql = $query->getSQL();
294 
295  $this->getLogger()->info("DB delete query {$sql} (params: " . print_r($params, true) . ')');
296 
297  $this->query_cache->clear();
298 
299  $result = $this->executeQuery($query);
300  return ($result instanceof Result) ? (int) $result->rowCount() : $result;
301  }
302 
314  protected function fingerprintCallback($callback): string {
315  if (is_string($callback)) {
316  return $callback;
317  }
318 
319  if (is_object($callback)) {
320  return spl_object_hash($callback) . '::__invoke';
321  }
322 
323  if (is_array($callback)) {
324  if (is_string($callback[0])) {
325  return "{$callback[0]}::{$callback[1]}";
326  }
327 
328  return spl_object_hash($callback[0]) . "::{$callback[1]}";
329  }
330 
331  // this should not happen
332  return '';
333  }
334 
347  protected function getResults(QueryBuilder $query, $callback = null, bool $single = false) {
348  $params = $query->getParameters();
349  $sql = $query->getSQL();
350 
351  // Since we want to cache results of running the callback, we need to
352  // namespace the query with the callback and single result request.
353  // https://github.com/elgg/elgg/issues/4049
354  $extras = (int) $single . '|';
355  if ($callback) {
356  if (!is_callable($callback)) {
357  throw new RuntimeException('$callback must be a callable function. Given ' . _elgg_services()->handlers->describeCallable($callback));
358  }
359 
360  $extras .= $this->fingerprintCallback($callback);
361  }
362 
363  $hash = $this->getCacheHash($sql, $params, $extras);
364 
365  $cached_results = $this->query_cache->load($hash);
366  if (isset($cached_results)) {
367  return $cached_results;
368  }
369 
370  $this->getLogger()->info("DB select query {$sql} (params: " . print_r($params, true) . ')');
371 
372  $return = [];
373 
374  $stmt = $this->executeQuery($query);
375 
376  while ($row = $stmt->fetchAssociative()) {
377  $row_obj = (object) $row;
378  if ($callback) {
379  $row_obj = call_user_func($callback, $row_obj);
380  }
381 
382  if ($single) {
383  $return = $row_obj;
384  break;
385  } else {
386  $return[] = $row_obj;
387  }
388  }
389 
390  $this->query_cache->save($hash, $return);
391 
392  return $return;
393  }
394 
406  protected function executeQuery(QueryBuilder $query) {
407 
408  try {
409  $result = $this->trackQuery($query, function() use ($query) {
410  if ($query instanceof \Elgg\Database\Select) {
411  return $query->executeQuery();
412  } else {
413  return $query->executeStatement();
414  }
415  });
416  } catch (\Exception $e) {
417  $ex = new DatabaseException($e->getMessage(), 0, $e);
418  $ex->setParameters($query->getParameters());
419  $ex->setQuery($query->getSQL());
420 
421  throw $ex;
422  }
423 
424  return $result;
425  }
426 
437  protected function getCacheHash(string $sql, array $params = [], string $extras = ''): string {
438  $query_id = $sql . '|';
439  if (!empty($params)) {
440  $query_id .= serialize($params) . '|';
441  }
442 
443  $query_id .= $extras;
444 
445  // MD5 yields smaller mem usage for cache
446  return md5($query_id);
447  }
448 
457  public function trackQuery(QueryBuilder $query, callable $callback) {
458  $params = $query->getParameters();
459  $sql = $query->getSQL();
460 
461  if ($this->config->db_enable_query_logging) {
462  $this->getLogger()->notice($sql, ['params' => $params]);
463  }
464 
465  $this->query_count++;
466 
467  $timer_key = preg_replace('~\\s+~', ' ', trim($sql . '|' . serialize($params)));
468  $this->beginTimer(['SQL', $timer_key]);
469 
470  $stop_timer = function() use ($timer_key) {
471  $this->endTimer(['SQL', $timer_key]);
472  };
473 
474  try {
475  $result = $callback();
476  } catch (\Exception $e) {
477  $stop_timer();
478 
479  throw $e;
480  }
481 
482  $stop_timer();
483 
484  return $result;
485  }
486 
498  public function registerDelayedQuery(QueryBuilder $query, $callback = null): void {
499  if (Application::isCli() && !$this->config->testing_mode) {
500  // during CLI execute delayed queries immediately (unless in testing mode, during PHPUnit)
501  // this should prevent OOM during long-running jobs
502  // @see Database::executeDelayedQueries()
503  try {
504  $stmt = $this->executeQuery($query);
505 
506  if (is_callable($callback)) {
507  call_user_func($callback, $stmt);
508  }
509  } catch (\Throwable $t) {
510  // Suppress all exceptions to not allow the application to crash
511  $this->getLogger()->error($t);
512  }
513 
514  return;
515  }
516 
517  $this->delayed_queries[] = [
518  self::DELAYED_QUERY => $query,
519  self::DELAYED_HANDLER => $callback,
520  ];
521  }
522 
529  public function executeDelayedQueries(): void {
530 
531  foreach ($this->delayed_queries as $set) {
532  $query = $set[self::DELAYED_QUERY];
533  $handler = $set[self::DELAYED_HANDLER];
534 
535  try {
536  $stmt = $this->executeQuery($query);
537 
538  if (is_callable($handler)) {
539  call_user_func($handler, $stmt);
540  }
541  } catch (\Throwable $t) {
542  // Suppress all exceptions since page already sent to requestor
543  $this->getLogger()->error($t);
544  }
545  }
546 
547  $this->delayed_queries = [];
548  }
549 
555  public function getQueryCount(): int {
556  return $this->query_count;
557  }
558 
566  public function getServerVersion(string $type = DbConfig::READ): string {
567  return $this->getConnection($type)->getServerVersion();
568  }
569 
577  public function isMariaDB(string $type = DbConfig::READ): bool {
578  return $this->getConnection($type)->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MariaDBPlatform;
579  }
580 
590  public function isMySQL(string $type = DbConfig::READ): bool {
591  return $this->getConnection($type)->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MySQLPlatform;
592  }
593 
602  public function __get($name) {
603  if ($name === 'prefix') {
604  return $this->table_prefix;
605  }
606 
607  throw new RuntimeException("Cannot read property '{$name}'");
608  }
609 
619  public function __set($name, $value): void {
620  throw new RuntimeException("Cannot write property '{$name}'");
621  }
622 }
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/invalidate'=>['access'=> 'admin'], 'admin/site/flush_cache'=>['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'], '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:73
foreach( $paths as $path)
Definition: autoloader.php:12
$query
Load, boot, and implement a front controller for an Elgg application.
Definition: Application.php:47
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:590
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:566
executeQuery(QueryBuilder $query)
Execute a query.
Definition: Database.php:406
getResults(QueryBuilder $query, $callback=null, bool $single=false)
Handles queries that return results, running the results through an optional callback function.
Definition: Database.php:347
getCacheHash(string $sql, array $params=[], string $extras='')
Returns a hashed key for storage in the cache.
Definition: Database.php:437
isMariaDB(string $type=DbConfig::READ)
Is the database MariaDB.
Definition: Database.php:577
registerDelayedQuery(QueryBuilder $query, $callback=null)
Queue a query for execution upon shutdown.
Definition: Database.php:498
__set($name, $value)
Handle magic property writes.
Definition: Database.php:619
trackQuery(QueryBuilder $query, callable $callback)
Tracks the query count and timers for a given query.
Definition: Database.php:457
getQueryCount()
Get the number of queries made to the database.
Definition: Database.php:555
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:291
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:314
__get($name)
Handle magic property reads.
Definition: Database.php:602
updateData(QueryBuilder $query, bool $get_num_rows=false)
Update the database.
Definition: Database.php:266
executeDelayedQueries()
Trigger all queries that were registered as "delayed" queries.
Definition: Database.php:529
__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:337
$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