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;
}
/**
* Returns unit config, used in tag
*
* @return kUnitConfig
* @access public
*/
public function getUnitConfig()
{
return $this->Application->getUnitConfig($this->Prefix);
}
/**
* Creates string representation of a class (for logging)
*
* @return string
* @access public
*/
public function __toString()
{
$ret = 'ClassName: ' . get_class($this);
try {
$ret .= '; PrefixSpecial: ' . $this->getPrefixSpecial();
} catch (Exception $e) {}
return $ret;
}
}
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 ();
/**
* Fields, that have current time as their default value.
*
* @var array
*/
protected $currentTimeFields = 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;
}
/**
* Set object' TableName to LIVE table, defined in unit config
*
* @access public
*/
public function SwitchToLive()
{
$this->TableName = $this->getUnitConfig()->getTableName();
}
/**
* Set object' TableName to TEMP table created based on table, defined in unit config
*
* @access public
*/
public function SwitchToTemp()
{
$table_name = $this->getUnitConfig()->getTableName();
$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 ();
// inside calculated field "%2$s" is current language
$fields = str_replace('%2$s', $this->Application->GetVar('m_lang'), $fields);
// can't use "%3$s" as usual, because it's already populated in kDBBase::replaceModePrefix() across whole query
$fields = str_replace('%4$s', $this->Application->GetDefaultLanguageId(), $fields);
foreach ($fields as $field_name => $field_expression) {
$sql[] = '(' . $field_expression . ') AS `' . $field_name . '`';
}
$sql = implode(',', $sql);
// inside sql "%2$s" is placeholder for calculated fields
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;
}
$config = $this->getUnitConfig();
$this->IDField = $config->getIDField();
$this->TableName = $config->getTableName();
$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)
{
$config = $this->getUnitConfig();
$this->formName = $form_name;
$this->formConfig = $config->getFormByName('default', Array ());
if ( !$this->formName ) {
return ;
}
$form_data = $config->getFormByName($this->formName);
if ( $form_data === false ) {
trigger_error('Form "' . $this->formName . '" isn\'t declared in "' . $this->Prefix . '" unit config.', E_USER_NOTICE);
}
else {
$this->formConfig = kUtil::array_merge_recursive($this->formConfig, $form_data);
}
}
/**
* 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->getUnitConfig()->getSetting($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;
}
$config = $this->getUnitConfig();
$fields = $config->getFields(Array ());
$virtual_fields = $config->getVirtualFields(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);
}
}
$config->setFields($fields);
$config->setVirtualFields($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) ) {
if ( $options['default'] === '#NOW#' ) {
$this->currentTimeFields[] = $field;
}
if ( in_array($field, $this->currentTimeFields) ) {
$this->Fields[$field]['default'] = time();
}
}
}
}
/**
* 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 "' . $field . '" is not defined' . ($form_name ? ' on "' . $this->getFormName() . '" form' : '') . ' in "' . $this->Prefix . '" 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);
/** @var kFormatter $formatter */
$formatter = $this->Application->recallObject($formatter_class);
return $formatter->Format($value, $name, $this, $format);
}
return $this->GetDBField($name);
}
/**
* Returns unformatted field value
*
* @param string $field
* @return string
* @access public
*/
abstract public function GetDBField($field);
/**
* Checks of object has given field
*
* @param string $name
* @return bool
* @access public
*/
abstract public function HasField($name);
/**
* Returns field values
*
* @return Array
* @access public
*/
abstract public 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']) ) {
/** @var kFormatter $formatter */
$formatter = $this->Application->recallObject($this->Fields[$field]['formatter']);
$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;
}
/** @var kFormatter $formatter */
$formatter = $this->Application->recallObject( $this->Fields[$field_name]['formatter'] );
$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)
{
$config = $this->getUnitConfig();
$parent_prefix = $config->getParentPrefix();
if ( $parent_prefix ) {
// if this is linked table, then set id from main table
$table_info = Array (
'TableName' => $config->getTableName(),
'IdField' => $config->getIDField(),
'ForeignKey' => $config->getForeignKey($parent_prefix),
'ParentTableKey' => $config->getParentTableKey($parent_prefix),
'ParentPrefix' => $parent_prefix
);
/** @var kDBItem $main_object */
$main_object = $this->Application->recallObject($parent_prefix . '.' . $special, null, Array ('raise_warnings' => 0));
if ( !$main_object->isLoaded() && $guess_special ) {
$main_object = $this->Application->recallObject($parent_prefix);
}
$table_info['ParentId'] = $main_object->GetDBField($table_info['ParentTableKey']);
return $table_info;
}
return false;
}
/**
* Returns true, when list/item was queried/loaded
*
* @return bool
* @access public
*/
abstract public function isLoaded();
/**
* Returns specified field value from all selected rows.
* Don't affect current record index
*
* @param string $field
* @return Array
* @access public
*/
abstract public function GetCol($field);
}
/**
* Base class for exceptions, that trigger redirect action once thrown
*/
class kRedirectException extends Exception {
/**
* Redirect template
*
* @var string
* @access protected
*/
protected $template = '';
/**
* Redirect params
*
* @var Array
* @access protected
*/
protected $params = Array ();
/**
* Creates redirect exception
*
* @param string $message
* @param int $code
* @param Exception $previous
*/
public function __construct($message = '', $code = 0, $previous = NULL)
{
parent::__construct($message, $code, $previous);
}
/**
* Initializes exception
*
* @param string $template
* @param Array $params
* @return void
* @access public
*/
public function setup($template, $params = Array ())
{
$this->template = $template;
$this->params = $params;
}
/**
* Display exception details in debugger (only useful, when DBG_REDIRECT is enabled) and performs redirect
*
* @return void
* @access public
*/
public function run()
{
$application =& kApplication::Instance();
if ( $application->isDebugMode() ) {
$application->Debugger->appendException($this);
}
$application->Redirect($this->template, $this->params);
}
}
/**
* Exception, that is thrown when user don't have permission to perform requested action
*/
class kNoPermissionException extends kRedirectException {
}