1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage Table
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 jimport('joomla.filesystem.path');
13
14 /**
15 * Abstract Table class
16 *
17 * Parent class to all tables.
18 *
19 * @since 11.1
20 * @tutorial Joomla.Platform/jtable.cls
21 */
22 abstract class JTable extends JObject implements JObservableInterface, JTableInterface
23 {
24 /**
25 * Include paths for searching for JTable classes.
26 *
27 * @var array
28 * @since 12.1
29 */
30 private static $_includePaths = array();
31
32 /**
33 * Name of the database table to model.
34 *
35 * @var string
36 * @since 11.1
37 */
38 protected $_tbl = '';
39
40 /**
41 * Name of the primary key field in the table.
42 *
43 * @var string
44 * @since 11.1
45 */
46 protected $_tbl_key = '';
47
48 /**
49 * Name of the primary key fields in the table.
50 *
51 * @var array
52 * @since 12.2
53 */
54 protected $_tbl_keys = array();
55
56 /**
57 * JDatabaseDriver object.
58 *
59 * @var JDatabaseDriver
60 * @since 11.1
61 */
62 protected $_db;
63
64 /**
65 * Should rows be tracked as ACL assets?
66 *
67 * @var boolean
68 * @since 11.1
69 */
70 protected $_trackAssets = false;
71
72 /**
73 * The rules associated with this record.
74 *
75 * @var JAccessRules A JAccessRules object.
76 * @since 11.1
77 */
78 protected $_rules;
79
80 /**
81 * Indicator that the tables have been locked.
82 *
83 * @var boolean
84 * @since 11.1
85 */
86 protected $_locked = false;
87
88 /**
89 * Indicates that the primary keys autoincrement.
90 *
91 * @var boolean
92 * @since 12.3
93 */
94 protected $_autoincrement = true;
95
96 /**
97 * Generic observers for this JTable (Used e.g. for tags Processing)
98 *
99 * @var JObserverUpdater
100 * @since 3.1.2
101 */
102 protected $_observers;
103
104 /**
105 * Array with alias for "special" columns such as ordering, hits etc etc
106 *
107 * @var array
108 * @since 3.4.0
109 */
110 protected $_columnAlias = array();
111
112 /**
113 * An array of key names to be json encoded in the bind function
114 *
115 * @var array
116 * @since 3.3
117 */
118 protected $_jsonEncode = array();
119
120 /**
121 * Object constructor to set table and key fields. In most cases this will
122 * be overridden by child classes to explicitly set the table and key fields
123 * for a particular database table.
124 *
125 * @param string $table Name of the table to model.
126 * @param mixed $key Name of the primary key field in the table or array of field names that compose the primary key.
127 * @param JDatabaseDriver $db JDatabaseDriver object.
128 *
129 * @since 11.1
130 */
131 public function __construct($table, $key, $db)
132 {
133 // Set internal variables.
134 $this->_tbl = $table;
135
136 // Set the key to be an array.
137 if (is_string($key))
138 {
139 $key = array($key);
140 }
141 elseif (is_object($key))
142 {
143 $key = (array) $key;
144 }
145
146 $this->_tbl_keys = $key;
147
148 if (count($key) == 1)
149 {
150 $this->_autoincrement = true;
151 }
152 else
153 {
154 $this->_autoincrement = false;
155 }
156
157 // Set the singular table key for backwards compatibility.
158 $this->_tbl_key = $this->getKeyName();
159
160 $this->_db = $db;
161
162 // Initialise the table properties.
163 $fields = $this->getFields();
164
165 if ($fields)
166 {
167 foreach ($fields as $name => $v)
168 {
169 // Add the field if it is not already present.
170 if (!property_exists($this, $name))
171 {
172 $this->$name = null;
173 }
174 }
175 }
176
177 // If we are tracking assets, make sure an access field exists and initially set the default.
178 if (property_exists($this, 'asset_id'))
179 {
180 $this->_trackAssets = true;
181 }
182
183 // If the access property exists, set the default.
184 if (property_exists($this, 'access'))
185 {
186 $this->access = (int) JFactory::getConfig()->get('access');
187 }
188
189 // Implement JObservableInterface:
190 // Create observer updater and attaches all observers interested by $this class:
191 $this->_observers = new JObserverUpdater($this);
192 JObserverMapper::attachAllObservers($this);
193 }
194
195 /**
196 * Implement JObservableInterface:
197 * Adds an observer to this instance.
198 * This method will be called fron the constructor of classes implementing JObserverInterface
199 * which is instanciated by the constructor of $this with JObserverMapper::attachAllObservers($this)
200 *
201 * @param JObserverInterface|JTableObserver $observer The observer object
202 *
203 * @return void
204 *
205 * @since 3.1.2
206 */
207 public function attachObserver(JObserverInterface $observer)
208 {
209 $this->_observers->attachObserver($observer);
210 }
211
212 /**
213 * Gets the instance of the observer of class $observerClass
214 *
215 * @param string $observerClass The observer class-name to return the object of
216 *
217 * @return JTableObserver|null
218 *
219 * @since 3.1.2
220 */
221 public function getObserverOfClass($observerClass)
222 {
223 return $this->_observers->getObserverOfClass($observerClass);
224 }
225
226 /**
227 * Get the columns from database table.
228 *
229 * @param bool $reload flag to reload cache
230 *
231 * @return mixed An array of the field names, or false if an error occurs.
232 *
233 * @since 11.1
234 * @throws UnexpectedValueException
235 */
236 public function getFields($reload = false)
237 {
238 static $cache = null;
239
240 if ($cache === null || $reload)
241 {
242 // Lookup the fields for this table only once.
243 $name = $this->_tbl;
244 $fields = $this->_db->getTableColumns($name, false);
245
246 if (empty($fields))
247 {
248 throw new UnexpectedValueException(sprintf('No columns found for %s table', $name));
249 }
250
251 $cache = $fields;
252 }
253
254 return $cache;
255 }
256
257 /**
258 * Static method to get an instance of a JTable class if it can be found in the table include paths.
259 *
260 * To add include paths for searching for JTable classes see JTable::addIncludePath().
261 *
262 * @param string $type The type (name) of the JTable class to get an instance of.
263 * @param string $prefix An optional prefix for the table class name.
264 * @param array $config An optional array of configuration values for the JTable object.
265 *
266 * @return JTable|boolean A JTable object if found or boolean false on failure.
267 *
268 * @since 11.1
269 */
270 public static function getInstance($type, $prefix = 'JTable', $config = array())
271 {
272 // Sanitize and prepare the table class name.
273 $type = preg_replace('/[^A-Z0-9_\.-]/i', '', $type);
274 $tableClass = $prefix . ucfirst($type);
275
276 // Only try to load the class if it doesn't already exist.
277 if (!class_exists($tableClass))
278 {
279 // Search for the class file in the JTable include paths.
280 jimport('joomla.filesystem.path');
281
282 $paths = self::addIncludePath();
283 $pathIndex = 0;
284
285 while (!class_exists($tableClass) && $pathIndex < count($paths))
286 {
287 if ($tryThis = JPath::find($paths[$pathIndex++], strtolower($type) . '.php'))
288 {
289 // Import the class file.
290 include_once $tryThis;
291 }
292 }
293
294 if (!class_exists($tableClass))
295 {
296 /*
297 * If unable to find the class file in the JTable include paths. Return false.
298 * The warning JLIB_DATABASE_ERROR_NOT_SUPPORTED_FILE_NOT_FOUND has been removed in 3.6.3.
299 * In 4.0 an Exception (type to be determined) will be thrown.
300 * For more info see https://github.com/joomla/joomla-cms/issues/11570
301 */
302
303 return false;
304 }
305 }
306
307 // If a database object was passed in the configuration array use it, otherwise get the global one from JFactory.
308 $db = isset($config['dbo']) ? $config['dbo'] : JFactory::getDbo();
309
310 // Instantiate a new table class and return it.
311 return new $tableClass($db);
312 }
313
314 /**
315 * Add a filesystem path where JTable should search for table class files.
316 *
317 * @param array|string $path A filesystem path or array of filesystem paths to add.
318 *
319 * @return array An array of filesystem paths to find JTable classes in.
320 *
321 * @since 11.1
322 */
323 public static function addIncludePath($path = null)
324 {
325 // If the internal paths have not been initialised, do so with the base table path.
326 if (empty(self::$_includePaths))
327 {
328 self::$_includePaths = array(__DIR__);
329 }
330
331 // Convert the passed path(s) to add to an array.
332 settype($path, 'array');
333
334 // If we have new paths to add, do so.
335 if (!empty($path))
336 {
337 // Check and add each individual new path.
338 foreach ($path as $dir)
339 {
340 // Sanitize path.
341 $dir = trim($dir);
342
343 // Add to the front of the list so that custom paths are searched first.
344 if (!in_array($dir, self::$_includePaths))
345 {
346 array_unshift(self::$_includePaths, $dir);
347 }
348 }
349 }
350
351 return self::$_includePaths;
352 }
353
354 /**
355 * Method to compute the default name of the asset.
356 * The default name is in the form table_name.id
357 * where id is the value of the primary key of the table.
358 *
359 * @return string
360 *
361 * @since 11.1
362 */
363 protected function _getAssetName()
364 {
365 $keys = array();
366
367 foreach ($this->_tbl_keys as $k)
368 {
369 $keys[] = (int) $this->$k;
370 }
371
372 return $this->_tbl . '.' . implode('.', $keys);
373 }
374
375 /**
376 * Method to return the title to use for the asset table.
377 *
378 * In tracking the assets a title is kept for each asset so that there is some context available in a unified access manager.
379 * Usually this would just return $this->title or $this->name or whatever is being used for the primary name of the row.
380 * If this method is not overridden, the asset name is used.
381 *
382 * @return string The string to use as the title in the asset table.
383 *
384 * @since 11.1
385 */
386 protected function _getAssetTitle()
387 {
388 return $this->_getAssetName();
389 }
390
391 /**
392 * Method to get the parent asset under which to register this one.
393 *
394 * By default, all assets are registered to the ROOT node with ID, which will default to 1 if none exists.
395 * An extended class can define a table and ID to lookup. If the asset does not exist it will be created.
396 *
397 * @param JTable $table A JTable object for the asset parent.
398 * @param integer $id Id to look up
399 *
400 * @return integer
401 *
402 * @since 11.1
403 */
404 protected function _getAssetParentId(JTable $table = null, $id = null)
405 {
406 // For simple cases, parent to the asset root.
407 /** @var JTableAsset $assets */
408 $assets = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo()));
409 $rootId = $assets->getRootId();
410
411 if (!empty($rootId))
412 {
413 return $rootId;
414 }
415
416 return 1;
417 }
418
419 /**
420 * Method to append the primary keys for this table to a query.
421 *
422 * @param JDatabaseQuery $query A query object to append.
423 * @param mixed $pk Optional primary key parameter.
424 *
425 * @return void
426 *
427 * @since 12.3
428 */
429 public function appendPrimaryKeys($query, $pk = null)
430 {
431 if (is_null($pk))
432 {
433 foreach ($this->_tbl_keys as $k)
434 {
435 $query->where($this->_db->quoteName($k) . ' = ' . $this->_db->quote($this->$k));
436 }
437 }
438 else
439 {
440 if (is_string($pk))
441 {
442 $pk = array($this->_tbl_key => $pk);
443 }
444
445 $pk = (object) $pk;
446
447 foreach ($this->_tbl_keys as $k)
448 {
449 $query->where($this->_db->quoteName($k) . ' = ' . $this->_db->quote($pk->$k));
450 }
451 }
452 }
453
454 /**
455 * Method to get the database table name for the class.
456 *
457 * @return string The name of the database table being modeled.
458 *
459 * @since 11.1
460 */
461 public function getTableName()
462 {
463 return $this->_tbl;
464 }
465
466 /**
467 * Method to get the primary key field name for the table.
468 *
469 * @param boolean $multiple True to return all primary keys (as an array) or false to return just the first one (as a string).
470 *
471 * @return mixed Array of primary key field names or string containing the first primary key field.
472 *
473 * @since 11.1
474 */
475 public function getKeyName($multiple = false)
476 {
477 // Count the number of keys
478 if (count($this->_tbl_keys))
479 {
480 if ($multiple)
481 {
482 // If we want multiple keys, return the raw array.
483 return $this->_tbl_keys;
484 }
485 else
486 {
487 // If we want the standard method, just return the first key.
488 return $this->_tbl_keys[0];
489 }
490 }
491
492 return '';
493 }
494
495 /**
496 * Method to get the JDatabaseDriver object.
497 *
498 * @return JDatabaseDriver The internal database driver object.
499 *
500 * @since 11.1
501 */
502 public function getDbo()
503 {
504 return $this->_db;
505 }
506
507 /**
508 * Method to set the JDatabaseDriver object.
509 *
510 * @param JDatabaseDriver $db A JDatabaseDriver object to be used by the table object.
511 *
512 * @return boolean True on success.
513 *
514 * @since 11.1
515 */
516 public function setDbo($db)
517 {
518 $this->_db = $db;
519
520 return true;
521 }
522
523 /**
524 * Method to set rules for the record.
525 *
526 * @param mixed $input A JAccessRules object, JSON string, or array.
527 *
528 * @return void
529 *
530 * @since 11.1
531 */
532 public function setRules($input)
533 {
534 if ($input instanceof JAccessRules)
535 {
536 $this->_rules = $input;
537 }
538 else
539 {
540 $this->_rules = new JAccessRules($input);
541 }
542 }
543
544 /**
545 * Method to get the rules for the record.
546 *
547 * @return JAccessRules object
548 *
549 * @since 11.1
550 */
551 public function getRules()
552 {
553 return $this->_rules;
554 }
555
556 /**
557 * Method to reset class properties to the defaults set in the class
558 * definition. It will ignore the primary key as well as any private class
559 * properties (except $_errors).
560 *
561 * @return void
562 *
563 * @since 11.1
564 */
565 public function reset()
566 {
567 // Get the default values for the class from the table.
568 foreach ($this->getFields() as $k => $v)
569 {
570 // If the property is not the primary key or private, reset it.
571 if (!in_array($k, $this->_tbl_keys) && (strpos($k, '_') !== 0))
572 {
573 $this->$k = $v->Default;
574 }
575 }
576
577 // Reset table errors
578 $this->_errors = array();
579 }
580
581 /**
582 * Method to bind an associative array or object to the JTable instance.This
583 * method only binds properties that are publicly accessible and optionally
584 * takes an array of properties to ignore when binding.
585 *
586 * @param array|object $src An associative array or object to bind to the JTable instance.
587 * @param array|string $ignore An optional array or space separated list of properties to ignore while binding.
588 *
589 * @return boolean True on success.
590 *
591 * @since 11.1
592 * @throws InvalidArgumentException
593 */
594 public function bind($src, $ignore = array())
595 {
596 // JSON encode any fields required
597 if (!empty($this->_jsonEncode))
598 {
599 foreach ($this->_jsonEncode as $field)
600 {
601 if (isset($src[$field]) && is_array($src[$field]))
602 {
603 $src[$field] = json_encode($src[$field]);
604 }
605 }
606 }
607
608 // Check if the source value is an array or object
609 if (!is_object($src) && !is_array($src))
610 {
611 throw new InvalidArgumentException(
612 sprintf(
613 'Could not bind the data source in %1$s::bind(), the source must be an array or object but a "%2$s" was given.',
614 get_class($this),
615 gettype($src)
616 )
617 );
618 }
619
620 // If the source value is an object, get its accessible properties.
621 if (is_object($src))
622 {
623 $src = get_object_vars($src);
624 }
625
626 // If the ignore value is a string, explode it over spaces.
627 if (!is_array($ignore))
628 {
629 $ignore = explode(' ', $ignore);
630 }
631
632 // Bind the source value, excluding the ignored fields.
633 foreach ($this->getProperties() as $k => $v)
634 {
635 // Only process fields not in the ignore array.
636 if (!in_array($k, $ignore))
637 {
638 if (isset($src[$k]))
639 {
640 $this->$k = $src[$k];
641 }
642 }
643 }
644
645 return true;
646 }
647
648 /**
649 * Method to load a row from the database by primary key and bind the fields to the JTable instance properties.
650 *
651 * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match.
652 * If not set the instance property value is used.
653 * @param boolean $reset True to reset the default values before loading the new row.
654 *
655 * @return boolean True if successful. False if row not found.
656 *
657 * @since 11.1
658 * @throws InvalidArgumentException
659 * @throws RuntimeException
660 * @throws UnexpectedValueException
661 */
662 public function load($keys = null, $reset = true)
663 {
664 // Implement JObservableInterface: Pre-processing by observers
665 $this->_observers->update('onBeforeLoad', array($keys, $reset));
666
667 if (empty($keys))
668 {
669 $empty = true;
670 $keys = array();
671
672 // If empty, use the value of the current key
673 foreach ($this->_tbl_keys as $key)
674 {
675 $empty = $empty && empty($this->$key);
676 $keys[$key] = $this->$key;
677 }
678
679 // If empty primary key there's is no need to load anything
680 if ($empty)
681 {
682 return true;
683 }
684 }
685 elseif (!is_array($keys))
686 {
687 // Load by primary key.
688 $keyCount = count($this->_tbl_keys);
689
690 if ($keyCount)
691 {
692 if ($keyCount > 1)
693 {
694 throw new InvalidArgumentException('Table has multiple primary keys specified, only one primary key value provided.');
695 }
696
697 $keys = array($this->getKeyName() => $keys);
698 }
699 else
700 {
701 throw new RuntimeException('No table keys defined.');
702 }
703 }
704
705 if ($reset)
706 {
707 $this->reset();
708 }
709
710 // Initialise the query.
711 $query = $this->_db->getQuery(true)
712 ->select('*')
713 ->from($this->_tbl);
714 $fields = array_keys($this->getProperties());
715
716 foreach ($keys as $field => $value)
717 {
718 // Check that $field is in the table.
719 if (!in_array($field, $fields))
720 {
721 throw new UnexpectedValueException(sprintf('Missing field in database: %s   %s.', get_class($this), $field));
722 }
723 // Add the search tuple to the query.
724 $query->where($this->_db->quoteName($field) . ' = ' . $this->_db->quote($value));
725 }
726
727 $this->_db->setQuery($query);
728
729 $row = $this->_db->loadAssoc();
730
731 // Check that we have a result.
732 if (empty($row))
733 {
734 $result = false;
735 }
736 else
737 {
738 // Bind the object with the row and return.
739 $result = $this->bind($row);
740 }
741
742 // Implement JObservableInterface: Post-processing by observers
743 $this->_observers->update('onAfterLoad', array(&$result, $row));
744
745 return $result;
746 }
747
748 /**
749 * Method to perform sanity checks on the JTable instance properties to ensure they are safe to store in the database.
750 *
751 * Child classes should override this method to make sure the data they are storing in the database is safe and as expected before storage.
752 *
753 * @return boolean True if the instance is sane and able to be stored in the database.
754 *
755 * @since 11.1
756 */
757 public function check()
758 {
759 return true;
760 }
761
762 /**
763 * Method to store a row in the database from the JTable instance properties.
764 *
765 * If a primary key value is set the row with that primary key value will be updated with the instance property values.
766 * If no primary key value is set a new row will be inserted into the database with the properties from the JTable instance.
767 *
768 * @param boolean $updateNulls True to update fields even if they are null.
769 *
770 * @return boolean True on success.
771 *
772 * @since 11.1
773 */
774 public function store($updateNulls = false)
775 {
776 $result = true;
777
778 $k = $this->_tbl_keys;
779
780 // Implement JObservableInterface: Pre-processing by observers
781 $this->_observers->update('onBeforeStore', array($updateNulls, $k));
782
783 $currentAssetId = 0;
784
785 if (!empty($this->asset_id))
786 {
787 $currentAssetId = $this->asset_id;
788 }
789
790 // The asset id field is managed privately by this class.
791 if ($this->_trackAssets)
792 {
793 unset($this->asset_id);
794 }
795
796 // If a primary key exists update the object, otherwise insert it.
797 if ($this->hasPrimaryKey())
798 {
799 $this->_db->updateObject($this->_tbl, $this, $this->_tbl_keys, $updateNulls);
800 }
801 else
802 {
803 $this->_db->insertObject($this->_tbl, $this, $this->_tbl_keys[0]);
804 }
805
806 // If the table is not set to track assets return true.
807 if ($this->_trackAssets)
808 {
809 if ($this->_locked)
810 {
811 $this->_unlock();
812 }
813
814 /*
815 * Asset Tracking
816 */
817 $parentId = $this->_getAssetParentId();
818 $name = $this->_getAssetName();
819 $title = $this->_getAssetTitle();
820
821 /** @var JTableAsset $asset */
822 $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo()));
823 $asset->loadByName($name);
824
825 // Re-inject the asset id.
826 $this->asset_id = $asset->id;
827
828 // Check for an error.
829 $error = $asset->getError();
830
831 if ($error)
832 {
833 $this->setError($error);
834
835 return false;
836 }
837 else
838 {
839 // Specify how a new or moved node asset is inserted into the tree.
840 if (empty($this->asset_id) || $asset->parent_id != $parentId)
841 {
842 $asset->setLocation($parentId, 'last-child');
843 }
844
845 // Prepare the asset to be stored.
846 $asset->parent_id = $parentId;
847 $asset->name = $name;
848 $asset->title = $title;
849
850 if ($this->_rules instanceof JAccessRules)
851 {
852 $asset->rules = (string) $this->_rules;
853 }
854
855 if (!$asset->check() || !$asset->store($updateNulls))
856 {
857 $this->setError($asset->getError());
858
859 return false;
860 }
861 else
862 {
863 // Create an asset_id or heal one that is corrupted.
864 if (empty($this->asset_id) || ($currentAssetId != $this->asset_id && !empty($this->asset_id)))
865 {
866 // Update the asset_id field in this table.
867 $this->asset_id = (int) $asset->id;
868
869 $query = $this->_db->getQuery(true)
870 ->update($this->_db->quoteName($this->_tbl))
871 ->set('asset_id = ' . (int) $this->asset_id);
872 $this->appendPrimaryKeys($query);
873 $this->_db->setQuery($query)->execute();
874 }
875 }
876 }
877 }
878
879 // Implement JObservableInterface: Post-processing by observers
880 $this->_observers->update('onAfterStore', array(&$result));
881
882 return $result;
883 }
884
885 /**
886 * Method to provide a shortcut to binding, checking and storing a JTable instance to the database table.
887 *
888 * The method will check a row in once the data has been stored and if an ordering filter is present will attempt to reorder
889 * the table rows based on the filter. The ordering filter is an instance property name. The rows that will be reordered
890 * are those whose value matches the JTable instance for the property specified.
891 *
892 * @param array|object $src An associative array or object to bind to the JTable instance.
893 * @param string $orderingFilter Filter for the order updating
894 * @param array|string $ignore An optional array or space separated list of properties to ignore while binding.
895 *
896 * @return boolean True on success.
897 *
898 * @since 11.1
899 */
900 public function save($src, $orderingFilter = '', $ignore = '')
901 {
902 // Attempt to bind the source to the instance.
903 if (!$this->bind($src, $ignore))
904 {
905 return false;
906 }
907
908 // Run any sanity checks on the instance and verify that it is ready for storage.
909 if (!$this->check())
910 {
911 return false;
912 }
913
914 // Attempt to store the properties to the database table.
915 if (!$this->store())
916 {
917 return false;
918 }
919
920 // Attempt to check the row in, just in case it was checked out.
921 if (!$this->checkin())
922 {
923 return false;
924 }
925
926 // If an ordering filter is set, attempt reorder the rows in the table based on the filter and value.
927 if ($orderingFilter)
928 {
929 $filterValue = $this->$orderingFilter;
930 $this->reorder($orderingFilter ? $this->_db->quoteName($orderingFilter) . ' = ' . $this->_db->quote($filterValue) : '');
931 }
932
933 // Set the error to empty and return true.
934 $this->setError('');
935
936 return true;
937 }
938
939 /**
940 * Method to delete a row from the database table by primary key value.
941 *
942 * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used.
943 *
944 * @return boolean True on success.
945 *
946 * @since 11.1
947 * @throws UnexpectedValueException
948 */
949 public function delete($pk = null)
950 {
951 if (is_null($pk))
952 {
953 $pk = array();
954
955 foreach ($this->_tbl_keys as $key)
956 {
957 $pk[$key] = $this->$key;
958 }
959 }
960 elseif (!is_array($pk))
961 {
962 $pk = array($this->_tbl_key => $pk);
963 }
964
965 foreach ($this->_tbl_keys as $key)
966 {
967 $pk[$key] = is_null($pk[$key]) ? $this->$key : $pk[$key];
968
969 if ($pk[$key] === null)
970 {
971 throw new UnexpectedValueException('Null primary key not allowed.');
972 }
973
974 $this->$key = $pk[$key];
975 }
976
977 // Implement JObservableInterface: Pre-processing by observers
978 $this->_observers->update('onBeforeDelete', array($pk));
979
980 // If tracking assets, remove the asset first.
981 if ($this->_trackAssets)
982 {
983 // Get the asset name
984 $name = $this->_getAssetName();
985 /** @var JTableAsset $asset */
986 $asset = self::getInstance('Asset');
987
988 if ($asset->loadByName($name))
989 {
990 if (!$asset->delete())
991 {
992 $this->setError($asset->getError());
993
994 return false;
995 }
996 }
997 }
998
999 // Delete the row by primary key.
1000 $query = $this->_db->getQuery(true)
1001 ->delete($this->_tbl);
1002 $this->appendPrimaryKeys($query, $pk);
1003
1004 $this->_db->setQuery($query);
1005
1006 // Check for a database error.
1007 $this->_db->execute();
1008
1009 // Implement JObservableInterface: Post-processing by observers
1010 $this->_observers->update('onAfterDelete', array($pk));
1011
1012 return true;
1013 }
1014
1015 /**
1016 * Method to check a row out if the necessary properties/fields exist.
1017 *
1018 * To prevent race conditions while editing rows in a database, a row can be checked out if the fields 'checked_out' and 'checked_out_time'
1019 * are available. While a row is checked out, any attempt to store the row by a user other than the one who checked the row out should be
1020 * held until the row is checked in again.
1021 *
1022 * @param integer $userId The Id of the user checking out the row.
1023 * @param mixed $pk An optional primary key value to check out. If not set the instance property value is used.
1024 *
1025 * @return boolean True on success.
1026 *
1027 * @since 11.1
1028 * @throws UnexpectedValueException
1029 */
1030 public function checkOut($userId, $pk = null)
1031 {
1032 $checkedOutField = $this->getColumnAlias('checked_out');
1033 $checkedOutTimeField = $this->getColumnAlias('checked_out_time');
1034
1035 // If there is no checked_out or checked_out_time field, just return true.
1036 if (!property_exists($this, $checkedOutField) || !property_exists($this, $checkedOutTimeField))
1037 {
1038 return true;
1039 }
1040
1041 if (is_null($pk))
1042 {
1043 $pk = array();
1044
1045 foreach ($this->_tbl_keys as $key)
1046 {
1047 $pk[$key] = $this->$key;
1048 }
1049 }
1050 elseif (!is_array($pk))
1051 {
1052 $pk = array($this->_tbl_key => $pk);
1053 }
1054
1055 foreach ($this->_tbl_keys as $key)
1056 {
1057 $pk[$key] = is_null($pk[$key]) ? $this->$key : $pk[$key];
1058
1059 if ($pk[$key] === null)
1060 {
1061 throw new UnexpectedValueException('Null primary key not allowed.');
1062 }
1063 }
1064
1065 // Get the current time in the database format.
1066 $time = JFactory::getDate()->toSql();
1067
1068 // Check the row out by primary key.
1069 $query = $this->_db->getQuery(true)
1070 ->update($this->_tbl)
1071 ->set($this->_db->quoteName($checkedOutField) . ' = ' . (int) $userId)
1072 ->set($this->_db->quoteName($checkedOutTimeField) . ' = ' . $this->_db->quote($time));
1073 $this->appendPrimaryKeys($query, $pk);
1074 $this->_db->setQuery($query);
1075 $this->_db->execute();
1076
1077 // Set table values in the object.
1078 $this->$checkedOutField = (int) $userId;
1079 $this->$checkedOutTimeField = $time;
1080
1081 return true;
1082 }
1083
1084 /**
1085 * Method to check a row in if the necessary properties/fields exist.
1086 *
1087 * Checking a row in will allow other users the ability to edit the row.
1088 *
1089 * @param mixed $pk An optional primary key value to check out. If not set the instance property value is used.
1090 *
1091 * @return boolean True on success.
1092 *
1093 * @since 11.1
1094 * @throws UnexpectedValueException
1095 */
1096 public function checkIn($pk = null)
1097 {
1098 $checkedOutField = $this->getColumnAlias('checked_out');
1099 $checkedOutTimeField = $this->getColumnAlias('checked_out_time');
1100
1101 // If there is no checked_out or checked_out_time field, just return true.
1102 if (!property_exists($this, $checkedOutField) || !property_exists($this, $checkedOutTimeField))
1103 {
1104 return true;
1105 }
1106
1107 if (is_null($pk))
1108 {
1109 $pk = array();
1110
1111 foreach ($this->_tbl_keys as $key)
1112 {
1113 $pk[$this->$key] = $this->$key;
1114 }
1115 }
1116 elseif (!is_array($pk))
1117 {
1118 $pk = array($this->_tbl_key => $pk);
1119 }
1120
1121 foreach ($this->_tbl_keys as $key)
1122 {
1123 $pk[$key] = empty($pk[$key]) ? $this->$key : $pk[$key];
1124
1125 if ($pk[$key] === null)
1126 {
1127 throw new UnexpectedValueException('Null primary key not allowed.');
1128 }
1129 }
1130
1131 // Check the row in by primary key.
1132 $query = $this->_db->getQuery(true)
1133 ->update($this->_tbl)
1134 ->set($this->_db->quoteName($checkedOutField) . ' = 0')
1135 ->set($this->_db->quoteName($checkedOutTimeField) . ' = ' . $this->_db->quote($this->_db->getNullDate()));
1136 $this->appendPrimaryKeys($query, $pk);
1137 $this->_db->setQuery($query);
1138
1139 // Check for a database error.
1140 $this->_db->execute();
1141
1142 // Set table values in the object.
1143 $this->$checkedOutField = 0;
1144 $this->$checkedOutTimeField = '';
1145
1146 return true;
1147 }
1148
1149 /**
1150 * Validate that the primary key has been set.
1151 *
1152 * @return boolean True if the primary key(s) have been set.
1153 *
1154 * @since 12.3
1155 */
1156 public function hasPrimaryKey()
1157 {
1158 if ($this->_autoincrement)
1159 {
1160 $empty = true;
1161
1162 foreach ($this->_tbl_keys as $key)
1163 {
1164 $empty = $empty && empty($this->$key);
1165 }
1166 }
1167 else
1168 {
1169 $query = $this->_db->getQuery(true)
1170 ->select('COUNT(*)')
1171 ->from($this->_tbl);
1172 $this->appendPrimaryKeys($query);
1173
1174 $this->_db->setQuery($query);
1175 $count = $this->_db->loadResult();
1176
1177 if ($count == 1)
1178 {
1179 $empty = false;
1180 }
1181 else
1182 {
1183 $empty = true;
1184 }
1185 }
1186
1187 return !$empty;
1188 }
1189
1190 /**
1191 * Method to increment the hits for a row if the necessary property/field exists.
1192 *
1193 * @param mixed $pk An optional primary key value to increment. If not set the instance property value is used.
1194 *
1195 * @return boolean True on success.
1196 *
1197 * @since 11.1
1198 * @throws UnexpectedValueException
1199 */
1200 public function hit($pk = null)
1201 {
1202 $hitsField = $this->getColumnAlias('hits');
1203
1204 // If there is no hits field, just return true.
1205 if (!property_exists($this, $hitsField))
1206 {
1207 return true;
1208 }
1209
1210 if (is_null($pk))
1211 {
1212 $pk = array();
1213
1214 foreach ($this->_tbl_keys as $key)
1215 {
1216 $pk[$key] = $this->$key;
1217 }
1218 }
1219 elseif (!is_array($pk))
1220 {
1221 $pk = array($this->_tbl_key => $pk);
1222 }
1223
1224 foreach ($this->_tbl_keys as $key)
1225 {
1226 $pk[$key] = is_null($pk[$key]) ? $this->$key : $pk[$key];
1227
1228 if ($pk[$key] === null)
1229 {
1230 throw new UnexpectedValueException('Null primary key not allowed.');
1231 }
1232 }
1233
1234 // Check the row in by primary key.
1235 $query = $this->_db->getQuery(true)
1236 ->update($this->_tbl)
1237 ->set($this->_db->quoteName($hitsField) . ' = (' . $this->_db->quoteName($hitsField) . ' + 1)');
1238 $this->appendPrimaryKeys($query, $pk);
1239 $this->_db->setQuery($query);
1240 $this->_db->execute();
1241
1242 // Set table values in the object.
1243 $this->hits++;
1244
1245 return true;
1246 }
1247
1248 /**
1249 * Method to determine if a row is checked out and therefore uneditable by a user.
1250 *
1251 * If the row is checked out by the same user, then it is considered not checked out -- as the user can still edit it.
1252 *
1253 * @param integer $with The user ID to preform the match with, if an item is checked out by this user the function will return false.
1254 * @param integer $against The user ID to perform the match against when the function is used as a static function.
1255 *
1256 * @return boolean True if checked out.
1257 *
1258 * @since 11.1
1259 */
1260 public function isCheckedOut($with = 0, $against = null)
1261 {
1262 // Handle the non-static case.
1263 if (isset($this) && ($this instanceof JTable) && is_null($against))
1264 {
1265 $checkedOutField = $this->getColumnAlias('checked_out');
1266 $against = $this->get($checkedOutField);
1267 }
1268
1269 // The item is not checked out or is checked out by the same user.
1270 if (!$against || ($against == $with))
1271 {
1272 return false;
1273 }
1274
1275 $db = JFactory::getDbo();
1276 $query = $db->getQuery(true)
1277 ->select('COUNT(userid)')
1278 ->from($db->quoteName('#__session'))
1279 ->where($db->quoteName('userid') . ' = ' . (int) $against);
1280 $db->setQuery($query);
1281 $checkedOut = (boolean) $db->loadResult();
1282
1283 // If a session exists for the user then it is checked out.
1284 return $checkedOut;
1285 }
1286
1287 /**
1288 * Method to get the next ordering value for a group of rows defined by an SQL WHERE clause.
1289 *
1290 * This is useful for placing a new item last in a group of items in the table.
1291 *
1292 * @param string $where WHERE clause to use for selecting the MAX(ordering) for the table.
1293 *
1294 * @return integer The next ordering value.
1295 *
1296 * @since 11.1
1297 * @throws UnexpectedValueException
1298 */
1299 public function getNextOrder($where = '')
1300 {
1301 // Check if there is an ordering field set
1302 $orderingField = $this->getColumnAlias('ordering');
1303
1304 if (!property_exists($this, $orderingField))
1305 {
1306 throw new UnexpectedValueException(sprintf('%s does not support ordering.', get_class($this)));
1307 }
1308
1309 // Get the largest ordering value for a given where clause.
1310 $query = $this->_db->getQuery(true)
1311 ->select('MAX(' . $this->_db->quoteName($orderingField) . ')')
1312 ->from($this->_tbl);
1313
1314 if ($where)
1315 {
1316 $query->where($where);
1317 }
1318
1319 $this->_db->setQuery($query);
1320 $max = (int) $this->_db->loadResult();
1321
1322 // Return the largest ordering value + 1.
1323 return $max + 1;
1324 }
1325
1326 /**
1327 * Get the primary key values for this table using passed in values as a default.
1328 *
1329 * @param array $keys Optional primary key values to use.
1330 *
1331 * @return array An array of primary key names and values.
1332 *
1333 * @since 12.3
1334 */
1335 public function getPrimaryKey(array $keys = array())
1336 {
1337 foreach ($this->_tbl_keys as $key)
1338 {
1339 if (!isset($keys[$key]))
1340 {
1341 if (!empty($this->$key))
1342 {
1343 $keys[$key] = $this->$key;
1344 }
1345 }
1346 }
1347
1348 return $keys;
1349 }
1350
1351 /**
1352 * Method to compact the ordering values of rows in a group of rows defined by an SQL WHERE clause.
1353 *
1354 * @param string $where WHERE clause to use for limiting the selection of rows to compact the ordering values.
1355 *
1356 * @return mixed Boolean True on success.
1357 *
1358 * @since 11.1
1359 * @throws UnexpectedValueException
1360 */
1361 public function reorder($where = '')
1362 {
1363 // Check if there is an ordering field set
1364 $orderingField = $this->getColumnAlias('ordering');
1365
1366 if (!property_exists($this, $orderingField))
1367 {
1368 throw new UnexpectedValueException(sprintf('%s does not support ordering.', get_class($this)));
1369 }
1370
1371 $quotedOrderingField = $this->_db->quoteName($orderingField);
1372
1373 $subquery = $this->_db->getQuery(true)
1374 ->from($this->_tbl)
1375 ->selectRowNumber($quotedOrderingField, 'new_ordering');
1376
1377 $query = $this->_db->getQuery(true)
1378 ->update($this->_tbl)
1379 ->set($quotedOrderingField . ' = sq.new_ordering');
1380
1381 $innerOn = array();
1382
1383 // Get the primary keys for the selection.
1384 foreach ($this->_tbl_keys as $i => $k)
1385 {
1386 $subquery->select($this->_db->quoteName($k, 'pk__' . $i));
1387 $innerOn[] = $this->_db->quoteName($k) . ' = sq.' . $this->_db->quoteName('pk__' . $i);
1388 }
1389
1390 // Setup the extra where and ordering clause data.
1391 if ($where)
1392 {
1393 $subquery->where($where);
1394 $query->where($where);
1395 }
1396
1397 $subquery->where($quotedOrderingField . ' >= 0');
1398 $query->where($quotedOrderingField . ' >= 0');
1399
1400 $query->innerJoin('(' . (string) $subquery . ') AS sq ON ' . implode(' AND ', $innerOn));
1401
1402 $this->_db->setQuery($query);
1403 $this->_db->execute();
1404
1405 return true;
1406 }
1407
1408 /**
1409 * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
1410 *
1411 * Negative numbers move the row up in the sequence and positive numbers move it down.
1412 *
1413 * @param integer $delta The direction and magnitude to move the row in the ordering sequence.
1414 * @param string $where WHERE clause to use for limiting the selection of rows to compact the ordering values.
1415 *
1416 * @return boolean True on success.
1417 *
1418 * @since 11.1
1419 * @throws UnexpectedValueException
1420 */
1421 public function move($delta, $where = '')
1422 {
1423 // Check if there is an ordering field set
1424 $orderingField = $this->getColumnAlias('ordering');
1425
1426 if (!property_exists($this, $orderingField))
1427 {
1428 throw new UnexpectedValueException(sprintf('%s does not support ordering.', get_class($this)));
1429 }
1430
1431 $quotedOrderingField = $this->_db->quoteName($orderingField);
1432
1433 // If the change is none, do nothing.
1434 if (empty($delta))
1435 {
1436 return true;
1437 }
1438
1439 $row = null;
1440 $query = $this->_db->getQuery(true);
1441
1442 // Select the primary key and ordering values from the table.
1443 $query->select(implode(',', $this->_tbl_keys) . ', ' . $quotedOrderingField)
1444 ->from($this->_tbl);
1445
1446 // If the movement delta is negative move the row up.
1447 if ($delta < 0)
1448 {
1449 $query->where($quotedOrderingField . ' < ' . (int) $this->$orderingField)
1450 ->order($quotedOrderingField . ' DESC');
1451 }
1452 // If the movement delta is positive move the row down.
1453 elseif ($delta > 0)
1454 {
1455 $query->where($quotedOrderingField . ' > ' . (int) $this->$orderingField)
1456 ->order($quotedOrderingField . ' ASC');
1457 }
1458
1459 // Add the custom WHERE clause if set.
1460 if ($where)
1461 {
1462 $query->where($where);
1463 }
1464
1465 // Select the first row with the criteria.
1466 $this->_db->setQuery($query, 0, 1);
1467 $row = $this->_db->loadObject();
1468
1469 // If a row is found, move the item.
1470 if (!empty($row))
1471 {
1472 // Update the ordering field for this instance to the row's ordering value.
1473 $query->clear()
1474 ->update($this->_tbl)
1475 ->set($quotedOrderingField . ' = ' . (int) $row->$orderingField);
1476 $this->appendPrimaryKeys($query);
1477 $this->_db->setQuery($query);
1478 $this->_db->execute();
1479
1480 // Update the ordering field for the row to this instance's ordering value.
1481 $query->clear()
1482 ->update($this->_tbl)
1483 ->set($quotedOrderingField . ' = ' . (int) $this->$orderingField);
1484 $this->appendPrimaryKeys($query, $row);
1485 $this->_db->setQuery($query);
1486 $this->_db->execute();
1487
1488 // Update the instance value.
1489 $this->$orderingField = $row->$orderingField;
1490 }
1491 else
1492 {
1493 // Update the ordering field for this instance.
1494 $query->clear()
1495 ->update($this->_tbl)
1496 ->set($quotedOrderingField . ' = ' . (int) $this->$orderingField);
1497 $this->appendPrimaryKeys($query);
1498 $this->_db->setQuery($query);
1499 $this->_db->execute();
1500 }
1501
1502 return true;
1503 }
1504
1505 /**
1506 * Method to set the publishing state for a row or list of rows in the database table.
1507 *
1508 * The method respects checked out rows by other users and will attempt to checkin rows that it can after adjustments are made.
1509 *
1510 * @param mixed $pks An optional array of primary key values to update. If not set the instance property value is used.
1511 * @param integer $state The publishing state. eg. [0 = unpublished, 1 = published]
1512 * @param integer $userId The user ID of the user performing the operation.
1513 *
1514 * @return boolean True on success; false if $pks is empty.
1515 *
1516 * @since 11.1
1517 */
1518 public function publish($pks = null, $state = 1, $userId = 0)
1519 {
1520 // Sanitize input
1521 $userId = (int) $userId;
1522 $state = (int) $state;
1523
1524 if (!is_null($pks))
1525 {
1526 if (!is_array($pks))
1527 {
1528 $pks = array($pks);
1529 }
1530
1531 foreach ($pks as $key => $pk)
1532 {
1533 if (!is_array($pk))
1534 {
1535 $pks[$key] = array($this->_tbl_key => $pk);
1536 }
1537 }
1538 }
1539
1540 // If there are no primary keys set check to see if the instance key is set.
1541 if (empty($pks))
1542 {
1543 $pk = array();
1544
1545 foreach ($this->_tbl_keys as $key)
1546 {
1547 if ($this->$key)
1548 {
1549 $pk[$key] = $this->$key;
1550 }
1551 // We don't have a full primary key - return false
1552 else
1553 {
1554 $this->setError(JText::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED'));
1555
1556 return false;
1557 }
1558 }
1559
1560 $pks = array($pk);
1561 }
1562
1563 $publishedField = $this->getColumnAlias('published');
1564 $checkedOutField = $this->getColumnAlias('checked_out');
1565
1566 foreach ($pks as $pk)
1567 {
1568 // Update the publishing state for rows with the given primary keys.
1569 $query = $this->_db->getQuery(true)
1570 ->update($this->_tbl)
1571 ->set($this->_db->quoteName($publishedField) . ' = ' . (int) $state);
1572
1573 // If publishing, set published date/time if not previously set
1574 if ($state && property_exists($this, 'publish_up') && (int) $this->publish_up == 0)
1575 {
1576 $nowDate = $this->_db->quote(JFactory::getDate()->toSql());
1577 $query->set($this->_db->quoteName($this->getColumnAlias('publish_up')) . ' = ' . $nowDate);
1578 }
1579
1580 // Determine if there is checkin support for the table.
1581 if (property_exists($this, 'checked_out') || property_exists($this, 'checked_out_time'))
1582 {
1583 $query->where('(' . $this->_db->quoteName($checkedOutField) . ' = 0 OR ' . $this->_db->quoteName($checkedOutField) . ' = ' . (int) $userId . ')');
1584 $checkin = true;
1585 }
1586 else
1587 {
1588 $checkin = false;
1589 }
1590
1591 // Build the WHERE clause for the primary keys.
1592 $this->appendPrimaryKeys($query, $pk);
1593
1594 $this->_db->setQuery($query);
1595
1596 try
1597 {
1598 $this->_db->execute();
1599 }
1600 catch (RuntimeException $e)
1601 {
1602 $this->setError($e->getMessage());
1603
1604 return false;
1605 }
1606
1607 // If checkin is supported and all rows were adjusted, check them in.
1608 if ($checkin && (count($pks) == $this->_db->getAffectedRows()))
1609 {
1610 $this->checkin($pk);
1611 }
1612
1613 // If the JTable instance value is in the list of primary keys that were set, set the instance.
1614 $ours = true;
1615
1616 foreach ($this->_tbl_keys as $key)
1617 {
1618 if ($this->$key != $pk[$key])
1619 {
1620 $ours = false;
1621 }
1622 }
1623
1624 if ($ours)
1625 {
1626 $this->$publishedField = $state;
1627 }
1628 }
1629
1630 $this->setError('');
1631
1632 return true;
1633 }
1634
1635 /**
1636 * Method to lock the database table for writing.
1637 *
1638 * @return boolean True on success.
1639 *
1640 * @since 11.1
1641 * @throws RuntimeException
1642 */
1643 protected function _lock()
1644 {
1645 $this->_db->lockTable($this->_tbl);
1646 $this->_locked = true;
1647
1648 return true;
1649 }
1650
1651 /**
1652 * Method to return the real name of a "special" column such as ordering, hits, published
1653 * etc etc. In this way you are free to follow your db naming convention and use the
1654 * built in Joomla functions.
1655 *
1656 * @param string $column Name of the "special" column (ie ordering, hits)
1657 *
1658 * @return string The string that identify the special
1659 *
1660 * @since 3.4
1661 */
1662 public function getColumnAlias($column)
1663 {
1664 // Get the column data if set
1665 if (isset($this->_columnAlias[$column]))
1666 {
1667 $return = $this->_columnAlias[$column];
1668 }
1669 else
1670 {
1671 $return = $column;
1672 }
1673
1674 // Sanitize the name
1675 $return = preg_replace('#[^A-Z0-9_]#i', '', $return);
1676
1677 return $return;
1678 }
1679
1680 /**
1681 * Method to register a column alias for a "special" column.
1682 *
1683 * @param string $column The "special" column (ie ordering)
1684 * @param string $columnAlias The real column name (ie foo_ordering)
1685 *
1686 * @return void
1687 *
1688 * @since 3.4
1689 */
1690 public function setColumnAlias($column, $columnAlias)
1691 {
1692 // Santize the column name alias
1693 $column = strtolower($column);
1694 $column = preg_replace('#[^A-Z0-9_]#i', '', $column);
1695
1696 // Set the column alias internally
1697 $this->_columnAlias[$column] = $columnAlias;
1698 }
1699
1700 /**
1701 * Method to unlock the database table for writing.
1702 *
1703 * @return boolean True on success.
1704 *
1705 * @since 11.1
1706 */
1707 protected function _unlock()
1708 {
1709 $this->_db->unlockTables();
1710 $this->_locked = false;
1711
1712 return true;
1713 }
1714 }
1715