Your IP : 216.73.216.43


Current Path : /home/cbequiperp/www/administrator/components/com_flexicontent/models/base/
Upload File :
Current File : /home/cbequiperp/www/administrator/components/com_flexicontent/models/base/base.php

<?php
/**
 * @package         FLEXIcontent
 * @version         3.3
 *
 * @author          Emmanuel Danan, Georgios Papadakis, Yannick Berges, others, see contributor page
 * @link            https://flexicontent.org
 * @copyright       Copyright © 2018, FLEXIcontent team, All Rights Reserved
 * @license         http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 */

defined('_JEXEC') or die('Restricted access');

use Joomla\String\StringHelper;
use Joomla\Utilities\ArrayHelper;
use Joomla\CMS\Table\Table;

jimport('legacy.model.admin');
require_once('traitbase.php');

/**
 * FLEXIcontent Component BASE (form) Model
 *
 */
abstract class FCModelAdmin extends JModelAdmin
{
	use FCModelTraitBase;

	/**
	 * Array of URL variable names that should be tried for getting record id
	 *
	 * @var array
	 */
	var $record_keys = array('id', 'cid');

	/**
	 * Record name, (parent class property), this is used for: naming session data, XML file of class, etc
	 *
	 * @var string
	 */
	protected $name = null;

	/**
	 * Record database table
	 *
	 * @var string
	 */
	var $records_dbtbl = null;

	/**
	 * Record jtable name
	 *
	 * @var string
	 */
	var $records_jtable = null;

	/**
	 * Column names
	 */
	var $state_col   = null;
	var $name_col    = null;
	var $parent_col  = null;

	/**
	 * Record primary key
	 *
	 * @var int
	 */
	var $_id = null;

	/**
	 * Record data
	 *
	 * @var object
	 */
	var $_record = null;

	/**
	 * Events context to use during model FORM events and diplay PREPARE events triggering
	 *
	 * @var object
	 */
	var $events_context = null;

	/**
	 * Record's type alias string
	 *
	 * @var        string
	 */
	var $type_alias = null;

	/**
	 * Flag to indicate adding new records with next available ordering (at the end),
	 * this is ignored if this record DB model does not have 'ordering'
	 *
	 * @var boolean
	 */
	var $useLastOrdering = false;

	/**
	 * Plugin group used to trigger events
	 *
	 * @var boolean
	 */
	var $plugins_group = null;

	/**
	 * Records real extension
	 *
	 * @var string
	 */
	var $extension_proxy = null;

	/**
	 * Context to use for registering (language) associations
	 *
	 * @var string
	 */
	var $associations_context = false;

	/**
	 * A message queue when appropriate
	 *
	 * @var string
	 */
	var $_messages= array();

	/**
	 * Various record specific properties
	 *
	 */

	/**
	 * List filters that are always applied
	 */
	var $hard_filters = array();

	/**
	 * Array of supported state conditions of the record
	 */
	var $supported_conditions = array(
		 1 => 'FLEXI_PUBLISHED',
		 0 => 'FLEXI_UNPUBLISHED',
		 2 => 'FLEXI_ARCHIVED',
		-2 => 'FLEXI_TRASHED'
	);

	/**
	 * Groups of Fields that can be partially present in the form
	 */
	var $mergeableGroups = array();


	/**
	 * Constructor
	 *
	 * @since 3.3.0
	 */
	public function __construct($config = array())
	{
		parent::__construct($config);

		// Make sure this is correct if called from different component ...
		$this->option = 'com_flexicontent';

		// Initialize using default naming if not already set
		$this->records_dbtbl  = $this->records_dbtbl  ?: 'flexicontent_' . $this->getName() . 's';
		$this->records_jtable = $this->records_jtable ?: 'flexicontent_' . $this->getName() . 's';

		$this->events_context = $this->events_context ?: $this->option . '.' . $this->getName();
		$this->type_alias     = $this->type_alias     ?: $this->option . '.' . $this->getName();

		$jinput = JFactory::getApplication()->input;
		$pk = null;

		// Try all key in prefered order
 		foreach($this->record_keys as $key)
 		{
			if ($pk === null)
			{
				$id = $jinput->get($key, array(), 'array');
				if (count($id))
				{
					$id = ArrayHelper::toInteger($id);
					$pk = (int) $id[0];
				}
			}
		}

		// Finally try getting id from JForm submitted data
		if ($pk === null)
		{
			$data = $jinput->get('jform', array(), 'array');
			$pk = isset($data['id']) ? (int) $data['id'] : 0;
		}

		$pk = $pk ?: 0;
		$this->setId($pk);
	}


	/**
	 * Method to set the record identifier
	 *
	 * @param		int	    $id        record identifier
	 *
	 * @since	3.3.0
	 */
	public function setId($id)
	{
		// Set record id and wipe data, if setting a different ID
		if ($this->_id != $id)
		{
			$this->_id     = (int) $id;
			$this->_record = null;
			$this->setState($this->getName() . '.id', $this->_id);
		}
	}


	/**
	 * Method to get the record identifier
	 *
	 * @return	int record identifier
	 */
	public function getId()
	{
		return $this->_id;
	}


	/**
	 * Overridden get method to get properties from the record
	 *
	 * @param	string	$property	The name of the property
	 * @param	mixed	$value		The value of the property to set
	 *
	 * @return 	mixed 				The value of the property
	 *
	 * @since	1.0
	 */
	public function get($property, $default=null)
	{
		if ($this->_record || $this->_loadRecord())
		{
			if (isset($this->_record->$property))
			{
				return $this->_record->$property;
			}
		}
		return $default;
	}


	/**
	 * Overridden set method to pass properties on to the record
	 *
	 * @param	  string	 $property	 The name of the property
	 * @param	  mixed	   $value		   The value of the property to set
	 *
	 * @return	boolean  True on success
	 *
	 * @since	1.5
	 */
	public function set($property, $value=null)
	{
		if ($this->_record || $this->_loadRecord())
		{
			$this->_record->$property = $value;
			return true;
		}

		return false;
	}


	/**
	 * Set method to pass properties on to the model object
	 *
	 * @param	  string	 $property	 The name of the property
	 * @param	  mixed	   $value		   The value of the property to set
	 *
	 * @return	void
	 *
	 * @since	3.2
	 */
	public function setProperty($property, $value=null)
	{
		$this->$property = $value;
	}


	/**
	 * Method to get a record via an alternative way that allows using multiple property values
	 *
	 * @return	object
	 *
	 * @since	1.0
	 */
	public function getRecord($pk = null)
	{
		if ($this->_loadRecord($pk))
		{
		}
		else
		{
			$this->_initRecord();
		}

		return $this->_record;
	}


	/**
	 * Method to load record data
	 *
	 * @return	boolean	True on success
	 *
	 * @since	1.0
	 */
	protected function _loadRecord($pk = null)
	{
		$table = $this->getTable();

		if (is_array($pk))
		{
			$data = $pk;
		}
		elseif ($pk)
		{
			/**
			 * If PK is not an integer, then possibly we were given a name,
			 * try to match it with columns 'name' or 'alias', if they exist
			 */
			if (!is_integer($pk))
			{
				$pk_col = null;
				$columns = array('name', 'alias');

				foreach($columns as $column)
				{
					$pk_col = !$pk_col && property_exists($table, $column) ? $column : $pk_col;
				}
			}

			/**
			 * Either use the 'name' / 'alias' / ... column, or use the primary column value
			 */
			$data = !empty($pk_col)
				? array($pk_col => (string) $pk)
				: array($table->getKeyName() => (int) $pk);
		}

		/**
		 * Lets load the record if not already loaded
		 */
		 
		if (!empty($data))
		{
			$data_matches = $data && (boolean) $this->_record;

			foreach ($data as $k => $v)
			{
				$data_matches = $data_matches && (string) $this->_record->$k === (string) $v;
			}

			if (!$data_matches)
			{
				$this->_record = false;
				
				// Attempt to load the row.
				$return = $table->load($data);

				// Check for a table object error.
				if ($return === false && $table->getError())
				{
					$this->setError($table->getError());
					return false;
				}

				// Set record
				$this->_id = $table->id;
				$this->_record = $table;

				// Extra steps after loading
				$this->_afterLoad($this->_record);
			}
		}

		return (boolean) $this->_record;
	}


	/**
	 * Method to initialise the record data
	 *
	 * @param   object      $record    The record being initialized
	 * @param   boolean     $initOnly  If true then only a new record will be initialized without running the _afterLoad() method
	 * @return	boolean	True on success
	 * @since	1.5
	 */
	protected function _initRecord(&$record = null, $initOnly = false)
	{
		// Initialize a given record object
		if ($record) ;

		// Only initialize MEMBER property '_record' if it is not already an object
		else if ( is_object($this->_record) )
		{
			$record = $this->_record;
			return true;
		}

		else
		{
			// Load a JTable object with all db columns as properties, then customize some or all the properites
			$record = $this->getTable();
		}

		// Set some new record specific properties, note most properties already have proper values
		// Either the DB default values (set by getTable() method) or the values set by _afterLoad() method
		// ...
		$this->_record = $record;

		// Extra steps after loading
		if ( !$initOnly )
		{
			$this->_afterLoad($this->_record);
		}

		return true;
	}


	/**
	 * Method to checkin/unlock the record
	 *
	 * @access	public
	 * @param	  int    $pk   The record id to checkin (unlock)
	 * @return	boolean	True on success
	 * @since	1.0
	 */
	function checkin($pk = NULL)
	{
		if (!$pk) $pk = $this->_id;

		if ($pk)
		{
			$tbl = $this->getTable();
			return $tbl->checkin($pk);
		}
		return false;
	}


	/**
	 * Method to checkout/lock the record
	 *
	 * @access	public
	 * @param	  int    $pk   The record id to checkin (unlock)
	 * @return	boolean	True on success
	 * @since	1.0
	 */
	function checkout($pk = null)
	{
		// Make sure we have a record id to checkout the record with
		if ( !$pk ) $pk = $this->_id;
		if ( !$pk ) return true;

		// Get current user
		$user	= JFactory::getUser();
		$uid	= $user->get('id');

		// Lets get table record and checkout the it
		$tbl = $this->getTable();
		if ( $tbl->checkout($uid, $this->_id) ) return true;

		// Reaching this points means checkout failed
		$this->setError( JText::_("FLEXI_ALERT_CHECKOUT_FAILED") . ' : ' . $tbl->getError() );
		return false;
	}


	/**
	 * Tests if the record is checked out
	 *
	 * @access	public
	 *
	 * @param	int	A user id
	 *
	 * @return	boolean	True if checked out
	 *
	 * @since   3.2.0
	 */
	public function isCheckedOut( $uid=0 )
	{
		if ($this->_id < 1)  return false;

		if ($this->_record || $this->_loadRecord())
		{
			if ($uid) {
				return ($this->_record->checked_out && $this->_record->checked_out != $uid);
			} else {
				return $this->_record->checked_out;
			}
		}
		else
		{
			$this->setError('Unable to Load Data');
			return false;
		}
	}


	/**
	 * Legacy method to store the record, use save() instead
	 *
	 * @param   array  $data  The form data.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   3.2.0
	 */
	public function store($data)
	{
		return $this->save($data);
	}


	/**
	 * Method to save the record
	 *
	 * @param   array  $data  The form data.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   3.2.0
	 */
	public function save($data)
	{
		// Initialise variables
		$app        = JFactory::getApplication();
		$dispatcher = JEventDispatcher::getInstance();

		// Note that 'data' is typically post['jform'] and it is validated by the caller e.g. the controller
		if (is_object($data))
		{
			$data = (array) $data;
		}

		// Get record 's primary key and set the 'isNew' Flag
		$pk = !empty($data['id'])
			? (int) $data['id']
			: (int) $this->getState($this->getName() . '.id');
		$isNew = true;

		// Include the plugins for the on save events.
		if ($this->plugins_group)
		{
			JPluginHelper::importPlugin($this->plugins_group);
		}

		// Get a JTable object
		$record = $this->getTable();

		// Load data of existing record to allow maintaining any not-set properties
		if ($pk > 0)
		{
			$record->load($pk);
			$isNew = false;
		}
		else
		{
			$record->reset();
		}

		// Extra steps after loading record, and before calling JTable::bind()
		if ($this->_prepareBind($record, $data) === false)
		{
			// Just return, the error was set already
			return false;
		}

		// Bind data to the jtable
		if (!$record->bind($data))
		{
			$this->setError($record->getError());
			return false;
		}

		// Make sure the data is valid
		if (!$record->check())
		{
			$this->setError($record->getError());
			return false;
		}

		// Trigger the before save event (typically: onContentBeforeSave)
		$results = FLEXI_J40GE
			? $app->triggerEvent($this->event_before_save, array($this->option . '.' . $this->name, &$record, $isNew, $data))
			: $dispatcher->trigger($this->event_before_save, array($this->option . '.' . $this->name, &$record, $isNew, $data));

		// Abort record store if any plugin returns a result === false
		if (is_array($results) && in_array(false, $results, true))
		{
			$this->setError($record->getError());
			return false;
		}

		// Store data in the db
		if (!$record->store())
		{
			$this->setError($record->getError());
			return false;
		}

		// Saving asset was handled by the JTable:store() of this CLASS model
		// ...

		$this->_record = $record;			 // Get the new / updated record object
		$this->_id     = $record->id;  // Get id of newly created records
		$this->setState($this->getName() . '.id', $record->id);  // Set new id into state

		// Update language Associations
		$this->saveAssociations($record, $data);

		// Trigger the after save event (typically: onContentAfterSave)
		$results = FLEXI_J40GE
			? $app->triggerEvent($this->event_after_save, array($this->option . '.' . $this->name, &$record, $isNew, $data))
			: $dispatcher->trigger($this->event_after_save, array($this->option . '.' . $this->name, &$record, $isNew, $data));

		// Abort further actions if any plugin returns a result === false
		/*if (is_array($results) && in_array(false, $results, true))
		{
			$this->setError($record->getError());
			return false;
		}*/

		// Extra steps after loading record, and before calling JTable::bind()
		$this->_afterStore($record, $data);

		// Clear the cache
		$this->cleanCache(null, 0);
		$this->cleanCache(null, 1);

		return true;
	}


	/**
	 * Method to get the record form.
	 *
	 * @param   array    $data      Data for the form.
	 * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
	 *
	 * @return  JForm|boolean  A JForm object on success, false on failure
	 *
	 * @since   1.6
	 */
	public function getForm($data = array(), $loadData = true)
	{
		// Set form path in case we are called form different extension
		\JForm::addFormPath(JPATH_BASE.DS.'components'.DS.'com_flexicontent' . '/models/forms');
		\JForm::addFieldPath(JPATH_BASE.DS.'components'.DS.'com_flexicontent' . '/models/fields');

		// Get the form.
		$form_name    = $this->events_context;
		$xml_filename = $this->getName();
		$form = $this->loadForm($form_name, $xml_filename, array('control' => 'jform', 'load_data' => $loadData));

		if (empty($form))
		{
			return false;
		}

		$form->option = $this->option;
		$form->context = $this->getName();

		return $form;
	}


	/**
	 * Method to get the data that should be injected in the form.
	 *
	 * @return  mixed  The data for the form.
	 *
	 * @since   1.6
	 */
	protected function loadFormData()
	{
		// Check the session for previously entered form data.
		$app = JFactory::getApplication();
		$data = $app->getUserState($this->option.'.edit.'.$this->getName().'.data', array());

		// Clear form data from session
		$app->setUserState($this->option.'.edit.'.$this->getName().'.data', false);

		if (empty($data))
		{
			$item = $this->getItem();

			/**
			 * Because the record data are meant for JForm before any other manipulations and before any
			 * other data is added, convert our JTable record to a JObject coping only public properies
			 */
			$_prop_arr = $item->getProperties($public_only = true);
			$data = ArrayHelper::toObject($_prop_arr, 'JObject');
		}
		else
		{
		}

		$this->preprocessData($this->events_context, $data);

		return $data;
	}


	/**
	 * Method to get a record.
	 *
	 * @param	integer  $pk An optional id of the object to get, otherwise the id from the model state is used.
	 *
	 * @return	mixed 	Record data object on success, false on failure.
	 *
	 * @since	1.6
	 */
	public function getItem($pk = null)
	{
		$pk = $pk ? (int) $pk : $this->_id;
		$pk = $pk ? $pk : (int) $this->getState($this->getName().'.id');

		static $items = array();

		if ($pk && isset($items[$pk]))
		{
			return $items[$pk];
		}

		// Instatiate the JTable
		$table = $this->getTable();

		if ($pk)
		{
			// Attempt to load the row.
			$return = $table->load($pk);

			// Check for a table object error.
			if ($return === false && $table->getError())
			{
				$this->setError($table->getError());
				return false;
			}
		}
		else
		{
			// New record set desired default values into the record
			$this->_initRecord($table);
		}

		// Set record
		$this->_id = $table->id;
		$this->_record = $table;

		// Extra steps after loading
		$this->_afterLoad($this->_record);

		// Add to cache if not a new record
		if ($pk)
		{
			$items[$pk] = $this->_record;
		}

		return $this->_record;
	}


	/**
	 * Method to preprocess the form.
	 *
	 * @param   JForm   $form   A JForm object.
	 * @param   mixed   $data   The data expected for the form.
	 * @param   string  $plugins_group  The name of the plugin group to import and trigger
	 *
	 * @return  void
	 *
	 * @see     JFormField
	 * @since   1.6
	 * @throws  Exception if there is an error in the form event.
	 */
	protected function preprocessForm(JForm $form, $data, $plugins_group = null)
	{
		// Trigger the default form events.
		$plugins_group = $plugins_group ?: $this->plugins_group;
		parent::preprocessForm($form, $data, $plugins_group);
	}


	/**
	 * Method to validate the form data.
	 *
	 * @param   \JForm  $form   The form to validate against.
	 * @param   array   $data   The data to validate.
	 * @param   string  $group  The name of the field group to validate.
	 *
	 * @return  array|boolean  Array of filtered data if valid, false otherwise.
	 *
	 * @see     \JFormRule
	 * @see     \JFilterInput
	 * @since   3.3.0
	 */
	public function validate($form, $data, $group = null)
	{
		return parent::validate($form, $data, $group);
	}


	/**
	 * Auto-populate the model state.
	 *
	 * Note. Calling getState in this method will result in recursion.
	 *
	 * @since	1.6
	 */
	protected function populateState()
	{
		$app = JFactory::getApplication();

		// Set parent_id into state (later ignored if not this record type has no such property)
		$parentId = $app->input->getInt('parent_id');
		$this->setState($this->getName().'.parent_id', $parentId);

		// Load the User state.
		$pk = $this->_id ?: $app->input->getInt('id');
		$this->_id = $pk;
		$this->setState($this->getName() . '.id', $pk);

		// Extract the extension name
		$extension = $app->input->getCmd('extension', $this->extension_proxy ?: 'com_flexicontent');
		$this->setState($this->getName().'.extension', $extension);
		$parts = explode('.', $extension);

		// Extract the component name
		$this->setState($this->getName().'.component', $parts[0]);

		// Extract the optional section name
		$this->setState($this->getName().'.section', (count($parts) > 1) ? $parts[1] : null);

		// Load the parameters.
		$params	= JComponentHelper::getParams('com_flexicontent');
		$this->setState('params', $params);
	}


	/**
	 * Method to change the title & alias.
	 *
	 * @param   integer  $parent_id  If applicable, the id of the parent (e.g. assigned category)
	 * @param   string   $alias      The alias / name.
	 * @param   string   $title      The title / label.
	 *
	 * @return  array    Contains the modified title and alias / name.
	 *
	 * @since   1.7
	 */
	protected function generateNewTitle($parent_id, $alias, $title)
	{
		// Alter the title & alias
		$table = $this->getTable();

		while ($table->load(array('alias' => $alias, 'parent_id' => $parent_id)))
		{
			$title = StringHelper::increment($title);
			$alias = StringHelper::increment($alias, 'dash');
		}

		return array($title, $alias);
	}


	/**
	 * Method to save language associations
	 *
	 * @return  boolean True if successful
	 */
	function saveAssociations(&$record, &$data, $add_current = true)
	{
		if( !$this->associations_context ) return true;

		$record = $record ? $record: $this->_record;
		return flexicontent_db::saveAssociations($record, $data, $this->associations_context, $add_current);
	}


	/**
	 * Method to determine if J3.1+ associations should be used
	 *
	 * @return  boolean True if using J3 associations; false otherwise.
	 */
	public function useAssociations()
	{
		return flexicontent_db::useAssociations();
	}


	/**
	 * Method to check if the user can edit the record
	 *
	 * @return	boolean	True on success
	 *
	 * @since	3.2.0
	 */
	public function canEdit($record = null, $user = null)
	{
		if ($user)
		{
			throw new Exception(__FUNCTION__ . '(): Error model does not support checking ACL of specific user', 500);
		}

		$record = $record ?: $this->_record;
		$user   = $user ?: JFactory::getUser();

		return false;
	}


	/**
	 * Method to check if the user can edit record 's state
	 *
	 * @return	boolean	True on success
	 *
	 * @since	3.2.0
	 */
	public function canEditState($record = null, $user = null)
	{
		if ($user)
		{
			throw new Exception(__FUNCTION__ . '(): Error model does not support checking ACL of specific user', 500);
		}

		$record = $record ?: $this->_record;
		$user   = $user ?: JFactory::getUser();

		return false;
	}


	/**
	 * Method to check if the user can delete the record
	 *
	 * @return	boolean	True on success
	 *
	 * @since	3.2.0
	 */
	public function canDelete($record = null)
	{
		$record = $record ?: $this->_record;

		return false;
	}


	/**
	 * Helper method to format a value as array
	 *
	 * @return object
	 * @since 3.2.0
	 */
	public function formatToArray($value)
	{
		if (is_object($value))
		{
			return (array) $value;
		}
		if (!is_array($value) && !strlen($value))
		{
			return array();
		}
		return is_array($value) ? $value : array($value);
	}


	/**
	 * Helper method to PARTLY bind LAYOUT and other ARRAY properties
	 * so that any fields missing completely from the form can maintain their old values
	 *
	 * @return object
	 * @since 3.2.0
	 */
	function mergeAttributes(&$item, &$data, $properties, $options)
	{
		$form = $this->getForm();

		/**
		 * Mark form fields that have been skipped (according to configuration),
		 * setting them to false to indicate maintaining their DB values
		 */
		$this->handlePartialForm($form, $data);

		/**
		 * Canonicalize data that should be present in the form by
		 * setting them to '' to indicate clearing their DB values
		 */
		$this->_canonicalData($form, $data);


		/**
		 * Filter layout parameters if the were given, and merge them into existing layout parameters (in DB)
		 */

		if (isset($options['params_fset']) && isset($options['layout_type']))
		{
			// Merge Layout parameters into parameters of the record
			JLoader::register('flexicontent_tmpl', JPATH_SITE.DS.'components'.DS.'com_flexicontent'.DS.'classes'.DS.'helpers'.DS.'tmpl.php');
			$layout_data = flexicontent_tmpl::mergeLayoutParams($item, $data, $options);

			// Unset layout data since these we handled above
			unset($data[$options['params_fset']]['layouts']);
		}


		/**
		 * Create one Registry object for every existing data property
		 */

		$db_data_registry = array();
		foreach($properties as $prop)
		{
			$db_data_registry[$prop] = new JRegistry();
		}


		//***
		//*** Filter via all given JForms
		//***

		foreach($options['model_names'] as $extension_name => $model_name)
		{
			// Check XML file exists
			$model_xml_filepath = JPath::clean(JPATH_BASE.DS.'components'.DS . $extension_name . DS.'models'.DS.'forms'.DS . $model_name . '.xml');
			$file_exists = file_exists($model_xml_filepath);

			if (!$file_exists && FLEXI_J40GE)
			{
				$model_xml_filepath = JPath::clean(JPATH_BASE.DS.'components'.DS . $extension_name . DS.'forms'.DS . $model_name . '.xml');
				$file_exists = file_exists($model_xml_filepath);
			}

			if (!$file_exists)
			{
				throw new Exception('Error reading model \'s form XML file : ' . $model_xml_filepath . ' file not found', 500);
			}

			// Attempt to parse the XML file
			$xml = simplexml_load_file($model_xml_filepath);
			if (!$xml)
			{
				throw new Exception('Error parsing model \'s XML form file : ' . $model_xml_filepath, 500);
			}

			// Create a JForm object to validate EXISTIND DB data according to the XML file of the model
			$jform = new JForm($extension_name . '.' . $model_name, array('control' => 'jform', 'load_data' => false));
			$xml_string = $xml->asXML();
			$jform->load($xml_string);

			foreach($properties as $prop)
			{
				if (isset($data[$prop]) && is_array($data[$prop]))
				{
					// Filter the existing data with the current JForm
					$db_data = new JRegistry($item->$prop);
					$db_data = array($prop => $db_data->toArray());
					$db_data = $jform->filter($db_data);

					// Merge the above into the existing data Registry object of the corresponding property
					if (!empty($db_data[$prop]))
					{
						$db_data_registry[$prop]->loadArray($db_data[$prop]);
					}
				}
			}
		}


		//***
		//*** Add to existing data the new data and the layout data
		//***

		foreach($properties as $prop)
		{
			if (isset($data[$prop]) && is_array($data[$prop]))
			{
				// Overwrite existing data with new data
				//$db_data_registry[$prop]->loadArray($data[$prop]);
				$db_data_registry[$prop] = $this->maintainDbData($db_data_registry[$prop], $data[$prop]);

				// Add the layout data too (validated above)
				if (!empty($layout_data) && $prop == $options['params_fset'])
				{
					//$db_data_registry[$prop]->loadArray($layout_data);
					$db_data_registry[$prop] = $this->maintainDbData($db_data_registry[$prop], $layout_data);
				}

				// Convert property back to string
				$item->$prop = $db_data_registry[$prop]->toString();
			}
		}
	}


	/**
	 * Method to get the records having the given name
	 *
	 * @since	3.2.1.8
	 */
	public function loadRecordsByName($name = null)
	{
		// Check 'name' columns and then check 'alias' column exists, if none then clear $name
		$table = $this->getTable();

		$name_property = property_exists($table, 'name')
			? 'name'
			: (property_exists($table, 'alias')
				? 'alias'
				: (property_exists($table, 'title')
					? 'title'
					: null
				)
			);

		$name = $name_property ? $name : null;

		if ($name)
		{
			$query = $this->_db->getQuery(true)
				->select('*')
				->from('#__' . $this->records_dbtbl)
				->where($this->_db->quoteName('name') . ' = ' . $this->_db->Quote($name))
			;
			return $this->_db->setQuery($query)->loadObjectList('id');
		}

		return false;
	}


	/**
	 * Method to initialize member variables used by batch methods and other methods like saveorder()
	 *
	 * @return  void
	 *
	 * @since   3.3.0
	 */
	public function initBatch()
	{
		if ((FLEXI_J38GE && $this->batchSet === null) || (!FLEXI_J38GE && empty($this->batchSet)))
		{
			$this->batchSet = true;

			// Get current user
			$this->user = \JFactory::getUser();

			// Get table
			$this->table = $this->getTable($this->records_dbtbl, 'JTable');

			// Get table class name
			$tc = explode('\\', get_class($this->table));
			$this->tableClassName = end($tc);

			// Get UCM Type data
			$this->contentType = new \JUcmType;
			$this->type = $this->contentType->getTypeByTable($this->tableClassName)
				?: $this->contentType->getTypeByAlias($this->type_alias);

			// Get tabs observer
			if (!FLEXI_J38GE)
			{
				$this->tagsObserver = $this->table->getObserverOfClass('JTableObserverTags');
			}
			elseif (!FLEXI_J40GE)
			{
				$this->tagsObserver = $this->table->getObserverOfClass('Joomla\CMS\Table\Observer\Tags');
			}
		}
	}


	/**
	 * Method to get the enqueued message array
	 *
	 * @since	3.2.0
	 */
	public function getMessageQueue()
	{
		return $this->_messages;
	}


	/**
	 * Method to register a message to the message Queue
	 *
	 * @param	object   $message   The message object
	 *
	 * @since	3.2.0
	 */
	protected function registerMessage($message)
	{
		$this->_messages[] = $message;
	}


	/**
	 * Method to add a message to the message Queue
	 *
	 * @param	object   $message   The message object
	 *
	 * @since	3.2.0
	 */
	public function enqueueMessages($exclude = array())
	{
		$app = JFactory::getApplication();
		$messages = $this->getMessageQueue();

		if ($messages)
		{
			foreach($messages as $message)
			{
				// Skip some message if this was requested
				if ( isset($exclude['showAfterLoad']) && !empty($message->showAfterLoad) )
				{
					continue;
				}
				$app->enqueueMessage($message->text, $message->type);
			}
		}
	}


	/**
	 * Method to modify specific attributes of a record saving them into the DB
	 *
	 * @param		int				$id				The record id
	 * @param		array			$values		The attributes values indexed by attribute names
	 * @param		int				$propname	The record's property that contains the attributes
	 *
	 * @return	boolean		True on success, false on failure
	 *
	 * @since	3.3.0
	 */
	public function setAttributeValues($id, $values, $propname = 'attribs')
	{
		// Attempt to load the row
		$record = $this->getTable();

		if (!$record->load($id))
		{
			$this->setError($record->getError());
			return false;
		}

		// Try to decode the attributes
		$attribs = json_decode($record->$propname);

		// Set new attribute values
		foreach ($values as $i => $v)
		{
			$attribs->$i = $v;
		}

		$record->$propname = json_encode($attribs);

		// Store data in the db
		if (!$record->store())
		{
			$this->setError($record->getError());
			return false;
		}

		return true;
	}


	/**
	 * Method to do some record / data preprocessing before call JTable::bind()
	 *
	 * Note. Typically called inside this MODEL 's store()
	 *
	 * @param   object     $record   The record object
	 * @param   array      $data     The new data array
	 *
	 * @since	3.2.0
	 */
	protected function _prepareBind($record, & $data)
	{
		// (For nested records) Set the new parent id if parent id not matched OR while New/Save as Copy .
		if (in_array('FCModelTraitNestableRecord', class_uses($this)))
		{
			if (property_exists($record, 'parent_id') && ($record->parent_id != $data['parent_id'] || $data['id'] == 0))
			{
				$record->setLocation($data['parent_id'], 'last-child');
			}
		}

		// Put the new records in last position
		if (property_exists($record, 'ordering') && !$record->id && !empty($this->useLastOrdering))
		{
			$record->ordering = $record->getNextOrder();
		}

		// Handle data of the selected ilayout
		$jinput = JFactory::getApplication()->input;
		$task   = $jinput->get('task', '', 'cmd');

		$parent_id  = isset($data['parent_id']) ? $data['parent_id'] : null;
		$alias_prop = isset($data['alias']) ? 'alias' : (isset($data['name']) ? 'name' : null);
		$title_prop = isset($data['title']) ? 'title' : (isset($data['label']) ? 'label' : (isset($data['name']) ? 'name' : null));

		// Alter the title for save as copy
		if ($task === 'save2copy')
		{
			list($title, $alias) = $this->generateNewTitle(
				$parent_id,
				$alias_prop ? $data[$alias_prop] : null,
				$title_prop ? $data[$title_prop] : null
			);
			if ($title && $title_prop)
			{
				$data[$title_prop] = $title;
			}

			if ($alias && $alias_prop)
			{
				$data[$alias_prop] = $alias;
			}
		}
	}


	/**
	 * Method to do some work after record has been stored
	 *
	 * Note. Typically called inside this MODEL 's store()
	 *
	 * @param   object     $record   The record object
	 * @param   array      $data     The new data array
	 *
	 * @since	3.2.0
	 */
	protected function _afterStore($record, & $data)
	{
	}


	/**
	 * Method to do some work after record has been loaded via JTable::load()
	 *
	 * Note. Typically called inside this MODEL 's store()
	 *
	 * @param	object   $record   The record object
	 *
	 * @since	3.2.0
	 */
	protected function _afterLoad($record)
	{
		// Record was not found / not created, nothing to do
		if (!$record)
		{
			return;
		}

		// Convert attributes to a JRegistry object
		if (property_exists($record, 'attribs') && !is_object($record->attribs))
		{
			$record->attribs = new JRegistry($record->attribs);
		}

		// Convert parameters to a JRegistry object
		if (property_exists($record, 'params') && !is_object($record->params))
		{
			$record->params = new JRegistry($record->params);
		}
	}


	/**
	 * Method to canonicalize the form data that should be present in the form by setting them to '' to indicate clearing their DB values
	 *
	 * @param   JForm   $form   A JForm object.
	 * @param   mixed   $data   The data expected for the form.
	 *
	 * @return  void
	 *
	 * @since   3.3.0
	 */
	protected function _canonicalData($form, & $data)
	{
		foreach($this->mergeableGroups as $grp_name)
		{
			if (is_object($data))
			{
				continue;
			}

			foreach ($form->getFieldsets($grp_name) as $fsname => $fieldSet)
			{
				foreach ($form->getFieldset($fsname) as $field)
				{
					if (!isset($data[$grp_name][$field->fieldname]))
					{
						/**
						 * Field was not skipped but also was no value was posted,
						 * set form value to '' so that DB value gets cleared during merging
						 */
						$data[$grp_name][$field->fieldname] = '';
					}
				}
			}
		}
	}


	/**
	 * Method to maintain database values if posted specific data are missing (or validation cleared them) from the posted form (value is === false)
	 *
	 * @param   array   $db_data     The existing db data
	 * @param   array   $form_data   The form submitted data but canonicalized data (missing parameters were set to false)
	 *
	 * @return  void
	 *
	 * @since   3.3.0
	 */
	protected function maintainDbData($db_data, $form_data)
	{
		foreach($form_data as $i => $v)
		{
			// Recursion
			if (is_array($v))
			{
				$db_data[$i] = $this->maintainDbData($db_data[$i], $form_data[$i]);
			}

			// Use form value
			elseif ($v !== false)
			{
				$db_data[$i] = $v;
			}

			// Clear DB value because it has bogus FALSE value from previous save operation
			elseif ($db_data[$i] === false)
			{
				$db_data[$i] = '';
			}
		}

		return $db_data;
	}


	/**
	 * Method to handle partial form data
	 *
	 * @param   JForm   $form   A JForm object.
	 * @param   mixed   $data   The data expected for the form.
	 *
	 * @return  void
	 *
	 * @since   3.3.0
	 */
	protected function handlePartialForm($form, & $data)
	{
	}


	/**
	 * Custom clean the cache
	 *
	 * @param   string   $group      Clean cache only in the given group
	 * @param   integer  $client_id  Site Cache (0) / Admin Cache (1)
	 *
	 * @return  void
	 *
	 * @since   3.2.0
	 */
	protected function cleanCache($group = NULL, $client_id = 0)
	{
		if ($group)
		{
			parent::cleanCache($group, $client_id);
		}

		// An empty '$group' will clean '$this->option' which is the Component VIEW Cache, we will do a little more ...
		else
		{
			/**
			 * Note: null should be the same as $this->option ...
			 * Maybe add option not clean Component's VIEW cache it will be too aggressive ...
			 */
			if (1)
			{
				parent::cleanCache(null, $client_id);
				parent::cleanCache('com_content', $client_id);
			}
		}
	}


	/**
	 * Method to change the state of given record (used by AJAX calls)
	 *
	 * @return  boolean    True on success
	 *
	 * @since	3.2.0
	 */
	public function setitemstate($id, $state = 1, $cleanCache = true)
	{
		$app  = JFactory::getApplication();
		$user = JFactory::getUser();

		static $event_failed_notice_added = false;

		if (!$id)
		{
			return false;
		}

		// Sanitize id
		$cid = (array) $id;
		$cid = ArrayHelper::toInteger($cid);

		// Recursively add all ancestors or descendants if this is a nested record
		if (in_array('FCModelTraitNestableRecord', class_uses($this)))
		{
			// If publishing then add all parents to the list, so that they get published too
			if ($state == 1)
			{
				foreach ($cid as $_id)
				{
					$this->_addPathRecords($_id, $cid, 'parents');
				}
			}

			// If not publishing then all children to the list, so that they get the new state too
			else
			{
				foreach ($cid as $_id)
				{
					$this->_addPathRecords($_id, $cid, 'children');
				}
			}
		}

		$cid = ArrayHelper::toInteger($cid);
		$cid_list = implode(',', $cid);

		if (!empty($this->event_recid_col))
		{
			$query = $this->_db->getQuery(true)
				->select($this->_db->qn($this->event_recid_col))
				->from('#__' . $this->records_dbtbl)
				->set($this->_db->qn($this->state_col) . ' = ' . (int) $state)
				->where('id IN (' . $cid_list . ')')
			;
			$event_ids = $this->_db->setQuery($query)->loadColumn();

			foreach ($event_ids as $i => $v)
			{
				if (!$v)
				{
					unset($event_ids[$i]);
				}
			}
		}
		else
		{
			$event_ids = $cid;
		}


		/**
		 * First update records
		 */

		if (!empty($this->use_jtable_publishing))
		{
			$table = $this->getTable();

			if (!$table->publish($cid, $state, $user->get('id')))
			{
				$this->setError($table->getError());

				return false;
			}
		}
		else
		{
			$query = $this->_db->getQuery(true)
				->update('#__' . $this->records_dbtbl)
				->set($this->_db->qn($this->state_col) . ' = ' . (int) $state)
				->where('id IN (' . $cid_list . ')')
			;
			$this->_db->setQuery($query)->execute();

			if (!empty($this->_setstate_tbls))
			{
				foreach($this->_setstate_tbls as $tbl_name => $config)
				{
					if (in_array($this->state_col, $this->tbl_ext_cols[$tbl_name]))
					{
						$config['rids'] = ArrayHelper::toInteger($config['rids']);
						$rids_list =  implode( ',', $config['rids']);

						$query = $this->_db->getQuery(true)
							->update('#__' . $tbl_name)
							->set($this->_db->qn($config['state_col']) . ' = ' . (int) $state)
							->where($this->_db->qn($config['recid_col']) . ' IN (' . $cid_list . ')')
						;
						$this->_db->setQuery($query)->execute();
					}
				}
			}
		}


		/**
		 * Update version table
		 */
		if ($this->getName() === 'item')
		{
			$v = FLEXIUtilities::getCurrentVersions((int) $id);

			$query = 'UPDATE #__flexicontent_items_versions'
				. ' SET value = ' . (int) $state
				. ' WHERE item_id = ' . (int) $id
				. ' AND valueorder = 1'
				. ' AND field_id = 10'
				. ' AND version = ' . (int) $v['version']
			;
			$this->_db->setQuery($query)->execute();
		}


		/**
		 * Trigger Event 'onContentChangeState' of Joomla's Content plugins
		 */
		if (empty($this->skipChangeStateEvent) && !empty($event_ids))
		{
			// Make sure we import flexicontent AND content plugins since we will be triggering their events
			JPluginHelper::importPlugin('content');

			$jinput     = JFactory::getApplication()->input;
			$dispatcher = JEventDispatcher::getInstance();

			// Compatibility steps, so that 3rd party plugins using the change state event work properly
			$jm_state = $state;

			$parts = explode('.', $this->events_context);
			$is_proxied_context = count($parts) >= 2 && $parts[0] !== $this->option;

			if ($is_proxied_context)
			{
				$current_view   = $jinput->getCmd('view', '');
				$current_option = $jinput->getCmd('option', '');

				$jinput->set('isflexicontent', 'yes');
				$jinput->set('view', $parts[1]);
				$jinput->set('option', $parts[0]);

				/**
				 * Set a Joomla compatible state as we need to map to
				 * a joomla's existing states ... when triggering events
				 */

				// Published states
				if (in_array($state, array(1,-5)))
				{
					$jm_state = 1;
				}

				// Unpublished states
				elseif (in_array($state, array(0,-3,-4)))
				{
					$jm_state = 0;
				}

				// Trashed & Archive states
				else
				{
					$jm_state = $state;
				}

				// Workaround for extensions using the model but not setting correct include path
				$component = $parts[1] === 'category' ? 'com_categories' : $parts[1];
				JModelLegacy::addIncludePath(JPATH_BASE . '/components/' . $component . '/models');
			}

			//Trigger the event
			$results = FLEXI_J40GE
				? $app->triggerEvent($this->event_change_state, array($this->events_context, $event_ids, $jm_state))
				: $dispatcher->trigger($this->event_change_state, array($this->events_context, $event_ids, $jm_state));

			// Revert compatibilty steps ...
			// besides the plugins using the change state event, should have updated DB state value anyway
			if ($is_proxied_context)
			{
				$jinput->set('view', $current_view);
				$jinput->set('option', $current_option);
			}

			// Abort further actions if any plugin returns a result === false
			if (is_array($results) && in_array(false, $results, true))
			{
				if (!$event_failed_notice_added)
				{
					$this->setError('State change events reported a failure during handling the change to the new state');
					$event_failed_notice_added = true;
				}

				return false;
			}
		}

		if ($cleanCache)
		{
			$this->cleanCache(null, 0);
			$this->cleanCache(null, 1);
		}

		return true;
	}

	/**
	 * START OF MODEL SPECIFIC METHODS
	 */

}