diff --git a/wcfsetup/install/files/acp/js/WCF.ACP.js b/wcfsetup/install/files/acp/js/WCF.ACP.js
index c34a1349ee2..989bea0b5ff 100644
--- a/wcfsetup/install/files/acp/js/WCF.ACP.js
+++ b/wcfsetup/install/files/acp/js/WCF.ACP.js
@@ -16,110 +16,6 @@ WCF.ACP = { };
*/
WCF.ACP.Application = { };
-/**
- * Namespace for ACP cronjob management.
- */
-WCF.ACP.Cronjob = { };
-
-/**
- * Handles the manual execution of cronjobs.
- */
-WCF.ACP.Cronjob.ExecutionHandler = Class.extend({
- /**
- * notification object
- * @var WCF.System.Notification
- */
- _notification: null,
-
- /**
- * action proxy
- * @var WCF.Action.Proxy
- */
- _proxy: null,
-
- /**
- * Initializes WCF.ACP.Cronjob.ExecutionHandler object.
- */
- init: function() {
- this._proxy = new WCF.Action.Proxy({
- success: $.proxy(this._success, this)
- });
-
- $('.jsCronjobRow .jsExecuteButton').click($.proxy(this._click, this));
-
- this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success');
- },
-
- /**
- * Handles a click on an execute button.
- *
- * @param object event
- */
- _click: function(event) {
- this._proxy.setOption('data', {
- actionName: 'execute',
- className: 'wcf\\data\\cronjob\\CronjobAction',
- objectIDs: [ $(event.target).data('objectID') ]
- });
-
- this._proxy.sendRequest();
- },
-
- /**
- * Handles successful cronjob execution.
- *
- * @param object data
- * @param string textStatus
- * @param jQuery jqXHR
- */
- _success: function(data, textStatus, jqXHR) {
- $('.jsCronjobRow').each($.proxy(function(index, row) {
- var $button = $(row).find('.jsExecuteButton');
- var $objectID = ($button).data('objectID');
-
- if (WCF.inArray($objectID, data.objectIDs)) {
- if (data.returnValues[$objectID]) {
- // insert feedback here
- $(row).find('td.columnNextExec').html(data.returnValues[$objectID].formatted);
- $(row).wcfHighlight();
- }
-
- this._notification.show();
-
- return false;
- }
- }, this));
- }
-});
-
-/**
- * Handles the cronjob log list.
- */
-WCF.ACP.Cronjob.LogList = Class.extend({
- /**
- * Initializes WCF.ACP.Cronjob.LogList object.
- */
- init: function() {
- // bind event listener to delete cronjob log button
- $('.jsCronjobLogDelete').click(function() {
- WCF.System.Confirmation.show(WCF.Language.get('wcf.acp.cronjob.log.clear.confirm'), function(action) {
- if (action == 'confirm') {
- new WCF.Action.Proxy({
- autoSend: true,
- data: {
- actionName: 'clearAll',
- className: 'wcf\\data\\cronjob\\log\\CronjobLogAction'
- },
- success: function() {
- window.location.reload();
- }
- });
- }
- });
- });
- }
-});
-
/**
* Namespace for ACP package management.
*/
diff --git a/wcfsetup/install/files/acp/templates/cronjobList.tpl b/wcfsetup/install/files/acp/templates/cronjobList.tpl
index 1a910d16fdd..a5e21562a15 100644
--- a/wcfsetup/install/files/acp/templates/cronjobList.tpl
+++ b/wcfsetup/install/files/acp/templates/cronjobList.tpl
@@ -1,14 +1,8 @@
{include file='header' pageTitle='wcf.acp.cronjob.list'}
-
-
-{hascontent}
-
-{/hascontent}
-
-{hascontent}
-
-
-
-
- | {lang}wcf.global.objectID{/lang} |
- {lang}wcf.acp.cronjob.expression{/lang} |
- {lang}wcf.acp.cronjob.description{/lang} |
- {lang}wcf.acp.package.name{/lang} |
- {lang}wcf.acp.cronjob.nextExec{/lang} |
-
- {event name='columnHeads'}
-
-
-
-
- {content}
- {foreach from=$objects item=cronjob}
-
- |
-
-
- {if $cronjob->canBeDisabled()}
- {objectAction action="toggle" isDisabled=$cronjob->isDisabled}
- {else}
- {if !$cronjob->isDisabled}
-
- {icon name='square-check'}
-
- {else}
-
- {icon name='square'}
-
- {/if}
- {/if}
-
- {if $cronjob->isEditable()}
- {icon name='pencil'}
- {else}
-
- {icon name='pencil'}
-
- {/if}
- {if $cronjob->isDeletable()}
- {objectAction action="delete" objectTitle=$cronjob->getDescription()}
- {else}
-
- {icon name='xmark'}
-
- {/if}
-
- {event name='rowButtons'}
- |
- {@$cronjob->cronjobID} |
-
- {$cronjob->getExpression()}
- |
-
- {if $cronjob->isEditable()}
- {$cronjob->getDescription()}
- {else}
- {$cronjob->getDescription()}
- {/if}
- |
-
- {$cronjob->getPackage()}
- |
-
- {if !$cronjob->isDisabled && $cronjob->nextExec != 1}
- {@$cronjob->nextExec|plainTime}
- {/if}
- |
-
- {event name='columns'}
-
- {/foreach}
- {/content}
-
-
-
-{hascontentelse}
- {lang}wcf.global.noItems{/lang}
-{/hascontent}
-
-
+
+ {unsafe:$gridView->render()}
+
{include file='footer'}
diff --git a/wcfsetup/install/files/lib/acp/page/CronjobListPage.class.php b/wcfsetup/install/files/lib/acp/page/CronjobListPage.class.php
index 598a7c44f23..722c8f55921 100755
--- a/wcfsetup/install/files/lib/acp/page/CronjobListPage.class.php
+++ b/wcfsetup/install/files/lib/acp/page/CronjobListPage.class.php
@@ -2,19 +2,20 @@
namespace wcf\acp\page;
-use wcf\data\cronjob\I18nCronjobList;
-use wcf\page\SortablePage;
+use wcf\page\AbstractGridViewPage;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\gridView\admin\CronjobGridView;
/**
* Shows information about configured cron jobs.
*
- * @author Alexander Ebert
- * @copyright 2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License
+ * @author Olaf Braun, Alexander Ebert
+ * @copyright 2001-2025 WoltLab GmbH
+ * @license GNU Lesser General Public License
*
- * @property I18nCronjobList $objectList
+ * @property CronjobGridView $gridView
*/
-class CronjobListPage extends SortablePage
+class CronjobListPage extends AbstractGridViewPage
{
/**
* @inheritDoc
@@ -26,38 +27,9 @@ class CronjobListPage extends SortablePage
*/
public $neededPermissions = ['admin.management.canManageCronjob'];
- /**
- * @inheritDoc
- */
- public $defaultSortField = 'descriptionI18n';
-
- /**
- * @inheritDoc
- */
- public $itemsPerPage = 100;
-
- /**
- * @inheritDoc
- */
- public $validSortFields = [
- 'cronjobID',
- 'nextExec',
- 'descriptionI18n',
- 'packageID',
- ];
-
- /**
- * @inheritDoc
- */
- public $objectListClassName = I18nCronjobList::class;
-
- /**
- * @inheritDoc
- */
- public function initObjectList()
+ #[\Override]
+ protected function createGridViewController(): AbstractGridView
{
- parent::initObjectList();
-
- $this->sqlOrderBy = "cronjob." . $this->sortField . " " . $this->sortOrder;
+ return new CronjobGridView();
}
}
diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php
index 9b972a7fa94..e40f59fa0e6 100644
--- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php
+++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php
@@ -162,6 +162,10 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) {
$event->register(new \wcf\system\endpoint\controller\core\articles\PublishArticle());
$event->register(new \wcf\system\endpoint\controller\core\articles\UnpublishArticle());
$event->register(new \wcf\system\endpoint\controller\core\attachments\DeleteAttachment());
+ $event->register(new \wcf\system\endpoint\controller\core\cronjobs\EnableCronjob());
+ $event->register(new \wcf\system\endpoint\controller\core\cronjobs\DisableCronjob());
+ $event->register(new \wcf\system\endpoint\controller\core\cronjobs\DeleteCronjob());
+ $event->register(new \wcf\system\endpoint\controller\core\cronjobs\ExecuteCronjob());
}
);
diff --git a/wcfsetup/install/files/lib/event/gridView/admin/CronjobGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/admin/CronjobGridViewInitialized.class.php
new file mode 100644
index 00000000000..d52281f098b
--- /dev/null
+++ b/wcfsetup/install/files/lib/event/gridView/admin/CronjobGridViewInitialized.class.php
@@ -0,0 +1,21 @@
+
+ * @since 6.2
+ */
+final class CronjobGridViewInitialized implements IPsr14Event
+{
+ public function __construct(public readonly CronjobGridView $gridView)
+ {
+ }
+}
diff --git a/wcfsetup/install/files/lib/event/interaction/admin/CronjobInteractionCollecting.class.php b/wcfsetup/install/files/lib/event/interaction/admin/CronjobInteractionCollecting.class.php
new file mode 100644
index 00000000000..78e13d6081a
--- /dev/null
+++ b/wcfsetup/install/files/lib/event/interaction/admin/CronjobInteractionCollecting.class.php
@@ -0,0 +1,21 @@
+
+ * @since 6.2
+ */
+final class CronjobInteractionCollecting implements IPsr14Event
+{
+ public function __construct(public readonly CronjobInteractions $provider)
+ {
+ }
+}
diff --git a/wcfsetup/install/files/lib/event/interaction/bulk/admin/CronjobBulkInteractionCollecting.class.php b/wcfsetup/install/files/lib/event/interaction/bulk/admin/CronjobBulkInteractionCollecting.class.php
new file mode 100644
index 00000000000..738f2fd1c9d
--- /dev/null
+++ b/wcfsetup/install/files/lib/event/interaction/bulk/admin/CronjobBulkInteractionCollecting.class.php
@@ -0,0 +1,21 @@
+
+ * @since 6.2
+ */
+final class CronjobBulkInteractionCollecting implements IPsr14Event
+{
+ public function __construct(public readonly CronjobBulkInteractions $provider)
+ {
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/DeleteCronjob.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/DeleteCronjob.class.php
new file mode 100644
index 00000000000..eeeb4335469
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/DeleteCronjob.class.php
@@ -0,0 +1,47 @@
+
+ * @since 6.2
+ */
+#[DeleteRequest('/core/cronjobs/{id:\d+}')]
+final class DeleteCronjob implements IController
+{
+ #[\Override]
+ public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+ {
+ $cronjob = Helper::fetchObjectFromRequestParameter($variables['id'], Cronjob::class);
+
+ $this->assertCronjobCanBeDeleted($cronjob);
+
+ (new CronjobAction([$cronjob], 'delete'))->executeAction();
+
+ return new JsonResponse([]);
+ }
+
+ private function assertCronjobCanBeDeleted(Cronjob $cronjob): void
+ {
+ WCF::getSession()->checkPermissions(['admin.management.canManageCronjob']);
+
+ if (!$cronjob->isDeletable()) {
+ throw new PermissionDeniedException();
+ }
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/DisableCronjob.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/DisableCronjob.class.php
new file mode 100644
index 00000000000..b9607383788
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/DisableCronjob.class.php
@@ -0,0 +1,51 @@
+
+ * @since 6.2
+ */
+#[PostRequest('/core/cronjobs/{id:\d+}/disable')]
+final class DisableCronjob implements IController
+{
+ #[\Override]
+ public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+ {
+ $cronjob = Helper::fetchObjectFromRequestParameter($variables['id'], Cronjob::class);
+
+ $this->assertCronjobCanBeDisabled($cronjob);
+
+ (new CronjobAction([$cronjob], 'toggle'))->executeAction();
+
+ return new JsonResponse([]);
+ }
+
+ private function assertCronjobCanBeDisabled(Cronjob $cronjob): void
+ {
+ WCF::getSession()->checkPermissions(['admin.management.canManageCronjob']);
+
+ if ($cronjob->canBeDisabled()) {
+ throw new PermissionDeniedException();
+ }
+
+ if ($cronjob->isDisabled) {
+ throw new PermissionDeniedException();
+ }
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/EnableCronjob.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/EnableCronjob.class.php
new file mode 100644
index 00000000000..c17b690a031
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/EnableCronjob.class.php
@@ -0,0 +1,51 @@
+
+ * @since 6.2
+ */
+#[PostRequest('/core/cronjobs/{id:\d+}/enable')]
+final class EnableCronjob implements IController
+{
+ #[\Override]
+ public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+ {
+ $cronjob = Helper::fetchObjectFromRequestParameter($variables['id'], Cronjob::class);
+
+ $this->assertCronjobCanBeEnabled($cronjob);
+
+ (new CronjobAction([$cronjob], 'toggle'))->executeAction();
+
+ return new JsonResponse([]);
+ }
+
+ private function assertCronjobCanBeEnabled(Cronjob $cronjob): void
+ {
+ WCF::getSession()->checkPermissions(['admin.management.canManageCronjob']);
+
+ if ($cronjob->canBeDisabled()) {
+ throw new PermissionDeniedException();
+ }
+
+ if (!$cronjob->isDisabled) {
+ throw new PermissionDeniedException();
+ }
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/ExecuteCronjob.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/ExecuteCronjob.class.php
new file mode 100644
index 00000000000..233791e23c1
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/cronjobs/ExecuteCronjob.class.php
@@ -0,0 +1,42 @@
+
+ * @since 6.2
+ */
+#[PostRequest('/core/cronjobs/{id:\d+}/execute')]
+final class ExecuteCronjob implements IController
+{
+ #[\Override]
+ public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+ {
+ $cronjob = Helper::fetchObjectFromRequestParameter($variables['id'], Cronjob::class);
+
+ $this->assertCronjobCanBeExecuted();
+
+ (new CronjobAction([$cronjob], 'execute'))->executeAction();
+
+ return new JsonResponse([]);
+ }
+
+ private function assertCronjobCanBeExecuted(): void
+ {
+ WCF::getSession()->checkPermissions(['admin.management.canManageCronjob']);
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/admin/CronjobGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/CronjobGridView.class.php
new file mode 100644
index 00000000000..0da1f93a65f
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/gridView/admin/CronjobGridView.class.php
@@ -0,0 +1,141 @@
+
+ * @since 6.2
+ */
+final class CronjobGridView extends AbstractGridView
+{
+ public function __construct()
+ {
+ $this->addColumns([
+ GridViewColumn::for('cronjobID')
+ ->label('wcf.global.objectID')
+ ->renderer(new ObjectIdColumnRenderer())
+ ->sortable(),
+ GridViewColumn::for('expression')
+ ->label('wcf.acp.cronjob.expression')
+ ->renderer(
+ new class extends AbstractColumnRenderer {
+ #[\Override]
+ public function render(mixed $value, DatabaseObject $row): string
+ {
+ \assert($row instanceof Cronjob);
+
+ return \sprintf('%s', $row->getExpression());
+ }
+ }
+ ),
+ GridViewColumn::for('description')
+ ->label('wcf.acp.cronjob.description')
+ ->sortable(sortByDatabaseColumn: 'descriptionI18n')
+ ->filter(new I18nTextFilter())
+ ->renderer(new PhraseColumnRenderer())
+ ->titleColumn(),
+ GridViewColumn::for('packageID')
+ ->label('wcf.acp.package.name')
+ ->filter(new SelectFilter(PackageCache::getInstance()->getPackages()))
+ ->renderer(
+ new class extends AbstractColumnRenderer {
+ #[\Override]
+ public function render(mixed $value, DatabaseObject $row): string
+ {
+ \assert($row instanceof Cronjob);
+
+ return StringUtil::encodeHTML($row->getPackage()->getTitle());
+ }
+ }
+ )
+ ->sortable(),
+ GridViewColumn::for('nextExec')
+ ->label('wcf.acp.cronjob.nextExec')
+ ->renderer(
+ new class extends TimeColumnRenderer {
+ #[\Override]
+ public function render(mixed $value, DatabaseObject $row): string
+ {
+ \assert($row instanceof Cronjob);
+
+ if ($row->isDisabled || $row->nextExec === 1) {
+ return '';
+ }
+
+ return parent::render($value, $row);
+ }
+ }
+ )
+ ->filter(new TimeFilter())
+ ->sortable(),
+ ]);
+
+ $interaction = new CronjobInteractions();
+ $interaction->addInteractions([
+ new Divider(),
+ new EditInteraction(CronjobEditForm::class, static fn(Cronjob $cronjob) => $cronjob->isEditable()),
+ ]);
+
+ $this->addQuickInteraction(
+ new ToggleInteraction(
+ 'enable',
+ 'core/cronjobs/%s/enable',
+ 'core/cronjobs/%s/disable',
+ isAvailableCallback: static fn(Cronjob $cronjob) => $cronjob->canBeDisabled()
+ )
+ );
+ $this->setInteractionProvider($interaction);
+ $this->setBulkInteractionProvider(new CronjobBulkInteractions());
+
+ $this->addRowLink(new GridViewRowLink(CronjobEditForm::class));
+ $this->setSortField('description');
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return WCF::getSession()->getPermission('admin.management.canManageCronjob');
+ }
+
+ #[\Override]
+ protected function createObjectList(): DatabaseObjectList
+ {
+ return new I18nCronjobList();
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new CronjobGridViewInitialized($this);
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/interaction/admin/CronjobInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/admin/CronjobInteractions.class.php
new file mode 100644
index 00000000000..114b764d0bd
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/interaction/admin/CronjobInteractions.class.php
@@ -0,0 +1,39 @@
+
+ * @since 6.2
+ */
+final class CronjobInteractions extends AbstractInteractionProvider
+{
+ public function __construct()
+ {
+ $this->addInteractions([
+ new DeleteInteraction('core/cronjobs/%s', static fn(Cronjob $cronjob) => $cronjob->isDeletable()),
+ new RpcInteraction('execute', 'core/cronjobs/%s/execute', 'wcf.acp.cronjob.execute')
+ ]);
+
+ EventHandler::getInstance()->fire(
+ new CronjobInteractionCollecting($this)
+ );
+ }
+
+ #[\Override]
+ public function getObjectClassName(): string
+ {
+ return Cronjob::class;
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/interaction/bulk/admin/CronjobBulkInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/bulk/admin/CronjobBulkInteractions.class.php
new file mode 100644
index 00000000000..2dc39fdadd2
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/interaction/bulk/admin/CronjobBulkInteractions.class.php
@@ -0,0 +1,40 @@
+
+ * @since 6.2
+ */
+final class CronjobBulkInteractions extends AbstractBulkInteractionProvider
+{
+ public function __construct()
+ {
+ $this->addInteractions([
+ new BulkDeleteInteraction('core/cronjobs/%s', static fn(Cronjob $cronjob) => $cronjob->isDeletable()),
+ new BulkRpcInteraction('execute', 'core/cronjobs/%s/execute', 'wcf.acp.cronjob.execute')
+ ]);
+
+ EventHandler::getInstance()->fire(
+ new CronjobBulkInteractionCollecting($this)
+ );
+ }
+
+ #[\Override]
+ public function getObjectListClassName(): string
+ {
+ return CronjobList::class;
+ }
+}