plugins/db/classes/yf_db.class.php
<?php
/**
* Database abstraction layer.
*
* @author YFix Team <yfix.dev@gmail.com>
* @version 1.0
*/
class yf_db
{
/** @var string Type of database (default) */
public $DB_TYPE = 'mysql';
/** @var bool Switch caching on/off */
public $NO_CACHE = false;
/** @var bool Use tables names caching */
public $CACHE_TABLE_NAMES = false;
/** @var int @conf_skip Number of queries */
public $NUM_QUERIES = 0;
/** @var array Query log array */
public $_LOG = [];
/** @var int Tables cache lifetime (while developing need to be short) (else need to be very large) */
public $TABLE_NAMES_CACHE_TTL = 3600; // 1*3600*24 = 1 day
/** @var bool Auto-connect on/off */
public $AUTO_CONNECT = false;
/** @var bool Use backtrace in error message */
public $ERROR_BACKTRACE = true;
/** @var bool Use backtrace to get where query was called from (will be used only when DEBUG_MODE is enabled) */
public $USE_QUERY_BACKTRACE = true;
/** @var bool Auto-repairing on error (table not exists) on/off */
public $ERROR_AUTO_REPAIR = true;
/** @var string Folder where databases drivers are stored */
public $DB_DRIVERS_DIR = 'classes/db/';
/** @var int Num tries to reconnect in common mode (will be useful if db server is overloaded) (Set to '0' for disabling) */
public $RECONNECT_NUM_TRIES = 3;
/** @var int Num tries to reconnect inside CONSOLE MODE (will be useful if db server is overloaded and sometimes we lost connection to it) (Set to '0' for disabling) */
public $RECONNECT_CONSOLE_TRIES = 1000;
/** @var int Time to wait between reconnects (in seconds) */
public $RECONNECT_DELAY = 1;
/** @var bool Use logarithmic increase or reconnect time */
public $RECONNECT_DELAY_LOG_INC = 1;
/** @var bool Use locking for reconnects or not */
public $RECONNECT_USE_LOCKING = false;
/** @var array List of mysql error codes to use for reconnect tries. See also http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html */
public $RECONNECT_MYSQL_ERRORS = [1053, 1317, 2000, 2002, 2003, 2004, 2005, 2006, 2008, 2012, 2013, 2020, 2027, 2055];
/** @var string */
public $RECONNECT_LOCK_FILE_NAME = 'db_cannot_connect_[DB_HOST]_[DB_NAME]_[DB_USER]_[DB_PORT].lock';
/** @var int Time in seconds between unlock reconnect */
public $RECONNECT_LOCK_TIMEOUT = 30;
/** @var bool Connection required or not (else E_USER_WARNING will be thrown not E_USER_ERROR) */
public $CONNECTION_REQUIRED = false;
/** @var bool Allow to use shutdown queries or not */
public $USE_SHUTDOWN_QUERIES = true;
/** @var bool Allow to cache specified queries results */
public $ALLOW_CACHE_QUERIES = false;
/** @var bool Max number of cached queries */
public $CACHE_QUERIES_LIMIT = 100;
/** @var bool Max number of logged queries (set to 0 to unlimited) */
public $LOGGED_QUERIES_LIMIT = 1000;
/** @var bool Gather affected rows stats (will be used only when DEBUG_MODE is enabled) */
public $GATHER_AFFECTED_ROWS = true;
/** @var bool Store db queries to file */
public $LOG_ALL_QUERIES = false;
/** @var bool Store db queries to file */
public $LOG_SLOW_QUERIES = false;
/** @var string Log queries file name */
public $FILE_NAME_LOG_ALL = 'db_queries.log';
/** @var string Log queries file name */
public $FILE_NAME_LOG_SLOW = 'slow_queries.log';
/** @var float */
public $SLOW_QUERIES_TIME_LIMIT = 0.2;
/** @var bool Add additional engine details to the SQL as comment for later use */
public $INSTRUMENT_QUERIES = false;
/** @var array */
public $_instrument_items = [];
/** @var bool Currently only in DEBUG_MODE */
public $SHOW_QUERY_WARNINGS = true;
/** @var bool Currently only in DEBUG_MODE */
public $SHOW_QUERY_INFO = true;
/** @var bool @conf_skip Internal var (default value) */
public $_tried_to_connect = false;
/** @var bool @conf_skip Internal var (default value) */
public $_connected = false;
/** @var mixed @conf_skip Driver instance */
public $db = null;
/** @var string Tables names prefix */
public $DB_PREFIX = null;
/** @var string */
public $DB_HOST = '';
/** @var string */
public $DB_NAME = '';
/** @var string */
public $DB_USER = '';
/** @var string */
public $DB_PSWD = '';
/** @var int */
public $DB_PORT = '';
/** @var string */
public $DB_CHARSET = '';
/** @var string */
public $DB_SOCKET = '';
/** @var bool */
public $DB_SSL = false;
/** @var bool */
public $DB_PERSIST = false;
/** @var bool In case of true - we will try to avoid any data/structure modification queries to not break replication */
public $DB_REPLICATION_SLAVE = false;
/** @var bool Adding SQL_NO_CACHE to SELECT queries: useful to find long running queries */
public $SQL_NO_CACHE = false;
/** @var bool Needed for installation and repairing process */
public $ALLOW_AUTO_CREATE_DB = false;
/** @var bool Use sql query revisions for update/insert/replace/delete */
public $QUERY_REVISIONS = false;
/** @var bool update_safe, insert_safe, update_batch_safe: use additional checking for exising table fields */
public $FIX_DATA_SAFE = true;
/** @var bool Trigger to act silently or not in *_safe methods */
public $FIX_DATA_SAFE_SILENT = false;
/** @var array Filled automatically from generated file */
public $_need_sys_prefix = [];
/**
* Constructor.
* @param mixed $db_type
* @param null|mixed $db_prefix
* @param null|mixed $db_replication_slave
*/
public function __construct($db_type = '', $db_prefix = null, $db_replication_slave = null)
{
global $DEBUG;
$this->_load_tables_with_sys_prefix();
// Type/driver of database server
$this->DB_TYPE = ! empty($db_type) ? $db_type : DB_TYPE;
if ( ! defined('DB_PREFIX') && empty($db_prefix)) {
define('DB_PREFIX', '');
}
$this->DB_PREFIX = ! empty($db_prefix) ? $db_prefix : DB_PREFIX;
// Check if this is primary database connection
$debug_index = $DEBUG['db_instances'] ? count((array) $DEBUG['db_instances']) : 0;
if ($debug_index < 1) {
$this->IS_PRIMARY_CONNECTION = true;
} else {
$this->IS_PRIMARY_CONNECTION = false;
}
// Trying to override replication slave setting
if (isset($db_replication_slave)) {
$this->DB_REPLICATION_SLAVE = (bool) $db_replication_slave;
} elseif ($this->IS_PRIMARY_CONNECTION && defined('DB_REPLICATION_SLAVE')) {
$this->DB_REPLICATION_SLAVE = (bool) DB_REPLICATION_SLAVE;
}
// Track db class instances
$DEBUG['db_instances'][$debug_index] = &$this;
if (defined('DEBUG_MODE') && DEBUG_MODE) {
$DEBUG['db_instances_trace'][$debug_index] = $this->_trace_string();
}
}
/**
* Catch missing method call.
* @param mixed $name
* @param mixed $args
*/
public function __call($name, $args)
{
return main()->extend_call($this, $name, $args);
}
/**
* @param mixed $db_type
*/
public function get_driver_family($db_type = '')
{
$db_type = strtolower($db_type ?: $this->DB_TYPE);
// Get current abstract db type
$families = [
'mysql' => ['db_type', 'mysql', 'mysqli', 'pdo_mysql'],
'pgsql' => ['pgsql', 'pdo_pgsql', 'postgre', 'postgres'],
];
foreach ($families as $family => $aliases) {
if (in_array($db_type, $aliases)) {
$name = $family;
break;
}
}
if ( ! $name) {
$name = $db_type;
}
return $name;
}
/**
* Framework constructor.
*/
public function _init()
{
// Perform auto-connection to db if needed
if (($this->AUTO_CONNECT || MAIN_TYPE == 'admin') && ! $this->NO_AUTO_CONNECT) {
$this->connect();
}
$this->_set_debug_items();
if (main()->is_console()) {
$this->enable_silent_mode();
}
// Set shutdown function
if ($this->USE_SHUTDOWN_QUERIES) {
register_shutdown_function([&$this, '_execute_shutdown_queries']);
}
if ($this->LOG_ALL_QUERIES || $this->LOG_SLOW_QUERIES) {
register_shutdown_function([$this, '_log_queries']);
}
// Turn off tables repairing if we are dealing with slave server
if ($this->DB_REPLICATION_SLAVE) {
$this->ERROR_AUTO_REPAIR = false;
}
}
public function _load_tables_with_sys_prefix()
{
if ($this->_need_sys_prefix) {
return $this->_need_sys_prefix;
}
$paths = [
'app' => APP_PATH . 'share/db_sys_prefix_tables.php',
'yf' => YF_PATH . 'plugins/db/share/db_sys_prefix_tables.php',
];
$data = [];
foreach ($paths as $path) {
if (file_exists($path)) {
$_data = require $path;
if ($_data && is_array($_data)) {
$data += $_data;
}
}
}
$this->_need_sys_prefix = $data;
return (array) $data;
}
public function is_ready()
{
return (bool) $this->_connected;
}
/**
* Connect db driver and then connect to db.
* @param mixed $db_host
* @param mixed $db_user
* @param null|mixed $db_pswd
* @param null|mixed $db_name
* @param mixed $force
* @param mixed $params
*/
public function connect($db_host = '', $db_user = '', $db_pswd = null, $db_name = null, $force = false, $params = [])
{
if (is_array($db_host)) {
$params = $db_host;
$db_host = '';
}
if ( ! is_array($params)) {
$params = [];
}
if ($params['reconnect'] || $params['force']) {
$force = true;
}
if ( ! empty($this->_tried_to_connect) && ! $force) {
return $this->_connected;
}
$this->_connect_start_time = microtime(true);
if ( ! $params['reconnect']) {
$this->_set_connect_params($db_host, $db_user, $db_pswd, $db_name, $force, $params);
}
$driver_class_name = main()->load_class_file('db_driver_' . $this->DB_TYPE, $this->DB_DRIVERS_DIR);
// Create new instanse of the driver class
if ( ! empty($driver_class_name) && class_exists($driver_class_name) && ! is_object($this->db)) {
if ($this->RECONNECT_USE_LOCKING) {
$lock_file = $this->_get_reconnect_lock_path();
if (file_exists($lock_file)) {
if ((time() - filemtime($lock_file)) > $this->RECONNECT_LOCK_TIMEOUT) {
unlink($lock_file);
} else {
return false;
}
}
}
$driver_params = $this->_get_connect_params();
// Try to connect several times
$tries = $this->RECONNECT_NUM_TRIES;
if (main()->is_console() && ! main()->is_unit_test()) {
$tries = $this->RECONNECT_CONSOLE_TRIES;
}
for ($i = 1; $i <= $tries; $i++) {
$this->db = new $driver_class_name($driver_params);
if ( ! is_object($this->db) || ! ($this->db instanceof yf_db_driver)) {
trigger_error('DB: Wrong driver', $this->CONNECTION_REQUIRED ? E_USER_ERROR : E_USER_WARNING);
break;
}
$implemented = [];
foreach (get_class_methods($this->db) as $method) {
if ($method[0] != '_') {
$implemented[$method] = $method;
}
}
$this->db->implemented = $implemented;
// Stop after success
if ( ! empty($this->db->db_connect_id)) {
break;
// Wait some time and try again (use logarithmic increase)
}
$multiplier = 1;
if ($this->RECONNECT_DELAY_LOG_INC) {
$multiplier = $i + ($this->RECONNECT_DELAY <= 1 ? 1 : 0);
}
$sleep_time = $this->RECONNECT_DELAY * $multiplier;
sleep($sleep_time);
}
if ($this->RECONNECT_USE_LOCKING && ! $this->db->db_connect_id) {
file_put_contents($lock_file, gmdate('Y-m-d H:i:s') . ' GMT');
}
}
$this->_tried_to_connect = true;
if ( ! $this->db->db_connect_id) {
trigger_error('DB: ERROR CONNECTING TO DATABASE', $this->CONNECTION_REQUIRED ? E_USER_ERROR : E_USER_WARNING);
} else {
$this->db->SQL_NO_CACHE = $this->SQL_NO_CACHE;
$this->_connected = true;
}
$this->_connection_time += (microtime(true) - $this->_connect_start_time);
return $this->_connected;
}
public function _get_connect_params()
{
return [
'host' => $this->DB_HOST,
'user' => $this->DB_USER,
'pswd' => $this->DB_PSWD,
'name' => $this->DB_NAME,
'persist' => $this->DB_PERSIST,
'ssl' => $this->DB_SSL,
'port' => $this->DB_PORT,
'socket' => $this->DB_SOCKET,
'charset' => $this->DB_CHARSET,
'allow_auto_create_db' => $this->ALLOW_AUTO_CREATE_DB,
];
}
/**
* @param mixed $db_host
* @param mixed $db_user
* @param null|mixed $db_pswd
* @param null|mixed $db_name
* @param mixed $force
* @param mixed $params
*/
public function _set_connect_params($db_host = '', $db_user = '', $db_pswd = null, $db_name = null, $force = false, $params = [])
{
if (is_array($db_host)) {
$params = $db_host;
$db_host = '';
}
if ( ! is_array($params)) {
$params = [];
}
if ($params['reconnect'] || $params['force']) {
$force = true;
}
$this->DB_HOST = ($params['host'] ?: $db_host) ?: (defined('DB_HOST') ? DB_HOST : 'localhost');
$this->DB_USER = ($params['user'] ?: $db_user) ?: (defined('DB_USER') ? DB_USER : 'root');
// db_pswd can be empty string
$_db_pswd = isset($params['pswd']) ? $params['pswd'] : $db_pswd;
$this->DB_PSWD = $_db_pswd !== null ? $_db_pswd : (defined('DB_PSWD') ? DB_PSWD : '');
// db_name can be empty string - means we working in special mode, just connecting to server
$_db_name = isset($params['name']) ? $params['name'] : $db_name;
$this->DB_NAME = $_db_name !== null ? $_db_name : (defined('DB_NAME') ? DB_NAME : '');
$this->DB_PORT = ($params['port'] ?: $db_port) ?: (defined('DB_PORT') ? DB_PORT : '');
$this->DB_SOCKET = ($params['socket'] ?: $db_socket) ?: (defined('DB_SOCKET') ? DB_SOCKET : '');
$this->DB_SSL = ($params['ssl'] ?: $db_ssl) ?: (defined('DB_SSL') ? DB_SSL : false);
$this->DB_CHARSET = ($params['charset'] ?: $db_charset) ?: (defined('DB_CHARSET') ? DB_CHARSET : '');
if (isset($params['prefix'])) {
$this->DB_PREFIX = $params['prefix'];
}
$allow_auto_create_db = isset($params['auto_create_db']) ? $params['auto_create_db'] : $allow_auto_create_db;
if ($allow_auto_create_db !== null) {
$this->ALLOW_AUTO_CREATE_DB = $allow_auto_create_db;
}
}
/**
* Close connection to db.
*/
public function close()
{
$this->_connected = false;
$this->_tried_to_connect = false;
$result = $this->db->close();
unset($this->db);
return $result;
}
/**
* Prepare statement to execute.
* @param mixed $sql
* @param mixed $params
*/
public function prepare($sql, $params = [])
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
if ( ! is_object($this->db) || ! $this->db->implemented['prepare']) {
return false;
}
return $this->db->prepare($sql, $params);
}
/**
* Execute prepared statement.
* @param mixed $stmt
* @param mixed $params
*/
public function execute($stmt, $params = [])
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
if ( ! is_object($this->db) || ! $this->db->implemented['execute']) {
return false;
}
return $this->db->execute($stmt, $params);
}
/**
* Function return resource ID of the query.
* @param mixed $sql
*/
public function &query($sql)
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
if ( ! is_object($this->db)) {
return false;
}
$this->NUM_QUERIES++;
if (DEBUG_MODE) {
$query_time_start = microtime(true);
if ($this->SQL_NO_CACHE && $this->get_driver_family() === 'mysql') {
$q = strtoupper(substr(ltrim($sql), 0, 100));
if (substr($q, 0, 6) == 'SELECT' && false === strpos($q, 'SQL_NO_CACHE')) {
$sql = preg_replace('/^[\s\t]*(SELECT)[\s\t]+/ims', '$1 SQL_NO_CACHE ', $sql);
}
}
}
if ($this->INSTRUMENT_QUERIES) {
$sql = $this->_instrument_query($sql);
}
$query_allowed = true;
if ($this->DB_REPLICATION_SLAVE && preg_match('/^[\s\t]*(UPDATE|INSERT|DELETE|ALTER|CREATE|RENAME|TRUNCATE)[\s\t]+/ims', $sql)) {
$query_allowed = false;
}
if ($query_allowed) {
$result = $this->db->query($sql);
}
$db_error = false;
if ( ! $result && $query_allowed) {
$db_error = $this->db->error();
}
if ( ! $result && $query_allowed && $db_error) {
// Try to reconnect if we see some these errors: http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html
if ($this->get_driver_family() === 'mysql' && in_array($db_error['code'], $this->RECONNECT_MYSQL_ERRORS)) {
$this->db = null;
$reconnect_successful = $this->connect(['reconnect' => true]);
if ($reconnect_successful) {
$result = $this->db->query($sql);
}
}
}
$log_allowed = (DEBUG_MODE || $this->LOG_ALL_QUERIES || $this->LOG_SLOW_QUERIES);
if ($log_allowed) {
$log_id = $this->_query_log($sql, $this->USE_QUERY_BACKTRACE ? $this->_trace_string() : [], $db_error);
}
if ( ! $result && $query_allowed && $db_error && $this->ERROR_AUTO_REPAIR) {
$result = $this->_repair_table($sql, $db_error);
if ($result) {
$repair_done_ok = true;
}
}
if ( ! $result && $db_error) {
$this->_query_show_error($sql, $db_error, (DEBUG_MODE && $this->ERROR_BACKTRACE) ? $this->_trace_string() : '');
$this->_last_query_error = $db_error;
} else {
$this->_last_query_error = null;
}
$need_insert_id = false;
$_sql_type = strtoupper(rtrim(substr(ltrim($sql), 0, 7)));
if (in_array($_sql_type, ['INSERT', 'UPDATE', 'REPLACE'])) {
$need_insert_id = true;
}
$this->_last_insert_id = $result && $need_insert_id ? (int) $this->db->insert_id() : null;
if ($this->GATHER_AFFECTED_ROWS) {
$this->_last_affected_rows = $result ? (int) $this->db->affected_rows() : null;
}
// This part needed to update debug log after executing query, but ensure correct order of queries
if ($log_allowed) {
if (DEBUG_MODE && $this->SHOW_QUERY_WARNINGS && method_exists($this->db, 'get_last_warnings')) {
$warnings = $this->db->get_last_warnings();
}
if (DEBUG_MODE && $this->SHOW_QUERY_INFO && method_exists($this->db, 'get_last_query_info')) {
$info = $this->db->get_last_query_info();
}
$this->_update_query_log($log_id, $result, $query_time_start, $warnings, $info);
}
return $result;
}
/**
* @param mixed $sql
* @param mixed $db_error
* @param mixed $_trace
*/
public function _query_show_error($sql, $db_error, $_trace = '')
{
$old_db_error = $db_error;
$db_error = $this->db->error();
if (empty($db_error) || empty($db_error['message'])) {
$db_error = $old_db_error;
}
$msg = 'DB: QUERY ERROR: ' . $sql . ';' . PHP_EOL . 'CAUSE: ' . $db_error['message']
. ($db_error['code'] ? ' (code:' . $db_error['code'] . ')' : '')
. ($db_error['offset'] ? ' (offset:' . $db_error['offset'] . ')' : '')
. (main()->USE_CUSTOM_ERRORS ? '' : $_trace . PHP_EOL);
trigger_error($msg, E_USER_WARNING);
}
/**
* @param mixed $sql
* @param mixed $_trace
* @param mixed $db_error
*/
public function _query_log($sql, $_trace = [], $db_error = false)
{
$_log_allowed = true;
// Save memory on high number of query log entries
if ($this->LOGGED_QUERIES_LIMIT && count((array) $this->_LOG) >= $this->LOGGED_QUERIES_LIMIT) {
$_log_allowed = false;
}
if ( ! $_log_allowed) {
return false;
}
$warnings = null;
$info = null;
$this->_LOG[] = [
'sql' => $sql,
'rows' => '',
'insert_id' => '',
'error' => $db_error,
'info' => $info,
'time' => '',
'trace' => $_trace,
];
return count((array) $this->_LOG) - 1;
}
/**
* @param mixed $log_id
* @param mixed $result
* @param mixed $query_time_start
* @param null|mixed $warnings
* @param null|mixed $info
*/
public function _update_query_log($log_id, $result, $query_time_start = 0, $warnings = null, $info = null)
{
if ( ! isset($this->_LOG[$log_id])) {
return false;
}
$log = &$this->_LOG[$log_id];
$time = (float) microtime(true) - (float) $query_time_start;
$sql = $log['sql[5~'];
if ($this->GATHER_AFFECTED_ROWS && $result) {
$_sql_type = strtoupper(rtrim(substr(ltrim($sql), 0, 7)));
if (substr($_sql_type, 0, 4) === 'SHOW') {
$_sql_type = 'SHOW';
}
$rows = null;
// if ($_sql_type == 'SELECT') {
// $rows = $this->num_rows($result);
// } elseif (in_array($_sql_type, ['INSERT', 'UPDATE', 'REPLACE', 'DELETE', 'SHOW'])) {
$rows = $this->_last_affected_rows;
// }
}
$log['time'] = $time;
$log['rows'] = $rows;
$log['warning'] = $warnings;
$log['info'] = $info;
$log['insert_id'] = $this->_last_insert_id;
}
/**
* Function execute unbuffered query.
* @param mixed $sql
*/
public function unbuffered_query($sql)
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->query($sql);
}
/**
* @param mixed $sql
*/
public function multi_query($sql = [])
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
if ( ! is_object($this->db)) {
return false;
}
if ( ! $this->db->HAS_MULTI_QUERY) {
$result = [];
foreach ((array) $sql as $k => $_sql) {
$result[$k] = $this->query($_sql);
}
return $result;
}
return $this->db->multi_query($sql);
}
/**
* Alias of insert() with auto-escaping of data.
* @param mixed $table
* @param mixed $data
* @param mixed $only_sql
* @param mixed $replace
* @param mixed $ignore
* @param mixed $on_duplicate_key_update
* @param mixed $extra
*/
public function insert_safe($table, $data, $only_sql = false, $replace = false, $ignore = false, $on_duplicate_key_update = false, $extra = [])
{
if ( ! is_array($extra)) {
$extra = [];
}
if (is_array($only_sql)) {
$extra += $only_sql;
$only_sql = $extra['only_sql'];
}
$data = $this->_fix_data_safe($table, $data, $extra);
return $this->insert($table, $this->es($data), $only_sql, $replace, $ignore, $on_duplicate_key_update, $extra);
}
/**
* Insert array of values into table.
* @param mixed $table
* @param mixed $data
* @param mixed $only_sql
* @param mixed $replace
* @param mixed $ignore
* @param mixed $on_duplicate_key_update
* @param mixed $extra
*/
public function insert($table, $data, $only_sql = false, $replace = false, $ignore = false, $on_duplicate_key_update = false, $extra = [])
{
if ($this->DB_REPLICATION_SLAVE && ! $only_sql) {
return false;
}
if ( ! strlen($table) || ! is_array($data)) {
return false;
}
if ( ! is_array($extra)) {
$extra = [];
}
if (is_array($only_sql)) {
$extra += $only_sql;
$only_sql = $extra['only_sql'];
}
isset($extra['only_sql']) && $only_sql = $extra['only_sql'];
isset($extra['replace']) && $replace = $extra['replace'];
isset($extra['ignore']) && $ignore = $extra['ignore'];
isset($extra['on_duplicate_key_update']) && $on_duplicate_key_update = $extra['on_duplicate_key_update'];
is_string($replace) && $replace = false;
$sql = $this->query_builder()->compile_insert($table, $data, compact('replace', 'ignore', 'on_duplicate_key_update') + $extra);
if ( ! $sql) {
return false;
}
if ($only_sql) {
return $sql;
}
if (MAIN_TYPE_ADMIN && $this->QUERY_REVISIONS) {
$this->_save_query_revision(__FUNCTION__, $table, ['data' => $sql]);
}
return $this->query($sql);
}
/**
* Alias, forced to add INSERT IGNORE.
* @param mixed $table
* @param mixed $data
* @param mixed $only_sql
* @param mixed $replace
* @param mixed $extra
*/
public function insert_ignore($table, $data, $only_sql = false, $replace = false, $extra = [])
{
return $this->insert($table, $data, $only_sql, $replace, $ignore = true, $on_duplicate_key_update = false, $extra);
}
/**
* Alias, forced to add INSERT ... ON DUPLICATE KEY UPDATE.
* @param mixed $table
* @param mixed $data
* @param mixed $only_sql
* @param mixed $replace
* @param mixed $extra
*/
public function insert_on_duplicate_key_update($table, $data, $only_sql = false, $replace = false, $extra = [])
{
$on_duplicate_key_update = (true && ($this->get_driver_family() === 'mysql'));
return $this->insert($table, $data, $only_sql, $replace, $ignore = false, $on_duplicate_key_update, $extra);
}
/**
* Alias of replace() with data auto-escape.
* @param mixed $table
* @param mixed $data
* @param mixed $only_sql
* @param mixed $extra
*/
public function replace_safe($table, $data, $only_sql = false, $extra = [])
{
$replace = (true && in_array($this->get_driver_family(), ['mysql']));
return $this->insert_safe($table, $data, $only_sql, $replace, $ignore = false, $on_duplicate_key_update = false, $extra);
}
/**
* Replace array of values into table.
* @param mixed $table
* @param mixed $data
* @param mixed $only_sql
*/
public function replace($table, $data, $only_sql = false)
{
$replace = (true && in_array($this->get_driver_family(), ['mysql']));
return $this->insert($table, $data, $only_sql, $replace);
}
/**
* Alias of update() with data auto-escape.
* @param mixed $table
* @param mixed $data
* @param mixed $where
* @param mixed $only_sql
* @param mixed $extra
*/
public function update_safe($table, $data, $where, $only_sql = false, $extra = [])
{
$data = $this->_fix_data_safe($table, $data, $extra);
return $this->update($table, $this->es($data), $where, $only_sql);
}
/**
* Update table with given values.
* @param mixed $table
* @param mixed $data
* @param mixed $where
* @param mixed $only_sql
* @param mixed $extra
*/
public function update($table, $data, $where, $only_sql = false, $extra = [])
{
if ($this->DB_REPLICATION_SLAVE && ! $only_sql) {
return false;
}
if (empty($table) || empty($data) || empty($where)) {
return false;
}
if ( ! is_array($extra)) {
$extra = [];
}
if (is_array($only_sql)) {
$extra += $only_sql;
$only_sql = $extra['only_sql'];
}
isset($extra['only_sql']) && $only_sql = $extra['only_sql'];
$sql = $this->query_builder()->compile_update($table, $data, $where, $extra);
if ( ! $sql) {
return false;
}
if ($only_sql) {
return $sql;
}
if (MAIN_TYPE_ADMIN && $this->QUERY_REVISIONS) {
$this->_save_query_revision(__FUNCTION__, $table, ['data' => $sql]);
}
return $this->query($sql);
}
/**
* Execute database query and fetch result as assoc array (for queries that returns only 1 row).
* @param mixed $sql
* @param mixed $use_cache
* @param mixed $assoc
* @param mixed $return_sql
*/
public function query_fetch($sql, $use_cache = true, $assoc = true, $return_sql = false)
{
if ( ! strlen($sql)) {
return false;
}
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
$storage = &$this->_db_results_cache;
if ($use_cache && $this->ALLOW_CACHE_QUERIES && ! $this->NO_CACHE && isset($storage[$sql])) {
return $storage[$sql];
}
$data = null;
if ($this->get_driver_family() === 'mysql' && strtoupper(substr(ltrim($sql), 0, 6)) === 'SELECT') {
$sql = rtrim(rtrim(rtrim($sql), ';'));
if ( ! preg_match('~\s+LIMIT\s+[0-9,\s]+$~ims', strtoupper($sql))) {
$sql .= ' LIMIT 1';
}
}
// Mostly for unit tests, not a real use case
if ($return_sql) {
return $sql;
}
$q = $this->query($sql);
if ( ! empty($q)) {
if ($assoc) {
$data = @$this->db->fetch_assoc($q);
} else {
$data = @$this->db->fetch_row($q);
}
$this->free_result($q);
// Store result in variable cache
if ($use_cache && $this->ALLOW_CACHE_QUERIES && ! $this->NO_CACHE && ! isset($storage[$sql])) {
$storage[$sql] = $data;
// Permanently turn off queries cache (and free some memory) if case of limit reached
if ($this->CACHE_QUERIES_LIMIT && count((array) $storage) > $this->CACHE_QUERIES_LIMIT) {
$this->ALLOW_CACHE_QUERIES = false;
$storage = null;
}
}
}
return $data;
}
/**
* Alias.
* @param mixed $sql
* @param mixed $use_cache
* @param mixed $assoc
* @param mixed $return_sql
*/
public function get($sql, $use_cache = true, $assoc = true, $return_sql = false)
{
return $this->query_fetch($sql, $use_cache, $assoc, $return_sql);
}
/**
* Alias, return first value.
* @param mixed $sql
* @param mixed $use_cache
* @param mixed $return_sql
*/
public function get_one($sql, $use_cache = true, $return_sql = false)
{
if ( ! strlen($sql)) {
return false;
}
$result = $this->query_fetch($sql, $use_cache, $assoc = true, $return_sql);
if ( ! $result) {
return false;
}
// Foreach needed here as we do not know first key name
foreach (array_keys($result) as $key) {
return $result[$key];
}
return false;
}
/**
* Alias, return 2d array, where key is first field and value is the second,
* Example: 'SELECT id, name FROM p_static_pages' => array('1' => 'page1', '2' => 'page2')
* Example: 'SELECT name FROM p_static_pages' => array('page1', 'page2').
* @param mixed $sql
* @param mixed $use_cache
*/
public function get_2d($sql, $use_cache = true)
{
if ( ! strlen($sql)) {
return false;
}
$result = $this->query_fetch_all($sql, $use_cache, true);
// Get 1st and 2nd keys from first sub-array
if (is_array($result) && $result) {
$keys = array_keys(current($result));
}
if ( ! $keys) {
return false;
}
$out = [];
foreach ((array) $result as $id => $data) {
if (isset($keys[1])) {
$out[$data[$keys[0]]] = $data[$keys[1]];
} else {
$out[] = $data[$keys[0]];
}
}
return $out;
}
/**
* Generate multi-level (up to 4) array from incoming query, useful to save some code on generating this often.
* Example: get_deep_array('SELECT department_id, user_id, name FROM t_personal', 2) =>
* [ 25 => [ 654 => [
* 'department_id' => 25,
* 'user_id' => 654,
* 'name' => 'Peter',
* ]]].
* @param mixed $sql
* @param mixed $max_levels
* @param mixed $use_cache
*/
public function get_deep_array($sql, $max_levels = 0, $use_cache = true)
{
if ( ! strlen($sql)) {
return false;
}
if ( ! $max_levels || $max_levels > 4) {
$max_levels = 4;
}
$out = [];
$q = $this->query($sql);
if ( ! $q) {
return false;
}
$row = $this->fetch_assoc($q);
$levels = count((array) $row);
if ( ! is_array($row) || ! $levels) {
return false;
}
if ($levels > $max_levels) {
$levels = $max_levels;
}
$k = array_keys($row);
$a = [];
do {
if ($levels == 1) {
$a[$row[$k[0]]] = $row;
} elseif ($levels == 2) {
$a[$row[$k[0]]][$row[$k[1]]] = $row;
} elseif ($levels == 3) {
$a[$row[$k[0]]][$row[$k[1]]][$row[$k[2]]] = $row;
} elseif ($levels == 4) {
$a[$row[$k[0]]][$row[$k[1]]][$row[$k[2]]][$row[$k[3]]] = $row;
}
} while ($row = $this->fetch_assoc($q));
return $a;
}
/**
* Alias.
* @param mixed $sql
* @param mixed $use_cache
*/
public function query_fetch_assoc($sql, $use_cache = true)
{
return $this->query_fetch($sql, $use_cache, true);
}
/**
* Same as 'query_fetch' except fetching as row not assoc.
* @param mixed $sql
* @param mixed $use_cache
*/
public function query_fetch_row($sql, $use_cache = true)
{
return $this->query_fetch($sql, $use_cache, false);
}
/**
* Alias.
* @param mixed $sql
* @param null|mixed $key_name
* @param mixed $use_cache
*/
public function get_all($sql, $key_name = null, $use_cache = true)
{
return $this->query_fetch_all($sql, $key_name, $use_cache);
}
/**
* Alias.
* @param mixed $sql
* @param null|mixed $key_name
* @param mixed $use_cache
*/
public function all($sql, $key_name = null, $use_cache = true)
{
return $this->query_fetch_all($sql, $key_name, $use_cache);
}
/**
* Execute database query and fetch result into assotiative array.
* @param mixed $sql
* @param null|mixed $key_name
* @param mixed $use_cache
*/
public function query_fetch_all($sql, $key_name = null, $use_cache = true)
{
if ( ! strlen($sql)) {
return false;
}
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
$params = [];
if (is_array($use_cache)) {
$params = $use_cache;
$use_cache = isset($params['use_cache']) ? $params['use_cache'] : true;
}
$storage = &$this->_db_results_cache;
if ($use_cache && $this->ALLOW_CACHE_QUERIES && ! $this->NO_CACHE && isset($storage[$sql])) {
if ($params['as_objects']) {
foreach ((array) $storage[$sql] as $k => $v) {
$storage[$sql][$k] = (object) $v;
}
}
return $storage[$sql];
}
$data = null;
$q = $this->query($sql);
if ($q) {
// If $key_name is specified - then save to $data using it as key
while ($a = @$this->db->fetch_assoc($q)) {
if ($key_name != null && $key_name != '-1') {
$data[$a[$key_name]] = $a;
} elseif (isset($a['id']) && $key_name != '-1') {
$data[$a['id']] = $a;
} else {
$data[] = $a;
}
}
@$this->free_result($q);
}
// Store result in variable cache
if ($use_cache && $this->ALLOW_CACHE_QUERIES && ! $this->NO_CACHE && ! isset($storage[$sql])) {
$storage[$sql] = $data;
// Permanently turn off queries cache (and free some memory) if case of limit reached
if ($this->CACHE_QUERIES_LIMIT && count((array) $storage) > $this->CACHE_QUERIES_LIMIT) {
$this->ALLOW_CACHE_QUERIES = false;
$storage = null;
}
}
if ($params['as_objects']) {
foreach ((array) $data as $k => $v) {
$data[$k] = (object) $v;
}
}
return $data;
}
/**
* Execute database query and fetch result as assoc array (for queries that returns only 1 row).
* @param mixed $sql
* @param mixed $cache_ttl
*/
public function query_fetch_cached($sql, $cache_ttl = 600)
{
$cache_key = 'SQL_' . __FUNCTION__ . '_' . $this->DB_HOST . '_' . $this->DB_NAME . '_' . abs(crc32($sql));
$use_cache = true;
if ($this->NO_CACHE) {
$use_cache = false;
}
$data = [];
if ($use_cache) {
$data = cache_get($cache_key);
}
if ( ! $data) {
$data = $this->query_fetch($sql);
if ($use_cache) {
cache_set($cache_key, $data);
}
}
return $data;
}
/**
* Alias with core cache.
* @param mixed $sql
* @param null|mixed $key_name
* @param mixed $cache_ttl
*/
public function query_fetch_all_cached($sql, $key_name = null, $cache_ttl = 600)
{
$cache_key = 'SQL_' . __FUNCTION__ . '_' . $this->DB_HOST . '_' . $this->DB_NAME . '_' . abs(crc32($sql));
$use_cache = true;
if ($this->NO_CACHE) {
$use_cache = false;
}
$data = [];
if ($use_cache) {
$data = cache_get($cache_key);
}
if ( ! $data) {
$data = $this->query_fetch_all($sql, $key_name);
if ($use_cache) {
cache_set($cache_key, $data);
}
}
return $data;
}
/**
* Alias.
* @param mixed $sql
* @param mixed $cache_ttl
*/
public function get_cached($sql, $cache_ttl = 600)
{
return $this->query_fetch_cached($sql, $cache_ttl);
}
/**
* Alias.
* @param mixed $sql
* @param null|mixed $key_name
* @param mixed $cache_ttl
*/
public function get_all_cached($sql, $key_name = null, $cache_ttl = 600)
{
return $this->query_fetch_all_cached($sql, $key_name, $cache_ttl);
}
/**
* Execute database query and the calculate number of rows.
* @param mixed $sql
*/
public function query_num_rows($sql)
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
$Q = $this->query($sql);
$result = $this->db->num_rows($Q);
$this->free_result($Q);
return $result;
}
/**
* Function return fetched array with both text and numeric indexes.
* @param mixed $result
*/
public function fetch_array($result)
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->fetch_array($result);
}
/**
* Function return fetched array with text indexes.
* @param mixed $result
*/
public function fetch_assoc($result)
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->fetch_assoc($result);
}
/**
* Alias.
* @param mixed $result
*/
public function fetch($result)
{
return $this->fetch_assoc($result);
}
/**
* Function return fetched array with numeric indexes.
* @param mixed $result
*/
public function fetch_row($result)
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->fetch_row($result);
}
/**
* Function return fetched object with assoc var names.
* @param mixed $result
*/
public function fetch_object($result)
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->fetch_object($result);
}
/**
* Function return number of rows in the query.
* @param mixed $result
*/
public function num_rows($result)
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->num_rows($result);
}
/**
* Transaction wrapper. Examples:
* db()->transaction(function($db) {
* $user = $db->from('user')->first();
* $user['verified'] = true;
* return $db->update('user', $user);
* }).
* @param mixed $callback
*/
public function transaction($callback)
{
if ( ! is_callable($callback)) {
return false;
}
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
$this->begin();
$rolled_back = false;
try {
$result = $callback($this);
} catch (Exception $e) {
$result = false;
$this->rollback();
$rolled_back = true;
throw $e;
}
if ($result === false || $rolled_back) {
! $rolled_back && $this->rollback();
return false;
}
return $this->commit();
}
/**
* Begin a transaction, or if a transaction has already started, continue it.
*/
public function begin()
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->begin();
}
/**
* End a transaction, or decrement the nest level if transactions are nested.
*/
public function commit()
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->commit();
}
/**
* Rollback a transaction.
*/
public function rollback()
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->rollback();
}
/**
* Return columns info for selected table.
* @param mixed $table
*/
public function meta_columns($table)
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
$table = $this->_escape_table_name($table);
if ( ! strlen($table)) {
return false;
}
return $this->utils()->meta_columns($table);
}
/**
* Return tables list for current database.
*/
public function meta_tables()
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->utils()->meta_tables($this->DB_PREFIX);
}
/**
* Free result assosiated with a given query resource.
* @param mixed $result
*/
public function free_result($result)
{
if ( ! $this->_connected && ! $this->connect() && empty($result)) {
return false;
}
return $this->db->free_result($result);
}
/**
* Return error of the latest executed query.
* Difference from error() is that it will work correctly when repair enabled
* (it executes lot of self queries and cleaning db api latest error).
*/
public function last_error()
{
$var = $this->_last_query_error;
return isset($var['code']) && ! empty($var['code']) ? $var : false;
// TODO: use this only when repair table enabled and called.
}
/**
* Return database error.
*/
public function error()
{
if ( ! is_object($this->db)) {
return false;
}
return $this->db->error();
}
/**
* Return last insert id.
*/
public function insert_id()
{
if (isset($this->_last_insert_id)) {
return $this->_last_insert_id;
}
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->insert_id();
}
/**
* Get number of affected rows.
*/
public function affected_rows()
{
if (isset($this->_last_affected_rows)) {
return $this->_last_affected_rows;
}
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->affected_rows();
}
/**
* Return database-specific limit of returned rows.
* @param mixed $count
* @param null|mixed $offset
*/
public function limit($count, $offset = null)
{
if ( ! $this->_connected && ! $this->connect()) {
$sql = '';
if ($count > 0) {
$offset = ($offset > 0) ? $offset : 0;
$sql = 'LIMIT ' . ($offset ? $offset . ', ' : '') . $count;
}
return $sql;
}
return $this->db->limit($count, $offset);
}
public function get_server_version()
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->get_server_version();
}
public function get_host_info()
{
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
return $this->db->get_host_info();
}
/**
* Helper.
* @param mixed $table
* @param mixed $where
* @param mixed $as_sql
*/
public function delete($table, $where, $as_sql = false)
{
$sql = $this->from($table)->delete($where, $_as_sql = true);
if (MAIN_TYPE_ADMIN && $this->QUERY_REVISIONS && ! $as_sql) {
$this->_save_query_revision(__FUNCTION__, $table, ['data' => $sql]);
}
return $as_sql ? $sql : $this->query($sql);
}
/**
* @param mixed $table
* @param mixed $data
* @param null|mixed $index
* @param mixed $only_sql
*/
public function update_batch_safe($table, $data, $index = null, $only_sql = false)
{
$data = $this->_fix_data_safe($table, $data);
return $this->update_batch($table, $this->es($data), $index, $only_sql);
}
/**
* @param mixed $table
* @param mixed $data
* @param null|mixed $index
* @param mixed $only_sql
*/
public function update_batch($table, $data, $index = null, $only_sql = false)
{
if ($this->DB_REPLICATION_SLAVE && ! $only_sql) {
return false;
}
if ( ! $this->_connected && ! $this->connect()) {
return false;
}
if ( ! is_object($this->db)) {
return false;
}
if ( ! $index) {
$index = 'id';
}
if ( ! strlen($table) || ! $data || ! is_array($data) || ! $index) {
return false;
}
if (MAIN_TYPE_ADMIN && $this->QUERY_REVISIONS) {
$_this = $this;
$fname = __FUNCTION__;
$params['slice_callback'] = function ($_data) use ($_this, $fname, $table, $index) {
$_this->_save_query_revision($fname, $table, ['data' => $_data, 'index' => $index]);
};
}
return $this->query_builder()->update_batch($table, $data, $index, $only_sql, $params);
}
/**
* @param mixed $sql
*/
public function split_sql($sql)
{
return $this->utils()->split_sql($sql);
}
/**
* Query builder shortcut.
*/
public function select()
{
return call_user_func_array([$this->query_builder(), __FUNCTION__], func_get_args());
}
/**
* Query builder shortcut.
*/
public function from()
{
return call_user_func_array([$this->query_builder(), __FUNCTION__], func_get_args());
}
/**
* Query builder shortcut.
*/
public function table()
{
return call_user_func_array([$this->query_builder(), __FUNCTION__], func_get_args());
}
public function utils()
{
if ( ! isset($this->utils)) {
$cname = 'db_utils_' . $this->get_driver_family();
$this->utils = _class($cname, 'classes/db/');
$this->utils->db = $this;
}
return $this->utils;
}
public function migrator()
{
if ( ! isset($this->migrator)) {
$cname = 'db_migrator_' . $this->get_driver_family();
$this->migrator = _class($cname, 'classes/db/');
$this->migrator->db = $this;
}
return $this->migrator;
}
public function installer()
{
if ( ! isset($this->installer)) {
$cname = 'db_installer_' . $this->get_driver_family();
$this->installer = _class($cname, 'classes/db/');
$this->installer->db = $this;
}
return $this->installer;
}
public function query_builder()
{
$cname = 'db_query_builder_' . $this->get_driver_family();
$qb = clone _class($cname, 'classes/db/');
$qb->db = $this;
return $qb;
}
/**
* ORM shortcut.
* @param mixed $name
* @param mixed $params
*/
public function model($name, $params = [])
{
$model = $this->_model_load($name);
$params && $model->_set_params($params);
return $model;
}
/**
* Load new model object.
* @param mixed $name
*/
public function _model_load($name)
{
$main = main();
$model_class = $name . '_model';
$custom_storages = &$main->_custom_class_storages;
$wildcard = '*_model';
if ( ! isset($$custom_storages[$wildcard]['yf'])) {
$yf_models_basic_storages = [
'project_app_path' => [APP_PATH . 'models/'],
'project_plugins' => [PROJECT_PATH . 'plugins/*/share/models/'],
'project' => [PROJECT_PATH . 'share/models/'],
'yf_plugins' => [YF_PATH . 'plugins/*/share/models/'],
'yf' => [YF_PATH . 'share/models/'],
];
foreach ($yf_models_basic_storages as $k => $v) {
if ( ! isset($custom_storages[$wildcard][$k])) {
$custom_storages[$wildcard][$k] = $v;
}
}
}
$obj = _class_safe($model_class);
// Special case to use preloaded models, try to load class name without postfix *_model
if ( ! is_object($obj) || ! ($obj instanceof yf_model)) {
$obj = _class_safe($name);
}
if ( ! is_object($obj) || ! ($obj instanceof yf_model)) {
throw new Exception('Not able to load model: ' . $name);
return false;
}
$model_obj = clone $obj;
$model_obj->set_db_object($this);
return $model_obj;
}
/**
* Add query to shutdown array.
* @param mixed $sql
*/
public function _add_shutdown_query($sql = '')
{
if (empty($sql) || strlen($sql) < 5) {
return false;
}
// If shutdown execution is disabled - then execute this query immediatelly
if ( ! $this->USE_SHUTDOWN_QUERIES) {
return $this->query($sql);
}
// Add query to the array
$this->_SHUTDOWN_QUERIES[] = $sql;
return true;
}
/**
* Execute shutdown queries.
*/
public function _execute_shutdown_queries()
{
if ( ! $this->USE_SHUTDOWN_QUERIES || isset($this->_shutdown_executed)) {
return false;
}
foreach ((array) $this->_SHUTDOWN_QUERIES as $sql) {
if (is_string($sql) && strlen($sql) > 5) {
$this->query($sql);
}
}
// Prevent executing this method more than once
$this->_shutdown_executed = true;
}
/**
* Create unique temporary table name.
*/
public function _get_unique_tmp_table_name()
{
return $this->DB_PREFIX . 'tmp__' . substr(abs(crc32(rand() . microtime(true))), 0, 8);
}
/**
* Do Log.
*/
public function _log_queries()
{
// Restore startup working directory
@chdir(main()->_CWD);
if ( ! isset($this->_queries_logged)) {
$this->_queries_logged = true;
} else {
return false;
}
_class_safe('logs')->store_db_queries_log();
}
/**
* Get reconnect lock file name.
*/
public function _get_reconnect_lock_path()
{
$pairs = [
'[DB_HOST]' => $this->DB_HOST,
'[DB_NAME]' => $this->DB_NAME,
'[DB_USER]' => $this->DB_USER,
'[DB_PORT]' => $this->DB_PORT,
];
return STORAGE_PATH . str_replace(array_keys($pairs), array_values($pairs), $this->RECONNECT_LOCK_FILE_NAME);
}
/**
* @param mixed $table
* @param mixed $no_cache
*/
public function get_table_columns_cached($table, $no_cache = false)
{
$cache_name = __FUNCTION__ . '|' . $table . '|' . $this->DB_HOST . '|' . $this->DB_PORT . '|' . $this->DB_NAME . '|' . $this->DB_PREFIX;
if ($this->NO_CACHE) {
$no_cache = true;
}
$data = [];
if ( ! $no_cache) {
$data = cache()->get($cache_name);
}
if ( ! $data) {
$data = $this->meta_columns($table);
if ( ! $no_cache) {
cache()->set($cache_name, $data);
}
}
return $data;
}
/**
* @param mixed $table
* @param mixed $data
* @param mixed $extra
*/
public function _fix_data_safe($table, $data = [], $extra = [])
{
if ( ! $this->FIX_DATA_SAFE) {
return $data;
}
$cols = $this->get_table_columns_cached($table, $extra['no_cache']);
if ( ! $cols) {
$msg = __CLASS__ . '->' . __FUNCTION__ . ': columns for table ' . $table . ' is empty, truncating data array';
if ( ! $extra['silent'] && ! $this->FIX_DATA_SAFE_SILENT) {
trigger_error($msg, E_USER_WARNING);
}
return false;
}
$is_data_3d = false;
// Try to check if array is two-dimensional
foreach ((array) $data as $cur_row) {
$is_data_3d = is_array($cur_row) ? 1 : 0;
break;
}
$not_existing_cols = [];
$virtual_cols = [];
$fixed_nulls = [];
if ($is_data_3d) {
foreach ((array) $data as $k => $_data) {
foreach ((array) $_data as $name => $v) {
if ( ! isset($cols[$name])) {
$not_existing_cols[$name] = $name;
unset($data[$k][$name]);
} elseif (isset($cols[$name]['virtual']) && $cols[$name]['virtual']) {
$virtual_cols[$name] = $name;
unset($data[$k][$name]);
} elseif (($v === null || $v === 'NULL') && ! $cols[$name]['nullable']) {
$fixed_nulls[$name] = $name;
unset($data[$k][$name]);
}
}
}
} else {
foreach ((array) $data as $name => $v) {
if ( ! isset($cols[$name])) {
$not_existing_cols[$name] = $name;
unset($data[$name]);
} elseif (isset($cols[$name]['virtual']) && $cols[$name]['virtual']) {
$virtual_cols[$name] = $name;
unset($data[$name]);
} elseif (($v === null || $v === 'NULL') && ! $cols[$name]['nullable']) {
$fixed_nulls[$name] = $name;
unset($data[$name]);
}
}
}
if ( ! $extra['silent'] && ! $this->FIX_DATA_SAFE_SILENT) {
if ($not_existing_cols) {
trigger_error(__CLASS__ . '->' . __FUNCTION__ . ': not existing columns for table "' . $table . '", columns: ' . implode(', ', $not_existing_cols), E_USER_NOTICE);
}
if ($virtual_cols) {
trigger_error(__CLASS__ . '->' . __FUNCTION__ . ': removed virtual columns for table "' . $table . '", columns: ' . implode(', ', $virtual_cols), E_USER_NOTICE);
}
if ($fixed_nulls) {
trigger_error(__CLASS__ . '->' . __FUNCTION__ . ': fixed nulls for table "' . $table . '", columns: ' . implode(', ', $fixed_nulls), E_USER_NOTICE);
}
}
return $data;
}
/**
* Get real table name from its short variant.
* @param mixed $name
*/
public function _real_name($name)
{
$name = trim($name);
if ( ! strlen($name)) {
return false;
}
$db = '';
$table = '';
if (strpos($name, '.') !== false) {
list($db, $table) = explode('.', $name);
$db = trim($db);
$table = trim($table);
} else {
$table = $name;
}
if (isset($this->_found_tables[$name])) {
return $this->_found_tables[$name];
}
$name = (in_array($name, $this->_need_sys_prefix) ? 'sys_' : '') . $name;
$plen = strlen($this->DB_PREFIX);
if ($plen && substr($name, 0, $plen) !== $this->DB_PREFIX) {
return ($db ? $db . '.' : '') . $this->DB_PREFIX . $name;
}
return ($db ? $db . '.' : '') . $name;
}
/**
* Try to fix table name.
* @param mixed $name
*/
public function _fix_table_name($name = '')
{
$name = trim($name);
if ( ! strlen($name)) {
return false;
}
$db = '';
$table = '';
if (strpos($name, '.') !== false) {
list($db, $name) = explode('.', $name);
$db = trim($db);
$name = trim($name);
}
if ( ! strlen($name)) {
return '';
}
if (substr($name, 0, strlen('dbt_')) == 'dbt_') {
$name = substr($name, strlen('dbt_'));
}
$name_wo_db_prefix = $name;
$plen = strlen($this->DB_PREFIX);
if ($plen && substr($name, 0, $plen) === $this->DB_PREFIX) {
$name_wo_db_prefix = substr($name, $plen);
}
return ($db ? $db . '.' : '') . $this->DB_PREFIX . (in_array($name_wo_db_prefix, $this->_need_sys_prefix) ? 'sys_' : '') . $name_wo_db_prefix;
}
/**
* Trying to repair given table structure (and possibly data).
* @param mixed $sql
* @param mixed $db_error
*/
public function _repair_table($sql, $db_error)
{
if (empty($db_error) || ! $this->ERROR_AUTO_REPAIR) {
return false;
}
$driver_family = $this->get_driver_family();
$code = $db_error['code'];
if ($driver_family === 'mysql' && ! in_array($code, [
1191, // Can't find FULLTEXT index matching the column list
2013, // Lost connection to MySQL server during query
1205, // Lock wait timeout expired. Transaction was rolled back (InnoDB)
1213, // Transaction deadlock. You should rerun the transaction. (InnoDB)
1146, // Table %s doesn't exist
1054, // Unknown column %s
])) {
return false;
}
return _class('db_installer_' . $driver_family, 'classes/db/')->repair($sql, $db_error, $this);
}
/**
* @param mixed $method
* @param mixed $table
* @param mixed $params
*/
public function _save_query_revision($method, $table, $params = [])
{
if (($allowed_methods = $this->QUERY_REVISIONS_METHODS)) {
if ( ! in_array($method, $allowed_methods)) {
return false;
}
}
if (($allowed_tables = $this->QUERY_REVISIONS_TABLES)) {
if ( ! in_array($table, $allowed_tables)) {
return false;
}
}
$to_insert = [
'date' => date('Y-m-d H:i:s'),
'data_new' => is_array($params['data']) ? 'json:' . json_encode($params['data']) : (string) $params['data'],
'data_old' => is_array($params['data_old']) ? 'json:' . json_encode($params['data_old']) : (string) $params['data_old'],
'data_diff' => is_array($params['data_diff']) ? 'json:' . json_encode($params['data_diff']) : (string) $params['data_diff'],
'user_id' => main()->ADMIN_ID,
'user_group' => main()->ADMIN_GROUP,
'site_id' => conf('SITE_ID'),
'server_id' => conf('SERVER_ID'),
'ip' => common()->get_ip(),
'query_method' => $method,
'query_table' => $table,
'extra' => json_encode([
'get_object' => $_GET['object'],
'get_action' => $_GET['action'],
'get_id' => $_GET['id'],
'get_page' => $_GET['page'],
'trace' => array_slice(explode(PHP_EOL, main()->trace_string()), 1, 5),
]),
'url' => (main()->is_https() ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'],
];
$sql = $this->insert_safe('sys_db_revisions', $to_insert, $only_sql = true);
$this->_add_shutdown_query($sql);
}
/**
* Simple trace without dumping whole objects.
*/
public function _trace()
{
$trace = [];
foreach (debug_backtrace() as $k => $v) {
if ( ! $k) {
continue;
}
$v['object'] = isset($v['object']) && is_object($v['object']) ? get_class($v['object']) : null;
$trace[$k - 1] = $v;
}
return $trace;
}
/**
* Print nice.
*/
public function _trace_string()
{
$e = new Exception();
return implode(PHP_EOL, array_slice(explode(PHP_EOL, $e->getTraceAsString()), 1, -1));
}
/**
* Special init for the debug info items.
*/
public function _set_debug_items()
{
if ( ! $this->INSTRUMENT_QUERIES) {
return false;
}
$cpu_usage = function_exists('getrusage') ? getrusage() : [];
$ip = common()->get_ip();
$this->_instrument_items = [
'memory_usage' => function_exists('memory_get_usage') ? memory_get_usage() : '',
'cpu_user' => $cpu_usage['ru_utime.tv_sec'] * 1e6 + $cpu_usage['ru_utime.tv_usec'],
'cpu_system' => $cpu_usage['ru_stime.tv_sec'] * 1e6 + $cpu_usage['ru_stime.tv_usec'],
'get_object' => $_GET['object'],
'get_action' => $_GET['action'],
'get_id' => $_GET['id'],
'get_page' => $_GET['page'],
'user_id' => $_SESSION['user_id'],
'user_group' => $_SESSION['user_group'],
'session_id' => session_id(),
'request_id' => md5($_SERVER['REMOTE_PORT'] . $ip . $_SERVER['REQUEST_URI'] . microtime(true)),
'request_method' => $_SERVER['REQUEST_METHOD'],
'request_uri' => $_SERVER['REQUEST_URI'],
'http_host' => $_SERVER['HTTP_HOST'],
'ip' => $ip,
];
return true;
}
/**
* Get debug item value.
* @param mixed $name
*/
public function _get_debug_item($name = '')
{
if ( ! $this->INSTRUMENT_QUERIES) {
return '';
}
return $this->_instrument_items[$name];
}
/**
* Add instrumentation info to the query for highload SQL debug and profile.
* @param mixed $sql
* @param mixed $keys
*/
public function _instrument_query($sql = '', $keys = ['request_id', 'session_id', 'SESSION_user_id', 'GET_object', 'GET_action'])
{
$header = '';
if ( ! $sql) {
return '';
}
$trace = trace($skip_before = 3, $skip_after = 2);
$trace = str_replace(["\t", "\n", "\0"], '', $trace);
$header = '-- ' . $trace . "\t";
foreach ((array) $keys as $x => $key) {
$val = $this->_get_debug_item($key);
if ( ! $val) {
continue;
}
$val = str_replace(["\t", "\n", "\0"], '', $val);
// all other chars are safe in comments
$key = strtolower(str_replace([': ', "\t", "\n", "\0"], '', $key));
// Add the requested instrumentation keys
$header .= "\t" . $key . ': ' . $this->es($val);
}
return $header . PHP_EOL . $sql;
}
/**
* 'Silent' mode (logging off, tracing off, debugging off).
*/
public function enable_silent_mode()
{
$this->ALLOW_CACHE_QUERIES = false;
$this->GATHER_AFFECTED_ROWS = false;
$this->USE_SHUTDOWN_QUERIES = false;
$this->LOG_ALL_QUERIES = false;
$this->LOG_SLOW_QUERIES = false;
$this->USE_QUERY_BACKTRACE = false;
$this->ERROR_BACKTRACE = false;
$this->LOGGED_QUERIES_LIMIT = 1;
}
/**
* Function escapes characters for using in query.
* @param mixed $string
*/
public function es($string)
{
if ($string === null || $string === 'NULL') {
return 'NULL';
}
if ( ! $this->_connected && ! $this->connect()) {
return $this->_mysql_escape_mimic($string);
}
// Helper method for passing here whole arrays as param
if (is_array($string)) {
foreach ((array) $string as $k => $v) {
if ($v === null || $v === 'NULL') {
$string[$k] = 'NULL';
} else {
$string[$k] = $this->real_escape_string($v);
}
}
return $string;
}
return $this->db->real_escape_string($string);
}
/**
* Alias.
* @param mixed $string
*/
public function real_escape_string($string)
{
return $this->es($string);
}
/**
* Alias.
* @param mixed $string
*/
public function escape_string($string)
{
return $this->es($string);
}
/**
* Alias.
* @param mixed $string
*/
public function escape($string)
{
return $this->es($string);
}
/**
* @param mixed $data
*/
public function escape_key($data)
{
if (is_array($data)) {
$func = __FUNCTION__;
foreach ((array) $data as $k => $v) {
$data[$k] = $this->$func($v);
}
return $data;
}
if ( ! is_object($this->db)) {
return '`' . trim($data, '`') . '`';
}
return $this->db->escape_key($data);
}
/**
* @param mixed $data
*/
public function escape_val($data)
{
if ($data === null || $data === 'NULL') {
return 'NULL';
} elseif (is_array($data)) {
$func = __FUNCTION__;
foreach ((array) $data as $k => $v) {
$data[$k] = $this->$func($v);
}
return $data;
}
if ( ! is_object($this->db)) {
return '\'' . $data . '\'';
}
return $this->db->escape_val($data);
}
/**
* @param mixed $name
*/
public function _escape_table_name($name = '')
{
$name = trim($name);
if ( ! strlen($name)) {
return false;
}
$db = '';
$table = '';
if (strpos($name, '.') !== false) {
list($db, $table) = explode('.', $name);
$db = trim($db);
$table = trim($table);
} else {
$table = $name;
}
if ( ! strlen($table)) {
return false;
}
$table = $this->_fix_table_name($table);
return (strlen($db) ? $this->escape_key($db) . '.' : '') . $this->escape_key($table);
}
/**
* @param mixed $string
*/
public function _mysql_escape_mimic($string)
{
if (is_array($string)) {
return array_map([$this, __FUNCTION__], $string);
}
if ($string === null || $string === 'NULL') {
return 'NULL';
} elseif (is_float($string)) {
return str_replace(',', '.', $string);
} elseif (is_int($string)) {
return $string;
} elseif (is_bool($string)) {
return (int) $string;
}
return str_replace(['\\', "\0", "\n", "\r", "'", '"', "\x1a"], ['\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'], $string);
}
}