1<?php
2/**
3 * @package     FrameworkOnFramework
4 * @subpackage  table
5 * @copyright   Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
6 * @license     GNU General Public License version 2 or later; see LICENSE.txt
7 */
8// Protect from unauthorized access
9defined('FOF_INCLUDED') or die;
10
11/**
12 * Normally this shouldn't be required. Some PHP versions, however, seem to
13 * require this. Why? No idea whatsoever. If I remove it, FOF crashes on some
14 * hosts. Same PHP version on another host and no problem occurs. Any takers?
15 */
16if (class_exists('FOFTable', false))
17{
18	return;
19}
20
21if (!interface_exists('JTableInterface', true))
22{
23	interface JTableInterface {}
24}
25
26/**
27 * FrameworkOnFramework Table class. The Table is one part controller, one part
28 * model and one part data adapter. It's supposed to handle operations for single
29 * records.
30 *
31 * @package  FrameworkOnFramework
32 * @since    1.0
33 */
34class FOFTable extends FOFUtilsObject implements JTableInterface
35{
36	/**
37	 * Cache array for instances
38	 *
39	 * @var    array
40	 */
41	protected static $instances = array();
42
43	/**
44	 * Include paths for searching for FOFTable classes.
45	 *
46	 * @var    array
47	 */
48	protected static $_includePaths = array();
49
50	/**
51	 * The configuration parameters array
52	 *
53	 * @var  array
54	 */
55	protected $config = array();
56
57	/**
58	 * Name of the database table to model.
59	 *
60	 * @var    string
61	 */
62	protected $_tbl = '';
63
64	/**
65	 * Name of the primary key field in the table.
66	 *
67	 * @var    string
68	 */
69	protected $_tbl_key = '';
70
71	/**
72	 * FOFDatabaseDriver object.
73	 *
74	 * @var    FOFDatabaseDriver
75	 */
76	protected $_db;
77
78	/**
79	 * Should rows be tracked as ACL assets?
80	 *
81	 * @var    boolean
82	 */
83	protected $_trackAssets = false;
84
85	/**
86	 * Does the resource support joomla tags?
87	 *
88	 * @var    boolean
89	 */
90	protected $_has_tags = false;
91
92	/**
93	 * The rules associated with this record.
94	 *
95	 * @var    JAccessRules  A JAccessRules object.
96	 */
97	protected $_rules;
98
99	/**
100	 * Indicator that the tables have been locked.
101	 *
102	 * @var    boolean
103	 */
104	protected $_locked = false;
105
106	/**
107	 * If this is set to true, it triggers automatically plugin events for
108	 * table actions
109	 *
110	 * @var    boolean
111	 */
112	protected $_trigger_events = false;
113
114	/**
115	 * Table alias used in queries
116	 *
117	 * @var    string
118	 */
119	protected $_tableAlias = false;
120
121	/**
122	 * Array with alias for "special" columns such as ordering, hits etc etc
123	 *
124	 * @var    array
125	 */
126	protected $_columnAlias = array();
127
128	/**
129	 * If set to true, it enabled automatic checks on fields based on columns properties
130	 *
131	 * @var    boolean
132	 */
133	protected $_autoChecks = false;
134
135	/**
136	 * Array with fields that should be skipped by automatic checks
137	 *
138	 * @var    array
139	 */
140	protected $_skipChecks = array();
141
142	/**
143	 * Does the table actually exist? We need that to avoid PHP notices on
144	 * table-less views.
145	 *
146	 * @var    boolean
147	 */
148	protected $_tableExists = true;
149
150	/**
151	 * The asset key for items in this table. It's usually something in the
152	 * com_example.viewname format. They asset name will be this key appended
153	 * with the item's ID, e.g. com_example.viewname.123
154	 *
155	 * @var    string
156	 */
157	protected $_assetKey = '';
158
159	/**
160	 * The input data
161	 *
162	 * @var    FOFInput
163	 */
164	protected $input = null;
165
166	/**
167	 * Extended query including joins with other tables
168	 *
169	 * @var    FOFDatabaseQuery
170	 */
171	protected $_queryJoin = null;
172
173	/**
174	 * The prefix for the table class
175	 *
176	 * @var		string
177	 */
178	protected $_tablePrefix = '';
179
180	/**
181	 * The known fields for this table
182	 *
183	 * @var		array
184	 */
185	protected $knownFields = array();
186
187	/**
188	 * A list of table fields, keyed per table
189	 *
190	 * @var array
191	 */
192	protected static $tableFieldCache = array();
193
194	/**
195	 * A list of tables in the database
196	 *
197	 * @var array
198	 */
199	protected static $tableCache = array();
200
201	/**
202	 * An instance of FOFConfigProvider to provision configuration overrides
203	 *
204	 * @var    FOFConfigProvider
205	 */
206	protected $configProvider = null;
207
208	/**
209	 * FOFTableDispatcherBehavior for dealing with extra behaviors
210	 *
211	 * @var    FOFTableDispatcherBehavior
212	 */
213	protected $tableDispatcher = null;
214
215	/**
216	 * List of default behaviors to apply to the table
217	 *
218	 * @var    array
219	 */
220	protected $default_behaviors = array('tags', 'assets');
221
222	/**
223	 * The relations object of the table. It's lazy-loaded by getRelations().
224	 *
225	 * @var   FOFTableRelations
226	 */
227	protected $_relations = null;
228
229	/**
230	 * The configuration provider's key for this table, e.g. foobar.tables.bar for the #__foobar_bars table. This is set
231	 * automatically by the constructor
232	 *
233	 * @var  string
234	 */
235	protected $_configProviderKey = '';
236
237	/**
238	 * The content type of the table. Required if using tags or content history behaviour
239	 *
240	 * @var  string
241	 */
242	protected $contentType = null;
243
244	/**
245	 * Returns a static object instance of a particular table type
246	 *
247	 * @param   string  $type    The table name
248	 * @param   string  $prefix  The prefix of the table class
249	 * @param   array   $config  Optional configuration variables
250	 *
251	 * @return FOFTable
252	 */
253	public static function getInstance($type, $prefix = 'JTable', $config = array())
254	{
255		return self::getAnInstance($type, $prefix, $config);
256	}
257
258	/**
259	 * Returns a static object instance of a particular table type
260	 *
261	 * @param   string  $type    The table name
262	 * @param   string  $prefix  The prefix of the table class
263	 * @param   array   $config  Optional configuration variables
264	 *
265	 * @return FOFTable
266	 */
267	public static function &getAnInstance($type = null, $prefix = 'JTable', $config = array())
268	{
269		// Make sure $config is an array
270		if (is_object($config))
271		{
272			$config = (array) $config;
273		}
274		elseif (!is_array($config))
275		{
276			$config = array();
277		}
278
279		// Guess the component name
280		if (!array_key_exists('input', $config))
281		{
282			$config['input'] = new FOFInput;
283		}
284
285		if ($config['input'] instanceof FOFInput)
286		{
287			$tmpInput = $config['input'];
288		}
289		else
290		{
291			$tmpInput = new FOFInput($config['input']);
292		}
293
294		$option = $tmpInput->getCmd('option', '');
295		$tmpInput->set('option', $option);
296		$config['input'] = $tmpInput;
297
298		if (!in_array($prefix, array('Table', 'JTable')))
299		{
300			preg_match('/(.*)Table$/', $prefix, $m);
301			$option = 'com_' . strtolower($m[1]);
302		}
303
304		if (array_key_exists('option', $config))
305		{
306			$option = $config['option'];
307		}
308
309		$config['option'] = $option;
310
311		if (!array_key_exists('view', $config))
312		{
313			$config['view'] = $config['input']->getCmd('view', 'cpanel');
314		}
315
316		if (is_null($type))
317		{
318			if ($prefix == 'JTable')
319			{
320				$prefix = 'Table';
321			}
322
323			$type = $config['view'];
324		}
325
326		$type       = preg_replace('/[^A-Z0-9_\.-]/i', '', $type);
327		$tableClass = $prefix . ucfirst($type);
328
329		$config['_table_type'] = $type;
330		$config['_table_class'] = $tableClass;
331
332		$configProvider = new FOFConfigProvider;
333		$configProviderKey = $option . '.views.' . FOFInflector::singularize($type) . '.config.';
334
335		if (!array_key_exists($tableClass, self::$instances))
336		{
337			if (!class_exists($tableClass))
338			{
339				$componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($config['option']);
340
341				$searchPaths = array(
342					$componentPaths['main'] . '/tables',
343					$componentPaths['admin'] . '/tables'
344				);
345
346				if (array_key_exists('tablepath', $config))
347				{
348					array_unshift($searchPaths, $config['tablepath']);
349				}
350
351				$altPath = $configProvider->get($configProviderKey . 'table_path', null);
352
353				if ($altPath)
354				{
355					array_unshift($searchPaths, $componentPaths['admin'] . '/' . $altPath);
356				}
357
358                $filesystem = FOFPlatform::getInstance()->getIntegrationObject('filesystem');
359
360				$path = $filesystem->pathFind(
361					$searchPaths, strtolower($type) . '.php'
362				);
363
364				if ($path)
365				{
366					require_once $path;
367				}
368			}
369
370			if (!class_exists($tableClass))
371			{
372				$tableClass = 'FOFTable';
373			}
374
375			$component = str_replace('com_', '', $config['option']);
376			$tbl_common = $component . '_';
377
378			if (!array_key_exists('tbl', $config))
379			{
380				$config['tbl'] = strtolower('#__' . $tbl_common . strtolower(FOFInflector::pluralize($type)));
381			}
382
383			$altTbl = $configProvider->get($configProviderKey . 'tbl', null);
384
385			if ($altTbl)
386			{
387				$config['tbl'] = $altTbl;
388			}
389
390			if (!array_key_exists('tbl_key', $config))
391			{
392				$keyName           = FOFInflector::singularize($type);
393				$config['tbl_key'] = strtolower($tbl_common . $keyName . '_id');
394			}
395
396			$altTblKey = $configProvider->get($configProviderKey . 'tbl_key', null);
397
398			if ($altTblKey)
399			{
400				$config['tbl_key'] = $altTblKey;
401			}
402
403			if (!array_key_exists('db', $config))
404			{
405				$config['db'] = FOFPlatform::getInstance()->getDbo();
406			}
407
408			// Assign the correct table alias
409			if (array_key_exists('table_alias', $config))
410			{
411				$table_alias = $config['table_alias'];
412			}
413			else
414			{
415				$configProviderTableAliasKey = $option . '.tables.' . FOFInflector::singularize($type) . '.tablealias';
416				$table_alias = $configProvider->get($configProviderTableAliasKey, false	);
417			}
418
419			// Can we use the FOF cache?
420			if (!array_key_exists('use_table_cache', $config))
421			{
422				$config['use_table_cache'] = FOFPlatform::getInstance()->isGlobalFOFCacheEnabled();
423			}
424
425			$alt_use_table_cache = $configProvider->get($configProviderKey . 'use_table_cache', null);
426
427			if (!is_null($alt_use_table_cache))
428			{
429				$config['use_table_cache'] = $alt_use_table_cache;
430			}
431
432			// Create a new table instance
433			$instance = new $tableClass($config['tbl'], $config['tbl_key'], $config['db'], $config);
434			$instance->setInput($tmpInput);
435			$instance->setTablePrefix($prefix);
436			$instance->setTableAlias($table_alias);
437
438			// Determine and set the asset key for this table
439			$assetKey = 'com_' . $component . '.' . strtolower(FOFInflector::singularize($type));
440			$assetKey = $configProvider->get($configProviderKey . 'asset_key', $assetKey);
441			$instance->setAssetKey($assetKey);
442
443			if (array_key_exists('trigger_events', $config))
444			{
445				$instance->setTriggerEvents($config['trigger_events']);
446			}
447
448			if (version_compare(JVERSION, '3.1', 'ge'))
449			{
450				if (array_key_exists('has_tags', $config))
451				{
452					$instance->setHasTags($config['has_tags']);
453				}
454
455				$altHasTags = $configProvider->get($configProviderKey . 'has_tags', null);
456
457				if ($altHasTags)
458				{
459					$instance->setHasTags($altHasTags);
460				}
461			}
462			else
463			{
464				$instance->setHasTags(false);
465			}
466
467			$configProviderFieldmapKey = $option . '.tables.' . FOFInflector::singularize($type) . '.field';
468			$aliases = $configProvider->get($configProviderFieldmapKey, $instance->_columnAlias);
469			$instance->_columnAlias = array_merge($instance->_columnAlias, $aliases);
470
471			self::$instances[$tableClass] = $instance;
472		}
473
474		return self::$instances[$tableClass];
475	}
476
477	/**
478	 * Force an instance inside class cache. Setting arguments to null nukes all or part of the cache
479	 *
480	 * @param    string|null       $key        TableClass to replace. Set it to null to nuke the entire cache
481	 * @param    FOFTable|null     $instance   Instance to replace. Set it to null to nuke $key instances
482	 *
483	 * @return   bool              Did I correctly switch the instance?
484	 */
485	public static function forceInstance($key = null, $instance = null)
486	{
487		if(is_null($key))
488		{
489			self::$instances = array();
490
491			return true;
492		}
493		elseif($key && isset(self::$instances[$key]))
494		{
495			// I'm forcing an instance, but it's not a FOFTable, abort! abort!
496			if(!$instance || ($instance && $instance instanceof FOFTable))
497			{
498				self::$instances[$key] = $instance;
499
500				return true;
501			}
502		}
503
504		return false;
505	}
506
507	/**
508	 * Class Constructor.
509	 *
510	 * @param   string           $table   Name of the database table to model.
511	 * @param   string           $key     Name of the primary key field in the table.
512	 * @param   FOFDatabaseDriver  &$db     Database driver
513	 * @param   array            $config  The configuration parameters array
514	 */
515	public function __construct($table, $key, &$db, $config = array())
516	{
517		$this->_tbl     = $table;
518		$this->_tbl_key = $key;
519		$this->_db      = $db;
520
521		// Make sure the use FOF cache information is in the config
522		if (!array_key_exists('use_table_cache', $config))
523		{
524			$config['use_table_cache'] = FOFPlatform::getInstance()->isGlobalFOFCacheEnabled();
525		}
526		$this->config   = $config;
527
528		// Load the configuration provider
529		$this->configProvider = new FOFConfigProvider;
530
531		// Load the behavior dispatcher
532		$this->tableDispatcher = new FOFTableDispatcherBehavior;
533
534		// Initialise the table properties.
535
536		if ($fields = $this->getTableFields())
537		{
538			// Do I have anything joined?
539			$j_fields = $this->getQueryJoinFields();
540
541			if ($j_fields)
542			{
543				$fields = array_merge($fields, $j_fields);
544			}
545
546			$this->setKnownFields(array_keys($fields), true);
547			$this->reset();
548		}
549		else
550		{
551			$this->_tableExists = false;
552		}
553
554		// Get the input
555		if (array_key_exists('input', $config))
556		{
557			if ($config['input'] instanceof FOFInput)
558			{
559				$this->input = $config['input'];
560			}
561			else
562			{
563				$this->input = new FOFInput($config['input']);
564			}
565		}
566		else
567		{
568			$this->input = new FOFInput;
569		}
570
571		// Set the $name/$_name variable
572		$component = $this->input->getCmd('option', 'com_foobar');
573
574		if (array_key_exists('option', $config))
575		{
576			$component = $config['option'];
577		}
578
579		$this->input->set('option', $component);
580
581		// Apply table behaviors
582		$type = explode("_", $this->_tbl);
583		$type = $type[count($type) - 1];
584
585		$this->_configProviderKey = $component . '.tables.' . FOFInflector::singularize($type);
586
587		$configKey = $this->_configProviderKey . '.behaviors';
588
589		if (isset($config['behaviors']))
590		{
591			$behaviors = (array) $config['behaviors'];
592		}
593		elseif ($behaviors = $this->configProvider->get($configKey, null))
594		{
595			$behaviors = explode(',', $behaviors);
596		}
597		else
598		{
599			$behaviors = $this->default_behaviors;
600		}
601
602		if (is_array($behaviors) && count($behaviors))
603		{
604			foreach ($behaviors as $behavior)
605			{
606				$this->addBehavior($behavior);
607			}
608		}
609
610		// If we are tracking assets, make sure an access field exists and initially set the default.
611		$asset_id_field	= $this->getColumnAlias('asset_id');
612		$access_field	= $this->getColumnAlias('access');
613
614		if (in_array($asset_id_field, $this->getKnownFields()))
615		{
616			JLoader::import('joomla.access.rules');
617			$this->_trackAssets = true;
618		}
619
620		// If the access property exists, set the default.
621		if (in_array($access_field, $this->getKnownFields()))
622		{
623			$this->$access_field = (int) FOFPlatform::getInstance()->getConfig()->get('access');
624		}
625
626		$this->config = $config;
627	}
628
629	/**
630	 * Replace the entire known fields array
631	 *
632	 * @param   array    $fields      A simple array of known field names
633	 * @param   boolean  $initialise  Should we initialise variables to null?
634	 *
635	 * @return  void
636	 */
637	public function setKnownFields($fields, $initialise = false)
638	{
639		$this->knownFields = $fields;
640
641		if ($initialise)
642		{
643			foreach ($this->knownFields as $field)
644			{
645				$this->$field = null;
646			}
647		}
648	}
649
650	/**
651	 * Get the known fields array
652	 *
653	 * @return  array
654	 */
655	public function getKnownFields()
656	{
657		return $this->knownFields;
658	}
659
660	/**
661	 * Does the specified field exist?
662	 *
663	 * @param   string  $fieldName  The field name to search (it's OK to use aliases)
664	 *
665	 * @return  bool
666	 */
667	public function hasField($fieldName)
668	{
669		$search = $this->getColumnAlias($fieldName);
670
671		return in_array($search, $this->knownFields);
672	}
673
674	/**
675	 * Add a field to the known fields array
676	 *
677	 * @param   string   $field       The name of the field to add
678	 * @param   boolean  $initialise  Should we initialise the variable to null?
679	 *
680	 * @return  void
681	 */
682	public function addKnownField($field, $initialise = false)
683	{
684		if (!in_array($field, $this->knownFields))
685		{
686			$this->knownFields[] = $field;
687
688			if ($initialise)
689			{
690				$this->$field = null;
691			}
692		}
693	}
694
695	/**
696	 * Remove a field from the known fields array
697	 *
698	 * @param   string  $field  The name of the field to remove
699	 *
700	 * @return  void
701	 */
702	public function removeKnownField($field)
703	{
704		if (in_array($field, $this->knownFields))
705		{
706			$pos = array_search($field, $this->knownFields);
707			unset($this->knownFields[$pos]);
708		}
709	}
710
711	/**
712	 * Adds a behavior to the table
713	 *
714	 * @param   string  $name    The name of the behavior
715	 * @param   array   $config  Optional Behavior configuration
716	 *
717	 * @return  boolean
718	 */
719	public function addBehavior($name, $config = array())
720	{
721		// First look for ComponentnameTableViewnameBehaviorName (e.g. FoobarTableItemsBehaviorTags)
722		if (isset($this->config['option']))
723		{
724			$option_name = str_replace('com_', '', $this->config['option']);
725			$behaviorClass = $this->config['_table_class'] . 'Behavior' . ucfirst(strtolower($name));
726
727			if (class_exists($behaviorClass))
728			{
729				$behavior = new $behaviorClass($this->tableDispatcher, $config);
730
731				return true;
732			}
733
734			// Then look for ComponentnameTableBehaviorName (e.g. FoobarTableBehaviorTags)
735			$option_name = str_replace('com_', '', $this->config['option']);
736			$behaviorClass = ucfirst($option_name) . 'TableBehavior' . ucfirst(strtolower($name));
737
738			if (class_exists($behaviorClass))
739			{
740				$behavior = new $behaviorClass($this->tableDispatcher, $config);
741
742				return true;
743			}
744		}
745
746		// Nothing found? Return false.
747
748		$behaviorClass = 'FOFTableBehavior' . ucfirst(strtolower($name));
749
750		if (class_exists($behaviorClass) && $this->tableDispatcher)
751		{
752			$behavior = new $behaviorClass($this->tableDispatcher, $config);
753
754			return true;
755		}
756
757		return false;
758	}
759
760	/**
761	 * Sets the events trigger switch state
762	 *
763	 * @param   boolean  $newState  The new state of the switch (what else could it be?)
764	 *
765	 * @return  void
766	 */
767	public function setTriggerEvents($newState = false)
768	{
769		$this->_trigger_events = $newState ? true : false;
770	}
771
772	/**
773	 * Gets the events trigger switch state
774	 *
775	 * @return  boolean
776	 */
777	public function getTriggerEvents()
778	{
779		return $this->_trigger_events;
780	}
781
782	/**
783	 * Gets the has tags switch state
784	 *
785	 * @return bool
786	 */
787	public function hasTags()
788	{
789		return $this->_has_tags;
790	}
791
792	/**
793	 * Sets the has tags switch state
794	 *
795	 * @param   bool  $newState
796	 */
797	public function setHasTags($newState = false)
798	{
799		$this->_has_tags = false;
800
801		// Tags are available only in 3.1+
802		if (version_compare(JVERSION, '3.1', 'ge'))
803		{
804			$this->_has_tags = $newState ? true : false;
805		}
806	}
807
808	/**
809	 * Set the class prefix
810	 *
811	 * @param string $prefix The prefix
812	 */
813	public function setTablePrefix($prefix)
814	{
815		$this->_tablePrefix = $prefix;
816	}
817
818	/**
819	 * Sets fields to be skipped from automatic checks.
820	 *
821	 * @param   array/string  $skip  Fields to be skipped by automatic checks
822	 *
823	 * @return void
824	 */
825	public function setSkipChecks($skip)
826	{
827		$this->_skipChecks = (array) $skip;
828	}
829
830	/**
831	 * Method to load a row from the database by primary key and bind the fields
832	 * to the FOFTable instance properties.
833	 *
834	 * @param   mixed    $keys   An optional primary key value to load the row by, or an array of fields to match.  If not
835	 *                           set the instance property value is used.
836	 * @param   boolean  $reset  True to reset the default values before loading the new row.
837	 *
838	 * @return  boolean  True if successful. False if row not found.
839	 *
840	 * @throws  RuntimeException
841	 * @throws  UnexpectedValueException
842	 */
843	public function load($keys = null, $reset = true)
844	{
845		if (!$this->_tableExists)
846		{
847			$result = false;
848
849            return $this->onAfterLoad($result);
850		}
851
852		if (empty($keys))
853		{
854			// If empty, use the value of the current key
855			$keyName = $this->_tbl_key;
856
857			if (isset($this->$keyName))
858			{
859				$keyValue = $this->$keyName;
860			}
861			else
862			{
863				$keyValue = null;
864			}
865
866			// If empty primary key there's is no need to load anything
867
868			if (empty($keyValue))
869			{
870				$result = true;
871
872				return $this->onAfterLoad($result);
873			}
874
875			$keys = array($keyName => $keyValue);
876		}
877		elseif (!is_array($keys))
878		{
879			// Load by primary key.
880			$keys = array($this->_tbl_key => $keys);
881		}
882
883		if ($reset)
884		{
885			$this->reset();
886		}
887
888		// Initialise the query.
889		$query = $this->_db->getQuery(true);
890		$query->select($this->_tbl . '.*');
891		$query->from($this->_tbl);
892
893		// Joined fields are ok, since I initialized them in the constructor
894		$fields = $this->getKnownFields();
895
896		foreach ($keys as $field => $value)
897		{
898			// Check that $field is in the table.
899
900			if (!in_array($field, $fields))
901			{
902				throw new UnexpectedValueException(sprintf('Missing field in table %s : %s.', $this->_tbl, $field));
903			}
904
905			// Add the search tuple to the query.
906			$query->where($this->_db->qn($this->_tbl . '.' . $field) . ' = ' . $this->_db->q($value));
907		}
908
909		// Do I have any joined table?
910		$j_query = $this->getQueryJoin();
911
912		if ($j_query)
913		{
914			if ($j_query->select && $j_query->select->getElements())
915			{
916				//$query->select($this->normalizeSelectFields($j_query->select->getElements(), true));
917				$query->select($j_query->select->getElements());
918			}
919
920			if ($j_query->join)
921			{
922				foreach ($j_query->join as $join)
923				{
924					$t = (string) $join;
925
926					// Joomla doesn't provide any access to the "name" variable, so I have to work with strings...
927					if (stripos($t, 'inner') !== false)
928					{
929						$query->innerJoin($join->getElements());
930					}
931					elseif (stripos($t, 'left') !== false)
932					{
933						$query->leftJoin($join->getElements());
934					}
935					elseif (stripos($t, 'right') !== false)
936					{
937						$query->rightJoin($join->getElements());
938					}
939					elseif (stripos($t, 'outer') !== false)
940					{
941						$query->outerJoin($join->getElements());
942					}
943				}
944			}
945		}
946
947		$this->_db->setQuery($query);
948
949		$row = $this->_db->loadAssoc();
950
951		// Check that we have a result.
952		if (empty($row))
953		{
954			$result = false;
955
956			return $this->onAfterLoad($result);
957		}
958
959		// Bind the object with the row and return.
960		$result = $this->bind($row);
961
962		$this->onAfterLoad($result);
963
964		return $result;
965	}
966
967	/**
968	 * Based on fields properties (nullable column), checks if the field is required or not
969	 *
970	 * @return boolean
971	 */
972	public function check()
973	{
974		if (!$this->_autoChecks)
975		{
976			return true;
977		}
978
979		$fields = $this->getTableFields();
980
981        // No fields? Why in the hell am I here?
982        if(!$fields)
983        {
984            return false;
985        }
986
987        $result       = true;
988        $known        = $this->getKnownFields();
989        $skipFields[] = $this->_tbl_key;
990
991		if(in_array($this->getColumnAlias('title'), $known)
992			&& in_array($this->getColumnAlias('slug'), $known))      $skipFields[] = $this->getColumnAlias('slug');
993        if(in_array($this->getColumnAlias('hits'), $known))         $skipFields[] = $this->getColumnAlias('hits');
994        if(in_array($this->getColumnAlias('created_on'), $known))   $skipFields[] = $this->getColumnAlias('created_on');
995        if(in_array($this->getColumnAlias('created_by'), $known))   $skipFields[] = $this->getColumnAlias('created_by');
996        if(in_array($this->getColumnAlias('modified_on'), $known))  $skipFields[] = $this->getColumnAlias('modified_on');
997        if(in_array($this->getColumnAlias('modified_by'), $known))  $skipFields[] = $this->getColumnAlias('modified_by');
998        if(in_array($this->getColumnAlias('locked_by'), $known))    $skipFields[] = $this->getColumnAlias('locked_by');
999        if(in_array($this->getColumnAlias('locked_on'), $known))    $skipFields[] = $this->getColumnAlias('locked_on');
1000
1001        // Let's merge it with custom skips
1002        $skipFields = array_merge($skipFields, $this->_skipChecks);
1003
1004		foreach ($fields as $field)
1005		{
1006			$fieldName = $field->Field;
1007
1008			if (empty($fieldName))
1009			{
1010				$fieldName = $field->column_name;
1011			}
1012
1013			// Field is not nullable but it's null, set error
1014
1015			if ($field->Null == 'NO' && $this->$fieldName == '' && !in_array($fieldName, $skipFields))
1016			{
1017				$text = str_replace('#__', 'COM_', $this->getTableName()) . '_ERR_' . $fieldName;
1018				$this->setError(JText::_(strtoupper($text)));
1019				$result = false;
1020			}
1021		}
1022
1023		return $result;
1024	}
1025
1026	/**
1027	 * Method to reset class properties to the defaults set in the class
1028	 * definition. It will ignore the primary key as well as any private class
1029	 * properties.
1030	 *
1031	 * @return void
1032	 */
1033	public function reset()
1034	{
1035		if (!$this->onBeforeReset())
1036		{
1037			return false;
1038		}
1039
1040		// Get the default values for the class from the table.
1041		$fields   = $this->getTableFields();
1042		$j_fields = $this->getQueryJoinFields();
1043
1044		if ($j_fields)
1045		{
1046			$fields = array_merge($fields, $j_fields);
1047		}
1048
1049		if (is_array($fields) && !empty($fields))
1050		{
1051			foreach ($fields as $k => $v)
1052			{
1053				// If the property is not the primary key or private, reset it.
1054				if ($k != $this->_tbl_key && (strpos($k, '_') !== 0))
1055				{
1056					$this->$k = $v->Default;
1057				}
1058			}
1059
1060			if (!$this->onAfterReset())
1061			{
1062				return false;
1063			}
1064		}
1065	}
1066
1067    /**
1068     * Clones the current object, after resetting it
1069     *
1070     * @return static
1071     */
1072    public function getClone()
1073    {
1074        $clone = clone $this;
1075        $clone->reset();
1076
1077        $key = $this->getKeyName();
1078        $clone->$key = null;
1079
1080        return $clone;
1081    }
1082
1083	/**
1084	 * Generic check for whether dependencies exist for this object in the db schema
1085	 *
1086	 * @param   integer  $oid    The primary key of the record to delete
1087	 * @param   array    $joins  Any joins to foreign table, used to determine if dependent records exist
1088	 *
1089	 * @return  boolean  True if the record can be deleted
1090	 */
1091	public function canDelete($oid = null, $joins = null)
1092	{
1093		$k = $this->_tbl_key;
1094
1095		if ($oid)
1096		{
1097			$this->$k = intval($oid);
1098		}
1099
1100		if (is_array($joins))
1101		{
1102			$db      = $this->_db;
1103			$query   = $db->getQuery(true)
1104				->select($db->qn('master') . '.' . $db->qn($k))
1105				->from($db->qn($this->_tbl) . ' AS ' . $db->qn('master'));
1106			$tableNo = 0;
1107
1108			foreach ($joins as $table)
1109			{
1110				$tableNo++;
1111				$query->select(
1112					array(
1113						'COUNT(DISTINCT ' . $db->qn('t' . $tableNo) . '.' . $db->qn($table['idfield']) . ') AS ' . $db->qn($table['idalias'])
1114					)
1115				);
1116				$query->join('LEFT', $db->qn($table['name']) .
1117					' AS ' . $db->qn('t' . $tableNo) .
1118					' ON ' . $db->qn('t' . $tableNo) . '.' . $db->qn($table['joinfield']) .
1119					' = ' . $db->qn('master') . '.' . $db->qn($k)
1120				);
1121			}
1122
1123			$query->where($db->qn('master') . '.' . $db->qn($k) . ' = ' . $db->q($this->$k));
1124			$query->group($db->qn('master') . '.' . $db->qn($k));
1125			$this->_db->setQuery((string) $query);
1126
1127			if (version_compare(JVERSION, '3.0', 'ge'))
1128			{
1129				try
1130				{
1131					$obj = $this->_db->loadObject();
1132				}
1133				catch (Exception $e)
1134				{
1135					$this->setError($e->getMessage());
1136				}
1137			}
1138			else
1139			{
1140				if (!$obj = $this->_db->loadObject())
1141				{
1142					$this->setError($this->_db->getErrorMsg());
1143
1144					return false;
1145				}
1146			}
1147
1148			$msg = array();
1149			$i   = 0;
1150
1151			foreach ($joins as $table)
1152			{
1153				$k = $table['idalias'];
1154
1155				if ($obj->$k > 0)
1156				{
1157					$msg[] = JText::_($table['label']);
1158				}
1159
1160				$i++;
1161			}
1162
1163			if (count($msg))
1164			{
1165				$option  = $this->input->getCmd('option', 'com_foobar');
1166				$comName = str_replace('com_', '', $option);
1167				$tview   = str_replace('#__' . $comName . '_', '', $this->_tbl);
1168				$prefix  = $option . '_' . $tview . '_NODELETE_';
1169
1170				foreach ($msg as $key)
1171				{
1172					$this->setError(JText::_($prefix . $key));
1173				}
1174
1175				return false;
1176			}
1177			else
1178			{
1179				return true;
1180			}
1181		}
1182
1183		return true;
1184	}
1185
1186	/**
1187	 * Method to bind an associative array or object to the FOFTable instance.This
1188	 * method only binds properties that are publicly accessible and optionally
1189	 * takes an array of properties to ignore when binding.
1190	 *
1191	 * @param   mixed  $src     An associative array or object to bind to the FOFTable instance.
1192	 * @param   mixed  $ignore  An optional array or space separated list of properties to ignore while binding.
1193	 *
1194	 * @return  boolean  True on success.
1195	 *
1196	 * @throws  InvalidArgumentException
1197	 */
1198	public function bind($src, $ignore = array())
1199	{
1200		if (!$this->onBeforeBind($src))
1201		{
1202			return false;
1203		}
1204
1205		// If the source value is not an array or object return false.
1206		if (!is_object($src) && !is_array($src))
1207		{
1208			throw new InvalidArgumentException(sprintf('%s::bind(*%s*)', get_class($this), gettype($src)));
1209		}
1210
1211		// If the source value is an object, get its accessible properties.
1212		if (is_object($src))
1213		{
1214			$src = get_object_vars($src);
1215		}
1216
1217		// If the ignore value is a string, explode it over spaces.
1218		if (!is_array($ignore))
1219		{
1220			$ignore = explode(' ', $ignore);
1221		}
1222
1223		// Bind the source value, excluding the ignored fields.
1224		foreach ($this->getKnownFields() as $k)
1225		{
1226			// Only process fields not in the ignore array.
1227			if (!in_array($k, $ignore))
1228			{
1229				if (isset($src[$k]))
1230				{
1231					$this->$k = $src[$k];
1232				}
1233			}
1234		}
1235
1236		$result = $this->onAfterBind($src);
1237
1238		return $result;
1239	}
1240
1241	/**
1242	 * Method to store a row in the database from the FOFTable instance properties.
1243	 * If a primary key value is set the row with that primary key value will be
1244	 * updated with the instance property values.  If no primary key value is set
1245	 * a new row will be inserted into the database with the properties from the
1246	 * FOFTable instance.
1247	 *
1248	 * @param   boolean  $updateNulls  True to update fields even if they are null.
1249	 *
1250	 * @return  boolean  True on success.
1251	 */
1252	public function store($updateNulls = false)
1253	{
1254		if (!$this->onBeforeStore($updateNulls))
1255		{
1256			return false;
1257		}
1258
1259		$k = $this->_tbl_key;
1260
1261		if ($this->$k == 0)
1262		{
1263			$this->$k = null;
1264		}
1265
1266		// Create the object used for inserting/updating data to the database
1267		$fields     = $this->getTableFields();
1268		$properties = $this->getKnownFields();
1269		$keys       = array();
1270
1271		foreach ($properties as $property)
1272		{
1273			// 'input' property is a reserved name
1274
1275			if (isset($fields[$property]))
1276			{
1277				$keys[] = $property;
1278			}
1279		}
1280
1281		$updateObject = array();
1282		foreach ($keys as $key)
1283		{
1284			$updateObject[$key] = $this->$key;
1285		}
1286		$updateObject = (object)$updateObject;
1287
1288		/**
1289		 * While the documentation for update/insertObject and execute() say they return a boolean,
1290		 * not all of the implemtnations.  Depending on the version of J! and the specific driver,
1291		 * they may return a database object, or boolean, or a mix, or toss an exception.  So try/catch,
1292		 * and test for false.
1293		 */
1294
1295		try
1296		{
1297			// If a primary key exists update the object, otherwise insert it.
1298			if ($this->$k)
1299			{
1300				$result = $this->_db->updateObject($this->_tbl, $updateObject, $this->_tbl_key, $updateNulls);
1301			}
1302			else
1303			{
1304				$result = $this->_db->insertObject($this->_tbl, $updateObject, $this->_tbl_key);
1305			}
1306
1307			if ($result === false)
1308			{
1309				$this->setError($this->_db->getErrorMsg());
1310
1311				return false;
1312			}
1313		}
1314		catch (Exception $e)
1315		{
1316			$this->setError($e->getMessage());
1317		}
1318
1319		$this->bind($updateObject);
1320
1321		if ($this->_locked)
1322		{
1323			$this->_unlock();
1324		}
1325
1326		$result = $this->onAfterStore();
1327
1328		return $result;
1329	}
1330
1331	/**
1332	 * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
1333	 * Negative numbers move the row up in the sequence and positive numbers move it down.
1334	 *
1335	 * @param   integer  $delta  The direction and magnitude to move the row in the ordering sequence.
1336	 * @param   string   $where  WHERE clause to use for limiting the selection of rows to compact the
1337	 *                           ordering values.
1338	 *
1339	 * @return  mixed    Boolean  True on success.
1340	 *
1341	 * @throws  UnexpectedValueException
1342	 */
1343	public function move($delta, $where = '')
1344	{
1345		if (!$this->onBeforeMove($delta, $where))
1346		{
1347			return false;
1348		}
1349
1350		// If there is no ordering field set an error and return false.
1351		$ordering_field = $this->getColumnAlias('ordering');
1352
1353		if (!in_array($ordering_field, $this->getKnownFields()))
1354		{
1355			throw new UnexpectedValueException(sprintf('%s does not support ordering.', $this->_tbl));
1356		}
1357
1358		// If the change is none, do nothing.
1359		if (empty($delta))
1360		{
1361			$result = $this->onAfterMove();
1362
1363			return $result;
1364		}
1365
1366		$k     = $this->_tbl_key;
1367		$row   = null;
1368		$query = $this->_db->getQuery(true);
1369
1370        // If the table is not loaded, return false
1371        if (empty($this->$k))
1372        {
1373            return false;
1374        }
1375
1376		// Select the primary key and ordering values from the table.
1377		$query->select(array($this->_db->qn($this->_tbl_key), $this->_db->qn($ordering_field)));
1378		$query->from($this->_tbl);
1379
1380		// If the movement delta is negative move the row up.
1381
1382		if ($delta < 0)
1383		{
1384			$query->where($this->_db->qn($ordering_field) . ' < ' . $this->_db->q((int) $this->$ordering_field));
1385			$query->order($this->_db->qn($ordering_field) . ' DESC');
1386		}
1387
1388		// If the movement delta is positive move the row down.
1389
1390		elseif ($delta > 0)
1391		{
1392			$query->where($this->_db->qn($ordering_field) . ' > ' . $this->_db->q((int) $this->$ordering_field));
1393			$query->order($this->_db->qn($ordering_field) . ' ASC');
1394		}
1395
1396		// Add the custom WHERE clause if set.
1397
1398		if ($where)
1399		{
1400			$query->where($where);
1401		}
1402
1403		// Select the first row with the criteria.
1404		$this->_db->setQuery($query, 0, 1);
1405		$row = $this->_db->loadObject();
1406
1407		// If a row is found, move the item.
1408
1409		if (!empty($row))
1410		{
1411			// Update the ordering field for this instance to the row's ordering value.
1412			$query = $this->_db->getQuery(true);
1413			$query->update($this->_tbl);
1414			$query->set($this->_db->qn($ordering_field) . ' = ' . $this->_db->q((int) $row->$ordering_field));
1415			$query->where($this->_tbl_key . ' = ' . $this->_db->q($this->$k));
1416			$this->_db->setQuery($query);
1417			$this->_db->execute();
1418
1419			// Update the ordering field for the row to this instance's ordering value.
1420			$query = $this->_db->getQuery(true);
1421			$query->update($this->_tbl);
1422			$query->set($this->_db->qn($ordering_field) . ' = ' . $this->_db->q((int) $this->$ordering_field));
1423			$query->where($this->_tbl_key . ' = ' . $this->_db->q($row->$k));
1424			$this->_db->setQuery($query);
1425			$this->_db->execute();
1426
1427			// Update the instance value.
1428			$this->$ordering_field = $row->$ordering_field;
1429		}
1430		else
1431		{
1432			// Update the ordering field for this instance.
1433			$query = $this->_db->getQuery(true);
1434			$query->update($this->_tbl);
1435			$query->set($this->_db->qn($ordering_field) . ' = ' . $this->_db->q((int) $this->$ordering_field));
1436			$query->where($this->_tbl_key . ' = ' . $this->_db->q($this->$k));
1437			$this->_db->setQuery($query);
1438			$this->_db->execute();
1439		}
1440
1441		$result = $this->onAfterMove();
1442
1443		return $result;
1444	}
1445
1446    /**
1447     * Change the ordering of the records of the table
1448     *
1449     * @param   string   $where  The WHERE clause of the SQL used to fetch the order
1450     *
1451     * @return  boolean  True is successful
1452     *
1453     * @throws  UnexpectedValueException
1454     */
1455	public function reorder($where = '')
1456	{
1457		if (!$this->onBeforeReorder($where))
1458		{
1459			return false;
1460		}
1461
1462		// If there is no ordering field set an error and return false.
1463
1464		$order_field = $this->getColumnAlias('ordering');
1465
1466		if (!in_array($order_field, $this->getKnownFields()))
1467		{
1468			throw new UnexpectedValueException(sprintf('%s does not support ordering.', $this->_tbl_key));
1469		}
1470
1471		$k = $this->_tbl_key;
1472
1473		// Get the primary keys and ordering values for the selection.
1474		$query = $this->_db->getQuery(true);
1475		$query->select($this->_tbl_key . ', ' . $this->_db->qn($order_field));
1476		$query->from($this->_tbl);
1477		$query->where($this->_db->qn($order_field) . ' >= ' . $this->_db->q(0));
1478		$query->order($this->_db->qn($order_field));
1479
1480		// Setup the extra where and ordering clause data.
1481
1482		if ($where)
1483		{
1484			$query->where($where);
1485		}
1486
1487		$this->_db->setQuery($query);
1488		$rows = $this->_db->loadObjectList();
1489
1490		// Compact the ordering values.
1491
1492		foreach ($rows as $i => $row)
1493		{
1494			// Make sure the ordering is a positive integer.
1495
1496			if ($row->$order_field >= 0)
1497			{
1498				// Only update rows that are necessary.
1499
1500				if ($row->$order_field != $i + 1)
1501				{
1502					// Update the row ordering field.
1503					$query = $this->_db->getQuery(true);
1504					$query->update($this->_tbl);
1505					$query->set($this->_db->qn($order_field) . ' = ' . $this->_db->q($i + 1));
1506					$query->where($this->_tbl_key . ' = ' . $this->_db->q($row->$k));
1507					$this->_db->setQuery($query);
1508					$this->_db->execute();
1509				}
1510			}
1511		}
1512
1513		$result = $this->onAfterReorder();
1514
1515		return $result;
1516	}
1517
1518	/**
1519	 * Check out (lock) a record
1520	 *
1521	 * @param   integer  $userId  The locking user's ID
1522	 * @param   integer  $oid     The primary key value of the record to lock
1523	 *
1524	 * @return  boolean  True on success
1525	 */
1526	public function checkout($userId, $oid = null)
1527	{
1528		$fldLockedBy = $this->getColumnAlias('locked_by');
1529		$fldLockedOn = $this->getColumnAlias('locked_on');
1530
1531		if (!(in_array($fldLockedBy, $this->getKnownFields())
1532			|| in_array($fldLockedOn, $this->getKnownFields())))
1533		{
1534			return true;
1535		}
1536
1537		$k = $this->_tbl_key;
1538
1539		if ($oid !== null)
1540		{
1541			$this->$k = $oid;
1542		}
1543
1544        // No primary key defined, stop here
1545        if (!$this->$k)
1546        {
1547            return false;
1548        }
1549
1550		$date = FOFPlatform::getInstance()->getDate();
1551
1552		if (method_exists($date, 'toSql'))
1553		{
1554			$time = $date->toSql();
1555		}
1556		else
1557		{
1558			$time = $date->toMySQL();
1559		}
1560
1561
1562		$query = $this->_db->getQuery(true)
1563			->update($this->_db->qn($this->_tbl))
1564			->set(
1565				array(
1566					$this->_db->qn($fldLockedBy) . ' = ' . $this->_db->q((int) $userId),
1567					$this->_db->qn($fldLockedOn) . ' = ' . $this->_db->q($time)
1568				)
1569			)
1570			->where($this->_db->qn($this->_tbl_key) . ' = ' . $this->_db->q($this->$k));
1571		$this->_db->setQuery((string) $query);
1572
1573		$this->$fldLockedBy = $userId;
1574		$this->$fldLockedOn = $time;
1575
1576		return $this->_db->execute();
1577	}
1578
1579	/**
1580	 * Check in (unlock) a record
1581	 *
1582	 * @param   integer  $oid  The primary key value of the record to unlock
1583	 *
1584	 * @return  boolean  True on success
1585	 */
1586	public function checkin($oid = null)
1587	{
1588		$fldLockedBy = $this->getColumnAlias('locked_by');
1589		$fldLockedOn = $this->getColumnAlias('locked_on');
1590
1591		if (!(in_array($fldLockedBy, $this->getKnownFields())
1592			|| in_array($fldLockedOn, $this->getKnownFields())))
1593		{
1594			return true;
1595		}
1596
1597		$k = $this->_tbl_key;
1598
1599		if ($oid !== null)
1600		{
1601			$this->$k = $oid;
1602		}
1603
1604		if ($this->$k == null)
1605		{
1606			return false;
1607		}
1608
1609		$query = $this->_db->getQuery(true)
1610			->update($this->_db->qn($this->_tbl))
1611			->set(
1612				array(
1613					$this->_db->qn($fldLockedBy) . ' = 0',
1614					$this->_db->qn($fldLockedOn) . ' = ' . $this->_db->q($this->_db->getNullDate())
1615				)
1616			)
1617			->where($this->_db->qn($this->_tbl_key) . ' = ' . $this->_db->q($this->$k));
1618		$this->_db->setQuery((string) $query);
1619
1620		$this->$fldLockedBy = 0;
1621		$this->$fldLockedOn = '';
1622
1623		return $this->_db->execute();
1624	}
1625
1626    /**
1627     * Is a record locked?
1628     *
1629     * @param   integer $with            The userid to preform the match with. If an item is checked
1630     *                                   out by this user the function will return false.
1631     * @param   integer $unused_against  Junk inherited from JTable; ignore
1632     *
1633     * @throws  UnexpectedValueException
1634     *
1635     * @return  boolean  True if the record is locked by another user
1636     */
1637	public function isCheckedOut($with = 0, $unused_against = null)
1638	{
1639        $against     = null;
1640		$fldLockedBy = $this->getColumnAlias('locked_by');
1641
1642        $k  = $this->_tbl_key;
1643
1644        // If no primary key is given, return false.
1645
1646        if ($this->$k === null)
1647        {
1648            throw new UnexpectedValueException('Null primary key not allowed.');
1649        }
1650
1651		if (isset($this) && is_a($this, 'FOFTable') && !$against)
1652		{
1653			$against = $this->get($fldLockedBy);
1654		}
1655
1656		// Item is not checked out, or being checked out by the same user
1657
1658		if (!$against || $against == $with)
1659		{
1660			return false;
1661		}
1662
1663		$session = JTable::getInstance('session');
1664
1665		return $session->exists($against);
1666	}
1667
1668	/**
1669	 * Copy (duplicate) one or more records
1670	 *
1671	 * @param   integer|array  $cid  The primary key value (or values) or the record(s) to copy
1672	 *
1673	 * @return  boolean  True on success
1674	 */
1675	public function copy($cid = null)
1676	{
1677		//We have to cast the id as array, or the helper function will return an empty set
1678		if($cid)
1679		{
1680			$cid = (array) $cid;
1681		}
1682
1683        FOFUtilsArray::toInteger($cid);
1684		$k = $this->_tbl_key;
1685
1686		if (count($cid) < 1)
1687		{
1688			if ($this->$k)
1689			{
1690				$cid = array($this->$k);
1691			}
1692			else
1693			{
1694				$this->setError("No items selected.");
1695
1696				return false;
1697			}
1698		}
1699
1700		$created_by  = $this->getColumnAlias('created_by');
1701		$created_on  = $this->getColumnAlias('created_on');
1702		$modified_by = $this->getColumnAlias('modified_by');
1703		$modified_on = $this->getColumnAlias('modified_on');
1704
1705		$locked_byName = $this->getColumnAlias('locked_by');
1706		$checkin       = in_array($locked_byName, $this->getKnownFields());
1707
1708		foreach ($cid as $item)
1709		{
1710			// Prevent load with id = 0
1711
1712			if (!$item)
1713			{
1714				continue;
1715			}
1716
1717			$this->load($item);
1718
1719			if ($checkin)
1720			{
1721				// We're using the checkin and the record is used by someone else
1722
1723				if ($this->isCheckedOut($item))
1724				{
1725					continue;
1726				}
1727			}
1728
1729			if (!$this->onBeforeCopy($item))
1730			{
1731				continue;
1732			}
1733
1734			$this->$k           = null;
1735			$this->$created_by  = null;
1736			$this->$created_on  = null;
1737			$this->$modified_on = null;
1738			$this->$modified_by = null;
1739
1740			// Let's fire the event only if everything is ok
1741			if ($this->store())
1742			{
1743				$this->onAfterCopy($item);
1744			}
1745
1746			$this->reset();
1747		}
1748
1749		return true;
1750	}
1751
1752	/**
1753	 * Publish or unpublish records
1754	 *
1755	 * @param   integer|array  $cid      The primary key value(s) of the item(s) to publish/unpublish
1756	 * @param   integer        $publish  1 to publish an item, 0 to unpublish
1757	 * @param   integer        $user_id  The user ID of the user (un)publishing the item.
1758	 *
1759	 * @return  boolean  True on success, false on failure (e.g. record is locked)
1760	 */
1761	public function publish($cid = null, $publish = 1, $user_id = 0)
1762	{
1763		$enabledName   = $this->getColumnAlias('enabled');
1764		$locked_byName = $this->getColumnAlias('locked_by');
1765
1766		// Mhm... you called the publish method on a table without publish support...
1767		if(!in_array($enabledName, $this->getKnownFields()))
1768		{
1769			return false;
1770		}
1771
1772		//We have to cast the id as array, or the helper function will return an empty set
1773		if($cid)
1774		{
1775			$cid = (array) $cid;
1776		}
1777
1778        FOFUtilsArray::toInteger($cid);
1779		$user_id = (int) $user_id;
1780		$publish = (int) $publish;
1781		$k       = $this->_tbl_key;
1782
1783		if (count($cid) < 1)
1784		{
1785			if ($this->$k)
1786			{
1787				$cid = array($this->$k);
1788			}
1789			else
1790			{
1791				$this->setError("No items selected.");
1792
1793				return false;
1794			}
1795		}
1796
1797		if (!$this->onBeforePublish($cid, $publish))
1798		{
1799			return false;
1800		}
1801
1802		$query = $this->_db->getQuery(true)
1803			->update($this->_db->qn($this->_tbl))
1804			->set($this->_db->qn($enabledName) . ' = ' . (int) $publish);
1805
1806		$checkin = in_array($locked_byName, $this->getKnownFields());
1807
1808		if ($checkin)
1809		{
1810			$query->where(
1811				' (' . $this->_db->qn($locked_byName) .
1812					' = 0 OR ' . $this->_db->qn($locked_byName) . ' = ' . (int) $user_id . ')', 'AND'
1813			);
1814		}
1815
1816		// TODO Rewrite this statment using IN. Check if it work in SQLServer and PostgreSQL
1817		$cids = $this->_db->qn($k) . ' = ' . implode(' OR ' . $this->_db->qn($k) . ' = ', $cid);
1818
1819		$query->where('(' . $cids . ')');
1820
1821		$this->_db->setQuery((string) $query);
1822
1823		if (version_compare(JVERSION, '3.0', 'ge'))
1824		{
1825			try
1826			{
1827				$this->_db->execute();
1828			}
1829			catch (Exception $e)
1830			{
1831				$this->setError($e->getMessage());
1832			}
1833		}
1834		else
1835		{
1836			if (!$this->_db->execute())
1837			{
1838				$this->setError($this->_db->getErrorMsg());
1839
1840				return false;
1841			}
1842		}
1843
1844		if (count($cid) == 1 && $checkin)
1845		{
1846			if ($this->_db->getAffectedRows() == 1)
1847			{
1848				$this->checkin($cid[0]);
1849
1850				if ($this->$k == $cid[0])
1851				{
1852					$this->$enabledName = $publish;
1853				}
1854			}
1855		}
1856
1857		$this->setError('');
1858
1859		return true;
1860	}
1861
1862	/**
1863	 * Delete a record
1864	 *
1865	 * @param   integer $oid  The primary key value of the item to delete
1866	 *
1867	 * @throws  UnexpectedValueException
1868	 *
1869	 * @return  boolean  True on success
1870	 */
1871	public function delete($oid = null)
1872	{
1873		if ($oid)
1874		{
1875			$this->load($oid);
1876		}
1877
1878		$k  = $this->_tbl_key;
1879		$pk = (!$oid) ? $this->$k : $oid;
1880
1881		// If no primary key is given, return false.
1882		if (!$pk)
1883		{
1884			throw new UnexpectedValueException('Null primary key not allowed.');
1885		}
1886
1887		// Execute the logic only if I have a primary key, otherwise I could have weird results
1888		if (!$this->onBeforeDelete($oid))
1889		{
1890			return false;
1891		}
1892
1893		// Delete the row by primary key.
1894		$query = $this->_db->getQuery(true);
1895		$query->delete();
1896		$query->from($this->_tbl);
1897		$query->where($this->_tbl_key . ' = ' . $this->_db->q($pk));
1898		$this->_db->setQuery($query);
1899
1900		$this->_db->execute();
1901
1902		$result = $this->onAfterDelete($oid);
1903
1904		return $result;
1905	}
1906
1907	/**
1908	 * Register a hit on a record
1909	 *
1910	 * @param   integer  $oid  The primary key value of the record
1911	 * @param   boolean  $log  Should I log the hit?
1912	 *
1913	 * @return  boolean  True on success
1914	 */
1915	public function hit($oid = null, $log = false)
1916	{
1917		if (!$this->onBeforeHit($oid, $log))
1918		{
1919			return false;
1920		}
1921
1922		// If there is no hits field, just return true.
1923		$hits_field = $this->getColumnAlias('hits');
1924
1925		if (!in_array($hits_field, $this->getKnownFields()))
1926		{
1927			return true;
1928		}
1929
1930		$k  = $this->_tbl_key;
1931		$pk = ($oid) ? $oid : $this->$k;
1932
1933		// If no primary key is given, return false.
1934		if (!$pk)
1935		{
1936			$result = false;
1937		}
1938		else
1939		{
1940			// Check the row in by primary key.
1941			$query = $this->_db->getQuery(true)
1942						  ->update($this->_tbl)
1943						  ->set($this->_db->qn($hits_field) . ' = (' . $this->_db->qn($hits_field) . ' + 1)')
1944						  ->where($this->_tbl_key . ' = ' . $this->_db->q($pk));
1945
1946			$this->_db->setQuery($query)->execute();
1947
1948			// In order to update the table object, I have to load the table
1949			if(!$this->$k)
1950			{
1951				$query = $this->_db->getQuery(true)
1952							  ->select($this->_db->qn($hits_field))
1953							  ->from($this->_db->qn($this->_tbl))
1954							  ->where($this->_db->qn($this->_tbl_key) . ' = ' . $this->_db->q($pk));
1955
1956				$this->$hits_field = $this->_db->setQuery($query)->loadResult();
1957			}
1958			else
1959			{
1960				// Set table values in the object.
1961				$this->$hits_field++;
1962			}
1963
1964			$result = true;
1965		}
1966
1967		if ($result)
1968		{
1969			$result = $this->onAfterHit($oid);
1970		}
1971
1972		return $result;
1973	}
1974
1975	/**
1976	 * Export the item as a CSV line
1977	 *
1978	 * @param   string  $separator  CSV separator. Tip: use "\t" to get a TSV file instead.
1979	 *
1980	 * @return  string  The CSV line
1981	 */
1982	public function toCSV($separator = ',')
1983	{
1984		$csv = array();
1985
1986		foreach (get_object_vars($this) as $k => $v)
1987		{
1988			if (!in_array($k, $this->getKnownFields()))
1989			{
1990				continue;
1991			}
1992
1993			$csv[] = '"' . str_replace('"', '""', $v) . '"';
1994		}
1995
1996		$csv = implode($separator, $csv);
1997
1998		return $csv;
1999	}
2000
2001	/**
2002	 * Exports the table in array format
2003	 *
2004	 * @return  array
2005	 */
2006	public function getData()
2007	{
2008		$ret = array();
2009
2010		foreach (get_object_vars($this) as $k => $v)
2011		{
2012			if (!in_array($k, $this->getKnownFields()))
2013			{
2014				continue;
2015			}
2016
2017			$ret[$k] = $v;
2018		}
2019
2020		return $ret;
2021	}
2022
2023	/**
2024	 * Get the header for exporting item list to CSV
2025	 *
2026	 * @param   string  $separator  CSV separator. Tip: use "\t" to get a TSV file instead.
2027	 *
2028	 * @return  string  The CSV file's header
2029	 */
2030	public function getCSVHeader($separator = ',')
2031	{
2032		$csv = array();
2033
2034		foreach (get_object_vars($this) as $k => $v)
2035		{
2036			if (!in_array($k, $this->getKnownFields()))
2037			{
2038				continue;
2039			}
2040
2041			$csv[] = '"' . str_replace('"', '\"', $k) . '"';
2042		}
2043
2044		$csv = implode($separator, $csv);
2045
2046		return $csv;
2047	}
2048
2049	/**
2050	 * Get the columns from a database table.
2051	 *
2052	 * @param   string  $tableName  Table name. If null current table is used
2053	 *
2054	 * @return  mixed  An array of the field names, or false if an error occurs.
2055	 */
2056	public function getTableFields($tableName = null)
2057	{
2058		// Should I load the cached data?
2059		$useCache = array_key_exists('use_table_cache', $this->config) ? $this->config['use_table_cache'] : false;
2060
2061		// Make sure we have a list of tables in this db
2062
2063		if (empty(self::$tableCache))
2064		{
2065			if ($useCache)
2066			{
2067				// Try to load table cache from a cache file
2068				$cacheData = FOFPlatform::getInstance()->getCache('tables', null);
2069
2070				// Unserialise the cached data, or set the table cache to empty
2071				// if the cache data wasn't loaded.
2072				if (!is_null($cacheData))
2073				{
2074					self::$tableCache = json_decode($cacheData, true);
2075				}
2076				else
2077				{
2078					self::$tableCache = array();
2079				}
2080			}
2081
2082			// This check is true if the cache data doesn't exist / is not loaded
2083			if (empty(self::$tableCache))
2084			{
2085				self::$tableCache = $this->_db->getTableList();
2086
2087				if ($useCache)
2088				{
2089					FOFPlatform::getInstance()->setCache('tables', json_encode(self::$tableCache));
2090				}
2091			}
2092		}
2093
2094		// Make sure the cached table fields cache is loaded
2095		if (empty(self::$tableFieldCache))
2096		{
2097			if ($useCache)
2098			{
2099				// Try to load table cache from a cache file
2100				$cacheData = FOFPlatform::getInstance()->getCache('tablefields', null);
2101
2102				// Unserialise the cached data, or set to empty if the cache
2103				// data wasn't loaded.
2104				if (!is_null($cacheData))
2105				{
2106					$decoded = json_decode($cacheData, true);
2107					$tableCache = array();
2108
2109					if (count($decoded))
2110					{
2111						foreach ($decoded as $myTableName => $tableFields)
2112						{
2113							$temp = array();
2114
2115							if (is_array($tableFields))
2116							{
2117								foreach($tableFields as $field => $def)
2118								{
2119									$temp[$field] = (object)$def;
2120								}
2121								$tableCache[$myTableName] = $temp;
2122							}
2123							elseif (is_object($tableFields) || is_bool($tableFields))
2124							{
2125								$tableCache[$myTableName] = $tableFields;
2126							}
2127						}
2128					}
2129
2130					self::$tableFieldCache = $tableCache;
2131				}
2132				else
2133				{
2134					self::$tableFieldCache = array();
2135				}
2136			}
2137		}
2138
2139		if (!$tableName)
2140		{
2141			$tableName = $this->_tbl;
2142		}
2143
2144		// Try to load again column specifications if the table is not loaded OR if it's loaded and
2145		// the previous call returned an error
2146		if (!array_key_exists($tableName, self::$tableFieldCache) ||
2147			(isset(self::$tableFieldCache[$tableName]) && !self::$tableFieldCache[$tableName]))
2148		{
2149			// Lookup the fields for this table only once.
2150			$name = $tableName;
2151
2152			$prefix = $this->_db->getPrefix();
2153
2154			if (substr($name, 0, 3) == '#__')
2155			{
2156				$checkName = $prefix . substr($name, 3);
2157			}
2158			else
2159			{
2160				$checkName = $name;
2161			}
2162
2163			if (!in_array($checkName, self::$tableCache))
2164			{
2165				// The table doesn't exist. Return false.
2166				self::$tableFieldCache[$tableName] = false;
2167			}
2168			elseif (version_compare(JVERSION, '3.0', 'ge'))
2169			{
2170				$fields = $this->_db->getTableColumns($name, false);
2171
2172				if (empty($fields))
2173				{
2174					$fields = false;
2175				}
2176
2177				self::$tableFieldCache[$tableName] = $fields;
2178			}
2179			else
2180			{
2181				$fields = $this->_db->getTableFields($name, false);
2182
2183				if (!isset($fields[$name]))
2184				{
2185					$fields = false;
2186				}
2187
2188				self::$tableFieldCache[$tableName] = $fields[$name];
2189			}
2190
2191			// PostgreSQL date type compatibility
2192			if (($this->_db->name == 'postgresql') && (self::$tableFieldCache[$tableName] != false))
2193			{
2194				foreach (self::$tableFieldCache[$tableName] as $field)
2195				{
2196					if (strtolower($field->type) == 'timestamp without time zone')
2197					{
2198						if (stristr($field->Default, '\'::timestamp without time zone'))
2199						{
2200							list ($date, $junk) = explode('::', $field->Default, 2);
2201							$field->Default = trim($date, "'");
2202						}
2203					}
2204				}
2205			}
2206
2207			// Save the data for this table into the cache
2208			if ($useCache)
2209			{
2210				$cacheData = FOFPlatform::getInstance()->setCache('tablefields', json_encode(self::$tableFieldCache));
2211			}
2212		}
2213
2214		return self::$tableFieldCache[$tableName];
2215	}
2216
2217	public function getTableAlias()
2218	{
2219		return $this->_tableAlias;
2220	}
2221
2222	public function setTableAlias($string)
2223	{
2224		$string = preg_replace('#[^A-Z0-9_]#i', '', $string);
2225		$this->_tableAlias = $string;
2226	}
2227
2228	/**
2229	 * Method to return the real name of a "special" column such as ordering, hits, published
2230	 * etc etc. In this way you are free to follow your db naming convention and use the
2231	 * built in Joomla functions.
2232	 *
2233	 * @param   string  $column  Name of the "special" column (ie ordering, hits etc etc)
2234	 *
2235	 * @return  string  The string that identify the special
2236	 */
2237	public function getColumnAlias($column)
2238	{
2239		if (isset($this->_columnAlias[$column]))
2240		{
2241			$return = $this->_columnAlias[$column];
2242		}
2243		else
2244		{
2245			$return = $column;
2246		}
2247
2248		$return = preg_replace('#[^A-Z0-9_]#i', '', $return);
2249
2250		return $return;
2251	}
2252
2253	/**
2254	 * Method to register a column alias for a "special" column.
2255	 *
2256	 * @param   string  $column       The "special" column (ie ordering)
2257	 * @param   string  $columnAlias  The real column name (ie foo_ordering)
2258	 *
2259	 * @return  void
2260	 */
2261	public function setColumnAlias($column, $columnAlias)
2262	{
2263		$column = strtolower($column);
2264
2265		$column                      = preg_replace('#[^A-Z0-9_]#i', '', $column);
2266		$this->_columnAlias[$column] = $columnAlias;
2267	}
2268
2269	/**
2270	 * Get a JOIN query, used to join other tables
2271	 *
2272	 * @param   boolean  $asReference  Return an object reference instead of a copy
2273	 *
2274	 * @return  FOFDatabaseQuery  Query used to join other tables
2275	 */
2276	public function getQueryJoin($asReference = false)
2277	{
2278		if ($asReference)
2279		{
2280			return $this->_queryJoin;
2281		}
2282		else
2283		{
2284			if ($this->_queryJoin)
2285			{
2286				return clone $this->_queryJoin;
2287			}
2288			else
2289			{
2290				return null;
2291			}
2292		}
2293	}
2294
2295	/**
2296	 * Sets the query with joins to other tables
2297	 *
2298	 * @param   FOFDatabaseQuery  $query  The JOIN query to use
2299	 *
2300	 * @return  void
2301	 */
2302	public function setQueryJoin($query)
2303	{
2304		$this->_queryJoin = $query;
2305	}
2306
2307	/**
2308	 * Extracts the fields from the join query
2309	 *
2310	 * @return   array    Fields contained in the join query
2311	 */
2312	protected function getQueryJoinFields()
2313	{
2314		$query = $this->getQueryJoin();
2315
2316		if (!$query)
2317		{
2318			return array();
2319		}
2320
2321		$tables   = array();
2322		$j_tables = array();
2323		$j_fields = array();
2324
2325		// Get joined tables. Ignore FROM clause, since it should not be used (the starting point is the table "table")
2326		$joins    = $query->join;
2327
2328		foreach ($joins as $join)
2329		{
2330			$tables = array_merge($tables, $join->getElements());
2331		}
2332
2333		// Clean up table names
2334		foreach($tables as $table)
2335		{
2336			preg_match('#(.*)((\w)*(on|using))(.*)#i', $table, $matches);
2337
2338			if($matches && isset($matches[1]))
2339			{
2340				// I always want the first part, no matter what
2341				$parts = explode(' ', $matches[1]);
2342				$t_table = $parts[0];
2343
2344				if($this->isQuoted($t_table))
2345				{
2346					$t_table = substr($t_table, 1, strlen($t_table) - 2);
2347				}
2348
2349				if(!in_array($t_table, $j_tables))
2350				{
2351					$j_tables[] =  $t_table;
2352				}
2353			}
2354		}
2355
2356		// Do I have the current table inside the query join? Remove it (its fields are already ok)
2357		$find = array_search($this->getTableName(), $j_tables);
2358		if($find !== false)
2359		{
2360			unset($j_tables[$find]);
2361		}
2362
2363		// Get table fields
2364		$fields = array();
2365
2366		foreach ($j_tables as $table)
2367		{
2368			$t_fields = $this->getTableFields($table);
2369
2370			if ($t_fields)
2371			{
2372				$fields = array_merge($fields, $t_fields);
2373			}
2374		}
2375
2376		// Remove any fields that aren't in the joined select
2377		$j_select = $query->select;
2378
2379		if ($j_select && $j_select->getElements())
2380		{
2381			$j_fields = $this->normalizeSelectFields($j_select->getElements());
2382		}
2383
2384		// I can intesect the keys
2385		$fields   = array_intersect_key($fields, $j_fields);
2386
2387		// Now I walk again the array to change the key of columns that have an alias
2388		foreach ($j_fields as $column => $alias)
2389		{
2390			if ($column != $alias)
2391			{
2392				$fields[$alias] = $fields[$column];
2393				unset($fields[$column]);
2394			}
2395		}
2396
2397		return $fields;
2398	}
2399
2400	/**
2401	 * Normalizes the fields, returning an associative array with all the fields.
2402	 * Ie array('foobar as foo, bar') becomes array('foobar' => 'foo', 'bar' => 'bar')
2403	 *
2404	 * @param   array $fields    Array with column fields
2405	 *
2406	 * @return  array  Normalized array
2407	 */
2408	protected function normalizeSelectFields($fields)
2409	{
2410		$db     = FOFPlatform::getInstance()->getDbo();
2411		$return = array();
2412
2413		foreach ($fields as $field)
2414		{
2415			$t_fields = explode(',', $field);
2416
2417			foreach ($t_fields as $t_field)
2418			{
2419				// Is there any alias?
2420				$parts  = preg_split('#\sas\s#i', $t_field);
2421
2422				// Do I have a table.column situation? Let's get the field name
2423				$tableField  = explode('.', $parts[0]);
2424
2425				if(isset($tableField[1]))
2426				{
2427					$column = trim($tableField[1]);
2428				}
2429				else
2430				{
2431					$column = trim($tableField[0]);
2432				}
2433
2434				// Is this field quoted? If so, remove the quotes
2435				if($this->isQuoted($column))
2436				{
2437					$column = substr($column, 1, strlen($column) - 2);
2438				}
2439
2440				if(isset($parts[1]))
2441				{
2442					$alias = trim($parts[1]);
2443
2444					// Is this field quoted? If so, remove the quotes
2445					if($this->isQuoted($alias))
2446					{
2447						$alias = substr($alias, 1, strlen($alias) - 2);
2448					}
2449				}
2450				else
2451				{
2452					$alias = $column;
2453				}
2454
2455				$return[$column] = $alias;
2456			}
2457		}
2458
2459		return $return;
2460	}
2461
2462	/**
2463	 * Is the field quoted?
2464	 *
2465	 * @param   string  $column     Column field
2466	 *
2467	 * @return  bool    Is the field quoted?
2468	 */
2469	protected function isQuoted($column)
2470	{
2471		// Empty string, un-quoted by definition
2472		if(!$column)
2473		{
2474			return false;
2475		}
2476
2477		// I need some "magic". If the first char is not a letter, a number
2478		// an underscore or # (needed for table), then most likely the field is quoted
2479		preg_match_all('/^[a-z0-9_#]/i', $column, $matches);
2480
2481		if(!$matches[0])
2482		{
2483			return true;
2484		}
2485
2486		return false;
2487	}
2488
2489	/**
2490	 * The event which runs before binding data to the table
2491	 *
2492	 * NOTE TO 3RD PARTY DEVELOPERS:
2493	 *
2494	 * When you override the following methods in your child classes,
2495	 * be sure to call parent::method *AFTER* your code, otherwise the
2496	 * plugin events do NOT get triggered
2497	 *
2498	 * Example:
2499	 * protected function onBeforeBind(){
2500	 *       // Your code here
2501	 *     return parent::onBeforeBind() && $your_result;
2502	 * }
2503	 *
2504	 * Do not do it the other way around, e.g. return $your_result && parent::onBeforeBind()
2505	 * Due to  PHP short-circuit boolean evaluation the parent::onBeforeBind()
2506	 * will not be called if $your_result is false.
2507	 *
2508	 * @param   object|array  &$from  The data to bind
2509	 *
2510	 * @return  boolean  True on success
2511	 */
2512	protected function onBeforeBind(&$from)
2513	{
2514		// Call the behaviors
2515		$result = $this->tableDispatcher->trigger('onBeforeBind', array(&$this, &$from));
2516
2517		if (in_array(false, $result, true))
2518		{
2519			// Behavior failed, return false
2520			return false;
2521		}
2522
2523		if ($this->_trigger_events)
2524		{
2525			$name = FOFInflector::pluralize($this->getKeyName());
2526
2527			$result     = FOFPlatform::getInstance()->runPlugins('onBeforeBind' . ucfirst($name), array(&$this, &$from));
2528
2529			if (in_array(false, $result, true))
2530			{
2531				return false;
2532			}
2533			else
2534			{
2535				return true;
2536			}
2537		}
2538
2539		return true;
2540	}
2541
2542	/**
2543	 * The event which runs after loading a record from the database
2544	 *
2545	 * @param   boolean  &$result  Did the load succeeded?
2546	 *
2547	 * @return  void
2548	 */
2549	protected function onAfterLoad(&$result)
2550	{
2551		// Call the behaviors
2552		$eventResult = $this->tableDispatcher->trigger('onAfterLoad', array(&$this, &$result));
2553
2554		if (in_array(false, $eventResult, true))
2555		{
2556			// Behavior failed, return false
2557			$result = false;
2558			return false;
2559		}
2560
2561		if ($this->_trigger_events)
2562		{
2563			$name = FOFInflector::pluralize($this->getKeyName());
2564
2565			FOFPlatform::getInstance()->runPlugins('onAfterLoad' . ucfirst($name), array(&$this, &$result));
2566		}
2567	}
2568
2569	/**
2570	 * The event which runs before storing (saving) data to the database
2571	 *
2572	 * @param   boolean  $updateNulls  Should nulls be saved as nulls (true) or just skipped over (false)?
2573	 *
2574	 * @return  boolean  True to allow saving
2575	 */
2576	protected function onBeforeStore($updateNulls)
2577	{
2578		// Do we have a "Created" set of fields?
2579		$created_on  = $this->getColumnAlias('created_on');
2580		$created_by  = $this->getColumnAlias('created_by');
2581		$modified_on = $this->getColumnAlias('modified_on');
2582		$modified_by = $this->getColumnAlias('modified_by');
2583		$locked_on   = $this->getColumnAlias('locked_on');
2584		$locked_by   = $this->getColumnAlias('locked_by');
2585		$title       = $this->getColumnAlias('title');
2586		$slug        = $this->getColumnAlias('slug');
2587
2588		$hasCreatedOn = in_array($created_on, $this->getKnownFields());
2589		$hasCreatedBy = in_array($created_by, $this->getKnownFields());
2590
2591		if ($hasCreatedOn && $hasCreatedBy)
2592		{
2593			$hasModifiedOn = in_array($modified_on, $this->getKnownFields());
2594			$hasModifiedBy = in_array($modified_by, $this->getKnownFields());
2595
2596			$nullDate = $this->_db->getNullDate();
2597
2598			if (empty($this->$created_by) || ($this->$created_on == $nullDate) || empty($this->$created_on))
2599			{
2600				$uid = FOFPlatform::getInstance()->getUser()->id;
2601
2602				if ($uid)
2603				{
2604					$this->$created_by = FOFPlatform::getInstance()->getUser()->id;
2605				}
2606
2607				$date = FOFPlatform::getInstance()->getDate('now', null, false);
2608
2609				$this->$created_on = method_exists($date, 'toSql') ? $date->toSql() : $date->toMySQL();
2610			}
2611			elseif ($hasModifiedOn && $hasModifiedBy)
2612			{
2613				$uid = FOFPlatform::getInstance()->getUser()->id;
2614
2615				if ($uid)
2616				{
2617					$this->$modified_by = FOFPlatform::getInstance()->getUser()->id;
2618				}
2619
2620                $date = FOFPlatform::getInstance()->getDate('now', null, false);
2621
2622				$this->$modified_on = method_exists($date, 'toSql') ? $date->toSql() : $date->toMySQL();
2623			}
2624		}
2625
2626		// Do we have a set of title and slug fields?
2627		$hasTitle = in_array($title, $this->getKnownFields());
2628		$hasSlug  = in_array($slug, $this->getKnownFields());
2629
2630		if ($hasTitle && $hasSlug)
2631		{
2632			if (empty($this->$slug))
2633			{
2634				// Create a slug from the title
2635				$this->$slug = FOFStringUtils::toSlug($this->$title);
2636			}
2637			else
2638			{
2639				// Filter the slug for invalid characters
2640				$this->$slug = FOFStringUtils::toSlug($this->$slug);
2641			}
2642
2643			// Make sure we don't have a duplicate slug on this table
2644			$db    = $this->getDbo();
2645			$query = $db->getQuery(true)
2646				->select($db->qn($slug))
2647				->from($this->_tbl)
2648				->where($db->qn($slug) . ' = ' . $db->q($this->$slug))
2649				->where('NOT ' . $db->qn($this->_tbl_key) . ' = ' . $db->q($this->{$this->_tbl_key}));
2650			$db->setQuery($query);
2651			$existingItems = $db->loadAssocList();
2652
2653			$count   = 0;
2654			$newSlug = $this->$slug;
2655
2656			while (!empty($existingItems))
2657			{
2658				$count++;
2659				$newSlug = $this->$slug . '-' . $count;
2660				$query   = $db->getQuery(true)
2661					->select($db->qn($slug))
2662					->from($this->_tbl)
2663					->where($db->qn($slug) . ' = ' . $db->q($newSlug))
2664					->where('NOT '. $db->qn($this->_tbl_key) . ' = ' . $db->q($this->{$this->_tbl_key}));
2665				$db->setQuery($query);
2666				$existingItems = $db->loadAssocList();
2667			}
2668
2669			$this->$slug = $newSlug;
2670		}
2671
2672		// Call the behaviors
2673		$result = $this->tableDispatcher->trigger('onBeforeStore', array(&$this, $updateNulls));
2674
2675		if (in_array(false, $result, true))
2676		{
2677			// Behavior failed, return false
2678			return false;
2679		}
2680
2681		// Execute onBeforeStore<tablename> events in loaded plugins
2682		if ($this->_trigger_events)
2683		{
2684			$name       = FOFInflector::pluralize($this->getKeyName());
2685			$result     = FOFPlatform::getInstance()->runPlugins('onBeforeStore' . ucfirst($name), array(&$this, $updateNulls));
2686
2687			if (in_array(false, $result, true))
2688			{
2689				return false;
2690			}
2691			else
2692			{
2693				return true;
2694			}
2695		}
2696
2697		return true;
2698	}
2699
2700	/**
2701	 * The event which runs after binding data to the class
2702	 *
2703	 * @param   object|array  &$src  The data to bind
2704	 *
2705	 * @return  boolean  True to allow binding without an error
2706	 */
2707	protected function onAfterBind(&$src)
2708	{
2709		// Call the behaviors
2710		$options = array(
2711			'component' 	=> $this->input->get('option'),
2712			'view'			=> $this->input->get('view'),
2713			'table_prefix'	=> $this->_tablePrefix
2714		);
2715
2716		$result = $this->tableDispatcher->trigger('onAfterBind', array(&$this, &$src, $options));
2717
2718		if (in_array(false, $result, true))
2719		{
2720			// Behavior failed, return false
2721			return false;
2722		}
2723
2724		if ($this->_trigger_events)
2725		{
2726			$name = FOFInflector::pluralize($this->getKeyName());
2727
2728			$result     = FOFPlatform::getInstance()->runPlugins('onAfterBind' . ucfirst($name), array(&$this, &$src));
2729
2730			if (in_array(false, $result, true))
2731			{
2732				return false;
2733			}
2734			else
2735			{
2736				return true;
2737			}
2738		}
2739
2740		return true;
2741	}
2742
2743	/**
2744	 * The event which runs after storing (saving) data to the database
2745	 *
2746	 * @return  boolean  True to allow saving without an error
2747	 */
2748	protected function onAfterStore()
2749	{
2750		// Call the behaviors
2751		$result = $this->tableDispatcher->trigger('onAfterStore', array(&$this));
2752
2753		if (in_array(false, $result, true))
2754		{
2755			// Behavior failed, return false
2756			return false;
2757		}
2758
2759		if ($this->_trigger_events)
2760		{
2761			$name = FOFInflector::pluralize($this->getKeyName());
2762
2763			$result     = FOFPlatform::getInstance()->runPlugins('onAfterStore' . ucfirst($name), array(&$this));
2764
2765			if (in_array(false, $result, true))
2766			{
2767				return false;
2768			}
2769			else
2770			{
2771				return true;
2772			}
2773		}
2774
2775		return true;
2776	}
2777
2778	/**
2779	 * The event which runs before moving a record
2780	 *
2781	 * @param   boolean  $updateNulls  Should nulls be saved as nulls (true) or just skipped over (false)?
2782	 *
2783	 * @return  boolean  True to allow moving
2784	 */
2785	protected function onBeforeMove($updateNulls)
2786	{
2787		// Call the behaviors
2788		$result = $this->tableDispatcher->trigger('onBeforeMove', array(&$this, $updateNulls));
2789
2790		if (in_array(false, $result, true))
2791		{
2792			// Behavior failed, return false
2793			return false;
2794		}
2795
2796		if ($this->_trigger_events)
2797		{
2798			$name = FOFInflector::pluralize($this->getKeyName());
2799
2800			$result     = FOFPlatform::getInstance()->runPlugins('onBeforeMove' . ucfirst($name), array(&$this, $updateNulls));
2801
2802			if (in_array(false, $result, true))
2803			{
2804				return false;
2805			}
2806			else
2807			{
2808				return true;
2809			}
2810		}
2811
2812		return true;
2813	}
2814
2815	/**
2816	 * The event which runs after moving a record
2817	 *
2818	 * @return  boolean  True to allow moving without an error
2819	 */
2820	protected function onAfterMove()
2821	{
2822		// Call the behaviors
2823		$result = $this->tableDispatcher->trigger('onAfterMove', array(&$this));
2824
2825		if (in_array(false, $result, true))
2826		{
2827			// Behavior failed, return false
2828			return false;
2829		}
2830
2831		if ($this->_trigger_events)
2832		{
2833			$name = FOFInflector::pluralize($this->getKeyName());
2834
2835			$result     = FOFPlatform::getInstance()->runPlugins('onAfterMove' . ucfirst($name), array(&$this));
2836
2837			if (in_array(false, $result, true))
2838			{
2839				return false;
2840			}
2841			else
2842			{
2843				return true;
2844			}
2845		}
2846
2847		return true;
2848	}
2849
2850	/**
2851	 * The event which runs before reordering a table
2852	 *
2853	 * @param   string  $where  The WHERE clause of the SQL query to run on reordering (record filter)
2854	 *
2855	 * @return  boolean  True to allow reordering
2856	 */
2857	protected function onBeforeReorder($where = '')
2858	{
2859		// Call the behaviors
2860		$result = $this->tableDispatcher->trigger('onBeforeReorder', array(&$this, $where));
2861
2862		if (in_array(false, $result, true))
2863		{
2864			// Behavior failed, return false
2865			return false;
2866		}
2867
2868		if ($this->_trigger_events)
2869		{
2870			$name = FOFInflector::pluralize($this->getKeyName());
2871
2872			$result     = FOFPlatform::getInstance()->runPlugins('onBeforeReorder' . ucfirst($name), array(&$this, $where));
2873
2874			if (in_array(false, $result, true))
2875			{
2876				return false;
2877			}
2878			else
2879			{
2880				return true;
2881			}
2882		}
2883
2884		return true;
2885	}
2886
2887	/**
2888	 * The event which runs after reordering a table
2889	 *
2890	 * @return  boolean  True to allow the reordering to complete without an error
2891	 */
2892	protected function onAfterReorder()
2893	{
2894		// Call the behaviors
2895		$result = $this->tableDispatcher->trigger('onAfterReorder', array(&$this));
2896
2897		if (in_array(false, $result, true))
2898		{
2899			// Behavior failed, return false
2900			return false;
2901		}
2902
2903		if ($this->_trigger_events)
2904		{
2905			$name = FOFInflector::pluralize($this->getKeyName());
2906
2907			$result     = FOFPlatform::getInstance()->runPlugins('onAfterReorder' . ucfirst($name), array(&$this));
2908
2909			if (in_array(false, $result, true))
2910			{
2911				return false;
2912			}
2913			else
2914			{
2915				return true;
2916			}
2917		}
2918
2919		return true;
2920	}
2921
2922	/**
2923	 * The event which runs before deleting a record
2924	 *
2925	 * @param   integer  $oid  The PK value of the record to delete
2926	 *
2927	 * @return  boolean  True to allow the deletion
2928	 */
2929	protected function onBeforeDelete($oid)
2930	{
2931		// Call the behaviors
2932		$result = $this->tableDispatcher->trigger('onBeforeDelete', array(&$this, $oid));
2933
2934		if (in_array(false, $result, true))
2935		{
2936			// Behavior failed, return false
2937			return false;
2938		}
2939
2940		if ($this->_trigger_events)
2941		{
2942			$name = FOFInflector::pluralize($this->getKeyName());
2943
2944			$result     = FOFPlatform::getInstance()->runPlugins('onBeforeDelete' . ucfirst($name), array(&$this, $oid));
2945
2946			if (in_array(false, $result, true))
2947			{
2948				return false;
2949			}
2950			else
2951			{
2952				return true;
2953			}
2954		}
2955
2956		return true;
2957	}
2958
2959	/**
2960	 * The event which runs after deleting a record
2961	 *
2962	 * @param   integer  $oid  The PK value of the record which was deleted
2963	 *
2964	 * @return  boolean  True to allow the deletion without errors
2965	 */
2966	protected function onAfterDelete($oid)
2967	{
2968		// Call the behaviors
2969		$result = $this->tableDispatcher->trigger('onAfterDelete', array(&$this, $oid));
2970
2971		if (in_array(false, $result, true))
2972		{
2973			// Behavior failed, return false
2974			return false;
2975		}
2976
2977		if ($this->_trigger_events)
2978		{
2979			$name = FOFInflector::pluralize($this->getKeyName());
2980
2981			$result     = FOFPlatform::getInstance()->runPlugins('onAfterDelete' . ucfirst($name), array(&$this, $oid));
2982
2983			if (in_array(false, $result, true))
2984			{
2985				return false;
2986			}
2987			else
2988			{
2989				return true;
2990			}
2991		}
2992
2993		return true;
2994	}
2995
2996	/**
2997	 * The event which runs before hitting a record
2998	 *
2999	 * @param   integer  $oid  The PK value of the record to hit
3000	 * @param   boolean  $log  Should we log the hit?
3001	 *
3002	 * @return  boolean  True to allow the hit
3003	 */
3004	protected function onBeforeHit($oid, $log)
3005	{
3006		// Call the behaviors
3007		$result = $this->tableDispatcher->trigger('onBeforeHit', array(&$this, $oid, $log));
3008
3009		if (in_array(false, $result, true))
3010		{
3011			// Behavior failed, return false
3012			return false;
3013		}
3014
3015		if ($this->_trigger_events)
3016		{
3017			$name = FOFInflector::pluralize($this->getKeyName());
3018
3019			$result     = FOFPlatform::getInstance()->runPlugins('onBeforeHit' . ucfirst($name), array(&$this, $oid, $log));
3020
3021			if (in_array(false, $result, true))
3022			{
3023				return false;
3024			}
3025			else
3026			{
3027				return true;
3028			}
3029		}
3030
3031		return true;
3032	}
3033
3034	/**
3035	 * The event which runs after hitting a record
3036	 *
3037	 * @param   integer  $oid  The PK value of the record which was hit
3038	 *
3039	 * @return  boolean  True to allow the hitting without errors
3040	 */
3041	protected function onAfterHit($oid)
3042	{
3043		// Call the behaviors
3044		$result = $this->tableDispatcher->trigger('onAfterHit', array(&$this, $oid));
3045
3046		if (in_array(false, $result, true))
3047		{
3048			// Behavior failed, return false
3049			return false;
3050		}
3051
3052		if ($this->_trigger_events)
3053		{
3054			$name = FOFInflector::pluralize($this->getKeyName());
3055
3056			$result     = FOFPlatform::getInstance()->runPlugins('onAfterHit' . ucfirst($name), array(&$this, $oid));
3057
3058			if (in_array(false, $result, true))
3059			{
3060				return false;
3061			}
3062			else
3063			{
3064				return true;
3065			}
3066		}
3067
3068		return true;
3069	}
3070
3071	/**
3072	 * The even which runs before copying a record
3073	 *
3074	 * @param   integer  $oid  The PK value of the record being copied
3075	 *
3076	 * @return  boolean  True to allow the copy to take place
3077	 */
3078	protected function onBeforeCopy($oid)
3079	{
3080		// Call the behaviors
3081		$result = $this->tableDispatcher->trigger('onBeforeCopy', array(&$this, $oid));
3082
3083		if (in_array(false, $result, true))
3084		{
3085			// Behavior failed, return false
3086			return false;
3087		}
3088
3089		if ($this->_trigger_events)
3090		{
3091			$name = FOFInflector::pluralize($this->getKeyName());
3092
3093			$result     = FOFPlatform::getInstance()->runPlugins('onBeforeCopy' . ucfirst($name), array(&$this, $oid));
3094
3095			if (in_array(false, $result, true))
3096			{
3097				return false;
3098			}
3099			else
3100			{
3101				return true;
3102			}
3103		}
3104
3105		return true;
3106	}
3107
3108	/**
3109	 * The even which runs after copying a record
3110	 *
3111	 * @param   integer  $oid  The PK value of the record which was copied (not the new one)
3112	 *
3113	 * @return  boolean  True to allow the copy without errors
3114	 */
3115	protected function onAfterCopy($oid)
3116	{
3117		// Call the behaviors
3118		$result = $this->tableDispatcher->trigger('onAfterCopy', array(&$this, $oid));
3119
3120		if (in_array(false, $result, true))
3121		{
3122			// Behavior failed, return false
3123			return false;
3124		}
3125
3126		if ($this->_trigger_events)
3127		{
3128			$name = FOFInflector::pluralize($this->getKeyName());
3129
3130			$result     = FOFPlatform::getInstance()->runPlugins('onAfterCopy' . ucfirst($name), array(&$this, $oid));
3131
3132			if (in_array(false, $result, true))
3133			{
3134				return false;
3135			}
3136			else
3137			{
3138				return true;
3139			}
3140		}
3141
3142		return true;
3143	}
3144
3145	/**
3146	 * The event which runs before a record is (un)published
3147	 *
3148	 * @param   integer|array  &$cid     The PK IDs of the records being (un)published
3149	 * @param   integer        $publish  1 to publish, 0 to unpublish
3150	 *
3151	 * @return  boolean  True to allow the (un)publish to proceed
3152	 */
3153	protected function onBeforePublish(&$cid, $publish)
3154	{
3155		// Call the behaviors
3156		$result = $this->tableDispatcher->trigger('onBeforePublish', array(&$this, &$cid, $publish));
3157
3158		if (in_array(false, $result, true))
3159		{
3160			// Behavior failed, return false
3161			return false;
3162		}
3163
3164		if ($this->_trigger_events)
3165		{
3166			$name = FOFInflector::pluralize($this->getKeyName());
3167
3168			$result     = FOFPlatform::getInstance()->runPlugins('onBeforePublish' . ucfirst($name), array(&$this, &$cid, $publish));
3169
3170			if (in_array(false, $result, true))
3171			{
3172				return false;
3173			}
3174			else
3175			{
3176				return true;
3177			}
3178		}
3179
3180		return true;
3181	}
3182
3183	/**
3184	 * The event which runs after the object is reset to its default values.
3185	 *
3186	 * @return  boolean  True to allow the reset to complete without errors
3187	 */
3188	protected function onAfterReset()
3189	{
3190		// Call the behaviors
3191		$result = $this->tableDispatcher->trigger('onAfterReset', array(&$this));
3192
3193		if (in_array(false, $result, true))
3194		{
3195			// Behavior failed, return false
3196			return false;
3197		}
3198
3199		if ($this->_trigger_events)
3200		{
3201			$name = FOFInflector::pluralize($this->getKeyName());
3202
3203			$result     = FOFPlatform::getInstance()->runPlugins('onAfterReset' . ucfirst($name), array(&$this));
3204
3205			if (in_array(false, $result, true))
3206			{
3207				return false;
3208			}
3209			else
3210			{
3211				return true;
3212			}
3213		}
3214
3215		return true;
3216	}
3217
3218	/**
3219	 * The even which runs before the object is reset to its default values.
3220	 *
3221	 * @return  boolean  True to allow the reset to complete
3222	 */
3223	protected function onBeforeReset()
3224	{
3225		// Call the behaviors
3226		$result = $this->tableDispatcher->trigger('onBeforeReset', array(&$this));
3227
3228		if (in_array(false, $result, true))
3229		{
3230			// Behavior failed, return false
3231			return false;
3232		}
3233
3234		if ($this->_trigger_events)
3235		{
3236			$name = FOFInflector::pluralize($this->getKeyName());
3237
3238			$result     = FOFPlatform::getInstance()->runPlugins('onBeforeReset' . ucfirst($name), array(&$this));
3239
3240			if (in_array(false, $result, true))
3241			{
3242				return false;
3243			}
3244			else
3245			{
3246				return true;
3247			}
3248		}
3249
3250		return true;
3251	}
3252
3253	/**
3254	 * Replace the input object of this table with the provided FOFInput object
3255	 *
3256	 * @param   FOFInput  $input  The new input object
3257	 *
3258	 * @return  void
3259	 */
3260	public function setInput(FOFInput $input)
3261	{
3262		$this->input = $input;
3263	}
3264
3265	/**
3266	 * Get the columns from database table.
3267	 *
3268	 * @return  mixed  An array of the field names, or false if an error occurs.
3269	 *
3270	 * @deprecated  2.1
3271	 */
3272	public function getFields()
3273	{
3274		return $this->getTableFields();
3275	}
3276
3277	/**
3278	 * Add a filesystem path where FOFTable should search for table class files.
3279	 * You may either pass a string or an array of paths.
3280	 *
3281	 * @param   mixed  $path  A filesystem path or array of filesystem paths to add.
3282	 *
3283	 * @return  array  An array of filesystem paths to find FOFTable classes in.
3284	 */
3285	public static function addIncludePath($path = null)
3286	{
3287		// If the internal paths have not been initialised, do so with the base table path.
3288		if (empty(self::$_includePaths))
3289		{
3290			self::$_includePaths = array(__DIR__);
3291		}
3292
3293		// Convert the passed path(s) to add to an array.
3294		settype($path, 'array');
3295
3296		// If we have new paths to add, do so.
3297		if (!empty($path) && !in_array($path, self::$_includePaths))
3298		{
3299			// Check and add each individual new path.
3300			foreach ($path as $dir)
3301			{
3302				// Sanitize path.
3303				$dir = trim($dir);
3304
3305				// Add to the front of the list so that custom paths are searched first.
3306				array_unshift(self::$_includePaths, $dir);
3307			}
3308		}
3309
3310		return self::$_includePaths;
3311	}
3312
3313	/**
3314	 * Loads the asset table related to this table.
3315	 * This will help tests, too, since we can mock this function.
3316	 *
3317	 * @return bool|JTableAsset     False on failure, otherwise JTableAsset
3318	 */
3319	protected function getAsset()
3320	{
3321		$name     = $this->_getAssetName();
3322
3323		// Do NOT touch JTable here -- we are loading the core asset table which is a JTable, not a FOFTable
3324		$asset    = JTable::getInstance('Asset');
3325
3326		if (!$asset->loadByName($name))
3327		{
3328			return false;
3329		}
3330
3331		return $asset;
3332	}
3333
3334    /**
3335     * Method to compute the default name of the asset.
3336     * The default name is in the form table_name.id
3337     * where id is the value of the primary key of the table.
3338     *
3339     * @throws  UnexpectedValueException
3340     *
3341     * @return  string
3342     */
3343	public function getAssetName()
3344	{
3345		$k = $this->_tbl_key;
3346
3347        // If there is no assetKey defined, stop here, or we'll get a wrong name
3348        if(!$this->_assetKey || !$this->$k)
3349        {
3350            throw new UnexpectedValueException('Table must have an asset key defined and a value for the table id in order to track assets');
3351        }
3352
3353		return $this->_assetKey . '.' . (int) $this->$k;
3354	}
3355
3356	/**
3357     * Method to compute the default name of the asset.
3358     * The default name is in the form table_name.id
3359     * where id is the value of the primary key of the table.
3360     *
3361     * @throws  UnexpectedValueException
3362     *
3363     * @return  string
3364     */
3365	public function getAssetKey()
3366	{
3367		return $this->_assetKey;
3368	}
3369
3370	/**
3371	 * Method to return the title to use for the asset table.  In
3372	 * tracking the assets a title is kept for each asset so that there is some
3373	 * context available in a unified access manager.  Usually this would just
3374	 * return $this->title or $this->name or whatever is being used for the
3375	 * primary name of the row. If this method is not overridden, the asset name is used.
3376	 *
3377	 * @return  string  The string to use as the title in the asset table.
3378	 */
3379	public function getAssetTitle()
3380	{
3381		return $this->getAssetName();
3382	}
3383
3384	/**
3385	 * Method to get the parent asset under which to register this one.
3386	 * By default, all assets are registered to the ROOT node with ID,
3387	 * which will default to 1 if none exists.
3388	 * The extended class can define a table and id to lookup.  If the
3389	 * asset does not exist it will be created.
3390	 *
3391	 * @param   FOFTable  $table  A FOFTable object for the asset parent.
3392	 * @param   integer   $id     Id to look up
3393	 *
3394	 * @return  integer
3395	 */
3396	public function getAssetParentId($table = null, $id = null)
3397	{
3398		// For simple cases, parent to the asset root.
3399		$assets = JTable::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo()));
3400		$rootId = $assets->getRootId();
3401
3402		if (!empty($rootId))
3403		{
3404			return $rootId;
3405		}
3406
3407		return 1;
3408	}
3409
3410	/**
3411	 * This method sets the asset key for the items of this table. Obviously, it
3412	 * is only meant to be used when you have a table with an asset field.
3413	 *
3414	 * @param   string  $assetKey  The name of the asset key to use
3415	 *
3416	 * @return  void
3417	 */
3418	public function setAssetKey($assetKey)
3419	{
3420		$this->_assetKey = $assetKey;
3421	}
3422
3423	/**
3424	 * Method to get the database table name for the class.
3425	 *
3426	 * @return  string  The name of the database table being modeled.
3427	 */
3428	public function getTableName()
3429	{
3430		return $this->_tbl;
3431	}
3432
3433	/**
3434	 * Method to get the primary key field name for the table.
3435	 *
3436	 * @return  string  The name of the primary key for the table.
3437	 */
3438	public function getKeyName()
3439	{
3440		return $this->_tbl_key;
3441	}
3442
3443	/**
3444	 * Returns the identity value of this record
3445	 *
3446	 * @return mixed
3447	 */
3448	public function getId()
3449	{
3450		$key = $this->getKeyName();
3451
3452		return $this->$key;
3453	}
3454
3455	/**
3456	 * Method to get the FOFDatabaseDriver object.
3457	 *
3458	 * @return  FOFDatabaseDriver  The internal database driver object.
3459	 */
3460	public function getDbo()
3461	{
3462		return $this->_db;
3463	}
3464
3465	/**
3466	 * Method to set the FOFDatabaseDriver object.
3467	 *
3468	 * @param   FOFDatabaseDriver  $db  A FOFDatabaseDriver object to be used by the table object.
3469	 *
3470	 * @return  boolean  True on success.
3471	 */
3472	public function setDBO($db)
3473	{
3474		$this->_db = $db;
3475
3476		return true;
3477	}
3478
3479	/**
3480	 * Method to set rules for the record.
3481	 *
3482	 * @param   mixed  $input  A JAccessRules object, JSON string, or array.
3483	 *
3484	 * @return  void
3485	 */
3486	public function setRules($input)
3487	{
3488		if ($input instanceof JAccessRules)
3489		{
3490			$this->_rules = $input;
3491		}
3492		else
3493		{
3494			$this->_rules = new JAccessRules($input);
3495		}
3496	}
3497
3498	/**
3499	 * Method to get the rules for the record.
3500	 *
3501	 * @return  JAccessRules object
3502	 */
3503	public function getRules()
3504	{
3505		return $this->_rules;
3506	}
3507
3508	/**
3509	 * Method to check if the record is treated as an ACL asset
3510	 *
3511	 * @return  boolean [description]
3512	 */
3513	public function isAssetsTracked()
3514	{
3515		return $this->_trackAssets;
3516	}
3517
3518    /**
3519     * Method to manually set this record as ACL asset or not.
3520     * We have to do this since the automatic check is made in the constructor, but here we can't set any alias.
3521     * So, even if you have an alias for `asset_id`, it wouldn't be reconized and assets won't be tracked.
3522     *
3523     * @param $state
3524     */
3525    public function setAssetsTracked($state)
3526    {
3527        $state = (bool) $state;
3528
3529        if($state)
3530        {
3531            JLoader::import('joomla.access.rules');
3532        }
3533
3534        $this->_trackAssets = $state;
3535    }
3536
3537	/**
3538	 * Method to provide a shortcut to binding, checking and storing a FOFTable
3539	 * instance to the database table.  The method will check a row in once the
3540	 * data has been stored and if an ordering filter is present will attempt to
3541	 * reorder the table rows based on the filter.  The ordering filter is an instance
3542	 * property name.  The rows that will be reordered are those whose value matches
3543	 * the FOFTable instance for the property specified.
3544	 *
3545	 * @param   mixed   $src             An associative array or object to bind to the FOFTable instance.
3546	 * @param   string  $orderingFilter  Filter for the order updating
3547	 * @param   mixed   $ignore          An optional array or space separated list of properties
3548	 *                                   to ignore while binding.
3549	 *
3550	 * @return  boolean  True on success.
3551	 */
3552	public function save($src, $orderingFilter = '', $ignore = '')
3553	{
3554		// Attempt to bind the source to the instance.
3555		if (!$this->bind($src, $ignore))
3556		{
3557			return false;
3558		}
3559
3560		// Run any sanity checks on the instance and verify that it is ready for storage.
3561		if (!$this->check())
3562		{
3563			return false;
3564		}
3565
3566		// Attempt to store the properties to the database table.
3567		if (!$this->store())
3568		{
3569			return false;
3570		}
3571
3572		// Attempt to check the row in, just in case it was checked out.
3573		if (!$this->checkin())
3574		{
3575			return false;
3576		}
3577
3578		// If an ordering filter is set, attempt reorder the rows in the table based on the filter and value.
3579		if ($orderingFilter)
3580		{
3581			$filterValue = $this->$orderingFilter;
3582			$this->reorder($orderingFilter ? $this->_db->qn($orderingFilter) . ' = ' . $this->_db->q($filterValue) : '');
3583		}
3584
3585		// Set the error to empty and return true.
3586		$this->setError('');
3587
3588		return true;
3589	}
3590
3591	/**
3592	 * Method to get the next ordering value for a group of rows defined by an SQL WHERE clause.
3593	 * This is useful for placing a new item last in a group of items in the table.
3594	 *
3595	 * @param   string  $where  WHERE clause to use for selecting the MAX(ordering) for the table.
3596	 *
3597	 * @return  mixed  Boolean false an failure or the next ordering value as an integer.
3598	 */
3599	public function getNextOrder($where = '')
3600	{
3601		// If there is no ordering field set an error and return false.
3602		$ordering = $this->getColumnAlias('ordering');
3603		if (!in_array($ordering, $this->getKnownFields()))
3604		{
3605			throw new UnexpectedValueException(sprintf('%s does not support ordering.', get_class($this)));
3606		}
3607
3608		// Get the largest ordering value for a given where clause.
3609		$query = $this->_db->getQuery(true);
3610		$query->select('MAX('.$this->_db->qn($ordering).')');
3611		$query->from($this->_tbl);
3612
3613		if ($where)
3614		{
3615			$query->where($where);
3616		}
3617
3618		$this->_db->setQuery($query);
3619		$max = (int) $this->_db->loadResult();
3620
3621		// Return the largest ordering value + 1.
3622		return ($max + 1);
3623	}
3624
3625	/**
3626	 * Method to lock the database table for writing.
3627	 *
3628	 * @return  boolean  True on success.
3629	 *
3630	 * @throws  RuntimeException
3631	 */
3632	protected function _lock()
3633	{
3634		$this->_db->lockTable($this->_tbl);
3635		$this->_locked = true;
3636
3637		return true;
3638	}
3639
3640	/**
3641	 * Method to unlock the database table for writing.
3642	 *
3643	 * @return  boolean  True on success.
3644	 */
3645	protected function _unlock()
3646	{
3647		$this->_db->unlockTables();
3648		$this->_locked = false;
3649
3650		return true;
3651	}
3652
3653	public function setConfig(array $config)
3654	{
3655		$this->config = $config;
3656	}
3657
3658	/**
3659	 * Get the content type for ucm
3660	 *
3661	 * @return string The content type alias
3662	 */
3663	public function getContentType()
3664	{
3665		if ($this->contentType)
3666		{
3667			return $this->contentType;
3668		}
3669
3670		/**
3671		 * When tags was first introduced contentType variable didn't exist - so we guess one
3672		 * This will fail if content history behvaiour is enabled. This code is deprecated
3673		 * and will be removed in FOF 3.0 in favour of the content type class variable
3674		 */
3675		$component = $this->input->get('option');
3676
3677		$view = FOFInflector::singularize($this->input->get('view'));
3678		$alias = $component . '.' . $view;
3679
3680		return $alias;
3681	}
3682
3683	/**
3684	 * Returns the table relations object of the current table, lazy-loading it if necessary
3685	 *
3686	 * @return  FOFTableRelations
3687	 */
3688	public function getRelations()
3689	{
3690		if (is_null($this->_relations))
3691		{
3692			$this->_relations = new FOFTableRelations($this);
3693		}
3694
3695		return $this->_relations;
3696	}
3697
3698	/**
3699	 * Gets a reference to the configuration parameters provider for this table
3700	 *
3701	 * @return  FOFConfigProvider
3702	 */
3703	public function getConfigProvider()
3704	{
3705		return $this->configProvider;
3706	}
3707
3708	/**
3709	 * Returns the configuration parameters provider's key for this table
3710	 *
3711	 * @return  string
3712	 */
3713	public function getConfigProviderKey()
3714	{
3715		return $this->_configProviderKey;
3716	}
3717
3718	/**
3719	 * Check if a UCM content type exists for this resource, and
3720	 * create it if it does not
3721	 *
3722	 * @param  string  $alias  The content type alias (optional)
3723	 *
3724	 * @return  null
3725	 */
3726	public function checkContentType($alias = null)
3727	{
3728		$contentType = new JTableContenttype($this->getDbo());
3729
3730		if (!$alias)
3731		{
3732			$alias = $this->getContentType();
3733		}
3734
3735		$aliasParts = explode('.', $alias);
3736
3737		// Fetch the extension name
3738		$component = $aliasParts[0];
3739		$component = JComponentHelper::getComponent($component);
3740
3741		// Fetch the name using the menu item
3742		$query = $this->getDbo()->getQuery(true);
3743		$query->select('title')->from('#__menu')->where('component_id = ' . (int) $component->id);
3744		$this->getDbo()->setQuery($query);
3745		$component_name = JText::_($this->getDbo()->loadResult());
3746
3747		$name = $component_name . ' ' . ucfirst($aliasParts[1]);
3748
3749		// Create a new content type for our resource
3750		if (!$contentType->load(array('type_alias' => $alias)))
3751		{
3752			$contentType->type_title = $name;
3753			$contentType->type_alias = $alias;
3754			$contentType->table = json_encode(
3755				array(
3756					'special' => array(
3757						'dbtable' => $this->getTableName(),
3758						'key'     => $this->getKeyName(),
3759						'type'    => $name,
3760						'prefix'  => $this->_tablePrefix,
3761						'class'   => 'FOFTable',
3762						'config'  => 'array()'
3763					),
3764					'common' => array(
3765						'dbtable' => '#__ucm_content',
3766						'key' => 'ucm_id',
3767						'type' => 'CoreContent',
3768						'prefix' => 'JTable',
3769						'config' => 'array()'
3770					)
3771				)
3772			);
3773
3774			$contentType->field_mappings = json_encode(
3775				array(
3776					'common' => array(
3777						0 => array(
3778							"core_content_item_id" => $this->getKeyName(),
3779							"core_title"           => $this->getUcmCoreAlias('title'),
3780							"core_state"           => $this->getUcmCoreAlias('enabled'),
3781							"core_alias"           => $this->getUcmCoreAlias('alias'),
3782							"core_created_time"    => $this->getUcmCoreAlias('created_on'),
3783							"core_modified_time"   => $this->getUcmCoreAlias('created_by'),
3784							"core_body"            => $this->getUcmCoreAlias('body'),
3785							"core_hits"            => $this->getUcmCoreAlias('hits'),
3786							"core_publish_up"      => $this->getUcmCoreAlias('publish_up'),
3787							"core_publish_down"    => $this->getUcmCoreAlias('publish_down'),
3788							"core_access"          => $this->getUcmCoreAlias('access'),
3789							"core_params"          => $this->getUcmCoreAlias('params'),
3790							"core_featured"        => $this->getUcmCoreAlias('featured'),
3791							"core_metadata"        => $this->getUcmCoreAlias('metadata'),
3792							"core_language"        => $this->getUcmCoreAlias('language'),
3793							"core_images"          => $this->getUcmCoreAlias('images'),
3794							"core_urls"            => $this->getUcmCoreAlias('urls'),
3795							"core_version"         => $this->getUcmCoreAlias('version'),
3796							"core_ordering"        => $this->getUcmCoreAlias('ordering'),
3797							"core_metakey"         => $this->getUcmCoreAlias('metakey'),
3798							"core_metadesc"        => $this->getUcmCoreAlias('metadesc'),
3799							"core_catid"           => $this->getUcmCoreAlias('cat_id'),
3800							"core_xreference"      => $this->getUcmCoreAlias('xreference'),
3801							"asset_id"             => $this->getUcmCoreAlias('asset_id')
3802						)
3803					),
3804					'special' => array(
3805						0 => array(
3806						)
3807					)
3808				)
3809			);
3810
3811			$ignoreFields = array(
3812				$this->getUcmCoreAlias('modified_on', null),
3813				$this->getUcmCoreAlias('modified_by', null),
3814				$this->getUcmCoreAlias('locked_by', null),
3815				$this->getUcmCoreAlias('locked_on', null),
3816				$this->getUcmCoreAlias('hits', null),
3817				$this->getUcmCoreAlias('version', null)
3818			);
3819
3820			$contentType->content_history_options = json_encode(
3821				array(
3822					"ignoreChanges" => array_filter($ignoreFields, 'strlen')
3823				)
3824			);
3825
3826			$contentType->router = '';
3827
3828			$contentType->store();
3829		}
3830	}
3831
3832	/**
3833	 * Utility methods that fetches the column name for the field.
3834	 * If it does not exists, returns a "null" string
3835	 *
3836	 * @param  string  $alias  The alias for the column
3837	 * @param  string  $null   What to return if no column exists
3838	 *
3839	 * @return string The column name
3840	 */
3841	protected function getUcmCoreAlias($alias, $null = "null")
3842	{
3843		$alias = $this->getColumnAlias($alias);
3844
3845		if (in_array($alias, $this->getKnownFields()))
3846		{
3847			return $alias;
3848		}
3849
3850		return $null;
3851	}
3852}
3853