We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New language relevant PR in upstream repo: joomla/joomla-cms#42530 Here are the upstream changes:
diff --git a/administrator/components/com_admin/sql/updates/mysql/5.3.0-2024-10-26.sql b/administrator/components/com_admin/sql/updates/mysql/5.3.0-2024-10-26.sql new file mode 100644 index 0000000000000..b2e6c94cd0fcf --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/5.3.0-2024-10-26.sql @@ -0,0 +1,20 @@ +-- +-- Table structure for table `#__scheduler_logs` +-- + +CREATE TABLE IF NOT EXISTS `#__scheduler_logs` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `taskname` varchar(255) NOT NULL DEFAULT '', + `tasktype` varchar(128) NOT NULL COMMENT 'unique identifier for job defined by plugin', + `duration` DECIMAL(5,3) NOT NULL, + `jobid` int UNSIGNED NOT NULL, + `taskid` int UNSIGNED NOT NULL, + `exitcode` int NOT NULL, + `lastdate` datetime COMMENT 'Timestamp of last run', + `nextdate` datetime COMMENT 'Timestamp of next (planned) run, referred for execution on trigger', + PRIMARY KEY (id), + KEY `idx_taskname` (`taskname`), + KEY `idx_tasktype` (`tasktype`), + KEY `idx_lastdate` (`lastdate`), + KEY `idx_nextdate` (`nextdate`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; diff --git a/administrator/components/com_admin/sql/updates/postgresql/5.3.0-2024-10-26.sql b/administrator/components/com_admin/sql/updates/postgresql/5.3.0-2024-10-26.sql new file mode 100644 index 0000000000000..49794391fb4c4 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/5.3.0-2024-10-26.sql @@ -0,0 +1,20 @@ +-- +-- Table structure for table "#__scheduler_logs" +-- + +CREATE TABLE IF NOT EXISTS "#__scheduler_logs" ( + "id" serial NOT NULL, + "taskname" varchar(255) DEFAULT '' NOT NULL, + "tasktype" varchar(128) NOT NULL, + "duration" NUMERIC(5,3) NOT NULL, + "jobid" integer NOT NULL, + "taskid" integer NOT NULL, + "exitcode" integer NOT NULL, + "lastdate" timestamp without time zone, + "nextdate" timestamp without time zone, + PRIMARY KEY (id) +); +CREATE INDEX "#__scheduler_logs_idx_taskname" ON "#__scheduler_logs" ("taskname"); +CREATE INDEX "#__scheduler_logs_idx_tasktype" ON "#__scheduler_logs" ("tasktype"); +CREATE INDEX "#__scheduler_logs_idx_lastdate" ON "#__scheduler_logs" ("lastdate"); +CREATE INDEX "#__scheduler_logs_idx_nextdate" ON "#__scheduler_logs" ("nextdate"); diff --git a/administrator/components/com_scheduler/forms/filter_logs.xml b/administrator/components/com_scheduler/forms/filter_logs.xml new file mode 100644 index 0000000000000..8fcfddc8e24f1 --- /dev/null +++ b/administrator/components/com_scheduler/forms/filter_logs.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<form addfieldprefix="Joomla\Component\Scheduler\Administrator\Field"> + <fields name="filter"> + <field + name="search" + type="text" + label="COM_SCHEDULER_FILTER_SEARCH_LABEL" + description="COM_SCHEDULER_FILTER_SEARCH_HISTORY_DESC" + inputmode="search" + hint="JSEARCH_FILTER" + /> + <field + name="type" + type="taskType" + label="COM_SCHEDULER_HEADING_TASK_TYPE" + class="js-select-submit-on-change" + > + <option value="">COM_SCHEDULER_SELECT_TYPE</option> + </field> + <field + name="exitcode" + type="exitCode" + label="COM_SCHEDULER_EXITCODE" + class="js-select-submit-on-change" + > + <option value="">COM_SCHEDULER_FILTER_SELECT_OPTION_EXITCODE</option> + </field> + </fields> + <fields name="list"> + <field + name="fullordering" + type="list" + label="JGLOBAL_SORT_BY" + class="js-select-submit-on-change" + default="a.id DESC" + validate="options" + > + <option value="">JGLOBAL_SORT_BY</option> + <option value="a.taskname ASC">JGLOBAL_TITLE_ASC</option> + <option value="a.taskname DESC">JGLOBAL_TITLE_DESC</option> + <option value="a.tasktype ASC">COM_SCHEDULER_TASK_TYPE_ASC</option> + <option value="a.tasktype DESC">COM_SCHEDULER_TASK_TYPE_DESC</option> + <option value="a.taskid ASC">COM_SCHEDULER_TASK_TIMES_ASC</option> + <option value="a.taskid DESC">COM_SCHEDULER_TASK_TIMES_DESC</option> + <option value="a.lastdate ASC">COM_SCHEDULER_LAST_RUN_ASC</option> + <option value="a.lastdate DESC">COM_SCHEDULER_LAST_RUN_DESC</option> + <option value="a.duration ASC">COM_SCHEDULER_DURATION_ASC</option> + <option value="a.duration DESC">COM_SCHEDULER_DURATION_DESC</option> + <option value="a.exitcode ASC">COM_SCHEDULER_EXIT_CODE_ASC</option> + <option value="a.exitcode DESC">COM_SCHEDULER_EXIT_CODE_DESC</option> + <option value="a.nextdate ASC">COM_SCHEDULER_NEXT_RUN_ASC</option> + <option value="a.nextdate DESC">COM_SCHEDULER_NEXT_RUN_DESC</option> + <option value="a.id ASC">JGRID_HEADING_ID_ASC</option> + <option value="a.id DESC">JGRID_HEADING_ID_DESC</option> + </field> + <field + name="limit" + type="limitbox" + label="JGLOBAL_LIST_LIMIT" + default="5" + class="js-select-submit-on-change" + /> + </fields> +</form> \ No newline at end of file diff --git a/administrator/components/com_scheduler/src/Controller/LogsController.php b/administrator/components/com_scheduler/src/Controller/LogsController.php new file mode 100644 index 0000000000000..41fcf97720531 --- /dev/null +++ b/administrator/components/com_scheduler/src/Controller/LogsController.php @@ -0,0 +1,111 @@ +<?php + +/** + * @package Joomla.Administrator + * @subpackage com_scheduler + * + * @copyright (C) 2024 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\Controller; + +use Joomla\CMS\Access\Exception\NotAllowed; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\AdminController; +use Joomla\Utilities\ArrayHelper; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Logs list controller class. + * + * @since __DEPLOY_VERSION__ + */ +class LogsController extends AdminController +{ + /** + * The prefix to use with controller messages. + * + * @var string + * + * @since __DEPLOY_VERSION__ + */ + protected $text_prefix = 'COM_SCHEDULER_LOGS'; + + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since __DEPLOY_VERSION__ + */ + public function getModel($name = 'Logs', $prefix = 'Administrator', $config = ['ignore_request' => true]) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Clean out the logs. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function purge() + { + // Check for request forgeries. + $this->checkToken(); + + $model = $this->getModel('Logs'); + + if ($model->purge()) { + $message = Text::_('COM_SCHEDULER_LOGS_CLEAR'); + } else { + $message = Text::_('COM_SCHEDULER_CLEAR_FAIL'); + } + + $this->setRedirect('index.php?option=com_scheduler&view=logs', $message); + } + + /** + * Removes an item. + * + * Overrides Joomla\CMS\MVC\Controller\FormController::delete to check the core.admin permission. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function delete(): void + { + $ids = $this->input->get('cid', [], 'array'); + + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + if (empty($ids)) { + $this->setMessage(Text::_('COM_SCHEDULER_NO_LOGS_SELECTED'), 'warning'); + $this->setRedirect('index.php?option=com_scheduler&view=logs'); + return; + } + + // Get the model. + $model = $this->getModel(); + $ids = ArrayHelper::toInteger($ids); + + // Remove the items. + if ($model->delete($ids)) { + $this->setMessage(Text::plural('COM_SCHEDULER_N_ITEMS_DELETED', \count($ids))); + } + + $this->setRedirect('index.php?option=com_scheduler&view=logs'); + } +} diff --git a/administrator/components/com_scheduler/src/Field/ExitCodeField.php b/administrator/components/com_scheduler/src/Field/ExitCodeField.php new file mode 100644 index 0000000000000..58867d1884e3c --- /dev/null +++ b/administrator/components/com_scheduler/src/Field/ExitCodeField.php @@ -0,0 +1,46 @@ +<?php + +/** + * @package Joomla.Administrator + * @subpackage com_scheduler + * + * @copyright (C) 2024 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\Field; + +use Joomla\CMS\Form\Field\PredefinedlistField; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * A predefined list field with all possible states for a com_scheduler entry. + * + * @since __DEPLOY_VERSION__ + */ +class ExitCodeField extends PredefinedlistField +{ + /** + * The form field type. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + public $type = 'exitCode'; + + /** + * Available states + * + * @var string[] + * @since __DEPLOY_VERSION__ + */ + protected $predefinedOptions = [ + 5 => 'COM_SCHEDULER_EXIT_CODE_FAILED', + 0 => 'COM_SCHEDULER_EXIT_CODE_EXECUTED', + 123 => 'COM_SCHEDULER_EXIT_CODE_WILLRESUME', + '*' => 'JALL', + ]; +} diff --git a/administrator/components/com_scheduler/src/Model/LogsModel.php b/administrator/components/com_scheduler/src/Model/LogsModel.php new file mode 100644 index 0000000000000..979bf17b4d13b --- /dev/null +++ b/administrator/components/com_scheduler/src/Model/LogsModel.php @@ -0,0 +1,275 @@ +<?php + +/** + * @package Joomla.Administrator + * @subpackage com_scheduler + * + * @copyright (C) 2024 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\Model; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\MVC\Model\ListModel; +use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper; +use Joomla\Database\ParameterType; + +/** + * Supporting a list of logs. + * + * @since __DEPLOY_VERSION__ + */ +class LogsModel extends ListModel +{ + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \JControllerLegacy + * @since __DEPLOY_VERSION__ + */ + public function __construct($config = [], ?MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'exitcode', 'a.exitcode', + 'duration', 'a.duration', + 'taskname', 'a.taskname', + 'type', 'a.tasktype', + 'jobid', 'a.jobid', + 'taskid', 'a.taskid', + 'lastdate', 'a.lastdate', + 'nextdate', 'a.nextdate', + ]; + } + + parent::__construct($config, $factory); + } + + /** + * Removes all the logs from the table. + * + * @return boolean result of operation + * + * @since __DEPLOY_VERSION__ + */ + public function purge() + { + try { + $this->getDatabase()->truncateTable('#__scheduler_logs'); + } catch (\Exception $e) { + return false; + } + + return true; + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_scheduler'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since __DEPLOY_VERSION__ + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.exitcode'); + $id .= ':' . $this->getState('filter.type'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \JDatabaseQuery + * + * @since __DEPLOY_VERSION__ + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__scheduler_logs', 'a')); + + // Filter the items over the exit code. + $exitCode = $this->getState('filter.exitcode'); + + if (is_numeric($exitCode)) { + $exitCode = (int) $exitCode; + if ($exitCode >= 0) { + $query->where($db->quoteName('a.exitcode') . ' = :exitcode') + ->bind(':exitcode', $exitCode, ParameterType::INTEGER); + } else { + $query->whereNotIn($db->quoteName('a.exitcode'), [0, 123], ParameterType::INTEGER); + } + } + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'); + $query->where($db->quoteName('taskname') . ' LIKE :taskname') + ->bind(':taskname', $search); + } + } + + // Filter over type + $typeFilter = $this->getState('filter.type'); + + if ($typeFilter) { + $taskOptions = SchedulerHelper::getTaskOptions(); + $safeTypeTitle = $taskOptions->findOption($typeFilter)->title ?? ''; + $query->where($db->quotename('a.tasktype') . ' = :type') + ->bind(':type', $safeTypeTitle); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lastdate')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); + + return $query; + } + + /** + * Delete rows. + * + * @param array $pks The ids of the items to delete. + * + * @return boolean Returns true on success, false on failure. + */ + public function delete($pks) + { + if ($this->canDelete($pks)) { + // Delete logs from list + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__scheduler_logs')) + ->whereIn($db->quoteName('id'), $pks); + + $db->setQuery($query); + $this->setError((string) $query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } else { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + } + + return true; + } + + /** + * Determine whether a record may be deleted taking into consideration + * the user's permissions over the record. + * + * @param object $record The database row/record in question + * + * @return boolean True if the record may be deleted + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + protected function canDelete($record): bool + { + // Record doesn't exist, can't delete + if (empty($record)) { + return false; + } + + return Factory::getApplication()->getIdentity()->authorise('core.delete', 'com_scheduler'); + } + + /** + * @param array $data The task execution data. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + public function logTask(array $data): void + { + $model = Factory::getApplication()->bootComponent('com_scheduler') + ->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]); + $taskInfo = $model->getItem($data['TASK_ID']); + $taskOptions = SchedulerHelper::getTaskOptions(); + $safeTypeTitle = $taskOptions->findOption($taskInfo->type)->title ?? ''; + $duration = ($data['TASK_DURATION'] ?? 0); + $created = Factory::getDate()->toSql(); + + /** @var \Joomla\Component\Schduler\Administrator\Table\LogsTable $table */ + $logsTable = $this->getTable(); + $logsTable->tasktype = $safeTypeTitle; + $logsTable->taskname = $data['TASK_TITLE']; + $logsTable->duration = $duration; + $logsTable->jobid = $data['TASK_ID']; + $logsTable->exitcode = $data['EXIT_CODE']; + $logsTable->taskid = $data['TASK_TIMES']; + $logsTable->lastdate = $created; + $logsTable->nextdate = $taskInfo->next_execution; + + // Log the execution of the task. + $logsTable->store(); + } +} diff --git a/administrator/components/com_scheduler/src/Table/LogsTable.php b/administrator/components/com_scheduler/src/Table/LogsTable.php new file mode 100644 index 0000000000000..78ec38c5345ca --- /dev/null +++ b/administrator/components/com_scheduler/src/Table/LogsTable.php @@ -0,0 +1,40 @@ +<?php + +/** + * @package Joomla.Administrator + * @subpackage com_scheduler + * + * @copyright (C) 2024 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\Table; + +use Joomla\CMS\Table\Table; +use Joomla\Database\DatabaseDriver; +use Joomla\Event\DispatcherInterface; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Logs Table class. + * + * @since __DEPLOY_VERSION__ + */ +class LogsTable extends Table +{ + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * @param ?DispatcherInterface $dispatcher Event dispatcher for this table + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(DatabaseDriver $db, ?DispatcherInterface $dispatcher = null) + { + parent::__construct('#__scheduler_logs', 'id', $db, $dispatcher); + } +} diff --git a/administrator/components/com_scheduler/src/View/Logs/HtmlView.php b/administrator/components/com_scheduler/src/View/Logs/HtmlView.php new file mode 100644 index 0000000000000..083d4382c180b --- /dev/null +++ b/administrator/components/com_scheduler/src/View/Logs/HtmlView.php @@ -0,0 +1,140 @@ +<?php + +/** + * @package Joomla.Administrator + * @subpackage com_scheduler + * + * @copyright (C) 2024 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\View\Logs; + +use Joomla\CMS\Factory; +use Joomla\CMS\Form\Form; +use Joomla\CMS\Helper\ContentHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\GenericDataException; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Pagination\Pagination; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\Registry\Registry; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * View class for a list of logs. + * + * @since __DEPLOY_VERSION__ + */ +class HtmlView extends BaseHtmlView +{ + /** + * The search tools form + * + * @var Form + * @since __DEPLOY_VERSION__ + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since __DEPLOY_VERSION__ + */ + public $activeFilters = []; + + /** + * An array of items + * + * @var array + * @since __DEPLOY_VERSION__ + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * @since __DEPLOY_VERSION__ + */ + protected $pagination; + + /** + * The model state + * + * @var Registry + * @since __DEPLOY_VERSION__ + */ + protected $state; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function display($tpl = null): void + { + /** @var LogsModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_scheduler'); + $user = Factory::getApplication()->getIdentity(); + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title(Text::_('COM_SCHEDULER_FIELDSET_EXEC_HIST'), 'list'); + + $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + $toolbar->link('JTOOLBAR_BACK', 'index.php?option=com_scheduler') + ->icon('icon-' . $arrow); + + if ($canDo->get('core.delete') && \count($this->items)) { + $toolbar->delete('logs.delete') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + + $toolbar->confirmButton('trash', 'COM_SCHEDULER_TOOLBAR_PURGE', 'logs.purge') + ->message('COM_SCHEDULER_TOOLBAR_PURGE_CONFIRM') + ->listCheck(false); + } + + // Link to component preferences if user has admin privileges + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_scheduler'); + } + + $toolbar->help('Scheduled_Tasks'); + } +} diff --git a/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php b/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php index 169ffbd258ce1..d58d60c9f9552 100644 --- a/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php +++ b/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php @@ -168,6 +168,11 @@ protected function addToolbar(): void } } + $toolbar->linkButton('history', 'COM_SCHEDULER_EXECUTION_HISTORY') + ->url('index.php?option=com_scheduler&view=logs&layout=default') + ->buttonClass('btn btn-info') + ->icon('icon-menu'); + // Add "Empty Trash" button if filtering by trashed. if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { $toolbar->delete('tasks.delete', 'JTOOLBAR_DELETE_FROM_TRASH') diff --git a/administrator/components/com_scheduler/tmpl/logs/default.php b/administrator/components/com_scheduler/tmpl/logs/default.php new file mode 100644 index 0000000000000..d9436aeb49a2a --- /dev/null +++ b/administrator/components/com_scheduler/tmpl/logs/default.php @@ -0,0 +1,142 @@ +<?php + +/** + * @package Joomla.Administrator + * @subpackage com_scheduler + * + * @copyright (C) 2024 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\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; + +/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ +$wa = $this->document->getWebAssetManager(); +$wa->useScript('table.columns') + ->useScript('multiselect'); + +$user = $this->getCurrentUser(); +$listOrder = $this->escape($this->state->get('list.ordering')); +$listDirn = $this->escape($this->state->get('list.direction')); +$canEdit = $user->authorise('core.edit', 'com_scheduler'); +$canChange = $user->authorise('core.edit.state', 'com_scheduler'); +?> +<form action="<?php echo Route::_('index.php?option=com_scheduler&view=logs'); ?>" method="post" name="adminForm" id="adminForm"> + <div class="row"> + <div class="col-md-12"> + <div id="j-main-container" class="j-main-container"> + <?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?> + <?php if (empty($this->items)) : ?> + <div class="alert alert-info"> + <span class="icon-info-circle" aria-hidden="true"></span><span class="sr-only"><?php echo Text::_('INFO'); ?></span> + <?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?> + </div> + <?php else : ?> + <table class="table itemList"> + <caption id="captionTable" class="visually-hidden"> + <?php echo Text::_('COM_SCHEDULER_TABLE_CAPTION'); ?>, + <span id="orderedBy"><?php echo Text::_('JGLOBAL_SORTED_BY'); ?> </span>, + <span id="filteredBy"><?php echo Text::_('JGLOBAL_FILTERED_BY'); ?></span> + </caption> + <thead> + <tr> + <td class="w-1 text-center"> + <?php echo HTMLHelper::_('grid.checkall'); ?> + </td> + <th scope="col" style="min-width:100px"> + <?php echo HTMLHelper::_('searchtools.sort', 'JGLOBAL_TITLE', 'a.taskname', $listDirn, $listOrder); ?> + </th> + <!-- Task type header --> + <th scope="col" class="w-1 d-none d-md-table-cell"> + <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_TASK_TYPE', 'a.tasktype', $listDirn, $listOrder) ?> + </th> + <th scope="col" class="w-5 d-none d-md-table-cell"> + <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LABEL_TIMES_EXEC', 'a.taskid', $listDirn, $listOrder); ?> + </th> + <th scope="col" class="w-5 d-none d-md-table-cell"> + <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LAST_RUN_DATE', 'a.lastdate', $listDirn, $listOrder); ?> + </th> + <th scope="col" class="w-1 d-none d-md-table-cell"> + <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LABEL_DURATION', 'a.duration', $listDirn, $listOrder); ?> + </th> + <th scope="col" class="w-5 d-none d-md-table-cell"> + <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LABEL_EXIT_CODE', 'a.exitcode', $listDirn, $listOrder); ?> + </th> + <th scope="col" class="w-5 d-none d-md-table-cell"> + <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LABEL_NEXT_EXEC', 'a.nextdate', $listDirn, $listOrder); ?> + </th> + <th scope="col" class="w-1 d-none d-md-table-cell"> + <?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?> + </th> + </tr> + </thead> + <tbody> + <?php foreach ($this->items as $i => $item) : ?> + <tr class="row<?php echo $i % 2; ?>"> + <td class="text-center"> + <?php echo HTMLHelper::_('grid.id', $i, $item->id); ?> + </td> + <th scope="row"> + <?php if ($canEdit) : ?> + <a href="<?php echo Route::_('index.php?option=com_scheduler&task=task.edit&id=' . $item->jobid); ?>" + title="<?php echo Text::_('JACTION_EDIT'); ?> <?php echo $this->escape($item->taskname); ?>"> <?php echo $this->escape($item->taskname); ?> + </a> + <?php else : ?> + <?php echo $this->escape(str_replace(Uri::root(), '', rawurldecode($item->taskname))); ?> + <?php endif; ?> + </th> + <!-- Item type --> + <td class="small d-none d-md-table-cell"> + <?php echo $this->escape($item->tasktype); ?> + </td> + <td class="small d-none d-md-table-cell"> + <?php echo (int) $item->taskid; ?> + </td> + <td class="small d-none d-md-table-cell"> + <?php echo HTMLHelper::_('date.relative', $item->lastdate, Text::_('DATE_FORMAT_LC6')); ?> + </td> + <td class="small d-none d-md-table-cell"> + <?php echo $item->duration; ?> + </td> + <td class="small d-none d-md-table-cell"> + <?php + switch ($item->exitcode) { + case '123': + echo '<span class="badge bg-secondary">' . Text::sprintf('COM_SCHEDULER_TASK_RESUME', $item->exitcode) . '</span>'; + break; + case '0': + echo '<span class="badge bg-success">' . Text::sprintf('COM_SCHEDULER_TASK_EXECUTED', $item->exitcode) . '</span>'; + break; + default: + echo '<span class="badge bg-danger">' . Text::sprintf('COM_SCHEDULER_TASK_FAILED', $item->exitcode) . '</span>'; + break; + } + ?> + </td> + <td class="small d-none d-md-table-cell"> + <?php echo HTMLHelper::_('date', $item->nextdate, Text::_('DATE_FORMAT_LC6')); ?> + </td> + <td class="small d-none d-md-table-cell"> + <?php echo (int) $item->id; ?> + </td> + </tr> + <?php endforeach; ?> + </tbody> + </table> + <?php // load the pagination. + ?> + <?php echo $this->pagination->getListFooter(); ?> + <?php endif; ?> + <input type="hidden" name="task" value=""> + <input type="hidden" name="boxchecked" value="0"> + <?php echo HTMLHelper::_('form.token'); ?> + </div> + </div> + </div> +</form> diff --git a/administrator/language/en-GB/com_scheduler.ini b/administrator/language/en-GB/com_scheduler.ini index f5eee988a7c2c..eac54933d7d34 100644 --- a/administrator/language/en-GB/com_scheduler.ini +++ b/administrator/language/en-GB/com_scheduler.ini @@ -4,6 +4,7 @@ ; Note : All ini files need to be saved as UTF-8 COM_SCHEDULER="Scheduled Tasks" +COM_SCHEDULER_CLEAR_FAIL="Failed to delete execution history logs." COM_SCHEDULER_CONFIGURATION="Scheduled Tasks Configuration" COM_SCHEDULER_CONFIG_FIELDSET_LAZY_SCHEDULER_DESC="Configure how site visits trigger Scheduled Tasks." COM_SCHEDULER_CONFIG_FIELDSET_LAZY_SCHEDULER_LABEL="Lazy Scheduler" @@ -26,6 +27,8 @@ COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_DESC="Copy the link to your clipboard." COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_FAIL="Could not copy link." COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_SUCCESS="Link copied." COM_SCHEDULER_DESCRIPTION_TASK_PRIORITY="Higher priority tasks can potentially block lower priority tasks." +COM_SCHEDULER_DURATION_ASC="Duration ascending" +COM_SCHEDULER_DURATION_DESC="Duration descending" COM_SCHEDULER_EDIT_TASK="Edit Task" COM_SCHEDULER_EMPTYSTATE_BUTTON_ADD="Add your first Task" COM_SCHEDULER_EMPTYSTATE_CONTENT="Tasks are actions on your website that are scheduled to occur at set times." @@ -33,10 +36,17 @@ COM_SCHEDULER_EMPTYSTATE_TITLE="No Tasks have been created yet." COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW="You need to select a Task type first." COM_SCHEDULER_ERROR_INVALID_TASK_TYPE="Invalid Task Type" COM_SCHEDULER_EXECUTION_CRON_EXPRESSION="Cron Expression (Advanced)" +COM_SCHEDULER_EXECUTION_HISTORY="Execution History" COM_SCHEDULER_EXECUTION_INTERVAL_DAYS="Interval, Days" COM_SCHEDULER_EXECUTION_INTERVAL_HOURS="Interval, Hours" COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES="Interval, Minutes" COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS="Interval, Months" +COM_SCHEDULER_EXITCODE="Exit Code" +COM_SCHEDULER_EXIT_CODE_ASC="Exit Code ascending" +COM_SCHEDULER_EXIT_CODE_DESC="Exit Code descending" +COM_SCHEDULER_EXIT_CODE_EXECUTED="Executed" +COM_SCHEDULER_EXIT_CODE_FAILED="Failed" +COM_SCHEDULER_EXIT_CODE_WILLRESUME="Task Will Resume" COM_SCHEDULER_FIELDSET_BASIC="Basic Fields" COM_SCHEDULER_FIELDSET_CRON_OPTIONS="Cron Match" COM_SCHEDULER_FIELDSET_EXEC_HIST="Execution History" @@ -60,10 +70,13 @@ COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_MINUTES="Minutes" COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_MONTHS="Months" COM_SCHEDULER_FIELD_TASK_TYPE="Type ID" COM_SCHEDULER_FILTER_SEARCH_DESC="Search in task title and note. Prefix with ID: to search for a task ID." +COM_SCHEDULER_FILTER_SEARCH_HISTORY_DESC="Search in task title. Prefix with ID: to search for a history ID." COM_SCHEDULER_FILTER_SEARCH_LABEL="Search Tasks" +COM_SCHEDULER_FILTER_SELECT_OPTION_EXITCODE="- Exit Code -" COM_SCHEDULER_FORM_TITLE_EDIT="Edit Task" COM_SCHEDULER_FORM_TITLE_NEW="New Task" COM_SCHEDULER_HEADING_TASK_TYPE="- Task Type -" +COM_SCHEDULER_LABEL_DURATION="Duration" COM_SCHEDULER_LABEL_EXEC_DAY="Execution Day" COM_SCHEDULER_LABEL_EXEC_INTERVAL="Execution Interval" COM_SCHEDULER_LABEL_EXEC_TIME="Execution Time (UTC)" @@ -80,6 +93,7 @@ COM_SCHEDULER_LABEL_TIMES_FAIL="Times Failed" COM_SCHEDULER_LAST_RUN_ASC="Last Run ascending" COM_SCHEDULER_LAST_RUN_DATE="Last Run Date" COM_SCHEDULER_LAST_RUN_DESC="Last Run descending" +COM_SCHEDULER_LOGS_CLEAR="All execution history logs have been deleted." COM_SCHEDULER_MANAGER_TASKS="Scheduled Tasks" COM_SCHEDULER_MANAGER_TASK_EDIT="Edit Task" COM_SCHEDULER_MANAGER_TASK_NEW="New Task" @@ -105,6 +119,7 @@ COM_SCHEDULER_N_ITEMS_UNPUBLISHED="%d tasks disabled." COM_SCHEDULER_N_ITEMS_UNPUBLISHED_1="Task disabled." COM_SCHEDULER_N_ITEMS_UNLOCKED="%d tasks unlocked." COM_SCHEDULER_N_ITEMS_UNLOCKED_1="Task unlocked." +COM_SCHEDULER_NO_LOGS_SELECTED="No execution history logs selected." COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL="Manual Execution" COM_SCHEDULER_OPTION_ORPHANED_HIDE="Hide Orphaned" COM_SCHEDULER_OPTION_ORPHANED_ONLY="Only Orphaned" @@ -127,11 +142,16 @@ COM_SCHEDULER_TABLE_CAPTION="Tasks" COM_SCHEDULER_TASK="Task" COM_SCHEDULER_TASKS_VIEW_DEFAULT_DESC="Schedule and Manage Task Routines." COM_SCHEDULER_TASKS_VIEW_DEFAULT_TITLE="Scheduled Tasks" +COM_SCHEDULER_TASK_EXECUTED="Executed: %s" +COM_SCHEDULER_TASK_FAILED="Failed: %s" COM_SCHEDULER_TASK_PARAMS_FIELDSET_LABEL="Task Parameters" COM_SCHEDULER_TASK_PRIORITY="Task Priority" COM_SCHEDULER_TASK_PRIORITY_ASC="Task Priority ascending" COM_SCHEDULER_TASK_PRIORITY_DESC="Task Priority descending" +COM_SCHEDULER_TASK_RESUME="Will Resume: %s" COM_SCHEDULER_TASK_ROUTINE_EXCEPTION="Routine threw exception: %1$s" +COM_SCHEDULER_TASK_TIMES_ASC="Times ascending" +COM_SCHEDULER_TASK_TIMES_DESC="Times descending" COM_SCHEDULER_TASK_TYPE="Task Type" COM_SCHEDULER_TASK_TYPE_ASC="Task Type ascending" COM_SCHEDULER_TASK_TYPE_DESC="Task Type descending" @@ -144,6 +164,8 @@ COM_SCHEDULER_TEST_RUN_STATUS_TERMINATED="Status: Terminated" COM_SCHEDULER_TEST_RUN_TASK="Task: \"%s\"" COM_SCHEDULER_TEST_RUN_TITLE="Test task (ID: %d)" COM_SCHEDULER_TEST_TASK="Test Task" +COM_SCHEDULER_TOOLBAR_PURGE="Clear History" +COM_SCHEDULER_TOOLBAR_PURGE_CONFIRM="Are you sure you want to delete all Execution History?" COM_SCHEDULER_TOOLBAR_UNLOCK="Unlock" COM_SCHEDULER_TYPE_CHOOSE="Select a Task type" COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND="The task routine for this task could not be found!<br>It's likely that the provider plugin was removed or disabled." diff --git a/installation/sql/mysql/extensions.sql b/installation/sql/mysql/extensions.sql index bffc70e49c150..7d4678ca5c4e9 100644 --- a/installation/sql/mysql/extensions.sql +++ b/installation/sql/mysql/extensions.sql @@ -937,6 +937,29 @@ INSERT INTO `#__scheduler_tasks` (`id`, `asset_id`, `title`, `type`, `execution_ -- -------------------------------------------------------- +-- +-- Table structure for table `#__scheduler_logs` +-- + +CREATE TABLE IF NOT EXISTS `#__scheduler_logs` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `taskname` varchar(255) NOT NULL DEFAULT '', + `tasktype` varchar(128) NOT NULL COMMENT 'unique identifier for job defined by plugin', + `duration` DECIMAL(5,3) NOT NULL, + `jobid` int UNSIGNED NOT NULL, + `taskid` int UNSIGNED NOT NULL, + `exitcode` int NOT NULL, + `lastdate` datetime COMMENT 'Timestamp of last run', + `nextdate` datetime COMMENT 'Timestamp of next (planned) run, referred for execution on trigger', + PRIMARY KEY (id), + KEY `idx_taskname` (`taskname`), + KEY `idx_tasktype` (`tasktype`), + KEY `idx_lastdate` (`lastdate`), + KEY `idx_nextdate` (`nextdate`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + -- -- Table structure for table `#__schemaorg` -- diff --git a/installation/sql/postgresql/extensions.sql b/installation/sql/postgresql/extensions.sql index 3a5188e9d261f..6cb5dd37573eb 100644 --- a/installation/sql/postgresql/extensions.sql +++ b/installation/sql/postgresql/extensions.sql @@ -899,6 +899,29 @@ INSERT INTO "#__scheduler_tasks" ("id", "asset_id", "title", "type", "execution_ (3, 99, 'Update Notification', 'update.notification', CONCAT('{"rule-type":"interval-hours","interval-hours":"24","exec-day":"01","exec-time":"', TO_CHAR(CURRENT_TIMESTAMP AT TIME ZONE 'UTC', 'HH24:00'), '"}'), '{"type":"interval","exp":"PT24H"}', 1, NULL, TO_TIMESTAMP(TO_CHAR(CURRENT_TIMESTAMP AT TIME ZONE 'UTC' + INTERVAL '24 hours', 'YYYY-MM-DD HH24:00:00'), 'YYYY-MM-DD HH24:MI:SS'), NULL, '{"individual_log":false,"log_file":"","notifications":{"success_mail":"0","failure_mail":"1","fatal_failure_mail":"1","orphan_mail":"1"},"email":"","language_override":""}', CURRENT_TIMESTAMP AT TIME ZONE 'UTC', 42); SELECT setval('#__scheduler_tasks_id_seq', 4, false); +-- -------------------------------------------------------- + +-- +-- Table structure for table "#__scheduler_logs" +-- + +CREATE TABLE IF NOT EXISTS "#__scheduler_logs" ( + "id" serial NOT NULL, + "taskname" varchar(255) DEFAULT '' NOT NULL, + "tasktype" varchar(128) NOT NULL, + "duration" NUMERIC(5,3) NOT NULL, + "jobid" integer NOT NULL, + "taskid" integer NOT NULL, + "exitcode" integer NOT NULL, + "lastdate" timestamp without time zone, + "nextdate" timestamp without time zone, + PRIMARY KEY (id) +); +CREATE INDEX "#__scheduler_logs_idx_taskname" ON "#__scheduler_logs" ("taskname"); +CREATE INDEX "#__scheduler_logs_idx_tasktype" ON "#__scheduler_logs" ("tasktype"); +CREATE INDEX "#__scheduler_logs_idx_lastdate" ON "#__scheduler_logs" ("lastdate"); +CREATE INDEX "#__scheduler_logs_idx_nextdate" ON "#__scheduler_logs" ("nextdate"); + -- -------------------------------------------------------- diff --git a/plugins/system/tasknotification/src/Extension/TaskNotification.php b/plugins/system/tasknotification/src/Extension/TaskNotification.php index 3046974ecf23a..6be75dec3dade 100644 --- a/plugins/system/tasknotification/src/Extension/TaskNotification.php +++ b/plugins/system/tasknotification/src/Extension/TaskNotification.php @@ -12,6 +12,7 @@ use Joomla\CMS\Event\Model; use Joomla\CMS\Factory; +use Joomla\CMS\Form\Form; use Joomla\CMS\Log\Log; use Joomla\CMS\Mail\MailTemplate; use Joomla\CMS\Plugin\CMSPlugin; @@ -63,11 +64,12 @@ final class TaskNotification extends CMSPlugin implements SubscriberInterface public static function getSubscribedEvents(): array { return [ - 'onContentPrepareForm' => 'injectTaskNotificationFieldset', - 'onTaskExecuteSuccess' => 'notifySuccess', - 'onTaskExecuteFailure' => 'notifyFailure', - 'onTaskRoutineNotFound' => 'notifyOrphan', - 'onTaskRecoverFailure' => 'notifyFatalRecovery', + 'onContentPrepareForm' => 'injectTaskNotificationFieldset', + 'onTaskExecuteSuccess' => 'notifySuccess', + 'onTaskRoutineWillResume' => 'notifyWillResume', + 'onTaskExecuteFailure' => 'notifyFailure', + 'onTaskRoutineNotFound' => 'notifyOrphan', + 'onTaskRecoverFailure' => 'notifyFatalRecovery', ]; } @@ -124,6 +126,13 @@ public function notifyFailure(Event $event): void /** @var Task $task */ $task = $event->getArgument('subject'); + // @todo safety checks, multiple files [?] + $outFile = $event->getArgument('subject')->snapshot['output_file'] ?? ''; + $data = $this->getDataFromTask($event->getArgument('subject')); + $model = $this->getApplication()->bootComponent('com_scheduler') + ->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]); + $model->logTask($data); + if (!(int) $task->get('params.notifications.failure_mail', 1)) { return; } @@ -131,9 +140,6 @@ public function notifyFailure(Event $event): void // Load translations $this->loadLanguage(); - // @todo safety checks, multiple files [?] - $outFile = $event->getArgument('subject')->snapshot['output_file'] ?? ''; - $data = $this->getDataFromTask($event->getArgument('subject')); $this->sendMail('plg_system_tasknotification.failure_mail', $data, $outFile); } @@ -180,19 +186,40 @@ public function notifySuccess(Event $event): void /** @var Task $task */ $task = $event->getArgument('subject'); + // @todo safety checks, multiple files [?] + $outFile = $event->getArgument('subject')->snapshot['output_file'] ?? ''; + $data = $this->getDataFromTask($event->getArgument('subject')); + $model = $this->getApplication()->bootComponent('com_scheduler') + ->getMVCFactory()->createModel('Logs', 'Administrator', ['ignore_request' => true]); + $model->logTask($data); + if (!(int) $task->get('params.notifications.success_mail', 0)) { return; } // Load translations $this->loadLanguage(); - - // @todo safety checks, multiple files [?] - $outFile = $event->getArgument('subject')->snapshot['output_file'] ?? ''; - $data = $this->getDataFromTask($event->getArgument('subject')); $this->sendMail('plg_system_tasknotification.success_mail', $data, $outFile); } + /** + * Log Task execution will resume. + * + * @param Event $event The onTaskRoutineWillResume event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function notifyWillResume(Event $event): void + { + $data = $this->getDataFromTask($event->getArgument('subject')); + $model = $this->getApplication()->bootComponent('com_scheduler') + ->getMVCFactory()->createModel('Logs', 'Administrator', ['ignore_request' => true]); + $model->logTask($data); + } + /** * Send out email notifications on fatal recovery of task execution if task configuration allows.<br/> * Fatal recovery indicated that the task either crashed the parent process or its execution lasted longer @@ -217,9 +244,6 @@ public function notifyFatalRecovery(Event $event): void return; } - // Load translations - $this->loadLanguage(); - $data = $this->getDataFromTask($event->getArgument('subject')); $this->sendMail('plg_system_tasknotification.fatal_recovery_mail', $data); } @@ -238,9 +262,12 @@ private function getDataFromTask(Task $task): array return [ 'TASK_ID' => $task->get('id'), 'TASK_TITLE' => $task->get('title'), + 'TASK_TYPE' => $task->get('type'), 'EXIT_CODE' => $task->getContent()['status'] ?? Status::NO_EXIT, 'EXEC_DATE_TIME' => $lockOrExecTime, 'TASK_OUTPUT' => $task->getContent()['output_body'] ?? '', + 'TASK_TIMES' => $task->get('times_executed'), + 'TASK_DURATION' => $task->getContent()['duration'], ]; }
The text was updated successfully, but these errors were encountered:
tecpromotion
Successfully merging a pull request may close this issue.
New language relevant PR in upstream repo: joomla/joomla-cms#42530 Here are the upstream changes:
Click to expand the diff!
The text was updated successfully, but these errors were encountered: