File manager - Edit - /home/opticamezl/www/newok/com_scheduler.tar
Back
src/Field/ExecutionRuleField.php 0000644 00000002440 15172736354 0012643 0 ustar 00 <?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\Field; use Joomla\CMS\Form\Field\PredefinedlistField; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * A select list containing valid Cron interval types. * * @since 4.1.0 */ class ExecutionRuleField extends PredefinedlistField { /** * The form field type. * * @var string * @since 4.1.0 */ protected $type = 'ExecutionRule'; /** * Available execution rules. * * @var string[] * @since 4.1.0 */ protected $predefinedOptions = [ 'interval-minutes' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES', 'interval-hours' => 'COM_SCHEDULER_EXECUTION_INTERVAL_HOURS', 'interval-days' => 'COM_SCHEDULER_EXECUTION_INTERVAL_DAYS', 'interval-months' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS', 'cron-expression' => 'COM_SCHEDULER_EXECUTION_CRON_EXPRESSION', 'manual' => 'COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL', ]; } src/Field/TaskTypeField.php 0000644 00000003361 15172736354 0011617 0 ustar 00 <?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\Field; use Joomla\CMS\Form\Field\ListField; use Joomla\CMS\HTML\HTMLHelper; use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper; use Joomla\Component\Scheduler\Administrator\Task\TaskOption; use Joomla\Utilities\ArrayHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * A list field with all available task routines. * * @since 4.1.0 */ class TaskTypeField extends ListField { /** * The form field type. * * @var string * @since 4.1.0 */ protected $type = 'taskType'; /** * Method to get field options * * @return array * * @since 4.1.0 * @throws \Exception */ protected function getOptions(): array { $options = parent::getOptions(); // Get all available task types and sort by title $types = ArrayHelper::sortObjects( SchedulerHelper::getTaskOptions()->options, 'title', 1 ); // Closure to add a TaskOption as a <select> option in $options: array $addTypeAsOption = function (TaskOption $type) use (&$options) { try { $options[] = HTMLHelper::_('select.option', $type->id, $type->title); } catch (\InvalidArgumentException $e) { } }; // Call $addTypeAsOption on each type array_map($addTypeAsOption, $types); return $options; } } src/Field/CronField.php 0000644 00000012275 15172736354 0010760 0 ustar 00 <?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\Field; use Joomla\CMS\Form\Field\ListField; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Multi-select form field, supporting inputs of: * minutes, hours, days of week, days of month and months. * * @since 4.1.0 */ class CronField extends ListField { /** * The subtypes supported by this field type. * * @var string[] * * @since 4.1.0 */ private const SUBTYPES = [ 'minutes', 'hours', 'days_month', 'months', 'days_week', ]; /** * Count of predefined options for each subtype * * @var int[][] * * @since 4.1.0 */ private const OPTIONS_RANGE = [ 'minutes' => [0, 59], 'hours' => [0, 23], 'days_week' => [1, 7], 'days_month' => [1, 31], 'months' => [1, 12], ]; /** * Response labels for the 'month' and 'days_week' subtypes. * The labels are language constants translated when needed. * * @var string[][] * @since 4.1.0 */ private const PREPARED_RESPONSE_LABELS = [ 'months' => [ 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', ], 'days_week' => [ 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY', ], ]; /** * The form field type. * * @var string * * @since 4.1.0 */ protected $type = 'cronIntervals'; /** * The subtype of the CronIntervals field * * @var string * @since 4.1.0 */ private $subtype; /** * If true, field options will include a wildcard * * @var boolean * @since 4.1.0 */ private $wildcard; /** * If true, field will only have numeric labels (for days_week and months) * * @var boolean * @since 4.1.0 */ private $onlyNumericLabels; /** * Override the parent method to set deal with subtypes. * * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `<field>` tag for the form * field object. * @param mixed $value The form field value to validate. * @param string $group The field name group control value. This acts as an array container for * the field. For example if the field has `name="foo"` and the group value is * set to "bar" then the full field name would end up being "bar[foo]". * * @return boolean True on success. * * @since 4.1.0 */ public function setup(\SimpleXMLElement $element, $value, $group = null): bool { $parentResult = parent::setup($element, $value, $group); $subtype = ((string) $element['subtype'] ?? '') ?: null; $wildcard = ((string) $element['wildcard'] ?? '') === 'true'; $onlyNumericLabels = ((string) $element['onlyNumericLabels']) === 'true'; if (!($subtype && \in_array($subtype, self::SUBTYPES))) { return false; } $this->subtype = $subtype; $this->wildcard = $wildcard; $this->onlyNumericLabels = $onlyNumericLabels; return $parentResult; } /** * Method to get field options * * @return array Array of objects representing options in the options list * * @since 4.1.0 */ protected function getOptions(): array { $subtype = $this->subtype; $options = parent::getOptions(); if (!\in_array($subtype, self::SUBTYPES)) { return $options; } if ($this->wildcard) { try { $options[] = HTMLHelper::_('select.option', '*', '*'); } catch (\InvalidArgumentException $e) { } } [$optionLower, $optionUpper] = self::OPTIONS_RANGE[$subtype]; // If we need text labels, we translate them first if (\array_key_exists($subtype, self::PREPARED_RESPONSE_LABELS) && !$this->onlyNumericLabels) { $labels = array_map( static function (string $string): string { return Text::_($string); }, self::PREPARED_RESPONSE_LABELS[$subtype] ); } else { $labels = range(...self::OPTIONS_RANGE[$subtype]); } for ([$i, $l] = [$optionLower, 0]; $i <= $optionUpper; $i++, $l++) { try { $options[] = HTMLHelper::_('select.option', (string) ($i), $labels[$l]); } catch (\InvalidArgumentException $e) { } } return $options; } } src/Field/TaskStateField.php 0000644 00000001725 15172736354 0011760 0 ustar 00 <?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\Field; use Joomla\CMS\Form\Field\PredefinedlistField; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * A predefined list field with all possible states for a com_scheduler entry. * * @since 4.1.0 */ class TaskStateField extends PredefinedlistField { /** * The form field type. * * @var string * @since 4.1.0 */ public $type = 'taskState'; /** * Available states * * @var string[] * @since 4.1.0 */ protected $predefinedOptions = [ -2 => 'JTRASHED', 0 => 'JDISABLED', 1 => 'JENABLED', '*' => 'JALL', ]; } src/Field/WebcronLinkField.php 0000644 00000002561 15172736354 0012271 0 ustar 00 <?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\Field; use Joomla\CMS\Form\Field\TextField; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Field to override the text field layout to add a copy-text button, used in the com_scheduler * configuration form. * This field class is only needed because the layout file is in a non-global directory, so this should * be made redundant and removed if/once the layout is shifted to `JPATH_SITE/layout/` * * @since 4.1.0 */ class WebcronLinkField extends TextField { /** * We use a custom layout that allows for the link to be copied. * * @var string * @since 4.1.0 */ protected $layout = 'form.field.webcron_link'; /** * Override layout paths. * * @inheritDoc * @return string[] * * @since 4.1.0 */ protected function getLayoutPaths(): array { $s = DIRECTORY_SEPARATOR; return array_merge( [JPATH_ADMINISTRATOR . "{$s}/components{$s}com_scheduler{$s}layouts{$s}"], parent::getLayoutPaths() ); } } src/Field/IntervalField.php 0000644 00000005177 15172736354 0011646 0 ustar 00 <?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\Field; use Joomla\CMS\Form\Field\NumberField; use Joomla\CMS\Form\FormField; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Select style field for interval(s) in minutes, hours, days and months. * * @since 4.1.0 */ class IntervalField extends NumberField { /** * The form field type. * * @var string * @since 4.1.0 */ protected $type = 'Intervals'; /** * The subtypes supported by this field type => [minVal, maxVal] * * @var string[] * @since 4.1.0 */ private const SUBTYPES = [ 'minutes' => [1, 59], 'hours' => [1, 23], 'days' => [1, 30], 'months' => [1, 12], ]; /** * The allowable maximum value of the field. * * @var float * @since 4.1.0 */ protected $max; /** * The allowable minimum value of the field. * * @var float * @since 4.1.0 */ protected $min; /** * The step by which value of the field increased or decreased. * * @var float * @since 4.1.0 */ protected $step = 1; /** * Override the parent method to set deal with subtypes. * * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `<field>` tag for the form * field object. * @param mixed $value The form field value to validate. * @param string $group The field name group control value. This acts as an array container for * the field. For example if the field has `name="foo"` and the group value is * set to "bar" then the full field name would end up being "bar[foo]". * * @return boolean True on success. * * @since 4.1.0 */ public function setup(\SimpleXMLElement $element, $value, $group = null): bool { $parentResult = FormField::setup($element, $value, $group); $subtype = ((string) $element['subtype'] ?? '') ?: null; if (empty($subtype) || !\array_key_exists($subtype, self::SUBTYPES)) { return false; } [$this->min, $this->max] = self::SUBTYPES[$subtype]; return $parentResult; } } src/Extension/SchedulerComponent.php 0000644 00000002434 15172736354 0013641 0 ustar 00 <?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\Extension; use Joomla\CMS\Extension\BootableExtensionInterface; use Joomla\CMS\Extension\MVCComponent; use Joomla\CMS\HTML\HTMLRegistryAwareTrait; use Psr\Container\ContainerInterface; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Component class for com_scheduler. * * @since 4.1.0 * @todo Set up logger(s) here. */ class SchedulerComponent extends MVCComponent implements BootableExtensionInterface { use HTMLRegistryAwareTrait; /** * Booting the extension. This is the function to set up the environment of the extension like * registering new class loaders, etc. * * If required, some initial set up can be done from services of the container, eg. * registering HTML services. * * @param ContainerInterface $container The container * * @return void * * @since 4.1.0 */ public function boot(ContainerInterface $container): void { // Pass } } src/Task/TaskOption.php 0000644 00000006063 15172736354 0011063 0 ustar 00 <?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\Task; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * The TaskOption class is used as a utility container for available plugin-provided task routines. * Each task-supporting plugin calls the {@see TaskOptions::addOptions()} method with an array of TaskOption constructor * argument pairs as argument. Internally, the TaskOption object generates the routine title and description from the * language constant prefix. * * @since 4.1.0 * * @property-read string $desc The routine description. * @property-read string $id The routine ID. * @property-read string $langConstPrefix The routine's language constant prefix. * @property-read string $title The routine title. */ class TaskOption { /** * Task routine title * * @var string * @since 4.1.0 */ protected $title; /** * Task routine description. * * @var string * @since 4.1.0 */ protected $desc; /** * Routine type-ID. * * @var string * @since 4.1.0 */ protected $id; /** * @var string * @since 4.1.0 */ protected $langConstPrefix; /** * TaskOption constructor. * * @param string $type A unique ID string for a plugin task routine. * @param string $langConstPrefix The Language constant prefix $p. Expects $p . _TITLE and $p . _DESC to exist. * * @since 4.1.0 */ public function __construct(string $type, string $langConstPrefix) { $this->id = $type; $this->title = Text::_("{$langConstPrefix}_TITLE"); $this->desc = Text::_("{$langConstPrefix}_DESC"); $this->langConstPrefix = $langConstPrefix; } /** * Magic method to allow read-only access to private properties. * * @param string $name The object property requested. * * @return ?string * * @since 4.1.0 */ public function __get(string $name) { if (property_exists($this, $name)) { return $this->$name; } // Trigger a deprecation for the 'type' property (replaced with {@see id}). if ($name === 'type') { try { Log::add( sprintf( 'The %1$s property is deprecated. Use %2$s instead.', $name, 'id' ), Log::WARNING, 'deprecated' ); } catch (\RuntimeException $e) { // Pass } return $this->id; } return null; } } src/Task/Status.php 0000644 00000005257 15172736354 0010257 0 ustar 00 <?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\Task; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * A namespace mapping Task statuses to integer values. * * @since 4.1.0 */ abstract class Status { /** * Replacement exit code used when a routine returns an invalid (non-integer) exit code. * * @since 4.1.0 */ public const INVALID_EXIT = -2; /** * Replacement exit code used when a routine does not return an exit code. * * @since 4.1.0 */ public const NO_EXIT = -1; /** * Status code used when the routine just starts. This is not meant to be an exit code. * * @since 4.1.0 */ public const RUNNING = 1; /** * Exit code used on failure to acquire a pseudo-lock. * * @since 4.1.0 */ public const NO_LOCK = 2; /** * Exit code used on failure to run the task. * * @since 4.1.0 */ public const NO_RUN = 3; /** * Exit code used on failure to release lock/update the record. * * @since 4.1.0 */ public const NO_RELEASE = 4; /** * Exit code used when a routine is either "knocked out" by an exception or encounters an exception it cannot handle * gracefully. * ? Should this be retained ? * * @since 4.1.0 */ public const KNOCKOUT = 5; /** * Exit code used when a task needs to resume (reschedule it to run a.s.a.p.). * * Use this for long running tasks, e.g. batch processing of hundreds or thousands of files, * sending newsletters with thousands of subscribers etc. These are tasks which might run out of * memory and/or hit a time limit when lazy scheduling or web triggering of tasks is being used. * Split them into smaller batches which return Status::WILL_RESUME. When the last batch is * executed return Status::OK. * * @since 4.1.0 */ public const WILL_RESUME = 123; /** * Exit code used when a task times out. * * @since 4.1.0 */ public const TIMEOUT = 124; /** * Exit code when a *task* does not exist. * * @since 4.1.0 */ public const NO_TASK = 125; /** * Exit code used when a *routine* is missing. * * @since 4.1.0 */ public const NO_ROUTINE = 127; /** * Exit code on success. * * @since 4.1.0 */ public const OK = 0; } src/Task/TaskOptions.php 0000644 00000003636 15172736354 0011251 0 ustar 00 <?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\Task; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * The TaskOptions class. * Used as the subject argument for the `onTaskOptionsList` event, plugins that support tasks must add them to the * object through the addOptions() method. * * @since 4.1.0 */ class TaskOptions { /** * An array of TaskOptions * * @var TaskOption[] * @since 4.1.0 */ public $options = []; /** * A plugin can support several task routines * This method is used by a plugin's onTaskOptionsList subscriber to advertise supported routines. * * @param array $taskRoutines An associative array of {@var TaskOption} constructor argument pairs: * [ 'routineId' => 'languageConstantPrefix', ... ] * * @return void * * @since 4.1.0 */ public function addOptions(array $taskRoutines): void { foreach ($taskRoutines as $routineId => $langConstPrefix) { $this->options[] = new TaskOption($routineId, $langConstPrefix); } } /** * @param ?string $routineId A unique identifier for a plugin task routine * * @return ?TaskOption A matching TaskOption if available, null otherwise * * @since 4.1.0 */ public function findOption(?string $routineId): ?TaskOption { if ($routineId === null) { return null; } foreach ($this->options as $option) { if ($option->id === $routineId) { return $option; } } return null; } } src/Task/Task.php 0000644 00000041740 15172736354 0007673 0 ustar 00 <?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\Task; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Event\AbstractEvent; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Plugin\PluginHelper; use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; use Joomla\Component\Scheduler\Administrator\Helper\ExecRuleHelper; use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper; use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler; use Joomla\Component\Scheduler\Administrator\Table\TaskTable; use Joomla\Database\DatabaseInterface; use Joomla\Database\ParameterType; use Joomla\Registry\Registry; use Joomla\Utilities\ArrayHelper; use Psr\Log\InvalidArgumentException; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * The Task class defines methods for the execution, logging and * related properties of Tasks as supported by `com_scheduler`, * a Task Scheduling component. * * @since 4.1.0 */ class Task implements LoggerAwareInterface { use LoggerAwareTrait; /** * Enumerated state for enabled tasks. * * @since 4.1.0 */ public const STATE_ENABLED = 1; /** * Enumerated state for disabled tasks. * * @since 4.1.0 */ public const STATE_DISABLED = 0; /** * Enumerated state for trashed tasks. * * @since 4.1.0 */ public const STATE_TRASHED = -2; /** * Map state enumerations to logical language adjectives. * * @since 4.1.0 */ public const STATE_MAP = [ self::STATE_TRASHED => 'trashed', self::STATE_DISABLED => 'disabled', self::STATE_ENABLED => 'enabled', ]; /** * The task snapshot * * @var array * @since 4.1.0 */ protected $snapshot = []; /** * @var Registry * @since 4.1.0 */ protected $taskRegistry; /** * @var string * @since 4.1.0 */ public $logCategory; /** * @var CMSApplication * @since 4.1.0 */ protected $app; /** * @var DatabaseInterface * @since 4.1.0 */ protected $db; /** * Maps task exit codes to events which should be dispatched when the task finishes. * 'NA' maps to the event for general task failures. * * @var string[] * @since 4.1.0 */ protected const EVENTS_MAP = [ Status::OK => 'onTaskExecuteSuccess', Status::NO_ROUTINE => 'onTaskRoutineNotFound', Status::WILL_RESUME => 'onTaskRoutineWillResume', 'NA' => 'onTaskExecuteFailure', ]; /** * Constructor for {@see Task}. * * @param object $record A task from {@see TaskTable}. * * @since 4.1.0 * @throws \Exception */ public function __construct(object $record) { // Workaround because Registry dumps private properties otherwise. $taskOption = $record->taskOption; $record->params = json_decode($record->params, true); $this->taskRegistry = new Registry($record); $this->set('taskOption', $taskOption); $this->app = Factory::getApplication(); $this->db = Factory::getContainer()->get(DatabaseInterface::class); $this->setLogger(Log::createDelegatedLogger()); $this->logCategory = 'task' . $this->get('id'); if ($this->get('params.individual_log')) { $logFile = $this->get('params.log_file') ?? 'task_' . $this->get('id') . '.log.php'; $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; $options['text_file'] = $logFile; Log::addLogger($options, Log::ALL, [$this->logCategory]); } } /** * Get the task as a data object that can be stored back in the database. * ! This method should be removed or changed as part of a better API implementation for the driver. * * @return object * * @since 4.1.0 */ public function getRecord(): object { // ! Probably, an array instead $recObject = $this->taskRegistry->toObject(); $recObject->cron_rules = (array) $recObject->cron_rules; return $recObject; } /** * Execute the task. * * @return boolean True if success * * @since 4.1.0 * @throws \Exception */ public function run(): bool { /** * We try to acquire the lock here, only if we don't already have one. * We do this, so we can support two ways of running tasks: * 1. Directly through {@see Scheduler}, which optimises acquiring a lock while fetching from the task queue. * 2. Running a task without a pre-acquired lock. * ! This needs some more thought, for whether it should be allowed or if the single-query optimisation * should be used everywhere, although it doesn't make sense in the context of fetching * a task when it doesn't need to be run. This might be solved if we force a re-fetch * with the lock or do it here ourselves (using acquireLock as a proxy to the model's * getter). */ if ($this->get('locked') === null) { $this->acquireLock(); } // Exit early if task routine is not available if (!SchedulerHelper::getTaskOptions()->findOption($this->get('type'))) { $this->snapshot['status'] = Status::NO_ROUTINE; $this->skipExecution(); $this->dispatchExitEvent(); return $this->isSuccess(); } $this->snapshot['status'] = Status::RUNNING; $this->snapshot['taskStart'] = $this->snapshot['taskStart'] ?? microtime(true); $this->snapshot['netDuration'] = 0; /** @var ExecuteTaskEvent $event */ $event = AbstractEvent::create( 'onExecuteTask', [ 'eventClass' => ExecuteTaskEvent::class, 'subject' => $this, 'routineId' => $this->get('type'), 'langConstPrefix' => $this->get('taskOption')->langConstPrefix, 'params' => $this->get('params'), ] ); PluginHelper::importPlugin('task'); try { $this->app->getDispatcher()->dispatch('onExecuteTask', $event); } catch (\Exception $e) { // Suppress the exception for now, we'll throw it again once it's safe $this->log(Text::sprintf('COM_SCHEDULER_TASK_ROUTINE_EXCEPTION', $e->getMessage()), 'error'); $this->snapshot['exception'] = $e; $this->snapshot['status'] = Status::KNOCKOUT; } $resultSnapshot = $event->getResultSnapshot(); $this->snapshot['taskEnd'] = microtime(true); $this->snapshot['netDuration'] = $this->snapshot['taskEnd'] - $this->snapshot['taskStart']; $this->snapshot = array_merge($this->snapshot, $resultSnapshot); // @todo make the ExecRuleHelper usage less ugly, perhaps it should be composed into Task // Update object state. $this->set('last_execution', Factory::getDate('@' . (int) $this->snapshot['taskStart'])->toSql()); $this->set('last_exit_code', $this->snapshot['status']); if ($this->snapshot['status'] !== Status::WILL_RESUME) { $this->set('next_execution', (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec()); $this->set('times_executed', $this->get('times_executed') + 1); } else { /** * Resumable tasks need special handling. * * They are rescheduled as soon as possible to let their next step to be executed without * a very large temporal gap to the previous step. * * Moreover, the times executed does NOT increase for each step. It will increase once, * after the last step, when they return Status::OK. */ $this->set('next_execution', Factory::getDate('now', 'UTC')->sub(new \DateInterval('PT1M'))->toSql()); } // The only acceptable "successful" statuses are either clean exit or resuming execution. if (!in_array($this->snapshot['status'], [Status::WILL_RESUME, Status::OK])) { $this->set('times_failed', $this->get('times_failed') + 1); } if (!$this->releaseLock()) { $this->snapshot['status'] = Status::NO_RELEASE; } $this->dispatchExitEvent(); if (!empty($this->snapshot['exception'])) { throw $this->snapshot['exception']; } return $this->isSuccess(); } /** * Get the task execution snapshot. * ! Access locations will need updates once a more robust Snapshot container is implemented. * * @return array * * @since 4.1.0 */ public function getContent(): array { return $this->snapshot; } /** * Acquire a pseudo-lock on the task record. * ! At the moment, this method is not used anywhere as task locks are already * acquired when they're fetched. As such this method is not functional and should * not be reviewed until it is updated. * * @return boolean * * @since 4.1.0 * @throws \Exception */ public function acquireLock(): bool { $db = $this->db; $query = $db->getQuery(true); $id = $this->get('id'); $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(); // @todo update or remove this method $query->update($db->quoteName('#__scheduler_tasks')) ->set('locked = :now') ->where($db->quoteName('id') . ' = :taskId') ->extendWhere( 'AND', [ $db->quoteName('locked') . ' < :threshold', $db->quoteName('locked') . 'IS NULL', ], 'OR' ) ->bind(':taskId', $id, ParameterType::INTEGER) ->bind(':now', $now) ->bind(':threshold', $timeoutThreshold); try { $db->lockTable('#__scheduler_tasks'); $db->setQuery($query)->execute(); } catch (\RuntimeException $e) { return false; } finally { $db->unlockTables(); } if ($db->getAffectedRows() === 0) { return false; } $this->set('locked', $now); return true; } /** * Remove the pseudo-lock and optionally update the task record. * * @param bool $update If true, the record is updated with the snapshot * * @return boolean * * @since 4.1.0 * @throws \Exception */ public function releaseLock(bool $update = true): bool { $db = $this->db; $query = $db->getQuery(true); $id = $this->get('id'); $query->update($db->quoteName('#__scheduler_tasks', 't')) ->set('locked = NULL') ->where($db->quoteName('id') . ' = :taskId') ->where($db->quoteName('locked') . ' IS NOT NULL') ->bind(':taskId', $id, ParameterType::INTEGER); if ($update) { $exitCode = $this->get('last_exit_code'); $lastExec = $this->get('last_execution'); $nextExec = $this->get('next_execution'); $timesFailed = $this->get('times_failed'); $timesExecuted = $this->get('times_executed'); $query->set( [ 'last_exit_code = :exitCode', 'last_execution = :lastExec', 'next_execution = :nextExec', 'times_executed = :times_executed', 'times_failed = :times_failed', ] ) ->bind(':exitCode', $exitCode, ParameterType::INTEGER) ->bind(':lastExec', $lastExec) ->bind(':nextExec', $nextExec) ->bind(':times_executed', $timesExecuted) ->bind(':times_failed', $timesFailed); } try { $db->setQuery($query)->execute(); } catch (\RuntimeException $e) { return false; } if (!$db->getAffectedRows()) { return false; } $this->set('locked', null); return true; } /** * @param string $message Log message * @param string $priority Log level, defaults to 'info' * * @return void * * @since 4.1.0 * @throws InvalidArgumentException */ public function log(string $message, string $priority = 'info'): void { $this->logger->log($priority, $message, ['category' => $this->logCategory]); } /** * Advance the task entry's next calculated execution, effectively skipping the current execution. * * @return void * * @since 4.1.0 * @throws \Exception */ public function skipExecution(): void { $db = $this->db; $query = $db->getQuery(true); $id = $this->get('id'); $nextExec = (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec(true, true); $query->update($db->quoteName('#__scheduler_tasks', 't')) ->set('t.next_execution = :nextExec') ->where('t.id = :id') ->bind(':nextExec', $nextExec) ->bind(':id', $id); try { $db->setQuery($query)->execute(); } catch (\RuntimeException $e) { } $this->set('next_execution', $nextExec); } /** * Handles task exit (dispatch event). * * @return void * * @since 4.1.0 * * @throws \UnexpectedValueException|\BadMethodCallException */ protected function dispatchExitEvent(): void { $exitCode = $this->snapshot['status'] ?? 'NA'; $eventName = self::EVENTS_MAP[$exitCode] ?? self::EVENTS_MAP['NA']; $event = AbstractEvent::create( $eventName, [ 'subject' => $this, ] ); $this->app->getDispatcher()->dispatch($eventName, $event); } /** * Was the task successful? * * @return boolean True if the task was successful. * @since 4.1.0 */ public function isSuccess(): bool { return in_array(($this->snapshot['status'] ?? null), [Status::OK, Status::WILL_RESUME]); } /** * Set a task property. This method is a proxy to {@see Registry::set()}. * * @param string $path Registry path of the task property. * @param mixed $value The value to set to the property. * @param ?string $separator The key separator. * * @return mixed|null * * @since 4.1.0 */ protected function set(string $path, $value, string $separator = null) { return $this->taskRegistry->set($path, $value, $separator); } /** * Get a task property. This method is a proxy to {@see Registry::get()}. * * @param string $path Registry path of the task property. * @param mixed $default Default property to return, if the actual value is null. * * @return mixed The task property. * * @since 4.1.0 */ public function get(string $path, $default = null) { return $this->taskRegistry->get($path, $default); } /** * Static method to determine whether an enumerated task state (as a string) is valid. * * @param string $state The task state (enumerated, as a string). * * @return boolean * * @since 4.1.0 */ public static function isValidState(string $state): bool { if (!is_numeric($state)) { return false; } // Takes care of interpreting as float/int $state = $state + 0; return ArrayHelper::getValue(self::STATE_MAP, $state) !== null; } /** * Static method to determine whether a task id is valid. Note that this does not * validate ids against the database, but only verifies that an id may exist. * * @param string $id The task id (as a string). * * @return boolean * * @since 4.1.0 */ public static function isValidId(string $id): bool { $id = is_numeric($id) ? ($id + 0) : $id; if (!\is_int($id) || $id <= 0) { return false; } return true; } } src/Model/TasksModel.php 0000644 00000044550 15172736354 0011177 0 ustar 00 <?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 { protected $listForbiddenList = ['select', 'multi_ordering']; /** * 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 { $orderClauses = []; // Loop through provided clauses foreach ($multiOrdering as $ordering) { [$column, $direction] = explode(' ', $ordering); $orderClauses[] = $db->quoteName($column) . ' ' . $direction; } // At least one correct order clause if (count($orderClauses) > 0) { $query->order($orderClauses); } } 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 { $app = Factory::getApplication(); // Clean the multiorder values if ($list = $app->getUserStateFromRequest($this->context . '.list', 'list', [], 'array')) { if (!empty($list['multi_ordering']) && \is_array($list['multi_ordering'])) { $orderClauses = []; // Loop through provided clauses foreach ($list['multi_ordering'] as $multiOrdering) { // Split the combined string into individual variables $multiOrderingParts = explode(' ', $multiOrdering, 2); // Check that at least the column is present if (count($multiOrderingParts) < 1) { continue; } // Assign variables $multiOrderingColumn = $multiOrderingParts[0]; $multiOrderingDir = count($multiOrderingParts) === 2 ? $multiOrderingParts[1] : 'asc'; // Validate provided column if (!\in_array($multiOrderingColumn, $this->filter_fields)) { continue; } // Validate order dir if (strtolower($multiOrderingDir) !== 'asc' && strtolower($multiOrderingDir) !== 'desc') { continue; } $orderClauses[] = $multiOrderingColumn . ' ' . $multiOrderingDir; } $this->setState('list.multi_ordering', $orderClauses); } } // 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; } } src/Model/SelectModel.php 0000644 00000003220 15172736354 0011316 0 ustar 00 <?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; } } src/Model/TaskModel.php 0000644 00000071350 15172736354 0011012 0 ustar 00 <?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'); } 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(); $this->configureTaskGetterOptions($resolver); 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 4.4.9 */ 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 4.4.9 * @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 4.4.9 * @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 = Factory::getApplication()->getIdentity(); $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); } } src/Rule/ExecutionRulesRule.php 0000644 00000007022 15172736354 0012577 0 ustar 00 <?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\Rule; use Joomla\CMS\Form\Form; use Joomla\CMS\Form\FormRule; use Joomla\CMS\Form\Rule\OptionsRule; use Joomla\Registry\Registry; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * The ExecutionRulesRule Class. * Validates execution rules, with input for other fields as context. * * @since 4.1.0 */ class ExecutionRulesRule extends FormRule { /** * @var string RULE_TYPE_FIELD The field containing the rule type to test against * @since 4.1.0 */ private const RULE_TYPE_FIELD = "execution_rules.rule-type"; /** * @var string CUSTOM_RULE_GROUP The field group containing custom execution rules * @since 4.1.0 */ private const CUSTOM_RULE_GROUP = "execution_rules.cron-expression"; /** * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `<field>` tag for the form * field object. * @param mixed $value The form field value to validate. * @param ?string $group The field name group control value. This acts as an array container for the * field. For example if the field has `name="foo"` and the group value is set * to "bar" then the full field name would end up being "bar[foo]". * @param ?Registry $input An optional Registry object with the entire data set to validate against * the entire form. * @param ?Form $form The form object for which the field is being tested. * * @return boolean * * @since 4.1.0 */ public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): bool { $fieldName = (string) $element['name']; $ruleType = $input->get(self::RULE_TYPE_FIELD); if ($ruleType === $fieldName || ($ruleType === 'cron-expression' && $group === self::CUSTOM_RULE_GROUP)) { return $this->validateField($element, $value, $group, $form); } return true; } /** * @param \SimpleXMLElement $element The SimpleXMLElement for the field. * @param mixed $value The field value. * @param ?string $group The form field group the element belongs to. * @param Form|null $form The Form object against which the field is tested/ * * @return boolean True if field is valid * * @since 4.1.0 */ private function validateField(\SimpleXMLElement $element, $value, ?string $group = null, ?Form $form = null): bool { $elementType = (string) $element['type']; // If element is of cron type, we test against options and return if ($elementType === 'cron') { $clonedElement = clone $element; $clonedElement->addAttribute('required', 'true'); return (new OptionsRule())->test($clonedElement, $value, $group, null, $form); } // Test for a positive integer value and return return filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); } } src/View/Task/HtmlView.php 0000644 00000010047 15172736354 0011436 0 ustar 00 <?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\View\Task; use Joomla\CMS\Application\AdministratorApplication; use Joomla\CMS\Factory; use Joomla\CMS\Form\Form; use Joomla\CMS\Helper\ContentHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; use Joomla\CMS\Object\CMSObject; use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Toolbar\ToolbarHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * The MVC View for Task configuration page (TaskView). * * @since 4.1.0 */ class HtmlView extends BaseHtmlView { /** * @var AdministratorApplication $app * @since 4.1.0 */ protected $app; /** * The Form object * * @var Form * @since 4.1.0 */ protected $form; /** * The active item * * @var object * @since 4.1.0 */ protected $item; /** * The model state * * @var CMSObject * @since 4.1.0 */ protected $state; /** * The actions the user is authorised to perform * * @var CMSObject * @since 4.1.0 */ protected $canDo; /** * Overloads the parent constructor. * Just needed to fetch the Application object. * * @param array $config A named configuration array for object construction. * name: the name (optional) of the view (defaults to the view class name suffix). * charset: the character set to use for display * escape: the name (optional) of the function to use for escaping strings * base_path: the parent path (optional) of the `views` directory (defaults to the * component folder) template_plath: the path (optional) of the layout directory (defaults * to base_path + /views/ + view name helper_path: the path (optional) of the helper files * (defaults to base_path + /helpers/) layout: the layout (optional) to use to display the * view * * @since 4.1.0 * @throws \Exception */ public function __construct($config = []) { $this->app = Factory::getApplication(); parent::__construct($config); } /** * @param string $tpl The name of the template file to parse; automatically searches through the template paths. * * @return void * * @since 4.1.0 * @throws \Exception */ public function display($tpl = null): void { /* * Will call the getForm() method of TaskModel */ $this->form = $this->get('Form'); $this->item = $this->get('Item'); $this->state = $this->get('State'); $this->canDo = ContentHelper::getActions('com_scheduler', 'task', $this->item->id); $this->addToolbar(); parent::display($tpl); } /** * Adds the page title and toolbar * * @return void * * @since 4.1.0 */ protected function addToolbar(): void { $this->app->getInput()->set('hidemainmenu', true); $isNew = ($this->item->id == 0); $canDo = $this->canDo; $toolbar = Toolbar::getInstance(); ToolbarHelper::title($isNew ? Text::_('COM_SCHEDULER_MANAGER_TASK_NEW') : Text::_('COM_SCHEDULER_MANAGER_TASK_EDIT'), 'clock'); if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) { $toolbar->apply('task.apply'); $toolbar->save('task.save'); } // @todo | ? : Do we need save2new, save2copy? $toolbar->cancel('task.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE'); $toolbar->help('Scheduled_Tasks:_Edit'); } } src/View/Tasks/HtmlView.php 0000644 00000012175 15172736354 0011625 0 ustar 00 <?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\View\Tasks; use Joomla\CMS\Factory; use Joomla\CMS\Form\Form; use Joomla\CMS\Helper\ContentHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\GenericDataException; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; use Joomla\CMS\Object\CMSObject; use Joomla\CMS\Pagination\Pagination; use Joomla\CMS\Toolbar\Button\DropdownButton; use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Toolbar\ToolbarHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * MVC View for the Tasks list page. * * @since 4.1.0 */ class HtmlView extends BaseHtmlView { /** * Array of task items. * * @var array * @since 4.1.0 */ protected $items; /** * The pagination object. * * @var Pagination * @since 4.1.0 * @todo Test pagination. */ protected $pagination; /** * The model state. * * @var CMSObject * @since 4.1.0 */ protected $state; /** * A Form object for search filters. * * @var Form * @since 4.1.0 */ public $filterForm; /** * The active search filters. * * @var array * @since 4.1.0 */ public $activeFilters; /** * Is this view in an empty state? * * @var boolean * @since 4.1.0 */ private $isEmptyState = false; /** * @inheritDoc * * @param string $tpl The name of the template file to parse; automatically searches through the template paths. * * @return void * * @since 4.1.0 * @throws \Exception */ public function display($tpl = null): void { $this->items = $this->get('Items'); $this->pagination = $this->get('Pagination'); $this->state = $this->get('State'); $this->filterForm = $this->get('FilterForm'); $this->activeFilters = $this->get('ActiveFilters'); if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { $this->setLayout('empty_state'); } // Check for errors. if (\count($errors = $this->get('Errors'))) { throw new GenericDataException(implode("\n", $errors), 500); } $this->addToolbar(); parent::display($tpl); } /** * Add the page title and toolbar. * * @return void * * @since 4.1.0 * @throws \Exception */ protected function addToolbar(): void { $canDo = ContentHelper::getActions('com_scheduler'); $user = Factory::getApplication()->getIdentity(); $toolbar = Toolbar::getInstance(); ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); if ($canDo->get('core.create')) { $toolbar->linkButton('new', 'JTOOLBAR_NEW') ->url('index.php?option=com_scheduler&view=select&layout=default') ->buttonClass('btn btn-success') ->icon('icon-new'); } if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { /** @var DropdownButton $dropdown */ $dropdown = $toolbar->dropdownButton('status-group') ->toggleSplit(false) ->text('JTOOLBAR_CHANGE_STATUS') ->icon('icon-ellipsis-h') ->buttonClass('btn btn-action') ->listCheck(true); $childBar = $dropdown->getChildToolbar(); // Add the batch Enable, Disable and Trash buttons if privileged if ($canDo->get('core.edit.state')) { $childBar->publish('tasks.publish', 'JTOOLBAR_ENABLE')->listCheck(true); $childBar->unpublish('tasks.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); if ($canDo->get('core.admin')) { $childBar->checkin('tasks.checkin'); } $childBar->checkin('tasks.unlock', 'COM_SCHEDULER_TOOLBAR_UNLOCK')->icon('icon-unlock'); // We don't want the batch Trash button if displayed entries are all trashed if ($this->state->get('filter.state') != -2) { $childBar->trash('tasks.trash')->listCheck(true); } } } // Add "Empty Trash" button if filtering by trashed. if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { $toolbar->delete('tasks.delete', 'JTOOLBAR_EMPTY_TRASH') ->message('JGLOBAL_CONFIRM_DELETE') ->listCheck(true); } // Link to component preferences if user has admin privileges if ($canDo->get('core.admin') || $canDo->get('core.options')) { $toolbar->preferences('com_scheduler'); } $toolbar->help('Scheduled_Tasks'); } } src/View/Select/HtmlView.php 0000644 00000006735 15172736354 0011764 0 ustar 00 <?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\View\Select; use Joomla\CMS\Application\AdministratorApplication; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\GenericDataException; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; use Joomla\CMS\Object\CMSObject; use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\Component\Scheduler\Administrator\Task\TaskOption; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * The MVC View for the routine selection page (SelectView). * This view lets the user choose from a list of plugin defined task routines. * * @since 4.1.0 */ class HtmlView extends BaseHtmlView { /** * @var AdministratorApplication * @since 4.1.0 */ protected $app; /** * The model state * * @var CMSObject * @since 4.1.0 */ protected $state; /** * An array of items * * @var TaskOption[] * @since 4.1.0 */ protected $items; /** * HtmlView constructor. * * @param array $config A named configuration array for object construction. * name: the name (optional) of the view (defaults to the view class name suffix). * charset: the character set to use for display * escape: the name (optional) of the function to use for escaping strings * base_path: the parent path (optional) of the `views` directory (defaults to the component * folder) template_plath: the path (optional) of the layout directory (defaults to * base_path + /views/ + view name helper_path: the path (optional) of the helper files * (defaults to base_path + /helpers/) layout: the layout (optional) to use to display the * view * * @since 4.1.0 * @throws \Exception */ public function __construct($config = []) { $this->app = Factory::getApplication(); parent::__construct($config); } /** * @param string $tpl The name of the template file to parse; automatically searches through the template paths. * * @return void * * @since 4.1.0 * @throws \Exception */ public function display($tpl = null): void { $this->state = $this->get('State'); $this->items = $this->get('Items'); // Check for errors. if (\count($errors = $this->get('Errors'))) { throw new GenericDataException(implode("\n", $errors), 500); } $this->addToolbar(); parent::display($tpl); } /** * Add the page title and toolbar. * * @return void * * @since 4.1.0 */ protected function addToolbar(): void { $toolbar = Toolbar::getInstance(); ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); $toolbar->linkButton('cancel') ->url('index.php?option=com_scheduler') ->buttonClass('btn btn-danger') ->icon('icon-times') ->text('JCANCEL'); } } src/Controller/TaskController.php 0000644 00000006633 15172736354 0013162 0 ustar 00 <?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\Controller; use Joomla\CMS\Application\AdministratorApplication; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\FormController; use Joomla\CMS\Router\Route; use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * MVC Controller for the item configuration page (TaskView). * * @since 4.1.0 */ class TaskController extends FormController { /** * Add a new record * * @return boolean * @since 4.1.0 * @throws \Exception */ public function add(): bool { /** @var AdministratorApplication $app */ $app = $this->app; $input = $app->getInput(); $validTaskOptions = SchedulerHelper::getTaskOptions(); $canAdd = parent::add(); if ($canAdd !== true) { return false; } $taskType = $input->get('type'); $taskOption = $validTaskOptions->findOption($taskType) ?: null; if (!$taskOption) { // ? : Is this the right redirect [review] $redirectUrl = 'index.php?option=' . $this->option . '&view=select&layout=edit'; $this->setRedirect(Route::_($redirectUrl, false)); $app->enqueueMessage(Text::_('COM_SCHEDULER_ERROR_INVALID_TASK_TYPE'), 'warning'); $canAdd = false; } $app->setUserState('com_scheduler.add.task.task_type', $taskType); $app->setUserState('com_scheduler.add.task.task_option', $taskOption); // @todo : Parameter array handling below? return $canAdd; } /** * Override parent cancel method to reset the add task state * * @param ?string $key Primary key from the URL param * * @return boolean True if access level checks pass * * @since 4.1.0 */ public function cancel($key = null): bool { $result = parent::cancel($key); $this->app->setUserState('com_scheduler.add.task.task_type', null); $this->app->setUserState('com_scheduler.add.task.task_option', null); // ? Do we need to redirect based on URL's 'return' param? {@see ModuleController} return $result; } /** * Check if user has the authority to edit an asset * * @param array $data Array of input data * @param string $key Name of key for primary key, defaults to 'id' * * @return boolean True if user is allowed to edit record * * @since 4.1.0 */ protected function allowEdit($data = [], $key = 'id'): bool { // Extract the recordId from $data, will come in handy $recordId = (int) $data[$key] ?? 0; /** * Zero record (id:0), return component edit permission by calling parent controller method * ?: Is this the right way to do this? */ if ($recordId === 0) { return parent::allowEdit($data, $key); } // @todo : Check if this works as expected return $this->app->getIdentity()->authorise('core.edit', 'com_scheduler.task.' . $recordId); } } src/Controller/DisplayController.php 0000644 00000006236 15172736354 0013664 0 ustar 00 <?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\Controller; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\Router\Route; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Default controller for com_scheduler. * * @since 4.1.0 */ class DisplayController extends BaseController { /** * @var string * @since 4.1.0 */ protected $default_view = 'tasks'; /** * @param boolean $cachable If true, the view output will be cached * @param array $urlparams An array of safe url parameters and their variable types, for valid values see * {@link InputFilter::clean()}. * * @return BaseController|boolean Returns either a BaseController object to support chaining, or false on failure * * @since 4.1.0 * @throws \Exception */ public function display($cachable = false, $urlparams = []) { $layout = $this->input->get('layout', 'default'); // Check for edit form. if ($layout === 'edit') { if (!$this->validateEntry()) { $tasksViewUrl = Route::_('index.php?option=com_scheduler&view=tasks', false); $this->setRedirect($tasksViewUrl); return false; } } // Let the parent method take over return parent::display($cachable, $urlparams); } /** * Validates entry to the view * * @param string $layout The layout to validate entry for (defaults to 'edit') * * @return boolean True is entry is valid * * @since 4.1.0 */ private function validateEntry(string $layout = 'edit'): bool { $context = 'com_scheduler'; $id = $this->input->getInt('id'); $isValid = true; switch ($layout) { case 'edit': // True if controller was called and verified permissions $inEditList = $this->checkEditId("$context.edit.task", $id); $isNew = ($id == 0); // For new item, entry is invalid if task type was not selected through SelectView if ($isNew && !$this->app->getUserState("$context.add.task.task_type")) { $this->setMessage((Text::_('COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW')), 'error'); $isValid = false; } elseif (!$inEditList) { // For existing item, entry is invalid if TaskController has not granted access if (!\count($this->app->getMessageQueue())) { $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); } $isValid = false; } break; default: break; } return $isValid; } } src/Controller/TasksController.php 0000644 00000006150 15172736354 0013337 0 ustar 00 <?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\Controller; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\AdminController; use Joomla\CMS\MVC\Model\BaseDatabaseModel; use Joomla\CMS\Router\Route; use Joomla\Component\Scheduler\Administrator\Model\TaskModel; use Joomla\Utilities\ArrayHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * MVC Controller for TasksView. * * @since 4.1.0 */ class TasksController extends AdminController { /** * Proxy for the parent method. * * @param string $name The name of the model. * @param string $prefix The prefix for the PHP class name. * @param array $config Array of configuration parameters. * * @return BaseDatabaseModel * * @since 4.1.0 */ public function getModel($name = 'Task', $prefix = 'Administrator', $config = ['ignore_request' => true]): BaseDatabaseModel { return parent::getModel($name, $prefix, $config); } /** * Unlock a locked task, i.e., a task that is presumably still running but might have crashed and got stuck in the * "locked" state. * * @return void * * @since 4.1.0 */ public function unlock(): void { // Check for request forgeries $this->checkToken(); /** @var integer[] $cid Items to publish (from request parameters). */ $cid = (array) $this->input->get('cid', [], 'int'); // Remove zero values resulting from input filter $cid = array_filter($cid); if (empty($cid)) { $this->app->getLogger() ->warning(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), ['category' => 'jerror']); } else { /** @var TaskModel $model */ $model = $this->getModel(); // Make sure the item IDs are integers $cid = ArrayHelper::toInteger($cid); // Unlock the items. try { $model->unlock($cid); $errors = $model->getErrors(); $noticeText = null; if ($errors) { $this->app->enqueueMessage(Text::plural($this->text_prefix . '_N_ITEMS_FAILED_UNLOCKING', \count($cid)), 'error'); } else { $noticeText = $this->text_prefix . '_N_ITEMS_UNLOCKED'; } if (\count($cid)) { $this->setMessage(Text::plural($noticeText, \count($cid))); } } catch (\Exception $e) { $this->setMessage($e->getMessage(), 'error'); } } $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false ) ); } } src/Scheduler/Scheduler.php 0000644 00000024777 15172736354 0011736 0 ustar 00 <?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\Scheduler; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\Component\Scheduler\Administrator\Extension\SchedulerComponent; use Joomla\Component\Scheduler\Administrator\Model\TaskModel; use Joomla\Component\Scheduler\Administrator\Model\TasksModel; use Joomla\Component\Scheduler\Administrator\Task\Status; use Joomla\Component\Scheduler\Administrator\Task\Task; 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 /** * The Scheduler class provides the core functionality of ComScheduler. * Currently, this includes fetching scheduled tasks from the database * and execution of any or the next due task. * It is planned that this class is extended with C[R]UD methods for * scheduled tasks. * * @since 4.1.0 * @todo A global instance? */ class Scheduler { private const LOG_TEXT = [ Status::OK => 'COM_SCHEDULER_SCHEDULER_TASK_COMPLETE', Status::WILL_RESUME => 'COM_SCHEDULER_SCHEDULER_TASK_WILL_RESUME', Status::NO_LOCK => 'COM_SCHEDULER_SCHEDULER_TASK_LOCKED', Status::NO_RUN => 'COM_SCHEDULER_SCHEDULER_TASK_UNLOCKED', Status::NO_ROUTINE => 'COM_SCHEDULER_SCHEDULER_TASK_ROUTINE_NA', ]; /** * Filters for the task queue. Can be used with fetchTaskRecords(). * * @since 4.1.0 * @todo remove? */ public const TASK_QUEUE_FILTERS = [ 'due' => 1, 'locked' => -1, ]; /** * List config for the task queue. Can be used with fetchTaskRecords(). * * @since 4.1.0 * @todo remove? */ public const TASK_QUEUE_LIST_CONFIG = [ 'multi_ordering' => ['a.priority DESC ', 'a.next_execution ASC'], ]; /** * Run a scheduled task. * Runs a single due task from the task queue by default if $id and $title are not passed. * * @param array $options Array with options to configure the method's behavior. Supports: * 1. `id`: (Optional) ID of the task to run. * 2. `allowDisabled`: Allow running disabled tasks. * 3. `allowConcurrent`: Allow concurrent execution, i.e., running the task when another * task may be running. * * @return ?Task The task executed or null if not exists * * @since 4.1.0 * @throws \RuntimeException */ public function runTask(array $options): ?Task { $resolver = new OptionsResolver(); try { $this->configureTaskRunnerOptions($resolver); } catch (\Exception $e) { } try { $options = $resolver->resolve($options); } catch (\Exception $e) { if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { throw $e; } } /** @var CMSApplication $app */ $app = Factory::getApplication(); // ? Sure about inferring scheduling bypass? $task = $this->getTask( [ 'id' => (int) $options['id'], 'allowDisabled' => $options['allowDisabled'], 'bypassScheduling' => (int) $options['id'] !== 0, 'allowConcurrent' => $options['allowConcurrent'], 'includeCliExclusive' => ($app->isClient('cli')), ] ); // ? Should this be logged? (probably, if an ID is passed?) if (empty($task)) { return null; } $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; $options['text_file'] = 'joomla_scheduler.php'; Log::addLogger($options, Log::ALL, $task->logCategory); $taskId = $task->get('id'); $taskTitle = $task->get('title'); $task->log(Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_START', $taskId, $taskTitle), 'info'); // Let's try to avoid time-outs if (\function_exists('set_time_limit')) { set_time_limit(0); } try { $task->run(); } catch (\Exception $e) { // We suppress the exception here, it's still accessible with `$task->getContent()['exception']`. } $executionSnapshot = $task->getContent(); $exitCode = $executionSnapshot['status'] ?? Status::NO_EXIT; $netDuration = $executionSnapshot['netDuration'] ?? 0; $duration = $executionSnapshot['duration'] ?? 0; if (\array_key_exists($exitCode, self::LOG_TEXT)) { $level = in_array($exitCode, [Status::OK, Status::WILL_RESUME]) ? 'info' : 'warning'; $task->log(Text::sprintf(self::LOG_TEXT[$exitCode], $taskId, $duration, $netDuration), $level); return $task; } $task->log( Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_UNKNOWN_EXIT', $taskId, $duration, $netDuration, $exitCode), 'warning' ); return $task; } /** * Set up an {@see OptionsResolver} to resolve options compatible with {@see runTask}. * * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. * * @return void * * @since 4.1.0 * @throws AccessException */ protected function configureTaskRunnerOptions(OptionsResolver $resolver): void { $resolver->setDefaults( [ 'id' => 0, 'allowDisabled' => false, 'allowConcurrent' => false, ] ) ->setAllowedTypes('id', 'numeric') ->setAllowedTypes('allowDisabled', 'bool') ->setAllowedTypes('allowConcurrent', 'bool'); } /** * Get the next task which is due to run, limit to a specific task when ID is given * * @param array $options Options for the getter, see {@see TaskModel::getTask()}. * ! should probably also support a non-locking getter. * * @return Task $task The task to execute * * @since 4.1.0 * @throws \RuntimeException */ public function getTask(array $options = []): ?Task { $resolver = new OptionsResolver(); try { TaskModel::configureTaskGetterOptions($resolver); } catch (\Exception $e) { } try { $options = $resolver->resolve($options); } catch (\Exception $e) { if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { throw $e; } } try { /** @var SchedulerComponent $component */ $component = Factory::getApplication()->bootComponent('com_scheduler'); /** @var TaskModel $model */ $model = $component->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]); } catch (\Exception $e) { } if (!isset($model)) { throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); } $task = $model->getTask($options); if (empty($task)) { return null; } return new Task($task); } /** * Fetches a single scheduled task in a Task instance. * If no id or title is specified, a due task is returned. * * @param int $id The task ID. * @param bool $allowDisabled Allow disabled/trashed tasks? * * @return ?object A matching task record, if it exists * * @since 4.1.0 * @throws \RuntimeException */ public function fetchTaskRecord(int $id = 0, bool $allowDisabled = false): ?object { $filters = []; $listConfig = ['limit' => 1]; if ($id > 0) { $filters['id'] = $id; } else { // Filters and list config for scheduled task queue $filters['due'] = 1; $filters['locked'] = -1; $listConfig['multi_ordering'] = [ 'a.priority DESC', 'a.next_execution ASC', ]; } if ($allowDisabled) { $filters['state'] = ''; } return $this->fetchTaskRecords($filters, $listConfig)[0] ?? null; } /** * @param array $filters The filters to set to the model * @param array $listConfig The list config (ordering, etc.) to set to the model * * @return array * * @since 4.1.0 * @throws \RunTimeException */ public function fetchTaskRecords(array $filters, array $listConfig): array { $model = null; try { /** @var SchedulerComponent $component */ $component = Factory::getApplication()->bootComponent('com_scheduler'); /** @var TasksModel $model */ $model = $component->getMVCFactory() ->createModel('Tasks', 'Administrator', ['ignore_request' => true]); } catch (\Exception $e) { } if (!$model) { throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); } $model->setState('list.select', 'a.*'); // Default to only enabled tasks if (!isset($filters['state'])) { $model->setState('filter.state', 1); } // Default to including orphaned tasks $model->setState('filter.orphaned', 0); // Default to ordering by ID $model->setState('list.ordering', 'a.id'); $model->setState('list.direction', 'ASC'); // List options foreach ($listConfig as $key => $value) { $model->setState('list.' . $key, $value); } // Filter options foreach ($filters as $type => $filter) { $model->setState('filter.' . $type, $filter); } return $model->getItems() ?: []; } } src/Event/ExecuteTaskEvent.php 0000644 00000004416 15172736354 0012376 0 ustar 00 <?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\Event; use Joomla\CMS\Event\AbstractEvent; use Joomla\Component\Scheduler\Administrator\Task\Task; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Event class for onExecuteTask event. * * @since 4.1.0 */ class ExecuteTaskEvent extends AbstractEvent { /** * Constructor. * * @param string $name The event name. * @param array $arguments The event arguments. * * @since 4.1.0 * @throws \BadMethodCallException */ public function __construct($name, array $arguments = []) { parent::__construct($name, $arguments); $arguments['resultSnapshot'] = null; if (!($arguments['subject'] ?? null) instanceof Task) { throw new \BadMethodCallException("The subject given for $name event must be an instance of " . Task::class); } } /** * Sets the task result snapshot and stops event propagation. * * @param array $snapshot The task snapshot. * * @return void * * @since 4.1.0 */ public function setResult(array $snapshot = []): void { $this->arguments['resultSnapshot'] = $snapshot; if (!empty($snapshot)) { $this->stopPropagation(); } } /** * @return integer The task's taskId. * * @since 4.1.0 */ public function getTaskId(): int { return $this->arguments['subject']->get('id'); } /** * @return string The task's 'type'. * * @since 4.1.0 */ public function getRoutineId(): string { return $this->arguments['subject']->get('type'); } /** * Returns the snapshot of the triggered task if available, else an empty array * * @return array The task snapshot if available, else null * * @since 4.1.0 */ public function getResultSnapshot(): array { return $this->arguments['resultSnapshot'] ?? []; } } src/Traits/TaskPluginTrait.php 0000644 00000030232 15172736354 0012414 0 ustar 00 <?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\Traits; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Path; use Joomla\CMS\Form\Form; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; use Joomla\Component\Scheduler\Administrator\Task\Status; use Joomla\Event\EventInterface; use Joomla\Utilities\ArrayHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Utility trait for plugins that offer `com_scheduler` compatible task routines. This trait defines a lot * of handy methods that make it really simple to support task routines in a J4.x plugin. This trait includes standard * methods to broadcast routines {@see TaskPluginTrait::advertiseRoutines()}, enhance task forms * {@see TaskPluginTrait::enhanceTaskItemForm()} and call routines * {@see TaskPluginTrait::standardRoutineHandler()}. With standard cookie-cutter behaviour, a task plugin may only need * to include this trait, and define methods corresponding to each routine along with the `TASKS_MAP` class constant to * declare supported routines and related properties. * * @since 4.1.0 */ trait TaskPluginTrait { /** * A snapshot of the routine state. * * @var array * @since 4.1.0 */ protected $snapshot = []; /** * Set information to {@see $snapshot} when initializing a routine. * * @param ExecuteTaskEvent $event The onExecuteTask event. * * @return void * * @since 4.1.0 */ protected function startRoutine(ExecuteTaskEvent $event): void { if (!$this instanceof CMSPlugin) { return; } $this->snapshot['logCategory'] = $event->getArgument('subject')->logCategory; $this->snapshot['plugin'] = $this->_name; $this->snapshot['startTime'] = microtime(true); $this->snapshot['status'] = Status::RUNNING; } /** * Set information to {@see $snapshot} when ending a routine. This information includes the routine exit code and * timing information. * * @param ExecuteTaskEvent $event The event * @param ?int $exitCode The task exit code * * @return void * * @since 4.1.0 * @throws \Exception */ protected function endRoutine(ExecuteTaskEvent $event, int $exitCode): void { if (!$this instanceof CMSPlugin) { return; } $this->snapshot['endTime'] = $endTime = microtime(true); $this->snapshot['duration'] = $endTime - $this->snapshot['startTime']; $this->snapshot['status'] = $exitCode ?? Status::OK; $event->setResult($this->snapshot); } /** * Enhance the task form with routine-specific fields from an XML file declared through the TASKS_MAP constant. * If a plugin only supports the task form and does not need additional logic, this method can be mapped to the * `onContentPrepareForm` event through {@see SubscriberInterface::getSubscribedEvents()} and will take care * of injecting the fields without additional logic in the plugin class. * * @param EventInterface|Form $context The onContentPrepareForm event or the Form object. * @param mixed $data The form data, required when $context is a {@see Form} instance. * * @return boolean True if the form was successfully enhanced or the context was not relevant. * * @since 4.1.0 * @throws \Exception */ public function enhanceTaskItemForm($context, $data = null): bool { if ($context instanceof EventInterface) { /** @var Form $form */ [$form, $data] = array_values($context->getArguments()); } elseif ($context instanceof Form) { $form = $context; } else { throw new \InvalidArgumentException( sprintf( 'Argument 0 of %1$s must be an instance of %2$s or %3$s', __METHOD__, EventInterface::class, Form::class ) ); } if ($form->getName() !== 'com_scheduler.task') { return true; } $routineId = $this->getRoutineId($form, $data); $isSupported = \array_key_exists($routineId, self::TASKS_MAP); $enhancementFormName = self::TASKS_MAP[$routineId]['form'] ?? ''; // Return if routine is not supported by the plugin or the routine does not have a form linked in TASKS_MAP. if (!$isSupported || \strlen($enhancementFormName) === 0) { return true; } // We expect the form XML in "{PLUGIN_PATH}/forms/{FORM_NAME}.xml" $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; $enhancementFormFile = $path . '/forms/' . $enhancementFormName . '.xml'; try { $enhancementFormFile = Path::check($enhancementFormFile); } catch (\Exception $e) { return false; } if (is_file($enhancementFormFile)) { return $form->loadFile($enhancementFormFile); } return false; } /** * Advertise the task routines supported by the plugin. This method should be mapped to the `onTaskOptionsList`, * enabling the plugin to advertise its routines without any custom logic.<br/> * **Note:** This method expects the `TASKS_MAP` class constant to have relevant information. * * @param EventInterface $event onTaskOptionsList Event * * @return void * * @since 4.1.0 */ public function advertiseRoutines(EventInterface $event): void { $options = []; foreach (self::TASKS_MAP as $routineId => $details) { // Sanity check against non-compliant plugins if (isset($details['langConstPrefix'])) { $options[$routineId] = $details['langConstPrefix']; } } $subject = $event->getArgument('subject'); $subject->addOptions($options); } /** * Get the relevant task routine ID in the context of a form event, e.g., the `onContentPrepareForm` event. * * @param Form $form The form * @param mixed $data The data * * @return string * * @since 4.1.0 * @throws \Exception */ protected function getRoutineId(Form $form, $data): string { /* * Depending on when the form is loaded, the ID may either be in $data or the data already bound to the form. * $data can also either be an object or an array. */ $routineId = $data->taskOption->id ?? $data->type ?? $data['type'] ?? $form->getValue('type') ?? $data['taskOption']->id ?? ''; // If we're unable to find a routineId, it might be in the form input. if (empty($routineId)) { $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); $form = $app->getInput()->get('jform', []); $routineId = ArrayHelper::getValue($form, 'type', '', 'STRING'); } return $routineId; } /** * Add a log message to the task log. * * @param string $message The log message * @param string $priority The log message priority * * @return void * * @since 4.1.0 * @throws \Exception * @todo : use dependency injection here (starting from the Task & Scheduler classes). */ protected function logTask(string $message, string $priority = 'info'): void { static $langLoaded; static $priorityMap = [ 'debug' => Log::DEBUG, 'error' => Log::ERROR, 'info' => Log::INFO, 'notice' => Log::NOTICE, 'warning' => Log::WARNING, ]; if (!$langLoaded) { $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); $langLoaded = true; } $category = $this->snapshot['logCategory']; Log::add(Text::_('COM_SCHEDULER_ROUTINE_LOG_PREFIX') . $message, $priorityMap[$priority] ?? Log::INFO, $category); } /** * Handler for *standard* task routines. Standard routines are mapped to valid class methods 'method' through * `static::TASKS_MAP`. These methods are expected to take a single argument (the Event) and return an integer * return status (see {@see Status}). For a plugin that maps each of its task routines to valid methods and does * not need non-standard handling, this method can be mapped to the `onExecuteTask` event through * {@see SubscriberInterface::getSubscribedEvents()}, which would allow it to then check if the event wants to * execute a routine offered by the parent plugin, call the routine and do some other housework without any code * in the parent classes.<br/> * **Compatible routine method signature:** ({@see ExecuteTaskEvent::class}, ...): int * * @param ExecuteTaskEvent $event The `onExecuteTask` event. * * @return void * * @since 4.1.0 * @throws \Exception */ public function standardRoutineHandler(ExecuteTaskEvent $event): void { if (!\array_key_exists($event->getRoutineId(), self::TASKS_MAP)) { return; } $this->startRoutine($event); $routineId = $event->getRoutineId(); $methodName = (string) self::TASKS_MAP[$routineId]['method'] ?? ''; $exitCode = Status::NO_EXIT; // We call the mapped method if it exists and confirms to the ($event) -> int signature. if (!empty($methodName) && ($staticReflection = new \ReflectionClass($this))->hasMethod($methodName)) { $method = $staticReflection->getMethod($methodName); // Might need adjustments here for PHP8 named parameters. if ( !($method->getNumberOfRequiredParameters() === 1) || !$method->getParameters()[0]->hasType() || $method->getParameters()[0]->getType()->getName() !== ExecuteTaskEvent::class || !$method->hasReturnType() || $method->getReturnType()->getName() !== 'int' ) { $this->logTask( sprintf( 'Incorrect routine method signature for %1$s(). See checks in %2$s()', $method->getName(), __METHOD__ ), 'error' ); return; } try { // Enable invocation of private/protected methods. $method->setAccessible(true); $exitCode = $method->invoke($this, $event); } catch (\ReflectionException $e) { // @todo replace with language string (?) $this->logTask('Exception when calling routine: ' . $e->getMessage(), 'error'); $exitCode = Status::NO_RUN; } } else { $this->logTask( sprintf( 'Incorrectly configured TASKS_MAP in class %s. Missing valid method for `routine_id` %s', static::class, $routineId ), 'error' ); } /** * Closure to validate a status against {@see Status} * * @since 4.1.0 */ $validateStatus = static function (int $statusCode): bool { return \in_array( $statusCode, (new \ReflectionClass(Status::class))->getConstants() ); }; // Validate the exit code. if (!\is_int($exitCode) || !$validateStatus($exitCode)) { $exitCode = Status::INVALID_EXIT; } $this->endRoutine($event, $exitCode); } } src/Helper/ExecRuleHelper.php 0000644 00000013501 15172736354 0012154 0 ustar 00 <?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\Helper; use Cron\CronExpression; use Joomla\CMS\Date\Date; use Joomla\CMS\Factory; use Joomla\Component\Scheduler\Administrator\Task\Task; use Joomla\Database\DatabaseInterface; use Joomla\Utilities\ArrayHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Helper class for supporting task execution rules. * * @since 4.1.0 * @todo This helper should probably be merged into the {@see Task} class. */ class ExecRuleHelper { /** * The execution rule type * * @var string * @since 4.1.0 */ private $type; /** * @var array * @since 4.1.0 */ private $task; /** * @var object * @since 4.1.0 */ private $rule; /** * @param array|object $task A task entry * * @since 4.1.0 */ public function __construct($task) { $this->task = \is_array($task) ? $task : ArrayHelper::fromObject($task); $rule = $this->getFromTask('cron_rules'); $this->rule = \is_string($rule) ? (object) json_decode($rule) : (\is_array($rule) ? (object) $rule : $rule); $this->type = $this->rule->type; } /** * Get a property from the task array * * @param string $property The property to get * @param mixed $default The default value returned if property does not exist * * @return mixed * * @since 4.1.0 */ private function getFromTask(string $property, $default = null) { $property = ArrayHelper::getValue($this->task, $property); return $property ?? $default; } /** * @param boolean $string If true, an SQL formatted string is returned. * @param boolean $basisNow If true, the current date-time is used as the basis for projecting the next * execution. * * @return ?Date|string * * @since 4.1.0 * @throws \Exception */ public function nextExec(bool $string = true, bool $basisNow = false) { $executionRules = $this->getFromTask('execution_rules'); $type = $executionRules['rule-type']; switch ($type) { case 'interval-minutes': $now = Factory::getDate('now', 'UTC'); $intervalMinutes = (int) $executionRules['interval-minutes']; $interval = new \DateInterval('PT' . $intervalMinutes . 'M'); $nextExec = $now->add($interval); $nextExec = $string ? $nextExec->toSql() : $nextExec; break; case 'interval-hours': $now = Factory::getDate('now', 'UTC'); $intervalHours = $executionRules['interval-hours']; $interval = new \DateInterval('PT' . $intervalHours . 'H'); $nextExec = $now->add($interval); $nextExec = $string ? $nextExec->toSql() : $nextExec; break; case 'interval-days': $now = Factory::getDate('now', 'UTC'); $intervalDays = $executionRules['interval-days']; $interval = new \DateInterval('P' . $intervalDays . 'D'); $nextExec = $now->add($interval); $execTime = $executionRules['exec-time']; list($hour, $minute) = explode(':', $execTime); $nextExec->setTime($hour, $minute); $nextExec = $string ? $nextExec->toSql() : $nextExec; break; case 'interval-months': $now = Factory::getDate('now', 'UTC'); $intervalMonths = $executionRules['interval-months']; $interval = new \DateInterval('P' . $intervalMonths . 'M'); $nextExec = $now->add($interval); $execDay = $executionRules['exec-day']; $nextExecYear = $nextExec->format('Y'); $nextExecMonth = $nextExec->format('n'); $nextExec->setDate($nextExecYear, $nextExecMonth, $execDay); $execTime = $executionRules['exec-time']; list($hour, $minute) = explode(':', $execTime); $nextExec->setTime($hour, $minute); $nextExec = $string ? $nextExec->toSql() : $nextExec; break; case 'cron-expression': // @todo: testing $cExp = new CronExpression((string) $this->rule->exp); $nextExec = $cExp->getNextRunDate('now', 0, false, Factory::getApplication()->get('offset', 'UTC')); $nextExec->setTimezone(new \DateTimeZone('UTC')); $nextExec = $string ? $this->dateTimeToSql($nextExec) : $nextExec; break; default: // 'manual' execution is handled here. $nextExec = null; } return $nextExec; } /** * Returns a sql-formatted string for a DateTime object. * Only needed for DateTime objects returned by CronExpression, JDate supports this as class method. * * @param \DateTime $dateTime A DateTime object to format * * @return string * * @since 4.1.0 */ private function dateTimeToSql(\DateTime $dateTime): string { static $db; $db = $db ?? Factory::getContainer()->get(DatabaseInterface::class); return $dateTime->format($db->getDateFormat()); } } src/Helper/SchedulerHelper.php 0000644 00000003404 15172736354 0012357 0 ustar 00 <?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\Helper; use Joomla\CMS\Application\AdministratorApplication; use Joomla\CMS\Event\AbstractEvent; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\PluginHelper; use Joomla\Component\Scheduler\Administrator\Task\TaskOptions; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * The SchedulerHelper class. * Provides static methods used across com_scheduler * * @since 4.1.0 */ abstract class SchedulerHelper { /** * Cached TaskOptions object * * @var TaskOptions * @since 4.1.0 */ protected static $taskOptionsCache; /** * Returns available task routines as a TaskOptions object. * * @return TaskOptions A TaskOptions object populated with task routines offered by plugins * * @since 4.1.0 * @throws \Exception */ public static function getTaskOptions(): TaskOptions { if (self::$taskOptionsCache !== null) { return self::$taskOptionsCache; } /** @var AdministratorApplication $app */ $app = Factory::getApplication(); $options = new TaskOptions(); $event = AbstractEvent::create( 'onTaskOptionsList', [ 'subject' => $options, ] ); PluginHelper::importPlugin('task'); $app->getDispatcher()->dispatch('onTaskOptionsList', $event); self::$taskOptionsCache = $options; return $options; } } src/Table/TaskTable.php 0000644 00000020560 15172736354 0010765 0 ustar 00 <?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\Table; use Joomla\CMS\Event\AbstractEvent; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Model\AdminModel; use Joomla\CMS\Table\Asset; use Joomla\CMS\Table\Table; use Joomla\Database\DatabaseDriver; use Joomla\Database\Exception\QueryTypeAlreadyDefinedException; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Table class for tasks scheduled through `com_scheduler`. * The type alias for Task table entries is `com_scheduler.task`. * * @since 4.1.0 */ class TaskTable extends Table { /** * Indicates that columns fully support the NULL value in the database * * @var boolean * @since 4.1.1 */ protected $_supportNullValue = true; /** * Ensure params are json encoded by the bind method. * * @var string[] * @since 4.1.0 */ protected $_jsonEncode = ['params', 'execution_rules', 'cron_rules']; /** * The 'created' column. * * @var string * @since 4.1.0 */ public $created; /** * The 'title' column. * * @var string * @since 4.1.0 */ public $title; /** * @var string * @since 4.1.0 */ public $typeAlias = 'com_scheduler.task'; /** * TaskTable constructor override, needed to pass the DB table name and primary key to {@see Table::__construct()}. * * @param DatabaseDriver $db A database connector object. * * @since 4.1.0 */ public function __construct(DatabaseDriver $db) { $this->setColumnAlias('published', 'state'); parent::__construct('#__scheduler_tasks', 'id', $db); } /** * Overloads {@see Table::check()} to perform sanity checks on properties and make sure they're * safe to store. * * @return boolean True if checks pass. * * @since 4.1.0 * @throws \Exception */ public function check(): bool { try { parent::check(); } catch (\Exception $e) { Factory::getApplication()->enqueueMessage($e->getMessage()); return false; } $this->title = htmlspecialchars_decode($this->title, ENT_QUOTES); // Set created date if not set. // ? Might not need since the constructor already sets this if (!(int) $this->created) { $this->created = Factory::getDate()->toSql(); } // @todo : Add more checks if needed return true; } /** * Override {@see Table::store()} to update null fields as a default, which is needed when DATETIME * fields need to be updated to NULL. This override is needed because {@see AdminModel::save()} does not * expose an option to pass true to Table::store(). Also ensures the `created` and `created_by` fields are * set. * * @param boolean $updateNulls True to update fields even if they're null. * * @return boolean True if successful. * * @since 4.1.0 * @throws \Exception */ public function store($updateNulls = true): bool { $isNew = empty($this->getId()); // Set creation date if not set for a new item. if ($isNew && empty($this->created)) { $this->created = Factory::getDate()->toSql(); } // Set `created_by` if not set for a new item. if ($isNew && empty($this->created_by)) { $this->created_by = Factory::getApplication()->getIdentity()->id; } // @todo : Should we add modified, modified_by fields? [ ] return parent::store($updateNulls); } /** * Returns the asset name of the entry as it appears in the {@see Asset} table. * * @return string The asset name. * * @since 4.1.0 */ protected function _getAssetName(): string { $k = $this->_tbl_key; return 'com_scheduler.task.' . (int) $this->$k; } /** * Override {@see Table::bind()} to bind some fields even if they're null given they're present in $src. * This override is needed specifically for DATETIME fields, of which the `next_execution` field is updated to * null if a task is configured to execute only on manual trigger. * * @param array|object $src An associative array or object to bind to the Table instance. * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. * * @return boolean * * @since 4.1.0 */ public function bind($src, $ignore = []): bool { $fields = ['next_execution']; foreach ($fields as $field) { if (\array_key_exists($field, $src) && \is_null($src[$field])) { $this->$field = $src[$field]; } } return parent::bind($src, $ignore); } /** * Release pseudo-locks on a set of task records. If an empty set is passed, this method releases lock on its * instance primary key, if available. * * @param integer[] $pks An optional array of primary key values to update. If not set the instance property * value is used. * @param ?int $userId ID of the user unlocking the tasks. * * @return boolean True on success; false if $pks is empty. * * @since 4.1.0 * @throws QueryTypeAlreadyDefinedException|\UnexpectedValueException|\BadMethodCallException */ public function unlock(array $pks = [], ?int $userId = null): bool { // Pre-processing by observers $event = AbstractEvent::create( 'onTaskBeforeUnlock', [ 'subject' => $this, 'pks' => $pks, 'userId' => $userId, ] ); $this->getDispatcher()->dispatch('onTaskBeforeUnlock', $event); // Some pre-processing before we can work with the keys. if (!empty($pks)) { foreach ($pks as $key => $pk) { if (!\is_array($pk)) { $pks[$key] = [$this->_tbl_key => $pk]; } } } // If there are no primary keys set check to see if the instance key is set and use that. if (empty($pks)) { $pk = []; foreach ($this->_tbl_keys as $key) { if ($this->$key) { $pk[$key] = $this->$key; } else { // We don't have a full primary key - return false. $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); return false; } } $pks = [$pk]; } $lockedField = $this->getColumnAlias('locked'); foreach ($pks as $pk) { // Update the publishing state for rows with the given primary keys. $query = $this->_db->getQuery(true) ->update($this->_tbl) ->set($this->_db->quoteName($lockedField) . ' = NULL'); // Build the WHERE clause for the primary keys. $this->appendPrimaryKeys($query, $pk); $this->_db->setQuery($query); try { $this->_db->execute(); } catch (\RuntimeException $e) { $this->setError($e->getMessage()); return false; } // If the Table instance value is in the list of primary keys that were set, set the instance. $ours = true; foreach ($this->_tbl_keys as $key) { if ($this->$key != $pk[$key]) { $ours = false; } } if ($ours) { $this->$lockedField = null; } } // Pre-processing by observers $event = AbstractEvent::create( 'onTaskAfterUnlock', [ 'subject' => $this, 'pks' => $pks, 'userId' => $userId, ] ); $this->getDispatcher()->dispatch('onTaskAfterUnlock', $event); return true; } } tmpl/task/edit.php 0000644 00000021245 15172736354 0010141 0 ustar 00 <?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 */ // Restrict direct access defined('_JEXEC') or die; use Joomla\CMS\Application\AdministratorApplication; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Router\Route; use Joomla\Component\Scheduler\Administrator\Task\TaskOption; use Joomla\Component\Scheduler\Administrator\View\Task\HtmlView; /** @var HtmlView $this */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive'); $wa->useScript('form.validate'); $wa->useStyle('com_scheduler.admin-view-task-css'); /** @var AdministratorApplication $app */ $app = $this->app; $input = $app->getInput(); // Fieldsets to be ignored by the `joomla.edit.params` template. $this->ignore_fieldsets = ['aside', 'details', 'exec_hist', 'custom-cron-rules', 'basic', 'advanced', 'priority']; // Used by the `joomla.edit.params` template to render the right template for UI tabs. $this->useCoreUI = true; $advancedFieldsets = $this->form->getFieldsets('params'); // Don't show the params fieldset, they will be loaded later foreach ($advancedFieldsets as $name => $fieldset) : if ($name === 'task_params') : unset($advancedFieldsets[$name]); continue; endif; $this->ignore_fieldsets[] = $fieldset->name; endforeach; ?> <form action="<?php echo Route::_('index.php?option=com_scheduler&view=task&layout=edit&id=' . (int) $this->item->id); ?>" method="post" name="adminForm" id="task-form" aria-label="<?php echo Text::_('COM_SCHEDULER_FORM_TITLE_' . ((int) $this->item->id === 0 ? 'NEW' : 'EDIT'), true); ?>" class="form-validate"> <!-- The task title field --> <?php echo LayoutHelper::render('joomla.edit.title_alias', $this); ?> <!-- The main form card --> <div class="main-card"> <?php echo HTMLHelper::_('uitab.startTabSet', 'myTab', ['active' => 'general']); ?> <!-- The first (and the main) tab in the form --> <?php echo HTMLHelper::_( 'uitab.addTab', 'myTab', 'general', empty($this->item->id) ? Text::_('COM_SCHEDULER_NEW_TASK') : Text::_('COM_SCHEDULER_EDIT_TASK') ); ?> <div class="row"> <div class="col-lg-9"> <!-- Task type title, description go here --> <?php if ($this->item->taskOption) : /** @var TaskOption $taskOption */ $taskOption = $this->item->taskOption; ?> <div id="taskOptionInfo"> <h2 id="taskOptionTitle"> <?php echo $taskOption->title ?> </h2> <?php $this->fieldset = 'description'; $short_description = $taskOption->desc; $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); if (!$long_description) { $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); if (strlen($truncated) > 500) { $long_description = $short_description; $short_description = HTMLHelper::_('string.truncate', $truncated, 250); if ($short_description == $long_description) { $long_description = ''; } } } ?> <p><?php echo $short_description; ?></p> <?php if ($long_description) : ?> <p class="readmore"> <a href="#" onclick="document.getElementById('myTab').activateTab(document.getElementById('description'));"> <?php echo Text::_('JGLOBAL_SHOW_FULL_DESCRIPTION'); ?> </a> </p> <?php endif; ?> </div> <!-- If TaskOption does not exist --> <?php else : $app->enqueueMessage(Text::_('COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND'), 'warning'); ?> <?php endif; ?> <fieldset class="options-form"> <legend><?php echo Text::_('COM_SCHEDULER_FIELDSET_BASIC'); ?></legend> <?php echo $this->form->renderFieldset('basic'); ?> </fieldset> <fieldset class="options-form match-custom" data-showon='[{"field":"jform[execution_rules][rule-type]","values":["cron-expression"],"sign":"=","op":""}]' > <legend><?php echo Text::_('COM_SCHEDULER_FIELDSET_CRON_OPTIONS'); ?></legend> <?php echo $this->form->renderFieldset('custom-cron-rules'); ?> </fieldset> <?php echo LayoutHelper::render('joomla.edit.params', $this); ?> </div> <div class="col-lg-3"> <?php echo $this->form->renderFieldset('aside'); ?> </div> </div> <?php echo HTMLHelper::_('uitab.endTab'); ?> <?php if (isset($long_description) && $long_description != '') : ?> <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'description', Text::_('JGLOBAL_FIELDSET_DESCRIPTION')); ?> <div class="card"> <div class="card-body"> <?php echo $long_description; ?> </div> </div> <?php echo HTMLHelper::_('uitab.endTab'); ?> <?php endif; ?> <!-- Tab for advanced options --> <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'advanced', Text::_('JGLOBAL_FIELDSET_ADVANCED')) ?> <div class="row"> <div class="col-lg-9"> <fieldset class="options-form"> <legend><?php echo Text::_('COM_SCHEDULER_FIELDSET_PRIORITY') ?></legend> <?php echo $this->form->renderFieldset('priority') ?> </fieldset> <?php foreach ($advancedFieldsets as $fieldset) : ?> <fieldset class="options-form"> <legend><?php echo Text::_($fieldset->label ?: 'COM_SCHEDULER_FIELDSET_' . $fieldset->name) ?></legend> <?php echo $this->form->renderFieldset($fieldset->name) ?> </fieldset> <?php endforeach; ?> </div> </div> <?php echo HTMLHelper::_('uitab.endTab') ?> <!-- Tab to show execution history --> <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'exec_hist', Text::_('COM_SCHEDULER_FIELDSET_EXEC_HIST')); ?> <div class="row"> <div class="col-lg-9"> <fieldset class="options-form"> <legend><?php echo Text::_('COM_SCHEDULER_FIELDSET_EXEC_HIST'); ?></legend> <?php echo $this->form->renderFieldset('exec_hist'); ?> </fieldset> </div> </div> <?php echo HTMLHelper::_('uitab.endTab'); ?> <!-- Tab to show creation details--> <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'details', Text::_('JDETAILS')); ?> <div class="row"> <div class="col-lg-9"> <fieldset class="options-form"> <legend><?php echo Text::_('JDETAILS'); ?></legend> <?php echo $this->form->renderFieldset('details'); ?> </fieldset> </div> </div> <?php echo HTMLHelper::_('uitab.endTab'); ?> <!-- Item permissions tab, if user has admin privileges --> <?php if ($this->canDo->get('core.admin')) : ?> <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'permissions', Text::_('JCONFIG_PERMISSIONS_LABEL')); ?> <fieldset id="fieldset-permissions" class="options-form"> <legend><?php echo Text::_('JCONFIG_PERMISSIONS_LABEL'); ?></legend> <div> <?php echo $this->form->getInput('rules'); ?> </div> </fieldset> <?php echo HTMLHelper::_('uitab.endTab'); ?> <?php endif; ?> <?php echo HTMLHelper::_('uitab.endTabSet'); ?> <?php echo $this->form->getInput('context'); ?> <input type="hidden" name="task" value=""> <?php echo HTMLHelper::_('form.token'); ?> </div> </form> tmpl/select/default.php 0000644 00000006660 15172736354 0011161 0 ustar 00 <?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 */ /** The SelectView default layout template. */ // Restrict direct access defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; use Joomla\Component\Scheduler\Administrator\View\Select\HtmlView; /** @var HtmlView $this */ $app = $this->app; $wa = $this->document->getWebAssetManager(); $wa->useStyle('com_scheduler.admin-view-select-task-css'); $wa->useScript('com_scheduler.admin-view-select-task-search'); ?> <!-- Tasks search box on below the toolbar begins --> <div class="d-none" id="comSchedulerSelectSearchContainer"> <div class="d-flex mt-2"> <div class="m-auto"> <label class="visually-hidden" for="comSchedulerSelectSearch"> <?php echo Text::_('COM_SCHEDULER_TYPE_CHOOSE'); ?> </label> <div class="input-group mb-3 me-sm-2"> <input type="text" value="" class="form-control" id="comSchedulerSelectSearch" placeholder="<?php echo Text::_('JSEARCH_FILTER'); ?>" > <div class="input-group-text"> <span class="icon-search" aria-hidden="true"></span> </div> </div> </div> </div> </div> <!-- Search box and related elements end --> <div id="new-tasks-list"> <div class="new-tasks"> <!-- Hidden alert div --> <div class="tasks-alert alert alert-info d-none"> <span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span> <?php echo Text::_('COM_SCHEDULER_MSG_MANAGE_NO_TASK_PLUGINS'); ?> </div> <h2 class="pb-3 ms-3" id="comSchedulerSelectTypeHeader"> <?php echo Text::_('COM_SCHEDULER_TYPE_CHOOSE'); ?> </h2> <!-- Parent card --> <div class="main-card card-columns p-4" id="comSchedulerSelectResultsContainer"> <!-- Plugin task cards start below --> <?php foreach ($this->items as $item) : ?> <?php // Prepare variables for the link. ?> <?php $link = 'index.php?option=com_scheduler&task=task.add&type=' . $item->id; ?> <?php $name = $this->escape($item->title); ?> <?php $desc = HTMLHelper::_('string.truncate', $this->escape(strip_tags($item->desc)), 200); ?> <!-- The task card begins --> <a href="<?php echo Route::_($link); ?>" class="new-task mb-3 comSchedulerSelectCard" aria-label="<?php echo Text::sprintf('COM_SCHEDULER_SELECT_TASK_TYPE', $name); ?>"> <div class="new-task-details"> <h3 class="new-task-title"><?php echo $name; ?></h3> <p class="card-body new-task-caption p-0"> <?php echo $desc; ?> </p> </div> <span class="new-task-link"> <span class="icon-plus" aria-hidden="true"></span> </span> </a> <!-- The task card ends here --> <?php endforeach; ?> </div> </div> </div> tmpl/tasks/empty_state.php 0000644 00000001472 15172736354 0011735 0 ustar 00 <?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 */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Layout\LayoutHelper; $displayData = [ 'textPrefix' => 'COM_SCHEDULER', 'formURL' => 'index.php?option=com_scheduler&task=task.add', 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J4.x:Task_Scheduler', 'icon' => 'icon-clock clock', ]; if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_scheduler')) { $displayData['createURL'] = 'index.php?option=com_scheduler&view=select&layout=default'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); tmpl/tasks/default.php 0000644 00000033723 15172736354 0011027 0 ustar 00 <?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 */ // Restrict direct access defined('_JEXEC') or die; use Joomla\CMS\Application\CMSWebApplicationInterface; use Joomla\CMS\Factory; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Router\Route; use Joomla\CMS\Session\Session; use Joomla\Component\Scheduler\Administrator\Task\Status; use Joomla\Component\Scheduler\Administrator\View\Tasks\HtmlView; /** @var HtmlView $this*/ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('table.columns') ->useScript('multiselect') ->useScript('com_scheduler.test-task') ->useStyle('com_scheduler.admin-view-tasks-css'); Text::script('COM_SCHEDULER_TEST_RUN_TITLE'); Text::script('COM_SCHEDULER_TEST_RUN_TASK'); Text::script('COM_SCHEDULER_TEST_RUN_DURATION'); Text::script('COM_SCHEDULER_TEST_RUN_OUTPUT'); Text::script('COM_SCHEDULER_TEST_RUN_STATUS_STARTED'); Text::script('COM_SCHEDULER_TEST_RUN_STATUS_COMPLETED'); Text::script('COM_SCHEDULER_TEST_RUN_STATUS_TERMINATED'); Text::script('JLIB_JS_AJAX_ERROR_OTHER'); Text::script('JLIB_JS_AJAX_ERROR_CONNECTION_ABORT'); Text::script('JLIB_JS_AJAX_ERROR_TIMEOUT'); Text::script('JLIB_JS_AJAX_ERROR_NO_CONTENT'); Text::script('JLIB_JS_AJAX_ERROR_PARSE'); try { /** @var CMSWebApplicationInterface $app */ $app = Factory::getApplication(); } catch (Exception $e) { die('Failed to get app'); } $user = $app->getIdentity(); $userId = $user->get('id'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; $section = null; $mode = false; if ($saveOrder && !empty($this->items)) { $saveOrderingUrl = 'index.php?option=com_scheduler&task=tasks.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; HTMLHelper::_('draggablelist.draggable'); } $this->document->addScriptOptions('com_scheduler.test-task.token', Session::getFormToken()); ?> <form action="<?php echo Route::_('index.php?option=com_scheduler&view=tasks'); ?>" method="post" name="adminForm" id="adminForm"> <div id="j-main-container" class="j-main-container"> <?php // Search tools bar echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?> <!-- If no tasks --> <?php if (empty($this->items)) : ?> <!-- No tasks --> <div class="alert alert-info"> <span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span> <?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?> </div> <?php endif; ?> <!-- If there are tasks, we start with the table --> <?php if (!empty($this->items)) : ?> <!-- Tasks table starts here --> <table class="table" id="categoryList"> <caption class="visually-hidden"> <?php echo Text::_('COM_SCHEDULER_TABLE_CAPTION'); ?>, <span id="orderedBy"><?php echo Text::_('JGLOBAL_SORTED_BY'); ?> </span>, <span id="filteredBy"><?php echo Text::_('JGLOBAL_FILTERED_BY'); ?></span> </caption> <!-- Tasks table header --> <thead> <tr> <!-- Select all --> <td class="w-1 text-center"> <?php echo HTMLHelper::_('grid.checkall'); // "Select all" checkbox ?> </td> <!-- Ordering?--> <th scope="col" class="w-1 d-none d-md-table-cell text-center"> <!-- Might need to adjust method args here --> <?php echo HTMLHelper::_('searchtools.sort', '', 'a.ordering', $listDirn, $listOrder, null, 'asc', 'JGRID_HEADING_ORDERING', 'icon-sort'); ?> </th> <!-- Task State --> <th scope="col" class="w-1 text-center"> <?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.state', $listDirn, $listOrder); ?> </th> <!-- Task title header --> <th scope="col"> <?php echo HTMLHelper::_('searchtools.sort', 'JGLOBAL_TITLE', 'a.title', $listDirn, $listOrder); ?> </th> <!-- Task type header --> <th scope="col" class="d-none d-md-table-cell"> <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_TASK_TYPE', 'j.type_title', $listDirn, $listOrder) ?> </th> <!-- Last runs --> <th scope="col" class="d-none d-lg-table-cell"> <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LAST_RUN_DATE', 'a.last_execution', $listDirn, $listOrder) ?> </th> <!-- Test task --> <th scope="col" class="d-none d-md-table-cell"> <?php echo Text::_('COM_SCHEDULER_TEST_TASK'); ?> </th> <!-- Priority --> <th scope="col" class="d-none d-lg-table-cell"> <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_TASK_PRIORITY', 'a.priority', $listDirn, $listOrder) ?> </th> <!-- Task ID --> <th scope="col" class="w-5 d-none d-md-table-cell"> <?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?> </th> </tr> </thead> <!-- Table body begins --> <tbody <?php if ($saveOrder) : ?> class="js-draggable" data-url="<?php echo $saveOrderingUrl; ?>" data-direction="<?php echo strtolower($listDirn); ?>" data-nested="true" <?php endif; ?>> <?php foreach ($this->items as $i => $item) : $canCreate = $user->authorise('core.create', 'com_scheduler'); $canEdit = $user->authorise('core.edit', 'com_scheduler'); $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); $canChange = $user->authorise('core.edit.state', 'com_scheduler') && $canCheckin; ?> <!-- Row begins --> <tr class="row<?php echo $i % 2; ?>" data-draggable-group="none" > <!-- Item Checkbox --> <td class="text-center"> <?php echo HTMLHelper::_('grid.id', $i, $item->id, false, 'cid', 'cb', $item->title); ?> </td> <!-- Draggable handle --> <td class="text-center d-none d-md-table-cell"> <?php $iconClass = ''; if (!$canChange) { $iconClass = ' inactive'; } elseif (!$saveOrder) { $iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED'); } ?> <span class="sortable-handler <?php echo $iconClass ?>"> <span class="icon-ellipsis-v" aria-hidden="true"></span> </span> <?php if ($canChange && $saveOrder) : ?> <input type="text" class="hidden text-area-order" name="order[]" size="5" value="<?php echo $item->ordering; ?>" > <?php endif; ?> </td> <!-- Item State --> <td class="text-center"> <?php echo HTMLHelper::_('jgrid.published', $item->state, $i, 'tasks.', $canChange); ?> </td> <!-- Item name, edit link, and note (@todo: should it be moved?) --> <th scope="row"> <?php if ($item->checked_out) : ?> <?php echo HTMLHelper::_('jgrid.checkedout', $i, $item->editor, $item->checked_out_time, 'tasks.', $canCheckin); ?> <?php endif; ?> <?php if ($item->locked) : ?> <?php echo HTMLHelper::_('jgrid.action', $i, 'unlock', ['enabled' => $canChange, 'prefix' => 'tasks.', 'active_class' => 'none fa fa-running border-dark text-body', 'inactive_class' => 'none fa fa-running', 'tip' => true, 'translate' => false, 'active_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), 'inactive_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), ]); ?> <?php endif; ?> <span class="task-title"> <?php if ($canEdit) : ?> <a href="<?php echo Route::_('index.php?option=com_scheduler&task=task.edit&id=' . $item->id); ?>" title="<?php echo Text::_('JACTION_EDIT'); ?> <?php echo $this->escape($item->title); ?>"> <?php echo $this->escape($item->title); ?> </a> <?php else : ?> <?php echo $this->escape($item->title); ?> <?php endif; ?> <?php if (!in_array($item->last_exit_code, [Status::OK, Status::WILL_RESUME])) : ?> <span class="failure-indicator icon-exclamation-triangle" aria-hidden="true"></span> <div role="tooltip"> <?php echo Text::sprintf("COM_SCHEDULER_MANAGER_TOOLTIP_TASK_FAILING", $item->last_exit_code); ?> </div> <?php endif; ?> </span> <?php if ($item->note) : ?> <span class="small"> <?php echo Text::sprintf('JGLOBAL_LIST_NOTE', $this->escape($item->note)); ?> </span> <?php endif; ?> </th> <!-- Item type --> <td class="small d-none d-md-table-cell"> <?php echo $this->escape($item->safeTypeTitle); ?> </td> <!-- Last run date --> <td class="small d-none d-lg-table-cell"> <?php echo $item->last_execution ? HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5') : '-'; ?> </td> <!-- Test task --> <td class="small d-none d-md-table-cell"> <button type="button" class="btn btn-sm btn-warning" <?php echo $item->state < 0 ? 'disabled' : ''; ?> data-id="<?php echo (int) $item->id; ?>" data-title="<?php echo htmlspecialchars($item->title); ?>" data-bs-toggle="modal" data-bs-backdrop="static" data-bs-target="#scheduler-test-modal"> <span class="fa fa-play fa-sm me-2"></span> <?php echo Text::_('COM_SCHEDULER_TEST_RUN'); ?> </button> </td> <!-- Priority --> <td class="small d-none d-lg-table-cell"> <?php if ($item->priority === -1) : ?> <span class="badge bg-info"><?php echo Text::_('COM_SCHEDULER_LABEL_TASK_PRIORITY_LOW'); ?></span> <?php elseif ($item->priority === 0) : ?> <span class="badge bg-success"><?php echo Text::_('COM_SCHEDULER_LABEL_TASK_PRIORITY_NORMAL'); ?></span> <?php elseif ($item->priority === 1) : ?> <span class="badge bg-danger"><?php echo Text::_('COM_SCHEDULER_LABEL_TASK_PRIORITY_HIGH'); ?></span> <?php endif; ?> </td> <!-- Item ID --> <td class="d-none d-md-table-cell"> <?php echo (int) $item->id; ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php // Load the pagination. (@todo: testing) echo $this->pagination->getListFooter(); // Modal for test runs $modalparams = [ 'title' => '', ]; $modalbody = '<div class="p-3"></div>'; echo HTMLHelper::_('bootstrap.renderModal', 'scheduler-test-modal', $modalparams, $modalbody); ?> <?php endif; ?> <input type="hidden" name="task" value=""> <input type="hidden" name="boxchecked" value="0"> <?php echo HTMLHelper::_('form.token'); ?> </div> </form> tmpl/tasks/default.xml 0000644 00000000320 15172736354 0011023 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <metadata> <layout title="COM_SCHEDULER_TASKS_VIEW_DEFAULT_TITLE"> <message> <![CDATA[COM_SCHEDULER_TASKS_VIEW_DEFAULT_DESC]]> </message> </layout> </metadata> services/provider.php 0000644 00000004254 15172736354 0010754 0 ustar 00 <?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 */ // Restrict direct access defined('_JEXEC') or die; use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface; use Joomla\CMS\Extension\ComponentInterface; use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory; use Joomla\CMS\Extension\Service\Provider\MVCFactory; use Joomla\CMS\HTML\Registry; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\Component\Scheduler\Administrator\Extension\SchedulerComponent; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; /** * The com_scheduler service provider. * Returns an instance of the Component's Service Provider Interface * used to register the components initializers into a DI container * created by the application. * * @since 4.1.0 */ return new class () implements ServiceProviderInterface { /** * Registers the service provider with a DI container. * * @param Container $container The DI container. * * @return void * * @since 4.1.0 */ public function register(Container $container) { /** * Register the MVCFactory and ComponentDispatcherFactory providers to map * 'MVCFactoryInterface' and 'ComponentDispatcherFactoryInterface' to their * initializers and register them with the component's DI container. */ $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Scheduler')); $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Scheduler')); $container->set( ComponentInterface::class, function (Container $container) { $component = new SchedulerComponent($container->get(ComponentDispatcherFactoryInterface::class)); $component->setRegistry($container->get(Registry::class)); $component->setMVCFactory($container->get(MVCFactoryInterface::class)); return $component; } ); } }; layouts/form/field/webcron_link.php 0000644 00000003000 15172736354 0013465 0 ustar 00 <?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 */ defined('_JEXEC') or die; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; extract($displayData); /** * Layout variables * ----------------- * * @var string $id DOM id of the field. * @var string $label Label of the field. * @var string $name Name of the input field. * @var string $value Value attribute of the field. */ Text::script('ERROR'); Text::script('MESSAGE'); Text::script('COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_SUCCESS'); Text::script('COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_FAIL'); /** @var CMSApplication $app */ $app = Factory::getApplication(); $wa = $app->getDocument()->getWebAssetManager(); $wa->getRegistry()->addExtensionRegistryFile('com_scheduler'); $wa->useScript('com_scheduler.scheduler-config'); ?> <div class="input-group"> <input type="text" class="form-control" name="<?php echo $name; ?>" id="<?php echo $id; ?>" readonly value="<?php echo htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); ?>" > <button class="btn btn-primary" type="button" id="link-copy" title="<?php echo Text::_('COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_DESC'); ?>"><?php echo Text::_('JLIB_HTML_BATCH_COPY'); ?> </button> </div> forms/task.xml 0000644 00000013570 15172736354 0007401 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <form addfieldprefix="Joomla\Component\Scheduler\Administrator\Field" addruleprefix="Joomla\Component\Scheduler\Administrator\Rule" > <fields> <field name="title" type="text" label="JGLOBAL_TITLE" size="40" maxlength="100" required="true" /> <fieldset name="aside"> <field name="state" type="taskState" label="JSTATUS" default="1" class="form-select-color-state" size="1" validate="options" optionsFilter="-2,0,1" /> <field name="note" type="text" label="COM_SCHEDULER_LABEL_NOTES" maxlength="255" /> </fieldset> <fieldset name="exec_hist"> <field name="last_exit_code" type="number" label="COM_SCHEDULER_LABEL_EXIT_CODE" default="0" disabled="true" filter="unset" /> <field name="last_execution" type="text" label="COM_SCHEDULER_LABEL_LAST_EXEC" disabled="true" filter="unset" /> <field name="next_execution" type="text" label="COM_SCHEDULER_LABEL_NEXT_EXEC" disabled="true" filter="unset" /> <field name="times_executed" type="number" label="COM_SCHEDULER_LABEL_TIMES_EXEC" disabled="true" filter="unset" /> <field name="times_failed" type="number" label="COM_SCHEDULER_LABEL_TIMES_FAIL" disabled="true" filter="unset" /> </fieldset> <fieldset name="details"> <field name="id" type="text" label="JGLOBAL_FIELD_ID_LABEL" default="0" class="readonly" readonly="true" /> <field name="type" type="text" label="COM_SCHEDULER_FIELD_TASK_TYPE" required="true" readonly="true" maxlength="1024" /> <field name="asset_id" type="hidden" filter="unset" /> <field name="created" type="calendar" label="JGLOBAL_CREATED" size="22" translateformat="true" showtime="true" filter="user_utc" /> <field name="created_by" type="user" label="JGLOBAL_FIELD_CREATED_BY_LABEL" validate="UserId" /> </fieldset> <field name="rules" type="rules" label="JFIELD_RULES_LABEL" translate_label="false" filter="rules" component="com_scheduler" section="task" validate="rules" /> </fields> <fields name="execution_rules"> <fieldset name="basic"> <field name="rule-type" type="ExecutionRule" label="COM_SCHEDULER_FIELD_LABEL_EXEC_RULE" required="true" validate="options" /> <field name="interval-minutes" type="interval" label="COM_SCHEDULER_FIELD_LABEL_INTERVAL_MINUTES" subtype="minutes" validate="ExecutionRules" showon="rule-type:interval-minutes"> <option value="" disabled="true" hidden="true"> COM_SCHEDULER_SELECT_INTERVAL_MINUTES </option> </field> <field name="interval-hours" type="interval" label="COM_SCHEDULER_FIELD_LABEL_INTERVAL_HOURS" subtype="hours" validate="ExecutionRules" showon="rule-type:interval-hours" /> <field name="interval-days" type="interval" label="COM_SCHEDULER_FIELD_LABEL_INTERVAL_DAYS" subtype="days" validate="ExecutionRules" showon="rule-type:interval-days" /> <field name="interval-months" type="interval" label="COM_SCHEDULER_FIELD_LABEL_INTERVAL_MONTHS" subtype="months" onlyNumericLabels="true" validate="ExecutionRules" showon="rule-type:interval-months" /> <field name="exec-day" type="number" label="COM_SCHEDULER_LABEL_EXEC_DAY" min="1" max="31" step="1" showon="rule-type:interval-months" /> <field name="exec-time" type="time" label="COM_SCHEDULER_LABEL_EXEC_TIME" showon="rule-type:interval-days[OR]rule-type:interval-months" /> </fieldset> <fieldset name="custom-cron-rules" label="COM_SCHEDULER_LABEL_EXEC_INTERVAL" > <fields name="cron-expression"> <field name="minutes" type="cron" label="COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_MINUTES" subtype="minutes" multiple="true" validate="ExecutionRules" /> <field name="hours" type="cron" label="COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_HOURS" subtype="hours" multiple="true" validate="ExecutionRules" /> <field name="days_month" type="cron" label="COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_DAYS_M" subtype="days_month" multiple="true" validate="ExecutionRules" /> <field name="months" type="cron" label="COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_MONTHS" subtype="months" multiple="true" validate="ExecutionRules" /> <field name="days_week" type="cron" label="COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_DAYS_W" subtype="days_week" multiple="true" validate="ExecutionRules" /> </fields> </fieldset> </fields> <fieldset name="priority"> <field name="priority" type="list" label="COM_SCHEDULER_LABEL_TASK_PRIORITY" description="COM_SCHEDULER_DESCRIPTION_TASK_PRIORITY" validate="options" required="true" default="0" > <option value="-1">COM_SCHEDULER_LABEL_TASK_PRIORITY_LOW</option> <option value="0">COM_SCHEDULER_LABEL_TASK_PRIORITY_NORMAL</option> <option value="1">COM_SCHEDULER_LABEL_TASK_PRIORITY_HIGH</option> </field> </fieldset> <fields name="params"> <fieldset name="logging"> <field name="individual_log" type="radio" label="COM_SCHEDULER_FIELD_LABEL_INDIVIDUAL_LOG" layout="joomla.form.field.radio.switcher" default="0" required="true" filter="boolean" > <option value="0">JDISABLED</option> <option value="1">JENABLED</option> </field> <field name="log_file" type="text" label="COM_SCHEDULER_FIELD_LABEL_LOG_FILE" showon="individual_log:1" hint="COM_SCHEDULER_FIELD_HINT_LOG_FILE_AUTO" /> </fieldset> </fields> </form> forms/filter_tasks.xml 0000644 00000004557 15172736354 0011136 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <form addfieldprefix="Joomla\Component\Scheduler\Administrator\Field"> <fields name="filter"> <field name="search" type="text" label="COM_SCHEDULER_FILTER_SEARCH_LABEL" description="COM_SCHEDULER_FILTER_SEARCH_DESC" inputmode="search" hint="JSEARCH_FILTER" /> <field name="state" type="taskState" label="JSTATUS" class="js-select-submit-on-change" validate="options" > <option value="">JOPTION_SELECT_PUBLISHED</option> </field> <field name="type" type="taskType" label="COM_SCHEDULER_HEADING_TASK_TYPE" class="js-select-submit-on-change" > <option value="">COM_SCHEDULER_SELECT_TYPE</option> </field> <field name="orphaned" type="list" label="COM_SCHEDULER_FIELD_LABEL_SHOW_ORPHANED" default="-1" class="js-select-submit-on-change" > <option value="-1">COM_SCHEDULER_OPTION_ORPHANED_HIDE</option> <option value="0">COM_SCHEDULER_OPTION_ORPHANED_SHOW</option> <option value="1">COM_SCHEDULER_OPTION_ORPHANED_ONLY</option> </field> </fields> <fields name="list"> <field name="fullordering" type="list" label="JGLOBAL_SORT_BY" class="js-select-submit-on-change" default="a.title ASC" validate="options" > <option value="">JGLOBAL_SORT_BY</option> <option value="a.ordering ASC">JGRID_HEADING_ORDERING_ASC</option> <option value="a.ordering DESC">JGRID_HEADING_ORDERING_DESC</option> <option value="a.state ASC">JSTATUS_ASC</option> <option value="a.state DESC">JSTATUS_DESC</option> <option value="a.title ASC">JGLOBAL_TITLE_ASC</option> <option value="a.title DESC">JGLOBAL_TITLE_DESC</option> <option value="j.type_title ASC">COM_SCHEDULER_TASK_TYPE_ASC</option> <option value="j.type_title DESC">COM_SCHEDULER_TASK_TYPE_DESC</option> <option value="a.last_execution ASC">COM_SCHEDULER_LAST_RUN_ASC</option> <option value="a.last_execution DESC">COM_SCHEDULER_LAST_RUN_DESC</option> <option value="a.priority ASC">COM_SCHEDULER_TASK_PRIORITY_ASC</option> <option value="a.priority DESC">COM_SCHEDULER_TASK_PRIORITY_DESC</option> <option value="a.id ASC">JGRID_HEADING_ID_ASC</option> <option value="a.id DESC">JGRID_HEADING_ID_DESC</option> </field> <field name="limit" type="limitbox" label="JGLOBAL_LIST_LIMIT" default="25" class="js-select-submit-on-change" /> </fields> </form> access.xml 0000644 00000001537 15172736354 0006552 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <access component="com_scheduler"> <section name="component"> <action name="core.admin" title="JACTION_ADMIN" /> <action name="core.options" title="JACTION_OPTIONS" /> <action name="core.manage" title="JACTION_MANAGE" /> <action name="core.create" title="JACTION_CREATE" /> <action name="core.delete" title="JACTION_DELETE" /> <action name="core.edit" title="JACTION_EDIT" /> <action name="core.edit.state" title="JACTION_EDITSTATE" /> <action name="core.testrun" title="COM_SCHEDULER_PERMISSION_TESTRUN" /> </section> <section name="task"> <action name="core.delete" title="JACTION_DELETE" /> <action name="core.edit" title="JACTION_EDIT" /> <action name="core.edit.state" title="JACTION_EDITSTATE" /> <action name="core.testrun" title="COM_SCHEDULER_PERMISSION_TESTRUN" /> </section> </access> scheduler.xml 0000644 00000002121 15172736354 0007255 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <extension type="component" method="upgrade"> <name>com_scheduler</name> <author>Joomla! Project</author> <creationDate>2021-07</creationDate> <copyright>(C) 2021 Open Source Matters, Inc.</copyright> <license>GNU General Public License version 2 or later; see LICENSE.txt</license> <authorEmail>admin@joomla.org</authorEmail> <authorUrl>www.joomla.org</authorUrl> <version>4.1.0</version> <description>COM_SCHEDULER_XML_DESCRIPTION</description> <namespace path="src">Joomla\Component\Scheduler</namespace> <media destination="com_scheduler" folder="media"> <folder>js</folder> <folder>css</folder> </media> <administration> <filename>access.xml</filename> <filename>config.xml</filename> <filename>scheduler.xml</filename> <folder>forms</folder> <folder>services</folder> <folder>src</folder> <folder>tmpl</folder> <languages folder="admin"> <language tag="en-GB">language/en-GB/com_scheduler.ini</language> <language tag="en-GB">language/en-GB/com_scheduler.sys.ini</language> </languages> </administration> </extension> config.xml 0000644 00000006037 15172736354 0006556 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <config> <help key="Scheduled_Tasks:_Options"/> <inlinehelp button="show"/> <fieldset name="task_config" label="COM_SCHEDULER_CONFIG_TASKS_FIELDSET_LABEL" > <field name="timeout" type="number" label="COM_SCHEDULER_CONFIG_TASK_TIMEOUT_LABEL" default="300" required="true" min="10" step="1" validate="number" filter="int" /> </fieldset> <fieldset name="lazy_scheduler_config" label="COM_SCHEDULER_CONFIG_FIELDSET_LAZY_SCHEDULER_LABEL" description="COM_SCHEDULER_CONFIG_FIELDSET_LAZY_SCHEDULER_DESC" > <fields name="lazy_scheduler"> <field name="enabled" type="radio" label="COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_ENABLED_LABEL" description="COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_ENABLED_DESC" layout="joomla.form.field.radio.switcher" default="1" required="true" filter="integer" > <option value="0">JDISABLED</option> <option value="1">JENABLED</option> </field> <field name="interval" type="NumberField" label="COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_INTERVAL_LABEL" description="COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_INTERVAL_DESC" showon="enabled:1" min="60" max="16383" step="1" default="300" /> </fields> </fieldset> <fieldset name="webcron_config" label="COM_SCHEDULER_CONFIG_WEBCRON_LABEL" description="COM_SCHEDULER_CONFIG_WEBCRON_DESC" > <fields name="webcron" addfieldprefix="Joomla\Component\Scheduler\Administrator\Field"> <field name="enabled" type="radio" label="COM_SCHEDULER_CONFIG_WEBCRON_LABEL" description="COM_SCHEDULER_CONFIG_HASH_PROTECTION_DESC" layout="joomla.form.field.radio.switcher" default="0" required="true" filter="integer" > <option value="0">JDISABLED</option> <option value="1">JENABLED</option> </field> <field name="generate_key_on_save" type="note" description="COM_SCHEDULER_CONFIG_GENERATE_WEBCRON_KEY_DESC" class="alert alert-warning" showon="enabled:1" /> <field name="key" type="hidden" label="COM_SCHEDULER_CONFIG_GLOBAL_WEBCRON_KEY_LABEL" readonly="true" hidden="true" /> <field name="base_link" type="WebcronLink" label="COM_SCHEDULER_CONFIG_GLOBAL_WEBCRON_LINK_LABEL" description="COM_SCHEDULER_CONFIG_GLOBAL_WEBCRON_LINK_DESC" showon="enabled:1" readonly="true" filter="unset" /> <field name="reset_key" type="radio" label="COM_SCHEDULER_CONFIG_RESET_WEBCRON_KEY_LABEL" layout="joomla.form.field.radio.switcher" default="0" showon="enabled:1" required="true" filter="integer" > <option value="0">JNO</option> <option value="1">JYES</option> </field> </fields> </fieldset> <fieldset name="permissions" label="JCONFIG_PERMISSIONS_LABEL" description="JCONFIG_PERMISSIONS_DESC" > <field name="rules" type="rules" label="JCONFIG_PERMISSIONS_LABEL" validate="rules" filter="rules" section="component" component="com_scheduler" /> </fieldset> </config>
| ver. 1.4 |
Github
|
.
| PHP 8.3.23 | Generation time: 0 |
proxy
|
phpinfo
|
Settings