diff --git a/classes/core/PKPApplication.inc.php b/classes/core/PKPApplication.inc.php index afd1518bdbd..91f7fe5827d 100644 --- a/classes/core/PKPApplication.inc.php +++ b/classes/core/PKPApplication.inc.php @@ -48,6 +48,8 @@ define('ASSOC_TYPE_SUBMISSION', 0x0100009); define('ASSOC_TYPE_QUERY', 0x010000a); define('ASSOC_TYPE_QUEUED_PAYMENT', 0x010000b); +define('ASSOC_TYPE_ACCESSIBLE_FILE_STAGES', 0x010000d); + // Constant used in UsageStats for submission files that are not full texts define('ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER', 0x0000213); diff --git a/classes/security/authorization/NoteAccessPolicy.inc.php b/classes/security/authorization/NoteAccessPolicy.inc.php new file mode 100644 index 00000000000..fd900bcd8c7 --- /dev/null +++ b/classes/security/authorization/NoteAccessPolicy.inc.php @@ -0,0 +1,94 @@ +_request = $request; + $this->_noteId = $noteId; + $this->_accessMode = $accessMode; + } + + // + // Implement template methods from AuthorizationPolicy + // + /** + * @see AuthorizationPolicy::effect() + */ + function effect() { + + if (!$this->_noteId) { + return AUTHORIZATION_DENY; + } + + $query = $this->getAuthorizedContextObject(ASSOC_TYPE_QUERY); + $submission = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION); + $assignedStages = $this->getAuthorizedContextObject(ASSOC_TYPE_ACCESSIBLE_WORKFLOW_STAGES); + + if (!$query || !$submission || empty($assignedStages)) { + return AUTHORIZATION_DENY; + } + + $noteDao = DAORegistry::getDAO('NoteDAO'); /* @var $noteDao NoteDAO */ + $note = $noteDao->getById($this->_noteId); + + if (!is_a($note, 'Note')) { + return AUTHORIZATION_DENY; + } + + // Note, query, submission and assigned stages must match + if ($note->getAssocId() != $query->getId() + || $query->getAssocId() != $submission->getId() + || !array_key_exists($query->getStageId(), $assignedStages) + || empty($assignedStages[$query->getStageId()])) { + return AUTHORIZATION_DENY; + } + + // Notes can only be edited by their original creators + if ($this->_accessMode === NOTE_ACCESS_WRITE + && $note->getUserId() != $this->_request->getUser()->getId()) { + return AUTHORIZATION_DENY; + } + + $this->addAuthorizedContextObject(ASSOC_TYPE_NOTE, $note); + + return AUTHORIZATION_PERMIT; + } +} + + diff --git a/classes/security/authorization/ReviewAssignmentFileWritePolicy.inc.php b/classes/security/authorization/ReviewAssignmentFileWritePolicy.inc.php new file mode 100644 index 00000000000..33abe57031d --- /dev/null +++ b/classes/security/authorization/ReviewAssignmentFileWritePolicy.inc.php @@ -0,0 +1,100 @@ +_request = $request; + $this->_reviewAssignmentId = $reviewAssignmentId; + } + + // + // Implement template methods from AuthorizationPolicy + // + /** + * @see AuthorizationPolicy::effect() + */ + function effect() { + + if (!$this->_reviewAssignmentId) { + return AUTHORIZATION_DENY; + } + + $reviewRound = $this->getAuthorizedContextObject(ASSOC_TYPE_REVIEW_ROUND); + $submission = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION); + $assignedStages = $this->getAuthorizedContextObject(ASSOC_TYPE_ACCESSIBLE_WORKFLOW_STAGES); + $userRoles = $this->getAuthorizedContextObject(ASSOC_TYPE_USER_ROLES); + + if (!$reviewRound || !$submission) { + return AUTHORIZATION_DENY; + } + + $reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /* @var $noteDao ReviewAssignmentDAO */ + $reviewAssignment = $reviewAssignmentDao->getById($this->_reviewAssignmentId); + + if (!is_a($reviewAssignment, 'ReviewAssignment')) { + return AUTHORIZATION_DENY; + } + + // Review assignment, review round and submission must match + if ($reviewAssignment->getReviewRoundId() != $reviewRound->getId() + || $reviewRound->getSubmissionId() != $submission->getId()) { + return AUTHORIZATION_DENY; + } + + // Managers can write review attachments when they are not assigned to a submission + if (empty($stageAssignments) && in_array(ROLE_ID_MANAGER, $userRoles)) { + $this->addAuthorizedContextObject(ASSOC_TYPE_REVIEW_ASSIGNMENT, $reviewAssignment); + return AUTHORIZATION_PERMIT; + } + + // Managers, editors and assistants can write review attachments when they are assigned + // to the correct stage. + if (!empty($assignedStages[$reviewRound->getStageId()])) { + $allowedRoles = [ROLE_ID_MANAGER, ROLE_ID_SUB_EDITOR, ROLE_ID_ASSISTANT]; + if (!empty(array_intersect($allowedRoles, $assignedStages[$reviewRound->getStageId()]))) { + $this->addAuthorizedContextObject(ASSOC_TYPE_REVIEW_ASSIGNMENT, $reviewAssignment); + return AUTHORIZATION_PERMIT; + } + } + + // Reviewers can write review attachments to their own review assigments, + // if the assignment is not yet complete, cancelled or declined. + if ($reviewAssignment->getReviewerId() == $this->_request->getUser()->getId()) { + $notAllowedStatuses = [REVIEW_ASSIGNMENT_STATUS_DECLINED, REVIEW_ASSIGNMENT_STATUS_COMPLETE, REVIEW_ASSIGNMENT_STATUS_THANKED, REVIEW_ASSIGNMENT_STATUS_CANCELLED]; + if (!in_array($reviewAssignment->getStatus(), $notAllowedStatuses)) { + $this->addAuthorizedContextObject(ASSOC_TYPE_REVIEW_ASSIGNMENT, $reviewAssignment); + return AUTHORIZATION_PERMIT; + } + } + + return AUTHORIZATION_DENY; + } +} diff --git a/classes/security/authorization/SubmissionFileAccessPolicy.inc.php b/classes/security/authorization/SubmissionFileAccessPolicy.inc.php index d9f9e873260..5c17690de68 100644 --- a/classes/security/authorization/SubmissionFileAccessPolicy.inc.php +++ b/classes/security/authorization/SubmissionFileAccessPolicy.inc.php @@ -74,6 +74,10 @@ function buildFileAccessPolicy($request, $args, $roleAssignments, $mode, $fileId // been assigned to a lesser role in the stage. $managerFileAccessPolicy = new PolicySet(COMBINING_DENY_OVERRIDES); $managerFileAccessPolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, ROLE_ID_MANAGER, $roleAssignments[ROLE_ID_MANAGER])); + + $stageId = $request->getUserVar('stageId'); // WORKFLOW_STAGE_ID_... + import('lib.pkp.classes.security.authorization.internal.SubmissionFileMatchesWorkflowStageIdPolicy'); + $managerFileAccessPolicy->addPolicy(new SubmissionFileMatchesWorkflowStageIdPolicy($request, $fileIdAndRevision, $stageId)); import('lib.pkp.classes.security.authorization.WorkflowStageAccessPolicy'); $managerFileAccessPolicy->addPolicy(new WorkflowStageAccessPolicy($request, $args, $roleAssignments, 'submissionId', $request->getUserVar('stageId'))); import('lib.pkp.classes.security.authorization.AssignedStageRoleHandlerOperationPolicy'); @@ -94,6 +98,11 @@ function buildFileAccessPolicy($request, $args, $roleAssignments, $mode, $fileId // 2) ...if they are assigned to the workflow stage as an author. Note: This loads the application-specific policy class. import('lib.pkp.classes.security.authorization.WorkflowStageAccessPolicy'); $authorFileAccessPolicy->addPolicy(new WorkflowStageAccessPolicy($request, $args, $roleAssignments, 'submissionId', $request->getUserVar('stageId'))); + + $stageId = $request->getUserVar('stageId'); // WORKFLOW_STAGE_ID_... + import('lib.pkp.classes.security.authorization.internal.SubmissionFileMatchesWorkflowStageIdPolicy'); + $authorFileAccessPolicy->addPolicy(new SubmissionFileMatchesWorkflowStageIdPolicy($request, $fileIdAndRevision, $stageId)); + import('lib.pkp.classes.security.authorization.AssignedStageRoleHandlerOperationPolicy'); $authorFileAccessPolicy->addPolicy(new AssignedStageRoleHandlerOperationPolicy($request, ROLE_ID_AUTHOR, $roleAssignments[ROLE_ID_AUTHOR], $request->getUserVar('stageId'))); @@ -216,6 +225,8 @@ function buildFileAccessPolicy($request, $args, $roleAssignments, $mode, $fileId $subEditorFileAccessPolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, ROLE_ID_SUB_EDITOR, $roleAssignments[ROLE_ID_SUB_EDITOR])); // 2) ... but only if they have been assigned as a subeditor to the requested submission ... + import('lib.pkp.classes.security.authorization.WorkflowStageAccessPolicy'); + $subEditorFileAccessPolicy->addPolicy(new WorkflowStageAccessPolicy($request, $args, $roleAssignments, 'submissionId', $request->getUserVar('stageId'))); import('lib.pkp.classes.security.authorization.internal.UserAccessibleWorkflowStageRequiredPolicy'); $subEditorFileAccessPolicy->addPolicy(new UserAccessibleWorkflowStageRequiredPolicy($request)); import('lib.pkp.classes.security.authorization.AssignedStageRoleHandlerOperationPolicy'); diff --git a/classes/security/authorization/internal/QueryRequiredPolicy.inc.php b/classes/security/authorization/internal/QueryRequiredPolicy.inc.php index f04819b904e..7b513fa4493 100644 --- a/classes/security/authorization/internal/QueryRequiredPolicy.inc.php +++ b/classes/security/authorization/internal/QueryRequiredPolicy.inc.php @@ -23,7 +23,7 @@ class QueryRequiredPolicy extends DataObjectRequiredPolicy { * the submission id in. */ function __construct($request, &$args, $parameterName = 'queryId', $operations = null) { - parent::__construct($request, $args, $parameterName, 'user.authorization.invalidQuery', $operations); + parent::__construct($request, $args, $parameterName, 'user.authorization.accessDenied', $operations); } // diff --git a/classes/security/authorization/internal/RepresentationUploadAccessPolicy.inc.php b/classes/security/authorization/internal/RepresentationUploadAccessPolicy.inc.php new file mode 100644 index 00000000000..4fabcdd1c1b --- /dev/null +++ b/classes/security/authorization/internal/RepresentationUploadAccessPolicy.inc.php @@ -0,0 +1,76 @@ +_representationId = $representationId; + } + + // + // Implement template methods from AuthorizationPolicy + // + /** + * @see DataObjectRequiredPolicy::dataObjectEffect() + */ + function dataObjectEffect() { + AppLocale::requireComponents([LOCALE_COMPONENT_PKP_SUBMISSION, LOCALE_COMPONENT_APP_SUBMISSION]); + + $assignedFileStageIds = $this->getAuthorizedContextObject(ASSOC_TYPE_ACCESSIBLE_FILE_STAGES); + if (empty($assignedFileStageIds) || !in_array(SUBMISSION_FILE_PROOF, $assignedFileStageIds)) { + return AUTHORIZATION_DENY; + } + + if (empty($this->_representationId)) { + $this->setAdvice(AUTHORIZATION_ADVICE_DENY_MESSAGE, $msg = 'user.authorization.representationNotFound'); + return AUTHORIZATION_DENY; + } + + $representationDao = Application::get()->getRepresentationDAO(); + $representation = $representationDao->getById($this->_representationId); + + if (!$representation) { + $this->setAdvice(AUTHORIZATION_ADVICE_DENY_MESSAGE, $msg = 'user.authorization.representationNotFound'); + return AUTHORIZATION_DENY; + } + + $submission = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION); + if (!$submission || $representation->getSubmissionId() !== $submission->getId()) { + $this->setAdvice(AUTHORIZATION_ADVICE_DENY_MESSAGE, $msg = 'user.authorization.invalidSubmission'); + return AUTHORIZATION_DENY; + } + + $this->addAuthorizedContextObject(ASSOC_TYPE_REPRESENTATION, $representation); + + return AUTHORIZATION_PERMIT; + } +} + + diff --git a/classes/security/authorization/internal/ReviewRoundRequiredPolicy.inc.php b/classes/security/authorization/internal/ReviewRoundRequiredPolicy.inc.php index 421126c7768..a853c74c7c4 100644 --- a/classes/security/authorization/internal/ReviewRoundRequiredPolicy.inc.php +++ b/classes/security/authorization/internal/ReviewRoundRequiredPolicy.inc.php @@ -24,7 +24,7 @@ class ReviewRoundRequiredPolicy extends DataObjectRequiredPolicy { * @param $operations array Optional list of operations for which this check takes effect. If specified, operations outside this set will not be checked against this policy. */ function __construct($request, &$args, $parameterName = 'reviewRoundId', $operations = null) { - parent::__construct($request, $args, $parameterName, 'user.authorization.invalidReviewRound', $operations); + parent::__construct($request, $args, $parameterName, 'user.authorization.accessDenied', $operations); } // diff --git a/classes/security/authorization/internal/SubmissionFileMatchesWorkflowStageIdPolicy.inc.php b/classes/security/authorization/internal/SubmissionFileMatchesWorkflowStageIdPolicy.inc.php new file mode 100644 index 00000000000..a78407d6e55 --- /dev/null +++ b/classes/security/authorization/internal/SubmissionFileMatchesWorkflowStageIdPolicy.inc.php @@ -0,0 +1,54 @@ +_stageId = (int) $stageId; + } + + + // + // Implement template methods from AuthorizationPolicy + // + /** + * @see AuthorizationPolicy::effect() + */ + function effect() { + // Get the submission file + $request = $this->getRequest(); + $submissionFile = $this->getSubmissionFile($request); + if (!is_a($submissionFile, 'SubmissionFile')) return AUTHORIZATION_DENY; + + + $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ + $workflowStageId = $submissionFileDao->getWorkflowStageId($submissionFile); + + // Check if the submission file belongs to the specified workflow stage. + if ($workflowStageId != $this->_stageId) return AUTHORIZATION_DENY; + + return AUTHORIZATION_PERMIT; + } +} + diff --git a/classes/security/authorization/internal/SubmissionFileStageAccessPolicy.inc.php b/classes/security/authorization/internal/SubmissionFileStageAccessPolicy.inc.php new file mode 100644 index 00000000000..58f9ac4ad91 --- /dev/null +++ b/classes/security/authorization/internal/SubmissionFileStageAccessPolicy.inc.php @@ -0,0 +1,113 @@ +_fileStage = $fileStage; + $this->_action = $action; + } + + + // + // Implement template methods from AuthorizationPolicy + // + /** + * @see AuthorizationPolicy::effect() + */ + function effect() { + $submission = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION); + $userRoles = $this->getAuthorizedContextObject(ASSOC_TYPE_USER_ROLES); + $stageAssignments = $this->getAuthorizedContextObject(ASSOC_TYPE_ACCESSIBLE_WORKFLOW_STAGES); + + // File stage required + if (empty($this->_fileStage)) { + return AUTHORIZATION_DENY; + } + + // Managers can access file stages when not assigned or when assigned as a manager + if (empty($stageAssignments)) { + if (in_array(ROLE_ID_MANAGER, $userRoles)) { + return AUTHORIZATION_PERMIT; + } + return AUTHORIZATION_DENY; + } + + // Determine the allowed file stages + $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ + $assignedFileStageIds = $submissionFileDao->getAssignedFileStageIds($stageAssignments, $this->_action); + + // Authors may write to the submission files stage if the submission + // is not yet complete + if ($this->_fileStage === SUBMISSION_FILE_SUBMISSION && $this->_action === SUBMISSION_FILE_ACCESS_MODIFY) { + if (!empty($stageAssignments[WORKFLOW_STAGE_ID_SUBMISSION]) + && count($stageAssignments[WORKFLOW_STAGE_ID_SUBMISSION]) === 1 + && in_array(ROLE_ID_AUTHOR, $stageAssignments[WORKFLOW_STAGE_ID_SUBMISSION]) + && $submission->getData('submissionProgress') > 0) { + $assignedFileStageIds[] = SUBMISSION_FILE_SUBMISSION; + } + } + + // Authors may write to the revision files stage if an accept or request revisions + // decision has been made in the latest round + if ($this->_fileStage === SUBMISSION_FILE_REVIEW_REVISION && $this->_action === SUBMISSION_FILE_ACCESS_MODIFY) { + $assignedReviewRoles = array_merge( + $stageAssignments[WORKFLOW_STAGE_ID_INTERNAL_REVIEW] ?? [], + $stageAssignments[WORKFLOW_STAGE_ID_EXTERNAL_REVIEW] ?? [] + ); + $assignedReviewRoles = array_unique(array_values($assignedReviewRoles)); + if (count($assignedReviewRoles) === 1 && in_array(ROLE_ID_AUTHOR, $assignedReviewRoles)) { + $reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /* @var $reviewRoundDao ReviewRoundDAO */ + $reviewRound = $reviewRoundDao->getLastReviewRoundBySubmissionId($submission->getId()); + if ($reviewRound) { + $editDecisionDao = DAORegistry::getDAO('EditDecisionDAO'); /* @var $editDecisionDao EditDecisionDAO */ + $decisions = $editDecisionDao->getEditorDecisions($submission->getId(), $reviewRound->getStageId(), $reviewRound->getRound()); + if (!empty($decisions)) { + foreach ($decisions as $decision) { + if ($decision['decision'] == SUBMISSION_EDITOR_DECISION_ACCEPT + || $decision['decision'] == SUBMISSION_EDITOR_DECISION_PENDING_REVISIONS + || $decision['decision'] == SUBMISSION_EDITOR_DECISION_NEW_ROUND) { + $assignedFileStageIds[] = SUBMISSION_FILE_REVIEW_REVISION; + break; + } + } + } + } + } + } + + if (in_array($this->_fileStage, $assignedFileStageIds)) { + $this->addAuthorizedContextObject(ASSOC_TYPE_ACCESSIBLE_FILE_STAGES, $assignedFileStageIds); + return AUTHORIZATION_PERMIT; + } + + return AUTHORIZATION_DENY; + } +} diff --git a/classes/submission/SubmissionFileDAO.inc.php b/classes/submission/SubmissionFileDAO.inc.php index c6fa4a2df56..0b422bafecb 100644 --- a/classes/submission/SubmissionFileDAO.inc.php +++ b/classes/submission/SubmissionFileDAO.inc.php @@ -764,8 +764,10 @@ public function getWorkflowStageId($submissionFile) { return WORKFLOW_STAGE_ID_EDITING; case SUBMISSION_FILE_PROOF: case SUBMISSION_FILE_PRODUCTION_READY: - case SUBMISSION_FILE_DEPENDENT: return WORKFLOW_STAGE_ID_PRODUCTION; + case SUBMISSION_FILE_DEPENDENT: + $parentFile = $this->getLatestRevision($submissionFile->getData('assocId')); + return $this->getWorkflowStageId($parentFile); case SUBMISSION_FILE_QUERY: $noteDao = DAORegistry::getDAO('NoteDAO'); $note = $noteDao->getById($submissionFile->getAssocId()); @@ -775,6 +777,89 @@ public function getWorkflowStageId($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 or discussion + * 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 does not return those as allowed file + * stages for author assignments. + * + * @param array $stageAssignments The stage assignments of this user. + * Each key is a workflow stage and value is an array of assigned roles + * @param int $action Read or write to file stages. One of SUBMISSION_FILE_ACCESS_ + * @return array List of file stages (SUBMISSION_FILE_*) + */ + public function getAssignedFileStageIds($stageAssignments, $action) { + $allowedRoles = [ROLE_ID_MANAGER, ROLE_ID_SUB_EDITOR, ROLE_ID_ASSISTANT, ROLE_ID_AUTHOR]; + $notAuthorRoles = array_diff($allowedRoles, [ROLE_ID_AUTHOR]); + + $allowedFileStageIds = []; + + 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 === SUBMISSION_FILE_ACCESS_READ || $hasEditorialAssignment) { + $allowedFileStageIds[] = SUBMISSION_FILE_SUBMISSION; + } + } + + 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 === SUBMISSION_FILE_ACCESS_READ || $hasEditorialAssignment) { + $allowedFileStageIds[] = SUBMISSION_FILE_COPYEDIT; + } + if ($hasEditorialAssignment) { + $allowedFileStageIds[] = 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 === SUBMISSION_FILE_ACCESS_READ || $hasEditorialAssignment) { + $allowedFileStageIds[] = SUBMISSION_FILE_PROOF; + } + if ($hasEditorialAssignment) { + $allowedFileStageIds[] = SUBMISSION_FILE_PRODUCTION_READY; + $allowedFileStageIds[] = SUBMISSION_FILE_DEPENDENT; + } + } + + if (array_key_exists(WORKFLOW_STAGE_ID_INTERNAL_REVIEW, $stageAssignments) + || array_key_exists(WORKFLOW_STAGE_ID_EXTERNAL_REVIEW, $stageAssignments) ) { + $assignedUserRoles = array_merge( + $stageAssignments[WORKFLOW_STAGE_ID_INTERNAL_REVIEW] ?? [], + $stageAssignments[WORKFLOW_STAGE_ID_EXTERNAL_REVIEW] ?? [] + ); + $hasEditorialAssignment = !empty(array_intersect($notAuthorRoles, $assignedUserRoles)); + // Authors can only write revision files under specific conditions + if ($action === SUBMISSION_FILE_ACCESS_READ || $hasEditorialAssignment) { + $allowedFileStageIds[] = SUBMISSION_FILE_REVIEW_REVISION; + } + // Authors can never access review files + if ($hasEditorialAssignment) { + $allowedFileStageIds[] = SUBMISSION_FILE_REVIEW_FILE; + } + } + + HookRegistry::call('SubmissionFile::assignedFileStageIds', [&$allowedFileStageIds, $stageAssignments, $action]); + + return $allowedFileStageIds; + } + // // Private helper methods // diff --git a/controllers/api/file/linkAction/AddFileLinkAction.inc.php b/controllers/api/file/linkAction/AddFileLinkAction.inc.php index e0c32638c6a..0976510901f 100644 --- a/controllers/api/file/linkAction/AddFileLinkAction.inc.php +++ b/controllers/api/file/linkAction/AddFileLinkAction.inc.php @@ -37,9 +37,11 @@ class AddFileLinkAction extends BaseAddFileLinkAction { * @param $revisedFileId int Revised file ID, if any * @param $dependentFilesOnly bool whether to only include dependent * files in the Genres dropdown. + * @param $queryId int The query id. Use when the assoc details point + * to a note */ function __construct($request, $submissionId, $stageId, $uploaderRoles, - $fileStage, $assocType = null, $assocId = null, $reviewRoundId = null, $revisedFileId = null, $dependentFilesOnly = false) { + $fileStage, $assocType = null, $assocId = null, $reviewRoundId = null, $revisedFileId = null, $dependentFilesOnly = false, $queryId = null) { // Create the action arguments array. $actionArgs = array('fileStage' => $fileStage, 'reviewRoundId' => $reviewRoundId); @@ -53,6 +55,10 @@ function __construct($request, $submissionId, $stageId, $uploaderRoles, } if ($dependentFilesOnly) $actionArgs['dependentFilesOnly'] = true; + if ($queryId) { + $actionArgs['queryId'] = $queryId; + } + // Identify text labels based on the file stage. $textLabels = AddFileLinkAction::_getTextLabels($fileStage); diff --git a/controllers/grid/files/query/QueryNoteFilesGridDataProvider.inc.php b/controllers/grid/files/query/QueryNoteFilesGridDataProvider.inc.php index 71cbe111612..7829cf1fd78 100644 --- a/controllers/grid/files/query/QueryNoteFilesGridDataProvider.inc.php +++ b/controllers/grid/files/query/QueryNoteFilesGridDataProvider.inc.php @@ -102,7 +102,11 @@ function getAddFileAction($request) { return new AddFileLinkAction( $request, $submission->getId(), $this->getStageId(), $this->getUploaderRoles(), $this->getFileStage(), - ASSOC_TYPE_NOTE, $this->_noteId + ASSOC_TYPE_NOTE, $this->_noteId, + null, + null, + null, + $query->getId() ); } } diff --git a/controllers/wizard/fileUpload/PKPFileUploadWizardHandler.inc.php b/controllers/wizard/fileUpload/PKPFileUploadWizardHandler.inc.php index 0fe22f8328a..2bc5616627b 100644 --- a/controllers/wizard/fileUpload/PKPFileUploadWizardHandler.inc.php +++ b/controllers/wizard/fileUpload/PKPFileUploadWizardHandler.inc.php @@ -42,6 +42,9 @@ class PKPFileUploadWizardHandler extends Handler { /** @var integer */ var $_assocId; + /** @var integer */ + var $_queryId; + /** * Constructor @@ -63,6 +66,116 @@ function __construct() { // // Implement template methods from PKPHandler // + function authorize($request, &$args, $roleAssignments) { + // We validate file stage outside a policy because + // we don't need to validate in another places. + $fileStage = (int) $request->getUserVar('fileStage'); + if ($fileStage) { + $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ + $fileStages = $submissionFileDao->getAllFileStages(); + if (!in_array($fileStage, $fileStages)) { + return false; + } + } + + // Validate file ids. We have two cases where we might have a file id. + // CASE 1: user is uploading a revision to a file, the revised file id + // will need validation. + $revisedFileId = (int)$request->getUserVar('revisedFileId'); + // CASE 2: user already have uploaded a file (and it's editing the metadata), + // we will need to validate the uploaded file id. + $fileId = (int)$request->getUserVar('fileId'); + // Get the right one to validate. + $fileIdToValidate = null; + if ($revisedFileId && !$fileId) { + $fileIdToValidate = $revisedFileId; + } else if ($fileId && !$revisedFileId) { + $fileIdToValidate = $fileId; + } else if ($revisedFileId && $fileId) { + // Those two cases will not happen at the same time. + return false; + } + + // Allow access to modify a specific file + if ($fileIdToValidate) { + import('lib.pkp.classes.security.authorization.SubmissionFileAccessPolicy'); + $this->addPolicy(new SubmissionFileAccessPolicy($request, $args, $roleAssignments, SUBMISSION_FILE_ACCESS_MODIFY, $fileIdToValidate)); + + // Allow uploading to review attachments + } elseif ($fileStage === SUBMISSION_FILE_REVIEW_ATTACHMENT) { + $assocType = (int) $request->getUserVar('assocType'); + $assocId = (int) $request->getUserVar('assocId'); + $stageId = (int) $request->getUserVar('stageId'); + if (empty($assocType) || $assocType !== ASSOC_TYPE_REVIEW_ASSIGNMENT || empty($assocId)) { + return false; + } + + $stageId = (int) $request->getUserVar('stageId'); + import('lib.pkp.classes.security.authorization.ReviewStageAccessPolicy'); + $this->addPolicy(new ReviewStageAccessPolicy($request, $args, $roleAssignments, 'submissionId', $stageId)); + import('lib.pkp.classes.security.authorization.internal.ReviewRoundRequiredPolicy'); + $this->addPolicy(new ReviewRoundRequiredPolicy($request, $args)); + import('lib.pkp.classes.security.authorization.ReviewAssignmentFileWritePolicy'); + $this->addPolicy(new ReviewAssignmentFileWritePolicy($request, $assocId)); + + // Allow uploading to a note + } elseif ($fileStage === SUBMISSION_FILE_QUERY) { + $assocType = (int) $request->getUserVar('assocType'); + $assocId = (int) $request->getUserVar('assocId'); + $stageId = (int) $request->getUserVar('stageId'); + if (empty($assocType) || $assocType !== ASSOC_TYPE_NOTE || empty($assocId)) { + return false; + } + + import('lib.pkp.classes.security.authorization.QueryAccessPolicy'); + $this->addPolicy(new QueryAccessPolicy($request, $args, $roleAssignments, $stageId)); + import('lib.pkp.classes.security.authorization.NoteAccessPolicy'); + $this->addPolicy(new NoteAccessPolicy($request, $assocId, NOTE_ACCESS_WRITE)); + + // Allow uploading a dependent file to another file + } elseif ($fileStage === SUBMISSION_FILE_DEPENDENT) { + $assocType = (int) $request->getUserVar('assocType'); + $assocId = (int) $request->getUserVar('assocId'); + if (empty($assocType) || $assocType !== ASSOC_TYPE_SUBMISSION_FILE || empty($assocId)) { + return false; + } + + import('lib.pkp.classes.security.authorization.SubmissionFileAccessPolicy'); + $this->addPolicy(new SubmissionFileAccessPolicy($request, $args, $roleAssignments, SUBMISSION_FILE_ACCESS_MODIFY, $assocId)); + + // Allow uploading to other file stages in the workflow + } else { + $stageId = (int) $request->getUserVar('stageId'); + $assocType = (int) $request->getUserVar('assocType'); + $assocId = (int) $request->getUserVar('assocId'); + import('lib.pkp.classes.security.authorization.WorkflowStageAccessPolicy'); + $this->addPolicy(new WorkflowStageAccessPolicy($request, $args, $roleAssignments, 'submissionId', $stageId)); + + AppLocale::requireComponents(LOCALE_COMPONENT_PKP_API, LOCALE_COMPONENT_APP_API); + import('lib.pkp.classes.security.authorization.SubmissionFileAccessPolicy'); // SUBMISSION_FILE_ACCESS_MODIFY + import('lib.pkp.classes.security.authorization.internal.SubmissionFileStageAccessPolicy'); + $this->addPolicy(new SubmissionFileStageAccessPolicy($fileStage, SUBMISSION_FILE_ACCESS_MODIFY, 'user.authorization.accessDenied')); + + // Additional checks before uploading to a review file stage + if (in_array($fileStage, [SUBMISSION_FILE_REVIEW_REVISION, SUBMISSION_FILE_REVIEW_FILE]) + || $assocType === ASSOC_TYPE_REVIEW_ROUND) { + import('lib.pkp.classes.security.authorization.internal.ReviewRoundRequiredPolicy'); + $this->addPolicy(new ReviewRoundRequiredPolicy($request, $args)); + } + + // Additional checks before uploading to a galley + if ($fileStage === SUBMISSION_FILE_PROOF || $assocType === ASSOC_TYPE_REPRESENTATION) { + if (empty($assocType) || $assocType !== ASSOC_TYPE_REPRESENTATION || empty($assocId)) { + return false; + } + import('lib.pkp.classes.security.authorization.internal.RepresentationUploadAccessPolicy'); + $this->addPolicy(new RepresentationUploadAccessPolicy($request, $args, $assocId)); + } + } + + return parent::authorize($request, $args, $roleAssignments); + } + /** * @copydoc PKPHandler::initialize() */ @@ -85,9 +198,9 @@ function initialize($request) { // Do we allow revisions only? $this->_revisionOnly = (boolean)$request->getUserVar('revisionOnly'); - $reviewRound = $this->getReviewRound(); $this->_assocType = $request->getUserVar('assocType') ? (int)$request->getUserVar('assocType') : null; $this->_assocId = $request->getUserVar('assocId') ? (int)$request->getUserVar('assocId') : null; + $this->_queryId = $request->getUserVar('queryId') ? (int)$request->getUserVar('queryId') : null; // The revised file will be non-null if we revise a single existing file. if ($this->getRevisionOnly() && $request->getUserVar('revisedFileId')) { @@ -206,6 +319,7 @@ function startWizard($args, $request) { 'revisedFileId' => $this->getRevisedFileId(), 'assocType' => $this->getAssocType(), 'assocId' => $this->getAssocId(), + 'queryId' => $this->_queryId, 'dependentFilesOnly' => $request->getUserVar('dependentFilesOnly'), )); return $templateMgr->fetchJson('controllers/wizard/fileUpload/fileUploadWizard.tpl'); @@ -224,7 +338,7 @@ function displayFileUploadForm($args, $request) { $fileForm = new SubmissionFilesUploadForm( $request, $submission->getId(), $this->getStageId(), $this->getUploaderRoles(), $this->getFileStage(), $this->getRevisionOnly(), $this->getReviewRound(), $this->getRevisedFileId(), - $this->getAssocType(), $this->getAssocId() + $this->getAssocType(), $this->getAssocId(), $this->_queryId ); $fileForm->initData(); @@ -244,7 +358,7 @@ function uploadFile($args, $request) { import('lib.pkp.controllers.wizard.fileUpload.form.SubmissionFilesUploadForm'); $uploadForm = new SubmissionFilesUploadForm( $request, $submission->getId(), $this->getStageId(), null, $this->getFileStage(), - $this->getRevisionOnly(), $this->getReviewRound(), null, $this->getAssocType(), $this->getAssocId() + $this->getRevisionOnly(), $this->getReviewRound(), null, $this->getAssocType(), $this->getAssocId(), $this->_queryId ); $uploadForm->readInputData(); @@ -273,7 +387,7 @@ function uploadFile($args, $request) { if ($revisedFileId) { // Instantiate the revision confirmation form. import('lib.pkp.controllers.wizard.fileUpload.form.SubmissionFilesUploadConfirmationForm'); - $confirmationForm = new SubmissionFilesUploadConfirmationForm($request, $submission->getId(), $this->getStageId(), $this->getFileStage(), $reviewRound, $revisedFileId, $this->getAssocType(), $this->getAssocId(), $uploadedFile); + $confirmationForm = new SubmissionFilesUploadConfirmationForm($request, $submission->getId(), $this->getStageId(), $this->getFileStage(), $reviewRound, $revisedFileId, $this->getAssocType(), $this->getAssocId(), $uploadedFile, $this->_queryId); $confirmationForm->initData(); // Render the revision confirmation form. @@ -385,7 +499,12 @@ function confirmRevision($args, $request) { // FIXME?: need assocType and assocId? Not sure if they would be used, so not adding now. $reviewRound = $this->getReviewRound(); $confirmationForm = new SubmissionFilesUploadConfirmationForm( - $request, $submission->getId(), $this->getStageId(), $this->getFileStage(), $reviewRound + $request, $submission->getId(), $this->getStageId(), $this->getFileStage(), $reviewRound, + null, + null, + null, + null, + $this->_queryId ); $confirmationForm->readInputData(); diff --git a/controllers/wizard/fileUpload/form/PKPSubmissionFilesUploadBaseForm.inc.php b/controllers/wizard/fileUpload/form/PKPSubmissionFilesUploadBaseForm.inc.php index 8952d43002b..1b4852e5f9f 100644 --- a/controllers/wizard/fileUpload/form/PKPSubmissionFilesUploadBaseForm.inc.php +++ b/controllers/wizard/fileUpload/form/PKPSubmissionFilesUploadBaseForm.inc.php @@ -38,9 +38,12 @@ class PKPSubmissionFilesUploadBaseForm extends Form { * @param $revisionOnly boolean * @param $reviewRound ReviewRound * @param $revisedFileId integer + * @param $assocType integer + * @param $assocId integer + * @param $queryId integer */ function __construct($request, $template, $submissionId, $stageId, $fileStage, - $revisionOnly = false, $reviewRound = null, $revisedFileId = null, $assocType = null, $assocId = null) { + $revisionOnly = false, $reviewRound = null, $revisedFileId = null, $assocType = null, $assocId = null, $queryId = null) { // Check the incoming parameters. if ( !is_numeric($submissionId) || $submissionId <= 0 || @@ -76,6 +79,7 @@ function __construct($request, $template, $submissionId, $stageId, $fileStage, $this->setData('reviewRoundId', $reviewRound?$reviewRound->getId():null); $this->setData('assocType', $assocType ? (int)$assocType : null); $this->setData('assocId', $assocId ? (int)$assocId : null); + $this->setData('queryId', $queryId ? (int) $queryId : null); // Add validators. $this->addCheck(new FormValidatorPost($this)); diff --git a/controllers/wizard/fileUpload/form/SubmissionFilesUploadConfirmationForm.inc.php b/controllers/wizard/fileUpload/form/SubmissionFilesUploadConfirmationForm.inc.php index 1c5d368a76c..f50a534fd35 100644 --- a/controllers/wizard/fileUpload/form/SubmissionFilesUploadConfirmationForm.inc.php +++ b/controllers/wizard/fileUpload/form/SubmissionFilesUploadConfirmationForm.inc.php @@ -28,14 +28,15 @@ class SubmissionFilesUploadConfirmationForm extends PKPSubmissionFilesUploadBase * @param $assocType int optional * @param $assocId int optional * @param $uploadedFile integer + * @param $queryId integer */ function __construct($request, $submissionId, $stageId, $fileStage, - $reviewRound, $revisedFileId = null, $assocType = null, $assocId = null, $uploadedFile = null) { + $reviewRound, $revisedFileId = null, $assocType = null, $assocId = null, $uploadedFile = null, $queryId = null) { // Initialize class. parent::__construct( $request, 'controllers/wizard/fileUpload/form/fileUploadConfirmationForm.tpl', - $submissionId, $stageId, $fileStage, false, $reviewRound, $revisedFileId, $assocType, $assocId + $submissionId, $stageId, $fileStage, false, $reviewRound, $revisedFileId, $assocType, $assocId, $queryId ); if (is_a($uploadedFile, 'SubmissionFile')) { diff --git a/controllers/wizard/fileUpload/form/SubmissionFilesUploadForm.inc.php b/controllers/wizard/fileUpload/form/SubmissionFilesUploadForm.inc.php index 3279ee60f17..cedfe512236 100644 --- a/controllers/wizard/fileUpload/form/SubmissionFilesUploadForm.inc.php +++ b/controllers/wizard/fileUpload/form/SubmissionFilesUploadForm.inc.php @@ -33,9 +33,12 @@ class SubmissionFilesUploadForm extends PKPSubmissionFilesUploadBaseForm { * @param $stageId integer * @param $reviewRound ReviewRound * @param $revisedFileId integer + * @param $assocType integer + * @param $assocId integer + * @param $queryId integer */ function __construct($request, $submissionId, $stageId, $uploaderRoles, $fileStage, - $revisionOnly = false, $reviewRound = null, $revisedFileId = null, $assocType = null, $assocId = null) { + $revisionOnly = false, $reviewRound = null, $revisedFileId = null, $assocType = null, $assocId = null, $queryId = null) { // Initialize class. assert(is_null($uploaderRoles) || (is_array($uploaderRoles) && count($uploaderRoles) >= 1)); @@ -45,7 +48,7 @@ function __construct($request, $submissionId, $stageId, $uploaderRoles, $fileSta parent::__construct( $request, 'controllers/wizard/fileUpload/form/fileUploadForm.tpl', - $submissionId, $stageId, $fileStage, $revisionOnly, $reviewRound, $revisedFileId, $assocType, $assocId + $submissionId, $stageId, $fileStage, $revisionOnly, $reviewRound, $revisedFileId, $assocType, $assocId, $queryId ); // Disable the genre selector for review file attachments @@ -177,9 +180,9 @@ function execute() { 'username' => $user->getUsername() ) ); - + $hookResult = parent::execute($submissionFile); - if ($hookResult) { + if ($hookResult) { return $hookResult; } diff --git a/templates/controllers/wizard/fileUpload/fileUploadWizard.tpl b/templates/controllers/wizard/fileUpload/fileUploadWizard.tpl index 5f477d36e1b..348bf8c5bcf 100644 --- a/templates/controllers/wizard/fileUpload/fileUploadWizard.tpl +++ b/templates/controllers/wizard/fileUpload/fileUploadWizard.tpl @@ -25,8 +25,8 @@ continueButtonText: {translate|json_encode key="common.continue"}, finishButtonText: {translate|json_encode key="common.complete"}, deleteUrl: {url|json_encode component="api.file.ManageFileApiHandler" op="deleteFile" submissionId=$submissionId stageId=$stageId fileStage=$fileStage suppressNotification=true escape=false}, - metadataUrl: {url|json_encode op="editMetadata" submissionId=$submissionId stageId=$stageId reviewRoundId=$reviewRoundId fileStage=$fileStage escape=false}, - finishUrl: {url|json_encode op="finishFileSubmission" submissionId=$submissionId stageId=$stageId reviewRoundId=$reviewRoundId fileStage=$fileStage escape=false} + metadataUrl: {url|json_encode op="editMetadata" submissionId=$submissionId stageId=$stageId reviewRoundId=$reviewRoundId fileStage=$fileStage assocType=$assocType assocId=$assocId queryId=$queryId escape=false}, + finishUrl: {url|json_encode op="finishFileSubmission" submissionId=$submissionId stageId=$stageId reviewRoundId=$reviewRoundId fileStage=$fileStage assocType=$assocType assocId=$assocId queryId=$queryId escape=false} {rdelim} ); {rdelim}); @@ -34,7 +34,7 @@
diff --git a/templates/controllers/wizard/fileUpload/form/fileUploadForm.tpl b/templates/controllers/wizard/fileUpload/form/fileUploadForm.tpl index b6b1ec2dfbc..d15909aa0ba 100644 --- a/templates/controllers/wizard/fileUpload/form/fileUploadForm.tpl +++ b/templates/controllers/wizard/fileUpload/form/fileUploadForm.tpl @@ -140,7 +140,7 @@ {rdelim}, $uploader: $('#{$pluploadControl}'), uploaderOptions: {ldelim} - uploadUrl: {url|json_encode op="uploadFile" submissionId=$submissionId stageId=$stageId fileStage=$fileStage reviewRoundId=$reviewRoundId assocType=$assocType assocId=$assocId escape=false}, + uploadUrl: {url|json_encode op="uploadFile" submissionId=$submissionId stageId=$stageId fileStage=$fileStage reviewRoundId=$reviewRoundId assocType=$assocType assocId=$assocId queryId=$queryId escape=false}, baseUrl: {$baseUrl|json_encode}, browse_button: '{$browseButtonId}' {rdelim}