aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav')
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php5
-rw-r--r--apps/dav/composer/composer/autoload_static.php5
-rw-r--r--apps/dav/lib/BulkUpload/BulkUploadPlugin.php100
-rw-r--r--apps/dav/lib/BulkUpload/MultipartRequestParser.php239
-rw-r--r--apps/dav/lib/BundleUpload/BundledFile.php214
-rw-r--r--apps/dav/lib/BundleUpload/BundlingPlugin.php456
-rw-r--r--apps/dav/lib/BundleUpload/MultipartContentsParser.php497
-rw-r--r--apps/dav/lib/Capabilities.php2
-rw-r--r--apps/dav/lib/Connector/Sabre/File.php8
-rw-r--r--apps/dav/lib/Server.php8
-rwxr-xr-xapps/dav/tests/benchmarks/benchmark.sh (renamed from apps/dav/tests/temporary/benchmark.sh)10
-rwxr-xr-xapps/dav/tests/benchmarks/bulk_upload.sh (renamed from apps/dav/tests/temporary/bundle_upload.sh)22
-rwxr-xr-xapps/dav/tests/benchmarks/single_upload.sh (renamed from apps/dav/tests/temporary/single_upload.sh)13
-rw-r--r--apps/dav/tests/unit/CapabilitiesTest.php1
-rw-r--r--apps/dav/tests/unit/Files/BundlePluginTest.php711
-rw-r--r--apps/dav/tests/unit/Files/BundledFileTest.php220
-rw-r--r--apps/dav/tests/unit/Files/MultipartContentsParserTest.php416
-rw-r--r--apps/dav/tests/unit/Files/MultipartRequestParserTest.php281
18 files changed, 665 insertions, 2543 deletions
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index e7e29d85d89..1a536c98272 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -22,9 +22,8 @@ return array(
'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => $baseDir . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php',
'OCA\\DAV\\BackgroundJob\\UpdateCalendarResourcesRoomsBackgroundJob' => $baseDir . '/../lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php',
'OCA\\DAV\\BackgroundJob\\UploadCleanup' => $baseDir . '/../lib/BackgroundJob/UploadCleanup.php',
- 'OCA\\DAV\\BundleUpload\\BundledFile' => $baseDir . '/../lib/BundleUpload/BundledFile.php',
- 'OCA\\DAV\\BundleUpload\\BundlingPlugin' => $baseDir . '/../lib/BundleUpload/BundlingPlugin.php',
- 'OCA\\DAV\\BundleUpload\\MultipartContentsParser' => $baseDir . '/../lib/BundleUpload/MultipartContentsParser.php',
+ 'OCA\\DAV\\BulkUpload\\BulkUploadPlugin' => $baseDir . '/../lib/BulkUpload/BulkUploadPlugin.php',
+ 'OCA\\DAV\\BulkUpload\\MultipartRequestParser' => $baseDir . '/../lib/BulkUpload/MultipartRequestParser.php',
'OCA\\DAV\\CalDAV\\Activity\\Backend' => $baseDir . '/../lib/CalDAV/Activity/Backend.php',
'OCA\\DAV\\CalDAV\\Activity\\Filter\\Calendar' => $baseDir . '/../lib/CalDAV/Activity/Filter/Calendar.php',
'OCA\\DAV\\CalDAV\\Activity\\Filter\\Todo' => $baseDir . '/../lib/CalDAV/Activity/Filter/Todo.php',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index ee23085eee5..b65d4477800 100644
--- a/apps/dav/composer/composer/autoload_static.php
+++ b/apps/dav/composer/composer/autoload_static.php
@@ -37,9 +37,8 @@ class ComposerStaticInitDAV
'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => __DIR__ . '/..' . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php',
'OCA\\DAV\\BackgroundJob\\UpdateCalendarResourcesRoomsBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php',
'OCA\\DAV\\BackgroundJob\\UploadCleanup' => __DIR__ . '/..' . '/../lib/BackgroundJob/UploadCleanup.php',
- 'OCA\\DAV\\BundleUpload\\BundledFile' => __DIR__ . '/..' . '/../lib/BundleUpload/BundledFile.php',
- 'OCA\\DAV\\BundleUpload\\BundlingPlugin' => __DIR__ . '/..' . '/../lib/BundleUpload/BundlingPlugin.php',
- 'OCA\\DAV\\BundleUpload\\MultipartContentsParser' => __DIR__ . '/..' . '/../lib/BundleUpload/MultipartContentsParser.php',
+ 'OCA\\DAV\\BulkUpload\\BulkUploadPlugin' => __DIR__ . '/..' . '/../lib/BulkUpload/BulkUploadPlugin.php',
+ 'OCA\\DAV\\BulkUpload\\MultipartRequestParser' => __DIR__ . '/..' . '/../lib/BulkUpload/MultipartRequestParser.php',
'OCA\\DAV\\CalDAV\\Activity\\Backend' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Backend.php',
'OCA\\DAV\\CalDAV\\Activity\\Filter\\Calendar' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Filter/Calendar.php',
'OCA\\DAV\\CalDAV\\Activity\\Filter\\Todo' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Filter/Todo.php',
diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
new file mode 100644
index 00000000000..0766ae37a17
--- /dev/null
+++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\BulkUpload;
+
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use OCP\Files\Folder;
+use OCP\AppFramework\Http;
+
+class BulkUploadPlugin extends ServerPlugin {
+
+ /** @var Folder */
+ private $userFolder;
+
+ /** @var LoggerInterface */
+ private $logger;
+
+ public function __construct(Folder $userFolder, LoggerInterface $logger) {
+ $this->userFolder = $userFolder;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Register listener on POST requests with the httpPost method.
+ */
+ public function initialize(Server $server): void {
+ $server->on('method:POST', [$this, 'httpPost'], 10);
+ }
+
+ /**
+ * Handle POST requests on /dav/bulk
+ * - parsing is done with a MultipartContentsParser object
+ * - writing is done with the userFolder service
+ *
+ * Will respond with an object containing an ETag for every written files.
+ */
+ public function httpPost(RequestInterface $request, ResponseInterface $response): bool {
+ // Limit bulk upload to the /dav/bulk endpoint
+ if ($request->getPath() !== "bulk") {
+ return true;
+ }
+
+ $multiPartParser = new MultipartRequestParser($request);
+ $writtenFiles = [];
+
+ while (!$multiPartParser->isAtLastBoundary()) {
+ try {
+ [$headers, $content] = $multiPartParser->parseNextPart();
+ } catch (\Exception $e) {
+ // Return early if an error occurs during parsing.
+ $this->logger->error($e->getMessage());
+ $response->setStatus(Http::STATUS_BAD_REQUEST);
+ $response->setBody(json_encode($writtenFiles));
+ return false;
+ }
+
+ try {
+ $node = $this->userFolder->newFile($headers['x-file-path'], $content);
+ $writtenFiles[$headers['x-file-path']] = [
+ "error" => false,
+ "etag" => $node->getETag(),
+ ];
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['path' => $headers['x-file-path']]);
+ $writtenFiles[$headers['x-file-path']] = [
+ "error" => true,
+ "message" => $e->getMessage(),
+ ];
+ }
+ }
+
+ $response->setStatus(Http::STATUS_OK);
+ $response->setBody(json_encode($writtenFiles));
+
+ return false;
+ }
+}
diff --git a/apps/dav/lib/BulkUpload/MultipartRequestParser.php b/apps/dav/lib/BulkUpload/MultipartRequestParser.php
new file mode 100644
index 00000000000..7554447fc93
--- /dev/null
+++ b/apps/dav/lib/BulkUpload/MultipartRequestParser.php
@@ -0,0 +1,239 @@
+<?php
+/**
+ * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\BulkUpload;
+
+use Sabre\HTTP\RequestInterface;
+use Sabre\DAV\Exception;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\Exception\LengthRequired;
+use OCP\AppFramework\Http;
+
+class MultipartRequestParser {
+
+ /** @var resource */
+ private $stream;
+
+ /** @var string */
+ private $boundary = "";
+
+ /** @var string */
+ private $lastBoundary = "";
+
+ /**
+ * @throws BadRequest
+ */
+ public function __construct(RequestInterface $request) {
+ $stream = $request->getBody();
+ $contentType = $request->getHeader('Content-Type');
+
+ if (!is_resource($stream)) {
+ throw new BadRequest('Body should be of type resource');
+ }
+
+ if ($contentType === null) {
+ throw new BadRequest("Content-Type can not be null");
+ }
+
+ $this->stream = $stream;
+
+ $boundary = $this->parseBoundaryFromHeaders($contentType);
+ $this->boundary = '--'.$boundary."\r\n";
+ $this->lastBoundary = '--'.$boundary."--\r\n";
+ }
+
+ /**
+ * Parse the boundary from the Content-Type header.
+ * Example: Content-Type: "multipart/related; boundary=boundary_bf38b9b4b10a303a28ed075624db3978"
+ *
+ * @throws BadRequest
+ */
+ private function parseBoundaryFromHeaders(string $contentType): string {
+ try {
+ [$mimeType, $boundary] = explode(';', $contentType);
+ [$boundaryKey, $boundaryValue] = explode('=', $boundary);
+ } catch (\Exception $e) {
+ throw new BadRequest("Error while parsing boundary in Content-Type header.", Http::STATUS_BAD_REQUEST, $e);
+ }
+
+ $boundaryValue = trim($boundaryValue);
+
+ // Remove potential quotes around boundary value.
+ if (substr($boundaryValue, 0, 1) == '"' && substr($boundaryValue, -1) == '"') {
+ $boundaryValue = substr($boundaryValue, 1, -1);
+ }
+
+ if (trim($mimeType) !== 'multipart/related') {
+ throw new BadRequest('Content-Type must be multipart/related');
+ }
+
+ if (trim($boundaryKey) !== 'boundary') {
+ throw new BadRequest('Boundary is invalid');
+ }
+
+ return $boundaryValue;
+ }
+
+ /**
+ * Check whether the stream's cursor is sitting right before the provided string.
+ *
+ * @throws Exception
+ */
+ private function isAt(string $expectedContent): bool {
+ $expectedContentLength = strlen($expectedContent);
+
+ $content = fread($this->stream, $expectedContentLength);
+ if ($content === false) {
+ throw new Exception('An error occurred while checking content');
+ }
+
+ $seekBackResult = fseek($this->stream, -$expectedContentLength, SEEK_CUR);
+ if ($seekBackResult === -1) {
+ throw new Exception("Unknown error while seeking content", Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ return $expectedContent === $content;
+ }
+
+
+ /**
+ * Check whether the stream's cursor is sitting right before the boundary.
+ */
+ private function isAtBoundary(): bool {
+ return $this->isAt($this->boundary);
+ }
+
+ /**
+ * Check whether the stream's cursor is sitting right before the last boundary.
+ */
+ public function isAtLastBoundary(): bool {
+ return $this->isAt($this->lastBoundary);
+ }
+
+ /**
+ * Parse and return the next part of the multipart headers.
+ *
+ * Example:
+ * --boundary_azertyuiop
+ * Header1: value
+ * Header2: value
+ *
+ * Content of
+ * the part
+ *
+ */
+ public function parseNextPart(): array {
+ $this->readBoundary();
+
+ $headers = $this->readPartHeaders();
+
+ $content = $this->readPartContent($headers["content-length"], $headers["x-file-md5"]);
+
+ return [$headers, $content];
+ }
+
+ /**
+ * Read the boundary and check its content.
+ *
+ * @throws BadRequest
+ */
+ private function readBoundary(): string {
+ if (!$this->isAtBoundary()) {
+ throw new BadRequest("Boundary not found where it should be.");
+ }
+
+ return fread($this->stream, strlen($this->boundary));
+ }
+
+ /**
+ * Return the headers of a part of the multipart body.
+ *
+ * @throws Exception
+ * @throws BadRequest
+ * @throws LengthRequired
+ */
+ private function readPartHeaders(): array {
+ $headers = [];
+
+ while (($line = fgets($this->stream)) !== "\r\n") {
+ if ($line === false) {
+ throw new Exception('An error occurred while reading headers of a part');
+ }
+
+ try {
+ [$key, $value] = explode(':', $line, 2);
+ $headers[strtolower(trim($key))] = trim($value);
+ } catch (\Exception $e) {
+ throw new BadRequest('An error occurred while parsing headers of a part', Http::STATUS_BAD_REQUEST, $e);
+ }
+ }
+
+ if (!isset($headers["content-length"])) {
+ throw new LengthRequired("The Content-Length header must not be null.");
+ }
+
+ if (!isset($headers["x-file-md5"])) {
+ throw new BadRequest("The X-File-MD5 header must not be null.");
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Return the content of a part of the multipart body.
+ *
+ * @throws Exception
+ * @throws BadRequest
+ */
+ private function readPartContent(int $length, string $md5): string {
+ $computedMd5 = $this->computeMd5Hash($length);
+
+ if ($md5 !== $computedMd5) {
+ throw new BadRequest("Computed md5 hash is incorrect.");
+ }
+
+ $content = stream_get_line($this->stream, $length);
+
+ if ($content === false) {
+ throw new Exception("Fail to read part's content.");
+ }
+
+ if (feof($this->stream)) {
+ throw new Exception("Unexpected EOF while reading stream.");
+ }
+
+ // Read '\r\n'.
+ stream_get_contents($this->stream, 2);
+
+ return $content;
+ }
+
+ /**
+ * Compute the MD5 hash of the next x bytes.
+ */
+ private function computeMd5Hash(int $length): string {
+ $context = hash_init('md5');
+ hash_update_stream($context, $this->stream, $length);
+ fseek($this->stream, -$length, SEEK_CUR);
+ return hash_final($context);
+ }
+}
diff --git a/apps/dav/lib/BundleUpload/BundledFile.php b/apps/dav/lib/BundleUpload/BundledFile.php
deleted file mode 100644
index db9b5bbd3fe..00000000000
--- a/apps/dav/lib/BundleUpload/BundledFile.php
+++ /dev/null
@@ -1,214 +0,0 @@
-<?php
-/**
- * @author Piotr Mrowczynski <Piotr.Mrowczynski@owncloud.com>
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\BundleUpload;
-
-use OCA\DAV\Connector\Sabre\Exception\FileLocked;
-use OCP\Files\StorageNotAvailableException;
-use OCP\Lock\ILockingProvider;
-use OCP\Lock\LockedException;
-use Sabre\DAV\Exception;
-use Sabre\DAV\Exception\Forbidden;
-use Sabre\DAV\Exception\ServiceUnavailable;
-use OCA\DAV\Connector\Sabre\File;
-use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge;
-use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException;
-use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType;
-use OCP\Files\ForbiddenException;
-use Sabre\DAV\Exception\BadRequest;
-
-class BundledFile extends File {
-
-
- /**
- * This class is a wrapper around the bundled request body and provides access to its contents
- *
- * @var \OCA\DAV\BundleUpload\MultipartContentsParser
- *
- */
- private $contentHandler;
-
- public function __construct($view, $info, $contentHandler){
- $this->contentHandler = $contentHandler;
- parent::__construct($view, $info);
- }
- /**
- * Updates the data
- *
- * The $data['data] argument is a readable stream resource.
- * The other $data key-values should be header fields in form of string
- *
- * After a successful put operation, you may choose to return an ETag. The
- * ETag must always be surrounded by double-quotes. These quotes must
- * appear in the actual string you're returning.
- *
- * Clients may use the ETag from a PUT request to later on make sure that
- * when they update the file, the contents haven't changed in the mean
- * time.
- *
- * If you don't plan to store the file byte-by-byte, and you return a
- * different object on a subsequent GET you are strongly recommended to not
- * return an ETag, and just return null.
- *
- * @param array $data
- *
- * @throws Forbidden
- * @throws UnsupportedMediaType
- * @throws BadRequest
- * @throws Exception
- * @throws EntityTooLarge
- * @throws ServiceUnavailable
- * @throws FileLocked
- * @return array $properties
- */
- public function putFile($data) {
- $properties = array();
-
- if (!isset($data['oc-total-length'])) {
- //this should not happen, since upper layer takes care of that
- //Thus, return Forbidden as sign of code inconsistency
- throw new Forbidden('File requires oc-total-length header to be read');
- }
-
- try {
- $exists = $this->fileView->file_exists($this->path);
- if ($this->info && $exists) {
- $this->contentHandler->multipartContentSeekToContentLength($data['oc-total-length']);
- throw new Forbidden('Bundling not supported for already existing files');
- }
- } catch (StorageNotAvailableException $e) {
- $this->contentHandler->multipartContentSeekToContentLength($data['oc-total-length']);
- throw new ServiceUnavailable("StorageNotAvailableException raised");
- }
-
- // verify path of the target
- $this->verifyPath();
-
- $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand();
-
- // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
- /** @var \OC\Files\Storage\Storage $partStorage */
- list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath);
- /** @var \OC\Files\Storage\Storage $storage */
- list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
- try {
- $target = $partStorage->fopen($internalPartPath, 'wb');
- if ($target === false || $target === null) {
- \OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::fopen() failed', \OCP\Util::ERROR);
- // because we have no clue about the cause we can only throw back a 500/Internal Server Error
- $this->contentHandler->multipartContentSeekToContentLength($data['oc-total-length']);
- throw new Exception('Could not write file contents');
- }
-
- $result = $this->contentHandler->streamReadToStream($target, $data['oc-total-length']);
-
- if ($result === false) {
- throw new Exception('Error while copying file to target location (expected filesize: ' . $data['oc-total-length'] . ' )');
- }
-
- } catch (\Exception $e) {
- $partStorage->unlink($internalPartPath);
- $this->convertToSabreException($e);
- }
-
- try {
- $view = \OC\Files\Filesystem::getView();
- if ($view) {
- $run = $this->emitPreHooks($exists);
- } else {
- $run = true;
- }
-
- try {
- $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
- } catch (LockedException $e) {
- $partStorage->unlink($internalPartPath);
- throw new FileLocked($e->getMessage(), $e->getCode(), $e);
- }
-
- try {
- if ($run) {
- $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
- $fileExists = $storage->file_exists($internalPath);
- }
- if (!$run || $renameOkay === false || $fileExists === false) {
- \OCP\Util::writeLog('webdav', 'renaming part file to final file failed', \OCP\Util::ERROR);
- throw new Exception('Could not rename part file to final file');
- }
- } catch (ForbiddenException $ex) {
- throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
- } catch (\Exception $e) {
- $partStorage->unlink($internalPartPath);
- $this->convertToSabreException($e);
- }
-
- // since we skipped the view we need to scan and emit the hooks ourselves
- $storage->getUpdater()->update($internalPath);
-
- try {
- $this->changeLock(ILockingProvider::LOCK_SHARED);
- } catch (LockedException $e) {
- throw new FileLocked($e->getMessage(), $e->getCode(), $e);
- }
-
- if ($view) {
- $this->emitPostHooks($exists);
- }
-
- // allow sync clients to send the mtime along in a header
- if (isset($data['oc-mtime'])) {
- if ($this->fileView->touch($this->path, $data['oc-mtime'])) {
- $properties['{DAV:}oc-mtime'] = 'accepted';
- }
- }
-
- $this->refreshInfo();
-
- if (isset($data['oc-checksum'])) {
- $checksum = trim($data['oc-checksum']);
- $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
- $this->refreshInfo();
- } else if ($this->getChecksum() !== null && $this->getChecksum() !== '') {
- $this->fileView->putFileInfo($this->path, ['checksum' => '']);
- $this->refreshInfo();
- }
-
- } catch (StorageNotAvailableException $e) {
- throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage());
- }
-
- $etag = $this->getEtag();
- $properties['{DAV:}etag'] = $etag;
- $properties['{DAV:}oc-etag'] = $etag;
- $properties['{DAV:}oc-fileid'] = $this->getFileId();
- return $properties;
- }
-
- /*
- * @param resource $data
- *
- * @throws Forbidden
- */
- public function put($data) {
- throw new Forbidden('PUT method not supported for bundling');
- }
-} \ No newline at end of file
diff --git a/apps/dav/lib/BundleUpload/BundlingPlugin.php b/apps/dav/lib/BundleUpload/BundlingPlugin.php
deleted file mode 100644
index b3c7a007ac2..00000000000
--- a/apps/dav/lib/BundleUpload/BundlingPlugin.php
+++ /dev/null
@@ -1,456 +0,0 @@
-<?php
-/**
- * @author Piotr Mrowczynski <Piotr.Mrowczynski@owncloud.com>
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\BundleUpload;
-
-use Sabre\DAV\ServerPlugin;
-use Sabre\HTTP\RequestInterface;
-use Sabre\HTTP\ResponseInterface;
-use OC\Files\View;
-use Sabre\HTTP\URLUtil;
-use OCP\Lock\ILockingProvider;
-use OC\Files\FileInfo;
-use Sabre\DAV\Exception\BadRequest;
-use OCA\DAV\Connector\Sabre\Exception\Forbidden;
-use OCP\Files\Folder;
-use OCP\AppFramework\Http\JSONResponse;
-use Psr\Log\LoggerInterface;
-
-/**
- * This plugin is responsible for interconnecting three components of the OC server:
- * - RequestInterface object handler for request incoming from the client
- * - MultipartContentsParser responsible for reading the contents of the request body
- * - BundledFile responsible for storage of the file associated with request in the OC server
- *
- * Bundling plugin is responsible for receiving, validation and processing of the multipart/related request containing files.
- *
- */
-class BundlingPlugin extends ServerPlugin {
- /**
- * Reference to main server object
- *
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var \Sabre\HTTP\RequestInterface
- */
- private $request;
-
- /**
- * @var \Sabre\HTTP\ResponseInterface
- */
- private $response;
-
- /**
- * @var \OCA\DAV\FilesBundle
- */
- private $contentHandler = null;
-
- /**
- * @var String
- */
- private $userFilesHome = null;
-
- /**
- * @var View
- */
- private $fileView;
-
- /**
- * @var Array
- */
- // private $cacheValidParents = null;
-
- /** @var IFolder */
- private $userFolder;
-
- /** @var LoggerInterface */
- private $logger;
-
- /**
- * Plugin constructor
- */
- public function __construct(View $view, Folder $userFolder) {
- $this->fileView = $view;
- $this->userFolder = $userFolder;
- }
-
- /**
- * This initializes the plugin.
- *
- * This function is called by \Sabre\DAV\Server, after
- * addPlugin is called.
- *
- * This method should set up the requires event subscriptions.
- *
- * @param \Sabre\DAV\Server $server
- * @return void
- */
- public function initialize(\Sabre\DAV\Server $server) {
- $this->server = $server;
- $this->logger = $this->server->getLogger();
-
- $server->on('method:POST', array($this, 'handleBundle'));
- }
-
- /**
- * We intercept this to handle method:POST on a dav resource and process the bundled files multipart HTTP request.
- *
- * @throws /Sabre\DAV\Exception\BadRequest
- * @throws /Sabre\DAV\Exception\Forbidden
- */
- public function handleBundle(RequestInterface $request, ResponseInterface $response) {
- // Limit bundle upload to the /bundle endpoint
- if ($request->getPath() !== "files/bundle") {
- return true;
- }
-
- $multiPartParser = new MultipartContentsParser($request);
- $writtenFiles = [];
-
- // $multiPartParser->eof()
- while (!$multiPartParser->lastBoundary()) {
- try {
- [$headers, $content] = $multiPartParser->readNextPart();
-
- if ((int)$headers['content-length'] !== strlen($content)) {
- throw new BadRequest("Content read with different size than declared. Got " . $headers['content-length'] . ", expected" . strlen($content));
- }
-
- $node = $this->userFolder->newFile($headers['x-file-path'], $content);
- $writtenFiles[$headers['x-file-path']] = $node->getSize();
-
- if ((int)$headers['content-length'] !== $node->getSize()) {
- throw new BadRequest("Written file length is different than declared length. Got " . $headers['content-length'] . ", expected" . $node->getSize());
- }
-
- // TODO - check md5 hash
- // $context = hash_init('md5');
- // hash_update_stream($context, $stream);
- // echo hash_final($context);
- // if ($header['x-file-md5'] !== hash_final($context)) {
- // }
- } catch (\Exception $e) {
- throw $e;
- $this->logger->error($e->getMessage(), ['path' => $header['x-file-path']]);
- }
- }
-
- $response->setStatus(200);
- $response->setBody(new JSONResponse([
- $writtenFiles
- ]));
-
- return false;
-
- // $this->contentHandler = $this->getContentHandler($this->request);
-
- // $multipleRequestsData = $this->parseBundleMetadata();
-
- //Process bundle and send a multi-status response
- // $result = $this->processBundle($multipleRequestsData);
-
- // return $result;
- }
-
- public function handleBundleWithMetadata(RequestInterface $request, ResponseInterface $response) {
- // Limit bundle upload to the /bundle endpoint
- if ($request->getPath() !== "files/bundle") {
- return true;
- }
-
- $multiPartParser = new MultipartContentsParser($request);
-
- [$metadataHeaders, $rawMetadata] = $multiPartParser->getMetadata();
-
- if ($metadataHeaders['content-type'] !== "text/xml; charset=utf-8") {
- throw new BadRequest("Incorrect Content-Type for metadata.");
- }
-
- if ((int)$metadataHeaders['content-length'] !== strlen($rawMetadata)) {
- throw new BadRequest("Content read with different size than declared.");
- }
-
- $metadata = $this->parseMetadata($rawMetadata);
-
- $writtenFiles = [];
-
- foreach ($metadata as $fileMetadata) {
- try {
- [$headers, $content] = $multiPartParser->readNextPart((int)$fileMetadata['oc-total-length']);
-
- if ($fileMetadata['oc-id'] !== $headers['content-id']) {
- throw new BadRequest("Content-ID do not match oc-id. Check the order of your metadata.");
- }
-
- if (isset($file[$fileMetadata['oc-id']])) {
- throw new BadRequest("Content-ID appear twice. Check the order of your metadata.");
- }
-
- if ((int)$fileMetadata['oc-total-length'] !== strlen($content)) {
- throw new BadRequest("Content read with different size than declared.");
- }
-
- $node = $this->userFolder->newFile($fileMetadata['oc-path'], $content);
- $writtenFiles[$fileMetadata['oc-id']] = $node->getSize();
-
- // TODO - check md5 hash
- // $context = hash_init('md5');
- // hash_update_stream($context, $stream);
- // echo hash_final($context);
- if ($fileMetadata['oc-md5'] !== hash_final($context)) {
-
- }
- } catch (\Exception $e) {
- throw $e;
- $this->logger->error($e->getMessage(), ['path' => $fileMetadata['oc-path']]);
- }
- }
-
- $response->setStatus(200);
- $response->setBody(new JSONResponse([
- $writtenFiles
- ]));
-
- return false;
-
- // $this->contentHandler = $this->getContentHandler($this->request);
-
- // $multipleRequestsData = $this->parseBundleMetadata();
-
- //Process bundle and send a multi-status response
- // $result = $this->processBundle($multipleRequestsData);
-
- // return $result;
- }
-
- private function parseMetadata(string $rawMetadata) {
- $xml = simplexml_load_string($rawMetadata);
- if ($xml === false) {
- $error = libxml_get_errors();
- throw new \Exception('Bundle metadata contains incorrect xml structure. Unable to parse whole bundle request', $error);
- }
-
- libxml_clear_errors();
-
- $xml->registerXPathNamespace('d','urn:DAV');
-
- $metadataXml = $xml->xpath('/d:multipart/d:part/d:prop');
-
- if($metadataXml === false){
- throw new \Exception('Fail to access d:multipart/d:part/d:prop elements');
- }
-
- return array_map(function($xmlObject) { return get_object_vars($xmlObject->children('d', TRUE));}, $metadataXml);
- }
-
- /**
- * Parses multipart contents and send appropriate response
- *
- * @throws \Sabre\DAV\Exception\Forbidden
- *
- * @return array $multipleRequestsData
- */
- private function parseBundleMetadata() {
- $multipleRequestsData = array();
- try {
- // Verify metadata part headers
- $bundleMetadata = null;
- try{
- $bundleMetadata = $this->contentHandler->getPartHeaders($this->boundary);
- }
- catch (\Exception $e) {
- throw new \Exception($e->getMessage());
- }
- $contentParts = explode(';', $bundleMetadata['content-type']);
- if (count($contentParts) != 2) {
- throw new \Exception('Incorrect Content-type format. Charset might be missing');
- }
- $contentType = trim($contentParts[0]);
- $expectedContentType = 'text/xml';
- if ($contentType != $expectedContentType) {
- throw new BadRequest(sprintf(
- 'Content-Type must be %s',
- $expectedContentType
- ));
- }
- if (!isset($bundleMetadata['content-length'])) {
- throw new \Exception('Bundle metadata header does not contain Content-Length. Unable to parse whole bundle request');
- }
-
- // Read metadata part headers
- $bundleMetadataBody = $this->contentHandler->streamReadToString($bundleMetadata['content-length']);
-
- $bundleMetadataBody = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/","xmlns\\1=\"urn:DAV\"",$bundleMetadataBody);
-
- //Try to load xml
- $xml = simplexml_load_string($bundleMetadataBody);
- if (false === $xml) {
- $mlerror = libxml_get_errors();
- throw new \Exception('Bundle metadata contains incorrect xml structure. Unable to parse whole bundle request');
- }
- $xml->registerXPathNamespace('d','urn:DAV');
- unset($bundleMetadataBody);
-
- if(1 != count($xml->xpath('/d:multipart'))){
- throw new \Exception('Bundle metadata does not contain d:multipart children elements');
- }
-
- $fileMetadataObjectXML = $xml->xpath('/d:multipart/d:part/d:prop');
-
- if(0 == count($fileMetadataObjectXML)){
- throw new \Exception('Bundle metadata does not contain d:multipart/d:part/d:prop children elements');
- }
-
- foreach ($fileMetadataObjectXML as $prop) {
- $fileMetadata = get_object_vars($prop->children('d', TRUE));
-
- // if any of the field is not contained,
- // bthe try-catch clausule will raise Undefined index exception
- $contentID = intval($fileMetadata['oc-id']);
- if(array_key_exists($contentID, $multipleRequestsData)){
- throw new \Exception('One or more files have the same Content-ID '.$contentID.'. Unable to parse whole bundle request');
- }
- $multipleRequestsData[$contentID]['oc-path'] = $fileMetadata['oc-path'];
- $multipleRequestsData[$contentID]['oc-mtime'] = $fileMetadata['oc-mtime'];
- $multipleRequestsData[$contentID]['oc-total-length'] = intval($fileMetadata['oc-total-length']);
- $multipleRequestsData[$contentID]['response'] = null;
- }
- } catch (\Exception $e) {
- libxml_clear_errors();
- throw new Forbidden($e->getMessage());
- }
- return $multipleRequestsData;
- }
-
- /**
- * Process multipart contents and send appropriate response
- *
- * @param RequestInterface $request
- *
- * @return boolean
- */
- private function processBundle($multipleRequestsData) {
- $bundleResponseProperties = array();
-
- while(!$this->contentHandler->getEndDelimiterReached()) {
- // Verify metadata part headers
- $fileContentHeader = null;
-
- //If something fails at this point, just continue, $multipleRequestsData[$contentID]['response'] will be null for this content
- try{
- $fileContentHeader = $this->contentHandler->getPartHeaders($this->boundary);
- if(is_null($fileContentHeader) || !isset($fileContentHeader['content-id']) || !array_key_exists(intval($fileContentHeader['content-id']), $multipleRequestsData)){
- continue;
- }
- }
- catch (\Exception $e) {
- continue;
- }
-
- $fileID = intval($fileContentHeader['content-id']);
- $fileMetadata = $multipleRequestsData[$fileID];
-
- $filePath = $fileMetadata['oc-path'];
-
- list($folderPath, $fileName) = \OC\URLUtil::splitPath($filePath);
-
- try {
- //get absolute path of the file
- $absoluteFilePath = $this->fileView->getAbsolutePath($folderPath) . '/' . $fileName;
- $info = new FileInfo($absoluteFilePath, null, null, array(), null);
- $node = new BundledFile($this->fileView, $info, $this->contentHandler);
- $node->acquireLock(ILockingProvider::LOCK_SHARED);
- $properties = $node->putFile($fileMetadata);
- $multipleRequestsData[$fileID]['response'] = $this->handleFileMultiStatus($filePath, $properties);
- } catch (\Exception $exc) {
- //TODO: This should not be BadRequest! This should be any exception - how to do it carefully?
- $exc = new BadRequest($exc->getMessage());
- $multipleRequestsData[$fileID]['response'] = $this->handleFileMultiStatusError($filePath, $exc);
- continue;
- }
-
- //TODO: do we need to unlock file if putFile failed? In this version we dont (does continue)
- //release lock as in dav/lib/Connector/Sabre/LockPlugin.php
- $node->releaseLock(ILockingProvider::LOCK_SHARED);
- $this->server->tree->markDirty($filePath);
- }
-
- foreach($multipleRequestsData as $requestData) {
- $response = $requestData['response'];
- if (is_null($response)){
- $exc = new BadRequest('File parsing error');
- $response = $this->handleFileMultiStatusError($requestData['oc-path'], $exc);
- }
- $bundleResponseProperties[] = $response;
- }
-
- //multi-status response announced
- $this->response->setHeader('Content-Type', 'application/xml; charset=utf-8');
- $this->response->setStatus(207);
- $body = $this->server->generateMultiStatus($bundleResponseProperties);
- $this->response->setBody($body);
-
- return false;
- }
-
- /**
- * Adds to multi-status response exception class string and exception message for specific file
- *
- * @return array $entry
- */
- private function handleFileMultiStatusError($ocPath, $exc){
- $status = $exc->getHTTPCode();
- $entry['href'] = $this->userFilesHome;
- $entry[$status]['{DAV:}error']['{http://sabredav.org/ns}exception'] = get_class($exc);
- $entry[$status]['{DAV:}error']['{http://sabredav.org/ns}message'] = $exc->getMessage();
- $entry[$status]['{DAV:}oc-path'] = $ocPath;
- return $entry;
- }
-
- /**
- * Adds to multi-status response properties for specific file
- *
- * @return array $entry
- */
- private function handleFileMultiStatus($ocPath, $properties){
- $entry['href'] = $this->userFilesHome;
- $entry[200] = $properties;
- $entry[200]['{DAV:}oc-path'] = $ocPath;
- return $entry;
- }
-
- /**
- * Get content handler
- *
- * @param RequestInterface $request
- * @return \OCA\DAV\BundleUpload\MultipartContentsParser
- */
- // private function getContentHandler(RequestInterface $request) {
- // if ($this->contentHandler === null) {
- // return new MultipartContentsParser($request);
- // }
- // return $this->contentHandler;
- // }
-} \ No newline at end of file
diff --git a/apps/dav/lib/BundleUpload/MultipartContentsParser.php b/apps/dav/lib/BundleUpload/MultipartContentsParser.php
deleted file mode 100644
index 93b24539c49..00000000000
--- a/apps/dav/lib/BundleUpload/MultipartContentsParser.php
+++ /dev/null
@@ -1,497 +0,0 @@
-<?php
-/**
- * @author Piotr Mrowczynski <Piotr.Mrowczynski@owncloud.com>
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\BundleUpload;
-
-use Exception;
-use Sabre\HTTP\RequestInterface;
-use Sabre\DAV\Exception\BadRequest;
-
-/**
- * This class is used to parse multipart/related HTTP message according to RFC http://www.rfc-archive.org/getrfc.php?rfc=2387
- * This class requires a message to contain Content-length parameters, which is used in high performance reading of file contents.
- */
-
-class MultipartContentsParser {
- /**
- * @var \Sabre\HTTP\RequestInterface
- */
- // private $request;
-
- /** @var resource */
- private $stream = null;
-
- /** @var string */
- private $boundary = "";
- private $lastBoundary = "";
-
- /**
- * @var Bool
- */
- // private $endDelimiterReached = false;
-
- /**
- * Constructor.
- */
- public function __construct(RequestInterface $request) {
- $this->stream = $request->getBody();
- if (gettype($this->stream) !== 'resource') {
- throw new BadRequest('Wrong body type');
- }
-
- $this->boundary = '--'.$this->getBoundary($request->getHeader('Content-Type'))."\r\n";
- $this->lastBoundary = '--'.$this->getBoundary($request->getHeader('Content-Type'))."--\r\n";
- }
-
- /**
- * Parse the boundary from a Content-Type header
- *
- * @throws \Sabre\DAV\Exception\BadRequest
- */
- private function getBoundary(string $contentType) {
- // Making sure the end node exists
- //TODO: add support for user creation if that is first sync. Currently user has to be created.
- // $this->userFilesHome = $this->request->getPath();
- // $userFilesHomeNode = $this->server->tree->getNodeForPath($this->userFilesHome);
- // if (!($userFilesHomeNode instanceof FilesHome)){
- // throw new Forbidden('URL endpoint has to be instance of \OCA\DAV\Files\FilesHome');
- // }
-
- // $headers = array('Content-Type');
- // foreach ($headers as $header) {
- // $value = $this->request->getHeader($header);
- // if ($value === null) {
- // throw new Forbidden(sprintf('%s header is needed', $header));
- // } elseif (!is_int($value) && empty($value)) {
- // throw new Forbidden(sprintf('%s header must not be empty', $header));
- // }
- // }
-
- // Validate content-type
- // Ex: Content-Type: "multipart/related; boundary=boundary_bf38b9b4b10a303a28ed075624db3978"
- [$mimeType, $boundary] = explode(';', $contentType);
-
- if (trim($mimeType) !== 'multipart/related') {
- throw new BadRequest('Content-Type must be multipart/related');
- }
-
- // Validate boundary
- [$key, $value] = explode('=', $boundary);
- if (trim($key) !== 'boundary') {
- throw new BadRequest('Boundary is invalid');
- }
-
- $value=trim($value);
-
- // Remove potential quotes around boundary value
- if (substr($value, 0, 1) == '"' && substr($value, -1) == '"') {
- $value = substr($value, 1, -1);
- }
-
- return $value;
- }
-
- /**
- * Get a line.
- *
- * If false is return, it's the end of file.
- *
- * @throws \Sabre\DAV\Exception\BadRequest
- */
- // public function gets() {
- // $content = $this->getContent();
- // if (!is_resource($content)) {
- // throw new BadRequest('Unable to get request content');
- // }
-
- // return fgets($content);
- // }
-
- /**
- */
- // public function getCursor() {
- // return ftell($this->getContent());
- // }
-
- /**
- */
- // public function getEndDelimiterReached() {
- // return $this->endDelimiterReached;
- // }
-
- /**
- * Return if end of file.
- */
- public function eof() {
- return feof($this->stream);
- }
-
- /**
- * Seeks to offset of some file contentLength from the current cursor position in the
- * multipartContent.
- *
- * Return true on success and false on failure
- */
- // public function multipartContentSeekToContentLength(int $contentLength) {
- // return (fseek($this->getContent(), $contentLength, SEEK_CUR) === 0 ? true : false);
- // }
-
- /**
- * Get request content.
- *
- * @throws \Sabre\DAV\Exception\BadRequest
- *
- * @return resource
- */
- // public function getContent() {
- // if ($this->stream === null) {
- // // Pass body by reference, so other objects can have global access
- // $content = $this->request->getBody();
-
- // if (!$this->stream) {
- // throw new BadRequest('Unable to get request content');
- // }
-
- // if (gettype($this->stream) !== 'resource') {
- // throw new BadRequest('Wrong body type');
- // }
-
- // $this->stream = $content;
- // }
-
- // return $this->stream;
- // }
-
- // public function getBoundary(string $boundary) {
- // return "\r\n--$boundary\r\n";
- // }
-
- public function checkBoundary(string $boundary, string $line) {
- if ($line !== $boundary) {
- throw new Exception("Invalid boundary, is '$line', should be '$this->boundary'.");
- }
-
- return true;
- }
-
- public function lastBoundary() {
- $content = fread($this->stream, strlen($this->lastBoundary));
- $result = fseek($this->stream, -strlen($this->lastBoundary), SEEK_CUR);
-
- if ($result === -1) {
- throw new Exception("Unknown error while seeking content");
- }
-
- return $content === $this->lastBoundary;
- }
-
- /**
- * Return the next part of the request.
- *
- * @throws Exception
- */
- public function readNextPart(int $length = 0) {
- $this->checkBoundary($this->boundary, fread($this->stream, strlen($this->boundary)));
-
- $headers = $this->readPartHeaders();
-
- if ($length === 0 && isset($headers["content-length"])) {
- $length = $headers["content-length"];
- }
-
- if ($length === 0) {
- throw new Exception("Part cannot be of length 0.");
- }
-
- $content = $this->readPartContent2($length);
-
- return [$headers, $content];
- }
-
- /**
- * Return the next part of the request.
- *
- * @throws Exception
- */
- public function readNextStream() {
- $this->checkBoundary($this->boundary, fread($this->stream, strlen($this->boundary)));
-
- $headers = $this->readPartHeaders();
-
- return [$headers, $this->stream];
- }
-
- /**
- * Return the headers of a part of the request.
- *
- * @throws \Sabre\DAV\Exception\BadRequest
- * @throws Exception
- */
- public function readPartHeaders() {
- $headers = [];
- $blankLineCount = 0;
-
- while($blankLineCount < 1) {
- $line = fgets($this->stream);
-
- if ($line === false) {
- throw new Exception('An error appears while reading headers of a part');
- }
-
- if ($line === "\r\n") {
- break;
- }
-
- try {
- [$key, $value] = explode(':', $line, 2);
- $headers[strtolower(trim($key))] = trim($value);
- } catch (Exception $e) {
- throw new BadRequest('An error appears while parsing headers of a part', $e);
- }
- }
-
- return $headers;
- }
-
- /**
- * Return the content of the current part of the stream.
- *
- * @throws \Sabre\DAV\Exception\BadRequest
- * @throws Exception
- */
- public function readPartContent() {
- $line = '';
- $content = '';
-
- do {
- $content .= $line;
-
- if (feof($this->stream)) {
- throw new BadRequest("Unexpected EOF while reading stream.");
- }
-
- $line = fgets($this->stream);
-
- if ($line === false) {
- throw new Exception("Fail to read part's content.");
- }
- } while ($line !== $this->boundary);
-
- // We need to be before $boundary for the next parsing.
- $result = fseek($this->stream, -strlen($this->boundary), SEEK_CUR);
-
- if ($result === -1) {
- throw new Exception("Fail to seek upstream.");
- }
-
- // Remove the extra new line "\r\n" that is not part of the content
- return substr($content, 0, -2);
- }
-
- public function readPartContent2(int $length) {
- // Read stream until file's $length, EOF or $boundary is reached
- $content = stream_get_line($this->stream, $length);
-
- if ($content === false) {
- throw new Exception("Fail to read part's content.");
- }
-
- if (feof($this->stream)) {
- throw new Exception("Unexpected EOF while reading stream.");
- }
-
- stream_get_contents($this->stream, 2);
-
- return $content;
- }
-
- public function getContentPosition() {
- return ftell($this->stream);
- }
-
- public function getMetadata() {
- fseek($this->stream, 0);
- return $this->readNextPart();
- }
-
- public function getContent(int $pos, int $length) {
- $previousPos = ftell($this->stream);
-
- $content = stream_get_contents($this->stream, $length, $pos);
-
- fseek($this->stream, $previousPos);
-
- return $content;
- }
-
- /**
- * Get a part of request separated by boundary $boundary.
- *
- * If this method returns an exception, it means whole request has to be abandoned,
- * Request part without correct headers might corrupt the message and parsing is impossible
- *
- * @throws \Exception
- */
- // public function getPartHeaders(string $boundary) {
- // $delimiter = '--'.$boundary."\r\n";
- // $endDelimiter = '--'.$boundary.'--';
- // $boundaryCount = 0;
- // $content = '';
- // $headers = null;
-
- // while (!$this->eof()) {
- // $line = $this->gets();
- // if ($line === false) {
- // if ($boundaryCount == 0) {
- // // Empty part, ignore
- // break;
- // }
- // else{
- // throw new \Exception('An error appears while reading and parsing header of content part using fgets');
- // }
- // }
-
- // if ($boundaryCount == 0) {
- // if ($line != $delimiter) {
- // if ($this->getCursor() == strlen($line)) {
- // throw new \Exception('Expected boundary delimiter in content part - this is not a multipart request');
- // }
- // elseif ($line == $endDelimiter || $line == $endDelimiter."\r\n") {
- // $this->endDelimiterReached = true;
- // break;
- // }
- // elseif ($line == "\r\n") {
- // continue;
- // }
- // } else {
- // continue;
- // }
- // // At this point we know, that first line was boundary
- // $boundaryCount++;
- // }
- // elseif ($boundaryCount == 1 && $line == "\r\n"){
- // //header-end according to RFC
- // $content .= $line;
- // $headers = $this->readHeaders($content);
- // break;
- // }
- // elseif ($line == $endDelimiter || $line == $endDelimiter."\r\n") {
- // $this->endDelimiterReached = true;
- // break;
- // }
-
- // $content .= $line;
- // }
-
- // if ($this->eof()){
- // $this->endDelimiterReached = true;
- // }
-
- // return $headers;
- // }
-
- /**
- * Read the contents from the current file pointer to the specified length
- *
- * @throws \Sabre\DAV\Exception\BadRequest
- */
- // public function streamReadToString(int $length) {
- // if ($length<0) {
- // throw new BadRequest('Method streamRead cannot read contents with negative length');
- // }
- // $source = $this->getContent();
- // $bufChunkSize = 8192;
- // $count = $length;
- // $buf = '';
-
- // while ($count!=0) {
- // $bufSize = (($count - $bufChunkSize)<0) ? $count : $bufChunkSize;
- // $buf .= fread($source, $bufSize);
- // $count -= $bufSize;
- // }
-
- // $bytesWritten = strlen($buf);
- // if ($length != $bytesWritten){
- // throw new BadRequest('Method streamRead read '.$bytesWritten.' expected '.$length);
- // }
- // return $buf;
- // }
-
- /**
- * Read the contents from the current file pointer to the specified length and pass
- *
- * @param resource $target
- *
- * @throws \Sabre\DAV\Exception\BadRequest
- */
- // public function streamReadToStream($target, int $length) {
- // if ($length<0) {
- // throw new BadRequest('Method streamRead cannot read contents with negative length');
- // }
- // $source = $this->getContent();
- // $bufChunkSize = 8192;
- // $count = $length;
- // $returnStatus = true;
-
- // while ($count!=0) {
- // $bufSize = (($count - $bufChunkSize)<0) ? $count : $bufChunkSize;
- // $buf = fread($source, $bufSize);
- // $bytesWritten = fwrite($target, $buf);
-
- // // note: strlen is expensive so only use it when necessary,
- // // on the last block
- // if ($bytesWritten === false
- // || ($bytesWritten < $bufSize)
- // ) {
- // // write error, could be disk full ?
- // $returnStatus = false;
- // break;
- // }
- // $count -= $bufSize;
- // }
-
- // return $returnStatus;
- // }
-
-
- /**
- * Get headers from content
- */
- // public function readHeaders($content) {
- // $headers = null;
- // $headerLimitation = strpos($content, "\r\n\r\n");
- // if ($headerLimitation === false) {
- // return null;
- // }
- // $headersContent = substr($content, 0, $headerLimitation);
- // $headersContent = trim($headersContent);
- // foreach (explode("\r\n", $headersContent) as $header) {
- // $parts = explode(':', $header, 2);
- // if (count($parts) != 2) {
- // //has incorrect header, abort
- // return null;
- // }
- // $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
- // }
-
- // return $headers;
- // }
-} \ No newline at end of file
diff --git a/apps/dav/lib/Capabilities.php b/apps/dav/lib/Capabilities.php
index 17db2346c68..ce60bccfd0b 100644
--- a/apps/dav/lib/Capabilities.php
+++ b/apps/dav/lib/Capabilities.php
@@ -29,7 +29,7 @@ class Capabilities implements ICapability {
return [
'dav' => [
'chunking' => '1.0',
- 'bundleupload' => '1.0',
+ 'bulkupload' => '1.0',
]
];
}
diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php
index ec33b44fe4b..5ff5f831eb5 100644
--- a/apps/dav/lib/Connector/Sabre/File.php
+++ b/apps/dav/lib/Connector/Sabre/File.php
@@ -356,7 +356,7 @@ class File extends Node implements IFile {
return '"' . $this->info->getEtag() . '"';
}
- protected function getPartFileBasePath($path) {
+ private function getPartFileBasePath($path) {
$partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true);
if ($partFileInStorage) {
return $path;
@@ -368,7 +368,7 @@ class File extends Node implements IFile {
/**
* @param string $path
*/
- protected function emitPreHooks($exists, $path = null) {
+ private function emitPreHooks($exists, $path = null) {
if (is_null($path)) {
$path = $this->path;
}
@@ -396,7 +396,7 @@ class File extends Node implements IFile {
/**
* @param string $path
*/
- protected function emitPostHooks($exists, $path = null) {
+ private function emitPostHooks($exists, $path = null) {
if (is_null($path)) {
$path = $this->path;
}
@@ -633,7 +633,7 @@ class File extends Node implements IFile {
*
* @throws \Sabre\DAV\Exception
*/
- protected function convertToSabreException(\Exception $e) {
+ private function convertToSabreException(\Exception $e) {
if ($e instanceof \Sabre\DAV\Exception) {
throw $e;
}
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index 74ae94a6d51..055c37f8472 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -34,6 +34,7 @@
*/
namespace OCA\DAV;
+use Psr\Log\LoggerInterface;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\BirthdayService;
use OCA\DAV\CardDAV\HasPhotoPlugin;
@@ -62,12 +63,11 @@ use OCA\DAV\DAV\PublicAuth;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\DAV\Files\BrowserErrorPagePlugin;
use OCA\DAV\Files\LazySearchBackend;
-use OCA\DAV\BundleUpload\BundlingPlugin;
+use OCA\DAV\BulkUpload\BulkUploadPlugin;
use OCA\DAV\Provisioning\Apple\AppleProvisioningPlugin;
use OCA\DAV\SystemTag\SystemTagPlugin;
use OCA\DAV\Upload\ChunkingPlugin;
use OCP\EventDispatcher\IEventDispatcher;
-use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\SabrePluginEvent;
use Sabre\CardDAV\VCFExportPlugin;
@@ -296,9 +296,9 @@ class Server {
\OC::$server->getShareManager(),
$view
));
- $rootFolder = \OC::$server->query(IRootFolder::class);
+ $logger = \OC::$server->get(LoggerInterface::class);
$this->server->addPlugin(
- new BundlingPlugin($view, $userFolder)
+ new BulkUploadPlugin($userFolder, $logger)
);
}
$this->server->addPlugin(new \OCA\DAV\CalDAV\BirthdayCalendar\EnablePlugin(
diff --git a/apps/dav/tests/temporary/benchmark.sh b/apps/dav/tests/benchmarks/benchmark.sh
index 4a2f283e320..27d7c4ecbc7 100755
--- a/apps/dav/tests/temporary/benchmark.sh
+++ b/apps/dav/tests/benchmarks/benchmark.sh
@@ -2,6 +2,8 @@
set -eu
+# benchmark.sh
+
export KB=1000
export MB=$((KB*1000))
@@ -32,10 +34,10 @@ do
echo "- Upload of $nb tiny file of ${size}B"
echo " - Bundled"
start=$(date +%s)
- echo "$requests_count" | xargs -d ' ' -P $CONCURRENCY -I{} ./bundle_upload.sh "$nb" "$size"
+ echo "$requests_count" | xargs -d ' ' -P $CONCURRENCY -I{} ./bulk_upload.sh "$nb" "$size"
end=$(date +%s)
- bundle_exec_time=$((end-start))
- echo "${bundle_exec_time}s"
+ bulk_exec_time=$((end-start))
+ echo "${bulk_exec_time}s"
echo " - Single"
start=$(date +%s)
@@ -44,7 +46,7 @@ do
single_exec_time=$((end-start))
echo "${single_exec_time}s"
- md_output+="| $nb | $size | $bundle_exec_time | $single_exec_time |\n"
+ md_output+="| $nb | $size | $bulk_exec_time | $single_exec_time |\n"
done
echo -en "$md_output" \ No newline at end of file
diff --git a/apps/dav/tests/temporary/bundle_upload.sh b/apps/dav/tests/benchmarks/bulk_upload.sh
index 9d2b9c6f200..862ddfe461f 100755
--- a/apps/dav/tests/temporary/bundle_upload.sh
+++ b/apps/dav/tests/benchmarks/bulk_upload.sh
@@ -2,6 +2,8 @@
set -eu
+# bulk_upload.sh <nb-of-files> <size-of-files>
+
KB=${KB:-100}
MB=${MB:-$((KB*1000))}
@@ -14,10 +16,10 @@ BANDWIDTH=${BANDWIDTH:-$((100*MB/CONCURRENCY))}
USER="admin"
PASS="password"
SERVER="nextcloud.test"
-UPLOAD_PATH="/tmp/bundle_upload_request_$(openssl rand --hex 8).txt"
+UPLOAD_PATH="/tmp/bulk_upload_request_$(openssl rand --hex 8).txt"
BOUNDARY="boundary_$(openssl rand --hex 8)"
-LOCAL_FOLDER="/tmp/bundle_upload/${BOUNDARY}_${NB}_${SIZE}"
-REMOTE_FOLDER="/bundle_upload/${BOUNDARY}_${NB}_${SIZE}"
+LOCAL_FOLDER="/tmp/bulk_upload/${BOUNDARY}_${NB}_${SIZE}"
+REMOTE_FOLDER="/bulk_upload/${BOUNDARY}_${NB}_${SIZE}"
mkdir --parent "$LOCAL_FOLDER"
@@ -48,7 +50,13 @@ done
echo -en "--$BOUNDARY--\r\n" >> "$UPLOAD_PATH"
-echo "Creating folder /${BOUNDARY}_${NB}_${SIZE}"
+echo "Creating folder /bulk_upload"
+curl \
+ -X MKCOL \
+ -k \
+ "https://$USER:$PASS@$SERVER/remote.php/dav/files/$USER/bulk_upload" > /dev/null
+
+echo "Creating folder $REMOTE_FOLDER"
curl \
-X MKCOL \
-k \
@@ -56,15 +64,15 @@ curl \
echo "Uploading $NB files with total size: $(du -sh "$UPLOAD_PATH" | cut -d ' ' -f1)"
echo "Local file is: $UPLOAD_PATH"
-blackfire curl \
+curl \
-X POST \
-k \
--progress-bar \
--limit-rate "${BANDWIDTH}k" \
- --cookie "XDEBUG_PROFILE=MROW4A;path=/;" \
+ --cookie "XDEBUG_PROFILE=true;path=/;" \
-H "Content-Type: multipart/related; boundary=$BOUNDARY" \
--data-binary "@$UPLOAD_PATH" \
- "https://$USER:$PASS@$SERVER/remote.php/dav/files/bundle"
+ "https://$USER:$PASS@$SERVER/remote.php/dav/bulk"
rm -rf "${LOCAL_FOLDER:?}"
rm "$UPLOAD_PATH"
diff --git a/apps/dav/tests/temporary/single_upload.sh b/apps/dav/tests/benchmarks/single_upload.sh
index da8e414be60..ec57e66668d 100755
--- a/apps/dav/tests/temporary/single_upload.sh
+++ b/apps/dav/tests/benchmarks/single_upload.sh
@@ -2,6 +2,8 @@
set -eu
+# single_upload.sh <nb-of-files> <size-of-files>
+
export KB=${KB:-100}
export MB=${MB:-$((KB*1000))}
@@ -15,15 +17,20 @@ export USER="admin"
export PASS="password"
export SERVER="nextcloud.test"
export UPLOAD_ID="single_$(openssl rand --hex 8)"
-export LOCAL_FOLDER="/tmp/bundle_upload/${UPLOAD_ID}_${NB}_${SIZE}"
-export REMOTE_FOLDER="/bundle_upload/${UPLOAD_ID}_${NB}_${SIZE}"
+export LOCAL_FOLDER="/tmp/single_upload/${UPLOAD_ID}_${NB}_${SIZE}"
+export REMOTE_FOLDER="/single_upload/${UPLOAD_ID}_${NB}_${SIZE}"
mkdir --parent "$LOCAL_FOLDER"
curl \
-X MKCOL \
-k \
- --cookie "XDEBUG_SESSION=MROW4A;path=/;" \
+ "https://$USER:$PASS@$SERVER/remote.php/dav/files/$USER/bulk_upload" > /dev/null
+
+curl \
+ -X MKCOL \
+ -k \
+ --cookie "XDEBUG_SESSION=true;path=/;" \
"https://$USER:$PASS@$SERVER/remote.php/dav/files/$USER/$REMOTE_FOLDER"
upload_file() {
diff --git a/apps/dav/tests/unit/CapabilitiesTest.php b/apps/dav/tests/unit/CapabilitiesTest.php
index 719b62115d9..399467f6ed8 100644
--- a/apps/dav/tests/unit/CapabilitiesTest.php
+++ b/apps/dav/tests/unit/CapabilitiesTest.php
@@ -35,6 +35,7 @@ class CapabilitiesTest extends TestCase {
$expected = [
'dav' => [
'chunking' => '1.0',
+ 'bulkupload' => '1.0',
],
];
$this->assertSame($expected, $capabilities->getCapabilities());
diff --git a/apps/dav/tests/unit/Files/BundlePluginTest.php b/apps/dav/tests/unit/Files/BundlePluginTest.php
deleted file mode 100644
index 92309fe5021..00000000000
--- a/apps/dav/tests/unit/Files/BundlePluginTest.php
+++ /dev/null
@@ -1,711 +0,0 @@
-<?php
-/**
- * @author Piotr Mrowczynski <Piotr.Mrowczynski@owncloud.com>
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Files;
-
-use OC\Files\FileInfo;
-use OC\Files\Storage\Local;
-use Sabre\HTTP\RequestInterface;
-use Test\TestCase;
-use OC\Files\View;
-use OCP\Files\Storage;
-use Sabre\DAV\Exception;
-use OC\Files\Filesystem;
-use OCP\Files\StorageNotAvailableException;
-
-/**
- * Class BundlingPlugin
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\unit\Files
- */
-class BundlingPluginTest extends TestCase {
-
- /**
- * @var string
- */
- private $user;
-
- /** @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject */
- private $view;
-
- /** @var \OC\Files\FileInfo | \PHPUnit_Framework_MockObject_MockObject */
- private $info;
-
- /**
- * @var \Sabre\DAV\Server | \PHPUnit_Framework_MockObject_MockObject
- */
- private $server;
-
- /**
- * @var FilesPlugin
- */
- private $plugin;
-
- /**
- * @var \Sabre\HTTP\RequestInterface | \PHPUnit_Framework_MockObject_MockObject
- */
- private $request;
- /**
- * @var \Sabre\HTTP\ResponseInterface | \PHPUnit_Framework_MockObject_MockObject
- */
- private $response;
-
- /**
- * @var MultipartContentsParser | \PHPUnit_Framework_MockObject_MockObject
- */
- private $contentHandler;
-
- const BOUNDRARY = 'test_boundrary';
-
- public function setUp() {
- parent::setUp();
-// $this->server = new \Sabre\DAV\Server();
-
- $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
- ->setConstructorArgs(array())
- ->setMethods(array('emit'))
- ->getMock();
-
- $this->server->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
- ->disableOriginalConstructor()
- ->getMock();
-
- // setup
- $storage = $this->getMockBuilder(Local::class)
- ->setMethods(["fopen","moveFromStorage","file_exists"])
- ->setConstructorArgs([['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]])
- ->getMock();
- $storage->method('fopen')
- ->will($this->returnCallback(
- function ($path,$mode) {
- $bodyStream = fopen('php://temp', 'r+');
- return $bodyStream;
- }
- ));
- $storage->method('moveFromStorage')
- ->will($this->returnValue(true));
- $storage->method('file_exists')
- ->will($this->returnValue(true));
-
- \OC_Hook::clear();
-
- $this->user = $this->getUniqueID('user_');
- $userManager = \OC::$server->getUserManager();
- $userManager->createUser($this->user, 'pass');
-
- $this->loginAsUser($this->user);
-
- Filesystem::mount($storage, [], $this->user . '/');
-
- $this->view = $this->getMockBuilder(View::class)
- ->setMethods(['resolvePath', 'touch', 'file_exists', 'getFileInfo'])
- ->setConstructorArgs([])
- ->getMock();
-
- $this->view->method('touch')
- ->will($this->returnValue(true));
-
- $this->view
- ->method('resolvePath')
- ->will($this->returnCallback(
- function ($path) use ($storage) {
- return [$storage, $path];
- }
- ));
-
- $this->view
- ->method('getFileInfo')
- ->will($this->returnCallback(
- function ($path) {
- $props = array();
- $props['checksum'] = null;
- $props['etag'] = $path;
- $props['fileid'] = $path;
- $info = new FileInfo($path, null, null, $props, null);
- return $info;
- }
- ));
-
- $this->info = $this->createMock('OC\Files\FileInfo', [], [], '', false);
-
- $this->request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->response = new \Sabre\HTTP\Response();
-
- $this->plugin = new BundlingPlugin(
- $this->view
- );
-
- $this->plugin->initialize($this->server);
- }
-
- /*TESTS*/
-
- /**
- * This test checks that if url endpoint is wrong, plugin with return exception
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage URL endpoint has to be instance of \OCA\DAV\Files\FilesHome
- */
- public function testHandleBundleNotHomeCollection() {
-
- $this->request
- ->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue('notFilesHome.xml'));
-
- $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->server->tree->expects($this->once())
- ->method('getNodeForPath')
- ->with('notFilesHome.xml')
- ->will($this->returnValue($node));
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Simulate NULL request header
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Content-Type header is needed
- */
- public function testHandleBundleNoHeader() {
- $this->setupServerTillFilesHome();
-
- $this->request
- ->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue(null));
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Simulate empty request header
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Content-Type header must not be empty
- */
- public function testHandleBundleEmptyHeader() {
- $this->setupServerTillFilesHome();
-
- $this->request
- ->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue(""));
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Simulate content-type header without boundrary specification request header
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Improper Content-type format. Boundary may be missing
- */
- public function testHandleBundleNoBoundraryHeader() {
- $this->setupServerTillFilesHome();
-
- $this->request
- ->expects($this->atLeastOnce())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue("multipart/related"));
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Simulate content-type header with wrong boundrary specification request header
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Boundary is not set
- */
- public function testHandleBundleWrongBoundraryHeader() {
- $this->setupServerTillFilesHome();
-
- $this->request
- ->expects($this->atLeastOnce())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue("multipart/related;thisIsNotBoundrary"));
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Simulate content-type header with wrong boundrary specification request header
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Content-Type must be multipart/related
- */
- public function testHandleBundleWrongContentTypeHeader() {
- $this->setupServerTillFilesHome();
-
- $this->request
- ->expects($this->atLeastOnce())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue("multipart/mixed; boundary=".self::BOUNDRARY));
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Simulate content-type header with alternative correct boundrary specification request header
- *
- * Request with user out of quota
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage beforeWriteBundle preconditions failed
- */
- public function testHandleAlternativeBoundraryPlusBundleOutOfQuota() {
- $this->setupServerTillFilesHome();
-
- $this->request
- ->expects($this->atLeastOnce())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue("multipart/related; boundary=\"".self::BOUNDRARY."\""));
-
- $this->server
- ->expects($this->once())
- ->method('emit')
- ->will($this->returnValue(false));
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Request without request body
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Unable to get request content
- */
- public function testHandleBundleWithNullBody() {
- $this->setupServerTillHeader();
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Test empty request body. This will pass getPartHeader, but exception will be raised after we ready headers
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Incorrect Content-type format. Charset might be missing
- */
- public function testHandleBundleWithEmptyBody() {
- $this->setupServerTillHeader();
-
- $this->fillMultipartContentsParserStreamWithBody("");
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Test wrong request body
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Expected boundary delimiter in content part - this is not a multipart request
- */
- public function testHandleBundleWithWrongBody() {
- $this->setupServerTillHeader();
-
- $this->fillMultipartContentsParserStreamWithBody("WrongBody");
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Test wrong request body, with metadata header containing no charset
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Incorrect Content-type format. Charset might be missing
- */
- public function testHandleMetadataNoCharsetType(){
- $bodyContent = 'I am wrong metadata not in utf-8';
- $headers['content-length'] = strlen($bodyContent);
- $headers['content-type'] = 'text/xml';
-
- //this part will have some arbitrary, correct headers
- $bodyFull = "--".self::BOUNDRARY
- ."\r\nContent-Type: ".$headers['content-type']
- ."\r\n\r\n"
- ."$bodyContent\r\n--".self::BOUNDRARY."--";
-
- $this->setupServerTillHeader();
-
- $this->fillMultipartContentsParserStreamWithBody($bodyFull);
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Test wrong request body, with metadata header containing wrong content-type
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Content-Type must be text/xml
- */
- public function testHandleMetadataWrongContentType(){
- $bodyContent = 'I am wrong metadata content type';
- $headers['content-type'] = 'text/plain; charset=utf-8';
-
- //this part will have some arbitrary, correct headers
- $bodyFull = "--".self::BOUNDRARY
- ."\r\nContent-Type: ".$headers['content-type']
- ."\r\n\r\n"
- ."$bodyContent\r\n--".self::BOUNDRARY."--";
-
- $this->setupServerTillHeader();
-
- $this->fillMultipartContentsParserStreamWithBody($bodyFull);
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Test wrong request body, with metadata header containing wrong content-type
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Bundle metadata header does not contain Content-Length. Unable to parse whole bundle request
- */
- public function testHandleMetadataNoContentLength(){
- $bodyContent = 'I am wrong metadata content type';
- //$headers['content-length'] = strlen($bodyContent);
- $headers['content-type'] = 'text/xml; charset=utf-8';
-
- //this part will have some arbitrary, correct headers
- $bodyFull = "--".self::BOUNDRARY
- ."\r\nContent-Type: ".$headers['content-type']
- //."\r\nContent-length: ".$headers['content-length']
- ."\r\n\r\n"
- ."$bodyContent\r\n--".self::BOUNDRARY."--";
-
- $this->setupServerTillHeader();
-
- $this->fillMultipartContentsParserStreamWithBody($bodyFull);
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Try to parse body which is not xml
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Bundle metadata contains incorrect xml structure. Unable to parse whole bundle request
- */
- public function testHandleWrongMetadataNoXML(){
- $bodyContent = "I am not xml";
-
- $this->setupServerTillMetadata($bodyContent);
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Try to parse body which has xml d:multipart element which
- * has not been declared <d:multipart xmlns:d='DAV:'>
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Bundle metadata does not contain d:multipart children elements
- */
- public function testHandleWrongMetadataWrongXMLdElement(){
- $bodyContent = "<?xml version='1.0' encoding='UTF-8'?><d:multipart></d:multipart>";
-
- $this->setupServerTillMetadata($bodyContent);
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * This test checks that exception is raised for
- * parsed XML which contains empty(without d:part elements) d:multipart section in metadata XML
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Bundle metadata does not contain d:multipart/d:part/d:prop children elements
- */
- public function testHandleEmptyMultipartMetadataSection(){
- $bodyContent = "<?xml version='1.0' encoding='UTF-8'?><d:multipart xmlns:d='DAV:'></d:multipart>";
-
- $this->setupServerTillMetadata($bodyContent);
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Metadata contains part properties not containing obligatory field will raise exception
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Undefined index: oc-id
- */
- public function testHandleWrongMetadataNoPartID(){
- $bodyContent = "<?xml version='1.0' encoding='UTF-8'?>
- <d:multipart xmlns:d='DAV:'>
- <d:part>
- <d:prop>
- </d:prop>
- </d:part>
- </d:multipart>";
-
- $this->setupServerTillMetadata($bodyContent);
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * In the request, insert two files with the same Content-ID
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage One or more files have the same Content-ID 1. Unable to parse whole bundle request
- */
- public function testHandleWrongMetadataMultipleIDs(){
- $bodyContent = "<?xml version='1.0' encoding='UTF-8'?>
- <d:multipart xmlns:d='DAV:'>
- <d:part>
- <d:prop>
- <d:oc-path>/test/zombie1.jpg</d:oc-path>\n
- <d:oc-mtime>1476393386</d:oc-mtime>\n
- <d:oc-id>1</d:oc-id>\n
- <d:oc-total-length>6</d:oc-total-length>\n
- </d:prop>
- </d:part>
- <d:part>
- <d:prop>
- <d:oc-path>/test/zombie2.jpg</d:oc-path>\n
- <d:oc-mtime>1476393386</d:oc-mtime>\n
- <d:oc-id>1</d:oc-id>\n
- <d:oc-total-length>6</d:oc-total-length>\n
- </d:prop>
- </d:part>
- </d:multipart>";
-
- $this->setupServerTillMetadata($bodyContent);
-
- $this->plugin->handleBundle($this->request, $this->response);
- }
-
- /**
- * Specify metadata part without corresponding binary content
- *
- */
- public function testHandleWithoutBinaryContent(){
- $bodyContent = "<?xml version='1.0' encoding='UTF-8'?>
- <d:multipart xmlns:d='DAV:'>
- <d:part>
- <d:prop>
- <d:oc-path>/test/zombie1.jpg</d:oc-path>\n
- <d:oc-mtime>1476393386</d:oc-mtime>\n
- <d:oc-id>1</d:oc-id>\n
- <d:oc-total-length>6</d:oc-total-length>\n
- </d:prop>
- </d:part>
- </d:multipart>";
-
- $this->setupServerTillMetadata($bodyContent);
- $this->plugin->handleBundle($this->request, $this->response);
- $return = $this->response->getBody();
- $this->assertTrue(false != $return);
- $xml = simplexml_load_string($return);
- $this->assertTrue(false != $xml);
- $xml->registerXPathNamespace('d','urn:DAV');
- $xml->registerXPathNamespace('s','http://sabredav.org/ns');
-
- $this->assertEquals(1, count($xml->xpath('/d:multistatus')));
-
- $fileMetadataObjectXML = $xml->xpath('/d:multistatus/d:response/d:propstat/d:status');
- $this->assertTrue(false != $fileMetadataObjectXML);
- $this->assertEquals(1, count($fileMetadataObjectXML));
- $this->assertEquals("HTTP/1.1 400 Bad Request", (string) $fileMetadataObjectXML[0]);
-
- $fileMetadataObjectXML = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:error/s:message');
- $this->assertTrue(false != $fileMetadataObjectXML);
- $this->assertEquals(1, count($fileMetadataObjectXML));
- $this->assertEquals("File parsing error", (string) $fileMetadataObjectXML[0]);
- }
-
- /**
- * This test will simulate success and failure in putFile class.
- *
- */
- public function testHandlePutFile(){
- $this->setupServerTillData();
-
- $this->view
- ->method('file_exists')
- ->will($this->onConsecutiveCalls(true, false, $this->throwException(new StorageNotAvailableException())));
-
- $this->plugin->handleBundle($this->request, $this->response);
-
- $return = $this->response->getBody();
- $this->assertTrue(false != $return);
- $xml = simplexml_load_string($return);
- $this->assertTrue(false != $xml);
- $xml->registerXPathNamespace('d','urn:DAV');
- $xml->registerXPathNamespace('s','http://sabredav.org/ns');
-
- $this->assertEquals(1, count($xml->xpath('/d:multistatus')));
-
- $fileMetadataObjectXML = $xml->xpath('/d:multistatus/d:response/d:propstat/d:status');
- $this->assertTrue(false != $fileMetadataObjectXML);
- $this->assertEquals(3, count($fileMetadataObjectXML));
- $this->assertEquals("HTTP/1.1 400 Bad Request", (string) $fileMetadataObjectXML[0]);
- $this->assertEquals("HTTP/1.1 200 OK", (string) $fileMetadataObjectXML[1]);
- $this->assertEquals("HTTP/1.1 400 Bad Request", (string) $fileMetadataObjectXML[2]);
-
- $fileMetadataObjectXML = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:error/s:message');
- $this->assertTrue(false != $fileMetadataObjectXML);
- $this->assertEquals(2, count($fileMetadataObjectXML));
- $this->assertEquals("Bundling not supported for already existing files", (string) $fileMetadataObjectXML[0]);
- $this->assertEquals("StorageNotAvailableException raised", (string) $fileMetadataObjectXML[1]);
- }
-
- /*UTILITIES*/
-
- private function setupServerTillData(){
- $bodyContent = "<?xml version='1.0' encoding='UTF-8'?>
- <d:multipart xmlns:d='DAV:'>
- <d:part>
- <d:prop>
- <d:oc-path>/test/zombie1.jpg</d:oc-path>\n
- <d:oc-mtime>1476393386</d:oc-mtime>\n
- <d:oc-id>0</d:oc-id>\n
- <d:oc-total-length>7</d:oc-total-length>\n
- </d:prop>
- </d:part>
- <d:part>
- <d:prop>
- <d:oc-path>/test/zombie2.jpg</d:oc-path>\n
- <d:oc-mtime>1476393386</d:oc-mtime>\n
- <d:oc-id>1</d:oc-id>\n
- <d:oc-total-length>7</d:oc-total-length>\n
- </d:prop>
- </d:part>
- <d:part>
- <d:prop>
- <d:oc-path>zombie3.jpg</d:oc-path>\n
- <d:oc-mtime>1476393232</d:oc-mtime>\n
- <d:oc-id>2</d:oc-id>\n
- <d:oc-total-length>7</d:oc-total-length>\n
- </d:prop>
- </d:part>
- </d:multipart>";
-
- $headers['content-length'] = strlen($bodyContent);
- $headers['content-type'] = 'text/xml; charset=utf-8';
-
- //this part will have some arbitrary, correct headers
- $bodyFull = "--".self::BOUNDRARY
- ."\r\nContent-Type: ".$headers['content-type']
- ."\r\nContent-length: ".$headers['content-length']
- ."\r\n\r\n"
- ."$bodyContent"
- ."\r\n--".self::BOUNDRARY
- ."\r\nContent-ID: 0"
- ."\r\n\r\n"
- ."zombie1"
- ."\r\n--".self::BOUNDRARY
- ."\r\nContent-ID: 1"
- ."\r\n\r\n"
- ."zombie2"
- ."\r\n--".self::BOUNDRARY
- ."\r\nContent-ID: 2"
- ."\r\n\r\n"
- ."zombie3"
- ."\r\n--".self::BOUNDRARY."--";
-
- $this->setupServerTillHeader();
-
- $this->fillMultipartContentsParserStreamWithBody($bodyFull);
- }
-
- private function setupServerTillMetadata($bodyContent){
- $headers['content-length'] = strlen($bodyContent);
- $headers['content-type'] = 'text/xml; charset=utf-8';
-
- //this part will have some arbitrary, correct headers
- $bodyFull = "--".self::BOUNDRARY
- ."\r\nContent-Type: ".$headers['content-type']
- ."\r\nContent-length: ".$headers['content-length']
- ."\r\n\r\n"
- ."$bodyContent\r\n--".self::BOUNDRARY."--";
-
- $this->setupServerTillHeader();
-
- $this->fillMultipartContentsParserStreamWithBody($bodyFull);
- }
-
- private function setupServerTillHeader(){
- $this->setupServerTillFilesHome();
-
- $this->request
- ->expects($this->atLeastOnce())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue("multipart/related; boundary=".self::BOUNDRARY));
-
- $this->server
- ->expects($this->once())
- ->method('emit')
- ->will($this->returnValue(true));
- }
-
- private function setupServerTillFilesHome(){
- $this->request
- ->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue('files/admin'));
-
- $node = $this->getMockBuilder('\OCA\DAV\Files\FilesHome')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->server->tree->expects($this->once())
- ->method('getNodeForPath')
- ->with('files/admin')
- ->will($this->returnValue($node));
- }
-
- private function fillMultipartContentsParserStreamWithBody($bodyString){
- $bodyStream = fopen('php://temp', 'r+');
- fwrite($bodyStream, $bodyString);
- rewind($bodyStream);
-
- $this->request->expects($this->any())
- ->method('getBody')
- ->willReturn($bodyStream);
- }
-
- public function tearDown() {
- $userManager = \OC::$server->getUserManager();
- $userManager->get($this->user)->delete();
- unset($_SERVER['HTTP_OC_CHUNKED']);
-
- parent::tearDown();
- }
-}
diff --git a/apps/dav/tests/unit/Files/BundledFileTest.php b/apps/dav/tests/unit/Files/BundledFileTest.php
deleted file mode 100644
index e1a65459b60..00000000000
--- a/apps/dav/tests/unit/Files/BundledFileTest.php
+++ /dev/null
@@ -1,220 +0,0 @@
-<?php
-/**
- * @author Piotr Mrowczynski <Piotr.Mrowczynski@owncloud.com>
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Files;
-
-use OCP\Lock\ILockingProvider;
-
-/**
- * Class File
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\unit\Connector\Sabre
- */
-class BundledFileTest extends \Test\TestCase {
-
- /**
- * @var string
- */
- private $user;
-
- /* BASICS */
-
- public function setUp() {
- parent::setUp();
-
- \OC_Hook::clear();
-
- $this->user = $this->getUniqueID('user_');
- $userManager = \OC::$server->getUserManager();
- $userManager->createUser($this->user, 'pass');
-
- $this->loginAsUser($this->user);
- }
-
- /* TESTS */
-
- /**
- * Test basic successful bundled file PutFile
- */
- public function testPutFile() {
- $bodyContent = 'blabla';
- $headers['oc-total-length'] = 6;
- $headers['oc-path'] = '/foo.txt';
- $headers['oc-mtime'] = '1473336321';
- $headers['response'] = null;
-
- //this part will have some arbitrary, correct headers
- $bodyFull = "$bodyContent\r\n--boundary--";
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyFull);
-
- $this->doPutFIle($headers, $multipartContentsParser);
- }
-
- /**
- * Test basic successful bundled file PutFile
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage File requires oc-total-length header to be read
- */
- public function testPutFileNoLength() {
- $bodyContent = 'blabla';
- $headers['oc-path'] = '/foo.txt';
- $headers['oc-mtime'] = '1473336321';
- $headers['response'] = null;
-
- //this part will have some arbitrary, correct headers
- $bodyFull = "$bodyContent\r\n--boundary--";
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyFull);
-
- $this->doPutFIle($headers, $multipartContentsParser);
- }
-
- /**
- * Test putting a single file
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage PUT method not supported for bundling
- */
- public function testThrowIfPut() {
- $fileContents = $this->getStream('test data');
- $this->doPut('/foo.txt', $fileContents);
- }
-
- /* UTILITIES */
-
- private function getMockStorage() {
- $storage = $this->getMockBuilder('\OCP\Files\Storage')
- ->getMock();
- $storage->expects($this->any())
- ->method('getId')
- ->will($this->returnValue('home::someuser'));
- return $storage;
- }
-
- public function tearDown() {
- $userManager = \OC::$server->getUserManager();
- $userManager->get($this->user)->delete();
- unset($_SERVER['HTTP_OC_CHUNKED']);
-
- parent::tearDown();
- }
-
- /**
- * @param string $string
- */
- private function getStream($string) {
- $stream = fopen('php://temp', 'r+');
- fwrite($stream, $string);
- fseek($stream, 0);
- return $stream;
- }
-
- /**
- * Do basic put for single bundled file
- */
- private function doPutFIle($fileMetadata, $contentHandler, $view = null, $viewRoot = null) {
- $path = $fileMetadata['oc-path'];
-
- if(is_null($view)){
- $view = \OC\Files\Filesystem::getView();
- }
- if (!is_null($viewRoot)) {
- $view = new \OC\Files\View($viewRoot);
- } else {
- $viewRoot = '/' . $this->user . '/files';
- }
-
- $info = new \OC\Files\FileInfo(
- $viewRoot . '/' . ltrim($path, '/'),
- $this->getMockStorage(),
- null,
- ['permissions' => \OCP\Constants::PERMISSION_ALL],
- null
- );
-
- $file = new BundledFile($view, $info, $contentHandler);
-
- // beforeMethod locks
- $view->lockFile($path, ILockingProvider::LOCK_SHARED);
-
- $result = $file->putFile($fileMetadata);
-
- // afterMethod unlocks
- $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
-
- return $result;
- }
-
- private function fillMultipartContentsParserStreamWithBody($bodyString){
- $bodyStream = fopen('php://temp', 'r+');
- fwrite($bodyStream, $bodyString);
- rewind($bodyStream);
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $request->expects($this->any())
- ->method('getBody')
- ->willReturn($bodyStream);
-
- $mcp = new \OCA\DAV\Files\MultipartContentsParser($request);
- return $mcp;
- }
-
- /**
- * Simulate putting a file to the given path.
- *
- * @param string $path path to put the file into
- * @param string $viewRoot root to use for the view
- *
- * @return null|string of the PUT operaiton which is usually the etag
- */
- private function doPut($path, $fileContents, $viewRoot = null) {
- $view = \OC\Files\Filesystem::getView();
- if (!is_null($viewRoot)) {
- $view = new \OC\Files\View($viewRoot);
- } else {
- $viewRoot = '/' . $this->user . '/files';
- }
-
- $info = new \OC\Files\FileInfo(
- $viewRoot . '/' . ltrim($path, '/'),
- $this->getMockStorage(),
- null,
- ['permissions' => \OCP\Constants::PERMISSION_ALL],
- null
- );
-
- $file = new BundledFile($view, $info, null);
-
- // beforeMethod locks
- $view->lockFile($path, ILockingProvider::LOCK_SHARED);
-
- $result = $file->put($fileContents);
-
- // afterMethod unlocks
- $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
-
- return $result;
- }
-}
diff --git a/apps/dav/tests/unit/Files/MultipartContentsParserTest.php b/apps/dav/tests/unit/Files/MultipartContentsParserTest.php
deleted file mode 100644
index 6d23fe296d2..00000000000
--- a/apps/dav/tests/unit/Files/MultipartContentsParserTest.php
+++ /dev/null
@@ -1,416 +0,0 @@
-<?php
-/**
- * @author Piotr Mrowczynski <Piotr.Mrowczynski@owncloud.com>
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\unit\DAV;
-
-use Test\TestCase;
-
-class MultipartContentsParserTest extends TestCase {
- private $boundrary;
-
- protected function setUp() {
- parent::setUp();
-
- $this->boundrary = 'boundary';
-
- }
-
- /*TESTS*/
-
- /**
- * Test basic gets() functionality, that if passed string instead of resource, it should fail
- *
- * @expectedException \Sabre\DAV\Exception\BadRequest
- * @expectedExceptionMessage Unable to get request content
- */
- public function testGetsThrowWrongContents() {
- //TODO
- $bodyStream = "I am not a stream, but pretend to be";
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $request->expects($this->any())
- ->method('getBody')
- ->willReturn($bodyStream);
-
- $mcp = new \OCA\DAV\Files\MultipartContentsParser($request);
-
- $mcp->gets();
- }
-
- /**
- * Test function readHeaders(), so if passed empty string, it will return null
- *
- */
- public function testReadHeadersThrowEmptyHeader() {
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
- $mcp = new \OCA\DAV\Files\MultipartContentsParser($request);
- $mcp->readHeaders('');
- $this->assertEquals(null, $mcp->readHeaders(''));
- }
-
- /**
- * streamRead function with incorrect parameter
- *
- * @expectedException \Sabre\DAV\Exception\BadRequest
- * @expectedExceptionMessage Method streamRead cannot read contents with negative length
- */
- public function testStreamReadToStringThrowNegativeLength() {
- $bodyContent = 'blabla';
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyContent);
- //give negative length
- $multipartContentsParser->streamReadToString(-1);
- }
-
- /**
- * streamRead function with incorrect parameter
- *
- * @expectedException \Sabre\DAV\Exception\BadRequest
- * @expectedExceptionMessage Method streamRead cannot read contents with negative length
- */
- public function testStreamReadToStreamThrowNegativeLength() {
- $target = fopen('php://temp', 'r+');
- $bodyContent = 'blabla';
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyContent);
- //give negative length
- $multipartContentsParser->streamReadToStream($target,-1);
- }
-
- public function testStreamReadToString() {
- $length = 0;
- list($multipartContentsParser, $bodyString) = $this->fillMultipartContentsParserStreamWithChars($length);
- $this->assertEquals($bodyString, $multipartContentsParser->streamReadToString($length));
-
- $length = 1000;
- list($multipartContentsParser, $bodyString) = $this->fillMultipartContentsParserStreamWithChars($length);
- $this->assertEquals($bodyString, $multipartContentsParser->streamReadToString($length));
-
- $length = 8192;
- list($multipartContentsParser, $bodyString) = $this->fillMultipartContentsParserStreamWithChars($length);
- $this->assertEquals($bodyString, $multipartContentsParser->streamReadToString($length));
-
- $length = 20000;
- list($multipartContentsParser, $bodyString) = $this->fillMultipartContentsParserStreamWithChars($length);
- $this->assertEquals($bodyString, $multipartContentsParser->streamReadToString($length));
- }
-
- public function testStreamReadToStream() {
- $length = 0;
- $this->streamReadToStreamBuilder($length);
-
- $length = 1000;
- $this->streamReadToStreamBuilder($length);
-
- $length = 8192;
- $this->streamReadToStreamBuilder($length);
-
- $length = 20000;
- $this->streamReadToStreamBuilder($length);
- }
-
- private function streamReadToStreamBuilder($length) {
- $target = fopen('php://temp', 'r+');
- list($multipartContentsParser, $bodyString) = $this->fillMultipartContentsParserStreamWithChars($length);
- $this->assertEquals(true, $multipartContentsParser->streamReadToStream($target,$length));
- rewind($target);
- $this->assertEquals($bodyString, stream_get_contents($target));
- }
-
- /**
- * @expectedException \Exception
- * @expectedExceptionMessage An error appears while reading and parsing header of content part using fgets
- */
- public function testGetPartThrowFailfgets() {
- $bodyStream = fopen('php://temp', 'r+');
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $request->expects($this->any())
- ->method('getBody')
- ->willReturn($bodyStream);
-
- $mcp = $this->getMockBuilder('OCA\DAV\Files\MultipartContentsParser')
- ->setConstructorArgs(array($request))
- ->setMethods(array('gets'))
- ->getMock();
-
- $mcp->expects($this->any())
- ->method('gets')
- ->will($this->onConsecutiveCalls("--boundary\r\n", "Content-ID: 0\r\n", false));
-
- $mcp->getPartHeaders($this->boundrary);
- }
-
- /**
- * If one one the content parts does not contain boundrary, means that received wrong request
- *
- * @expectedException \Exception
- * @expectedExceptionMessage Expected boundary delimiter in content part
- */
- public function testGetPartThrowNoBoundraryFound() {
- // Calling multipletimes getPart on parts without contents should return null,null and signal immedietaly that endDelimiter was reached
- $bodyFull = "--boundary_wrong\r\n--boundary--";
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyFull);
- $multipartContentsParser->getPartHeaders($this->boundrary);
- }
-
- /**
- * Reading from request which method getBody returns false
- *
- * @expectedException \Sabre\DAV\Exception\BadRequest
- * @expectedExceptionMessage Unable to get request content
- */
- public function testStreamReadThrowWrongBody() {
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $request->expects($this->any())
- ->method('getBody')
- ->willReturn(false);
-
- $mcp = new \OCA\DAV\Files\MultipartContentsParser($request);
- $mcp->getPartHeaders($this->boundrary);
- }
-
- /**
- * Reading from request which method getBody returns false
- *
- */
- public function testMultipartContentSeekToContentLength() {
- $bodyStream = fopen('php://temp', 'r+');
- $bodyString = '';
- $length = 1000;
- for ($x = 0; $x < $length; $x++) {
- $bodyString .= 'k';
- }
- fwrite($bodyStream, $bodyString);
- rewind($bodyStream);
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $request->expects($this->any())
- ->method('getBody')
- ->willReturn($bodyStream);
-
- $mcp = new \OCA\DAV\Files\MultipartContentsParser($request);
- $this->assertEquals(true,$mcp->multipartContentSeekToContentLength($length));
- }
-
- /**
- * Test cases with wrong or incomplete boundraries
- *
- */
- public function testGetPartHeadersWrongBoundaryCases() {
- // Calling multipletimes getPart on parts without contents should return null and signal immedietaly that endDelimiter was reached
- $bodyFull = "--boundary\r\n--boundary_wrong\r\n--boundary--";
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyFull);
- $this->assertEquals(null,$multipartContentsParser->getPartHeaders($this->boundrary));
- $this->assertEquals(true,$multipartContentsParser->getEndDelimiterReached());
-
- // Test empty content
- $bodyFull = "--boundary\r\n";
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyFull);
- $this->assertEquals(null, $multipartContentsParser->getPartHeaders($this->boundrary));
- $this->assertEquals(true,$multipartContentsParser->getEndDelimiterReached());
-
- // Test empty content
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody('');
- $this->assertEquals(null, $multipartContentsParser->getPartHeaders($this->boundrary));
- $this->assertEquals(true,$multipartContentsParser->getEndDelimiterReached());
-
- // Calling multipletimes getPart on parts without contents should return null and signal immedietaly that endDelimiter was reached
- // endDelimiter should be signaled after first getPart since it will read --boundrary till it finds contents.
- $bodyFull = "--boundary\r\n--boundary\r\n--boundary--";
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyFull);
- $this->assertEquals(null,$multipartContentsParser->getPartHeaders($this->boundrary));
- $this->assertEquals(true,$multipartContentsParser->getEndDelimiterReached());
- $this->assertEquals(null,$multipartContentsParser->getPartHeaders($this->boundrary));
- $this->assertEquals(true,$multipartContentsParser->getEndDelimiterReached());
- $this->assertEquals(null,$multipartContentsParser->getPartHeaders($this->boundrary));
- $this->assertEquals(true,$multipartContentsParser->getEndDelimiterReached());
- $this->assertEquals(null,$multipartContentsParser->getPartHeaders($this->boundrary));
- $this->assertEquals(true,$multipartContentsParser->getEndDelimiterReached());
- }
-
- /**
- * Test will check if we can correctly parse headers and content using streamReadToString
- *
- */
- public function testReadHeaderBodyCorrect() {
- //multipart part will have some content bodyContent and some headers
- $bodyContent = 'blabla';
- $headers['content-length'] = '6';
- $headers['content-type'] = 'text/xml; charset=utf-8';
-
- //this part will have some arbitrary, correct headers
- $bodyFull = '--boundary'
- ."\r\nContent-Type: ".$headers['content-type']
- ."\r\nContent-length: ".$headers['content-length']
- ."\r\n\r\n"
- ."$bodyContent\r\n--boundary--";
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyFull);
-
- //parse it
- $headersParsed = $multipartContentsParser->getPartHeaders($this->boundrary);
- $bodyParsed = $multipartContentsParser->streamReadToString(6);
-
- //check if end delimiter is not reached, since we just read 6 bytes, and stopped at \r\n
- $this->assertEquals(false,$multipartContentsParser->getEndDelimiterReached());
-
- //check that we parsed correct headers
- $this->assertEquals($bodyContent, $bodyParsed);
- $this->assertEquals($headers, $headersParsed);
-
- //parse further to check if there is new part. There is no, so headers are null and delimiter reached
- $headersParsed = $multipartContentsParser->getPartHeaders($this->boundrary);
- $this->assertEquals(null,$headersParsed);
- $this->assertEquals(true,$multipartContentsParser->getEndDelimiterReached());
- }
-
- /**
- * Test will check parsing incorrect headers and content using streamReadToString
- *
- */
- public function testReadHeaderBodyIncorrect() {
-
- //multipart part will have some content bodyContent and some headers
- $bodyContent = 'blabla';
- $headers['content-length'] = '6';
- $headers['content-type'] = 'text/xml; charset=utf-8';
-
- //this part will one correct and one incorrect header
- $bodyFull = '--boundary'
- ."\r\nContent-Type: ".$headers['content-type']
- ."\r\nContent-length"
- ."\r\n\r\n"
- ."$bodyContent\r\n--boundary--";
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyFull);
-
- //parse it and expect null, since contains incorrect headers
- $headersParsed = $multipartContentsParser->getPartHeaders($this->boundrary);
- $this->assertEquals(null, $headersParsed);
- $this->assertEquals(false,$multipartContentsParser->getEndDelimiterReached());
-
- //parse further to check if next call with not read headers again
- //this should return null again and get to end of delimiter
- $headersParsed = $multipartContentsParser->getPartHeaders($this->boundrary);
- $this->assertEquals(null,$headersParsed);
- $this->assertEquals(true,$multipartContentsParser->getEndDelimiterReached());
- }
-
- /**
- * Test will check reading error in StreamReadToString
- *
- * @expectedException \Sabre\DAV\Exception\BadRequest
- * @expectedExceptionMessage Method streamRead read 20 expeceted 60
- */
- public function testReadBodyIncorrect() {
- //multipart part will have some content bodyContent and content-length header will specify to big value
- //this
- $bodyContent = 'blabla';
- $headers['content-length'] = '60';
- $headers['content-type'] = 'text/xml; charset=utf-8';
-
- //this part will have some arbitrary, correct headers
- $bodyFull = '--boundary'
- ."\r\nContent-Type: ".$headers['content-type']
- ."\r\nContent-length: ".$headers['content-length']
- ."\r\n\r\n"
- ."$bodyContent\r\n--boundary--";
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyFull);
-
- //parse headers
- $headersParsed = $multipartContentsParser->getPartHeaders($this->boundrary);
- $this->assertEquals($headers, $headersParsed);
-
- $this->assertEquals(true, array_key_exists('content-length',$headersParsed));
- $multipartContentsParser->streamReadToString($headersParsed['content-length']);
- }
-
- /**
- * Test will check reading error in StreamReadToString return false
- *
- */
- public function testReadBodyStreamIncorrect() {
- //multipart part will have some content bodyContent and content-length header will specify to big value
- //this
- $bodyContent = 'blabla';
- $headers['content-length'] = '60';
- $headers['content-type'] = 'text/xml; charset=utf-8';
-
- //this part will have some arbitrary, correct headers
- $bodyFull = '--boundary'
- ."\r\nContent-Type: ".$headers['content-type']
- ."\r\nContent-length: ".$headers['content-length']
- ."\r\n\r\n"
- ."$bodyContent\r\n--boundary--";
- $multipartContentsParser = $this->fillMultipartContentsParserStreamWithBody($bodyFull);
-
- //parse headers
- $headersParsed = $multipartContentsParser->getPartHeaders($this->boundrary);
- $this->assertEquals($headers, $headersParsed);
-
- $this->assertEquals(true, array_key_exists('content-length',$headersParsed));
- $target = fopen('php://temp', 'r+');
- $bodyParsed = $multipartContentsParser->streamReadToStream($target, $headersParsed['content-length']);
- $this->assertEquals(false, $bodyParsed);
- }
-
- /*UTILITIES*/
-
- private function fillMultipartContentsParserStreamWithChars($length){
- $bodyStream = fopen('php://temp', 'r+');
- $bodyString = '';
- for ($x = 0; $x < $length; $x++) {
- $bodyString .= 'k';
- }
- fwrite($bodyStream, $bodyString);
- rewind($bodyStream);
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $request->expects($this->any())
- ->method('getBody')
- ->willReturn($bodyStream);
-
- $mcp = new \OCA\DAV\Files\MultipartContentsParser($request);
- return array($mcp, $bodyString);
- }
-
- private function fillMultipartContentsParserStreamWithBody($bodyString){
- $bodyStream = fopen('php://temp', 'r+');
- fwrite($bodyStream, $bodyString);
- rewind($bodyStream);
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $request->expects($this->any())
- ->method('getBody')
- ->willReturn($bodyStream);
-
- $mcp = new \OCA\DAV\Files\MultipartContentsParser($request);
- return $mcp;
- }
-}
diff --git a/apps/dav/tests/unit/Files/MultipartRequestParserTest.php b/apps/dav/tests/unit/Files/MultipartRequestParserTest.php
new file mode 100644
index 00000000000..ec9e2d0a383
--- /dev/null
+++ b/apps/dav/tests/unit/Files/MultipartRequestParserTest.php
@@ -0,0 +1,281 @@
+<?php
+/**
+ * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Tests\unit\DAV;
+
+use Test\TestCase;
+use \OCA\DAV\BulkUpload\MultipartRequestParser;
+
+class MultipartRequestParserTest extends TestCase {
+ private function getValidBodyObject() {
+ return [
+ [
+ "headers" => [
+ "Content-Length" => 7,
+ "X-File-MD5" => "4f2377b4d911f7ec46325fe603c3af03",
+ "X-File-Path" => "/coucou.txt"
+ ],
+ "content" => "Coucou\n"
+ ]
+ ];
+ }
+
+ private function getMultipartParser(array $parts, array $headers = [], string $boundary = "boundary_azertyuiop"): MultipartRequestParser {
+ $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $headers = array_merge(['Content-Type' => 'multipart/related; boundary='.$boundary], $headers);
+ $request->expects($this->any())
+ ->method('getHeader')
+ ->willReturnCallback(function (string $key) use (&$headers) {
+ return $headers[$key];
+ });
+
+ $body = "";
+ foreach ($parts as $part) {
+ $body .= '--'.$boundary."\r\n";
+
+ foreach ($part['headers'] as $headerKey => $headerPart) {
+ $body .= $headerKey.": ".$headerPart."\r\n";
+ }
+
+ $body .= "\r\n";
+ $body .= $part['content']."\r\n";
+ }
+
+ $body .= '--'.$boundary."--";
+
+ $stream = fopen('php://temp','r+');
+ fwrite($stream, $body);
+ rewind($stream);
+
+ $request->expects($this->any())
+ ->method('getBody')
+ ->willReturn($stream);
+
+ return new MultipartRequestParser($request);
+ }
+
+
+ /**
+ * Test validation of the request's body type
+ */
+ public function testBodyTypeValidation() {
+ $bodyStream = "I am not a stream, but pretend to be";
+ $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $request->expects($this->any())
+ ->method('getBody')
+ ->willReturn($bodyStream);
+
+ $this->expectExceptionMessage('Body should be of type resource');
+ new MultipartRequestParser($request);
+ }
+
+ /**
+ * Test with valid request.
+ * - valid boundary
+ * - valid md5 hash
+ * - valid content-length
+ * - valid file content
+ * - valid file path
+ */
+ public function testValidRequest() {
+ $multipartParser = $this->getMultipartParser(
+ $this->getValidBodyObject()
+ );
+
+ [$headers, $content] = $multipartParser->parseNextPart();
+
+ $this->assertSame((int)$headers["content-length"], 7, "Content-Length header should be the same as provided.");
+ $this->assertSame($headers["x-file-md5"], "4f2377b4d911f7ec46325fe603c3af03", "X-File-MD5 header should be the same as provided.");
+ $this->assertSame($headers["x-file-path"], "/coucou.txt", "X-File-Path header should be the same as provided.");
+
+ $this->assertSame($content, "Coucou\n", "Content should be the same");
+ }
+
+ /**
+ * Test with invalid md5 hash.
+ */
+ public function testInvalidMd5Hash() {
+ $bodyObject = $this->getValidBodyObject();
+ $bodyObject["0"]["headers"]["X-File-MD5"] = "f2377b4d911f7ec46325fe603c3af03";
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('Computed md5 hash is incorrect.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with a null md5 hash.
+ */
+ public function testNullMd5Hash() {
+ $bodyObject = $this->getValidBodyObject();
+ unset($bodyObject["0"]["headers"]["X-File-MD5"]);
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('The X-File-MD5 header must not be null.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with a null Content-Length.
+ */
+ public function testNullContentLength() {
+ $bodyObject = $this->getValidBodyObject();
+ unset($bodyObject["0"]["headers"]["Content-Length"]);
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('The Content-Length header must not be null.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with a lower Content-Length.
+ */
+ public function testLowerContentLength() {
+ $bodyObject = $this->getValidBodyObject();
+ $bodyObject["0"]["headers"]["Content-Length"] = 6;
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('Computed md5 hash is incorrect.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with a higher Content-Length.
+ */
+ public function testHigherContentLength() {
+ $bodyObject = $this->getValidBodyObject();
+ $bodyObject["0"]["headers"]["Content-Length"] = 8;
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('Computed md5 hash is incorrect.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with wrong boundary in body.
+ */
+ public function testWrongBoundary() {
+ $bodyObject = $this->getValidBodyObject();
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/related; boundary=boundary_poiuytreza']
+ );
+
+ $this->expectExceptionMessage('Boundary not found where it should be.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with no boundary in request headers.
+ */
+ public function testNoBoundaryInHeader() {
+ $bodyObject = $this->getValidBodyObject();
+ $this->expectExceptionMessage('Error while parsing boundary in Content-Type header.');
+ $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/related']
+ );
+ }
+
+ /**
+ * Test with no boundary in the request's headers.
+ */
+ public function testNoBoundaryInBody() {
+ $bodyObject = $this->getValidBodyObject();
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/related; boundary=boundary_azertyuiop'],
+ ''
+ );
+
+ $this->expectExceptionMessage('Boundary not found where it should be.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with a boundary with quotes in the request's headers.
+ */
+ public function testBoundaryWithQuotes() {
+ $bodyObject = $this->getValidBodyObject();
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/related; boundary="boundary_azertyuiop"'],
+ );
+
+ $multipartParser->parseNextPart();
+
+ // Dummy assertion, we just want to test that the parsing works.
+ $this->assertTrue(true);
+ }
+
+ /**
+ * Test with a wrong Content-Type in the request's headers.
+ */
+ public function testWrongContentType() {
+ $bodyObject = $this->getValidBodyObject();
+ $this->expectExceptionMessage('Content-Type must be multipart/related');
+ $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/form-data; boundary="boundary_azertyuiop"'],
+ );
+ }
+
+ /**
+ * Test with a wrong key after the content type in the request's headers.
+ */
+ public function testWrongKeyInContentType() {
+ $bodyObject = $this->getValidBodyObject();
+ $this->expectExceptionMessage('Boundary is invalid');
+ $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/related; wrongkey="boundary_azertyuiop"'],
+ );
+ }
+
+ /**
+ * Test with a null Content-Type in the request's headers.
+ */
+ public function testNullContentType() {
+ $bodyObject = $this->getValidBodyObject();
+ $this->expectExceptionMessage('Content-Type can not be null');
+ $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => null],
+
+ );
+ }
+}