Page MenuHomeIn-Portal Phabricator

in-portal
No OneTemporary

File Metadata

Created
Sat, Feb 22, 12:04 AM

in-portal

Index: branches/5.3.x/core/kernel/utility/temp_handler.php
===================================================================
--- branches/5.3.x/core/kernel/utility/temp_handler.php (revision 15946)
+++ branches/5.3.x/core/kernel/utility/temp_handler.php (revision 15947)
@@ -1,1628 +1,1638 @@
<?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!');
/**
* Create a public interface to temp table structure of one unit and it's sub-items
*
* Pattern: Facade
*/
class kTempTablesHandler extends kBase {
/**
* Tables
*
* @var kTempHandlerTopTable
*/
protected $_tables;
/**
* 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;
if ( is_object($this->_tables) ) {
$this->_tables->setParentEvent($event);
}
}
/**
* Scans table structure of given unit
*
* @param string $prefix
* @param Array $ids
* @return void
* @access public
*/
public function BuildTables($prefix, $ids)
{
$this->_tables = new kTempHandlerTopTable($prefix, $ids);
if ( is_object($this->parentEvent) ) {
$this->_tables->setParentEvent($this->parentEvent);
}
}
/**
* Returns reference to top table.
*
* @return kTempHandlerTopTable
*/
public function getTopTable()
{
return $this->_tables;
}
/**
* Create temp table for editing db record from live table. If none ids are given, then just empty tables are created.
*
* @return void
* @access public
*/
public function PrepareEdit()
{
$this->_tables->doCopyLiveToTemp();
$this->_tables->checkSimultaneousEdit();
}
/**
* Deletes temp tables without copying their data back to live tables
*
* @return void
* @access public
*/
public function CancelEdit()
{
$this->_tables->deleteAll();
}
/**
* Saves changes made in temp tables to live tables
*
* @param Array $master_ids
* @return bool
* @access public
*/
public function SaveEdit($master_ids = Array())
{
// SessionKey field is required for deleting records from expired sessions
$sleep_count = 0;
$conn = $this->_getSeparateConnection();
do {
// acquire lock
$conn->ChangeQuery('LOCK TABLES ' . TABLE_PREFIX . 'Semaphores WRITE');
$sql = 'SELECT SessionKey
FROM ' . TABLE_PREFIX . 'Semaphores
WHERE (MainPrefix = ' . $conn->qstr($this->_tables->getPrefix()) . ')';
$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' => time(),
'MainPrefix' => $this->_tables->getPrefix(),
);
$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->_tables->doCopyTempToLive($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;
}
/**
* Deletes unit data for given items along with related sub-items
*
* @param string $prefix
* @param string $special
* @param Array $ids
* @throws InvalidArgumentException
*/
function DeleteItems($prefix, $special, $ids)
{
if ( strpos($prefix, '.') !== false ) {
throw new InvalidArgumentException("Pass prefix and special as separate arguments");
}
if ( !is_array($ids) ) {
throw new InvalidArgumentException('Incorrect ids format');
}
$this->_tables->doDeleteItems(rtrim($prefix . '.' . $special, '.'), $ids);
}
/**
* Clones given ids
*
* @param string $prefix
* @param string $special
* @param Array $ids
* @param Array $master
* @param int $foreign_key
* @param string $parent_prefix
* @param bool $skip_filenames
* @return Array
*/
function CloneItems($prefix, $special, $ids, $master = null, $foreign_key = null, $parent_prefix = null, $skip_filenames = false)
{
return $this->_tables->doCloneItems($prefix . '.' . $special, $ids, $foreign_key, $skip_filenames);
}
/**
* Create separate connection for locking purposes
*
* @return kDBConnection
* @access protected
*/
protected function _getSeparateConnection()
{
static $connection = null;
if (!isset($connection)) {
$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);
}
return $connection;
}
}
/**
* Base class, that represents one table
*
* Pattern: Composite
*/
abstract class kTempHandlerTable extends kBase {
/**
* Temp table was created from live table OR it was copied back to live table
*/
const STATE_COPIED = 1;
/**
* Temp table was deleted
*/
const STATE_DELETED = 2;
/**
* Reference to parent table
*
* @var kTempHandlerTable
* @access protected
*/
protected $_parent;
/**
* Field in this db table, that holds ID from it's parent table
*
* @var string
* @access protected
*/
protected $_foreignKey = '';
/**
* This table is connected to multiple parent tables
*
* @var bool
* @access protected
*/
protected $_multipleParents = false;
/**
* Foreign key cache
*
* @var Array
* @access protected
*/
protected $_foreignKeyCache = Array ();
/**
* Field in parent db table from where foreign key field value originates
*
* @var string
* @access protected
*/
protected $_parentTableKey = '';
/**
* Additional WHERE filter, that determines what records needs to be processed
*
* @var string
* @access protected
*/
protected $_constrain = '';
/**
* Automatically clone records from this table when parent table record is cloned
*
* @var bool
* @access protected
*/
protected $_autoClone = true;
/**
* Automatically delete records from this table when parent table record is deleted
*
* @var bool
* @access protected
*/
protected $_autoDelete = true;
/**
* List of sub-tables
*
* @var kTempHandlerSubTable[]
* @access protected
*/
protected $_subTables = Array ();
/**
* Window ID of current window
*
* @var int
* @access protected
*/
protected $_windowID = '';
/**
* Unit prefix
*
* @var string
* @access protected
*/
protected $_prefix = '';
/**
* IDs, that needs to be processed
*
* @var Array
* @access protected
*/
protected $_ids = Array ();
/**
* Table name-based 2-level array of cloned ids
*
* @static
* @var array
* @access protected
*/
static protected $_clonedIds = Array ();
/**
* IDs of newly cloned items (key - special, value - array of ids)
*
* @var Array
* @access protected
*/
protected $_savedIds = Array ();
/**
* ID field of associated db table
*
* @var string
* @access protected
*/
protected $_idField = '';
/**
* Name of associated db table
*
* @var string
* @access protected
*/
protected $_tableName = '';
/**
* State of the table
*
* @var int
* @access protected
*/
protected $_state = 0;
/**
* Tells that this is last usage of this table
*
* @var bool
* @access protected
*/
protected $_lastUsage = false;
/**
* Event, that was used to create this object
*
* @var kEvent
* @access protected
*/
protected $_parentEvent = null;
/**
* Creates table object
*
* @param string $prefix
* @param Array $ids
*/
public function __construct($prefix, $ids = Array ())
{
parent::__construct();
$this->_windowID = $this->Application->GetVar('m_wid');
$this->_prefix = $prefix;
$this->_ids = $ids;
if ( !$this->unitRegistered() ) {
return;
}
$this->_collectTableInfo();
}
/**
* Creates temp tables (recursively) and optionally fills them with data from live table
*
* @param Array $foreign_keys
* @return void
* @access public
*/
public function doCopyLiveToTemp($foreign_keys = Array ())
{
$parsed_prefix = $this->Application->processPrefix($this->_prefix);
$foreign_key_field = $this->_foreignKey ? $this->_foreignKey : $this->_idField;
if ( !is_numeric($parsed_prefix['special']) ) {
// TODO: find out what numeric specials are used for
if ( $this->_delete() ) {
$this->_create();
}
}
$foreign_keys = $this->_parseLiveIds($foreign_keys);
if ( $foreign_keys != '' && !$this->_inState(self::STATE_COPIED) ) {
// 1. copy data from live table into temp table
$sql = 'INSERT INTO ' . $this->_getTempTableName() . '
SELECT *
FROM ' . $this->_tableName . '
WHERE ' . $foreign_key_field . ' IN (' . $foreign_keys . ')';
$this->Conn->Query($this->_addConstrain($sql));
$this->_setAsCopied();
// 2. get ids, that were actually copied into temp table
$sql = 'SELECT ' . $this->_idField . '
FROM ' . $this->_tableName . '
WHERE ' . $foreign_key_field . ' IN (' . $foreign_keys . ')';
$copied_ids = $this->Conn->GetCol($this->_addConstrain($sql));
$this->_raiseEvent('OnAfterCopyToTemp', '', $copied_ids);
}
foreach ($this->_subTables as $sub_table) {
if ( !$sub_table->_parentTableKey ) {
continue;
}
if ( $foreign_keys != '' && $sub_table->_parentTableKey != $foreign_key_field ) {
// if sub-table isn't connected this this table by id field, then get foreign keys
$sql = 'SELECT ' . $sub_table->_parentTableKey . '
FROM ' . $this->_tableName . '
WHERE ' . $foreign_key_field . ' IN (' . $foreign_keys . ')';
$sub_foreign_keys = implode(',', $this->Conn->GetCol($sql));
}
else {
$sub_foreign_keys = $foreign_keys;
}
$sub_table->doCopyLiveToTemp($sub_foreign_keys);
}
}
/**
* Ensures, that ids are always a comma-separated string, that is ready to be used in SQLs
*
* @param Array|string $ids
* @return string
* @access protected
*/
protected function _parseLiveIds($ids)
{
if ( !$ids ) {
$ids = $this->_ids;
}
if ( is_array($ids) ) {
$ids = implode(',', $ids);
}
return $ids;
}
/**
* Copies data from temp to live table and returns IDs of copied records
*
* @param Array $current_ids
* @return Array
* @access public
*/
public function doCopyTempToLive($current_ids = Array())
{
$current_ids = $this->_parseTempIds($current_ids);
if ( $current_ids ) {
$this->_deleteFromLive($current_ids);
if ( $this->_subTables ) {
if ( $this->_inState(self::STATE_COPIED) || !$this->_lastUsage ) {
return Array ();
}
$this->_copyTempToLiveWithSubTables($current_ids);
}
elseif ( !$this->_inState(self::STATE_COPIED) && $this->_lastUsage ) {
// 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
$this->_copyTempToLiveWithoutSubTables($current_ids);
// no need to clear temp table - it will be dropped by next statement
}
}
if ( !$this->_lastUsage ) {
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->_delete();
$this->Application->resetCounters($this->_tableName);
return $this->getSavedIds();
}
/**
* Deletes unit db records along with related sub-items by id field
*
* @param string $prefix_special
* @param Array $ids
* @return void
* @access public
*/
public function doDeleteItems($prefix_special, $ids)
{
if ( !$ids ) {
return;
}
$object = $this->_getItem($prefix_special);
$parsed_prefix = $this->Application->processPrefix($prefix_special);
foreach ($ids as $id) {
$object->Load($id);
$original_values = $object->GetFieldValues();
if ( !$object->Delete($id) ) {
continue;
}
foreach ($this->_subTables as $sub_table) {
$sub_table->subDeleteItems($object, $parsed_prefix['special'], $original_values);
}
}
}
/**
* Clones item by id and it's sub-items by foreign key
*
* @param string $prefix_special
* @param Array $ids
* @param string $foreign_key
* @param bool $skip_filenames
* @return Array
* @access public
*/
public function doCloneItems($prefix_special, $ids, $foreign_key = null, $skip_filenames = false)
{
$object = $this->_getItem($prefix_special);
$object->PopulateMultiLangFields();
foreach ($ids as $id) {
$mode = 'create';
$cloned_ids = getArrayValue(self::$_clonedIds, $this->_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 ) {
$master = Array ('ForeignKey' => $this->_foreignKey, 'TableName' => $this->_tableName);
$object->NameCopy($master, $foreign_key);
}
elseif ( $object instanceof kCatDBItem ) {
$object->useFilenames = false;
}
if ( isset($foreign_key) ) {
$object->SetDBField($this->_foreignKey, $foreign_key);
}
if ( $mode == 'create' ) {
$this->_raiseEvent('OnBeforeClone', $object->Special, Array ($object->GetID()), $foreign_key);
}
$object->inCloning = true;
$res = $mode == 'update' ? $object->Update() : $object->Create();
$object->inCloning = false;
if ( $res ) {
if ( $mode == 'create' && $this->_multipleParents ) {
// remember original => clone mapping for dual ForeignKey updating
self::$_clonedIds[$this->_tableName][$id] = $object->GetID();
}
if ( $mode == 'create' ) {
$this->_raiseEvent('OnAfterClone', $object->Special, Array ($object->GetID()), $foreign_key, Array ('original_id' => $id));
$this->_saveId($object->Special, $object->GetID());
}
foreach ($this->_subTables as $sub_table) {
$sub_table->subCloneItems($object, $original_values);
}
}
}
return $this->getSavedIds($object->Special);
}
/**
* Returns item, associated with this table
*
* @param string $prefix_special
* @return kDBItem
* @access protected
*/
protected function _getItem($prefix_special)
{
// recalling by different name, because we may get kDBList, if we recall just by prefix
$parsed_prefix = $this->Application->processPrefix($prefix_special);
$recall_prefix = $parsed_prefix['prefix'] . '.' . preg_replace('/-item$/', '', $parsed_prefix['special']) . '-item';
$object = $this->Application->recallObject($recall_prefix, null, Array ('skip_autoload' => true, 'parent_event' => $this->_parentEvent));
/* @var $object kDBItem */
return $object;
}
/**
* Copies data from temp table that has sub-tables one-by-one record
*
* @param $temp_ids
* @return void
* @access protected
*/
protected function _copyTempToLiveWithSubTables($temp_ids)
{
$live_ids = Array ();
foreach ($temp_ids as $index => $temp_id) {
$this->_raiseEvent('OnBeforeCopyToLive', '', Array ($temp_id));
list ($new_temp_id, $live_id) = $this->_copyOneTempID($temp_id);
$live_ids[$index] = $live_id;
$this->_saveId('', Array ($temp_id => $live_id));
$this->_raiseEvent('OnAfterCopyToLive', '', Array ($temp_id => $live_id));
$this->_updateChangeLogForeignKeys($live_id, $temp_id);
foreach ($this->_subTables as $sub_table) {
$sub_table->subUpdateForeignKeys($live_id, $temp_id);
}
// delete only after sub-table foreign key update !
$this->_deleteOneTempID($new_temp_id);
}
$this->_setAsCopied();
// when all of ids in current master has been processed, copy all sub-tables data
foreach ($this->_subTables as $sub_table) {
$sub_table->subCopyToLive($live_ids, $temp_ids);
}
}
/**
* Copies data from temp table that has no sub-tables all records together
*
* @param $temp_ids
* @return void
* @access protected
*/
protected function _copyTempToLiveWithoutSubTables($temp_ids)
{
$live_ids = Array ();
$this->_raiseEvent('OnBeforeCopyToLive', '', $temp_ids);
foreach ($temp_ids as $temp_id) {
if ( $temp_id > 0 ) {
$live_ids[$temp_id] = $temp_id;
// positive ids (already live) will be copied together below
continue;
}
// copy negative IDs (exists only in temp) one-by-one
list ($new_temp_id, $live_id) = $this->_copyOneTempID($temp_id);
$live_ids[$temp_id] = $live_id;
$this->_updateChangeLogForeignKeys($live_ids[$temp_id], $temp_id);
$this->_deleteOneTempID($new_temp_id);
}
// copy ALL records with positive ids (since negative ids were processed above) to live table
$sql = 'INSERT INTO ' . $this->_tableName . '
SELECT *
FROM ' . $this->_getTempTableName();
$this->Conn->Query($this->_addConstrain($sql));
$this->_saveId('', $live_ids);
$this->_raiseEvent('OnAfterCopyToLive', '', $live_ids);
$this->_setAsCopied();
}
/**
* Copies one record with 0/negative ID from temp to live table to obtain it's live auto-increment id
*
* @param int $temp_id
* @return Array Pair of temp id and live id
* @access protected
*/
protected function _copyOneTempID($temp_id)
{
$copy_id = $temp_id;
if ( $temp_id < 0 ) {
$sql = 'UPDATE ' . $this->_getTempTableName() . '
SET ' . $this->_idField . ' = 0
WHERE ' . $this->_idField . ' = ' . $temp_id;
$this->Conn->Query($this->_addConstrain($sql));
$copy_id = 0;
}
$sql = 'INSERT INTO ' . $this->_tableName . '
SELECT *
FROM ' . $this->_getTempTableName() . '
WHERE ' . $this->_idField . ' = ' . $copy_id;
$this->Conn->Query($sql);
return Array ($copy_id, $copy_id == 0 ? $this->Conn->getInsertID() : $copy_id);
}
/**
* Delete already copied record from master temp table
*
* @param int $temp_id
* @return void
* @access protected
*/
protected function _deleteOneTempID($temp_id)
{
$sql = 'DELETE FROM ' . $this->_getTempTableName() . '
WHERE ' . $this->_idField . ' = ' . $temp_id;
$this->Conn->Query($this->_addConstrain($sql));
}
/**
* Deletes records from live table
*
* @param $ids
* @return void
* @access protected
*/
abstract protected function _deleteFromLive($ids);
/**
* Ensures, that ids are always an array
*
* @param Array $ids
* @return Array
* @access protected
*/
protected function _parseTempIds($ids)
{
if ( !$ids ) {
$sql = 'SELECT ' . $this->_idField . '
FROM ' . $this->_getTempTableName();
$ids = $this->Conn->GetCol($this->_addConstrain($sql));
}
return $ids;
}
/**
* Sets new parent event to the object
*
* @param kEvent $event
* @return void
* @access public
*/
public function setParentEvent(kEvent $event)
{
$this->_parentEvent = $event;
$this->_top()->_drillDown($this, 'setParentEvent');
}
/**
* Collects information about table
*
* @return void
* @access protected
*/
protected function _collectTableInfo()
{
$config = $this->Application->getUnitConfig($this->_prefix);
$this->_idField = $config->getIDField();
$this->_tableName = $config->getTableName();
$this->_foreignKey = $config->getForeignKey();
$this->_parentTableKey = $config->getParentTableKey();
$this->_constrain = $config->getConstrain('');
$this->_autoClone = $config->getAutoClone();
$this->_autoDelete = $config->getAutoDelete();
}
/**
* Discovers and adds sub-tables to this table
*
* @return void
* @access protected
* @throws InvalidArgumentException
*/
protected function _addSubTables()
{
$sub_items = $this->Application->getUnitConfig($this->_prefix)->getSubItems(Array ());
if ( !is_array($sub_items) ) {
throw new InvalidArgumentException('TempHandler: SubItems property in unit config must be an array');
}
foreach ($sub_items as $sub_item_prefix) {
$this->add(new kTempHandlerSubTable($sub_item_prefix));
}
}
/**
* Adds new sub-table
*
* @param kTempHandlerSubTable $table
* @return void
* @access public
*/
public function add(kTempHandlerSubTable $table)
{
if ( !$table->unitRegistered() ) {
trigger_error('TempHandler: unit "' . $table->_prefix . '" not registered', E_USER_WARNING);
return ;
}
$this->_subTables[] = $table;
$table->setParent($this);
}
/**
* Finds sub-table by prefix.
*
* @param string $prefix Unit config prefix.
*
* @return kTempHandlerSubTable
*/
public function get($prefix)
{
if ( $this->_prefix == $prefix ) {
return $this;
}
foreach ( $this->_subTables as $sub_table ) {
$found_table = $sub_table->get($prefix);
if ( $found_table !== null ) {
return $found_table;
}
}
return null;
}
/**
* Sets parent table
*
* @param kTempHandlerTable $parent
* @return void
* @access public
*/
public function setParent(kTempHandlerTable $parent)
{
$this->_parent = $parent;
if ( is_array($this->_foreignKey) ) {
$this->_multipleParents = true;
$this->_foreignKey = $this->_foreignKey[$parent->_prefix];
}
if ( is_array($this->_parentTableKey) ) {
$this->_parentTableKey = $this->_parentTableKey[$parent->_prefix];
}
$this->_setAsLastUsed();
$this->_addSubTables();
}
/**
* Returns unit prefix
*
* @return string
* @access public
*/
public function getPrefix()
{
return $this->_prefix;
}
/**
* Determines if unit used to create table exists
*
* @return bool
* @access public
*/
public function unitRegistered()
{
return $this->Application->prefixRegistred($this->_prefix);
}
/**
* Returns topmost table
*
* @return kTempHandlerTopTable
* @access protected
*/
protected function _top()
{
$top = $this;
while ( is_object($top->_parent) ) {
$top = $top->_parent;
}
return $top;
}
/**
* Performs given operation on current table and all it's sub-tables
*
* @param kTempHandlerTable $table
* @param string $operation
* @param bool $same_table
* @param bool $same_constrain
* @return void
* @access protected
*/
protected function _drillDown(kTempHandlerTable $table, $operation, $same_table = false, $same_constrain = false)
{
$table_match = $same_table ? $this->_tableName == $table->_tableName : true;
$constrain_match = $same_constrain ? $this->_constrain == $table->_constrain : true;
if ( $table_match && $constrain_match ) {
switch ( $operation ) {
case 'state:copied':
$this->_addState(self::STATE_COPIED);
break;
case 'state:deleted':
$this->_addState(self::STATE_DELETED);
break;
case 'setParentEvent':
$this->_parentEvent = $table->_parentEvent;
break;
case 'resetLastUsed':
$this->_lastUsage = false;
break;
}
}
foreach ($this->_subTables as $sub_table) {
$sub_table->_drillDown($table, $operation, $same_table, $same_constrain);
}
}
/**
* Marks this instance of a table as it's last usage
*
* @return void
* @access protected
*/
protected function _setAsLastUsed()
{
$this->_top()->_drillDown($this, 'resetLastUsed', true, true);
$this->_lastUsage = true;
}
/**
* Marks table and all it's clones as copied
*
* @return void
* @access protected
*/
protected function _setAsCopied()
{
$this->_top()->_drillDown($this, 'state:copied', true, true);
}
/**
* Update foreign key columns after new ids were assigned instead of temporary ids in change log
*
* @param int $live_id
* @param int $temp_id
*/
function _updateChangeLogForeignKeys($live_id, $temp_id)
{
if ( $live_id == $temp_id ) {
return;
}
$main_prefix = $this->Application->GetTopmostPrefix($this->_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'] == $this->_prefix && $rec['ItemId'] == $temp_id ) {
// main item change log record
$changes[$key]['ItemId'] = $live_id;
}
if ( $rec['MasterPrefix'] == $this->_prefix && $rec['MasterId'] == $temp_id ) {
// sub item change log record
$changes[$key]['MasterId'] = $live_id;
}
if ( in_array($this->_prefix, $rec['ParentPrefix']) && $rec['ParentId'][$this->_prefix] == $temp_id ) {
// parent item change log record
$changes[$key]['ParentId'][$this->_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
$config = $this->Application->getUnitConfig($rec['Prefix']);
$parent_table_key = $config->getParentTableKey($this->_prefix);
if ( $parent_table_key == $this->_idField ) {
$foreign_key = $config->getForeignKey($this->_prefix);
$changes[$key]['DependentFields'][$foreign_key] = $live_id;
}
}
}
}
$this->Application->StoreVar($ses_var_name, serialize($changes));
}
/**
* Returns foreign key pairs for given ids and $sub_table
*
* USE: MainTable
*
* @param kTempHandlerSubTable $sub_table
* @param int|Array $live_id
* @param int|Array $temp_id
* @return Array
* @access protected
*/
protected function _getForeignKeys(kTempHandlerSubTable $sub_table, $live_id, $temp_id = null)
{
$single_mode = false;
if ( !is_array($live_id) ) {
$single_mode = true;
$live_id = Array ($live_id);
}
if ( isset($temp_id) && !is_array($temp_id) ) {
$temp_id = Array ($temp_id);
}
$cache_key = serialize($live_id);
$parent_key_field = $sub_table->_parentTableKey ? $sub_table->_parentTableKey : $this->_idField;
$cached = getArrayValue($this->_foreignKeyCache, $parent_key_field);
if ( $cached ) {
if ( array_key_exists($cache_key, $cached) ) {
list($live_foreign_key, $temp_foreign_key) = $cached[$cache_key];
return $single_mode ? Array ($live_foreign_key[0], $temp_foreign_key[0]) : $live_foreign_key;
}
}
if ( $parent_key_field != $this->_idField ) {
$sql = 'SELECT ' . $parent_key_field . '
FROM ' . $this->_tableName . '
WHERE ' . $this->_idField . ' IN (' . implode(',', $live_id) . ')';
$live_foreign_key = $this->Conn->GetCol($sql);
if ( isset($temp_id) ) {
// because doCopyTempToLive resets negative IDs to 0 in temp table (one by one) before copying to live
$temp_key = $temp_id < 0 ? 0 : $temp_id;
$sql = 'SELECT ' . $parent_key_field . '
FROM ' . $this->_getTempTableName() . '
WHERE ' . $this->_idField . ' IN (' . implode(',', $temp_key) . ')';
$temp_foreign_key = $this->Conn->GetCol($sql);
}
else {
$temp_foreign_key = Array ();
}
}
else {
$live_foreign_key = $live_id;
$temp_foreign_key = $temp_id;
}
$this->_foreignKeyCache[$parent_key_field][$cache_key] = Array ($live_foreign_key, $temp_foreign_key);
if ( $single_mode ) {
return Array ($live_foreign_key[0], $temp_foreign_key[0]);
}
return $live_foreign_key;
}
/**
* Adds constrain to given sql
*
* @param $sql
* @return string
* @access protected
*/
protected function _addConstrain($sql)
{
if ( $this->_constrain ) {
$sql .= ' AND ' . $this->_constrain;
}
return $sql;
}
/**
* Creates temp table
* Don't use CREATE TABLE ... LIKE because it also copies indexes
*
* @return void
* @access protected
*/
protected function _create()
{
$sql = 'CREATE TABLE ' . $this->_getTempTableName() . '
SELECT *
FROM ' . $this->_tableName . '
WHERE 0';
$this->Conn->Query($sql);
}
/**
* Deletes temp table
*
* @return bool
* @access protected
*/
protected function _delete()
{
if ( $this->_inState(self::STATE_DELETED) ) {
return false;
}
$sql = 'DROP TABLE IF EXISTS ' . $this->_getTempTableName();
$this->Conn->Query($sql);
$this->_top()->_drillDown($this, 'state:deleted', true);
return true;
}
/**
* Deletes table and all it's sub-tables
*
* @return void
* @access public
*/
public function deleteAll()
{
$this->_delete();
foreach ($this->_subTables as $sub_table) {
$sub_table->deleteAll();
}
}
/**
* Returns temp table name for current table
*
* @return string
* @access protected
*/
protected function _getTempTableName()
{
return $this->Application->GetTempName($this->_tableName, $this->_windowID);
}
/**
* Adds table state
*
* @param int $state
* @return kTempHandlerTable
* @access protected
*/
protected function _addState($state)
{
$this->_state |= $state;
return $this;
}
/**
* Removes table state
*
* @param int $state
* @return kTempHandlerTable
* @access protected
*/
protected function _removeState($state)
{
$this->_state = $this->_state &~ $state;
return $this;
}
/**
* Checks that table has given state
*
* @param int $state
* @return bool
* @access protected
*/
protected function _inState($state)
{
return ($this->_state & $state) == $state;
}
/**
* Saves id for later usage
*
* @param string $special
* @param int|Array $id
* @return void
* @access protected
*/
protected function _saveId($special = '', $id = null)
{
if ( !isset($this->_savedIds[$special]) ) {
$this->_savedIds[$special] = Array ();
}
if ( is_array($id) ) {
foreach ($id as $tmp_id => $live_id) {
$this->_savedIds[$special][$tmp_id] = $live_id;
}
}
else {
$this->_savedIds[$special][] = $id;
}
}
/**
* Returns saved ids for given special.
*
* @param string $special Special.
*
* @return array
*/
public function getSavedIds($special = '')
{
return isset($this->_savedIds[$special]) ? $this->_savedIds[$special] : Array ();
}
/**
* Raises event using IDs, that are currently being processed in temp handler
*
* @param string $name
* @param string $special
* @param Array $ids
* @param string $foreign_key
* @param Array $add_params
* @return bool
* @access protected
*/
protected function _raiseEvent($name, $special, $ids, $foreign_key = null, $add_params = null)
{
if ( !is_array($ids) ) {
return true;
}
$event = new kEvent($this->_prefix . ($special ? '.' : '') . $special . ':' . $name);
$event->MasterEvent = $this->_parentEvent;
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;
}
}
/**
* Represents topmost table, that has related tables inside it
*
* Pattern: Composite
*/
class kTempHandlerTopTable extends kTempHandlerTable {
/**
* Creates table object
*
* @param string $prefix
* @param Array $ids
*/
public function __construct($prefix, $ids = Array ())
{
parent::__construct($prefix, $ids);
+ // editing sub-item, linked to multiple parents directly
+ if ( is_array($this->_foreignKey) ) {
+ $this->_multipleParents = true;
+ $this->_foreignKey = reset($this->_foreignKey);
+ }
+
+ if ( is_array($this->_parentTableKey) ) {
+ $this->_parentTableKey = reset($this->_parentTableKey);
+ }
+
$this->_setAsLastUsed();
$this->_addSubTables();
}
/**
* Checks, that someone is editing selected records and returns true, when no one.
*
* @param Array $ids
* @return bool
* @access public
*/
public function checkSimultaneousEdit($ids = null)
{
if ( !$this->Application->getUnitConfig($this->_prefix)->getCheckSimulatniousEdit() ) {
return true;
}
$tables = $this->Conn->GetCol('SHOW TABLES');
$mask_edit_table = '/' . TABLE_PREFIX . 'ses_(.*)_edit_' . $this->_tableName . '$/';
$my_sid = $this->Application->GetSID();
$my_wid = $this->Application->GetVar('m_wid');
$ids = implode(',', isset($ids) ? $ids : $this->_ids);
$sids = Array ();
if ( !$ids ) {
return true;
}
foreach ($tables as $table) {
if ( !preg_match($mask_edit_table, $table, $regs) ) {
continue;
}
// remove popup's wid from sid
$sid = preg_replace('/(.*)_(.*)/', '\\1', $regs[1]);
if ( $sid == $my_sid ) {
if ( $my_wid ) {
// using popups for editing
if ( preg_replace('/(.*)_(.*)/', '\\2', $regs[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->_idField . ')
FROM ' . $table . '
WHERE ' . $this->_idField . ' IN (' . $ids . ')';
$found = $this->Conn->GetOne($sql);
if ( !$found || in_array($sid, $sids) ) {
continue;
}
$sids[] = $sid;
}
if ( !$sids ) {
return true;
}
// detect who is it
$sql = 'SELECT CONCAT(
(CASE s.PortalUserId WHEN ' . USER_ROOT . ' THEN "root" WHEN ' . USER_GUEST . ' THEN "Guest" ELSE CONCAT(u.FirstName, " ", u.LastName, " (", u.Username, ")") END),
" 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('_simultaneous_edit_message', sprintf($this->Application->Phrase('la_record_being_edited_by'), implode(",\n", $users)));
return false;
}
return true;
}
/**
* Deletes records from live table
*
* @param $ids
* @return void
* @access protected
*/
protected function _deleteFromLive($ids)
{
if ( !$this->_raiseEvent('OnBeforeDeleteFromLive', '', $ids) ) {
return;
}
$sql = 'DELETE FROM ' . $this->_tableName . '
WHERE ' . $this->_idField . ' IN (' . implode(',', $ids) . ')';
$this->Conn->Query($sql);
}
}
/**
* Represents sub table, that has related tables inside it
*
* Pattern: Composite
*/
class kTempHandlerSubTable extends kTempHandlerTable {
/**
* Deletes records from live table
*
* @param $ids
* @return void
* @access protected
*/
protected function _deleteFromLive($ids)
{
// for sub-tables records get deleted in "subCopyToLive" method !BY Foreign Key!
}
/**
* Copies sub-table contents to live
*
* @param Array $live_ids
* @param Array $temp_ids
* @return void
* @access public
*/
public function subCopyToLive($live_ids, $temp_ids)
{
// delete records from live table by foreign key, so that records deleted from temp table
// get deleted from live
if ( $temp_ids && !$this->_inState(self::STATE_COPIED) ) {
if ( !$this->_foreignKey ) {
return;
}
$foreign_keys = $this->_parent->_getForeignKeys($this, $live_ids, $temp_ids);
if ( count($foreign_keys) > 0 ) {
$sql = 'SELECT ' . $this->_idField . '
FROM ' . $this->_tableName . '
WHERE ' . $this->_foreignKey . ' IN (' . implode(',', $foreign_keys) . ')';
$ids = $this->Conn->GetCol($this->_addConstrain($sql));
if ( $this->_raiseEvent('OnBeforeDeleteFromLive', '', $ids, $foreign_keys) ) {
$sql = 'DELETE FROM ' . $this->_tableName . '
WHERE ' . $this->_foreignKey . ' IN (' . implode(',', $foreign_keys) . ')';
$this->Conn->Query($this->_addConstrain($sql));
}
}
}
// sub_table passed here becomes master in the method, and recursively updated and copy its sub tables
$this->doCopyTempToLive();
}
/**
* Deletes unit db records and it's sub-items by foreign key
*
* @param kDBItem $object
* @param string $special
* @param Array $original_values
* @return void
* @access public
*/
public function subDeleteItems(kDBItem $object, $special, $original_values)
{
if ( !$this->_autoDelete || !$this->_foreignKey || !$this->_parentTableKey ) {
return;
}
$table_name = $object->IsTempTable() ? $this->_getTempTableName() : $this->_tableName;
$sql = 'SELECT ' . $this->_idField . '
FROM ' . $table_name . '
WHERE ' . $this->_foreignKey . ' = ' . $original_values[$this->_parentTableKey];
$sub_ids = $this->Conn->GetCol($sql);
$this->doDeleteItems($this->_prefix .'.' . $special, $sub_ids);
}
/**
* Clones unit db records and it's sub-items by foreign key
*
* @param kDBItem $object
* @param Array $original_values
* @return void
* @access public
*/
public function subCloneItems(kDBItem $object, $original_values)
{
if ( !$this->_autoClone || !$this->_foreignKey || !$this->_parentTableKey ) {
return;
}
$table_name = $object->IsTempTable() ? $this->_getTempTableName() : $this->_tableName;
$sql = 'SELECT ' . $this->_idField . '
FROM ' . $table_name . '
WHERE ' . $this->_foreignKey . ' = ' . $original_values[$this->_parentTableKey];
$sub_ids = $this->Conn->GetCol($this->_addConstrain($sql));
if ( $this->_multipleParents ) {
// $sub_ids could contain newly cloned items, we need to remove it here to escape double cloning
$cloned_ids = getArrayValue(self::$_clonedIds, $this->_tableName);
if ( !$cloned_ids ) {
$cloned_ids = Array ();
}
$sub_ids = array_diff($sub_ids, array_values($cloned_ids));
}
$parent_key = $object->GetDBField($this->_parentTableKey);
$this->doCloneItems($this->_prefix . '.' . $object->Special, $sub_ids, $parent_key);
}
/**
* Update foreign key columns after new ids were assigned instead of temporary ids in db
*
* @param int $live_id
* @param int $temp_id
* @return void
* @access public
*/
public function subUpdateForeignKeys($live_id, $temp_id)
{
if ( !$this->_foreignKey ) {
return;
}
list ($live_foreign_key, $temp_foreign_key) = $this->_parent->_getForeignKeys($this, $live_id, $temp_id);
// update ForeignKey in temporary sub-table
if ( $live_foreign_key == $temp_foreign_key ) {
return;
}
$sql = 'UPDATE ' . $this->_getTempTableName() . '
SET ' . $this->_foreignKey . ' = ' . $live_foreign_key . '
WHERE ' . $this->_foreignKey . ' = ' . $temp_foreign_key;
$this->Conn->Query($this->_addConstrain($sql));
}
}
\ No newline at end of file

Event Timeline