'Database connect failed', BX_DB_ERR_QUERY_ERROR => 'Database query error', BX_DB_ERR_ESCAPE => 'Escape string error' ); protected static $_bReadOnlyMode = true; protected static $_bMultuServersMode = false; protected $_bPdoPersistent; protected $_iPdoFetchType; protected $_iPdoErrorMode; protected $_bErrorChecking; protected $_aError; protected $_sHost, $_sPort, $_sSocket, $_sDbname, $_sUser, $_sPassword, $_sCharset, $_sStorageEngine; protected $_oStatement = null; protected $_oDbCacheObject = null; /** * set database parameters and connect to it */ protected function __construct($aDbConf = false) { if (isset($GLOBALS['bxDolClasses'][get_class($this)])) trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR); parent::__construct(); $this->_bPdoPersistent = true; if (defined('BX_DATABASE_PERSISTENT')) $this->_bPdoPersistent = (bool)BX_DATABASE_PERSISTENT; $this->_iPdoFetchType = PDO::FETCH_ASSOC; $this->_iPdoErrorMode = BX_DB_MODE_EXCEPTION; $this->_bErrorChecking = true; $this->_aError = array(); $this->_sStorageEngine = 'MYISAM'; if (defined('BX_DATABASE_ENGINE')) $this->_sStorageEngine = BX_DATABASE_ENGINE; $this->_sCharset = 'utf8mb4'; if($aDbConf === false) { $this->_sHost = BX_DATABASE_HOST; $this->_sPort = BX_DATABASE_PORT; $this->_sSocket = BX_DATABASE_SOCK; $this->_sDbname = BX_DATABASE_NAME; $this->_sUser = BX_DATABASE_USER; $this->_sPassword = BX_DATABASE_PASS; } else { $this->_sHost = $aDbConf['host']; $this->_sPort = $aDbConf['port']; $this->_sSocket = $aDbConf['sock']; $this->_sDbname = $aDbConf['name']; $this->_sUser = $aDbConf['user']; $this->_sPassword = $aDbConf['pwd']; if(isset($aDbConf['charset'])) $this->_sCharset = $aDbConf['charset']; if(isset($aDbConf['error_checking'])) $this->_bErrorChecking = $aDbConf['error_checking']; if(isset($aDbConf['storage_engine'])) $this->_sStorageEngine = $aDbConf['storage_engine']; } @set_exception_handler(array($this, 'pdoExceptionHandler')); } /** * Prevent cloning the instance */ public function __clone() { if (isset($GLOBALS['bxDolClasses'][get_class($this)])) trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR); } /** * Get singleton instance of the class */ public static function getInstance() { $null = null; return self::getInstanceWithConf(false, $null); } public static function getInstanceWithConf($aDbConf, &$sError) { if(!isset($GLOBALS['bxDolClasses'][__CLASS__])) { if($aDbConf === false && !defined('BX_DATABASE_HOST')) return null; $o = new BxDolDb($aDbConf); $sErrorMessage = $o->connect(); if($sErrorMessage) { if($sError !== null) $sError = $sErrorMessage; return null; } $GLOBALS['bxDolClasses'][__CLASS__] = $o; } return $GLOBALS['bxDolClasses'][__CLASS__]; } public static function getLink() { return self::$_rLink; } public static function getQueriesCounter() { return self::$_iQueriesCounter; } public function setReadOnlyMode($b) { if ($b == self::$_bReadOnlyMode) return; self::$_bReadOnlyMode = $b; if (self::$_bMultuServersMode) { $this->disconnect(); $this->connect(); } } protected function balancer() { self::$_bMultuServersMode = false; // regular mode - one DB server if (!is_array($this->_sHost)) return array($this->_sHost, $this->_sPort, $this->_sSocket, $this->_sUser, $this->_sPassword, $this->_sDbname); $iServersNum = count($this->_sHost); if ($iServersNum > 1) self::$_bMultuServersMode = true; // in read/write mode always use first server, which should be always master (also check for regular mode but with array with 1 item) if (!self::$_bReadOnlyMode || 1 == $iServersNum) $i = 0; // in case of 2 servers - always use second server, in read only mode elseif (2 == $iServersNum) $i = 1; // when there are more that 2 servers, randomly select read only server else $i = rand(1, $iServersNum-1); return array($this->_sHost[$i], $this->_sPort[$i], $this->_sSocket[$i], $this->_sUser[$i], $this->_sPassword[$i], $this->_sDbname[$i]); } /** * connect to database with appointed parameters */ public function connect() { if(self::$_rLink) return; list ($sHost, $sPort, $sSocket, $sUser, $sPassword, $sDBName) = $this->balancer(); try { $sDsn = "mysql:host=" . $sHost . ";"; $sDsn .= $sPort ? "port=" . $sPort . ";" : ""; $sDsn .= $sSocket ? "unix_socket=" . $sSocket . ";" : ""; $sDsn .= "dbname=" . $sDBName . ";charset=" . $this->_sCharset; self::$_rLink = new PDO($sDsn, $sUser, $sPassword, array( PDO::ATTR_ERRMODE => $this->_iPdoErrorMode, PDO::ATTR_DEFAULT_FETCH_MODE => $this->_iPdoFetchType, PDO::ATTR_PERSISTENT => $this->_bPdoPersistent )); $this->pdoExec("SET NAMES 'utf8mb4' COLLATE '" . (defined('BX_DATABASE_COLLATE') ? BX_DATABASE_COLLATE : 'utf8mb4_unicode_ci') . "'"); $this->pdoExec("SET sql_mode = ''"); if (0 == strcasecmp($this->_sStorageEngine, 'INNODB')) $this->pdoExec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); $sVer = $this->getVersion(); $sStorageEngine = !$sVer || version_compare($sVer, '5.7.5', '>=') ? 'default_storage_engine' : 'storage_engine'; $this->pdoExec("SET $sStorageEngine=" . $this->_sStorageEngine); $this->cleanMemoryAll(); } catch (PDOException $oException) { $oException->errorInfo[self::$_sErrorKey] = array( 'code' => BX_DB_ERR_CONNECT_FAILD, 'message' => $oException->getMessage(), 'trace' => $oException->getTrace() ); throw $oException; } } /** * close mysql connection */ public function disconnect() { self::$_rLink = null; } /** * check mysql connection */ public function ping() { try { $this->pdoQuery("SELECT 1"); } catch (PDOException $e) { return false; } return true; } /** * Can be used to execute queries which shouldn't return data */ public function pdoExec($sQuery) { return self::$_rLink->exec($sQuery); } /** * Executes query and returns PDOStatement object or false */ public function pdoQuery($sQuery) { return self::$_rLink->query($sQuery); } /** * database query exception handler for exceptions appeared out of the try/catch block */ public function pdoExceptionHandler($oException) { if(!($oException instanceof PDOException)) { throw $oException; return; } if(!isset($oException->errorInfo[self::$_sErrorKey])) $oException->errorInfo[self::$_sErrorKey] = array( 'code' => BX_DB_ERR_QUERY_ERROR, 'message' => !empty($oException->errorInfo[2]) ? $oException->errorInfo[2] : $oException->getMessage(), 'trace' => $oException->getTrace() ); $this->error($oException->errorInfo[self::$_sErrorKey]); exit(255); } /** * get mysql option */ function getOption($sName) { $oStatement = $this->pdoQuery("SELECT @@{$sName}"); return $this->getOne($oStatement); } /** * execute sql query and return one value result */ public function getOne($oStatement, $aBindings = array(), $iIndex = 0) { if(!$oStatement) return false; else if(!($oStatement instanceof PDOStatement) && is_string($oStatement)) $oStatement = $this->prepare($oStatement); $aResult = array(); if($this->res($oStatement, $aBindings)) $aResult = $oStatement->fetch(PDO::FETCH_NUM); return is_array($aResult) && count($aResult) ? $aResult[$iIndex] : false; } /** * execute sql query and return one row result */ function getRow($oStatement, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC) { $aResult = array(); if(!$oStatement) return $aResult; else if(!($oStatement instanceof PDOStatement) && is_string($oStatement)) $oStatement = $this->prepare($oStatement); if(!in_array($iFetchType, array(PDO::FETCH_NUM, PDO::FETCH_ASSOC, PDO::FETCH_BOTH))) $iFetchType = $this->_iPdoFetchType; if(!$this->res($oStatement, $aBindings)) return array(); $aResult = $oStatement->fetch($iFetchType); if($aResult === false) return array(); return $aResult; } /** * execute sql query and return a column as result */ function getColumn($oStatement, $aBindings = array(), $iFetchColumnNumber = 0) { $aResult = array(); if(!$oStatement) return $aResult; else if(!($oStatement instanceof PDOStatement) && is_string($oStatement)) $oStatement = $this->prepare($oStatement); if(!$this->res($oStatement, $aBindings)) return array(); $aResult = $oStatement->fetchAll(PDO::FETCH_COLUMN, $iFetchColumnNumber); if($aResult === false) return array(); return $aResult; } /** * execute sql query and return the first row of result * and keep $array type and poiter to all data */ public function getFirstRow($oStatement, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC) { if(!$oStatement) return array(); else if(!($oStatement instanceof PDOStatement) && is_string($oStatement)) $oStatement = $this->prepare($oStatement); if(!in_array($iFetchType, array(PDO::FETCH_NUM, PDO::FETCH_ASSOC, PDO::FETCH_BOTH))) $iFetchType = $this->_iPdoFetchType; if(!$this->res($oStatement, $aBindings)) return array(); $aResult = $oStatement->fetch($iFetchType); if($aResult === false) { $oStatement->closeCursor(); return array(); } $this->_oStatement = $oStatement; return $aResult; } /** * return next row of pointed last getFirstRow calling data */ public function getNextRow($iFetchType = PDO::FETCH_ASSOC) { if(!$this->_oStatement) return array(); if(!in_array($iFetchType, array(PDO::FETCH_NUM, PDO::FETCH_ASSOC, PDO::FETCH_BOTH))) $iFetchType = $this->_iPdoFetchType; $aResult = $this->_oStatement->fetch($iFetchType); if($aResult !== false) return $aResult; $this->_oStatement->closeCursor(); $this->_oStatement = null; return array(); } /** * execute sql query and return table of records as result */ public function getAll($oStatement, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC) { $aResult = array(); if(!$oStatement) return $aResult; else if(!($oStatement instanceof PDOStatement) && is_string($oStatement)) $oStatement = $this->prepare($oStatement); if(!in_array($iFetchType, array(PDO::FETCH_NUM, PDO::FETCH_ASSOC, PDO::FETCH_BOTH))) $iFetchType = $this->_iPdoFetchType; if(!$this->res($oStatement, $aBindings)) return array(); $aResult = $oStatement->fetchAll($iFetchType); if($aResult === false) return array(); return $aResult; } /** * Executes sql query and returns table of records as result. * * @deprecated use getAll instead. */ public function fillArray($oStatement, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC) { return $this->getAll($oStatement, $aBindings, $iFetchType); } /** * execute sql query and return table of records as result */ public function getAllWithKey($oStatement, $sFieldKey, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC) { $aResult = array(); if(!$oStatement) return $aResult; else if(!($oStatement instanceof PDOStatement) && is_string($oStatement)) $oStatement = $this->prepare($oStatement); $aRow = $this->getFirstRow($oStatement, $aBindings, $iFetchType); while(!empty($aRow)) { $aResult[$aRow[$sFieldKey]] = $aRow; $aRow = $this->getNextRow($iFetchType); } return $aResult; } /** * execute sql query and return table of records as result */ public function getPairs($oStatement, $sFieldKey, $sFieldValue, $aBindings = [], $iFetchType = PDO::FETCH_ASSOC) { $aResult = []; if(!$oStatement) return $aResult; else if(!($oStatement instanceof PDOStatement) && is_string($oStatement)) $oStatement = $this->prepare($oStatement); $aRow = $this->getFirstRow($oStatement, $aBindings, $iFetchType); while(!empty($aRow)) { $aResult[$aRow[$sFieldKey] ?? ''] = $aRow[$sFieldValue]; $aRow = $this->getNextRow($iFetchType); } return $aResult; } /** * return number of affected rows in current mysql result * * NOTE: PDOStatement::rowCount works for SELECT queries in MySQL. * So, this method should be rewritten if the other DB engine will be used. */ public function getNumRows($oStatement = null) { if($oStatement && ($oStatement instanceof PDOStatement)) return $oStatement->rowCount(); if($this->_oStatement && ($this->_oStatement instanceof PDOStatement)) return $this->_oStatement->rowCount(); return 0; } /** * returns the number of rows affected by the last DELETE, INSERT, or UPDATE statement. */ public function getAffectedRows($oStatement = null) { return $this->getNumRows($oStatement); } public function lastId() { return self::$_rLink->lastInsertId(); } /** * execute any query return number of rows affected/false */ public function query($oStatement, $aBindings = array(), $bVerbose = null) { if(!$oStatement) return false; else if(!($oStatement instanceof PDOStatement) && is_string($oStatement)) $oStatement = $this->prepare($oStatement); if($this->res($oStatement, $aBindings, $bVerbose)) return $oStatement->rowCount(); return false; } /** * execute any query */ public function res($oStatement, $aBindings = array(), $bVerbose = null) { if(!$oStatement || !($oStatement instanceof PDOStatement)) return false; if($oStatement->errorCode() == BX_PDO_STATE_SUCCESS) return true; if(isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginQuery($oStatement->queryString); for ($iAttempts = 3; $iAttempts > 0; --$iAttempts) { $oException = null; $iErrorCode = 0; $bResult = false; try { $bResult = $this->executeStatement($oStatement, $aBindings, $bVerbose, $iErrorCode); } catch (PDOException $oException) { $iErrorCode = $oException->errorInfo[1]; } // if deadlock timeout - try again, make sure that deadlock timeout is short if (!$bResult && (1205 == $iErrorCode || 1213 == $iErrorCode)) { continue; } // if mysql connection is lost - reconnect and try again if(!$bResult && (2006 == $iErrorCode || !$this->ping())) { $this->disconnect(); $this->connect(); continue; } // in all other cases exit from the loop if ($oException) throw $oException; break; } if(isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endQuery($oStatement); ++self::$_iQueriesCounter; //is needed for SILENT mode if(!$bResult && !empty($this->_aError)) $this->error($this->_aError); return $bResult; } /** * get mysql server info */ public function getServerInfo() { return self::$_rLink->getAttribute(PDO::ATTR_SERVER_VERSION); } /** * get mysql version */ public function getVersion() { $s = $this->getOne("SELECT VERSION()"); return preg_match("/([0-9\.]+)/", $s, $m) ? $m[1] : false; } /** * get list of tables in database */ public function listTables() { $sDBName = is_array($this->_sDbname) ? $this->_sDbname[0] : $this->_sDbname; $oStatement = $this->pdoQuery("SHOW TABLES FROM `" . $sDBName . "`"); return $this->getColumn($oStatement); } public function getFields($sTable) { $oStatement = $this->pdoQuery("SHOW COLUMNS FROM `" . $sTable . "`"); $aFields = $this->getAll($oStatement); $aResult = array('original' => array(), 'uppercase' => array()); foreach($aFields as $aField) { $aResult['original'][] = $aField['Field']; $aResult['uppercase'][] = strtoupper($aField['Field']); } return $aResult; } public function fetchField($oStatement, $iField, $aBindings = array()) { $aResult = array(); if(!$oStatement) return $aResult; else if(!($oStatement instanceof PDOStatement) && is_string($oStatement)) { $oStatement = $this->prepare($oStatement); if(!$this->res($oStatement, $aBindings)) return $aResult; } return $oStatement->getColumnMeta($iField); } public function isTableExists($sTable) { $aTableNames = $this->listTables(); foreach($aTableNames as $iKey => $sTableName) $aTableNames[$iKey] = strtoupper($sTableName); return in_array(strtoupper($sTable), $aTableNames); } public function isFieldExists($sTable, $sFieldName) { $aFields = $this->getFields($sTable); return in_array(strtoupper($sFieldName), $aFields['uppercase']); } public function isIndexExists($sTable, $sIndexName) { $bIndex = false; $aIndexes = $this->getAll("SHOW INDEXES FROM `" . $sTable . "`"); foreach($aIndexes as $aIndex) if($aIndex['Key_name'] == $sIndexName) { $bIndex = true; break; } return $bIndex; } public function error($aError) { $sErrorType = self::$_aErrors[$aError['code']]; $bVerbose = isset($aError['verbose']) ? (bool)$aError['verbose'] : $this->_bErrorChecking; if(!$bVerbose) { $this->log($sErrorType . ': ' . $aError['message']); if (!defined('BX_DOL_INSTALL')) // this is needed to display error during installation return; } if((defined('BX_DB_FULL_VISUAL_PROCESSING') && BX_DB_FULL_VISUAL_PROCESSING) || defined('BX_DOL_INSTALL')) { $sOutput = '
'."\n"; $sOutput .= '
Error
'."\n"; $sOutput .= '
' . $sErrorType . '
'."\n"; if((defined('BX_DB_FULL_DEBUG_MODE') && BX_DB_FULL_DEBUG_MODE) || defined('BX_DOL_INSTALL')) $sOutput .= $this->errorOutput($aError); $sOutput .= '
'; } if (self::$_rLink) { // connection errors aren't logged since bx_log required DB connection bx_log('sys_db', "$sErrorType\n" . (empty($aError['message']) ? '' : " Error: {$aError['message']}\n") . (empty($aError['query']) ? '' : " Query: {$aError['query']}\n") . (!function_exists('getLoggedId') || !getLoggedId() ? '' : " Account ID: " . getLoggedId() . "\n"), BX_LOG_ERR ); } if(defined('BX_DB_DO_EMAIL_ERROR_REPORT') && BX_DB_DO_EMAIL_ERROR_REPORT) { $sSiteTitle = $this->getParam('site_title'); $sMailBody = "Database error in " . $sSiteTitle . "

\n"; $sMailBody .= $this->errorOutput($aError); $sMailBody .= "
Auto-report system"; sendMail($this->getParam('site_email'), "Database error in " . $sSiteTitle, $sMailBody, 0, array(), BX_EMAIL_SYSTEM, 'html', true); } bx_show_service_unavailable_error_and_exit($sOutput); } public function isParamInCache($sKey) { return is_array(self::$_aParams) && isset(self::$_aParams[$sKey]); } public function cacheParams($bForceCacheInvalidate = false, $bForceCacheInvalidateMixed = false) { if ($bForceCacheInvalidate) $this->cacheParamsClear(); self::$_aParams = $this->getPairs("SELECT `name`, `value` FROM `sys_options`", "name", "value"); list($sTmplCode, $sTmplName) = BxDolTemplate::retrieveCode(); if(!empty($sTmplCode) && !empty($sTmplName)) { $iTmplMix = 0; if(is_array($sTmplCode)) list($sTmplCode, $iTmplMix) = $sTmplCode; $bMixesDiabled = ($GLOBALS['glMixesDisabled'] ?? false); if(empty($iTmplMix) && !$bMixesDiabled) $iTmplMix = (int)$this->getParam($sTmplName . '_default_mix'); if(!empty($iTmplMix) && !$bMixesDiabled) { $sCacheNameMixed = self::$_sParamsCacheNameMixed . $sTmplCode . '_' . $iTmplMix; if($bForceCacheInvalidateMixed) $this->cacheParamsClear($sCacheNameMixed); $aMixed = $this->fromCache($sCacheNameMixed, 'getPairs', "SELECT `option`, `value` FROM `sys_options_mixes2options` WHERE `mix_id`=:mix_id", "option", "value", array( 'mix_id' => $iTmplMix )); if(!empty($aMixed) && is_array($aMixed)) self::$_aParams = array_merge(self::$_aParams, $aMixed); } } if (empty(self::$_aParams)) { self::$_aParams = array(); return false; } return true; } public function cacheParamsClear($sCacheName = '') { if(empty($sCacheName)) { self::$_aParams = []; $sCacheName = self::$_sParamsCacheName; } return $this->cleanCache($sCacheName); } public function isParam($sKey, $bFromCache = true) { if ($bFromCache && $this->isParamInCache($sKey)) return true; $sQuery = $this->prepare("SELECT `name` FROM `sys_options` WHERE `name` = ? LIMIT 1", $sKey); return $this->getOne($sQuery) == $sKey; } public function addParam($sName, $sValue, $iKateg, $sDesc, $sType) { $sQuery = $this->prepare("INSERT INTO `sys_options` SET `category_id` = ?, `name` = ?, `caption` = ?, `value` = ?, `type` = ?", $iKateg, $sName, $sDesc, $sValue, $sType); $this->query($sQuery); // renew params cache $this->cacheParams(true); } public function getParam($sKey, $bFromCache = true) { if (!$sKey) return false; if ($bFromCache) { return $this->isParamInCache($sKey) ? self::$_aParams[$sKey] : false; } else { $sQuery = $this->prepare("SELECT `tmo`.`value` AS `value` FROM `sys_options_mixes2options` AS `tmo` INNER JOIN `sys_options_mixes` AS `tm` ON `tmo`.`mix_id`=`tm`.`id` AND `tm`.`active`='1' WHERE `tmo`.`option`=? LIMIT 1", $sKey); $s = $this->getOne($sQuery); if($s === false) { $sQuery = $this->prepare("SELECT `value` FROM `sys_options` WHERE `name` = ? LIMIT 1", $sKey); $s = $this->getOne($sQuery); } self::$_aParams[$sKey] = $s; // cache it return $s; } } public function setParam($sKey, $mixedValue, $iMixId = 0) { if(empty($iMixId)) $sQuery = $this->prepare("UPDATE `sys_options` SET `value` = ? WHERE `name` = ? LIMIT 1", $mixedValue, $sKey); else $sQuery = $this->prepare("REPLACE INTO `sys_options_mixes2options` SET `option` = ?, `mix_id` = ?, `value` = ?", $sKey, $iMixId, $mixedValue); $bResult = (int)$this->query($sQuery) > 0; // renew params cache self::$_aParams[$sKey] = $mixedValue; return $bResult; } public function getParamsMix($iId) { return $this->fromCache(self::$_sParamsCacheNameMix . $iId, 'getRow', 'SELECT * FROM `sys_options_mixes` WHERE `id`=:id', array( 'id' => $iId )); } public function getParamsMixActive($sType) { return $this->getRow("SELECT * FROM `sys_options_mixes` WHERE `type`=:type AND `active`='1'", array( 'type' => $sType )); } public function getParamsMixes($sType, $mixedPublished = false) { $aBindings = array('type' => $sType); $sQuery = "SELECT * FROM `sys_options_mixes` WHERE `type`=:type"; if($mixedPublished !== false) { $aBindings['published'] = (int)$mixedPublished; $sQuery .= " AND `published`=:published"; } return $this->getAll($sQuery, $aBindings); } public function setTimezone($sTimezone) { $oDate = new DateTime('now', new DateTimeZone($sTimezone)); return $this->pdoExec('SET time_zone = "' . $oDate->format('P') . '"') !== false; } public function getEncoding() { $oStatement = $this->pdoQuery('SELECT @@character_set_database'); if($oStatement !== false) return $this->getOne($oStatement); return false; } public function setErrorChecking ($b) { $this->_bErrorChecking = $b; } /** * Cache functions. */ public function getDbCacheObject () { if($this->_oDbCacheObject != null) return $this->_oDbCacheObject; $sEngine = $this->getParam('sys_db_cache_engine'); $this->_oDbCacheObject = bx_instance('BxDolCache'.$sEngine); if(!$this->_oDbCacheObject->isAvailable()) $this->_oDbCacheObject = bx_instance('BxDolCacheFile'); return $this->_oDbCacheObject; } public function genDbCacheKey ($sName) { return 'db_' . $sName . '_' . bx_site_hash() . '.php'; } /** * @param mixed $iTTL seconds, when an integer. A non-integer third argument is ignored: * existing callers pass a query string in that position. The file engine only * drops the entry when this read passes a TTL; other engines expire on write. */ public function getCache ($sName, $sFunc, $iTTL = false) { if (!$this->getParam('sys_db_cache_enable')) return false; if (!is_int($iTTL) || $iTTL < 1) $iTTL = false; $oCache = $this->getDbCacheObject (); $sKey = $this->genDbCacheKey($sName); return $oCache->getData($sKey, $iTTL); } /** * @param mixed $iTTL seconds, when an integer greater than zero. Otherwise the engine default * (the file engine keeps the entry until it is deleted). */ public function setCache ($sName, $mData, $iTTL = false) { if (!$this->getParam('sys_db_cache_enable')) return false; if (!is_int($iTTL) || $iTTL < 1) $iTTL = false; $oCache = $this->getDbCacheObject (); $sKey = $this->genDbCacheKey($sName); $oCache->setData($sKey, $mData, $iTTL); } public function fromCache ($sName, $sFunc) { $aArgs = func_get_args(); array_shift ($aArgs); // shift $sName array_shift ($aArgs); // shift $sFunc if (!$this->getParam('sys_db_cache_enable')) return call_user_func_array (array ($this, $sFunc), $aArgs); // pass other function parameters as database function parameters $oCache = $this->getDbCacheObject (); $sKey = $this->genDbCacheKey($sName); $mixedRet = $oCache->getData($sKey); if ($mixedRet !== null) { return $mixedRet; } else { $mixedRet = call_user_func_array (array ($this, $sFunc), $aArgs); // pass other function parameters as database function parameters $oCache->setData($sKey, $mixedRet); } return $mixedRet; } public function cleanCache ($sName) { $oCache = $this->getDbCacheObject(); $sKey = $this->genDbCacheKey($sName); return $oCache->delData($sKey); } public function &fromMemory ($sName, $sFunc) { if(array_key_exists($sName, self::$_aDbCacheData) && !defined('BX_DOL_INSTALL') && !defined('BX_DOL_CRON_EXECUTE')) return self::$_aDbCacheData[$sName]; $aArgs = func_get_args(); array_shift($aArgs); // shift $sName array_shift($aArgs); // shift $sFunc self::$_aDbCacheData[$sName] = call_user_func_array (array ($this, $sFunc), $aArgs); // pass other function parameters as database function parameters return self::$_aDbCacheData[$sName]; } public function cleanMemory ($sName) { if(!isset(self::$_aDbCacheData[$sName])) return false; unset(self::$_aDbCacheData[$sName]); return true; } public function cleanMemoryAll () { self::$_aDbCacheData = array(); return true; } /** * It escapes string to pass to mysql query. * Try to use "prepare" function always (@see BxDolDb::prepare), use "escape" only if "prepare" function is not possible at all. * Also consider using "implode_escape" function (@see BxDolDb::implode_escape). * * @param string $s string to escape * @return escaped string whcich is ready to pass to SQL query. */ public function escape($s) { try { $s = self::$_rLink->quote($s); } catch (PDOException $oException) { $oException->errorInfo[self::$_sErrorKey] = array( 'code' => BX_DB_ERR_ESCAPE, 'message' => $oException->getMessage(), 'trace' => $oException->getTrace() ); throw $oException; } return $s; } /** * @param bool $bStrict one identifier of letters, digits and underscores. The loose check also allows a qualified name and most characters inside backticks. */ public function isValidFieldName($s, $bStrict = false) { if ($bStrict) return is_string($s) && (bool)preg_match('/^[A-Za-z0-9_]+$/', $s); if (!is_string($s)) return false; if (false !== strpos($s, '`.`')) { $a = explode('`.`', $s, 2); foreach ($a as $sField) { if (false === $this->isValidFieldName($sField, $bStrict)) return false; } return true; } $inner = $s; // Disallow trailing space if ($inner === '' || preg_match('/\s$/u', $inner)) { return false; } // Disallow all-numeric content (even if quoted) if (preg_match('/^\d+$/u', $inner)) { return false; } $i = 0; $len = mb_strlen($inner, 'UTF-8'); while ($i < $len) { $char = mb_substr($inner, $i, 1, 'UTF-8'); // Allow escaped backtick (``) if ($char === '`') { if ($i + 1 >= $len || mb_substr($inner, $i + 1, 1, 'UTF-8') !== '`') { return false; // Single backtick is invalid } $i += 2; continue; } // Convert to UTF-8 bytes to check for supplementary chars $utf8 = mb_convert_encoding($char, 'UTF-8'); $byteLen = strlen($utf8); // 1-byte to 3-byte UTF-8 sequences = BMP (valid) // 4-byte UTF-8 sequences = Supplementary chars (invalid) if ($byteLen === 4) { return false; } // Disallow ASCII NUL (0x00) if ($utf8 === "\x00") { return false; } $i++; } return true; /* // alternative quick checking without checking BMP range if (preg_match('/^[0-9]+$/', $s)) return false; return preg_match('/^(?:(?!`)[\x01-\x7F\x{0080}-\x{FFFF}]|``)*[^[:space:]]$/u', $s); */ } /** * This function is usefull when you need to form array of parameters to pass to IN(...) SQL construction. * Example: * @code * $a = array(2, 4.5, 'apple', 'car'); * $s = "SELECT * FROM `t` WHERE `a` IN (" . $oDb->implode_escape($a) . ")"; * echo $s; // outputs: SELECT * FROM `t` WHERE `a` IN (2, 4.5, 'apple', 'car') * @endcode * * @param $mixed array or parameters or just one paramter * @return string which is ready to pass to IN(...) SQL construction */ public function implode_escape ($mixed) { if (is_array($mixed)) { $s = ''; foreach ($mixed as $v) $s .= (is_numeric($v) ? $v : $this->escape($v)) . ','; if ($s) return substr($s, 0, -1); else return 'NULL'; } return is_numeric($mixed) ? $mixed : ($mixed ? $this->escape($mixed) : 'NULL'); } /** * @deprecated */ public function unescape ($mixed) { if(is_array($mixed)) { foreach($mixed as $k => $v) $mixed[$k] = $this->getOne("SELECT '$v'"); return $mixed; } else return $this->getOne("SELECT '$mixed'"); } /** * Prepare SQL query before execution if some arguments are need to be passed to it. * All parameters marked with question (?) symbol in SQL query are replaced with parameters passed after SQL query parameter. * Parameters are properly excaped and surrounded by qutes if needed. * Example: * @code * $sSql = $oDb->prepare("SELECT `a`, `b` from `t` WHERE `c` = ? and `d` = ?", 12, 'aa'); * echo $sSql;// outputs: SELECT `a`, `b` from `t` WHERE `c` = 12 and `d` = 'aa' * $a = $oDb->getAll($sSql); * @endcode * * @param string $sQuery SQL query, parameters for replacing are marked with ? symbol * @param mixed $mixed any number if parameters to replace, number of parameters whould match number of ? symbols in SQL query * @return PDOStatement object with SQL query ready for execution */ public function prepare($sQuery) { if(!self::$_rLink) return false; if (self::$_bMultuServersMode && (0 != strncasecmp(ltrim($sQuery, " \t\n\r(\0\x0B"), 'SELECT', 6))) $this->setReadOnlyMode(false); $aArgs = func_get_args(); $sQuery = array_shift($aArgs); $oStatement = self::$_rLink->prepare($sQuery); $iIndex = 1; foreach($aArgs as $mixedArg) { if(is_null($mixedArg)) $iValueType = PDO::PARAM_NULL; else if(is_numeric($mixedArg) && is_int($mixedArg)) $iValueType = PDO::PARAM_INT; else $iValueType = PDO::PARAM_STR; $oStatement->bindValue($iIndex++, $mixedArg, $iValueType); } return $oStatement; } /** * Prepare SQL query before execution if some arguments are need to be passed to it. * All parameters marked with question (?) symbol in SQL query are replaced with parameters passed after SQL query parameter. * Parameters are properly excaped and surrounded by qutes if needed. * Example: * @code * $sSql = $oDb->prepare("SELECT `a`, `b` from `t` WHERE `c` = ? and `d` = ?", 12, 'aa'); * echo $sSql;// outputs: SELECT `a`, `b` from `t` WHERE `c` = 12 and `d` = 'aa' * $a = $oDb->getAll($sSql); * @endcode * * @param string $sQuery SQL query, parameters for replacing are marked with ? symbol * @param mixed $mixed any number if parameters to replace, number of parameters whould match number of ? symbols in SQL query * @return string with SQL query. */ function prepareAsString($sQuery) { $aArgs = func_get_args(); $sQuery = array_shift($aArgs); $iPos = 0; foreach ($aArgs as $mixedArg) { if (is_null($mixedArg)) $s = 'NULL'; elseif (is_numeric($mixedArg)) $s = $mixedArg; else $s = $this->escape($mixedArg); $i = bx_mb_strpos($sQuery, '?', $iPos); $sQuery = bx_mb_substr_replace($sQuery, $s, $i, 1); $iPos = $i + get_mb_len($s); } return $sQuery; } /** * Convert array of key => values to SQL query. * Array keys are field names and array values are field values. * @param $a array * @param $sDiv fields separator, by default it is ',', another useful value is ' AND ' * @param $sOperator operator for comparision: '=' or 'LIKE' * @param $bWildcardSpaceChars substiture any space and dash characters with single wildcard symbol * @return part of SQL query string */ public function arrayToSQL($a, $sDiv = ',', $sOperator = '=', $bWildcardSpaceChars = false) { $aOperators = ['=', 'LIKE', 'like']; $sOperator = in_array($sOperator, $aOperators) ? $sOperator : '='; $s = ''; foreach($a as $k => $v) { if (!$this->isValidFieldName($k)) throw new Exception('Invalid field name in arrayToSQL method'); if ($bWildcardSpaceChars) $v = preg_replace('/[\p{Zs}\p{Cc}\p{Pd}]/', '_', $v); $s .= "`{$k}` {$sOperator} " . (is_null($v) ? 'NULL' : $this->escape($v)) . $sDiv; } return trim($s, $sDiv); } function isValidOperator(string $operator): bool { static $operators = [ // Comparison '=', '!=', '<>', '<', '>', '<=', '>=', '<=>', // Logical 'AND', 'OR', 'XOR', 'NOT', // Arithmetic '+', '-', '*', '/', '%', 'DIV', 'MOD', // Bitwise '&', '|', '^', '<<', '>>', // Other MySQL operators 'IS', 'IS NOT', 'LIKE', 'NOT LIKE', 'REGEXP', 'NOT REGEXP', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', // Custom functions/operators 'COUNT', 'LOCATE', 'EMPTY VALUE', 'NOT EMPTY VALUE', 'NOTHING', 'IN_SET', ]; return in_array(strtoupper(trim($operator)), $operators, true); } protected function log($s) { if (defined('BX_DIRECTORY_PATH_LOGS')) { $sPath = BX_DIRECTORY_PATH_LOGS; } else { $sDirName = pathinfo(__FILE__, PATHINFO_DIRNAME); $sPath = $sDirName . '/../../logs/'; } return file_put_contents($sPath . 'db.err.log', date('Y-m-d H:i:s') . "\t" . $s . "\n", FILE_APPEND); } public function executeSQL($sPath, $aReplace = array (), $isBreakOnError = true) { if(!file_exists($sPath) || !($rHandler = fopen($sPath, "r"))) return array(array ('query' => "fopen($sPath, 'r')", 'error' => 'file not found or permission denied')); self::$_rLink->setAttribute(PDO::ATTR_ERRMODE, BX_DB_MODE_SILENT); $sQuery = ""; $sDelimiter = ';'; $aResult = array(); while(!feof($rHandler)) { $sStr = trim(fgets($rHandler)); if(empty($sStr) || $sStr[0] == "" || $sStr[0] == "#" || ($sStr[0] == "-" && $sStr[1] == "-")) continue; //--- Change delimiter ---// if(strpos($sStr, "DELIMITER //") !== false || strpos($sStr, "DELIMITER ;") !== false) { $sDelimiter = trim(str_replace('DELIMITER', '', $sStr)); continue; } $sQuery .= $sStr; //--- Check for multiline query ---// if(substr($sStr, -strlen($sDelimiter)) != $sDelimiter) continue; //--- Execute query ---// if ($aReplace) $sQuery = str_replace($aReplace['from'], $aReplace['to'], $sQuery); if($sDelimiter != ';') $sQuery = str_replace($sDelimiter, "", $sQuery); if($this->query(trim($sQuery), array(), false) === false) { $aResult[] = array('query' => $sQuery, 'error' => $this->_aError['message']); if ($isBreakOnError) break; } $sQuery = ""; } fclose($rHandler); self::$_rLink->setAttribute(PDO::ATTR_ERRMODE, $this->_iPdoErrorMode); return empty($aResult) ? true : $aResult; } protected function executeStatement($oStatement, $aBindings = array(), $bVerbose = null, &$iErrorCode = null) { $bResult = false; if (!isset($aBindings[0])) { foreach($aBindings as $sKey => $mixedValue) { if(is_null($mixedValue)) $oStatement->bindValue(":{$sKey}", $mixedValue, PDO::PARAM_NULL); else if(is_numeric($mixedValue) && is_int($mixedValue)) $oStatement->bindValue(":{$sKey}", $mixedValue, PDO::PARAM_INT); else $oStatement->bindValue(":{$sKey}", $mixedValue, PDO::PARAM_STR); } $aBindings = array(); } switch (self::$_rLink->getAttribute(PDO::ATTR_ERRMODE)) { case PDO::ERRMODE_SILENT: $bResult = $this->executeStatementSilent($oStatement, $aBindings, $bVerbose); break; case PDO::ERRMODE_EXCEPTION: $bResult = $this->executeStatementException($oStatement, $aBindings, $bVerbose); break; } if (!$bResult && !is_null($iErrorCode)) { $aError = $oStatement->errorInfo(); $iErrorCode = $aError[1]; } return $bResult; } protected function executeStatementException($oStatement, $aBindings = array(), $bVerbose = null) { $bResult = false; try { $bResult = $oStatement->execute(!empty($aBindings) && is_array($aBindings) ? $aBindings : null); } catch (PDOException $oException) { $aError = $oStatement->errorInfo(); $oException->errorInfo[self::$_sErrorKey] = array( 'code' => BX_DB_ERR_QUERY_ERROR, 'message' => !empty($aError[2]) ? $aError[2] : $oException->getMessage(), 'query' => $oStatement->queryString, 'trace' => $oException->getTrace(), 'verbose' => $bVerbose ); throw $oException; } return $bResult; } protected function executeStatementSilent($oStatement, $aBindings = array(), $bVerbose = null) { $bResult = $oStatement->execute(!empty($aBindings) && is_array($aBindings) ? $aBindings : null); if($bResult) return true; $aError = $oStatement->errorInfo(); $aTrace = debug_backtrace(); unset($aTrace[0]); $this->_aError = array( 'code' => BX_DB_ERR_QUERY_ERROR, 'message' => !empty($aError[2]) ? $aError[2] : '', 'query' => $oStatement->queryString, 'trace' => $aTrace, 'verbose' => $bVerbose ); return false; } protected function errorOutput($aError) { $aErrorLocation = []; if(!empty($aError['query']) && !empty($aError['trace'])) foreach($aError['trace'] as $aCall ) if(isset($aCall['args']) && is_array($aCall['args'])) foreach($aCall['args'] as $argNum => $argVal) if((is_string($argVal) && strcmp($argVal, $aError['query']) == 0) || ($argVal instanceof PDOStatement && strcmp($argVal->queryString, $aError['query']) == 0)) { $aErrorLocation['file'] = isset($aCall['file']) ? $aCall['file'] : (isset($aCall['class']) ? 'class: ' . $aCall['class'] : 'undefined'); $aErrorLocation['line'] = isset($aCall['line']) ? $aCall['line'] : 'undefined'; $aErrorLocation['function'] = $aCall['function']; $aErrorLocation['arg'] = $argNum; } $sOutput = ''; if(!empty($aError['query'])) $sOutput .= '

Query:
' . bx_process_output($aError['query']) . '

'."\n"; if(!empty($aError['message'])) $sOutput .= '

Mysql error:
' . $aError['message'] . '

'."\n"; if(!empty($aErrorLocation)) $sOutput .= '

Location:
The error was found in ' . $aErrorLocation['function'] . ' function in the file ' . $aErrorLocation['file'] . ' at line ' . $aErrorLocation['line'] . '.

'."\n"; $sOutput .= '

collation_connection:
' . $this->getOne("SELECT @@collation_connection") . '

'."\n"; if(!empty($aError['trace'])) { $sBackTrace = print_r($aError['trace'], true); if(defined ('BX_DATABASE_USER') && !is_array(BX_DATABASE_USER)) $sBackTrace = str_replace('[_sUser:protected] => ' . BX_DATABASE_USER, '[_sUser:protected] => *****', $sBackTrace); if(defined ('BX_DATABASE_PASS') && !is_array(BX_DATABASE_PASS)) $sBackTrace = str_replace('[_sPassword:protected] => ' . BX_DATABASE_PASS, '[_sPassword:protected] => *****', $sBackTrace); $sOutput .= '
Debug backtrace:
' . htmlspecialchars_adv($sBackTrace) . '
'; } if(!empty(self::$_aParams)) { $aParams = self::$_aParams; $aParamsSecret = $this->getColumn("SELECT `name` FROM `sys_options` WHERE `type`='secret'", "name"); if($aParamsSecret && is_array($aParamsSecret)) array_walk($aParams, function(&$sValue, $sKey) use ($aParamsSecret) { if(in_array($sKey, $aParamsSecret)) { $sValue = bx_gen_secret($sValue); } elseif (mb_strlen($sValue) > 100) { $sValue = mb_substr($sValue, 0, 100) . '...'; } }); $sOutput .= '
Settings:
' . htmlspecialchars_adv(var_export($aParams, true)) . '
'; } $sOutput .= '

Called script:
' . htmlspecialchars_adv($_SERVER['PHP_SELF']) . '

'; if(!empty($_REQUEST)) $sOutput .= '

Request parameters:

' . htmlspecialchars_adv(var_export($_REQUEST, true)) . '

'; return $sOutput; } } /** * Create the very first instance and initiate connetion to database. */ BxDolDb::getInstance(); function getParam($sParamName, $bUseCache = true) { if (bx_is_api() && $sParamName == 'permalinks_seo_links') return true; $oObj = BxDolDb::getInstance(); if ($oObj === null) return false; return $oObj->getParam($sParamName, $bUseCache); } function setParam($sParamName, $sParamVal) { $oObj = BxDolDb::getInstance(); if ($oObj === null) return false; return $oObj->setParam($sParamName, $sParamVal); } /** @} */