* @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 = $context->getArgument('0'); $data = $context->getArgument('1'); } 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.
* **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.
* **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); } }