primo commit
This commit is contained in:
		
							
								
								
									
										867
									
								
								administrator/components/com_scheduler/src/Model/TaskModel.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										867
									
								
								administrator/components/com_scheduler/src/Model/TaskModel.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,867 @@ | ||||
| <?php | ||||
|  | ||||
| /** | ||||
|  * @package     Joomla.Administrator | ||||
|  * @subpackage  com_scheduler | ||||
|  * | ||||
|  * @copyright   (C) 2021 Open Source Matters, Inc. <https://www.joomla.org> | ||||
|  * @license     GNU General Public License version 2 or later; see LICENSE.txt | ||||
|  */ | ||||
|  | ||||
| namespace Joomla\Component\Scheduler\Administrator\Model; | ||||
|  | ||||
| use Joomla\CMS\Application\AdministratorApplication; | ||||
| use Joomla\CMS\Component\ComponentHelper; | ||||
| use Joomla\CMS\Event\AbstractEvent; | ||||
| use Joomla\CMS\Factory; | ||||
| use Joomla\CMS\Form\Form; | ||||
| use Joomla\CMS\Form\FormFactoryInterface; | ||||
| use Joomla\CMS\Language\Text; | ||||
| use Joomla\CMS\Log\Log; | ||||
| use Joomla\CMS\MVC\Factory\MVCFactoryInterface; | ||||
| use Joomla\CMS\MVC\Model\AdminModel; | ||||
| use Joomla\CMS\Object\CMSObject; | ||||
| use Joomla\CMS\Plugin\PluginHelper; | ||||
| use Joomla\CMS\Table\Table; | ||||
| use Joomla\Component\Scheduler\Administrator\Helper\ExecRuleHelper; | ||||
| use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper; | ||||
| use Joomla\Component\Scheduler\Administrator\Table\TaskTable; | ||||
| use Joomla\Component\Scheduler\Administrator\Task\TaskOption; | ||||
| use Joomla\Database\ParameterType; | ||||
| use Symfony\Component\OptionsResolver\Exception\AccessException; | ||||
| use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; | ||||
| use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; | ||||
| use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
|  | ||||
| // phpcs:disable PSR1.Files.SideEffects | ||||
| \defined('_JEXEC') or die; | ||||
| // phpcs:enable PSR1.Files.SideEffects | ||||
|  | ||||
| /** | ||||
|  * MVC Model to interact with the Scheduler DB. | ||||
|  * Implements methods to add, remove, edit tasks. | ||||
|  * | ||||
|  * @since  4.1.0 | ||||
|  */ | ||||
| class TaskModel extends AdminModel | ||||
| { | ||||
|     /** | ||||
|      * Maps logical states to their values in the DB | ||||
|      * ? Do we end up using this? | ||||
|      * | ||||
|      * @var array | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     protected const TASK_STATES = [ | ||||
|         'enabled'  => 1, | ||||
|         'disabled' => 0, | ||||
|         'trashed'  => -2, | ||||
|     ]; | ||||
|  | ||||
|     /** | ||||
|      * The name of the  database table with task records. | ||||
|      * | ||||
|      * @var  string | ||||
|      * @since 4.1.0 | ||||
|      */ | ||||
|     public const TASK_TABLE = '#__scheduler_tasks'; | ||||
|  | ||||
|     /** | ||||
|      * Prefix used with controller messages | ||||
|      * | ||||
|      * @var string | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     protected $text_prefix = 'COM_SCHEDULER'; | ||||
|  | ||||
|     /** | ||||
|      * Type alias for content type | ||||
|      * | ||||
|      * @var string | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     public $typeAlias = 'com_scheduler.task'; | ||||
|  | ||||
|     /** | ||||
|      * The Application object, for convenience | ||||
|      * | ||||
|      * @var AdministratorApplication $app | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     protected $app; | ||||
|  | ||||
|     /** | ||||
|      * The event to trigger before unlocking the data. | ||||
|      * | ||||
|      * @var    string | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     protected $event_before_unlock = null; | ||||
|  | ||||
|     /** | ||||
|      * The event to trigger after unlocking the data. | ||||
|      * | ||||
|      * @var    string | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     protected $event_unlock = null; | ||||
|  | ||||
|     /** | ||||
|      * TaskModel constructor. Needed just to set $app | ||||
|      * | ||||
|      * @param   array                  $config       An array of configuration options | ||||
|      * @param   ?MVCFactoryInterface   $factory      The factory | ||||
|      * @param   ?FormFactoryInterface  $formFactory  The form factory | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     public function __construct($config = [], ?MVCFactoryInterface $factory = null, ?FormFactoryInterface $formFactory = null) | ||||
|     { | ||||
|         $config['events_map'] = $config['events_map'] ?? []; | ||||
|  | ||||
|         $config['events_map'] = array_merge( | ||||
|             [ | ||||
|                 'save'     => 'task', | ||||
|                 'validate' => 'task', | ||||
|                 'unlock'   => 'task', | ||||
|             ], | ||||
|             $config['events_map'] | ||||
|         ); | ||||
|  | ||||
|         if (isset($config['event_before_unlock'])) { | ||||
|             $this->event_before_unlock = $config['event_before_unlock']; | ||||
|         } elseif (empty($this->event_before_unlock)) { | ||||
|             $this->event_before_unlock = 'onContentBeforeUnlock'; | ||||
|         } | ||||
|  | ||||
|         if (isset($config['event_unlock'])) { | ||||
|             $this->event_unlock = $config['event_unlock']; | ||||
|         } elseif (empty($this->event_unlock)) { | ||||
|             $this->event_unlock = 'onContentUnlock'; | ||||
|         } | ||||
|  | ||||
|         $this->app = Factory::getApplication(); | ||||
|  | ||||
|         parent::__construct($config, $factory, $formFactory); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fetches the form object associated with this model. By default, | ||||
|      * loads the corresponding data from the DB and binds it with the form. | ||||
|      * | ||||
|      * @param   array  $data      Data that needs to go into the form | ||||
|      * @param   bool   $loadData  Should the form load its data from the DB? | ||||
|      * | ||||
|      * @return Form|boolean  A Form object on success, false on failure. | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     public function getForm($data = [], $loadData = true) | ||||
|     { | ||||
|         Form::addFieldPath(JPATH_ADMINISTRATOR . 'components/com_scheduler/src/Field'); | ||||
|  | ||||
|         /** | ||||
|          *  loadForm() (defined by FormBehaviourTrait) also loads the form data by calling | ||||
|          *  loadFormData() : $data [implemented here] and binds it to the form by calling | ||||
|          *  $form->bind($data). | ||||
|          */ | ||||
|         $form = $this->loadForm('com_scheduler.task', 'task', ['control' => 'jform', 'load_data' => $loadData]); | ||||
|  | ||||
|         if (empty($form)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         $user = $this->app->getIdentity(); | ||||
|  | ||||
|         // If new entry, set task type from state | ||||
|         if ($this->getState('task.id', 0) === 0 && $this->getState('task.type') !== null) { | ||||
|             $form->setValue('type', null, $this->getState('task.type')); | ||||
|         } | ||||
|  | ||||
|         // @todo : Check if this is working as expected for new items (id == 0) | ||||
|         if (!$user->authorise('core.edit.state', 'com_scheduler.task.' . $this->getState('task.id'))) { | ||||
|             // Disable fields | ||||
|             $form->setFieldAttribute('state', 'disabled', 'true'); | ||||
|  | ||||
|             // No "hacking" ._. | ||||
|             $form->setFieldAttribute('state', 'filter', 'unset'); | ||||
|         } | ||||
|  | ||||
|         return $form; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Determine whether a record may be deleted taking into consideration | ||||
|      * the user's permissions over the record. | ||||
|      * | ||||
|      * @param   object  $record  The database row/record in question | ||||
|      * | ||||
|      * @return  boolean  True if the record may be deleted | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     protected function canDelete($record): bool | ||||
|     { | ||||
|         // Record doesn't exist, can't delete | ||||
|         if (empty($record->id)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return $this->app->getIdentity()->authorise('core.delete', 'com_scheduler.task.' . $record->id); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Populate the model state, we use these instead of toying with input or the global state | ||||
|      * | ||||
|      * @return  void | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     protected function populateState(): void | ||||
|     { | ||||
|         $app = $this->app; | ||||
|  | ||||
|         $taskId   = $app->getInput()->getInt('id'); | ||||
|         $taskType = $app->getUserState('com_scheduler.add.task.task_type'); | ||||
|  | ||||
|         // @todo: Remove this. Get the option through a helper call. | ||||
|         $taskOption = $app->getUserState('com_scheduler.add.task.task_option'); | ||||
|  | ||||
|         $this->setState('task.id', $taskId); | ||||
|         $this->setState('task.type', $taskType); | ||||
|         $this->setState('task.option', $taskOption); | ||||
|  | ||||
|         // Load component params, though com_scheduler does not (yet) have any params | ||||
|         $cParams = ComponentHelper::getParams($this->option); | ||||
|         $this->setState('params', $cParams); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Don't need to define this method since the parent getTable() | ||||
|      * implicitly deduces $name and $prefix anyways. This makes the object | ||||
|      * more transparent though. | ||||
|      * | ||||
|      * @param   string  $name     Name of the table | ||||
|      * @param   string  $prefix   Class prefix | ||||
|      * @param   array   $options  Model config array | ||||
|      * | ||||
|      * @return Table | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     public function getTable($name = 'Task', $prefix = 'Table', $options = []): Table | ||||
|     { | ||||
|         return parent::getTable($name, $prefix, $options); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fetches the data to be injected into the form | ||||
|      * | ||||
|      * @return object  Associative array of form data. | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     protected function loadFormData() | ||||
|     { | ||||
|         $data = $this->app->getUserState('com_scheduler.edit.task.data', []); | ||||
|  | ||||
|         // If the data from UserState is empty, we fetch it with getItem() | ||||
|         if (empty($data)) { | ||||
|             /** @var CMSObject $data */ | ||||
|             $data = $this->getItem(); | ||||
|  | ||||
|             // @todo : further data processing goes here | ||||
|  | ||||
|             // For a fresh object, set exec-day and exec-time | ||||
|             if (!($data->id ?? 0)) { | ||||
|                 $data->execution_rules['exec-day']  = gmdate('d'); | ||||
|                 $data->execution_rules['exec-time'] = gmdate('H:i'); | ||||
|             } | ||||
|  | ||||
|             if ($data->next_execution) { | ||||
|                 $data->next_execution = Factory::getDate($data->next_execution); | ||||
|                 $data->next_execution->setTimezone(new \DateTimeZone($this->app->get('offset', 'UTC'))); | ||||
|                 $data->next_execution = $data->next_execution->toSql(true); | ||||
|             } | ||||
|  | ||||
|             if ($data->last_execution) { | ||||
|                 $data->last_execution = Factory::getDate($data->last_execution); | ||||
|                 $data->last_execution->setTimezone(new \DateTimeZone($this->app->get('offset', 'UTC'))); | ||||
|                 $data->last_execution = $data->last_execution->toSql(true); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Let plugins manipulate the data | ||||
|         $this->preprocessData('com_scheduler.task', $data, 'task'); | ||||
|  | ||||
|         return $data; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Overloads the parent getItem() method. | ||||
|      * | ||||
|      * @param   integer  $pk  Primary key | ||||
|      * | ||||
|      * @return  object|boolean  Object on success, false on failure | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     public function getItem($pk = null) | ||||
|     { | ||||
|         $item = parent::getItem($pk); | ||||
|  | ||||
|         if (!\is_object($item)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // Parent call leaves `execution_rules` and `cron_rules` JSON encoded | ||||
|         $item->set('execution_rules', json_decode($item->get('execution_rules', ''))); | ||||
|         $item->set('cron_rules', json_decode($item->get('cron_rules', ''))); | ||||
|  | ||||
|         $taskOption = SchedulerHelper::getTaskOptions()->findOption( | ||||
|             ($item->id ?? 0) ? ($item->type ?? 0) : $this->getState('task.type') | ||||
|         ); | ||||
|  | ||||
|         $item->set('taskOption', $taskOption); | ||||
|  | ||||
|         return $item; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a task from the database, only if an exclusive "lock" on the task can be acquired. | ||||
|      * The method supports options to customise the limitations on the fetch. | ||||
|      * | ||||
|      * @param   array  $options  Array with options to fetch the task: | ||||
|      *                           1. `id`: Optional id of the task to fetch. | ||||
|      *                           2. `allowDisabled`: If true, disabled tasks can also be fetched. | ||||
|      *                           (default: false) | ||||
|      *                           3. `bypassScheduling`: If true, tasks that are not due can also be | ||||
|      *                           fetched. Should only be true if an `id` is targeted instead of the | ||||
|      *                           task queue. (default: false) | ||||
|      *                           4. `allowConcurrent`: If true, fetches even when another task is | ||||
|      *                           running ('locked'). (default: false) | ||||
|      *                           5. `includeCliExclusive`: If true, can also fetch CLI exclusive tasks. (default: true) | ||||
|      * | ||||
|      * @return ?\stdClass  Task entry as in the database. | ||||
|      * | ||||
|      * @since   4.1.0 | ||||
|      * @throws UndefinedOptionsException|InvalidOptionsException | ||||
|      * @throws \RuntimeException | ||||
|      */ | ||||
|     public function getTask(array $options = []): ?\stdClass | ||||
|     { | ||||
|         $resolver = new OptionsResolver(); | ||||
|  | ||||
|         try { | ||||
|             static::configureTaskGetterOptions($resolver); | ||||
|         } catch (\Exception $e) { | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             $options = $resolver->resolve($options); | ||||
|         } catch (UndefinedOptionsException | InvalidOptionsException $e) { | ||||
|             throw $e; | ||||
|         } | ||||
|  | ||||
|         $db           = $this->getDatabase(); | ||||
|         $now          = Factory::getDate()->toSql(); | ||||
|         $affectedRows = 0; | ||||
|  | ||||
|         try { | ||||
|             $db->lockTable(self::TASK_TABLE); | ||||
|  | ||||
|             if (!$options['allowConcurrent'] && $this->hasRunningTasks($db)) { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             $lockQuery = $this->buildLockQuery($db, $now, $options); | ||||
|  | ||||
|             if ($options['id'] > 0) { | ||||
|                 $lockQuery->where($db->quoteName('id') . ' = :taskId') | ||||
|                     ->bind(':taskId', $options['id'], ParameterType::INTEGER); | ||||
|             } else { | ||||
|                 $id = $this->getNextTaskId($db, $now, $options); | ||||
|                 if (\count($id) === 0) { | ||||
|                     return null; | ||||
|                 } | ||||
|                 $lockQuery->where($db->quoteName('id') . ' = :taskId') | ||||
|                     ->bind(':taskId', $id, ParameterType::INTEGER); | ||||
|             } | ||||
|  | ||||
|             $db->setQuery($lockQuery)->execute(); | ||||
|             $affectedRows = $db->getAffectedRows(); | ||||
|         } catch (\RuntimeException $e) { | ||||
|             return null; | ||||
|         } finally { | ||||
|             $db->unlockTables(); | ||||
|         } | ||||
|  | ||||
|         if ($affectedRows != 1) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         return $this->fetchTask($db, $now); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks if there are any running tasks in the database. | ||||
|      * | ||||
|      * @param \JDatabaseDriver $db The database driver to use. | ||||
|      * @return bool True if there are running tasks, false otherwise. | ||||
|      * @since 4.4.9 | ||||
|      */ | ||||
|     private function hasRunningTasks($db): bool | ||||
|     { | ||||
|         $lockCountQuery = $db->getQuery(true) | ||||
|             ->select('COUNT(id)') | ||||
|             ->from($db->quoteName(self::TASK_TABLE)) | ||||
|             ->where($db->quoteName('locked') . ' IS NOT NULL') | ||||
|             ->where($db->quoteName('state') . ' = 1'); | ||||
|  | ||||
|         try { | ||||
|             $runningCount = $db->setQuery($lockCountQuery)->loadResult(); | ||||
|         } catch (\RuntimeException $e) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return $runningCount != 0; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Builds a query to lock a task. | ||||
|      * | ||||
|      * @param Database $db The database object. | ||||
|      * @param string $now The current time. | ||||
|      * @param array $options The options for building the query. | ||||
|      *                      - includeCliExclusive: Whether to include CLI exclusive tasks. | ||||
|      *                      - bypassScheduling: Whether to bypass scheduling. | ||||
|      *                      - allowDisabled: Whether to allow disabled tasks. | ||||
|      *                      - id: The ID of the task. | ||||
|      * @return Query The lock query. | ||||
|      * @since 5.2.0 | ||||
|      */ | ||||
|     private function buildLockQuery($db, $now, $options) | ||||
|     { | ||||
|         $lockQuery = $db->getQuery(true) | ||||
|             ->update($db->quoteName(self::TASK_TABLE)) | ||||
|             ->set($db->quoteName('locked') . ' = :now1') | ||||
|             ->bind(':now1', $now); | ||||
|  | ||||
|         $activeRoutines = array_map( | ||||
|             static function (TaskOption $taskOption): string { | ||||
|                 return $taskOption->id; | ||||
|             }, | ||||
|             SchedulerHelper::getTaskOptions()->options | ||||
|         ); | ||||
|  | ||||
|         $lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); | ||||
|  | ||||
|         if (!$options['includeCliExclusive']) { | ||||
|             $lockQuery->where($db->quoteName('cli_exclusive') . ' = 0'); | ||||
|         } | ||||
|  | ||||
|         if (!$options['bypassScheduling']) { | ||||
|             $lockQuery->where($db->quoteName('next_execution') . ' <= :now2') | ||||
|                 ->bind(':now2', $now); | ||||
|         } | ||||
|  | ||||
|         $stateCondition = $options['allowDisabled'] ? [0, 1] : [1]; | ||||
|         $lockQuery->whereIn($db->quoteName('state'), $stateCondition); | ||||
|  | ||||
|         return $lockQuery; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Retrieves the ID of the next task based on the given criteria. | ||||
|      * | ||||
|      * @param \JDatabaseDriver $db The database object. | ||||
|      * @param string $now The current time. | ||||
|      * @param array $options The options for retrieving the next task. | ||||
|      *                       - includeCliExclusive: Whether to include CLI exclusive tasks. | ||||
|      *                       - bypassScheduling: Whether to bypass scheduling. | ||||
|      *                       - allowDisabled: Whether to allow disabled tasks. | ||||
|      * @return array The ID of the next task, or an empty array if no task is found. | ||||
|      * | ||||
|      * @since 5.2.0 | ||||
|      * @throws \RuntimeException If there is an error executing the query. | ||||
|      */ | ||||
|     private function getNextTaskId($db, $now, $options) | ||||
|     { | ||||
|         $idQuery = $db->getQuery(true) | ||||
|             ->from($db->quoteName(self::TASK_TABLE)) | ||||
|             ->select($db->quoteName('id')); | ||||
|  | ||||
|         $activeRoutines = array_map( | ||||
|             static function (TaskOption $taskOption): string { | ||||
|                 return $taskOption->id; | ||||
|             }, | ||||
|             SchedulerHelper::getTaskOptions()->options | ||||
|         ); | ||||
|  | ||||
|         $idQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); | ||||
|  | ||||
|         if (!$options['includeCliExclusive']) { | ||||
|             $idQuery->where($db->quoteName('cli_exclusive') . ' = 0'); | ||||
|         } | ||||
|  | ||||
|         if (!$options['bypassScheduling']) { | ||||
|             $idQuery->where($db->quoteName('next_execution') . ' <= :now2') | ||||
|                 ->bind(':now2', $now); | ||||
|         } | ||||
|  | ||||
|         $stateCondition = $options['allowDisabled'] ? [0, 1] : [1]; | ||||
|         $idQuery->whereIn($db->quoteName('state'), $stateCondition); | ||||
|  | ||||
|         $idQuery->where($db->quoteName('next_execution') . ' IS NOT NULL') | ||||
|             ->order($db->quoteName('priority') . ' DESC') | ||||
|             ->order($db->quoteName('next_execution') . ' ASC') | ||||
|             ->setLimit(1); | ||||
|  | ||||
|         try { | ||||
|             return $db->setQuery($idQuery)->loadColumn(); | ||||
|         } catch (\RuntimeException $e) { | ||||
|             return []; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fetches a task from the database based on the current time. | ||||
|      * | ||||
|      * @param \JDatabaseDriver $db The database driver to use. | ||||
|      * @param string $now The current time in the database's time format. | ||||
|      * @return \stdClass|null The fetched task object, or null if no task was found. | ||||
|      * @since 5.2.0 | ||||
|      * @throws \RuntimeException If there was an error executing the query. | ||||
|      */ | ||||
|     private function fetchTask($db, $now): ?\stdClass | ||||
|     { | ||||
|         $getQuery = $db->getQuery(true) | ||||
|             ->select('*') | ||||
|             ->from($db->quoteName(self::TASK_TABLE)) | ||||
|             ->where($db->quoteName('locked') . ' = :now') | ||||
|             ->bind(':now', $now); | ||||
|  | ||||
|         try { | ||||
|             $task = $db->setQuery($getQuery)->loadObject(); | ||||
|         } catch (\RuntimeException $e) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         $task->execution_rules = json_decode($task->execution_rules); | ||||
|         $task->cron_rules      = json_decode($task->cron_rules); | ||||
|         $task->taskOption      = SchedulerHelper::getTaskOptions()->findOption($task->type); | ||||
|  | ||||
|         return $task; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set up an {@see OptionsResolver} to resolve options compatible with the {@see GetTask()} method. | ||||
|      * | ||||
|      * @param   OptionsResolver  $resolver  The {@see OptionsResolver} instance to set up. | ||||
|      * | ||||
|      * @return OptionsResolver | ||||
|      * | ||||
|      * @since 4.1.0 | ||||
|      * @throws AccessException | ||||
|      */ | ||||
|     public static function configureTaskGetterOptions(OptionsResolver $resolver): OptionsResolver | ||||
|     { | ||||
|         $resolver->setDefaults( | ||||
|             [ | ||||
|                 'id'                  => 0, | ||||
|                 'allowDisabled'       => false, | ||||
|                 'bypassScheduling'    => false, | ||||
|                 'allowConcurrent'     => false, | ||||
|                 'includeCliExclusive' => true, | ||||
|             ] | ||||
|         ) | ||||
|             ->setAllowedTypes('id', 'numeric') | ||||
|             ->setAllowedTypes('allowDisabled', 'bool') | ||||
|             ->setAllowedTypes('bypassScheduling', 'bool') | ||||
|             ->setAllowedTypes('allowConcurrent', 'bool') | ||||
|             ->setAllowedTypes('includeCliExclusive', 'bool'); | ||||
|  | ||||
|         return $resolver; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param   array  $data  The form data | ||||
|      * | ||||
|      * @return  boolean  True on success, false on failure | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     public function save($data): bool | ||||
|     { | ||||
|         $id    = (int) ($data['id'] ?? $this->getState('task.id')); | ||||
|         $isNew = $id === 0; | ||||
|  | ||||
|         // Clean up execution rules | ||||
|         $data['execution_rules'] = $this->processExecutionRules($data['execution_rules']); | ||||
|  | ||||
|         // If a new entry, we'll have to put in place a pseudo-last_execution | ||||
|         if ($isNew) { | ||||
|             $basisDayOfMonth           = $data['execution_rules']['exec-day']; | ||||
|             [$basisHour, $basisMinute] = explode(':', $data['execution_rules']['exec-time']); | ||||
|  | ||||
|             $data['last_execution'] = Factory::getDate('now', 'GMT')->format('Y-m') | ||||
|                 . "-$basisDayOfMonth $basisHour:$basisMinute:00"; | ||||
|         } else { | ||||
|             $data['last_execution'] = $this->getItem($id)->last_execution; | ||||
|         } | ||||
|  | ||||
|         // Build the `cron_rules` column from `execution_rules` | ||||
|         $data['cron_rules'] = $this->buildExecutionRules($data['execution_rules']); | ||||
|  | ||||
|         // `next_execution` would be null if scheduling is disabled with the "manual" rule! | ||||
|         $data['next_execution'] = (new ExecRuleHelper($data))->nextExec(); | ||||
|  | ||||
|         if ($isNew) { | ||||
|             $data['last_execution'] = null; | ||||
|         } | ||||
|  | ||||
|         // If no params, we set as empty array. | ||||
|         // ? Is this the right place to do this | ||||
|         $data['params'] = $data['params'] ?? []; | ||||
|  | ||||
|         // Parent method takes care of saving to the table | ||||
|         return parent::save($data); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clean up and standardise execution rules | ||||
|      * | ||||
|      * @param   array  $unprocessedRules  The form data [? can just replace with execution_interval] | ||||
|      * | ||||
|      * @return array  Processed rules | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     private function processExecutionRules(array $unprocessedRules): array | ||||
|     { | ||||
|         $executionRules = $unprocessedRules; | ||||
|  | ||||
|         $ruleType       = $executionRules['rule-type']; | ||||
|         $retainKeys     = ['rule-type', $ruleType, 'exec-day', 'exec-time']; | ||||
|         $executionRules = array_intersect_key($executionRules, array_flip($retainKeys)); | ||||
|  | ||||
|         // Default to current date-time in UTC/GMT as the basis | ||||
|         $executionRules['exec-day']  = $executionRules['exec-day'] ?: (string) gmdate('d'); | ||||
|         $executionRules['exec-time'] = $executionRules['exec-time'] ?: (string) gmdate('H:i'); | ||||
|  | ||||
|         // If custom ruleset, sort it | ||||
|         // ? Is this necessary | ||||
|         if ($ruleType === 'cron-expression') { | ||||
|             foreach ($executionRules['cron-expression'] as &$values) { | ||||
|                 sort($values); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return $executionRules; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Private method to build execution expression from input execution rules. | ||||
|      * This expression is used internally to determine execution times/conditions. | ||||
|      * | ||||
|      * @param   array  $executionRules  Execution rules from the Task form, post-processing. | ||||
|      * | ||||
|      * @return array | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     private function buildExecutionRules(array $executionRules): array | ||||
|     { | ||||
|         // Maps interval strings, use with \sprintf($map[intType], $interval) | ||||
|         $intervalStringMap = [ | ||||
|             'minutes' => 'PT%dM', | ||||
|             'hours'   => 'PT%dH', | ||||
|             'days'    => 'P%dD', | ||||
|             'months'  => 'P%dM', | ||||
|             'years'   => 'P%dY', | ||||
|         ]; | ||||
|  | ||||
|         $ruleType        = $executionRules['rule-type']; | ||||
|         $ruleClass       = strpos($ruleType, 'interval') === 0 ? 'interval' : $ruleType; | ||||
|         $buildExpression = ''; | ||||
|  | ||||
|         if ($ruleClass === 'interval') { | ||||
|             // Rule type for intervals interval-<minute/hours/...> | ||||
|             $intervalType    = explode('-', $ruleType)[1]; | ||||
|             $interval        = $executionRules["interval-$intervalType"]; | ||||
|             $buildExpression = \sprintf($intervalStringMap[$intervalType], $interval); | ||||
|         } | ||||
|  | ||||
|         if ($ruleClass === 'cron-expression') { | ||||
|             // ! custom matches are disabled in the form | ||||
|             $matches         = $executionRules['cron-expression']; | ||||
|             $buildExpression .= $this->wildcardIfMatch($matches['minutes'], range(0, 59), true); | ||||
|             $buildExpression .= ' ' . $this->wildcardIfMatch($matches['hours'], range(0, 23), true); | ||||
|             $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_month'], range(1, 31), true); | ||||
|             $buildExpression .= ' ' . $this->wildcardIfMatch($matches['months'], range(1, 12), true); | ||||
|             $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_week'], range(0, 6), true); | ||||
|         } | ||||
|  | ||||
|         return [ | ||||
|             'type' => $ruleClass, | ||||
|             'exp'  => $buildExpression, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This method releases "locks" on a set of tasks from the database. | ||||
|      * These locks are pseudo-locks that are used to keep a track of running tasks. However, they require require manual | ||||
|      * intervention to release these locks in cases such as when a task process crashes, leaving the task "locked". | ||||
|      * | ||||
|      * @param   array  $pks  A list of the primary keys to unlock. | ||||
|      * | ||||
|      * @return  boolean  True on success. | ||||
|      * | ||||
|      * @since   4.1.0 | ||||
|      * @throws \RuntimeException|\UnexpectedValueException|\BadMethodCallException | ||||
|      */ | ||||
|     public function unlock(array &$pks): bool | ||||
|     { | ||||
|         /** @var TaskTable $table */ | ||||
|         $table = $this->getTable(); | ||||
|  | ||||
|         $user = $this->getCurrentUser(); | ||||
|  | ||||
|         $context = $this->option . '.' . $this->name; | ||||
|  | ||||
|         // Include the plugins for the change of state event. | ||||
|         PluginHelper::importPlugin($this->events_map['unlock']); | ||||
|  | ||||
|         // Access checks. | ||||
|         foreach ($pks as $i => $pk) { | ||||
|             $table->reset(); | ||||
|  | ||||
|             if ($table->load($pk)) { | ||||
|                 if (!$this->canEditState($table)) { | ||||
|                     // Prune items that you can't change. | ||||
|                     unset($pks[$i]); | ||||
|                     Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); | ||||
|  | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 // Prune items that are already at the given state. | ||||
|                 $lockedColumnName = $table->getColumnAlias('locked'); | ||||
|  | ||||
|                 if (property_exists($table, $lockedColumnName) && \is_null($table->$lockedColumnName)) { | ||||
|                     unset($pks[$i]); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Check if there are items to change. | ||||
|         if (!\count($pks)) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         $event = AbstractEvent::create( | ||||
|             $this->event_before_unlock, | ||||
|             [ | ||||
|                 'subject' => $this, | ||||
|                 'context' => $context, | ||||
|                 'pks'     => $pks, | ||||
|             ] | ||||
|         ); | ||||
|  | ||||
|         try { | ||||
|             Factory::getApplication()->getDispatcher()->dispatch($this->event_before_unlock, $event); | ||||
|         } catch (\RuntimeException $e) { | ||||
|             $this->setError($e->getMessage()); | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // Attempt to unlock the records. | ||||
|         if (!$table->unlock($pks, $user->id)) { | ||||
|             $this->setError($table->getError()); | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // Trigger the after unlock event | ||||
|         $event = AbstractEvent::create( | ||||
|             $this->event_unlock, | ||||
|             [ | ||||
|                 'subject' => $this, | ||||
|                 'context' => $context, | ||||
|                 'pks'     => $pks, | ||||
|             ] | ||||
|         ); | ||||
|  | ||||
|         try { | ||||
|             Factory::getApplication()->getDispatcher()->dispatch($this->event_unlock, $event); | ||||
|         } catch (\RuntimeException $e) { | ||||
|             $this->setError($e->getMessage()); | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // Clear the component's cache | ||||
|         $this->cleanCache(); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Determine if an array is populated by all its possible values by comparison to a reference array, if found a | ||||
|      * match a wildcard '*' is returned. | ||||
|      * | ||||
|      * @param   array  $target       The target array | ||||
|      * @param   array  $reference    The reference array, populated by the complete set of possible values in $target | ||||
|      * @param   bool   $targetToInt  If true, converts $target array values to integers before comparing | ||||
|      * | ||||
|      * @return string  A wildcard string if $target is fully populated, else $target itself. | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     private function wildcardIfMatch(array $target, array $reference, bool $targetToInt = false): string | ||||
|     { | ||||
|         if ($targetToInt) { | ||||
|             $target = array_map( | ||||
|                 static function (string $x): int { | ||||
|                     return (int) $x; | ||||
|                 }, | ||||
|                 $target | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         $isMatch = array_diff($reference, $target) === []; | ||||
|  | ||||
|         return $isMatch ? "*" : implode(',', $target); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Method to allow derived classes to preprocess the form. | ||||
|      * | ||||
|      * @param   Form    $form   A Form object. | ||||
|      * @param   mixed   $data   The data expected for the form. | ||||
|      * @param   string  $group  The name of the plugin group to import (defaults to "content"). | ||||
|      * | ||||
|      * @return  void | ||||
|      * | ||||
|      * @since   4.1.0 | ||||
|      * @throws  \Exception if there is an error in the form event. | ||||
|      */ | ||||
|     protected function preprocessForm(Form $form, $data, $group = 'content'): void | ||||
|     { | ||||
|         // Load the 'task' plugin group | ||||
|         PluginHelper::importPlugin('task'); | ||||
|  | ||||
|         // Let the parent method take over | ||||
|         parent::preprocessForm($form, $data, $group); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user