<?php
/**
* @file classes/submissionFile/Repository.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 Repository
*
* @brief A repository to find and manage submission files.
*/
namespace PKP\submissionFile;
use APP\core\Application;
use APP\core\Request;
use APP\core\Services;
use APP\facades\Repo;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use PKP\core\Core;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
use PKP\log\SubmissionEmailLogDAO;
use PKP\log\SubmissionEmailLogEntry;
use PKP\log\event\SubmissionFileEventLogEntry;
use PKP\mail\mailables\RevisedVersionNotify;
use PKP\note\NoteDAO;
use PKP\notification\PKPNotification;
use PKP\plugins\Hook;
use PKP\query\QueryDAO;
use PKP\security\authorization\SubmissionFileAccessPolicy;
use PKP\security\Role;
use PKP\security\Validation;
use PKP\services\PKPSchemaService;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\submissionFile\maps\Schema;
use PKP\validation\ValidatorFactory;
abstract class Repository
{
public DAO $dao;
public string $schemaMap = Schema::class;
protected Request $request;
/** @var PKPSchemaService<SubmissionFile> */
protected PKPSchemaService $schemaService;
/** @var array<int> $reviewFileStages The file stages that are part of a review workflow stage */
public array $reviewFileStages = [];
public function __construct(DAO $dao, Request $request, PKPSchemaService $schemaService)
{
$this->schemaService = $schemaService;
$this->dao = $dao;
$this->request = $request;
}
/** @copydoc DAO::newDataObject() */
public function newDataObject(array $params = []): SubmissionFile
{
$object = $this->dao->newDataObject();
if (!empty($params)) {
$object->setAllData($params);
}
return $object;
}
/** @copydoc DAO::get() */
public function get(int $id, int $submissionId = null): ?SubmissionFile
{
return $this->dao->get($id, $submissionId);
}
/** @copydoc DAO::exists() */
public function exists(int $id, int $submissionId = null): bool
{
return $this->dao->exists($id, $submissionId);
}
/** @copydoc DAO::getCollector() */
public function getCollector(): Collector
{
return app(Collector::class);
}
/**
* Get an instance of the map class for mapping
* submission Files to their schema
*/
public function getSchemaMap(): Schema
{
return app('maps')->withExtensions($this->schemaMap);
}
/**
* Validate properties for a submission file
*
* Perform validation checks on data used to add or edit a submission file.
*
* @param array $props A key/value array with the new data to validate
* @param array $allowedLocales The context's supported locales
* @param string $primaryLocale The context's primary locale
*
* @return array A key/value array with validation errors. Empty if no errors
*/
public function validate(
?SubmissionFile $object,
array $props,
array $allowedLocales,
string $primaryLocale
): array {
$validator = ValidatorFactory::make(
$props,
$this->schemaService->getValidationRules($this->dao->schema, $allowedLocales),
[]
);
// Check required fields
ValidatorFactory::required(
$validator,
$object,
$this->schemaService->getRequiredProps($this->dao->schema),
$this->schemaService->getMultilingualProps($this->dao->schema),
$allowedLocales,
$primaryLocale
);
// Check for input from disallowed locales
ValidatorFactory::allowedLocales($validator, $this->schemaService->getMultilingualProps($this->dao->schema), $allowedLocales);
// Do not allow the uploaderUserId or createdAt properties to be modified
if ($object) {
$validator->after(function ($validator) use ($props) {
if (
!empty($props['uploaderUserId']) &&
!$validator->errors()->get('uploaderUserId')
) {
$validator
->errors()
->add(
'uploaderUserId',
__('submission.file.notAllowedUploaderUserId')
);
}
if (
!empty($props['createdAt']) &&
!$validator->errors()->get('createdAt')
) {
$validator
->errors()
->add(
'createdAt',
__('api.files.400.notAllowedCreatedAt')
);
}
});
}
// Make sure that file stage and assocType match
if (isset($props['assocType'])) {
$validator->after(function ($validator) use ($props) {
if (
$props['assocType'] === PKPApplication::ASSOC_TYPE_REVIEW_ROUND &&
!in_array(
$props['fileStage'],
[SubmissionFile::SUBMISSION_FILE_REVIEW_FILE, SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION, SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE, SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION]
)
) {
$validator
->errors()
->add(
'assocType',
__('api.submissionFiles.400.badReviewRoundAssocType')
);
}
if ($props['assocType'] === PKPApplication::ASSOC_TYPE_REVIEW_ASSIGNMENT && $props['fileStage'] !== SubmissionFile::SUBMISSION_FILE_REVIEW_ATTACHMENT) {
$validator
->errors()
->add(
'assocType',
__('api.submissionFiles.400.badReviewAssignmentAssocType')
);
}
if (
$props['assocType'] === PKPApplication::ASSOC_TYPE_SUBMISSION_FILE &&
$props['fileStage'] !== SubmissionFile::SUBMISSION_FILE_DEPENDENT
) {
$validator
->errors()
->add(
'assocType',
__('api.submissionFiles.400.badDependentFileAssocType')
);
}
if (
$props['assocType'] === PKPApplication::ASSOC_TYPE_NOTE &&
$props['fileStage'] !== SubmissionFile::SUBMISSION_FILE_NOTE
) {
$validator
->errors()
->add(
'assocType',
__('api.submissionFiles.400.badNoteAssocType')
);
}
if (
$props['assocType'] === PKPApplication::ASSOC_TYPE_REPRESENTATION &&
$props['fileStage'] !== SubmissionFile::SUBMISSION_FILE_PROOF
) {
$validator
->errors()
->add(
'assocType',
__('api.submissionFiles.400.badRepresentationAssocType')
);
}
});
}
$errors = [];
if ($validator->fails()) {
$errors = $this->schemaService->formatValidationErrors($validator->errors());
}
Hook::call(
'SubmissionFile::validate',
[
&$errors,
$object,
$props,
$allowedLocales,
$primaryLocale
]
);
return $errors;
}
/** @copydoc DAO::insert() */
public function add(SubmissionFile $submissionFile): int
{
$submissionFile->setData('createdAt', Core::getCurrentDate());
$submissionFile->setData('updatedAt', Core::getCurrentDate());
$submissionFileId = $this->dao->insert($submissionFile);
$submissionFile = $this->get($submissionFileId);
Hook::call('SubmissionFile::add', [$submissionFile]);
$logData = $this->getSubmissionFileLogData($submissionFile);
$logEntry = Repo::eventLog()->newDataObject(array_merge(
$logData,
[
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION_FILE,
'assocId' => $submissionFile->getId(),
'eventType' => SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_UPLOAD,
'dateLogged' => Core::getCurrentDate(),
'message' => 'submission.event.fileUploaded',
'isTranslated' => false,
]
));
Repo::eventLog()->add($logEntry);
$submission = Repo::submission()->get($submissionFile->getData('submissionId'));
$logEntry = Repo::eventLog()->newDataObject(array_merge(
$logData,
[
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION,
'assocId' => $submission->getId(),
'eventType' => SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_REVISION_UPLOAD,
'dateLogged' => Core::getCurrentDate(),
'message' => 'submission.event.fileRevised',
'isTranslated' => false,
]
));
Repo::eventLog()->add($logEntry);
// Update status and notifications when revisions have been uploaded
if ($submissionFile->getData('fileStage') === SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION ||
$submissionFile->getData('fileStage') === SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION) {
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getById($submissionFile->getData('assocId'));
if (!$reviewRound) {
throw new Exception('Submission file added to review round that does not exist.');
}
$reviewRoundDao->updateStatus($reviewRound);
// Update author notifications
$authorUserIds = [];
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$authorAssignments = $stageAssignmentDao->getBySubmissionAndRoleIds($submissionFile->getData('submissionId'), [Role::ROLE_ID_AUTHOR]);
while ($assignment = $authorAssignments->next()) {
if ($assignment->getStageId() == $reviewRound->getStageId()) {
$authorUserIds[] = (int) $assignment->getUserId();
}
}
$notificationMgr = new NotificationManager();
$notificationMgr->updateNotification(
$this->request,
[PKPNotification::NOTIFICATION_TYPE_PENDING_INTERNAL_REVISIONS, PKPNotification::NOTIFICATION_TYPE_PENDING_EXTERNAL_REVISIONS],
$authorUserIds,
PKPApplication::ASSOC_TYPE_SUBMISSION,
$submissionFile->getData('submissionId')
);
// Notify editors if the file is uploaded by an author
if (in_array($submissionFile->getData('uploaderUserId'), $authorUserIds)) {
if (!$submission) {
throw new Exception('Submission file added to submission that does not exist.');
}
$this->notifyEditorsRevisionsUploaded($submissionFile);
}
}
return $submissionFileId;
}
/** @copydoc DAO::update() */
public function edit(
SubmissionFile $submissionFile,
array $params
): void {
$newSubmissionFile = clone $submissionFile;
$newSubmissionFile->setAllData(array_merge($newSubmissionFile->_data, $params));
Hook::call(
'SubmissionFile::edit',
[
$newSubmissionFile,
$submissionFile,
$params
]
);
$newSubmissionFile->setData('updatedAt', Core::getCurrentDate());
$this->dao->update($newSubmissionFile);
$newFileUploaded = !empty($params['fileId']) && $params['fileId'] !== $submissionFile->getData('fileId');
$logData = $this->getSubmissionFileLogData($submissionFile);
$logEntry = Repo::eventLog()->newDataObject(array_merge(
$logData,
[
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION_FILE,
'assocId' => $submissionFile->getId(),
'eventType' => $newFileUploaded ? SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_REVISION_UPLOAD : SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_EDIT,
'message' => $newFileUploaded ? 'submission.event.revisionUploaded' : 'submission.event.fileEdited',
'isTranslated' => false,
'dateLogged' => Core::getCurrentDate(),
]
));
Repo::eventLog()->add($logEntry);
$submission = Repo::submission()->get($submissionFile->getData('submissionId'));
Repo::eventLog()->newDataObject(array_merge(
$logData,
[
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION,
'assocId' => $submission->getId(),
'eventType' => $newFileUploaded ? SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_REVISION_UPLOAD : SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_EDIT,
'message' => $newFileUploaded ? 'submission.event.revisionUploaded' : 'submission.event.fileEdited',
'isTranslate' => false,
'dateLogged' => Core::getCurrentDate(),
]
));
}
/**
* Copy a submission file to another stage
*
* @return int ID of the new submission file
*/
public function copy(SubmissionFile $submissionFile, int $toFileStage, ?int $reviewRoundId = null): int
{
$newSubmissionFile = clone $submissionFile;
$newSubmissionFile->setData('fileStage', $toFileStage);
$newSubmissionFile->setData('sourceSubmissionFileId', $submissionFile->getId());
$newSubmissionFile->setData('assocType', null);
$newSubmissionFile->setData('assocId', null);
if ($reviewRoundId) {
$newSubmissionFile->setData('assocType', Application::ASSOC_TYPE_REVIEW_ROUND);
$newSubmissionFile->setData('assocId', $reviewRoundId);
}
return Repo::submissionFile()->add($newSubmissionFile);
}
/** @copydoc DAO::delete() */
public function delete(SubmissionFile $submissionFile): void
{
Hook::call('SubmissionFile::delete::before', [$submissionFile]);
// Delete dependent files
$this
->getCollector()
->includeDependentFiles(true)
->filterByFileStages([SubmissionFile::SUBMISSION_FILE_DEPENDENT])
->filterByAssoc(Application::ASSOC_TYPE_SUBMISSION_FILE, [$submissionFile->getId()])
->getMany()
->each(function (SubmissionFile $dependentFile) {
$this->delete($dependentFile);
});
// Delete notes for this submission file
$noteDao = DAORegistry::getDAO('NoteDAO'); /** @var NoteDAO $noteDao */
$noteDao->deleteByAssoc(Application::ASSOC_TYPE_SUBMISSION_FILE, $submissionFile->getId());
// Update tasks
$notificationMgr = new NotificationManager();
switch ($submissionFile->getData('fileStage')) {
case SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION:
$authorUserIds = [];
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$submitterAssignments = $stageAssignmentDao->getBySubmissionAndRoleIds($submissionFile->getData('submissionId'), [Role::ROLE_ID_AUTHOR]);
while ($assignment = $submitterAssignments->next()) {
$authorUserIds[] = $assignment->getUserId();
}
$notificationMgr->updateNotification(
Application::get()->getRequest(),
[
Notification::NOTIFICATION_TYPE_PENDING_INTERNAL_REVISIONS,
Notification::NOTIFICATION_TYPE_PENDING_EXTERNAL_REVISIONS
],
$authorUserIds,
Application::ASSOC_TYPE_SUBMISSION,
$submissionFile->getData('submissionId')
);
break;
case SubmissionFile::SUBMISSION_FILE_COPYEDIT:
$notificationMgr->updateNotification(
Application::get()->getRequest(),
[
Notification::NOTIFICATION_TYPE_ASSIGN_COPYEDITOR,
Notification::NOTIFICATION_TYPE_AWAITING_COPYEDITS
],
null,
Application::ASSOC_TYPE_SUBMISSION,
$submissionFile->getData('submissionId')
);
break;
}
// Get all revision file ids before they are deleted
$revisions = $this->getRevisions($submissionFile->getId());
// Get the review round before review round files are deleted
if ($submissionFile->getData('fileStage') === SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION) {
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getBySubmissionFileId($submissionFile->getId());
}
$this->dao->delete($submissionFile);
// Delete all files that are not referenced by other submission files
foreach ($revisions as $revision) {
$countFileShares = $this
->getCollector()
->filterByFileIds([$revision->fileId])
->includeDependentFiles(true)
->getCount();
if (!$countFileShares) {
Services::get('file')->delete($revision->fileId);
}
}
// Update the review round status after deletion
if ($submissionFile->getData('fileStage') === SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION) {
$reviewRoundDao->updateStatus($reviewRound);
}
// Log the deletion
$logEntry = Repo::eventLog()->newDataObject(array_merge(
$this->getSubmissionFileLogData($submissionFile),
[
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION_FILE,
'assocId' => $submissionFile->getId(),
'eventType' => SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_DELETE,
'message' => 'submission.event.fileDeleted',
'isTranslated' => false,
'dateLogged' => Core::getCurrentDate(),
]
));
Repo::eventLog()->add($logEntry);
Hook::call('SubmissionFile::delete', [$submissionFile]);
}
/**
* Get the file stage ids that a user can access based on their
* stage assignments
*
* This does not return file stages for ROLE_ID_REVIEWER or ROLE_ID_READER.
* These roles are not granted stage assignments and this method should not
* be used for these roles.
*
* This method does not define access to review attachments, discussion
* files or dependent files. Access to these files are not determined by
* stage assignment.
*
* In some cases it may be necessary to apply additional restrictions. For example,
* authors are granted write access to submission files or revisions only when other
* conditions are met. This method only considers these an assigned file stage for
* authors when read access is requested.
*
* $stageAssignments it's an array holding the stage assignments of this user.
* Each key is a workflow stage and value is an array of assigned roles
* $action it's an integer holding a flag to read or write to file stages. One of SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_
*
* @return array List of file stages (SubmissionFile::SUBMISSION_FILE_*)
*/
public function getAssignedFileStages(
array $stageAssignments,
int $action
): array {
$allowedRoles = [
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_SITE_ADMIN,
Role::ROLE_ID_SUB_EDITOR,
Role::ROLE_ID_ASSISTANT,
Role::ROLE_ID_AUTHOR
];
$notAuthorRoles = array_diff($allowedRoles, [Role::ROLE_ID_AUTHOR]);
$allowedFileStages = [];
if (
array_key_exists(WORKFLOW_STAGE_ID_SUBMISSION, $stageAssignments) &&
!empty(array_intersect($allowedRoles, $stageAssignments[WORKFLOW_STAGE_ID_SUBMISSION]))
) {
$hasEditorialAssignment = !empty(array_intersect($notAuthorRoles, $stageAssignments[WORKFLOW_STAGE_ID_SUBMISSION]));
// Authors only have read access
if ($action === SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_READ || $hasEditorialAssignment) {
$allowedFileStages[] = SubmissionFile::SUBMISSION_FILE_SUBMISSION;
}
}
if (array_key_exists(WORKFLOW_STAGE_ID_INTERNAL_REVIEW, $stageAssignments)) {
$hasEditorialAssignment = !empty(array_intersect($notAuthorRoles, $stageAssignments[WORKFLOW_STAGE_ID_INTERNAL_REVIEW]));
// Authors can only write revision files under specific conditions
if ($action === SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_READ || $hasEditorialAssignment) {
$allowedFileStages[] = SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION;
}
// Authors can never access review files
if ($hasEditorialAssignment) {
$allowedFileStages[] = SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE;
}
}
if (array_key_exists(WORKFLOW_STAGE_ID_EXTERNAL_REVIEW, $stageAssignments)) {
$hasEditorialAssignment = !empty(array_intersect($notAuthorRoles, $stageAssignments[WORKFLOW_STAGE_ID_EXTERNAL_REVIEW]));
// Authors can only write revision files under specific conditions
if ($action === SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_READ || $hasEditorialAssignment) {
$allowedFileStages[] = SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION;
$allowedFileStages[] = SubmissionFile::SUBMISSION_FILE_ATTACHMENT;
}
// Authors can never access review files
if ($hasEditorialAssignment) {
$allowedFileStages[] = SubmissionFile::SUBMISSION_FILE_REVIEW_FILE;
}
}
if (
array_key_exists(WORKFLOW_STAGE_ID_EDITING, $stageAssignments) &&
!empty(array_intersect($allowedRoles, $stageAssignments[WORKFLOW_STAGE_ID_EDITING]))
) {
$hasEditorialAssignment = !empty(array_intersect($notAuthorRoles, $stageAssignments[WORKFLOW_STAGE_ID_EDITING]));
// Authors only have read access
if ($action === SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_READ || $hasEditorialAssignment) {
$allowedFileStages[] = SubmissionFile::SUBMISSION_FILE_COPYEDIT;
}
if ($hasEditorialAssignment) {
$allowedFileStages[] = SubmissionFile::SUBMISSION_FILE_FINAL;
}
}
if (array_key_exists(WORKFLOW_STAGE_ID_PRODUCTION, $stageAssignments) &&
!empty(array_intersect($allowedRoles, $stageAssignments[WORKFLOW_STAGE_ID_PRODUCTION]))
) {
$hasEditorialAssignment = !empty(array_intersect($notAuthorRoles, $stageAssignments[WORKFLOW_STAGE_ID_PRODUCTION]));
// Authors only have read access
if ($action === SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_READ || $hasEditorialAssignment) {
$allowedFileStages[] = SubmissionFile::SUBMISSION_FILE_PROOF;
}
if ($hasEditorialAssignment) {
$allowedFileStages[] = SubmissionFile::SUBMISSION_FILE_PRODUCTION_READY;
}
}
return $allowedFileStages;
}
/**
* Get all valid file stages
*
* Valid file stages should be passed through
* the hook SubmissionFile::fileStages.
*/
abstract public function getFileStages(): array;
/**
* Get the path to a submission's file directory
*
* This returns the relative path from the files_dir set in the config.
*/
public function getSubmissionDir(
int $contextId,
int $submissionId
): string {
$dirNames = Application::getFileDirectories();
return sprintf(
'%s/%d/%s/%d',
str_replace('/', '', $dirNames['context']),
$contextId,
str_replace('/', '', $dirNames['submission']),
$submissionId
);
}
/**
* Get the workflow stage for a submission file
*/
public function getWorkflowStageId(SubmissionFile $submissionFile): ?int
{
$fileStage = $submissionFile->getData('fileStage');
if ($fileStage === SubmissionFile::SUBMISSION_FILE_SUBMISSION) {
return WORKFLOW_STAGE_ID_SUBMISSION;
}
if (
$fileStage === SubmissionFile::SUBMISSION_FILE_FINAL ||
$fileStage === SubmissionFile::SUBMISSION_FILE_COPYEDIT
) {
return WORKFLOW_STAGE_ID_EDITING;
}
if (
$fileStage === SubmissionFile::SUBMISSION_FILE_PROOF ||
$fileStage === SubmissionFile::SUBMISSION_FILE_PRODUCTION_READY
) {
return WORKFLOW_STAGE_ID_PRODUCTION;
}
if (
$fileStage === SubmissionFile::SUBMISSION_FILE_DEPENDENT
) {
$parentFile = $this->get($submissionFile->getData('assocId'));
return $parentFile ? $this->getWorkflowStageId($parentFile) : null;
}
if (
$fileStage === SubmissionFile::SUBMISSION_FILE_REVIEW_FILE ||
$fileStage === SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE ||
$fileStage === SubmissionFile::SUBMISSION_FILE_REVIEW_ATTACHMENT ||
$fileStage === SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION ||
$fileStage === SubmissionFile::SUBMISSION_FILE_ATTACHMENT ||
$fileStage === SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION
) {
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getBySubmissionFileId($submissionFile->getId());
return $reviewRound?->getStageId();
}
if ($fileStage === SubmissionFile::SUBMISSION_FILE_QUERY) {
// This file should be associated with a note. If not, fail.
if ($submissionFile->getData('assocType') != PKPApplication::ASSOC_TYPE_NOTE) {
return null;
}
// Get the associated note.
$noteDao = DAORegistry::getDAO('NoteDAO'); /** @var NoteDAO $noteDao */
$note = $noteDao->getById($submissionFile->getData('assocId'));
// The note should be associated with a query. If not, fail.
if ($note?->getAssocType() != PKPApplication::ASSOC_TYPE_QUERY) {
return null;
}
// Get the associated query.
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
$query = $queryDao->getById($note->getAssocId());
// The query will have an associated file stage.
return $query ? $query->getStageId() : null;
}
throw new Exception('Could not determine the workflow stage id from submission file ' . $submissionFile->getId() . ' with file stage ' . $submissionFile->getData('fileStage'));
}
/**
* Check if a submission file supports dependent files
*/
public function supportsDependentFiles(SubmissionFile $submissionFile): bool
{
$fileStage = $submissionFile->getData('fileStage');
$excludedFileStages = [
SubmissionFile::SUBMISSION_FILE_DEPENDENT,
SubmissionFile::SUBMISSION_FILE_QUERY,
];
$allowedMimetypes = [
'text/html',
'application/xml',
'text/xml',
];
$result = !in_array($fileStage, $excludedFileStages) && in_array($submissionFile->getData('mimetype'), $allowedMimetypes);
Hook::call('SubmissionFile::supportsDependentFiles', [&$result, $submissionFile]);
return $result;
}
/**
* Get the files for each revision of a submission file
*/
public function getRevisions(int $submissionFileId): Collection
{
return DB::table('submission_file_revisions as sfr')
->leftJoin('files as f', 'f.file_id', '=', 'sfr.file_id')
->where('submission_file_id', '=', $submissionFileId)
->orderBy('revision_id', 'desc')
->select(['f.file_id as fileId', 'f.path', 'f.mimetype', 'sfr.revision_id'])
->get();
}
/**
* Sends email to notify editors about new revision of a submission file
*/
protected function notifyEditorsRevisionsUploaded(SubmissionFile $submissionFile): void
{
$submission = Repo::submission()->get($submissionFile->getData('submissionId'));
$context = Services::get('context')->get($submission->getData('contextId'));
$uploader = Repo::user()->get($submissionFile->getData('uploaderUserId'));
$user = $this->request->getUser();
// Fetch the latest notification email timestamp
$submissionEmailLogDao = DAORegistry::getDAO('SubmissionEmailLogDAO');
/** @var SubmissionEmailLogDAO $submissionEmailLogDao */
$submissionEmails = $submissionEmailLogDao->getByEventType(
$submission->getId(),
SubmissionEmailLogEntry::SUBMISSION_EMAIL_AUTHOR_NOTIFY_REVISED_VERSION
);
$lastNotification = null;
$sentDates = [];
if ($submissionEmails) {
while ($email = $submissionEmails->next()) {
if ($email->getDateSent()) {
$sentDates[] = $email->getDateSent();
}
}
if (!empty($sentDates)) {
$lastNotification = max(array_map('strtotime', $sentDates));
}
}
// Get editors assigned to the submission, consider also the recommendOnly editors
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao*/
$reviewRound = $reviewRoundDao->getById($submissionFile->getData('assocId'));
$editorsStageAssignments = $stageAssignmentDao->getEditorsAssignedToStage(
$submission->getId(),
$reviewRound->getStageId()
);
$recipients = [];
foreach ($editorsStageAssignments as $editorsStageAssignment) {
$editor = Repo::user()->get($editorsStageAssignment->getUserId());
// IF no prior notification exists
// OR if editor has logged in after the last revision upload
// OR the last upload and notification was sent more than a day ago,
// THEN send a new notification
if (is_null($lastNotification) || strtotime($editor->getDateLastLogin()) > $lastNotification || strtotime('-1 day') > $lastNotification) {
$recipients[] = $editor;
}
}
if (empty($recipients)) {
return;
}
$mailable = new RevisedVersionNotify($context, $submission, $uploader, $reviewRound);
$template = Repo::emailTemplate()->getByKey($context->getId(), RevisedVersionNotify::getEmailTemplateKey());
$mailable->body($template->getLocalizedData('body'))
->subject($template->getLocalizedData('subject'))
->sender($user)
->recipients($recipients)
->replyTo($context->getData('contactEmail'), $context->getData('contactName'));
Mail::send($mailable);
$submissionEmailLogDao = DAORegistry::getDAO('SubmissionEmailLogDAO'); /** @var SubmissionEmailLogDAO $submissionEmailLogDao */
$submissionEmailLogDao->logMailable(
SubmissionEmailLogEntry::SUBMISSION_EMAIL_AUTHOR_NOTIFY_REVISED_VERSION,
$mailable,
$submission,
$user
);
}
/**
* Derive data from the submission file to record in the event log
*/
protected function getSubmissionFileLogData(SubmissionFile $submissionFile): array
{
$user = $this->request->getUser();
return [
'userId' => Validation::loggedInAs() ?: $user?->getId(),
'fileStage' => $submissionFile->getData('fileStage'),
'submissionFileId' => $submissionFile->getId(),
'sourceSubmissionFileId' => $submissionFile->getData('sourceSubmissionFileId'),
'fileId' => $submissionFile->getData('fileId'),
'submissionId' => $submissionFile->getData('submissionId'),
'filename' => $submissionFile->getData('name'),
'username' => $user?->getUsername(),
];
}
}
|