first 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;
|
||||
}
|
||||
}
|
||||
797
administrator/components/com_scheduler/src/Model/TaskModel.php
Normal file
797
administrator/components/com_scheduler/src/Model/TaskModel.php
Normal file
@ -0,0 +1,797 @@
|
||||
<?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|null $factory The factory
|
||||
* @param FormFactoryInterface|null $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 JForm 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');
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
$this->configureTaskGetterOptions($resolver);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$options = $resolver->resolve($options);
|
||||
} catch (\Exception $e) {
|
||||
if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
$db = $this->getDatabase();
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
// Get lock on the table to help with concurrency issues
|
||||
$db->lockTable(self::TASK_TABLE);
|
||||
|
||||
// If concurrency is not allowed, we only get a task if another one does not have a "lock"
|
||||
if (!$options['allowConcurrent']) {
|
||||
// Get count of locked (presumed running) tasks
|
||||
$lockCountQuery = $db->getQuery(true)
|
||||
->from($db->quoteName(self::TASK_TABLE))
|
||||
->select('COUNT(id)')
|
||||
->where($db->quoteName('locked') . ' IS NOT NULL');
|
||||
|
||||
try {
|
||||
$runningCount = $db->setQuery($lockCountQuery)->loadResult();
|
||||
} catch (\RuntimeException $e) {
|
||||
$db->unlockTables();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($runningCount !== 0) {
|
||||
$db->unlockTables();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$lockQuery = $db->getQuery(true);
|
||||
|
||||
$lockQuery->update($db->quoteName(self::TASK_TABLE))
|
||||
->set($db->quoteName('locked') . ' = :now1')
|
||||
->bind(':now1', $now);
|
||||
|
||||
// Array of all active routine ids
|
||||
$activeRoutines = array_map(
|
||||
static function (TaskOption $taskOption): string {
|
||||
return $taskOption->id;
|
||||
},
|
||||
SchedulerHelper::getTaskOptions()->options
|
||||
);
|
||||
|
||||
// "Orphaned" tasks are not a part of the task queue!
|
||||
$lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING);
|
||||
|
||||
// If directed, exclude CLI exclusive tasks
|
||||
if (!$options['includeCliExclusive']) {
|
||||
$lockQuery->where($db->quoteName('cli_exclusive') . ' = 0');
|
||||
}
|
||||
|
||||
if (!$options['bypassScheduling']) {
|
||||
$lockQuery->where($db->quoteName('next_execution') . ' <= :now2')
|
||||
->bind(':now2', $now);
|
||||
}
|
||||
|
||||
if ($options['allowDisabled']) {
|
||||
$lockQuery->whereIn($db->quoteName('state'), [0, 1]);
|
||||
} else {
|
||||
$lockQuery->where($db->quoteName('state') . ' = 1');
|
||||
}
|
||||
|
||||
if ($options['id'] > 0) {
|
||||
$lockQuery->where($db->quoteName('id') . ' = :taskId')
|
||||
->bind(':taskId', $options['id'], ParameterType::INTEGER);
|
||||
} else {
|
||||
// Pick from the front of the task queue if no 'id' is specified
|
||||
// Get the id of the next task in the task queue
|
||||
$idQuery = $db->getQuery(true)
|
||||
->from($db->quoteName(self::TASK_TABLE))
|
||||
->select($db->quoteName('id'))
|
||||
->where($db->quoteName('state') . ' = 1')
|
||||
->order($db->quoteName('priority') . ' DESC')
|
||||
->order($db->quoteName('next_execution') . ' ASC')
|
||||
->setLimit(1);
|
||||
|
||||
try {
|
||||
$ids = $db->setQuery($idQuery)->loadColumn();
|
||||
} catch (\RuntimeException $e) {
|
||||
$db->unlockTables();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\count($ids) === 0) {
|
||||
$db->unlockTables();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$lockQuery->whereIn($db->quoteName('id'), $ids);
|
||||
}
|
||||
|
||||
try {
|
||||
$db->setQuery($lockQuery)->execute();
|
||||
} catch (\RuntimeException $e) {
|
||||
} finally {
|
||||
$affectedRows = $db->getAffectedRows();
|
||||
|
||||
$db->unlockTables();
|
||||
}
|
||||
|
||||
if ($affectedRows != 1) {
|
||||
/*
|
||||
// @todo
|
||||
// ? Fatal failure handling here?
|
||||
// ! Question is, how? If we check for tasks running beyond there time here, we have no way of
|
||||
// ! what's already been notified (since we're not auto-unlocking/recovering tasks anymore).
|
||||
// The solution __may__ be in a "last_successful_finish" (or something) column.
|
||||
*/
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$getQuery = $db->getQuery(true);
|
||||
|
||||
$getQuery->select('*')
|
||||
->from($db->quoteName(self::TASK_TABLE))
|
||||
->where($db->quoteName('locked') . ' = :now')
|
||||
->bind(':now', $now);
|
||||
|
||||
$task = $db->setQuery($getQuery)->loadObject();
|
||||
|
||||
$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->get($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);
|
||||
}
|
||||
}
|
||||
471
administrator/components/com_scheduler/src/Model/TasksModel.php
Normal file
471
administrator/components/com_scheduler/src/Model/TasksModel.php
Normal file
@ -0,0 +1,471 @@
|
||||
<?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\DatabaseQuery;
|
||||
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|null $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 DatabaseQuery
|
||||
*
|
||||
* @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.title');
|
||||
$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 DatabaseQuery $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.title');
|
||||
$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.title', $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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user