Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file.
- Show locale date as tooltip for relative date time information
- Add popover to counter to show voters by answer
- Add forced display mode to poll configuration
### Changed
- Updated job control for the administration

## [8.5.0] - 2025-10-03
### Fixed
Expand Down
18 changes: 18 additions & 0 deletions lib/Attributes/ManuallyRunnableCronJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Polls\Attributes;

use Attribute;

/**
* Attribute for Cron jobs, if they support manual execution
*/
#[Attribute]
class ManuallyRunnableCronJob {
}
21 changes: 21 additions & 0 deletions lib/Controller/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
use OCA\Polls\Cron\JanitorCron;
use OCA\Polls\Cron\NotificationCron;
use OCA\Polls\Service\PollService;
use OCA\Polls\Service\SystemService;
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\BackgroundJob\IJob;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IRequest;
Expand All @@ -37,6 +39,7 @@ public function __construct(
private AutoReminderCron $autoReminderCron,
private JanitorCron $janitorCron,
private NotificationCron $notificationCron,
private SystemService $systemService,
) {
parent::__construct($appName, $request);
}
Expand All @@ -53,6 +56,24 @@ public function index(): TemplateResponse {
return new TemplateResponse(AppConstants::APP_ID, 'main', ['urlGenerator' => $this->urlGenerator]);
}

/**
* Get list of cron jobs
* @return JSONResponse A list of cron jobs
*/
#[FrontpageRoute(verb: 'GET', url: '/administration/jobs')]
public function getJobs(): JSONResponse {
return $this->response(fn () => ['jobs' => $this->systemService->getCronJobs()]);
}

/**
* Run job
* @param class-string<IJob> $job Job to run
*/
#[FrontpageRoute(verb: 'POST', url: '/administration/job/run')]
public function runJob($job): JSONResponse {
return $this->response(fn () => $this->systemService->runJob($job));
}

/**
* Get list of polls for administrative purposes
*/
Expand Down
14 changes: 7 additions & 7 deletions lib/Cron/AutoReminderCron.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,24 @@

use Exception;
use OCA\Polls\AppConstants;
use OCA\Polls\Attributes\ManuallyRunnableCronJob;
use OCA\Polls\Service\MailService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\ISession;
use Psr\Log\LoggerInterface;

/**
* @psalm-api
*/
class AutoReminderCron extends TimedJob {
#[ManuallyRunnableCronJob]
class AutoReminderCron extends TimedCronJob {
public function __construct(
protected ITimeFactory $time,
protected LoggerInterface $logger,
private MailService $mailService,
private ISession $session,
private LoggerInterface $logger,
protected bool $supportsManualRun = true,
) {
parent::__construct($time);
parent::setInterval(30); // run every 30 minutes
}

Expand All @@ -39,16 +40,15 @@ protected function run($argument) {
try {
$this->mailService->sendAutoReminder();
} catch (Exception $e) {
$this->logger->error('AutoReminderCron: An error occurred while running the auto reminder cron:' . $e->getMessage());
return;
} finally {
$this->session->remove(AppConstants::SESSION_KEY_CRON_JOB);
}
}

public function manuallyRun(): string {
$this->logger->info('AutoReminderCron manually run.');
$this->logger->Info('AutoReminderCron will manually run.');
$this->run(null);
return 'AutoReminderCron manually run.';
return 'AutoReminderCron was manually executed.';
}
}
21 changes: 10 additions & 11 deletions lib/Cron/JanitorCron.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,38 @@

use Exception;
use OCA\Polls\AppConstants;
use OCA\Polls\Attributes\ManuallyRunnableCronJob;
use OCA\Polls\Db\CommentMapper;
use OCA\Polls\Db\LogMapper;
use OCA\Polls\Db\OptionMapper;
use OCA\Polls\Db\PollMapper;
use OCA\Polls\Db\ShareMapper;
use OCA\Polls\Db\V5\TableManager;
use OCA\Polls\Db\VoteMapper;
use OCA\Polls\Helper\Container;
use OCA\Polls\Model\Settings\AppSettings;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\ISession;
use Psr\Log\LoggerInterface;

/**
* @psalm-api
*/
class JanitorCron extends TimedJob {
private AppSettings $appSettings;
#[ManuallyRunnableCronJob]
class JanitorCron extends TimedCronJob {
// private AppSettings $appSettings;

public function __construct(
protected ITimeFactory $time,
private CommentMapper $commentMapper,
protected LoggerInterface $logger,
private ISession $session,
private LoggerInterface $logger,
private CommentMapper $commentMapper,
private LogMapper $logMapper,
private OptionMapper $optionMapper,
private PollMapper $pollMapper,
private ShareMapper $shareMapper,
private VoteMapper $voteMapper,
private TableManager $tableManager,
private AppSettings $appSettings,
protected bool $supportsManualRun = true,
) {
parent::__construct($time);
parent::setInterval(86400); // run once a day
$this->appSettings = Container::queryClass(AppSettings::class);
}

/**
Expand All @@ -69,6 +66,7 @@ protected function run($argument) {
$this->tableManager->updateHashes();

// purge entries virtually deleted more than 12 hours ago
$deleted = [];
$deleted['comments'] = $this->commentMapper->purgeDeletedComments(time() - 4320);
$deleted['options'] = $this->optionMapper->purgeDeletedOptions(time() - 4320);
$deleted['shares'] = $this->shareMapper->purgeDeletedShares(time() - 4320);
Expand Down Expand Up @@ -104,6 +102,7 @@ protected function run($argument) {
$autoArchiveOffset = $this->appSettings->getAutoArchiveOffsetDays();

if ($this->appSettings->getAutoArchiveEnabled() && $autoArchiveOffset > 0) {
$archived = [];
$archived['poll'] = $this->pollMapper->archiveExpiredPolls(
time() - ($autoArchiveOffset * 86400)
);
Expand Down
16 changes: 8 additions & 8 deletions lib/Cron/NotificationCron.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@

use Exception;
use OCA\Polls\AppConstants;
use OCA\Polls\Attributes\ManuallyRunnableCronJob;
use OCA\Polls\Service\MailService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\ISession;
use Psr\Log\LoggerInterface;

/**
* @psalm-api
*/
class NotificationCron extends TimedJob {
#[ManuallyRunnableCronJob]
class NotificationCron extends TimedCronJob {
public function __construct(
protected ITimeFactory $time,
private MailService $mailService,
protected LoggerInterface $logger,
private ISession $session,
private LoggerInterface $logger,
private MailService $mailService,
protected bool $supportsManualRun = true,
) {
parent::__construct($time);
parent::setInterval(5); // run every 5 minutes
// parent::__construct($time);
parent::setInterval(300); // run every 5 minutes
}

/**
Expand Down
43 changes: 43 additions & 0 deletions lib/Cron/TimedCronJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Polls\Cron;

use Exception;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use Psr\Log\LoggerInterface;

/**
* @psalm-api
*/
abstract class TimedCronJob extends TimedJob {
public function __construct(
protected ITimeFactory $time,
protected LoggerInterface $logger,
protected bool $supportsManualRun = false,
) {
parent::__construct($time);
parent::setInterval(86400); // default run once a day
}

/**
* Manually run the cron job is disabled by default
* You can override this method in your cron job class to enable it
*
* @return string
*/
public function manuallyRun(): string {
$reflection_class = new \ReflectionClass($this);
$jobClassName = $reflection_class->getName();

$this->logger->error('{job} does not support manual execution', ['job' => $jobClassName]);
throw new Exception('Job does not support manual execution');
}

}
68 changes: 68 additions & 0 deletions lib/Service/SystemService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

use Exception;
use OCA\Polls\AppConstants;
use OCA\Polls\Attributes\ManuallyRunnableCronJob;
use OCA\Polls\Cron\TimedCronJob;
use OCA\Polls\Db\Share;
use OCA\Polls\Db\ShareMapper;
use OCA\Polls\Db\UserMapper;
Expand All @@ -24,9 +26,12 @@
use OCA\Polls\Model\User\Contact;
use OCA\Polls\Model\User\Email;
use OCA\Polls\Model\User\User;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\IJobList;
use OCP\Collaboration\Collaborators\ISearch;
use OCP\L10N\IFactory;
use OCP\Share\IShare;
use phpDocumentor\Reflection\Types\This;
use Psr\Log\LoggerInterface;

class SystemService {
Expand All @@ -49,6 +54,7 @@ class SystemService {
public function __construct(
private IFactory $transFactory,
private ISearch $userSearch,
private IJobList $jobList,
private LoggerInterface $logger,
private ShareMapper $shareMapper,
private VoteMapper $voteMapper,
Expand Down Expand Up @@ -260,6 +266,68 @@ public function validatePublicUsernameByToken(string $userName, string $token):
return $this->validatePublicUsername($userName, $share);
}

/**
* Run a cron job immediately
* @param class-string<IJob> $className The class name of the cron job to run
* @throws Exception
*/
public function runJob($className): string {
$jobs = $this->jobList->getJobsIterator($className, null, 0);

foreach ($jobs as $job) {
$jobClassName = get_class($job);
$reflection_class = new \ReflectionClass($jobClassName);

if ($reflection_class->getNamespaceName() !== 'OCA\\Polls\\Cron') {
continue;
}

if (get_class($job) === $className) {
/** @var TimedCronJob $job */
$job->setLastRun(time());

if (
!empty($reflection_class->getAttributes(ManuallyRunnableCronJob::class))
&& $reflection_class->hasMethod('manuallyRun')
) {
return $job->manuallyRun();
} else {
$this->logger->error('Job {job} does not support manual execution', ['job' => $jobClassName]);
throw new Exception('Job does not support manual execution');
}
}
}
throw new Exception('Job not found');
}

/**
* Get a list of cron jobs within Polls' namespace
* @return array A list of cron jobs of Polls' namespace
*/
public function getCronJobs(): array {
$jobs = $this->jobList->getJobsIterator(null, 1000, 1);
$joblist = [];
foreach ($jobs as $job) {
$jobClassName = get_class($job);
$reflection_class = new \ReflectionClass($jobClassName);

if ($reflection_class->getNamespaceName() !== 'OCA\\Polls\\Cron') {
continue;
}

$joblist[] = [
'id' => $job->getId(),
'className' => get_class($job),
'lastRun' => $job->getLastRun(),
'argument' => $job->getArgument(),
'nameSpace' => $reflection_class->getNamespaceName(),
'name' => $reflection_class->getShortName(),
'manuallyRunnable' => !empty($reflection_class->getAttributes(ManuallyRunnableCronJob::class)),
];
}
return $joblist;
}

/**
* Validate if the user name is reserved
* return false, if the requested userId or displayName exists as a user or as
Expand Down
Loading