[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |