- // Renumber the array keys to be sure that they fit
- $i = 0;
- $args = [];
- foreach ($params AS $param) {
- // Avoid problems with some MySQL servers and boolean values. See issue #3645
- if (is_bool($param)) {
- $param = (int)$param;
- }
- $args[++$i] = $param;
- }
-
- if (!self::$connected) {
- return false;
- }
-
- if ((substr_count($sql, '?') != count($args)) && (count($args) > 0)) {
- // Question: Should we continue or stop the query here?
- self::$logger->warning('Query parameters mismatch.', ['query' => $sql, 'args' => $args, 'callstack' => System::callstack()]);
- }
-
- $sql = self::cleanQuery($sql);
- $sql = self::anyValueFallback($sql);
-
- $orig_sql = $sql;
-
- if (self::$configCache->get('system', 'db_callstack') !== null) {
- $sql = "/*".System::callstack()." */ ".$sql;
- }
-
- self::$error = '';
- self::$errorno = 0;
- self::$affected_rows = 0;
-
- // We have to make some things different if this function is called from "e"
- $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
-
- if (isset($trace[1])) {
- $called_from = $trace[1];
- } else {
- // We use just something that is defined to avoid warnings
- $called_from = $trace[0];
- }
- // We are having an own error logging in the function "e"
- $called_from_e = ($called_from['function'] == 'e');
-
- switch (self::$driver) {
- case 'pdo':
- // If there are no arguments we use "query"
- if (count($args) == 0) {
- if (!$retval = self::$connection->query($sql)) {
- $errorInfo = self::$connection->errorInfo();
- self::$error = $errorInfo[2];
- self::$errorno = $errorInfo[1];
- $retval = false;
- break;
- }
- self::$affected_rows = $retval->rowCount();
- break;
- }
-
- if (!$stmt = self::$connection->prepare($sql)) {
- $errorInfo = self::$connection->errorInfo();
- self::$error = $errorInfo[2];
- self::$errorno = $errorInfo[1];
- $retval = false;
- break;
- }
-
- foreach ($args AS $param => $value) {
- if (is_int($args[$param])) {
- $data_type = PDO::PARAM_INT;
- } else {
- $data_type = PDO::PARAM_STR;
- }
- $stmt->bindParam($param, $args[$param], $data_type);
- }
-
- if (!$stmt->execute()) {
- $errorInfo = $stmt->errorInfo();
- self::$error = $errorInfo[2];
- self::$errorno = $errorInfo[1];
- $retval = false;
- } else {
- $retval = $stmt;
- self::$affected_rows = $retval->rowCount();
- }
- break;
- case 'mysqli':
- // There are SQL statements that cannot be executed with a prepared statement
- $parts = explode(' ', $orig_sql);
- $command = strtolower($parts[0]);
- $can_be_prepared = in_array($command, ['select', 'update', 'insert', 'delete']);
-
- // The fallback routine is called as well when there are no arguments
- if (!$can_be_prepared || (count($args) == 0)) {
- $retval = self::$connection->query(self::replaceParameters($sql, $args));
- if (self::$connection->errno) {
- self::$error = self::$connection->error;
- self::$errorno = self::$connection->errno;
- $retval = false;
- } else {
- if (isset($retval->num_rows)) {
- self::$affected_rows = $retval->num_rows;
- } else {
- self::$affected_rows = self::$connection->affected_rows;
- }
- }
- break;
- }
-
- $stmt = self::$connection->stmt_init();
-
- if (!$stmt->prepare($sql)) {
- self::$error = $stmt->error;
- self::$errorno = $stmt->errno;
- $retval = false;
- break;
- }
-
- $param_types = '';
- $values = [];
- foreach ($args AS $param => $value) {
- if (is_int($args[$param])) {
- $param_types .= 'i';
- } elseif (is_float($args[$param])) {
- $param_types .= 'd';
- } elseif (is_string($args[$param])) {
- $param_types .= 's';
- } else {
- $param_types .= 'b';
- }
- $values[] = &$args[$param];
- }
-
- if (count($values) > 0) {
- array_unshift($values, $param_types);
- call_user_func_array([$stmt, 'bind_param'], $values);
- }
-
- if (!$stmt->execute()) {
- self::$error = self::$connection->error;
- self::$errorno = self::$connection->errno;
- $retval = false;
- } else {
- $stmt->store_result();
- $retval = $stmt;
- self::$affected_rows = $retval->affected_rows;
- }
- break;
- }
-
- // We are having an own error logging in the function "e"
- if ((self::$errorno != 0) && !$called_from_e) {
- // We have to preserve the error code, somewhere in the logging it get lost
- $error = self::$error;
- $errorno = self::$errorno;
-
- self::$logger->error('DB Error', [
- 'code' => self::$errorno,
- 'error' => self::$error,
- 'callstack' => System::callstack(8),
- 'params' => self::replaceParameters($sql, $args),
- ]);
-
- // On a lost connection we try to reconnect - but only once.
- if ($errorno == 2006) {
- if (self::$in_retrial || !self::reconnect()) {
- // It doesn't make sense to continue when the database connection was lost
- if (self::$in_retrial) {
- self::$logger->notice('Giving up retrial because of database error', [
- 'code' => self::$errorno,
- 'error' => self::$error,
- ]);
- } else {
- self::$logger->notice('Couldn\'t reconnect after database error', [
- 'code' => self::$errorno,
- 'error' => self::$error,
- ]);
- }
- exit(1);
- } else {
- // We try it again
- self::$logger->notice('Reconnected after database error', [
- 'code' => self::$errorno,
- 'error' => self::$error,
- ]);
- self::$in_retrial = true;
- $ret = self::p($sql, $args);
- self::$in_retrial = false;
- return $ret;
- }
- }
-
- self::$error = $error;
- self::$errorno = $errorno;
- }
-
- self::$profiler->saveTimestamp($stamp1, 'database', System::callstack());
-
- if (self::$configCache->get('system', 'db_log')) {
- $stamp2 = microtime(true);
- $duration = (float)($stamp2 - $stamp1);
-
- if (($duration > self::$configCache->get('system', 'db_loglimit'))) {
- $duration = round($duration, 3);
- $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
-
- @file_put_contents(self::$configCache->get('system', 'db_log'), DateTimeFormat::utcNow()."\t".$duration."\t".
- basename($backtrace[1]["file"])."\t".
- $backtrace[1]["line"]."\t".$backtrace[2]["function"]."\t".
- substr(self::replaceParameters($sql, $args), 0, 2000)."\n", FILE_APPEND);
- }
- }
- return $retval;