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