first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,328 @@
<?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() ?: [];
}
}