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
+49
View File
@@ -0,0 +1,49 @@
<?php
/**
* @file classes/task/DepositDois.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2003-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DepositDois
*
* @ingroup classes_task
*
* @brief Dispatches job to automatically deposit DOIs for all configured contexts
*/
namespace PKP\task;
use APP\core\Services;
use APP\services\ContextService;
use PKP\jobs\doi\DepositContext;
use PKP\scheduledTask\ScheduledTask;
class DepositDois extends ScheduledTask
{
/**
* @copydoc ScheduledTask::getName()
*/
public function getName()
{
return __('admin.scheduledTask.depositDois');
}
/**
* @inheritDoc
*/
protected function executeActions()
{
/** @var ContextService $contextService */
$contextService = Services::get('context');
$contextIds = $contextService->getIds(['isEnabled' => true]);
foreach ($contextIds as $contextId) {
dispatch(new DepositContext($contextId));
}
return true;
}
}
@@ -0,0 +1,71 @@
<?php
/**
* @file classes/task/EditorialReminders.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2003-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class EditorialReminders
*
* @ingroup classes_task
*
* @brief Dispatches jobs to send a reminder email to editors of outstanding tasks
*/
namespace PKP\task;
use APP\core\Services;
use APP\facades\Repo;
use APP\services\ContextService;
use PKP\jobs\email\EditorialReminder;
use PKP\scheduledTask\ScheduledTask;
use PKP\scheduledTask\ScheduledTaskHelper;
use PKP\security\Role;
use PKP\user\Collector;
class EditorialReminders extends ScheduledTask
{
public function getName()
{
return __('mailable.editorialReminder.description');
}
protected function executeActions()
{
/** @var ContextService $contextService */
$contextService = Services::get('context');
$contextIds = $contextService->getIds(['isEnabled' => true]);
foreach ($contextIds as $contextId) {
$this->addExecutionLogEntry(
__('admin.scheduledTask.editorialReminder.logStart', ['contextId' => $contextId]),
ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_NOTICE
);
$userIds = Repo::user()->getCollector()
->filterByContextIds([$contextId])
->filterByRoleIds([Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR])
->filterByStatus(Collector::STATUS_ACTIVE)
->getIds();
/** @var int $userId */
foreach ($userIds as $userId) {
dispatch(new EditorialReminder($userId, $contextId));
}
$this->addExecutionLogEntry(
__(
'admin.scheduledTask.editorialReminder.logEnd',
[
'count' => $userIds->count(),
'userIds' => $userIds->join(', '),
'contextId' => $contextId,
]
),
ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_NOTICE
);
}
return true;
}
}
+443
View File
@@ -0,0 +1,443 @@
<?php
/**
* @file classes/task/FileLoader.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FileLoader
*
* @ingroup classes_task
*
* @brief Base scheduled task class to reliably handle files processing.
*/
namespace PKP\task;
use Exception;
use PKP\config\Config;
use PKP\db\DAORegistry;
use PKP\file\FileManager;
use PKP\scheduledTask\ScheduledTask;
use PKP\scheduledTask\ScheduledTaskHelper;
use PKP\site\Site;
use PKP\site\SiteDAO;
abstract class FileLoader extends ScheduledTask
{
public const FILE_LOADER_RETURN_TO_STAGING = 1;
public const FILE_LOADER_RETURN_TO_DISPATCH = 2;
public const FILE_LOADER_ERROR_MESSAGE_TYPE = 'common.error';
public const FILE_LOADER_WARNING_MESSAGE_TYPE = 'common.warning';
public const FILE_LOADER_PATH_STAGING = 'stage';
public const FILE_LOADER_PATH_PROCESSING = 'processing';
public const FILE_LOADER_PATH_REJECT = 'reject';
public const FILE_LOADER_PATH_ARCHIVE = 'archive';
public const FILE_LOADER_PATH_DISPATCH = 'dispatch';
/** The current claimed filename that the script is working on. */
private string $_claimedFilename;
/** Base directory path for the filesystem. */
private string $_basePath;
/** Stage directory path. */
private string $_stagePath;
/** Processing directory path. */
private string $_processingPath;
/** Archive directory path. */
private string $_archivePath;
/** Dispatch directory path. */
private string $_dispatchPath;
/** Reject directory path. */
private string $_rejectPath;
/** Admin email. */
private string $_adminEmail;
/** Admin name. */
private string $_adminName;
/** List of staged back files after processing. */
private array $_stagedBackFiles = [];
/** Whether to compress the archived files or not. */
private bool $_compressArchives = false;
/** List of files that should only be considered. */
private array $_onlyConsiderFiles = [];
/**
* Constructor.
*
* @param array $args script arguments
*/
public function __construct(array $args)
{
parent::__construct($args);
// Canonicalize the base path.
$basePath = rtrim($args[0], '/');
$basePathFolder = basename($basePath);
// We assume that the parent folder of the base path
// does already exist and can be canonicalized.
$basePathParent = realpath(dirname($basePath));
if ($basePathParent === false) {
$basePath = null;
} else {
$basePath = "{$basePathParent}/{$basePathFolder}";
}
$this->_basePath = $basePath;
// Configure paths.
if (!is_null($basePath)) {
$this->_stagePath = "{$basePath}/" . self::FILE_LOADER_PATH_STAGING;
$this->_archivePath = "{$basePath}/" . self::FILE_LOADER_PATH_ARCHIVE;
$this->_rejectPath = "{$basePath}/" . self::FILE_LOADER_PATH_REJECT;
$this->_processingPath = "{$basePath}/" . self::FILE_LOADER_PATH_PROCESSING;
$this->_dispatchPath = "{$basePath}/" . self::FILE_LOADER_PATH_DISPATCH;
}
// Set admin email and name.
$siteDao = DAORegistry::getDAO('SiteDAO'); /** @var SiteDAO $siteDao */
$site = $siteDao->getSite(); /** @var Site $site */
$this->_adminEmail = $site->getLocalizedContactEmail();
$this->_adminName = $site->getLocalizedContactName();
}
//
// Getters and setters.
//
/**
* Return the staging path.
*/
public function getStagePath(): string
{
return $this->_stagePath;
}
/**
* Return the processing path.
*/
public function getProcessingPath(): string
{
return $this->_processingPath;
}
/**
* Return the reject path.
*/
public function getRejectPath(): string
{
return $this->_rejectPath;
}
/**
* Return the archive path.
*/
public function getArchivePath(): string
{
return $this->_archivePath;
}
/**
* Return the dispatch path.
*/
public function getDispatchPath(): string
{
return $this->_dispatchPath;
}
/**
* Return whether the archives must be compressed or not.
*/
public function getCompressArchives(): bool
{
return $this->_compressArchives;
}
/**
* Set whether the archives must be compressed or not.
*/
public function setCompressArchives(bool $compressArchives): void
{
$this->_compressArchives = $compressArchives;
}
/**
* Get the files that should only be considered.
*/
public function getOnlyConsiderFiles(): array
{
return $this->_onlyConsiderFiles;
}
/**
* Set the files that should only be considered.
*/
public function setOnlyConsiderFiles(array $onlyConsiderFiles): void
{
$this->_onlyConsiderFiles = $onlyConsiderFiles;
}
//
// Public methods
//
/**
* A public helper function that can be used to ensure
* that the file structure has actually been installed.
*
* @param bool $install Set this parameter to true to
* install the folder structure if it is missing.
*
* @return bool True if the folder structure exists,
* otherwise false.
*/
public function checkFolderStructure(bool $install = false): bool
{
// Make sure that the base path is inside the private files dir.
// The files dir has appropriate write permissions and is assumed
// to be protected against information leak and symlink attacks.
$filesDir = realpath(Config::getVar('files', 'files_dir'));
if (is_null($this->_basePath) || strpos($this->_basePath, $filesDir) !== 0) {
$this->addExecutionLogEntry(
__('admin.fileLoader.wrongBasePathLocation', ['path' => $this->_basePath]),
ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR
);
return false;
}
// Check folder presence and readability.
$pathsToCheck = [
$this->_stagePath,
$this->_archivePath,
$this->_rejectPath,
$this->_processingPath,
$this->_dispatchPath
];
$fileManager = null;
foreach ($pathsToCheck as $path) {
if (!(is_dir($path) && is_readable($path))) {
if ($install) {
// Try installing the folder if it is missing.
if (is_null($fileManager)) {
$fileManager = new FileManager();
}
$fileManager->mkdirtree($path);
}
// Try again.
if (!(is_dir($path) && is_readable($path))) {
// Give up...
$this->addExecutionLogEntry(
__('admin.fileLoader.pathNotAccessible', ['path' => $path]),
ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR
);
return false;
}
}
}
return true;
}
//
// Protected methods.
//
/**
* @copydoc ScheduledTask::executeActions()
*/
protected function executeActions(): bool
{
if (!$this->checkFolderStructure()) {
return false;
}
$foundErrors = false;
while (!is_null($filePath = $this->claimNextFile())) {
if ($filePath === false) {
// Problem claiming the file.
$foundErrors = true;
break;
}
try {
$result = $this->processFile($filePath);
} catch (Exception $e) {
$foundErrors = true;
$this->rejectFile();
$this->addExecutionLogEntry($e->getMessage(), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR);
continue;
}
if ($result === self::FILE_LOADER_RETURN_TO_STAGING) {
// Send the file back to staging
$foundErrors = true;
$this->stageFile();
// Let the script know what files were sent back to staging,
// so it doesn't claim them again thereby entering an infinite loop.
$this->_stagedBackFiles[] = $this->_claimedFilename;
} elseif ($result === self::FILE_LOADER_RETURN_TO_DISPATCH) {
// Move the file to dispatch folder, where a dispatched job will find it
$this->dispatchFile();
$this->addExecutionLogEntry(__(
'admin.fileLoader.fileDispatched',
['filename' => $filePath]
), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_NOTICE);
} else {
$this->archiveFile();
}
if ($result === true) {
$this->addExecutionLogEntry(__(
'admin.fileLoader.fileProcessed',
['filename' => $filePath]
), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_NOTICE);
}
}
return !$foundErrors;
}
/**
* Process the passed file.
*
* @throws \Exception
*
* @return mixed True or self::FILE_LOADER_RETURN_TO_STAGING
*
* @see FileLoader::executeActions() to understand the expected return values.
*
*/
abstract protected function processFile(string $filePath): bool|int;
/**
* Move file between filesystem directories.
*
* @return string The destination path of the moved file.
*/
protected function moveFile(string $sourceDir, string $destDir, string $filename): string
{
$currentFilePath = "{$sourceDir}/{$filename}";
$destinationPath = "{$destDir}/{$filename}";
if (!rename($currentFilePath, $destinationPath)) {
$message = __('admin.fileLoader.moveFileFailed', ['filename' => $filename,
'currentFilePath' => $currentFilePath, 'destinationPath' => $destinationPath]);
$this->addExecutionLogEntry($message, ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR);
// Script should always stop if it can't manipulate files inside
// its own directory system.
fatalError($message);
}
return $destinationPath;
}
//
// Private helper methods.
//
/**
* Claim the first file that's inside the staging folder.
*
* @return mixed The claimed file path or false if
* the claim was not successful.
*/
private function claimNextFile(): string|false|null
{
$stageDir = opendir($this->_stagePath);
$processingFilePath = false;
while ($filename = readdir($stageDir)) {
if ($filename == '..' || $filename == '.' ||
in_array($filename, $this->_stagedBackFiles) ||
(!empty($this->_onlyConsiderFiles) && !in_array($filename, $this->_onlyConsiderFiles))) {
continue;
}
$processingFilePath = $this->moveFile($this->_stagePath, $this->_processingPath, $filename);
break;
}
if (pathinfo($processingFilePath, PATHINFO_EXTENSION) == 'gz') {
$fileMgr = new FileManager();
try {
$processingFilePath = $fileMgr->gzDecompressFile($processingFilePath);
$filename = pathinfo($processingFilePath, PATHINFO_BASENAME);
} catch (Exception $e) {
$this->moveFile($this->_processingPath, $this->_stagePath, $filename);
$this->addExecutionLogEntry($e->getMessage(), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR);
return false;
}
}
if ($processingFilePath) {
$this->_claimedFilename = $filename;
return $processingFilePath;
} else {
return null;
}
}
/**
* Reject the current claimed file.
*/
private function rejectFile(): void
{
$this->moveFile($this->_processingPath, $this->_rejectPath, $this->_claimedFilename);
}
/**
* Move the current claimed file into the dispatch folder.
*/
protected function dispatchFile(): void
{
$this->moveFile($this->_processingPath, $this->_dispatchPath, $this->_claimedFilename);
}
/**
* Archive the current claimed file.
*/
private function archiveFile(): void
{
$this->moveFile($this->_processingPath, $this->_archivePath, $this->_claimedFilename);
if ($this->getCompressArchives()) {
try {
$fileMgr = new FileManager();
$filePath = "{$this->_archivePath}/{$this->_claimedFilename}";
$fileMgr->gzCompressFile($filePath);
} catch (Exception $e) {
$this->addExecutionLogEntry($e->getMessage(), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR);
}
}
}
/**
* Stage the current claimed file.
*/
private function stageFile(): void
{
$this->moveFile($this->_processingPath, $this->_stagePath, $this->_claimedFilename);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\task\FileLoader', '\FileLoader');
foreach ([
'FILE_LOADER_RETURN_TO_STAGING',
'FILE_LOADER_ERROR_MESSAGE_TYPE',
'FILE_LOADER_WARNING_MESSAGE_TYPE',
'FILE_LOADER_PATH_STAGING',
'FILE_LOADER_PATH_PROCESSING',
'FILE_LOADER_PATH_REJECT',
'FILE_LOADER_PATH_ARCHIVE',
] as $constantName) {
define($constantName, constant('\FileLoader::' . $constantName));
}
}
@@ -0,0 +1,292 @@
<?php
/**
* @file classes/tasks/PKPUsageStatsLoader.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 PKPUsageStatsLoader
*
* @ingroup tasks
*
* @brief Scheduled task to extract transform and load usage statistics data into database.
*/
namespace PKP\task;
use APP\core\Application;
use APP\core\Services;
use APP\statistics\StatisticsHelper;
use Illuminate\Support\Facades\Bus;
use PKP\file\FileManager;
use PKP\jobs\statistics\CompileMonthlyMetrics;
use PKP\scheduledTask\ScheduledTaskHelper;
use PKP\site\Site;
use Throwable;
abstract class PKPUsageStatsLoader extends FileLoader
{
/**
* If the log files should be automatically moved to te stage folder.
* This is the case for daily log file processing.
* This is not the case if the whole month is reprocessed - all log files for the given month should be manually placed in the stage folder.
*/
private bool $autoStage;
/** List of months the processed daily log files are from, to consider for monthly aggregation */
private array $months = [];
/** List of log files that needs to be processed within this scheduled task, and the jobs needs to be chained for. */
private array $logFiles = [];
/**
* Constructor.
*/
public function __construct(array $args)
{
$this->autoStage = true;
// if log files for a whole month should be reprocessed,
// the month is given as parameter
if (!empty($args)) {
$reprocessMonth = current($args);
$reprocessFiles = $this->getStagedFilesByMonth($reprocessMonth);
$this->setOnlyConsiderFiles($reprocessFiles);
$this->autoStage = false;
}
// shall the archived log files be compressed
$site = Application::get()->getRequest()->getSite();
if ($site->getData('compressStatsLogs')) {
$this->setCompressArchives(true);
}
// Define the base filesystem path.
$basePath = StatisticsHelper::getUsageStatsDirPath();
$args[0] = $basePath;
parent::__construct($args);
$this->checkFolderStructure(true);
}
/**
* @copydoc FileLoader::getName()
*/
public function getName(): string
{
return __('admin.scheduledTask.usageStatsLoader');
}
/**
* Get the jobs needed to process a usage stats log file and compile the stats.
* The jobs have to be in the right execution order.
*
* @return BaseJob[]
*/
abstract protected function getFileJobs(string $filePath, Site $site): array;
/**
* @copydoc FileLoader::executeActions()
*/
protected function executeActions(): bool
{
// It's possible that the processing directory has files that
// were being processed but the php process was stopped before
// finishing the processing, or there may be a concurrent process running.
// Warn the user if this is the case.
$processingDirFiles = glob($this->getProcessingPath() . '/' . '*');
$processingDirError = is_array($processingDirFiles) && count($processingDirFiles);
// If the processing directory is not empty (and this is not the reprocessing of the older log files)
// log that message
if ($processingDirError && !empty($this->getOnlyConsiderFiles())) {
$this->addExecutionLogEntry(__('admin.scheduledTask.usageStatsLoader.processingPathNotEmpty', ['directory' => $this->getProcessingPath()]), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR);
}
if ($this->autoStage) {
$this->autoStage();
}
$processFilesResult = parent::executeActions();
if (!$processFilesResult) {
return false;
}
$site = Application::get()->getRequest()->getSite();
$jobs = [];
foreach ($this->logFiles as $filePath) {
$jobsPerFile = $this->getFileJobs($filePath, $site);
$jobs = array_merge($jobs, $jobsPerFile);
}
foreach ($this->months as $month) {
$compileMonthlyMetricsJob = new CompileMonthlyMetrics($month, $site);
$jobs = array_merge($jobs, [$compileMonthlyMetricsJob]);
}
// Bus::chain() cannot accept an empty array
if (!empty($jobs)) {
Bus::chain($jobs)
->catch(function (Throwable $e) {
})
->dispatch();
$this->addExecutionLogEntry(__(
'admin.scheduledTask.usageStatsLoader.jobDispatched'
), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_NOTICE);
}
return (!$processingDirError);
}
/**
* Check if the log file's date is later than the first installation of the new log file format,
* so that the log file can be processed.
*/
protected function isDateValid(string $loadId): bool
{
$date = substr($loadId, -12, 8);
// Get the date when the version that uses the new log file format (and COUNTER R5) is installed.
// Only the log files later than that day can be (regularly) processed here.
$statsService = Services::get('sushiStats');
$dateR5Installed = date('Ymd', strtotime($statsService->getEarliestDate()));
if ($date < $dateR5Installed) {
// the log file is in old log file format
// return the file to staging and
// log the error
$this->addExecutionLogEntry(__(
'admin.scheduledTask.usageStatsLoader.veryOldLogFile',
['file' => $loadId]
), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR);
return false;
}
return true;
}
/**
* Check if stats for the log file's month do not already exist.
* Return true if they do not exist, so that log file can be processed.
* Else, return the file to staging and log the error that
* the CLI script for reprocessing should be called.
* If the log files of the month are being reprocessed,
* the CLI reprocessing script will first remove all the stats for the month,
* so that this function will return true in that case.
*/
protected function isMonthValid(string $loadId, string $month): bool
{
$currentMonth = date('Ym');
$lastMonth = date('Ym', strtotime('last month'));
$site = Application::get()->getRequest()->getSite();
// If the daily metrics are not kept, and this is not the current month (which is kept in the DB)
// the CLI script to reprocess the whole month should be called.
if (!$site->getData('keepDailyUsageStats') && $month != $currentMonth && $month != $lastMonth) {
$statsService = Services::get('sushiStats');
$counterMonthExists = $statsService->monthExists($month);
$geoService = Services::get('geoStats');
$geoMonthExists = $geoService->monthExists($month);
if ($counterMonthExists || $geoMonthExists) {
$this->addExecutionLogEntry(__(
'admin.scheduledTask.usageStatsLoader.monthExists',
['file' => $loadId]
), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR);
return false;
}
}
return true;
}
/**
* Add the log file's month to the list of months to be considered for the
* stats aggregation after the current log files are processed.
*/
protected function considerMonthForStatsAggregation(string $month): void
{
if (!in_array($month, $this->months)) {
$this->months[] = $month;
}
}
/**
* @copydoc FileLoader::processFile()
* The file name MUST be of form usage_events_YYYYMMDD.log
* If the function successfully finishes, the file will be archived.
*/
protected function processFile(string $filePath): bool|int
{
$loadId = basename($filePath);
$month = substr($loadId, -12, 6);
// if the file is not being reprocessed using the CLI tool
if (!in_array($loadId, $this->getOnlyConsiderFiles())) {
// Check if the log file is an old log file and if the stats for the month already exist
if (!$this->isDateValid($loadId) || !$this->isMonthValid($loadId, $month)) {
return self::FILE_LOADER_RETURN_TO_STAGING;
}
}
// Add this log file to the list, so that all jobs, for all files can be chained.
$this->logFiles[] = $loadId;
// Add this log file's month to the list of months the stats need to be aggregated for.
$this->considerMonthForStatsAggregation($month);
return self::FILE_LOADER_RETURN_TO_DISPATCH;
}
/**
* Auto stage usage stats log files, also moving files that
* might be in processing folder to stage folder.
*/
protected function autoStage(): void
{
// Copy all log files to stage directory, except the current day one.
$fileManager = new FileManager();
$logFiles = [];
$logsDirFiles = glob($this->getUsageEventLogsPath() . '/*');
if (is_array($logsDirFiles)) {
$logFiles = array_merge($logFiles, $logsDirFiles);
}
// It's possible that the processing directory have files that
// were being processed but the php process was stopped before
// finishing the processing. Just copy them to the stage directory too.
$processingDirFiles = glob($this->getProcessingPath() . '/*');
if (is_array($processingDirFiles)) {
$logFiles = array_merge($logFiles, $processingDirFiles);
}
foreach ($logFiles as $filePath) {
if ($fileManager->fileExists($filePath)) {
$filename = pathinfo($filePath, PATHINFO_BASENAME);
$currentDayFilename = $this->getUsageEventCurrentDayLogName();
if ($filename == $currentDayFilename) {
continue;
}
$this->moveFile(pathinfo($filePath, PATHINFO_DIRNAME), $this->getStagePath(), $filename);
}
}
}
/**
* Get staged usage log files belonging to a month, that should be reprocessed
*/
protected function getStagedFilesByMonth(string $month): array
{
$files = [];
$stagePath = StatisticsHelper::getUsageStatsDirPath() . '/' . self::FILE_LOADER_PATH_STAGING;
$stageDir = opendir($stagePath);
while ($filename = readdir($stageDir)) {
if (str_starts_with($filename, 'usage_events_' . $month)) {
$files[] = $filename;
}
}
return $files;
}
/**
* Get the usage event logs directory path.
*/
protected function getUsageEventLogsPath(): string
{
return StatisticsHelper::getUsageStatsDirPath() . '/usageEventLogs';
}
/**
* Get current day usage event log name.
*/
protected function getUsageEventCurrentDayLogName(): string
{
return 'usage_events_' . date('Ymd') . '.log';
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
/**
* @file classes/task/ProcessQueueJobs.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 ProcessQueueJobs
*
* @ingroup tasks
*
* @brief Class to process queue jobs via the schedular task
*/
namespace PKP\task;
use APP\core\Application;
use PKP\config\Config;
use PKP\queue\JobRunner;
use PKP\scheduledTask\ScheduledTask;
class ProcessQueueJobs extends ScheduledTask
{
/**
* @copydoc ScheduledTask::getName()
*/
public function getName()
{
return __('admin.scheduledTask.processQueueJobs');
}
/**
* @copydoc ScheduledTask::executeActions()
*/
public function executeActions()
{
if (Application::isUnderMaintenance() || !Config::getVar('queues', 'job_runner', true)) {
return true;
}
$jobQueue = app('pkpJobQueue');
$jobBuilder = $jobQueue->getJobModelBuilder();
if ($jobBuilder->count() <= 0) {
return true;
}
// Executes all pending jobs when running the runScheduledTasks.php on the CLI
if (runOnCLI('runScheduledTasks.php')) {
while ($jobBuilder->count()) {
$jobQueue->runJobInQueue();
}
return true;
}
// Executes a limited number of jobs when processing a request
(new JobRunner($jobQueue))
->withMaxExecutionTimeConstrain()
->withMaxJobsConstrain()
->withMaxMemoryConstrain()
->withEstimatedTimeToProcessNextJobConstrain()
->processJobs($jobBuilder);
return true;
}
}
@@ -0,0 +1,64 @@
<?php
/**
* @file classes/task/PublishSubmissions.php
*
* Copyright (c) 2013-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PublishSubmissions
*
* @ingroup tasks
*
* @brief Class to published submissions scheduled for publication.
*/
namespace PKP\task;
use APP\core\Services;
use APP\facades\Repo;
use APP\submission\Submission;
use PKP\core\Core;
use PKP\scheduledTask\ScheduledTask;
class PublishSubmissions extends ScheduledTask
{
/**
* @copydoc ScheduledTask::getName()
*/
public function getName()
{
return __('admin.scheduledTask.publishSubmissions');
}
/**
* @copydoc ScheduledTask::executeActions()
*/
public function executeActions()
{
$contextIds = Services::get('context')->getIds([
'isEnabled' => true,
]);
foreach ($contextIds as $contextId) {
$submissions = Repo::submission()
->getCollector()
->filterByContextIds([$contextId])
->filterByStatus([Submission::STATUS_SCHEDULED])
->getMany();
foreach ($submissions as $submission) {
$datePublished = $submission->getCurrentPublication()->getData('datePublished');
if ($datePublished && strtotime($datePublished) <= strtotime(Core::getCurrentDate())) {
Repo::publication()->publish($submission->getCurrentPublication());
}
}
}
return true;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\task\PublishSubmissions', '\PublishSubmissions');
}
+50
View File
@@ -0,0 +1,50 @@
<?php
/**
* @file classes/task/RemoveFailedJobs.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 RemoveFailedJobs
*
* @ingroup tasks
*
* @brief Remove the much older failed jobs form the failed list
*/
namespace PKP\task;
use Carbon\Carbon;
use PKP\config\Config;
use PKP\scheduledTask\ScheduledTask;
class RemoveFailedJobs extends ScheduledTask
{
/**
* @copydoc ScheduledTask::getName()
*/
public function getName()
{
return __('admin.scheduledTask.removeFailedJobs');
}
/**
* @copydoc ScheduledTask::executeActions()
*/
public function executeActions()
{
$cleanUpPeriod = (int) Config::getVar('queues', 'delete_failed_jobs_after', null);
// No need to run the clean up process if the cleaning period is not defined in config
if (!$cleanUpPeriod) {
return true;
}
app('queue.failer')->prune(Carbon::now()->subDays($cleanUpPeriod)->startOfDay());
return true;
}
}
@@ -0,0 +1,57 @@
<?php
/**
* @file classes/task/RemoveUnvalidatedExpiredUser.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 RemoveUnvalidatedExpiredUser
*
* @ingroup tasks
*
* @brief Class to remove all unvalidated and expired users after validation timeout
*/
namespace PKP\task;
use APP\facades\Repo;
use Carbon\Carbon;
use PKP\config\Config;
use PKP\scheduledTask\ScheduledTask;
class RemoveUnvalidatedExpiredUsers extends ScheduledTask
{
/**
* @copydoc ScheduledTask::getName()
*/
public function getName()
{
return __('admin.scheduledTask.removeUnvalidatedExpiredUsers');
}
/**
* @copydoc ScheduledTask::executeActions()
*/
public function executeActions()
{
// No need to remove invalidated users if validation requirement is turned off
if (!Config::getVar('email', 'require_validation', false)) {
return true;
}
$validationMaxDeadlineInDays = (int) Config::getVar('general', 'user_validation_period');
if ($validationMaxDeadlineInDays <= 0) {
return true;
}
$dateTillValid = Carbon::now()->startOfDay()->subDays($validationMaxDeadlineInDays);
Repo::user()->deleteUnvalidatedExpiredUsers($dateTillValid);
return true;
}
}
+176
View File
@@ -0,0 +1,176 @@
<?php
/**
* @file classes/task/ReviewReminder.php
*
* Copyright (c) 2013-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewReminder
*
* @ingroup tasks
*
* @brief Class to perform automated reminders for reviewers.
*/
namespace PKP\task;
use APP\core\Application;
use APP\facades\Repo;
use Illuminate\Support\Facades\Mail;
use PKP\context\Context;
use PKP\core\Core;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
use PKP\log\event\PKPSubmissionEventLogEntry;
use PKP\mail\mailables\ReviewRemindAuto;
use PKP\mail\mailables\ReviewResponseRemindAuto;
use PKP\scheduledTask\ScheduledTask;
use PKP\security\AccessKeyManager;
use PKP\submission\PKPSubmission;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
class ReviewReminder extends ScheduledTask
{
/**
* @copydoc ScheduledTask::getName()
*/
public function getName()
{
return __('admin.scheduledTask.reviewReminder');
}
/**
* Send the automatic review reminder to the reviewer.
*/
public function sendReminder(
ReviewAssignment $reviewAssignment,
PKPSubmission $submission,
Context $context,
ReviewRemindAuto|ReviewResponseRemindAuto $mailable
): void {
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewId = $reviewAssignment->getId();
$reviewer = Repo::user()->get($reviewAssignment->getReviewerId());
if (!isset($reviewer)) {
return;
}
$primaryLocale = $context->getPrimaryLocale();
$emailTemplate = Repo::emailTemplate()->getByKey($context->getId(), $mailable::getEmailTemplateKey());
$mailable->subject($emailTemplate->getLocalizedData('subject', $primaryLocale))
->body($emailTemplate->getLocalizedData('body', $primaryLocale))
->from($context->getData('contactEmail'), $context->getData('contactName'))
->recipients([$reviewer]);
$mailable->setData($primaryLocale);
$application = Application::get();
$request = $application->getRequest();
$dispatcher = $application->getDispatcher();
$reviewerAccessKeysEnabled = $context->getData('reviewerAccessKeysEnabled');
if ($reviewerAccessKeysEnabled) { // Give one-click access if enabled
$accessKeyManager = new AccessKeyManager();
// Key lifetime is the typical review period plus four weeks
$keyLifetime = ($context->getData('numWeeksPerReview') + 4) * 7;
$accessKey = $accessKeyManager->createKey($context->getId(), $reviewer->getId(), $reviewId, $keyLifetime);
$reviewUrlArgs = ['submissionId' => $reviewAssignment->getSubmissionId(), 'reviewId' => $reviewId, 'key' => $accessKey];
$submissionReviewUrl = $dispatcher->url($request, PKPApplication::ROUTE_PAGE, $context->getPath(), 'reviewer', 'submission', null, $reviewUrlArgs);
$mailable->addData(['submissionReviewUrl' => $submissionReviewUrl]);
}
// deprecated template variables OJS 2.x
$mailable->addData([
'messageToReviewer' => __('reviewer.step1.requestBoilerplate'),
'abstractTermIfEnabled' => ($submission->getLocalizedAbstract() == '' ? '' : __('common.abstract')),
]);
Mail::send($mailable);
$reviewAssignment->setDateReminded(Core::getCurrentDate());
$reviewAssignment->setReminderWasAutomatic(1);
$reviewAssignmentDao->updateObject($reviewAssignment);
$eventLog = Repo::eventLog()->newDataObject([
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION,
'assocId' => $submission->getId(),
'eventType' => PKPSubmissionEventLogEntry::SUBMISSION_LOG_REVIEW_REMIND_AUTO,
'userId' => null,
'message' => 'submission.event.reviewer.reviewerRemindedAuto',
'isTranslated' => false,
'dateLogged' => Core::getCurrentDate(),
'recipientId' => $reviewer->getId(),
'recipientName' => $reviewer->getFullName(),
]);
Repo::eventLog()->add($eventLog);
}
/**
* @copydoc ScheduledTask::executeActions()
*/
public function executeActions()
{
$submission = null;
$context = null;
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$contextDao = Application::getContextDAO();
$incompleteAssignments = $reviewAssignmentDao->getIncompleteReviewAssignments();
$inviteReminderDays = $submitReminderDays = null;
foreach ($incompleteAssignments as $reviewAssignment) {
// Avoid review assignments that a reminder exists for.
if ($reviewAssignment->getDateReminded() !== null) {
continue;
}
// Fetch the submission
if ($submission == null || $submission->getId() != $reviewAssignment->getSubmissionId()) {
unset($submission);
$submission = Repo::submission()->get($reviewAssignment->getSubmissionId());
// Avoid review assignments without submission in database.
if (!$submission) {
continue;
}
}
if ($submission->getStatus() != PKPSubmission::STATUS_QUEUED) {
continue;
}
// Fetch the context
if ($context == null || $context->getId() != $submission->getContextId()) {
unset($context);
$context = $contextDao->getById($submission->getContextId());
$inviteReminderDays = $context->getData('numDaysBeforeInviteReminder');
$submitReminderDays = $context->getData('numDaysBeforeSubmitReminder');
}
$mailable = null;
if ($submitReminderDays >= 1 && $reviewAssignment->getDateDue() != null) {
$checkDate = strtotime($reviewAssignment->getDateDue());
if (time() - $checkDate > 60 * 60 * 24 * $submitReminderDays) {
$mailable = new ReviewRemindAuto($context, $submission, $reviewAssignment);
}
}
if ($inviteReminderDays >= 1 && $reviewAssignment->getDateConfirmed() == null) {
$checkDate = strtotime($reviewAssignment->getDateResponseDue());
if (time() - $checkDate > 60 * 60 * 24 * $inviteReminderDays) {
$mailable = new ReviewResponseRemindAuto($context, $submission, $reviewAssignment);
}
}
if ($mailable) {
$this->sendReminder($reviewAssignment, $submission, $context, $mailable);
}
}
return true;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
/**
* @file classes/task/StatisticsReport.php
*
* Copyright (c) 2013-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class StatisticsReport
*
* @ingroup tasks
*
* @brief Class responsible to send the monthly statistics report.
*/
namespace PKP\task;
use APP\core\Application;
use DateTimeImmutable;
use Illuminate\Support\Facades\Bus;
use PKP\db\DAORegistry;
use PKP\jobs\notifications\StatisticsReportMail;
use PKP\jobs\notifications\StatisticsReportNotify;
use PKP\mail\Mailer;
use PKP\notification\managerDelegate\EditorialReportNotificationManager;
use PKP\notification\NotificationSubscriptionSettingsDAO;
use PKP\notification\PKPNotification;
use PKP\scheduledTask\ScheduledTask;
use PKP\security\Role;
class StatisticsReport extends ScheduledTask
{
/** @var array List of roles that might be notified */
private $_roleIds = [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR];
/**
* @copydoc ScheduledTask::getName()
*/
public function getName(): string
{
return __('admin.scheduledTask.statisticsReport');
}
/**
* @copydoc ScheduledTask::executeActions()
*/
public function executeActions(): bool
{
$contextDao = Application::get()->getContextDAO();
$dateStart = new DateTimeImmutable('first day of previous month midnight');
$dateEnd = new DateTimeImmutable('first day of this month midnight');
$jobs = [];
for ($contexts = $contextDao->getAll(true); $context = $contexts->next();) {
if (!$context->getData('editorialStatsEmail')) {
continue;
}
/** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
$notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');
$editorialReportNotificationManager = new EditorialReportNotificationManager(PKPNotification::NOTIFICATION_TYPE_EDITORIAL_REPORT);
$editorialReportNotificationManager->initialize(
$context
);
$userIdsToNotify = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY],
[PKPNotification::NOTIFICATION_TYPE_EDITORIAL_REPORT],
[$context->getId()],
$this->_roleIds
);
foreach ($userIdsToNotify->chunk(PKPNotification::NOTIFICATION_CHUNK_SIZE_LIMIT) as $notifyUserIds) {
$notifyJob = new StatisticsReportNotify(
$notifyUserIds,
$editorialReportNotificationManager
);
$jobs[] = $notifyJob;
}
$userIdsToMail = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY, NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY],
[PKPNotification::NOTIFICATION_TYPE_EDITORIAL_REPORT],
[$context->getId()],
$this->_roleIds
);
foreach ($userIdsToMail->chunk(Mailer::BULK_EMAIL_SIZE_LIMIT) as $mailUserIds) {
$mailJob = new StatisticsReportMail(
$mailUserIds,
$context->getId(),
$dateStart,
$dateEnd
);
$jobs[] = $mailJob;
}
}
Bus::batch($jobs)->dispatch();
return true;
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
/**
* @file classes/task/UpdateIPGeoDB.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 UpdateIPGeoDB
*
* @ingroup tasks
*
* @brief Class responsible to monthly update of the DB-IP city lite database used for Geo statistics.
*/
namespace PKP\task;
use APP\core\Application;
use Exception;
use PKP\file\FileManager;
use PKP\file\PrivateFileManager;
use PKP\scheduledTask\ScheduledTask;
use PKP\scheduledTask\ScheduledTaskHelper;
use PKP\statistics\PKPStatisticsHelper;
class UpdateIPGeoDB extends ScheduledTask
{
/**
* @copydoc ScheduledTask::getName()
*/
public function getName(): string
{
return __('admin.scheduledTask.updateGeoDB');
}
/**
* @copydoc ScheduledTask::executeActions()
*/
public function executeActions(): bool
{
$dbipCityLiteFileName = 'dbip-city-lite-' . date('Y') . '-' . date('m') . '.mmdb.gz';
$dbipCityLiteFile = 'https://download.db-ip.com/free/' . $dbipCityLiteFileName;
$fileMgr = new PrivateFileManager();
$downloadedFile = PKPStatisticsHelper::getUsageStatsDirPath() . '/' . $dbipCityLiteFileName;
$finalFileName = PKPStatisticsHelper::getGeoDBPath();
try {
$client = Application::get()->getHttpClient();
$client->request('GET', $dbipCityLiteFile, ['sink' => $downloadedFile]);
} catch (Exception $e) {
$this->addExecutionLogEntry($e->getMessage(), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR);
return false;
}
try {
$decompressedFile = $fileMgr->gzDecompressFile($downloadedFile);
} catch (Exception $e) {
$this->addExecutionLogEntry($e->getMessage(), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR);
return false;
}
if (!rename($decompressedFile, $finalFileName)) {
$this->addExecutionLogEntry(__('admin.scheduledTask.updateGeoDB.fileRename.error', ['sourceFilename' => $decompressedFile,
'targetFilename' => $finalFileName]), ScheduledTaskHelper::SCHEDULED_TASK_MESSAGE_TYPE_ERROR);
return false;
}
return $fileMgr->setMode($finalFileName, FileManager::FILE_MODE_MASK);
}
}