_handlerPath = '_uploadPublicFile'; $roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_READER]; $this->_endpoints = [ 'OPTIONS' => [ [ 'pattern' => $this->getEndpointPattern(), 'handler' => [$this, 'getOptions'], 'roles' => $roles, ], ], 'POST' => [ [ 'pattern' => $this->getEndpointPattern(), 'handler' => [$this, 'uploadFile'], 'roles' => $roles, ], ], ]; parent::__construct(); } /** * @copydoc PKPHandler::authorize */ public function authorize($request, &$args, $roleAssignments) { $this->addPolicy(new UserRolesRequiredPolicy($request), true); $rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES); foreach ($roleAssignments as $role => $operations) { $rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations)); } $this->addPolicy($rolePolicy); return parent::authorize($request, $args, $roleAssignments); } /** * A helper method which adds the necessary response headers to allow * file uploads * * @param \PKP\core\APIResponse $response object * * @return \PKP\core\APIResponse */ private function getResponse($response) { return $response->withHeader('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With, X-PINGOTHER, X-File-Name, Cache-Control'); } /** * Upload a requested file * * @param \Slim\Http\Request $slimRequest Slim request object * @param \PKP\core\APIResponse $response object * @param array $args arguments * * @return \PKP\core\APIResponse */ public function uploadFile($slimRequest, $response, $args) { $request = $this->getRequest(); if (empty($_FILES) || empty($_FILES['file'])) { return $response->withStatus(400)->withJsonError('api.files.400.noUpload'); } $siteDir = Core::getBaseDir() . '/' . Config::getVar('files', 'public_files_dir') . '/site'; if (!file_exists($siteDir) || !is_writeable($siteDir)) { return $response->withStatus(500)->withJsonError('api.publicFiles.500.badFilesDir'); } $userDir = $siteDir . '/images/' . $request->getUser()->getUsername(); $isUserAllowed = true; $allowedDirSize = Config::getVar('files', 'public_user_dir_size', 5000) * 1024; $allowedFileTypes = ['gif', 'jpg', 'png', 'webp']; Hook::call('API::uploadPublicFile::permissions', [ &$userDir, &$isUserAllowed, &$allowedDirSize, &$allowedFileTypes, $request, $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES), ]); // Allow plugins to control who can upload files if (!$isUserAllowed) { return $response->withStatus(403)->withJsonError('api.publicFiles.403.unauthorized'); } // Don't allow user to exceed the alotted space in their public directory $currentSize = 0; if ($allowedDirSize > 0 && file_exists($userDir)) { foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($userDir, FilesystemIterator::SKIP_DOTS)) as $object) { $currentSize += $object->getSize(); } } if (($currentSize + $_FILES['file']['size']) > $allowedDirSize) { return $response->withStatus(413)->withJsonError('api.publicFiles.413.noDirSpace', [ 'fileUploadSize' => ceil($_FILES['file']['size'] / 1024), 'dirSizeLeft' => ceil(($allowedDirSize - $currentSize) / 1024), ]); } $fileManager = new FileManager(); $filename = $fileManager->getUploadedFileName('file'); $filename = trim( preg_replace( "/[^a-z0-9\.\-]+/", '', str_replace( [' ', '_', ':'], '-', strtolower($filename) ) ) ); $extension = pathinfo(strtolower(trim($filename)), PATHINFO_EXTENSION); // Only allow permitted file types if (!in_array($extension, $allowedFileTypes)) { return $response->withStatus(400)->withJsonError('api.publicFiles.400.extensionNotSupported', [ 'fileTypes' => join(__('common.commaListSeparator'), $allowedFileTypes) ]); } // Perform additional checks on images if (in_array($extension, ['gif', 'jpg', 'jpeg', 'png', 'jpe'])) { if (getimagesize($_FILES['file']['tmp_name']) === false) { return $response->withStatus(400)->withJsonError('api.publicFiles.400.invalidImage'); } $extensionFromMimeType = $fileManager->getImageExtension(PKPString::mime_content_type($_FILES['file']['tmp_name'])); if ($extensionFromMimeType !== '.' . $extension) { return $response->withStatus(400)->withJsonError('api.publicFiles.400.mimeTypeNotMatched'); } } // Save the file $destinationPath = $this->_getFilename($siteDir . '/images/' . $request->getUser()->getUsername() . '/' . $filename, $fileManager); $success = $fileManager->uploadFile('file', $destinationPath); if ($success === false) { if ($fileManager->uploadError($filename)) { switch ($fileManager->getUploadErrorCode($filename)) { 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'); } return $this->getResponse($response->withJson([ 'url' => $request->getBaseUrl() . '/' . Config::getVar('files', 'public_files_dir') . '/site/images/' . $request->getUser()->getUsername() . '/' . pathinfo($destinationPath, PATHINFO_BASENAME), ])); } /** * Respond affirmatively to a HTTP OPTIONS request with headers which allow * file uploads * * @param \Slim\Http\Request $slimRequest Slim request object * @param \PKP\core\APIResponse $response object * @param array $args arguments * * @return \PKP\core\APIResponse */ public function getOptions($slimRequest, $response, $args) { return $this->getResponse($response); } /** * A recursive function to get a filename that will not overwrite an * existing file * * @param string $path Preferred filename * @param FileManager $fileManager * * @return string */ private function _getFilename($path, $fileManager) { if ($fileManager->fileExists($path)) { $pathParts = pathinfo($path); $filename = $pathParts['filename'] . '-' . md5(microtime()) . '.' . $pathParts['extension']; if (strlen($filename > 255)) { $filename = substr($filename, -255, 255); } return $this->_getFilename($pathParts['dirname'] . '/' . $filename, $fileManager); } return $path; } }