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); $this->_tables->setParentEvent($this->parentEvent); } /** * 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' => 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; } /** * 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, true); } 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 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->_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); } /* @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; } $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 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 ( !$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; } /* @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(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()); } /* @var $sub_table kTempHandlerSubTable */ foreach ($this->_subTables as $sub_table) { $sub_table->subCloneItems($object, $original_values); } } } return isset($this->_savedIds[$object->Special]) ? $this->_savedIds[$object->Special] : Array (); } /** * 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) { /* @var $sub_table kTempHandlerSubTable */ $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); } /** * 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; } } /* @var $sub_table kTempHandlerSubTable */ 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(); /* @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 $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); $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)); } }