Page MenuHomeIn-Portal Phabricator

in-portal
No OneTemporary

File Metadata

Created
Tue, Feb 25, 5:52 AM

in-portal

Index: branches/5.2.x/core/kernel/db/db_connection.php
===================================================================
--- branches/5.2.x/core/kernel/db/db_connection.php (revision 15517)
+++ branches/5.2.x/core/kernel/db/db_connection.php (revision 15518)
@@ -1,1509 +1,1509 @@
<?php
/**
* @version $Id$
* @package In-Portal
* @copyright Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/
defined('FULL_PATH') or die('restricted access!');
/**
* Multi database connection class
*
*/
class kDBConnection extends kBase {
/**
* Current database type
*
* @var string
* @access protected
*/
protected $dbType = 'mysql';
/**
* Created connection handle
*
* @var resource
* @access protected
*/
protected $connectionID = null;
/**
* Remembers, that database connection was opened successfully
*
* @var bool
* @access public
*/
public $connectionOpened = false;
/**
* Connection parameters, that were used
*
* @var Array
* @access protected
*/
protected $connectionParams = Array ('host' => '', 'user' => '', 'pass' => '', 'db' => '');
/**
* Index of database server
*
* @var int
* @access protected
*/
protected $serverIndex = 0;
/**
* Handle of currently processed recordset
*
* @var resource
* @access protected
*/
protected $queryID = null;
/**
* DB type specific function mappings
*
* @var Array
* @access protected
*/
protected $metaFunctions = Array ();
/**
* Function to handle sql errors
*
* @var Array|string
* @access public
*/
public $errorHandler = '';
/**
* Error code
*
* @var int
* @access protected
*/
protected $errorCode = 0;
/**
* Error message
*
* @var string
* @access protected
*/
protected $errorMessage = '';
/**
* Defines if database connection
* operations should generate debug
* information
*
* @var bool
* @access public
*/
public $debugMode = false;
/**
* Save query execution statistics
*
* @var bool
* @access protected
*/
protected $_captureStatistics = false;
/**
* Last query to database
*
* @var string
* @access public
*/
public $lastQuery = '';
/**
* Total processed queries count
*
* @var int
* @access protected
*/
protected $_queryCount = 0;
/**
* Total time, used for serving queries
*
* @var Array
* @access protected
*/
protected $_queryTime = 0;
/**
* Indicates, that next database query could be cached, when memory caching is enabled
*
* @var bool
* @access public
*/
public $nextQueryCachable = false;
/**
* For backwards compatibility with kDBLoadBalancer class
*
* @var bool
* @access public
*/
public $nextQueryFromMaster = false;
/**
* Initializes connection class with
* db type to used in future
*
* @param string $dbType
* @param string $errorHandler
* @param int $server_index
* @access public
*/
public function __construct($dbType, $errorHandler = '', $server_index = 0)
{
if ( class_exists('kApplication') ) {
// prevents "Fatal Error" on 2nd installation step (when database is empty)
parent::__construct();
}
$this->dbType = $dbType;
$this->serverIndex = $server_index;
// $this->initMetaFunctions();
if (!$errorHandler) {
$this->errorHandler = Array(&$this, 'handleError');
}
else {
$this->errorHandler = $errorHandler;
}
$this->_captureStatistics = defined('DBG_CAPTURE_STATISTICS') && DBG_CAPTURE_STATISTICS && !(defined('ADMIN') && ADMIN);
}
/**
* Set's custom error
*
* @param int $code
* @param string $msg
* @access protected
*/
protected function setError($code, $msg)
{
$this->errorCode = $code;
$this->errorMessage = $msg;
}
/**
* Checks if previous query execution
* raised an error.
*
* @return bool
* @access public
*/
public function hasError()
{
return $this->errorCode != 0;
}
/**
* Caches function specific to requested
* db type
*
* @access protected
*/
protected function initMetaFunctions()
{
$ret = Array ();
switch ( $this->dbType ) {
case 'mysql':
$ret = Array (); // only define functions, that name differs from "dbType_<meta_name>"
break;
}
$this->metaFunctions = $ret;
}
/**
* Gets function for specific db type
* based on it's meta name
*
* @param string $name
* @return string
* @access protected
*/
protected function getMetaFunction($name)
{
/*if ( !isset($this->metaFunctions[$name]) ) {
$this->metaFunctions[$name] = $name;
}*/
return $this->dbType . '_' . $name;
}
/**
* Try to connect to database server
* using specified parameters and set
* database to $db if connection made
*
* @param string $host
* @param string $user
* @param string $pass
* @param string $db
* @param bool $force_new
* @param bool $retry
* @return bool
* @access public
*/
public function Connect($host, $user, $pass, $db, $force_new = false, $retry = false)
{
$this->connectionParams = Array ('host' => $host, 'user' => $user, 'pass' => $pass, 'db' => $db);
$func = $this->getMetaFunction('connect');
$this->connectionID = $func($host, $user, $pass, $force_new);
if ($this->connectionID) {
if (defined('DBG_SQL_MODE')) {
$this->Query('SET sql_mode = \''.DBG_SQL_MODE.'\'');
}
if (defined('SQL_COLLATION') && defined('SQL_CHARSET')) {
$this->Query('SET NAMES \''.SQL_CHARSET.'\' COLLATE \''.SQL_COLLATION.'\'');
}
$this->setError(0, ''); // reset error
$this->setDB($db);
}
// process error (fatal in most cases)
$func = $this->getMetaFunction('errno');
$this->errorCode = $this->connectionID ? $func($this->connectionID) : $func();
if ( is_resource($this->connectionID) && !$this->hasError() ) {
$this->connectionOpened = true;
return true;
}
$func = $this->getMetaFunction('error');
$this->errorMessage = $this->connectionID ? $func($this->connectionID) : $func();
$error_msg = 'Database connection failed, please check your connection settings.<br/>Error (' . $this->errorCode . '): ' . $this->errorMessage;
if ( (defined('IS_INSTALL') && IS_INSTALL) || $retry ) {
trigger_error($error_msg, E_USER_WARNING);
}
else {
$this->Application->redirectToMaintenance();
- throw new Exception($error_msg);
+ throw new RuntimeException($error_msg);
}
$this->connectionOpened = false;
return false;
}
/**
* Setups the connection according given configuration
*
* @param Array $config
* @return bool
* @access public
*/
public function setup($config)
{
if ( is_object($this->Application) ) {
$this->debugMode = $this->Application->isDebugMode();
}
return $this->Connect(
$config['Database']['DBHost'],
$config['Database']['DBUser'],
$config['Database']['DBUserPassword'],
$config['Database']['DBName']
);
}
/**
* Performs 3 reconnect attempts in case if connection to a DB was lost in the middle of script run (e.g. server restart)
*
* @param bool $force_new
* @return bool
* @access protected
*/
protected function ReConnect($force_new = false)
{
$retry_count = 0;
$connected = false;
$func = $this->getMetaFunction('close');
$func($this->connectionID);
while ( $retry_count < 3 ) {
sleep(5); // wait 5 seconds before each reconnect attempt
$connected = $this->Connect(
$this->connectionParams['host'],
$this->connectionParams['user'],
$this->connectionParams['pass'],
$this->connectionParams['db'],
$force_new, true
);
if ( $connected ) {
break;
}
$retry_count++;
}
return $connected;
}
/**
* Shows error message from previous operation
* if it failed
*
* @param string $sql
* @param string $key_field
* @param bool $no_debug
* @return bool
* @access protected
*/
protected function showError($sql = '', $key_field = null, $no_debug = false)
{
static $retry_count = 0;
$func = $this->getMetaFunction('errno');
if (!$this->connectionID) {
// no connection while doing mysql_query
$this->errorCode = $func();
if ( $this->hasError() ) {
$func = $this->getMetaFunction('error');
$this->errorMessage = $func();
$ret = $this->callErrorHandler($sql);
if (!$ret) {
exit;
}
}
return false;
}
// checking if there was an error during last mysql_query
$this->errorCode = $func($this->connectionID);
if ( $this->hasError() ) {
$func = $this->getMetaFunction('error');
$this->errorMessage = $func($this->connectionID);
$ret = $this->callErrorHandler($sql);
if ( ($this->errorCode == 2006 || $this->errorCode == 2013) && ($retry_count < 3) ) {
// #2006 - MySQL server has gone away
// #2013 - Lost connection to MySQL server during query
$retry_count++;
if ( $this->ReConnect() ) {
return $this->Query($sql, $key_field, $no_debug);
}
}
if (!$ret) {
exit;
}
}
else {
$retry_count = 0;
}
return false;
}
/**
* Sends db error to a predefined error handler
*
* @param $sql
* @return bool
* @access protected
*/
protected function callErrorHandler($sql)
{
if (is_array($this->errorHandler)) {
$func = $this->errorHandler[1];
$ret = $this->errorHandler[0]->$func($this->errorCode, $this->errorMessage, $sql);
}
else {
$func = $this->errorHandler;
$ret = $func($this->errorCode, $this->errorMessage, $sql);
}
return $ret;
}
/**
* Default error handler for sql errors
*
* @param int $code
* @param string $msg
* @param string $sql
* @return bool
* @access public
*/
public function handleError($code, $msg, $sql)
{
echo '<strong>Processing SQL</strong>: ' . $sql . '<br/>';
echo '<strong>Error (' . $code . '):</strong> ' . $msg . '<br/>';
return false;
}
/**
* Set's database name for connection
* to $new_name
*
* @param string $new_name
* @return bool
* @access protected
*/
protected function setDB($new_name)
{
if (!$this->connectionID) return false;
$func = $this->getMetaFunction('select_db');
return $func($new_name, $this->connectionID);
}
/**
* Returns first field of first line
* of recordset if query ok or false
* otherwise
*
* @param string $sql
* @param int $offset
* @return string
* @access public
*/
public function GetOne($sql, $offset = 0)
{
$row = $this->GetRow($sql, $offset);
if ( !$row ) {
return false;
}
return array_shift($row);
}
/**
* Returns first row of recordset
* if query ok, false otherwise
*
* @param string $sql
* @param int $offset
* @return Array
* @access public
*/
public function GetRow($sql, $offset = 0)
{
$sql .= ' ' . $this->getLimitClause($offset, 1);
$ret = $this->Query($sql);
if ( !$ret ) {
return false;
}
return array_shift($ret);
}
/**
* Returns 1st column of recordset as
* one-dimensional array or false otherwise
* Optional parameter $key_field can be used
* to set field name to be used as resulting
* array key
*
* @param string $sql
* @param string $key_field
* @return Array
* @access public
*/
public function GetCol($sql, $key_field = null)
{
$rows = $this->Query($sql);
if ( !$rows ) {
return $rows;
}
$i = 0;
$row_count = count($rows);
$ret = Array ();
if ( isset($key_field) ) {
while ( $i < $row_count ) {
$ret[$rows[$i][$key_field]] = array_shift($rows[$i]);
$i++;
}
}
else {
while ( $i < $row_count ) {
$ret[] = array_shift($rows[$i]);
$i++;
}
}
return $ret;
}
/**
* Returns iterator for 1st column of a recordset or false in case of error.
* Optional parameter $key_field can be used to set field name to be used
* as resulting array key.
*
* @param string $sql
* @param string $key_field
* @return bool|kMySQLQueryCol
*/
public function GetColIterator($sql, $key_field = null)
{
return $this->GetIterator($sql, $key_field, false, 'kMySQLQueryCol');
}
/**
* Queries db with $sql query supplied
* and returns rows selected if any, false
* otherwise. Optional parameter $key_field
* allows to set one of the query fields
* value as key in string array.
*
* @param string $sql
* @param string $key_field
* @param bool $no_debug
* @return Array
* @access public
*/
public function Query($sql, $key_field = null, $no_debug = false)
{
$this->_queryCount++;
$this->lastQuery = $sql;
$query_func = $this->getMetaFunction('query');
// set 1st checkpoint: begin
$start_time = $this->_captureStatistics ? microtime(true) : 0;
// set 1st checkpoint: end
$this->setError(0, ''); // reset error
$this->queryID = $query_func($sql, $this->connectionID);
if ( is_resource($this->queryID) ) {
$ret = Array ();
$fetch_func = $this->getMetaFunction('fetch_assoc');
if ( isset($key_field) ) {
while (($row = $fetch_func($this->queryID))) {
$ret[$row[$key_field]] = $row;
}
}
else {
while (($row = $fetch_func($this->queryID))) {
$ret[] = $row;
}
}
// set 2nd checkpoint: begin
if ( $this->_captureStatistics ) {
$query_time = microtime(true) - $start_time;
if ( $query_time > DBG_MAX_SQL_TIME ) {
$this->Application->logSlowQuery($sql, $query_time);
}
$this->_queryTime += $query_time;
}
// set 2nd checkpoint: end
$this->Destroy();
return $ret;
}
else {
// set 2nd checkpoint: begin
if ( $this->_captureStatistics ) {
$this->_queryTime += microtime(true) - $start_time;
}
// set 2nd checkpoint: end
}
return $this->showError($sql, $key_field, $no_debug);
}
/**
* Returns iterator to a recordset, produced from running $sql query Queries db with $sql query supplied and returns kMySQLQuery iterator
* or false in case of error. Optional parameter $key_field allows to
* set one of the query fields value as key in string array.
*
* @param string $sql
* @param string $key_field
* @param bool $no_debug
* @param string $iterator_class
* @return kMySQLQuery|bool
* @access public
*/
public function GetIterator($sql, $key_field = null, $no_debug = false, $iterator_class = 'kMySQLQuery')
{
$this->_queryCount++;
$this->lastQuery = $sql;
$query_func = $this->getMetaFunction('query');
// set 1st checkpoint: begin
$start_time = $this->_captureStatistics ? microtime(true) : 0;
// set 1st checkpoint: end
$this->setError(0, ''); // reset error
$this->queryID = $query_func($sql, $this->connectionID);
if ( is_resource($this->queryID) ) {
$ret = new $iterator_class($this->queryID, $key_field);
/* @var $ret kMySQLQuery */
// set 2nd checkpoint: begin
if ( $this->_captureStatistics ) {
$query_time = microtime(true) - $start_time;
if ( $query_time > DBG_MAX_SQL_TIME ) {
$this->Application->logSlowQuery($sql, $query_time);
}
$this->_queryTime += $query_time;
}
// set 2nd checkpoint: end
return $ret;
}
else {
// set 2nd checkpoint: begin
if ( $this->_captureStatistics ) {
$this->_queryTime += microtime(true) - $start_time;
}
// set 2nd checkpoint: end
}
return $this->showError($sql, $key_field, $no_debug);
}
/**
* Free memory used to hold recordset handle
*
* @access public
*/
public function Destroy()
{
$free_func = $this->getMetaFunction('free_result');
$free_func($this->queryID);
unset($this->queryID);
}
/**
* Performs sql query, that will change database content
*
* @param string $sql
* @return bool
* @access public
*/
public function ChangeQuery($sql)
{
$this->Query($sql);
return !$this->hasError();
}
/**
* Returns auto increment field value from
* insert like operation if any, zero otherwise
*
* @return int
* @access public
*/
public function getInsertID()
{
$func = $this->getMetaFunction('insert_id');
return $func($this->connectionID);
}
/**
* Returns row count affected by last query
*
* @return int
* @access public
*/
public function getAffectedRows()
{
$func = $this->getMetaFunction('affected_rows');
return $func($this->connectionID);
}
/**
* Returns LIMIT sql clause part for specific db
*
* @param int $offset
* @param int $rows
* @return string
* @access public
*/
public function getLimitClause($offset, $rows)
{
if ( !($rows > 0) ) {
return '';
}
switch ( $this->dbType ) {
default:
return 'LIMIT ' . $offset . ',' . $rows;
break;
}
}
/**
* If it's a string, adds quotes and backslashes (only work since PHP 4.3.0)
* Otherwise returns as-is
*
* @param mixed $string
* @return string
* @access public
*/
public function qstr($string)
{
if ( is_null($string) ) {
return 'NULL';
}
# This will also quote numeric values. This should be harmless,
# and protects against weird problems that occur when they really
# _are_ strings such as article titles and string->number->string
# conversion is not 1:1.
return "'" . mysql_real_escape_string($string, $this->connectionID) . "'";
}
/**
* Calls "qstr" function for each given array element
*
* @param Array $array
* @param string $function
* @return Array
*/
public function qstrArray($array, $function = 'qstr')
{
return array_map(Array (&$this, $function), $array);
}
/**
* Escapes strings (only work since PHP 4.3.0)
*
* @param mixed $string
* @return string
* @access public
*/
public function escape($string)
{
if ( is_null($string) ) {
return 'NULL';
}
$string = mysql_real_escape_string($string, $this->connectionID);
// prevent double-escaping of MySQL wildcard symbols ("%" and "_") in case if they were already escaped
return str_replace(Array ('\\\\%', '\\\\_'), Array ('\\%', '\\_'), $string);
}
/**
* Returns last error code occurred
*
* @return int
* @access public
*/
public function getErrorCode()
{
return $this->errorCode;
}
/**
* Returns last error message
*
* @return string
* @access public
*/
public function getErrorMsg()
{
return $this->errorMessage;
}
/**
* Performs insert of given data (useful with small number of queries)
* or stores it to perform multiple insert later (useful with large number of queries)
*
* @param Array $fields_hash
* @param string $table
* @param string $type
* @param bool $insert_now
* @return bool
* @access public
*/
public function doInsert($fields_hash, $table, $type = 'INSERT', $insert_now = true)
{
static $value_sqls = Array ();
if ($insert_now) {
$fields_sql = '`' . implode('`,`', array_keys($fields_hash)) . '`';
}
$values_sql = '';
foreach ($fields_hash as $field_name => $field_value) {
$values_sql .= $this->qstr($field_value) . ',';
}
// don't use preg here, as it may fail when string is too long
$value_sqls[] = rtrim($values_sql, ',');
$insert_result = true;
if ($insert_now) {
$insert_count = count($value_sqls);
if (($insert_count > 1) && ($value_sqls[$insert_count - 1] == $value_sqls[$insert_count - 2])) {
// last two records are the same
array_pop($value_sqls);
}
$sql = strtoupper($type) . ' INTO `' . $table . '` (' . $fields_sql . ') VALUES (' . implode('),(', $value_sqls) . ')';
$value_sqls = Array (); // reset before query to prevent repeated call from error handler to insert 2 records instead of 1
$insert_result = $this->ChangeQuery($sql);
}
return $insert_result;
}
/**
* Update given field values to given record using $key_clause
*
* @param Array $fields_hash
* @param string $table
* @param string $key_clause
* @return bool
* @access public
*/
public function doUpdate($fields_hash, $table, $key_clause)
{
if (!$fields_hash) return true;
$fields_sql = '';
foreach ($fields_hash as $field_name => $field_value) {
$fields_sql .= '`'.$field_name.'` = ' . $this->qstr($field_value) . ',';
}
// don't use preg here, as it may fail when string is too long
$fields_sql = rtrim($fields_sql, ',');
$sql = 'UPDATE `'.$table.'` SET '.$fields_sql.' WHERE '.$key_clause;
return $this->ChangeQuery($sql);
}
/**
* Allows to detect table's presence in database
*
* @param string $table_name
* @param bool $force
* @return bool
* @access public
*/
public function TableFound($table_name, $force = false)
{
static $table_found = false;
if ( $table_found === false ) {
$table_found = array_flip($this->GetCol('SHOW TABLES'));
}
if ( !preg_match('/^' . preg_quote(TABLE_PREFIX, '/') . '(.*)/', $table_name) ) {
$table_name = TABLE_PREFIX . $table_name;
}
if ( $force ) {
if ( $this->Query('SHOW TABLES LIKE ' . $this->qstr($table_name)) ) {
$table_found[$table_name] = 1;
}
else {
unset($table_found[$table_name]);
}
}
return isset($table_found[$table_name]);
}
/**
* Returns query processing statistics
*
* @return Array
* @access public
*/
public function getQueryStatistics()
{
return Array ('time' => $this->_queryTime, 'count' => $this->_queryCount);
}
/**
* Get status information from SHOW STATUS in an associative array
*
* @param string $which
* @return Array
* @access public
*/
public function getStatus($which = '%')
{
$status = Array ();
$records = $this->Query('SHOW STATUS LIKE "' . $which . '"');
foreach ($records as $record) {
$status[ $record['Variable_name'] ] = $record['Value'];
}
return $status;
}
/**
* Get slave replication lag. It will only work if the DB user has the PROCESS privilege.
*
* @return int
* @access public
*/
public function getSlaveLag()
{
// don't use kDBConnection::Query method, since it will create an array of all server processes
$processes = $this->GetIterator('SHOW PROCESSLIST');
$skip_states = Array (
'Waiting for master to send event',
'Connecting to master',
'Queueing master event to the relay log',
'Waiting for master update',
'Requesting binlog dump',
);
// find slave SQL thread
foreach ($processes as $process) {
if ( $process['User'] == 'system user' && !in_array($process['State'], $skip_states) ) {
// this is it, return the time (except -ve)
return $process['Time'] > 0x7fffffff ? false : $process['Time'];
}
}
return false;
}
}
class kDBConnectionDebug extends kDBConnection {
protected $_profileSQLs = false;
/**
* Info about this database connection to show in debugger report
*
* @var string
* @access protected
*/
protected $serverInfoLine = '';
/**
* Initializes connection class with
* db type to used in future
*
* @param string $dbType
* @param string $errorHandler
* @param int $server_index
* @access public
*/
public function __construct($dbType, $errorHandler = '', $server_index = 0)
{
parent::__construct($dbType, $errorHandler, $server_index);
$this->_profileSQLs = defined('DBG_SQL_PROFILE') && DBG_SQL_PROFILE;
}
/**
* Try to connect to database server
* using specified parameters and set
* database to $db if connection made
*
* @param string $host
* @param string $user
* @param string $pass
* @param string $db
* @param bool $force_new
* @param bool $retry
* @return bool
* @access public
*/
public function Connect($host, $user, $pass, $db, $force_new = false, $retry = false)
{
if ( defined('DBG_SQL_SERVERINFO') && DBG_SQL_SERVERINFO ) {
$this->serverInfoLine = $this->serverIndex . ' (' . $host . ')';
}
return parent::Connect($host, $user, $pass, $db, $force_new, $retry);
}
/**
* Queries db with $sql query supplied
* and returns rows selected if any, false
* otherwise. Optional parameter $key_field
* allows to set one of the query fields
* value as key in string array.
*
* @param string $sql
* @param string $key_field
* @param bool $no_debug
* @return Array
* @access public
*/
public function Query($sql, $key_field = null, $no_debug = false)
{
if ( $no_debug ) {
return parent::Query($sql, $key_field, $no_debug);
}
global $debugger;
$this->_queryCount++;
$this->lastQuery = $sql;
$query_func = $this->getMetaFunction('query');
// set 1st checkpoint: begin
if ( $this->_profileSQLs ) {
$queryID = $debugger->generateID();
$debugger->profileStart('sql_' . $queryID, $debugger->formatSQL($sql));
}
// set 1st checkpoint: end
$this->setError(0, ''); // reset error
$this->queryID = $query_func($sql, $this->connectionID);
if ( is_resource($this->queryID) ) {
$ret = Array ();
$fetch_func = $this->getMetaFunction('fetch_assoc');
if ( isset($key_field) ) {
while (($row = $fetch_func($this->queryID))) {
$ret[$row[$key_field]] = $row;
}
}
else {
while (($row = $fetch_func($this->queryID))) {
$ret[] = $row;
}
}
// set 2nd checkpoint: begin
if ( $this->_profileSQLs ) {
$current_element = current($ret);
$first_cell = count($ret) == 1 && count($current_element) == 1 ? current($current_element) : null;
if ( strlen($first_cell) > 200 ) {
$first_cell = substr($first_cell, 0, 50) . ' ...';
}
$debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable, $this->serverInfoLine);
$debugger->profilerAddTotal('sql', 'sql_' . $queryID);
$this->nextQueryCachable = false;
}
// set 2nd checkpoint: end
$this->Destroy();
return $ret;
}
else {
// set 2nd checkpoint: begin
if ( $this->_profileSQLs ) {
$debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable, $this->serverInfoLine);
$debugger->profilerAddTotal('sql', 'sql_' . $queryID);
$this->nextQueryCachable = false;
}
// set 2nd checkpoint: end
}
return $this->showError($sql, $key_field);
}
/**
* Queries db with $sql query supplied and returns kMySQLQuery iterator
* or false in case of error. Optional parameter $key_field allows to
* set one of the query fields value as key in string array.
*
* @param string $sql
* @param string $key_field
* @param bool $no_debug
* @param string $iterator_class
* @return kMySQLQuery|bool
* @access public
*/
public function GetIterator($sql, $key_field = null, $no_debug = false, $iterator_class = 'kMySQLQuery')
{
if ( $no_debug ) {
return parent::Query($sql, $key_field, $no_debug, $iterator_class);
}
global $debugger;
$this->_queryCount++;
$this->lastQuery = $sql;
$query_func = $this->getMetaFunction('query');
// set 1st checkpoint: begin
if ( $this->_profileSQLs ) {
$queryID = $debugger->generateID();
$debugger->profileStart('sql_' . $queryID, $debugger->formatSQL($sql));
}
// set 1st checkpoint: end
$this->setError(0, ''); // reset error
$this->queryID = $query_func($sql, $this->connectionID);
if ( is_resource($this->queryID) ) {
$ret = new $iterator_class($this->queryID, $key_field);
/* @var $ret kMySQLQuery */
// set 2nd checkpoint: begin
if ( $this->_profileSQLs ) {
$first_cell = count($ret) == 1 && $ret->fieldCount() == 1 ? current($ret->current()) : null;
if ( strlen($first_cell) > 200 ) {
$first_cell = substr($first_cell, 0, 50) . ' ...';
}
$debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable, $this->serverInfoLine);
$debugger->profilerAddTotal('sql', 'sql_' . $queryID);
$this->nextQueryCachable = false;
}
// set 2nd checkpoint: end
return $ret;
}
else {
// set 2nd checkpoint: begin
if ( $this->_profileSQLs ) {
$debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable, $this->serverInfoLine);
$debugger->profilerAddTotal('sql', 'sql_' . $queryID);
$this->nextQueryCachable = false;
}
// set 2nd checkpoint: end
}
return $this->showError($sql, $key_field);
}
}
class kMySQLQuery implements Iterator, Countable, SeekableIterator {
/**
* Current index in recordset
*
* @var int
* @access protected
*/
protected $position = -1;
/**
* Query resource
*
* @var resource
* @access protected
*/
protected $result;
/**
* Field to act as key in a resulting array
*
* @var string
* @access protected
*/
protected $keyField = null;
/**
* Data in current row of recordset
*
* @var Array
* @access protected
*/
protected $rowData = Array ();
/**
* Row count in a result
*
* @var int
* @access protected
*/
protected $rowCount = 0;
/**
* Creates new instance of a class
*
* @param resource $result
* @param null|string $key_field
*/
public function __construct($result, $key_field = null)
{
$this->result = $result;
$this->keyField = $key_field;
$this->rowCount = mysql_num_rows($this->result);
$this->rewind();
}
/**
* Moves recordset pointer to first element
*
* @return void
* @access public
* @implements Iterator::rewind
*/
public function rewind()
{
$this->seek(0);
}
/**
* Returns value at current position
*
* @return mixed
* @access public
* @implements Iterator::current
*/
function current()
{
return $this->rowData;
}
/**
* Returns key at current position
*
* @return mixed
* @access public
* @implements Iterator::key
*/
function key()
{
return $this->keyField ? $this->rowData[$this->keyField] : $this->position;
}
/**
* Moves recordset pointer to next position
*
* @return void
* @access public
* @implements Iterator::next
*/
function next()
{
$this->seek($this->position + 1);
}
/**
* Detects if current position is within recordset bounds
*
* @return bool
* @access public
* @implements Iterator::valid
*/
public function valid()
{
return $this->position < $this->rowCount;
}
/**
* Counts recordset rows
*
* @return int
* @access public
* @implements Countable::count
*/
public function count()
{
return $this->rowCount;
}
/**
* Counts fields in current row
*
* @return int
* @access public
*/
public function fieldCount()
{
return count($this->rowData);
}
/**
* Moves cursor into given position within recordset
*
* @param int $position
* @throws OutOfBoundsException
* @access public
* @implements SeekableIterator::seek
*/
public function seek($position)
{
if ( $this->position == $position ) {
return;
}
$this->position = $position;
if ( $this->valid() ) {
mysql_data_seek($this->result, $this->position);
$this->rowData = mysql_fetch_assoc($this->result);
}
/*if ( !$this->valid() ) {
throw new OutOfBoundsException('Invalid seek position (' . $position . ')');
}*/
}
/**
* Returns first recordset row
*
* @return Array
* @access public
*/
public function first()
{
$this->seek(0);
return $this->rowData;
}
/**
* Closes recordset and freese memory
*
* @return void
* @access public
*/
public function close()
{
mysql_free_result($this->result);
unset($this->result);
}
/**
* Frees memory when object is destroyed
*
* @return void
* @access public
*/
public function __destruct()
{
$this->close();
}
/**
* Returns all keys
*
* @return Array
* @access public
*/
public function keys()
{
$ret = Array ();
foreach ($this as $key => $value) {
$ret[] = $key;
}
return $ret;
}
/**
* Returns all values
*
* @return Array
* @access public
*/
public function values()
{
$ret = Array ();
foreach ($this as $value) {
$ret[] = $value;
}
return $ret;
}
/**
* Returns whole recordset as array
*
* @return Array
* @access public
*/
public function toArray()
{
$ret = Array ();
foreach ($this as $key => $value) {
$ret[$key] = $value;
}
return $ret;
}
}
class kMySQLQueryCol extends kMySQLQuery {
/**
* Returns value at current position
*
* @return mixed
* @access public
* @implements Iterator::current
*/
function current()
{
return reset($this->rowData);
}
/**
* Returns first column of first recordset row
*
* @return string
* @access public
*/
public function first()
{
$this->seek(0);
return reset($this->rowData);
}
}
Index: branches/5.2.x/core/kernel/utility/temp_handler.php
===================================================================
--- branches/5.2.x/core/kernel/utility/temp_handler.php (revision 15517)
+++ branches/5.2.x/core/kernel/utility/temp_handler.php (revision 15518)
@@ -1,1055 +1,1055 @@
<?php
/**
* @version $Id$
* @package In-Portal
* @copyright Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/
defined('FULL_PATH') or die('restricted access!');
class kTempTablesHandler extends kBase {
var $Tables = Array();
/**
* Master table name for temp handler
*
* @var string
* @access private
*/
var $MasterTable = '';
/**
* IDs from master table
*
* @var Array
* @access private
*/
var $MasterIDs = Array();
var $AlreadyProcessed = Array();
var $DroppedTables = Array();
var $FinalRefs = Array();
var $TableIdCounter = 0;
var $CopiedTables = Array();
/**
* Foreign key cache
*
* @var Array
*/
var $FKeysCache = Array ();
/**
* IDs of newly cloned items (key - prefix.special, value - array of ids)
*
* @var Array
*/
var $savedIDs = Array();
/**
* Window ID of current window
*
* @var mixed
*/
var $WindowID = '';
/**
* Event, that was used to create this object
*
* @var kEvent
* @access protected
*/
protected $parentEvent = null;
/**
* Sets new parent event to the object
*
* @param kEvent $event
* @return void
* @access public
*/
public function setParentEvent($event)
{
$this->parentEvent = $event;
}
function SetTables($tables)
{
// set table name as key for tables array
$this->Tables = $tables;
$this->MasterTable = $tables['TableName'];
}
function saveID($prefix, $special = '', $id = null)
{
if (!isset($this->savedIDs[$prefix.($special ? '.' : '').$special])) {
$this->savedIDs[$prefix.($special ? '.' : '').$special] = array();
}
if (is_array($id)) {
foreach ($id as $tmp_id => $live_id) {
$this->savedIDs[$prefix.($special ? '.' : '').$special][$tmp_id] = $live_id;
}
}
else {
$this->savedIDs[$prefix.($special ? '.' : '').$special][] = $id;
}
}
/**
* Get temp table name
*
* @param string $table
* @return string
*/
function GetTempName($table)
{
return $this->Application->GetTempName($table, $this->WindowID);
}
function GetTempTablePrefix()
{
return $this->Application->GetTempTablePrefix($this->WindowID);
}
/**
* Return live table name based on temp table name
*
* @param string $temp_table
* @return string
*/
function GetLiveName($temp_table)
{
return $this->Application->GetLiveName($temp_table);
}
function IsTempTable($table)
{
return $this->Application->IsTempTable($table);
}
/**
* Return temporary table name for master table
*
* @return string
* @access public
*/
function GetMasterTempName()
{
return $this->GetTempName($this->MasterTable);
}
function CreateTempTable($table)
{
$sql = 'CREATE TABLE ' . $this->GetTempName($table) . '
SELECT *
FROM ' . $table . '
WHERE 0';
$this->Conn->Query($sql);
}
function BuildTables($prefix, $ids)
{
$this->WindowID = $this->Application->GetVar('m_wid');
$this->TableIdCounter = 0;
$tables = Array(
'TableName' => $this->Application->getUnitOption($prefix, 'TableName'),
'IdField' => $this->Application->getUnitOption($prefix, 'IDField'),
'IDs' => $ids,
'Prefix' => $prefix,
'TableId' => $this->TableIdCounter++,
);
/*$parent_prefix = $this->Application->getUnitOption($prefix, 'ParentPrefix');
if ($parent_prefix) {
$tables['ForeignKey'] = $this->Application->getUnitOption($prefix, 'ForeignKey');
$tables['ParentPrefix'] = $parent_prefix;
$tables['ParentTableKey'] = $this->Application->getUnitOption($prefix, 'ParentTableKey');
}*/
$this->FinalRefs[ $tables['TableName'] ] = $tables['TableId']; // don't forget to add main table to FinalRefs too
$sub_items = $this->Application->getUnitOption($prefix, 'SubItems', Array ());
/* @var $sub_items Array */
if ( is_array($sub_items) ) {
foreach ($sub_items as $prefix) {
$this->AddTables($prefix, $tables);
}
}
$this->SetTables($tables);
}
/**
* Searches through TempHandler tables info for required prefix
*
* @param string $prefix
* @param Array $master
* @return mixed
*/
function SearchTable($prefix, $master = null)
{
if (is_null($master)) {
$master = $this->Tables;
}
if ($master['Prefix'] == $prefix) {
return $master;
}
if (isset($master['SubTables'])) {
foreach ($master['SubTables'] as $sub_table) {
$found = $this->SearchTable($prefix, $sub_table);
if ($found !== false) {
return $found;
}
}
}
return false;
}
function AddTables($prefix, &$tables)
{
if ( !$this->Application->prefixRegistred($prefix) ) {
// allows to skip subitem processing if subitem module not enabled/installed
return ;
}
$tmp = Array(
'TableName' => $this->Application->getUnitOption($prefix,'TableName'),
'IdField' => $this->Application->getUnitOption($prefix,'IDField'),
'ForeignKey' => $this->Application->getUnitOption($prefix,'ForeignKey'),
'ParentPrefix' => $this->Application->getUnitOption($prefix, 'ParentPrefix'),
'ParentTableKey' => $this->Application->getUnitOption($prefix,'ParentTableKey'),
'Prefix' => $prefix,
'AutoClone' => $this->Application->getUnitOption($prefix,'AutoClone'),
'AutoDelete' => $this->Application->getUnitOption($prefix,'AutoDelete'),
'TableId' => $this->TableIdCounter++,
);
$this->FinalRefs[ $tmp['TableName'] ] = $tmp['TableId'];
$constrain = $this->Application->getUnitOption($prefix, 'Constrain');
if ( $constrain ) {
$tmp['Constrain'] = $constrain;
$this->FinalRefs[ $tmp['TableName'] . $tmp['Constrain'] ] = $tmp['TableId'];
}
$sub_items = $this->Application->getUnitOption($prefix, 'SubItems', Array ());
/* @var $sub_items Array */
if ( is_array($sub_items) ) {
foreach ($sub_items as $prefix) {
$this->AddTables($prefix, $tmp);
}
}
if ( !is_array(getArrayValue($tables, 'SubTables')) ) {
$tables['SubTables'] = Array ();
}
$tables['SubTables'][] = $tmp;
}
function CloneItems($prefix, $special, $ids, $master = null, $foreign_key = null, $parent_prefix = null, $skip_filenames = false)
{
if (!isset($master)) $master = $this->Tables;
// recalling by different name, because we may get kDBList, if we recall just by prefix
if (!preg_match('/(.*)-item$/', $special)) {
$special .= '-item';
}
$object = $this->Application->recallObject($prefix.'.'.$special, $prefix, Array('skip_autoload' => true, 'parent_event' => $this->parentEvent));
/* @var $object kCatDBItem */
$object->PopulateMultiLangFields();
foreach ($ids as $id) {
$mode = 'create';
$cloned_ids = getArrayValue($this->AlreadyProcessed, $master['TableName']);
if ( $cloned_ids ) {
// if we have already cloned the id, replace it with cloned id and set mode to update
// update mode is needed to update second ForeignKey for items cloned by first ForeignKey
if ( getArrayValue($cloned_ids, $id) ) {
$id = $cloned_ids[$id];
$mode = 'update';
}
}
$object->Load($id);
$original_values = $object->GetFieldValues();
if (!$skip_filenames) {
$object->NameCopy($master, $foreign_key);
}
elseif ($master['TableName'] == $this->MasterTable) {
// kCatDBItem class only has this attribute
$object->useFilenames = false;
}
if (isset($foreign_key)) {
$master_foreign_key_field = is_array($master['ForeignKey']) ? $master['ForeignKey'][$parent_prefix] : $master['ForeignKey'];
$object->SetDBField($master_foreign_key_field, $foreign_key);
}
if ($mode == 'create') {
$this->RaiseEvent('OnBeforeClone', $master['Prefix'], $special, Array($object->GetId()), $foreign_key);
}
$object->inCloning = true;
$res = $mode == 'update' ? $object->Update() : $object->Create();
$object->inCloning = false;
if ($res)
{
if ( $mode == 'create' && is_array( getArrayValue($master, 'ForeignKey')) ) {
// remember original => clone mapping for dual ForeignKey updating
$this->AlreadyProcessed[$master['TableName']][$id] = $object->GetId();
}
if ($mode == 'create') {
$this->RaiseEvent('OnAfterClone', $master['Prefix'], $special, Array($object->GetId()), $foreign_key, array('original_id' => $id) );
$this->saveID($master['Prefix'], $special, $object->GetID());
}
if ( is_array(getArrayValue($master, 'SubTables')) ) {
foreach($master['SubTables'] as $sub_table) {
if (!getArrayValue($sub_table, 'AutoClone')) continue;
$sub_TableName = $object->IsTempTable() ? $this->GetTempName($sub_table['TableName']) : $sub_table['TableName'];
$foreign_key_field = is_array($sub_table['ForeignKey']) ? $sub_table['ForeignKey'][$master['Prefix']] : $sub_table['ForeignKey'];
$parent_key_field = is_array($sub_table['ParentTableKey']) ? $sub_table['ParentTableKey'][$master['Prefix']] : $sub_table['ParentTableKey'];
if (!$foreign_key_field || !$parent_key_field) continue;
$query = 'SELECT '.$sub_table['IdField'].' FROM '.$sub_TableName.'
WHERE '.$foreign_key_field.' = '.$original_values[$parent_key_field];
if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain'];
$sub_ids = $this->Conn->GetCol($query);
if ( is_array(getArrayValue($sub_table, 'ForeignKey')) ) {
// $sub_ids could containt newly cloned items, we need to remove it here
// to escape double cloning
$cloned_ids = getArrayValue($this->AlreadyProcessed, $sub_table['TableName']);
if ( !$cloned_ids ) $cloned_ids = Array();
$new_ids = array_values($cloned_ids);
$sub_ids = array_diff($sub_ids, $new_ids);
}
$parent_key = $object->GetDBField($parent_key_field);
$this->CloneItems($sub_table['Prefix'], $special, $sub_ids, $sub_table, $parent_key, $master['Prefix']);
}
}
}
}
if (!$ids) {
$this->savedIDs[$prefix.($special ? '.' : '').$special] = Array();
}
return $this->savedIDs[$prefix.($special ? '.' : '').$special];
}
function DeleteItems($prefix, $special, $ids, $master=null, $foreign_key=null)
{
if ( !$ids ) {
return;
}
if ( !isset($master) ) {
$master = $this->Tables;
}
if ( strpos($prefix, '.') !== false ) {
list($prefix, $special) = explode('.', $prefix, 2);
}
$prefix_special = rtrim($prefix . '.' . $special, '.');
//recalling by different name, because we may get kDBList, if we recall just by prefix
$recall_prefix = $prefix_special . ($special ? '' : '.') . '-item';
$object = $this->Application->recallObject($recall_prefix, $prefix, Array ('skip_autoload' => true, 'parent_event' => $this->parentEvent));
/* @var $object kDBItem */
foreach ($ids as $id) {
$object->Load($id);
$original_values = $object->GetFieldValues();
if ( !$object->Delete($id) ) {
continue;
}
if ( is_array(getArrayValue($master, 'SubTables')) ) {
foreach ($master['SubTables'] as $sub_table) {
if ( !getArrayValue($sub_table, 'AutoDelete') ) {
continue;
}
$sub_TableName = $object->IsTempTable() ? $this->GetTempName($sub_table['TableName']) : $sub_table['TableName'];
$foreign_key_field = is_array($sub_table['ForeignKey']) ? getArrayValue($sub_table, 'ForeignKey', $master['Prefix']) : $sub_table['ForeignKey'];
$parent_key_field = is_array($sub_table['ParentTableKey']) ? getArrayValue($sub_table, 'ParentTableKey', $master['Prefix']) : $sub_table['ParentTableKey'];
if ( !$foreign_key_field || !$parent_key_field ) {
continue;
}
$sql = 'SELECT ' . $sub_table['IdField'] . '
FROM ' . $sub_TableName . '
WHERE ' . $foreign_key_field . ' = ' . $original_values[$parent_key_field];
$sub_ids = $this->Conn->GetCol($sql);
$parent_key = $object->GetDBField(is_array($sub_table['ParentTableKey']) ? $sub_table['ParentTableKey'][$prefix] : $sub_table['ParentTableKey']);
$this->DeleteItems($sub_table['Prefix'], $special, $sub_ids, $sub_table, $parent_key);
}
}
}
}
function DoCopyLiveToTemp($master, $ids, $parent_prefix=null)
{
// when two tables refers the same table as sub-sub-table, and ForeignKey and ParentTableKey are arrays
// the table will be first copied by first sub-table, then dropped and copied over by last ForeignKey in the array
// this should not do any problems :)
if ( !preg_match("/.*\.[0-9]+/", $master['Prefix']) ) {
if( $this->DropTempTable($master['TableName']) )
{
$this->CreateTempTable($master['TableName']);
}
}
if (is_array($ids)) {
$ids = join(',', $ids);
}
$table_sig = $master['TableName'].(isset($master['Constrain']) ? $master['Constrain'] : '');
if ($ids != '' && !in_array($table_sig, $this->CopiedTables)) {
if ( getArrayValue($master, 'ForeignKey') ) {
if ( is_array($master['ForeignKey']) ) {
$key_field = $master['ForeignKey'][$parent_prefix];
}
else {
$key_field = $master['ForeignKey'];
}
}
else {
$key_field = $master['IdField'];
}
$query = 'INSERT INTO '.$this->GetTempName($master['TableName']).'
SELECT * FROM '.$master['TableName'].'
WHERE '.$key_field.' IN ('.$ids.')';
if (isset($master['Constrain'])) $query .= ' AND '.$master['Constrain'];
$this->Conn->Query($query);
$this->CopiedTables[] = $table_sig;
$query = 'SELECT '.$master['IdField'].' FROM '.$master['TableName'].'
WHERE '.$key_field.' IN ('.$ids.')';
if (isset($master['Constrain'])) $query .= ' AND '.$master['Constrain'];
$this->RaiseEvent( 'OnAfterCopyToTemp', $master['Prefix'], '', $this->Conn->GetCol($query) );
}
if ( getArrayValue($master, 'SubTables') ) {
foreach ($master['SubTables'] as $sub_table) {
$parent_key = is_array($sub_table['ParentTableKey']) ? $sub_table['ParentTableKey'][$master['Prefix']] : $sub_table['ParentTableKey'];
if (!$parent_key) continue;
if ( $ids != '' && $parent_key != $key_field ) {
$query = 'SELECT '.$parent_key.' FROM '.$master['TableName'].'
WHERE '.$key_field.' IN ('.$ids.')';
$sub_foreign_keys = join(',', $this->Conn->GetCol($query));
}
else {
$sub_foreign_keys = $ids;
}
$this->DoCopyLiveToTemp($sub_table, $sub_foreign_keys, $master['Prefix']);
}
}
}
function GetForeignKeys($master, $sub_table, $live_id, $temp_id=null)
{
$mode = 1; //multi
if (!is_array($live_id)) {
$live_id = Array($live_id);
$mode = 2; //single
}
if (isset($temp_id) && !is_array($temp_id)) $temp_id = Array($temp_id);
if ( isset($sub_table['ParentTableKey']) ) {
if ( is_array($sub_table['ParentTableKey']) ) {
$parent_key_field = $sub_table['ParentTableKey'][$master['Prefix']];
}
else {
$parent_key_field = $sub_table['ParentTableKey'];
}
}
else {
$parent_key_field = $master['IdField'];
}
$cached = getArrayValue($this->FKeysCache, $master['TableName'].'.'.$parent_key_field);
if ( $cached ) {
if ( array_key_exists(serialize($live_id), $cached) ) {
list($live_foreign_key, $temp_foreign_key) = $cached[serialize($live_id)];
if ($mode == 1) {
return $live_foreign_key;
}
else {
return Array($live_foreign_key[0], $temp_foreign_key[0]);
}
}
}
if ($parent_key_field != $master['IdField']) {
$query = 'SELECT '.$parent_key_field.' FROM '.$master['TableName'].'
WHERE '.$master['IdField'].' IN ('.join(',', $live_id).')';
$live_foreign_key = $this->Conn->GetCol($query);
if (isset($temp_id)) {
// because DoCopyTempToOriginal resets negative IDs to 0 in temp table (one by one) before copying to live
$temp_key = $temp_id < 0 ? 0 : $temp_id;
$query = 'SELECT '.$parent_key_field.' FROM '.$this->GetTempName($master['TableName']).'
WHERE '.$master['IdField'].' IN ('.join(',', $temp_key).')';
$temp_foreign_key = $this->Conn->GetCol($query);
}
else {
$temp_foreign_key = Array();
}
}
else {
$live_foreign_key = $live_id;
$temp_foreign_key = $temp_id;
}
$this->FKeysCache[$master['TableName'].'.'.$parent_key_field][serialize($live_id)] = Array($live_foreign_key, $temp_foreign_key);
if ($mode == 1) {
return $live_foreign_key;
}
else {
return Array($live_foreign_key[0], $temp_foreign_key[0]);
}
}
/**
* Copies data from temp to live table and returns IDs of copied records
*
* @param Array $master
* @param string $parent_prefix
* @param Array $current_ids
* @return Array
* @access public
*/
public function DoCopyTempToOriginal($master, $parent_prefix = null, $current_ids = Array())
{
if ( !$current_ids ) {
$query = 'SELECT ' . $master['IdField'] . ' FROM ' . $this->GetTempName($master['TableName']);
if ( isset($master['Constrain']) ) {
$query .= ' WHERE ' . $master['Constrain'];
}
$current_ids = $this->Conn->GetCol($query);
}
$table_sig = $master['TableName'] . (isset($master['Constrain']) ? $master['Constrain'] : '');
if ($current_ids) {
// delete all ids from live table - for MasterTable ONLY!
// because items from Sub Tables get deteleted in CopySubTablesToLive !BY ForeignKey!
if ( $master['TableName'] == $this->MasterTable ) {
$this->RaiseEvent('OnBeforeDeleteFromLive', $master['Prefix'], '', $current_ids);
$query = 'DELETE FROM ' . $master['TableName'] . ' WHERE ' . $master['IdField'] . ' IN (' . join(',', $current_ids) . ')';
$this->Conn->Query($query);
}
if ( getArrayValue($master, 'SubTables') ) {
if ( in_array($table_sig, $this->CopiedTables) || $this->FinalRefs[$table_sig] != $master['TableId'] ) {
return Array ();
}
foreach ($current_ids AS $id) {
$this->RaiseEvent('OnBeforeCopyToLive', $master['Prefix'], '', Array ($id));
//reset negative ids to 0, so autoincrement in live table works fine
if ( $id < 0 ) {
$query = ' UPDATE ' . $this->GetTempName($master['TableName']) . '
SET ' . $master['IdField'] . ' = 0
WHERE ' . $master['IdField'] . ' = ' . $id;
if ( isset($master['Constrain']) ) {
$query .= ' AND ' . $master['Constrain'];
}
$this->Conn->Query($query);
$id_to_copy = 0;
}
else {
$id_to_copy = $id;
}
//copy current id_to_copy (0 for new or real id) to live table
$query = ' INSERT INTO ' . $master['TableName'] . '
SELECT * FROM ' . $this->GetTempName($master['TableName']) . '
WHERE ' . $master['IdField'] . ' = ' . $id_to_copy;
$this->Conn->Query($query);
$insert_id = $id_to_copy == 0 ? $this->Conn->getInsertID() : $id_to_copy;
$this->saveID($master['Prefix'], '', array ($id => $insert_id));
$this->RaiseEvent('OnAfterCopyToLive', $master['Prefix'], '', Array ($insert_id), null, Array ('temp_id' => $id));
$this->UpdateForeignKeys($master, $insert_id, $id);
//delete already copied record from master temp table
$query = ' DELETE FROM ' . $this->GetTempName($master['TableName']) . '
WHERE ' . $master['IdField'] . ' = ' . $id_to_copy;
if ( isset($master['Constrain']) ) {
$query .= ' AND ' . $master['Constrain'];
}
$this->Conn->Query($query);
}
$this->CopiedTables[] = $table_sig;
// when all of ids in current master has been processed, copy all sub-tables data
$this->CopySubTablesToLive($master, $current_ids);
}
elseif ( !in_array($table_sig, $this->CopiedTables) && ($this->FinalRefs[$table_sig] == $master['TableId']) ) { //If current master doesn't have sub-tables - we could use mass operations
// We don't need to delete items from live here, as it get deleted in the beginning of the method for MasterTable
// or in parent table processing for sub-tables
$live_ids = Array ();
$this->RaiseEvent('OnBeforeCopyToLive', $master['Prefix'], '', $current_ids);
foreach ($current_ids as $an_id) {
if ( $an_id > 0 ) {
$live_ids[$an_id] = $an_id;
// positive (already live) IDs will be copied in on query all togather below,
// so we just store it here
continue;
}
else { // zero or negative ids should be copied one by one to get their InsertId
// resetting to 0 so it get inserted into live table with autoincrement
$query = ' UPDATE ' . $this->GetTempName($master['TableName']) . '
SET ' . $master['IdField'] . ' = 0
WHERE ' . $master['IdField'] . ' = ' . $an_id;
// constrain is not needed here because ID is already unique
$this->Conn->Query($query);
// copying
$query = ' INSERT INTO ' . $master['TableName'] . '
SELECT * FROM ' . $this->GetTempName($master['TableName']) . '
WHERE ' . $master['IdField'] . ' = 0';
$this->Conn->Query($query);
$live_ids[$an_id] = $this->Conn->getInsertID(); //storing newly created live id
//delete already copied record from master temp table
$query = ' DELETE FROM ' . $this->GetTempName($master['TableName']) . '
WHERE ' . $master['IdField'] . ' = 0';
$this->Conn->Query($query);
$this->UpdateChangeLogForeignKeys($master, $live_ids[$an_id], $an_id);
}
}
// copy ALL records to live table
$query = ' INSERT INTO ' . $master['TableName'] . '
SELECT * FROM ' . $this->GetTempName($master['TableName']);
if ( isset($master['Constrain']) ) {
$query .= ' WHERE ' . $master['Constrain'];
}
$this->Conn->Query($query);
$this->CopiedTables[] = $table_sig;
$this->RaiseEvent('OnAfterCopyToLive', $master['Prefix'], '', $live_ids);
$this->saveID($master['Prefix'], '', $live_ids);
// no need to clear temp table - it will be dropped by next statement
}
}
if ( $this->FinalRefs[ $master['TableName'] ] != $master['TableId'] ) {
return Array ();
}
/*if ( is_array(getArrayValue($master, 'ForeignKey')) ) { //if multiple ForeignKeys
if ( $master['ForeignKey'][$parent_prefix] != end($master['ForeignKey']) ) {
return; // Do not delete temp table if not all ForeignKeys have been processed (current is not the last)
}
}*/
$this->DropTempTable($master['TableName']);
$this->Application->resetCounters($master['TableName']);
if ( !isset($this->savedIDs[ $master['Prefix'] ]) ) {
$this->savedIDs[ $master['Prefix'] ] = Array ();
}
return $this->savedIDs[ $master['Prefix'] ];
}
/**
* Create separate connection for locking purposes
*
* @return kDBConnection
*/
function &_getSeparateConnection()
{
static $connection = null;
if (!isset($connection)) {
- $connection = $this->Application->makeClass( 'kDBConnection', Array (SQL_TYPE, Array (&$this->Application, 'handleSQLError')) );
+ $connection = $this->Application->makeClass( 'kDBConnection', Array (SQL_TYPE, Array ($this->Application, 'handleSQLError')) );
/* @var $connection kDBConnection */
$connection->debugMode = $this->Application->isDebugMode();
$connection->Connect(SQL_SERVER, SQL_USER, SQL_PASS, SQL_DB, true);
}
return $connection;
}
function UpdateChangeLogForeignKeys($master, $live_id, $temp_id)
{
if ($live_id == $temp_id) {
return ;
}
$prefix = $master['Prefix'];
$main_prefix = $this->Application->GetTopmostPrefix($prefix);
$ses_var_name = $main_prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix);
$changes = $this->Application->RecallVar($ses_var_name);
$changes = $changes ? unserialize($changes) : Array ();
foreach ($changes as $key => $rec) {
if ($rec['Prefix'] == $prefix && $rec['ItemId'] == $temp_id) {
// main item change log record
$changes[$key]['ItemId'] = $live_id;
}
if ($rec['MasterPrefix'] == $prefix && $rec['MasterId'] == $temp_id) {
// sub item change log record
$changes[$key]['MasterId'] = $live_id;
}
if (in_array($prefix, $rec['ParentPrefix']) && $rec['ParentId'][$prefix] == $temp_id) {
// parent item change log record
$changes[$key]['ParentId'][$prefix] = $live_id;
if (array_key_exists('DependentFields', $rec)) {
// these are fields from table of $rec['Prefix'] table!
// when one of dependent fields goes into idfield of it's parent item, that was changed
$parent_table_key = $this->Application->getUnitOption($rec['Prefix'], 'ParentTableKey');
$parent_table_key = is_array($parent_table_key) ? $parent_table_key[$prefix] : $parent_table_key;
if ($parent_table_key == $master['IdField']) {
$foreign_key = $this->Application->getUnitOption($rec['Prefix'], 'ForeignKey');
$foreign_key = is_array($foreign_key) ? $foreign_key[$prefix] : $foreign_key;
$changes[$key]['DependentFields'][$foreign_key] = $live_id;
}
}
}
}
$this->Application->StoreVar($ses_var_name, serialize($changes));
}
function UpdateForeignKeys($master, $live_id, $temp_id)
{
$this->UpdateChangeLogForeignKeys($master, $live_id, $temp_id);
foreach ($master['SubTables'] as $sub_table) {
$foreign_key_field = is_array($sub_table['ForeignKey']) ? getArrayValue($sub_table, 'ForeignKey', $master['Prefix']) : $sub_table['ForeignKey'];
if (!$foreign_key_field) {
continue;
}
list ($live_foreign_key, $temp_foreign_key) = $this->GetForeignKeys($master, $sub_table, $live_id, $temp_id);
//Update ForeignKey in sub TEMP table
if ($live_foreign_key != $temp_foreign_key) {
$query = 'UPDATE '.$this->GetTempName($sub_table['TableName']).'
SET '.$foreign_key_field.' = '.$live_foreign_key.'
WHERE '.$foreign_key_field.' = '.$temp_foreign_key;
if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain'];
$this->Conn->Query($query);
}
}
}
function CopySubTablesToLive($master, $current_ids) {
foreach ($master['SubTables'] as $sub_table) {
$table_sig = $sub_table['TableName'].(isset($sub_table['Constrain']) ? $sub_table['Constrain'] : '');
// delete records from live table by foreign key, so that records deleted from temp table
// get deleted from live
if (count($current_ids) > 0 && !in_array($table_sig, $this->CopiedTables) ) {
$foreign_key_field = is_array($sub_table['ForeignKey']) ? getArrayValue($sub_table, 'ForeignKey', $master['Prefix']) : $sub_table['ForeignKey'];
if (!$foreign_key_field) continue;
$foreign_keys = $this->GetForeignKeys($master, $sub_table, $current_ids);
if (count($foreign_keys) > 0) {
$query = 'SELECT '.$sub_table['IdField'].' FROM '.$sub_table['TableName'].'
WHERE '.$foreign_key_field.' IN ('.join(',', $foreign_keys).')';
if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain'];
if ( $this->RaiseEvent( 'OnBeforeDeleteFromLive', $sub_table['Prefix'], '', $this->Conn->GetCol($query), $foreign_keys ) ){
$query = 'DELETE FROM '.$sub_table['TableName'].'
WHERE '.$foreign_key_field.' IN ('.join(',', $foreign_keys).')';
if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain'];
$this->Conn->Query($query);
}
}
}
//sub_table passed here becomes master in the method, and recursively updated and copy its sub tables
$this->DoCopyTempToOriginal($sub_table, $master['Prefix']);
}
}
/**
* Raises event using IDs, that are currently being processed in temp handler
*
* @param string $name
* @param string $prefix
* @param string $special
* @param Array $ids
* @param string $foreign_key
* @param Array $add_params
* @return bool
* @access protected
*/
protected function RaiseEvent($name, $prefix, $special, $ids, $foreign_key = null, $add_params = null)
{
if ( !is_array($ids) ) {
return true;
}
$event_key = $prefix . ($special ? '.' : '') . $special . ':' . $name;
$event = new kEvent($event_key);
if ( isset($foreign_key) ) {
$event->setEventParam('foreign_key', $foreign_key);
}
$set_temp_id = ($name == 'OnAfterCopyToLive') && (!is_array($add_params) || !array_key_exists('temp_id', $add_params));
foreach ($ids as $index => $id) {
$event->setEventParam('id', $id);
if ( $set_temp_id ) {
$event->setEventParam('temp_id', $index);
}
if ( is_array($add_params) ) {
foreach ($add_params as $name => $val) {
$event->setEventParam($name, $val);
}
}
$this->Application->HandleEvent($event);
}
return $event->status == kEvent::erSUCCESS;
}
function DropTempTable($table)
{
if ( in_array($table, $this->DroppedTables) ) {
return false;
}
$query = 'DROP TABLE IF EXISTS ' . $this->GetTempName($table);
array_push($this->DroppedTables, $table);
$this->DroppedTables = array_unique($this->DroppedTables);
$this->Conn->Query($query);
return true;
}
function PrepareEdit()
{
$this->DoCopyLiveToTemp($this->Tables, $this->Tables['IDs']);
if ($this->Application->getUnitOption($this->Tables['Prefix'],'CheckSimulatniousEdit')) {
$this->CheckSimultaniousEdit();
}
}
function SaveEdit($master_ids = Array())
{
// SessionKey field is required for deleting records from expired sessions
$conn =& $this->_getSeparateConnection();
$sleep_count = 0;
do {
// acquire lock
$conn->ChangeQuery('LOCK TABLES '.TABLE_PREFIX.'Semaphores WRITE');
$sql = 'SELECT SessionKey
FROM ' . TABLE_PREFIX . 'Semaphores
WHERE (MainPrefix = ' . $conn->qstr($this->Tables['Prefix']) . ')';
$another_coping_active = $conn->GetOne($sql);
if ($another_coping_active) {
// another user is coping data from temp table to live -> release lock and try again after 1 second
$conn->ChangeQuery('UNLOCK TABLES');
$sleep_count++;
sleep(1);
}
} while ($another_coping_active && ($sleep_count <= 30));
if ($sleep_count > 30) {
// another coping process failed to finished in 30 seconds
$error_message = $this->Application->Phrase('la_error_TemporaryTableCopyingFailed');
$this->Application->SetVar('_temp_table_message', $error_message);
return false;
}
// mark, that we are coping from temp to live right now, so other similar attempt (from another script) will fail
$fields_hash = Array (
'SessionKey' => $this->Application->GetSID(),
'Timestamp' => adodb_mktime(),
'MainPrefix' => $this->Tables['Prefix'],
);
$conn->doInsert($fields_hash, TABLE_PREFIX.'Semaphores');
$semaphore_id = $conn->getInsertID();
// unlock table now to prevent permanent lock in case, when coping will end with SQL error in the middle
$conn->ChangeQuery('UNLOCK TABLES');
$ids = $this->DoCopyTempToOriginal($this->Tables, null, $master_ids);
// remove mark, that we are coping from temp to live
$conn->Query('LOCK TABLES '.TABLE_PREFIX.'Semaphores WRITE');
$sql = 'DELETE FROM ' . TABLE_PREFIX . 'Semaphores
WHERE SemaphoreId = ' . $semaphore_id;
$conn->ChangeQuery($sql);
$conn->ChangeQuery('UNLOCK TABLES');
return $ids;
}
function CancelEdit($master=null)
{
if (!isset($master)) $master = $this->Tables;
$this->DropTempTable($master['TableName']);
if ( getArrayValue($master, 'SubTables') ) {
foreach ($master['SubTables'] as $sub_table) {
$this->CancelEdit($sub_table);
}
}
}
/**
* Checks, that someone is editing selected records and returns true, when no one.
*
* @param Array $ids
*
* @return bool
*/
function CheckSimultaniousEdit($ids = null)
{
$tables = $this->Conn->GetCol('SHOW TABLES');
$mask_edit_table = '/' . TABLE_PREFIX . 'ses_(.*)_edit_' . $this->MasterTable . '$/';
$my_sid = $this->Application->GetSID();
$my_wid = $this->Application->GetVar('m_wid');
$ids = implode(',', isset($ids) ? $ids : $this->Tables['IDs']);
$sids = Array ();
if (!$ids) {
return true;
}
foreach ($tables as $table) {
if ( preg_match($mask_edit_table, $table, $rets) ) {
$sid = preg_replace('/(.*)_(.*)/', '\\1', $rets[1]); // remove popup's wid from sid
if ($sid == $my_sid) {
if ($my_wid) {
// using popups for editing
if (preg_replace('/(.*)_(.*)/', '\\2', $rets[1]) == $my_wid) {
// don't count window, that is being opened right now
continue;
}
}
else {
// not using popups for editing -> don't count my session tables
continue;
}
}
$sql = 'SELECT COUNT(' . $this->Tables['IdField'] . ')
FROM ' . $table . '
WHERE ' . $this->Tables['IdField'] . ' IN (' . $ids . ')';
$found = $this->Conn->GetOne($sql);
if (!$found || in_array($sid, $sids)) {
continue;
}
$sids[] = $sid;
}
}
if ($sids) {
// detect who is it
$sql = 'SELECT
CONCAT(IF (s.PortalUserId = ' . USER_ROOT . ', \'root\',
IF (s.PortalUserId = ' . USER_GUEST . ', \'Guest\',
CONCAT(u.FirstName, \' \', u.LastName, \' (\', u.Username, \')\')
)
), \' IP: \', s.IpAddress, \'\') FROM ' . TABLE_PREFIX . 'UserSessions AS s
LEFT JOIN ' . TABLE_PREFIX . 'Users AS u
ON u.PortalUserId = s.PortalUserId
WHERE s.SessionKey IN (' . implode(',', $sids) . ')';
$users = $this->Conn->GetCol($sql);
if ($users) {
$this->Application->SetVar('_simultanious_edit_message',
sprintf($this->Application->Phrase('la_record_being_edited_by'), join(",\n", $users))
);
return false;
}
}
return true;
}
}
\ No newline at end of file

Event Timeline