mirror of https://github.com/nextcloud/server.git
Clean BulkUpload plugin
Signed-off-by: Louis Chemineau <louis@chmn.me>pull/28991/head
parent
dd938dadef
commit
def983dc7e
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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;
|
||||
// }
|
||||
}
|
@ -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;
|
||||
// }
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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],
|
||||
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue