diff --git a/api/v1/_uploadPublicFile/PKPUploadPublicFileHandler.inc.php b/api/v1/_uploadPublicFile/PKPUploadPublicFileHandler.inc.php index e9d952e9dc6..d5b8cc84c1a 100644 --- a/api/v1/_uploadPublicFile/PKPUploadPublicFileHandler.inc.php +++ b/api/v1/_uploadPublicFile/PKPUploadPublicFileHandler.inc.php @@ -79,7 +79,7 @@ public function uploadFile($slimRequest, $response, $args) { $request = $this->getRequest(); if (empty($_FILES) || empty($_FILES['file'])) { - return $response->withStatus(400)->withJsonError('api.temporaryFiles.400.noUpload'); + return $response->withStatus(400)->withJsonError('api.files.400.noUpload'); } $siteDir = Core::getBaseDir() . '/' . Config::getVar('files', 'public_files_dir') . '/site'; @@ -163,18 +163,18 @@ public function uploadFile($slimRequest, $response, $args) { switch ($fileManager->getUploadErrorCode($filename)) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: - return $response->withStatus(400)->withJsonError('api.temporaryFiles.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]); + return $response->withStatus(400)->withJsonError('api.files.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]); case UPLOAD_ERR_PARTIAL: - return $response->withStatus(400)->withJsonError('api.temporaryFiles.409.uploadFailed'); + return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed'); case UPLOAD_ERR_NO_FILE: - return $response->withStatus(400)->withJsonError('api.temporaryFiles.400.noUpload'); + return $response->withStatus(400)->withJsonError('api.files.400.noUpload'); case UPLOAD_ERR_NO_TMP_DIR: case UPLOAD_ERR_CANT_WRITE: case UPLOAD_ERR_EXTENSION: - return $response->withStatus(400)->withJsonError('api.temporaryFiles.400.config'); + return $response->withStatus(400)->withJsonError('api.files.400.config'); } } - return $response->withStatus(400)->withJsonError('api.temporaryFiles.409.uploadFailed'); + return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed'); } return $this->getResponse($response->withJson([ diff --git a/api/v1/submissions/PKPSubmissionFileHandler.inc.php b/api/v1/submissions/PKPSubmissionFileHandler.inc.php new file mode 100644 index 00000000000..072b01eb2e1 --- /dev/null +++ b/api/v1/submissions/PKPSubmissionFileHandler.inc.php @@ -0,0 +1,455 @@ +_handlerPath = 'submissions/{submissionId}/files'; + $this->_endpoints = [ + 'GET' => [ + [ + 'pattern' => $this->getEndpointPattern(), + 'handler' => [$this, 'getMany'], + 'roles' => [ROLE_ID_MANAGER, ROLE_ID_SUB_EDITOR, ROLE_ID_ASSISTANT, ROLE_ID_AUTHOR], + ], + [ + 'pattern' => $this->getEndpointPattern() . '/{submissionFileId}', + 'handler' => [$this, 'get'], + 'roles' => [ROLE_ID_MANAGER, ROLE_ID_SUB_EDITOR, ROLE_ID_ASSISTANT, ROLE_ID_AUTHOR], + ], + ], + 'POST' => [ + [ + 'pattern' => $this->getEndpointPattern(), + 'handler' => [$this, 'add'], + 'roles' => [ROLE_ID_MANAGER, ROLE_ID_SUB_EDITOR, ROLE_ID_ASSISTANT, ROLE_ID_AUTHOR], + ], + ], + 'PUT' => [ + [ + 'pattern' => $this->getEndpointPattern() . '/{submissionFileId}', + 'handler' => [$this, 'edit'], + 'roles' => [ROLE_ID_MANAGER, ROLE_ID_SUB_EDITOR, ROLE_ID_ASSISTANT, ROLE_ID_AUTHOR], + ], + ], + 'DELETE' => [ + [ + 'pattern' => $this->getEndpointPattern() . '/{submissionFileId}', + 'handler' => [$this, 'delete'], + 'roles' => [ROLE_ID_MANAGER, ROLE_ID_SUB_EDITOR, ROLE_ID_ASSISTANT, ROLE_ID_AUTHOR], + ], + ], + ]; + parent::__construct(); + } + + // + // Implement methods from PKPHandler + // + function authorize($request, &$args, $roleAssignments) { + $route = $this->getSlimRequest()->getAttribute('route'); + import('lib.pkp.classes.security.authorization.SubmissionFileAccessPolicy'); // SUBMISSION_FILE_ACCESS_ + + import('lib.pkp.classes.security.authorization.ContextAccessPolicy'); + $this->addPolicy(new ContextAccessPolicy($request, $roleAssignments)); + + import('lib.pkp.classes.security.authorization.SubmissionAccessPolicy'); + $this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments)); + + if ($route->getName() === 'add') { + $params = $this->getSlimRequest()->getParsedBody(); + $fileStage = isset($params['fileStage']) ? (int) $params['fileStage'] : 0; + import('lib.pkp.classes.security.authorization.internal.SubmissionFileStageAccessPolicy'); + $this->addPolicy(new SubmissionFileStageAccessPolicy($fileStage, SUBMISSION_FILE_ACCESS_MODIFY, 'api.submissionFiles.403.unauthorizedFileStageIdWrite')); + + } elseif ($route->getName() === 'getMany') { + // Anyone passing SubmissionAccessPolicy is allowed to access getMany, + // but the endpoint will return different files depending on the user's + // stage assignments. + + } else { + $accessMode = $this->getSlimRequest()->getMethod() === 'GET' + ? SUBMISSION_FILE_ACCESS_READ + : SUBMISSION_FILE_ACCESS_MODIFY; + import('lib.pkp.classes.security.authorization.SubmissionFileAccessPolicy'); + $this->addPolicy(new SubmissionFileAccessPolicy($request, $args, $roleAssignments, $accessMode, (int) $route->getArgument('submissionFileId'))); + } + + return parent::authorize($request, $args, $roleAssignments); + } + + /** + * Get a collection of submission files + * + * @param \Slim\Http\Request $slimRequest + * @param APIResponse $response + * @param array $args arguments + * @return Response + */ + public function getMany($slimRequest, $response, $args) { + $request = $this->getRequest(); + + $params = []; + + foreach ($slimRequest->getQueryParams() as $param => $val) { + switch ($param) { + case 'fileStages': + case 'reviewRoundIds': + if (is_string($val) && strpos($val, ',') > -1) { + $val = explode(',', $val); + } elseif (!is_array($val)) { + $val = array($val); + } + $params[$param] = array_map('intval', $val); + break; + } + } + + $userRoles = $this->getAuthorizedContextObject(ASSOC_TYPE_USER_ROLES); + $stageAssignments = $this->getAuthorizedContextObject(ASSOC_TYPE_ACCESSIBLE_WORKFLOW_STAGES); + + // Managers can access files for submissions they are not assigned to + if (empty($stageAssignments)) { + if (!in_array(ROLE_ID_MANAGER, $userRoles)) { + return $response->withStatus(403)->withJsonError('api.403.unauthorized'); + } + // @see PKPSubmissionFileService::getAssignedFileStages() for excluded file stages + $params['fileStages'] = [ + SUBMISSION_FILE_SUBMISSION, + SUBMISSION_FILE_REVIEW_FILE, + SUBMISSION_FILE_FINAL, + SUBMISSION_FILE_COPYEDIT, + SUBMISSION_FILE_PROOF, + SUBMISSION_FILE_PRODUCTION_READY, + SUBMISSION_FILE_ATTACHMENT, + SUBMISSION_FILE_REVIEW_REVISION, + SUBMISSION_FILE_INTERNAL_REVIEW_FILE, + SUBMISSION_FILE_INTERNAL_REVIEW_REVISION, + ]; + + // Set the allowed file stages based on stage assignment + // @see PKPSubmissionFileService::getAssignedFileStages() for excluded file stages + } else { + $allowedFileStages = Services::get('submissionFile')->getAssignedFileStages($stageAssignments, SUBMISSION_FILE_ACCESS_READ); + if (empty($params['fileStages'])) { + $params['fileStages'] = $allowedFileStages; + } else { + foreach ($params['fileStages'] as $fileStage) { + if (!in_array($fileStage, $allowedFileStages)) { + return $response->withStatus(403)->withJsonError('api.submissionFiles.403.unauthorizedFileStageId'); + } + } + } + } + + // Check if requested reviewRounds are valid + if (!empty($params['reviewRoundIds'])) { + + // Get the valid review round ids for allowed file stage ids + $allowedReviewRoundIds = []; + $reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); + $submission = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION); + if (!empty(array_intersect([SUBMISSION_FILE_INTERNAL_REVIEW_FILE, SUBMISSION_FILE_INTERNAL_REVIEW_REVISION], $params['fileStages']))) { + $result = $reviewRoundDao->getBySubmissionId($submission->getId(), WORKFLOW_STAGE_ID_INTERNAL_REVIEW,); + while ($reviewRound = $result->next()) { + $allowedReviewRoundIds[] = $reviewRound->getId(); + } + } + if (!empty(array_intersect([SUBMISSION_FILE_REVIEW_FILE, SUBMISSION_FILE_REVIEW_REVISION], $params['fileStages']))) { + $result = $reviewRoundDao->getBySubmissionId($submission->getId(), WORKFLOW_STAGE_ID_EXTERNAL_REVIEW); + while ($reviewRound = $result->next()) { + $allowedReviewRoundIds[] = $reviewRound->getId(); + } + } + + foreach ($params['reviewRoundIds'] as $reviewRoundId) { + if (!in_array($reviewRoundId, $allowedReviewRoundIds)) { + return $response->withStatus(403)->withJsonError('api.submissionFiles.403.unauthorizedReviewRound'); + } + } + } + + \HookRegistry::call('API::submissions::files::params', [&$params, $slimRequest]); + + $params['submissionIds'] = [$this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION)->getId()]; + + $items = []; + $filesIterator = Services::get('submissionFile')->getMany($params); + if (count($filesIterator)) { + $propertyArgs = [ + 'request' => $request, + 'slimRequest' => $slimRequest, + 'submission' => $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION), + ]; + foreach ($filesIterator as $file) { + $items[] = Services::get('submissionFile')->getSummaryProperties($file, $propertyArgs); + } + } + + $data = [ + 'itemsMax' => Services::get('submissionFile')->getCount($params), + 'items' => $items, + ]; + + return $response->withJson($data, 200); + } + + /** + * Get a single submission file + * + * @param \Slim\Http\Request $slimRequest + * @param APIResponse $response + * @param array $args arguments + * @return Response + */ + public function get($slimRequest, $response, $args) { + $submissionFile = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION_FILE); + + $data = Services::get('submissionFile')->getFullProperties($submissionFile, [ + 'request' => $this->getRequest(), + 'slimRequest' => $slimRequest, + 'submission' => $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION), + ]); + + return $response->withJson($data, 200); + } + + /** + * Add a new submission file + * + * @param \Slim\Http\Request $slimRequest + * @param APIResponse $response + * @param array $args arguments + * @return Response + */ + public function add($slimRequest, $response, $args) { + $request = $this->getRequest(); + $submission = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION); + + if (empty($_FILES)) { + return $response->withStatus(400)->withJsonError('api.files.400.noUpload'); + } + + if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) { + return $this->getUploadErrorResponse($response, $_FILES['file']['error']); + } + + import('lib.pkp.classes.file.FileManager'); + $fileManager = new FileManager(); + $extension = $fileManager->parseFileExtension($_FILES['file']['name']); + + $submissionDir = Services::get('submissionFile')->getSubmissionDir($request->getContext()->getId(), $submission->getId()); + $fileId = Services::get('file')->add( + $_FILES['file']['tmp_name'], + $submissionDir . '/' . uniqid() . '.' . $extension + ); + + $params = $this->convertStringsToSchema(SCHEMA_SUBMISSION_FILE, $slimRequest->getParsedBody()); + $params['fileId'] = $fileId; + $params['submissionId'] = $submission->getId(); + $params['uploaderUserId'] = (int) $request->getUser()->getId(); + + $primaryLocale = $request->getContext()->getPrimaryLocale(); + $allowedLocales = $request->getContext()->getData('supportedSubmissionLocales'); + + // Set the name if not passed with the request + if (empty($params['name'])) { + $params['name'][$primaryLocale] = $_FILES['file']['name']; + } + + // If no genre has been set and there is only one genre possible, set it automatically + if (empty($params['genreId'])) { + $genres = DAORegistry::getDAO('GenreDAO')->getEnabledByContextId($request->getContext()->getId()); + if ($genres->count === 1) { + $params['genreId'] = $genres->next()->getId(); + } + } + + $errors = Services::get('submissionFile')->validate(VALIDATE_ACTION_ADD, $params, $allowedLocales, $primaryLocale); + + if (!empty($errors)) { + return $response->withStatus(400)->withJson($errors); + } + + // Review attachments and discussion files can not be uploaded through this API endpoint + $notAllowedFileStages = [ + SUBMISSION_FILE_NOTE, + SUBMISSION_FILE_REVIEW_ATTACHMENT, + SUBMISSION_FILE_QUERY, + ]; + if (in_array($params['fileStage'], $notAllowedFileStages)) { + return $response->withStatus(400)->withJsonError('api.submissionFiles.403.unauthorizedFileStageIdWrite'); + } + + // A valid review round is required when uploading to a review file stage + $reviewFileStages = [ + SUBMISSION_FILE_INTERNAL_REVIEW_FILE, + SUBMISSION_FILE_INTERNAL_REVIEW_REVISION, + SUBMISSION_FILE_REVIEW_FILE, + SUBMISSION_FILE_REVIEW_REVISION, + ]; + if (in_array($params['fileStage'], $reviewFileStages)) { + if (empty($params['assocType']) || $params['assocType'] !== ASSOC_TYPE_REVIEW_ROUND || empty($params['assocId'])) { + return $response->withStatus(400)->withJsonError('api.submissionFiles.400.missingReviewRoundAssocType'); + } + $reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /* @var $reviewRoundDao ReviewRoundDAO */ + $reviewRound = $reviewRoundDao->getById($params['assocId']); + $stageId = in_array($params['fileStage'], [SUBMISSION_FILE_INTERNAL_REVIEW_FILE, SUBMISSION_FILE_INTERNAL_REVIEW_REVISION]) + ? WORKFLOW_STAGE_ID_INTERNAL_REVIEW + : WORKFLOW_STAGE_ID_EXTERNAL_REVIEW; + if (!$reviewRound + || $reviewRound->getData('submissionId') != $params['submissionId'] + || $reviewRound->getData('stageId') != $stageId) { + return $response->withStatus(400)->withJsonError('api.submissionFiles.400.reviewRoundSubmissionNotMatch'); + } + } + + $submissionFile = DAORegistry::getDao('SubmissionFileDAO')->newDataObject(); + $submissionFile->_data = $params; + + $submissionFile = Services::get('submissionFile')->add($submissionFile, $request); + + $data = Services::get('submissionFile')->getFullProperties($submissionFile, [ + 'request' => $request, + 'slimRequest' => $slimRequest, + 'submission' => $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION), + ]); + + return $response->withJson($data, 200); + } + + /** + * Edit a submission file + * + * @param \Slim\Http\Request $slimRequest + * @param APIResponse $response + * @param array $args arguments + * @return Response + */ + public function edit($slimRequest, $response, $args) { + $request = $this->getRequest(); + $submission = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION); + $submissionFile = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION_FILE); + + $params = $this->convertStringsToSchema(SCHEMA_SUBMISSION_FILE, $slimRequest->getParsedBody()); + + // Don't allow these properties to be modified + unset($params['submissionId']); + unset($params['fileId']); + unset($params['uploaderUserId']); + + if (empty($params) && empty($_FILES['file'])) { + return $response->withStatus(400)->withJsonError('api.submissions.files.400.noParams'); + } + + $primaryLocale = $request->getContext()->getPrimaryLocale(); + $allowedLocales = $request->getContext()->getData('supportedSubmissionLocales'); + + $errors = Services::get('submissionFile')->validate(VALIDATE_ACTION_EDIT, $params, $allowedLocales, $primaryLocale); + + if (!empty($errors)) { + return $response->withStatus(400)->withJson($errors); + } + + // Upload a new file + if (!empty($_FILES['file'])) { + + if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) { + return $this->getUploadErrorResponse($response, $_FILES['file']['error']); + } + + import('lib.pkp.classes.file.FileManager'); + $fileManager = new FileManager(); + $extension = $fileManager->parseFileExtension($_FILES['file']['name']); + $submissionDir = Services::get('submissionFile')->getSubmissionDir($request->getContext()->getId(), $submission->getId()); + $fileId = Services::get('file')->add( + $_FILES['file']['tmp_name'], + $submissionDir . '/' . uniqid() . '.' . $extension + ); + + $params['fileId'] = $fileId; + $params['uploaderUserId'] = $request->getUser()->getId(); + if (empty($params['name'])) { + $params['name'][$primaryLocale] = $_FILES['file']['name']; + } + } + + $submissionFile = Services::get('submissionFile')->edit($submissionFile, $params, $request); + + $data = Services::get('submissionFile')->getFullProperties($submissionFile, [ + 'request' => $request, + 'slimRequest' => $slimRequest, + 'submission' => $submission, + ]); + + return $response->withJson($data, 200); + } + + /** + * Delete a submission file + * + * @param \Slim\Http\Request $slimRequest + * @param APIResponse $response + * @param array $args arguments + * @return Response + */ + public function delete($slimRequest, $response, $args) { + $request = $this->getRequest(); + $submission = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION); + $submissionFile = $this->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION_FILE); + + $data = Services::get('submissionFile')->getFullProperties($submissionFile, [ + 'request' => $request, + 'slimRequest' => $slimRequest, + 'submission' => $submission, + ]); + + Services::get('submissionFile')->delete($submissionFile); + + return $response->withJson($data, 200); + } + + /** + * Helper method to get the appropriate response when an error + * has occurred during a file upload + * + * @param APIResponse $response + * @param int $error One of the UPLOAD_ERR_ constants + * @return APIResponse + */ + private function getUploadErrorResponse($response, $error) { + switch ($error) { + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + return $response->withStatus(400)->withJsonError('api.files.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]); + case UPLOAD_ERR_PARTIAL: + return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed'); + case UPLOAD_ERR_NO_FILE: + return $response->withStatus(400)->withJsonError('api.files.400.noUpload'); + case UPLOAD_ERR_NO_TMP_DIR: + case UPLOAD_ERR_CANT_WRITE: + case UPLOAD_ERR_EXTENSION: + return $response->withStatus(400)->withJsonError('api.files.400.config'); + } + return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed'); + } +} diff --git a/api/v1/temporaryFiles/PKPTemporaryFilesHandler.inc.php b/api/v1/temporaryFiles/PKPTemporaryFilesHandler.inc.php index 1a7e24e62ce..a5f102ac9f0 100644 --- a/api/v1/temporaryFiles/PKPTemporaryFilesHandler.inc.php +++ b/api/v1/temporaryFiles/PKPTemporaryFilesHandler.inc.php @@ -79,7 +79,7 @@ public function uploadFile($slimRequest, $response, $args) { $request = $this->getRequest(); if (empty($_FILES)) { - return $response->withStatus(400)->withJsonError('api.temporaryFiles.400.noUpload'); + return $response->withStatus(400)->withJsonError('api.files.400.noUpload'); } import('lib.pkp.classes.file.TemporaryFileManager'); @@ -92,18 +92,18 @@ public function uploadFile($slimRequest, $response, $args) { switch ($temporaryFileManager->getUploadErrorCode($fileName)) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: - return $response->withStatus(400)->withJsonError('api.temporaryFiles.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]); + return $response->withStatus(400)->withJsonError('api.files.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]); case UPLOAD_ERR_PARTIAL: - return $response->withStatus(400)->withJsonError('api.temporaryFiles.409.uploadFailed'); + return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed'); case UPLOAD_ERR_NO_FILE: - return $response->withStatus(400)->withJsonError('api.temporaryFiles.400.noUpload'); + return $response->withStatus(400)->withJsonError('api.files.400.noUpload'); case UPLOAD_ERR_NO_TMP_DIR: case UPLOAD_ERR_CANT_WRITE: case UPLOAD_ERR_EXTENSION: - return $response->withStatus(400)->withJsonError('api.temporaryFiles.400.config'); + return $response->withStatus(400)->withJsonError('api.files.400.config'); } } - return $response->withStatus(400)->withJsonError('api.temporaryFiles.409.uploadFailed'); + return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed'); } return $this->getResponse($response->withJson(['id' => $uploadedFile->getId()])); diff --git a/classes/components/forms/submission/PKPSubmissionFileForm.inc.php b/classes/components/forms/submission/PKPSubmissionFileForm.inc.php new file mode 100644 index 00000000000..22ebbd460ac --- /dev/null +++ b/classes/components/forms/submission/PKPSubmissionFileForm.inc.php @@ -0,0 +1,49 @@ +action = $action; + + $this->addField(new FieldOptions('genreId', [ + 'label' => __('submission.submit.genre.label'), + 'description' => __('submission.submit.genre.description'), + 'type' => 'radio', + 'options' => array_map(function($genre) { + return [ + 'value' => (int) $genre->getId(), + 'label' => $genre->getLocalizedName(), + ]; + }, $genres), + 'value' => 0, + ])); + } +} diff --git a/classes/controllers/modals/editorDecision/form/EditorDecisionForm.inc.php b/classes/controllers/modals/editorDecision/form/EditorDecisionForm.inc.php index 2ab9f0980de..d95cb3326ea 100644 --- a/classes/controllers/modals/editorDecision/form/EditorDecisionForm.inc.php +++ b/classes/controllers/modals/editorDecision/form/EditorDecisionForm.inc.php @@ -178,19 +178,17 @@ function _initiateReviewRound($submission, $stageId, $request, $status = null) { // Add the selected files to the new round. $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ - // Bring in the SUBMISSION_FILE_* constants. - import('lib.pkp.classes.submission.SubmissionFile'); - // Bring in the Manager (we need it). - import('lib.pkp.classes.file.SubmissionFileManager'); - $submissionFileManager = new SubmissionFileManager($submission->getContextId(), $submission->getId()); foreach (array('selectedFiles', 'selectedAttachments') as $userVar) { $selectedFiles = $this->getData($userVar); if(is_array($selectedFiles)) { foreach ($selectedFiles as $fileId) { - // Retrieve the file last revision number. - $revisionNumber = $submissionFileDao->getLatestRevisionNumber($fileId); - list($newFileId, $newRevision) = $submissionFileManager->copyFileToFileStage($fileId, $revisionNumber, SUBMISSION_FILE_REVIEW_FILE, null, true); - $submissionFileDao->assignRevisionToReviewRound($newFileId, $newRevision, $reviewRound); + $newSubmissionFile = Services::get('submissionFile')->get($fileId); + $newSubmissionFile->setData('fileStage', SUBMISSION_FILE_REVIEW_FILE); + $newSubmissionFile->setData('sourceSubmissionFileId', $fileId); + $newSubmissionFile->setData('assocType', null); + $newSubmissionFile->setData('assocId', null); + $newSubmissionFile = Services::get('submissionFile')->add($newSubmissionFile, $request); + $submissionFileDao->assignRevisionToReviewRound($newSubmissionFile->getId(), $reviewRound); } } } diff --git a/classes/core/PKPApplication.inc.php b/classes/core/PKPApplication.inc.php index 3ef20f269a5..27ddfe43acb 100644 --- a/classes/core/PKPApplication.inc.php +++ b/classes/core/PKPApplication.inc.php @@ -52,6 +52,7 @@ define('ASSOC_TYPE_QUERY', 0x010000a); define('ASSOC_TYPE_QUEUED_PAYMENT', 0x010000b); define('ASSOC_TYPE_PUBLICATION', 0x010000c); +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); @@ -471,7 +472,7 @@ public function getDAOMap() { 'SubmissionDisciplineEntryDAO' => 'lib.pkp.classes.submission.SubmissionDisciplineEntryDAO', 'SubmissionEmailLogDAO' => 'lib.pkp.classes.log.SubmissionEmailLogDAO', 'SubmissionEventLogDAO' => 'lib.pkp.classes.log.SubmissionEventLogDAO', - 'SubmissionFileDAO' => 'lib.pkp.classes.submission.SubmissionFileDAO', + 'SubmissionFileDAO' => 'classes.submission.SubmissionFileDAO', 'SubmissionFileEventLogDAO' => 'lib.pkp.classes.log.SubmissionFileEventLogDAO', 'QueryDAO' => 'lib.pkp.classes.query.QueryDAO', 'SubmissionLanguageDAO' => 'lib.pkp.classes.submission.SubmissionLanguageDAO', diff --git a/classes/db/SchemaDAO.inc.php b/classes/db/SchemaDAO.inc.php index 7a1add21d11..62811d2c548 100644 --- a/classes/db/SchemaDAO.inc.php +++ b/classes/db/SchemaDAO.inc.php @@ -281,10 +281,15 @@ private function _getPrimaryDbProps($object) { $primaryDbProps = []; foreach ($this->primaryTableColumns as $propName => $columnName) { if ($propName !== 'id' && array_key_exists($propName, $sanitizedProps)) { - $primaryDbProps[$columnName] = $this->convertToDB($sanitizedProps[$propName], $schema->properties->{$propName}->type); + // If the value is null and the prop is nullable, leave it null + if (is_null($sanitizedProps[$propName]) + && isset($schema->properties->{$propName}->validation) + && in_array('nullable', $schema->properties->{$propName}->validation)) { + $primaryDbProps[$columnName] = null; + // Convert empty string values for DATETIME columns into null values // because an empty string can not be saved to a DATETIME column - if ($primaryDbProps[$columnName] === '' + } elseif ($sanitizedProps[$columnName] === '' && isset($schema->properties->{$propName}->validation) && ( in_array('date_format:Y-m-d H:i:s', $schema->properties->{$propName}->validation) @@ -292,6 +297,8 @@ private function _getPrimaryDbProps($object) { ) ) { $primaryDbProps[$columnName] = null; + } else { + $primaryDbProps[$columnName] = $this->convertToDB($sanitizedProps[$propName], $schema->properties->{$propName}->type); } } } diff --git a/classes/file/BaseSubmissionFileManager.inc.php b/classes/file/BaseSubmissionFileManager.inc.php deleted file mode 100644 index 08bde5dfab5..00000000000 --- a/classes/file/BaseSubmissionFileManager.inc.php +++ /dev/null @@ -1,66 +0,0 @@ -_submissionId = (int) $submissionId; - } - - - // - // Public methods - // - /** - * Get the base path for file storage. - * @return string - */ - function getBasePath() { - $dirNames = Application::getFileDirectories(); - return parent::getBasePath() . $dirNames['submission'] . $this->_submissionId . '/'; - } - - /** - * Get the submission ID that this manager operates upon. - * @return int - */ - function getSubmissionId() { - return $this->_submissionId; - } -} - - diff --git a/classes/file/FileArchive.inc.php b/classes/file/FileArchive.inc.php index 5382a29e45f..955a9fd35de 100644 --- a/classes/file/FileArchive.inc.php +++ b/classes/file/FileArchive.inc.php @@ -15,9 +15,6 @@ class FileArchive { - function __construct() { - } - /** * Assembles an array of filenames into either a tar.gz or a .zip * file, based on what is available. Returns a string representing diff --git a/classes/file/FileManager.inc.php b/classes/file/FileManager.inc.php index 1538c833650..ffe7a58ed13 100644 --- a/classes/file/FileManager.inc.php +++ b/classes/file/FileManager.inc.php @@ -24,12 +24,14 @@ define('DIRECTORY_MODE_MASK', 0777); define('DOCUMENT_TYPE_DEFAULT', 'default'); +define('DOCUMENT_TYPE_AUDIO', 'audio'); define('DOCUMENT_TYPE_EXCEL', 'excel'); define('DOCUMENT_TYPE_HTML', 'html'); define('DOCUMENT_TYPE_IMAGE', 'image'); define('DOCUMENT_TYPE_PDF', 'pdf'); define('DOCUMENT_TYPE_WORD', 'word'); define('DOCUMENT_TYPE_EPUB', 'epub'); +define('DOCUMENT_TYPE_VIDEO', 'video'); define('DOCUMENT_TYPE_ZIP', 'zip'); class FileManager { diff --git a/classes/file/PKPFile.inc.php b/classes/file/PKPFile.inc.php index dfb94254cdb..11638df2d86 100644 --- a/classes/file/PKPFile.inc.php +++ b/classes/file/PKPFile.inc.php @@ -103,12 +103,7 @@ function setFileSize($fileSize) { * @return string */ function getNiceFileSize() { - $niceFileSizeUnits = array('B', 'KB', 'MB', 'GB'); - $size = $this->getData('fileSize'); - for($i = 0; $i < 4 && $size > 1024; $i++) { - $size >>= 10; - } - return $size . $niceFileSizeUnits[$i]; + return Services::get('file')->getNiceFileSize($this->getFileSize()); } diff --git a/classes/file/SubmissionFileManager.inc.php b/classes/file/SubmissionFileManager.inc.php deleted file mode 100644 index b25d37c2f47..00000000000 --- a/classes/file/SubmissionFileManager.inc.php +++ /dev/null @@ -1,398 +0,0 @@ -_handleUpload( - $fileName, $fileStage, $uploaderUserId, - $revisedFileId, $genreId, $assocType, $assocId - ); - } - - /** - * Copy a submission file. - * @param $filePath string the path of the file on the file system - * @param $fileStage int submission file workflow stage - * @param $copyUserId int The id of the user that originates the file copy - * @param $revisedFileId int - * @param $genreId int (e.g. Manuscript, Appendix, etc.) - * @return SubmissionFile - */ - function copySubmissionFile($filePath, $fileStage, $copyUserId, - $revisedFileId = null, $genreId = null, $assocType = null, $assocId = null) { - return $this->_handleCopy( - $filePath, $fileStage, $copyUserId, - $revisedFileId, $genreId, $assocType, $assocId - ); - } - - /** - * Delete a file. - * @param $fileId integer - * @param $revisionId integer - * @return boolean returns true if successful - */ - function deleteById($fileId, $revision = null) { - $submissionFile = $this->_getFile($fileId, $revision); - if (isset($submissionFile)) { - return parent::deleteByPath($submissionFile->getfilePath()); - } else { - return false; - } - } - - /** - * Download a file. - * @param $fileId int the file id of the file to download - * @param $revision int the revision of the file to download - * @param $inline boolean print file as inline instead of attachment, optional - * @param $filename string The client-side download filename (optional) - * @return boolean - */ - function downloadById($fileId, $revision = null, $inline = false, $filename = null) { - $returner = false; - $submissionFile = $this->_getFile($fileId, $revision); - if (isset($submissionFile)) { - // Make sure that the file belongs to the submission. - if ($submissionFile->getSubmissionId() != $this->getSubmissionId()) fatalError('Invalid file id!'); - - $this->recordView($submissionFile); - - // Send the file to the user. - $filePath = $submissionFile->getFilePath(); - $mediaType = $submissionFile->getFileType(); - if(!isset($filename)) $filename = $submissionFile->getClientFileName(); - $returner = parent::downloadByPath($filePath, $mediaType, $inline, $filename); - } - - return $returner; - } - - /** - * Record a file view in database. - * @param $submissionFile SubmissionFile - */ - function recordView($submissionFile) { - // Mark the file as viewed by this user. - $sessionManager = SessionManager::getManager(); - $session = $sessionManager->getUserSession(); - $user = $session->getUser(); - if (is_a($user, 'User')) { - $viewsDao = DAORegistry::getDAO('ViewsDAO'); /* @var $viewsDao ViewsDAO */ - $viewsDao->recordView( - ASSOC_TYPE_SUBMISSION_FILE, $submissionFile->getFileIdAndRevision(), - $user->getId() - ); - } - } - - /** - * Copies an existing SubmissionFile and renames it. - * @param $sourceFileId int - * @param $sourceRevision int - * @param $fileStage int - * @param $destFileId int (optional) - * @param $viewable boolean (optional) - * @return array? array(file_id, revision) on success; null on failure - */ - function copyFileToFileStage($sourceFileId, $sourceRevision, $newFileStage, $destFileId = null, $viewable = false) { - if (HookRegistry::call('SubmissionFileManager::copyFileToFileStage', array(&$sourceFileId, &$sourceRevision, &$newFileStage, &$destFileId, &$result))) return $result; - - $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ - $sourceFile = $submissionFileDao->getRevision($sourceFileId, $sourceRevision); /* @var $sourceFile SubmissionFile */ - if (!$sourceFile) return false; - - // Rename the variable just so that we don't get confused. - $destFile = $sourceFile; - - // Find out where the source file lives. - $sourcePath = $sourceFile->getFilePath(); - - // Update the ID (or clear if making a new file) and get new revision number. - if ($destFileId != null) { - $currentRevision = $submissionFileDao->getLatestRevisionNumber($destFileId); - $revision = $currentRevision + 1; - $destFile->setFileId($destFileId); - } else { - $destFile->setFileId(null); - $revision = 1; - } - - // Update the necessary fields of the destination file. - $destFile->setRevision($revision); - $destFile->setFileStage($newFileStage); - $destFile->setDateModified(Core::getCurrentDate()); - $destFile->setViewable($viewable); - // Set the old file as the source - $destFile->setSourceFileId($sourceFileId); - $destFile->setSourceRevision($sourceRevision); - - // Find out where the file should go. - $destPath = $destFile->getFilePath(); - - // Now insert the row into the DB and get the inserted file id. - $insertedFile = $submissionFileDao->insertObject($destFile, $sourcePath); - - return $insertedFile ? array($insertedFile->getFileId(), $insertedFile->getRevision()) : null; - } - - // - // Private helper methods - // - /** - * Upload the file and add it to the database. - * @param $fileName string index into the $_FILES array - * @param $fileStage int submission file stage (one of the SUBMISSION_FILE_* constants) - * @param $uploaderUserId int The id of the user that uploaded the file. - * @param $revisedFileId int ID of an existing file to revise - * @param $genreId int foreign key into genres table (e.g. manuscript, etc.) - * @param $assocType int - * @param $assocId int - * @return SubmissionFile the uploaded submission file or null if an error occured. - */ - function _handleUpload($fileName, $fileStage, $uploaderUserId, - $revisedFileId = null, $genreId = null, $assocType = null, $assocId = null) { - - // Ensure that the file has been correctly uploaded to the server. - if (!$this->uploadedFileExists($fileName)) return null; - - // Retrieve the location of the uploaded file. - $sourceFile = $this->getUploadedFilePath($fileName); - - // Instantiate and pre-populate a new submission file object. - $submissionFile = $this->_instantiateSubmissionFile($sourceFile, $fileStage, $revisedFileId, $genreId, $assocType, $assocId); - if (is_null($submissionFile)) return null; - - // Retrieve and copy the file type of the uploaded file. - $fileType = $this->getUploadedFileType($fileName); - assert($fileType !== false); - $submissionFile->setFileType($fileType); - - // Retrieve and copy the file name of the uploaded file. - $originalFileName = $this->getUploadedFileName($fileName); - assert($originalFileName !== false); - $submissionFile->setOriginalFileName($this->truncateFileName($originalFileName)); - - // Set the uploader's user and user group id. - $submissionFile->setUploaderUserId($uploaderUserId); - - // Copy the uploaded file to its final destination and - // persist its meta-data to the database. - $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ - return $submissionFileDao->insertObject($submissionFile, $fileName, true); - } - - /** - * Copy a file and add it to the database. - * @param $filePath string full path to file on the file system - * @param $fileStage int submission file stage (one of the SUBMISSION_FILE_* constants) - * @param $copyUserId int The id of the user that is copying the file. - * @param $revisedFileId int ID of an existing file to revise - * @param $genreId int foreign key into genres table (e.g. manuscript, etc.) - * @param $assocType int - * @param $assocId int - * @return SubmissionFile the submission file or null if an error occured. - */ - function _handleCopy($filePath, $fileStage, $copyUserId, - $revisedFileId = null, $genreId = null, $assocType = null, $assocId = null) { - - // Ensure that the file exists on the file system - if (!$this->fileExists($filePath)) return null; - - // Instantiate and pre-populate a new submission file object. - $submissionFile = $this->_instantiateSubmissionFile($filePath, $fileStage, $revisedFileId, $genreId, $assocType, $assocId); - if (is_null($submissionFile)) return null; - - // Retrieve and copy the file type of the uploaded file. - $fileType = PKPString::mime_content_type($filePath); - assert($fileType !== false); - $submissionFile->setFileType($fileType); - - // Retrieve the file name from the file path - $originalFileName = basename($filePath); - assert($originalFileName !== false); - $submissionFile->setOriginalFileName($this->truncateFileName($originalFileName)); - - // Set the user and user group id for the copied file - $submissionFile->setUploaderUserId($copyUserId); - - // Save the submission file to the database. - $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ - return $submissionFileDao->insertObject($submissionFile, $filePath, false); - } - - /** - * Routine to instantiate and pre-populate a new submission file. - * @param $sourceFilePath string - * @param $fileStage integer SUBMISSION_FILE_... - * @param $revisedFileId integer optional - * @param $genreId integer optional - * @param $assocId integer optional - * @param $assocType integer optional - * @return SubmissionFile returns the instantiated submission file or null if an error occurs. - */ - function _instantiateSubmissionFile($sourceFilePath, $fileStage, $revisedFileId = null, $genreId = null, $assocType = null, $assocId = null) { - $revisedFile = null; - - // Retrieve the submission file DAO. - $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ - - // Except for reviewer file attachments we either need a genre id or a - // revised file, otherwise we cannot identify the target file - // implementation. - if ($fileStage != SUBMISSION_FILE_REVIEW_ATTACHMENT) { - assert(isset($genreId) || isset($revisedFileId)); - if (!$genreId || $revisedFileId) { - // Retrieve the revised file. (null $fileStage in case the revision is from a previous stage). - $revisedFile = $submissionFileDao->getLatestRevision($revisedFileId, null, $this->getSubmissionId()); - if (!is_a($revisedFile, 'SubmissionFile')) return null; - } - } - - // If we don't have a genre then use the genre from the - // existing file. - if ($revisedFile && !$genreId) { - $genreId = $revisedFile->getGenreId(); - } - - // Instantiate a new submission file implementation. - $submissionFile = $submissionFileDao->newDataObjectByGenreId($genreId); /* @var $submissionFile SubmissionFile */ - $submissionFile->setSubmissionId($this->getSubmissionId()); - - // Instantiate submission locale for the file - $submissionDao = DAORegistry::getDAO('SubmissionDAO'); /* @var $submissionDao SubmissionDAO */ - $submission = $submissionDao->getById($submissionFile->getSubmissionId()); - $submissionFile->setSubmissionLocale($submission->getLocale()); - - // Do we create a new file or a new revision of an existing file? - if ($revisedFileId) { - // Make sure that the submission of the revised file is - // the same as that of the uploaded file. - if ($revisedFile->getSubmissionId() != $this->getSubmissionId()) return null; - - // If file stages are different we reference with the sourceFileId - // Otherwise, we keep the file id, update the revision, and copy other fields. - if(!is_null($fileStage) && $fileStage !== $revisedFile->getFileStage()) { - $submissionFile->setSourceFileId($revisedFileId); - $submissionFile->setSourceRevision($revisedFile->getRevision()); - $submissionFile->setRevision(1); - $submissionFile->setViewable(false); - } else { - // Create a new revision of the file with the existing file id. - $submissionFile->setFileId($revisedFileId); - $submissionFile->setRevision($revisedFile->getRevision()+1); - - // Copy the file stage (in case of null passed in). - $fileStage = (int)$revisedFile->getFileStage(); - - // Copy the assoc type. - if(!is_null($assocType) && $assocType !== $revisedFile->getAssocType()) fatalError('Invalid submission file assoc type!'); - $assocType = (int)$revisedFile->getAssocType(); - - // Copy the assoc id. - if (!is_null($assocId) && $assocId !== $revisedFile->getAssocId()) fatalError('Invalid submission file assoc ID!'); - $assocId = (int)$revisedFile->getAssocId(); - - // Copy the viewable flag. - $submissionFile->setViewable($revisedFile->getViewable()); - } - - // Copy assorted user-facing metadata. - $submissionFile->copyEditableMetadataFrom($revisedFile); - } else { - // Create the first revision of a new file. - $submissionFile->setRevision(1); - $submissionFile->setViewable($fileStage == SUBMISSION_FILE_SUBMISSION?true:false); // Bug #8308: Submission files should be selected for promotion by default - } - - // Determine and set the file size of the file. - $submissionFile->setFileSize(filesize($sourceFilePath)); - - // Set the file file stage. - $submissionFile->setFileStage($fileStage); - - // Set the file genre. - $submissionFile->setGenreId($genreId); - - // Set dates to the current system date. - $submissionFile->setDateUploaded(Core::getCurrentDate()); - $submissionFile->setDateModified(Core::getCurrentDate()); - - // Is the submission file associated to another entity? - if(isset($assocId)) { - assert(isset($assocType)); - $submissionFile->setAssocType($assocType); - $submissionFile->setAssocId($assocId); - } - - // Return the pre-populated submission file. - return $submissionFile; - } - - /** - * Internal helper method to retrieve file - * information by file ID. - * @param $fileId integer - * @param $revision integer - * @return SubmissionFile - */ - function _getFile($fileId, $revision = null) { - $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ - if ($revision) { - return $submissionFileDao->getRevision($fileId, $revision); - } else { - return $submissionFileDao->getLatestRevision($fileId); - } - } -} - - diff --git a/classes/file/TemporaryFileManager.inc.php b/classes/file/TemporaryFileManager.inc.php index 65673ce4fdf..77381c729fa 100644 --- a/classes/file/TemporaryFileManager.inc.php +++ b/classes/file/TemporaryFileManager.inc.php @@ -111,44 +111,6 @@ function handleUpload($fileName, $userId) { } } - /** - * Create a new temporary file from a submission file. - * @param $submissionFile object - * @param $userId int - * @return object The new TemporaryFile or false on failure - */ - function submissionToTemporaryFile($submissionFile, $userId) { - // Get the file extension, then rename the file. - $fileExtension = $this->parseFileExtension($submissionFile->getServerFileName()); - - if (!$this->fileExists($this->filesDir, 'dir')) { - // Try to create destination directory - $this->mkdirtree($this->filesDir); - } - - $newFileName = basename(tempnam($this->filesDir, $fileExtension)); - if (!$newFileName) return false; - - if (copy($submissionFile->getFilePath(), $this->filesDir . $newFileName)) { - $temporaryFileDao = DAORegistry::getDAO('TemporaryFileDAO'); /* @var $temporaryFileDao TemporaryFileDAO */ - $temporaryFile = $temporaryFileDao->newDataObject(); - - $temporaryFile->setUserId($userId); - $temporaryFile->setServerFileName($newFileName); - $temporaryFile->setFileType($submissionFile->getFileType()); - $temporaryFile->setFileSize($submissionFile->getFileSize()); - $temporaryFile->setOriginalFileName($submissionFile->getOriginalFileName()); - $temporaryFile->setDateUploaded(Core::getCurrentDate()); - - $temporaryFileDao->insertObject($temporaryFile); - - return $temporaryFile; - - } else { - return false; - } - } - /** * Perform periodic cleanup tasks. This is used to occasionally * remove expired temporary files. diff --git a/classes/handler/APIHandler.inc.php b/classes/handler/APIHandler.inc.php index 3ea293cccf0..305681b82f9 100644 --- a/classes/handler/APIHandler.inc.php +++ b/classes/handler/APIHandler.inc.php @@ -138,7 +138,7 @@ public function __construct() { /** * Return PKP request object * - * @return PKPRequest + * @return Request */ public function getRequest() { return $this->_request; @@ -273,8 +273,7 @@ public function getParameter($parameterName, $default = null) { * @return array Converted parameters */ public function convertStringsToSchema($schema, $params) { - $schemaService = Services::get('schema'); - $schema = $schemaService->get($schema); + $schema = Services::get('schema')->get($schema); foreach ($params as $paramName => $paramValue) { if (!property_exists($schema->properties, $paramName)) { diff --git a/classes/linkAction/LinkAction.inc.php b/classes/linkAction/LinkAction.inc.php index d4a752a60f5..c093d23a017 100644 --- a/classes/linkAction/LinkAction.inc.php +++ b/classes/linkAction/LinkAction.inc.php @@ -99,22 +99,6 @@ function getToolTip() { return $this->_toolTip; } - /** - * Get a title for display when a user hovers over the - * link action. Default to the regular title if it is set. - * @return string - */ - function getHoverTitle() { - if ($this->getToolTip()) { - return $this->getToolTip(); - } else { - // for the locale key, remove any unique ids from the id. - $id = preg_replace('/([^-]+)\-.+$/', '$1', $this->getId()); - $title = __('grid.action.' . $id); - return $title; - } - } - /** * Get the action image. * @return string diff --git a/classes/log/EventLogEntry.inc.php b/classes/log/EventLogEntry.inc.php index a32f5f92ff9..fac9f0448df 100644 --- a/classes/log/EventLogEntry.inc.php +++ b/classes/log/EventLogEntry.inc.php @@ -172,10 +172,9 @@ function getTranslatedMessage($locale = null, $hideReviewerName = false) { if (isset($params['fileStage']) && $params['fileStage'] === SUBMISSION_FILE_REVIEW_ATTACHMENT) { assert(isset($params['fileId']) && isset($params['submissionId'])); $anonymousAuthor = true; - $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ - $submissionFile = $submissionFileDao->getLatestRevision($params['fileId']); - if ($submissionFile && $submissionFile->getAssocType() === ASSOC_TYPE_REVIEW_ASSIGNMENT) { - $reviewAssignment = $reviewAssignmentDao->getById($submissionFile->getAssocId()); + $submissionFile = Services::get('submissionFile')->get($params['id']); + if ($submissionFile && $submissionFile->getData('assocType') === ASSOC_TYPE_REVIEW_ASSIGNMENT) { + $reviewAssignment = $reviewAssignmentDao->getById($submissionFile->getData('assocId')); if ($reviewAssignment && !in_array($reviewAssignment->getReviewMethod(), array(SUBMISSION_REVIEW_METHOD_ANONYMOUS, SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS))) { $anonymousAuthor = false; } diff --git a/classes/log/PKPSubmissionEventLogEntry.inc.php b/classes/log/PKPSubmissionEventLogEntry.inc.php index 8ea80ef72ee..8490632576b 100644 --- a/classes/log/PKPSubmissionEventLogEntry.inc.php +++ b/classes/log/PKPSubmissionEventLogEntry.inc.php @@ -58,9 +58,6 @@ define('SUBMISSION_LOG_REVIEW_READY', 0x40000018); define('SUBMISSION_LOG_REVIEW_CONFIRMED', 0x40000019); -// Deletion of the last revision of a file -define('SUBMISSION_LOG_LAST_REVISION_DELETED', 0x50000003); - // Production events define('SUBMISSION_LOG_PROOFS_APPROVED', 0x50000008); diff --git a/classes/log/SubmissionFileEventLogDAO.inc.php b/classes/log/SubmissionFileEventLogDAO.inc.php index 90a59c84651..2354d0c92ae 100644 --- a/classes/log/SubmissionFileEventLogDAO.inc.php +++ b/classes/log/SubmissionFileEventLogDAO.inc.php @@ -31,11 +31,11 @@ function newDataObject() { /** * Get event log entries by submission file ID. - * @param $fileId int + * @param $submissionFileId int * @return DAOResultFactory */ - function getByFileId($fileId) { - return $this->getByAssoc(ASSOC_TYPE_SUBMISSION_FILE, $fileId); + function getById($submissionFileId) { + return $this->getByAssoc(ASSOC_TYPE_SUBMISSION_FILE, $submissionFileId); } } diff --git a/classes/log/SubmissionFileEventLogEntry.inc.php b/classes/log/SubmissionFileEventLogEntry.inc.php index cc87ea39163..13bf8fa6d5d 100644 --- a/classes/log/SubmissionFileEventLogEntry.inc.php +++ b/classes/log/SubmissionFileEventLogEntry.inc.php @@ -21,7 +21,7 @@ define('SUBMISSION_LOG_FILE_UPLOAD', 0x50000001); define('SUBMISSION_LOG_FILE_DELETE', 0x50000002); define('SUBMISSION_LOG_FILE_REVISION_UPLOAD', 0x50000008); -define('SUBMISSION_LOG_FILE_REVISION_DELETE', 0x50000009); +define('SUBMISSION_LOG_FILE_EDIT', 0x50000009); // Audit events define('SUBMISSION_LOG_FILE_AUDITOR_ASSIGN', 0x50000004); @@ -30,22 +30,6 @@ define('SUBMISSION_LOG_FILE_SIGNOFF_SIGNOFF', 0x50000007); class SubmissionFileEventLogEntry extends EventLogEntry { - - /** - * Set the associated file ID. - * @param $fileId int File ID - */ - function setFileId($fileId) { - return $this->setAssocId($fileId); - } - - /** - * Get the associated file ID. - * @return int File ID - */ - function getFileId() { - return $this->getAssocId(); - } } diff --git a/classes/log/SubmissionFileLog.inc.php b/classes/log/SubmissionFileLog.inc.php index 306712f112b..0153041d7d9 100644 --- a/classes/log/SubmissionFileLog.inc.php +++ b/classes/log/SubmissionFileLog.inc.php @@ -37,7 +37,7 @@ static function logEvent($request, $submissionFile, $eventType, $messageKey, $pa if ($user) $entry->setUserId($user->getId()); $entry->setAssocType(ASSOC_TYPE_SUBMISSION_FILE); - $entry->setAssocId($submissionFile->getFileId()); + $entry->setAssocId($submissionFile->getId()); // Set explicit parts of the log entry $entry->setEventType($eventType); diff --git a/classes/migration/FilesMigration.inc.php b/classes/migration/FilesMigration.inc.php new file mode 100644 index 00000000000..a13fd044e3b --- /dev/null +++ b/classes/migration/FilesMigration.inc.php @@ -0,0 +1,38 @@ +create('files', function (Blueprint $table) { + $table->bigInteger('file_id')->autoIncrement(); + $table->string('path', 255); + }); + } + + /** + * Reverse the migration. + * @return void + */ + public function down() { + Capsule::schema()->drop('files'); + } +} diff --git a/classes/migration/ReviewsMigration.inc.php b/classes/migration/ReviewsMigration.inc.php index ef398d4b383..bf6d7e41b5a 100644 --- a/classes/migration/ReviewsMigration.inc.php +++ b/classes/migration/ReviewsMigration.inc.php @@ -77,18 +77,20 @@ public function up() { $table->bigInteger('submission_id'); $table->bigInteger('review_round_id'); $table->smallInteger('stage_id'); - $table->bigInteger('file_id'); + $table->bigInteger('submission_file_id'); $table->bigInteger('revision')->default(1); $table->index(['submission_id'], 'review_round_files_submission_id'); - $table->unique(['submission_id', 'review_round_id', 'file_id', 'revision'], 'review_round_files_pkey'); + $table->unique(['submission_id', 'review_round_id', 'submission_file_id'], 'review_round_files_pkey'); + $table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files'); }); // Associates reviewable submission files with reviews Capsule::schema()->create('review_files', function (Blueprint $table) { $table->bigInteger('review_id'); - $table->bigInteger('file_id'); + $table->bigInteger('submission_file_id'); $table->index(['review_id'], 'review_files_review_id'); - $table->unique(['review_id', 'file_id'], 'review_files_pkey'); + $table->unique(['review_id', 'submission_file_id'], 'review_files_pkey'); + $table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files'); }); } diff --git a/classes/migration/SubmissionFilesMigration.inc.php b/classes/migration/SubmissionFilesMigration.inc.php index 976d15a2dc2..835c3d214d6 100644 --- a/classes/migration/SubmissionFilesMigration.inc.php +++ b/classes/migration/SubmissionFilesMigration.inc.php @@ -12,7 +12,6 @@ */ use Illuminate\Database\Migrations\Migration; -use Illuminate\Database\Schema\Builder; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Capsule\Manager as Capsule; @@ -24,63 +23,44 @@ class SubmissionFilesMigration extends Migration { public function up() { // Files associated with submission. Includes submission files, etc. Capsule::schema()->create('submission_files', function (Blueprint $table) { - $table->bigInteger('file_id')->autoIncrement(); - $table->bigInteger('revision'); - $table->bigInteger('source_file_id')->nullable(); - $table->bigInteger('source_revision')->nullable(); + $table->bigInteger('submission_file_id')->autoIncrement(); $table->bigInteger('submission_id'); - $table->string('file_type', 255); + $table->bigInteger('file_id'); + $table->bigInteger('source_submission_file_id')->nullable(); $table->bigInteger('genre_id')->nullable(); - $table->bigInteger('file_size'); - $table->string('original_file_name', 127)->nullable(); $table->bigInteger('file_stage'); $table->string('direct_sales_price', 255)->nullable(); $table->string('sales_type', 255)->nullable(); $table->smallInteger('viewable')->nullable(); - $table->datetime('date_uploaded'); - $table->datetime('date_modified'); + $table->datetime('created_at'); + $table->datetime('updated_at'); $table->bigInteger('uploader_user_id')->nullable(); $table->bigInteger('assoc_type')->nullable(); $table->bigInteger('assoc_id')->nullable(); $table->index(['submission_id'], 'submission_files_submission_id'); - // pkp/pkp-lib#5804 + // pkp/pkp-lib#5804 $table->index(['file_stage', 'assoc_type', 'assoc_id'], 'submission_files_stage_assoc'); + $table->foreign('file_id')->references('file_id')->on('files'); }); - // Work-around for compound primary key - switch (Capsule::connection()->getDriverName()) { - case 'mysql': Capsule::connection()->unprepared("ALTER TABLE submission_files DROP PRIMARY KEY, ADD PRIMARY KEY (file_id, revision)"); break; - case 'pgsql': Capsule::connection()->unprepared("ALTER TABLE submission_files DROP CONSTRAINT submission_files_pkey; ALTER TABLE submission_files ADD PRIMARY KEY (file_id, revision);"); break; - } // Article supplementary file metadata. Capsule::schema()->create('submission_file_settings', function (Blueprint $table) { - $table->bigInteger('file_id'); + $table->bigInteger('submission_file_id'); $table->string('locale', 14)->default(''); $table->string('setting_name', 255); $table->text('setting_value')->nullable(); - $table->string('setting_type', 6)->comment('(bool|int|float|string|object|date)'); - $table->index(['file_id'], 'submission_file_settings_id'); - $table->unique(['file_id', 'locale', 'setting_name'], 'submission_file_settings_pkey'); - }); - - // Submission visuals. - Capsule::schema()->create('submission_artwork_files', function (Blueprint $table) { - $table->bigInteger('file_id'); - $table->bigInteger('revision'); - $table->text('caption')->nullable(); - $table->string('credit', 255)->nullable(); - $table->string('copyright_owner', 255)->nullable(); - $table->text('copyright_owner_contact')->nullable(); - $table->text('permission_terms')->nullable(); - $table->bigInteger('permission_file_id')->nullable(); - $table->bigInteger('chapter_id')->nullable(); - $table->bigInteger('contact_author')->nullable(); + $table->string('setting_type', 6)->default('string')->comment('(bool|int|float|string|object|date)'); + $table->index(['submission_file_id'], 'submission_file_settings_id'); + $table->unique(['submission_file_id', 'locale', 'setting_name'], 'submission_file_settings_pkey'); }); - // Submission supplementary content. - Capsule::schema()->create('submission_supplementary_files', function (Blueprint $table) { + // Submission file revisions + Capsule::schema()->create('submission_file_revisions', function (Blueprint $table) { + $table->bigInteger('revision_id')->autoIncrement(); + $table->bigInteger('submission_file_id'); $table->bigInteger('file_id'); - $table->bigInteger('revision'); + $table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files'); + $table->foreign('file_id')->references('file_id')->on('files'); }); } @@ -89,8 +69,7 @@ public function up() { * @return void */ public function down() { - Capsule::schema()->drop('submission_supplementary_files'); - Capsule::schema()->drop('submission_artwork_files'); + Capsule::schema()->drop('submission_file_revisions'); Capsule::schema()->drop('submission_file_settings'); Capsule::schema()->drop('submission_files'); } diff --git a/classes/migration/SubmissionsMigration.inc.php b/classes/migration/SubmissionsMigration.inc.php index d0882c085a4..d51c7e57345 100644 --- a/classes/migration/SubmissionsMigration.inc.php +++ b/classes/migration/SubmissionsMigration.inc.php @@ -31,12 +31,13 @@ public function up() { $table->datetime('date_submitted')->nullable(); $table->datetime('last_modified')->nullable(); $table->bigInteger('stage_id')->default(WORKFLOW_STAGE_ID_SUBMISSION); + $table->string('locale', 14)->nullable(); import('lib.pkp.classes.submission.PKPSubmission'); // for constant $table->smallInteger('status')->default(STATUS_QUEUED); $table->smallInteger('submission_progress')->default(1); - // Used in OMP only; should not be null there + // Used in OMP only; should not be null there $table->smallInteger('work_type')->default(0)->nullable(); $table->index(['context_id'], 'submissions_context_id'); $table->index(['current_publication_id'], 'submissions_publication_id'); diff --git a/classes/migration/upgrade/PKPv3_3_0UpgradeMigration.inc.php b/classes/migration/upgrade/PKPv3_3_0UpgradeMigration.inc.php index ba5935dff09..88776ea5de2 100755 --- a/classes/migration/upgrade/PKPv3_3_0UpgradeMigration.inc.php +++ b/classes/migration/upgrade/PKPv3_3_0UpgradeMigration.inc.php @@ -114,6 +114,26 @@ public function up() { $this->_populateEmailTemplates(); $this->_makeRemoteUrlLocalizable(); + + // pkp/pkp-lib#6057: Migrate locale property from publications to submissions + Capsule::schema()->table('submissions', function (Blueprint $table) { + $table->string('locale', 14)->nullable(); + }); + $currentPublicationIds = Capsule::table('submissions')->pluck('current_publication_id'); + $submissionLocales = Capsule::table('publications') + ->whereIn('publication_id', $currentPublicationIds) + ->pluck('locale', 'submission_id'); + foreach ($submissionLocales as $submissionId => $locale) { + Capsule::table('submissions as s') + ->where('s.submission_id', '=', $submissionId) + ->update(['locale' => $locale]); + } + Capsule::schema()->table('publications', function (Blueprint $table) { + $table->dropColumn('locale'); + }); + + // pkp/pkp-lib#6057 Submission files refactor + $this->_migrateSubmissionFiles(); } /** @@ -185,4 +205,235 @@ private function _makeRemoteUrlLocalizable() { $table->dropColumn('url'); }); } + + /** + * Migrate submission files after major refactor + * + * - Add files table to manage underlying file storage + * - Replace the use of file_id/revision as a unique id with a single + * auto-incrementing submission_file_id, and update all references. + * - Move revisions to a submission_file_revisons table. + * - Drop unused columns in submission_files table. + * + * @see pkp/pkp-lib#6057 + */ + private function _migrateSubmissionFiles() { + import('lib.pkp.classes.submission.SubmissionFile'); // SUBMISSION_FILE_ constants + + // Create a new table to track files in file storage + Capsule::schema()->create('files', function (Blueprint $table) { + $table->bigInteger('file_id')->autoIncrement(); + $table->string('path', 255); + }); + + // Create a new table to track submission file revisions + Capsule::schema()->create('submission_file_revisions', function (Blueprint $table) { + $table->bigInteger('revision_id')->autoIncrement(); + $table->bigInteger('submission_file_id'); + $table->bigInteger('file_id'); + }); + + // Add columns to submission_files table + Capsule::schema()->table('submission_files', function (Blueprint $table) { + $table->bigInteger('new_file_id'); // Renamed at the end of the migration + }); + + // Drop unique keys that will cause trouble while we're migrating + Capsule::schema()->table('review_round_files', function (Blueprint $table) { + $table->dropUnique('review_round_files_pkey'); + }); + + // Create entry in files and revisions tables for every submission_file + import('lib.pkp.classes.file.FileManager'); + $fileManager = new FileManager(); + $rows = Capsule::table('submission_files') + ->orderBy('file_id') + ->orderBy('revision') + ->get([ + 'file_id', + 'revision', + 'submission_id', + 'genre_id', + 'file_stage', + 'date_uploaded', + 'original_file_name' + ]); + foreach ($rows as $row) { + // Reproduces the removed method SubmissionFile::_generateFileName() + // genre is %s because it can be blank with review attachments + $filename = sprintf( + '%d-%s-%d-%d-%d-%s.%s', + $row->submission_id, + $row->genre_id, + $row->file_id, + $row->revision, + $row->file_stage, + date('Ymd', strtotime($row->date_uploaded)), + strtolower_codesafe($fileManager->parseFileExtension($row->original_file_name)) + ); + $contextId = Capsule::table('submissions')->where('submission_id', '=', $row->submission_id)->first()->context_id; + $path = sprintf( + '%s/%s/%s', + Services::get('submissionFile')->getSubmissionDir($contextId, $row->submission_id), + $this->_fileStageToPath($row->file_stage), + $filename + ); + if (!Services::get('file')->fs->has($path)) { + throw new Exception("A submission file was expected but not found at $path."); + } + $newFileId = Capsule::table('files')->insertGetId(['path' => $path]); + Capsule::table('submission_files') + ->where('file_id', $row->file_id) + ->where('revision', $row->revision) + ->update(['new_file_id' => $newFileId]); + Capsule::table('submission_file_revisions')->insert([ + 'submission_file_id' => $row->file_id, + 'file_id' => $newFileId, + ]); + + // Update revision data in event logs + $eventLogIds = Capsule::table('event_log_settings') + ->where('setting_name', '=', 'fileId') + ->where('setting_value', '=', $row->file_id) + ->pluck('log_id'); + Capsule::table('event_log_settings') + ->whereIn('log_id', $eventLogIds) + ->where('setting_name', 'fileRevision') + ->where('setting_value', '=', $row->revision) + ->update(['setting_value' => $newFileId]); + } + + // Collect rows that will be deleted because they are old revisions + // They are identified by the new_file_id column, which is the only unique + // column on the table at this point. + $newFileIdsToDelete = []; + + // Get all the unique file_ids. For each one, determine the latest revision + // in order to keep it in the table. The others will be flagged for removal + $revisionRowFileIds = Capsule::table('submission_files') + ->groupBy('file_id') + ->pluck('file_id'); + foreach ($revisionRowFileIds as $revisionRowFileId) { + $submissionFileRows = Capsule::table('submission_files') + ->where('file_id', '=', $revisionRowFileId) + ->orderBy('revision', 'desc') + ->get([ + 'file_id', + 'new_file_id', + ]); + $latestFileId = $submissionFileRows[0]->new_file_id; + foreach ($submissionFileRows as $submissionFileRow) { + if ($submissionFileRow->new_file_id !== $latestFileId) { + $newFileIdsToDelete[] = $submissionFileRow->new_file_id; + } + } + } + + // Delete the rows for old revisions + Capsule::table('submission_files') + ->whereIn('new_file_id', $newFileIdsToDelete) + ->delete(); + + // Set assoc_type and assoc_id for all review round files + // Run this before migration to internal review file stages + $rows = Capsule::table('review_round_files')->get(); + foreach ($rows as $row) { + Capsule::table('submission_files') + ->where('file_id', '=', $row->file_id) + ->whereIn('file_stage', [SUBMISSION_FILE_REVIEW_FILE, SUBMISSION_FILE_REVIEW_REVISION]) + ->update([ + 'assoc_type' => ASSOC_TYPE_REVIEW_ROUND, + 'assoc_id' => $row->review_round_id, + ]); + } + + // Update file stage for all internal review files + Capsule::table('submission_files as sf') + ->leftJoin('review_round_files as rrf', 'sf.file_id', '=', 'rrf.file_id') + ->where('sf.file_stage', '=', SUBMISSION_FILE_REVIEW_FILE) + ->where('rrf.stage_id', '=', WORKFLOW_STAGE_ID_INTERNAL_REVIEW) + ->update(['sf.file_stage' => SUBMISSION_FILE_INTERNAL_REVIEW_FILE]); + Capsule::table('submission_files as sf') + ->leftJoin('review_round_files as rrf', 'sf.file_id', '=', 'rrf.file_id') + ->where('sf.file_stage', '=', SUBMISSION_FILE_REVIEW_REVISION) + ->where('rrf.stage_id', '=', WORKFLOW_STAGE_ID_INTERNAL_REVIEW) + ->update(['sf.file_stage' => SUBMISSION_FILE_INTERNAL_REVIEW_REVISION]); + + // Update name of event log params to reflect new file structure + Capsule::table('event_log_settings') + ->where('setting_name', 'fileId') + ->update(['setting_name' => 'submissionFileId']); + Capsule::table('event_log_settings') + ->where('setting_name', 'fileRevision') + ->update(['setting_name' => 'fileId']); + + // Restructure submission_files and submission_file_settings tables + Capsule::schema()->table('submission_files', function (Blueprint $table) { + $table->renameColumn('file_id', 'submission_file_id'); + $table->renameColumn('new_file_id', 'file_id'); + $table->renameColumn('source_file_id', 'source_submission_file_id'); + $table->renameColumn('date_uploaded', 'created_at'); + $table->renameColumn('date_modified', 'updated_at'); + $table->dropColumn('revision'); + $table->dropColumn('source_revision'); + $table->dropColumn('file_size'); + $table->dropColumn('file_type'); + $table->dropColumn('original_file_name'); + $table->foreign('file_id')->references('file_id')->on('files'); + }); + Capsule::schema()->table('submission_file_settings', function (Blueprint $table) { + $table->renameColumn('file_id', 'submission_file_id'); + $table->string('setting_type', 6)->default('string')->change(); + }); + + // Update columns in related tables + Capsule::schema()->table('review_round_files', function (Blueprint $table) { + $table->renameColumn('file_id', 'submission_file_id'); + $table->dropColumn('revision'); + $table->unique(['submission_id', 'review_round_id', 'submission_file_id'], 'review_round_files_pkey'); + $table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files'); + }); + Capsule::schema()->table('review_files', function (Blueprint $table) { + $table->renameColumn('file_id', 'submission_file_id'); + $table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files'); + }); + Capsule::schema()->table('publication_galleys', function (Blueprint $table) { + $table->renameColumn('file_id', 'submission_file_id'); + $table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files'); + }); + Capsule::schema()->table('submission_file_revisions', function (Blueprint $table) { + $table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files'); + $table->foreign('file_id')->references('file_id')->on('files'); + }); + } + + /** + * Get the directory of a file based on its file stage + * + * @param int $fileStage ONe of SUBMISSION_FILE_ constants + * @return string + */ + private function _fileStageToPath($fileStage) { + import('lib.pkp.classes.submission.SubmissionFile'); + static $fileStagePathMap = [ + SUBMISSION_FILE_SUBMISSION => 'submission', + SUBMISSION_FILE_NOTE => 'note', + SUBMISSION_FILE_REVIEW_FILE => 'submission/review', + SUBMISSION_FILE_REVIEW_ATTACHMENT => 'submission/review/attachment', + SUBMISSION_FILE_REVIEW_REVISION => 'submission/review/revision', + SUBMISSION_FILE_FINAL => 'submission/final', + SUBMISSION_FILE_COPYEDIT => 'submission/copyedit', + SUBMISSION_FILE_DEPENDENT => 'submission/proof', + SUBMISSION_FILE_PROOF => 'submission/proof', + SUBMISSION_FILE_PRODUCTION_READY => 'submission/productionReady', + SUBMISSION_FILE_ATTACHMENT => 'attachment', + SUBMISSION_FILE_QUERY => 'submission/query', + ]; + + if (!isset($fileStagePathMap[$fileStage])) { + throw new Exception('A file assigned to the file stage ' . $fileStage . ' could not be migrated.'); + } + + return $fileStagePathMap[$fileStage]; + } } diff --git a/classes/notification/managerDelegate/PKPEditingProductionStatusNotificationManager.inc.php b/classes/notification/managerDelegate/PKPEditingProductionStatusNotificationManager.inc.php index 3c66a9886e9..cc164956bd0 100644 --- a/classes/notification/managerDelegate/PKPEditingProductionStatusNotificationManager.inc.php +++ b/classes/notification/managerDelegate/PKPEditingProductionStatusNotificationManager.inc.php @@ -91,9 +91,11 @@ public function updateNotification($request, $userIds, $assocType, $assocId) { $productionQueries = $queryDao->getByAssoc(ASSOC_TYPE_SUBMISSION, $submissionId, WORKFLOW_STAGE_ID_PRODUCTION); // Get the copyedited files - $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */ import('lib.pkp.classes.submission.SubmissionFile'); - $copyeditedFiles = $submissionFileDao->getLatestRevisions($submissionId, SUBMISSION_FILE_COPYEDIT); + $countCopyeditedFiles = Services::get('submissionFile')->getCount([ + 'submissionIds' => [$submissionId], + 'fileStages' => [SUBMISSION_FILE_COPYEDIT], + ]); // Get representations $representationDao = Application::getRepresentationDAO(); @@ -147,7 +149,7 @@ public function updateNotification($request, $userIds, $assocType, $assocId) { } break; case WORKFLOW_STAGE_ID_EDITING: - if (!empty($copyeditedFiles)) { + if ($countCopyeditedFiles) { // Remove 'assign a copyeditor' and 'awaiting copyedits' notification $this->_removeNotification($submissionId, $editorStageAssignment->getUserId(), $notificationType, $contextId); } else { diff --git a/classes/plugins/ImportExportPlugin.inc.php b/classes/plugins/ImportExportPlugin.inc.php index a7c184a2fb0..c114aa2477f 100644 --- a/classes/plugins/ImportExportPlugin.inc.php +++ b/classes/plugins/ImportExportPlugin.inc.php @@ -178,7 +178,7 @@ function displayXMLValidationErrors($errors, $xml) { echo '
' . htmlspecialchars($xml) . ''; echo '