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