first commit
This commit is contained in:
19
administrator/components/com_scheduler/access.xml
Normal file
19
administrator/components/com_scheduler/access.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<access component="com_scheduler">
|
||||
<section name="component">
|
||||
<action name="core.admin" title="JACTION_ADMIN" />
|
||||
<action name="core.options" title="JACTION_OPTIONS" />
|
||||
<action name="core.manage" title="JACTION_MANAGE" />
|
||||
<action name="core.create" title="JACTION_CREATE" />
|
||||
<action name="core.delete" title="JACTION_DELETE" />
|
||||
<action name="core.edit" title="JACTION_EDIT" />
|
||||
<action name="core.edit.state" title="JACTION_EDITSTATE" />
|
||||
<action name="core.testrun" title="COM_SCHEDULER_PERMISSION_TESTRUN" />
|
||||
</section>
|
||||
<section name="task">
|
||||
<action name="core.delete" title="JACTION_DELETE" />
|
||||
<action name="core.edit" title="JACTION_EDIT" />
|
||||
<action name="core.edit.state" title="JACTION_EDITSTATE" />
|
||||
<action name="core.testrun" title="COM_SCHEDULER_PERMISSION_TESTRUN" />
|
||||
</section>
|
||||
</access>
|
||||
125
administrator/components/com_scheduler/config.xml
Normal file
125
administrator/components/com_scheduler/config.xml
Normal file
@ -0,0 +1,125 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config>
|
||||
<help key="Scheduled_Tasks:_Options"/>
|
||||
<inlinehelp button="show"/>
|
||||
<fieldset
|
||||
name="task_config"
|
||||
label="COM_SCHEDULER_CONFIG_TASKS_FIELDSET_LABEL"
|
||||
>
|
||||
<field
|
||||
name="timeout"
|
||||
type="number"
|
||||
label="COM_SCHEDULER_CONFIG_TASK_TIMEOUT_LABEL"
|
||||
default="300"
|
||||
required="true"
|
||||
min="10"
|
||||
step="1"
|
||||
validate="number"
|
||||
filter="int"
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
name="lazy_scheduler_config"
|
||||
label="COM_SCHEDULER_CONFIG_FIELDSET_LAZY_SCHEDULER_LABEL"
|
||||
description="COM_SCHEDULER_CONFIG_FIELDSET_LAZY_SCHEDULER_DESC"
|
||||
>
|
||||
<fields name="lazy_scheduler">
|
||||
<field
|
||||
name="enabled"
|
||||
type="radio"
|
||||
label="COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_ENABLED_LABEL"
|
||||
description="COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_ENABLED_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="1"
|
||||
required="true"
|
||||
filter="integer"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="interval"
|
||||
type="NumberField"
|
||||
label="COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_INTERVAL_LABEL"
|
||||
description="COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_INTERVAL_DESC"
|
||||
showon="enabled:1"
|
||||
min="60"
|
||||
max="16383"
|
||||
step="1"
|
||||
default="300"
|
||||
/>
|
||||
</fields>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
name="webcron_config"
|
||||
label="COM_SCHEDULER_CONFIG_WEBCRON_LABEL"
|
||||
description="COM_SCHEDULER_CONFIG_WEBCRON_DESC"
|
||||
>
|
||||
<fields name="webcron" addfieldprefix="Joomla\Component\Scheduler\Administrator\Field">
|
||||
<field
|
||||
name="enabled"
|
||||
type="radio"
|
||||
label="COM_SCHEDULER_CONFIG_WEBCRON_LABEL"
|
||||
description="COM_SCHEDULER_CONFIG_HASH_PROTECTION_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
required="true"
|
||||
filter="integer"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="generate_key_on_save"
|
||||
type="note"
|
||||
description="COM_SCHEDULER_CONFIG_GENERATE_WEBCRON_KEY_DESC"
|
||||
class="alert alert-warning"
|
||||
showon="enabled:1"
|
||||
/>
|
||||
<field
|
||||
name="key"
|
||||
type="hidden"
|
||||
label="COM_SCHEDULER_CONFIG_GLOBAL_WEBCRON_KEY_LABEL"
|
||||
readonly="true"
|
||||
hidden="true"
|
||||
/>
|
||||
<field
|
||||
name="base_link"
|
||||
type="WebcronLink"
|
||||
label="COM_SCHEDULER_CONFIG_GLOBAL_WEBCRON_LINK_LABEL"
|
||||
description="COM_SCHEDULER_CONFIG_GLOBAL_WEBCRON_LINK_DESC"
|
||||
showon="enabled:1"
|
||||
readonly="true"
|
||||
filter="unset"
|
||||
/>
|
||||
<field
|
||||
name="reset_key"
|
||||
type="radio"
|
||||
label="COM_SCHEDULER_CONFIG_RESET_WEBCRON_KEY_LABEL"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
showon="enabled:1"
|
||||
required="true"
|
||||
filter="integer"
|
||||
>
|
||||
<option value="0">JNO</option>
|
||||
<option value="1">JYES</option>
|
||||
</field>
|
||||
</fields>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
name="permissions"
|
||||
label="JCONFIG_PERMISSIONS_LABEL"
|
||||
description="JCONFIG_PERMISSIONS_DESC"
|
||||
>
|
||||
<field
|
||||
name="rules"
|
||||
type="rules"
|
||||
label="JCONFIG_PERMISSIONS_LABEL"
|
||||
validate="rules"
|
||||
filter="rules"
|
||||
section="component"
|
||||
component="com_scheduler"
|
||||
/>
|
||||
</fieldset>
|
||||
</config>
|
||||
@ -0,0 +1,75 @@
|
||||
<?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_DESC"
|
||||
inputmode="search"
|
||||
hint="JSEARCH_FILTER"
|
||||
/>
|
||||
<field
|
||||
name="state"
|
||||
type="taskState"
|
||||
label="JSTATUS"
|
||||
class="js-select-submit-on-change"
|
||||
validate="options"
|
||||
>
|
||||
<option value="">JOPTION_SELECT_PUBLISHED</option>
|
||||
</field>
|
||||
<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="orphaned"
|
||||
type="list"
|
||||
label="COM_SCHEDULER_FIELD_LABEL_SHOW_ORPHANED"
|
||||
default="-1"
|
||||
class="js-select-submit-on-change"
|
||||
>
|
||||
<option value="-1">COM_SCHEDULER_OPTION_ORPHANED_HIDE</option>
|
||||
<option value="0">COM_SCHEDULER_OPTION_ORPHANED_SHOW</option>
|
||||
<option value="1">COM_SCHEDULER_OPTION_ORPHANED_ONLY</option>
|
||||
</field>
|
||||
</fields>
|
||||
|
||||
<fields name="list">
|
||||
<field
|
||||
name="fullordering"
|
||||
type="list"
|
||||
label="JGLOBAL_SORT_BY"
|
||||
class="js-select-submit-on-change"
|
||||
default="a.title ASC"
|
||||
validate="options"
|
||||
>
|
||||
<option value="">JGLOBAL_SORT_BY</option>
|
||||
<option value="a.ordering ASC">JGRID_HEADING_ORDERING_ASC</option>
|
||||
<option value="a.ordering DESC">JGRID_HEADING_ORDERING_DESC</option>
|
||||
<option value="a.state ASC">JSTATUS_ASC</option>
|
||||
<option value="a.state DESC">JSTATUS_DESC</option>
|
||||
<option value="a.title ASC">JGLOBAL_TITLE_ASC</option>
|
||||
<option value="a.title DESC">JGLOBAL_TITLE_DESC</option>
|
||||
<option value="j.type_title ASC">COM_SCHEDULER_TASK_TYPE_ASC</option>
|
||||
<option value="j.type_title DESC">COM_SCHEDULER_TASK_TYPE_DESC</option>
|
||||
<option value="a.last_execution ASC">COM_SCHEDULER_LAST_RUN_ASC</option>
|
||||
<option value="a.last_execution DESC">COM_SCHEDULER_LAST_RUN_DESC</option>
|
||||
<option value="a.priority ASC">COM_SCHEDULER_TASK_PRIORITY_ASC</option>
|
||||
<option value="a.priority DESC">COM_SCHEDULER_TASK_PRIORITY_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="25"
|
||||
class="js-select-submit-on-change"
|
||||
/>
|
||||
</fields>
|
||||
</form>
|
||||
275
administrator/components/com_scheduler/forms/task.xml
Normal file
275
administrator/components/com_scheduler/forms/task.xml
Normal file
@ -0,0 +1,275 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form
|
||||
addfieldprefix="Joomla\Component\Scheduler\Administrator\Field"
|
||||
addruleprefix="Joomla\Component\Scheduler\Administrator\Rule"
|
||||
>
|
||||
<fields>
|
||||
<field
|
||||
name="title"
|
||||
type="text"
|
||||
label="JGLOBAL_TITLE"
|
||||
size="40"
|
||||
maxlength="100"
|
||||
required="true"
|
||||
/>
|
||||
|
||||
<fieldset name="aside">
|
||||
<field
|
||||
name="state"
|
||||
type="taskState"
|
||||
label="JSTATUS"
|
||||
default="1"
|
||||
class="form-select-color-state"
|
||||
size="1"
|
||||
validate="options"
|
||||
optionsFilter="-2,0,1"
|
||||
/>
|
||||
<field
|
||||
name="note"
|
||||
type="text"
|
||||
label="COM_SCHEDULER_LABEL_NOTES"
|
||||
maxlength="255"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="exec_hist">
|
||||
<field
|
||||
name="last_exit_code"
|
||||
type="number"
|
||||
label="COM_SCHEDULER_LABEL_EXIT_CODE"
|
||||
default="0"
|
||||
disabled="true"
|
||||
filter="unset"
|
||||
/>
|
||||
<field
|
||||
name="last_execution"
|
||||
type="text"
|
||||
label="COM_SCHEDULER_LABEL_LAST_EXEC"
|
||||
disabled="true"
|
||||
filter="unset"
|
||||
/>
|
||||
<field
|
||||
name="next_execution"
|
||||
type="text"
|
||||
label="COM_SCHEDULER_LABEL_NEXT_EXEC"
|
||||
disabled="true"
|
||||
filter="unset"
|
||||
/>
|
||||
<field
|
||||
name="times_executed"
|
||||
type="number"
|
||||
label="COM_SCHEDULER_LABEL_TIMES_EXEC"
|
||||
disabled="true"
|
||||
filter="unset"
|
||||
/>
|
||||
<field
|
||||
name="times_failed"
|
||||
type="number"
|
||||
label="COM_SCHEDULER_LABEL_TIMES_FAIL"
|
||||
disabled="true"
|
||||
filter="unset"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="details">
|
||||
<field
|
||||
name="id"
|
||||
type="text"
|
||||
label="JGLOBAL_FIELD_ID_LABEL"
|
||||
default="0"
|
||||
class="readonly"
|
||||
readonly="true"
|
||||
/>
|
||||
<field
|
||||
name="type"
|
||||
type="text"
|
||||
label="COM_SCHEDULER_FIELD_TASK_TYPE"
|
||||
required="true"
|
||||
readonly="true"
|
||||
maxlength="1024"
|
||||
/>
|
||||
<field
|
||||
name="asset_id"
|
||||
type="hidden"
|
||||
filter="unset"
|
||||
/>
|
||||
<field
|
||||
name="created"
|
||||
type="calendar"
|
||||
label="JGLOBAL_CREATED"
|
||||
size="22"
|
||||
translateformat="true"
|
||||
showtime="true"
|
||||
filter="user_utc"
|
||||
/>
|
||||
<field
|
||||
name="created_by"
|
||||
type="user"
|
||||
label="JGLOBAL_FIELD_CREATED_BY_LABEL"
|
||||
validate="UserId"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<field
|
||||
name="rules"
|
||||
type="rules"
|
||||
label="JFIELD_RULES_LABEL"
|
||||
translate_label="false"
|
||||
filter="rules"
|
||||
component="com_scheduler"
|
||||
section="task"
|
||||
validate="rules"
|
||||
/>
|
||||
</fields>
|
||||
|
||||
<fields name="execution_rules">
|
||||
<fieldset name="basic">
|
||||
<field
|
||||
name="rule-type"
|
||||
type="ExecutionRule"
|
||||
label="COM_SCHEDULER_FIELD_LABEL_EXEC_RULE"
|
||||
required="true"
|
||||
validate="options"
|
||||
/>
|
||||
<field
|
||||
name="interval-minutes"
|
||||
type="interval"
|
||||
label="COM_SCHEDULER_FIELD_LABEL_INTERVAL_MINUTES"
|
||||
subtype="minutes"
|
||||
validate="ExecutionRules"
|
||||
showon="rule-type:interval-minutes">
|
||||
<option value="" disabled="true" hidden="true">
|
||||
COM_SCHEDULER_SELECT_INTERVAL_MINUTES
|
||||
</option>
|
||||
</field>
|
||||
<field
|
||||
name="interval-hours"
|
||||
type="interval"
|
||||
label="COM_SCHEDULER_FIELD_LABEL_INTERVAL_HOURS"
|
||||
subtype="hours"
|
||||
validate="ExecutionRules"
|
||||
showon="rule-type:interval-hours"
|
||||
/>
|
||||
<field
|
||||
name="interval-days"
|
||||
type="interval"
|
||||
label="COM_SCHEDULER_FIELD_LABEL_INTERVAL_DAYS"
|
||||
subtype="days"
|
||||
validate="ExecutionRules"
|
||||
showon="rule-type:interval-days"
|
||||
/>
|
||||
<field
|
||||
name="interval-months"
|
||||
type="interval"
|
||||
label="COM_SCHEDULER_FIELD_LABEL_INTERVAL_MONTHS"
|
||||
subtype="months"
|
||||
onlyNumericLabels="true"
|
||||
validate="ExecutionRules"
|
||||
showon="rule-type:interval-months"
|
||||
/>
|
||||
<field
|
||||
name="exec-day"
|
||||
type="number"
|
||||
label="COM_SCHEDULER_LABEL_EXEC_DAY"
|
||||
min="1"
|
||||
max="31"
|
||||
step="1"
|
||||
showon="rule-type:interval-months"
|
||||
/>
|
||||
<field
|
||||
name="exec-time"
|
||||
type="time"
|
||||
label="COM_SCHEDULER_LABEL_EXEC_TIME"
|
||||
showon="rule-type:interval-days[OR]rule-type:interval-months"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset
|
||||
name="custom-cron-rules"
|
||||
label="COM_SCHEDULER_LABEL_EXEC_INTERVAL"
|
||||
>
|
||||
<fields name="cron-expression">
|
||||
<field
|
||||
name="minutes"
|
||||
type="cron"
|
||||
label="COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_MINUTES"
|
||||
subtype="minutes"
|
||||
multiple="true"
|
||||
validate="ExecutionRules"
|
||||
/>
|
||||
<field
|
||||
name="hours"
|
||||
type="cron"
|
||||
label="COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_HOURS"
|
||||
subtype="hours"
|
||||
multiple="true"
|
||||
validate="ExecutionRules"
|
||||
/>
|
||||
<field
|
||||
name="days_month"
|
||||
type="cron"
|
||||
label="COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_DAYS_M"
|
||||
subtype="days_month"
|
||||
multiple="true"
|
||||
validate="ExecutionRules"
|
||||
/>
|
||||
<field
|
||||
name="months"
|
||||
type="cron"
|
||||
label="COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_MONTHS"
|
||||
subtype="months"
|
||||
multiple="true"
|
||||
validate="ExecutionRules"
|
||||
/>
|
||||
<field
|
||||
name="days_week"
|
||||
type="cron"
|
||||
label="COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_DAYS_W"
|
||||
subtype="days_week"
|
||||
multiple="true"
|
||||
validate="ExecutionRules"
|
||||
/>
|
||||
</fields>
|
||||
</fieldset>
|
||||
</fields>
|
||||
|
||||
<fieldset name="priority">
|
||||
<field
|
||||
name="priority"
|
||||
type="list"
|
||||
label="COM_SCHEDULER_LABEL_TASK_PRIORITY"
|
||||
description="COM_SCHEDULER_DESCRIPTION_TASK_PRIORITY"
|
||||
validate="options"
|
||||
required="true"
|
||||
default="0"
|
||||
>
|
||||
<option value="-1">COM_SCHEDULER_LABEL_TASK_PRIORITY_LOW</option>
|
||||
<option value="0">COM_SCHEDULER_LABEL_TASK_PRIORITY_NORMAL</option>
|
||||
<option value="1">COM_SCHEDULER_LABEL_TASK_PRIORITY_HIGH</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
|
||||
<fields name="params">
|
||||
<fieldset name="logging">
|
||||
<field
|
||||
name="individual_log"
|
||||
type="radio"
|
||||
label="COM_SCHEDULER_FIELD_LABEL_INDIVIDUAL_LOG"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
required="true"
|
||||
filter="boolean"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="log_file"
|
||||
type="text"
|
||||
label="COM_SCHEDULER_FIELD_LABEL_LOG_FILE"
|
||||
showon="individual_log:1"
|
||||
hint="COM_SCHEDULER_FIELD_HINT_LOG_FILE_AUTO"
|
||||
/>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</form>
|
||||
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @copyright (C) 2021 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\Application\CMSApplication;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
|
||||
extract($displayData);
|
||||
|
||||
/**
|
||||
* Layout variables
|
||||
* -----------------
|
||||
*
|
||||
* @var string $id DOM id of the field.
|
||||
* @var string $label Label of the field.
|
||||
* @var string $name Name of the input field.
|
||||
* @var string $value Value attribute of the field.
|
||||
*/
|
||||
|
||||
Text::script('ERROR');
|
||||
Text::script('MESSAGE');
|
||||
Text::script('COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_SUCCESS');
|
||||
Text::script('COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_FAIL');
|
||||
|
||||
/** @var CMSApplication $app */
|
||||
$app = Factory::getApplication();
|
||||
$wa = $app->getDocument()->getWebAssetManager();
|
||||
$wa->getRegistry()->addExtensionRegistryFile('com_scheduler');
|
||||
$wa->useScript('com_scheduler.scheduler-config');
|
||||
?>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="<?php echo $name; ?>"
|
||||
id="<?php echo $id; ?>"
|
||||
readonly
|
||||
value="<?php echo htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); ?>"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
id="link-copy"
|
||||
title="<?php echo Text::_('COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_DESC'); ?>"><?php echo Text::_('JLIB_HTML_BATCH_COPY'); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
30
administrator/components/com_scheduler/scheduler.xml
Normal file
30
administrator/components/com_scheduler/scheduler.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="component" method="upgrade">
|
||||
<name>com_scheduler</name>
|
||||
<author>Joomla! Project</author>
|
||||
<creationDate>2021-07</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.0</version>
|
||||
<description>COM_SCHEDULER_XML_DESCRIPTION</description>
|
||||
<namespace path="src">Joomla\Component\Scheduler</namespace>
|
||||
<media destination="com_scheduler" folder="media">
|
||||
<folder>js</folder>
|
||||
<folder>css</folder>
|
||||
</media>
|
||||
<administration>
|
||||
<filename>access.xml</filename>
|
||||
<filename>config.xml</filename>
|
||||
<filename>scheduler.xml</filename>
|
||||
<folder>forms</folder>
|
||||
<folder>services</folder>
|
||||
<folder>src</folder>
|
||||
<folder>tmpl</folder>
|
||||
<languages folder="admin">
|
||||
<language tag="en-GB">language/en-GB/com_scheduler.ini</language>
|
||||
<language tag="en-GB">language/en-GB/com_scheduler.sys.ini</language>
|
||||
</languages>
|
||||
</administration>
|
||||
</extension>
|
||||
64
administrator/components/com_scheduler/services/provider.php
Normal file
64
administrator/components/com_scheduler/services/provider.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
// Restrict direct access
|
||||
\defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
|
||||
use Joomla\CMS\Extension\ComponentInterface;
|
||||
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
|
||||
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
|
||||
use Joomla\CMS\HTML\Registry;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\Component\Scheduler\Administrator\Extension\SchedulerComponent;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
|
||||
/**
|
||||
* The com_scheduler service provider.
|
||||
* Returns an instance of the Component's Service Provider Interface
|
||||
* used to register the components initializers into a DI container
|
||||
* created by the application.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
return new class () implements ServiceProviderInterface {
|
||||
/**
|
||||
* Registers the service provider with a DI container.
|
||||
*
|
||||
* @param Container $container The DI container.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function register(Container $container)
|
||||
{
|
||||
/**
|
||||
* Register the MVCFactory and ComponentDispatcherFactory providers to map
|
||||
* 'MVCFactoryInterface' and 'ComponentDispatcherFactoryInterface' to their
|
||||
* initializers and register them with the component's DI container.
|
||||
*/
|
||||
$container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Scheduler'));
|
||||
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Scheduler'));
|
||||
|
||||
$container->set(
|
||||
ComponentInterface::class,
|
||||
function (Container $container) {
|
||||
$component = new SchedulerComponent($container->get(ComponentDispatcherFactoryInterface::class));
|
||||
|
||||
$component->setRegistry($container->get(Registry::class));
|
||||
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
|
||||
|
||||
return $component;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Default controller for com_scheduler.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class DisplayController extends BaseController
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $default_view = 'tasks';
|
||||
|
||||
/**
|
||||
* @param boolean $cachable If true, the view output will be cached
|
||||
* @param array $urlparams An array of safe url parameters and their variable types.
|
||||
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
|
||||
*
|
||||
* @return BaseController|boolean Returns either a BaseController object to support chaining, or false on failure
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function display($cachable = false, $urlparams = [])
|
||||
{
|
||||
$layout = $this->input->get('layout', 'default');
|
||||
|
||||
// Check for edit form.
|
||||
if ($layout === 'edit') {
|
||||
if (!$this->validateEntry()) {
|
||||
$tasksViewUrl = Route::_('index.php?option=com_scheduler&view=tasks', false);
|
||||
$this->setRedirect($tasksViewUrl);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Let the parent method take over
|
||||
return parent::display($cachable, $urlparams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates entry to the view
|
||||
*
|
||||
* @param string $layout The layout to validate entry for (defaults to 'edit')
|
||||
*
|
||||
* @return boolean True is entry is valid
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private function validateEntry(string $layout = 'edit'): bool
|
||||
{
|
||||
$context = 'com_scheduler';
|
||||
$id = $this->input->getInt('id');
|
||||
$isValid = true;
|
||||
|
||||
switch ($layout) {
|
||||
case 'edit':
|
||||
// True if controller was called and verified permissions
|
||||
$inEditList = $this->checkEditId("$context.edit.task", $id);
|
||||
$isNew = ($id == 0);
|
||||
|
||||
// For new item, entry is invalid if task type was not selected through SelectView
|
||||
if ($isNew && !$this->app->getUserState("$context.add.task.task_type")) {
|
||||
$this->setMessage((Text::_('COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW')), 'error');
|
||||
$isValid = false;
|
||||
} elseif (!$inEditList) {
|
||||
// For existing item, entry is invalid if TaskController has not granted access
|
||||
if (!\count($this->app->getMessageQueue())) {
|
||||
$this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error');
|
||||
}
|
||||
|
||||
$isValid = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return $isValid;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\FormController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* MVC Controller for the item configuration page (TaskView).
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class TaskController extends FormController
|
||||
{
|
||||
/**
|
||||
* Add a new record
|
||||
*
|
||||
* @return boolean
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function add(): bool
|
||||
{
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->app;
|
||||
$input = $app->getInput();
|
||||
$validTaskOptions = SchedulerHelper::getTaskOptions();
|
||||
|
||||
$canAdd = parent::add();
|
||||
|
||||
if ($canAdd !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$taskType = $input->get('type');
|
||||
$taskOption = $validTaskOptions->findOption($taskType) ?: null;
|
||||
|
||||
if (!$taskOption) {
|
||||
// ? : Is this the right redirect [review]
|
||||
$redirectUrl = 'index.php?option=' . $this->option . '&view=select&layout=edit';
|
||||
$this->setRedirect(Route::_($redirectUrl, false));
|
||||
$app->enqueueMessage(Text::_('COM_SCHEDULER_ERROR_INVALID_TASK_TYPE'), 'warning');
|
||||
$canAdd = false;
|
||||
}
|
||||
|
||||
$app->setUserState('com_scheduler.add.task.task_type', $taskType);
|
||||
$app->setUserState('com_scheduler.add.task.task_option', $taskOption);
|
||||
|
||||
// @todo : Parameter array handling below?
|
||||
|
||||
return $canAdd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override parent cancel method to reset the add task state
|
||||
*
|
||||
* @param ?string $key Primary key from the URL param
|
||||
*
|
||||
* @return boolean True if access level checks pass
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function cancel($key = null): bool
|
||||
{
|
||||
$result = parent::cancel($key);
|
||||
|
||||
$this->app->setUserState('com_scheduler.add.task.task_type', null);
|
||||
$this->app->setUserState('com_scheduler.add.task.task_option', null);
|
||||
|
||||
// ? Do we need to redirect based on URL's 'return' param? {@see ModuleController}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has the authority to edit an asset
|
||||
*
|
||||
* @param array $data Array of input data
|
||||
* @param string $key Name of key for primary key, defaults to 'id'
|
||||
*
|
||||
* @return boolean True if user is allowed to edit record
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected function allowEdit($data = [], $key = 'id'): bool
|
||||
{
|
||||
// Extract the recordId from $data, will come in handy
|
||||
$recordId = (int) $data[$key] ?? 0;
|
||||
|
||||
/**
|
||||
* Zero record (id:0), return component edit permission by calling parent controller method
|
||||
* ?: Is this the right way to do this?
|
||||
*/
|
||||
if ($recordId === 0) {
|
||||
return parent::allowEdit($data, $key);
|
||||
}
|
||||
|
||||
// @todo : Check if this works as expected
|
||||
return $this->app->getIdentity()->authorise('core.edit', 'com_scheduler.task.' . $recordId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\AdminController;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\Component\Scheduler\Administrator\Model\TaskModel;
|
||||
use Joomla\Utilities\ArrayHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* MVC Controller for TasksView.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class TasksController extends AdminController
|
||||
{
|
||||
/**
|
||||
* Proxy for the parent method.
|
||||
*
|
||||
* @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 BaseDatabaseModel
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function getModel($name = 'Task', $prefix = 'Administrator', $config = ['ignore_request' => true]): BaseDatabaseModel
|
||||
{
|
||||
return parent::getModel($name, $prefix, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a locked task, i.e., a task that is presumably still running but might have crashed and got stuck in the
|
||||
* "locked" state.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function unlock(): void
|
||||
{
|
||||
// Check for request forgeries
|
||||
$this->checkToken();
|
||||
|
||||
/** @var integer[] $cid Items to publish (from request parameters). */
|
||||
$cid = (array) $this->input->get('cid', [], 'int');
|
||||
|
||||
// Remove zero values resulting from input filter
|
||||
$cid = array_filter($cid);
|
||||
|
||||
if (empty($cid)) {
|
||||
$this->app->getLogger()
|
||||
->warning(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), ['category' => 'jerror']);
|
||||
} else {
|
||||
/** @var TaskModel $model */
|
||||
$model = $this->getModel();
|
||||
|
||||
// Make sure the item IDs are integers
|
||||
$cid = ArrayHelper::toInteger($cid);
|
||||
|
||||
// Unlock the items.
|
||||
try {
|
||||
$model->unlock($cid);
|
||||
$errors = $model->getErrors();
|
||||
$noticeText = null;
|
||||
|
||||
if ($errors) {
|
||||
$this->app->enqueueMessage(Text::plural($this->text_prefix . '_N_ITEMS_FAILED_UNLOCKING', \count($cid)), 'error');
|
||||
} else {
|
||||
$noticeText = $this->text_prefix . '_N_ITEMS_UNLOCKED';
|
||||
}
|
||||
|
||||
if (\count($cid)) {
|
||||
$this->setMessage(Text::plural($noticeText, \count($cid)));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->setMessage($e->getMessage(), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
$this->setRedirect(
|
||||
Route::_(
|
||||
'index.php?option=' . $this->option . '&view=' . $this->view_list
|
||||
. $this->getRedirectToListAppend(),
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Event;
|
||||
|
||||
use Joomla\CMS\Event\AbstractEvent;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\Task;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Event class for onExecuteTask event.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class ExecuteTaskEvent extends AbstractEvent
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $name The event name.
|
||||
* @param array $arguments The event arguments.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function __construct($name, array $arguments = [])
|
||||
{
|
||||
parent::__construct($name, $arguments);
|
||||
|
||||
$arguments['resultSnapshot'] = null;
|
||||
|
||||
if (!($arguments['subject'] ?? null) instanceof Task) {
|
||||
throw new \BadMethodCallException("The subject given for $name event must be an instance of " . Task::class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the task result snapshot and stops event propagation.
|
||||
*
|
||||
* @param array $snapshot The task snapshot.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function setResult(array $snapshot = []): void
|
||||
{
|
||||
$this->arguments['resultSnapshot'] = $snapshot;
|
||||
|
||||
if (!empty($snapshot)) {
|
||||
$this->stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return integer The task's taskId.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function getTaskId(): int
|
||||
{
|
||||
return $this->arguments['subject']->get('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The task's 'type'.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function getRoutineId(): string
|
||||
{
|
||||
return $this->arguments['subject']->get('type');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the snapshot of the triggered task if available, else an empty array
|
||||
*
|
||||
* @return array The task snapshot if available, else null
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function getResultSnapshot(): array
|
||||
{
|
||||
return $this->arguments['resultSnapshot'] ?? [];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Extension;
|
||||
|
||||
use Joomla\CMS\Extension\BootableExtensionInterface;
|
||||
use Joomla\CMS\Extension\MVCComponent;
|
||||
use Joomla\CMS\HTML\HTMLRegistryAwareTrait;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Component class for com_scheduler.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @todo Set up logger(s) here.
|
||||
*/
|
||||
class SchedulerComponent extends MVCComponent implements BootableExtensionInterface
|
||||
{
|
||||
use HTMLRegistryAwareTrait;
|
||||
|
||||
/**
|
||||
* Booting the extension. This is the function to set up the environment of the extension like
|
||||
* registering new class loaders, etc.
|
||||
*
|
||||
* If required, some initial set up can be done from services of the container, eg.
|
||||
* registering HTML services.
|
||||
*
|
||||
* @param ContainerInterface $container The container
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function boot(ContainerInterface $container): void
|
||||
{
|
||||
// Pass
|
||||
}
|
||||
}
|
||||
189
administrator/components/com_scheduler/src/Field/CronField.php
Normal file
189
administrator/components/com_scheduler/src/Field/CronField.php
Normal file
@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Field;
|
||||
|
||||
use Joomla\CMS\Form\Field\ListField;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Multi-select form field, supporting inputs of:
|
||||
* minutes, hours, days of week, days of month and months.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class CronField extends ListField
|
||||
{
|
||||
/**
|
||||
* The subtypes supported by this field type.
|
||||
*
|
||||
* @var string[]
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private const SUBTYPES = [
|
||||
'minutes',
|
||||
'hours',
|
||||
'days_month',
|
||||
'months',
|
||||
'days_week',
|
||||
];
|
||||
|
||||
/**
|
||||
* Count of predefined options for each subtype
|
||||
*
|
||||
* @var int[][]
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private const OPTIONS_RANGE = [
|
||||
'minutes' => [0, 59],
|
||||
'hours' => [0, 23],
|
||||
'days_week' => [1, 7],
|
||||
'days_month' => [1, 31],
|
||||
'months' => [1, 12],
|
||||
];
|
||||
|
||||
/**
|
||||
* Response labels for the 'month' and 'days_week' subtypes.
|
||||
* The labels are language constants translated when needed.
|
||||
*
|
||||
* @var string[][]
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private const PREPARED_RESPONSE_LABELS = [
|
||||
'months' => [
|
||||
'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE',
|
||||
'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER',
|
||||
],
|
||||
'days_week' => [
|
||||
'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY',
|
||||
'FRIDAY', 'SATURDAY', 'SUNDAY',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The form field type.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $type = 'cronIntervals';
|
||||
|
||||
/**
|
||||
* The subtype of the CronIntervals field
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private $subtype;
|
||||
|
||||
/**
|
||||
* If true, field options will include a wildcard
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private $wildcard;
|
||||
|
||||
/**
|
||||
* If true, field will only have numeric labels (for days_week and months)
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private $onlyNumericLabels;
|
||||
|
||||
/**
|
||||
* Override the parent method to set deal with subtypes.
|
||||
*
|
||||
* @param \SimpleXMLElement $element The SimpleXMLElement object representing the `<field>` tag for the form
|
||||
* field object.
|
||||
* @param mixed $value The form field value to validate.
|
||||
* @param string $group The field name group control value. This acts as an array container for
|
||||
* the field. For example if the field has `name="foo"` and the group value is
|
||||
* set to "bar" then the full field name would end up being "bar[foo]".
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function setup(\SimpleXMLElement $element, $value, $group = null): bool
|
||||
{
|
||||
$parentResult = parent::setup($element, $value, $group);
|
||||
|
||||
$subtype = ((string) $element['subtype'] ?? '') ?: null;
|
||||
$wildcard = ((string) $element['wildcard'] ?? '') === 'true';
|
||||
$onlyNumericLabels = ((string) $element['onlyNumericLabels']) === 'true';
|
||||
|
||||
if (!($subtype && \in_array($subtype, self::SUBTYPES))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->subtype = $subtype;
|
||||
$this->wildcard = $wildcard;
|
||||
$this->onlyNumericLabels = $onlyNumericLabels;
|
||||
|
||||
return $parentResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get field options
|
||||
*
|
||||
* @return array Array of objects representing options in the options list
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected function getOptions(): array
|
||||
{
|
||||
$subtype = $this->subtype;
|
||||
$options = parent::getOptions();
|
||||
|
||||
if (!\in_array($subtype, self::SUBTYPES)) {
|
||||
return $options;
|
||||
}
|
||||
|
||||
if ($this->wildcard) {
|
||||
try {
|
||||
$options[] = HTMLHelper::_('select.option', '*', '*');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
[$optionLower, $optionUpper] = self::OPTIONS_RANGE[$subtype];
|
||||
|
||||
// If we need text labels, we translate them first
|
||||
if (\array_key_exists($subtype, self::PREPARED_RESPONSE_LABELS) && !$this->onlyNumericLabels) {
|
||||
$labels = array_map(
|
||||
static function (string $string): string {
|
||||
return Text::_($string);
|
||||
},
|
||||
self::PREPARED_RESPONSE_LABELS[$subtype]
|
||||
);
|
||||
} else {
|
||||
$labels = range(...self::OPTIONS_RANGE[$subtype]);
|
||||
}
|
||||
|
||||
for ([$i, $l] = [$optionLower, 0]; $i <= $optionUpper; $i++, $l++) {
|
||||
try {
|
||||
$options[] = HTMLHelper::_('select.option', (string) ($i), $labels[$l]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\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 select list containing valid Cron interval types.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class ExecutionRuleField extends PredefinedlistField
|
||||
{
|
||||
/**
|
||||
* The form field type.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $type = 'ExecutionRule';
|
||||
|
||||
/**
|
||||
* Available execution rules.
|
||||
*
|
||||
* @var string[]
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $predefinedOptions = [
|
||||
'interval-minutes' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES',
|
||||
'interval-hours' => 'COM_SCHEDULER_EXECUTION_INTERVAL_HOURS',
|
||||
'interval-days' => 'COM_SCHEDULER_EXECUTION_INTERVAL_DAYS',
|
||||
'interval-months' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS',
|
||||
'cron-expression' => 'COM_SCHEDULER_EXECUTION_CRON_EXPRESSION',
|
||||
'manual' => 'COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL',
|
||||
];
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Field;
|
||||
|
||||
use Joomla\CMS\Form\Field\NumberField;
|
||||
use Joomla\CMS\Form\FormField;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Select style field for interval(s) in minutes, hours, days and months.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class IntervalField extends NumberField
|
||||
{
|
||||
/**
|
||||
* The form field type.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $type = 'Intervals';
|
||||
|
||||
/**
|
||||
* The subtypes supported by this field type => [minVal, maxVal]
|
||||
*
|
||||
* @var string[]
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private const SUBTYPES = [
|
||||
'minutes' => [1, 59],
|
||||
'hours' => [1, 23],
|
||||
'days' => [1, 30],
|
||||
'months' => [1, 12],
|
||||
];
|
||||
|
||||
/**
|
||||
* The allowable maximum value of the field.
|
||||
*
|
||||
* @var float
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $max;
|
||||
|
||||
/**
|
||||
* The allowable minimum value of the field.
|
||||
*
|
||||
* @var float
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $min;
|
||||
|
||||
/**
|
||||
* The step by which value of the field increased or decreased.
|
||||
*
|
||||
* @var float
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $step = 1;
|
||||
|
||||
/**
|
||||
* Override the parent method to set deal with subtypes.
|
||||
*
|
||||
* @param \SimpleXMLElement $element The SimpleXMLElement object representing the `<field>` tag for the form
|
||||
* field object.
|
||||
* @param mixed $value The form field value to validate.
|
||||
* @param string $group The field name group control value. This acts as an array container for
|
||||
* the field. For example if the field has `name="foo"` and the group value is
|
||||
* set to "bar" then the full field name would end up being "bar[foo]".
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function setup(\SimpleXMLElement $element, $value, $group = null): bool
|
||||
{
|
||||
$parentResult = FormField::setup($element, $value, $group);
|
||||
$subtype = ((string) $element['subtype'] ?? '') ?: null;
|
||||
|
||||
if (empty($subtype) || !\array_key_exists($subtype, self::SUBTYPES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[$this->min, $this->max] = self::SUBTYPES[$subtype];
|
||||
|
||||
return $parentResult;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\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 4.1.0
|
||||
*/
|
||||
class TaskStateField extends PredefinedlistField
|
||||
{
|
||||
/**
|
||||
* The form field type.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public $type = 'taskState';
|
||||
|
||||
/**
|
||||
* Available states
|
||||
*
|
||||
* @var string[]
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $predefinedOptions = [
|
||||
-2 => 'JTRASHED',
|
||||
0 => 'JDISABLED',
|
||||
1 => 'JENABLED',
|
||||
'*' => 'JALL',
|
||||
];
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Field;
|
||||
|
||||
use Joomla\CMS\Form\Field\ListField;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\TaskOption;
|
||||
use Joomla\Utilities\ArrayHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* A list field with all available task routines.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class TaskTypeField extends ListField
|
||||
{
|
||||
/**
|
||||
* The form field type.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $type = 'taskType';
|
||||
|
||||
/**
|
||||
* Method to get field options
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getOptions(): array
|
||||
{
|
||||
$options = parent::getOptions();
|
||||
|
||||
// Get all available task types and sort by title
|
||||
$types = ArrayHelper::sortObjects(
|
||||
SchedulerHelper::getTaskOptions()->options,
|
||||
'title',
|
||||
1
|
||||
);
|
||||
|
||||
// Closure to add a TaskOption as a <select> option in $options: array
|
||||
$addTypeAsOption = function (TaskOption $type) use (&$options) {
|
||||
try {
|
||||
$options[] = HTMLHelper::_('select.option', $type->id, $type->title);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
}
|
||||
};
|
||||
|
||||
// Call $addTypeAsOption on each type
|
||||
array_map($addTypeAsOption, $types);
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Field;
|
||||
|
||||
use Joomla\CMS\Form\Field\TextField;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Field to override the text field layout to add a copy-text button, used in the com_scheduler
|
||||
* configuration form.
|
||||
* This field class is only needed because the layout file is in a non-global directory, so this should
|
||||
* be made redundant and removed if/once the layout is shifted to `JPATH_SITE/layout/`
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class WebcronLinkField extends TextField
|
||||
{
|
||||
/**
|
||||
* We use a custom layout that allows for the link to be copied.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $layout = 'form.field.webcron_link';
|
||||
|
||||
/**
|
||||
* Override layout paths.
|
||||
*
|
||||
* @inheritDoc
|
||||
* @return string[]
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected function getLayoutPaths(): array
|
||||
{
|
||||
$s = DIRECTORY_SEPARATOR;
|
||||
|
||||
return array_merge(
|
||||
[JPATH_ADMINISTRATOR . "{$s}/components{$s}com_scheduler{$s}layouts{$s}"],
|
||||
parent::getLayoutPaths()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Helper;
|
||||
|
||||
use Cron\CronExpression;
|
||||
use Joomla\CMS\Date\Date;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\Task;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
use Joomla\Utilities\ArrayHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Helper class for supporting task execution rules.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @todo This helper should probably be merged into the {@see Task} class.
|
||||
*/
|
||||
class ExecRuleHelper
|
||||
{
|
||||
/**
|
||||
* The execution rule type
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private $task;
|
||||
|
||||
/**
|
||||
* @var object
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private $rule;
|
||||
|
||||
/**
|
||||
* @param array|object $task A task entry
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function __construct($task)
|
||||
{
|
||||
$this->task = \is_array($task) ? $task : ArrayHelper::fromObject($task);
|
||||
$rule = $this->getFromTask('cron_rules');
|
||||
$this->rule = \is_string($rule)
|
||||
? (object) json_decode($rule)
|
||||
: (\is_array($rule) ? (object) $rule : $rule);
|
||||
$this->type = $this->rule->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a property from the task array
|
||||
*
|
||||
* @param string $property The property to get
|
||||
* @param mixed $default The default value returned if property does not exist
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private function getFromTask(string $property, $default = null)
|
||||
{
|
||||
$property = ArrayHelper::getValue($this->task, $property);
|
||||
|
||||
return $property ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param boolean $string If true, an SQL formatted string is returned.
|
||||
* @param boolean $basisNow If true, the current date-time is used as the basis for projecting the next
|
||||
* execution.
|
||||
*
|
||||
* @return ?Date|string
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function nextExec(bool $string = true, bool $basisNow = false)
|
||||
{
|
||||
// Exception handling here
|
||||
switch ($this->type) {
|
||||
case 'interval':
|
||||
$lastExec = Factory::getDate($basisNow ? 'now' : $this->getFromTask('last_execution'), 'UTC');
|
||||
$interval = new \DateInterval($this->rule->exp);
|
||||
$nextExec = $lastExec->add($interval);
|
||||
$nextExec = $string ? $nextExec->toSql() : $nextExec;
|
||||
break;
|
||||
case 'cron-expression':
|
||||
// @todo: testing
|
||||
$cExp = new CronExpression((string) $this->rule->exp);
|
||||
$nextExec = $cExp->getNextRunDate('now', 0, false, 'UTC');
|
||||
$nextExec = $string ? $this->dateTimeToSql($nextExec) : $nextExec;
|
||||
break;
|
||||
default:
|
||||
// 'manual' execution is handled here.
|
||||
$nextExec = null;
|
||||
}
|
||||
|
||||
return $nextExec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sql-formatted string for a DateTime object.
|
||||
* Only needed for DateTime objects returned by CronExpression, JDate supports this as class method.
|
||||
*
|
||||
* @param \DateTime $dateTime A DateTime object to format
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private function dateTimeToSql(\DateTime $dateTime): string
|
||||
{
|
||||
static $db;
|
||||
$db = $db ?? Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
return $dateTime->format($db->getDateFormat());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Helper;
|
||||
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\Event\AbstractEvent;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\TaskOptions;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* The SchedulerHelper class.
|
||||
* Provides static methods used across com_scheduler
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
abstract class SchedulerHelper
|
||||
{
|
||||
/**
|
||||
* Cached TaskOptions object
|
||||
*
|
||||
* @var TaskOptions
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected static $taskOptionsCache;
|
||||
|
||||
/**
|
||||
* Returns available task routines as a TaskOptions object.
|
||||
*
|
||||
* @return TaskOptions A TaskOptions object populated with task routines offered by plugins
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getTaskOptions(): TaskOptions
|
||||
{
|
||||
if (self::$taskOptionsCache !== null) {
|
||||
return self::$taskOptionsCache;
|
||||
}
|
||||
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = Factory::getApplication();
|
||||
$options = new TaskOptions();
|
||||
$event = AbstractEvent::create(
|
||||
'onTaskOptionsList',
|
||||
[
|
||||
'subject' => $options,
|
||||
]
|
||||
);
|
||||
|
||||
PluginHelper::importPlugin('task');
|
||||
$app->getDispatcher()->dispatch('onTaskOptionsList', $event);
|
||||
|
||||
self::$taskOptionsCache = $options;
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
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\Component\Scheduler\Administrator\Task\TaskOption;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* The MVC Model for SelectView.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class SelectModel extends ListModel
|
||||
{
|
||||
/**
|
||||
* The Application object, due removal.
|
||||
*
|
||||
* @var AdministratorApplication
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* SelectModel constructor.
|
||||
*
|
||||
* @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request).
|
||||
* @param ?MVCFactoryInterface $factory The factory.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function __construct($config = [], ?MVCFactoryInterface $factory = null)
|
||||
{
|
||||
$this->app = Factory::getApplication();
|
||||
|
||||
parent::__construct($config, $factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TaskOption[] An array of TaskOption objects
|
||||
*
|
||||
* @throws \Exception
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function getItems(): array
|
||||
{
|
||||
return SchedulerHelper::getTaskOptions()->options;
|
||||
}
|
||||
}
|
||||
797
administrator/components/com_scheduler/src/Model/TaskModel.php
Normal file
797
administrator/components/com_scheduler/src/Model/TaskModel.php
Normal file
@ -0,0 +1,797 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Event\AbstractEvent;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\Form;
|
||||
use Joomla\CMS\Form\FormFactoryInterface;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\MVC\Model\AdminModel;
|
||||
use Joomla\CMS\Object\CMSObject;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\Component\Scheduler\Administrator\Helper\ExecRuleHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Table\TaskTable;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\TaskOption;
|
||||
use Joomla\Database\ParameterType;
|
||||
use Symfony\Component\OptionsResolver\Exception\AccessException;
|
||||
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
|
||||
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* MVC Model to interact with the Scheduler DB.
|
||||
* Implements methods to add, remove, edit tasks.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class TaskModel extends AdminModel
|
||||
{
|
||||
/**
|
||||
* Maps logical states to their values in the DB
|
||||
* ? Do we end up using this?
|
||||
*
|
||||
* @var array
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected const TASK_STATES = [
|
||||
'enabled' => 1,
|
||||
'disabled' => 0,
|
||||
'trashed' => -2,
|
||||
];
|
||||
|
||||
/**
|
||||
* The name of the database table with task records.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const TASK_TABLE = '#__scheduler_tasks';
|
||||
|
||||
/**
|
||||
* Prefix used with controller messages
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $text_prefix = 'COM_SCHEDULER';
|
||||
|
||||
/**
|
||||
* Type alias for content type
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public $typeAlias = 'com_scheduler.task';
|
||||
|
||||
/**
|
||||
* The Application object, for convenience
|
||||
*
|
||||
* @var AdministratorApplication $app
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* The event to trigger before unlocking the data.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $event_before_unlock = null;
|
||||
|
||||
/**
|
||||
* The event to trigger after unlocking the data.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $event_unlock = null;
|
||||
|
||||
/**
|
||||
* TaskModel constructor. Needed just to set $app
|
||||
*
|
||||
* @param array $config An array of configuration options
|
||||
* @param MVCFactoryInterface|null $factory The factory
|
||||
* @param FormFactoryInterface|null $formFactory The form factory
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null)
|
||||
{
|
||||
$config['events_map'] = $config['events_map'] ?? [];
|
||||
|
||||
$config['events_map'] = array_merge(
|
||||
[
|
||||
'save' => 'task',
|
||||
'validate' => 'task',
|
||||
'unlock' => 'task',
|
||||
],
|
||||
$config['events_map']
|
||||
);
|
||||
|
||||
if (isset($config['event_before_unlock'])) {
|
||||
$this->event_before_unlock = $config['event_before_unlock'];
|
||||
} elseif (empty($this->event_before_unlock)) {
|
||||
$this->event_before_unlock = 'onContentBeforeUnlock';
|
||||
}
|
||||
|
||||
if (isset($config['event_unlock'])) {
|
||||
$this->event_unlock = $config['event_unlock'];
|
||||
} elseif (empty($this->event_unlock)) {
|
||||
$this->event_unlock = 'onContentUnlock';
|
||||
}
|
||||
|
||||
$this->app = Factory::getApplication();
|
||||
|
||||
parent::__construct($config, $factory, $formFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the form object associated with this model. By default,
|
||||
* loads the corresponding data from the DB and binds it with the form.
|
||||
*
|
||||
* @param array $data Data that needs to go into the form
|
||||
* @param bool $loadData Should the form load its data from the DB?
|
||||
*
|
||||
* @return Form|boolean A JForm object on success, false on failure.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getForm($data = [], $loadData = true)
|
||||
{
|
||||
Form::addFieldPath(JPATH_ADMINISTRATOR . 'components/com_scheduler/src/Field');
|
||||
|
||||
/**
|
||||
* loadForm() (defined by FormBehaviourTrait) also loads the form data by calling
|
||||
* loadFormData() : $data [implemented here] and binds it to the form by calling
|
||||
* $form->bind($data).
|
||||
*/
|
||||
$form = $this->loadForm('com_scheduler.task', 'task', ['control' => 'jform', 'load_data' => $loadData]);
|
||||
|
||||
if (empty($form)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $this->app->getIdentity();
|
||||
|
||||
// If new entry, set task type from state
|
||||
if ($this->getState('task.id', 0) === 0 && $this->getState('task.type') !== null) {
|
||||
$form->setValue('type', null, $this->getState('task.type'));
|
||||
}
|
||||
|
||||
// @todo : Check if this is working as expected for new items (id == 0)
|
||||
if (!$user->authorise('core.edit.state', 'com_scheduler.task.' . $this->getState('task.id'))) {
|
||||
// Disable fields
|
||||
$form->setFieldAttribute('state', 'disabled', 'true');
|
||||
|
||||
// No "hacking" ._.
|
||||
$form->setFieldAttribute('state', 'filter', 'unset');
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function canDelete($record): bool
|
||||
{
|
||||
// Record doesn't exist, can't delete
|
||||
if (empty($record->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->app->getIdentity()->authorise('core.delete', 'com_scheduler.task.' . $record->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the model state, we use these instead of toying with input or the global state
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function populateState(): void
|
||||
{
|
||||
$app = $this->app;
|
||||
|
||||
$taskId = $app->getInput()->getInt('id');
|
||||
$taskType = $app->getUserState('com_scheduler.add.task.task_type');
|
||||
|
||||
// @todo: Remove this. Get the option through a helper call.
|
||||
$taskOption = $app->getUserState('com_scheduler.add.task.task_option');
|
||||
|
||||
$this->setState('task.id', $taskId);
|
||||
$this->setState('task.type', $taskType);
|
||||
$this->setState('task.option', $taskOption);
|
||||
|
||||
// Load component params, though com_scheduler does not (yet) have any params
|
||||
$cParams = ComponentHelper::getParams($this->option);
|
||||
$this->setState('params', $cParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't need to define this method since the parent getTable()
|
||||
* implicitly deduces $name and $prefix anyways. This makes the object
|
||||
* more transparent though.
|
||||
*
|
||||
* @param string $name Name of the table
|
||||
* @param string $prefix Class prefix
|
||||
* @param array $options Model config array
|
||||
*
|
||||
* @return Table
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getTable($name = 'Task', $prefix = 'Table', $options = []): Table
|
||||
{
|
||||
return parent::getTable($name, $prefix, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the data to be injected into the form
|
||||
*
|
||||
* @return object Associative array of form data.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function loadFormData()
|
||||
{
|
||||
$data = $this->app->getUserState('com_scheduler.edit.task.data', []);
|
||||
|
||||
// If the data from UserState is empty, we fetch it with getItem()
|
||||
if (empty($data)) {
|
||||
/** @var CMSObject $data */
|
||||
$data = $this->getItem();
|
||||
|
||||
// @todo : further data processing goes here
|
||||
|
||||
// For a fresh object, set exec-day and exec-time
|
||||
if (!($data->id ?? 0)) {
|
||||
$data->execution_rules['exec-day'] = gmdate('d');
|
||||
$data->execution_rules['exec-time'] = gmdate('H:i');
|
||||
}
|
||||
}
|
||||
|
||||
// Let plugins manipulate the data
|
||||
$this->preprocessData('com_scheduler.task', $data, 'task');
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloads the parent getItem() method.
|
||||
*
|
||||
* @param integer $pk Primary key
|
||||
*
|
||||
* @return object|boolean Object on success, false on failure
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getItem($pk = null)
|
||||
{
|
||||
$item = parent::getItem($pk);
|
||||
|
||||
if (!\is_object($item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parent call leaves `execution_rules` and `cron_rules` JSON encoded
|
||||
$item->set('execution_rules', json_decode($item->get('execution_rules', '')));
|
||||
$item->set('cron_rules', json_decode($item->get('cron_rules', '')));
|
||||
|
||||
$taskOption = SchedulerHelper::getTaskOptions()->findOption(
|
||||
($item->id ?? 0) ? ($item->type ?? 0) : $this->getState('task.type')
|
||||
);
|
||||
|
||||
$item->set('taskOption', $taskOption);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a task from the database, only if an exclusive "lock" on the task can be acquired.
|
||||
* The method supports options to customise the limitations on the fetch.
|
||||
*
|
||||
* @param array $options Array with options to fetch the task:
|
||||
* 1. `id`: Optional id of the task to fetch.
|
||||
* 2. `allowDisabled`: If true, disabled tasks can also be fetched.
|
||||
* (default: false)
|
||||
* 3. `bypassScheduling`: If true, tasks that are not due can also be
|
||||
* fetched. Should only be true if an `id` is targeted instead of the
|
||||
* task queue. (default: false)
|
||||
* 4. `allowConcurrent`: If true, fetches even when another task is
|
||||
* running ('locked'). (default: false)
|
||||
* 5. `includeCliExclusive`: If true, can also fetch CLI exclusive tasks. (default: true)
|
||||
*
|
||||
* @return ?\stdClass Task entry as in the database.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws UndefinedOptionsException|InvalidOptionsException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getTask(array $options = []): ?\stdClass
|
||||
{
|
||||
$resolver = new OptionsResolver();
|
||||
|
||||
try {
|
||||
$this->configureTaskGetterOptions($resolver);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$options = $resolver->resolve($options);
|
||||
} catch (\Exception $e) {
|
||||
if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
$db = $this->getDatabase();
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
// Get lock on the table to help with concurrency issues
|
||||
$db->lockTable(self::TASK_TABLE);
|
||||
|
||||
// If concurrency is not allowed, we only get a task if another one does not have a "lock"
|
||||
if (!$options['allowConcurrent']) {
|
||||
// Get count of locked (presumed running) tasks
|
||||
$lockCountQuery = $db->getQuery(true)
|
||||
->from($db->quoteName(self::TASK_TABLE))
|
||||
->select('COUNT(id)')
|
||||
->where($db->quoteName('locked') . ' IS NOT NULL');
|
||||
|
||||
try {
|
||||
$runningCount = $db->setQuery($lockCountQuery)->loadResult();
|
||||
} catch (\RuntimeException $e) {
|
||||
$db->unlockTables();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($runningCount !== 0) {
|
||||
$db->unlockTables();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$lockQuery = $db->getQuery(true);
|
||||
|
||||
$lockQuery->update($db->quoteName(self::TASK_TABLE))
|
||||
->set($db->quoteName('locked') . ' = :now1')
|
||||
->bind(':now1', $now);
|
||||
|
||||
// Array of all active routine ids
|
||||
$activeRoutines = array_map(
|
||||
static function (TaskOption $taskOption): string {
|
||||
return $taskOption->id;
|
||||
},
|
||||
SchedulerHelper::getTaskOptions()->options
|
||||
);
|
||||
|
||||
// "Orphaned" tasks are not a part of the task queue!
|
||||
$lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING);
|
||||
|
||||
// If directed, exclude CLI exclusive tasks
|
||||
if (!$options['includeCliExclusive']) {
|
||||
$lockQuery->where($db->quoteName('cli_exclusive') . ' = 0');
|
||||
}
|
||||
|
||||
if (!$options['bypassScheduling']) {
|
||||
$lockQuery->where($db->quoteName('next_execution') . ' <= :now2')
|
||||
->bind(':now2', $now);
|
||||
}
|
||||
|
||||
if ($options['allowDisabled']) {
|
||||
$lockQuery->whereIn($db->quoteName('state'), [0, 1]);
|
||||
} else {
|
||||
$lockQuery->where($db->quoteName('state') . ' = 1');
|
||||
}
|
||||
|
||||
if ($options['id'] > 0) {
|
||||
$lockQuery->where($db->quoteName('id') . ' = :taskId')
|
||||
->bind(':taskId', $options['id'], ParameterType::INTEGER);
|
||||
} else {
|
||||
// Pick from the front of the task queue if no 'id' is specified
|
||||
// Get the id of the next task in the task queue
|
||||
$idQuery = $db->getQuery(true)
|
||||
->from($db->quoteName(self::TASK_TABLE))
|
||||
->select($db->quoteName('id'))
|
||||
->where($db->quoteName('state') . ' = 1')
|
||||
->order($db->quoteName('priority') . ' DESC')
|
||||
->order($db->quoteName('next_execution') . ' ASC')
|
||||
->setLimit(1);
|
||||
|
||||
try {
|
||||
$ids = $db->setQuery($idQuery)->loadColumn();
|
||||
} catch (\RuntimeException $e) {
|
||||
$db->unlockTables();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\count($ids) === 0) {
|
||||
$db->unlockTables();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$lockQuery->whereIn($db->quoteName('id'), $ids);
|
||||
}
|
||||
|
||||
try {
|
||||
$db->setQuery($lockQuery)->execute();
|
||||
} catch (\RuntimeException $e) {
|
||||
} finally {
|
||||
$affectedRows = $db->getAffectedRows();
|
||||
|
||||
$db->unlockTables();
|
||||
}
|
||||
|
||||
if ($affectedRows != 1) {
|
||||
/*
|
||||
// @todo
|
||||
// ? Fatal failure handling here?
|
||||
// ! Question is, how? If we check for tasks running beyond there time here, we have no way of
|
||||
// ! what's already been notified (since we're not auto-unlocking/recovering tasks anymore).
|
||||
// The solution __may__ be in a "last_successful_finish" (or something) column.
|
||||
*/
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$getQuery = $db->getQuery(true);
|
||||
|
||||
$getQuery->select('*')
|
||||
->from($db->quoteName(self::TASK_TABLE))
|
||||
->where($db->quoteName('locked') . ' = :now')
|
||||
->bind(':now', $now);
|
||||
|
||||
$task = $db->setQuery($getQuery)->loadObject();
|
||||
|
||||
$task->execution_rules = json_decode($task->execution_rules);
|
||||
$task->cron_rules = json_decode($task->cron_rules);
|
||||
|
||||
$task->taskOption = SchedulerHelper::getTaskOptions()->findOption($task->type);
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up an {@see OptionsResolver} to resolve options compatible with the {@see GetTask()} method.
|
||||
*
|
||||
* @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up.
|
||||
*
|
||||
* @return OptionsResolver
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws AccessException
|
||||
*/
|
||||
public static function configureTaskGetterOptions(OptionsResolver $resolver): OptionsResolver
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'id' => 0,
|
||||
'allowDisabled' => false,
|
||||
'bypassScheduling' => false,
|
||||
'allowConcurrent' => false,
|
||||
'includeCliExclusive' => true,
|
||||
]
|
||||
)
|
||||
->setAllowedTypes('id', 'numeric')
|
||||
->setAllowedTypes('allowDisabled', 'bool')
|
||||
->setAllowedTypes('bypassScheduling', 'bool')
|
||||
->setAllowedTypes('allowConcurrent', 'bool')
|
||||
->setAllowedTypes('includeCliExclusive', 'bool');
|
||||
|
||||
return $resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data The form data
|
||||
*
|
||||
* @return boolean True on success, false on failure
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function save($data): bool
|
||||
{
|
||||
$id = (int) ($data['id'] ?? $this->getState('task.id'));
|
||||
$isNew = $id === 0;
|
||||
|
||||
// Clean up execution rules
|
||||
$data['execution_rules'] = $this->processExecutionRules($data['execution_rules']);
|
||||
|
||||
// If a new entry, we'll have to put in place a pseudo-last_execution
|
||||
if ($isNew) {
|
||||
$basisDayOfMonth = $data['execution_rules']['exec-day'];
|
||||
[$basisHour, $basisMinute] = explode(':', $data['execution_rules']['exec-time']);
|
||||
|
||||
$data['last_execution'] = Factory::getDate('now', 'GMT')->format('Y-m')
|
||||
. "-$basisDayOfMonth $basisHour:$basisMinute:00";
|
||||
} else {
|
||||
$data['last_execution'] = $this->getItem($id)->last_execution;
|
||||
}
|
||||
|
||||
// Build the `cron_rules` column from `execution_rules`
|
||||
$data['cron_rules'] = $this->buildExecutionRules($data['execution_rules']);
|
||||
|
||||
// `next_execution` would be null if scheduling is disabled with the "manual" rule!
|
||||
$data['next_execution'] = (new ExecRuleHelper($data))->nextExec();
|
||||
|
||||
if ($isNew) {
|
||||
$data['last_execution'] = null;
|
||||
}
|
||||
|
||||
// If no params, we set as empty array.
|
||||
// ? Is this the right place to do this
|
||||
$data['params'] = $data['params'] ?? [];
|
||||
|
||||
// Parent method takes care of saving to the table
|
||||
return parent::save($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up and standardise execution rules
|
||||
*
|
||||
* @param array $unprocessedRules The form data [? can just replace with execution_interval]
|
||||
*
|
||||
* @return array Processed rules
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private function processExecutionRules(array $unprocessedRules): array
|
||||
{
|
||||
$executionRules = $unprocessedRules;
|
||||
|
||||
$ruleType = $executionRules['rule-type'];
|
||||
$retainKeys = ['rule-type', $ruleType, 'exec-day', 'exec-time'];
|
||||
$executionRules = array_intersect_key($executionRules, array_flip($retainKeys));
|
||||
|
||||
// Default to current date-time in UTC/GMT as the basis
|
||||
$executionRules['exec-day'] = $executionRules['exec-day'] ?: (string) gmdate('d');
|
||||
$executionRules['exec-time'] = $executionRules['exec-time'] ?: (string) gmdate('H:i');
|
||||
|
||||
// If custom ruleset, sort it
|
||||
// ? Is this necessary
|
||||
if ($ruleType === 'cron-expression') {
|
||||
foreach ($executionRules['cron-expression'] as &$values) {
|
||||
sort($values);
|
||||
}
|
||||
}
|
||||
|
||||
return $executionRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to build execution expression from input execution rules.
|
||||
* This expression is used internally to determine execution times/conditions.
|
||||
*
|
||||
* @param array $executionRules Execution rules from the Task form, post-processing.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function buildExecutionRules(array $executionRules): array
|
||||
{
|
||||
// Maps interval strings, use with sprintf($map[intType], $interval)
|
||||
$intervalStringMap = [
|
||||
'minutes' => 'PT%dM',
|
||||
'hours' => 'PT%dH',
|
||||
'days' => 'P%dD',
|
||||
'months' => 'P%dM',
|
||||
'years' => 'P%dY',
|
||||
];
|
||||
|
||||
$ruleType = $executionRules['rule-type'];
|
||||
$ruleClass = strpos($ruleType, 'interval') === 0 ? 'interval' : $ruleType;
|
||||
$buildExpression = '';
|
||||
|
||||
if ($ruleClass === 'interval') {
|
||||
// Rule type for intervals interval-<minute/hours/...>
|
||||
$intervalType = explode('-', $ruleType)[1];
|
||||
$interval = $executionRules["interval-$intervalType"];
|
||||
$buildExpression = sprintf($intervalStringMap[$intervalType], $interval);
|
||||
}
|
||||
|
||||
if ($ruleClass === 'cron-expression') {
|
||||
// ! custom matches are disabled in the form
|
||||
$matches = $executionRules['cron-expression'];
|
||||
$buildExpression .= $this->wildcardIfMatch($matches['minutes'], range(0, 59), true);
|
||||
$buildExpression .= ' ' . $this->wildcardIfMatch($matches['hours'], range(0, 23), true);
|
||||
$buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_month'], range(1, 31), true);
|
||||
$buildExpression .= ' ' . $this->wildcardIfMatch($matches['months'], range(1, 12), true);
|
||||
$buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_week'], range(0, 6), true);
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $ruleClass,
|
||||
'exp' => $buildExpression,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method releases "locks" on a set of tasks from the database.
|
||||
* These locks are pseudo-locks that are used to keep a track of running tasks. However, they require require manual
|
||||
* intervention to release these locks in cases such as when a task process crashes, leaving the task "locked".
|
||||
*
|
||||
* @param array $pks A list of the primary keys to unlock.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \RuntimeException|\UnexpectedValueException|\BadMethodCallException
|
||||
*/
|
||||
public function unlock(array &$pks): bool
|
||||
{
|
||||
/** @var TaskTable $table */
|
||||
$table = $this->getTable();
|
||||
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
$context = $this->option . '.' . $this->name;
|
||||
|
||||
// Include the plugins for the change of state event.
|
||||
PluginHelper::importPlugin($this->events_map['unlock']);
|
||||
|
||||
// Access checks.
|
||||
foreach ($pks as $i => $pk) {
|
||||
$table->reset();
|
||||
|
||||
if ($table->load($pk)) {
|
||||
if (!$this->canEditState($table)) {
|
||||
// Prune items that you can't change.
|
||||
unset($pks[$i]);
|
||||
Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prune items that are already at the given state.
|
||||
$lockedColumnName = $table->getColumnAlias('locked');
|
||||
|
||||
if (property_exists($table, $lockedColumnName) && \is_null($table->get($lockedColumnName))) {
|
||||
unset($pks[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are items to change.
|
||||
if (!\count($pks)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$event = AbstractEvent::create(
|
||||
$this->event_before_unlock,
|
||||
[
|
||||
'subject' => $this,
|
||||
'context' => $context,
|
||||
'pks' => $pks,
|
||||
]
|
||||
);
|
||||
|
||||
try {
|
||||
Factory::getApplication()->getDispatcher()->dispatch($this->event_before_unlock, $event);
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attempt to unlock the records.
|
||||
if (!$table->unlock($pks, $user->id)) {
|
||||
$this->setError($table->getError());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trigger the after unlock event
|
||||
$event = AbstractEvent::create(
|
||||
$this->event_unlock,
|
||||
[
|
||||
'subject' => $this,
|
||||
'context' => $context,
|
||||
'pks' => $pks,
|
||||
]
|
||||
);
|
||||
|
||||
try {
|
||||
Factory::getApplication()->getDispatcher()->dispatch($this->event_unlock, $event);
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear the component's cache
|
||||
$this->cleanCache();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an array is populated by all its possible values by comparison to a reference array, if found a
|
||||
* match a wildcard '*' is returned.
|
||||
*
|
||||
* @param array $target The target array
|
||||
* @param array $reference The reference array, populated by the complete set of possible values in $target
|
||||
* @param bool $targetToInt If true, converts $target array values to integers before comparing
|
||||
*
|
||||
* @return string A wildcard string if $target is fully populated, else $target itself.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private function wildcardIfMatch(array $target, array $reference, bool $targetToInt = false): string
|
||||
{
|
||||
if ($targetToInt) {
|
||||
$target = array_map(
|
||||
static function (string $x): int {
|
||||
return (int) $x;
|
||||
},
|
||||
$target
|
||||
);
|
||||
}
|
||||
|
||||
$isMatch = array_diff($reference, $target) === [];
|
||||
|
||||
return $isMatch ? "*" : implode(',', $target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to allow derived classes to preprocess the form.
|
||||
*
|
||||
* @param Form $form A Form object.
|
||||
* @param mixed $data The data expected for the form.
|
||||
* @param string $group The name of the plugin group to import (defaults to "content").
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception if there is an error in the form event.
|
||||
*/
|
||||
protected function preprocessForm(Form $form, $data, $group = 'content'): void
|
||||
{
|
||||
// Load the 'task' plugin group
|
||||
PluginHelper::importPlugin('task');
|
||||
|
||||
// Let the parent method take over
|
||||
parent::preprocessForm($form, $data, $group);
|
||||
}
|
||||
}
|
||||
471
administrator/components/com_scheduler/src/Model/TasksModel.php
Normal file
471
administrator/components/com_scheduler/src/Model/TasksModel.php
Normal file
@ -0,0 +1,471 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Date\Date;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\MVC\Model\ListModel;
|
||||
use Joomla\CMS\Object\CMSObject;
|
||||
use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\TaskOption;
|
||||
use Joomla\Database\DatabaseQuery;
|
||||
use Joomla\Database\ParameterType;
|
||||
use Joomla\Database\QueryInterface;
|
||||
use Joomla\Utilities\ArrayHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* The MVC Model for TasksView.
|
||||
* Defines methods to deal with operations concerning multiple `#__scheduler_tasks` entries.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class TasksModel extends ListModel
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
* @param MVCFactoryInterface|null $factory The factory.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
* @see \JControllerLegacy
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null)
|
||||
{
|
||||
if (empty($config['filter_fields'])) {
|
||||
$config['filter_fields'] = [
|
||||
'id', 'a.id',
|
||||
'asset_id', 'a.asset_id',
|
||||
'title', 'a.title',
|
||||
'type', 'a.type',
|
||||
'type_title', 'j.type_title',
|
||||
'state', 'a.state',
|
||||
'last_exit_code', 'a.last_exit_code',
|
||||
'last_execution', 'a.last_execution',
|
||||
'next_execution', 'a.next_execution',
|
||||
'times_executed', 'a.times_executed',
|
||||
'times_failed', 'a.times_failed',
|
||||
'ordering', 'a.ordering',
|
||||
'priority', 'a.priority',
|
||||
'note', 'a.note',
|
||||
'created', 'a.created',
|
||||
'created_by', 'a.created_by',
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($config, $factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to 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 4.1.0
|
||||
*/
|
||||
protected function getStoreId($id = ''): string
|
||||
{
|
||||
// Compile the store id.
|
||||
$id .= ':' . $this->getState('filter.search');
|
||||
$id .= ':' . $this->getState('filter.state');
|
||||
$id .= ':' . $this->getState('filter.type');
|
||||
$id .= ':' . $this->getState('filter.orphaned');
|
||||
$id .= ':' . $this->getState('filter.due');
|
||||
$id .= ':' . $this->getState('filter.locked');
|
||||
$id .= ':' . $this->getState('filter.trigger');
|
||||
$id .= ':' . $this->getState('list.select');
|
||||
|
||||
return parent::getStoreId($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create a query for a list of items.
|
||||
*
|
||||
* @return DatabaseQuery
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getListQuery(): QueryInterface
|
||||
{
|
||||
// Create a new query object.
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
/**
|
||||
* Select the required fields from the table.
|
||||
* ? Do we need all these defaults ?
|
||||
* ? Does 'list.select' exist ?
|
||||
*/
|
||||
$query->select(
|
||||
$this->getState(
|
||||
'list.select',
|
||||
[
|
||||
$db->quoteName('a.id'),
|
||||
$db->quoteName('a.asset_id'),
|
||||
$db->quoteName('a.title'),
|
||||
$db->quoteName('a.type'),
|
||||
$db->quoteName('a.execution_rules'),
|
||||
$db->quoteName('a.state'),
|
||||
$db->quoteName('a.last_exit_code'),
|
||||
$db->quoteName('a.locked'),
|
||||
$db->quoteName('a.last_execution'),
|
||||
$db->quoteName('a.next_execution'),
|
||||
$db->quoteName('a.times_executed'),
|
||||
$db->quoteName('a.times_failed'),
|
||||
$db->quoteName('a.priority'),
|
||||
$db->quoteName('a.ordering'),
|
||||
$db->quoteName('a.note'),
|
||||
$db->quoteName('a.checked_out'),
|
||||
$db->quoteName('a.checked_out_time'),
|
||||
]
|
||||
)
|
||||
)
|
||||
->select(
|
||||
[
|
||||
$db->quoteName('uc.name', 'editor'),
|
||||
]
|
||||
)
|
||||
->from($db->quoteName('#__scheduler_tasks', 'a'))
|
||||
->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out'));
|
||||
|
||||
// Filters go below
|
||||
$filterCount = 0;
|
||||
|
||||
/**
|
||||
* Extends query if already filtered.
|
||||
*
|
||||
* @param string $outerGlue
|
||||
* @param array $conditions
|
||||
* @param string $innerGlue
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
$extendWhereIfFiltered = static function (
|
||||
string $outerGlue,
|
||||
array $conditions,
|
||||
string $innerGlue
|
||||
) use (
|
||||
$query,
|
||||
&$filterCount
|
||||
) {
|
||||
if ($filterCount++) {
|
||||
$query->extendWhere($outerGlue, $conditions, $innerGlue);
|
||||
} else {
|
||||
$query->where($conditions, $innerGlue);
|
||||
}
|
||||
};
|
||||
|
||||
// Filter over ID, title (redundant to search, but) ---
|
||||
if (is_numeric($id = $this->getState('filter.id'))) {
|
||||
$filterCount++;
|
||||
$id = (int) $id;
|
||||
$query->where($db->quoteName('a.id') . ' = :id')
|
||||
->bind(':id', $id, ParameterType::INTEGER);
|
||||
} elseif ($title = $this->getState('filter.title')) {
|
||||
$filterCount++;
|
||||
$match = "%$title%";
|
||||
$query->where($db->quoteName('a.title') . ' LIKE :match')
|
||||
->bind(':match', $match);
|
||||
}
|
||||
|
||||
// Filter orphaned (-1: exclude, 0: include, 1: only) ----
|
||||
$filterOrphaned = (int) $this->getState('filter.orphaned');
|
||||
|
||||
if ($filterOrphaned !== 0) {
|
||||
$filterCount++;
|
||||
$taskOptions = SchedulerHelper::getTaskOptions();
|
||||
|
||||
// Array of all active routine ids
|
||||
$activeRoutines = array_map(
|
||||
static function (TaskOption $taskOption): string {
|
||||
return $taskOption->id;
|
||||
},
|
||||
$taskOptions->options
|
||||
);
|
||||
|
||||
if ($filterOrphaned === -1) {
|
||||
$query->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING);
|
||||
} else {
|
||||
$query->whereNotIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter over state ----
|
||||
$state = $this->getState('filter.state');
|
||||
|
||||
if ($state !== '*') {
|
||||
$filterCount++;
|
||||
|
||||
if (is_numeric($state)) {
|
||||
$state = (int) $state;
|
||||
|
||||
$query->where($db->quoteName('a.state') . ' = :state')
|
||||
->bind(':state', $state, ParameterType::INTEGER);
|
||||
} else {
|
||||
$query->whereIn($db->quoteName('a.state'), [0, 1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter over type ----
|
||||
$typeFilter = $this->getState('filter.type');
|
||||
|
||||
if ($typeFilter) {
|
||||
$filterCount++;
|
||||
$query->where($db->quotename('a.type') . '= :type')
|
||||
->bind(':type', $typeFilter);
|
||||
}
|
||||
|
||||
// Filter over exit code ----
|
||||
$exitCode = $this->getState('filter.last_exit_code');
|
||||
|
||||
if (is_numeric($exitCode)) {
|
||||
$filterCount++;
|
||||
$exitCode = (int) $exitCode;
|
||||
$query->where($db->quoteName('a.last_exit_code') . '= :last_exit_code')
|
||||
->bind(':last_exit_code', $exitCode, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
// Filter due (-1: exclude, 0: include, 1: only) ----
|
||||
$due = $this->getState('filter.due');
|
||||
|
||||
if (is_numeric($due) && $due != 0) {
|
||||
$now = Factory::getDate('now', 'GMT')->toSql();
|
||||
$operator = $due == 1 ? ' <= ' : ' > ';
|
||||
$filterCount++;
|
||||
$query->where($db->quoteName('a.next_execution') . $operator . ':now')
|
||||
->bind(':now', $now);
|
||||
}
|
||||
|
||||
/*
|
||||
* Filter locked ---
|
||||
* Locks can be either hard locks or soft locks. Locks that have expired (exceeded the task timeout) are soft
|
||||
* locks. Hard-locked tasks are assumed to be running. Soft-locked tasks are assumed to have suffered a fatal
|
||||
* failure.
|
||||
* {-2: exclude-all, -1: exclude-hard-locked, 0: include, 1: include-only-locked, 2: include-only-soft-locked}
|
||||
*/
|
||||
$locked = $this->getState('filter.locked');
|
||||
|
||||
if (is_numeric($locked) && $locked != 0) {
|
||||
$now = Factory::getDate('now', 'GMT');
|
||||
$timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300);
|
||||
$timeout = new \DateInterval(sprintf('PT%dS', $timeout));
|
||||
$timeoutThreshold = (clone $now)->sub($timeout)->toSql();
|
||||
$now = $now->toSql();
|
||||
|
||||
switch ($locked) {
|
||||
case -2:
|
||||
$query->where($db->quoteName('a.locked') . 'IS NULL');
|
||||
break;
|
||||
case -1:
|
||||
$extendWhereIfFiltered(
|
||||
'AND',
|
||||
[
|
||||
$db->quoteName('a.locked') . ' IS NULL',
|
||||
$db->quoteName('a.locked') . ' < :threshold',
|
||||
],
|
||||
'OR'
|
||||
);
|
||||
$query->bind(':threshold', $timeoutThreshold);
|
||||
break;
|
||||
case 1:
|
||||
$query->where($db->quoteName('a.locked') . ' IS NOT NULL');
|
||||
break;
|
||||
case 2:
|
||||
$query->where($db->quoteName('a.locked') . ' < :threshold')
|
||||
->bind(':threshold', $timeoutThreshold);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter over search string if set (title, type title, note, id) ----
|
||||
$searchStr = $this->getState('filter.search');
|
||||
|
||||
if (!empty($searchStr)) {
|
||||
// Allow search by ID
|
||||
if (stripos($searchStr, 'id:') === 0) {
|
||||
// Add array support [?]
|
||||
$id = (int) substr($searchStr, 3);
|
||||
$query->where($db->quoteName('a.id') . '= :id')
|
||||
->bind(':id', $id, ParameterType::INTEGER);
|
||||
} elseif (stripos($searchStr, 'type:') !== 0) {
|
||||
// Search by type is handled exceptionally in _getList() [@todo: remove refs]
|
||||
$searchStr = "%$searchStr%";
|
||||
|
||||
// Bind keys to query
|
||||
$query->bind(':title', $searchStr)
|
||||
->bind(':note', $searchStr);
|
||||
$conditions = [
|
||||
$db->quoteName('a.title') . ' LIKE :title',
|
||||
$db->quoteName('a.note') . ' LIKE :note',
|
||||
];
|
||||
$extendWhereIfFiltered('AND', $conditions, 'OR');
|
||||
}
|
||||
}
|
||||
|
||||
// Add list ordering clause. ----
|
||||
// @todo implement multi-column ordering someway
|
||||
$multiOrdering = $this->state->get('list.multi_ordering');
|
||||
|
||||
if (!$multiOrdering || !\is_array($multiOrdering)) {
|
||||
$orderCol = $this->state->get('list.ordering', 'a.title');
|
||||
$orderDir = $this->state->get('list.direction', 'asc');
|
||||
|
||||
// Type title ordering is handled exceptionally in _getList()
|
||||
if ($orderCol !== 'j.type_title') {
|
||||
$query->order($db->quoteName($orderCol) . ' ' . $orderDir);
|
||||
|
||||
// If ordering by type or state, also order by title.
|
||||
if (\in_array($orderCol, ['a.type', 'a.state', 'a.priority'])) {
|
||||
// @todo : Test if things are working as expected
|
||||
$query->order($db->quoteName('a.title') . ' ' . $orderDir);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// @todo Should add quoting here
|
||||
$query->order($multiOrdering);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloads the parent _getList() method.
|
||||
* Takes care of attaching TaskOption objects and sorting by type titles.
|
||||
*
|
||||
* @param DatabaseQuery $query The database query to get the list with
|
||||
* @param int $limitstart The list offset
|
||||
* @param int $limit Number of list items to fetch
|
||||
*
|
||||
* @return object[]
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function _getList($query, $limitstart = 0, $limit = 0): array
|
||||
{
|
||||
// Get stuff from the model state
|
||||
$listOrder = $this->getState('list.ordering', 'a.title');
|
||||
$listDirectionN = strtolower($this->getState('list.direction', 'asc')) === 'desc' ? -1 : 1;
|
||||
|
||||
// Set limit parameters and get object list
|
||||
$query->setLimit($limit, $limitstart);
|
||||
$this->getDatabase()->setQuery($query);
|
||||
|
||||
// Return optionally an extended class.
|
||||
// @todo: Use something other than CMSObject..
|
||||
if ($this->getState('list.customClass')) {
|
||||
$responseList = array_map(
|
||||
static function (array $arr) {
|
||||
$o = new CMSObject();
|
||||
|
||||
foreach ($arr as $k => $v) {
|
||||
$o->{$k} = $v;
|
||||
}
|
||||
|
||||
return $o;
|
||||
},
|
||||
$this->getDatabase()->loadAssocList() ?: []
|
||||
);
|
||||
} else {
|
||||
$responseList = $this->getDatabase()->loadObjectList();
|
||||
}
|
||||
|
||||
// Attach TaskOptions objects and a safe type title
|
||||
$this->attachTaskOptions($responseList);
|
||||
|
||||
// If ordering by non-db fields, we need to sort here in code
|
||||
if ($listOrder === 'j.type_title') {
|
||||
$responseList = ArrayHelper::sortObjects($responseList, 'safeTypeTitle', $listDirectionN, true, false);
|
||||
}
|
||||
|
||||
return $responseList;
|
||||
}
|
||||
|
||||
/**
|
||||
* For an array of items, attaches TaskOption objects and (safe) type titles to each.
|
||||
*
|
||||
* @param array $items Array of items, passed by reference
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function attachTaskOptions(array $items): void
|
||||
{
|
||||
$taskOptions = SchedulerHelper::getTaskOptions();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$item->taskOption = $taskOptions->findOption($item->type);
|
||||
$item->safeTypeTitle = $item->taskOption->title ?? Text::_('JGLOBAL_NONAPPLICABLE');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy for the parent method.
|
||||
* Sets ordering defaults.
|
||||
*
|
||||
* @param string $ordering Field to order/sort list by
|
||||
* @param string $direction Direction in which to sort list
|
||||
*
|
||||
* @return void
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected function populateState($ordering = 'a.title', $direction = 'ASC'): void
|
||||
{
|
||||
// Call the parent method
|
||||
parent::populateState($ordering, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have any enabled due tasks and no locked tasks.
|
||||
*
|
||||
* @param Date $time The next execution time to check against
|
||||
*
|
||||
* @return boolean
|
||||
* @since 4.4.0
|
||||
*/
|
||||
public function hasDueTasks(Date $time): bool
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$now = $time->toSql();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
// Count due tasks
|
||||
->select('SUM(CASE WHEN ' . $db->quoteName('a.next_execution') . ' <= :now THEN 1 ELSE 0 END) AS due_count')
|
||||
// Count locked tasks
|
||||
->select('SUM(CASE WHEN ' . $db->quoteName('a.locked') . ' IS NULL THEN 0 ELSE 1 END) AS locked_count')
|
||||
->from($db->quoteName('#__scheduler_tasks', 'a'))
|
||||
->where($db->quoteName('a.state') . ' = 1')
|
||||
->bind(':now', $now);
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
$taskDetails = $db->loadObject();
|
||||
|
||||
// False if we don't have due tasks, or we have locked tasks
|
||||
return $taskDetails && $taskDetails->due_count && !$taskDetails->locked_count;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Rule;
|
||||
|
||||
use Joomla\CMS\Form\Form;
|
||||
use Joomla\CMS\Form\FormRule;
|
||||
use Joomla\CMS\Form\Rule\OptionsRule;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* The ExecutionRulesRule Class.
|
||||
* Validates execution rules, with input for other fields as context.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class ExecutionRulesRule extends FormRule
|
||||
{
|
||||
/**
|
||||
* @var string RULE_TYPE_FIELD The field containing the rule type to test against
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private const RULE_TYPE_FIELD = "execution_rules.rule-type";
|
||||
|
||||
/**
|
||||
* @var string CUSTOM_RULE_GROUP The field group containing custom execution rules
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private const CUSTOM_RULE_GROUP = "execution_rules.custom";
|
||||
|
||||
/**
|
||||
* @param \SimpleXMLElement $element The SimpleXMLElement object representing the `<field>` tag for the form
|
||||
* field object.
|
||||
* @param mixed $value The form field value to validate.
|
||||
* @param ?string $group The field name group control value. This acts as an array container for the
|
||||
* field. For example if the field has `name="foo"` and the group value is set
|
||||
* to "bar" then the full field name would end up being "bar[foo]".
|
||||
* @param ?Registry $input An optional Registry object with the entire data set to validate against
|
||||
* the entire form.
|
||||
* @param ?Form $form The form object for which the field is being tested.
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): bool
|
||||
{
|
||||
$fieldName = (string) $element['name'];
|
||||
$ruleType = $input->get(self::RULE_TYPE_FIELD);
|
||||
|
||||
if ($ruleType === $fieldName || ($ruleType === 'custom' && $group === self::CUSTOM_RULE_GROUP)) {
|
||||
return $this->validateField($element, $value, $group, $form);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SimpleXMLElement $element The SimpleXMLElement for the field.
|
||||
* @param mixed $value The field value.
|
||||
* @param ?string $group The form field group the element belongs to.
|
||||
* @param Form|null $form The Form object against which the field is tested/
|
||||
*
|
||||
* @return boolean True if field is valid
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private function validateField(\SimpleXMLElement $element, $value, ?string $group = null, ?Form $form = null): bool
|
||||
{
|
||||
$elementType = (string) $element['type'];
|
||||
|
||||
// If element is of cron type, we test against options and return
|
||||
if ($elementType === 'cron') {
|
||||
return (new OptionsRule())->test($element, $value, $group, null, $form);
|
||||
}
|
||||
|
||||
// Test for a positive integer value and return
|
||||
return filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Scheduler;
|
||||
|
||||
use Joomla\CMS\Application\CMSApplication;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\Component\Scheduler\Administrator\Extension\SchedulerComponent;
|
||||
use Joomla\Component\Scheduler\Administrator\Model\TaskModel;
|
||||
use Joomla\Component\Scheduler\Administrator\Model\TasksModel;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\Status;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\Task;
|
||||
use Symfony\Component\OptionsResolver\Exception\AccessException;
|
||||
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
|
||||
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* The Scheduler class provides the core functionality of ComScheduler.
|
||||
* Currently, this includes fetching scheduled tasks from the database
|
||||
* and execution of any or the next due task.
|
||||
* It is planned that this class is extended with C[R]UD methods for
|
||||
* scheduled tasks.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @todo A global instance?
|
||||
*/
|
||||
class Scheduler
|
||||
{
|
||||
private const LOG_TEXT = [
|
||||
Status::OK => 'COM_SCHEDULER_SCHEDULER_TASK_COMPLETE',
|
||||
Status::WILL_RESUME => 'COM_SCHEDULER_SCHEDULER_TASK_WILL_RESUME',
|
||||
Status::NO_LOCK => 'COM_SCHEDULER_SCHEDULER_TASK_LOCKED',
|
||||
Status::NO_RUN => 'COM_SCHEDULER_SCHEDULER_TASK_UNLOCKED',
|
||||
Status::NO_ROUTINE => 'COM_SCHEDULER_SCHEDULER_TASK_ROUTINE_NA',
|
||||
];
|
||||
|
||||
/**
|
||||
* Filters for the task queue. Can be used with fetchTaskRecords().
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @todo remove?
|
||||
*/
|
||||
public const TASK_QUEUE_FILTERS = [
|
||||
'due' => 1,
|
||||
'locked' => -1,
|
||||
];
|
||||
|
||||
/**
|
||||
* List config for the task queue. Can be used with fetchTaskRecords().
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @todo remove?
|
||||
*/
|
||||
public const TASK_QUEUE_LIST_CONFIG = [
|
||||
'multi_ordering' => ['a.priority DESC ', 'a.next_execution ASC'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Run a scheduled task.
|
||||
* Runs a single due task from the task queue by default if $id and $title are not passed.
|
||||
*
|
||||
* @param array $options Array with options to configure the method's behavior. Supports:
|
||||
* 1. `id`: (Optional) ID of the task to run.
|
||||
* 2. `allowDisabled`: Allow running disabled tasks.
|
||||
* 3. `allowConcurrent`: Allow concurrent execution, i.e., running the task when another
|
||||
* task may be running.
|
||||
*
|
||||
* @return ?Task The task executed or null if not exists
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function runTask(array $options): ?Task
|
||||
{
|
||||
$resolver = new OptionsResolver();
|
||||
|
||||
try {
|
||||
$this->configureTaskRunnerOptions($resolver);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$options = $resolver->resolve($options);
|
||||
} catch (\Exception $e) {
|
||||
if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var CMSApplication $app */
|
||||
$app = Factory::getApplication();
|
||||
|
||||
// ? Sure about inferring scheduling bypass?
|
||||
$task = $this->getTask(
|
||||
[
|
||||
'id' => (int) $options['id'],
|
||||
'allowDisabled' => $options['allowDisabled'],
|
||||
'bypassScheduling' => (int) $options['id'] !== 0,
|
||||
'allowConcurrent' => $options['allowConcurrent'],
|
||||
'includeCliExclusive' => ($app->isClient('cli')),
|
||||
]
|
||||
);
|
||||
|
||||
// ? Should this be logged? (probably, if an ID is passed?)
|
||||
if (empty($task)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR);
|
||||
|
||||
$options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}';
|
||||
$options['text_file'] = 'joomla_scheduler.php';
|
||||
Log::addLogger($options, Log::ALL, $task->logCategory);
|
||||
|
||||
$taskId = $task->get('id');
|
||||
$taskTitle = $task->get('title');
|
||||
|
||||
$task->log(Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_START', $taskId, $taskTitle), 'info');
|
||||
|
||||
// Let's try to avoid time-outs
|
||||
if (\function_exists('set_time_limit')) {
|
||||
set_time_limit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
$task->run();
|
||||
} catch (\Exception $e) {
|
||||
// We suppress the exception here, it's still accessible with `$task->getContent()['exception']`.
|
||||
}
|
||||
|
||||
$executionSnapshot = $task->getContent();
|
||||
$exitCode = $executionSnapshot['status'] ?? Status::NO_EXIT;
|
||||
$netDuration = $executionSnapshot['netDuration'] ?? 0;
|
||||
$duration = $executionSnapshot['duration'] ?? 0;
|
||||
|
||||
if (\array_key_exists($exitCode, self::LOG_TEXT)) {
|
||||
$level = \in_array($exitCode, [Status::OK, Status::WILL_RESUME]) ? 'info' : 'warning';
|
||||
$task->log(Text::sprintf(self::LOG_TEXT[$exitCode], $taskId, $duration, $netDuration), $level);
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
$task->log(
|
||||
Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_UNKNOWN_EXIT', $taskId, $duration, $netDuration, $exitCode),
|
||||
'warning'
|
||||
);
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up an {@see OptionsResolver} to resolve options compatible with {@see runTask}.
|
||||
*
|
||||
* @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws AccessException
|
||||
*/
|
||||
protected function configureTaskRunnerOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'id' => 0,
|
||||
'allowDisabled' => false,
|
||||
'allowConcurrent' => false,
|
||||
]
|
||||
)
|
||||
->setAllowedTypes('id', 'numeric')
|
||||
->setAllowedTypes('allowDisabled', 'bool')
|
||||
->setAllowedTypes('allowConcurrent', 'bool');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next task which is due to run, limit to a specific task when ID is given
|
||||
*
|
||||
* @param array $options Options for the getter, see {@see TaskModel::getTask()}.
|
||||
* ! should probably also support a non-locking getter.
|
||||
*
|
||||
* @return Task $task The task to execute
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getTask(array $options = []): ?Task
|
||||
{
|
||||
$resolver = new OptionsResolver();
|
||||
|
||||
try {
|
||||
TaskModel::configureTaskGetterOptions($resolver);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$options = $resolver->resolve($options);
|
||||
} catch (\Exception $e) {
|
||||
if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var SchedulerComponent $component */
|
||||
$component = Factory::getApplication()->bootComponent('com_scheduler');
|
||||
|
||||
/** @var TaskModel $model */
|
||||
$model = $component->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
if (!isset($model)) {
|
||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'));
|
||||
}
|
||||
|
||||
$task = $model->getTask($options);
|
||||
|
||||
if (empty($task)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Task($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single scheduled task in a Task instance.
|
||||
* If no id or title is specified, a due task is returned.
|
||||
*
|
||||
* @param int $id The task ID.
|
||||
* @param bool $allowDisabled Allow disabled/trashed tasks?
|
||||
*
|
||||
* @return ?object A matching task record, if it exists
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function fetchTaskRecord(int $id = 0, bool $allowDisabled = false): ?object
|
||||
{
|
||||
$filters = [];
|
||||
$listConfig = ['limit' => 1];
|
||||
|
||||
if ($id > 0) {
|
||||
$filters['id'] = $id;
|
||||
} else {
|
||||
// Filters and list config for scheduled task queue
|
||||
$filters['due'] = 1;
|
||||
$filters['locked'] = -1;
|
||||
$listConfig['multi_ordering'] = [
|
||||
'a.priority DESC',
|
||||
'a.next_execution ASC',
|
||||
];
|
||||
}
|
||||
|
||||
if ($allowDisabled) {
|
||||
$filters['state'] = '';
|
||||
}
|
||||
|
||||
return $this->fetchTaskRecords($filters, $listConfig)[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $filters The filters to set to the model
|
||||
* @param array $listConfig The list config (ordering, etc.) to set to the model
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \RunTimeException
|
||||
*/
|
||||
public function fetchTaskRecords(array $filters, array $listConfig): array
|
||||
{
|
||||
$model = null;
|
||||
|
||||
try {
|
||||
/** @var SchedulerComponent $component */
|
||||
$component = Factory::getApplication()->bootComponent('com_scheduler');
|
||||
|
||||
/** @var TasksModel $model */
|
||||
$model = $component->getMVCFactory()
|
||||
->createModel('Tasks', 'Administrator', ['ignore_request' => true]);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
if (!$model) {
|
||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'));
|
||||
}
|
||||
|
||||
$model->setState('list.select', 'a.*');
|
||||
|
||||
// Default to only enabled tasks
|
||||
if (!isset($filters['state'])) {
|
||||
$model->setState('filter.state', 1);
|
||||
}
|
||||
|
||||
// Default to including orphaned tasks
|
||||
$model->setState('filter.orphaned', 0);
|
||||
|
||||
// Default to ordering by ID
|
||||
$model->setState('list.ordering', 'a.id');
|
||||
$model->setState('list.direction', 'ASC');
|
||||
|
||||
// List options
|
||||
foreach ($listConfig as $key => $value) {
|
||||
$model->setState('list.' . $key, $value);
|
||||
}
|
||||
|
||||
// Filter options
|
||||
foreach ($filters as $type => $filter) {
|
||||
$model->setState('filter.' . $type, $filter);
|
||||
}
|
||||
|
||||
return $model->getItems() ?: [];
|
||||
}
|
||||
}
|
||||
298
administrator/components/com_scheduler/src/Table/TaskTable.php
Normal file
298
administrator/components/com_scheduler/src/Table/TaskTable.php
Normal file
@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Table;
|
||||
|
||||
use Joomla\CMS\Event\AbstractEvent;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Model\AdminModel;
|
||||
use Joomla\CMS\Table\Asset;
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\CMS\User\CurrentUserInterface;
|
||||
use Joomla\CMS\User\CurrentUserTrait;
|
||||
use Joomla\Database\DatabaseDriver;
|
||||
use Joomla\Database\Exception\QueryTypeAlreadyDefinedException;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Table class for tasks scheduled through `com_scheduler`.
|
||||
* The type alias for Task table entries is `com_scheduler.task`.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class TaskTable extends Table implements CurrentUserInterface
|
||||
{
|
||||
use CurrentUserTrait;
|
||||
|
||||
/**
|
||||
* Indicates that columns fully support the NULL value in the database
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.1.1
|
||||
*/
|
||||
protected $_supportNullValue = true;
|
||||
|
||||
/**
|
||||
* Ensure params are json encoded by the bind method.
|
||||
*
|
||||
* @var string[]
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $_jsonEncode = ['params', 'execution_rules', 'cron_rules'];
|
||||
|
||||
/**
|
||||
* The 'created' column.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public $created;
|
||||
|
||||
/**
|
||||
* The 'title' column.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public $title;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public $typeAlias = 'com_scheduler.task';
|
||||
|
||||
/**
|
||||
* TaskTable constructor override, needed to pass the DB table name and primary key to {@see Table::__construct()}.
|
||||
*
|
||||
* @param DatabaseDriver $db Database connector object
|
||||
* @param ?DispatcherInterface $dispatcher Event dispatcher for this table
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function __construct(DatabaseDriver $db, DispatcherInterface $dispatcher = null)
|
||||
{
|
||||
$this->setColumnAlias('published', 'state');
|
||||
|
||||
parent::__construct('#__scheduler_tasks', 'id', $db, $dispatcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloads {@see Table::check()} to perform sanity checks on properties and make sure they're
|
||||
* safe to store.
|
||||
*
|
||||
* @return boolean True if checks pass.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function check(): bool
|
||||
{
|
||||
try {
|
||||
parent::check();
|
||||
} catch (\Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->title = htmlspecialchars_decode($this->title, ENT_QUOTES);
|
||||
|
||||
// Set created date if not set.
|
||||
// ? Might not need since the constructor already sets this
|
||||
if (!(int) $this->created) {
|
||||
$this->created = Factory::getDate()->toSql();
|
||||
}
|
||||
|
||||
// @todo : Add more checks if needed
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override {@see Table::store()} to update null fields as a default, which is needed when DATETIME
|
||||
* fields need to be updated to NULL. This override is needed because {@see AdminModel::save()} does not
|
||||
* expose an option to pass true to Table::store(). Also ensures the `created` and `created_by` fields are
|
||||
* set.
|
||||
*
|
||||
* @param boolean $updateNulls True to update fields even if they're null.
|
||||
*
|
||||
* @return boolean True if successful.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function store($updateNulls = true): bool
|
||||
{
|
||||
$isNew = empty($this->getId());
|
||||
|
||||
// Set creation date if not set for a new item.
|
||||
if ($isNew && empty($this->created)) {
|
||||
$this->created = Factory::getDate()->toSql();
|
||||
}
|
||||
|
||||
// Set `created_by` if not set for a new item.
|
||||
if ($isNew && empty($this->created_by)) {
|
||||
$this->created_by = $this->getCurrentUser()->id;
|
||||
}
|
||||
|
||||
// @todo : Should we add modified, modified_by fields? [ ]
|
||||
|
||||
return parent::store($updateNulls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the asset name of the entry as it appears in the {@see Asset} table.
|
||||
*
|
||||
* @return string The asset name.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected function _getAssetName(): string
|
||||
{
|
||||
$k = $this->_tbl_key;
|
||||
|
||||
return 'com_scheduler.task.' . (int) $this->$k;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override {@see Table::bind()} to bind some fields even if they're null given they're present in $src.
|
||||
* This override is needed specifically for DATETIME fields, of which the `next_execution` field is updated to
|
||||
* null if a task is configured to execute only on manual trigger.
|
||||
*
|
||||
* @param array|object $src An associative array or object to bind to the Table instance.
|
||||
* @param array|string $ignore An optional array or space separated list of properties to ignore while binding.
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function bind($src, $ignore = []): bool
|
||||
{
|
||||
$fields = ['next_execution'];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (\array_key_exists($field, $src) && \is_null($src[$field])) {
|
||||
$this->$field = $src[$field];
|
||||
}
|
||||
}
|
||||
|
||||
return parent::bind($src, $ignore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release pseudo-locks on a set of task records. If an empty set is passed, this method releases lock on its
|
||||
* instance primary key, if available.
|
||||
*
|
||||
* @param integer[] $pks An optional array of primary key values to update. If not set the instance property
|
||||
* value is used.
|
||||
* @param ?int $userId ID of the user unlocking the tasks.
|
||||
*
|
||||
* @return boolean True on success; false if $pks is empty.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws QueryTypeAlreadyDefinedException|\UnexpectedValueException|\BadMethodCallException
|
||||
*/
|
||||
public function unlock(array $pks = [], ?int $userId = null): bool
|
||||
{
|
||||
// Pre-processing by observers
|
||||
$event = AbstractEvent::create(
|
||||
'onTaskBeforeUnlock',
|
||||
[
|
||||
'subject' => $this,
|
||||
'pks' => $pks,
|
||||
'userId' => $userId,
|
||||
]
|
||||
);
|
||||
|
||||
$this->getDispatcher()->dispatch('onTaskBeforeUnlock', $event);
|
||||
|
||||
// Some pre-processing before we can work with the keys.
|
||||
if (!empty($pks)) {
|
||||
foreach ($pks as $key => $pk) {
|
||||
if (!\is_array($pk)) {
|
||||
$pks[$key] = [$this->_tbl_key => $pk];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no primary keys set check to see if the instance key is set and use that.
|
||||
if (empty($pks)) {
|
||||
$pk = [];
|
||||
|
||||
foreach ($this->_tbl_keys as $key) {
|
||||
if ($this->$key) {
|
||||
$pk[$key] = $this->$key;
|
||||
} else {
|
||||
// We don't have a full primary key - return false.
|
||||
$this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED'));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$pks = [$pk];
|
||||
}
|
||||
|
||||
$lockedField = $this->getColumnAlias('locked');
|
||||
|
||||
foreach ($pks as $pk) {
|
||||
// Update the publishing state for rows with the given primary keys.
|
||||
$query = $this->_db->getQuery(true)
|
||||
->update($this->_tbl)
|
||||
->set($this->_db->quoteName($lockedField) . ' = NULL');
|
||||
|
||||
// Build the WHERE clause for the primary keys.
|
||||
$this->appendPrimaryKeys($query, $pk);
|
||||
|
||||
$this->_db->setQuery($query);
|
||||
|
||||
try {
|
||||
$this->_db->execute();
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the Table instance value is in the list of primary keys that were set, set the instance.
|
||||
$ours = true;
|
||||
|
||||
foreach ($this->_tbl_keys as $key) {
|
||||
if ($this->$key != $pk[$key]) {
|
||||
$ours = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($ours) {
|
||||
$this->$lockedField = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-processing by observers
|
||||
$event = AbstractEvent::create(
|
||||
'onTaskAfterUnlock',
|
||||
[
|
||||
'subject' => $this,
|
||||
'pks' => $pks,
|
||||
'userId' => $userId,
|
||||
]
|
||||
);
|
||||
|
||||
$this->getDispatcher()->dispatch('onTaskAfterUnlock', $event);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
115
administrator/components/com_scheduler/src/Task/Status.php
Normal file
115
administrator/components/com_scheduler/src/Task/Status.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Task;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* A namespace mapping Task statuses to integer values.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
abstract class Status
|
||||
{
|
||||
/**
|
||||
* Replacement exit code used when a routine returns an invalid (non-integer) exit code.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const INVALID_EXIT = -2;
|
||||
|
||||
/**
|
||||
* Replacement exit code used when a routine does not return an exit code.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const NO_EXIT = -1;
|
||||
|
||||
/**
|
||||
* Status code used when the routine just starts. This is not meant to be an exit code.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const RUNNING = 1;
|
||||
|
||||
/**
|
||||
* Exit code used on failure to acquire a pseudo-lock.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const NO_LOCK = 2;
|
||||
|
||||
/**
|
||||
* Exit code used on failure to run the task.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const NO_RUN = 3;
|
||||
|
||||
/**
|
||||
* Exit code used on failure to release lock/update the record.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const NO_RELEASE = 4;
|
||||
|
||||
/**
|
||||
* Exit code used when a routine is either "knocked out" by an exception or encounters an exception it cannot handle
|
||||
* gracefully.
|
||||
* ? Should this be retained ?
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const KNOCKOUT = 5;
|
||||
|
||||
/**
|
||||
* Exit code used when a task needs to resume (reschedule it to run a.s.a.p.).
|
||||
*
|
||||
* Use this for long running tasks, e.g. batch processing of hundreds or thousands of files,
|
||||
* sending newsletters with thousands of subscribers etc. These are tasks which might run out of
|
||||
* memory and/or hit a time limit when lazy scheduling or web triggering of tasks is being used.
|
||||
* Split them into smaller batches which return Status::WILL_RESUME. When the last batch is
|
||||
* executed return Status::OK.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const WILL_RESUME = 123;
|
||||
|
||||
/**
|
||||
* Exit code used when a task times out.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const TIMEOUT = 124;
|
||||
|
||||
/**
|
||||
* Exit code when a *task* does not exist.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const NO_TASK = 125;
|
||||
|
||||
/**
|
||||
* Exit code used when a *routine* is missing.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const NO_ROUTINE = 127;
|
||||
|
||||
/**
|
||||
* Exit code on success.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const OK = 0;
|
||||
}
|
||||
562
administrator/components/com_scheduler/src/Task/Task.php
Normal file
562
administrator/components/com_scheduler/src/Task/Task.php
Normal file
@ -0,0 +1,562 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Task;
|
||||
|
||||
use Joomla\CMS\Application\CMSApplication;
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Event\AbstractEvent;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent;
|
||||
use Joomla\Component\Scheduler\Administrator\Helper\ExecRuleHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler;
|
||||
use Joomla\Component\Scheduler\Administrator\Table\TaskTable;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
use Joomla\Database\ParameterType;
|
||||
use Joomla\Registry\Registry;
|
||||
use Joomla\Utilities\ArrayHelper;
|
||||
use Psr\Log\InvalidArgumentException;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* The Task class defines methods for the execution, logging and
|
||||
* related properties of Tasks as supported by `com_scheduler`,
|
||||
* a Task Scheduling component.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class Task implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* Enumerated state for enabled tasks.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const STATE_ENABLED = 1;
|
||||
|
||||
/**
|
||||
* Enumerated state for disabled tasks.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const STATE_DISABLED = 0;
|
||||
|
||||
/**
|
||||
* Enumerated state for trashed tasks.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const STATE_TRASHED = -2;
|
||||
|
||||
/**
|
||||
* Map state enumerations to logical language adjectives.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public const STATE_MAP = [
|
||||
self::STATE_TRASHED => 'trashed',
|
||||
self::STATE_DISABLED => 'disabled',
|
||||
self::STATE_ENABLED => 'enabled',
|
||||
];
|
||||
|
||||
/**
|
||||
* The task snapshot
|
||||
*
|
||||
* @var array
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $snapshot = [];
|
||||
|
||||
/**
|
||||
* @var Registry
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $taskRegistry;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public $logCategory;
|
||||
|
||||
/**
|
||||
* @var CMSApplication
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @var DatabaseInterface
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Maps task exit codes to events which should be dispatched when the task finishes.
|
||||
* 'NA' maps to the event for general task failures.
|
||||
*
|
||||
* @var string[]
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected const EVENTS_MAP = [
|
||||
Status::OK => 'onTaskExecuteSuccess',
|
||||
Status::NO_ROUTINE => 'onTaskRoutineNotFound',
|
||||
Status::WILL_RESUME => 'onTaskRoutineWillResume',
|
||||
'NA' => 'onTaskExecuteFailure',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor for {@see Task}.
|
||||
*
|
||||
* @param object $record A task from {@see TaskTable}.
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(object $record)
|
||||
{
|
||||
// Workaround because Registry dumps private properties otherwise.
|
||||
$taskOption = $record->taskOption;
|
||||
$record->params = json_decode($record->params, true);
|
||||
|
||||
$this->taskRegistry = new Registry($record);
|
||||
|
||||
$this->set('taskOption', $taskOption);
|
||||
$this->app = Factory::getApplication();
|
||||
$this->db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$this->setLogger(Log::createDelegatedLogger());
|
||||
$this->logCategory = 'task' . $this->get('id');
|
||||
|
||||
if ($this->get('params.individual_log')) {
|
||||
$logFile = $this->get('params.log_file') ?? 'task_' . $this->get('id') . '.log.php';
|
||||
|
||||
$options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}';
|
||||
$options['text_file'] = $logFile;
|
||||
Log::addLogger($options, Log::ALL, [$this->logCategory]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the task as a data object that can be stored back in the database.
|
||||
* ! This method should be removed or changed as part of a better API implementation for the driver.
|
||||
*
|
||||
* @return object
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function getRecord(): object
|
||||
{
|
||||
// ! Probably, an array instead
|
||||
$recObject = $this->taskRegistry->toObject();
|
||||
|
||||
$recObject->cron_rules = (array) $recObject->cron_rules;
|
||||
|
||||
return $recObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the task.
|
||||
*
|
||||
* @return boolean True if success
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function run(): bool
|
||||
{
|
||||
/**
|
||||
* We try to acquire the lock here, only if we don't already have one.
|
||||
* We do this, so we can support two ways of running tasks:
|
||||
* 1. Directly through {@see Scheduler}, which optimises acquiring a lock while fetching from the task queue.
|
||||
* 2. Running a task without a pre-acquired lock.
|
||||
* ! This needs some more thought, for whether it should be allowed or if the single-query optimisation
|
||||
* should be used everywhere, although it doesn't make sense in the context of fetching
|
||||
* a task when it doesn't need to be run. This might be solved if we force a re-fetch
|
||||
* with the lock or do it here ourselves (using acquireLock as a proxy to the model's
|
||||
* getter).
|
||||
*/
|
||||
if ($this->get('locked') === null) {
|
||||
$this->acquireLock();
|
||||
}
|
||||
|
||||
// Exit early if task routine is not available
|
||||
if (!SchedulerHelper::getTaskOptions()->findOption($this->get('type'))) {
|
||||
$this->snapshot['status'] = Status::NO_ROUTINE;
|
||||
$this->skipExecution();
|
||||
$this->dispatchExitEvent();
|
||||
|
||||
return $this->isSuccess();
|
||||
}
|
||||
|
||||
$this->snapshot['status'] = Status::RUNNING;
|
||||
$this->snapshot['taskStart'] = $this->snapshot['taskStart'] ?? microtime(true);
|
||||
$this->snapshot['netDuration'] = 0;
|
||||
|
||||
/** @var ExecuteTaskEvent $event */
|
||||
$event = AbstractEvent::create(
|
||||
'onExecuteTask',
|
||||
[
|
||||
'eventClass' => ExecuteTaskEvent::class,
|
||||
'subject' => $this,
|
||||
'routineId' => $this->get('type'),
|
||||
'langConstPrefix' => $this->get('taskOption')->langConstPrefix,
|
||||
'params' => $this->get('params'),
|
||||
]
|
||||
);
|
||||
|
||||
PluginHelper::importPlugin('task');
|
||||
|
||||
try {
|
||||
$this->app->getDispatcher()->dispatch('onExecuteTask', $event);
|
||||
} catch (\Exception $e) {
|
||||
// Suppress the exception for now, we'll throw it again once it's safe
|
||||
$this->log(Text::sprintf('COM_SCHEDULER_TASK_ROUTINE_EXCEPTION', $e->getMessage()), 'error');
|
||||
$this->snapshot['exception'] = $e;
|
||||
$this->snapshot['status'] = Status::KNOCKOUT;
|
||||
}
|
||||
|
||||
$resultSnapshot = $event->getResultSnapshot();
|
||||
|
||||
$this->snapshot['taskEnd'] = microtime(true);
|
||||
$this->snapshot['netDuration'] = $this->snapshot['taskEnd'] - $this->snapshot['taskStart'];
|
||||
$this->snapshot = array_merge($this->snapshot, $resultSnapshot);
|
||||
|
||||
// @todo make the ExecRuleHelper usage less ugly, perhaps it should be composed into Task
|
||||
// Update object state.
|
||||
$this->set('last_execution', Factory::getDate('@' . (int) $this->snapshot['taskStart'])->toSql());
|
||||
$this->set('last_exit_code', $this->snapshot['status']);
|
||||
|
||||
if ($this->snapshot['status'] !== Status::WILL_RESUME) {
|
||||
$this->set('next_execution', (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec());
|
||||
$this->set('times_executed', $this->get('times_executed') + 1);
|
||||
} else {
|
||||
/**
|
||||
* Resumable tasks need special handling.
|
||||
*
|
||||
* They are rescheduled as soon as possible to let their next step to be executed without
|
||||
* a very large temporal gap to the previous step.
|
||||
*
|
||||
* Moreover, the times executed does NOT increase for each step. It will increase once,
|
||||
* after the last step, when they return Status::OK.
|
||||
*/
|
||||
$this->set('next_execution', Factory::getDate('now', 'UTC')->sub(new \DateInterval('PT1M'))->toSql());
|
||||
}
|
||||
|
||||
// The only acceptable "successful" statuses are either clean exit or resuming execution.
|
||||
if (!\in_array($this->snapshot['status'], [Status::WILL_RESUME, Status::OK])) {
|
||||
$this->set('times_failed', $this->get('times_failed') + 1);
|
||||
}
|
||||
|
||||
if (!$this->releaseLock()) {
|
||||
$this->snapshot['status'] = Status::NO_RELEASE;
|
||||
}
|
||||
|
||||
$this->dispatchExitEvent();
|
||||
|
||||
if (!empty($this->snapshot['exception'])) {
|
||||
throw $this->snapshot['exception'];
|
||||
}
|
||||
|
||||
return $this->isSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the task execution snapshot.
|
||||
* ! Access locations will need updates once a more robust Snapshot container is implemented.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function getContent(): array
|
||||
{
|
||||
return $this->snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a pseudo-lock on the task record.
|
||||
* ! At the moment, this method is not used anywhere as task locks are already
|
||||
* acquired when they're fetched. As such this method is not functional and should
|
||||
* not be reviewed until it is updated.
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function acquireLock(): bool
|
||||
{
|
||||
$db = $this->db;
|
||||
$query = $db->getQuery(true);
|
||||
$id = $this->get('id');
|
||||
$now = Factory::getDate('now', 'GMT');
|
||||
|
||||
$timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300);
|
||||
$timeout = new \DateInterval(sprintf('PT%dS', $timeout));
|
||||
$timeoutThreshold = (clone $now)->sub($timeout)->toSql();
|
||||
$now = $now->toSql();
|
||||
|
||||
// @todo update or remove this method
|
||||
$query->update($db->quoteName('#__scheduler_tasks'))
|
||||
->set('locked = :now')
|
||||
->where($db->quoteName('id') . ' = :taskId')
|
||||
->extendWhere(
|
||||
'AND',
|
||||
[
|
||||
$db->quoteName('locked') . ' < :threshold',
|
||||
$db->quoteName('locked') . 'IS NULL',
|
||||
],
|
||||
'OR'
|
||||
)
|
||||
->bind(':taskId', $id, ParameterType::INTEGER)
|
||||
->bind(':now', $now)
|
||||
->bind(':threshold', $timeoutThreshold);
|
||||
|
||||
try {
|
||||
$db->lockTable('#__scheduler_tasks');
|
||||
$db->setQuery($query)->execute();
|
||||
} catch (\RuntimeException $e) {
|
||||
return false;
|
||||
} finally {
|
||||
$db->unlockTables();
|
||||
}
|
||||
|
||||
if ($db->getAffectedRows() === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->set('locked', $now);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the pseudo-lock and optionally update the task record.
|
||||
*
|
||||
* @param bool $update If true, the record is updated with the snapshot
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function releaseLock(bool $update = true): bool
|
||||
{
|
||||
$db = $this->db;
|
||||
$query = $db->getQuery(true);
|
||||
$id = $this->get('id');
|
||||
|
||||
$query->update($db->quoteName('#__scheduler_tasks', 't'))
|
||||
->set('locked = NULL')
|
||||
->where($db->quoteName('id') . ' = :taskId')
|
||||
->where($db->quoteName('locked') . ' IS NOT NULL')
|
||||
->bind(':taskId', $id, ParameterType::INTEGER);
|
||||
|
||||
if ($update) {
|
||||
$exitCode = $this->get('last_exit_code');
|
||||
$lastExec = $this->get('last_execution');
|
||||
$nextExec = $this->get('next_execution');
|
||||
$timesFailed = $this->get('times_failed');
|
||||
$timesExecuted = $this->get('times_executed');
|
||||
|
||||
$query->set(
|
||||
[
|
||||
'last_exit_code = :exitCode',
|
||||
'last_execution = :lastExec',
|
||||
'next_execution = :nextExec',
|
||||
'times_executed = :times_executed',
|
||||
'times_failed = :times_failed',
|
||||
]
|
||||
)
|
||||
->bind(':exitCode', $exitCode, ParameterType::INTEGER)
|
||||
->bind(':lastExec', $lastExec)
|
||||
->bind(':nextExec', $nextExec)
|
||||
->bind(':times_executed', $timesExecuted)
|
||||
->bind(':times_failed', $timesFailed);
|
||||
}
|
||||
|
||||
try {
|
||||
$db->setQuery($query)->execute();
|
||||
} catch (\RuntimeException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$db->getAffectedRows()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->set('locked', null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message Log message
|
||||
* @param string $priority Log level, defaults to 'info'
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function log(string $message, string $priority = 'info'): void
|
||||
{
|
||||
$this->logger->log($priority, $message, ['category' => $this->logCategory]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the task entry's next calculated execution, effectively skipping the current execution.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function skipExecution(): void
|
||||
{
|
||||
$db = $this->db;
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
$id = $this->get('id');
|
||||
$nextExec = (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec(true, true);
|
||||
|
||||
$query->update($db->quoteName('#__scheduler_tasks', 't'))
|
||||
->set('t.next_execution = :nextExec')
|
||||
->where('t.id = :id')
|
||||
->bind(':nextExec', $nextExec)
|
||||
->bind(':id', $id);
|
||||
|
||||
try {
|
||||
$db->setQuery($query)->execute();
|
||||
} catch (\RuntimeException $e) {
|
||||
}
|
||||
|
||||
$this->set('next_execution', $nextExec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles task exit (dispatch event).
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
*
|
||||
* @throws \UnexpectedValueException|\BadMethodCallException
|
||||
*/
|
||||
protected function dispatchExitEvent(): void
|
||||
{
|
||||
$exitCode = $this->snapshot['status'] ?? 'NA';
|
||||
$eventName = self::EVENTS_MAP[$exitCode] ?? self::EVENTS_MAP['NA'];
|
||||
|
||||
$event = AbstractEvent::create(
|
||||
$eventName,
|
||||
[
|
||||
'subject' => $this,
|
||||
]
|
||||
);
|
||||
|
||||
$this->app->getDispatcher()->dispatch($eventName, $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Was the task successful?
|
||||
*
|
||||
* @return boolean True if the task was successful.
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function isSuccess(): bool
|
||||
{
|
||||
return \in_array(($this->snapshot['status'] ?? null), [Status::OK, Status::WILL_RESUME]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a task property. This method is a proxy to {@see Registry::set()}.
|
||||
*
|
||||
* @param string $path Registry path of the task property.
|
||||
* @param mixed $value The value to set to the property.
|
||||
* @param ?string $separator The key separator.
|
||||
*
|
||||
* @return mixed|null
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected function set(string $path, $value, string $separator = null)
|
||||
{
|
||||
return $this->taskRegistry->set($path, $value, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a task property. This method is a proxy to {@see Registry::get()}.
|
||||
*
|
||||
* @param string $path Registry path of the task property.
|
||||
* @param mixed $default Default property to return, if the actual value is null.
|
||||
*
|
||||
* @return mixed The task property.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function get(string $path, $default = null)
|
||||
{
|
||||
return $this->taskRegistry->get($path, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to determine whether an enumerated task state (as a string) is valid.
|
||||
*
|
||||
* @param string $state The task state (enumerated, as a string).
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public static function isValidState(string $state): bool
|
||||
{
|
||||
if (!is_numeric($state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Takes care of interpreting as float/int
|
||||
$state += 0;
|
||||
|
||||
return ArrayHelper::getValue(self::STATE_MAP, $state) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to determine whether a task id is valid. Note that this does not
|
||||
* validate ids against the database, but only verifies that an id may exist.
|
||||
*
|
||||
* @param string $id The task id (as a string).
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public static function isValidId(string $id): bool
|
||||
{
|
||||
$id = is_numeric($id) ? ($id + 0) : $id;
|
||||
|
||||
if (!\is_int($id) || $id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
117
administrator/components/com_scheduler/src/Task/TaskOption.php
Normal file
117
administrator/components/com_scheduler/src/Task/TaskOption.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Task;
|
||||
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* The TaskOption class is used as a utility container for available plugin-provided task routines.
|
||||
* Each task-supporting plugin calls the {@see TaskOptions::addOptions()} method with an array of TaskOption constructor
|
||||
* argument pairs as argument. Internally, the TaskOption object generates the routine title and description from the
|
||||
* language constant prefix.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*
|
||||
* @property-read string $desc The routine description.
|
||||
* @property-read string $id The routine ID.
|
||||
* @property-read string $langConstPrefix The routine's language constant prefix.
|
||||
* @property-read string $title The routine title.
|
||||
*/
|
||||
class TaskOption
|
||||
{
|
||||
/**
|
||||
* Task routine title
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $title;
|
||||
|
||||
/**
|
||||
* Task routine description.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $desc;
|
||||
|
||||
/**
|
||||
* Routine type-ID.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $langConstPrefix;
|
||||
|
||||
/**
|
||||
* TaskOption constructor.
|
||||
*
|
||||
* @param string $type A unique ID string for a plugin task routine.
|
||||
* @param string $langConstPrefix The Language constant prefix $p. Expects $p . _TITLE and $p . _DESC to exist.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function __construct(string $type, string $langConstPrefix)
|
||||
{
|
||||
$this->id = $type;
|
||||
$this->title = Text::_("{$langConstPrefix}_TITLE");
|
||||
$this->desc = Text::_("{$langConstPrefix}_DESC");
|
||||
$this->langConstPrefix = $langConstPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method to allow read-only access to private properties.
|
||||
*
|
||||
* @param string $name The object property requested.
|
||||
*
|
||||
* @return ?string
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
if (property_exists($this, $name)) {
|
||||
return $this->$name;
|
||||
}
|
||||
|
||||
// Trigger a deprecation for the 'type' property (replaced with {@see id}).
|
||||
if ($name === 'type') {
|
||||
try {
|
||||
Log::add(
|
||||
sprintf(
|
||||
'The %1$s property is deprecated. Use %2$s instead.',
|
||||
$name,
|
||||
'id'
|
||||
),
|
||||
Log::WARNING,
|
||||
'deprecated'
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
// Pass
|
||||
}
|
||||
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Task;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* The TaskOptions class.
|
||||
* Used as the subject argument for the `onTaskOptionsList` event, plugins that support tasks must add them to the
|
||||
* object through the addOptions() method.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class TaskOptions
|
||||
{
|
||||
/**
|
||||
* An array of TaskOptions
|
||||
*
|
||||
* @var TaskOption[]
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public $options = [];
|
||||
|
||||
/**
|
||||
* A plugin can support several task routines
|
||||
* This method is used by a plugin's onTaskOptionsList subscriber to advertise supported routines.
|
||||
*
|
||||
* @param array $taskRoutines An associative array of {@var TaskOption} constructor argument pairs:
|
||||
* [ 'routineId' => 'languageConstantPrefix', ... ]
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function addOptions(array $taskRoutines): void
|
||||
{
|
||||
foreach ($taskRoutines as $routineId => $langConstPrefix) {
|
||||
$this->options[] = new TaskOption($routineId, $langConstPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $routineId A unique identifier for a plugin task routine
|
||||
*
|
||||
* @return ?TaskOption A matching TaskOption if available, null otherwise
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public function findOption(?string $routineId): ?TaskOption
|
||||
{
|
||||
if ($routineId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->options as $option) {
|
||||
if ($option->id === $routineId) {
|
||||
return $option;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,337 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\Traits;
|
||||
|
||||
use Joomla\CMS\Event\Model;
|
||||
use Joomla\CMS\Factory;
|
||||
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\Filesystem\Path;
|
||||
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 Model\PrepareFormEvent|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 Model\PrepareFormEvent) {
|
||||
$form = $context->getForm();
|
||||
$data = $context->getData();
|
||||
} 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.<br/>
|
||||
* **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.<br/>
|
||||
* **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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\View\Select;
|
||||
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\TaskOption;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* The MVC View for the routine selection page (SelectView).
|
||||
* This view lets the user choose from a list of plugin defined task routines.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* @var AdministratorApplication
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* The model state
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* An array of items
|
||||
*
|
||||
* @var TaskOption[]
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* HtmlView constructor.
|
||||
*
|
||||
* @param array $config A named configuration array for object construction.
|
||||
* name: the name (optional) of the view (defaults to the view class name suffix).
|
||||
* charset: the character set to use for display
|
||||
* escape: the name (optional) of the function to use for escaping strings
|
||||
* base_path: the parent path (optional) of the `views` directory (defaults to the component
|
||||
* folder) template_plath: the path (optional) of the layout directory (defaults to
|
||||
* base_path + /views/ + view name helper_path: the path (optional) of the helper files
|
||||
* (defaults to base_path + /helpers/) layout: the layout (optional) to use to display the
|
||||
* view
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct($config = [])
|
||||
{
|
||||
$this->app = Factory::getApplication();
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
$this->state = $this->get('State');
|
||||
$this->items = $this->get('Items');
|
||||
|
||||
// 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 4.1.0
|
||||
*/
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock');
|
||||
|
||||
$toolbar->linkButton('cancel')
|
||||
->url('index.php?option=com_scheduler')
|
||||
->buttonClass('btn btn-danger')
|
||||
->icon('icon-times')
|
||||
->text('JCANCEL');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\View\Task;
|
||||
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\Form;
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* The MVC View for Task configuration page (TaskView).
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* @var AdministratorApplication $app
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* The Form object
|
||||
*
|
||||
* @var Form
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $form;
|
||||
|
||||
/**
|
||||
* The active item
|
||||
*
|
||||
* @var object
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $item;
|
||||
|
||||
/**
|
||||
* The model state
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* The actions the user is authorised to perform
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $canDo;
|
||||
|
||||
/**
|
||||
* Overloads the parent constructor.
|
||||
* Just needed to fetch the Application object.
|
||||
*
|
||||
* @param array $config A named configuration array for object construction.
|
||||
* name: the name (optional) of the view (defaults to the view class name suffix).
|
||||
* charset: the character set to use for display
|
||||
* escape: the name (optional) of the function to use for escaping strings
|
||||
* base_path: the parent path (optional) of the `views` directory (defaults to the
|
||||
* component folder) template_plath: the path (optional) of the layout directory (defaults
|
||||
* to base_path + /views/ + view name helper_path: the path (optional) of the helper files
|
||||
* (defaults to base_path + /helpers/) layout: the layout (optional) to use to display the
|
||||
* view
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct($config = [])
|
||||
{
|
||||
$this->app = Factory::getApplication();
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
/*
|
||||
* Will call the getForm() method of TaskModel
|
||||
*/
|
||||
$this->form = $this->get('Form');
|
||||
$this->item = $this->get('Item');
|
||||
$this->state = $this->get('State');
|
||||
$this->canDo = ContentHelper::getActions('com_scheduler', 'task', $this->item->id);
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the page title and toolbar
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
$this->app->getInput()->set('hidemainmenu', true);
|
||||
|
||||
$isNew = ($this->item->id == 0);
|
||||
$canDo = $this->canDo;
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title($isNew ? Text::_('COM_SCHEDULER_MANAGER_TASK_NEW') : Text::_('COM_SCHEDULER_MANAGER_TASK_EDIT'), 'clock');
|
||||
|
||||
if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) {
|
||||
$toolbar->apply('task.apply');
|
||||
$toolbar->save('task.save');
|
||||
}
|
||||
|
||||
// @todo | ? : Do we need save2new, save2copy?
|
||||
|
||||
$toolbar->cancel('task.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE');
|
||||
$toolbar->help('Scheduled_Tasks:_Edit');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @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\Component\Scheduler\Administrator\View\Tasks;
|
||||
|
||||
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\Button\DropdownButton;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* MVC View for the Tasks list page.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* Array of task items.
|
||||
*
|
||||
* @var array
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* The pagination object.
|
||||
*
|
||||
* @var Pagination
|
||||
* @since 4.1.0
|
||||
* @todo Test pagination.
|
||||
*/
|
||||
protected $pagination;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 4.1.0
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* A Form object for search filters.
|
||||
*
|
||||
* @var Form
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public $filterForm;
|
||||
|
||||
/**
|
||||
* The active search filters.
|
||||
*
|
||||
* @var array
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public $activeFilters;
|
||||
|
||||
/**
|
||||
* Is this view in an empty state?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private $isEmptyState = false;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
$this->items = $this->get('Items');
|
||||
$this->pagination = $this->get('Pagination');
|
||||
$this->state = $this->get('State');
|
||||
$this->filterForm = $this->get('FilterForm');
|
||||
$this->activeFilters = $this->get('ActiveFilters');
|
||||
|
||||
if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) {
|
||||
$this->setLayout('empty_state');
|
||||
}
|
||||
|
||||
// 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 4.1.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
$canDo = ContentHelper::getActions('com_scheduler');
|
||||
$user = $this->getCurrentUser();
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock');
|
||||
|
||||
if ($canDo->get('core.create')) {
|
||||
$toolbar->linkButton('new', 'JTOOLBAR_NEW')
|
||||
->url('index.php?option=com_scheduler&view=select&layout=default')
|
||||
->buttonClass('btn btn-success')
|
||||
->icon('icon-new');
|
||||
}
|
||||
|
||||
if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) {
|
||||
/** @var DropdownButton $dropdown */
|
||||
$dropdown = $toolbar->dropdownButton('status-group')
|
||||
->toggleSplit(false)
|
||||
->text('JTOOLBAR_CHANGE_STATUS')
|
||||
->icon('icon-ellipsis-h')
|
||||
->buttonClass('btn btn-action')
|
||||
->listCheck(true);
|
||||
|
||||
$childBar = $dropdown->getChildToolbar();
|
||||
|
||||
// Add the batch Enable, Disable and Trash buttons if privileged
|
||||
if ($canDo->get('core.edit.state')) {
|
||||
$childBar->publish('tasks.publish', 'JTOOLBAR_ENABLE')->listCheck(true);
|
||||
$childBar->unpublish('tasks.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true);
|
||||
|
||||
if ($canDo->get('core.admin')) {
|
||||
$childBar->checkin('tasks.checkin');
|
||||
}
|
||||
|
||||
$childBar->checkin('tasks.unlock', 'COM_SCHEDULER_TOOLBAR_UNLOCK')->icon('icon-unlock');
|
||||
|
||||
// We don't want the batch Trash button if displayed entries are all trashed
|
||||
if ($this->state->get('filter.state') != -2) {
|
||||
$childBar->trash('tasks.trash')->listCheck(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add "Empty Trash" button if filtering by trashed.
|
||||
if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) {
|
||||
$toolbar->delete('tasks.delete', 'JTOOLBAR_EMPTY_TRASH')
|
||||
->message('JGLOBAL_CONFIRM_DELETE')
|
||||
->listCheck(true);
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
/** The SelectView default layout template. */
|
||||
|
||||
// Restrict direct access
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\Component\Scheduler\Administrator\View\Select\HtmlView;
|
||||
|
||||
/** @var HtmlView $this */
|
||||
|
||||
$app = $this->app;
|
||||
|
||||
$wa = $this->document->getWebAssetManager();
|
||||
$wa->useScript('com_scheduler.admin-view-select-task-search');
|
||||
|
||||
?>
|
||||
|
||||
<!-- Tasks search box on below the toolbar begins -->
|
||||
<div class="d-none" id="comSchedulerSelectSearchContainer">
|
||||
<div class="d-flex mt-2">
|
||||
<div class="m-auto">
|
||||
<label class="visually-hidden" for="comSchedulerSelectSearch">
|
||||
<?php echo Text::_('COM_SCHEDULER_TYPE_CHOOSE'); ?>
|
||||
</label>
|
||||
<div class="input-group mb-3 me-sm-2">
|
||||
<input type="text" value=""
|
||||
class="form-control" id="comSchedulerSelectSearch"
|
||||
placeholder="<?php echo Text::_('JSEARCH_FILTER'); ?>"
|
||||
>
|
||||
<div class="input-group-text">
|
||||
<span class="icon-search" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Search box and related elements end -->
|
||||
|
||||
<div id="new-tasks-list">
|
||||
<div class="new-tasks">
|
||||
<!-- Hidden alert div -->
|
||||
<div class="tasks-alert alert alert-info d-none">
|
||||
<span class="icon-info-circle" aria-hidden="true"></span><span
|
||||
class="visually-hidden"><?php echo Text::_('INFO'); ?></span>
|
||||
<?php echo Text::_('COM_SCHEDULER_MSG_MANAGE_NO_TASK_PLUGINS'); ?>
|
||||
</div>
|
||||
<h2 class="pb-3 ms-3" id="comSchedulerSelectTypeHeader">
|
||||
<?php echo Text::_('COM_SCHEDULER_TYPE_CHOOSE'); ?>
|
||||
</h2>
|
||||
|
||||
<!-- Parent card -->
|
||||
<div class="main-card card-columns p-4" id="comSchedulerSelectResultsContainer">
|
||||
|
||||
<!-- Plugin task cards start below -->
|
||||
<?php foreach ($this->items as $item) : ?>
|
||||
<?php // Prepare variables for the link. ?>
|
||||
<?php $link = 'index.php?option=com_scheduler&task=task.add&type=' . $item->id; ?>
|
||||
<?php $name = $this->escape($item->title); ?>
|
||||
<?php $desc = HTMLHelper::_('string.truncate', $this->escape(strip_tags($item->desc)), 200); ?>
|
||||
<!-- The task card begins -->
|
||||
<a href="<?php echo Route::_($link); ?>" class="new-task mb-3 comSchedulerSelectCard"
|
||||
aria-label="<?php echo Text::sprintf('COM_SCHEDULER_SELECT_TASK_TYPE', $name); ?>">
|
||||
<div class="new-task-details">
|
||||
<h3 class="new-task-title"><?php echo $name; ?></h3>
|
||||
<p class="new-task-caption p-0">
|
||||
<?php echo $desc; ?>
|
||||
</p>
|
||||
</div>
|
||||
<span class="new-task-link">
|
||||
<span class="icon-plus" aria-hidden="true"></span>
|
||||
</span>
|
||||
</a>
|
||||
<!-- The task card ends here -->
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
204
administrator/components/com_scheduler/tmpl/task/edit.php
Normal file
204
administrator/components/com_scheduler/tmpl/task/edit.php
Normal file
@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
// Restrict direct access
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Layout\LayoutHelper;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\TaskOption;
|
||||
use Joomla\Component\Scheduler\Administrator\View\Task\HtmlView;
|
||||
|
||||
/** @var HtmlView $this */
|
||||
|
||||
$wa = $this->document->getWebAssetManager();
|
||||
|
||||
$wa->useScript('keepalive');
|
||||
$wa->useScript('form.validate');
|
||||
$wa->useStyle('com_scheduler.admin-view-task-css');
|
||||
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->app;
|
||||
|
||||
$input = $app->getInput();
|
||||
|
||||
// Fieldsets to be ignored by the `joomla.edit.params` template.
|
||||
$this->ignore_fieldsets = ['aside', 'details', 'exec_hist', 'custom-cron-rules', 'basic', 'advanced', 'priority'];
|
||||
|
||||
// Used by the `joomla.edit.params` template to render the right template for UI tabs.
|
||||
$this->useCoreUI = true;
|
||||
|
||||
$advancedFieldsets = $this->form->getFieldsets('params');
|
||||
|
||||
// Don't show the params fieldset, they will be loaded later
|
||||
foreach ($advancedFieldsets as $name => $fieldset) :
|
||||
if ($name === 'task_params') :
|
||||
unset($advancedFieldsets[$name]);
|
||||
continue;
|
||||
endif;
|
||||
|
||||
$this->ignore_fieldsets[] = $fieldset->name;
|
||||
endforeach;
|
||||
|
||||
?>
|
||||
|
||||
<form action="<?php echo Route::_('index.php?option=com_scheduler&view=task&layout=edit&id=' . (int) $this->item->id); ?>"
|
||||
method="post" name="adminForm" id="task-form"
|
||||
aria-label="<?php echo Text::_('COM_SCHEDULER_FORM_TITLE_' . ((int) $this->item->id === 0 ? 'NEW' : 'EDIT'), true); ?>"
|
||||
class="form-validate">
|
||||
|
||||
<!-- The task title field -->
|
||||
<?php echo LayoutHelper::render('joomla.edit.title_alias', $this); ?>
|
||||
|
||||
<!-- The main form card -->
|
||||
<div class="main-card">
|
||||
<?php echo HTMLHelper::_('uitab.startTabSet', 'myTab', ['active' => 'general']); ?>
|
||||
|
||||
<!-- The first (and the main) tab in the form -->
|
||||
<?php echo
|
||||
HTMLHelper::_(
|
||||
'uitab.addTab',
|
||||
'myTab',
|
||||
'general',
|
||||
empty($this->item->id) ? Text::_('COM_SCHEDULER_NEW_TASK') : Text::_('COM_SCHEDULER_EDIT_TASK')
|
||||
);
|
||||
?>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<!-- Task type title, description go here -->
|
||||
<?php if ($this->item->taskOption) :
|
||||
/** @var TaskOption $taskOption */
|
||||
$taskOption = $this->item->taskOption; ?>
|
||||
<div id="taskOptionInfo">
|
||||
<h2 id="taskOptionTitle">
|
||||
<?php echo $taskOption->title ?>
|
||||
</h2>
|
||||
<?php
|
||||
$this->fieldset = 'description';
|
||||
$short_description = $taskOption->desc;
|
||||
$long_description = LayoutHelper::render('joomla.edit.fieldset', $this);
|
||||
|
||||
if (!$long_description) {
|
||||
$truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false);
|
||||
|
||||
if (strlen($truncated) > 500) {
|
||||
$long_description = $short_description;
|
||||
$short_description = HTMLHelper::_('string.truncate', $truncated, 250);
|
||||
|
||||
if ($short_description == $long_description) {
|
||||
$long_description = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<p><?php echo $short_description; ?></p>
|
||||
<?php if ($long_description) : ?>
|
||||
<p class="readmore">
|
||||
<a href="#" onclick="document.getElementById('myTab').activateTab(document.getElementById('description'));">
|
||||
<?php echo Text::_('JGLOBAL_SHOW_FULL_DESCRIPTION'); ?>
|
||||
</a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<!-- If TaskOption does not exist -->
|
||||
<?php else :
|
||||
$app->enqueueMessage(Text::_('COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND'), 'warning');
|
||||
?>
|
||||
<?php endif; ?>
|
||||
<fieldset class="options-form">
|
||||
<legend><?php echo Text::_('COM_SCHEDULER_FIELDSET_BASIC'); ?></legend>
|
||||
<?php echo $this->form->renderFieldset('basic'); ?>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="options-form match-custom"
|
||||
data-showon='[{"field":"jform[execution_rules][rule-type]","values":["cron-expression"],"sign":"=","op":""}]'
|
||||
>
|
||||
<legend><?php echo Text::_('COM_SCHEDULER_FIELDSET_CRON_OPTIONS'); ?></legend>
|
||||
<?php echo $this->form->renderFieldset('custom-cron-rules'); ?>
|
||||
</fieldset>
|
||||
<?php echo LayoutHelper::render('joomla.edit.params', $this); ?>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
<?php echo $this->form->renderFieldset('aside'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
<?php if (isset($long_description) && $long_description != '') : ?>
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'description', Text::_('JGLOBAL_FIELDSET_DESCRIPTION')); ?>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<?php echo $long_description; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
<?php endif; ?>
|
||||
<!-- Tab for advanced options -->
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'advanced', Text::_('JGLOBAL_FIELDSET_ADVANCED')) ?>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<fieldset class="options-form">
|
||||
<legend><?php echo Text::_('COM_SCHEDULER_FIELDSET_PRIORITY') ?></legend>
|
||||
<?php echo $this->form->renderFieldset('priority') ?>
|
||||
</fieldset>
|
||||
<?php foreach ($advancedFieldsets as $fieldset) : ?>
|
||||
<fieldset class="options-form">
|
||||
<legend><?php echo Text::_($fieldset->label ?: 'COM_SCHEDULER_FIELDSET_' . $fieldset->name) ?></legend>
|
||||
<?php echo $this->form->renderFieldset($fieldset->name) ?>
|
||||
</fieldset>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab') ?>
|
||||
|
||||
<!-- Tab to show execution history -->
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'exec_hist', Text::_('COM_SCHEDULER_FIELDSET_EXEC_HIST')); ?>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<fieldset class="options-form">
|
||||
<legend><?php echo Text::_('COM_SCHEDULER_FIELDSET_EXEC_HIST'); ?></legend>
|
||||
<?php echo $this->form->renderFieldset('exec_hist'); ?>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
<!-- Tab to show creation details-->
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'details', Text::_('JDETAILS')); ?>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<fieldset class="options-form">
|
||||
<legend><?php echo Text::_('JDETAILS'); ?></legend>
|
||||
<?php echo $this->form->renderFieldset('details'); ?>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
<!-- Item permissions tab, if user has admin privileges -->
|
||||
<?php if ($this->canDo->get('core.admin')) : ?>
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'permissions', Text::_('JCONFIG_PERMISSIONS_LABEL')); ?>
|
||||
<fieldset id="fieldset-permissions" class="options-form">
|
||||
<legend><?php echo Text::_('JCONFIG_PERMISSIONS_LABEL'); ?></legend>
|
||||
<div>
|
||||
<?php echo $this->form->getInput('rules'); ?>
|
||||
</div>
|
||||
</fieldset>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
<?php endif; ?>
|
||||
<?php echo HTMLHelper::_('uitab.endTabSet'); ?>
|
||||
<?php echo $this->form->getInput('context'); ?>
|
||||
<input type="hidden" name="task" value="">
|
||||
<?php echo HTMLHelper::_('form.token'); ?>
|
||||
</div>
|
||||
</form>
|
||||
292
administrator/components/com_scheduler/tmpl/tasks/default.php
Normal file
292
administrator/components/com_scheduler/tmpl/tasks/default.php
Normal file
@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
// Restrict direct access
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Application\CMSWebApplicationInterface;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Layout\LayoutHelper;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\Status;
|
||||
use Joomla\Component\Scheduler\Administrator\View\Tasks\HtmlView;
|
||||
|
||||
/** @var HtmlView $this*/
|
||||
|
||||
/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */
|
||||
$wa = $this->document->getWebAssetManager();
|
||||
$wa->useScript('table.columns')
|
||||
->useScript('multiselect')
|
||||
->useScript('com_scheduler.test-task')
|
||||
->useStyle('com_scheduler.admin-view-tasks-css');
|
||||
|
||||
Text::script('COM_SCHEDULER_TEST_RUN_TITLE');
|
||||
Text::script('COM_SCHEDULER_TEST_RUN_TASK');
|
||||
Text::script('COM_SCHEDULER_TEST_RUN_DURATION');
|
||||
Text::script('COM_SCHEDULER_TEST_RUN_OUTPUT');
|
||||
Text::script('COM_SCHEDULER_TEST_RUN_STATUS_STARTED');
|
||||
Text::script('COM_SCHEDULER_TEST_RUN_STATUS_COMPLETED');
|
||||
Text::script('COM_SCHEDULER_TEST_RUN_STATUS_TERMINATED');
|
||||
Text::script('JLIB_JS_AJAX_ERROR_OTHER');
|
||||
Text::script('JLIB_JS_AJAX_ERROR_CONNECTION_ABORT');
|
||||
Text::script('JLIB_JS_AJAX_ERROR_TIMEOUT');
|
||||
Text::script('JLIB_JS_AJAX_ERROR_NO_CONTENT');
|
||||
Text::script('JLIB_JS_AJAX_ERROR_PARSE');
|
||||
|
||||
try {
|
||||
/** @var CMSWebApplicationInterface $app */
|
||||
$app = Factory::getApplication();
|
||||
} catch (Exception $e) {
|
||||
die('Failed to get app');
|
||||
}
|
||||
|
||||
$user = $app->getIdentity();
|
||||
$userId = $user->get('id');
|
||||
$listOrder = $this->escape($this->state->get('list.ordering'));
|
||||
$listDirn = $this->escape($this->state->get('list.direction'));
|
||||
$saveOrder = $listOrder == 'a.ordering';
|
||||
$section = null;
|
||||
$mode = false;
|
||||
|
||||
if ($saveOrder && !empty($this->items)) {
|
||||
$saveOrderingUrl = 'index.php?option=com_scheduler&task=tasks.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1';
|
||||
HTMLHelper::_('draggablelist.draggable');
|
||||
}
|
||||
|
||||
$this->document->addScriptOptions('com_scheduler.test-task.token', Session::getFormToken());
|
||||
?>
|
||||
|
||||
<form action="<?php echo Route::_('index.php?option=com_scheduler&view=tasks'); ?>" method="post" name="adminForm"
|
||||
id="adminForm">
|
||||
<div id="j-main-container" class="j-main-container">
|
||||
<?php
|
||||
// Search tools bar
|
||||
echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]);
|
||||
?>
|
||||
|
||||
<!-- If no tasks -->
|
||||
<?php if (empty($this->items)) : ?>
|
||||
<!-- No tasks -->
|
||||
<div class="alert alert-info">
|
||||
<span class="icon-info-circle" aria-hidden="true"></span><span
|
||||
class="visually-hidden"><?php echo Text::_('INFO'); ?></span>
|
||||
<?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- If there are tasks, we start with the table -->
|
||||
<?php if (!empty($this->items)) : ?>
|
||||
<!-- Tasks table starts here -->
|
||||
<table class="table" id="categoryList">
|
||||
|
||||
<caption 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>
|
||||
|
||||
<!-- Tasks table header -->
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
<!-- Select all -->
|
||||
<td class="w-1 text-center">
|
||||
<?php echo HTMLHelper::_('grid.checkall'); // "Select all" checkbox
|
||||
?>
|
||||
</td>
|
||||
|
||||
<!-- Ordering?-->
|
||||
<th scope="col" class="w-1 d-none d-md-table-cell text-center">
|
||||
<!-- Might need to adjust method args here -->
|
||||
<?php echo HTMLHelper::_('searchtools.sort', '', 'a.ordering', $listDirn, $listOrder, null, 'asc', 'JGRID_HEADING_ORDERING', 'icon-sort'); ?>
|
||||
</th>
|
||||
<!-- Task State -->
|
||||
<th scope="col" class="w-1 text-center">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.state', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
|
||||
<!-- Task title header -->
|
||||
<th scope="col">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'JGLOBAL_TITLE', 'a.title', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
|
||||
<!-- Task type header -->
|
||||
<th scope="col" class="d-none d-md-table-cell">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_TASK_TYPE', 'j.type_title', $listDirn, $listOrder) ?>
|
||||
</th>
|
||||
|
||||
<!-- Last runs -->
|
||||
<th scope="col" class="d-none d-lg-table-cell">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LAST_RUN_DATE', 'a.last_execution', $listDirn, $listOrder) ?>
|
||||
</th>
|
||||
|
||||
<!-- Test task -->
|
||||
<th scope="col" class="d-none d-md-table-cell">
|
||||
<?php echo Text::_('COM_SCHEDULER_TEST_TASK'); ?>
|
||||
</th>
|
||||
|
||||
<!-- Priority -->
|
||||
<th scope="col" class="d-none d-lg-table-cell">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_TASK_PRIORITY', 'a.priority', $listDirn, $listOrder) ?>
|
||||
</th>
|
||||
|
||||
<!-- Task ID -->
|
||||
<th scope="col" class="w-5 d-none d-md-table-cell">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<!-- Table body begins -->
|
||||
<tbody <?php if ($saveOrder) : ?>
|
||||
class="js-draggable" data-url="<?php echo $saveOrderingUrl; ?>" data-direction="<?php echo strtolower($listDirn); ?>" data-nested="true" <?php
|
||||
endif; ?>>
|
||||
<?php foreach ($this->items as $i => $item) :
|
||||
$canCreate = $user->authorise('core.create', 'com_scheduler');
|
||||
$canEdit = $user->authorise('core.edit', 'com_scheduler');
|
||||
$canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out);
|
||||
$canChange = $user->authorise('core.edit.state', 'com_scheduler') && $canCheckin;
|
||||
?>
|
||||
|
||||
<!-- Row begins -->
|
||||
<tr class="row<?php echo $i % 2; ?>"
|
||||
data-draggable-group="none"
|
||||
>
|
||||
<!-- Item Checkbox -->
|
||||
<td class="text-center">
|
||||
<?php echo HTMLHelper::_('grid.id', $i, $item->id, false, 'cid', 'cb', $item->title); ?>
|
||||
</td>
|
||||
|
||||
<!-- Draggable handle -->
|
||||
<td class="text-center d-none d-md-table-cell">
|
||||
<?php
|
||||
$iconClass = '';
|
||||
if (!$canChange) {
|
||||
$iconClass = ' inactive';
|
||||
} elseif (!$saveOrder) {
|
||||
$iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED');
|
||||
}
|
||||
?>
|
||||
|
||||
<span class="sortable-handler <?php echo $iconClass ?>">
|
||||
<span class="icon-ellipsis-v" aria-hidden="true"></span>
|
||||
</span>
|
||||
|
||||
<?php if ($canChange && $saveOrder) : ?>
|
||||
<input type="text" class="hidden text-area-order" name="order[]" size="5"
|
||||
value="<?php echo $item->ordering; ?>"
|
||||
>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<!-- Item State -->
|
||||
<td class="text-center">
|
||||
<?php echo HTMLHelper::_('jgrid.published', $item->state, $i, 'tasks.', $canChange); ?>
|
||||
</td>
|
||||
|
||||
<!-- Item name, edit link, and note (@todo: should it be moved?) -->
|
||||
<th scope="row">
|
||||
<?php if ($item->checked_out) : ?>
|
||||
<?php echo HTMLHelper::_('jgrid.checkedout', $i, $item->editor, $item->checked_out_time, 'tasks.', $canCheckin); ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($item->locked) : ?>
|
||||
<?php echo HTMLHelper::_('jgrid.action', $i, 'unlock', ['enabled' => $canChange, 'prefix' => 'tasks.',
|
||||
'active_class' => 'none fa fa-running border-dark text-body',
|
||||
'inactive_class' => 'none fa fa-running', 'tip' => true, 'translate' => false,
|
||||
'active_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')),
|
||||
'inactive_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')),
|
||||
]); ?>
|
||||
<?php endif; ?>
|
||||
<span class="task-title">
|
||||
<?php if ($canEdit) : ?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_scheduler&task=task.edit&id=' . $item->id); ?>"
|
||||
title="<?php echo Text::_('JACTION_EDIT'); ?> <?php echo $this->escape($item->title); ?>"> <?php echo $this->escape($item->title); ?>
|
||||
</a>
|
||||
<?php else : ?>
|
||||
<?php echo $this->escape($item->title); ?>
|
||||
<?php endif; ?>
|
||||
<?php if (!in_array($item->last_exit_code, [Status::OK, Status::WILL_RESUME])) : ?>
|
||||
<span class="failure-indicator icon-exclamation-triangle" aria-hidden="true"></span>
|
||||
<div role="tooltip">
|
||||
<?php echo Text::sprintf("COM_SCHEDULER_MANAGER_TOOLTIP_TASK_FAILING", $item->last_exit_code); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
|
||||
<?php if ($item->note) : ?>
|
||||
<span class="small">
|
||||
<?php echo Text::sprintf('JGLOBAL_LIST_NOTE', $this->escape($item->note)); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</th>
|
||||
|
||||
<!-- Item type -->
|
||||
<td class="small d-none d-md-table-cell">
|
||||
<?php echo $this->escape($item->safeTypeTitle); ?>
|
||||
</td>
|
||||
|
||||
<!-- Last run date -->
|
||||
<td class="small d-none d-lg-table-cell">
|
||||
<?php echo $item->last_execution ? HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5') : '-'; ?>
|
||||
</td>
|
||||
|
||||
<!-- Test task -->
|
||||
<td class="small d-none d-md-table-cell">
|
||||
<button type="button" class="btn btn-sm btn-warning" <?php echo $item->state < 0 ? 'disabled' : ''; ?> data-id="<?php echo (int) $item->id; ?>" data-title="<?php echo htmlspecialchars($item->title); ?>" data-bs-toggle="modal" data-bs-backdrop="static" data-bs-target="#scheduler-test-modal">
|
||||
<span class="fa fa-play fa-sm me-2"></span>
|
||||
<?php echo Text::_('COM_SCHEDULER_TEST_RUN'); ?>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
<!-- Priority -->
|
||||
<td class="small d-none d-lg-table-cell">
|
||||
<?php if ($item->priority === -1) : ?>
|
||||
<span class="badge bg-info"><?php echo Text::_('COM_SCHEDULER_LABEL_TASK_PRIORITY_LOW'); ?></span>
|
||||
<?php elseif ($item->priority === 0) : ?>
|
||||
<span class="badge bg-success"><?php echo Text::_('COM_SCHEDULER_LABEL_TASK_PRIORITY_NORMAL'); ?></span>
|
||||
<?php elseif ($item->priority === 1) : ?>
|
||||
<span class="badge bg-danger"><?php echo Text::_('COM_SCHEDULER_LABEL_TASK_PRIORITY_HIGH'); ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<!-- Item ID -->
|
||||
<td class="d-none d-md-table-cell">
|
||||
<?php echo (int) $item->id; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php
|
||||
// Load the pagination. (@todo: testing)
|
||||
echo $this->pagination->getListFooter();
|
||||
|
||||
// Modal for test runs
|
||||
$modalparams = [
|
||||
'title' => '',
|
||||
];
|
||||
|
||||
$modalbody = '<div class="p-3"></div>';
|
||||
|
||||
echo HTMLHelper::_('bootstrap.renderModal', 'scheduler-test-modal', $modalparams, $modalbody);
|
||||
|
||||
?>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<input type="hidden" name="task" value="">
|
||||
<input type="hidden" name="boxchecked" value="0">
|
||||
<?php echo HTMLHelper::_('form.token'); ?>
|
||||
</div>
|
||||
</form>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<layout title="COM_SCHEDULER_TASKS_VIEW_DEFAULT_TITLE">
|
||||
<message>
|
||||
<![CDATA[COM_SCHEDULER_TASKS_VIEW_DEFAULT_DESC]]>
|
||||
</message>
|
||||
</layout>
|
||||
</metadata>
|
||||
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_scheduler
|
||||
*
|
||||
* @copyright (C) 2021 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\Layout\LayoutHelper;
|
||||
|
||||
$displayData = [
|
||||
'textPrefix' => 'COM_SCHEDULER',
|
||||
'formURL' => 'index.php?option=com_scheduler&task=task.add',
|
||||
'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J4.x:Task_Scheduler',
|
||||
'icon' => 'icon-clock clock',
|
||||
];
|
||||
|
||||
if ($this->getCurrentUser()->authorise('core.create', 'com_scheduler')) {
|
||||
$displayData['createURL'] = 'index.php?option=com_scheduler&view=select&layout=default';
|
||||
}
|
||||
|
||||
echo LayoutHelper::render('joomla.content.emptystate', $displayData);
|
||||
Reference in New Issue
Block a user