primo commit
This commit is contained in:
		| @ -0,0 +1,65 @@ | ||||
| <?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\Factory; | ||||
| use Joomla\CMS\MVC\Factory\MVCFactoryInterface; | ||||
| use Joomla\CMS\MVC\Model\ListModel; | ||||
| use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper; | ||||
| use Joomla\Component\Scheduler\Administrator\Task\TaskOption; | ||||
|  | ||||
| // phpcs:disable PSR1.Files.SideEffects | ||||
| \defined('_JEXEC') or die; | ||||
| // phpcs:enable PSR1.Files.SideEffects | ||||
|  | ||||
| /** | ||||
|  * The MVC Model for SelectView. | ||||
|  * | ||||
|  * @since  4.1.0 | ||||
|  */ | ||||
| class SelectModel extends ListModel | ||||
| { | ||||
|     /** | ||||
|      * The Application object, due removal. | ||||
|      * | ||||
|      * @var AdministratorApplication | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     protected $app; | ||||
|  | ||||
|     /** | ||||
|      * SelectModel constructor. | ||||
|      * | ||||
|      * @param   array                 $config   An array of configuration options (name, state, dbo, table_path, ignore_request). | ||||
|      * @param   ?MVCFactoryInterface  $factory  The factory. | ||||
|      * | ||||
|      * @throws \Exception | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     public function __construct($config = [], ?MVCFactoryInterface $factory = null) | ||||
|     { | ||||
|         $this->app = Factory::getApplication(); | ||||
|  | ||||
|         parent::__construct($config, $factory); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return TaskOption[]  An array of TaskOption objects | ||||
|      * | ||||
|      * @throws \Exception | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     public function getItems(): array | ||||
|     { | ||||
|         return SchedulerHelper::getTaskOptions()->options; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										481
									
								
								administrator/components/com_scheduler/src/Model/TasksModel.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										481
									
								
								administrator/components/com_scheduler/src/Model/TasksModel.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,481 @@ | ||||
| <?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\Component\ComponentHelper; | ||||
| use Joomla\CMS\Date\Date; | ||||
| use Joomla\CMS\Factory; | ||||
| use Joomla\CMS\Language\Text; | ||||
| use Joomla\CMS\MVC\Factory\MVCFactoryInterface; | ||||
| use Joomla\CMS\MVC\Model\ListModel; | ||||
| use Joomla\CMS\Object\CMSObject; | ||||
| use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper; | ||||
| use Joomla\Component\Scheduler\Administrator\Task\TaskOption; | ||||
| use Joomla\Database\ParameterType; | ||||
| use Joomla\Database\QueryInterface; | ||||
| use Joomla\Utilities\ArrayHelper; | ||||
|  | ||||
| // phpcs:disable PSR1.Files.SideEffects | ||||
| \defined('_JEXEC') or die; | ||||
| // phpcs:enable PSR1.Files.SideEffects | ||||
|  | ||||
| /** | ||||
|  * The MVC Model for TasksView. | ||||
|  * Defines methods to deal with operations concerning multiple `#__scheduler_tasks` entries. | ||||
|  * | ||||
|  * @since  4.1.0 | ||||
|  */ | ||||
| class TasksModel extends ListModel | ||||
| { | ||||
|     /** | ||||
|      * Constructor. | ||||
|      * | ||||
|      * @param   array                 $config   An optional associative array of configuration settings. | ||||
|      * @param   ?MVCFactoryInterface  $factory  The factory. | ||||
|      * | ||||
|      * @since   4.1.0 | ||||
|      * @throws  \Exception | ||||
|      * @see     \JControllerLegacy | ||||
|      */ | ||||
|     public function __construct($config = [], ?MVCFactoryInterface $factory = null) | ||||
|     { | ||||
|         if (empty($config['filter_fields'])) { | ||||
|             $config['filter_fields'] = [ | ||||
|                 'id', 'a.id', | ||||
|                 'asset_id', 'a.asset_id', | ||||
|                 'title', 'a.title', | ||||
|                 'type', 'a.type', | ||||
|                 'type_title', 'j.type_title', | ||||
|                 'state', 'a.state', | ||||
|                 'last_exit_code', 'a.last_exit_code', | ||||
|                 'last_execution', 'a.last_execution', | ||||
|                 'next_execution', 'a.next_execution', | ||||
|                 'times_executed', 'a.times_executed', | ||||
|                 'times_failed', 'a.times_failed', | ||||
|                 'ordering', 'a.ordering', | ||||
|                 'priority', 'a.priority', | ||||
|                 'note', 'a.note', | ||||
|                 'created', 'a.created', | ||||
|                 'created_by', 'a.created_by', | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         parent::__construct($config, $factory); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Method to get a store id based on model configuration state. | ||||
|      * | ||||
|      * This is necessary because the model is used by the component and | ||||
|      * different modules that might need different sets of data or different | ||||
|      * ordering requirements. | ||||
|      * | ||||
|      * @param   string  $id  A prefix for the store id. | ||||
|      * | ||||
|      * @return string  A store id. | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     protected function getStoreId($id = ''): string | ||||
|     { | ||||
|         // Compile the store id. | ||||
|         $id .= ':' . $this->getState('filter.search'); | ||||
|         $id .= ':' . $this->getState('filter.state'); | ||||
|         $id .= ':' . $this->getState('filter.type'); | ||||
|         $id .= ':' . $this->getState('filter.orphaned'); | ||||
|         $id .= ':' . $this->getState('filter.due'); | ||||
|         $id .= ':' . $this->getState('filter.locked'); | ||||
|         $id .= ':' . $this->getState('filter.trigger'); | ||||
|         $id .= ':' . $this->getState('list.select'); | ||||
|  | ||||
|         return parent::getStoreId($id); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Method to create a query for a list of items. | ||||
|      * | ||||
|      * @return  QueryInterface | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     protected function getListQuery(): QueryInterface | ||||
|     { | ||||
|         // Create a new query object. | ||||
|         $db    = $this->getDatabase(); | ||||
|         $query = $db->getQuery(true); | ||||
|  | ||||
|         /** | ||||
|          * Select the required fields from the table. | ||||
|          * ? Do we need all these defaults ? | ||||
|          * ? Does 'list.select' exist ? | ||||
|          */ | ||||
|         $query->select( | ||||
|             $this->getState( | ||||
|                 'list.select', | ||||
|                 [ | ||||
|                     $db->quoteName('a.id'), | ||||
|                     $db->quoteName('a.asset_id'), | ||||
|                     $db->quoteName('a.title'), | ||||
|                     $db->quoteName('a.type'), | ||||
|                     $db->quoteName('a.execution_rules'), | ||||
|                     $db->quoteName('a.state'), | ||||
|                     $db->quoteName('a.last_exit_code'), | ||||
|                     $db->quoteName('a.locked'), | ||||
|                     $db->quoteName('a.last_execution'), | ||||
|                     $db->quoteName('a.next_execution'), | ||||
|                     $db->quoteName('a.times_executed'), | ||||
|                     $db->quoteName('a.times_failed'), | ||||
|                     $db->quoteName('a.priority'), | ||||
|                     $db->quoteName('a.ordering'), | ||||
|                     $db->quoteName('a.note'), | ||||
|                     $db->quoteName('a.checked_out'), | ||||
|                     $db->quoteName('a.checked_out_time'), | ||||
|                 ] | ||||
|             ) | ||||
|         ) | ||||
|             ->select( | ||||
|                 [ | ||||
|                     $db->quoteName('uc.name', 'editor'), | ||||
|                 ] | ||||
|             ) | ||||
|             ->from($db->quoteName('#__scheduler_tasks', 'a')) | ||||
|             ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); | ||||
|  | ||||
|         // Filters go below | ||||
|         $filterCount = 0; | ||||
|  | ||||
|         /** | ||||
|          * Extends query if already filtered. | ||||
|          * | ||||
|          * @param   string  $outerGlue | ||||
|          * @param   array   $conditions | ||||
|          * @param   string  $innerGlue | ||||
|          * | ||||
|          * @since  4.1.0 | ||||
|          */ | ||||
|         $extendWhereIfFiltered = static function ( | ||||
|             string $outerGlue, | ||||
|             array $conditions, | ||||
|             string $innerGlue | ||||
|         ) use ( | ||||
|             $query, | ||||
|             &$filterCount | ||||
|         ) { | ||||
|             if ($filterCount++) { | ||||
|                 $query->extendWhere($outerGlue, $conditions, $innerGlue); | ||||
|             } else { | ||||
|                 $query->where($conditions, $innerGlue); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         // Filter over ID, title (redundant to search, but) --- | ||||
|         if (is_numeric($id = $this->getState('filter.id'))) { | ||||
|             $filterCount++; | ||||
|             $id = (int) $id; | ||||
|             $query->where($db->quoteName('a.id') . ' = :id') | ||||
|                 ->bind(':id', $id, ParameterType::INTEGER); | ||||
|         } elseif ($title = $this->getState('filter.title')) { | ||||
|             $filterCount++; | ||||
|             $match = "%$title%"; | ||||
|             $query->where($db->quoteName('a.title') . ' LIKE :match') | ||||
|                 ->bind(':match', $match); | ||||
|         } | ||||
|  | ||||
|         // Filter orphaned (-1: exclude, 0: include, 1: only) ---- | ||||
|         $filterOrphaned = (int) $this->getState('filter.orphaned'); | ||||
|  | ||||
|         if ($filterOrphaned !== 0) { | ||||
|             $filterCount++; | ||||
|             $taskOptions = SchedulerHelper::getTaskOptions(); | ||||
|  | ||||
|             // Array of all active routine ids | ||||
|             $activeRoutines = array_map( | ||||
|                 static function (TaskOption $taskOption): string { | ||||
|                     return $taskOption->id; | ||||
|                 }, | ||||
|                 $taskOptions->options | ||||
|             ); | ||||
|  | ||||
|             if ($filterOrphaned === -1) { | ||||
|                 $query->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); | ||||
|             } else { | ||||
|                 $query->whereNotIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Filter over state ---- | ||||
|         $state = $this->getState('filter.state'); | ||||
|  | ||||
|         if ($state !== '*') { | ||||
|             $filterCount++; | ||||
|  | ||||
|             if (is_numeric($state)) { | ||||
|                 $state = (int) $state; | ||||
|  | ||||
|                 $query->where($db->quoteName('a.state') . ' = :state') | ||||
|                     ->bind(':state', $state, ParameterType::INTEGER); | ||||
|             } else { | ||||
|                 $query->whereIn($db->quoteName('a.state'), [0, 1]); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Filter over type ---- | ||||
|         $typeFilter = $this->getState('filter.type'); | ||||
|  | ||||
|         if ($typeFilter) { | ||||
|             $filterCount++; | ||||
|             $query->where($db->quotename('a.type') . '= :type') | ||||
|                 ->bind(':type', $typeFilter); | ||||
|         } | ||||
|  | ||||
|         // Filter over exit code ---- | ||||
|         $exitCode = $this->getState('filter.last_exit_code'); | ||||
|  | ||||
|         if (is_numeric($exitCode)) { | ||||
|             $filterCount++; | ||||
|             $exitCode = (int) $exitCode; | ||||
|             $query->where($db->quoteName('a.last_exit_code') . '= :last_exit_code') | ||||
|                 ->bind(':last_exit_code', $exitCode, ParameterType::INTEGER); | ||||
|         } | ||||
|  | ||||
|         // Filter due (-1: exclude, 0: include, 1: only) ---- | ||||
|         $due = $this->getState('filter.due'); | ||||
|  | ||||
|         if (is_numeric($due) && $due != 0) { | ||||
|             $now      = Factory::getDate('now', 'GMT')->toSql(); | ||||
|             $operator = $due == 1 ? ' <= ' : ' > '; | ||||
|             $filterCount++; | ||||
|             $query->where($db->quoteName('a.next_execution') . $operator . ':now') | ||||
|                 ->bind(':now', $now); | ||||
|         } | ||||
|  | ||||
|         /* | ||||
|          * Filter locked --- | ||||
|          * Locks can be either hard locks or soft locks. Locks that have expired (exceeded the task timeout) are soft | ||||
|          * locks. Hard-locked tasks are assumed to be running. Soft-locked tasks are assumed to have suffered a fatal | ||||
|          * failure. | ||||
|          * {-2: exclude-all, -1: exclude-hard-locked, 0: include, 1: include-only-locked, 2: include-only-soft-locked} | ||||
|          */ | ||||
|         $locked = $this->getState('filter.locked'); | ||||
|  | ||||
|         if (is_numeric($locked) && $locked != 0) { | ||||
|             $now              = Factory::getDate('now', 'GMT'); | ||||
|             $timeout          = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); | ||||
|             $timeout          = new \DateInterval(\sprintf('PT%dS', $timeout)); | ||||
|             $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); | ||||
|             $now              = $now->toSql(); | ||||
|  | ||||
|             switch ($locked) { | ||||
|                 case -2: | ||||
|                     $query->where($db->quoteName('a.locked') . 'IS NULL'); | ||||
|                     break; | ||||
|                 case -1: | ||||
|                     $extendWhereIfFiltered( | ||||
|                         'AND', | ||||
|                         [ | ||||
|                             $db->quoteName('a.locked') . ' IS NULL', | ||||
|                             $db->quoteName('a.locked') . ' < :threshold', | ||||
|                         ], | ||||
|                         'OR' | ||||
|                     ); | ||||
|                     $query->bind(':threshold', $timeoutThreshold); | ||||
|                     break; | ||||
|                 case 1: | ||||
|                     $query->where($db->quoteName('a.locked') . ' IS NOT NULL'); | ||||
|                     break; | ||||
|                 case 2: | ||||
|                     $query->where($db->quoteName('a.locked') . ' < :threshold') | ||||
|                         ->bind(':threshold', $timeoutThreshold); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Filter over search string if set (title, type title, note, id) ---- | ||||
|         $searchStr = $this->getState('filter.search'); | ||||
|  | ||||
|         if (!empty($searchStr)) { | ||||
|             // Allow search by ID | ||||
|             if (stripos($searchStr, 'id:') === 0) { | ||||
|                 // Add array support [?] | ||||
|                 $id = (int) substr($searchStr, 3); | ||||
|                 $query->where($db->quoteName('a.id') . '= :id') | ||||
|                     ->bind(':id', $id, ParameterType::INTEGER); | ||||
|             } elseif (stripos($searchStr, 'type:') !== 0) { | ||||
|                 // Search by type is handled exceptionally in _getList() [@todo: remove refs] | ||||
|                 $searchStr = "%$searchStr%"; | ||||
|  | ||||
|                 // Bind keys to query | ||||
|                 $query->bind(':title', $searchStr) | ||||
|                     ->bind(':note', $searchStr); | ||||
|                 $conditions = [ | ||||
|                     $db->quoteName('a.title') . ' LIKE :title', | ||||
|                     $db->quoteName('a.note') . ' LIKE :note', | ||||
|                 ]; | ||||
|                 $extendWhereIfFiltered('AND', $conditions, 'OR'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Add list ordering clause. ---- | ||||
|         // @todo implement multi-column ordering someway | ||||
|         $multiOrdering = $this->state->get('list.multi_ordering'); | ||||
|  | ||||
|         if (!$multiOrdering || !\is_array($multiOrdering)) { | ||||
|             $orderCol = $this->state->get('list.ordering', 'a.next_execution'); | ||||
|             $orderDir = $this->state->get('list.direction', 'asc'); | ||||
|  | ||||
|             // Type title ordering is handled exceptionally in _getList() | ||||
|             if ($orderCol !== 'j.type_title') { | ||||
|                 $query->order($db->quoteName($orderCol) . ' ' . $orderDir); | ||||
|  | ||||
|                 // If ordering by type or state, also order by title. | ||||
|                 if (\in_array($orderCol, ['a.type', 'a.state', 'a.priority'])) { | ||||
|                     // @todo : Test if things are working as expected | ||||
|                     $query->order($db->quoteName('a.title') . ' ' . $orderDir); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             // @todo Should add quoting here | ||||
|             $query->order($multiOrdering); | ||||
|         } | ||||
|  | ||||
|         return $query; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Overloads the parent _getList() method. | ||||
|      * Takes care of attaching TaskOption objects and sorting by type titles. | ||||
|      * | ||||
|      * @param   QueryInterface  $query       The database query to get the list with | ||||
|      * @param   int             $limitstart  The list offset | ||||
|      * @param   int             $limit       Number of list items to fetch | ||||
|      * | ||||
|      * @return object[] | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     protected function _getList($query, $limitstart = 0, $limit = 0): array | ||||
|     { | ||||
|         // Get stuff from the model state | ||||
|         $listOrder      = $this->getState('list.ordering', 'a.next_execution'); | ||||
|         $listDirectionN = strtolower($this->getState('list.direction', 'asc')) === 'desc' ? -1 : 1; | ||||
|  | ||||
|         // Set limit parameters and get object list | ||||
|         $query->setLimit($limit, $limitstart); | ||||
|         $this->getDatabase()->setQuery($query); | ||||
|  | ||||
|         // Return optionally an extended class. | ||||
|         // @todo: Use something other than CMSObject.. | ||||
|         if ($this->getState('list.customClass')) { | ||||
|             $responseList = array_map( | ||||
|                 static function (array $arr) { | ||||
|                     $o = new CMSObject(); | ||||
|  | ||||
|                     foreach ($arr as $k => $v) { | ||||
|                         $o->{$k} = $v; | ||||
|                     } | ||||
|  | ||||
|                     return $o; | ||||
|                 }, | ||||
|                 $this->getDatabase()->loadAssocList() ?: [] | ||||
|             ); | ||||
|         } else { | ||||
|             $responseList = $this->getDatabase()->loadObjectList(); | ||||
|         } | ||||
|  | ||||
|         // Attach TaskOptions objects and a safe type title | ||||
|         $this->attachTaskOptions($responseList); | ||||
|  | ||||
|         // If ordering by non-db fields, we need to sort here in code | ||||
|         if ($listOrder === 'j.type_title') { | ||||
|             $responseList = ArrayHelper::sortObjects($responseList, 'safeTypeTitle', $listDirectionN, true, false); | ||||
|         } | ||||
|  | ||||
|         return $responseList; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * For an array of items, attaches TaskOption objects and (safe) type titles to each. | ||||
|      * | ||||
|      * @param   array  $items  Array of items, passed by reference | ||||
|      * | ||||
|      * @return void | ||||
|      * | ||||
|      * @since  4.1.0 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     private function attachTaskOptions(array $items): void | ||||
|     { | ||||
|         $taskOptions = SchedulerHelper::getTaskOptions(); | ||||
|  | ||||
|         foreach ($items as $item) { | ||||
|             $item->taskOption    = $taskOptions->findOption($item->type); | ||||
|             $item->safeTypeTitle = $item->taskOption->title ?? Text::_('JGLOBAL_NONAPPLICABLE'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Proxy for the parent method. | ||||
|      * Sets ordering defaults. | ||||
|      * | ||||
|      * @param   string  $ordering   Field to order/sort list by | ||||
|      * @param   string  $direction  Direction in which to sort list | ||||
|      * | ||||
|      * @return void | ||||
|      * @since  4.1.0 | ||||
|      */ | ||||
|     protected function populateState($ordering = 'a.next_execution', $direction = 'ASC'): void | ||||
|     { | ||||
|         // Call the parent method | ||||
|         parent::populateState($ordering, $direction); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if we have any enabled due tasks and no locked tasks. | ||||
|      * | ||||
|      * @param   Date  $time  The next execution time to check against | ||||
|      * | ||||
|      * @return boolean | ||||
|      * @since  4.4.0 | ||||
|      */ | ||||
|     public function hasDueTasks(Date $time): bool | ||||
|     { | ||||
|         $db  = $this->getDatabase(); | ||||
|         $now = $time->toSql(); | ||||
|  | ||||
|         $query = $db->getQuery(true) | ||||
|             // Count due tasks | ||||
|             ->select('SUM(CASE WHEN ' . $db->quoteName('a.next_execution') . ' <= :now THEN 1 ELSE 0 END) AS due_count') | ||||
|             // Count locked tasks | ||||
|             ->select('SUM(CASE WHEN ' . $db->quoteName('a.locked') . ' IS NULL THEN 0 ELSE 1 END) AS locked_count') | ||||
|             ->from($db->quoteName('#__scheduler_tasks', 'a')) | ||||
|             ->where($db->quoteName('a.state') . ' = 1') | ||||
|             ->bind(':now', $now); | ||||
|  | ||||
|         $db->setQuery($query); | ||||
|  | ||||
|         $taskDetails = $db->loadObject(); | ||||
|  | ||||
|         // False if we don't have due tasks, or we have locked tasks | ||||
|         return $taskDetails && $taskDetails->due_count && !$taskDetails->locked_count; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if we have right now any enabled due tasks and no locked tasks. | ||||
|      * | ||||
|      * @return boolean | ||||
|      * @since  5.2.0 | ||||
|      */ | ||||
|     public function getHasDueTasks() | ||||
|     { | ||||
|         return $this->hasDueTasks(Factory::getDate('now', 'UTC')); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user