From 3be67ab04261f582570ea25531bf66f0aa83d0ab Mon Sep 17 00:00:00 2001 From: Tushar Date: Fri, 26 Nov 2021 17:12:10 +0530 Subject: [PATCH] [4.1] [SOC 21] Add a Scheduled Tasks Infrastructure to Joomla (#35143) * Fix task filter ordering Order now matches the order of columns the db table. Ref: joomla/joomla-cms#35143#discussion_r700213211 / @brianteeman. * Improve com_scheduler language strings Improves a task form hint, a label. Refs: joomla/joomla-cms#35143#discussion_r700203125 joomla/joomla-cms#35143#discussion_r700202862 @brianteeman. * Scheduler runner as a shutdown_function The schedule runner trigger is now bound as a shutdown_function. The function is registered by a onBeforeRender listener. * Enhance task locks and events - Updates task locks (the `locked` column) so they now use a timestamp to allow for recovery from a fatal failure. Includes updates to SQL scripts. - Dispatch a new onTaskRecoverFailure event on "recovery" from a fatal failure. - Actually dispatch task exit events (oops!) - Update the event class declaration for onExecuteTask to be more elegant, readable. [1] [1]: http://joomla/joomla-cms#35143#discussion_r700897628 / @Denitz * Add a sanity check to TaskPluginTrait Adds a pre-broadcast sanity check for plugins with invalid an TASK_MAP. The advertiseRoutines method will no longer try to advertise tasks that don't have a corresponding 'langConstPrefix'. * Fix fatal task failure check Fixes the failure check (the lock should not be null) and enhances a comment + styling. * Increase scheduler plugin hook priority Increases the priority for the registerRunner hook to PHP_INT_MAX, although I'm not yet sure if onBeforeRender is the first event to be fired (probably not). * Fix event dispatchers in the Task class The onTaskRecoverFailure and exit task weren't passing the event name to dispatchers. Oops! P.S. I don't know why that's required with the new Event classes. * Cleanup com_scheduler manifest Removes menu link (was supposed to go before). * Add filtering by lock state in TasksModel Adds support for filtering by the "lock" state of a task. Also in the process defines different lock levels: hard locks and soft locks. I think in the future, the filtering states should maybe go into a namespace/class for constants. * Add task timeout config to "com_scheduler" Adds a configuration field for a global task timeout. The timeout serves the purpose of safely declaring a hard-limit on how long tasks can be considered as running once a lock on them is acquired. Right now the default is 300s (5min) but this can be re-evaluated in the future to a more sensible value if needed. * Update language file for "com_scheduler" Adds new constants for the component configuration form. * Bugfix and update Scheduler API class - Now fetches all fields from the DB (this broke things as the `locked` field was never fetched!!) - Adds locked state filtering as a default (excludes hard-locked jobs). * Add priority column to `#__scheduler_tasks` - Adds a priority column to the tasks table, also a linked index. - Fixes update SQL script styling. * Add task priority to Task form and filter form * Add priority, multi-ordering support to TasksModel - Adds priority to the filter_fields config. - Adds support for a `list.multi_ordering` model state variable which can be used to sort by multiple fields (limitation for the standard `list.ordering` `list.direction` model). The way this is right now means we pass unquoted names to the db which should be fixed in the future. In any case this is not exposed in the filter form but only meant to be used by internal handlers interacting with the model. - Fixes behaviour for high collision ordering fields (used to order first by title and then by the targeted column). * Update task timeout with config fetch The Task driver class now fetches the task timeout from the component config. * Add priority-aware task queue to Scheduler class The fetchTasks() method now uses multi-ordering with priority as the primary ordering column to make the task queue priority aware. * Update language files for com_scheduler - Adds language constants for the task priority config in the task and tasks filter form. - Sneaks in a description attribute for the priority form field. * Bugfix and add an 'Advanced' tab to the task form - Moves the log config and task priority to the advanced tab. - Fixes a bug with using `joomla.edit.params`, which is now used for rendering all injected task routine parameters. * Enhance orphan task handling in Task driver - Dispatches now an event if task to run is orphaned, then skip the execution and exit early with a new exit code. - Adds a skipExecution method to the Task class. - Enhanced the handleExit method through an events map to dispatch other events based on exit code. - Adds a new code for orphaned tasks to the Status namespace class. * Enhance orphan handling in Scheduler, update lang - Scheduler now includes orphaned tasks by default. Before this, the ::runTask() method never got to handling or logging orphaned tasks because of the default filtering excluding them. - Adds exceptional log for orphaned tasks. - Update language file to match. * Add demo task routines to stress memory Adds task routines to test scheduled tasks when either the system memory runs out or the PHP memory_limit is exceeded. * Add sleep demo task routine Renames the first demo task routine and adds a form field to configure the sleep duration. * Update language files for plgTaskDemotasks Adds constants for new task routines and updates some others. * Cleanup and bugfix com_scheduler - Removes redundant pass-by-reference - Makes closures static where possible - Fixes problematic casts - Adds some missing parenthesis - Removes unused variables - Removes unused method overrides - Miscellaneous changes Ref: joomla/joomla-cms#35143 / @Denitz * Improve consistency, extend `#__scheduler_tasks` Adds `checked_out` and `checkout_out_time` to `#__scheduler_tasks`. Also adds index for `checked_out`. Ref: joomla/joomla-cms#35143#issuecomment-911819994 / @Denitz * Remove language autoload from ScheduleRunner Ref: joomla/joomla-cms#35143#issuecomment-911819994 / @Denitz * Fix missed errant cast Fixes a leftover problematic cast in IntervalField. * Patch Task::releaseLock() The method now updates the Task properties, so they now keep in sync with db updates and can be used reliably by other code. * Add plgSystemTaskNotification This plugin is responsible for sending out email notifications for task failures and optionally also successful executions. * Add language files for plgSystemTaskNotifications * Add SQL DDL for task notification mail templates * Add auto-install SQL (plgSystemTaskNotifications) * Add language autoload for plgSysTaskNotification * Patch auto-install SQL scripts - Adds installation DDL in installation script (supports.sql) - Adds the `extension` column in update scripts. * Cleanup plg taskNotification * Fix Scheduler::fetchTasks() default behaviour Shifts the list config for the task queue to the fetchTaskRecord() method. This makes more sense as the fetchTasks() method is supposed to be a transparent API method to interact with the TasksModel rather than have an overriding default like the list.multi_ordering state variable as a default. Also changes the default ordering behaviour for sensibility and adds some comments for clarity. * Add explicit id, title filtering to TasksModel These are largely redundant to the search filter but convenient to use and when only the title should be targeted. It turned out I assumed we had this in the model and so was an outstanding bug with the Scheduler class. Probably, can use refactor later (or _should_). * Enhance ExecRuleHelper::nextExec() Adds a new option $nowBasis argument, when provided uses 'now' as the last execution. Useful when skipping executions, so we're not actually updating the last exec. * Add new Task::Status constant Adds a status code for when no (matching) task exists. * Improve comments in the TaskOption class Improves comment structure, adds some missing punctuation. * Update the Scheduler class - Renames fetchTasks() => fetchTaskRecords(). - Fixes broken id search (l190). - Updates runTask() return (now returns the Task exit code). - Adds TASK_QUEUE filters and list config as class constants so other extensions can reliably use them to get the queue behaviour (should this somehow be baked in?). - Updates return for fetchTaskRecords(), which could return a false from the model query before but now would an empty array. * Bugfix Task::skipExecution() Now actually skips the execution and advances the task to the next execution slot relative to 'now'. * Add scheduler:list console command Command to list scheduled tasks. * Add scheduler:run command Command to run scheduled tasks. Supports triggering the task queue as well as individual tasks, matching either by id or title. Right now, due some code-smell fixes and perhaps behavior adjustments. An example, perhaps the title match could support running all tasks matching a title with the --all flag. * Add autoload for new commands Adds new commands to the application loader and dependency injection class used by other core commands. * Add plugin plgTaskCheckFiles Adds plgTaskCheckFiles. This plugin includes currently a single task routine which offers the ability to check images in a directory for dimensions and resize ones that are too big! * Enhance form for image size routine Adds sensible defaults, require attributes, etc. * Update form for get_request routine Updates the fieldset so its translated right. * Update language file for com_scheduler Adds a new constant for the task_params fieldset so plugins can use it without defining their own. * Patch task form Patches the "trigger" field to make it hidden. This is temporary as it should be removed altogether soon both in the form and the db table. * Patch exit handling in Task class The handleExit() method needed the exit code to be passed while it was available already in the $snapshot class property. This also created room for error and the final handleExit() return meant that generic failures were never broadcasted through the failure event. * Patch exit handling in Task class The handleExit() method needed the exit code to be passed while it was available already in the $snapshot class property. This also created room for error and the final handleExit() return meant that generic failures were never broadcasted through the failure event. * Add support for mailing task outputs Implements support for mailing out task output attachments and in the body. Not robust but just works for a demo (needs to be redone). * Update SQL for task notification templates Adds the "task_output" param. * Add output file support in GET request routine Hastily done and to be redone but this works okay as a POC. * Patch task class Fixes the execution time record/next execution which broke somewhere along before. This is a quick patch and should be fixed as suggested by the comment. * Disable deprecated routineEndMessage [CheckFiles] * Improve code quality, consistency and style - Removes some redundant comments. - Reduces reliance on implicit PHP casts. - Refactors to a more functional approach in TaskModel::save() and calls nested within. - Uses allowed Priority::MAX as plgSystemSchedulerunner listener priority. - Refactors some abbreviated variable names. - Changes uses of the 'GMT' to 'UTC' (more correct and consistent with existing Joomla code) - Separates some variable assignments and tests for improved readability. - Starts removing imports for native global functions and objects. This should be a gradual process. - Cover up a couple copy-and-paste inconsistencies. oops! - Reverts a styling regression in old code. Ref: joomla/joomla-cms#35143 / @nibra / @HLeithner / @brianteeman. * Make SchedulerHelper abstract Makes SchedulerHelper an abstract class. This also eliminates the need for a private constructor. Refs: joomla/joomla-cms/#35143#discussion_r710998565 / joomla/joomla-cms#35143#discussion_r710999157 / @PhilETaylor / @nibra. * Add request hash protection for scheduler runner - Adds a configuration tab for the lazy scheduler in ComScheduler with configuration switches for disabling the lazy scheduler and protecting it with a request hash. - Adds configuration checks in plgSystemScheduleRunner to only run if either hash matches or it's not protected. * Bugfix scheduleRunner event subscription behaviour ScheduleRunner attached itself to events even with comScheduler disabled. This was likely because it utilised a constructor short which is useful only with the pre-4.x event-method model. This commit adds checks for the component straight to the getSubscribedEvents() method and also adds a check for the lazy scheduler switch in com_scheduler config params. Refs: @Denitz / joomla/joomla-cms#35143#issuecomment-911453500. * Add rule for manual-only task invocation Adds an execution rule for when a task should only run when manually invoked. Also renames internally the "custom" rule to "cron-expression". * Refactor and extend TaskModel, TaskTable - Updates TaskModel for changes in the Task form. - Extends and refactors TaskModel to both support to both support the new manual invocation only rule and for better behaviour with setting some fields initially in the database. - Adds a Task::bind() override to TaskTable and changes a default param to support updating fields to NULL through AdminModel::save(). * Update ExecRuleHelper - Update with changes to the Task form. - Removes imports for native global functions. * Update language constants for com_scheduler - Adds language constants for new Scheduler config fields. - Refactors some language constants for better, more uniform naming. * Cleanup Task class - Removes outdated info from handleExit() PHP-doc. - Removes imports for native global symbols. * Update com_scheduler manifest - Adds some missing fields and fixes code-style. * Apply code-style suggestions from code review - Fixes styling for some docblocks. - Fixes copyright header alignment for consistency (non-exhaustive). - Adds trailing commas for multi-line array declarations (probably non-exhaustive). - Adds explicit name-spacing for global native symbols (likely non-exhaustive). - Misc code-style improvements. Co-authored-by: Phil E. Taylor * Apply code-style suggestions from code review - Fixes styling for some docblocks. - Fixes copyright header alignment for consistency (non-exhaustive). - Adds trailing commas for multi-line array declarations (probably non-exhaustive). - Adds explicit name-spacing for global native symbols (likely non-exhaustive). - Misc code-style improvements. Co-authored-by: Phil E. Taylor * Clean up usage * Add AJAX requests script to scheduleRunner JS sets up a navigator.sendBeacon() callback to make requests to trigger a `com_ajax` backed AJAX listener offered by plgSystemScheduleRunner. * Update manifest for plgSystemScheduleRunner Adds the new media asset folder, fills in some missing fields and fixes the language field. * Add scheduleRunner listener to inject JS - Removes `shutdown_function` behavior to register the scheduleRunner. - Adds listener method to inject the trigger JS on the `onBeforeCompileHead` event. - Fixes copyright header styling. - Reduces event subscription stage check complexity. (Ref: @Denitz / joomla-projects/soc21_website-cronjob#4#commitcomment-57308547) - Adds `com_scheduler` component config as class property. - Change scheduleRunner's subscription event to `onAjaxRunScheduler`. * Add scheduleRunner doc and missing checks - Adds descriptive doc in the class docblock. - Adds missing checks to the runScheduler() method. * Fix `created_by` auto set on task save - TaskTable::save() now sets the `created_by` field correctly. - Adds and fixes existing checks for field auto-set. * Cleanup and enhance TaskTable docs - Fix copyright style. - Enhance doc blocks. - Cleanup. * Fix plugin manifests - Lowercase . - Adds missing fields and missing nodes. - Fixes XML styling (tasknotification.xml) Ref: @bembelimen * Revert changes to the modules/select template Reverts file to 4.1-dev state. * Add standard routine handler `standardRoutineHandler()` can take care of initialising the routine, calling the associated callable and ending the routine all without any logic in the plugin class if each routine has a corresponding callable. * Fix copyright formatting * Update Task\Status Adds status code for invalid return and improves member doc blocks. * Refactor TaskPluginTrait - Renames methods: - `taskLog()` => `logTask()` - `taskEnd()` => `endRoutine()` - `taskStart()` => `initRoutine()` - Upgrade `enhanceTaskItemForm()` to a complete event handler. For plugins targeting only the task form, this method can now be mapped straight to the `onContentPrepareForm` event. - Code style improvements and fixes for global namespace qualification on some `\Exception` references. - More explicit checks on variables. - `Event` type arguments are now `EventInterface` so other implementations stay compatible. - Removes deprecated logging nd `$log` param from `endRoutine()`. * Improve TaskPluginTrait documentation Adds and improves on the information in the doc blocks with context, usage hints and related information. Also updates some parameter information and fixes the tag order (`@since` <=> `@throws`). * Implement suggestions on TaskPluginTrait - Refactors `initRoutine()` => `startRoutine()` - Return true for irrelevant enhanceTaskItemForm() contexts. - Path checks for routine forms. - Allow only class methods with `standardRoutineHandler()` - Adds signature checks on standard routine methods. - Adds validation on routine return codes against `Task\Status`. * Update TaskPluginTrait::standardRoutineHandler() Now enforces a single required parameter of type `ExecuteTaskEvent` (from `EventInterface`). * Update task plugins - Remove methods now offered by TaskPluginTrait. - The 'call' TASKS_MAP param is now 'method'. - Update event subscriptions. - Update docs. - Improve code-style and copyright blocks. * Ignore user aborts on scheduler trigger Adds attempt to set the INI `ignore_user_abort` to true. Also enhances some docs. * Update TaskPluginTrait doc Updates signature reference for standard routine methods. * Rename status code Renames `Task\Status::NO_TIME` to `Task\Status::RUNNING`. * Add config options for scheduleRunner Adds config option for the client side request intervals for the scheduler trigger. Also fixes some showon attributes in the com_scheduler config form. * Add removal of time limit on task runs Add test task permission Add webcron, test task, lazycron entry points * Fix broken interval run when setting custom value * Set correct lazyCron URL and run it at the beginning Prevent running LazyCron when not in html view or if lazyCron is not enabled Implement hash check for webCron * Rebuild JS Implement test runner * Finish test cron * Fix CS * Revert SQL query * Add webcron to scheduler config Updates the `com_scheduler` config.xml with the webcron fieldset. * Add webcron key autogen and more to Schedulerunner - Schedulerunner auto-generates the webcron key and does some form usability enhancements on the config form (much like the user API token plugin). * Add custom field for webcron link - Adds new field 'WebcronLinkField', this field is not really needed except for to support the custom layout location. - Allows for the webcron link to be copies on click, much like the user API token field. - Adds field JS to the `com_scheduler` media source provider. * Update com_scheduler language file Adds new strings for the config form and updates some refs. * Fix regression in Scheduler class - Fix regression due to newly protected status of Task::snapshot + accessor. - Update some doc-blocks and related formatting. - Optimise some imports. - Minor miscellaneous changes. * Fix test run JS Fixes apparent JS parse error on non-zero task exits. * Improve form manipulation code - Check for `com_scheduler` in the subject table for table event. - Minor code simplification. * Clean up redundant field from Tasks table - Remove redundant `trigger` field from `#__scheduler_tasks`. - From install and update SQL. - Task item form. - Language file. - Fix regression in GenericDataException ref. - Minor styling/doc fixes and upgrades. * Remove global task configuration config - Removes task configuration config from `plgSystemTasknotifications` plugin config. - ! Does not update any usages. * Add task notification config as injected form - Task notification config is now injected into the task item form. - ! Usages are not updated. * Improve plgSystemTaskNotification code style - Updates docs. - Improves docblocks, general code style for compliance with unenforced Joomla! style guide. * Fix params display in task view * Add fieldset labels for task form - Adds missing fieldset labels. - Adds some comments and fixes marginally code-style. * Update task notification logic - Replaces checks with updated task item configuration. - Improves logging (additional checks). - Makes file attachment handling safer. - Fixes/updates code-style. * Update plgSystemTaskNotification language file - Adds new constants for logging. - Fixes ordering. * Fix Task driver behavior The run() method now updates the object state instead of leaving it to releaseLock(). The class continues to appear a clunky build :D. * Fix TasksRunCommand regression * Fix scheduleRunner default behaviour - Uses again sensible defaults (enabled lazy cron). - Check webcron.enabled. - Some useful logging. - Improved docblocks. * Update scheduleRunner language files Adds new constant for a logging string. * Update and fix code-style - Fix license header formatting. - Fix doc phpdoc tag ordering. - Cleanup and marginally improve doc-blocks. - Fix unqualified global refs in doc-blocks. - Misc formatting fixes/improvements. - Remove phpcs ignores. * Revert drone * Update com_scheduler language file - Missing constant. - Ordering. * Apply suggestions from code review Code-style and language improvements. Co-authored-by: Brian Teeman * Apply suggestions from code review Code-style and language improvements. Co-authored-by: Brian Teeman * Update language constants for plgSysScheduleRunner Updates plugin description string. * Bugfix Task::run() For some reason, the UNIX timestamp with microseconds `microtime()` broke the DateTime breaking down the task usual scheduling. This commit introduces an int cast for the timestamp which makes things work as expected. * Fix webcron url & description Fixes webcron url and description as exposed in the Scheduler component config. Ref: joomla-projects/soc21_website-cronjob#37 * Update joomla.asset.json * Improve the locking mechanism * Add table locking when locking task * CS * Simplify null date check * Add column quote * Extend TaskModel's getter method ! Fails in MySQL 5.6 with "Table was not locked with LOCK TABLES" - Adds options array to customise behavior. - Adds static option resolver for proxies to the getter. - Wrap queue behavior in a sub-query for compatibility. ... * Update Scheduler::getTask() and runTask() Updates Scheduler::getTask() and Scheduler::runTask() to match the updated TaskModel::getTask() method. * Fix typos and improve static analysis support Fixes some typos and adds some doc IDE typehint for improved static analysis to the Scheduler class. * Update/fix Schedulerunner Updates primarily the Schedulerunner::runTestCron() method. Other methods might need to be updated still. * Update Task class Minor updates, comments for the future. * Update Task::releaseLock() Fixes compatibility with PostgreSQL (tested on 11). Removes table specificity from columns (this confuses Postgres for some reason). * Update TaskModel::getTask() Fixes task queue behavior (now only applies when an ID is not passed). * Improve TaskModel::getTask() mysql compat ! Still fails because of table locks - Adds pseudo-source for sub-query. https://stackoverflow.com/q/44970574 - Adds reference comments to try to make the SQL gymnastics make sense. * Add unlock function * Fix language strings for the schedulerunner plugin * Update description and remove plugin files * Implement Scheduler CLI state changer * Update TasksStateCommand.php * Fix list state filter in scheduler * Fix MySQL lock violations Fixes lock violation by getting rid of sub-queries. * Fix label a11y [#42] * Quote 'id' Co-authored-by: Benjamin Trenkle * update publish/unpublish => enable/disable * Add check for checked out tasks Adds check for checked out state of tasks, so it doesn't silently fail anymore if such a task is attempted to be updated. * Improve static analysis support. Adds type hints for classes so static analysis and IDE autocomplete works better. * Update exit codes and fix typos - Updates failure exit codes so all are now distinct. - Fixes typos and other minor things. - Uses syntactic string composition instead of concatenation. * Cleanup Removes debug string eval * Fix SQL scripts Reduces length of `#__scheduler_tasks.type` to evade exceeding maximum length of indices and considering 128 chars is sufficient for a UUID and for context aware string currently used for routine IDs. Refs: @HLeithner * Apply Code-style and doc-block improvements from code review Co-authored-by: Phil E. Taylor * Replace Registry inheritance with composition Task no longer extends Registry, but uses a new `taskRegistry` property to store a Registry object. This compositional pattern makes the code stink less. * Refactor Task::isSuccess() No longer takes care of dispatching exit events, which is done by Task::dispatchExitEvent() now. Return API is unchanged. * Apply Code-style and doc-block improvements from code review Co-authored-by: Phil E. Taylor * Update doc-blocks for the Task driver Adds doc-block for Task::EVENTS_MAP and updates the constructor's. * Update composer.lock * Update composer.lock * Make Task property getter public [bug] Makes the task property getter public. Protected visibility here breaks things. * Improve task routine exception handling - The driver now allows for and preserves exceptions from routines, throwing them again after wrapping things up and releasing the task lock. - Removes the nonNull assertion for the routine snapshot (this already did not work). * Update plgScheduleRunner Webcron now throws the exception from the task, if it exists. This is caught by com_ajax and the exception message included in the output. * Update Task::Status Renames the KO_RUN status to KNOCKOUT, adds description in docblock. * Fix exception handling in TaskPluginTrait Catches only ReflectionExceptions now, leaving out others for the Task driver to access. * Cleanup assert exception refs Cleans up imports and docblocks. * Improve ScheduleRunner documentation. * Improve code style Majorly fixes from the latest updates in Joomla's PHP CS Fixer config, * Fix demo task form * Update language file for `com_scheduler` * Use output buffering for lazyCron to avoid dying * Merge update SQL * Update plgTaskCheckfiles - Adds support for WEBP. - Better format support while resizing. - Minor refactoring and more informative logging. * Update scheduleRunner docs * Improve code quality for task unlocking - Improved static analysis support and type-hinting. - Fix usage of undefined variables. - Add/modify comments and variable names. - Update event names and interface for relevance. - Improve docs in doc-blocks. - Fix code-style. * Fix com_scheduler, task plugin code-style Fixes code-style with the updated php-cs-fixer config. * Fix WebcronLinkField::getLayoutPaths() The union operator... did not do the right job. * Merge more update SQL Missed one! * Rename update SQL Updates date signatures of update files to a fresher date. * Make plgTaskCheckfiles logging translatable Adds language constants for logging strings. Refs: @PhilETaylor * Update plgTaskRequests - Add logging on exception. - Improve variable naming/file write safety. - Update language file. Refs: @PhilETaylor * Improve exception handling plgTaskCheckfiles - Improves handling exceptions and failures. - Adds more informative logging. Refs: @PhilETaylor * Update code-style and language - Fixes some misc code-style, reverses regression. - Adds language for some log. * Fix installation and update SQL scripts * Fix update SQL for PostgreSQL for the database checker * Add new core extensions to the extensions helper * Add auto-install SQL for plg_task_checkfiles - Adds auto-install SQL for both installation and update. - Also adds relevant entry to ExtensionHelper. Refs: joomla-projects/soc21_website-cronjob#49 / @richard67 * Add DB column for cli exclusive tasks - Adds new DB column `#__scheduler_tasks.cli_exclusive`, updating both install and update scripts. Also adds relevant index. * Add support for CLI exclusive tasks - Updates TaskModel::getTask() with an option to include/exclude CLI exclusives. - Adds runtime check to Scheduler::runTask() so it now includes CLI exclusives only in a CLI context. * Add comments, fix doc signatures + style - Add comments to schedulerunner. - Fix some type signatures in doc-blocks. - Other minor style fixes. * Fix get request response body Shouldn't throw an undefined constant again. * Add `Scheduler::run()` option for concurrency - Scheduler::run() now uses an options array backed by the Symfony OptionsResolver. - Offers a new option that allows for execution concurrency (this was specifically required by the CLI command). * Update plgSystemSchedulerunner Updates Schedulerunner for changes in Scheduler::run() * Make `scheduler:run` safer - Null safety against Tasks. - More informative stderr log on lock acquisition failures. - Now allows concurrency so Web doesn't block out CLI anymore (thanks to the updates in Scheduler::run()). Refs: joomla/joomla-cms#35143#issuecomment-976391914 / @alikon / @bembelimen * Refactor `scheduler:state` - Better (expectedly safer) flow of logic with attempts to get the logical/enumerated state from command mode and interactive modes. - Terminate on invalid command invocation. - Fix exit success message (did not show action). - Better formatting of description, etc. - Code-style fixes. Refs: joomla/joomla-cms#35143#issuecomment-977608144 / @alikon / @bembelimen * Improve `scheduler:run` description style Minor improvement to `scheduler:run` description for consistency. * Add utilities, bugfix Task class - Adds state enumerations and map. - Adds basic static state and id validators. - Fix use of un-imported Text class, $->getMessage() call. * Improve `scheduler:state` command - Better validation and improved consistency. - Removes now redundant state enumerations, mapping and validators which the Task class offers. * Add page reload after testing * Code sniffer fixes * Rename param for Scheduler::fetchTaskRecord() Co-authored-by: Phil E. Taylor Co-authored-by: Benjamin Trenkle Co-authored-by: Brian Teeman Co-authored-by: Benjamin Trenkle Co-authored-by: Richard Fath --- .../components/com_actionlogs/config.xml | 2 +- .../sql/updates/mysql/4.1.0-2021-11-20.sql | 63 ++ .../updates/postgresql/4.1.0-2021-11-20.sql | 64 ++ .../components/com_menus/presets/system.xml | 8 + .../components/com_scheduler/access.xml | 19 + .../components/com_scheduler/config.xml | 123 +++ .../com_scheduler/forms/filter_tasks.xml | 74 ++ .../components/com_scheduler/forms/task.xml | 275 ++++++ .../layouts/form/field/webcron_link.php | 56 ++ .../components/com_scheduler/scheduler.xml | 30 + .../com_scheduler/services/provider.php | 64 ++ .../src/Controller/DisplayController.php | 108 +++ .../src/Controller/TaskController.php | 118 +++ .../src/Controller/TasksController.php | 111 +++ .../src/Event/ExecuteTaskEvent.php | 97 ++ .../src/Extension/SchedulerComponent.php | 47 + .../com_scheduler/src/Field/CronField.php | 200 +++++ .../src/Field/ExecutionRuleField.php | 46 + .../com_scheduler/src/Field/IntervalField.php | 98 +++ .../src/Field/TaskStateField.php | 44 + .../com_scheduler/src/Field/TaskTypeField.php | 71 ++ .../src/Field/WebcronLinkField.php | 52 ++ .../src/Helper/ExecRuleHelper.php | 134 +++ .../src/Helper/SchedulerHelper.php | 69 ++ .../com_scheduler/src/Model/SelectModel.php | 63 ++ .../com_scheduler/src/Model/TaskModel.php | 832 ++++++++++++++++++ .../com_scheduler/src/Model/TasksModel.php | 445 ++++++++++ .../src/Rule/ExecutionRulesRule.php | 91 ++ .../com_scheduler/src/Scheduler/Scheduler.php | 361 ++++++++ .../com_scheduler/src/Table/TaskTable.php | 305 +++++++ .../com_scheduler/src/Task/Status.php | 97 ++ .../com_scheduler/src/Task/Task.php | 575 ++++++++++++ .../com_scheduler/src/Task/TaskOption.php | 91 ++ .../com_scheduler/src/Task/TaskOptions.php | 75 ++ .../src/Traits/TaskPluginTrait.php | 360 ++++++++ .../src/View/Select/HtmlView.php | 148 ++++ .../com_scheduler/src/View/Task/HtmlView.php | 150 ++++ .../com_scheduler/src/View/Tasks/HtmlView.php | 195 ++++ .../com_scheduler/tmpl/select/default.php | 90 ++ .../com_scheduler/tmpl/select/modal.php | 36 + .../com_scheduler/tmpl/task/edit.php | 183 ++++ .../com_scheduler/tmpl/tasks/default.php | 260 ++++++ .../com_scheduler/tmpl/tasks/default.xml | 8 + .../com_scheduler/tmpl/tasks/empty_state.php | 27 + .../language/en-GB/com_scheduler.ini | 152 ++++ .../language/en-GB/com_scheduler.sys.ini | 14 + administrator/language/en-GB/mod_menu.ini | 1 + .../language/en-GB/plg_actionlog_joomla.ini | 3 +- .../en-GB/plg_system_schedulerunner.ini | 6 + .../en-GB/plg_system_schedulerunner.sys.ini | 6 + .../css/admin-view-select-task.css | 58 ++ .../com_scheduler/css/admin-view-task.css | 24 + .../com_scheduler/joomla.asset.json | 88 ++ .../js/admin-view-run-test-task.es6.js | 92 ++ .../js/admin-view-select-task-search.es6.js | 107 +++ .../com_scheduler/js/scheduler-config.es6.js | 51 ++ .../joomla.asset.json | 34 + .../js/run-schedule.es6.js | 36 + composer.json | 3 +- composer.lock | 179 ++-- installation/sql/mysql/base.sql | 14 +- installation/sql/mysql/extensions.sql | 48 +- installation/sql/mysql/supports.sql | 6 +- installation/sql/postgresql/base.sql | 14 +- installation/sql/postgresql/extensions.sql | 50 +- installation/sql/postgresql/supports.sql | 6 +- libraries/src/Console/TasksListCommand.php | 140 +++ libraries/src/Console/TasksRunCommand.php | 155 ++++ libraries/src/Console/TasksStateCommand.php | 201 +++++ libraries/src/Extension/ExtensionHelper.php | 9 + .../src/Service/Provider/Application.php | 42 +- libraries/src/Service/Provider/Console.php | 30 +- .../system/schedulerunner/schedulerunner.php | 389 ++++++++ .../system/schedulerunner/schedulerunner.xml | 23 + .../forms/task_notification.xml | 49 ++ .../en-GB/plg_system_tasknotification.ini | 16 + .../en-GB/plg_system_tasknotification.sys.ini | 2 + .../tasknotification/tasknotification.php | 339 +++++++ .../tasknotification/tasknotification.xml | 20 + plugins/task/checkfiles/checkfiles.php | 139 +++ plugins/task/checkfiles/checkfiles.xml | 21 + plugins/task/checkfiles/forms/image_size.xml | 34 + .../language/en-GB/plg_task_checkfiles.ini | 16 + .../en-GB/plg_task_checkfiles.sys.ini | 7 + plugins/task/demotasks/demotasks.php | 172 ++++ plugins/task/demotasks/demotasks.xml | 21 + plugins/task/demotasks/forms/testTaskForm.xml | 18 + .../language/en-GB/plg_task_demotasks.ini | 15 + .../language/en-GB/plg_task_demotasks.sys.ini | 7 + plugins/task/requests/forms/get_requests.xml | 53 ++ .../language/en-GB/plg_task_requests.ini | 20 + .../language/en-GB/plg_task_requests.sys.ini | 7 + plugins/task/requests/requests.php | 141 +++ plugins/task/requests/requests.xml | 21 + .../language/en-GB/plg_task_sitestatus.ini | 20 + .../en-GB/plg_task_sitestatus.sys.ini | 7 + plugins/task/sitestatus/sitestatus.php | 172 ++++ plugins/task/sitestatus/sitestatus.xml | 21 + 98 files changed, 9591 insertions(+), 93 deletions(-) create mode 100644 administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-11-20.sql create mode 100644 administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-11-20.sql create mode 100644 administrator/components/com_scheduler/access.xml create mode 100644 administrator/components/com_scheduler/config.xml create mode 100644 administrator/components/com_scheduler/forms/filter_tasks.xml create mode 100644 administrator/components/com_scheduler/forms/task.xml create mode 100644 administrator/components/com_scheduler/layouts/form/field/webcron_link.php create mode 100644 administrator/components/com_scheduler/scheduler.xml create mode 100644 administrator/components/com_scheduler/services/provider.php create mode 100644 administrator/components/com_scheduler/src/Controller/DisplayController.php create mode 100644 administrator/components/com_scheduler/src/Controller/TaskController.php create mode 100644 administrator/components/com_scheduler/src/Controller/TasksController.php create mode 100644 administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php create mode 100644 administrator/components/com_scheduler/src/Extension/SchedulerComponent.php create mode 100644 administrator/components/com_scheduler/src/Field/CronField.php create mode 100644 administrator/components/com_scheduler/src/Field/ExecutionRuleField.php create mode 100644 administrator/components/com_scheduler/src/Field/IntervalField.php create mode 100644 administrator/components/com_scheduler/src/Field/TaskStateField.php create mode 100644 administrator/components/com_scheduler/src/Field/TaskTypeField.php create mode 100644 administrator/components/com_scheduler/src/Field/WebcronLinkField.php create mode 100644 administrator/components/com_scheduler/src/Helper/ExecRuleHelper.php create mode 100644 administrator/components/com_scheduler/src/Helper/SchedulerHelper.php create mode 100644 administrator/components/com_scheduler/src/Model/SelectModel.php create mode 100644 administrator/components/com_scheduler/src/Model/TaskModel.php create mode 100644 administrator/components/com_scheduler/src/Model/TasksModel.php create mode 100644 administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php create mode 100644 administrator/components/com_scheduler/src/Scheduler/Scheduler.php create mode 100644 administrator/components/com_scheduler/src/Table/TaskTable.php create mode 100644 administrator/components/com_scheduler/src/Task/Status.php create mode 100644 administrator/components/com_scheduler/src/Task/Task.php create mode 100644 administrator/components/com_scheduler/src/Task/TaskOption.php create mode 100644 administrator/components/com_scheduler/src/Task/TaskOptions.php create mode 100644 administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php create mode 100644 administrator/components/com_scheduler/src/View/Select/HtmlView.php create mode 100644 administrator/components/com_scheduler/src/View/Task/HtmlView.php create mode 100644 administrator/components/com_scheduler/src/View/Tasks/HtmlView.php create mode 100644 administrator/components/com_scheduler/tmpl/select/default.php create mode 100644 administrator/components/com_scheduler/tmpl/select/modal.php create mode 100644 administrator/components/com_scheduler/tmpl/task/edit.php create mode 100644 administrator/components/com_scheduler/tmpl/tasks/default.php create mode 100644 administrator/components/com_scheduler/tmpl/tasks/default.xml create mode 100644 administrator/components/com_scheduler/tmpl/tasks/empty_state.php create mode 100644 administrator/language/en-GB/com_scheduler.ini create mode 100644 administrator/language/en-GB/com_scheduler.sys.ini create mode 100644 administrator/language/en-GB/plg_system_schedulerunner.ini create mode 100644 administrator/language/en-GB/plg_system_schedulerunner.sys.ini create mode 100644 build/media_source/com_scheduler/css/admin-view-select-task.css create mode 100644 build/media_source/com_scheduler/css/admin-view-task.css create mode 100644 build/media_source/com_scheduler/joomla.asset.json create mode 100644 build/media_source/com_scheduler/js/admin-view-run-test-task.es6.js create mode 100644 build/media_source/com_scheduler/js/admin-view-select-task-search.es6.js create mode 100644 build/media_source/com_scheduler/js/scheduler-config.es6.js create mode 100644 build/media_source/plg_system_schedulerunner/joomla.asset.json create mode 100644 build/media_source/plg_system_schedulerunner/js/run-schedule.es6.js create mode 100644 libraries/src/Console/TasksListCommand.php create mode 100644 libraries/src/Console/TasksRunCommand.php create mode 100644 libraries/src/Console/TasksStateCommand.php create mode 100644 plugins/system/schedulerunner/schedulerunner.php create mode 100644 plugins/system/schedulerunner/schedulerunner.xml create mode 100644 plugins/system/tasknotification/forms/task_notification.xml create mode 100644 plugins/system/tasknotification/language/en-GB/plg_system_tasknotification.ini create mode 100644 plugins/system/tasknotification/language/en-GB/plg_system_tasknotification.sys.ini create mode 100644 plugins/system/tasknotification/tasknotification.php create mode 100644 plugins/system/tasknotification/tasknotification.xml create mode 100644 plugins/task/checkfiles/checkfiles.php create mode 100644 plugins/task/checkfiles/checkfiles.xml create mode 100644 plugins/task/checkfiles/forms/image_size.xml create mode 100644 plugins/task/checkfiles/language/en-GB/plg_task_checkfiles.ini create mode 100644 plugins/task/checkfiles/language/en-GB/plg_task_checkfiles.sys.ini create mode 100644 plugins/task/demotasks/demotasks.php create mode 100644 plugins/task/demotasks/demotasks.xml create mode 100644 plugins/task/demotasks/forms/testTaskForm.xml create mode 100644 plugins/task/demotasks/language/en-GB/plg_task_demotasks.ini create mode 100644 plugins/task/demotasks/language/en-GB/plg_task_demotasks.sys.ini create mode 100644 plugins/task/requests/forms/get_requests.xml create mode 100644 plugins/task/requests/language/en-GB/plg_task_requests.ini create mode 100644 plugins/task/requests/language/en-GB/plg_task_requests.sys.ini create mode 100644 plugins/task/requests/requests.php create mode 100644 plugins/task/requests/requests.xml create mode 100644 plugins/task/sitestatus/language/en-GB/plg_task_sitestatus.ini create mode 100644 plugins/task/sitestatus/language/en-GB/plg_task_sitestatus.sys.ini create mode 100644 plugins/task/sitestatus/sitestatus.php create mode 100644 plugins/task/sitestatus/sitestatus.xml diff --git a/administrator/components/com_actionlogs/config.xml b/administrator/components/com_actionlogs/config.xml index 03bf106f1c1c9..bfb6c100dca6d 100644 --- a/administrator/components/com_actionlogs/config.xml +++ b/administrator/components/com_actionlogs/config.xml @@ -28,7 +28,7 @@ type="logtype" label="COM_ACTIONLOGS_LOG_EXTENSIONS_LABEL" multiple="true" - default="com_banners,com_cache,com_categories,com_checkin,com_config,com_contact,com_content,com_installer,com_media,com_menus,com_messages,com_modules,com_newsfeeds,com_plugins,com_redirect,com_tags,com_templates,com_users" + default="com_banners,com_cache,com_categories,com_checkin,com_config,com_contact,com_content,com_installer,com_media,com_menus,com_messages,com_modules,com_newsfeeds,com_plugins,com_redirect,com_scheduler,com_tags,com_templates,com_users" /> + + + +
+ + + + + + + + +
+
+ + + + +
+
diff --git a/administrator/components/com_scheduler/config.xml b/administrator/components/com_scheduler/config.xml new file mode 100644 index 0000000000000..05bac35af1db5 --- /dev/null +++ b/administrator/components/com_scheduler/config.xml @@ -0,0 +1,123 @@ + + +
+ +
+
+ + + + + + + +
+
+ + + + + + + +
+
+ +
+
diff --git a/administrator/components/com_scheduler/forms/filter_tasks.xml b/administrator/components/com_scheduler/forms/filter_tasks.xml new file mode 100644 index 0000000000000..c374746c76a07 --- /dev/null +++ b/administrator/components/com_scheduler/forms/filter_tasks.xml @@ -0,0 +1,74 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/administrator/components/com_scheduler/forms/task.xml b/administrator/components/com_scheduler/forms/task.xml new file mode 100644 index 0000000000000..1121ec6483904 --- /dev/null +++ b/administrator/components/com_scheduler/forms/task.xml @@ -0,0 +1,275 @@ + +
+ + + +
+ + +
+ +
+ + + + + +
+ +
+ + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ +
+ + + + + + + +
+
+ +
+ + + + + +
+ + +
+ + + + + +
+
+
diff --git a/administrator/components/com_scheduler/layouts/form/field/webcron_link.php b/administrator/components/com_scheduler/layouts/form/field/webcron_link.php new file mode 100644 index 0000000000000..82535e109201f --- /dev/null +++ b/administrator/components/com_scheduler/layouts/form/field/webcron_link.php @@ -0,0 +1,56 @@ + + * @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'); +?> + +
+ + +
+ diff --git a/administrator/components/com_scheduler/scheduler.xml b/administrator/components/com_scheduler/scheduler.xml new file mode 100644 index 0000000000000..5945633f1108d --- /dev/null +++ b/administrator/components/com_scheduler/scheduler.xml @@ -0,0 +1,30 @@ + + + COM_SCHEDULER + Joomla! Project + July 2021 + (C) 2021 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 4.1.0 + COM_SCHEDULER_XML_DESCRIPTION + Joomla\Component\Scheduler + + js + css + + + access.xml + config.xml + scheduler.xml + forms + services + src + tmpl + + language/en-GB/com_scheduler.ini + language/en-GB/com_scheduler.sys.ini + + + diff --git a/administrator/components/com_scheduler/services/provider.php b/administrator/components/com_scheduler/services/provider.php new file mode 100644 index 0000000000000..dfb7f317a679e --- /dev/null +++ b/administrator/components/com_scheduler/services/provider.php @@ -0,0 +1,64 @@ + + * @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 __DEPLOY_VERSION__ + */ +return new class implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + 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; + } + ); + } +}; diff --git a/administrator/components/com_scheduler/src/Controller/DisplayController.php b/administrator/components/com_scheduler/src/Controller/DisplayController.php new file mode 100644 index 0000000000000..839f70c34f1d0 --- /dev/null +++ b/administrator/components/com_scheduler/src/Controller/DisplayController.php @@ -0,0 +1,108 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Scheduler\Administrator\Controller; + +// Restrict direct access +\defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Router\Route; + +/** + * Default controller for com_scheduler. + * + * @since __DEPLOY_VERSION__ + */ +class DisplayController extends BaseController +{ + /** + * @var string + * @since __DEPLOY_VERSION__ + */ + 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, for valid values see + * {@link InputFilter::clean()}. + * + * @return BaseController|boolean Returns either a BaseController object to support chaining, or false on failure + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function display($cachable = false, $urlparams = array()) + { + $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 __DEPLOY_VERSION__ + */ + 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; + } + // For existing item, entry is invalid if TaskController has not granted access + elseif (!$inEditList) + { + if (!\count($this->app->getMessageQueue())) + { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $isValid = false; + } + break; + default: + break; + } + + return $isValid; + } +} diff --git a/administrator/components/com_scheduler/src/Controller/TaskController.php b/administrator/components/com_scheduler/src/Controller/TaskController.php new file mode 100644 index 0000000000000..e8206ec9692c0 --- /dev/null +++ b/administrator/components/com_scheduler/src/Controller/TaskController.php @@ -0,0 +1,118 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Scheduler\Administrator\Controller; + +// Restrict direct access +\defined('_JEXEC') or die; + +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; + +/** + * MVC Controller for the item configuration page (TaskView). + * + * @since __DEPLOY_VERSION__ + */ +class TaskController extends FormController +{ + /** + * Add a new record + * + * @return boolean + * @since __DEPLOY_VERSION__ + * @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 __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + protected function allowEdit($data = array(), $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); + + } +} diff --git a/administrator/components/com_scheduler/src/Controller/TasksController.php b/administrator/components/com_scheduler/src/Controller/TasksController.php new file mode 100644 index 0000000000000..5a4039f2e3372 --- /dev/null +++ b/administrator/components/com_scheduler/src/Controller/TasksController.php @@ -0,0 +1,111 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Scheduler\Administrator\Controller; + +// Restrict direct access +\defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +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; + +/** + * MVC Controller for TasksView. + * + * @since __DEPLOY_VERSION__ + */ +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 __DEPLOY_VERSION__ + */ + 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 __DEPLOY__VERSION__ + */ + public function unlock(): void + { + // Check for request forgeries + $this->checkToken(); + + /** @var integer[] $cid Items to publish (from request parameters). */ + $cid = $this->input->get('cid', [], 'array'); + + if (empty($cid)) + { + $this->app->getLogger() + ->warning(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), array('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) + { + Factory::getApplication() + ->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 + ) + ); + } +} diff --git a/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php b/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php new file mode 100644 index 0000000000000..9a1635410cf27 --- /dev/null +++ b/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php @@ -0,0 +1,97 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Scheduler\Administrator\Event; + +// Restrict direct access +\defined('_JEXEC') or die; + +use Joomla\CMS\Event\AbstractEvent; +use Joomla\Component\Scheduler\Administrator\Task\Task; + +/** + * Event class for onExecuteTask event. + * + * @since __DEPLOY_VERSION__ + */ +class ExecuteTaskEvent extends AbstractEvent +{ + /** + * Constructor. + * + * @param string $name The event name. + * @param array $arguments The event arguments. + * + * @since __DEPLOY_VERSION__ + * @throws \BadMethodCallException + */ + public function __construct($name, array $arguments = array()) + { + 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 __DEPLOY_VERSION__ + */ + public function setResult(array $snapshot = []): void + { + $this->arguments['resultSnapshot'] = $snapshot; + + if (!empty($snapshot)) + { + $this->stopPropagation(); + } + } + + /** + * @return integer The task's taskId. + * + * @since __DEPLOY_VERSION__ + */ + public function getTaskId(): int + { + return $this->arguments['subject']->get('id'); + } + + /** + * @return string The task's 'type'. + * + * @since __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + public function getResultSnapshot(): array + { + return $this->arguments['resultSnapshot'] ?? []; + } +} diff --git a/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php b/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php new file mode 100644 index 0000000000000..64f03f1f53873 --- /dev/null +++ b/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php @@ -0,0 +1,47 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Scheduler\Administrator\Extension; + +// Restrict direct access +\defined('_JEXEC') or die; + +use Joomla\CMS\Extension\BootableExtensionInterface; +use Joomla\CMS\Extension\MVCComponent; +use Joomla\CMS\HTML\HTMLRegistryAwareTrait; +use Psr\Container\ContainerInterface; + +/** + * Component class for com_scheduler. + * + * @since __DEPLOY_VERSION__ + * @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 __DEPLOY_VERSION__ + */ + public function boot(ContainerInterface $container): void + { + // Pass + } +} diff --git a/administrator/components/com_scheduler/src/Field/CronField.php b/administrator/components/com_scheduler/src/Field/CronField.php new file mode 100644 index 0000000000000..8410b9695f026 --- /dev/null +++ b/administrator/components/com_scheduler/src/Field/CronField.php @@ -0,0 +1,200 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Scheduler\Administrator\Field; + +// Restrict direct access +\defined('_JEXEC') or die; + +use Joomla\CMS\Form\Field\ListField; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; + +/** + * Multi-select form field, supporting inputs of: + * minutes, hours, days of week, days of month and months. + * + * @since __DEPLOY_VERSION__ + */ +class CronField extends ListField +{ + /** + * The subtypes supported by this field type. + * + * @var string[] + * + * @since __DEPLOY_VERSION__ + */ + private const SUBTYPES = [ + 'minutes', + 'hours', + 'days_month', + 'months', + 'days_week', + ]; + + /** + * Count of predefined options for each subtype + * + * @var int[][] + * + * @since __DEPLOY_VERSION__ + */ + private const OPTIONS_RANGE = [ + 'minutes' => [0, 59], + 'hours' => [0, 23], + 'days_week' => [0, 6], + '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 __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + protected $type = 'cronIntervals'; + + /** + * The subtype of the CronIntervals field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private $subtype; + + /** + * If true, field options will include a wildcard + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + private $wildcard; + + /** + * If true, field will only have numeric labels (for days_week and months) + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + private $onlyNumericLabels; + + /** + * Override the parent method to set deal with subtypes. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` 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 __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + 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; + } +} diff --git a/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php b/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php new file mode 100644 index 0000000000000..2b11cdc6ad8d3 --- /dev/null +++ b/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php @@ -0,0 +1,46 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Scheduler\Administrator\Field; + +// Restrict direct access +\defined('_JEXEC') or die; + +use Joomla\CMS\Form\Field\PredefinedlistField; + +/** + * A select list containing valid Cron interval types. + * + * @since __DEPLOY_VERSION__ + */ +class ExecutionRuleField extends PredefinedlistField +{ + /** + * The form field type. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $type = 'ExecutionRule'; + + /** + * Available execution rules. + * + * @var string[] + * @since __DEPLOY_VERSION__ + */ + 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', + ]; +} diff --git a/administrator/components/com_scheduler/src/Field/IntervalField.php b/administrator/components/com_scheduler/src/Field/IntervalField.php new file mode 100644 index 0000000000000..b5038cfacf2d6 --- /dev/null +++ b/administrator/components/com_scheduler/src/Field/IntervalField.php @@ -0,0 +1,98 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Scheduler\Administrator\Field; + +// Restrict direct access +\defined('_JEXEC') or die; + +use Joomla\CMS\Form\Field\NumberField; +use Joomla\CMS\Form\FormField; + +/** + * Select style field for interval(s) in minutes, hours, days and months. + * + * @since __DEPLOY_VERSION__ + */ +class IntervalField extends NumberField +{ + /** + * The form field type. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $type = 'Intervals'; + + /** + * The subtypes supported by this field type => [minVal, maxVal] + * + * @var string[] + * @since __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + protected $max; + + /** + * The allowable minimum value of the field. + * + * @var float + * @since __DEPLOY_VERSION__ + */ + protected $min; + + /** + * The step by which value of the field increased or decreased. + * + * @var float + * @since __DEPLOY_VERSION__ + */ + protected $step = 1; + + /** + * Override the parent method to set deal with subtypes. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` 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 __DEPLOY_VERSION__ + */ + 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; + } +} diff --git a/administrator/components/com_scheduler/src/Field/TaskStateField.php b/administrator/components/com_scheduler/src/Field/TaskStateField.php new file mode 100644 index 0000000000000..b800e629c5068 --- /dev/null +++ b/administrator/components/com_scheduler/src/Field/TaskStateField.php @@ -0,0 +1,44 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Scheduler\Administrator\Field; + +// Restrict direct access +\defined('_JEXEC') or die; + +use Joomla\CMS\Form\Field\PredefinedlistField; + +/** + * A predefined list field with all possible states for a com_scheduler entry. + * + * @since __DEPLOY_VERSION__ + */ +class TaskStateField extends PredefinedlistField +{ + /** + * The form field type. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + public $type = 'taskState'; + + /** + * Available states + * + * @var string[] + * @since __DEPLOY_VERSION__ + */ + protected $predefinedOptions = [ + -2 => 'JTRASHED', + 0 => 'JDISABLED', + 1 => 'JENABLED', + '*' => 'JALL', + ]; +} diff --git a/administrator/components/com_scheduler/src/Field/TaskTypeField.php b/administrator/components/com_scheduler/src/Field/TaskTypeField.php new file mode 100644 index 0000000000000..d39ffc242e0dc --- /dev/null +++ b/administrator/components/com_scheduler/src/Field/TaskTypeField.php @@ -0,0 +1,71 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Scheduler\Administrator\Field; + +// Restrict direct access +\defined('_JEXEC') or die; + +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; + +/** + * A list field with all available task routines. + * + * @since __DEPLOY_VERSION__ + */ +class TaskTypeField extends ListField +{ + /** + * The form field type. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $type = 'taskType'; + + /** + * Method to get field options + * + * @return array + * + * @since __DEPLOY_VERSION__ + * @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 +
+ +
+ + + + + + +
+
+ +
+ + +
+

+ +

+ + +
+ + + items as $item) : ?> + + type; ?> + escape($item->title); ?> + escape(strip_tags($item->desc)), 200); ?> + + +
+

+

+ +

+
+ + + +
+ + +
+
+
diff --git a/administrator/components/com_scheduler/tmpl/select/modal.php b/administrator/components/com_scheduler/tmpl/select/modal.php new file mode 100644 index 0000000000000..745adeeef782e --- /dev/null +++ b/administrator/components/com_scheduler/tmpl/select/modal.php @@ -0,0 +1,36 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +/** The SelectView modal layout template. */ + +// Restrict direct access +defined('_JEXEC') or die; + +use Joomla\Component\Scheduler\Administrator\View\Select\HtmlView; + +/** @var HtmlView $this */ + +// Is this needed? +$this->modalLink = '&tmpl=component&view=select&layout=modal'; + +// Wrap the default layout in a div.container-popup +?> +
+ setLayout('default'); ?> + + loadTemplate(); + } + catch (Exception $e) + { + die('Exception while loading template..'); + } + ?> +
diff --git a/administrator/components/com_scheduler/tmpl/task/edit.php b/administrator/components/com_scheduler/tmpl/task/edit.php new file mode 100644 index 0000000000000..87aab93f00e94 --- /dev/null +++ b/administrator/components/com_scheduler/tmpl/task/edit.php @@ -0,0 +1,183 @@ + + * @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', 'task-params']; + +// 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 $fieldset) : + if (!empty($fieldset->showFront) || $fieldset->name === 'task_params') : + continue; + endif; + + $this->ignore_fieldsets[] = $fieldset->name; +endforeach; + +// ? : Are these of use here? +$isModal = $input->get('layout') === 'modal'; +$layout = $isModal ? 'modal' : 'edit'; +$tmpl = $isModal || $input->get('tmpl', '') === 'component' ? '&tmpl=component' : ''; +?> + +
+ + + + + +
+ 'general')); ?> + + + item->id) ? Text::_('COM_SCHEDULER_NEW_TASK') : Text::_('COM_SCHEDULER_EDIT_TASK') + ); + ?> +
+
+ + item->taskOption): + /** @var TaskOption $taskOption */ + $taskOption = $this->item->taskOption; ?> +
+

+ title ?> +

+

+ escape(strip_tags($taskOption->desc)), 250); + echo $desc; + ?> +

+
+ + enqueueMessage(Text::_('COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND'), 'warning'); + ?> + +
+ + form->renderFieldset('basic'); ?> +
+ +
+ + form->renderFieldset('custom-cron-rules'); ?> +
+ + +
+ +
+ form->renderFieldset('aside'); ?> +
+
+ + + + +
+
+
+ + form->renderFieldset('priority') ?> +
+ + showFront)) : + continue; + endif; ?> +
+ label ?: 'COM_SCHEDULER_FIELDSET_' . $fieldset->name) ?> + form->renderFieldset($fieldset->name) ?> +
+ +
+
+ + + + +
+
+
+ + form->renderFieldset('exec_hist'); ?> +
+
+
+ + + + +
+
+
+ + form->renderFieldset('details'); ?> +
+
+
+ + + + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + form->getInput('context'); ?> + + +
+
diff --git a/administrator/components/com_scheduler/tmpl/tasks/default.php b/administrator/components/com_scheduler/tmpl/tasks/default.php new file mode 100644 index 0000000000000..ee84718f854e3 --- /dev/null +++ b/administrator/components/com_scheduler/tmpl/tasks/default.php @@ -0,0 +1,260 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Restrict direct access +defined('_JEXEC') or die; + +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\View\Tasks\HtmlView; + +/** @var HtmlView $this*/ + +HTMLHelper::_('behavior.multiselect'); + +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 +{ + $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'); +} + +$app->getDocument()->getWebAssetManager()->useScript('com_scheduler.test-task'); +?> + +
+
+ $this)); + ?> + + + items)): ?> + +
+ + +
+ + + + items)): ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true" > + items as $i => $item): + $canCreate = $user->authorise('core.create', 'com_scheduler'); + $canEdit = $user->authorise('core.edit', 'com_scheduler'); + $canChange = $user->authorise('core.edit.state', 'com_scheduler'); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + state, $i, 'tasks.', $canChange); ?> + + locked) : ?> + $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')), + ]); ?> + + + escape($item->title); ?> + + escape($item->title); ?> + + + + note)): ?> + + + escape($item->note)); ?> + + + + escape($item->safeTypeTitle); ?> + + last_execution ? HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5') : '-'; ?> + + + + id; ?> +
+ + pagination->getListFooter(); + + // Modal for test runs + $modalparams = [ + 'title' => '', + ]; + + $modalbody = '
'; + + echo HTMLHelper::_('bootstrap.renderModal', 'scheduler-test-modal', $modalparams, $modalbody); + + ?> + + + + + + +
+
diff --git a/administrator/components/com_scheduler/tmpl/tasks/default.xml b/administrator/components/com_scheduler/tmpl/tasks/default.xml new file mode 100644 index 0000000000000..2a93aaf81afb6 --- /dev/null +++ b/administrator/components/com_scheduler/tmpl/tasks/default.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/administrator/components/com_scheduler/tmpl/tasks/empty_state.php b/administrator/components/com_scheduler/tmpl/tasks/empty_state.php new file mode 100644 index 0000000000000..dc155208aaaf7 --- /dev/null +++ b/administrator/components/com_scheduler/tmpl/tasks/empty_state.php @@ -0,0 +1,27 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Layout\LayoutHelper; + +$displayData = [ + 'textPrefix' => 'COM_SCHEDULER', + 'formURL' => 'index.php?option=com_scheduler&task=task.add', + 'helpURL' => 'https://github.com/joomla-projects/soc21_website-cronjob', + 'icon' => 'icon-clock clock', +]; + +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_scheduler')) +{ + $displayData['createURL'] = 'index.php?option=com_scheduler&view=select&layout=default'; +} + +echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/language/en-GB/com_scheduler.ini b/administrator/language/en-GB/com_scheduler.ini new file mode 100644 index 0000000000000..1fd19a58063fb --- /dev/null +++ b/administrator/language/en-GB/com_scheduler.ini @@ -0,0 +1,152 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +COM_SCHEDULER="Scheduled Tasks" +COM_SCHEDULER_CONFIGURATION="Scheduled Tasks Manager Configuration" +COM_SCHEDULER_CONFIG_FIELDSET_LAZY_SCHEDULER_DESC="Configure how site visits trigger Scheduled Tasks." +COM_SCHEDULER_CONFIG_FIELDSET_LAZY_SCHEDULER_LABEL="Lazy Scheduler" +COM_SCHEDULER_CONFIG_GENERATE_WEBCRON_KEY_DESC="The webcron needs a protection key before it is functional. Saving this configuration will autogenerate the key." +COM_SCHEDULER_CONFIG_GLOBAL_WEBCRON_KEY_LABEL="Global Key" +COM_SCHEDULER_CONFIG_GLOBAL_WEBCRON_LINK_DESC="By default, requesting this base link will only run tasks due for execution. To execute a specific task, use the task's ID as a query parameter appended to the URL: BASE_URL&id=<task's id>" +COM_SCHEDULER_CONFIG_GLOBAL_WEBCRON_LINK_LABEL="Webcron Link (Base)" +COM_SCHEDULER_CONFIG_HASH_PROTECTION_DESC="If enabled, tasks will only be triggered when URLs have the scheduler hash as a parameter." +COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_ENABLED_DESC="If disabled, scheduled tasks will not be triggered by visitors on the site.
Recommended if triggering with native cron." +COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_ENABLED_LABEL="Lazy Scheduler" +COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_INTERVAL_DESC="Interval between scheduler trigger requests from the client." +COM_SCHEDULER_CONFIG_LAZY_SCHEDULER_INTERVAL_LABEL="Request Interval" +COM_SCHEDULER_CONFIG_RESET_WEBCRON_KEY_LABEL="Reset Access Key" +COM_SCHEDULER_CONFIG_TASKS_FIELDSET_LABEL="Configure Tasks" +COM_SCHEDULER_CONFIG_TASK_TIMEOUT_LABEL="Task Timeout (seconds)" +COM_SCHEDULER_CONFIG_WEBCRON_DESC="Trigger and manage task execution with an external service." +COM_SCHEDULER_CONFIG_WEBCRON_ENABLED_LABEL="Web Cron" +COM_SCHEDULER_CONFIG_WEBCRON_LABEL="Web Cron" +COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_DESC="Copy the link to your clipboard." +COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_FAIL="Could not copy link!" +COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_SUCCESS="Link copied!" +COM_SCHEDULER_DASHBOARD_TITLE="Scheduled Tasks Manager" +COM_SCHEDULER_DESCRIPTION_TASK_PRIORITY="This is an advanced option. Higher priority tasks can potentially block lower priority tasks." +COM_SCHEDULER_EDIT_TASK="Edit Task" +COM_SCHEDULER_EMPTYSTATE_BUTTON_ADD="Add a Task!" +COM_SCHEDULER_EMPTYSTATE_CONTENT="No Tasks!" +COM_SCHEDULER_EMPTYSTATE_TITLE="No Tasks have been created yet!" +COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW="You need to select a Task type first!" +COM_SCHEDULER_ERROR_INVALID_TASK_TYPE="Invalid Task Type!" +COM_SCHEDULER_EXECUTION_CRON_EXPRESSION="Cron Expression (Advanced)" +COM_SCHEDULER_EXECUTION_INTERVAL_DAYS="Interval, Days" +COM_SCHEDULER_EXECUTION_INTERVAL_HOURS="Interval, Hours" +COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES="Interval, Minutes" +COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS="Interval, Months" +COM_SCHEDULER_FIELDSET_BASIC="Basic Fields" +COM_SCHEDULER_FIELDSET_CRON_OPTIONS="Cron Match" +COM_SCHEDULER_FIELDSET_EXEC_HIST="Execution History" +COM_SCHEDULER_FIELDSET_LOGGING="Logging" +COM_SCHEDULER_FIELDSET_NOTIFICATIONS="Notifications" +COM_SCHEDULER_FIELDSET_PRIORITY="Priority" +COM_SCHEDULER_FIELDSET_TASK_PARAMS="Task Parameters" +COM_SCHEDULER_FIELD_HINT_LOG_FILE_AUTO="defaults to 'task_.log.php'" +COM_SCHEDULER_FIELD_LABEL_EXEC_RULE="Execution Rule" +COM_SCHEDULER_FIELD_LABEL_INDIVIDUAL_LOG="Individual Task Logs" +COM_SCHEDULER_FIELD_LABEL_INTERVAL_DAYS="Interval in Days" +COM_SCHEDULER_FIELD_LABEL_INTERVAL_HOURS="Interval in Hours" +COM_SCHEDULER_FIELD_LABEL_INTERVAL_MINUTES="Interval in Minutes" +COM_SCHEDULER_FIELD_LABEL_INTERVAL_MONTHS="Interval in Months" +COM_SCHEDULER_FIELD_LABEL_LOG_FILE="Log Filename" +COM_SCHEDULER_FIELD_LABEL_SHOW_ORPHANED="Show Orphaned" +COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_DAYS_M="Days of Month" +COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_DAYS_W="Days of Week" +COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_HOURS="Hours" +COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_MINUTES="Minutes" +COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_MONTHS="Months" +COM_SCHEDULER_FIELD_TASK_TYPE="Type ID" +COM_SCHEDULER_FILTER_SEARCH_DESC="Search in Task title and note. Prefix with 'ID:' to search for a task ID" +COM_SCHEDULER_FILTER_SEARCH_LABEL="Search Tasks" +COM_SCHEDULER_FORM_TITLE_EDIT="Edit Task" +COM_SCHEDULER_FORM_TITLE_NEW="New Task" +COM_SCHEDULER_HEADING_TASK_TYPE="- Task Type -" +COM_SCHEDULER_LABEL_EXEC_DAY="Execution Day" +COM_SCHEDULER_LABEL_EXEC_INTERVAL="Execution Interval" +COM_SCHEDULER_LABEL_EXEC_TIME="Execution Time (UTC)" +COM_SCHEDULER_LABEL_EXIT_CODE="Last Exit Code" +COM_SCHEDULER_LABEL_HOURS="Hours" +COM_SCHEDULER_LABEL_LAST_EXEC="Last Executed" +COM_SCHEDULER_LABEL_MINUTES="Minutes" +COM_SCHEDULER_LABEL_NEXT_EXEC="Next Execution" +COM_SCHEDULER_LABEL_NOTES="Note" +COM_SCHEDULER_LABEL_TASK_PRIORITY="Priority" +COM_SCHEDULER_LABEL_TASK_PRIORITY_HIGH="High" +COM_SCHEDULER_LABEL_TASK_PRIORITY_LOW="Low" +COM_SCHEDULER_LABEL_TASK_PRIORITY_NORMAL="Normal" +COM_SCHEDULER_LABEL_TIMES_EXEC="Times Executed" +COM_SCHEDULER_LABEL_TIMES_FAIL="Times Failed" +COM_SCHEDULER_LAST_RUN_DATE="Last Run Date" +COM_SCHEDULER_MANAGER_TASK="Task Manager" +COM_SCHEDULER_MANAGER_TASKS="Tasks Manager" +COM_SCHEDULER_MANAGER_TASK_EDIT="Edit Task" +COM_SCHEDULER_MANAGER_TASK_NEW="New Task" +COM_SCHEDULER_MSG_MANAGE_NO_TASK_PLUGINS="There are no task types matching your query!" +COM_SCHEDULER_NEW_TASK="New Task" +COM_SCHEDULER_NO_NOTE="" +COM_SCHEDULER_N_ITEMS_DELETED="%s tasks deleted." +COM_SCHEDULER_N_ITEMS_DELETED_1="Task deleted." +COM_SCHEDULER_N_ITEMS_FAILED_UNLOCKING="Failed to unlock %s tasks" +COM_SCHEDULER_N_ITEMS_FAILED_UNLOCKING_1="Failed to unlock one tasks" +COM_SCHEDULER_N_ITEMS_PUBLISHED="%s tasks enabled." +COM_SCHEDULER_N_ITEMS_PUBLISHED_1="Task enabled." +COM_SCHEDULER_N_ITEMS_TRASHED="%s tasks trashed." +COM_SCHEDULER_N_ITEMS_TRASHED_1="Task trashed." +COM_SCHEDULER_N_ITEMS_UNPUBLISHED="%s tasks disabled." +COM_SCHEDULER_N_ITEMS_UNPUBLISHED_1="Task disabled." +COM_SCHEDULER_N_ITEMS_UNLOCKED="%s tasks unlocked." +COM_SCHEDULER_N_ITEMS_UNLOCKED_1="Task unlocked." +COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL="Manual Execution" +COM_SCHEDULER_OPTION_ORPHANED_HIDE="Hide Orphaned" +COM_SCHEDULER_OPTION_ORPHANED_ONLY="Only Orphaned" +COM_SCHEDULER_OPTION_ORPHANED_SHOW="Show Orphaned" +COM_SCHEDULER_PERMISSION_TESTRUN="Test task" +COM_SCHEDULER_ROUTINE_LOG_PREFIX="Task> " +COM_SCHEDULER_RUNNING_SINCE="Running since %s" +COM_SCHEDULER_SCHEDULER="Scheduler" +COM_SCHEDULER_SCHEDULER_TASK_COMPLETE="Successfully finished task#%1$02d in %2$.2f (net %3$.2f) seconds." +COM_SCHEDULER_SCHEDULER_TASK_LOCKED="task#%1$02d is locked." +COM_SCHEDULER_SCHEDULER_TASK_ROUTINE_NA="Task#%1$02d has no corresponding plugin routine. The plugin might have been disabled or removed. Skipping execution." +COM_SCHEDULER_SCHEDULER_TASK_START="Running task#%1$02d '%2$s'." +COM_SCHEDULER_SCHEDULER_TASK_UNKNOWN_EXIT="Task#%1$02d exited with code %4$d in %2$.2f (net %3$.2f) seconds." +; Maybe not this +COM_SCHEDULER_SCHEDULER_TASK_UNLOCKED="Task#%1$02d was unlocked." +COM_SCHEDULER_SELECT_EXEC_RULE="--- Select Rule ---" +COM_SCHEDULER_SELECT_INTERVAL_DAYS="-- Select Interval in Days --" +COM_SCHEDULER_SELECT_INTERVAL_HOURS="-- Select Interval in Hours --" +COM_SCHEDULER_SELECT_INTERVAL_MINUTES="-- Select Interval in Minutes --" +COM_SCHEDULER_SELECT_INTERVAL_MONTHS="-- Select Interval in Months --" +COM_SCHEDULER_SELECT_TASK_TYPE="Select task, %s" +COM_SCHEDULER_SELECT_TYPE="- Task Type -" +COM_SCHEDULER_TABLE_CAPTION="Tasks List" +COM_SCHEDULER_TASK="Task" +COM_SCHEDULER_TASKS_VIEW_DEFAULT_DESC="Schedule and Manage Task Routines." +COM_SCHEDULER_TASKS_VIEW_DEFAULT_TITLE="Scheduled Tasks Manager" +COM_SCHEDULER_TASK_PARAMS_FIELDSET_LABEL="Task Parameters" +COM_SCHEDULER_TASK_PRIORITY_ASC="Task Priority, Ascending" +COM_SCHEDULER_TASK_PRIORITY_DESC="Task Priority, Descending" +COM_SCHEDULER_TASK_ROUTINE_EXCEPTION="Routine threw exception: %1$s" +COM_SCHEDULER_TASK_TYPE="Task Type" +COM_SCHEDULER_TASK_TYPE_ASC="Task Type Ascending" +COM_SCHEDULER_TASK_TYPE_DESC="Task Type Descending" +COM_SCHEDULER_TEST_RUN="Run Test" +COM_SCHEDULER_TEST_RUN_DURATION="Duration: %s seconds" +COM_SCHEDULER_TEST_RUN_OUTPUT="Output:
%s" +COM_SCHEDULER_TEST_RUN_STATUS_COMPLETED="Status: Completed" +COM_SCHEDULER_TEST_RUN_STATUS_STARTED="Status: Started" +COM_SCHEDULER_TEST_RUN_STATUS_TERMINATED="Status: Terminated" +COM_SCHEDULER_TEST_RUN_TASK="Task: \"%s\"" +COM_SCHEDULER_TEST_RUN_TITLE="Test task (ID: %d)" +COM_SCHEDULER_TEST_TASK="Test task" +COM_SCHEDULER_TRIGGER_CRON="Cron" +COM_SCHEDULER_TRIGGER_PSEUDOCRON="Pseudocron" +COM_SCHEDULER_TRIGGER_XVISITS="X-Visits" +COM_SCHEDULER_TOOLBAR_UNLOCK="Unlock" +COM_SCHEDULER_TYPE_CHOOSE="Select a Task type" +COM_SCHEDULER_TYPE_OR_SELECT_OPTIONS="Type or select options" +COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND="The task routine for this task could not be found!
It's likely that the provider plugin was removed or disabled." +COM_SCHEDULER_XML_DESCRIPTION="Component for managing scheduled tasks and cronjobs (if supported by the server)." diff --git a/administrator/language/en-GB/com_scheduler.sys.ini b/administrator/language/en-GB/com_scheduler.sys.ini new file mode 100644 index 0000000000000..7f17b0054d015 --- /dev/null +++ b/administrator/language/en-GB/com_scheduler.sys.ini @@ -0,0 +1,14 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +COM_SCHEDULER="Scheduled Tasks" +COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW="You need to select a Task type first!" +COM_SCHEDULER_ERROR_INVALID_TASK_TYPE="Invalid Task Type!" +COM_SCHEDULER_MANAGER_TASK="Task Manager" +COM_SCHEDULER_MANAGER_TASK_EDIT="Edit Task" +COM_SCHEDULER_MANAGER_TASK_NEW="New Task" +COM_SCHEDULER_MANAGER_TASKS="Tasks Manager" +COM_SCHEDULER_MSG_MANAGE_NO_TASK_PLUGINS="There are no task types matching your query!" +COM_SCHEDULER_XML_DESCRIPTION="Component for managing scheduled tasks and cronjobs (if supported by the server)." diff --git a/administrator/language/en-GB/mod_menu.ini b/administrator/language/en-GB/mod_menu.ini index d7ebafdb67e82..1734cfa17c9db 100644 --- a/administrator/language/en-GB/mod_menu.ini +++ b/administrator/language/en-GB/mod_menu.ini @@ -109,6 +109,7 @@ MOD_MENU_MANAGE_LANGUAGES_CONTENT="Content Languages" MOD_MENU_MANAGE_LANGUAGES_OVERRIDES="Language Overrides" MOD_MENU_MANAGE_PLUGINS="Plugins" MOD_MENU_MANAGE_REDIRECTS="Redirects" +MOD_MENU_MANAGE_SCHEDULED_TASKS="Scheduled Tasks" MOD_MENU_MASS_MAIL_USERS="Mass Mail Users" MOD_MENU_MEDIA_MANAGER="Media" MOD_MENU_MENU_MANAGER="Manage" diff --git a/administrator/language/en-GB/plg_actionlog_joomla.ini b/administrator/language/en-GB/plg_actionlog_joomla.ini index 18a905d74f71e..5bfee992e3551 100644 --- a/administrator/language/en-GB/plg_actionlog_joomla.ini +++ b/administrator/language/en-GB/plg_actionlog_joomla.ini @@ -34,6 +34,7 @@ PLG_ACTIONLOG_JOOMLA_TYPE_PACKAGE="package" PLG_ACTIONLOG_JOOMLA_TYPE_PLUGIN="plugin" PLG_ACTIONLOG_JOOMLA_TYPE_STYLE="template style" PLG_ACTIONLOG_JOOMLA_TYPE_TAG="tag" +PLG_ACTIONLOG_JOOMLA_TYPE_TASK="Task" PLG_ACTIONLOG_JOOMLA_TYPE_TEMPLATE="template" PLG_ACTIONLOG_JOOMLA_TYPE_USER="user" PLG_ACTIONLOG_JOOMLA_TYPE_USER_GROUP="user group" @@ -45,8 +46,8 @@ PLG_ACTIONLOG_JOOMLA_USER_LOGEXPORT="User {username} PLG_ACTIONLOG_JOOMLA_USER_LOGGED_IN="User {username} logged in to {app}" PLG_ACTIONLOG_JOOMLA_USER_LOGGED_OUT="User {username} logged out from {app}" PLG_ACTIONLOG_JOOMLA_USER_LOGIN_FAILED="A failed attempt was made to login as {username} to {app}" -PLG_ACTIONLOG_JOOMLA_USER_REGISTRATION_ACTIVATE="User {username} activated the account" PLG_ACTIONLOG_JOOMLA_USER_REGISTERED="User {username} registered for an account" +PLG_ACTIONLOG_JOOMLA_USER_REGISTRATION_ACTIVATE="User {username} activated the account" PLG_ACTIONLOG_JOOMLA_USER_REMIND="User {username} requested a username reminder for their account" PLG_ACTIONLOG_JOOMLA_USER_RESET_COMPLETE="User {username} completed the password reset for their account" PLG_ACTIONLOG_JOOMLA_USER_RESET_REQUEST="User {username} requested a password reset for their account" diff --git a/administrator/language/en-GB/plg_system_schedulerunner.ini b/administrator/language/en-GB/plg_system_schedulerunner.ini new file mode 100644 index 0000000000000..4c638f443e558 --- /dev/null +++ b/administrator/language/en-GB/plg_system_schedulerunner.ini @@ -0,0 +1,6 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 +PLG_SYSTEM_SCHEDULERUNNER="System - Schedule Runner" +PLG_SYSTEM_SCHEDULERUNNER_XML_DESCRIPTION="This plugin is responsible for the lazy scheduling, webcron and click to run functionalities of com_scheduler. Besides that, this also implements form enhancers/manipulators for the com_scheduler component configuration." diff --git a/administrator/language/en-GB/plg_system_schedulerunner.sys.ini b/administrator/language/en-GB/plg_system_schedulerunner.sys.ini new file mode 100644 index 0000000000000..4c638f443e558 --- /dev/null +++ b/administrator/language/en-GB/plg_system_schedulerunner.sys.ini @@ -0,0 +1,6 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 +PLG_SYSTEM_SCHEDULERUNNER="System - Schedule Runner" +PLG_SYSTEM_SCHEDULERUNNER_XML_DESCRIPTION="This plugin is responsible for the lazy scheduling, webcron and click to run functionalities of com_scheduler. Besides that, this also implements form enhancers/manipulators for the com_scheduler component configuration." diff --git a/build/media_source/com_scheduler/css/admin-view-select-task.css b/build/media_source/com_scheduler/css/admin-view-select-task.css new file mode 100644 index 0000000000000..f92c756b956da --- /dev/null +++ b/build/media_source/com_scheduler/css/admin-view-select-task.css @@ -0,0 +1,58 @@ +.new-task { + display: flex; + overflow: hidden; + color: hsl(var(--hue), 30%, 40%); + background-color: hsl(var(--hue), 60%, 97%); + border: 1px solid hsl(var(--hue), 50%, 93%); + border-radius: .25rem; +} + +.new-task-title { + margin-bottom: .25rem; + font-size: 1rem; + font-weight: 700; +} + +.new-task-link { + display: flex; + align-items: flex-end; + justify-content: center; + width: 2.5rem; + font-size: 1.2rem; + background: hsl(var(--hue), 50%, 93%); +} + +.new-task-caption { + display: box; + margin: 0; + overflow: hidden; + font-size: .875rem; + box-orient: vertical; + -webkit-line-clamp: 3; +} + +.new-task-details { + flex: 1 0; + padding: 1rem; +} + +.new-task * { + transition: all .25s ease; +} + +.new-task:hover .new-task-link { + background: var(--template-bg-dark); +} + +.new-task-link span { + margin-bottom: 10px; + color: hsl(var(--hue), 30%, 40%); +} + +.new-task:hover .new-task-link span { + color: #fff; +} + +.new-tasks .card-columns { + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)) !important; +} diff --git a/build/media_source/com_scheduler/css/admin-view-task.css b/build/media_source/com_scheduler/css/admin-view-task.css new file mode 100644 index 0000000000000..fa9fea928e813 --- /dev/null +++ b/build/media_source/com_scheduler/css/admin-view-task.css @@ -0,0 +1,24 @@ +.match-custom .control-group .control-label { + width: auto; + padding: 0 0 0 0; +} + +.match-custom .control-group .controls { + /* flex: 1; */ + display: inline-block; + min-width: 7rem; +} + +.match-custom .control-group { + /* display: flex; */ + display: inline-block; + margin-right: 1.5rem; +} + +.match-custom label { + font-size: small; +} + +.match-custom select[multiple] { + height: 15rem; +} diff --git a/build/media_source/com_scheduler/joomla.asset.json b/build/media_source/com_scheduler/joomla.asset.json new file mode 100644 index 0000000000000..f09df3fa3e4d6 --- /dev/null +++ b/build/media_source/com_scheduler/joomla.asset.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json", + "name": "com_scheduler", + "version": "4.0.0", + "description": "Joomla CMS", + "license": "GNU General Public License version 2 or later; see LICENSE.txt", + "assets": [ + { + "name": "com_scheduler.test-task.es5", + "type": "script", + "uri": "com_scheduler/admin-view-run-test-task-es5.js", + "dependencies": [ + "core" + ], + "attributes": { + "nomodule": true, + "defer": true + } + }, + { + "name": "com_scheduler.test-task", + "type": "script", + "uri": "com_scheduler/admin-view-run-test-task.js", + "dependencies": [ + "com_scheduler.test-task.es5" + ], + "attributes": { + "type" : "module" + } + }, + { + "name": "com_scheduler.admin-view-select-task-search.es5", + "type": "script", + "uri": "com_scheduler/admin-view-select-task-search-es5.js", + "dependencies": [ + "core" + ], + "attributes": { + "nomodule": true, + "defer": true + } + }, + { + "name": "com_scheduler.admin-view-select-task-search", + "type": "script", + "uri": "com_scheduler/admin-view-select-task-search.js", + "dependencies": [ + "com_scheduler.admin-view-select-task-search.es5" + ], + "attributes": { + "type": "module" + } + }, + { + "name": "com_scheduler.scheduler-config.es5", + "type": "script", + "uri": "com_scheduler/scheduler-config-es5.js", + "dependencies": [ + "core" + ], + "attributes": { + "nomodule": true + } + }, + { + "name": "com_scheduler.scheduler-config", + "type": "script", + "uri": "com_scheduler/scheduler-config.js", + "dependencies": [ + "core", + "com_scheduler.scheduler-config.es5" + ], + "attributes": { + "type": "module" + } + }, + { + "name": "com_scheduler.admin-view-select-task-css", + "type": "style", + "uri": "com_scheduler/admin-view-select-task.css" + }, + { + "name": "com_scheduler.admin-view-task-css", + "type": "style", + "uri": "com_scheduler/admin-view-task.css" + } + ] +} diff --git a/build/media_source/com_scheduler/js/admin-view-run-test-task.es6.js b/build/media_source/com_scheduler/js/admin-view-run-test-task.es6.js new file mode 100644 index 0000000000000..49b1b2af420fb --- /dev/null +++ b/build/media_source/com_scheduler/js/admin-view-run-test-task.es6.js @@ -0,0 +1,92 @@ +/** + * @copyright (C) 2021 Open Source Matters, Inc. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +/** + * Provides the manual-run functionality for tasks over the com_scheduler administrator backend. + * + * @package Joomla.Components + * @subpackage Scheduler.Tasks + * + * @since __DEPLOY_VERSION__ + */ +if (!window.Joomla) { + throw new Error('Joomla API was not properly initialised'); +} + +const initRunner = () => { + const paths = Joomla.getOptions('system.paths'); + const uri = `${paths ? `${paths.base}/index.php` : window.location.pathname}?option=com_ajax&format=json&plugin=RunSchedulerTest&group=system&id=%d`; + const modal = document.getElementById('scheduler-test-modal'); + + // Task output template + const template = ` +

${Joomla.Text._('COM_SCHEDULER_TEST_RUN_TASK')}

+
${Joomla.Text._('COM_SCHEDULER_TEST_RUN_STATUS_STARTED')}
+
+ `; + + const sanitiseTaskOutput = (text) => text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); + + // Trigger the task through a GET request, populate the modal with output on completion. + const triggerTaskAndShowOutput = (e) => { + const button = e.relatedTarget; + const id = parseInt(button.dataset.id, 10); + const { title } = button.dataset; + + modal.querySelector('.modal-title').innerHTML = Joomla.Text._('COM_SCHEDULER_TEST_RUN_TITLE').replace('%d', id); + modal.querySelector('.modal-body > div').innerHTML = template.replace('%s', title); + + Joomla.request({ + url: uri.replace('%d', id.toString()), + onSuccess: (data, xhr) => { + [].slice.call(modal.querySelectorAll('.modal-body > div > div')).forEach((el) => { + el.parentNode.removeChild(el); + }); + + const output = JSON.parse(data); + + if (output && output.success && output.data) { + modal.querySelector('.modal-body > div').innerHTML += `
${Joomla.Text._('COM_SCHEDULER_TEST_RUN_STATUS_COMPLETED')}
`; + + if (output.data.duration > 0) { + modal.querySelector('.modal-body > div').innerHTML += `
${Joomla.Text._('COM_SCHEDULER_TEST_RUN_DURATION').replace('%s', output.data.duration.toFixed(2))}
`; + } + + if (output.data.output) { + const result = Joomla.sanitizeHtml((output.data.output), null, sanitiseTaskOutput); + + // Can use an indication for non-0 exit codes + modal.querySelector('.modal-body > div').innerHTML += `
${Joomla.Text._('COM_SCHEDULER_TEST_RUN_OUTPUT').replace('%s', result)}
`; + } + } else { + modal.querySelector('.modal-body > div').innerHTML += `
${Joomla.Text._('COM_SCHEDULER_TEST_RUN_STATUS_TERMINATED')}
`; + modal.querySelector('.modal-body > div').innerHTML += `
${Joomla.Text._('COM_SCHEDULER_TEST_RUN_OUTPUT').replace('%s', Joomla.Text._('JLIB_JS_AJAX_ERROR_OTHER').replace('%s', xhr.status))}
`; + } + }, + onError: (xhr) => { + modal.querySelector('.modal-body > div').innerHTML += `
${Joomla.Text._('COM_SCHEDULER_TEST_RUN_STATUS_TERMINATED')}
`; + + const msg = Joomla.ajaxErrorsMessages(xhr); + modal.querySelector('.modal-body > div').innerHTML += `
${Joomla.Text._('COM_SCHEDULER_TEST_RUN_OUTPUT').replace('%s', msg.error)}
`; + }, + }); + }; + + const reloadOnClose = () => { + window.location.href = `${paths ? `${paths.base}/index.php` : window.location.pathname}?option=com_scheduler&view=tasks`; + }; + + modal.addEventListener('show.bs.modal', triggerTaskAndShowOutput); + modal.addEventListener('hidden.bs.modal', reloadOnClose); + document.removeEventListener('DOMContentLoaded', initRunner); +}; + +document.addEventListener('DOMContentLoaded', initRunner); diff --git a/build/media_source/com_scheduler/js/admin-view-select-task-search.es6.js b/build/media_source/com_scheduler/js/admin-view-select-task-search.es6.js new file mode 100644 index 0000000000000..78215fb8b7857 --- /dev/null +++ b/build/media_source/com_scheduler/js/admin-view-select-task-search.es6.js @@ -0,0 +1,107 @@ +/** + * @copyright (C) 2021 Open Source Matters, Inc. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +/** + * Add a keyboard event listener to the Select a Task Type search element. + * + * IMPORTANT! This script is meant to be loaded deferred. This means that a. it's non-blocking + * (the browser can load it whenever) and b. it doesn't need an on DOMContentLoaded event handler + * because the browser is guaranteed to execute it only after the DOM content has loaded, the + * whole point of it being deferred. + * + * The search box has a keyboard handler that fires every time you press a keyboard button or send + * a keypress with a touch / virtual keyboard. We then iterate all task type cards and check if + * the plain text (HTML stripped out) representation of the task title or description partially + * matches the text you entered in the search box. If it doesn't we add a Bootstrap class to hide + * the task. + * + * This way we limit the displayed tasks only to those searched. + * + * This feature follows progressive enhancement. The search box is hidden by default and only + * displayed when this JavaScript here executes. Furthermore, session storage is only used if it + * is available in the browser. That's a bit of a pain but makes sure things won't break in older + * browsers. + * + * Furthermore and to facilitate the user experience we auto-focus the search element which has a + * suitable title so that non-sighted users are not startled. This way we address both UX concerns + * and accessibility. + * + * Finally, the search string is saved into session storage on the assumption that the user is + * probably going to be creating multiple instances of the same task, one after another, as is + * typical when building a new Joomla! site. + * phpcs:ignoreFile + */ +// Make sure the element exists i.e. a template override has not removed it. +const elSearch = document.getElementById('comSchedulerSelectSearch'); +const elSearchContainer = document.getElementById('comSchedulerSelectSearchContainer'); +const elSearchHeader = document.getElementById('comSchedulerSelectTypeHeader'); +const elSearchResults = document.getElementById('comSchedulerSelectResultsContainer'); +const alertElement = document.querySelector('.tasks-alert'); +const elCards = [].slice.call(document.querySelectorAll('.comSchedulerSelectCard')); + +if (elSearch && elSearchContainer) { + // Add the keyboard event listener which performs the live search in the cards + elSearch.addEventListener('keyup', ({ target }) => { + /** @type {KeyboardEvent} event */ + const partialSearch = target.value; + let hasSearchResults = false; // Save the search string into session storage + + if (typeof sessionStorage !== 'undefined') { + sessionStorage.setItem('Joomla.com_scheduler.new.search', partialSearch); + } + + // Iterate over all the task cards + elCards.forEach((card) => { + // First remove the class which hide the task cards + card.classList.remove('d-none'); + + // An empty search string means that we should show everything + if (!partialSearch) { + return; + } + + const cardHeader = card.querySelector('.new-task-title'); + const cardBody = card.querySelector('.card-body'); + const title = cardHeader ? cardHeader.textContent : ''; + const description = cardBody ? cardBody.textContent : ''; + + // If the task title and description don’t match add a class to hide it. + if (title && !title.toLowerCase().includes(partialSearch.toLowerCase()) + && description && !description.toLowerCase().includes(partialSearch.toLowerCase())) { + card.classList.add('d-none'); + } else { + hasSearchResults = true; + } + }); + + if (hasSearchResults || !partialSearch) { + alertElement.classList.add('d-none'); + elSearchHeader.classList.remove('d-none'); + elSearchResults.classList.remove('d-none'); + } else { + alertElement.classList.remove('d-none'); + elSearchHeader.classList.add('d-none'); + elSearchResults.classList.add('d-none'); + } + }); + + // For reasons of progressive enhancement the search box is hidden by default. + elSearchContainer.classList.remove('d-none'); + + // Focus the just show element + elSearch.focus(); + + try { + if (typeof sessionStorage !== 'undefined') { + // Load the search string from session storage + elSearch.value = sessionStorage.getItem('Joomla.com_scheduler.new.search') || ''; + + // Trigger the keyboard handler event manually to initiate the search + elSearch.dispatchEvent(new KeyboardEvent('keyup')); + } + } catch (e) { + // This is probably Internet Explorer which doesn't support the KeyboardEvent constructor :( + } +} diff --git a/build/media_source/com_scheduler/js/scheduler-config.es6.js b/build/media_source/com_scheduler/js/scheduler-config.es6.js new file mode 100644 index 0000000000000..b980848ad2ce8 --- /dev/null +++ b/build/media_source/com_scheduler/js/scheduler-config.es6.js @@ -0,0 +1,51 @@ +/** + * @copyright (C) 2021 Open Source Matters, Inc. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +if (!window.Joomla) { + throw new Error('Joomla API was not properly initialised!'); +} + +const copyToClipboardFallback = (input) => { + input.focus(); + input.select(); + + try { + const copy = document.execCommand('copy'); + if (copy) { + Joomla.renderMessages({ message: [Joomla.Text._('COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_SUCCESS')] }); + } else { + Joomla.renderMessages({ error: [Joomla.Text._('COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_FAIL')] }); + } + } catch (err) { + Joomla.renderMessages({ error: [err] }); + } +}; + +const copyToClipboard = () => { + const button = document.getElementById('link-copy'); + + button.addEventListener('click', ({ currentTarget }) => { + const input = currentTarget.previousElementSibling; + + if (!navigator.clipboard) { + copyToClipboardFallback(input); + return; + } + + navigator.clipboard.writeText(input.value).then(() => { + Joomla.renderMessages({ message: [Joomla.Text._('COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_SUCCESS')] }); + }, () => { + Joomla.renderMessages({ error: [Joomla.Text._('COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_FAIL')] }); + }); + }); +}; + +const onBoot = () => { + copyToClipboard(); + + document.removeEventListener('DOMContentLoaded', onBoot); +}; + +document.addEventListener('DOMContentLoaded', onBoot); diff --git a/build/media_source/plg_system_schedulerunner/joomla.asset.json b/build/media_source/plg_system_schedulerunner/joomla.asset.json new file mode 100644 index 0000000000000..0feb3c9850ef5 --- /dev/null +++ b/build/media_source/plg_system_schedulerunner/joomla.asset.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json", + "name": "plg_system_schedulerunner", + "version": "4.0.0", + "description": "Joomla CMS", + "license": "GPL-2.0-or-later", + "assets": [ + { + "name": "plg_system_schedulerunner.run-schedule.es5", + "type": "script", + "uri": "plg_system_schedulerunner/run-schedule-es5.min.js", + "dependencies": [ + "core" + ], + "attributes": { + "nomodule": true, + "defer": true + } + }, + { + "name": "plg_system_schedulerunner.run-schedule", + "type": "script", + "uri": "plg_system_schedulerunner/run-schedule.min.js", + "dependencies": [ + "plg_system_schedulerunner.run-schedule.es5", + "core" + ], + "atrributes": { + "nomodule": true, + "defer": true + } + } + ] +} diff --git a/build/media_source/plg_system_schedulerunner/js/run-schedule.es6.js b/build/media_source/plg_system_schedulerunner/js/run-schedule.es6.js new file mode 100644 index 0000000000000..c2d1e92418a82 --- /dev/null +++ b/build/media_source/plg_system_schedulerunner/js/run-schedule.es6.js @@ -0,0 +1,36 @@ +/** + * @copyright (C) 2021 Open Source Matters, Inc. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +/** + * Makes calls to com_ajax to trigger the Scheduler. + * + * Used for lazy scheduling of tasks. + * + * @package Joomla.Plugins + * @subpackage System.ScheduleRunner + * + * @since __DEPLOY_VERSION__ + */ +if (!window.Joomla) { + throw new Error('Joomla API was not properly initialised'); +} + +const initScheduler = () => { + const options = Joomla.getOptions('plg_system_schedulerunner'); + const paths = Joomla.getOptions('system.paths'); + const interval = (options && options.inverval ? parseInt(options.interval, 10) : 300) * 1000; + const uri = `${paths ? `${paths.root}/index.php` : window.location.pathname}?option=com_ajax&format=raw&plugin=RunSchedulerLazy&group=system`; + + setInterval(() => navigator.sendBeacon(uri), interval); + + // Run it at the beginning at least once + navigator.sendBeacon(uri); +}; + +((document) => { + document.addEventListener('DOMContentLoaded', () => { + initScheduler(); + }); +})(document); diff --git a/composer.json b/composer.json index c760f66ef3f7c..064ed320b4c03 100644 --- a/composer.json +++ b/composer.json @@ -82,7 +82,8 @@ "psr/log": "~1.0", "ext-gd": "*", "web-auth/webauthn-lib": "2.1.*", - "composer/ca-bundle": "^1.2" + "composer/ca-bundle": "^1.2", + "dragonmantank/cron-expression": "^3.1" }, "require-dev": { "phpunit/phpunit": "^8.5", diff --git a/composer.lock b/composer.lock index df658ab10845b..92776fb40dbcc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7a38a492e1140d3acdd45a4fb7f42486", + "content-hash": "f35173335d5258a86474fcd8e70fdd58", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -423,6 +423,67 @@ ], "time": "2021-04-16T17:34:40+00:00" }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "7a8c6e56ab3ffcc538d05e8155bb42269abf1a0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/7a8c6e56ab3ffcc538d05e8155bb42269abf1a0c", + "reference": "7a8c6e56ab3ffcc538d05e8155bb42269abf1a0c", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.7.0" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-webmozart-assert": "^0.12.7", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.1.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2020-11-24T19:55:57+00:00" + }, { "name": "fgrosse/phpasn1", "version": "v2.3.0", @@ -5185,6 +5246,64 @@ }, "time": "2019-09-09T12:04:09+00:00" }, + { + "name": "webmozart/assert", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "time": "2021-03-09T10:59:23+00:00" + }, { "name": "willdurand/negotiation", "version": "3.0.0", @@ -10378,64 +10497,6 @@ } ], "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" } ], "aliases": [], diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index 82fd5d03509e1..1ae991be460bf 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -25,7 +25,7 @@ CREATE TABLE IF NOT EXISTS `#__assets` ( -- INSERT INTO `#__assets` (`id`, `parent_id`, `lft`, `rgt`, `level`, `name`, `title`, `rules`) VALUES -(1, 0, 0, 161, 0, 'root.1', 'Root Asset', '{"core.login.site":{"6":1,"2":1},"core.login.admin":{"6":1},"core.login.api":{"8":1},"core.login.offline":{"6":1},"core.admin":{"8":1},"core.manage":{"7":1},"core.create":{"6":1,"3":1},"core.delete":{"6":1},"core.edit":{"6":1,"4":1},"core.edit.state":{"6":1,"5":1},"core.edit.own":{"6":1,"3":1}}'), +(1, 0, 0, 163, 0, 'root.1', 'Root Asset', '{"core.login.site":{"6":1,"2":1},"core.login.admin":{"6":1},"core.login.api":{"8":1},"core.login.offline":{"6":1},"core.admin":{"8":1},"core.manage":{"7":1},"core.create":{"6":1,"3":1},"core.delete":{"6":1},"core.edit":{"6":1,"4":1},"core.edit.state":{"6":1,"5":1},"core.edit.own":{"6":1,"3":1}}'), (2, 1, 1, 2, 1, 'com_admin', 'com_admin', '{}'), (3, 1, 3, 6, 1, 'com_banners', 'com_banners', '{"core.admin":{"7":1},"core.manage":{"6":1}}'), (4, 1, 7, 8, 1, 'com_cache', 'com_cache', '{"core.admin":{"7":1},"core.manage":{"7":1}}'), @@ -105,7 +105,8 @@ INSERT INTO `#__assets` (`id`, `parent_id`, `lft`, `rgt`, `level`, `name`, `titl (85, 18, 120, 121, 2, 'com_modules.module.108', 'Privacy Status', '{}'), (86, 18, 122, 123, 2, 'com_modules.module.96', 'Popular Articles', '{}'), (87, 18, 124, 125, 2, 'com_modules.module.97', 'Recently Added Articles', '{}'), -(88, 18, 126, 127, 2, 'com_modules.module.98', 'Logged-in Users', '{}'); +(88, 18, 126, 127, 2, 'com_modules.module.98', 'Logged-in Users', '{}'), +(89, 1, 161, 162, 1, 'com_scheduler', 'com_scheduler', '{}'); -- -------------------------------------------------------- @@ -179,7 +180,8 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, (0, 'com_privacy', 'component', 'com_privacy', '', 1, 1, 1, 0, 1, '', '', ''), (0, 'com_actionlogs', 'component', 'com_actionlogs', '', 1, 1, 1, 0, 1, '', '{"ip_logging":0,"csv_delimiter":",","loggable_extensions":["com_banners","com_cache","com_categories","com_checkin","com_config","com_contact","com_content","com_installer","com_media","com_menus","com_messages","com_modules","com_newsfeeds","com_plugins","com_redirect","com_tags","com_templates","com_users"]}', ''), (0, 'com_workflow', 'component', 'com_workflow', '', 1, 1, 0, 1, 1, '', '{}', ''), -(0, 'com_mails', 'component', 'com_mails', '', 1, 1, 1, 1, 1, '', '', ''); +(0, 'com_mails', 'component', 'com_mails', '', 1, 1, 1, 1, 1, '', '', ''), +(0, 'com_scheduler', 'component', 'com_scheduler', '', 1, 1, 1, 0, 1, '', '{}', ''); -- Libraries INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `locked`, `manifest_cache`, `params`, `custom_data`) VALUES @@ -334,12 +336,18 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, (0, 'plg_system_privacyconsent', 'plugin', 'privacyconsent', 'system', 0, 0, 1, 0, 1, '', '{}', '', 13, 0), (0, 'plg_system_redirect', 'plugin', 'redirect', 'system', 0, 0, 1, 0, 1, '', '', '', 14, 0), (0, 'plg_system_remember', 'plugin', 'remember', 'system', 0, 1, 1, 0, 1, '', '', '', 15, 0), +(0, 'plg_system_schedulerunner', 'plugin', 'schedulerunner', 'system', 0, 1, 1, 0, 0, '', '{}', '', 15, 0), (0, 'plg_system_sef', 'plugin', 'sef', 'system', 0, 1, 1, 0, 1, '', '', '', 16, 0), (0, 'plg_system_sessiongc', 'plugin', 'sessiongc', 'system', 0, 1, 1, 0, 1, '', '', '', 17, 0), (0, 'plg_system_skipto', 'plugin', 'skipto', 'system', 0, 1, 1, 0, 1, '', '{}', '', 18, 0), (0, 'plg_system_stats', 'plugin', 'stats', 'system', 0, 1, 1, 0, 1, '', '', '', 19, 0), +(0, 'plg_system_tasknotification', 'plugin', 'tasknotification', 'system', 0, 1, 1, 0, 1, '', '', '', 22, 0), (0, 'plg_system_updatenotification', 'plugin', 'updatenotification', 'system', 0, 1, 1, 0, 1, '', '', '', 20, 0), (0, 'plg_system_webauthn', 'plugin', 'webauthn', 'system', 0, 1, 1, 0, 1, '', '{}', '', 21, 0), +(0, 'plg_task_checkfiles', 'plugin', 'checkfiles', 'task', 0, 1, 1, 0, 0, '', '{}', '', 15, 0), +(0, 'plg_task_demotasks', 'plugin', 'demotasks', 'task', 0, 1, 1, 0, 0, '', '{}', '', 15, 0), +(0, 'plg_task_requests', 'plugin', 'requests', 'task', 0, 1, 1, 0, 0, '', '{}', '', 15, 0), +(0, 'plg_task_sitestatus', 'plugin', 'sitestatus', 'task', 0, 1, 1, 0, 0, '', '{}', '', 15, 0), (0, 'plg_twofactorauth_totp', 'plugin', 'totp', 'twofactorauth', 0, 0, 1, 0, 1, '', '', '', 1, 0), (0, 'plg_twofactorauth_yubikey', 'plugin', 'yubikey', 'twofactorauth', 0, 0, 1, 0, 1, '', '', '', 2, 0), (0, 'plg_user_contactcreator', 'plugin', 'contactcreator', 'user', 0, 0, 1, 0, 1, '', '{"autowebpage":"","category":"4","autopublish":"0"}', '', 1, 0), diff --git a/installation/sql/mysql/extensions.sql b/installation/sql/mysql/extensions.sql index 52a6c316af3e5..f37b17ece25e7 100644 --- a/installation/sql/mysql/extensions.sql +++ b/installation/sql/mysql/extensions.sql @@ -829,7 +829,8 @@ INSERT INTO `#__action_logs_extensions` (`id`, `extension`) VALUES (15, 'com_tags'), (16, 'com_templates'), (17, 'com_users'), -(18, 'com_checkin'); +(18, 'com_checkin'), +(19, 'com_scheduler'); -- -------------------------------------------------------- @@ -867,7 +868,8 @@ INSERT INTO `#__action_log_config` (`id`, `type_title`, `type_alias`, `id_holder (16, 'module', 'com_modules.module', 'id' ,'title', '#__modules', 'PLG_ACTIONLOG_JOOMLA'), (17, 'access_level', 'com_users.level', 'id' , 'title', '#__viewlevels', 'PLG_ACTIONLOG_JOOMLA'), (18, 'banner_client', 'com_banners.client', 'id', 'name', '#__banner_clients', 'PLG_ACTIONLOG_JOOMLA'), -(19, 'application_config', 'com_config.application', '', 'name', '', 'PLG_ACTIONLOG_JOOMLA'); +(19, 'application_config', 'com_config.application', '', 'name', '', 'PLG_ACTIONLOG_JOOMLA'), +(20, 'task', 'com_scheduler.task', 'id', 'title', '#__scheduler_tasks', 'PLG_ACTIONLOG_JOOMLA'); -- -------------------------------------------------------- @@ -882,3 +884,45 @@ CREATE TABLE IF NOT EXISTS `#__action_logs_users` ( PRIMARY KEY (`user_id`), KEY `idx_notify` (`notify`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `#__scheduler_tasks` +-- + +CREATE TABLE IF NOT EXISTS `#__scheduler_tasks` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `asset_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'FK to the #__assets table.', + `title` varchar(255) NOT NULL DEFAULT '', + `type` varchar(128) NOT NULL COMMENT 'unique identifier for job defined by plugin', + `execution_rules` text COMMENT 'Execution Rules, Unprocessed', + `cron_rules` text COMMENT 'Processed execution rules, crontab-like JSON form', + `state` tinyint NOT NULL DEFAULT FALSE, + `last_exit_code` int NOT NULL DEFAULT 0 COMMENT 'Exit code when job was last run', + `last_execution` datetime COMMENT 'Timestamp of last run', + `next_execution` datetime COMMENT 'Timestamp of next (planned) run, referred for execution on trigger', + `times_executed` int DEFAULT 0 COMMENT 'Count of successful triggers', + `times_failed` int DEFAULT 0 COMMENT 'Count of failures', + `locked` datetime, + `priority` smallint NOT NULL DEFAULT 0, + `ordering` int NOT NULL DEFAULT 0 COMMENT 'Configurable list ordering', + `cli_exclusive` smallint NOT NULL DEFAULT 0 COMMENT 'If 1, the task is only accessible via CLI', + `params` text NOT NULL, + `note` text, + `created` datetime NOT NULL, + `created_by` int UNSIGNED NOT NULL DEFAULT 0, + `checked_out` int unsigned, + `checked_out_time` datetime, + PRIMARY KEY (id), + KEY `idx_type` (`type`), + KEY `idx_state` (`state`), + KEY `idx_last_exit` (`last_exit_code`), + KEY `idx_next_exec` (`next_execution`), + KEY `idx_locked` (`locked`), + KEY `idx_priority` (`priority`), + KEY `idx_cli_exclusive` (`cli_exclusive`), + KEY `idx_checked_out` (`checked_out`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; + +-- -------------------------------------------------------- diff --git a/installation/sql/mysql/supports.sql b/installation/sql/mysql/supports.sql index 80304d524482f..1b60a4c48935c 100644 --- a/installation/sql/mysql/supports.sql +++ b/installation/sql/mysql/supports.sql @@ -436,4 +436,8 @@ INSERT INTO `#__mail_templates` (`template_id`, `extension`, `language`, `subjec ('com_users.registration.user.admin_activated', 'com_users', '', 'COM_USERS_EMAIL_ACTIVATED_BY_ADMIN_ACTIVATION_SUBJECT', 'COM_USERS_EMAIL_ACTIVATED_BY_ADMIN_ACTIVATION_BODY', '', '', '{"tags":["name","sitename","siteurl","username"]}'), ('com_users.registration.admin.verification_request', 'com_users', '', 'COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_SUBJECT', 'COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_BODY', '', '', '{"tags":["name","sitename","email","username","activate"]}'), ('plg_system_privacyconsent.request.reminder', 'plg_system_privacyconsent', '', 'PLG_SYSTEM_PRIVACYCONSENT_EMAIL_REMIND_SUBJECT', 'PLG_SYSTEM_PRIVACYCONSENT_EMAIL_REMIND_BODY', '', '', '{"tags":["sitename","url","tokenurl","formurl","token"]}'), -('com_messages.new_message', 'com_messages', '', 'COM_MESSAGES_NEW_MESSAGE', 'COM_MESSAGES_NEW_MESSAGE_BODY', '', '', '{"tags":["subject","message","fromname","sitename","siteurl","fromemail","toname","toemail"]}'); +('com_messages.new_message', 'com_messages', '', 'COM_MESSAGES_NEW_MESSAGE', 'COM_MESSAGES_NEW_MESSAGE_BODY', '', '', '{"tags":["subject","message","fromname","sitename","siteurl","fromemail","toname","toemail"]}'), +('plg_system_tasknotification.failure_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_FAILURE_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_FAILURE_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title", "exit_code", "exec_data_time", "task_output"]}'), +('plg_system_tasknotification.fatal_recovery_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title"]}'), +('plg_system_tasknotification.orphan_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title", ""]}'), +('plg_system_tasknotification.success_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_BODY', '', '', '{"tags":["task_id", "task_title", "exec_data_time", "task_output"]}'); diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index 8786f98bd20e3..e44188862d656 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -31,7 +31,7 @@ COMMENT ON COLUMN "#__assets"."rules" IS 'JSON encoded access control.'; -- INSERT INTO "#__assets" ("id", "parent_id", "lft", "rgt", "level", "name", "title", "rules") VALUES -(1, 0, 0, 161, 0, 'root.1', 'Root Asset', '{"core.login.site":{"6":1,"2":1},"core.login.admin":{"6":1},"core.login.api":{"8":1},"core.login.offline":{"6":1},"core.admin":{"8":1},"core.manage":{"7":1},"core.create":{"6":1,"3":1},"core.delete":{"6":1},"core.edit":{"6":1,"4":1},"core.edit.state":{"6":1,"5":1},"core.edit.own":{"6":1,"3":1}}'), +(1, 0, 0, 163, 0, 'root.1', 'Root Asset', '{"core.login.site":{"6":1,"2":1},"core.login.admin":{"6":1},"core.login.api":{"8":1},"core.login.offline":{"6":1},"core.admin":{"8":1},"core.manage":{"7":1},"core.create":{"6":1,"3":1},"core.delete":{"6":1},"core.edit":{"6":1,"4":1},"core.edit.state":{"6":1,"5":1},"core.edit.own":{"6":1,"3":1}}'), (2, 1, 1, 2, 1, 'com_admin', 'com_admin', '{}'), (3, 1, 3, 6, 1, 'com_banners', 'com_banners', '{"core.admin":{"7":1},"core.manage":{"6":1}}'), (4, 1, 7, 8, 1, 'com_cache', 'com_cache', '{"core.admin":{"7":1},"core.manage":{"7":1}}'), @@ -111,7 +111,8 @@ INSERT INTO "#__assets" ("id", "parent_id", "lft", "rgt", "level", "name", "titl (85, 18, 120, 121, 2, 'com_modules.module.108', 'Privacy Status', '{}'), (86, 18, 122, 123, 2, 'com_modules.module.96', 'Popular Articles', '{}'), (87, 18, 124, 125, 2, 'com_modules.module.97', 'Recently Added Articles', '{}'), -(88, 18, 126, 127, 2, 'com_modules.module.98', 'Logged-in Users', '{}'); +(88, 18, 126, 127, 2, 'com_modules.module.98', 'Logged-in Users', '{}'), +(89, 1, 161, 162, 1, 'com_scheduler', 'com_scheduler', '{}'); SELECT setval('#__assets_id_seq', 89, false); @@ -185,7 +186,8 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", (0, 'com_privacy', 'component', 'com_privacy', '', 1, 1, 1, 0, 1, '', '', '', 0, 0), (0, 'com_actionlogs', 'component', 'com_actionlogs', '', 1, 1, 1, 0, 1, '', '{"ip_logging":0,"csv_delimiter":",","loggable_extensions":["com_banners","com_cache","com_categories","com_checkin","com_config","com_contact","com_content","com_installer","com_media","com_menus","com_messages","com_modules","com_newsfeeds","com_plugins","com_redirect","com_tags","com_templates","com_users"]}', '', 0, 0), (0, 'com_workflow', 'component', 'com_workflow', '', 1, 1, 0, 1, 1, '', '{}', '', 0, 0), -(0, 'com_mails', 'component', 'com_mails', '', 1, 1, 1, 1, 1, '', '', '', 0, 0); +(0, 'com_mails', 'component', 'com_mails', '', 1, 1, 1, 1, 1, '', '', '', 0, 0), +(0, 'com_scheduler', 'component', 'com_scheduler', '', 1, 1, 1, 0, 1, '', '{}', '', 0, 0); -- Libraries INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "locked", "manifest_cache", "params", "custom_data", "ordering", "state") VALUES @@ -340,12 +342,18 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", (0, 'plg_system_privacyconsent', 'plugin', 'privacyconsent', 'system', 0, 0, 1, 0, 1, '', '{}', '', 13, 0), (0, 'plg_system_redirect', 'plugin', 'redirect', 'system', 0, 0, 1, 0, 1, '', '', '', 14, 0), (0, 'plg_system_remember', 'plugin', 'remember', 'system', 0, 1, 1, 0, 1, '', '', '', 15, 0), +(0, 'plg_system_schedulerunner', 'plugin', 'schedulerunner', 'system', 0, 1, 1, 0, 0, '', '{}', '', 15, 0), (0, 'plg_system_sef', 'plugin', 'sef', 'system', 0, 1, 1, 0, 1, '', '', '', 16, 0), (0, 'plg_system_sessiongc', 'plugin', 'sessiongc', 'system', 0, 1, 1, 0, 1, '', '', '', 17, 0), (0, 'plg_system_skipto', 'plugin', 'skipto', 'system', 0, 1, 1, 0, 1, '', '{}', '', 18, 0), (0, 'plg_system_stats', 'plugin', 'stats', 'system', 0, 1, 1, 0, 1, '', '', '', 19, 0), +(0, 'plg_system_tasknotification', 'plugin', 'tasknotification', 'system', 0, 1, 1, 0, 1, '', '', '', 22, 0), (0, 'plg_system_updatenotification', 'plugin', 'updatenotification', 'system', 0, 1, 1, 0, 1, '', '', '', 20, 0), (0, 'plg_system_webauthn', 'plugin', 'webauthn', 'system', 0, 1, 1, 0, 1, '', '{}', '', 21, 0), +(0, 'plg_task_checkfiles', 'plugin', 'checkfiles', 'task', 0, 1, 1, 0, 0, '', '{}', '', 15, 0), +(0, 'plg_task_demotasks', 'plugin', 'demotasks', 'task', 0, 1, 1, 0, 0, '', '{}', '', 15, 0), +(0, 'plg_task_requests', 'plugin', 'requests', 'task', 0, 1, 1, 0, 0, '', '{}', '', 15, 0), +(0, 'plg_task_sitestatus', 'plugin', 'sitestatus', 'task', 0, 1, 1, 0, 0, '', '{}', '', 15, 0), (0, 'plg_twofactorauth_totp', 'plugin', 'totp', 'twofactorauth', 0, 0, 1, 0, 1, '', '', '', 1, 0), (0, 'plg_twofactorauth_yubikey', 'plugin', 'yubikey', 'twofactorauth', 0, 0, 1, 0, 1, '', '', '', 2, 0), (0, 'plg_user_contactcreator', 'plugin', 'contactcreator', 'user', 0, 0, 1, 0, 1, '', '{"autowebpage":"","category":"4","autopublish":"0"}', '', 1, 0), diff --git a/installation/sql/postgresql/extensions.sql b/installation/sql/postgresql/extensions.sql index 628c12098f02f..699f6454c3cd5 100644 --- a/installation/sql/postgresql/extensions.sql +++ b/installation/sql/postgresql/extensions.sql @@ -787,7 +787,8 @@ INSERT INTO "#__action_logs_extensions" ("id", "extension") VALUES (15, 'com_tags'), (16, 'com_templates'), (17, 'com_users'), -(18, 'com_checkin'); +(18, 'com_checkin'), +(19, 'com_scheduler'); SELECT setval('#__action_logs_extensions_id_seq', 19, false); -- -------------------------------------------------------- @@ -828,7 +829,8 @@ INSERT INTO "#__action_log_config" ("id", "type_title", "type_alias", "id_holder (16, 'module', 'com_modules.module', 'id' ,'title', '#__modules', 'PLG_ACTIONLOG_JOOMLA'), (17, 'access_level', 'com_users.level', 'id' , 'title', '#__viewlevels', 'PLG_ACTIONLOG_JOOMLA'), (18, 'banner_client', 'com_banners.client', 'id', 'name', '#__banner_clients', 'PLG_ACTIONLOG_JOOMLA'), -(19, 'application_config', 'com_config.application', '', 'name', '', 'PLG_ACTIONLOG_JOOMLA'); +(19, 'application_config', 'com_config.application', '', 'name', '', 'PLG_ACTIONLOG_JOOMLA'), +(20, 'task', 'com_scheduler.task', 'id', 'title', '#__scheduler_tasks', 'PLG_ACTIONLOG_JOOMLA'); SELECT setval('#__action_log_config_id_seq', 20, false); @@ -846,6 +848,50 @@ CREATE TABLE "#__action_logs_users" ( CREATE INDEX "#__action_logs_users_idx_notify" ON "#__action_logs_users" ("notify"); +-- -------------------------------------------------------- + +-- +-- Table structure for table "#__scheduler_tasks" +-- + +CREATE TABLE IF NOT EXISTS "#__scheduler_tasks" +( + "id" serial NOT NULL, + "asset_id" bigint DEFAULT 0 NOT NULL, + "title" varchar(255) NOT NULL, + "type" varchar(128) NOT NULL, + "execution_rules" text, + "cron_rules" text, + "state" smallint DEFAULT 0 NOT NULL, + "last_exit_code" integer DEFAULT 0 NOT NULL, + "last_execution" timestamp without time zone, + "next_execution" timestamp without time zone, + "times_executed" integer DEFAULT 0 NOT NULL, + "times_failed" integer DEFAULT 0, + "locked" timestamp without time zone, + "priority" smallint DEFAULT 0 NOT NULL, + "ordering" bigint DEFAULT 0 NOT NULL, + "cli_exclusive" smallint DEFAULT 0 NOT NULL, + "params" text NOT NULL, + "note" text, + "created" timestamp without time zone NOT NULL, + "created_by" bigint DEFAULT 0 NOT NULL, + "checked_out" integer, + "checked_out_time" timestamp without time zone, + PRIMARY KEY ("id") +); + +CREATE INDEX "#__scheduler_tasks_idx_type" ON "#__scheduler_tasks" ("type"); +CREATE INDEX "#__scheduler_tasks_idx_state" ON "#__scheduler_tasks" ("state"); +CREATE INDEX "#__scheduler_tasks_idx_last_exit" ON "#__scheduler_tasks" ("last_exit_code"); +CREATE INDEX "#__scheduler_tasks_idx_next_exec" ON "#__scheduler_tasks" ("next_execution"); +CREATE INDEX "#__scheduler_tasks_idx_locked" ON "#__scheduler_tasks" ("locked"); +CREATE INDEX "#__scheduler_tasks_idx_priority" ON "#__scheduler_tasks" ("priority"); +CREATE INDEX "#__scheduler_tasks_idx_cli_exclusive" ON "#__scheduler_tasks" ("cli_exclusive"); +CREATE INDEX "#__scheduler_tasks_idx_checked_out" ON "#__scheduler_tasks" ("checked_out"); + +-- -------------------------------------------------------- + -- -- Here is SOUNDEX replacement for those who can't enable fuzzystrmatch module -- from contrib folder. diff --git a/installation/sql/postgresql/supports.sql b/installation/sql/postgresql/supports.sql index 121a79a1257b2..f9a053ee6e152 100644 --- a/installation/sql/postgresql/supports.sql +++ b/installation/sql/postgresql/supports.sql @@ -447,4 +447,8 @@ INSERT INTO "#__mail_templates" ("template_id", "extension", "language", "subjec ('com_users.registration.user.admin_activated', 'com_users', '', 'COM_USERS_EMAIL_ACTIVATED_BY_ADMIN_ACTIVATION_SUBJECT', 'COM_USERS_EMAIL_ACTIVATED_BY_ADMIN_ACTIVATION_BODY', '', '', '{"tags":["name","sitename","siteurl","username"]}'), ('com_users.registration.admin.verification_request', 'com_users', '', 'COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_SUBJECT', 'COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_BODY', '', '', '{"tags":["name","sitename","email","username","activate"]}'), ('plg_system_privacyconsent.request.reminder', 'plg_system_privacyconsent', '', 'PLG_SYSTEM_PRIVACYCONSENT_EMAIL_REMIND_SUBJECT', 'PLG_SYSTEM_PRIVACYCONSENT_EMAIL_REMIND_BODY', '', '', '{"tags":["sitename","url","tokenurl","formurl","token"]}'), -('com_messages.new_message', 'com_messages', '', 'COM_MESSAGES_NEW_MESSAGE', 'COM_MESSAGES_NEW_MESSAGE_BODY', '', '', '{"tags":["subject","message","fromname","sitename","siteurl","fromemail","toname","toemail"]}'); +('com_messages.new_message', 'com_messages', '', 'COM_MESSAGES_NEW_MESSAGE', 'COM_MESSAGES_NEW_MESSAGE_BODY', '', '', '{"tags":["subject","message","fromname","sitename","siteurl","fromemail","toname","toemail"]}'), +('plg_system_tasknotification.failure_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_FAILURE_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_FAILURE_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title", "exit_code", "exec_data_time", "task_output"]}'), +('plg_system_tasknotification.fatal_recovery_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title"]}'), +('plg_system_tasknotification.orphan_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title", ""]}'), +('plg_system_tasknotification.success_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_BODY', '', '', '{"tags":["task_id", "task_title", "exec_data_time", "task_output"]}'); diff --git a/libraries/src/Console/TasksListCommand.php b/libraries/src/Console/TasksListCommand.php new file mode 100644 index 0000000000000..71f6dccaf7927 --- /dev/null +++ b/libraries/src/Console/TasksListCommand.php @@ -0,0 +1,140 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Console; + +// Restrict direct access +defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Factory; +use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler; +use Joomla\Console\Application; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Console command to list scheduled tasks. + * + * @since __DEPLOY_VERSION__ + */ +class TasksListCommand extends AbstractCommand +{ + /** + * The default command name + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected static $defaultName = 'scheduler:list'; + + /** + * The console application object + * + * @var Application + * @since __DEPLOY_VERSION__ + */ + protected $application; + + /** + * @var SymfonyStyle + * @since __DEPLOY_VERSION__ + */ + private $ioStyle; + + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + Factory::getApplication()->getLanguage()->load('joomla', JPATH_ADMINISTRATOR); + + $this->configureIO($input, $output); + $this->ioStyle->title('List Scheduled Tasks'); + + $tasks = array_map( + function (\stdClass $task): array { + $enabled = $task->state === 1; + $nextExec = Factory::getDate($task->next_execution, 'UTC'); + $due = $enabled && $task->taskOption && Factory::getDate('now', 'UTC') > $nextExec; + + return [ + 'id' => $task->id, + 'title' => $task->title, + 'type' => $task->safeTypeTitle, + 'state' => $task->state === 1 ? 'Enabled' : ($task->state === 0 ? 'Disabled' : 'Trashed'), + 'next_execution' => $due ? 'DUE!' : $nextExec->toRFC822(), + ]; + }, + $this->getTasks() + ); + + $this->ioStyle->table(['id', 'title', 'type', 'state', 'next run'], $tasks); + + return 0; + } + + /** + * Returns a stdClass object array of scheduled tasks. + * + * @return array + * + * @since __DEPLOY_VERSION__ + * @throws \RunTimeException + */ + private function getTasks(): array + { + $scheduler = new Scheduler; + + return $scheduler->fetchTaskRecords( + ['state' => '*'], + ['ordering' => 'a.title', 'select' => 'a.id, a.title, a.type, a.state, a.next_execution'] + ); + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function configure(): void + { + $help = "%command.name% lists all scheduled tasks. + \nUsage: php %command.full_name%"; + + $this->setDescription('List all scheduled tasks'); + $this->setHelp($help); + } +} diff --git a/libraries/src/Console/TasksRunCommand.php b/libraries/src/Console/TasksRunCommand.php new file mode 100644 index 0000000000000..09090c12f714c --- /dev/null +++ b/libraries/src/Console/TasksRunCommand.php @@ -0,0 +1,155 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Console; + +// Restrict direct access +\defined('JPATH_PLATFORM') or die; + +use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler; +use Joomla\Component\Scheduler\Administrator\Task\Status; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Console command to run scheduled tasks. + * + * @since __DEPLOY_VERSION__ + */ +class TasksRunCommand extends AbstractCommand +{ + /** + * The default command name + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected static $defaultName = 'scheduler:run'; + + /** + * @var SymfonyStyle + * @since __DEPLOY_VERSION__ + */ + private $ioStyle; + + /** + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code. + * + * @since __DEPLOY_VERSION__ + * @throws \RunTimeException + * @throws InvalidArgumentException + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + /** + * Not as a class constant because of some the autoload order doesn't let us + * load the namespace when it's time to do that (why?) + */ + static $outTextMap = [ + Status::OK => 'Task#%1$02d \'%2$s\' processed in %3$.2f seconds.', + Status::NO_RUN => 'Task#%1$02d \'%2$s\' failed to run. Is it already running?', + Status::NO_ROUTINE => 'Task#%1$02d \'%2$s\' is orphaned! Visit the backend to resolve.', + 'N/A' => 'Task#%1$02d \'%2$s\' exited with code %4$d in %3$.2f seconds.', + ]; + + $this->configureIo($input, $output); + $this->ioStyle->title('Run tasks'); + + $scheduler = new Scheduler; + + $id = $input->getOption('id'); + $all = $input->getOption('all'); + + if ($id) + { + $records[] = $scheduler->fetchTaskRecord($id); + } + else + { + $filters = $scheduler::TASK_QUEUE_FILTERS; + $listConfig = $scheduler::TASK_QUEUE_LIST_CONFIG; + $listConfig['limit'] = ($all ? null : 1); + + $records = $scheduler->fetchTaskRecords($filters, $listConfig); + } + + if ($id && !$records[0]) + { + $this->ioStyle->writeln('No matching task found!'); + + return Status::NO_TASK; + } + elseif (!$records) + { + $this->ioStyle->writeln('No tasks due!'); + + return Status::NO_TASK; + } + + $status = ['startTime' => microtime(true)]; + $taskCount = \count($records); + $exit = Status::OK; + + foreach ($records as $record) + { + $cStart = microtime(true); + $task = $scheduler->runTask(['id' => $record->id, 'allowDisabled' => true, 'allowConcurrent' => true]); + $exit = empty($task) ? Status::NO_RUN : $task->getContent()['status']; + $duration = microtime(true) - $cStart; + $key = (\array_key_exists($exit, $outTextMap)) ? $exit : 'N/A'; + $this->ioStyle->writeln(sprintf($outTextMap[$key], $record->id, $record->title, $duration, $exit)); + } + + $netTime = round(microtime(true) - $status['startTime'], 2); + $this->ioStyle->newLine(); + $this->ioStyle->writeln("Finished running $taskCount tasks in $netTime seconds."); + + return $taskCount === 1 ? $exit : Status::OK; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function configure(): void + { + $this->addOption('id', 'i', InputOption::VALUE_REQUIRED, 'The id of the task to run.'); + $this->addOption('all', '', InputOption::VALUE_NONE, 'Run all due tasks. Note that this is overridden if --id is used.'); + + $help = "%command.name% run scheduled tasks. + \nUsage: php %command.full_name% [flags]"; + + $this->setDescription('Run one or more scheduled tasks'); + $this->setHelp($help); + } +} diff --git a/libraries/src/Console/TasksStateCommand.php b/libraries/src/Console/TasksStateCommand.php new file mode 100644 index 0000000000000..aaf7dafb01334 --- /dev/null +++ b/libraries/src/Console/TasksStateCommand.php @@ -0,0 +1,201 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Console; + +// Restrict direct access +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Application\ConsoleApplication; +use Joomla\CMS\Factory; +use Joomla\Component\Jobs\Administrator\Table\TaskTable; +use Joomla\Component\Scheduler\Administrator\Model\TaskModel; +use Joomla\Component\Scheduler\Administrator\Task\Task; +use Joomla\Console\Application; +use Joomla\Console\Command\AbstractCommand; +use Joomla\Utilities\ArrayHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Console command to change the state of tasks. + * + * @since __DEPLOY_VERSION__ + */ +class TasksStateCommand extends AbstractCommand +{ + /** + * The default command name + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected static $defaultName = 'scheduler:state'; + + /** + * The console application object + * + * @var Application + * + * @since __DEPLOY_VERSION__ + */ + protected $application; + + /** + * @var SymfonyStyle + * + * @since __DEPLOY_VERSION__ + */ + private $ioStyle; + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + Factory::getApplication()->getLanguage()->load('joomla', JPATH_ADMINISTRATOR); + + $this->configureIO($input, $output); + + $id = (string) $input->getOption('id'); + $state = (string) $input->getOption('state'); + + // Try to validate and process ID, if passed + if (\strlen($id)) + { + if (!Task::isValidId($id)) + { + $this->ioStyle->error('Invalid id passed!'); + + return 2; + } + + $id = (is_numeric($id)) ? ($id + 0) : $id; + } + + // Try to validate and process state, if passed + if (\strlen($state)) + { + // If we get the logical state, we try to get the enumeration (but as a string) + if (!is_numeric($state)) + { + $state = (string) ArrayHelper::arraySearch($state, Task::STATE_MAP); + } + + if (!\strlen($state) || !Task::isValidState($state)) + { + $this->ioStyle->error('Invalid state passed!'); + + return 2; + } + } + + // If we didn't get ID as a flag, ask for it interactively + while (!Task::isValidId($id)) + { + $id = $this->ioStyle->ask('Please specify the ID of the task'); + } + + // If we didn't get state as a flag, ask for it interactively + while ($state === false || !Task::isValidState($state)) + { + $state = (string) $this->ioStyle->ask('Should the state be "enable" (1), "disable" (0) or "trash" (-2)'); + + // Ensure we have the enumerated value (still as a string) + $state = (Task::isValidState($state)) ?: ArrayHelper::arraySearch($state, Task::STATE_MAP); + } + + // Finally, the enumerated state and id in their pure form + $state = (int) $state; + $id = (int) $id; + + /** @var ConsoleApplication $app */ + $app = $this->getApplication(); + + /** @var TaskModel $taskModel */ + $taskModel = $app->bootComponent('com_scheduler')->getMVCFactory()->createModel('Task', 'Administrator'); + + $task = $taskModel->getItem($id); + + // We couldn't fetch that task :( + if (empty($task->id)) + { + $this->ioStyle->error("Task ID '${id}' does not exist!"); + + return 1; + } + + // If the item is checked-out we need a check in (currently not possible through the CLI) + if ($taskModel->isCheckedOut($task)) + { + $this->ioStyle->error("Task ID '${id}' is checked out!"); + + return 1; + } + + /** @var TaskTable $table */ + $table = $taskModel->getTable(); + + $action = Task::STATE_MAP[$state]; + + if (!$table->publish($id, $state)) + { + $this->ioStyle->error("Can't ${action} Task ID '${id}'"); + + return 3; + } + + $this->ioStyle->success("Task ID ${id} ${action}."); + + return 0; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function configure(): void + { + $this->addOption('id', 'i', InputOption::VALUE_REQUIRED, 'The id of the task to change state.'); + $this->addOption('state', 's', InputOption::VALUE_REQUIRED, 'The new state of the task, can be 1/enable, 0/disable, or -2/trash.'); + + $help = "%command.name% changes the state of a task. + \nUsage: php %command.full_name%"; + + $this->setDescription('Enable, disable or trash a scheduled task'); + $this->setHelp($help); + } +} diff --git a/libraries/src/Extension/ExtensionHelper.php b/libraries/src/Extension/ExtensionHelper.php index a13bcf45985ac..dac98d8f63263 100644 --- a/libraries/src/Extension/ExtensionHelper.php +++ b/libraries/src/Extension/ExtensionHelper.php @@ -79,6 +79,7 @@ class ExtensionHelper array('component', 'com_postinstall', '', 1), array('component', 'com_privacy', '', 1), array('component', 'com_redirect', '', 1), + array('component', 'com_scheduler', '', 1), array('component', 'com_tags', '', 1), array('component', 'com_templates', '', 1), array('component', 'com_users', '', 1), @@ -283,13 +284,21 @@ class ExtensionHelper array('plugin', 'privacyconsent', 'system', 0), array('plugin', 'redirect', 'system', 0), array('plugin', 'remember', 'system', 0), + array('plugin', 'schedulerunner', 'system', 0), array('plugin', 'sef', 'system', 0), array('plugin', 'sessiongc', 'system', 0), array('plugin', 'skipto', 'system', 0), array('plugin', 'stats', 'system', 0), + array('plugin', 'tasknotification', 'system', 0), array('plugin', 'updatenotification', 'system', 0), array('plugin', 'webauthn', 'system', 0), + // Core plugin extensions - task scheduler + array('plugin', 'checkfiles', 'task', 0), + array('plugin', 'demotasks', 'task', 0), + array('plugin', 'requests', 'task', 0), + array('plugin', 'sitestatus', 'task', 0), + // Core plugin extensions - two factor authentication array('plugin', 'totp', 'twofactorauth', 0), array('plugin', 'yubikey', 'twofactorauth', 0), diff --git a/libraries/src/Service/Provider/Application.php b/libraries/src/Service/Provider/Application.php index 24ba5cdd4d099..d9dac4adababa 100644 --- a/libraries/src/Service/Provider/Application.php +++ b/libraries/src/Service/Provider/Application.php @@ -15,10 +15,10 @@ use Joomla\CMS\Application\ConsoleApplication; use Joomla\CMS\Application\SiteApplication; use Joomla\CMS\Console\CheckJoomlaUpdatesCommand; -use Joomla\CMS\Console\ExtensionInstallCommand; use Joomla\CMS\Console\ExtensionDiscoverCommand; use Joomla\CMS\Console\ExtensionDiscoverInstallCommand; use Joomla\CMS\Console\ExtensionDiscoverListCommand; +use Joomla\CMS\Console\ExtensionInstallCommand; use Joomla\CMS\Console\ExtensionRemoveCommand; use Joomla\CMS\Console\ExtensionsListCommand; use Joomla\CMS\Console\FinderIndexCommand; @@ -30,6 +30,9 @@ use Joomla\CMS\Console\SetConfigurationCommand; use Joomla\CMS\Console\SiteDownCommand; use Joomla\CMS\Console\SiteUpCommand; +use Joomla\CMS\Console\TasksListCommand; +use Joomla\CMS\Console\TasksRunCommand; +use Joomla\CMS\Console\TasksStateCommand; use Joomla\CMS\Console\UpdateCoreCommand; use Joomla\CMS\Factory; use Joomla\CMS\Language\LanguageFactoryInterface; @@ -147,23 +150,26 @@ function (Container $container) function (Container $container) { $mapping = [ - SessionGcCommand::getDefaultName() => SessionGcCommand::class, - SessionMetadataGcCommand::getDefaultName() => SessionMetadataGcCommand::class, - ExportCommand::getDefaultName() => ExportCommand::class, - ImportCommand::getDefaultName() => ImportCommand::class, - SiteDownCommand::getDefaultName() => SiteDownCommand::class, - SiteUpCommand::getDefaultName() => SiteUpCommand::class, - SetConfigurationCommand::getDefaultName() => SetConfigurationCommand::class, - GetConfigurationCommand::getDefaultName() => GetConfigurationCommand::class, - ExtensionsListCommand::getDefaultName() => ExtensionsListCommand::class, - CheckJoomlaUpdatesCommand::getDefaultName() => CheckJoomlaUpdatesCommand::class, - ExtensionRemoveCommand::getDefaultName() => ExtensionRemoveCommand::class, - ExtensionInstallCommand::getDefaultName() => ExtensionInstallCommand::class, - ExtensionDiscoverCommand::getDefaultName() => ExtensionDiscoverCommand::class, - ExtensionDiscoverInstallCommand::getDefaultName() => ExtensionDiscoverInstallCommand::class, - ExtensionDiscoverListCommand::getDefaultName() => ExtensionDiscoverListCommand::class, - UpdateCoreCommand::getDefaultName() => UpdateCoreCommand::class, - FinderIndexCommand::getDefaultName() => FinderIndexCommand::class, + SessionGcCommand::getDefaultName() => SessionGcCommand::class, + SessionMetadataGcCommand::getDefaultName() => SessionMetadataGcCommand::class, + ExportCommand::getDefaultName() => ExportCommand::class, + ImportCommand::getDefaultName() => ImportCommand::class, + SiteDownCommand::getDefaultName() => SiteDownCommand::class, + SiteUpCommand::getDefaultName() => SiteUpCommand::class, + SetConfigurationCommand::getDefaultName() => SetConfigurationCommand::class, + GetConfigurationCommand::getDefaultName() => GetConfigurationCommand::class, + ExtensionsListCommand::getDefaultName() => ExtensionsListCommand::class, + CheckJoomlaUpdatesCommand::getDefaultName() => CheckJoomlaUpdatesCommand::class, + ExtensionRemoveCommand::getDefaultName() => ExtensionRemoveCommand::class, + ExtensionInstallCommand::getDefaultName() => ExtensionInstallCommand::class, + ExtensionDiscoverCommand::getDefaultName() => ExtensionDiscoverCommand::class, + ExtensionDiscoverInstallCommand::getDefaultName() => ExtensionDiscoverInstallCommand::class, + ExtensionDiscoverListCommand::getDefaultName() => ExtensionDiscoverListCommand::class, + UpdateCoreCommand::getDefaultName() => UpdateCoreCommand::class, + FinderIndexCommand::getDefaultName() => FinderIndexCommand::class, + TasksListCommand::getDefaultName() => TasksListCommand::class, + TasksRunCommand::getDefaultName() => TasksRunCommand::class, + TasksStateCommand::getDefaultName() => TasksStateCommand::class, ]; return new WritableContainerLoader($container, $mapping); diff --git a/libraries/src/Service/Provider/Console.php b/libraries/src/Service/Provider/Console.php index e3ac6823daeee..797a07f0d7b1c 100644 --- a/libraries/src/Service/Provider/Console.php +++ b/libraries/src/Service/Provider/Console.php @@ -11,10 +11,10 @@ \defined('JPATH_PLATFORM') or die; use Joomla\CMS\Console\CheckJoomlaUpdatesCommand; -use Joomla\CMS\Console\ExtensionInstallCommand; use Joomla\CMS\Console\ExtensionDiscoverCommand; use Joomla\CMS\Console\ExtensionDiscoverInstallCommand; use Joomla\CMS\Console\ExtensionDiscoverListCommand; +use Joomla\CMS\Console\ExtensionInstallCommand; use Joomla\CMS\Console\ExtensionRemoveCommand; use Joomla\CMS\Console\ExtensionsListCommand; use Joomla\CMS\Console\FinderIndexCommand; @@ -24,6 +24,9 @@ use Joomla\CMS\Console\SetConfigurationCommand; use Joomla\CMS\Console\SiteDownCommand; use Joomla\CMS\Console\SiteUpCommand; +use Joomla\CMS\Console\TasksListCommand; +use Joomla\CMS\Console\TasksRunCommand; +use Joomla\CMS\Console\TasksStateCommand; use Joomla\CMS\Console\UpdateCoreCommand; use Joomla\CMS\Session\MetadataManager; use Joomla\Database\Command\ExportCommand; @@ -208,5 +211,30 @@ function (Container $container) }, true ); + + $container->share( + TasksListCommand::class, + function (Container $container) + { + return new TasksListCommand; + }, + true + ); + + $container->share( + TasksRunCommand::class, + function (Container $container) + { + return new TasksRunCommand; + } + ); + + $container->share( + TasksStateCommand::class, + function (Container $container) + { + return new TasksStateCommand; + } + ); } } diff --git a/plugins/system/schedulerunner/schedulerunner.php b/plugins/system/schedulerunner/schedulerunner.php new file mode 100644 index 0000000000000..dfea6eb541846 --- /dev/null +++ b/plugins/system/schedulerunner/schedulerunner.php @@ -0,0 +1,389 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Restrict direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Component\ComponentHelper; +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\CMS\Router\Route; +use Joomla\CMS\Table\Extension; +use Joomla\CMS\User\UserHelper; +use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler; +use Joomla\Component\Scheduler\Administrator\Task\Task; +use Joomla\Event\Event; +use Joomla\Event\EventInterface; +use Joomla\Event\SubscriberInterface; +use Joomla\Registry\Registry; + +/** + * This plugin implements listeners to support a visitor-triggered lazy-scheduling pattern. + * If `com_scheduler` is installed/enabled and its configuration allows unprotected lazy scheduling, this plugin + * injects into each response with an HTML context a JS file {@see PlgSystemSchedulerunner::injectScheduleRunner()} that + * sets up an AJAX callback to trigger the scheduler {@see PlgSystemSchedulerunner::runScheduler()}. This is achieved + * through a call to the `com_ajax` component. + * Also supports the scheduler component configuration form through auto-generation of the webcron key and injection + * of JS of usability enhancement. + * + * @since __DEPLOY_VERSION__ + */ +class PlgSystemSchedulerunner extends CMSPlugin implements SubscriberInterface +{ + /** + * Length of auto-generated webcron key. + * + * @var integer + * @since __DEPLOY_VERSION__ + */ + private const WEBCRON_KEY_LENGTH = 20; + + /** + * @var CMSApplication + * @since __DEPLOY_VERSION__ + */ + protected $app; + + /** + * @inheritDoc + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + * + * @throws Exception + */ + public static function getSubscribedEvents(): array + { + $config = ComponentHelper::getParams('com_scheduler'); + $app = Factory::getApplication(); + + $mapping = []; + + if ($app->isClient('site') || $app->isClient('administrator')) + { + $mapping['onBeforeCompileHead'] = 'injectLazyJS'; + $mapping['onAjaxRunSchedulerLazy'] = 'runLazyCron'; + + // Only allowed in the frontend + if ($app->isClient('site')) + { + if ($config->get('webcron.enabled')) + { + $mapping['onAjaxRunSchedulerWebcron'] = 'runWebCron'; + } + } + elseif ($app->isClient('administrator')) + { + $mapping['onContentPrepareForm'] = 'enhanceSchedulerConfig'; + $mapping['onExtensionBeforeSave'] = 'generateWebcronKey'; + + $mapping['onAjaxRunSchedulerTest'] = 'runTestCron'; + } + } + + return $mapping; + } + + /** + * Inject JavaScript to trigger the scheduler in HTML contexts. + * + * @param EventInterface $event The onBeforeCompileHead event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function injectLazyJS(EventInterface $event): void + { + // Only inject in HTML documents + if ($this->app->getDocument()->getType() !== 'html') + { + return; + } + + $config = ComponentHelper::getParams('com_scheduler'); + + if (!$config->get('lazy_scheduler.enabled', true)) + { + return; + } + + // Check if any task is due to decrease the load + $model = $this->app->bootComponent('com_scheduler') + ->getMVCFactory()->createModel('Tasks', 'Administrator', ['ignore_request' => true]); + + $model->setState('filter.state', 1); + $model->setState('filter.due', 1); + + $items = $model->getItems(); + + // See if we are running currently + $model->setState('filter.locked', 1); + $model->setState('filter.due', 0); + + $items2 = $model->getItems(); + + if (empty($items) || !empty($items2)) + { + return; + } + + // Add configuration options + $triggerInterval = $config->get('lazy_scheduler.interval', 300); + $this->app->getDocument()->addScriptOptions('plg_system_schedulerunner', ['interval' => $triggerInterval]); + + // Load and injection directive + $wa = $this->app->getDocument()->getWebAssetManager(); + $wa->getRegistry()->addExtensionRegistryFile('plg_system_schedulerunner'); + $wa->useScript('plg_system_schedulerunner.run-schedule'); + } + + /** + * Acts on the LazyCron trigger from the frontend when Lazy Cron is enabled in the Scheduler component + * configuration. The lazy cron trigger is implemented in client-side JavaScript which is injected on every page + * load with an HTML context when the component configuration allows it. This method then triggers the Scheduler, + * which effectively runs the next Task in the Scheduler's task queue. + * + * @param EventInterface $e The onAjaxRunSchedulerLazy event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws Exception + */ + public function runLazyCron(EventInterface $e) + { + $config = ComponentHelper::getParams('com_scheduler'); + + if (!$config->get('lazy_scheduler.enabled', true)) + { + return; + } + + // Since `navigator.sendBeacon()` may time out, allow execution after disconnect if possible. + if (function_exists('ignore_user_abort')) + { + ignore_user_abort(true); + } + + // Prevent PHP from trying to output to the user pipe. PHP may kill the script otherwise if the pipe is not accessible. + ob_start(); + + // Suppress all errors to avoid any output + try + { + $this->runScheduler(); + } + catch (Exception $e) + { + } + + ob_end_clean(); + } + + /** + * This method is responsible for the WebCron functionality of the Scheduler component.
+ * Acting on a `com_ajax` call, this method can work in two ways: + * 1. If no Task ID is specified, it triggers the Scheduler to run the next task in + * the task queue. + * 2. If a Task ID is specified, it fetches the task (if it exists) from the Scheduler API and executes it.
+ * + * URL query parameters: + * - `hash` string (required) Webcron hash (from the Scheduler component configuration). + * - `id` int (optional) ID of the task to trigger. + * + * @param Event $event The onAjaxRunSchedulerWebcron event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws Exception + */ + public function runWebCron(Event $event) + { + $config = ComponentHelper::getParams('com_scheduler'); + $hash = $config->get('webcron.key', ''); + + if (!$config->get('webcron.enabled', false)) + { + Log::add(Text::_('PLG_SYSTEM_SCHEDULE_RUNNER_WEBCRON_DISABLED')); + throw new Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + if (!strlen($hash) || $hash !== $this->app->input->get('hash')) + { + throw new Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $id = (int) $this->app->input->getInt('id', 0); + + $task = $this->runScheduler($id); + + if (!empty($task) && !empty($task->getContent()['exception'])) + { + throw $task->getContent()['exception']; + } + } + + /** + * This method is responsible for the "test run" functionality in the Scheduler administrator backend interface. + * Acting on a `com_ajax` call, this method requires the URL to have a `id` query parameter (corresponding to an + * existing Task ID). + * + * @param Event $event The onAjaxRunScheduler event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws Exception + */ + public function runTestCron(Event $event) + { + $id = (int) $this->app->input->getInt('id'); + $allowConcurrent = $this->app->input->getBool('allowConcurrent', false); + + $user = Factory::getApplication()->getIdentity(); + + if (empty($id) || !$user->authorise('core.testrun', 'com_scheduler.task.' . $id)) + { + throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** + * ?: About allow simultaneous, how do we detect if it failed because of pre-existing lock? + * + * We will allow CLI exclusive tasks to be fetched and executed, it's left to routines to do a runtime check + * if they want to refuse normal operation. + */ + $task = (new Scheduler)->getTask( + [ + 'id' => $id, + 'allowDisabled' => true, + 'bypassScheduling' => true, + 'allowConcurrent' => $allowConcurrent, + ] + ); + + if (!is_null($task)) + { + $task->run(); + $event->addArgument('result', $task->getContent()); + } + + else + { + /** + * Placeholder result, but the idea is if we failed to fetch the task, it's likely because another task was + * already running. This is a fair assumption if this test run was triggered through the administrator backend, + * so we know the task probably exists and is either enabled/disabled (not trashed). + */ + // @todo language constant + review if this is done right. + $event->addArgument('result', ['message' => 'could not acquire lock on task. retry or allow concurrency.']); + } + } + + /** + * Run the scheduler, allowing execution of a single due task. + * Does not bypass task scheduling, meaning that even if an ID is passed the task is only + * triggered if it is due. + * + * @param integer $id The optional ID of the task to run + * + * @return ?Task + * + * @since __DEPLOY_VERSION__ + * @throws RuntimeException + */ + protected function runScheduler(int $id = 0): ?Task + { + return (new Scheduler)->runTask(['id' => $id]); + } + + /** + * Enhance the scheduler config form by dynamically populating or removing display fields. + * + * @param EventInterface $event The onContentPrepareForm event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws UnexpectedValueException|RuntimeException + * + * @todo Move to another plugin? + */ + public function enhanceSchedulerConfig(EventInterface $event): void + { + /** @var Form $form */ + $form = $event->getArgument('0'); + $data = $event->getArgument('1'); + + if ($form->getName() !== 'com_config.component' + || $this->app->input->get('component') !== 'com_scheduler') + { + return; + } + + if (!empty($data['webcron']['key'])) + { + $form->removeField('generate_key_on_save', 'webcron'); + + $relative = 'index.php?option=com_ajax&plugin=RunSchedulerWebcron&group=system&format=json&hash=' . $data['webcron']['key']; + $link = Route::link('site', $relative, false, Route::TLS_IGNORE, true); + $form->setValue('base_link', 'webcron', $link); + } + else + { + $form->removeField('base_link', 'webcron'); + $form->removeField('reset_key', 'webcron'); + } + } + + /** + * Auto-generate a key/hash for the webcron functionality. + * This method acts on table save, when a hash doesn't already exist or a reset is required. + * @todo Move to another plugin? + * + * @param EventInterface $event The onExtensionBeforeSave event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function generateWebcronKey(EventInterface $event): void + { + /** @var Extension $table */ + [$context, $table] = $event->getArguments(); + + if ($context !== 'com_config.component' + || ($table->name ?? '') !== 'COM_SCHEDULER') + { + return; + } + + $params = new Registry($table->params ?? ''); + + if (empty($params->get('webcron.key')) + || $params->get('webcron.reset_key') === 1) + { + $params->set('webcron.key', UserHelper::genRandomPassword(self::WEBCRON_KEY_LENGTH)); + } + + $params->remove('webcron.base_link'); + $params->remove('webcron.reset_key'); + $table->params = $params->toString(); + } +} diff --git a/plugins/system/schedulerunner/schedulerunner.xml b/plugins/system/schedulerunner/schedulerunner.xml new file mode 100644 index 0000000000000..9c5f0607d64c5 --- /dev/null +++ b/plugins/system/schedulerunner/schedulerunner.xml @@ -0,0 +1,23 @@ + + + PLG_SYSTEM_SCHEDULERUNNER + Joomla! Project + August 2021 + (C) 2021 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 4.1 + PLG_SYSTEM_SCHEDULERUNNER_XML_DESCRIPTION + + js + joomla.asset.json + + + schedulerunner.php + + + language/en-GB/plg_system_schedulerunner.ini + language/en-GB/plg_system_schedulerunner.sys.ini + + diff --git a/plugins/system/tasknotification/forms/task_notification.xml b/plugins/system/tasknotification/forms/task_notification.xml new file mode 100644 index 0000000000000..45f4d2dd38a5b --- /dev/null +++ b/plugins/system/tasknotification/forms/task_notification.xml @@ -0,0 +1,49 @@ + +
+ + +
+ + + + + + + + + + + + + + + + +
+
+
+
diff --git a/plugins/system/tasknotification/language/en-GB/plg_system_tasknotification.ini b/plugins/system/tasknotification/language/en-GB/plg_system_tasknotification.ini new file mode 100644 index 0000000000000..ba7edac52e748 --- /dev/null +++ b/plugins/system/tasknotification/language/en-GB/plg_system_tasknotification.ini @@ -0,0 +1,16 @@ +PLG_SYSTEM_TASK_NOTIFICATION="System - Task Notification" +PLG_SYSTEM_TASK_NOTIFICATION_FAILURE_MAIL_BODY="Hello,\n\n\nPlanned execution of Scheduled Task#{TASK_ID}, {TASK_TITLE}, has failed with exit code {EXIT_CODE} at {EXEC_DATE_TIME}.\n\nPlease visit the Joomla! backend for more information.\n\n{TASK_OUTPUT}" +PLG_SYSTEM_TASK_NOTIFICATION_FAILURE_MAIL_SUBJECT="Task Failure" +PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_BODY="Hello,\n\nPlanned execution of Scheduler Task#{TASK_ID}, {TASK_TITLE}, recovered from a fatal failure.\n\nThis could mean that the task execution exhausted the system resources or the restrictions from the PHP INI.\n\nPlease visit the Joomla! backend for more information." +PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_SUBJECT="Task Recover from Fatal Failure" +PLG_SYSTEM_TASK_NOTIFICATION_LABEL_FAILURE_MAIL_TOGGLE="Notifications on Task Failure" +PLG_SYSTEM_TASK_NOTIFICATION_LABEL_FATAL_FAILURE_MAIL_TOGGLE="Notifications on Fatal Failures/Crashes (Recommended)" +PLG_SYSTEM_TASK_NOTIFICATION_LABEL_ORPHANED_TASK_MAIL_TOGGLE="Notifications on Orphaned Tasks (Recommended)" +PLG_SYSTEM_TASK_NOTIFICATION_LABEL_SUCCESS_MAIL_TOGGLE="Notifications on Task Success" +PLG_SYSTEM_TASK_NOTIFICATION_NO_MAIL_SENT="Could not send task notification to any user. This either means that mailer is not set up properly or no user with system emails enabled, com_scheduler `core.manage` privilege exists." +PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_BODY="Hello,\n\nScheduled Task#{TASK_ID}, {TASK_TITLE}, has been orphaned. This likely means that the provider plugin was removed or disabled from your Joomla! installation.\n\nPlease visit the Joomla! backend to investigate." +PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_SUBJECT="New Orphaned Task" +PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_BODY="Hello,\n\nScheduled Task#{TASK_ID}, {TASK_TITLE}, has been successfully executed at {EXEC_DATE_TIME}.\n\n{TASK_OUTPUT}" +PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_SUBJECT="Task Successful" +PLG_SYSTEM_TASK_NOTIFICATION_USER_FETCH_FAIL="Failed to fetch users to send notifications to." +PLG_SYSTEM_TASK_NOTIFICATION_XML_DESCRIPTION="Responsible for email notifications for execution of Scheduled tasks." diff --git a/plugins/system/tasknotification/language/en-GB/plg_system_tasknotification.sys.ini b/plugins/system/tasknotification/language/en-GB/plg_system_tasknotification.sys.ini new file mode 100644 index 0000000000000..dffb2ef6e97f2 --- /dev/null +++ b/plugins/system/tasknotification/language/en-GB/plg_system_tasknotification.sys.ini @@ -0,0 +1,2 @@ +PLG_SYSTEM_TASK_NOTIFICATION="System - Task Notification" +PLG_SYSTEM_TASK_NOTIFICATION_XML_DESCRIPTION="Responsible for email notifications for execution of Scheduled tasks." diff --git a/plugins/system/tasknotification/tasknotification.php b/plugins/system/tasknotification/tasknotification.php new file mode 100644 index 0000000000000..31798a0f141b5 --- /dev/null +++ b/plugins/system/tasknotification/tasknotification.php @@ -0,0 +1,339 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Restrict direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Factory; +use Joomla\CMS\Filesystem\File; +use Joomla\CMS\Filesystem\Path; +use Joomla\CMS\Form\Form; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Log\Log; +use Joomla\CMS\Mail\MailTemplate; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Scheduler\Administrator\Task\Status; +use Joomla\Component\Scheduler\Administrator\Task\Task; +use Joomla\Database\DatabaseInterface; +use Joomla\Event\Event; +use Joomla\Event\EventInterface; +use Joomla\Event\SubscriberInterface; +use PHPMailer\PHPMailer\Exception as MailerException; + +/** + * This plugin implements email notification functionality for Tasks configured through the Scheduler component. + * Notification configuration is supported on a per-task basis, which can be set-up through the Task item form, made + * possible by injecting the notification fields into the item form with a `onContentPrepareForm` listener.
+ * + * Notifications can be set-up on: task success, failure, fatal failure (task running too long or crashing the request), + * or on _orphaned_ task routines (missing parent plugin - either uninstalled, disabled or no longer offering a routine + * with the same ID). + * + * @since __DEPLOY_VERSION__ + */ +class PlgSystemTasknotification extends CMSPlugin implements SubscriberInterface +{ + /** + * The task notification form. This form is merged into the task item form by {@see + * injectTaskNotificationFieldset()}. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private const TASK_NOTIFICATION_FORM = 'task_notification'; + + /** + * @var CMSApplication + * @since __DEPLOY_VERSION__ + */ + protected $app; + + /** + * @var DatabaseInterface + * @since __DEPLOY_VERSION__ + */ + protected $db; + + /** + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $autoloadLanguage = true; + + + /** + * @inheritDoc + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public static function getSubscribedEvents(): array + { + return [ + 'onContentPrepareForm' => 'injectTaskNotificationFieldset', + 'onTaskExecuteSuccess' => 'notifySuccess', + 'onTaskExecuteFailure' => 'notifyFailure', + 'onTaskRoutineNotFound' => 'notifyOrphan', + 'onTaskRecoverFailure' => 'notifyFatalRecovery', + ]; + } + + /** + * Inject fields to support configuration of post-execution notifications into the task item form. + * + * @param EventInterface $event The onContentPrepareForm event. + * + * @return boolean True if successful. + * + * @since __DEPLOY_VERSION__ + */ + public function injectTaskNotificationFieldset(EventInterface $event): bool + { + /** @var Form $form */ + $form = $event->getArgument('0'); + + if ($form->getName() !== 'com_scheduler.task') + { + return true; + } + + $formFile = __DIR__ . "/forms/" . self::TASK_NOTIFICATION_FORM . '.xml'; + + try + { + $formFile = Path::check($formFile); + } + catch (Exception $e) + { + // Log? + return false; + } + + if (!File::exists($formFile)) + { + return false; + } + + return $form->loadFile($formFile); + } + + /** + * Send out email notifications on Task execution failure if task configuration allows it. + * + * @param Event $event The onTaskExecuteFailure event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + public function notifyFailure(Event $event): void + { + /** @var Task $task */ + $task = $event->getArgument('subject'); + + if (!(int) $task->get('params.notifications.failure_mail', 1)) + { + return; + } + + // @todo safety checks, multiple files [?] + $outFile = $event->getArgument('subject')->snapshot['output_file'] ?? ''; + $data = $this->getDataFromTask($event->getArgument('subject')); + $this->sendMail('plg_system_tasknotification.failure_mail', $data, $outFile); + } + + /** + * Send out email notifications on orphaned task if task configuration allows.
+ * A task is `orphaned` if the task's parent plugin has been removed/disabled, or no longer offers a task + * with the same routine ID. + * + * @param Event $event The onTaskRoutineNotFound event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + public function notifyOrphan(Event $event): void + { + /** @var Task $task */ + $task = $event->getArgument('subject'); + + if (!(int) $task->get('params.notifications.orphan_mail', 1)) + { + return; + } + + $data = $this->getDataFromTask($event->getArgument('subject')); + $this->sendMail('plg_system_tasknotification.orphan_mail', $data); + } + + /** + * Send out email notifications on Task execution success if task configuration allows. + * + * @param Event $event The onTaskExecuteSuccess event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + public function notifySuccess(Event $event): void + { + /** @var Task $task */ + $task = $event->getArgument('subject'); + + if (!(int) $task->get('params.notifications.success_mail', 0)) + { + return; + } + + // @todo safety checks, multiple files [?] + $outFile = $event->getArgument('subject')->snapshot['output_file'] ?? ''; + $data = $this->getDataFromTask($event->getArgument('subject')); + $this->sendMail('plg_system_tasknotification.success_mail', $data, $outFile); + } + + /** + * Send out email notifications on fatal recovery of task execution if task configuration allows.
+ * Fatal recovery indicated that the task either crashed the parent process or its execution lasted longer + * than the global task timeout (this is configurable through the Scheduler component configuration). + * In the latter case, the global task timeout should be adjusted so that this false positive can be avoided. + * This stands as a limitation of the Scheduler's current task execution implementation, which doesn't involve + * keeping track of the parent PHP process which could enable keeping track of the task's status. + * + * @param Event $event The onTaskRecoverFailure event. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + public function notifyFatalRecovery(Event $event): void + { + /** @var Task $task */ + $task = $event->getArgument('subject'); + + if (!(int) $task->get('params.notifications.fatal_failure_mail', 1)) + { + return; + } + + $data = $this->getDataFromTask($event->getArgument('subject')); + $this->sendMail('plg_system_tasknotification.fatal_recovery_mail', $data); + } + + /** + * @param Task $task A task object + * + * @return array An array of data to bind to a mail template. + * + * @since __DEPLOY_VERSION__ + */ + private function getDataFromTask(Task $task): array + { + $lockOrExecTime = Factory::getDate($task->get('locked') ?? $task->get('last_execution'))->toRFC822(); + + return [ + 'TASK_ID' => $task->get('id'), + 'TASK_TITLE' => $task->get('title'), + 'EXIT_CODE' => $task->getContent()['status'] ?? Status::NO_EXIT, + 'EXEC_DATE_TIME' => $lockOrExecTime, + 'TASK_OUTPUT' => $task->getContent()['output_body'] ?? '', + ]; + } + + /** + * @param string $template The mail template. + * @param array $data The data to bind to the mail template. + * @param string $attachment The attachment to send with the mail (@todo multiple) + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + private function sendMail(string $template, array $data, string $attachment = ''): void + { + $app = $this->app; + $db = $this->db; + + /** @var UserFactoryInterface $userFactory */ + $userFactory = Factory::getContainer()->get('user.factory'); + + // Get all users who are not blocked and have opted in for system mails. + $query = $db->getQuery(true); + + $query->select($db->qn(['name', 'email', 'sendEmail', 'id'])) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('sendEmail') . ' = 1') + ->where($db->quoteName('block') . ' = 0'); + + $db->setQuery($query); + + try + { + $users = $db->loadObjectList(); + } + catch (RuntimeException $e) + { + return; + } + + if ($users === null) + { + Log::add(Text::_('PLG_SYSTEM_TASK_NOTIFICATION_USER_FETCH_FAIL'), Log::ERROR); + + return; + } + + $mailSent = false; + + // Mail all matching users who also have the `core.manage` privilege for com_scheduler. + foreach ($users as $user) + { + $user = $userFactory->loadUserById($user->id); + + if ($user->authorise('core.manage', 'com_scheduler')) + { + try + { + $mailer = new MailTemplate($template, $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($user->email); + + if (!empty($attachment) + && File::exists($attachment) + && is_file($attachment)) + { + // @todo we allow multiple files [?] + $attachName = pathinfo($attachment, PATHINFO_BASENAME); + $mailer->addAttachment($attachName, $attachment); + } + + $mailer->send(); + $mailSent = true; + } + catch (MailerException $exception) + { + Log::add(Text::_('PLG_SYSTEM_TASK_NOTIFICATION_NOTIFY_SEND_EMAIL_FAIL'), Log::ERROR); + } + } + } + + if (!$mailSent) + { + Log::add(Text::_('PLG_SYSTEM_TASK_NOTIFICATION_NO_MAIL_SENT'), Log::WARNING); + } + } +} diff --git a/plugins/system/tasknotification/tasknotification.xml b/plugins/system/tasknotification/tasknotification.xml new file mode 100644 index 0000000000000..f238225a69a59 --- /dev/null +++ b/plugins/system/tasknotification/tasknotification.xml @@ -0,0 +1,20 @@ + + + plg_system_task_notification + Joomla! Project + September 2021 + (C) 2021 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 4.1 + PLG_SYSTEM_TASK_NOTIFICATION_XML_DESCRIPTION + + tasknotification.php + language + + + language/en-GB/plg_system_tasknotification.ini + language/en-GB/plg_system_tasknotification.sys.ini + + diff --git a/plugins/task/checkfiles/checkfiles.php b/plugins/task/checkfiles/checkfiles.php new file mode 100644 index 0000000000000..c7df59a6c7e84 --- /dev/null +++ b/plugins/task/checkfiles/checkfiles.php @@ -0,0 +1,139 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Restrict direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Filesystem\Folder; +use Joomla\CMS\Image\Image; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; +use Joomla\Component\Scheduler\Administrator\Task\Status as TaskStatus; +use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait; +use Joomla\Event\SubscriberInterface; + +/** + * Task plugin with routines that offer checks on files.
+ * At the moment, offers a single routine to check and resize image files in a directory. + * + * @since __DEPLOY_VERSION__ + */ +class PlgTaskCheckfiles extends CMSPlugin implements SubscriberInterface +{ + use TaskPluginTrait; + + /** + * @var string[] + * + * @since __DEPLOY_VERSION__ + */ + protected const TASKS_MAP = [ + 'checkfiles.imagesize' => [ + 'langConstPrefix' => 'PLG_TASK_CHECK_FILES_TASK_IMAGE_SIZE', + 'form' => 'image_size', + 'method' => 'checkImages', + ], + ]; + + /** + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $autoloadLanguage = true; + + /** + * @inheritDoc + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public static function getSubscribedEvents(): array + { + return [ + 'onTaskOptionsList' => 'advertiseRoutines', + 'onExecuteTask' => 'standardRoutineHandler', + 'onContentPrepareForm' => 'enhanceTaskItemForm', + ]; + } + + /** + * @param ExecuteTaskEvent $event The onExecuteTask event + * + * @return integer The exit code + * + * @since __DEPLOY_VERSION__ + * @throws RuntimeException + * @throws LogicException + */ + protected function checkImages(ExecuteTaskEvent $event): int + { + $params = $event->getArgument('params'); + + $path = JPATH_ROOT . '/images/' . $params->path; + $dimension = $params->dimension; + $limit = $params->limit; + + if (!Folder::exists($path)) + { + $this->logTask(Text::_('PLG_TASK_CHECK_FILES_LOG_IMAGE_PATH_NA'), 'warning'); + + return TaskStatus::NO_RUN; + } + + $images = Folder::files($path, '^.*\.(jpg|jpeg|png|gif|webp)', 2, true); + + foreach ($images as $imageFilename) + { + $properties = Image::getImageFileProperties($imageFilename); + $resize = $properties->$dimension > $limit; + + if (!$resize) + { + continue; + } + + $height = $properties->height; + $width = $properties->width; + + $newHeight = $dimension === 'height' ? $limit : $height * $limit / $width; + $newWidth = $dimension === 'width' ? $limit : $width * $limit / $height; + + $this->logTask(Text::sprintf('PLG_TASK_CHECK_FILES_LOG_RESIZING_IMAGE', $width, $height, $newWidth, $newHeight, $imageFilename)); + + $image = new Image($imageFilename); + + try + { + $image->resize($newWidth, $newHeight); + } + catch (LogicException $e) + { + $this->logTask('PLG_TASK_CHECK_FILES_LOG_RESIZE_FAIL', 'error'); + $resizeFail = true; + } + + if (!empty($resizeFail)) + { + return TaskStatus::KNOCKOUT; + } + + if (!$image->toFile($imageFilename, $properties->type)) + { + $this->logTask('PLG_TASK_CHECK_FILES_LOG_IMAGE_SAVE_FAIL', 'error'); + } + + // We do at most a single resize per execution + break; + } + + return TaskStatus::OK; + } +} diff --git a/plugins/task/checkfiles/checkfiles.xml b/plugins/task/checkfiles/checkfiles.xml new file mode 100644 index 0000000000000..1c0b366f646e4 --- /dev/null +++ b/plugins/task/checkfiles/checkfiles.xml @@ -0,0 +1,21 @@ + + + plg_task_check_files + Joomla! Project + August 2021 + (C) 2021 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 4.1 + PLG_TASK_CHECK_FILES_XML_DESCRIPTION + + checkfiles.php + language + forms + + + language/en-GB/plg_task_checkfiles.ini + language/en-GB/plg_task_checkfiles.sys.ini + + diff --git a/plugins/task/checkfiles/forms/image_size.xml b/plugins/task/checkfiles/forms/image_size.xml new file mode 100644 index 0000000000000..16e403b30b337 --- /dev/null +++ b/plugins/task/checkfiles/forms/image_size.xml @@ -0,0 +1,34 @@ + +
+ +
+ + + + + + +
+
+
diff --git a/plugins/task/checkfiles/language/en-GB/plg_task_checkfiles.ini b/plugins/task/checkfiles/language/en-GB/plg_task_checkfiles.ini new file mode 100644 index 0000000000000..118ab90d9cf58 --- /dev/null +++ b/plugins/task/checkfiles/language/en-GB/plg_task_checkfiles.ini @@ -0,0 +1,16 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_TASK_CHECK_FILES="Task - Check Files" +PLG_TASK_CHECK_FILES_LABEL_DIMENSION_LIMIT="Limit" +PLG_TASK_CHECK_FILES_LABEL_DIRECTORY="Directory" +PLG_TASK_CHECK_FILES_LABEL_IMAGE_DIMENSION="Dimension" +PLG_TASK_CHECK_FILES_LOG_IMAGE_PATH_NA="Image path does exist!" +PLG_TASK_CHECK_FILES_LOG_IMAGE_SAVE_FAIL="Failed to save image file" +PLG_TASK_CHECK_FILES_LOG_RESIZE_FAIL="Failed to resize image due to an error in plugin logic..." +PLG_TASK_CHECK_FILES_LOG_RESIZING_IMAGE="Found image of size %1$sx%2$s px; resizing to %3$sx%4$s px. File: %5$s" +PLG_TASK_CHECK_FILES_TASK_IMAGE_SIZE_DESC="Check images, resize if larger than allowed." +PLG_TASK_CHECK_FILES_TASK_IMAGE_SIZE_TITLE="Image Size Check!" +PLG_TASK_CHECK_FILES_XML_DESCRIPTION="Offers task routines for checking for oversized files, and related actions if possible." diff --git a/plugins/task/checkfiles/language/en-GB/plg_task_checkfiles.sys.ini b/plugins/task/checkfiles/language/en-GB/plg_task_checkfiles.sys.ini new file mode 100644 index 0000000000000..11efd630767fd --- /dev/null +++ b/plugins/task/checkfiles/language/en-GB/plg_task_checkfiles.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_TASK_CHECK_FILES="Task - Check Files" +PLG_TASK_CHECK_FILES_XML_DESCRIPTION="Offers task routines for checking for oversized files, and related actions if possible." diff --git a/plugins/task/demotasks/demotasks.php b/plugins/task/demotasks/demotasks.php new file mode 100644 index 0000000000000..97c385bf465a2 --- /dev/null +++ b/plugins/task/demotasks/demotasks.php @@ -0,0 +1,172 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Restrict direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; +use Joomla\Component\Scheduler\Administrator\Task\Status; +use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait; +use Joomla\Event\SubscriberInterface; + +/** + * A demo task plugin. Offers 3 task routines and demonstrates the use of {@see TaskPluginTrait}, + * {@see ExecuteTaskEvent}. + * + * @since __DEPLOY__VERSION__ + */ +class PlgTaskDemotasks extends CMSPlugin implements SubscriberInterface +{ + use TaskPluginTrait; + + /** + * @var string[] + * @since __DEPLOY_VERSION__ + */ + private const TASKS_MAP = [ + 'demoTask_r1.sleep' => [ + 'langConstPrefix' => 'PLG_TASK_DEMO_TASKS_TASK_SLEEP', + 'method' => 'sleep', + 'form' => 'testTaskForm', + ], + 'demoTask_r2.memoryStressTest' => [ + 'langConstPrefix' => 'PLG_TASK_DEMO_TASKS_STRESS_MEMORY', + 'method' => 'stressMemory', + ], + 'demoTask_r3.memoryStressTestOverride' => [ + 'langConstPrefix' => 'PLG_TASK_DEMO_TASKS_STRESS_MEMORY_OVERRIDE', + 'method' => 'stressMemoryRemoveLimit', + ], + ]; + + /** + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $autoloadLanguage = true; + + /** + * @inheritDoc + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public static function getSubscribedEvents(): array + { + return [ + 'onTaskOptionsList' => 'advertiseRoutines', + 'onExecuteTask' => 'standardRoutineHandler', + 'onContentPrepareForm' => 'enhanceTaskItemForm', + ]; + } + + /** + * @param ExecuteTaskEvent $event The `onExecuteTask` event. + * + * @return integer The routine exit code. + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + private function sleep(ExecuteTaskEvent $event): int + { + $timeout = (int) $event->getArgument('params')->timeout ?? 1; + + $this->logTask(sprintf('Starting %d timeout', $timeout)); + sleep($timeout); + $this->logTask(sprintf('%d timeout over!', $timeout)); + + return Status::OK; + } + + /** + * Standard routine method for the memory test routine. + * + * @param ExecuteTaskEvent $event The `onExecuteTask` event. + * + * @return integer The routine exit code. + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + private function stressMemory(ExecuteTaskEvent $event): int + { + $mLimit = $this->getMemoryLimit(); + $this->logTask(sprintf('Memory Limit: %d KB', $mLimit)); + + $iMem = $cMem = memory_get_usage(); + $i = 0; + + while ($cMem + ($cMem - $iMem) / ++$i <= $mLimit) + { + $this->logTask(sprintf('Current memory usage: %d KB', $cMem)); + ${"array" . $i} = array_fill(0, 100000, 1); + } + + return Status::OK; + } + + /** + * Standard routine method for the memory test routine, also attempts to override the memory limit set by the PHP + * INI. + * + * @param ExecuteTaskEvent $event The `onExecuteTask` event. + * + * @return integer The routine exit code. + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + private function stressMemoryRemoveLimit(ExecuteTaskEvent $event): int + { + $success = false; + + if (function_exists('ini_set')) + { + $success = ini_set('memory_limit', -1) !== false; + } + + $this->logTask('Memory limit override ' . $success ? 'successful' : 'failed'); + + return $this->stressMemory($event); + } + + /** + * Processes the PHP ini memory_limit setting, returning the memory limit in KB + * + * @return float + * + * @since __DEPLOY_VERSION__ + */ + private function getMemoryLimit(): float + { + $memoryLimit = ini_get('memory_limit'); + + if (preg_match('/^(\d+)(.)$/', $memoryLimit, $matches)) + { + if ($matches[2] == 'M') + { + // * nnnM -> nnn MB + $memoryLimit = $matches[1] * 1024 * 1024; + } + else + { + if ($matches[2] == 'K') + { + // * nnnK -> nnn KB + $memoryLimit = $matches[1] * 1024; + } + } + } + + return (float) $memoryLimit; + } +} diff --git a/plugins/task/demotasks/demotasks.xml b/plugins/task/demotasks/demotasks.xml new file mode 100644 index 0000000000000..25ec1ec22efb2 --- /dev/null +++ b/plugins/task/demotasks/demotasks.xml @@ -0,0 +1,21 @@ + + + plg_task_demo_tasks + Joomla! Project + July 2021 + (C) 2021 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 4.1 + Demo task routines for com_scheduler + + demotasks.php + language + forms + + + language/en-GB/plg_task_demotasks.ini + language/en-GB/plg_task_demotasks.sys.ini + + diff --git a/plugins/task/demotasks/forms/testTaskForm.xml b/plugins/task/demotasks/forms/testTaskForm.xml new file mode 100644 index 0000000000000..bba438e7c690e --- /dev/null +++ b/plugins/task/demotasks/forms/testTaskForm.xml @@ -0,0 +1,18 @@ + +
+ +
+ +
+
+
diff --git a/plugins/task/demotasks/language/en-GB/plg_task_demotasks.ini b/plugins/task/demotasks/language/en-GB/plg_task_demotasks.ini new file mode 100644 index 0000000000000..056a9de80e62e --- /dev/null +++ b/plugins/task/demotasks/language/en-GB/plg_task_demotasks.ini @@ -0,0 +1,15 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_TASK_DEMO_TASKS="Task - Demo Tasks!" +PLG_TASK_DEMO_TASKS_SLEEP_TIMEOUT_LABEL="Sleep Timeout (seconds)" +PLG_TASK_DEMO_TASKS_STRESS_MEMORY_DESC="What happens to a task when the PHP memory limit is exhausted?" +PLG_TASK_DEMO_TASKS_STRESS_MEMORY_OVERRIDE_DESC="What happens to a task when the system memory is exhausted?" +PLG_TASK_DEMO_TASKS_STRESS_MEMORY_OVERRIDE_TITLE="Stress Memory, Override Limit" +PLG_TASK_DEMO_TASKS_STRESS_MEMORY_TITLE="Stress Memory" +PLG_TASK_DEMO_TASKS_TASK_SLEEP_DESC="Sleep, do nothing for x seconds." +PLG_TASK_DEMO_TASKS_TASK_SLEEP_ROUTINE_END_LOG_MESSAGE="TestTask1 return code is: %1$d. Processing Time: %2$.2f seconds" +PLG_TASK_DEMO_TASKS_TASK_SLEEP_TITLE="Demo Task - Sleep" +PLG_TASK_DEMO_TASKS_XML_DESCRIPTION="This is a demo plugin for the development of Joomla! Scheduled Tasks." diff --git a/plugins/task/demotasks/language/en-GB/plg_task_demotasks.sys.ini b/plugins/task/demotasks/language/en-GB/plg_task_demotasks.sys.ini new file mode 100644 index 0000000000000..eae925bdd1289 --- /dev/null +++ b/plugins/task/demotasks/language/en-GB/plg_task_demotasks.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_TASK_DEMO_TASKS="Task - Demo Tasks!" +PLG_TASK_DEMO_TASKS_XML_DESCRIPTION="This is a demo plugin for the development of Joomla! Scheduled Tasks." diff --git a/plugins/task/requests/forms/get_requests.xml b/plugins/task/requests/forms/get_requests.xml new file mode 100644 index 0000000000000..00bdc0440bf73 --- /dev/null +++ b/plugins/task/requests/forms/get_requests.xml @@ -0,0 +1,53 @@ + +
+ +
+ + + + + + + + + + + +
+
+
diff --git a/plugins/task/requests/language/en-GB/plg_task_requests.ini b/plugins/task/requests/language/en-GB/plg_task_requests.ini new file mode 100644 index 0000000000000..293644e86a90f --- /dev/null +++ b/plugins/task/requests/language/en-GB/plg_task_requests.ini @@ -0,0 +1,20 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_TASK_REQUESTS="Task - Requests" +PLG_TASK_REQUESTS_BEARER="Bearer" +PLG_TASK_REQUESTS_JOOMLA_TOKEN="X-Joomla-Token" +PLG_TASK_REQUESTS_LABEL_AUTH="Authorization" +PLG_TASK_REQUESTS_LABEL_AUTH_HEADER="Authorization Header" +PLG_TASK_REQUESTS_LABEL_AUTH_KEY="Authorization Key" +PLG_TASK_REQUESTS_LABEL_REQUEST_TIMEOUT="Request Timeout" +PLG_TASK_REQUESTS_LABEL_REQUEST_URL="Request URL" +PLG_TASK_REQUESTS_TASK_GET_REQUEST_DESC="Make GET requests to a server. Supports a custom timeout and authorization headers." +PLG_TASK_REQUESTS_TASK_GET_REQUEST_LOG_RESPONSE="Request response code was: %1$d" +PLG_TASK_REQUESTS_TASK_GET_REQUEST_LOG_TIMEOUT="GET request failed or timed out." +PLG_TASK_REQUESTS_TASK_GET_REQUEST_LOG_UNWRITEABLE_OUTPUT="Unable write output file!" +PLG_TASK_REQUESTS_TASK_GET_REQUEST_ROUTINE_END_LOG_MESSAGE="GET return code is: %1$d. Processing Time: %2$.2f seconds" +PLG_TASK_REQUESTS_TASK_GET_REQUEST_TITLE="GET Request" +PLG_TASK_REQUESTS_XML_DESCRIPTION="Job plugin to make GET requests to a server." diff --git a/plugins/task/requests/language/en-GB/plg_task_requests.sys.ini b/plugins/task/requests/language/en-GB/plg_task_requests.sys.ini new file mode 100644 index 0000000000000..148a036d31676 --- /dev/null +++ b/plugins/task/requests/language/en-GB/plg_task_requests.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_TASK_REQUESTS="Task - Requests" +PLG_TASK_REQUESTS_XML_DESCRIPTION="Job plugin to make GET requests to a server." diff --git a/plugins/task/requests/requests.php b/plugins/task/requests/requests.php new file mode 100644 index 0000000000000..1e8b0337729ae --- /dev/null +++ b/plugins/task/requests/requests.php @@ -0,0 +1,141 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Restrict direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Filesystem\File; +use Joomla\CMS\Filesystem\Path; +use Joomla\CMS\Http\HttpFactory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; +use Joomla\Component\Scheduler\Administrator\Task\Status as TaskStatus; +use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait; +use Joomla\Event\SubscriberInterface; +use Joomla\Registry\Registry; + +/** + * Task plugin with routines to make HTTP requests.
+ * At the moment, offers a single routine for GET requests. + * + * @since __DEPLOY_VERSION__ + */ +class PlgTaskRequests extends CMSPlugin implements SubscriberInterface +{ + use TaskPluginTrait; + + /** + * @var string[] + * @since __DEPLOY_VERSION__ + */ + protected const TASKS_MAP = [ + 'plg_task_requests_task_get' => [ + 'langConstPrefix' => 'PLG_TASK_REQUESTS_TASK_GET_REQUEST', + 'form' => 'get_requests', + 'method' => 'makeGetRequest', + ], + ]; + + /** + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $autoloadLanguage = true; + + /** + * @inheritDoc + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public static function getSubscribedEvents(): array + { + return [ + 'onTaskOptionsList' => 'advertiseRoutines', + 'onExecuteTask' => 'standardRoutineHandler', + 'onContentPrepareForm' => 'enhanceTaskItemForm', + ]; + } + + /** + * Standard routine method for the get request routine. + * + * @param ExecuteTaskEvent $event The onExecuteTask event + * + * @return integer The exit code + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + protected function makeGetRequest(ExecuteTaskEvent $event): int + { + $id = $event->getTaskId(); + $params = $event->getArgument('params'); + + $url = $params->url; + $timeout = $params->timeout; + $auth = (string) $params->auth ?? 0; + $authType = (string) $params->authType ?? ''; + $authKey = (string) $params->authKey ?? ''; + $headers = []; + + if ($auth && $authType && $authKey) + { + $headers = [$authType => $authKey]; + } + + $options = new Registry; + + try + { + $response = HttpFactory::getHttp($options)->get($url, $headers, $timeout); + } + catch (Exception $e) + { + $this->logTask(Text::sprintf('PLG_TASK_REQUESTS_TASK_GET_REQUEST_LOG_TIMEOUT')); + + return TaskStatus::TIMEOUT; + } + + $responseCode = $response->code; + $responseBody = $response->body; + + // @todo this handling must be rethought and made safe. stands as a good demo right now. + $responseFilename = Path::clean(JPATH_ROOT . "/tmp/task_{$id}_response.html"); + + if (File::write($responseFilename, $responseBody)) + { + $this->snapshot['output_file'] = $responseFilename; + $responseStatus = 'SAVED'; + } + else + { + $this->logTask('PLG_TASK_REQUESTS_TASK_GET_REQUEST_LOG_UNWRITEABLE_OUTPUT', 'error'); + $responseStatus = 'NOT_SAVED'; + } + + $this->snapshot['output'] = <<< EOF +======= Task Output Body ======= +> URL: $url +> Response Code: $responseCode +> Response: $responseStatus +EOF; + + $this->logTask(Text::sprintf('PLG_TASK_REQUESTS_TASK_GET_REQUEST_LOG_RESPONSE', $responseCode)); + + if ($response->code !== 200) + { + return TaskStatus::KNOCKOUT; + } + + return TaskStatus::OK; + } +} diff --git a/plugins/task/requests/requests.xml b/plugins/task/requests/requests.xml new file mode 100644 index 0000000000000..1bb8a65e92d33 --- /dev/null +++ b/plugins/task/requests/requests.xml @@ -0,0 +1,21 @@ + + + plg_task_requests + Joomla! Project + August 2021 + (C) 2021 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 4.1 + PLG_TASK_REQUESTS_XML_DESCRIPTION + + requests.php + language + forms + + + language/en-GB/plg_task_requests.ini + language/en-GB/plg_task_requests.sys.ini + + diff --git a/plugins/task/sitestatus/language/en-GB/plg_task_sitestatus.ini b/plugins/task/sitestatus/language/en-GB/plg_task_sitestatus.ini new file mode 100644 index 0000000000000..508ea4e010ba4 --- /dev/null +++ b/plugins/task/sitestatus/language/en-GB/plg_task_sitestatus.ini @@ -0,0 +1,20 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_TASK_SITE_STATUS="Task - Site Status" +PLG_TASK_SITE_STATUS_DESC="Toggles the site's status on each run." +PLG_TASK_SITE_STATUS_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE="Could not make configuration.php un-writable." +PLG_TASK_SITE_STATUS_ERROR_CONFIGURATION_PHP_NOTWRITABLE="Could not make configuration.php writable." +PLG_TASK_SITE_STATUS_ERROR_WRITE_FAILED="Could not write to the configuration file!" +PLG_TASK_SITE_STATUS_ROUTINE_END_LOG_MESSAGE="ToggleOffline return code is: %1$d. Processing Time: %2$.2f seconds." +PLG_TASK_SITE_STATUS_TASK_LOG_SITE_STATUS="Site was %1$s, is now %2$s." +PLG_TASK_SITE_STATUS_SET_OFFLINE_DESC="Sets site offline to online on each run." +PLG_TASK_SITE_STATUS_SET_OFFLINE_ROUTINE_END_LOG_MESSAGE="SetOffline return code is: %1$d. Processing Time: %2$.2f seconds." +PLG_TASK_SITE_STATUS_SET_OFFLINE_TITLE="Set Site Offline." +PLG_TASK_SITE_STATUS_SET_ONLINE_DESC="Sets site status to online on each run." +PLG_TASK_SITE_STATUS_SET_ONLINE_ROUTINE_END_LOG_MESSAGE="SetOnline return code is: %1$d. Processing Time: %2$.2f seconds." +PLG_TASK_SITE_STATUS_SET_ONLINE_TITLE="Set Site Online." +PLG_TASK_SITE_STATUS_TITLE="Toggle Offline." +PLG_TASK_SITE_STATUS_XML_DESCRIPTION="Offers task routines to change the site's offline status." diff --git a/plugins/task/sitestatus/language/en-GB/plg_task_sitestatus.sys.ini b/plugins/task/sitestatus/language/en-GB/plg_task_sitestatus.sys.ini new file mode 100644 index 0000000000000..eb0735664baad --- /dev/null +++ b/plugins/task/sitestatus/language/en-GB/plg_task_sitestatus.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_TASK_SITE_STATUS="Task - Site Status" +PLG_TASK_SITE_STATUS_XML_DESCRIPTION="Offers task routines to change the site's offline status." diff --git a/plugins/task/sitestatus/sitestatus.php b/plugins/task/sitestatus/sitestatus.php new file mode 100644 index 0000000000000..7147f69d80c3e --- /dev/null +++ b/plugins/task/sitestatus/sitestatus.php @@ -0,0 +1,172 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Restrict direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Filesystem\File; +use Joomla\CMS\Filesystem\Path; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; +use Joomla\Component\Scheduler\Administrator\Task\Status; +use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait; +use Joomla\Event\SubscriberInterface; +use Joomla\Registry\Registry; +use Joomla\Utilities\ArrayHelper; + +/** + * Task plugin with routines to change the offline status of the site. These routines can be used to control planned + * maintenance periods and related operations. + * + * @since __DEPLOY_VERSION__ + */ +class PlgTaskSitestatus extends CMSPlugin implements SubscriberInterface +{ + use TaskPluginTrait; + + /** + * @var string[] + * @since __DEPLOY_VERSION__ + */ + protected const TASKS_MAP = [ + 'plg_task_toggle_offline' => [ + 'langConstPrefix' => 'PLG_TASK_SITE_STATUS', + 'toggle' => true, + ], + 'plg_task_toggle_offline_set_online' => [ + 'langConstPrefix' => 'PLG_TASK_SITE_STATUS_SET_ONLINE', + 'toggle' => false, + 'offline' => false, + ], + 'plg_task_toggle_offline_set_offline' => [ + 'langConstPrefix' => 'PLG_TASK_SITE_STATUS_SET_OFFLINE', + 'toggle' => false, + 'offline' => true, + ], + + ]; + + /** + * The application object. + * + * @var CMSApplication + * @since __DEPLOY_VERSION__ + */ + protected $app; + + /** + * Autoload the language file. + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $autoloadLanguage = true; + + /** + * @inheritDoc + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public static function getSubscribedEvents(): array + { + return [ + 'onTaskOptionsList' => 'advertiseRoutines', + 'onExecuteTask' => 'alterSiteStatus', + ]; + } + + /** + * @param ExecuteTaskEvent $event The onExecuteTask event + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + public function alterSiteStatus(ExecuteTaskEvent $event): void + { + if (!array_key_exists($event->getRoutineId(), self::TASKS_MAP)) + { + return; + } + + $this->startRoutine($event); + + $config = ArrayHelper::fromObject(new JConfig); + + $toggle = self::TASKS_MAP[$event->getRoutineId()]['toggle']; + $oldStatus = $config['offline'] ? 'offline' : 'online'; + + if ($toggle) + { + $config['offline'] = !$config['offline']; + } + else + { + $offline = self::TASKS_MAP[$event->getRoutineId()]['offline']; + $config['offline'] = $offline; + } + + $newStatus = $config['offline'] ? 'offline' : 'online'; + $exit = $this->writeConfigFile(new Registry($config)); + $this->logTask(Text::sprintf('PLG_TASK_SITE_STATUS_TASK_LOG_SITE_STATUS', $oldStatus, $newStatus)); + + $this->endRoutine($event, $exit); + } + + /** + * Method to write the configuration to a file. + * + * @param Registry $config A Registry object containing all global config data. + * + * @return integer The task exit code + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + private function writeConfigFile(Registry $config): int + { + // Set the configuration file path. + $file = JPATH_CONFIGURATION . '/configuration.php'; + + // Attempt to make the file writeable. + if (Path::isOwner($file) && !Path::setPermissions($file)) + { + $this->logTask(Text::_('PLG_TASK_SITE_STATUS_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'notice'); + } + + // Attempt to write the configuration file as a PHP class named JConfig. + $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); + + if (!File::write($file, $configuration)) + { + $this->logTask(Text::_('PLG_TASK_SITE_STATUS_ERROR_WRITE_FAILED'), 'error'); + + return Status::KNOCKOUT; + } + + // Invalidates the cached configuration file + if (function_exists('opcache_invalidate')) + { + opcache_invalidate($file); + } + + // Attempt to make the file un-writeable. + if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) + { + $this->logTask(Text::_('PLG_TASK_SITE_STATUS_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'notice'); + } + + return Status::OK; + } +} diff --git a/plugins/task/sitestatus/sitestatus.xml b/plugins/task/sitestatus/sitestatus.xml new file mode 100644 index 0000000000000..ce7ff4ec33d74 --- /dev/null +++ b/plugins/task/sitestatus/sitestatus.xml @@ -0,0 +1,21 @@ + + + plg_task_site_status + Joomla! Project + August 2021 + (C) 2021 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 4.1 + PLG_TASK_SITE_STATUS_XML_DESCRIPTION + + sitestatus.php + language + forms + + + language/en-GB/plg_task_sitestatus.ini + language/en-GB/plg_task_sitestatus.sys.ini + +