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,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_schedulerunner</name>
<author>Joomla! Project</author>
<creationDate>2021-08</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</version>
<description>PLG_SYSTEM_SCHEDULERUNNER_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\ScheduleRunner</namespace>
<media destination="plg_system_schedulerunner" folder="media">
<folder>js</folder>
<filename>joomla.asset.json</filename>
</media>
<files>
<folder plugin="schedulerunner">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_schedulerunner.ini</language>
<language tag="en-GB">language/en-GB/plg_system_schedulerunner.sys.ini</language>
</languages>
</extension>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.schedulerunner
*
* @copyright (C) 2023 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\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\ScheduleRunner\Extension\ScheduleRunner;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.4.0
*/
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = new ScheduleRunner(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'schedulerunner')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,357 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.schedulerunner
*
* @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\Plugin\System\ScheduleRunner\Extension;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Event\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Table\Extension;
use Joomla\CMS\User\UserHelper;
use Joomla\Component\Scheduler\Administrator\Model\TasksModel;
use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler;
use Joomla\Component\Scheduler\Administrator\Task\Task;
use Joomla\Event\Event;
use Joomla\Event\EventInterface;
use Joomla\Event\SubscriberInterface;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* This plugin implements listeners to support a visitor-triggered lazy-scheduling pattern.
* If `com_scheduler` is installed/enabled and its configuration allows unprotected lazy scheduling, this plugin
* injects into each response with an HTML context a JS file {@see PlgSystemScheduleRunner::injectScheduleRunner()} that
* sets up an AJAX callback to trigger the scheduler {@see PlgSystemScheduleRunner::runScheduler()}. This is achieved
* through a call to the `com_ajax` component.
* Also supports the scheduler component configuration form through auto-generation of the webcron key and injection
* of JS of usability enhancement.
*
* @since 4.1.0
*/
final class ScheduleRunner extends CMSPlugin implements SubscriberInterface
{
/**
* Length of auto-generated webcron key.
*
* @var integer
* @since 4.1.0
*/
private const WEBCRON_KEY_LENGTH = 20;
/**
* @inheritDoc
*
* @return string[]
*
* @since 4.1.0
*
* @throws \Exception
*/
public static function getSubscribedEvents(): array
{
$config = ComponentHelper::getParams('com_scheduler');
$app = Factory::getApplication();
$mapping = [];
if ($app->isClient('site') || $app->isClient('administrator')) {
$mapping['onBeforeCompileHead'] = 'injectLazyJS';
$mapping['onAjaxRunSchedulerLazy'] = 'runLazyCron';
// Only allowed in the frontend
if ($app->isClient('site')) {
if ($config->get('webcron.enabled')) {
$mapping['onAjaxRunSchedulerWebcron'] = 'runWebCron';
}
} elseif ($app->isClient('administrator')) {
$mapping['onContentPrepareForm'] = 'enhanceSchedulerConfig';
$mapping['onExtensionBeforeSave'] = 'generateWebcronKey';
$mapping['onAjaxRunSchedulerTest'] = 'runTestCron';
}
}
return $mapping;
}
/**
* Inject JavaScript to trigger the scheduler in HTML contexts.
*
* @param EventInterface $event The onBeforeCompileHead event.
*
* @return void
*
* @since 4.1.0
*/
public function injectLazyJS(EventInterface $event): void
{
// Only inject in HTML documents
if ($this->getApplication()->getDocument()->getType() !== 'html') {
return;
}
$config = ComponentHelper::getParams('com_scheduler');
if (!$config->get('lazy_scheduler.enabled', true)) {
return;
}
/** @var TasksModel $model */
$model = $this->getApplication()->bootComponent('com_scheduler')
->getMVCFactory()->createModel('Tasks', 'Administrator', ['ignore_request' => true]);
$now = Factory::getDate('now', 'UTC');
if (!$model->hasDueTasks($now)) {
return;
}
// Add configuration options
$triggerInterval = $config->get('lazy_scheduler.interval', 300);
$this->getApplication()->getDocument()->addScriptOptions('plg_system_schedulerunner', ['interval' => $triggerInterval]);
// Load and injection directive
$wa = $this->getApplication()->getDocument()->getWebAssetManager();
$wa->getRegistry()->addExtensionRegistryFile('plg_system_schedulerunner');
$wa->useScript('plg_system_schedulerunner.run-schedule');
}
/**
* Acts on the LazyCron trigger from the frontend when Lazy Cron is enabled in the Scheduler component
* configuration. The lazy cron trigger is implemented in client-side JavaScript which is injected on every page
* load with an HTML context when the component configuration allows it. This method then triggers the Scheduler,
* which effectively runs the next Task in the Scheduler's task queue.
*
* @param EventInterface $e The onAjaxRunSchedulerLazy event.
*
* @return void
*
* @since 4.1.0
*
* @throws \Exception
*/
public function runLazyCron(EventInterface $e)
{
$config = ComponentHelper::getParams('com_scheduler');
if (!$config->get('lazy_scheduler.enabled', true)) {
return;
}
// Since the the request from the frontend may time out, try allowing execution after disconnect.
if (\function_exists('ignore_user_abort')) {
ignore_user_abort(true);
}
// Prevent PHP from trying to output to the user pipe. PHP may kill the script otherwise if the pipe is not accessible.
ob_start();
// Suppress all errors to avoid any output
try {
$this->runScheduler();
} catch (\Exception $e) {
}
ob_end_clean();
}
/**
* This method is responsible for the WebCron functionality of the Scheduler component.<br/>
* Acting on a `com_ajax` call, this method can work in two ways:
* 1. If no Task ID is specified, it triggers the Scheduler to run the next task in
* the task queue.
* 2. If a Task ID is specified, it fetches the task (if it exists) from the Scheduler API and executes it.<br/>
*
* URL query parameters:
* - `hash` string (required) Webcron hash (from the Scheduler component configuration).
* - `id` int (optional) ID of the task to trigger.
*
* @param Event $event The onAjaxRunSchedulerWebcron event.
*
* @return void
*
* @since 4.1.0
*
* @throws \Exception
*/
public function runWebCron(Event $event)
{
$config = ComponentHelper::getParams('com_scheduler');
$hash = $config->get('webcron.key', '');
if (!$config->get('webcron.enabled', false)) {
Log::add($this->getApplication()->getLanguage()->_('PLG_SYSTEM_SCHEDULE_RUNNER_WEBCRON_DISABLED'));
throw new \Exception($this->getApplication()->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403);
}
if (!\strlen($hash) || $hash !== $this->getApplication()->getInput()->get('hash')) {
throw new \Exception($this->getApplication()->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403);
}
$id = (int) $this->getApplication()->getInput()->getInt('id', 0);
$task = $this->runScheduler($id);
if (!empty($task) && !empty($task->getContent()['exception'])) {
throw $task->getContent()['exception'];
}
}
/**
* This method is responsible for the "test run" functionality in the Scheduler administrator backend interface.
* Acting on a `com_ajax` call, this method requires the URL to have a `id` query parameter (corresponding to an
* existing Task ID).
*
* @param Event $event The onAjaxRunScheduler event.
*
* @return void
*
* @since 4.1.0
*
* @throws \Exception
*/
public function runTestCron(Event $event)
{
if (!Session::checkToken('GET')) {
return;
}
$id = (int) $this->getApplication()->getInput()->getInt('id');
$allowConcurrent = $this->getApplication()->getInput()->getBool('allowConcurrent', false);
$user = $this->getApplication()->getIdentity();
if (empty($id) || !$user->authorise('core.testrun', 'com_scheduler.task.' . $id)) {
throw new \Exception($this->getApplication()->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403);
}
/**
* ?: About allow simultaneous, how do we detect if it failed because of pre-existing lock?
*
* We will allow CLI exclusive tasks to be fetched and executed, it's left to routines to do a runtime check
* if they want to refuse normal operation.
*/
$task = (new Scheduler())->getTask(
[
'id' => $id,
'allowDisabled' => true,
'bypassScheduling' => true,
'allowConcurrent' => $allowConcurrent,
]
);
if ($task) {
$task->run();
$event->addArgument('result', $task->getContent());
} else {
/**
* Placeholder result, but the idea is if we failed to fetch the task, it's likely because another task was
* already running. This is a fair assumption if this test run was triggered through the administrator backend,
* so we know the task probably exists and is either enabled/disabled (not trashed).
*/
// @todo language constant + review if this is done right.
$event->addArgument('result', ['message' => 'could not acquire lock on task. retry or allow concurrency.']);
}
}
/**
* Run the scheduler, allowing execution of a single due task.
* Does not bypass task scheduling, meaning that even if an ID is passed the task is only
* triggered if it is due.
*
* @param integer $id The optional ID of the task to run
*
* @return ?Task
*
* @since 4.1.0
* @throws \RuntimeException
*/
private function runScheduler(int $id = 0): ?Task
{
return (new Scheduler())->runTask(['id' => $id]);
}
/**
* Enhance the scheduler config form by dynamically populating or removing display fields.
*
* @param Model\PrepareFormEvent $event The onContentPrepareForm event.
*
* @return void
*
* @since 4.1.0
* @throws \UnexpectedValueException|\RuntimeException
*
* @todo Move to another plugin?
*/
public function enhanceSchedulerConfig(Model\PrepareFormEvent $event): void
{
$form = $event->getForm();
$data = $event->getData();
if (
$form->getName() !== 'com_config.component'
|| $this->getApplication()->getInput()->get('component') !== 'com_scheduler'
) {
return;
}
if (!empty($data['webcron']['key'])) {
$form->removeField('generate_key_on_save', 'webcron');
$relative = 'index.php?option=com_ajax&plugin=RunSchedulerWebcron&group=system&format=json&hash=' . $data['webcron']['key'];
$link = Route::link('site', $relative, false, Route::TLS_IGNORE, true);
$form->setValue('base_link', 'webcron', $link);
} else {
$form->removeField('base_link', 'webcron');
$form->removeField('reset_key', 'webcron');
}
}
/**
* Auto-generate a key/hash for the webcron functionality.
* This method acts on table save, when a hash doesn't already exist or a reset is required.
* @todo Move to another plugin?
*
* @param EventInterface $event The onExtensionBeforeSave event.
*
* @return void
*
* @since 4.1.0
*/
public function generateWebcronKey(EventInterface $event): void
{
/** @var Extension $table */
[$context, $table] = array_values($event->getArguments());
if ($context !== 'com_config.component' || $table->name !== 'com_scheduler') {
return;
}
$params = new Registry($table->params ?? '');
if (
empty($params->get('webcron.key'))
|| $params->get('webcron.reset_key') === 1
) {
$params->set('webcron.key', UserHelper::genRandomPassword(self::WEBCRON_KEY_LENGTH));
}
$params->remove('webcron.base_link');
$params->remove('webcron.reset_key');
$table->params = $params->toString();
}
}