first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-06-08 17:09:23 -04:00
commit df3a033196
17887 changed files with 8637778 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/**
* @file jobs/BaseJob.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class BaseJob
*
* @ingroup support
*
* @brief Abstract class for Jobs
*/
namespace PKP\jobs;
use APP\core\Application;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use PKP\config\Config;
abstract class BaseJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 3;
/**
* The number of SECONDS to wait before retrying the job.
*/
public int $backoff = 5;
/**
* The maximum number of SECONDS a job should get processed before consider failed
*/
public int $timeout = 60;
/**
* The maximum number of unhandled exceptions to allow before failing.
*/
public int $maxExceptions = 3;
/**
* Indicate if the job should be marked as failed on timeout.
*/
public bool $failOnTimeout = true;
/**
* Initialize the job
*/
public function __construct()
{
$this->connection = $this->defaultConnection();
$this->queue = Config::getVar('queues', 'default_queue', 'queue');
}
/**
* Get the queue job default connection to execute
*/
protected function defaultConnection(): string
{
if (Application::isUnderMaintenance()) {
return 'sync';
}
return Config::getVar('queues', 'default_connection', 'database');
}
/**
* handle the queue job execution process
*/
abstract public function handle();
}
+98
View File
@@ -0,0 +1,98 @@
<?php
/**
* @file jobs/bulk/BulkEmailSender.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2000-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class BulkEmailSender
*
* @ingroup jobs
*
* @brief Job to send bulk emails
*/
namespace PKP\jobs\bulk;
use APP\facades\Repo;
use Illuminate\Bus\Batchable;
use Illuminate\Support\Facades\Mail;
use PKP\jobs\BaseJob;
use PKP\mail\Mailable;
class BulkEmailSender extends BaseJob
{
use Batchable;
/**
* The maximum number of SECONDS a job should get processed before consider failed
*/
public int $timeout = 180;
/**
* The user ids to send email
*/
protected array $userIds;
/**
* The associated context id
*/
protected int $contextId;
/**
* Mail subject
*/
protected string $subject;
/**
* Mail body
*/
protected string $body;
/**
* From email to send mail
*/
protected object|array|string $fromEmail;
/**
* From name to send mail
*/
protected mixed $fromName;
/**
* Create a new job instance.
*/
public function __construct(array $userIds, int $contextId, string $subject, string $body, object|array|string $fromEmail, mixed $fromName)
{
parent::__construct();
$this->userIds = $userIds;
$this->contextId = $contextId;
$this->subject = $subject;
$this->body = $body;
$this->fromEmail = $fromEmail;
$this->fromName = $fromName;
}
public function handle()
{
$users = Repo::user()
->getCollector()
->filterByContextIds([$this->contextId])
->filterByUserIds($this->userIds)
->getMany();
foreach ($users as $user) {
$mailable = new Mailable();
$mailable
->from($this->fromEmail, $this->fromName)
->to($user->getEmail(), $user->getFullName())
->subject($this->subject)
->body($this->body);
Mail::send($mailable);
}
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* @file jobs/doi/DepositContext.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DepositContext
*
* @ingroup jobs
*
* @brief Job to deposit all DOIs and associated metadata to the configured registration agency for a given context
*/
namespace PKP\jobs\doi;
use APP\core\Application;
use APP\facades\Repo;
use PKP\context\Context;
use PKP\context\ContextDAO;
use PKP\job\exceptions\JobException;
use PKP\jobs\BaseJob;
class DepositContext extends BaseJob
{
protected int $contextId;
/**
* Create a new job instance.
*
*/
public function __construct(int $contextId)
{
parent::__construct();
$this->contextId = $contextId;
}
/**
* Execute the job.
*
*/
public function handle()
{
/** @var ContextDAO $contextDao */
$contextDao = Application::getContextDAO();
/** @var Context $context */
$context = $contextDao->getById($this->contextId);
if (!$context) {
throw new JobException(JobException::INVALID_PAYLOAD);
}
// NB: Only run at context level if automatic deposit is enabled. Otherwise, automatic deposit will always run,
// regardless of configuration status.
if (!$context->getData(Context::SETTING_DOI_AUTOMATIC_DEPOSIT)) {
return;
}
Repo::doi()->depositAll($context);
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* @file jobs/doi/DepositSubmission.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DepositSubmission
*
* @ingroup jobs
*
* @brief Job to deposit submission DOI and metadata to the configured registration agency
*/
namespace PKP\jobs\doi;
use APP\facades\Repo;
use APP\plugins\IDoiRegistrationAgency;
use PKP\context\Context;
use PKP\job\exceptions\JobException;
use PKP\jobs\BaseJob;
class DepositSubmission extends BaseJob
{
protected int $submissionId;
protected Context $context;
/**
* @var IDoiRegistrationAgency The configured DOI registration agency
*/
protected IDoiRegistrationAgency $agency;
/**
* Create a new job instance.
*
*/
public function __construct(int $submissionId, Context $context, IDoiRegistrationAgency $agency)
{
parent::__construct();
$this->submissionId = $submissionId;
$this->context = $context;
$this->agency = $agency;
}
/**
* Execute the job.
*
*/
public function handle()
{
$submission = Repo::submission()->get($this->submissionId);
if (!$submission || !$this->agency) {
throw new JobException(JobException::INVALID_PAYLOAD);
}
$this->agency->depositSubmissions([$submission], $this->context);
}
}
+206
View File
@@ -0,0 +1,206 @@
<?php
/**
* @file jobs/email/EditorialReminder.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class EditorialReminder
*
* @ingroup jobs
*
* @brief Class to handle a job to send an editorial reminder
*/
namespace PKP\jobs\email;
use APP\core\Application;
use APP\core\Services;
use APP\facades\Repo;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use APP\submission\Submission;
use Illuminate\Support\Facades\Mail;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\jobs\BaseJob;
use PKP\mail\mailables\EditorialReminder as MailablesEditorialReminder;
use PKP\notification\NotificationSubscriptionSettingsDAO;
use PKP\notification\PKPNotification;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\user\User;
use PKP\workflow\WorkflowStageDAO;
class EditorialReminder extends BaseJob
{
protected int $editorId;
protected int $contextId;
public function __construct(int $editorId, int $contextId)
{
parent::__construct();
$this->editorId = $editorId;
$this->contextId = $contextId;
}
/**
* Execute the job.
*/
public function handle(): void
{
if (!$this->isSubscribed()) {
return;
}
/** @var Context $context */
$context = Services::get('context')->get($this->contextId);
$editor = Repo::user()->get($this->editorId);
// Don't use the request locale because this job is
// run during a scheduled task
$requestLocale = Locale::getLocale();
Locale::setLocale($this->getLocale($editor, $context));
$submissionIds = Repo::submission()
->getCollector()
->assignedTo([$this->editorId])
->filterByContextIds([$this->contextId])
->filterByStatus([Submission::STATUS_QUEUED])
->filterByIncomplete(false)
->getIds();
$outstanding = [];
$submissions = [];
/** @var int $submissionId */
foreach ($submissionIds as $submissionId) {
$submission = Repo::submission()->get($submissionId);
$submissions[$submissionId] = $submission;
if ($submission->getData('stageId') == WORKFLOW_STAGE_ID_SUBMISSION) {
$outstanding[$submissionId] = __('editor.submission.status.waitingInitialReview');
continue;
}
if (in_array($submission->getData('stageId'), [WORKFLOW_STAGE_ID_INTERNAL_REVIEW, WORKFLOW_STAGE_ID_EXTERNAL_REVIEW])) {
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
$reviewRound = $reviewRoundDao->getLastReviewRoundBySubmissionId($submission->getId(), $submission->getData('stageId'));
if ($reviewRound->getStatus() === ReviewRound::REVIEW_ROUND_STATUS_PENDING_REVIEWERS) {
$outstanding[$submissionId] = __('editor.submission.roundStatus.pendingReviewers');
continue;
}
if ($reviewRound->getStatus() === ReviewRound::REVIEW_ROUND_STATUS_REVIEWS_COMPLETED) {
$outstanding[$submissionId] = __('editor.submission.roundStatus.reviewsCompleted');
continue;
}
if ($reviewRound->getStatus() === ReviewRound::REVIEW_ROUND_STATUS_REVIEWS_OVERDUE) {
$outstanding[$submissionId] = __('editor.submission.roundStatus.reviewOverdue');
continue;
}
if ($reviewRound->getStatus() === ReviewRound::REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED) {
$outstanding[$submissionId] = __('editor.submission.roundStatus.revisionsSubmitted');
continue;
}
}
if (in_array($submission->getData('stageId'), [WORKFLOW_STAGE_ID_EDITING, WORKFLOW_STAGE_ID_PRODUCTION])) {
$lastActivityTimestamp = strtotime($submission->getData('dateLastActivity'));
if ($lastActivityTimestamp < strtotime('+30 days')) {
/** @var WorkflowStageDAO $workflowStageDao */
$workflowStageDao = DAORegistry::getDAO('WorkflowStageDAO');
$outstanding[$submissionId] = __(
'editor.submission.status.inactiveDaysInStage',
[
'days' => 30,
'stage' => __($workflowStageDao->getTranslationKeyFromId($submission->getData('stageId')))
]
);
}
}
if (count($outstanding) > 20) {
break;
}
}
if (empty($outstanding)) {
return;
}
// Context or user was removed since job was created, or the user was disabled
if (!$context || !$editor) {
return;
}
$notificationManager = new NotificationManager();
$notification = $notificationManager->createNotification(
Application::get()->getRequest(),
$this->editorId,
PKPNotification::NOTIFICATION_TYPE_EDITORIAL_REMINDER,
$this->contextId
);
$mailable = new MailablesEditorialReminder($context);
$emailTemplate = Repo::emailTemplate()->getByKey($context->getId(), $mailable::getEmailTemplateKey());
$mailable
->setOutstandingTasks($outstanding, $submissions, $submissionIds->count())
->from($context->getContactEmail(), $context->getLocalizedName(Locale::getLocale()))
->recipients([$editor])
->subject($emailTemplate->getLocalizedData('subject'))
->body($emailTemplate->getLocalizedData('body'))
->allowUnsubscribe($notification);
Mail::send($mailable);
// Restore the current locale after the email is sent
Locale::setLocale($requestLocale);
}
/**
* Is this editor subscribed to this email?
*/
protected function isSubscribed(): bool
{
/** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
$notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');
$blockedEmails = $notificationSubscriptionSettingsDao->getNotificationSubscriptionSettings('blocked_emailed_notification', $this->editorId, $this->contextId);
return !in_array(Notification::NOTIFICATION_TYPE_EDITORIAL_REMINDER, $blockedEmails);
}
/**
* Get the locale to use with this email
*
* Returns the context's primary locale, or the first locale
* supported by the context and the user.
*
* @return string Locale key. Example: en
*/
protected function getLocale(User $editor, Context $context): string
{
$locale = $context->getPrimaryLocale();
// A user's locales may not be an array due to bug with data structure
// See: https://github.com/pkp/pkp-lib/issues/8023
if ($editor->getLocales() === false) {
return $locale;
}
$locales = array_intersect($editor->getLocales(), $context->getSupportedLocales());
if (!empty($locales) && !in_array($context->getPrimaryLocale(), $locales)) {
$locale = $locales[0];
}
return $locale;
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/**
* @file jobs/metadata/BatchMetadataChangedJob.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class BatchMetadataChangedJob
*
* @ingroup jobs
*
* @brief Class to handle the Batch Metadata Changed job
*/
namespace PKP\jobs\metadata;
use APP\core\Application;
use APP\facades\Repo;
use PKP\job\exceptions\JobException;
use PKP\jobs\BaseJob;
class BatchMetadataChangedJob extends BaseJob
{
/** @var array $submissionIds Submission ids associated */
public $submissionIds;
/**
* Create a new job instance.
*
*/
public function __construct(array $submissionIds)
{
parent::__construct();
$this->submissionIds = $submissionIds;
}
/**
* Execute the job.
*
*/
public function handle(): void
{
$successful = 0;
$submissionSearchIndex = Application::getSubmissionSearchIndex();
foreach ($this->submissionIds as $currentSubmissionId) {
$submission = Repo::submission()->get($currentSubmissionId);
if (!$submission) {
continue;
}
$submissionSearchIndex->submissionMetadataChanged($submission);
$submissionSearchIndex->submissionFilesChanged($submission);
$successful++;
}
if (!$successful) {
throw new JobException(JobException::INVALID_PAYLOAD);
}
$submissionSearchIndex->submissionChangesFinished();
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* @file jobs/metadata/MetadataChangedJob.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class MetadataChangedJob
*
* @ingroup jobs
*
* @brief Class to handle the Metadata Changed job
*/
namespace PKP\jobs\metadata;
use APP\core\Application;
use APP\facades\Repo;
use PKP\job\exceptions\JobException;
use PKP\jobs\BaseJob;
class MetadataChangedJob extends BaseJob
{
/**
* @var int The submission ID
*/
protected $submissionId;
/**
* Create a new job instance.
*
*/
public function __construct(int $submissionId)
{
parent::__construct();
$this->submissionId = $submissionId;
}
/**
* Execute the job.
*
*/
public function handle(): void
{
$submission = Repo::submission()->get($this->submissionId);
if (!$submission) {
throw new JobException(JobException::INVALID_PAYLOAD);
}
$submissionSearchIndex = Application::getSubmissionSearchIndex();
$submissionSearchIndex->submissionMetadataChanged($submission);
$submissionSearchIndex->submissionFilesChanged($submission);
$submissionSearchIndex->submissionChangesFinished();
}
}
@@ -0,0 +1,114 @@
<?php
/**
* @file jobs/notifications/NewAnnouncementNotifyUsers.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class NewAnnouncementNotifyUsers
*
* @ingroup jobs
*
* @brief Class to send system notifications when a new announcement is added
*/
namespace PKP\jobs\notifications;
use APP\core\Application;
use APP\facades\Repo;
use APP\notification\Notification;
use Illuminate\Bus\Batchable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Mail;
use PKP\announcement\Announcement;
use PKP\context\Context;
use PKP\emailTemplate\EmailTemplate;
use PKP\job\exceptions\JobException;
use PKP\jobs\BaseJob;
use PKP\mail\mailables\AnnouncementNotify;
use PKP\notification\managerDelegate\AnnouncementNotificationManager;
use PKP\user\User;
class NewAnnouncementNotifyUsers extends BaseJob
{
use Batchable;
protected Collection $recipientIds;
protected int $contextId;
protected int $announcementId;
protected string $locale;
// Sender of the email
protected ?User $sender;
public function __construct(
Collection $recipientIds,
int $contextId,
int $announcementId,
string $locale,
?User $sender = null // Leave null to not send an email
) {
parent::__construct();
$this->recipientIds = $recipientIds;
$this->contextId = $contextId;
$this->announcementId = $announcementId;
$this->locale = $locale;
$this->sender = $sender;
}
public function handle()
{
$announcement = Repo::announcement()->get($this->announcementId);
// Announcement was removed
if (!$announcement) {
throw new JobException(JobException::INVALID_PAYLOAD);
}
$announcementNotificationManager = new AnnouncementNotificationManager(Notification::NOTIFICATION_TYPE_NEW_ANNOUNCEMENT);
$announcementNotificationManager->initialize($announcement);
$context = Application::getContextDAO()->getById($this->contextId);
$template = Repo::emailTemplate()->getByKey($context->getId(), AnnouncementNotify::getEmailTemplateKey());
foreach ($this->recipientIds as $recipientId) {
/** @var int $recipientId */
$recipient = Repo::user()->get($recipientId);
if (!$recipient) {
continue;
}
$notification = $announcementNotificationManager->notify($recipient);
if (!$this->sender) {
continue;
}
// Send email
$mailable = $this->createMailable($context, $recipient, $announcement, $template)
->allowUnsubscribe($notification);
$mailable->setData($this->locale);
Mail::send($mailable);
}
}
/**
* Creates new announcement notification email
*/
protected function createMailable(
Context $context,
User $recipient,
Announcement $announcement,
EmailTemplate $template
): AnnouncementNotify {
$mailable = new AnnouncementNotify($context, $announcement);
$mailable->sender($this->sender);
$mailable->recipients([$recipient]);
$mailable->body($template->getLocalizedData('body', $this->locale));
$mailable->subject($template->getLocalizedData('subject', $this->locale));
return $mailable;
}
}
@@ -0,0 +1,172 @@
<?php
/**
* @file jobs/notifications/StatisticsReportMail.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class StatisticsReportMail
*
* @ingroup jobs
*
* @brief Class to send email to editors with monthly editorial report
*/
namespace PKP\jobs\notifications;
use APP\core\Application;
use APP\core\Services;
use APP\facades\Repo;
use APP\notification\NotificationManager;
use DateTimeImmutable;
use Illuminate\Bus\Batchable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Mail;
use IntlDateFormatter;
use PKP\context\Context;
use PKP\jobs\BaseJob;
use PKP\mail\mailables\StatisticsReportNotify;
use PKP\notification\PKPNotification;
use SplFileObject;
class StatisticsReportMail extends BaseJob
{
use Batchable;
protected Collection $userIds;
protected int $contextId;
protected DateTimeImmutable $dateStart;
protected DateTimeImmutable $dateEnd;
public function __construct(Collection $userIds, int $contextId, DateTimeImmutable $dateStart, DateTimeImmutable $dateEnd)
{
parent::__construct();
$this->userIds = $userIds;
$this->contextId = $contextId;
$this->dateStart = $dateStart;
$this->dateEnd = $dateEnd;
}
public function handle()
{
$contextDao = Application::getContextDAO();
$context = $contextDao->getById($this->contextId); /** @var Context $context */
$locale = $context->getPrimaryLocale();
$template = Repo::emailTemplate()->getByKey($this->contextId, StatisticsReportNotify::getEmailTemplateKey());
$editorialTrends = Services::get('editorialStats')->getOverview([
'contextIds' => [$context->getId()],
'dateStart' => $this->dateStart->format('Y-m-d'),
'dateEnd' => $this->dateEnd->format('Y-m-d'),
]);
$editorialTrendsTotal = Services::get('editorialStats')->getOverview(['contextIds' => [$context->getId()]]);
$totalSubmissions = Services::get('editorialStats')->countSubmissionsReceived(['contextIds' => [$context->getId()]]);
$formatter = IntlDateFormatter::create(
$locale,
IntlDateFormatter::FULL,
IntlDateFormatter::FULL,
null,
null,
'MMMM'
);
$month = $formatter->format($this->dateStart);
$year = $this->dateStart->format('Y');
$filePath = $this->createCsvAttachment($editorialTrends, $editorialTrendsTotal, $locale, $month, $year);
foreach ($this->userIds as $userId) {
/** @var int $userId */
$user = Repo::user()->get($userId);
if (!$user) {
continue;
}
$notificationManager = new NotificationManager();
$notification = $notificationManager->createNotification(
Application::get()->getRequest(),
$user->getId(),
PKPNotification::NOTIFICATION_TYPE_EDITORIAL_REPORT,
$this->contextId
);
$mailable = new StatisticsReportNotify($context, $editorialTrends, $totalSubmissions, $month, $year);
$mailable->recipients([$user]);
$mailable->from($context->getContactEmail(), $context->getContactName());
$mailable->subject($template->getLocalizedData('subject', $locale));
$mailable->body($template->getLocalizedData('body', $locale));
$mailable->setData($locale);
$mailable->attach($filePath, ['as' => 'editorial-report.csv']);
$mailable->allowUnsubscribe($notification);
Mail::send($mailable);
}
unlink($filePath);
}
/**
* Create a csv file on the file system
*
* @return string the full path to the attachment
*/
protected function createCsvAttachment(array $editorialTrends, array $editorialTrendsTotal, string $locale, $month, $year): string
{
$userRolesOverview = Repo::user()->getRolesOverview(Repo::user()->getCollector()->filterByContextIds([$this->contextId]));
// Create the CSV file attachment
// Active submissions by stage
$file = new SplFileObject(tempnam(sys_get_temp_dir(), 'tmp'), 'wb');
// Adds BOM (byte order mark) to enforce the UTF-8 format
try {
$file->fwrite("\xEF\xBB\xBF");
$file->fputcsv([
__('stats.submissionsActive', [], $locale),
__('stats.total', [], $locale)
]);
foreach (Application::getApplicationStages() as $stageId) {
$file->fputcsv([
__(Application::getWorkflowStageName($stageId), [], $locale),
Services::get('editorialStats')->countActiveByStages($stageId)
]);
}
$file->fputcsv([]);
// Editorial trends
$file->fputcsv([
__('stats.trends', [], $locale),
$month . __('common.commaListSeparator', [], $locale) . $year,
__('stats.total', [], $locale)
]);
foreach ($editorialTrends as $i => $stat) {
$file->fputcsv([
__($stat['name'], [], $locale),
$stat['value'],
$editorialTrendsTotal[$i]['value']
]);
}
$file->fputcsv([]);
// Count of users by role
$file->fputcsv([
__('manager.users', [], $locale),
__('stats.total', [], $locale)
]);
foreach ($userRolesOverview as $role) {
$file->fputcsv([
__($role['name'], [], $locale),
$role['value']
]);
}
$filePath = $file->getRealPath();
} finally {
$file = null;
}
return $filePath;
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @file jobs/notifications/StatisticsReportNotify.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class StatisticsReportNotify
*
* @ingroup jobs
*
* @brief Class to create system notifications for editors about editorial report
*/
namespace PKP\jobs\notifications;
use APP\facades\Repo;
use Illuminate\Bus\Batchable;
use Illuminate\Support\Collection;
use PKP\jobs\BaseJob;
use PKP\notification\managerDelegate\EditorialReportNotificationManager;
class StatisticsReportNotify extends BaseJob
{
use Batchable;
protected Collection $userIds;
protected EditorialReportNotificationManager $notificationManager;
public function __construct(Collection $userIds, EditorialReportNotificationManager $notificationManager)
{
parent::__construct();
$this->userIds = $userIds;
$this->notificationManager = $notificationManager;
}
public function handle()
{
foreach ($this->userIds as $userId) {
/** @var int $userId */
$user = Repo::user()->get($userId);
if (!$user) {
continue;
}
$this->notificationManager->notify($user);
}
}
}
@@ -0,0 +1,75 @@
<?php
/**
* @file jobs/statistics/ArchiveUsageStatsLogFile.php
*
* Copyright (c) 2024 Simon Fraser University
* Copyright (c) 2024 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ArchiveUsageStatsLogFile
*
* @ingroup jobs
*
* @brief Archive usage stats log file.
*/
namespace PKP\jobs\statistics;
use APP\statistics\StatisticsHelper;
use Exception;
use PKP\file\FileManager;
use PKP\job\exceptions\JobException;
use PKP\jobs\BaseJob;
use PKP\site\Site;
use PKP\task\FileLoader;
class ArchiveUsageStatsLogFile extends BaseJob
{
/**
* The load ID = usage stats log file name
*/
protected string $loadId;
protected Site $site;
/**
* Create a new job instance.
*/
public function __construct(string $loadId, Site $site)
{
parent::__construct();
$this->loadId = $loadId;
$this->site = $site;
}
/**
* Execute the job.
*/
public function handle(): void
{
// Move the archived file back to staging
$filename = $this->loadId;
$archiveFilePath = StatisticsHelper::getUsageStatsDirPath() . '/' . FileLoader::FILE_LOADER_PATH_ARCHIVE . '/' . $filename;
$dispatchFilePath = StatisticsHelper::getUsageStatsDirPath() . '/' . FileLoader::FILE_LOADER_PATH_DISPATCH . '/' . $filename;
if (!rename($dispatchFilePath, $archiveFilePath)) {
$message = __(
'admin.job.archiveLogFile.error',
[
'file' => $filename,
'dispatchFilePath' => $dispatchFilePath,
'archiveFilePath' => $archiveFilePath
]
);
throw new JobException($message);
}
if ($this->site->getData('compressStatsLogs')) {
try {
$fileMgr = new FileManager();
$fileMgr->gzCompressFile($archiveFilePath);
} catch (Exception $e) {
error_log($e);
}
}
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @file jobs/statistics/CompileContextMetrics.php
*
* Copyright (c) 2024 Simon Fraser University
* Copyright (c) 2024 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class CompileContextMetrics
*
* @ingroup jobs
*
* @brief Compile context metrics.
*/
namespace PKP\jobs\statistics;
use APP\statistics\TemporaryTotalsDAO;
use PKP\db\DAORegistry;
use PKP\jobs\BaseJob;
class CompileContextMetrics extends BaseJob
{
/**
* The load ID = usage stats log file name
*/
protected string $loadId;
/**
* Create a new job instance.
*/
public function __construct(string $loadId)
{
parent::__construct();
$this->loadId = $loadId;
}
/**
* Execute the job.
*/
public function handle(): void
{
$temporaryTotalsDao = DAORegistry::getDAO('TemporaryTotalsDAO'); /** @var TemporaryTotalsDAO $temporaryTotalsDao */
$temporaryTotalsDao->compileContextMetrics($this->loadId);
}
}
@@ -0,0 +1,66 @@
<?php
/**
* @file jobs/statistics/CompileMonthlyMetrics.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class CompileMonthlyMetrics
*
* @ingroup jobs
*
* @brief Compile and store monthly usage stats from the daily records.
*/
namespace PKP\jobs\statistics;
use APP\core\Services;
use PKP\jobs\BaseJob;
use PKP\site\Site;
class CompileMonthlyMetrics extends BaseJob
{
/**
* The month the usage metrics should be aggregated by, in format YYYYMM.
*/
protected string $month;
protected Site $site;
/**
* Create a new job instance.
*
* @param string $month In format YYYYMM
*/
public function __construct(string $month, Site $site)
{
parent::__construct();
$this->month = $month;
$this->site = $site;
}
/**
* Execute the job.
*/
public function handle(): void
{
$currentMonth = date('Ym');
$lastMonth = date('Ym', strtotime('last month'));
$geoService = Services::get('geoStats');
$geoService->deleteMonthlyMetrics($this->month);
$geoService->addMonthlyMetrics($this->month);
if (!$this->site->getData('keepDailyUsageStats') && $this->month != $currentMonth && $this->month != $lastMonth) {
$geoService->deleteDailyMetrics($this->month);
}
$counterService = Services::get('sushiStats');
$counterService->deleteMonthlyMetrics($this->month);
$counterService->addMonthlyMetrics($this->month);
if (!$this->site->getData('keepDailyUsageStats') && $this->month != $currentMonth && $this->month != $lastMonth) {
$counterService->deleteDailyMetrics($this->month);
}
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @file jobs/statistics/CompileSubmissionMetrics.php
*
* Copyright (c) 2024 Simon Fraser University
* Copyright (c) 2024 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class CompileSubmissionMetrics
*
* @ingroup jobs
*
* @brief Compile submission metrics.
*/
namespace PKP\jobs\statistics;
use APP\statistics\TemporaryTotalsDAO;
use PKP\db\DAORegistry;
use PKP\jobs\BaseJob;
class CompileSubmissionMetrics extends BaseJob
{
/**
* The load ID = usage stats log file name
*/
protected string $loadId;
/**
* Create a new job instance.
*/
public function __construct(string $loadId)
{
parent::__construct();
$this->loadId = $loadId;
}
/**
* Execute the job.
*/
public function handle(): void
{
$temporaryTotalsDao = DAORegistry::getDAO('TemporaryTotalsDAO'); /** @var TemporaryTotalsDAO $temporaryTotalsDao */
$temporaryTotalsDao->compileSubmissionMetrics($this->loadId);
}
}
@@ -0,0 +1,185 @@
<?php
/**
* @file jobs/statistics/PKPProcessUsageStatsLogFile.php
*
* Copyright (c) 2024 Simon Fraser University
* Copyright (c) 2024 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPProcessUsageStatsLogFile
*
* @ingroup jobs
*
* @brief Compile context metrics.
*/
namespace PKP\jobs\statistics;
use APP\statistics\StatisticsHelper;
use DateTime;
use Exception;
use PKP\core\Core;
use PKP\job\exceptions\JobException;
use PKP\jobs\BaseJob;
use PKP\task\FileLoader;
use SplFileObject;
abstract class PKPProcessUsageStatsLogFile extends BaseJob
{
/**
* Create a new job instance.
*
* @param string $loadId Usage stats log file name
*/
public function __construct(protected string $loadId)
{
parent::__construct();
}
/**
* Delete entries in usage stats temporary tables by loadId
*/
abstract protected function deleteByLoadId(): void;
/**
* Get valid assoc types that an usage event can contain
*/
abstract protected function getValidAssocTypes(): array;
/**
* Insert usage stats log entry into temporary tables
*/
abstract protected function insertTemporaryUsageStatsData(object $entry, int $lineNumber): void;
/**
* Execute the job.
*/
public function handle(): void
{
$filename = $this->loadId;
$dispatchFilePath = StatisticsHelper::getUsageStatsDirPath() . '/' . FileLoader::FILE_LOADER_PATH_DISPATCH . '/' . $filename;
if (!file_exists($dispatchFilePath)) {
throw new JobException(__(
'admin.job.processLogFile.fileNotFound',
['file' => $dispatchFilePath]
));
}
$this->process($dispatchFilePath);
}
/**
* Parse log file line by line and add the lines into the usage stats temporary DB tables.
*/
protected function process(string $dispatchFilePath): void
{
$splFileObject = new SplFileObject($dispatchFilePath, 'r');
if (!$splFileObject) {
// reject file -- move the file from dispatch to reject folder
$filename = $this->loadId;
$rejectFilePath = StatisticsHelper::getUsageStatsDirPath() . '/' . FileLoader::FILE_LOADER_PATH_REJECT . '/' . $filename;
if (!rename($dispatchFilePath, $rejectFilePath)) {
$message = __('admin.job.compileMetrics.returnToStaging.error', ['file' => $filename,
'dispatchFilePath' => $dispatchFilePath, 'rejectFilePath' => $rejectFilePath]);
error_log($message);
}
throw new JobException(__('admin.job.processLogFile.openFileFailed', ['file' => $dispatchFilePath]));
}
// Make sure we don't have any temporary records associated
// with the current load ID in database.
$this->deleteByLoadId();
$lineNumber = 0;
while (!$splFileObject->eof()) {
$lineNumber++;
$line = $splFileObject->fgets();
if (empty($line) || substr($line, 0, 1) === '#') {
continue;
} // Spacing or comment lines. This actually should not occur in the new format.
$entryData = json_decode($line);
if ($entryData === null) {
// This line is not in the right format.
$message = __(
'admin.job.processLogFile.wrongLoglineFormat',
['file' => $this->loadId, 'lineNumber' => $lineNumber]
);
error_log($message);
continue;
}
try {
$this->validateLogEntry($entryData);
} catch (Exception $e) {
$message = __(
'admin.job.processLogFile.invalidLogEntry',
['file' => $this->loadId, 'lineNumber' => $lineNumber, 'error' => $e->getMessage()]
);
error_log($message);
continue;
}
// Avoid bots.
if (Core::isUserAgentBot($entryData->userAgent)) {
continue;
}
$this->insertTemporaryUsageStatsData($entryData, $lineNumber);
}
//explicitly assign null, so that the file can be deleted
$splFileObject = null;
}
/**
* Validate the usage stats log entry
*
* @throws Exception.
*/
protected function validateLogEntry(object $entry): void
{
if (!$this->validateDate($entry->time)) {
throw new Exception(__('admin.job.processLogFile.invalidLogEntry.time'));
}
// check hashed IP ?
// check canonicalUrl ?
if (!is_int($entry->contextId)) {
throw new Exception(__('admin.job.processLogFile.invalidLogEntry.contextId'));
}
if (!empty($entry->submissionId) && !is_int($entry->submissionId)) {
throw new Exception(__('admin.job.processLogFile.invalidLogEntry.submissionId'));
}
$validAssocTypes = $this->getValidAssocTypes();
if (!in_array($entry->assocType, $validAssocTypes)) {
throw new Exception(__('admin.job.processLogFile.invalidLogEntry.assocType'));
}
$validFileTypes = [
StatisticsHelper::STATISTICS_FILE_TYPE_PDF,
StatisticsHelper::STATISTICS_FILE_TYPE_DOC,
StatisticsHelper::STATISTICS_FILE_TYPE_HTML,
StatisticsHelper::STATISTICS_FILE_TYPE_OTHER,
];
if (!empty($entry->fileType) && !in_array($entry->fileType, $validFileTypes)) {
throw new Exception(__('admin.job.processLogFile.invalidLogEntry.fileType'));
}
if (!empty($entry->country) && (!ctype_alpha($entry->country) || (strlen($entry->country) !== 2))) {
throw new Exception(__('admin.job.processLogFile.invalidLogEntry.country'));
}
if (!empty($entry->region) && (!ctype_alnum($entry->region) || (strlen($entry->region) > 3))) {
throw new Exception(__('admin.job.processLogFile.invalidLogEntry.region'));
}
if (!is_array($entry->institutionIds)) {
throw new Exception(__('admin.job.processLogFile.invalidLogEntry.institutionIds'));
}
}
/**
* Validate date, check if the date is a valid date and in requested format
*/
protected function validateDate(string $datetime, string $format = 'Y-m-d H:i:s'): bool
{
$d = DateTime::createFromFormat($format, $datetime);
return $d && $d->format($format) === $datetime;
}
}
@@ -0,0 +1,48 @@
<?php
/**
* @file jobs/statistics/RemoveDoubleClicks.php
*
* Copyright (c) 2024 Simon Fraser University
* Copyright (c) 2024 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RemoveDoubleClicks
*
* @ingroup jobs
*
* @brief Remove Double Clicks according to COUNTER guidelines.
*/
namespace PKP\jobs\statistics;
use APP\statistics\StatisticsHelper;
use APP\statistics\TemporaryTotalsDAO;
use PKP\db\DAORegistry;
use PKP\jobs\BaseJob;
class RemoveDoubleClicks extends BaseJob
{
/**
* The load ID = usage stats log file name
*/
protected string $loadId;
/**
* Create a new job instance.
*/
public function __construct(string $loadId)
{
parent::__construct();
$this->loadId = $loadId;
}
/**
* Execute the job.
*/
public function handle(): void
{
$temporaryTotalsDao = DAORegistry::getDAO('TemporaryTotalsDAO'); /** @var TemporaryTotalsDAO $temporaryTotalsDao */
$temporaryTotalsDao->removeDoubleClicks($this->loadId, StatisticsHelper::COUNTER_DOUBLE_CLICK_TIME_FILTER_SECONDS);
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @file jobs/submissions/RemoveSubmissionFileFromSearchIndexJob.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RemoveSubmissionFileFromSearchIndexJob
*
* @ingroup jobs
*
* @brief Class to handle the Submission File deletion as a Job
*/
namespace PKP\jobs\submissions;
use APP\core\Application;
use PKP\jobs\BaseJob;
use PKP\search\SubmissionSearch;
class RemoveSubmissionFileFromSearchIndexJob extends BaseJob
{
/**
* The submission id of the targeted submission
*
* @var int
*/
protected $submissionId;
/**
* The submission file id of the targeted submission file to delete
*
* @var int
*/
protected $submissionFileId;
/**
* Create a new job instance.
*/
public function __construct(int $submissionId, int $submissionFileId)
{
parent::__construct();
$this->submissionId = $submissionId;
$this->submissionFileId = $submissionFileId;
}
/**
* Execute the job.
*
*/
public function handle(): void
{
$submissionSearchIndex = Application::getSubmissionSearchIndex();
$submissionSearchIndex->deleteTextIndex(
$this->submissionId,
SubmissionSearch::SUBMISSION_SEARCH_GALLEY_FILE,
$this->submissionFileId
);
$submissionSearchIndex->submissionChangesFinished();
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* @file jobs/submissions/RemoveSubmissionFromSearchIndexJob.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RemoveSubmissionFromSearchIndexJob
*
* @ingroup jobs
*
* @brief Class to handle the Deleted Submission data update as a Job
*/
namespace PKP\jobs\submissions;
use APP\core\Application;
use PKP\jobs\BaseJob;
class RemoveSubmissionFromSearchIndexJob extends BaseJob
{
/**
* The submission id of the targeted submission to delete
*
* @var int
*/
protected $submissionId;
/**
* Create a new job instance.
*
*/
public function __construct(int $submissionId)
{
parent::__construct();
$this->submissionId = $submissionId;
}
/**
* Execute the job.
*
*/
public function handle(): void
{
$submissionSearchIndex = Application::getSubmissionSearchIndex();
$submissionSearchIndex->deleteTextIndex($this->submissionId);
$submissionSearchIndex->submissionChangesFinished();
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* @file jobs/submissions/UpdateSubmissionSearchJob.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class UpdateSubmissionSearchJob
*
* @ingroup jobs
*
* @brief Class to handle the Submission Search data update as a Job
*/
namespace PKP\jobs\submissions;
use APP\core\Application;
use APP\facades\Repo;
use PKP\job\exceptions\JobException;
use PKP\jobs\BaseJob;
use PKP\submission\PKPSubmission;
class UpdateSubmissionSearchJob extends BaseJob
{
/**
* The maximum number of SECONDS a job should get processed before consider failed
*/
public int $timeout = 180;
/**
* The submission ID
*/
protected int $submissionId;
/**
* Create a new job instance.
*
*/
public function __construct(int $submissionId)
{
parent::__construct();
$this->submissionId = $submissionId;
}
/**
* Execute the job.
*
*/
public function handle(): void
{
$submission = Repo::submission()->get($this->submissionId);
if (!$submission) {
throw new JobException(JobException::INVALID_PAYLOAD);
}
$submissionSearchIndex = Application::getSubmissionSearchIndex();
if ($submission->getData('status') !== PKPSubmission::STATUS_PUBLISHED) {
$submissionSearchIndex->deleteTextIndex($submission->getId());
} else {
$submissionSearchIndex->submissionMetadataChanged($submission);
$submissionSearchIndex->submissionFilesChanged($submission);
}
Application::getSubmissionSearchDAO()->flushCache();
$submissionSearchIndex->submissionChangesFinished();
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* @file jobs/testJobs/TestJobFailure.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class TestJobFailure
*
* @brief Example failed TestJob with a valid FQN (@see https://www.php.net/manual/pt_BR/language.namespaces.rules.php)
*/
namespace PKP\jobs\testJobs;
use Exception;
use Illuminate\Bus\Batchable;
use PKP\job\models\Job;
use PKP\jobs\BaseJob;
class TestJobFailure extends BaseJob
{
use Batchable;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 1;
/**
* The maximum number of unhandled exceptions to allow before failing.
*/
public int $maxExceptions = 1;
/**
* Initialize the job
*/
public function __construct()
{
$this->connection = config('queue.default');
$this->queue = Job::TESTING_QUEUE;
}
/**
* handle the queue job execution process
*
* @throws \Exception
*/
public function handle(): void
{
throw new Exception('cli.test.job');
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* @file jobs/testJobs/TestJobSuccess.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class TestJobSuccess
*
* @brief Example successful TestJob with a valid FQN (@see https://www.php.net/manual/pt_BR/language.namespaces.rules.php)
*/
namespace PKP\jobs\testJobs;
use Illuminate\Bus\Batchable;
use PKP\job\models\Job;
use PKP\jobs\BaseJob;
class TestJobSuccess extends BaseJob
{
use Batchable;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 1;
/**
* Initialize the job
*/
public function __construct()
{
$this->connection = config('queue.default');
$this->queue = Job::TESTING_QUEUE;
}
/**
* handle the queue job execution process
*/
public function handle(): void
{
}
}