* @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Component\Scheduler\Administrator\Table; use Joomla\CMS\Event\AbstractEvent; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Model\AdminModel; use Joomla\CMS\Table\Asset; use Joomla\CMS\Table\Table; use Joomla\Database\DatabaseDriver; use Joomla\Database\Exception\QueryTypeAlreadyDefinedException; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Table class for tasks scheduled through `com_scheduler`. * The type alias for Task table entries is `com_scheduler.task`. * * @since 4.1.0 */ class TaskTable extends Table { /** * Indicates that columns fully support the NULL value in the database * * @var boolean * @since 4.1.1 */ protected $_supportNullValue = true; /** * Ensure params are json encoded by the bind method. * * @var string[] * @since 4.1.0 */ protected $_jsonEncode = ['params', 'execution_rules', 'cron_rules']; /** * The 'created' column. * * @var string * @since 4.1.0 */ public $created; /** * The 'title' column. * * @var string * @since 4.1.0 */ public $title; /** * @var string * @since 4.1.0 */ public $typeAlias = 'com_scheduler.task'; /** * TaskTable constructor override, needed to pass the DB table name and primary key to {@see Table::__construct()}. * * @param DatabaseDriver $db A database connector object. * * @since 4.1.0 */ public function __construct(DatabaseDriver $db) { $this->setColumnAlias('published', 'state'); parent::__construct('#__scheduler_tasks', 'id', $db); } /** * Overloads {@see Table::check()} to perform sanity checks on properties and make sure they're * safe to store. * * @return boolean True if checks pass. * * @since 4.1.0 * @throws \Exception */ public function check(): bool { try { parent::check(); } catch (\Exception $e) { Factory::getApplication()->enqueueMessage($e->getMessage()); return false; } $this->title = htmlspecialchars_decode($this->title, ENT_QUOTES); // Set created date if not set. // ? Might not need since the constructor already sets this if (!(int) $this->created) { $this->created = Factory::getDate()->toSql(); } // @todo : Add more checks if needed return true; } /** * Override {@see Table::store()} to update null fields as a default, which is needed when DATETIME * fields need to be updated to NULL. This override is needed because {@see AdminModel::save()} does not * expose an option to pass true to Table::store(). Also ensures the `created` and `created_by` fields are * set. * * @param boolean $updateNulls True to update fields even if they're null. * * @return boolean True if successful. * * @since 4.1.0 * @throws \Exception */ public function store($updateNulls = true): bool { $isNew = empty($this->getId()); // Set creation date if not set for a new item. if ($isNew && empty($this->created)) { $this->created = Factory::getDate()->toSql(); } // Set `created_by` if not set for a new item. if ($isNew && empty($this->created_by)) { $this->created_by = Factory::getApplication()->getIdentity()->id; } // @todo : Should we add modified, modified_by fields? [ ] return parent::store($updateNulls); } /** * Returns the asset name of the entry as it appears in the {@see Asset} table. * * @return string The asset name. * * @since 4.1.0 */ protected function _getAssetName(): string { $k = $this->_tbl_key; return 'com_scheduler.task.' . (int) $this->$k; } /** * Override {@see Table::bind()} to bind some fields even if they're null given they're present in $src. * This override is needed specifically for DATETIME fields, of which the `next_execution` field is updated to * null if a task is configured to execute only on manual trigger. * * @param array|object $src An associative array or object to bind to the Table instance. * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. * * @return boolean * * @since 4.1.0 */ public function bind($src, $ignore = array()): bool { $fields = ['next_execution']; foreach ($fields as $field) { if (\array_key_exists($field, $src) && \is_null($src[$field])) { $this->$field = $src[$field]; } } return parent::bind($src, $ignore); } /** * Release pseudo-locks on a set of task records. If an empty set is passed, this method releases lock on its * instance primary key, if available. * * @param integer[] $pks An optional array of primary key values to update. If not set the instance property * value is used. * @param ?int $userId ID of the user unlocking the tasks. * * @return boolean True on success; false if $pks is empty. * * @since 4.1.0 * @throws QueryTypeAlreadyDefinedException|\UnexpectedValueException|\BadMethodCallException */ public function unlock(array $pks = [], ?int $userId = null): bool { // Pre-processing by observers $event = AbstractEvent::create( 'onTaskBeforeUnlock', [ 'subject' => $this, 'pks' => $pks, 'userId' => $userId, ] ); $this->getDispatcher()->dispatch('onTaskBeforeUnlock', $event); // Some pre-processing before we can work with the keys. if (!empty($pks)) { foreach ($pks as $key => $pk) { if (!\is_array($pk)) { $pks[$key] = array($this->_tbl_key => $pk); } } } // If there are no primary keys set check to see if the instance key is set and use that. if (empty($pks)) { $pk = []; foreach ($this->_tbl_keys as $key) { if ($this->$key) { $pk[$key] = $this->$key; } else { // We don't have a full primary key - return false. $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); return false; } } $pks = [$pk]; } $lockedField = $this->getColumnAlias('locked'); foreach ($pks as $pk) { // Update the publishing state for rows with the given primary keys. $query = $this->_db->getQuery(true) ->update($this->_tbl) ->set($this->_db->quoteName($lockedField) . ' = NULL'); // Build the WHERE clause for the primary keys. $this->appendPrimaryKeys($query, $pk); $this->_db->setQuery($query); try { $this->_db->execute(); } catch (\RuntimeException $e) { $this->setError($e->getMessage()); return false; } // If the Table instance value is in the list of primary keys that were set, set the instance. $ours = true; foreach ($this->_tbl_keys as $key) { if ($this->$key != $pk[$key]) { $ours = false; } } if ($ours) { $this->$lockedField = null; } } // Pre-processing by observers $event = AbstractEvent::create( 'onTaskAfterUnlock', [ 'subject' => $this, 'pks' => $pks, 'userId' => $userId, ] ); $this->getDispatcher()->dispatch('onTaskAfterUnlock', $event); return true; } }