%PDF- %PDF-
Direktori : /var/www/projetos/suporte.iigd.com.br.old/src/ |
Current File : /var/www/projetos/suporte.iigd.com.br.old/src/DBmysql.php |
<?php /** * --------------------------------------------------------------------- * * GLPI - Gestionnaire Libre de Parc Informatique * * http://glpi-project.org * * @copyright 2015-2022 Teclib' and contributors. * @copyright 2003-2014 by the INDEPNET Development Team. * @licence https://www.gnu.org/licenses/gpl-3.0.html * * --------------------------------------------------------------------- * * LICENSE * * This file is part of GLPI. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * --------------------------------------------------------------------- */ use Glpi\Application\ErrorHandler; use Glpi\System\Requirement\DbTimezones; use Glpi\Toolbox\Sanitizer; /** * 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 = ""; /** * The database handler * @var mysqli */ protected $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; /** * Determine if timezones should be used for timestamp fields. * Defaults to false to keep backward compatibility with old DB. * * @var bool */ public $use_timezones = false; /** * Determine if warnings related to MySQL deprecations should be logged too. * Defaults to false as this option should only on development/test environment. * * @var bool */ public $log_deprecation_warnings = false; /** * Determine if utf8mb4 should be used for DB connection and tables altering operations. * Defaults to false to keep backward compatibility with old DB. * * @var bool */ public $use_utf8mb4 = false; /** * Determine if MyISAM engine usage should be allowed for tables creation/altering operations. * Defaults to true to keep backward compatibility with old DB. * * @var bool */ public $allow_myisam = true; /** * Determine if datetime fields usage should be allowed for tables creation/altering operations. * Defaults to true to keep backward compatibility with old DB. * * @var bool */ public $allow_datetime = true; /** * Determine if signed integers in primary/foreign keys usage should be allowed for tables creation/altering operations. * Defaults to true to keep backward compatibility with old DB. * * @var bool */ public $allow_signed_keys = true; /** 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 = []; /** * Last query warnings. * * @var array */ private $last_query_warnings = []; /** * Constructor / Connect to the MySQL Database * * @param integer $choice host number (default NULL) * * @return void */ public 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 */ public function connect($choice = null) { $this->connected = false; // Do not trigger errors nor throw exceptions at PHP level // as we already extract error and log while fetching result. mysqli_report(MYSQLI_REPORT_OFF); $this->dbh = @new mysqli(); if ($this->dbssl) { $this->dbh->ssl_set( $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 { $this->setConnectionCharset(); // force mysqlnd to return int and float types correctly (not as strings) $this->dbh->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true); $this->dbh->query("SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode, 'ONLY_FULL_GROUP_BY', ''))"); $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 */ public function guessTimezone() { if ($this->use_timezones) { 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' ] ])->current(); } $zone = !empty($conf_tz['value']) ? $conf_tz['value'] : date_default_timezone_get(); } } else { $zone = 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 */ public function escape($string) { if (!is_string($string)) { return $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 */ public function query($query) { global $CFG_GLPI, $DEBUG_SQL, $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(); } $this->checkForDeprecatedTableOptions($query); $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); ErrorHandler::getInstance()->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; $DEBUG_SQL['rows'][$SQL_TOTAL_REQUEST] = $this->affectedRows(); } $this->last_query_warnings = $this->fetchQueryWarnings(); $DEBUG_SQL['warnings'][$SQL_TOTAL_REQUEST] = $this->last_query_warnings; // Output warnings in SQL log if (!empty($this->last_query_warnings)) { $message = sprintf( " *** MySQL query warnings:\n SQL: %s\n Warnings: \n%s\n", $query, implode( "\n", array_map( function ($warning) { return sprintf('%s: %s', $warning['Code'], $warning['Message']); }, $this->last_query_warnings ) ) ); $message .= Toolbox::backtrace(false, 'DBmysql->query()', ['Toolbox::backtrace()']); Toolbox::logSqlWarning($message); ErrorHandler::getInstance()->handleSqlWarnings($this->last_query_warnings, $query); } 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 */ public 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. */ public 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::logSqlError($error); ErrorHandler::getInstance()->handleSqlError($this->dbh->errno, $this->dbh->error, $query); 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 */ public 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 */ public 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 */ public 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 */ public 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 */ public 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 */ public function fetchObject($result) { return $result->fetch_object(); } /** * 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 */ public function dataSeek($result, $num) { return $result->data_seek($num); } /** * Give ID of the last inserted item by Mysql * * @return mixed */ public function insertId() { return $this->dbh->insert_id; } /** * Give number of fields of a Mysql result * * @param mysqli_result $result MySQL result handler * * @return int number of fields */ public 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 */ public 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 */ public 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. * * @param bool $exclude_plugins * * @return DBmysqlIterator */ public function getMyIsamTables(bool $exclude_plugins = false): DBmysqlIterator { $criteria = [ 'engine' => 'MyIsam', ]; if ($exclude_plugins) { $criteria[] = ['NOT' => ['information_schema.tables.table_name' => ['LIKE', 'glpi\_plugin\_%']]]; } $iterator = $this->listTables('glpi\_%', $criteria); return $iterator; } /** * Returns tables not using "utf8mb4_unicode_ci" collation. * * @param bool $exclude_plugins * * @return DBmysqlIterator */ public function getNonUtf8mb4Tables(bool $exclude_plugins = false): DBmysqlIterator { // Find tables that does not use utf8mb4 collation $tables_query = [ 'SELECT' => ['information_schema.tables.table_name as TABLE_NAME'], 'DISTINCT' => true, 'FROM' => 'information_schema.tables', 'WHERE' => [ 'information_schema.tables.table_schema' => $this->dbdefault, 'information_schema.tables.table_name' => ['LIKE', 'glpi\_%'], 'information_schema.tables.table_type' => 'BASE TABLE', ['NOT' => ['information_schema.tables.table_collation' => 'utf8mb4_unicode_ci']], ], ]; // Find columns that does not use utf8mb4 collation $columns_query = [ 'SELECT' => ['information_schema.columns.table_name as TABLE_NAME'], 'DISTINCT' => true, 'FROM' => 'information_schema.columns', 'INNER JOIN' => [ 'information_schema.tables' => [ 'FKEY' => [ 'information_schema.tables' => 'table_name', 'information_schema.columns' => 'table_name', [ 'AND' => [ 'information_schema.tables.table_schema' => new QueryExpression( $this->quoteName('information_schema.columns.table_schema') ), ] ], ] ] ], 'WHERE' => [ 'information_schema.tables.table_schema' => $this->dbdefault, 'information_schema.tables.table_name' => ['LIKE', 'glpi\_%'], 'information_schema.tables.table_type' => 'BASE TABLE', ['NOT' => ['information_schema.columns.collation_name' => null]], ['NOT' => ['information_schema.columns.collation_name' => 'utf8mb4_unicode_ci']] ], ]; if ($exclude_plugins) { $tables_query['WHERE'][] = ['NOT' => ['information_schema.tables.table_name' => ['LIKE', 'glpi\_plugin\_%']]]; $columns_query['WHERE'][] = ['NOT' => ['information_schema.tables.table_name' => ['LIKE', 'glpi\_plugin\_%']]]; } $iterator = $this->request([ 'SELECT' => ['TABLE_NAME'], 'DISTINCT' => true, 'FROM' => new QueryUnion([$tables_query, $columns_query], true), 'ORDER' => ['TABLE_NAME'] ]); return $iterator; } /** * Returns tables not compatible with timezone usage, i.e. having "datetime" columns. * * @param bool $exclude_plugins * * @return DBmysqlIterator * * @since 10.0.0 */ public function getTzIncompatibleTables(bool $exclude_plugins = false): DBmysqlIterator { $query = [ 'SELECT' => ['information_schema.columns.table_name as TABLE_NAME'], 'DISTINCT' => true, 'FROM' => 'information_schema.columns', 'INNER JOIN' => [ 'information_schema.tables' => [ 'FKEY' => [ 'information_schema.tables' => 'table_name', 'information_schema.columns' => 'table_name', [ 'AND' => [ 'information_schema.tables.table_schema' => new QueryExpression( $this->quoteName('information_schema.columns.table_schema') ), ] ], ] ] ], 'WHERE' => [ 'information_schema.tables.table_schema' => $this->dbdefault, 'information_schema.tables.table_name' => ['LIKE', 'glpi\_%'], 'information_schema.tables.table_type' => 'BASE TABLE', 'information_schema.columns.data_type' => 'datetime', ], 'ORDER' => ['TABLE_NAME'] ]; if ($exclude_plugins) { $query['WHERE'][] = ['NOT' => ['information_schema.tables.table_name' => ['LIKE', 'glpi\_plugin\_%']]]; } $iterator = $this->request($query); return $iterator; } /** * Returns columns that uses signed integers for primary/foreign keys. * * @param bool $exclude_plugins * * @return DBmysqlIterator * * @since 9.5.7 */ public function getSignedKeysColumns(bool $exclude_plugins = false) { $query = [ 'SELECT' => [ 'information_schema.columns.table_name as TABLE_NAME', 'information_schema.columns.column_name as COLUMN_NAME', 'information_schema.columns.data_type as DATA_TYPE', 'information_schema.columns.column_default as COLUMN_DEFAULT', 'information_schema.columns.is_nullable as IS_NULLABLE', 'information_schema.columns.extra as EXTRA', ], 'FROM' => 'information_schema.columns', 'INNER JOIN' => [ 'information_schema.tables' => [ 'FKEY' => [ 'information_schema.tables' => 'table_name', 'information_schema.columns' => 'table_name', [ 'AND' => [ 'information_schema.tables.table_schema' => new QueryExpression( $this->quoteName('information_schema.columns.table_schema') ), ] ], ] ] ], 'WHERE' => [ 'information_schema.tables.table_schema' => $this->dbdefault, 'information_schema.tables.table_name' => ['LIKE', 'glpi\_%'], 'information_schema.tables.table_type' => 'BASE TABLE', [ 'OR' => [ ['information_schema.columns.column_name' => 'id'], ['information_schema.columns.column_name' => ['LIKE', '%\_id']], ['information_schema.columns.column_name' => ['LIKE', '%\_id\_%']], ], ], 'information_schema.columns.data_type' => ['tinyint', 'smallint', 'mediumint', 'int', 'bigint'], ['NOT' => ['information_schema.columns.column_type' => ['LIKE', '%unsigned%']]], ], 'ORDER' => ['TABLE_NAME'] ]; if ($exclude_plugins) { $query['WHERE'][] = ['NOT' => ['information_schema.tables.table_name' => ['LIKE', 'glpi\_plugin\_%']]]; } $iterator = $this->request($query); return $iterator; } /** * Returns foreign keys constraints. * * @return DBmysqlIterator * * @since 9.5.7 */ public function getForeignKeysContraints() { $query = [ 'SELECT' => [ 'table_schema as TABLE_SCHEMA', 'table_name as TABLE_NAME', 'column_name as COLUMN_NAME', 'constraint_name as CONSTRAINT_NAME', 'referenced_table_name as REFERENCED_TABLE_NAME', 'referenced_column_name as REFERENCED_COLUMN_NAME', 'ordinal_position as ORDINAL_POSITION', ], 'FROM' => 'information_schema.key_column_usage', 'WHERE' => [ 'referenced_table_schema' => $this->dbdefault, 'referenced_table_name' => ['LIKE', 'glpi\_%'], ], 'ORDER' => ['TABLE_NAME'] ]; $iterator = $this->request($query); 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 */ public 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 field of a table * * @param string $table * @param string $field * @param boolean $usecache * * @return array|null Field characteristics */ public function getField(string $table, string $field, $usecache = true): ?array { $fields = $this->listFields($table, $usecache); return $fields[$field] ?? null; } /** * Get number of affected rows in previous MySQL operation * * @return int number of affected rows on success, and -1 if the last query failed. */ public function affectedRows() { return $this->dbh->affected_rows; } /** * Free result memory * * @param mysqli_result $result MySQL result handler * * @return boolean */ public 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. */ public 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. */ public function error() { return $this->dbh->error; } /** * Close MySQL connection * * @return boolean TRUE on success or FALSE on failure. */ public function close() { if ($this->connected && $this->dbh) { return $this->dbh->close(); } return false; } /** * is a slave database ? * * @return boolean */ public 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 */ public 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, ENT_COMPAT, 'UTF-8'); 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 showSystemInformation * * @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->current())) { 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; } /** * 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 = []; foreach ($result as $data) { $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 if (is_bool($value)) { // transform boolean as int (prevent `false` to be transformed to empty string) $value = "'" . (int)$value . "'"; } else { if (Sanitizer::isNsClassOrCallableIdentifier($value)) { // Values that corresponds to an existing namespaced class are not sanitized (see `Glpi\Toolbox\Sanitizer::sanitize()`). // However, they have to be escaped in SQL queries. // Note: method is called statically, so `$DB` may be not defined yet in edge cases (install process). global $DB; $value = $DB instanceof DBmysql && $DB->connected ? $DB->escape($value) : $value; } // 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 */ public 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 */ public 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 { trigger_error('Update would change too many rows!', E_USER_WARNING); 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 */ public 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; } /** * Truncate table in the database * * @since 10.0.0 * * @param string $table Table name * * @return mysqli_result|boolean Query result handler */ public function truncate($table) { // Use delete to prevent table corruption on some MySQL operations // (i.e. when using mysqldump without `--single-transaction` option) return $this->delete($table, [1]); } /** * Truncate table in the database or die * (optionally with a message) if it fails * * @since 10.0.0 * * @param string $table Table name * @param string $message Explanation of query (default '') * * @return mysqli_result|boolean Query result handler */ public function truncateOrDie($table, $message = '') { $table_name = $this::quoteName($table); $res = $this->query("TRUNCATE $table_name"); 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, "TRUNCATE $table", $this->error() ); if (isCommandLine()) { throw new \RuntimeException($message); } echo $message . "\n"; die(1); } return $res; } /** * Get database raw version * * @return string */ public function getVersion() { $req = $this->request('SELECT version()')->current(); $raw = $req['version()']; return $raw; } /** * Starts a transaction * * @return boolean */ public function beginTransaction() { if ($this->in_transaction === true) { trigger_error('A database transaction has already been started!', E_USER_WARNING); } $this->in_transaction = true; return $this->dbh->begin_transaction(); } public function setSavepoint(string $name, $force = false) { if (!$this->in_transaction && $force) { $this->beginTransaction(); } if ($this->in_transaction) { $this->dbh->savepoint($name); } else { // Not already in transaction or failed to start one now trigger_error('Unable to set DB savepoint because no transaction was started', E_USER_WARNING); } } /** * Commits a transaction * * @return boolean */ public function commit() { $this->in_transaction = false; return $this->dbh->commit(); } /** * Rollbacks a transaction completely or to a specified savepoint * * @return boolean */ public function rollBack($savepoint = null) { if (!$savepoint) { $this->in_transaction = false; return $this->dbh->rollback(); } else { return $this->rollbackTo($savepoint); } } /** * Rollbacks a transaction to a specified savepoint * * @param string $name * * @return boolean */ protected function rollbackTo($name) { // No proper rollback to savepoint support in mysqli extension? $result = $this->query('ROLLBACK TO ' . self::quoteName($name)); return $result !== false; } /** * Are we in a transaction? * * @return boolean */ public function inTransaction() { return $this->in_transaction; } /** * Defines timezone to use. * * @param string $timezone * * @return DBmysql */ public function setTimezone($timezone) { //setup timezone if ($this->use_timezones) { 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() { if (!$this->use_timezones) { return []; } $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] ]); foreach ($iterator as $from_mysql) { $now->setTimezone(new \DateTimeZone($from_mysql['Name'])); $list[$from_mysql['Name']] = $from_mysql['Name'] . $now->format(" (T P)"); } return $list; } /** * Clear cached schema information. * * @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)); } /** * Fetch warnings from last query. * * @return array */ private function fetchQueryWarnings(): array { $warnings = []; if ($this->dbh->warning_count > 0 && $warnings_result = $this->dbh->query('SHOW WARNINGS')) { // Warnings to exclude $excludes = []; if (!$this->use_utf8mb4 || !$this->log_deprecation_warnings) { // Exclude warnings related to usage of "utf8mb3" charset, as database has not been migrated yet. $excludes[] = 1287; // 'utf8mb3' is deprecated and will be removed in a future release. Please use utf8mb4 instead. $excludes[] = 3719; // 'utf8' is currently an alias for the character set UTF8MB3, but will be an alias for UTF8MB4 in a future release. Please consider using UTF8MB4 in order to be unambiguous. $excludes[] = 3778; // 'utf8_unicode_ci' is a collation of the deprecated character set UTF8MB3. Please consider using UTF8MB4 with an appropriate collation instead. } if (!$this->log_deprecation_warnings) { // Mute deprecations related to elements that are heavilly used in old migrations and in plugins // as it may require a lot of work to fix them. $excludes[] = 1681; // Integer display width is deprecated and will be removed in a future release. } while ($warning = $warnings_result->fetch_assoc()) { if ($warning['Level'] === 'Note' || in_array($warning['Code'], $excludes)) { continue; } $warnings[] = $warning; } } return $warnings; } /** * Get SQL warnings related to last query. * @return array */ public function getLastQueryWarnings(): array { return $this->last_query_warnings; } /** * Set charset to use for DB connection. * * @return void */ public function setConnectionCharset(): void { DBConnection::setConnectionCharset($this->dbh, $this->use_utf8mb4); } /** * Executes a prepared statement * * @param mysqli_stmt $stmt STatement to execute * * @return void */ public function executeStatement(mysqli_stmt $stmt): void { if (!$stmt->execute()) { trigger_error($stmt->error, E_USER_ERROR); } } /** * Check for deprecated table options during ALTER/CREATE TABLE queries. * * @param string $query * * @return void */ private function checkForDeprecatedTableOptions(string $query): void { $table_matches = []; $table_pattern = '/' . '(ALTER|CREATE)' . '(\s+TEMPORARY)?' . '\s+TABLE' . '(\s+IF\s+NOT\s+EXISTS)?' . '\s*(\s|`)(?<table>[^`\s]+)(\s|`)' . '/i'; if (preg_match($table_pattern, $query, $table_matches) !== 1) { return; } // Wrong UTF8 charset/collation $charset_matches = []; if ($this->use_utf8mb4 && preg_match('/(?<invalid>(utf8(_[^\';\s]+)?))([\';\s]|$)/', $query, $charset_matches)) { trigger_error( sprintf( 'Usage of "%s" charset/collation detected, should be "%s"', $charset_matches['invalid'], str_replace('utf8', 'utf8mb4', $charset_matches['invalid']) ), E_USER_WARNING ); } else if (!$this->use_utf8mb4 && preg_match('/(?<invalid>(utf8mb4(_[^\';\s]+)?))([\';\s]|$)/', $query, $charset_matches)) { trigger_error( sprintf( 'Usage of "%s" charset/collation detected, should be "%s"', $charset_matches['invalid'], str_replace('utf8mb4', 'utf8', $charset_matches['invalid']) ), E_USER_WARNING ); } // Usage of MyISAM if (!$this->allow_myisam && preg_match('/[)\s]engine\s*=\s*\'?myisam([\';\s]|$)/i', $query)) { trigger_error('Usage of "MyISAM" engine is discouraged, please use "InnoDB" engine.', E_USER_WARNING); } // Usage of datetime if (!$this->allow_datetime && preg_match('/ datetime /i', $query)) { trigger_error('Usage of "DATETIME" fields is discouraged, please use "TIMESTAMP" fields instead.', E_USER_WARNING); } // Usage of signed integers in primary/foreign keys $pattern = '/' . '(\s|`)(?<field>id|[^`\s]+_id|[^`\s]+_id_[^`\s]+)(\s|`)' // `id`, `xxx_id` or `xxx_id_yyy` field . '\s*' . '(tiny|small|medium|big)?int' // with int type . '(?!\s+unsigned)' // not unsigned . '/i'; $field_matches = []; if (!$this->allow_signed_keys && preg_match($pattern, $query, $field_matches)) { trigger_error( sprintf( 'Usage of signed integers in primary or foreign keys is discouraged, please use unsigned integers instead in `%s`.`%s`.', $table_matches['table'], $field_matches['field'] ), E_USER_WARNING ); } } /** * Return configuration boolean properties computed using current state of tables. * * @return array */ public function getComputedConfigBooleanFlags(): array { $config_flags = []; if ($this->getTzIncompatibleTables(true)->count() === 0) { // Disallow datetime if there is no core table still using this field type. $config_flags[DBConnection::PROPERTY_ALLOW_DATETIME] = false; $timezones_requirement = new DbTimezones($this); if ($timezones_requirement->isValidated()) { // Activate timezone usage if timezones are available and all tables are already migrated. $config_flags[DBConnection::PROPERTY_USE_TIMEZONES] = true; } } if ($this->getNonUtf8mb4Tables(true)->count() === 0) { // Use utf8mb4 charset for update process if there all core table are using this charset. $config_flags[DBConnection::PROPERTY_USE_UTF8MB4] = true; } if ($this->getMyIsamTables(true)->count() === 0) { // Disallow MyISAM if there is no core table still using this engine. $config_flags[DBConnection::PROPERTY_ALLOW_MYISAM] = false; } if ($this->getSignedKeysColumns(true)->count() === 0) { // Disallow MyISAM if there is no core table still using this engine. $config_flags[DBConnection::PROPERTY_ALLOW_SIGNED_KEYS] = false; } return $config_flags; } }