. * --------------------------------------------------------------------- */ if (!defined('GLPI_ROOT')) { die("Sorry. You can't access this file directly"); } use Glpi\Application\ErrorHandler; /** * Database class for Mysql **/ class DBmysql { //! Database Host - string or Array of string (round robin) public $dbhost = ""; //! Database User public $dbuser = ""; //! Database Password public $dbpassword = ""; //! Default Database public $dbdefault = ""; //! Database Handler private $dbh; //! Database Error public $error = 0; // Slave management public $slave = false; private $in_transaction; /** * Defines if connection must use SSL. * * @var boolean */ public $dbssl = false; /** * The path name to the key file (used in case of SSL connection). * * @see mysqli::ssl_set() * @var string|null */ public $dbsslkey = null; /** * The path name to the certificate file (used in case of SSL connection). * * @see mysqli::ssl_set() * @var string|null */ public $dbsslcert = null; /** * The path name to the certificate authority file (used in case of SSL connection). * * @see mysqli::ssl_set() * @var string|null */ public $dbsslca = null; /** * The pathname to a directory that contains trusted SSL CA certificates in PEM format * (used in case of SSL connection). * * @see mysqli::ssl_set() * @var string|null */ public $dbsslcapath = null; /** * A list of allowable ciphers to use for SSL encryption (used in case of SSL connection). * * @see mysqli::ssl_set() * @var string|null */ public $dbsslcacipher = null; /** Is it a first connection ? * Indicates if the first connection attempt is successful or not * if first attempt fail -> display a warning which indicates that glpi is in readonly **/ public $first_connection = true; // Is connected to the DB ? public $connected = false; //to calculate execution time public $execution_time = false; private $cache_disabled = false; /** * Cached list fo tables. * * @var array * @see self::tableExists() */ private $table_cache = []; /** * Cached list of fields. * * @var array * @see self::listFields() */ private $field_cache = []; /** * Constructor / Connect to the MySQL Database * * @param integer $choice host number (default NULL) * * @return void */ function __construct($choice = null) { $this->connect($choice); } /** * Connect using current database settings * Use dbhost, dbuser, dbpassword and dbdefault * * @param integer $choice host number (default NULL) * * @return void */ function connect($choice = null) { $this->connected = false; $this->dbh = @new mysqli(); $this->dbh->init(); if ($this->dbssl) { mysqli_ssl_set( $this->dbh, $this->dbsslkey, $this->dbsslcert, $this->dbsslca, $this->dbsslcapath, $this->dbsslcacipher ); } if (is_array($this->dbhost)) { // Round robin choice $i = (isset($choice) ? $choice : mt_rand(0, count($this->dbhost)-1)); $host = $this->dbhost[$i]; } else { $host = $this->dbhost; } $hostport = explode(":", $host); if (count($hostport) < 2) { // Host $this->dbh->real_connect($host, $this->dbuser, rawurldecode($this->dbpassword), $this->dbdefault); } else if (intval($hostport[1])>0) { // Host:port $this->dbh->real_connect($hostport[0], $this->dbuser, rawurldecode($this->dbpassword), $this->dbdefault, $hostport[1]); } else { // :Socket $this->dbh->real_connect($hostport[0], $this->dbuser, rawurldecode($this->dbpassword), $this->dbdefault, ini_get('mysqli.default_port'), $hostport[1]); } if ($this->dbh->connect_error) { $this->connected = false; $this->error = 1; } else if (!defined('MYSQLI_OPT_INT_AND_FLOAT_NATIVE')) { $this->connected = false; $this->error = 2; } else { $dbenc = isset($this->dbenc) ? $this->dbenc : "utf8"; $this->dbh->set_charset($dbenc); if ($dbenc === "utf8") { // The mysqli::set_charset function will make COLLATE to be defined to the default one for used charset. // // For 'utf8' charset, default one is 'utf8_general_ci', // so we have to redefine it to 'utf8_unicode_ci'. // // If encoding used by connection is not the default one (i.e utf8), then we assume // that we cannot be sure of used COLLATE and that using the default one is the best option. $this->dbh->query("SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';"); } // force mysqlnd to return int and float types correctly (not as strings) $this->dbh->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true); if (GLPI_FORCE_EMPTY_SQL_MODE) { $this->dbh->query("SET SESSION sql_mode = ''"); } $this->connected = true; $this->setTimezone($this->guessTimezone()); } } /** * Guess timezone * * Will check for an existing loaded timezone from user, * then will check in preferences and finally will fallback to system one. * * @return string * * @since 9.5.0 */ protected function guessTimezone() { if (isset($_SESSION['glpi_tz'])) { $zone = $_SESSION['glpi_tz']; } else { $conf_tz = ['value' => null]; if ($this->tableExists(Config::getTable()) && $this->fieldExists(Config::getTable(), 'value')) { $conf_tz = $this->request([ 'SELECT' => 'value', 'FROM' => Config::getTable(), 'WHERE' => [ 'context' => 'core', 'name' => 'timezone' ] ])->next(); } $zone = !empty($conf_tz['value']) ? $conf_tz['value'] : date_default_timezone_get(); } return $zone; } /** * Escapes special characters in a string for use in an SQL statement, * taking into account the current charset of the connection * * @since 0.84 * * @param string $string String to escape * * @return string escaped string */ function escape($string) { return $this->dbh->real_escape_string($string); } /** * Execute a MySQL query * * @param string $query Query to execute * * @var array $CFG_GLPI * @var array $DEBUG_SQL * @var integer $SQL_TOTAL_REQUEST * * @return mysqli_result|boolean Query result handler * * @throws GlpitestSQLError */ function query($query) { global $CFG_GLPI, $DEBUG_SQL, $GLPI, $SQL_TOTAL_REQUEST; $is_debug = isset($_SESSION['glpi_use_mode']) && ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE); if ($is_debug && $CFG_GLPI["debug_sql"]) { $SQL_TOTAL_REQUEST++; $DEBUG_SQL["queries"][$SQL_TOTAL_REQUEST] = $query; } if ($is_debug && $CFG_GLPI["debug_sql"] || $this->execution_time === true) { $TIMER = new Timer(); $TIMER->start(); } $res = $this->dbh->query($query); if (!$res) { // no translation for error logs $error = " *** MySQL query error:\n SQL: ".$query."\n Error: ". $this->dbh->error."\n"; $error .= Toolbox::backtrace(false, 'DBmysql->query()', ['Toolbox::backtrace()']); Toolbox::logSqlError($error); $error_handler = $GLPI->getErrorHandler(); if ($error_handler instanceof ErrorHandler) { $error_handler->handleSqlError($this->dbh->errno, $this->dbh->error, $query); } if (($is_debug || isAPI()) && $CFG_GLPI["debug_sql"]) { $DEBUG_SQL["errors"][$SQL_TOTAL_REQUEST] = $this->error(); } } if ($is_debug && $CFG_GLPI["debug_sql"]) { $TIME = $TIMER->getTime(); $DEBUG_SQL["times"][$SQL_TOTAL_REQUEST] = $TIME; } if ($this->execution_time === true) { $this->execution_time = $TIMER->getTime(0, true); } return $res; } /** * Execute a MySQL query and die * (optionnaly with a message) if it fails * * @since 0.84 * * @param string $query Query to execute * @param string $message Explanation of query (default '') * * @return mysqli_result Query result handler */ function queryOrDie($query, $message = '') { $res = $this->query($query); if (!$res) { //TRANS: %1$s is the description, %2$s is the query, %3$s is the error message $message = sprintf( __('%1$s - Error during the database query: %2$s - Error is %3$s'), $message, $query, $this->error() ); if (isCommandLine()) { throw new \RuntimeException($message); } else { echo $message . "\n"; die(1); } } return $res; } /** * Prepare a MySQL query * * @param string $query Query to prepare * * @return mysqli_stmt|boolean statement object or FALSE if an error occurred. * * @throws GlpitestSQLError */ function prepare($query) { global $CFG_GLPI, $DEBUG_SQL, $SQL_TOTAL_REQUEST; $res = $this->dbh->prepare($query); if (!$res) { // no translation for error logs $error = " *** MySQL prepare error:\n SQL: ".$query."\n Error: ". $this->dbh->error."\n"; $error .= Toolbox::backtrace(false, 'DBmysql->prepare()', ['Toolbox::backtrace()']); Toolbox::logInFile("sql-errors", $error); if (class_exists('GlpitestSQLError')) { // For unit test throw new GlpitestSQLError($error); } if (isset($_SESSION['glpi_use_mode']) && $_SESSION['glpi_use_mode'] == Session::DEBUG_MODE && $CFG_GLPI["debug_sql"]) { $SQL_TOTAL_REQUEST++; $DEBUG_SQL["errors"][$SQL_TOTAL_REQUEST] = $this->error(); } } return $res; } /** * Give result from a sql result * * @param mysqli_result $result MySQL result handler * @param int $i Row offset to give * @param string $field Field to give * * @return mixed Value of the Row $i and the Field $field of the Mysql $result */ function result($result, $i, $field) { if ($result && ($result->data_seek($i)) && ($data = $result->fetch_array()) && isset($data[$field])) { return $data[$field]; } return null; } /** * Number of rows * * @param mysqli_result $result MySQL result handler * * @return integer number of rows */ function numrows($result) { return $result->num_rows; } /** * Fetch array of the next row of a Mysql query * Please prefer fetchRow or fetchAssoc * * @param mysqli_result $result MySQL result handler * * @return string[]|null array results * * @deprecated 9.5.0 */ function fetch_array($result) { Toolbox::deprecated('Use DBmysql::fetchArray()'); return $this->fetchArray($result); } /** * Fetch array of the next row of a Mysql query * Please prefer fetchRow or fetchAssoc * * @param mysqli_result $result MySQL result handler * * @return string[]|null array results */ function fetchArray($result) { return $result->fetch_array(); } /** * Fetch row of the next row of a Mysql query * * @param mysqli_result $result MySQL result handler * * @return mixed|null result row * * @deprecated 9.5.0 */ function fetch_row($result) { Toolbox::deprecated('Use DBmysql::fetchRow()'); return $this->fetchRow($result); } /** * Fetch row of the next row of a Mysql query * * @param mysqli_result $result MySQL result handler * * @return mixed|null result row */ function fetchRow($result) { return $result->fetch_row(); } /** * Fetch assoc of the next row of a Mysql query * * @param mysqli_result $result MySQL result handler * * @return string[]|null result associative array * * @deprecated 9.5.0 */ function fetch_assoc($result) { Toolbox::deprecated('Use DBmysql::fetchAssoc()'); return $this->fetchAssoc($result); } /** * Fetch assoc of the next row of a Mysql query * * @param mysqli_result $result MySQL result handler * * @return string[]|null result associative array */ function fetchAssoc($result) { return $result->fetch_assoc(); } /** * Fetch object of the next row of an SQL query * * @param mysqli_result $result MySQL result handler * * @return object|null */ function fetch_object($result) { Toolbox::deprecated('Use DBmysql::fetchObject()'); return $this->fetchObject(); } /** * Fetch object of the next row of an SQL query * * @param mysqli_result $result MySQL result handler * * @return object|null */ function fetchObject($result) { return $result->fetch_object(); } /** * Move current pointer of a Mysql result to the specific row * * @deprecated 9.5.0 * * @param mysqli_result $result MySQL result handler * @param integer $num Row to move current pointer * * @return boolean */ function data_seek($result, $num) { Toolbox::deprecated('Use DBmysql::dataSeek()'); return $this->dataSeek($result, $num); } /** * Move current pointer of a Mysql result to the specific row * * @param mysqli_result $result MySQL result handler * @param integer $num Row to move current pointer * * @return boolean */ function dataSeek($result, $num) { return $result->data_seek($num); } /** * Give ID of the last inserted item by Mysql * * @return mixed * * @deprecated 9.5.0 */ function insert_id() { Toolbox::deprecated('Use DBmysql::insertId()'); return $this->insertId(); } /** * Give ID of the last inserted item by Mysql * * @return mixed */ function insertId() { return $this->dbh->insert_id; } /** * Give number of fields of a Mysql result * * @deprecated 9.5.0 * * @param mysqli_result $result MySQL result handler * * @return int number of fields */ function num_fields($result) { Toolbox::deprecated('Use DBmysql::numFields()'); return $this->numFields($result); } /** * Give number of fields of a Mysql result * * @param mysqli_result $result MySQL result handler * * @return int number of fields */ function numFields($result) { return $result->field_count; } /** * Give name of a field of a Mysql result * * @param mysqli_result $result MySQL result handler * @param integer $nb ID of the field * * @return string name of the field * * @deprecated 9.5.0 */ function field_name($result, $nb) { Toolbox::deprecated('Use DBmysql::fieldName()'); return $this->fieldName($result, $nb); } /** * Give name of a field of a Mysql result * * @param mysqli_result $result MySQL result handler * @param integer $nb ID of the field * * @return string name of the field * * @deprecated 9.5.0 */ function fieldName($result, $nb) { $finfo = $result->fetch_fields(); return $finfo[$nb]->name; } /** * List tables in database * * @param string $table Table name condition (glpi_% as default to retrieve only glpi tables) * @param array $where Where clause to append * * @return DBmysqlIterator */ function listTables($table = 'glpi_%', array $where = []) { $iterator = $this->request([ 'SELECT' => 'table_name as TABLE_NAME', 'FROM' => 'information_schema.tables', 'WHERE' => [ 'table_schema' => $this->dbdefault, 'table_type' => 'BASE TABLE', 'table_name' => ['LIKE', $table] ] + $where ]); return $iterator; } /** * Returns tables using "MyIsam" engine. * * @return DBmysqlIterator */ public function getMyIsamTables(): DBmysqlIterator { $iterator = $this->listTables('glpi_%', ['engine' => 'MyIsam']); return $iterator; } /** * List fields of a table * * @param string $table Table name condition * @param boolean $usecache If use field list cache (default true) * * @return mixed list of fields * * @deprecated 9.5.0 */ function list_fields($table, $usecache = true) { Toolbox::deprecated('Use DBmysql::listFields()'); return $this->listFields($table, $usecache); } /** * List fields of a table * * @param string $table Table name condition * @param boolean $usecache If use field list cache (default true) * * @return mixed list of fields */ function listFields($table, $usecache = true) { if (!$this->cache_disabled && $usecache && isset($this->field_cache[$table])) { return $this->field_cache[$table]; } $result = $this->query("SHOW COLUMNS FROM `$table`"); if ($result) { if ($this->numrows($result) > 0) { $this->field_cache[$table] = []; while ($data = $this->fetchAssoc($result)) { $this->field_cache[$table][$data["Field"]] = $data; } return $this->field_cache[$table]; } return []; } return false; } /** * Get number of affected rows in previous MySQL operation * * @return int number of affected rows on success, and -1 if the last query failed. * * @deprecated 9.5.0 */ function affected_rows() { Toolbox::deprecated('Use DBmysql::affectedRows()'); return $this->affectedRows(); } /** * Get number of affected rows in previous MySQL operation * * @return int number of affected rows on success, and -1 if the last query failed. */ function affectedRows() { return $this->dbh->affected_rows; } /** * Free result memory * * @param mysqli_result $result MySQL result handler * * @return boolean * * @deprecated 9.5.0 */ function free_result($result) { Toolbox::deprecated('Use DBmysql::freeResult()'); return $this->freeResult($result); } /** * Free result memory * * @param mysqli_result $result MySQL result handler * * @return boolean */ function freeResult($result) { return $result->free(); } /** * Returns the numerical value of the error message from previous MySQL operation * * @return int error number from the last MySQL function, or 0 (zero) if no error occurred. */ function errno() { return $this->dbh->errno; } /** * Returns the text of the error message from previous MySQL operation * * @return string error text from the last MySQL function, or '' (empty string) if no error occurred. */ function error() { return $this->dbh->error; } /** * Close MySQL connection * * @return boolean TRUE on success or FALSE on failure. */ function close() { if ($this->connected && $this->dbh) { return $this->dbh->close(); } return false; } /** * is a slave database ? * * @return boolean */ function isSlave() { return $this->slave; } /** * Execute all the request in a file * * @param string $path with file full path * * @return boolean true if all query are successfull */ function runFile($path) { $script = fopen($path, 'r'); if (!$script) { return false; } $sql_query = @fread( $script, @filesize($path) ) . "\n"; $sql_query = html_entity_decode($sql_query, ENT_COMPAT, 'UTF-8'); $sql_query = $this->removeSqlRemarks($sql_query); $queries = preg_split('/;\s*$/m', $sql_query); foreach ($queries as $query) { $query = trim($query); if ($query != '') { $query = htmlentities($query); if (!$this->query($query)) { return false; } if (!isCommandLine()) { // Flush will prevent proxy to timeout as it will receive data. // Flush requires a content to be sent, so we sent spaces as multiple spaces // will be shown as a single one on browser. echo ' '; Html::glpi_flush(); } } } return true; } /** * Instanciate a Simple DBIterator * * Examples = * foreach ($DB->request("select * from glpi_states") as $data) { ... } * foreach ($DB->request("glpi_states") as $ID => $data) { ... } * foreach ($DB->request("glpi_states", "ID=1") as $ID => $data) { ... } * foreach ($DB->request("glpi_states", "", "name") as $ID => $data) { ... } * foreach ($DB->request("glpi_computers",array("name"=>"SBEI003W","entities_id"=>1),array("serial","otherserial")) { ... } * * Examples = * array("id"=>NULL) * array("OR"=>array("id"=>1, "NOT"=>array("state"=>3))); * array("AND"=>array("id"=>1, array("NOT"=>array("state"=>array(3,4,5),"toto"=>2)))) * * FIELDS name or array of field names * ORDER name or array of field names * LIMIT max of row to retrieve * START first row to retrieve * * @param string|string[] $tableorsql Table name, array of names or SQL query * @param string|string[] $crit String or array of filed/values, ex array("id"=>1), if empty => all rows * (default '') * @param boolean $debug To log the request (default false) * * @return DBmysqlIterator */ public function request ($tableorsql, $crit = "", $debug = false) { $iterator = new DBmysqlIterator($this); $iterator->execute($tableorsql, $crit, $debug); return $iterator; } /** * Get information about DB connection for showSystemInformations * * @since 0.84 * * @return string[] Array of label / value */ public function getInfo() { // No translation, used in sysinfo $ret = []; $req = $this->request("SELECT @@sql_mode as mode, @@version AS vers, @@version_comment AS stype"); if (($data = $req->next())) { if ($data['stype']) { $ret['Server Software'] = $data['stype']; } if ($data['vers']) { $ret['Server Version'] = $data['vers']; } else { $ret['Server Version'] = $this->dbh->server_info; } if ($data['mode']) { $ret['Server SQL Mode'] = $data['mode']; } else { $ret['Server SQL Mode'] = ''; } } $ret['Parameters'] = $this->dbuser."@".$this->dbhost."/".$this->dbdefault; $ret['Host info'] = $this->dbh->host_info; return $ret; } /** * Is MySQL strict mode ? * * @var DB $DB * * @param string $msg Mode * * @return boolean * * @since 0.90 * @deprecated 9.5.0 */ static public function isMySQLStrictMode(&$msg) { Toolbox::deprecated(); global $DB; $msg = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY,NO_AUTO_CREATE_USER'; $req = $DB->request("SELECT @@sql_mode as mode"); if (($data = $req->next())) { return (preg_match("/STRICT_TRANS/", $data['mode']) && preg_match("/NO_ZERO_/", $data['mode']) && preg_match("/ONLY_FULL_GROUP_BY/", $data['mode'])); } return false; } /** * Get a global DB lock * * @since 0.84 * * @param string $name lock's name * * @return boolean */ public function getLock($name) { $name = addslashes($this->dbdefault.'.'.$name); $query = "SELECT GET_LOCK('$name', 0)"; $result = $this->query($query); list($lock_ok) = $this->fetchRow($result); return (bool)$lock_ok; } /** * Release a global DB lock * * @since 0.84 * * @param string $name lock's name * * @return boolean */ public function releaseLock($name) { $name = addslashes($this->dbdefault.'.'.$name); $query = "SELECT RELEASE_LOCK('$name')"; $result = $this->query($query); list($lock_ok) = $this->fetchRow($result); return $lock_ok; } /** * Check if a table exists * * @since 9.2 * @since 9.5 Added $usecache parameter. * * @param string $tablename Table name * @param boolean $usecache If use table list cache * * @return boolean **/ public function tableExists($tablename, $usecache = true) { if (!$this->cache_disabled && $usecache && in_array($tablename, $this->table_cache)) { return true; } // Retrieve all tables if cache is empty but enabled, in order to fill cache // with all known tables $retrieve_all = !$this->cache_disabled && empty($this->table_cache); $result = $this->listTables($retrieve_all ? 'glpi_%' : $tablename); $found_tables = []; while ($data = $result->next()) { $found_tables[] = $data['TABLE_NAME']; } if (!$this->cache_disabled) { $this->table_cache = array_unique(array_merge($this->table_cache, $found_tables)); } if (in_array($tablename, $found_tables)) { return true; } return false; } /** * Check if a field exists * * @since 9.2 * * @param string $table Table name for the field we're looking for * @param string $field Field name * @param Boolean $usecache Use cache; @see DBmysql::listFields(), defaults to true * * @return boolean **/ public function fieldExists($table, $field, $usecache = true) { if (!$this->tableExists($table, $usecache)) { trigger_error("Table $table does not exists", E_USER_WARNING); return false; } if ($fields = $this->listFields($table, $usecache)) { if (isset($fields[$field])) { return true; } return false; } return false; } /** * Disable table cache globally; usefull for migrations * * @return void */ public function disableTableCaching() { $this->cache_disabled = true; } /** * Quote field name * * @since 9.3 * * @param string $name of field to quote (or table.field) * * @return string */ public static function quoteName($name) { //handle verbatim names if ($name instanceof QueryExpression) { return $name->getValue(); } //handle aliases $names = preg_split('/\s+AS\s+/i', $name); if (count($names) > 2) { throw new \RuntimeException( 'Invalid field name ' . $name ); } if (count($names) == 2) { $name = self::quoteName($names[0]); $name .= ' AS ' . self::quoteName($names[1]); return $name; } else { if (strpos($name, '.')) { $n = explode('.', $name, 2); $table = self::quoteName($n[0]); $field = ($n[1] === '*') ? $n[1] : self::quoteName($n[1]); return "$table.$field"; } return ($name[0] == '`' ? $name : ($name === '*' ? $name : "`$name`")); } } /** * Quote value for insert/update * * @param mixed $value Value * * @return mixed */ public static function quoteValue($value) { if ($value instanceof QueryParam || $value instanceof QueryExpression) { //no quote for query parameters nor expressions $value = $value->getValue(); } else if ($value === null || $value === 'NULL' || $value === 'null') { $value = 'NULL'; } else { //phone numbers may start with '+' and will be considered as numeric $value = "'$value'"; } return $value; } /** * Builds an insert statement * * @since 9.3 * * @param string $table Table name * @param array $params Query parameters ([field name => field value) * * @return string */ public function buildInsert($table, $params) { $query = "INSERT INTO " . self::quoteName($table) . " ("; $fields = []; foreach ($params as $key => &$value) { $fields[] = $this->quoteName($key); $value = $this->quoteValue($value); } $query .= implode(', ', $fields); $query .= ") VALUES ("; $query .= implode(", ", $params); $query .= ")"; return $query; } /** * Insert a row in the database * * @since 9.3 * * @param string $table Table name * @param array $params Query parameters ([field name => field value) * * @return mysqli_result|boolean Query result handler */ public function insert($table, $params) { $result = $this->query( $this->buildInsert($table, $params) ); return $result; } /** * Insert a row in the database and die * (optionnaly with a message) if it fails * * @since 9.3 * * @param string $table Table name * @param array $params Query parameters ([field name => field value) * @param string $message Explanation of query (default '') * * @return mysqli_result|boolean Query result handler */ function insertOrDie($table, $params, $message = '') { $insert = $this->buildInsert($table, $params); $res = $this->query($insert); if (!$res) { //TRANS: %1$s is the description, %2$s is the query, %3$s is the error message $message = sprintf( __('%1$s - Error during the database query: %2$s - Error is %3$s'), $message, $insert, $this->error() ); if (isCommandLine()) { throw new \RuntimeException($message); } else { echo $message . "\n"; die(1); } } return $res; } /** * Builds an update statement * * @since 9.3 * * @param string $table Table name * @param array $params Query parameters ([field name => field value) * @param array $clauses Clauses to use. If not 'WHERE' key specified, will b the WHERE clause (@see DBmysqlIterator capabilities) * @param array $joins JOINS criteria array * * @since 9.4.0 $joins parameter added * @return string */ public function buildUpdate($table, $params, $clauses, array $joins = []) { //when no explicit "WHERE", we only have a WHEre clause. if (!isset($clauses['WHERE'])) { $clauses = ['WHERE' => $clauses]; } else { $known_clauses = ['WHERE', 'ORDER', 'LIMIT', 'START']; foreach (array_keys($clauses) as $key) { if (!in_array($key, $known_clauses)) { throw new \RuntimeException( str_replace( '%clause', $key, 'Trying to use an unknonw clause (%clause) building update query!' ) ); } } } if (!count($clauses['WHERE'])) { throw new \RuntimeException('Cannot run an UPDATE query without WHERE clause!'); } $query = "UPDATE ". self::quoteName($table); //JOINS $it = new DBmysqlIterator($this); $query .= $it->analyseJoins($joins); $query .= " SET "; foreach ($params as $field => $value) { $query .= self::quoteName($field) . " = ".$this->quoteValue($value).", "; } $query = rtrim($query, ', '); $query .= " WHERE " . $it->analyseCrit($clauses['WHERE']); // ORDER BY if (isset($clauses['ORDER']) && !empty($clauses['ORDER'])) { $query .= $it->handleOrderClause($clauses['ORDER']); } if (isset($clauses['LIMIT']) && !empty($clauses['LIMIT'])) { $offset = (isset($clauses['START']) && !empty($clauses['START'])) ? $clauses['START'] : null; $query .= $it->handleLimits($clauses['LIMIT'], $offset); } return $query; } /** * Update a row in the database * * @since 9.3 * * @param string $table Table name * @param array $params Query parameters ([:field name => field value) * @param array $where WHERE clause * @param array $joins JOINS criteria array * * @since 9.4.0 $joins parameter added * @return mysqli_result|boolean Query result handler */ public function update($table, $params, $where, array $joins = []) { $query = $this->buildUpdate($table, $params, $where, $joins); $result = $this->query($query); return $result; } /** * Update a row in the database or die * (optionnaly with a message) if it fails * * @since 9.3 * * @param string $table Table name * @param array $params Query parameters ([:field name => field value) * @param array $where WHERE clause * @param string $message Explanation of query (default '') * @param array $joins JOINS criteria array * * @since 9.4.0 $joins parameter added * @return mysqli_result|boolean Query result handler */ function updateOrDie($table, $params, $where, $message = '', array $joins = []) { $update = $this->buildUpdate($table, $params, $where, $joins); $res = $this->query($update); if (!$res) { //TRANS: %1$s is the description, %2$s is the query, %3$s is the error message $message = sprintf( __('%1$s - Error during the database query: %2$s - Error is %3$s'), $message, $update, $this->error() ); if (isCommandLine()) { throw new \RuntimeException($message); } else { echo $message . "\n"; die(1); } } return $res; } /** * Update a row in the database or insert a new one * * @since 9.4 * * @param string $table Table name * @param array $params Query parameters ([:field name => field value) * @param array $where WHERE clause * @param boolean $onlyone Do the update only one one element, defaults to true * * @return mysqli_result|boolean Query result handler */ public function updateOrInsert($table, $params, $where, $onlyone = true) { $req = $this->request($table, $where); $data = array_merge($where, $params); if ($req->count() == 0) { return $this->insertOrDie($table, $data, 'Unable to create new element or update existing one'); } else if ($req->count() == 1 || !$onlyone) { return $this->updateOrDie($table, $data, $where, 'Unable to create new element or update existing one'); } else { Toolbox::logWarning('Update would change too many rows!'); return false; } } /** * Builds a delete statement * * @since 9.3 * * @param string $table Table name * @param array $params Query parameters ([field name => field value) * @param array $where WHERE clause (@see DBmysqlIterator capabilities) * @param array $joins JOINS criteria array * * @since 9.4.0 $joins parameter added * @return string */ public function buildDelete($table, $where, array $joins = []) { if (!count($where)) { throw new \RuntimeException('Cannot run an DELETE query without WHERE clause!'); } $query = "DELETE " . self::quoteName($table) . " FROM ". self::quoteName($table); $it = new DBmysqlIterator($this); $query .= $it->analyseJoins($joins); $query .= " WHERE " . $it->analyseCrit($where); return $query; } /** * Delete rows in the database * * @since 9.3 * * @param string $table Table name * @param array $where WHERE clause * @param array $joins JOINS criteria array * * @since 9.4.0 $joins parameter added * @return mysqli_result|boolean Query result handler */ public function delete($table, $where, array $joins = []) { $query = $this->buildDelete($table, $where, $joins); $result = $this->query($query); return $result; } /** * Delete a row in the database and die * (optionnaly with a message) if it fails * * @since 9.3 * * @param string $table Table name * @param array $where WHERE clause * @param string $message Explanation of query (default '') * @param array $joins JOINS criteria array * * @since 9.4.0 $joins parameter added * @return mysqli_result|boolean Query result handler */ function deleteOrDie($table, $where, $message = '', array $joins = []) { $update = $this->buildDelete($table, $where, $joins); $res = $this->query($update); if (!$res) { //TRANS: %1$s is the description, %2$s is the query, %3$s is the error message $message = sprintf( __('%1$s - Error during the database query: %2$s - Error is %3$s'), $message, $update, $this->error() ); if (isCommandLine()) { throw new \RuntimeException($message); } else { echo $message . "\n"; die(1); } } return $res; } /** * Get table schema * * @param string $table Table name, * @param string|null $structure Raw table structure * * @return array */ public function getTableSchema($table, $structure = null) { if ($structure === null) { $structure = $this->query("SHOW CREATE TABLE `$table`")->fetch_row(); $structure = $structure[1]; } //get table index $index = preg_grep( "/^\s\s+?KEY/", array_map( function($idx) { return rtrim($idx, ','); }, explode("\n", $structure) ) ); //get table schema, without index, without AUTO_INCREMENT $structure = preg_replace( [ "/\s\s+KEY .*/", "/AUTO_INCREMENT=\d+ /" ], "", $structure ); $structure = preg_replace('/,(\s)?$/m', '', $structure); $structure = preg_replace('/ COMMENT \'(.+)\'/', '', $structure); $structure = str_replace( [ " COLLATE utf8_unicode_ci", " CHARACTER SET utf8", ', ', ], [ '', '', ',', ], trim($structure) ); //do not check engine nor collation $structure = preg_replace( '/\) ENGINE.*$/', '', $structure ); //Mariadb 10.2 will return current_timestamp() //while older retuns CURRENT_TIMESTAMP... $structure = preg_replace( '/ CURRENT_TIMESTAMP\(\)/i', ' CURRENT_TIMESTAMP', $structure ); //Mariadb 10.2 allow default values on longblob, text and longtext $defaults = []; preg_match_all( '/^.+ (longblob|text|longtext) .+$/m', $structure, $defaults ); if (count($defaults[0])) { foreach ($defaults[0] as $line) { $structure = str_replace( $line, str_replace(' DEFAULT NULL', '', $line), $structure ); } } $structure = preg_replace("/(DEFAULT) ([-|+]?\d+)(\.\d+)?/", "$1 '$2$3'", $structure); //$structure = preg_replace("/(DEFAULT) (')?([-|+]?\d+)(\.\d+)(')?/", "$1 '$3'", $structure); $structure = preg_replace('/(BIGINT)\(\d+\)/i', '$1', $structure); $structure = preg_replace('/(TINYINT) /i', '$1(4) ', $structure); return [ 'schema' => strtolower($structure), 'index' => $index ]; } /** * Get database raw version * * @return string */ public function getVersion() { $req = $this->request('SELECT version()')->next(); $raw = $req['version()']; return $raw; } /** * Starts a transaction * * @return boolean */ public function beginTransaction() { $this->in_transaction = true; return $this->dbh->begin_transaction(); } /** * Commits a transaction * * @return boolean */ public function commit() { $this->in_transaction = false; return $this->dbh->commit(); } /** * Rollbacks a transaction * * @return boolean */ public function rollBack() { $this->in_transaction = false; return $this->dbh->rollback(); } /** * Are we in a transaction? * * @return boolean */ public function inTransaction() { return $this->in_transaction; } /** * Check if timezone data is accessible and available in database. * * @param string $msg Variable that would contain the reason of data unavailability. * * @return boolean * * @since 9.5.0 */ public function areTimezonesAvailable(string &$msg = '') { $mysql_db_res = $this->request('SHOW DATABASES LIKE ' . $this->quoteValue('mysql')); if ($mysql_db_res->count() === 0) { $msg = __('Access to timezone database (mysql) is not allowed.'); return false; } $tz_table_res = $this->request( 'SHOW TABLES FROM ' . $this->quoteName('mysql') . ' LIKE ' . $this->quoteValue('time_zone_name') ); if ($tz_table_res->count() === 0) { $msg = __('Access to timezone table (mysql.time_zone_name) is not allowed.'); return false; } $criteria = [ 'COUNT' => 'cpt', 'FROM' => 'mysql.time_zone_name', ]; $iterator = $this->request($criteria); $result = $iterator->next(); if ($result['cpt'] == 0) { $msg = __('Timezones seems not loaded, see https://glpi-install.readthedocs.io/en/latest/timezones.html.'); return false; } return true; } /** * Defines timezone to use. * * @param string $timezone * * @return DBmysql */ public function setTimezone($timezone) { //setup timezone if ($this->areTimezonesAvailable()) { date_default_timezone_set($timezone); $this->dbh->query("SET SESSION time_zone = '$timezone'"); $_SESSION['glpi_currenttime'] = date("Y-m-d H:i:s"); } return $this; } /** * Returns list of timezones. * * @return string[] * * @since 9.5.0 */ public function getTimezones() { $list = []; //default $tz is empty $from_php = \DateTimeZone::listIdentifiers(); $now = new \DateTime(); $iterator = $this->request([ 'SELECT' => 'Name', 'FROM' => 'mysql.time_zone_name', 'WHERE' => ['Name' => $from_php] ]); while ($from_mysql = $iterator->next()) { $now->setTimezone(new \DateTimeZone($from_mysql['Name'])); $list[$from_mysql['Name']] = $from_mysql['Name'] . $now->format(" (T P)"); } return $list; } /** * Returns count of tables that were not migrated to be compatible with timezones usage. * * @return number * * @since 9.5.0 */ public function notTzMigrated() { global $DB; $result = $DB->request([ 'COUNT' => 'cpt', 'FROM' => 'information_schema.columns', 'WHERE' => [ 'information_schema.columns.table_schema' => $DB->dbdefault, 'information_schema.columns.data_type' => ['datetime'] ] ])->next(); return (int)$result['cpt']; } /** * Clear cached schema informations. * * @return void */ public function clearSchemaCache() { $this->table_cache = []; $this->field_cache = []; } /** * Quote a value for a specified type * Should be used for PDO, but this will prevent heavy * replacements in the source code in the future. * * @param mixed $value Value to quote * @param integer $type Value type, defaults to PDO::PARAM_STR * * @return mixed * * @since 9.5.0 */ public function quote($value, int $type = 2/*\PDO::PARAM_STR*/) { return "'" . $this->escape($value) . "'"; //return $this->dbh->quote($value, $type); } /** * Get character used to quote names for current database engine * * @return string * * @since 9.5.0 */ public static function getQuoteNameChar(): string { return '`'; } /** * Is value quoted as database field/expression? * * @param string|\QueryExpression $value Value to check * * @return boolean * * @since 9.5.0 */ public static function isNameQuoted($value): bool { $quote = static::getQuoteNameChar(); return is_string($value) && trim($value, $quote) != $value; } /** * Remove SQL comments * © 2011 PHPBB Group * * @param string $output SQL statements * * @return string */ public function removeSqlComments($output) { $lines = explode("\n", $output); $output = ""; // try to keep mem. use down $linecount = count($lines); $in_comment = false; for ($i = 0; $i < $linecount; $i++) { if (preg_match("/^\/\*/", $lines[$i])) { $in_comment = true; } if (!$in_comment) { $output .= $lines[$i] . "\n"; } if (preg_match("/\*\/$/", preg_quote($lines[$i]))) { $in_comment = false; } } unset($lines); return trim($output); } /** * Remove remarks and comments from SQL * @see DBmysql::removeSqlComments() * © 2011 PHPBB Group * * @param $string $sql SQL statements * * @return string */ public function removeSqlRemarks($sql) { $lines = explode("\n", $sql); // try to keep mem. use down $sql = ""; $linecount = count($lines); $output = ""; for ($i = 0; $i < $linecount; $i++) { if (($i != ($linecount - 1)) || (strlen($lines[$i]) > 0)) { if (isset($lines[$i][0])) { if ($lines[$i][0] != "#" && substr($lines[$i], 0, 2) != "--") { $output .= $lines[$i] . "\n"; } else { $output .= "\n"; } } // Trading a bit of speed for lower mem. use here. $lines[$i] = ""; } } return trim($this->removeSqlComments($output)); } }