Index: branches/5.2.x/core/kernel/db/dbitem.php =================================================================== --- branches/5.2.x/core/kernel/db/dbitem.php (revision 15143) +++ branches/5.2.x/core/kernel/db/dbitem.php (revision 15144) @@ -1,1435 +1,1435 @@ <?php /** * @version $Id$ * @package In-Portal * @copyright Copyright (C) 1997 - 2009 Intechnic. All rights reserved. * @license GNU/GPL * In-Portal is Open Source software. * This means that this software may have been modified pursuant * the GNU General Public License, and as distributed it includes * or is derivative of works licensed under the GNU General Public License * or other free or open source software licenses. * See http://www.in-portal.org/license for copyright notices and details. */ defined('FULL_PATH') or die('restricted access!'); /** * DBItem * */ class kDBItem extends kDBBase { /** * Description * * @var array Associative array of current item' field values * @access protected */ protected $FieldValues = Array (); /** * Unformatted field values, before parse * * @var Array * @access protected */ protected $DirtyFieldValues = Array (); /** * Holds item values after loading (not affected by submit) * * @var Array * @access protected */ protected $OriginalFieldValues = Array (); /** * If set to true, Update will skip Validation before running * * @var array Associative array of current item' field values * @access public */ public $IgnoreValidation = false; /** * Remembers if object was loaded * * @var bool * @access protected */ protected $Loaded = false; /** * Holds item' primary key value * * @var int Value of primary key field for current item * @access protected */ protected $ID; /** * This object is used in cloning operations * * @var bool * @access public */ public $inCloning = false; /** * Validator object reference * * @var kValidator */ protected $validator = null; /** * Creates validator object, only when required * */ public function initValidator() { if ( !is_object($this->validator) ) { $validator_class = $this->Application->getUnitOption($this->Prefix, 'ValidatorClass', 'kValidator'); $this->validator = $this->Application->makeClass($validator_class); } $this->validator->setDataSource($this); } public function SetDirtyField($field_name, $field_value) { $this->DirtyFieldValues[$field_name] = $field_value; } public function GetDirtyField($field_name) { return $this->DirtyFieldValues[$field_name]; } public function GetOriginalField($field_name, $formatted = false, $format=null) { if (array_key_exists($field_name, $this->OriginalFieldValues)) { // item was loaded before $value = $this->OriginalFieldValues[$field_name]; } else { // no original fields -> use default field value $value = $this->Fields[$field_name]['default']; } if (!$formatted) { return $value; } $res = $value; $formatter = $this->GetFieldOption($field_name, 'formatter'); if ( $formatter ) { $formatter = $this->Application->recallObject($formatter); /* @var $formatter kFormatter */ $res = $formatter->Format($value, $field_name, $this, $format); } return $res; } /** * Sets original field value (useful for custom virtual fields) * * @param string $field_name * @param string $field_value */ public function SetOriginalField($field_name, $field_value) { $this->OriginalFieldValues[$field_name] = $field_value; } /** * Set's default values for all fields * * @access public */ public function SetDefaultValues() { parent::SetDefaultValues(); if ($this->populateMultiLangFields) { $this->PopulateMultiLangFields(); } foreach ($this->Fields as $field => $field_options) { $default_value = isset($field_options['default']) ? $field_options['default'] : NULL; $this->SetDBField($field, $default_value); } } /** * Sets current item field value * (applies formatting) * * @access public * @param string $name Name of the field * @param mixed $value Value to set the field to * @return void */ public function SetField($name,$value) { $options = $this->GetFieldOptions($name); $parsed = $value; if ($value == '') { $parsed = NULL; } // kFormatter is always used, to make sure, that numeric value is converted to normal representation // according to regional format, even when formatter is not set (try seting format to 1.234,56 to understand why) $formatter = $this->Application->recallObject(isset($options['formatter']) ? $options['formatter'] : 'kFormatter'); /* @var $formatter kFormatter */ $parsed = $formatter->Parse($value, $name, $this); $this->SetDBField($name,$parsed); } /** * Sets current item field value * (doesn't apply formatting) * * @access public * @param string $name Name of the field * @param mixed $value Value to set the field to * @return void */ public function SetDBField($name,$value) { $this->FieldValues[$name] = $value; } /** * Set's field error, if pseudo passed not found then create it with message text supplied. * Don't overwrite existing pseudo translation. * * @param string $field * @param string $pseudo * @param string $error_label * @param Array $error_params * * @return bool * @access public */ public function SetError($field, $pseudo, $error_label = null, $error_params = null) { $this->initValidator(); return $this->validator->SetError($field, $pseudo, $error_label, $error_params); } /** * Removes error on field * * @param string $field * @access public */ public function RemoveError($field) { if ( !is_object($this->validator) ) { return ; } $this->validator->RemoveError($field); } /** * Returns error pseudo * * @param string $field * @return string */ public function GetErrorPseudo($field) { if ( !is_object($this->validator) ) { return ''; } return $this->validator->GetErrorPseudo($field); } /** * Return current item' field value by field name * (doesn't apply formatter) * * @param string $name field name to return * @return mixed * @access public */ public function GetDBField($name) { /*if (!array_key_exists($name, $this->FieldValues) && defined('DEBUG_MODE') && DEBUG_MODE) { $this->Application->Debugger->appendTrace(); }*/ return $this->FieldValues[$name]; } public function HasField($name) { return array_key_exists($name, $this->FieldValues); } public function GetFieldValues() { return $this->FieldValues; } /** * Sets item' fields corresponding to elements in passed $hash values. * * The function sets current item fields to values passed in $hash, by matching $hash keys with field names * of current item. If current item' fields are unknown {@link kDBItem::PrepareFields()} is called before actually setting the fields * * @param Array $hash * @param Array $skip_fields Optional param, field names in target object not to set, other fields will be set * @param Array $set_fields Optional param, field names in target object to set, other fields will be skipped * @return void * @access public */ public function SetFieldsFromHash($hash, $skip_fields = Array (), $set_fields = Array ()) { if ( !$set_fields ) { $set_fields = array_keys($hash); } if ( $skip_fields ) { $set_fields = array_diff($set_fields, $skip_fields); } $set_fields = array_intersect($set_fields, array_keys($this->Fields)); // used in formatter which work with multiple fields together foreach ($set_fields as $field_name) { $this->SetDirtyField($field_name, $hash[$field_name]); } // formats all fields using associated formatters foreach ($set_fields as $field_name) { $this->SetField($field_name, $hash[$field_name]); } } /** * Sets object fields from $hash array * @param Array $hash * @param Array|null $skip_fields * @param Array|null $set_fields * @return void * @access public */ public function SetDBFieldsFromHash($hash, $skip_fields = Array (), $set_fields = Array ()) { if ( !$set_fields ) { $set_fields = array_keys($hash); } if ( $skip_fields ) { $set_fields = array_diff($set_fields, $skip_fields); } $set_fields = array_intersect($set_fields, array_keys($this->Fields)); foreach ($set_fields as $field_name) { $this->SetDBField($field_name, $hash[$field_name]); } } /** * Returns part of SQL WHERE clause identifying the record, ex. id = 25 * * @param string $method Child class may want to know who called GetKeyClause, Load(), Update(), Delete() send its names as method * @param Array $keys_hash alternative, then item id, keys hash to load item by * @see kDBItem::Load() * @see kDBItem::Update() * @see kDBItem::Delete() * @return string * @access protected */ protected function GetKeyClause($method = null, $keys_hash = null) { if ( !isset($keys_hash) ) { $keys_hash = Array ($this->IDField => $this->ID); } $ret = ''; foreach ($keys_hash as $field => $value) { $value_part = is_null($value) ? ' IS NULL' : ' = ' . $this->Conn->qstr($value); $ret .= '(' . (strpos($field, '.') === false ? '`' . $this->TableName . '`.' : '') . $field . $value_part . ') AND '; } return substr($ret, 0, -5); } /** * Loads item from the database by given id * * @access public * @param mixed $id item id of keys->values hash to load item by * @param string $id_field_name Optional parameter to load item by given Id field * @param bool $cachable cache this query result based on it's prefix serial * @return bool True if item has been loaded, false otherwise */ public function Load($id, $id_field_name = null, $cachable = false) { if ( isset($id_field_name) ) { $this->IDField = $id_field_name; // set new IDField } $keys_sql = ''; if (is_array($id)) { $keys_sql = $this->GetKeyClause('load', $id); } else { $this->setID($id); $keys_sql = $this->GetKeyClause('load'); } if ( isset($id_field_name) ) { // restore original IDField from unit config $this->IDField = $this->Application->getUnitOption($this->Prefix, 'IDField'); } if (($id === false) || !$keys_sql) { return $this->Clear(); } if (!$this->raiseEvent('OnBeforeItemLoad', $id)) { return false; } $q = $this->GetSelectSQL() . ' WHERE ' . $keys_sql; if ($cachable && $this->Application->isCachingType(CACHING_TYPE_MEMORY)) { $serial_name = $this->Application->incrementCacheSerial($this->Prefix == 'st' ? 'c' : $this->Prefix, isset($id_field_name) ? null : $id, false); $cache_key = 'kDBItem::Load_' . crc32(serialize($id) . '-' . $this->IDField) . '[%' . $serial_name . '%]'; $field_values = $this->Application->getCache($cache_key, false); if ($field_values === false) { $field_values = $this->Conn->GetRow($q); if ($field_values !== false) { // only cache, when data was retrieved $this->Application->setCache($cache_key, $field_values); } } } else { $field_values = $this->Conn->GetRow($q); } if ($field_values) { $this->FieldValues = array_merge($this->FieldValues, $field_values); $this->OriginalFieldValues = $this->FieldValues; } else { return $this->Clear(); } if (is_array($id) || isset($id_field_name)) { $this->setID($this->FieldValues[$this->IDField]); } $this->UpdateFormattersSubFields(); // used for updating separate virtual date/time fields from DB timestamp (for example) $this->raiseEvent('OnAfterItemLoad', $this->GetID()); $this->Loaded = true; return true; } /** * Loads object from hash (not db) * * @param Array $fields_hash * @param string $id_field */ public function LoadFromHash($fields_hash, $id_field = null) { if (!isset($id_field)) { $id_field = $this->IDField; } $this->Clear(); if (!$fields_hash || !array_key_exists($id_field, $fields_hash)) { // no data OR id field missing return false; } $id = $fields_hash[$id_field]; if ( !$this->raiseEvent('OnBeforeItemLoad', $id) ) { return false; } $this->FieldValues = array_merge($this->FieldValues, $fields_hash); $this->OriginalFieldValues = $this->FieldValues; $this->setID($id); $this->UpdateFormattersSubFields(); // used for updating separate virtual date/time fields from DB timestamp (for example) $this->raiseEvent('OnAfterItemLoad', $id); $this->Loaded = true; return true; } /** * Builds select sql, SELECT ... FROM parts only * * @access public * @return string */ /** * Returns SELECT part of list' query * * @param string $base_query * @param bool $replace_table * @return string * @access public */ public function GetSelectSQL($base_query = null, $replace_table = true) { if (!isset($base_query)) { $base_query = $this->SelectClause; } $base_query = $this->addCalculatedFields($base_query); return parent::GetSelectSQL($base_query, $replace_table); } public function UpdateFormattersMasterFields() { $this->initValidator(); // used, when called not from kValidator::Validate method foreach ($this->Fields as $field => $options) { if ( isset($options['formatter']) ) { $formatter = $this->Application->recallObject($options['formatter']); /* @var $formatter kFormatter */ $formatter->UpdateMasterFields($field, $this->GetDBField($field), $options, $this); } } } /** * Allows to skip certain fields from getting into sql queries * * @param string $field_name * @param mixed $force_id * @return bool */ public function skipField($field_name, $force_id = false) { $skip = false; // 1. skipping 'virtual' field $skip = $skip || array_key_exists($field_name, $this->VirtualFields); // 2. don't write empty field value to db, when "skip_empty" option is set $field_value = array_key_exists($field_name, $this->FieldValues) ? $this->FieldValues[$field_name] : false; if (array_key_exists($field_name, $this->Fields)) { $skip_empty = array_key_exists('skip_empty', $this->Fields[$field_name]) ? $this->Fields[$field_name]['skip_empty'] : false; } else { // field found in database, but not declared in unit config $skip_empty = false; } $skip = $skip || (!$field_value && $skip_empty); // 3. skipping field not in Fields (nor virtual, nor real) $skip = $skip || !array_key_exists($field_name, $this->Fields); return $skip; } /** * Updates previously loaded record with current item' values * * @access public * @param int $id Primary Key Id to update * @param bool $system_update * @return bool * @access public */ public function Update($id = null, $system_update = false) { if ( isset($id) ) { $this->setID($id); } if ( !$this->raiseEvent('OnBeforeItemUpdate') ) { return false; } if ( !isset($this->ID) ) { // ID could be set inside OnBeforeItemUpdate event, so don't combine this check with previous one return false; } // validate before updating if ( !$this->Validate() ) { return false; } if ( !$this->FieldValues ) { // nothing to update return true; } $sql = ''; foreach ($this->FieldValues as $field_name => $field_value) { if ( $this->skipField($field_name) ) { continue; } if ( is_null($field_value) ) { if ( array_key_exists('not_null', $this->Fields[$field_name]) && $this->Fields[$field_name]['not_null'] ) { // "kFormatter::Parse" methods converts empty values to NULL and for // not-null fields they are replaced with default value here $field_value = $this->Fields[$field_name]['default']; } } $sql .= '`' . $field_name . '` = ' . $this->Conn->qstr($field_value) . ', '; } $sql = 'UPDATE ' . $this->TableName . ' SET ' . substr($sql, 0, -2) . ' WHERE ' . $this->GetKeyClause('update'); if ( $this->Conn->ChangeQuery($sql) === false ) { // there was and sql error $this->SetError($this->IDField, 'sql_error', '#' . $this->Conn->getErrorCode() . ': ' . $this->Conn->getErrorMsg()); return false; } $affected_rows = $this->Conn->getAffectedRows(); if ( !$system_update && ($affected_rows > 0) ) { $this->setModifiedFlag(ChangeLog::UPDATE); } $this->saveCustomFields(); $this->raiseEvent('OnAfterItemUpdate'); $this->OriginalFieldValues = $this->FieldValues; $this->Loaded = true; if ( !$this->IsTempTable() ) { $this->Application->resetCounters($this->TableName); } return true; } /** * Validates given field * * @param string $field * @return bool * @access public */ public function ValidateField($field) { $this->initValidator(); return $this->validator->ValidateField($field); } /** * Validate all item fields based on * constraints set in each field options * in config * * @return bool * @access private */ public function Validate() { if ( $this->IgnoreValidation ) { return true; } $this->initValidator(); // will apply any custom validation to the item $this->raiseEvent('OnBeforeItemValidate'); if ( $this->validator->Validate() ) { // no validation errors $this->raiseEvent('OnAfterItemValidate'); return true; } return false; } /** * Check if item has errors * * @param Array $skip_fields fields to skip during error checking * @return bool */ public function HasErrors($skip_fields = Array ()) { if ( !is_object($this->validator) ) { return false; } return $this->validator->HasErrors($skip_fields); } /** * Check if value is set for required field * * @param string $field field name * @param Array $params field options from config * @return bool * @access public * @todo Find a way to get rid of direct call from kMultiLanguage::UpdateMasterFields method */ public function ValidateRequired($field, $params) { return $this->validator->ValidateRequired($field, $params); } /** * Return error message for field * * @param string $field * @param bool $force_escape * @return string * @access public */ public function GetErrorMsg($field, $force_escape = null) { if ( !is_object($this->validator) ) { return ''; } return $this->validator->GetErrorMsg($field, $force_escape); } /** * Returns field errors * * @return Array * @access public */ public function GetFieldErrors() { if ( !is_object($this->validator) ) { return Array (); } return $this->validator->GetFieldErrors(); } /** * Creates a record in the database table with current item' values * * @param mixed $force_id Set to TRUE to force creating of item's own ID or to value to force creating of passed id. Do not pass 1 for true, pass exactly TRUE! * @param bool $system_create * @return bool * @access public */ public function Create($force_id = false, $system_create = false) { if (!$this->raiseEvent('OnBeforeItemCreate')) { return false; } // Validating fields before attempting to create record if (!$this->Validate()) { return false; } if (is_int($force_id)) { $this->FieldValues[$this->IDField] = $force_id; } elseif (!$force_id || !is_bool($force_id)) { $this->FieldValues[$this->IDField] = $this->generateID(); } $fields_sql = ''; $values_sql = ''; foreach ($this->FieldValues as $field_name => $field_value) { if ($this->skipField($field_name, $force_id)) { continue; } if (is_null($field_value)) { if (array_key_exists('not_null', $this->Fields[$field_name]) && $this->Fields[$field_name]['not_null']) { // "kFormatter::Parse" methods converts empty values to NULL and for // not-null fields they are replaced with default value here $values_sql .= $this->Conn->qstr($this->Fields[$field_name]['default']); } else { $values_sql .= $this->Conn->qstr($field_value); } } else { if (($field_name == $this->IDField) && ($field_value == 0) && !is_int($force_id)) { // don't skip IDField in INSERT statement, just use DEFAULT keyword as it's value $values_sql .= 'DEFAULT'; } else { $values_sql .= $this->Conn->qstr($field_value); } } $fields_sql .= '`' . $field_name . '`, '; //Adding field name to fields block of Insert statement $values_sql .= ', '; } $sql = 'INSERT INTO ' . $this->TableName . ' (' . substr($fields_sql, 0, -2) . ') VALUES (' . substr($values_sql, 0, -2) . ')'; //Executing the query and checking the result if ($this->Conn->ChangeQuery($sql) === false) { $this->SetError($this->IDField, 'sql_error', '#' . $this->Conn->getErrorCode() . ': ' . $this->Conn->getErrorMsg()); return false; } $insert_id = $this->Conn->getInsertID(); if ($insert_id == 0) { // insert into temp table (id is not auto-increment field) $insert_id = $this->FieldValues[$this->IDField]; } $this->setID($insert_id); $this->OriginalFieldValues = $this->FieldValues; if (!$system_create){ $this->setModifiedFlag(ChangeLog::CREATE); } $this->saveCustomFields(); if (!$this->IsTempTable()) { $this->Application->resetCounters($this->TableName); } if ($this->IsTempTable() && ($this->Application->GetTopmostPrefix($this->Prefix) != $this->Prefix) && !is_int($force_id)) { // temp table + subitem = set negative id $this->setTempID(); } $this->raiseEvent('OnAfterItemCreate'); $this->Loaded = true; return true; } /** * Deletes the record from database * * @param int $id * @return bool * @access public */ public function Delete($id = null) { if ( isset($id) ) { $this->setID($id); } if ( !$this->raiseEvent('OnBeforeItemDelete') ) { return false; } $sql = 'DELETE FROM ' . $this->TableName . ' WHERE ' . $this->GetKeyClause('Delete'); $ret = $this->Conn->ChangeQuery($sql); $affected_rows = $this->Conn->getAffectedRows(); if ( $affected_rows > 0 ) { $this->setModifiedFlag(ChangeLog::DELETE); // will change affected rows, so get it before this line // something was actually deleted $this->raiseEvent('OnAfterItemDelete'); } if ( !$this->IsTempTable() ) { $this->Application->resetCounters($this->TableName); } return $ret; } public function PopulateMultiLangFields() { foreach ($this->Fields as $field => $options) { // master field is set only for CURRENT language $formatter = array_key_exists('formatter', $options) ? $options['formatter'] : false; if ( ($formatter == 'kMultiLanguage') && isset($options['master_field']) && isset($options['error_field']) ) { // MuliLanguage formatter sets error_field to master_field, but in PopulateMlFields mode, // we display ML fields directly so we set it back to itself, otherwise error won't be displayed unset( $this->Fields[$field]['error_field'] ); } } } /** * Sets new name for item in case if it is being copied in same table * * @param array $master Table data from TempHandler * @param int $foreign_key ForeignKey value to filter name check query by * @param string $title_field FieldName to alter, by default - TitleField of the prefix * @param string $format sprintf-style format of renaming pattern, by default Copy %1$s of %2$s which makes it Copy [Number] of Original Name * @access public */ public function NameCopy($master=null, $foreign_key=null, $title_field=null, $format='Copy %1$s of %2$s') { if (!isset($title_field)) { $title_field = $this->Application->getUnitOption($this->Prefix, 'TitleField'); if (!$title_field || isset($this->CalculatedFields[$title_field]) ) return; } $new_name = $this->GetDBField($title_field); $original_checked = false; do { if ( preg_match('/'.sprintf($format, '([0-9]*) *', '(.*)').'/', $new_name, $regs) ) { $new_name = sprintf($format, ($regs[1]+1), $regs[2]); } elseif ($original_checked) { $new_name = sprintf($format, '', $new_name); } // if we are cloning in temp table this will look for names in temp table, // since object' TableName contains correct TableName (for temp also!) // if we are cloning live - look in live $query = 'SELECT '.$title_field.' FROM '.$this->TableName.' WHERE '.$title_field.' = '.$this->Conn->qstr($new_name); $foreign_key_field = getArrayValue($master, 'ForeignKey'); $foreign_key_field = is_array($foreign_key_field) ? $foreign_key_field[ $master['ParentPrefix'] ] : $foreign_key_field; if ($foreign_key_field && isset($foreign_key)) { $query .= ' AND '.$foreign_key_field.' = '.$foreign_key; } $res = $this->Conn->GetOne($query); /*// if not found in live table, check in temp table if applicable if ($res === false && $object->Special == 'temp') { $query = 'SELECT '.$name_field.' FROM '.$this->GetTempName($master['TableName']).' WHERE '.$name_field.' = '.$this->Conn->qstr($new_name); $res = $this->Conn->GetOne($query); }*/ $original_checked = true; } while ($res !== false); $this->SetDBField($title_field, $new_name); } protected function raiseEvent($name, $id = null, $additional_params = Array()) { $additional_params['id'] = isset($id) ? $id : $this->GetID(); $event = new kEvent($this->getPrefixSpecial() . ':' . $name, $additional_params); if ( is_object($this->parentEvent) ) { - $event->MasterEvent =& $this->parentEvent; + $event->MasterEvent = $this->parentEvent; } $this->Application->HandleEvent($event); return $event->status == kEvent::erSUCCESS; } /** * Set's new ID for item * * @param int $new_id * @access public */ public function setID($new_id) { $this->ID = $new_id; $this->SetDBField($this->IDField, $new_id); } /** * Generate and set new temporary id * * @access private */ public function setTempID() { $new_id = (int)$this->Conn->GetOne('SELECT MIN('.$this->IDField.') FROM '.$this->TableName); if($new_id > 0) $new_id = 0; --$new_id; $this->Conn->Query('UPDATE '.$this->TableName.' SET `'.$this->IDField.'` = '.$new_id.' WHERE `'.$this->IDField.'` = '.$this->GetID()); if ($this->ShouldLogChanges(true)) { // Updating TempId in ChangesLog, if changes are disabled $ses_var_name = $this->Application->GetTopmostPrefix($this->Prefix) . '_changes_' . $this->Application->GetTopmostWid($this->Prefix); $changes = $this->Application->RecallVar($ses_var_name); $changes = $changes ? unserialize($changes) : Array (); if ($changes) { foreach ($changes as $key => $rec) { if ($rec['Prefix'] == $this->Prefix && $rec['ItemId'] == $this->GetID()) { // change log for record, that's ID was just updated -> update in change log record too $changes[$key]['ItemId'] = $new_id; } if ($rec['MasterPrefix'] == $this->Prefix && $rec['MasterId'] == $this->GetID()) { // master item id was changed $changes[$key]['MasterId'] = $new_id; } if (in_array($this->Prefix, $rec['ParentPrefix']) && $rec['ParentId'][$this->Prefix] == $this->GetID()) { // change log record of given item's sub item -> update changed id's in dependent fields $changes[$key]['ParentId'][$this->Prefix] = $new_id; if (array_key_exists('DependentFields', $rec)) { // these are fields from table of $rec['Prefix'] table! // when one of dependent fields goes into idfield of it's parent item, that was changed $parent_table_key = $this->Application->getUnitOption($rec['Prefix'], 'ParentTableKey'); $parent_table_key = is_array($parent_table_key) ? $parent_table_key[$this->Prefix] : $parent_table_key; if ($parent_table_key == $this->IDField) { $foreign_key = $this->Application->getUnitOption($rec['Prefix'], 'ForeignKey'); $foreign_key = is_array($foreign_key) ? $foreign_key[$this->Prefix] : $foreign_key; $changes[$key]['DependentFields'][$foreign_key] = $new_id; } } } } } $this->Application->StoreVar($ses_var_name, serialize($changes)); } $this->SetID($new_id); } /** * Set's modification flag for main prefix of current prefix to true * * @param int $mode * @access private */ public function setModifiedFlag($mode = null) { $main_prefix = $this->Application->GetTopmostPrefix($this->Prefix); $this->Application->StoreVar($main_prefix . '_modified', '1', true); // true for optional if ($this->ShouldLogChanges(true)) { $this->LogChanges($main_prefix, $mode); if (!$this->IsTempTable()) { $handler = $this->Application->recallObject($this->Prefix . '_EventHandler'); /* @var $handler kDBEventHandler */ $ses_var_name = $main_prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix); $handler->SaveLoggedChanges($ses_var_name, $this->ShouldLogChanges()); } } } /** * Determines, that changes made to this item should be written to change log * * @param bool $log_changes * @return bool */ public function ShouldLogChanges($log_changes = null) { if (!isset($log_changes)) { // specific logging mode no forced -> use global logging settings $log_changes = $this->Application->getUnitOption($this->Prefix, 'LogChanges') || $this->Application->ConfigValue('UseChangeLog'); } return $log_changes && !$this->Application->getUnitOption($this->Prefix, 'ForceDontLogChanges'); } protected function LogChanges($main_prefix, $mode) { if ( !$mode ) { return ; } $ses_var_name = $main_prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix); $changes = $this->Application->RecallVar($ses_var_name); $changes = $changes ? unserialize($changes) : Array (); $fields_hash = Array ( 'Prefix' => $this->Prefix, 'ItemId' => $this->GetID(), 'OccuredOn' => adodb_mktime(), 'MasterPrefix' => $main_prefix, 'Action' => $mode, ); if ( $this->Prefix == $main_prefix ) { // main item $fields_hash['MasterId'] = $this->GetID(); $fields_hash['ParentPrefix'] = Array ($main_prefix); $fields_hash['ParentId'] = Array ($main_prefix => $this->GetID()); } else { // sub item // collect foreign key values (for serial reset) $foreign_keys = $this->Application->getUnitOption($this->Prefix, 'ForeignKey', Array ()); $dependent_fields = $fields_hash['ParentId'] = $fields_hash['ParentPrefix'] = Array (); /* @var $foreign_keys Array */ if ( is_array($foreign_keys) ) { foreach ($foreign_keys as $prefix => $field_name) { $dependent_fields[$field_name] = $this->GetDBField($field_name); $fields_hash['ParentPrefix'][] = $prefix; $fields_hash['ParentId'][$prefix] = $this->getParentId($prefix); } } else { $dependent_fields[$foreign_keys] = $this->GetDBField($foreign_keys); $fields_hash['ParentPrefix'] = Array ( $this->Application->getUnitOption($this->Prefix, 'ParentPrefix') ); $fields_hash['ParentId'][ $fields_hash['ParentPrefix'][0] ] = $this->getParentId('auto'); } $fields_hash['DependentFields'] = $dependent_fields; // works only, when main item is present in url, when sub-item is changed $master_id = $this->Application->GetVar($main_prefix . '_id'); if ( $master_id === false ) { // works in case of we are not editing topmost item, when sub-item is created/updated/deleted $master_id = $this->getParentId('auto', true); } $fields_hash['MasterId'] = $master_id; } switch ( $mode ) { case ChangeLog::UPDATE: $to_save = array_merge($this->GetTitleField(), $this->GetChangedFields()); break; case ChangeLog::CREATE: $to_save = $this->GetTitleField(); break; case ChangeLog::DELETE: $to_save = array_merge($this->GetTitleField(), $this->GetRealFields()); break; default: $to_save = Array (); break; } $fields_hash['Changes'] = serialize($to_save); $changes[] = $fields_hash; $this->Application->StoreVar($ses_var_name, serialize($changes)); } /** * Returns current item parent's ID * * @param string $parent_prefix * @param bool $top_most return topmost parent, when used * @return int */ protected function getParentId($parent_prefix, $top_most = false) { $current_id = $this->GetID(); $current_prefix = $this->Prefix; if ($parent_prefix == 'auto') { $parent_prefix = $this->Application->getUnitOption($current_prefix, 'ParentPrefix'); } if (!$parent_prefix) { return $current_id; } do { // field in this table $foreign_key = $this->Application->getUnitOption($current_prefix, 'ForeignKey'); $foreign_key = is_array($foreign_key) ? $foreign_key[$parent_prefix] : $foreign_key; // get foreign key value for $current_prefix if ($current_prefix == $this->Prefix) { $foreign_key_value = $this->GetDBField($foreign_key); } else { $id_field = $this->Application->getUnitOption($current_prefix, 'IDField'); $table_name = $this->Application->getUnitOption($current_prefix, 'TableName'); if ($this->IsTempTable()) { $table_name = $this->Application->GetTempName($table_name, 'prefix:' . $current_prefix); } $sql = 'SELECT ' . $foreign_key . ' FROM ' . $table_name . ' WHERE ' . $id_field . ' = ' . $current_id; $foreign_key_value = $this->Conn->GetOne($sql); } // field in parent table $parent_table_key = $this->Application->getUnitOption($current_prefix, 'ParentTableKey'); $parent_table_key = is_array($parent_table_key) ? $parent_table_key[$parent_prefix] : $parent_table_key; $parent_id_field = $this->Application->getUnitOption($parent_prefix, 'IDField'); $parent_table_name = $this->Application->getUnitOption($parent_prefix, 'TableName'); if ($this->IsTempTable()) { $parent_table_name = $this->Application->GetTempName($parent_table_name, 'prefix:' . $current_prefix); } if ($parent_id_field == $parent_table_key) { // sub-item is related by parent item idfield $current_id = $foreign_key_value; } else { // sub-item is related by other parent item field $sql = 'SELECT ' . $parent_id_field . ' FROM ' . $parent_table_name . ' WHERE ' . $parent_table_key . ' = ' . $foreign_key_value; $current_id = $this->Conn->GetOne($sql); } $current_prefix = $parent_prefix; if (!$top_most) { break; } } while ( $parent_prefix = $this->Application->getUnitOption($current_prefix, 'ParentPrefix') ); return $current_id; } /** * Returns title field (if any) * * @return Array */ public function GetTitleField() { $title_field = $this->Application->getUnitOption($this->Prefix, 'TitleField'); if ($title_field) { $value = $this->GetField($title_field); return $value ? Array ($title_field => $value) : Array (); } return Array (); } /** * Returns only fields, that are present in database (no virtual and no calculated fields) * * @return Array */ public function GetRealFields() { return array_diff_key($this->FieldValues, $this->VirtualFields, $this->CalculatedFields); } /** * Returns only changed database field * * @param bool $include_virtual_fields * @return Array */ public function GetChangedFields($include_virtual_fields = false) { $changes = Array (); $fields = $include_virtual_fields ? $this->FieldValues : $this->GetRealFields(); $diff = array_diff_assoc($fields, $this->OriginalFieldValues); foreach ($diff as $field => $new_value) { $old_value = $this->GetOriginalField($field, true); $new_value = $this->GetField($field); if ($old_value != $new_value) { // "0.00" and "0.0000" are stored as strings and will differ. Double check to prevent that. $changes[$field] = Array ('old' => $old_value, 'new' => $new_value); } } return $changes; } /** * Returns ID of currently processed record * * @return int * @access public */ public function GetID() { return $this->ID; } /** * Generates ID for new items before inserting into database * * @return int * @access private */ protected function generateID() { return 0; } /** * Returns true if item was loaded successfully by Load method * * @return bool */ public function isLoaded() { return $this->Loaded; } /** * Checks if field is required * * @param string $field * @return bool */ public function isRequired($field) { return isset($this->Fields[$field]['required']) && $this->Fields[$field]['required']; } /** * Sets new required flag to field * * @param mixed $fields * @param bool $is_required */ public function setRequired($fields, $is_required = true) { if ( !is_array($fields) ) { $fields = explode(',', $fields); } foreach ($fields as $field) { $this->Fields[$field]['required'] = $is_required; } } /** * Removes all data from an object * * @param int $new_id * @return bool * @access public */ public function Clear($new_id = null) { $this->Loaded = false; $this->FieldValues = $this->OriginalFieldValues = Array (); $this->SetDefaultValues(); // will wear off kDBItem::setID effect, so set it later if ( is_object($this->validator) ) { $this->validator->reset(); } $this->setID($new_id); return $this->Loaded; } public function Query($force = false) { throw new Exception('<b>Query</b> method is called in class <strong>' . get_class($this) . '</strong> for prefix <strong>' . $this->getPrefixSpecial() . '</strong>'); } protected function saveCustomFields() { if ( !$this->customFields || $this->inCloning ) { return true; } $cdata_key = rtrim($this->Prefix . '-cdata.' . $this->Special, '.'); $cdata = $this->Application->recallObject($cdata_key, null, Array ('skip_autoload' => true)); /* @var $cdata kDBItem */ $resource_id = $this->GetDBField('ResourceId'); $cdata->Load($resource_id, 'ResourceId'); $cdata->SetDBField('ResourceId', $resource_id); $ml_formatter = $this->Application->recallObject('kMultiLanguage'); /* @var $ml_formatter kMultiLanguage */ $ml_helper = $this->Application->recallObject('kMultiLanguageHelper'); /* @var $ml_helper kMultiLanguageHelper */ $languages = $ml_helper->getLanguages(); foreach ($this->customFields as $custom_id => $custom_name) { $force_primary = $cdata->GetFieldOption('cust_' . $custom_id, 'force_primary'); if ( $force_primary ) { $cdata->SetDBField($ml_formatter->LangFieldName('cust_' . $custom_id, true), $this->GetDBField('cust_' . $custom_name)); } else { foreach ($languages as $language_id) { $cdata->SetDBField('l' . $language_id . '_cust_' . $custom_id, $this->GetDBField('l' . $language_id . '_cust_' . $custom_name)); } } } return $cdata->isLoaded() ? $cdata->Update() : $cdata->Create(); } /** * Returns specified field value from all selected rows. * Don't affect current record index * * @param string $field * @param bool $formatted * @param string $format * @return Array */ public function GetCol($field, $formatted = false, $format = null) { if ($formatted) { return Array (0 => $this->GetField($field, $format)); } return Array (0 => $this->GetDBField($field)); } /** * Set's loaded status of object * * @param bool $is_loaded * @access public * @todo remove this method, since item can't be marked as loaded externally */ public function setLoaded($is_loaded = true) { $this->Loaded = $is_loaded; } } \ No newline at end of file Index: branches/5.2.x/core/kernel/utility/temp_handler.php =================================================================== --- branches/5.2.x/core/kernel/utility/temp_handler.php (revision 15143) +++ branches/5.2.x/core/kernel/utility/temp_handler.php (revision 15144) @@ -1,1055 +1,1055 @@ <?php /** * @version $Id$ * @package In-Portal * @copyright Copyright (C) 1997 - 2009 Intechnic. All rights reserved. * @license GNU/GPL * In-Portal is Open Source software. * This means that this software may have been modified pursuant * the GNU General Public License, and as distributed it includes * or is derivative of works licensed under the GNU General Public License * or other free or open source software licenses. * See http://www.in-portal.org/license for copyright notices and details. */ defined('FULL_PATH') or die('restricted access!'); class kTempTablesHandler extends kBase { var $Tables = Array(); /** * Master table name for temp handler * * @var string * @access private */ var $MasterTable = ''; /** * IDs from master table * * @var Array * @access private */ var $MasterIDs = Array(); var $AlreadyProcessed = Array(); var $DroppedTables = Array(); var $FinalRefs = Array(); var $TableIdCounter = 0; var $CopiedTables = Array(); /** * Foreign key cache * * @var Array */ var $FKeysCache = Array (); /** * IDs of newly cloned items (key - prefix.special, value - array of ids) * * @var Array */ var $savedIDs = Array(); /** * Window ID of current window * * @var mixed */ var $WindowID = ''; /** * Event, that was used to create this object * * @var kEvent * @access protected */ protected $parentEvent = null; /** * Sets new parent event to the object * * @param kEvent $event * @return void * @access public */ public function setParentEvent($event) { - $this->parentEvent =& $event; + $this->parentEvent = $event; } function SetTables($tables) { // set table name as key for tables array $this->Tables = $tables; $this->MasterTable = $tables['TableName']; } function saveID($prefix, $special = '', $id = null) { if (!isset($this->savedIDs[$prefix.($special ? '.' : '').$special])) { $this->savedIDs[$prefix.($special ? '.' : '').$special] = array(); } if (is_array($id)) { foreach ($id as $tmp_id => $live_id) { $this->savedIDs[$prefix.($special ? '.' : '').$special][$tmp_id] = $live_id; } } else { $this->savedIDs[$prefix.($special ? '.' : '').$special][] = $id; } } /** * Get temp table name * * @param string $table * @return string */ function GetTempName($table) { return $this->Application->GetTempName($table, $this->WindowID); } function GetTempTablePrefix() { return $this->Application->GetTempTablePrefix($this->WindowID); } /** * Return live table name based on temp table name * * @param string $temp_table * @return string */ function GetLiveName($temp_table) { return $this->Application->GetLiveName($temp_table); } function IsTempTable($table) { return $this->Application->IsTempTable($table); } /** * Return temporary table name for master table * * @return string * @access public */ function GetMasterTempName() { return $this->GetTempName($this->MasterTable); } function CreateTempTable($table) { $sql = 'CREATE TABLE ' . $this->GetTempName($table) . ' SELECT * FROM ' . $table . ' WHERE 0'; $this->Conn->Query($sql); } function BuildTables($prefix, $ids) { $this->WindowID = $this->Application->GetVar('m_wid'); $this->TableIdCounter = 0; $tables = Array( 'TableName' => $this->Application->getUnitOption($prefix, 'TableName'), 'IdField' => $this->Application->getUnitOption($prefix, 'IDField'), 'IDs' => $ids, 'Prefix' => $prefix, 'TableId' => $this->TableIdCounter++, ); /*$parent_prefix = $this->Application->getUnitOption($prefix, 'ParentPrefix'); if ($parent_prefix) { $tables['ForeignKey'] = $this->Application->getUnitOption($prefix, 'ForeignKey'); $tables['ParentPrefix'] = $parent_prefix; $tables['ParentTableKey'] = $this->Application->getUnitOption($prefix, 'ParentTableKey'); }*/ $this->FinalRefs[ $tables['TableName'] ] = $tables['TableId']; // don't forget to add main table to FinalRefs too $sub_items = $this->Application->getUnitOption($prefix, 'SubItems', Array ()); /* @var $sub_items Array */ if ( is_array($sub_items) ) { foreach ($sub_items as $prefix) { $this->AddTables($prefix, $tables); } } $this->SetTables($tables); } /** * Searches through TempHandler tables info for required prefix * * @param string $prefix * @param Array $master * @return mixed */ function SearchTable($prefix, $master = null) { if (is_null($master)) { $master = $this->Tables; } if ($master['Prefix'] == $prefix) { return $master; } if (isset($master['SubTables'])) { foreach ($master['SubTables'] as $sub_table) { $found = $this->SearchTable($prefix, $sub_table); if ($found !== false) { return $found; } } } return false; } function AddTables($prefix, &$tables) { if ( !$this->Application->prefixRegistred($prefix) ) { // allows to skip subitem processing if subitem module not enabled/installed return ; } $tmp = Array( 'TableName' => $this->Application->getUnitOption($prefix,'TableName'), 'IdField' => $this->Application->getUnitOption($prefix,'IDField'), 'ForeignKey' => $this->Application->getUnitOption($prefix,'ForeignKey'), 'ParentPrefix' => $this->Application->getUnitOption($prefix, 'ParentPrefix'), 'ParentTableKey' => $this->Application->getUnitOption($prefix,'ParentTableKey'), 'Prefix' => $prefix, 'AutoClone' => $this->Application->getUnitOption($prefix,'AutoClone'), 'AutoDelete' => $this->Application->getUnitOption($prefix,'AutoDelete'), 'TableId' => $this->TableIdCounter++, ); $this->FinalRefs[ $tmp['TableName'] ] = $tmp['TableId']; $constrain = $this->Application->getUnitOption($prefix, 'Constrain'); if ( $constrain ) { $tmp['Constrain'] = $constrain; $this->FinalRefs[ $tmp['TableName'] . $tmp['Constrain'] ] = $tmp['TableId']; } $sub_items = $this->Application->getUnitOption($prefix, 'SubItems', Array ()); /* @var $sub_items Array */ if ( is_array($sub_items) ) { foreach ($sub_items as $prefix) { $this->AddTables($prefix, $tmp); } } if ( !is_array(getArrayValue($tables, 'SubTables')) ) { $tables['SubTables'] = Array (); } $tables['SubTables'][] = $tmp; } function CloneItems($prefix, $special, $ids, $master = null, $foreign_key = null, $parent_prefix = null, $skip_filenames = false) { if (!isset($master)) $master = $this->Tables; // recalling by different name, because we may get kDBList, if we recall just by prefix if (!preg_match('/(.*)-item$/', $special)) { $special .= '-item'; } - $object = $this->Application->recallObject($prefix.'.'.$special, $prefix, Array('skip_autoload' => true, 'parent_event' => &$this->parentEvent)); + $object = $this->Application->recallObject($prefix.'.'.$special, $prefix, Array('skip_autoload' => true, 'parent_event' => $this->parentEvent)); /* @var $object kCatDBItem */ $object->PopulateMultiLangFields(); foreach ($ids as $id) { $mode = 'create'; $cloned_ids = getArrayValue($this->AlreadyProcessed, $master['TableName']); if ( $cloned_ids ) { // if we have already cloned the id, replace it with cloned id and set mode to update // update mode is needed to update second ForeignKey for items cloned by first ForeignKey if ( getArrayValue($cloned_ids, $id) ) { $id = $cloned_ids[$id]; $mode = 'update'; } } $object->Load($id); $original_values = $object->GetFieldValues(); if (!$skip_filenames) { $object->NameCopy($master, $foreign_key); } elseif ($master['TableName'] == $this->MasterTable) { // kCatDBItem class only has this attribute $object->useFilenames = false; } if (isset($foreign_key)) { $master_foreign_key_field = is_array($master['ForeignKey']) ? $master['ForeignKey'][$parent_prefix] : $master['ForeignKey']; $object->SetDBField($master_foreign_key_field, $foreign_key); } if ($mode == 'create') { $this->RaiseEvent('OnBeforeClone', $master['Prefix'], $special, Array($object->GetId()), $foreign_key); } $object->inCloning = true; $res = $mode == 'update' ? $object->Update() : $object->Create(); $object->inCloning = false; if ($res) { if ( $mode == 'create' && is_array( getArrayValue($master, 'ForeignKey')) ) { // remember original => clone mapping for dual ForeignKey updating $this->AlreadyProcessed[$master['TableName']][$id] = $object->GetId(); } if ($mode == 'create') { $this->RaiseEvent('OnAfterClone', $master['Prefix'], $special, Array($object->GetId()), $foreign_key, array('original_id' => $id) ); $this->saveID($master['Prefix'], $special, $object->GetID()); } if ( is_array(getArrayValue($master, 'SubTables')) ) { foreach($master['SubTables'] as $sub_table) { if (!getArrayValue($sub_table, 'AutoClone')) continue; $sub_TableName = $object->IsTempTable() ? $this->GetTempName($sub_table['TableName']) : $sub_table['TableName']; $foreign_key_field = is_array($sub_table['ForeignKey']) ? $sub_table['ForeignKey'][$master['Prefix']] : $sub_table['ForeignKey']; $parent_key_field = is_array($sub_table['ParentTableKey']) ? $sub_table['ParentTableKey'][$master['Prefix']] : $sub_table['ParentTableKey']; if (!$foreign_key_field || !$parent_key_field) continue; $query = 'SELECT '.$sub_table['IdField'].' FROM '.$sub_TableName.' WHERE '.$foreign_key_field.' = '.$original_values[$parent_key_field]; if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain']; $sub_ids = $this->Conn->GetCol($query); if ( is_array(getArrayValue($sub_table, 'ForeignKey')) ) { // $sub_ids could containt newly cloned items, we need to remove it here // to escape double cloning $cloned_ids = getArrayValue($this->AlreadyProcessed, $sub_table['TableName']); if ( !$cloned_ids ) $cloned_ids = Array(); $new_ids = array_values($cloned_ids); $sub_ids = array_diff($sub_ids, $new_ids); } $parent_key = $object->GetDBField($parent_key_field); $this->CloneItems($sub_table['Prefix'], $special, $sub_ids, $sub_table, $parent_key, $master['Prefix']); } } } } if (!$ids) { $this->savedIDs[$prefix.($special ? '.' : '').$special] = Array(); } return $this->savedIDs[$prefix.($special ? '.' : '').$special]; } function DeleteItems($prefix, $special, $ids, $master=null, $foreign_key=null) { if ( !$ids ) { return; } if ( !isset($master) ) { $master = $this->Tables; } if ( strpos($prefix, '.') !== false ) { list($prefix, $special) = explode('.', $prefix, 2); } $prefix_special = rtrim($prefix . '.' . $special, '.'); //recalling by different name, because we may get kDBList, if we recall just by prefix $recall_prefix = $prefix_special . ($special ? '' : '.') . '-item'; - $object = $this->Application->recallObject($recall_prefix, $prefix, Array ('skip_autoload' => true, 'parent_event' => &$this->parentEvent)); + $object = $this->Application->recallObject($recall_prefix, $prefix, Array ('skip_autoload' => true, 'parent_event' => $this->parentEvent)); /* @var $object kDBItem */ foreach ($ids as $id) { $object->Load($id); $original_values = $object->GetFieldValues(); if ( !$object->Delete($id) ) { continue; } if ( is_array(getArrayValue($master, 'SubTables')) ) { foreach ($master['SubTables'] as $sub_table) { if ( !getArrayValue($sub_table, 'AutoDelete') ) { continue; } $sub_TableName = $object->IsTempTable() ? $this->GetTempName($sub_table['TableName']) : $sub_table['TableName']; $foreign_key_field = is_array($sub_table['ForeignKey']) ? getArrayValue($sub_table, 'ForeignKey', $master['Prefix']) : $sub_table['ForeignKey']; $parent_key_field = is_array($sub_table['ParentTableKey']) ? getArrayValue($sub_table, 'ParentTableKey', $master['Prefix']) : $sub_table['ParentTableKey']; if ( !$foreign_key_field || !$parent_key_field ) { continue; } $sql = 'SELECT ' . $sub_table['IdField'] . ' FROM ' . $sub_TableName . ' WHERE ' . $foreign_key_field . ' = ' . $original_values[$parent_key_field]; $sub_ids = $this->Conn->GetCol($sql); $parent_key = $object->GetDBField(is_array($sub_table['ParentTableKey']) ? $sub_table['ParentTableKey'][$prefix] : $sub_table['ParentTableKey']); $this->DeleteItems($sub_table['Prefix'], $special, $sub_ids, $sub_table, $parent_key); } } } } function DoCopyLiveToTemp($master, $ids, $parent_prefix=null) { // when two tables refers the same table as sub-sub-table, and ForeignKey and ParentTableKey are arrays // the table will be first copied by first sub-table, then dropped and copied over by last ForeignKey in the array // this should not do any problems :) if ( !preg_match("/.*\.[0-9]+/", $master['Prefix']) ) { if( $this->DropTempTable($master['TableName']) ) { $this->CreateTempTable($master['TableName']); } } if (is_array($ids)) { $ids = join(',', $ids); } $table_sig = $master['TableName'].(isset($master['Constrain']) ? $master['Constrain'] : ''); if ($ids != '' && !in_array($table_sig, $this->CopiedTables)) { if ( getArrayValue($master, 'ForeignKey') ) { if ( is_array($master['ForeignKey']) ) { $key_field = $master['ForeignKey'][$parent_prefix]; } else { $key_field = $master['ForeignKey']; } } else { $key_field = $master['IdField']; } $query = 'INSERT INTO '.$this->GetTempName($master['TableName']).' SELECT * FROM '.$master['TableName'].' WHERE '.$key_field.' IN ('.$ids.')'; if (isset($master['Constrain'])) $query .= ' AND '.$master['Constrain']; $this->Conn->Query($query); $this->CopiedTables[] = $table_sig; $query = 'SELECT '.$master['IdField'].' FROM '.$master['TableName'].' WHERE '.$key_field.' IN ('.$ids.')'; if (isset($master['Constrain'])) $query .= ' AND '.$master['Constrain']; $this->RaiseEvent( 'OnAfterCopyToTemp', $master['Prefix'], '', $this->Conn->GetCol($query) ); } if ( getArrayValue($master, 'SubTables') ) { foreach ($master['SubTables'] as $sub_table) { $parent_key = is_array($sub_table['ParentTableKey']) ? $sub_table['ParentTableKey'][$master['Prefix']] : $sub_table['ParentTableKey']; if (!$parent_key) continue; if ( $ids != '' && $parent_key != $key_field ) { $query = 'SELECT '.$parent_key.' FROM '.$master['TableName'].' WHERE '.$key_field.' IN ('.$ids.')'; $sub_foreign_keys = join(',', $this->Conn->GetCol($query)); } else { $sub_foreign_keys = $ids; } $this->DoCopyLiveToTemp($sub_table, $sub_foreign_keys, $master['Prefix']); } } } function GetForeignKeys($master, $sub_table, $live_id, $temp_id=null) { $mode = 1; //multi if (!is_array($live_id)) { $live_id = Array($live_id); $mode = 2; //single } if (isset($temp_id) && !is_array($temp_id)) $temp_id = Array($temp_id); if ( isset($sub_table['ParentTableKey']) ) { if ( is_array($sub_table['ParentTableKey']) ) { $parent_key_field = $sub_table['ParentTableKey'][$master['Prefix']]; } else { $parent_key_field = $sub_table['ParentTableKey']; } } else { $parent_key_field = $master['IdField']; } $cached = getArrayValue($this->FKeysCache, $master['TableName'].'.'.$parent_key_field); if ( $cached ) { if ( array_key_exists(serialize($live_id), $cached) ) { list($live_foreign_key, $temp_foreign_key) = $cached[serialize($live_id)]; if ($mode == 1) { return $live_foreign_key; } else { return Array($live_foreign_key[0], $temp_foreign_key[0]); } } } if ($parent_key_field != $master['IdField']) { $query = 'SELECT '.$parent_key_field.' FROM '.$master['TableName'].' WHERE '.$master['IdField'].' IN ('.join(',', $live_id).')'; $live_foreign_key = $this->Conn->GetCol($query); if (isset($temp_id)) { // because DoCopyTempToOriginal resets negative IDs to 0 in temp table (one by one) before copying to live $temp_key = $temp_id < 0 ? 0 : $temp_id; $query = 'SELECT '.$parent_key_field.' FROM '.$this->GetTempName($master['TableName']).' WHERE '.$master['IdField'].' IN ('.join(',', $temp_key).')'; $temp_foreign_key = $this->Conn->GetCol($query); } else { $temp_foreign_key = Array(); } } else { $live_foreign_key = $live_id; $temp_foreign_key = $temp_id; } $this->FKeysCache[$master['TableName'].'.'.$parent_key_field][serialize($live_id)] = Array($live_foreign_key, $temp_foreign_key); if ($mode == 1) { return $live_foreign_key; } else { return Array($live_foreign_key[0], $temp_foreign_key[0]); } } /** * Copies data from temp to live table and returns IDs of copied records * * @param Array $master * @param string $parent_prefix * @param Array $current_ids * @return Array * @access public */ public function DoCopyTempToOriginal($master, $parent_prefix = null, $current_ids = Array()) { if ( !$current_ids ) { $query = 'SELECT ' . $master['IdField'] . ' FROM ' . $this->GetTempName($master['TableName']); if ( isset($master['Constrain']) ) { $query .= ' WHERE ' . $master['Constrain']; } $current_ids = $this->Conn->GetCol($query); } $table_sig = $master['TableName'] . (isset($master['Constrain']) ? $master['Constrain'] : ''); if ($current_ids) { // delete all ids from live table - for MasterTable ONLY! // because items from Sub Tables get deteleted in CopySubTablesToLive !BY ForeignKey! if ( $master['TableName'] == $this->MasterTable ) { $this->RaiseEvent('OnBeforeDeleteFromLive', $master['Prefix'], '', $current_ids); $query = 'DELETE FROM ' . $master['TableName'] . ' WHERE ' . $master['IdField'] . ' IN (' . join(',', $current_ids) . ')'; $this->Conn->Query($query); } if ( getArrayValue($master, 'SubTables') ) { if ( in_array($table_sig, $this->CopiedTables) || $this->FinalRefs[$table_sig] != $master['TableId'] ) { return Array (); } foreach ($current_ids AS $id) { $this->RaiseEvent('OnBeforeCopyToLive', $master['Prefix'], '', Array ($id)); //reset negative ids to 0, so autoincrement in live table works fine if ( $id < 0 ) { $query = ' UPDATE ' . $this->GetTempName($master['TableName']) . ' SET ' . $master['IdField'] . ' = 0 WHERE ' . $master['IdField'] . ' = ' . $id; if ( isset($master['Constrain']) ) { $query .= ' AND ' . $master['Constrain']; } $this->Conn->Query($query); $id_to_copy = 0; } else { $id_to_copy = $id; } //copy current id_to_copy (0 for new or real id) to live table $query = ' INSERT INTO ' . $master['TableName'] . ' SELECT * FROM ' . $this->GetTempName($master['TableName']) . ' WHERE ' . $master['IdField'] . ' = ' . $id_to_copy; $this->Conn->Query($query); $insert_id = $id_to_copy == 0 ? $this->Conn->getInsertID() : $id_to_copy; $this->saveID($master['Prefix'], '', array ($id => $insert_id)); $this->RaiseEvent('OnAfterCopyToLive', $master['Prefix'], '', Array ($insert_id), null, Array ('temp_id' => $id)); $this->UpdateForeignKeys($master, $insert_id, $id); //delete already copied record from master temp table $query = ' DELETE FROM ' . $this->GetTempName($master['TableName']) . ' WHERE ' . $master['IdField'] . ' = ' . $id_to_copy; if ( isset($master['Constrain']) ) { $query .= ' AND ' . $master['Constrain']; } $this->Conn->Query($query); } $this->CopiedTables[] = $table_sig; // when all of ids in current master has been processed, copy all sub-tables data $this->CopySubTablesToLive($master, $current_ids); } elseif ( !in_array($table_sig, $this->CopiedTables) && ($this->FinalRefs[$table_sig] == $master['TableId']) ) { //If current master doesn't have sub-tables - we could use mass operations // We don't need to delete items from live here, as it get deleted in the beginning of the method for MasterTable // or in parent table processing for sub-tables $live_ids = Array (); $this->RaiseEvent('OnBeforeCopyToLive', $master['Prefix'], '', $current_ids); foreach ($current_ids as $an_id) { if ( $an_id > 0 ) { $live_ids[$an_id] = $an_id; // positive (already live) IDs will be copied in on query all togather below, // so we just store it here continue; } else { // zero or negative ids should be copied one by one to get their InsertId // resetting to 0 so it get inserted into live table with autoincrement $query = ' UPDATE ' . $this->GetTempName($master['TableName']) . ' SET ' . $master['IdField'] . ' = 0 WHERE ' . $master['IdField'] . ' = ' . $an_id; // constrain is not needed here because ID is already unique $this->Conn->Query($query); // copying $query = ' INSERT INTO ' . $master['TableName'] . ' SELECT * FROM ' . $this->GetTempName($master['TableName']) . ' WHERE ' . $master['IdField'] . ' = 0'; $this->Conn->Query($query); $live_ids[$an_id] = $this->Conn->getInsertID(); //storing newly created live id //delete already copied record from master temp table $query = ' DELETE FROM ' . $this->GetTempName($master['TableName']) . ' WHERE ' . $master['IdField'] . ' = 0'; $this->Conn->Query($query); $this->UpdateChangeLogForeignKeys($master, $live_ids[$an_id], $an_id); } } // copy ALL records to live table $query = ' INSERT INTO ' . $master['TableName'] . ' SELECT * FROM ' . $this->GetTempName($master['TableName']); if ( isset($master['Constrain']) ) { $query .= ' WHERE ' . $master['Constrain']; } $this->Conn->Query($query); $this->CopiedTables[] = $table_sig; $this->RaiseEvent('OnAfterCopyToLive', $master['Prefix'], '', $live_ids); $this->saveID($master['Prefix'], '', $live_ids); // no need to clear temp table - it will be dropped by next statement } } if ( $this->FinalRefs[ $master['TableName'] ] != $master['TableId'] ) { return Array (); } /*if ( is_array(getArrayValue($master, 'ForeignKey')) ) { //if multiple ForeignKeys if ( $master['ForeignKey'][$parent_prefix] != end($master['ForeignKey']) ) { return; // Do not delete temp table if not all ForeignKeys have been processed (current is not the last) } }*/ $this->DropTempTable($master['TableName']); $this->Application->resetCounters($master['TableName']); if ( !isset($this->savedIDs[ $master['Prefix'] ]) ) { $this->savedIDs[ $master['Prefix'] ] = Array (); } return $this->savedIDs[ $master['Prefix'] ]; } /** * Create separate connection for locking purposes * * @return kDBConnection */ function &_getSeparateConnection() { static $connection = null; if (!isset($connection)) { $connection = $this->Application->makeClass( 'kDBConnection', Array (SQL_TYPE, Array (&$this->Application, 'handleSQLError')) ); /* @var $connection kDBConnection */ $connection->debugMode = $this->Application->isDebugMode(); $connection->Connect(SQL_SERVER, SQL_USER, SQL_PASS, SQL_DB, true); } return $connection; } function UpdateChangeLogForeignKeys($master, $live_id, $temp_id) { if ($live_id == $temp_id) { return ; } $prefix = $master['Prefix']; $main_prefix = $this->Application->GetTopmostPrefix($prefix); $ses_var_name = $main_prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix); $changes = $this->Application->RecallVar($ses_var_name); $changes = $changes ? unserialize($changes) : Array (); foreach ($changes as $key => $rec) { if ($rec['Prefix'] == $prefix && $rec['ItemId'] == $temp_id) { // main item change log record $changes[$key]['ItemId'] = $live_id; } if ($rec['MasterPrefix'] == $prefix && $rec['MasterId'] == $temp_id) { // sub item change log record $changes[$key]['MasterId'] = $live_id; } if (in_array($prefix, $rec['ParentPrefix']) && $rec['ParentId'][$prefix] == $temp_id) { // parent item change log record $changes[$key]['ParentId'][$prefix] = $live_id; if (array_key_exists('DependentFields', $rec)) { // these are fields from table of $rec['Prefix'] table! // when one of dependent fields goes into idfield of it's parent item, that was changed $parent_table_key = $this->Application->getUnitOption($rec['Prefix'], 'ParentTableKey'); $parent_table_key = is_array($parent_table_key) ? $parent_table_key[$prefix] : $parent_table_key; if ($parent_table_key == $master['IdField']) { $foreign_key = $this->Application->getUnitOption($rec['Prefix'], 'ForeignKey'); $foreign_key = is_array($foreign_key) ? $foreign_key[$prefix] : $foreign_key; $changes[$key]['DependentFields'][$foreign_key] = $live_id; } } } } $this->Application->StoreVar($ses_var_name, serialize($changes)); } function UpdateForeignKeys($master, $live_id, $temp_id) { $this->UpdateChangeLogForeignKeys($master, $live_id, $temp_id); foreach ($master['SubTables'] as $sub_table) { $foreign_key_field = is_array($sub_table['ForeignKey']) ? getArrayValue($sub_table, 'ForeignKey', $master['Prefix']) : $sub_table['ForeignKey']; if (!$foreign_key_field) { continue; } list ($live_foreign_key, $temp_foreign_key) = $this->GetForeignKeys($master, $sub_table, $live_id, $temp_id); //Update ForeignKey in sub TEMP table if ($live_foreign_key != $temp_foreign_key) { $query = 'UPDATE '.$this->GetTempName($sub_table['TableName']).' SET '.$foreign_key_field.' = '.$live_foreign_key.' WHERE '.$foreign_key_field.' = '.$temp_foreign_key; if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain']; $this->Conn->Query($query); } } } function CopySubTablesToLive($master, $current_ids) { foreach ($master['SubTables'] as $sub_table) { $table_sig = $sub_table['TableName'].(isset($sub_table['Constrain']) ? $sub_table['Constrain'] : ''); // delete records from live table by foreign key, so that records deleted from temp table // get deleted from live if (count($current_ids) > 0 && !in_array($table_sig, $this->CopiedTables) ) { $foreign_key_field = is_array($sub_table['ForeignKey']) ? getArrayValue($sub_table, 'ForeignKey', $master['Prefix']) : $sub_table['ForeignKey']; if (!$foreign_key_field) continue; $foreign_keys = $this->GetForeignKeys($master, $sub_table, $current_ids); if (count($foreign_keys) > 0) { $query = 'SELECT '.$sub_table['IdField'].' FROM '.$sub_table['TableName'].' WHERE '.$foreign_key_field.' IN ('.join(',', $foreign_keys).')'; if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain']; if ( $this->RaiseEvent( 'OnBeforeDeleteFromLive', $sub_table['Prefix'], '', $this->Conn->GetCol($query), $foreign_keys ) ){ $query = 'DELETE FROM '.$sub_table['TableName'].' WHERE '.$foreign_key_field.' IN ('.join(',', $foreign_keys).')'; if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain']; $this->Conn->Query($query); } } } //sub_table passed here becomes master in the method, and recursively updated and copy its sub tables $this->DoCopyTempToOriginal($sub_table, $master['Prefix']); } } /** * Raises event using IDs, that are currently being processed in temp handler * * @param string $name * @param string $prefix * @param string $special * @param Array $ids * @param string $foreign_key * @param Array $add_params * @return bool * @access protected */ protected function RaiseEvent($name, $prefix, $special, $ids, $foreign_key = null, $add_params = null) { if ( !is_array($ids) ) { return true; } $event_key = $prefix . ($special ? '.' : '') . $special . ':' . $name; $event = new kEvent($event_key); if ( isset($foreign_key) ) { $event->setEventParam('foreign_key', $foreign_key); } $set_temp_id = ($name == 'OnAfterCopyToLive') && (!is_array($add_params) || !array_key_exists('temp_id', $add_params)); foreach ($ids as $index => $id) { $event->setEventParam('id', $id); if ( $set_temp_id ) { $event->setEventParam('temp_id', $index); } if ( is_array($add_params) ) { foreach ($add_params as $name => $val) { $event->setEventParam($name, $val); } } $this->Application->HandleEvent($event); } return $event->status == kEvent::erSUCCESS; } function DropTempTable($table) { if ( in_array($table, $this->DroppedTables) ) { return false; } $query = 'DROP TABLE IF EXISTS ' . $this->GetTempName($table); array_push($this->DroppedTables, $table); $this->DroppedTables = array_unique($this->DroppedTables); $this->Conn->Query($query); return true; } function PrepareEdit() { $this->DoCopyLiveToTemp($this->Tables, $this->Tables['IDs']); if ($this->Application->getUnitOption($this->Tables['Prefix'],'CheckSimulatniousEdit')) { $this->CheckSimultaniousEdit(); } } function SaveEdit($master_ids = Array()) { // SessionKey field is required for deleting records from expired sessions $conn =& $this->_getSeparateConnection(); $sleep_count = 0; do { // acquire lock $conn->ChangeQuery('LOCK TABLES '.TABLE_PREFIX.'Semaphores WRITE'); $sql = 'SELECT SessionKey FROM ' . TABLE_PREFIX . 'Semaphores WHERE (MainPrefix = ' . $conn->qstr($this->Tables['Prefix']) . ')'; $another_coping_active = $conn->GetOne($sql); if ($another_coping_active) { // another user is coping data from temp table to live -> release lock and try again after 1 second $conn->ChangeQuery('UNLOCK TABLES'); $sleep_count++; sleep(1); } } while ($another_coping_active && ($sleep_count <= 30)); if ($sleep_count > 30) { // another coping process failed to finished in 30 seconds $error_message = $this->Application->Phrase('la_error_TemporaryTableCopyingFailed'); $this->Application->SetVar('_temp_table_message', $error_message); return false; } // mark, that we are coping from temp to live right now, so other similar attempt (from another script) will fail $fields_hash = Array ( 'SessionKey' => $this->Application->GetSID(), 'Timestamp' => adodb_mktime(), 'MainPrefix' => $this->Tables['Prefix'], ); $conn->doInsert($fields_hash, TABLE_PREFIX.'Semaphores'); $semaphore_id = $conn->getInsertID(); // unlock table now to prevent permanent lock in case, when coping will end with SQL error in the middle $conn->ChangeQuery('UNLOCK TABLES'); $ids = $this->DoCopyTempToOriginal($this->Tables, null, $master_ids); // remove mark, that we are coping from temp to live $conn->Query('LOCK TABLES '.TABLE_PREFIX.'Semaphores WRITE'); $sql = 'DELETE FROM ' . TABLE_PREFIX . 'Semaphores WHERE SemaphoreId = ' . $semaphore_id; $conn->ChangeQuery($sql); $conn->ChangeQuery('UNLOCK TABLES'); return $ids; } function CancelEdit($master=null) { if (!isset($master)) $master = $this->Tables; $this->DropTempTable($master['TableName']); if ( getArrayValue($master, 'SubTables') ) { foreach ($master['SubTables'] as $sub_table) { $this->CancelEdit($sub_table); } } } /** * Checks, that someone is editing selected records and returns true, when no one. * * @param Array $ids * * @return bool */ function CheckSimultaniousEdit($ids = null) { $tables = $this->Conn->GetCol('SHOW TABLES'); $mask_edit_table = '/' . TABLE_PREFIX . 'ses_(.*)_edit_' . $this->MasterTable . '$/'; $my_sid = $this->Application->GetSID(); $my_wid = $this->Application->GetVar('m_wid'); $ids = implode(',', isset($ids) ? $ids : $this->Tables['IDs']); $sids = Array (); if (!$ids) { return true; } foreach ($tables as $table) { if ( preg_match($mask_edit_table, $table, $rets) ) { $sid = preg_replace('/(.*)_(.*)/', '\\1', $rets[1]); // remove popup's wid from sid if ($sid == $my_sid) { if ($my_wid) { // using popups for editing if (preg_replace('/(.*)_(.*)/', '\\2', $rets[1]) == $my_wid) { // don't count window, that is being opened right now continue; } } else { // not using popups for editing -> don't count my session tables continue; } } $sql = 'SELECT COUNT(' . $this->Tables['IdField'] . ') FROM ' . $table . ' WHERE ' . $this->Tables['IdField'] . ' IN (' . $ids . ')'; $found = $this->Conn->GetOne($sql); if (!$found || in_array($sid, $sids)) { continue; } $sids[] = $sid; } } if ($sids) { // detect who is it $sql = 'SELECT CONCAT(IF (s.PortalUserId = ' . USER_ROOT . ', \'root\', IF (s.PortalUserId = ' . USER_GUEST . ', \'Guest\', CONCAT(u.FirstName, \' \', u.LastName, \' (\', u.Username, \')\') ) ), \' IP: \', s.IpAddress, \'\') FROM ' . TABLE_PREFIX . 'UserSessions AS s LEFT JOIN ' . TABLE_PREFIX . 'Users AS u ON u.PortalUserId = s.PortalUserId WHERE s.SessionKey IN (' . implode(',', $sids) . ')'; $users = $this->Conn->GetCol($sql); if ($users) { $this->Application->SetVar('_simultanious_edit_message', sprintf($this->Application->Phrase('la_record_being_edited_by'), join(",\n", $users)) ); return false; } } return true; } } \ No newline at end of file Index: branches/5.2.x/core/kernel/utility/event.php =================================================================== --- branches/5.2.x/core/kernel/utility/event.php (revision 15143) +++ branches/5.2.x/core/kernel/utility/event.php (revision 15144) @@ -1,440 +1,441 @@ <?php /** * @version $Id$ * @package In-Portal * @copyright Copyright (C) 1997 - 2009 Intechnic. All rights reserved. * @license GNU/GPL * In-Portal is Open Source software. * This means that this software may have been modified pursuant * the GNU General Public License, and as distributed it includes * or is derivative of works licensed under the GNU General Public License * or other free or open source software licenses. * See http://www.in-portal.org/license for copyright notices and details. */ defined('FULL_PATH') or die('restricted access!'); final class kEvent extends kBase { /** * Event finished working succsessfully * */ const erSUCCESS = 0; /** * Event finished working, but result is unsuccsessfull * */ const erFAIL = -1; /** * Event experienced FATAL error - no hooks should continue! * */ const erFATAL = -2; /** * Event failed on internal permission checking (user has no permission) * */ const erPERM_FAIL = -3; /** * Event requested to stop processing (don't parse templates) * */ const erSTOP = -4; /** * Reference to event, that created given event * * @var kEvent * @access public */ public $MasterEvent; /** * Event name * * @var string * @access public */ public $Name; /** * Don't execute hooks, before event processing * * @var bool * @access public */ public $SkipBeforeHooks = false; /** * Don't execute hooks, after event processing * * @var bool * @access public */ public $SkipAfterHooks = false; /** * Perform redirect after event processing. * Redirect after event processing allows to prevent same event being present in resulting url. * Also could contain template name, that needs to be shown after redirect. * * @var mixed * @access public */ public $redirect = true; /** * Params, used during redirect url building after event successful processing * * @var bool * @access private */ private $redirectParams = Array (); /** * PHP file to redirect to. Defaults to "index.php" * * @var string * @access public */ public $redirectScript = null; /** * Event processing status * * @var int * @access public */ public $status = kEvent::erSUCCESS; /** * Event parameters * Usually indicate, how particular event should be processed. * * @var Array * @access private */ private $specificParams = Array (); /** * Pseudo class used, to create object, based on event contents * * @var string * @access private */ private $pseudoClass = ''; /** * Create event from given prefix, special, name and specific params. * Parameter $params could be be an an array with following keys: "prefix", "special" (optional), "name". * Parameter $params could be a string in format: "prefix:name" or "prefix.special:name". * * @param mixed $params * @param Array $specific_params event specific params (none by default) * @return kEvent * @access public */ public function __construct($params = Array(), $specific_params = null) { parent::__construct(); if ($params) { if ( is_array($params) ) { $prefix = isset($params['prefix']) ? $params['prefix'] : false; $special = isset($params['special']) ? $params['special'] : false; if ($prefix) { $this->Init($prefix, $special); } $this->Name = isset($params['name']) ? $params['name'] : ''; } elseif ( is_string($params) ) { if (preg_match('/([^.:]*)[.]{0,1}([^:]*):(.*)/', $params, $regs)) { $prefix = $regs[1]; $special = $regs[2]; if ($prefix) { $this->Init($prefix, $special); } $this->Name = $regs[3]; } else { throw new Exception('Invalid event string: <strong>' . $params . '</strong>. $params should be "prefix[.special]:OnEvent" format'); } } } if ( isset($specific_params) ) { $this->specificParams = $specific_params; } } /** * Returns joined prefix and special if any * * @param bool $from_submit if true, then joins prefix & special by "_", uses "." otherwise * @return string * @access public */ public function getPrefixSpecial($from_submit = false) { if (!$from_submit) { return parent::getPrefixSpecial(); } return rtrim($this->Prefix . '_' . $this->Special, '_'); } /** * Sets event parameter * * @param string $name * @param mixed $value * @access public */ public function setEventParam($name,$value) { $this->specificParams[$name] = $value; } /** * Returns event parameter by name (supports digging) * * @param string $name * @return mixed * @access public */ public function getEventParam($name) { $args = func_get_args(); if (count($args) > 1) { kUtil::array_unshift_ref($args, $this->specificParams); return call_user_func_array('getArrayValue', $args); // getArrayValue($this->specificParams, $name); } return array_key_exists($name, $this->specificParams) ? $this->specificParams[$name] : false; } /** * Returns all event parameters * * @return Array * @access public */ public function getEventParams() { return $this->specificParams; } /** * Set's pseudo class that differs from * the one specified in $Prefix * * @param string $appendix * @access public */ public function setPseudoClass($appendix) { $this->pseudoClass = $this->Prefix . $appendix; } /** * Performs event initialization * Also sets pseudo class same $prefix * * @param string $prefix * @param string $special * @access public */ public function Init($prefix, $special) { $this->pseudoClass = $prefix; parent::Init($prefix, $special); } /** * Returns object used in event * * @param Array $params * @return kDBBase * @access public */ public function &getObject(array $params = Array()) { if ( !$this->Application->hasObject($this->prefixSpecial) ) { - $top_event =& $this; + $top_event = $this; // when OnSave calls OnPreSave in first line, then this would make sure OnSave is used while ( is_object($top_event->MasterEvent) ) { - $top_event =& $top_event->MasterEvent; + $top_event = $top_event->MasterEvent; } - $params['parent_event'] =& $top_event; + $params['parent_event'] = $top_event; } $object = $this->Application->recallObject($this->prefixSpecial, $this->pseudoClass, $params); return $object; } /** * Executes given event in context of current event * Sub-event gets this event in "kEvent::MasterEvent" attribute. * Sub-event execution results (status and redirect* properties) are copied back to current event. * * @param string $name name of callable event (optionally could contain prefix_special as well) * @see kEvent::MasterEvent + * @todo Will overwrite master event data with called event data, which makes 'parent_event' useless in most cases */ public function CallSubEvent($name) { if ( strpos($name, ':') === false ) { // PrefixSpecial not specified -> use from current event $name = $this->getPrefixSpecial() . ':' . $name; } $child_event = new kEvent($name); $child_event->copyFrom($this, true); $this->Application->HandleEvent($child_event); $this->copyFrom($child_event); $this->specificParams = $child_event->specificParams; } /** * Allows to copy data between events * * @param kEvent $source_event * @param bool $inherit * @access public */ public function copyFrom(&$source_event, $inherit = false) { if ($inherit) { $this->MasterEvent =& $source_event; } else { $this->status = $source_event->status; } $this->redirect = $source_event->redirect; $this->redirectParams = $source_event->redirectParams; $this->redirectScript = $source_event->redirectScript; $this->specificParams = $source_event->specificParams; } /** * Returns all redirect parameters * * @return Array * @access public */ public function getRedirectParams() { return $this->redirectParams; } /** * Returns redirect parameter * * @param string $name * @return mixed * @access public */ public function getRedirectParam($name) { return array_key_exists($name, $this->redirectParams) ? $this->redirectParams[$name] : false; } /** * Set's redirect param for event * * @param string $name * @param string $value * @access public */ public function SetRedirectParam($name, $value) { $this->redirectParams[$name] = $value; } /** * Allows to merge passed redirect params hash with existing ones * * @param Array $params * @access public */ public function setRedirectParams($params) { // append new parameters to parameters set before $this->redirectParams = kUtil::array_merge_recursive($this->redirectParams, $params); } /** * Allows to tell if this event was called some how (e.g. subevent, hook) from event requested * * @param string $event_key event key in format [prefix[.special]:]event_name * @return bool * @access public */ public function hasAncestor($event_key) { if ( strpos($event_key, ':') === false ) { $event_key = $this->getPrefixSpecial() . ':' . $event_key; } return $this->Application->EventManager->eventRunning($event_key); } /** * Returns permission section associated with event * * @return string * @access public */ public function getSection() { $perm_section = $this->getEventParam('PermSection'); if ($perm_section) { return $perm_section; } // 1. get section by current top_prefix $top_prefix = $this->getEventParam('top_prefix'); if ($top_prefix == false) { $top_prefix = $this->Application->GetTopmostPrefix($this->Prefix, true); $this->setEventParam('top_prefix', $top_prefix); } $section = $this->Application->getUnitOption($top_prefix.'.main', 'PermSection'); // 2. check if this section has perm_prefix mapping to other prefix $sections_helper = $this->Application->recallObject('SectionsHelper'); /* @var $sections_helper kSectionsHelper */ $section_data =& $sections_helper->getSectionData($section); if ($section_data && isset($section_data['perm_prefix']) && $section_data['perm_prefix'] != $top_prefix) { $this->setEventParam('top_prefix', $section_data['perm_prefix']); $section = $this->Application->getUnitOption($section_data['perm_prefix'].'.main', 'PermSection'); } if (!$section) { throw new Exception('Permission <strong>section</strong> not specified for prefix <strong>' . $top_prefix . '</strong>'); } return $section; } public function __toString() { return $this->getPrefixSpecial() . ':' . $this->Name; } } \ No newline at end of file Index: branches/5.2.x/core/kernel/kbase.php =================================================================== --- branches/5.2.x/core/kernel/kbase.php (revision 15143) +++ branches/5.2.x/core/kernel/kbase.php (revision 15144) @@ -1,1084 +1,1084 @@ <?php /** * @version $Id$ * @package In-Portal * @copyright Copyright (C) 1997 - 2009 Intechnic. All rights reserved. * @license GNU/GPL * In-Portal is Open Source software. * This means that this software may have been modified pursuant * the GNU General Public License, and as distributed it includes * or is derivative of works licensed under the GNU General Public License * or other free or open source software licenses. * See http://www.in-portal.org/license for copyright notices and details. */ //defined('FULL_PATH') or die('restricted access!'); /** * Base * */ class kBase { /** * Reference to global kApplication instance * * @var kApplication * @access protected */ protected $Application = null; /** * Connection to database * * @var kDBConnection * @access protected */ protected $Conn = null; /** * Prefix, used during object creation * * @var string * @access public */ public $Prefix = ''; /** * Special, used during object creation * * @var string * @access public */ public $Special = ''; /** * Joined prefix and special, usually taken directly from * tag beeing processed, to use in kApplication::recallObject method * * @var string * @access protected * @see kApplication::recallObject */ protected $prefixSpecial = ''; /** * Set's references to kApplication and kDBConnection class instances * * @access public * @see kApplication * @see kDBConnection */ public function __construct() { $this->Application =& kApplication::Instance(); $this->Conn =& $this->Application->GetADODBConnection(); } /** * Set's prefix and special * * @param string $prefix * @param string $special * @access public */ public function Init($prefix, $special) { $prefix = explode('_', $prefix, 2); $this->Prefix = $prefix[0]; $this->Special = $special; $this->prefixSpecial = rtrim($this->Prefix . '.' . $this->Special, '.'); } /** * Returns prefix and special (when present) joined by a "." * * @return string * @access public */ public function getPrefixSpecial() { return $this->prefixSpecial; } } class kHelper extends kBase { /** * Performs helper initialization * * @access public */ public function InitHelper() { } /** * Append prefix and special to tag * params (get them from tagname) like * they were really passed as params * * @param string $prefix_special * @param Array $tag_params * @return Array * @access protected */ protected function prepareTagParams($prefix_special, $tag_params = Array()) { $parts = explode('.', $prefix_special); $ret = $tag_params; $ret['Prefix'] = $parts[0]; $ret['Special'] = count($parts) > 1 ? $parts[1] : ''; $ret['PrefixSpecial'] = $prefix_special; return $ret; } } abstract class kDBBase extends kBase { /** * Name of primary key field for the unit * * @var string * @access public * @see kDBBase::TableName */ public $IDField = ''; /** * Unit's database table name * * @var string * @access public */ public $TableName = ''; /** * Form name, used for validation * * @var string */ protected $formName = ''; /** * Final form configuration * * @var Array */ protected $formConfig = Array (); /** * SELECT, FROM, JOIN parts of SELECT query (no filters, no limit, no ordering) * * @var string * @access protected */ protected $SelectClause = ''; /** * Unit fields definitions (fields from database plus virtual fields) * * @var Array * @access protected */ protected $Fields = Array (); /** * Mapping between unit custom field IDs and their names * * @var Array * @access protected */ protected $customFields = Array (); /** * Unit virtual field definitions * * @var Array * @access protected * @see kDBBase::getVirtualFields() * @see kDBBase::setVirtualFields() */ protected $VirtualFields = Array (); /** * Fields that need to be queried using custom expression, e.g. IF(...) AS value * * @var Array * @access protected */ protected $CalculatedFields = Array (); /** * Fields that contain aggregated functions, e.g. COUNT, SUM, etc. * * @var Array * @access protected */ protected $AggregatedCalculatedFields = Array (); /** * Tells, that multilingual fields sould not be populated by default. * Can be overriden from kDBBase::Configure method * * @var bool * @access protected */ protected $populateMultiLangFields = false; /** * Event, that was used to create this object * * @var kEvent * @access protected */ protected $parentEvent = null; /** * Sets new parent event to the object * * @param kEvent $event * @return void * @access public */ public function setParentEvent($event) { - $this->parentEvent =& $event; + $this->parentEvent = $event; } /** * Set object' TableName to LIVE table, defined in unit config * * @access public */ public function SwitchToLive() { $this->TableName = $this->Application->getUnitOption($this->Prefix, 'TableName'); } /** * Set object' TableName to TEMP table created based on table, defined in unit config * * @access public */ public function SwitchToTemp() { $table_name = $this->Application->getUnitOption($this->Prefix, 'TableName'); $this->TableName = $this->Application->GetTempName($table_name, 'prefix:' . $this->Prefix); } /** * Checks if object uses temp table * * @return bool * @access public */ public function IsTempTable() { return $this->Application->IsTempTable($this->TableName); } /** * Sets SELECT part of list' query * * @param string $sql SELECT and FROM [JOIN] part of the query up to WHERE * @access public */ public function SetSelectSQL($sql) { $this->SelectClause = $sql; } /** * Returns object select clause without any transformations * * @return string * @access public */ public function GetPlainSelectSQL() { return $this->SelectClause; } /** * Returns SELECT part of list' query. * 1. Occurrences of "%1$s" and "%s" are replaced to kDBBase::TableName * 2. Occurrences of "%3$s" are replaced to temp table prefix (only for table, using TABLE_PREFIX) * * @param string $base_query given base query will override unit default select query * @param bool $replace_table replace all possible occurrences * @return string * @access public * @see kDBBase::replaceModePrefix */ public function GetSelectSQL($base_query = null, $replace_table = true) { if (!isset($base_query)) { $base_query = $this->SelectClause; } if (!$replace_table) { return $base_query; } $query = str_replace(Array('%1$s', '%s'), $this->TableName, $base_query); return $this->replaceModePrefix($query); } /** * Allows sub-stables to be in same mode as main item (e.g. LEFT JOINED ones) * * @param string $query * @return string * @access protected */ protected function replaceModePrefix($query) { $live_table = substr($this->Application->GetLiveName($this->TableName), strlen(TABLE_PREFIX)); if (preg_match('/'.preg_quote(TABLE_PREFIX, '/').'(.*)'.preg_quote($live_table, '/').'/', $this->TableName, $rets)) { // will only happen, when table has a prefix (like in K4) return str_replace('%3$s', $rets[1], $query); } // will happen, when K3 table without prefix is used return $query; } /** * Sets calculated fields * * @param Array $fields * @access public */ public function setCalculatedFields($fields) { $this->CalculatedFields = $fields; } /** * Adds calculated field declaration to object. * * @param string $name * @param string $sql_clause * @access public */ public function addCalculatedField($name, $sql_clause) { $this->CalculatedFields[$name] = $sql_clause; } /** * Returns required mixing of aggregated & non-aggregated calculated fields * * @param int $aggregated 0 - having + aggregated, 1 - having only, 2 - aggregated only * @return Array * @access public */ public function getCalculatedFields($aggregated = 1) { switch ($aggregated) { case 0: $fields = array_merge($this->CalculatedFields, $this->AggregatedCalculatedFields); break; case 1: $fields = $this->CalculatedFields; break; case 2: $fields = $this->AggregatedCalculatedFields; // TODO: never used break; default: $fields = Array(); break; } return $fields; } /** * Checks, that given field is a calculated field * * @param string $field * @return bool * @access public */ public function isCalculatedField($field) { return array_key_exists($field, $this->CalculatedFields); } /** * Insert calculated fields sql into query in place of %2$s, * return processed query. * * @param string $query * @param int $aggregated 0 - having + aggregated, 1 - having only, 2 - aggregated only * @return string * @access protected */ protected function addCalculatedFields($query, $aggregated = 1) { $fields = $this->getCalculatedFields($aggregated); if ($fields) { $sql = Array (); $fields = str_replace('%2$s', $this->Application->GetVar('m_lang'), $fields); foreach ($fields as $field_name => $field_expression) { $sql[] = '('.$field_expression.') AS `'.$field_name.'`'; } $sql = implode(',',$sql); return $this->Application->ReplaceLanguageTags( str_replace('%2$s', ','.$sql, $query) ); } return str_replace('%2$s', '', $query); } /** * Performs initial object configuration, which includes setting the following: * - primary key and table name * - field definitions (including field modifiers, formatters, default values) * * @param bool $populate_ml_fields create all ml fields from db in config or not * @param string $form_name form name for validation * @access public */ public function Configure($populate_ml_fields = null, $form_name = null) { if ( isset($populate_ml_fields) ) { $this->populateMultiLangFields = $populate_ml_fields; } $this->IDField = $this->Application->getUnitOption($this->Prefix, 'IDField'); $this->TableName = $this->Application->getUnitOption($this->Prefix, 'TableName'); $this->initForm($form_name); $this->defineFields(); $this->ApplyFieldModifiers(null, true); // should be called only after all fields definitions been set $this->prepareConfigOptions(); // this should go last, but before setDefaultValues, order is significant! // only set on first call of method if ( isset($populate_ml_fields) ) { $this->SetDefaultValues(); } } /** * Adjusts object according to given form name * * @param string $form_name * @return void * @access protected */ protected function initForm($form_name = null) { $forms = $this->Application->getUnitOption($this->Prefix, 'Forms', Array ()); $this->formName = $form_name; $this->formConfig = isset($forms['default']) ? $forms['default'] : Array (); if ( !$this->formName ) { return ; } if ( !isset($forms[$this->formName]) ) { trigger_error('Form "<strong>' . $this->formName . '</strong>" isn\'t declared in "<strong>' . $this->Prefix . '</strong>" unit config.', E_USER_NOTICE); } else { $this->formConfig = kUtil::array_merge_recursive($this->formConfig, $forms[$this->formName]); } } /** * Add field definitions from all possible sources * Used field sources: database fields, custom fields, virtual fields, calculated fields, aggregated calculated fields * * @access protected */ protected function defineFields() { $this->Fields = $this->getFormOption('Fields', Array ()); $this->customFields = $this->getFormOption('CustomFields', Array()); $this->setVirtualFields( $this->getFormOption('VirtualFields', Array ()) ); $calculated_fields = $this->getFormOption('CalculatedFields', Array()); $this->CalculatedFields = $this->getFieldsBySpecial($calculated_fields); $aggregated_calculated_fields = $this->getFormOption('AggregatedCalculatedFields', Array()); $this->AggregatedCalculatedFields = $this->getFieldsBySpecial($aggregated_calculated_fields); } /** * Returns form name, used for validation * * @return string */ public function getFormName() { return $this->formName; } /** * Reads unit (specified by $prefix) option specified by $option and applies form change to it * * @param string $option * @param mixed $default * @return string * @access public */ public function getFormOption($option, $default = false) { $ret = $this->Application->getUnitOption($this->Prefix, $option, $default); if ( isset($this->formConfig[$option]) ) { $ret = kUtil::array_merge_recursive($ret, $this->formConfig[$option]); } return $ret; } /** * Only exteracts fields, that match current object Special * * @param Array $fields * @return Array * @access protected */ protected function getFieldsBySpecial($fields) { if ( array_key_exists($this->Special, $fields) ) { return $fields[$this->Special]; } return array_key_exists('', $fields) ? $fields[''] : Array(); } /** * Sets aggeregated calculated fields * * @param Array $fields * @access public */ public function setAggregatedCalculatedFields($fields) { $this->AggregatedCalculatedFields = $fields; } /** * Set's field names from table from config * * @param Array $fields * @access public */ public function setCustomFields($fields) { $this->customFields = $fields; } /** * Returns custom fields information from table from config * * @return Array * @access public */ public function getCustomFields() { return $this->customFields; } /** * Set's fields information from table from config * * @param Array $fields * @access public */ public function setFields($fields) { $this->Fields = $fields; } /** * Returns fields information from table from config * * @return Array * @access public */ public function getFields() { return $this->Fields; } /** * Checks, that given field exists * * @param string $field * @return bool * @access public */ public function isField($field) { return array_key_exists($field, $this->Fields); } /** * Override field options with ones defined in submit via "field_modfiers" array (common for all prefixes) * * @param Array $field_modifiers * @param bool $from_submit * @return void * @access public * @author Alex */ public function ApplyFieldModifiers($field_modifiers = null, $from_submit = false) { $allowed_modifiers = Array ('required', 'multiple'); if ( $this->Application->isAdminUser ) { // can change upload dir on the fly (admin only!) $allowed_modifiers[] = 'upload_dir'; } if ( !isset($field_modifiers) ) { $field_modifiers = $this->Application->GetVar('field_modifiers'); if ( !$field_modifiers ) { // no field modifiers return; } $field_modifiers = getArrayValue($field_modifiers, $this->getPrefixSpecial()); } if ( !$field_modifiers ) { // no field modifiers for current prefix_special return; } $fields = $this->Application->getUnitOption($this->Prefix, 'Fields', Array ()); $virtual_fields = $this->Application->getUnitOption($this->Prefix, 'VirtualFields', Array ()); foreach ($field_modifiers as $field => $field_options) { foreach ($field_options as $option_name => $option_value) { if ( !in_array(strtolower($option_name), $allowed_modifiers) ) { continue; } if ( $from_submit ) { // there are no "lN_FieldName" fields, since ApplyFieldModifiers is // called before PrepareOptions method, which creates them $field = preg_replace('/^l[\d]+_(.*)/', '\\1', $field); } if ( $this->isVirtualField($field) ) { $virtual_fields[$field][$option_name] = $option_value; $this->SetFieldOption($field, $option_name, $option_value, true); } $fields[$field][$option_name] = $option_value; $this->SetFieldOption($field, $option_name, $option_value); } } $this->Application->setUnitOption($this->Prefix, 'Fields', $fields); $this->Application->setUnitOption($this->Prefix, 'VirtualFields', $virtual_fields); } /** * Set fields (+options) for fields that physically doesn't exist in database * * @param Array $fields * @access public */ public function setVirtualFields($fields) { if ($fields) { $this->VirtualFields = $fields; $this->Fields = array_merge($this->VirtualFields, $this->Fields); } } /** * Returns virtual fields * * @return Array * @access public */ public function getVirtualFields() { return $this->VirtualFields; } /** * Checks, that given field is a virtual field * * @param string $field * @return bool * @access public */ public function isVirtualField($field) { return array_key_exists($field, $this->VirtualFields); } /** * Performs additional initialization for field default values * * @access protected */ protected function SetDefaultValues() { foreach ($this->Fields as $field => $options) { if ( array_key_exists('default', $options) && $options['default'] === '#NOW#' ) { $this->Fields[$field]['default'] = adodb_mktime(); } } } /** * Overwrites field definition in unit config * * @param string $field * @param Array $options * @param bool $is_virtual * @access public */ public function SetFieldOptions($field, $options, $is_virtual = false) { if ($is_virtual) { $this->VirtualFields[$field] = $options; $this->Fields = array_merge($this->VirtualFields, $this->Fields); } else { $this->Fields[$field] = $options; } } /** * Changes/sets given option's value in given field definiton * * @param string $field * @param string $option_name * @param mixed $option_value * @param bool $is_virtual * @access public */ public function SetFieldOption($field, $option_name, $option_value, $is_virtual = false) { if ($is_virtual) { $this->VirtualFields[$field][$option_name] = $option_value; } $this->Fields[$field][$option_name] = $option_value; } /** * Returns field definition from unit config. * Also executes sql from "options_sql" field option to form "options" field option * * @param string $field * @param bool $is_virtual * @return Array * @access public */ public function GetFieldOptions($field, $is_virtual = false) { $property_name = $is_virtual ? 'VirtualFields' : 'Fields'; if ( !array_key_exists($field, $this->$property_name) ) { return Array (); } if (!$is_virtual) { if (!array_key_exists('options_prepared', $this->Fields[$field]) || !$this->Fields[$field]['options_prepared']) { // executes "options_sql" from field definition, only when field options are accessed (late binding) $this->PrepareFieldOptions($field); $this->Fields[$field]['options_prepared'] = true; } } return $this->{$property_name}[$field]; } /** * Returns field option * * @param string $field * @param string $option_name * @param bool $is_virtual * @param mixed $default * @return mixed * @access public */ public function GetFieldOption($field, $option_name, $is_virtual = false, $default = false) { $field_options = $this->GetFieldOptions($field, $is_virtual); if ( !$field_options && strpos($field, '#') === false ) { // we use "#FIELD_NAME#" as field for InputName tag in JavaScript, so ignore it $form_name = $this->getFormName(); trigger_error('Field "<strong>' . $field . '</strong>" is not defined' . ($form_name ? ' on "<strong>' . $this->getFormName() . '</strong>" form' : '') . ' in "<strong>' . $this->Prefix . '</strong>" unit config', E_USER_WARNING); return false; } return array_key_exists($option_name, $field_options) ? $field_options[$option_name] : $default; } /** * Returns formatted field value * * @param string $name * @param string $format * * @return string * @access protected */ public function GetField($name, $format = null) { $formatter_class = $this->GetFieldOption($name, 'formatter'); if ( $formatter_class ) { $value = ($formatter_class == 'kMultiLanguage') && !preg_match('/^l[0-9]+_/', $name) ? '' : $this->GetDBField($name); $formatter = $this->Application->recallObject($formatter_class); /* @var $formatter kFormatter */ return $formatter->Format($value, $name, $this, $format); } return $this->GetDBField($name); } /** * Returns unformatted field value * * @param string $field * @return string * @access protected */ abstract protected function GetDBField($field); /** * Checks of object has given field * * @param string $name * @return bool * @access protected */ abstract protected function HasField($name); /** * Returns field values * * @return Array * @access protected */ abstract protected function GetFieldValues(); /** * Populates values of sub-fields, based on formatters, set to mater fields * * @param Array $fields * @access public * @todo Maybe should not be publicly accessible */ public function UpdateFormattersSubFields($fields = null) { if ( !is_array($fields) ) { $fields = array_keys($this->Fields); } foreach ($fields as $field) { if ( isset($this->Fields[$field]['formatter']) ) { $formatter = $this->Application->recallObject($this->Fields[$field]['formatter']); /* @var $formatter kFormatter */ $formatter->UpdateSubFields($field, $this->GetDBField($field), $this->Fields[$field], $this); } } } /** * Use formatters, specified in field declarations to perform additional field initialization in unit config * * @access protected */ protected function prepareConfigOptions() { $field_names = array_keys($this->Fields); foreach ($field_names as $field_name) { if ( !array_key_exists('formatter', $this->Fields[$field_name]) ) { continue; } $formatter = $this->Application->recallObject( $this->Fields[$field_name]['formatter'] ); /* @var $formatter kFormatter */ $formatter->PrepareOptions($field_name, $this->Fields[$field_name], $this); } } /** * Escapes fields only, not expressions * * @param string $field_expr * @return string * @access protected */ protected function escapeField($field_expr) { return preg_match('/[.(]/', $field_expr) ? $field_expr : '`' . $field_expr . '`'; } /** * Replaces current language id in given field options * * @param string $field_name * @param Array $field_option_names * @access protected */ protected function _replaceLanguageId($field_name, $field_option_names) { // don't use GetVar('m_lang') since it's always equals to default language on editing form in admin $current_language_id = $this->Application->Phrases->LanguageId; $primary_language_id = $this->Application->GetDefaultLanguageId(); $field_options =& $this->Fields[$field_name]; foreach ($field_option_names as $option_name) { $field_options[$option_name] = str_replace('%2$s', $current_language_id, $field_options[$option_name]); $field_options[$option_name] = str_replace('%3$s', $primary_language_id, $field_options[$option_name]); } } /** * Transforms "options_sql" field option into valid "options" array for given field * * @param string $field_name * @access protected */ protected function PrepareFieldOptions($field_name) { $field_options =& $this->Fields[$field_name]; if (array_key_exists('options_sql', $field_options) ) { // get options based on given sql $replace_options = Array ('option_title_field', 'option_key_field', 'options_sql'); $this->_replaceLanguageId($field_name, $replace_options); $select_clause = $this->escapeField($field_options['option_title_field']) . ',' . $this->escapeField($field_options['option_key_field']); $sql = sprintf($field_options['options_sql'], $select_clause); if (array_key_exists('serial_name', $field_options)) { // try to cache option sql on serial basis $cache_key = 'sql_' . crc32($sql) . '[%' . $field_options['serial_name'] . '%]'; $dynamic_options = $this->Application->getCache($cache_key); if ($dynamic_options === false) { $this->Conn->nextQueryCachable = true; $dynamic_options = $this->Conn->GetCol($sql, preg_replace('/^.*?\./', '', $field_options['option_key_field'])); $this->Application->setCache($cache_key, $dynamic_options); } } else { // don't cache options sql $dynamic_options = $this->Conn->GetCol($sql, preg_replace('/^.*?\./', '', $field_options['option_key_field'])); } $options_hash = array_key_exists('options', $field_options) ? $field_options['options'] : Array (); $field_options['options'] = kUtil::array_merge_recursive($options_hash, $dynamic_options); // because of numeric keys } } /** * Returns ID of currently processed record * * @return int * @access public */ public function GetID() { return $this->GetDBField($this->IDField); } /** * Allows kDBTagProcessor.SectionTitle to detect if it's editing or new item creation * * @return bool * @access public */ public function IsNewItem() { return $this->GetID() ? false : true; } /** * Returns parent table information * * @param string $special special of main item * @param bool $guess_special if object retrieved with specified special is not loaded, then try not to use special * @return Array * @access public */ public function getLinkedInfo($special = '', $guess_special = false) { $parent_prefix = $this->Application->getUnitOption($this->Prefix, 'ParentPrefix'); if ($parent_prefix) { // if this is linked table, then set id from main table $table_info = Array ( 'TableName' => $this->Application->getUnitOption($this->Prefix,'TableName'), 'IdField' => $this->Application->getUnitOption($this->Prefix,'IDField'), 'ForeignKey' => $this->Application->getUnitOption($this->Prefix,'ForeignKey'), 'ParentTableKey' => $this->Application->getUnitOption($this->Prefix,'ParentTableKey'), 'ParentPrefix' => $parent_prefix ); if (is_array($table_info['ForeignKey'])) { $table_info['ForeignKey'] = getArrayValue($table_info, 'ForeignKey', $parent_prefix); } if (is_array($table_info['ParentTableKey'])) { $table_info['ParentTableKey'] = getArrayValue($table_info, 'ParentTableKey', $parent_prefix); } $main_object = $this->Application->recallObject($parent_prefix.'.'.$special, null, Array ('raise_warnings' => 0)); /* @var $main_object kDBItem */ if (!$main_object->isLoaded() && $guess_special) { $main_object = $this->Application->recallObject($parent_prefix); } return array_merge($table_info, Array('ParentId'=> $main_object->GetDBField( $table_info['ParentTableKey'] ) ) ); } return false; } /** * Returns true, when list/item was queried/loaded * * @return bool * @access protected */ abstract protected function isLoaded(); /** * Returns specified field value from all selected rows. * Don't affect current record index * * @param string $field * @return Array * @access protected */ abstract protected function GetCol($field); } \ No newline at end of file