Index: core/kernel/utility/temp_handler.php IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- core/kernel/utility/temp_handler.php (revision 15698) +++ core/kernel/utility/temp_handler.php (revision ) @@ -1,6 +1,6 @@ parentEvent = $event; + + if ( is_object($this->_tables) ) { + $this->_tables->setParentEvent($event); - } + } + } - function SetTables($tables) + /** + * Scans table structure of given unit + * + * @param string $prefix + * @param Array $ids + * @return void + * @access public + */ + public function BuildTables($prefix, $ids) { - // set table name as key for tables array - $this->Tables = $tables; - $this->MasterTable = $tables['TableName']; + $this->_tables = new kTempHandlerTopTable($prefix, $ids); + $this->_tables->setParentEvent($this->parentEvent); } - function saveID($prefix, $special = '', $id = null) + /** + * 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() { - if (!isset($this->savedIDs[$prefix.($special ? '.' : '').$special])) { - $this->savedIDs[$prefix.($special ? '.' : '').$special] = array(); + $this->_tables->doCopyLiveToTemp(); + $this->_tables->checkSimultaneousEdit(); - } + } - 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 + * Deletes temp tables without copying their data back to live tables * - * @param string $table - * @return string + * @return void + * @access public */ - function GetTempName($table) + public function CancelEdit() { - return $this->Application->GetTempName($table, $this->WindowID); + $this->_tables->deleteAll(); } - function GetTempTablePrefix() + /** + * Saves changes made in temp tables to live tables + * + * @param Array $master_ids + * @return bool + * @access public + */ + public function SaveEdit($master_ids = Array()) { - return $this->Application->GetTempTablePrefix($this->WindowID); + // 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' => adodb_mktime(), + '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; + } + /** - * Return live table name based on temp table name + * Deletes unit data for given items along with related sub-items * - * @param string $temp_table - * @return string + * @param string $prefix + * @param string $special + * @param Array $ids + * @throws InvalidArgumentException */ - function GetLiveName($temp_table) + function DeleteItems($prefix, $special, $ids) { - return $this->Application->GetLiveName($temp_table); + if ( strpos($prefix, '.') !== false ) { + throw new InvalidArgumentException("Pass prefix and special as separate arguments"); - } + } - function IsTempTable($table) - { - return $this->Application->IsTempTable($table); + if ( !is_array($ids) ) { + throw new InvalidArgumentException('Incorrect ids format'); - } + } + $this->_tables->doDeleteItems(rtrim($prefix . '.' . $special, '.'), $ids); + } + /** - * Return temporary table name for master table + * Clones given ids * - * @return string - * @access public + * @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 GetMasterTempName() + function CloneItems($prefix, $special, $ids, $master = null, $foreign_key = null, $parent_prefix = null, $skip_filenames = false) { - return $this->GetTempName($this->MasterTable); + return $this->_tables->doCloneItems($prefix . '.' . $special, $ids, $foreign_key, $skip_filenames); } - function CreateTempTable($table) + /** + * Create separate connection for locking purposes + * + * @return kDBConnection + * @access protected + */ + protected function _getSeparateConnection() { - $sql = 'CREATE TABLE ' . $this->GetTempName($table) . ' - SELECT * - FROM ' . $table . ' - WHERE 0'; + static $connection = null; - $this->Conn->Query($sql); + 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, true); - } + } - function BuildTables($prefix, $ids) - { - $this->TableIdCounter = 0; - $this->WindowID = $this->Application->GetVar('m_wid'); + return $connection; + } +} - $config = $this->Application->getUnitConfig($prefix); +/** + * Base class, that represents one table + * + * Pattern: Composite + */ +abstract class kTempHandlerTable extends kBase { - $tables = Array( - 'TableName' => $config->getTableName(), - 'IdField' => $config->getIDField(), - 'IDs' => $ids, - 'Prefix' => $prefix, - 'TableId' => $this->TableIdCounter++, - ); + /** + * Temp table was created from live table OR it was copied back to live table + */ + const STATE_COPIED = 1; - /*$config = $this->Application->getUnitConfig($prefix); - $parent_prefix = $config->getParentPrefix(); + /** + * Temp table was deleted + */ + const STATE_DELETED = 2; - if ( $parent_prefix ) { - $tables['ForeignKey'] = $config->getForeignKey(); - $tables['ParentPrefix'] = $parent_prefix; - $tables['ParentTableKey'] = $config->getParentTableKey(); - }*/ + /** + * Reference to parent table + * + * @var kTempHandlerTable + * @access protected + */ + protected $_parent; - $this->FinalRefs[ $tables['TableName'] ] = $tables['TableId']; // don't forget to add main table to FinalRefs too + /** + * Field in this db table, that holds ID from it's parent table + * + * @var string + * @access protected + */ + protected $_foreignKey = ''; - $sub_items = $this->Application->getUnitConfig($prefix)->getSubItems(Array ()); + /** + * This table is connected to multiple parent tables + * + * @var bool + * @access protected + */ + protected $_multipleParents = false; - if ( is_array($sub_items) ) { - foreach ($sub_items as $prefix) { - $this->AddTables($prefix, $tables); + /** + * 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 Array + * @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->SetTables($tables); + $this->_collectTableInfo(); } /** - * Searches through TempHandler tables info for required prefix + * Creates temp tables (recursively) and optionally fills them with data from live table * - * @param string $prefix - * @param Array $master - * @return mixed + * @param Array $foreign_keys + * @return void + * @access public */ - function SearchTable($prefix, $master = null) + public function doCopyLiveToTemp($foreign_keys = Array ()) { - if (is_null($master)) { - $master = $this->Tables; + $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(); - } + } + } - if ($master['Prefix'] == $prefix) { - return $master; + $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); } - if (isset($master['SubTables'])) { - foreach ($master['SubTables'] as $sub_table) { - $found = $this->SearchTable($prefix, $sub_table); - if ($found !== false) { - return $found; + /* @var $sub_table kTempHandlerSubTable */ + 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; - } + } - return false; + $sub_table->doCopyLiveToTemp($sub_foreign_keys); - } + } + } - function AddTables($prefix, &$tables) + /** + * 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 ( !$this->Application->prefixRegistred($prefix) ) { - // allows to skip subitem processing if subitem module not enabled/installed - return ; + if ( !$ids ) { + $ids = $this->_ids; } - $config = $this->Application->getUnitConfig($prefix); + if ( is_array($ids) ) { + $ids = implode(',', $ids); + } - $tmp = Array( - 'TableName' => $config->getTableName(), - 'IdField' => $config->getIDField(), - 'ForeignKey' => $config->getForeignKey(), - 'ParentPrefix' => $config->getParentPrefix(), - 'ParentTableKey' => $config->getParentTableKey(), - 'Prefix' => $prefix, - 'AutoClone' => $config->getAutoClone(), - 'AutoDelete' => $config->getAutoDelete(), - 'TableId' => $this->TableIdCounter++, - ); + return $ids; + } - $this->FinalRefs[ $tmp['TableName'] ] = $tmp['TableId']; + /** + * 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); - $constrain = $config->getConstrain(); + if ( $current_ids ) { + $this->_deleteFromLive($current_ids); - if ( $constrain ) { - $tmp['Constrain'] = $constrain; - $this->FinalRefs[ $tmp['TableName'] . $tmp['Constrain'] ] = $tmp['TableId']; + if ( $this->_subTables ) { + if ( $this->_inState(self::STATE_COPIED) || !$this->_lastUsage ) { + return Array (); - } + } - $sub_items = $config->getSubItems(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 - if ( is_array($sub_items) ) { - foreach ($sub_items as $prefix) { - $this->AddTables($prefix, $tmp); + $this->_copyTempToLiveWithoutSubTables($current_ids); + + // no need to clear temp table - it will be dropped by next statement } } - if ( !is_array(getArrayValue($tables, 'SubTables')) ) { - $tables['SubTables'] = Array (); + if ( !$this->_lastUsage ) { + return Array (); } - $tables['SubTables'][] = $tmp; + /*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) - } + } + }*/ - function CloneItems($prefix, $special, $ids, $master = null, $foreign_key = null, $parent_prefix = null, $skip_filenames = false) + $this->_delete(); + $this->Application->resetCounters($this->_tableName); + + return isset($this->_savedIds['']) ? $this->_savedIds[''] : Array (); + } + + /** + * 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 (!isset($master)) $master = $this->Tables; + if ( !$ids ) { + return; + } - // recalling by different name, because we may get kDBList, if we recall just by prefix - if (!preg_match('/(.*)-item$/', $special)) { - $special .= '-item'; + $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; - } + } - $object = $this->Application->recallObject($prefix.'.'.$special, $prefix, Array('skip_autoload' => true, 'parent_event' => $this->parentEvent)); - /* @var $object kCatDBItem */ + /* @var $sub_table kTempHandlerSubTable */ + 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($this->AlreadyProcessed, $master['TableName']); + $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 @@ -301,487 +598,454 @@ $object->Load($id); $original_values = $object->GetFieldValues(); - if (!$skip_filenames) { + if ( !$skip_filenames ) { + $master = Array ('ForeignKey' => $this->_foreignKey, 'TableName' => $this->_tableName); $object->NameCopy($master, $foreign_key); } - elseif ($master['TableName'] == $this->MasterTable) { - // kCatDBItem class only has this attribute + elseif ( $object instanceof kCatDBItem ) { $object->useFilenames = false; } - if (isset($foreign_key)) { + 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); + $object->SetDBField($this->_foreignKey, $foreign_key); } - if ($mode == 'create') { + if ( $mode == 'create' ) { - $this->RaiseEvent('OnBeforeClone', $master['Prefix'], $special, Array($object->GetId()), $foreign_key); + $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' && is_array( getArrayValue($master, 'ForeignKey')) ) { + if ( $res ) { + if ( $mode == 'create' && $this->_multipleParents ) { // remember original => clone mapping for dual ForeignKey updating - $this->AlreadyProcessed[$master['TableName']][$id] = $object->GetId(); + self::$_clonedIds[$this->_tableName][$id] = $object->GetID(); } - if ($mode == 'create') { + 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()); + $this->_raiseEvent('OnAfterClone', $object->Special, Array ($object->GetID()), $foreign_key, Array ('original_id' => $id)); + $this->_saveId($object->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); + /* @var $sub_table kTempHandlerSubTable */ + foreach ($this->_subTables as $sub_table) { + $sub_table->subCloneItems($object, $original_values); - } + } - - $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 isset($this->_savedIds[$object->Special]) ? $this->_savedIds[$object->Special] : Array (); - } + } - return $this->savedIDs[$prefix.($special ? '.' : '').$special]; - } - - function DeleteItems($prefix, $special, $ids, $master=null, $foreign_key=null) + /** + * Returns item, associated with this table + * + * @param string $prefix_special + * @return kDBItem + * @access protected + */ + protected function _getItem($prefix_special) { - if ( !$ids ) { - return; - } + // 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'; - if ( !isset($master) ) { - $master = $this->Tables; - } + $object = $this->Application->recallObject($recall_prefix, null, Array ('skip_autoload' => true, 'parent_event' => $this->_parentEvent)); + /* @var $object kDBItem */ - if ( strpos($prefix, '.') !== false ) { - list($prefix, $special) = explode('.', $prefix, 2); + return $object; - } + } - $prefix_special = rtrim($prefix . '.' . $special, '.'); + /** + * 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) + { + /* @var $sub_table kTempHandlerSubTable */ - //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 */ + $live_ids = Array (); - foreach ($ids as $id) { - $object->Load($id); - $original_values = $object->GetFieldValues(); + foreach ($temp_ids as $index => $temp_id) { + $this->_raiseEvent('OnBeforeCopyToLive', '', Array ($temp_id)); - if ( !$object->Delete($id) ) { - continue; - } + list ($new_temp_id, $live_id) = $this->_copyOneTempID($temp_id); + $live_ids[$index] = $live_id; - if ( is_array(getArrayValue($master, 'SubTables')) ) { - foreach ($master['SubTables'] as $sub_table) { - if ( !getArrayValue($sub_table, 'AutoDelete') ) { - continue; - } + $this->_saveId('', Array ($temp_id => $live_id)); + $this->_raiseEvent('OnAfterCopyToLive', '', Array ($temp_id => $live_id)); - $sub_TableName = $object->IsTempTable() ? $this->GetTempName($sub_table['TableName']) : $sub_table['TableName']; + $this->_updateChangeLogForeignKeys($live_id, $temp_id); - $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']; + foreach ($this->_subTables as $sub_table) { + $sub_table->subUpdateForeignKeys($live_id, $temp_id); + } - if ( !$foreign_key_field || !$parent_key_field ) { - continue; + // delete only after sub-table foreign key update ! + $this->_deleteOneTempID($new_temp_id); - } + } - $sql = 'SELECT ' . $sub_table['IdField'] . ' - FROM ' . $sub_TableName . ' - WHERE ' . $foreign_key_field . ' = ' . $original_values[$parent_key_field]; - $sub_ids = $this->Conn->GetCol($sql); + $this->_setAsCopied(); - $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); + // 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); - } - } + } + } - } - } - - function DoCopyLiveToTemp($master, $ids, $parent_prefix=null) + /** + * 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) { - // 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']); - } - } + $live_ids = Array (); + $this->_raiseEvent('OnBeforeCopyToLive', '', $temp_ids); - if (is_array($ids)) { - $ids = join(',', $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; - } + } - $table_sig = $master['TableName'].(isset($master['Constrain']) ? $master['Constrain'] : ''); + // 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; - if ($ids != '' && !in_array($table_sig, $this->CopiedTables)) { - if ( getArrayValue($master, 'ForeignKey') ) { - if ( is_array($master['ForeignKey']) ) { - $key_field = $master['ForeignKey'][$parent_prefix]; + $this->_updateChangeLogForeignKeys($live_ids[$temp_id], $temp_id); + $this->_deleteOneTempID($new_temp_id); - } + } - 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); + // 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->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) ); + $this->_saveId('', $live_ids); + $this->_raiseEvent('OnAfterCopyToLive', '', $live_ids); + $this->_setAsCopied(); - } + } - if ( getArrayValue($master, 'SubTables') ) { - foreach ($master['SubTables'] as $sub_table) { + /** + * 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; - $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)); + 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; - } + } - 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); + $sql = 'INSERT INTO ' . $this->_tableName . ' + SELECT * + FROM ' . $this->_getTempTableName() . ' + WHERE ' . $this->_idField . ' = ' . $copy_id; + $this->Conn->Query($sql); - if ( isset($sub_table['ParentTableKey']) ) { - if ( is_array($sub_table['ParentTableKey']) ) { - $parent_key_field = $sub_table['ParentTableKey'][$master['Prefix']]; + return Array ($copy_id, $copy_id == 0 ? $this->Conn->getInsertID() : $copy_id); - } + } - 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; + /** + * 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)); - } + } - 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); + /** + * Deletes records from live table + * + * @param $ids + * @return void + * @access protected + */ + abstract protected function _deleteFromLive($ids); - 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); + /** + * 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)); - } + } - 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; + return $ids; - } + } - else { - return Array($live_foreign_key[0], $temp_foreign_key[0]); - } - } /** - * Copies data from temp to live table and returns IDs of copied records + * Sets new parent event to the object * - * @param Array $master - * @param string $parent_prefix - * @param Array $current_ids - * @return Array + * @param kEvent $event + * @return void * @access public */ - public function DoCopyTempToOriginal($master, $parent_prefix = null, $current_ids = Array()) + public function setParentEvent(kEvent $event) { - if ( !$current_ids ) { - $query = 'SELECT ' . $master['IdField'] . ' FROM ' . $this->GetTempName($master['TableName']); - - if ( isset($master['Constrain']) ) { - $query .= ' WHERE ' . $master['Constrain']; + $this->_parentEvent = $event; + $this->_top()->_drillDown($this, 'setParentEvent'); - } + } - $current_ids = $this->Conn->GetCol($query); - } + /** + * Collects information about table + * + * @return void + * @access protected + */ + protected function _collectTableInfo() + { + $config = $this->Application->getUnitConfig($this->_prefix); - $table_sig = $master['TableName'] . (isset($master['Constrain']) ? $master['Constrain'] : ''); + $this->_idField = $config->getIDField(); + $this->_tableName = $config->getTableName(); - 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); + $this->_foreignKey = $config->getForeignKey(); + $this->_parentTableKey = $config->getParentTableKey(); + $this->_constrain = $config->getConstrain(''); - $query = 'DELETE FROM ' . $master['TableName'] . ' WHERE ' . $master['IdField'] . ' IN (' . join(',', $current_ids) . ')'; - $this->Conn->Query($query); + $this->_autoClone = $config->getAutoClone(); + $this->_autoDelete = $config->getAutoDelete(); - } + } - if ( getArrayValue($master, 'SubTables') ) { - if ( in_array($table_sig, $this->CopiedTables) || $this->FinalRefs[$table_sig] != $master['TableId'] ) { - return Array (); - } + /** + * 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 ()); - 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']; + if ( !is_array($sub_items) ) { + throw new InvalidArgumentException('TempHandler: SubItems property in unit config must be an array'); - } + } - $this->Conn->Query($query); - $id_to_copy = 0; + foreach ($sub_items as $sub_item_prefix) { + $this->add(new kTempHandlerSubTable($sub_item_prefix)); - } + } - 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); + /** + * 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); - $insert_id = $id_to_copy == 0 ? $this->Conn->getInsertID() : $id_to_copy; + return ; + } - $this->saveID($master['Prefix'], '', array ($id => $insert_id)); - $this->RaiseEvent('OnAfterCopyToLive', $master['Prefix'], '', Array ($insert_id), null, Array ('temp_id' => $id)); + $this->_subTables[] = $table; + $table->setParent($this); + } - $this->UpdateForeignKeys($master, $insert_id, $id); + /** + * Sets parent table + * + * @param kTempHandlerTable $parent + * @return void + * @access public + */ + public function setParent(kTempHandlerTable $parent) + { + $this->_parent = $parent; - //delete already copied record from master temp table - $query = ' DELETE FROM ' . $this->GetTempName($master['TableName']) . ' - WHERE ' . $master['IdField'] . ' = ' . $id_to_copy; + if ( is_array($this->_foreignKey) ) { + $this->_multipleParents = true; + $this->_foreignKey = $this->_foreignKey[$parent->_prefix]; + } - if ( isset($master['Constrain']) ) { - $query .= ' AND ' . $master['Constrain']; + if ( is_array($this->_parentTableKey) ) { + $this->_parentTableKey = $this->_parentTableKey[$parent->_prefix]; - } + } - $this->Conn->Query($query); + $this->_setAsLastUsed(); + $this->_addSubTables(); - } + } - $this->CopiedTables[] = $table_sig; - - // when all of ids in current master has been processed, copy all sub-tables data - $this->CopySubTablesToLive($master, $current_ids); + /** + * Returns unit prefix + * + * @return string + * @access public + */ + public function getPrefix() + { + return $this->_prefix; - } + } - 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; + /** + * Determines if unit used to create table exists + * + * @return bool + * @access public + */ + public function unitRegistered() + { + return $this->Application->prefixRegistred($this->_prefix); - } + } - 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); + /** + * Returns topmost table + * + * @return kTempHandlerTopTable + * @access protected + */ + protected function _top() + { + $top = $this; - $live_ids[$an_id] = $this->Conn->getInsertID(); //storing newly created live id + while ( is_object($top->_parent) ) { + $top = $top->_parent; + } - //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); + return $top; - } + } - } - // copy ALL records to live table - $query = ' INSERT INTO ' . $master['TableName'] . ' - SELECT * FROM ' . $this->GetTempName($master['TableName']); + /** + * 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 ( isset($master['Constrain']) ) { - $query .= ' WHERE ' . $master['Constrain']; - } + if ( $table_match && $constrain_match ) { + switch ( $operation ) { + case 'state:copied': + $this->_addState(self::STATE_COPIED); + break; - $this->Conn->Query($query); + case 'state:deleted': + $this->_addState(self::STATE_DELETED); + break; - $this->CopiedTables[] = $table_sig; - $this->RaiseEvent('OnAfterCopyToLive', $master['Prefix'], '', $live_ids); + case 'setParentEvent': + $this->_parentEvent = $table->_parentEvent; + break; - $this->saveID($master['Prefix'], '', $live_ids); - // no need to clear temp table - it will be dropped by next statement + case 'resetLastUsed': + $this->_lastUsage = false; + break; } } - if ( $this->FinalRefs[ $master['TableName'] ] != $master['TableId'] ) { - return Array (); + /* @var $sub_table kTempHandlerSubTable */ + foreach ($this->_subTables as $sub_table) { + $sub_table->_drillDown($table, $operation, $same_table, $same_constrain); } - - /*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 (); + /** + * 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; - } + } - return $this->savedIDs[ $master['Prefix'] ]; - } - /** - * Create separate connection for locking purposes + * Marks table and all it's clones as copied * - * @return kDBConnection + * @return void + * @access protected */ - function &_getSeparateConnection() + protected function _setAsCopied() { - 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, true); + $this->_top()->_drillDown($this, 'state:copied', true, true); - } + } - return $connection; - } - - function UpdateChangeLogForeignKeys($master, $live_id, $temp_id) + /** + * 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) { + if ( $live_id == $temp_id ) { - return ; + return; } - $prefix = $master['Prefix']; - $main_prefix = $this->Application->GetTopmostPrefix($prefix); - $ses_var_name = $main_prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix); + $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'] == $prefix && $rec['ItemId'] == $temp_id) { + if ( $rec['Prefix'] == $this->_prefix && $rec['ItemId'] == $temp_id ) { // main item change log record $changes[$key]['ItemId'] = $live_id; } - if ($rec['MasterPrefix'] == $prefix && $rec['MasterId'] == $temp_id) { + if ( $rec['MasterPrefix'] == $this->_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) { + if ( in_array($this->_prefix, $rec['ParentPrefix']) && $rec['ParentId'][$this->_prefix] == $temp_id ) { // parent item change log record - $changes[$key]['ParentId'][$prefix] = $live_id; + $changes[$key]['ParentId'][$this->_prefix] = $live_id; - if (array_key_exists('DependentFields', $rec)) { + 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($prefix); + $parent_table_key = $config->getParentTableKey($this->_prefix); - if ($parent_table_key == $master['IdField']) { - $foreign_key = $config->getForeignKey($prefix); + if ( $parent_table_key == $this->_idField ) { + $foreign_key = $config->getForeignKey($this->_prefix); $changes[$key]['DependentFields'][$foreign_key] = $live_id; } @@ -792,64 +1056,222 @@ $this->Application->StoreVar($ses_var_name, serialize($changes)); } - function UpdateForeignKeys($master, $live_id, $temp_id) + /** + * 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) { - $this->UpdateChangeLogForeignKeys($master, $live_id, $temp_id); + $single_mode = false; - foreach ($master['SubTables'] as $sub_table) { - $foreign_key_field = is_array($sub_table['ForeignKey']) ? getArrayValue($sub_table, 'ForeignKey', $master['Prefix']) : $sub_table['ForeignKey']; + if ( !is_array($live_id) ) { + $single_mode = true; + $live_id = Array ($live_id); + } - if (!$foreign_key_field) { - continue; + if ( isset($temp_id) && !is_array($temp_id) ) { + $temp_id = Array ($temp_id); - } + } - list ($live_foreign_key, $temp_foreign_key) = $this->GetForeignKeys($master, $sub_table, $live_id, $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); - //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); + 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; + } - function CopySubTablesToLive($master, $current_ids) { - foreach ($master['SubTables'] as $sub_table) { + $this->_foreignKeyCache[$parent_key_field][$cache_key] = Array ($live_foreign_key, $temp_foreign_key); - $table_sig = $sub_table['TableName'].(isset($sub_table['Constrain']) ? $sub_table['Constrain'] : ''); + if ( $single_mode ) { + return Array ($live_foreign_key[0], $temp_foreign_key[0]); + } - // 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']; + return $live_foreign_key; + } - 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); + /** + * 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); - } + } - //sub_table passed here becomes master in the method, and recursively updated and copy its sub tables - $this->DoCopyTempToOriginal($sub_table, $master['Prefix']); + + /** + * 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(); + + /* @var $sub_table kTempHandlerSubTable */ + 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; + } + } + + /** * 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 @@ -857,15 +1279,14 @@ * @return bool * @access protected */ - protected function RaiseEvent($name, $prefix, $special, $ids, $foreign_key = null, $add_params = null) + protected function _raiseEvent($name, $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); - $event->MasterEvent = $this->parentEvent; + $event = new kEvent($this->_prefix . ($special ? '.' : '') . $special . ':' . $name); + $event->MasterEvent = $this->_parentEvent; if ( isset($foreign_key) ) { $event->setEventParam('foreign_key', $foreign_key); @@ -891,173 +1312,275 @@ 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); +/** + * Represents topmost table, that has related tables inside it + * + * Pattern: Composite + */ +class kTempHandlerTopTable extends kTempHandlerTable { - return true; - } - - function PrepareEdit() + /** + * Creates table object + * + * @param string $prefix + * @param Array $ids + */ + public function __construct($prefix, $ids = Array ()) { - $this->DoCopyLiveToTemp($this->Tables, $this->Tables['IDs']); + parent::__construct($prefix, $ids); - if ($this->Application->getUnitConfig($this->Tables['Prefix'])->getCheckSimulatniousEdit()) { - $this->CheckSimultaniousEdit(); + $this->_setAsLastUsed(); + $this->_addSubTables(); - } + } - } - 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 + * @access public */ - function CheckSimultaniousEdit($ids = null) + 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->MasterTable . '$/'; + $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->Tables['IDs']); + $ids = implode(',', isset($ids) ? $ids : $this->_ids); + $sids = Array (); + - if (!$ids) { + 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 ( !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 ( $sid == $my_sid ) { - if ($my_wid) { + if ( $my_wid ) { - // using popups for editing + // using popups for editing - if (preg_replace('/(.*)_(.*)/', '\\2', $rets[1]) == $my_wid) { + 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; - } - } + // 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'] . ') + $sql = 'SELECT COUNT(' . $this->_idField . ') - FROM ' . $table . ' + FROM ' . $table . ' - WHERE ' . $this->Tables['IdField'] . ' IN (' . $ids . ')'; + WHERE ' . $this->_idField . ' IN (' . $ids . ')'; - $found = $this->Conn->GetOne($sql); + $found = $this->Conn->GetOne($sql); - if (!$found || in_array($sid, $sids)) { + if ( !$found || in_array($sid, $sids) ) { - continue; - } + continue; + } - $sids[] = $sid; - } + $sids[] = $sid; + } + + if ( !$sids ) { + return true; } - if ($sids) { - // detect who is it + // 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, \')\') + $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 - ) + ) - ), \' IP: \', s.IpAddress, \'\') FROM ' . TABLE_PREFIX . 'UserSessions AS s - LEFT JOIN ' . TABLE_PREFIX . 'Users AS u - ON u.PortalUserId = s.PortalUserId + 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); + WHERE s.SessionKey IN (' . implode(',', $sids) . ')'; + $users = $this->Conn->GetCol($sql); - if ($users) { + if ( $users ) { - $this->Application->SetVar('_simultanious_edit_message', - sprintf($this->Application->Phrase('la_record_being_edited_by'), join(",\n", $users)) - ); + $this->Application->SetVar('_Simultaneous_edit_message', sprintf($this->Application->Phrase('la_record_being_edited_by'), implode(",\n", $users))); - return false; - } + 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