[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/system/schedulerunner/ -> schedulerunner.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  System.ScheduleRunner
   6   *
   7   * @copyright   (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9  
  10   * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
  11   */
  12  
  13  use Joomla\CMS\Application\CMSApplication;
  14  use Joomla\CMS\Component\ComponentHelper;
  15  use Joomla\CMS\Factory;
  16  use Joomla\CMS\Form\Form;
  17  use Joomla\CMS\Language\Text;
  18  use Joomla\CMS\Log\Log;
  19  use Joomla\CMS\Plugin\CMSPlugin;
  20  use Joomla\CMS\Router\Route;
  21  use Joomla\CMS\Session\Session;
  22  use Joomla\CMS\Table\Extension;
  23  use Joomla\CMS\User\UserHelper;
  24  use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler;
  25  use Joomla\Component\Scheduler\Administrator\Task\Task;
  26  use Joomla\Event\Event;
  27  use Joomla\Event\EventInterface;
  28  use Joomla\Event\SubscriberInterface;
  29  use Joomla\Registry\Registry;
  30  
  31  // phpcs:disable PSR1.Files.SideEffects
  32  \defined('_JEXEC') or die;
  33  // phpcs:enable PSR1.Files.SideEffects
  34  
  35  /**
  36   * This plugin implements listeners to support a visitor-triggered lazy-scheduling pattern.
  37   * If `com_scheduler` is installed/enabled and its configuration allows unprotected lazy scheduling, this plugin
  38   * injects into each response with an HTML context a JS file {@see PlgSystemSchedulerunner::injectScheduleRunner()} that
  39   * sets up an AJAX callback to trigger the scheduler {@see PlgSystemSchedulerunner::runScheduler()}. This is achieved
  40   * through a call to the `com_ajax` component.
  41   * Also supports the scheduler component configuration form through auto-generation of the webcron key and injection
  42   * of JS of usability enhancement.
  43   *
  44   * @since 4.1.0
  45   */
  46  class PlgSystemSchedulerunner extends CMSPlugin implements SubscriberInterface
  47  {
  48      /**
  49       * Length of auto-generated webcron key.
  50       *
  51       * @var integer
  52       * @since 4.1.0
  53       */
  54      private const WEBCRON_KEY_LENGTH = 20;
  55  
  56      /**
  57       * @var  CMSApplication
  58       * @since  4.1.0
  59       */
  60      protected $app;
  61  
  62      /**
  63       * @inheritDoc
  64       *
  65       * @return string[]
  66       *
  67       * @since 4.1.0
  68       *
  69       * @throws Exception
  70       */
  71      public static function getSubscribedEvents(): array
  72      {
  73          $config = ComponentHelper::getParams('com_scheduler');
  74          $app = Factory::getApplication();
  75  
  76          $mapping  = [];
  77  
  78          if ($app->isClient('site') || $app->isClient('administrator')) {
  79              $mapping['onBeforeCompileHead'] = 'injectLazyJS';
  80              $mapping['onAjaxRunSchedulerLazy'] = 'runLazyCron';
  81  
  82              // Only allowed in the frontend
  83              if ($app->isClient('site')) {
  84                  if ($config->get('webcron.enabled')) {
  85                      $mapping['onAjaxRunSchedulerWebcron'] = 'runWebCron';
  86                  }
  87              } elseif ($app->isClient('administrator')) {
  88                  $mapping['onContentPrepareForm'] = 'enhanceSchedulerConfig';
  89                  $mapping['onExtensionBeforeSave'] = 'generateWebcronKey';
  90  
  91                  $mapping['onAjaxRunSchedulerTest'] = 'runTestCron';
  92              }
  93          }
  94  
  95          return $mapping;
  96      }
  97  
  98      /**
  99       * Inject JavaScript to trigger the scheduler in HTML contexts.
 100       *
 101       * @param   EventInterface  $event  The onBeforeCompileHead event.
 102       *
 103       * @return void
 104       *
 105       * @since 4.1.0
 106       */
 107      public function injectLazyJS(EventInterface $event): void
 108      {
 109          // Only inject in HTML documents
 110          if ($this->app->getDocument()->getType() !== 'html') {
 111              return;
 112          }
 113  
 114          $config = ComponentHelper::getParams('com_scheduler');
 115  
 116          if (!$config->get('lazy_scheduler.enabled', true)) {
 117              return;
 118          }
 119  
 120          // Check if any task is due to decrease the load
 121          $model = $this->app->bootComponent('com_scheduler')
 122              ->getMVCFactory()->createModel('Tasks', 'Administrator', ['ignore_request' => true]);
 123  
 124          $model->setState('filter.state', 1);
 125          $model->setState('filter.due', 1);
 126  
 127          $items = $model->getItems();
 128  
 129          // See if we are running currently
 130          $model->setState('filter.locked', 1);
 131          $model->setState('filter.due', 0);
 132  
 133          $items2 = $model->getItems();
 134  
 135          if (empty($items) || !empty($items2)) {
 136              return;
 137          }
 138  
 139          // Add configuration options
 140          $triggerInterval = $config->get('lazy_scheduler.interval', 300);
 141          $this->app->getDocument()->addScriptOptions('plg_system_schedulerunner', ['interval' => $triggerInterval]);
 142  
 143          // Load and injection directive
 144          $wa = $this->app->getDocument()->getWebAssetManager();
 145          $wa->getRegistry()->addExtensionRegistryFile('plg_system_schedulerunner');
 146          $wa->useScript('plg_system_schedulerunner.run-schedule');
 147      }
 148  
 149      /**
 150       * Acts on the LazyCron trigger from the frontend when Lazy Cron is enabled in the Scheduler component
 151       * configuration. The lazy cron trigger is implemented in client-side JavaScript which is injected on every page
 152       * load with an HTML context when the component configuration allows it. This method then triggers the Scheduler,
 153       * which effectively runs the next Task in the Scheduler's task queue.
 154       *
 155       * @param   EventInterface  $e  The onAjaxRunSchedulerLazy event.
 156       *
 157       * @return void
 158       *
 159       * @since 4.1.0
 160       *
 161       * @throws Exception
 162       */
 163      public function runLazyCron(EventInterface $e)
 164      {
 165          $config = ComponentHelper::getParams('com_scheduler');
 166  
 167          if (!$config->get('lazy_scheduler.enabled', true)) {
 168              return;
 169          }
 170  
 171          // Since `navigator.sendBeacon()` may time out, allow execution after disconnect if possible.
 172          if (function_exists('ignore_user_abort')) {
 173              ignore_user_abort(true);
 174          }
 175  
 176          // Prevent PHP from trying to output to the user pipe. PHP may kill the script otherwise if the pipe is not accessible.
 177          ob_start();
 178  
 179          // Suppress all errors to avoid any output
 180          try {
 181              $this->runScheduler();
 182          } catch (Exception $e) {
 183          }
 184  
 185          ob_end_clean();
 186      }
 187  
 188      /**
 189       * This method is responsible for the WebCron functionality of the Scheduler component.<br/>
 190       * Acting on a `com_ajax` call, this method can work in two ways:
 191       * 1. If no Task ID is specified, it triggers the Scheduler to run the next task in
 192       *   the task queue.
 193       * 2. If a Task ID is specified, it fetches the task (if it exists) from the Scheduler API and executes it.<br/>
 194       *
 195       * URL query parameters:
 196       * - `hash` string (required)   Webcron hash (from the Scheduler component configuration).
 197       * - `id`   int (optional)      ID of the task to trigger.
 198       *
 199       * @param   Event  $event  The onAjaxRunSchedulerWebcron event.
 200       *
 201       * @return void
 202       *
 203       * @since 4.1.0
 204       *
 205       * @throws Exception
 206       */
 207      public function runWebCron(Event $event)
 208      {
 209          $config = ComponentHelper::getParams('com_scheduler');
 210          $hash = $config->get('webcron.key', '');
 211  
 212          if (!$config->get('webcron.enabled', false)) {
 213              Log::add(Text::_('PLG_SYSTEM_SCHEDULE_RUNNER_WEBCRON_DISABLED'));
 214              throw new Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 215          }
 216  
 217          if (!strlen($hash) || $hash !== $this->app->input->get('hash')) {
 218              throw new Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 219          }
 220  
 221          $id = (int) $this->app->input->getInt('id', 0);
 222  
 223          $task = $this->runScheduler($id);
 224  
 225          if (!empty($task) && !empty($task->getContent()['exception'])) {
 226              throw $task->getContent()['exception'];
 227          }
 228      }
 229  
 230      /**
 231       * This method is responsible for the "test run" functionality in the Scheduler administrator backend interface.
 232       * Acting on a `com_ajax` call, this method requires the URL to have a `id` query parameter (corresponding to an
 233       * existing Task ID).
 234       *
 235       * @param   Event  $event  The onAjaxRunScheduler event.
 236       *
 237       * @return void
 238       *
 239       * @since 4.1.0
 240       *
 241       * @throws Exception
 242       */
 243      public function runTestCron(Event $event)
 244      {
 245          if (!Session::checkToken('GET')) {
 246              return;
 247          }
 248  
 249          $id = (int) $this->app->input->getInt('id');
 250          $allowConcurrent = $this->app->input->getBool('allowConcurrent', false);
 251  
 252          $user = Factory::getApplication()->getIdentity();
 253  
 254          if (empty($id) || !$user->authorise('core.testrun', 'com_scheduler.task.' . $id)) {
 255              throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 256          }
 257  
 258          /**
 259           * ?: About allow simultaneous, how do we detect if it failed because of pre-existing lock?
 260           *
 261           * We will allow CLI exclusive tasks to be fetched and executed, it's left to routines to do a runtime check
 262           * if they want to refuse normal operation.
 263           */
 264          $task = (new Scheduler())->getTask(
 265              [
 266                  'id' => $id,
 267                  'allowDisabled' => true,
 268                  'bypassScheduling' => true,
 269                  'allowConcurrent' => $allowConcurrent,
 270              ]
 271          );
 272  
 273          if (!is_null($task)) {
 274              $task->run();
 275              $event->addArgument('result', $task->getContent());
 276          } else {
 277              /**
 278               * Placeholder result, but the idea is if we failed to fetch the task, it's likely because another task was
 279               * already running. This is a fair assumption if this test run was triggered through the administrator backend,
 280               * so we know the task probably exists and is either enabled/disabled (not trashed).
 281               */
 282              // @todo language constant + review if this is done right.
 283              $event->addArgument('result', ['message' => 'could not acquire lock on task. retry or allow concurrency.']);
 284          }
 285      }
 286  
 287      /**
 288       * Run the scheduler, allowing execution of a single due task.
 289       * Does not bypass task scheduling, meaning that even if an ID is passed the task is only
 290       * triggered if it is due.
 291       *
 292       * @param   integer  $id  The optional ID of the task to run
 293       *
 294       * @return ?Task
 295       *
 296       * @since 4.1.0
 297       * @throws RuntimeException
 298       */
 299      protected function runScheduler(int $id = 0): ?Task
 300      {
 301          return (new Scheduler())->runTask(['id' => $id]);
 302      }
 303  
 304      /**
 305       * Enhance the scheduler config form by dynamically populating or removing display fields.
 306       *
 307       * @param   EventInterface  $event  The onContentPrepareForm event.
 308       *
 309       * @return void
 310       *
 311       * @since 4.1.0
 312       * @throws UnexpectedValueException|RuntimeException
 313       *
 314       * @todo  Move to another plugin?
 315       */
 316      public function enhanceSchedulerConfig(EventInterface $event): void
 317      {
 318          /** @var Form $form */
 319          $form = $event->getArgument('0');
 320          $data = $event->getArgument('1');
 321  
 322          if (
 323              $form->getName() !== 'com_config.component'
 324              || $this->app->input->get('component') !== 'com_scheduler'
 325          ) {
 326              return;
 327          }
 328  
 329          if (!empty($data['webcron']['key'])) {
 330              $form->removeField('generate_key_on_save', 'webcron');
 331  
 332              $relative = 'index.php?option=com_ajax&plugin=RunSchedulerWebcron&group=system&format=json&hash=' . $data['webcron']['key'];
 333              $link = Route::link('site', $relative, false, Route::TLS_IGNORE, true);
 334              $form->setValue('base_link', 'webcron', $link);
 335          } else {
 336              $form->removeField('base_link', 'webcron');
 337              $form->removeField('reset_key', 'webcron');
 338          }
 339      }
 340  
 341      /**
 342       * Auto-generate a key/hash for the webcron functionality.
 343       * This method acts on table save, when a hash doesn't already exist or a reset is required.
 344       * @todo Move to another plugin?
 345       *
 346       * @param   EventInterface  $event The onExtensionBeforeSave event.
 347       *
 348       * @return void
 349       *
 350       * @since 4.1.0
 351       */
 352      public function generateWebcronKey(EventInterface $event): void
 353      {
 354          /** @var Extension $table */
 355          [$context, $table] = $event->getArguments();
 356  
 357          if ($context !== 'com_config.component' || $table->name !== 'com_scheduler') {
 358              return;
 359          }
 360  
 361          $params = new Registry($table->params ?? '');
 362  
 363          if (
 364              empty($params->get('webcron.key'))
 365              || $params->get('webcron.reset_key') === 1
 366          ) {
 367              $params->set('webcron.key', UserHelper::genRandomPassword(self::WEBCRON_KEY_LENGTH));
 368          }
 369  
 370          $params->remove('webcron.base_link');
 371          $params->remove('webcron.reset_key');
 372          $table->params = $params->toString();
 373      }
 374  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer