* * @author Julius Härtl * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ namespace OCA\Deck\Service; use OC\Security\CSP\ContentSecurityPolicyManager; use OCA\Deck\Db\Attachment; use OCA\Deck\StatusException; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\EmptyContentSecurityPolicy; use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\StreamResponse; use OCP\Files\Cache\IScanner; use OCP\Files\Folder; use OCP\Files\IAppData; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFolder; use OCP\IConfig; use OCP\IL10N; use OCP\ILogger; use OCP\IRequest; class FileService implements IAttachmentService { private $l10n; private $appData; private $request; private $logger; private $rootFolder; private $config; public function __construct( IL10N $l10n, IAppData $appData, IRequest $request, ILogger $logger, IRootFolder $rootFolder, IConfig $config ) { $this->l10n = $l10n; $this->appData = $appData; $this->request = $request; $this->logger = $logger; $this->rootFolder = $rootFolder; $this->config = $config; } /** * @param Attachment $attachment * @return ISimpleFile * @throws NotFoundException * @throws NotPermittedException */ private function getFileForAttachment(Attachment $attachment) { return $this->getFolder($attachment) ->getFile($attachment->getData()); } /** * @param Attachment $attachment * @return ISimpleFolder * @throws NotPermittedException */ private function getFolder(Attachment $attachment) { $folderName = 'file-card-' . (int)$attachment->getCardId(); try { $folder = $this->appData->getFolder($folderName); } catch (NotFoundException $e) { $folder = $this->appData->newFolder($folderName); } return $folder; } public function extendData(Attachment $attachment) { try { $file = $this->getFileForAttachment($attachment); } catch (NotFoundException $e) { $this->logger->info('Extending data for file attachment failed'); return $attachment; } catch (NotPermittedException $e) { $this->logger->info('Extending data for file attachment failed'); return $attachment; } $attachment->setExtendedData([ 'filesize' => $file->getSize(), 'mimetype' => $file->getMimeType(), 'info' => pathinfo($file->getName()) ]); return $attachment; } /** * @return array * @throws StatusException */ private function getUploadedFile () { $file = $this->request->getUploadedFile('file'); $error = null; $phpFileUploadErrors = [ UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'), UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'), UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'), UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'), UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'), UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'), UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'), UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'), ]; if (empty($file)) { $error = $this->l10n->t('No file uploaded or file size exceeds maximum of %s', [\OCP\Util::humanFileSize(\OCP\Util::uploadLimit())]); } if (!empty($file) && array_key_exists('error', $file) && $file['error'] !== UPLOAD_ERR_OK) { $error = $phpFileUploadErrors[$file['error']]; } if ($error !== null) { throw new StatusException($error); } return $file; } /** * @param Attachment $attachment * @throws NotPermittedException * @throws StatusException */ public function create(Attachment $attachment) { $file = $this->getUploadedFile(); $folder = $this->getFolder($attachment); $fileName = $file['name']; if ($folder->fileExists($fileName)) { throw new StatusException('File already exists.'); } $target = $folder->newFile($fileName); $content = fopen($file['tmp_name'], 'rb'); if ($content === false) { throw new StatusException('Could not read file'); } $target->putContent($content); if (is_resource($content)) { fclose($content); } $attachment->setData($fileName); } /** * This method requires to be used with POST so we can properly get the form data * * @throws \Exception */ public function update(Attachment $attachment) { $file = $this->getUploadedFile(); $fileName = $file['name']; $attachment->setData($fileName); $target = $this->getFileForAttachment($attachment); $content = fopen($file['tmp_name'], 'rb'); if ($content === false) { throw new StatusException('Could not read file'); } $target->putContent($content); if (is_resource($content)) { fclose($content); } $attachment->setLastModified(time()); } /** * @param Attachment $attachment * @throws NotPermittedException */ public function delete(Attachment $attachment) { try { $file = $this->getFileForAttachment($attachment); $file->delete(); } catch (NotFoundException $e) { } } /** * Workaround until ISimpleFile can be fetched as a resource * * @throws \Exception */ private function getFileFromRootFolder(Attachment $attachment) { $folderName = 'file-card-' . (int)$attachment->getCardId(); $instanceId = $this->config->getSystemValue('instanceid', null); if ($instanceId === null) { throw new \Exception('no instance id!'); } $name = 'appdata_' . $instanceId; $appDataFolder = $this->rootFolder->get($name); $appDataFolder = $appDataFolder->get('deck'); $cardFolder = $appDataFolder->get($folderName); return $cardFolder->get($attachment->getData()); } /** * @param Attachment $attachment * @return FileDisplayResponse|\OCP\AppFramework\Http\Response|StreamResponse * @throws \Exception */ public function display(Attachment $attachment) { $file = $this->getFileFromRootFolder($attachment); if (method_exists($file, 'fopen')) { $response = new StreamResponse($file->fopen('r')); $response->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"'); } else { $response = new FileDisplayResponse($file); } if ($file->getMimeType() === 'application/pdf') { // We need those since otherwise chrome won't show the PDF file with CSP rule object-src 'none' // https://bugs.chromium.org/p/chromium/issues/detail?id=271452 $policy = new ContentSecurityPolicy(); $policy->addAllowedObjectDomain('\'self\''); $policy->addAllowedObjectDomain('blob:'); $response->setContentSecurityPolicy($policy); } $response->addHeader('Content-Type', $file->getMimeType()); return $response; } /** * Should undo be allowed and the delete action be done by a background job * * @return bool */ public function allowUndo() { return true; } /** * Mark an attachment as deleted * * @param Attachment $attachment */ public function markAsDeleted(Attachment $attachment) { $attachment->setDeletedAt(time()); } }