summaryrefslogtreecommitdiffstats
path: root/apps/dav
diff options
context:
space:
mode:
authorVincent Petry <pvince81@owncloud.com>2015-12-16 17:35:53 +0100
committerRoeland Jago Douma <roeland@famdouma.nl>2016-10-24 21:45:00 +0200
commit59c5be1cc572793a8d50e87ab589e1cc4cf2ed12 (patch)
tree97b096fee075115bac45d69f2e0da7af5ffb41f2 /apps/dav
parent4d01f23978549c2a33e9fdfdc3b9308cc9dc1078 (diff)
downloadnextcloud-server-59c5be1cc572793a8d50e87ab589e1cc4cf2ed12.tar.gz
nextcloud-server-59c5be1cc572793a8d50e87ab589e1cc4cf2ed12.zip
Use Webdav PUT for uploads in the web browser
- uses PUT method with jquery.fileupload for regular and public file lists - for IE and browsers that don't support it, use POST with iframe transport - implemented Sabre plugin to handle iframe transport and redirect the embedded PUT request to the proper handler - added RFC5995 POST to file collection with "add-member" property to make it possible to auto-rename conflicting file names - remove obsolete ajax/upload.php and obsolete ajax routes Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
Diffstat (limited to 'apps/dav')
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesPlugin.php51
-rw-r--r--apps/dav/lib/Connector/Sabre/IFrameTransportPlugin.php188
-rw-r--r--apps/dav/lib/Connector/Sabre/ServerFactory.php1
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php85
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/IFrameTransportPluginTest.php164
5 files changed, 487 insertions, 2 deletions
diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
index aa5bacea5bb..39d15e0c6e9 100644
--- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
@@ -46,6 +46,8 @@ use \Sabre\HTTP\ResponseInterface;
use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;
use OCP\IRequest;
+use Sabre\DAV\Exception\BadRequest;
+use OCA\DAV\Connector\Sabre\Directory;
class FilesPlugin extends ServerPlugin {
@@ -170,6 +172,8 @@ class FilesPlugin extends ServerPlugin {
$this->server = $server;
$this->server->on('propFind', array($this, 'handleGetProperties'));
$this->server->on('propPatch', array($this, 'handleUpdateProperties'));
+ // RFC5995 to add file to the collection with a suggested name
+ $this->server->on('method:POST', [$this, 'httpPost']);
$this->server->on('afterBind', array($this, 'sendFileIdHeader'));
$this->server->on('afterWriteContent', array($this, 'sendFileIdHeader'));
$this->server->on('afterMethod:GET', [$this,'httpGet']);
@@ -432,4 +436,51 @@ class FilesPlugin extends ServerPlugin {
}
}
+ /**
+ * POST operation on directories to create a new file
+ * with suggested name
+ *
+ * @param RequestInterface $request request object
+ * @param ResponseInterface $response response object
+ * @return null|false
+ */
+ public function httpPost(RequestInterface $request, ResponseInterface $response) {
+ // TODO: move this to another plugin ?
+ if (!\OC::$CLI && !\OC::$server->getRequest()->passesCSRFCheck()) {
+ throw new BadRequest('Invalid CSRF token');
+ }
+
+ list($parentPath, $name) = \Sabre\HTTP\URLUtil::splitPath($request->getPath());
+
+ // Making sure the parent node exists and is a directory
+ $node = $this->tree->getNodeForPath($parentPath);
+
+ if ($node instanceof Directory) {
+ // no Add-Member found
+ if (empty($name) || $name[0] !== '&') {
+ // suggested name required
+ throw new BadRequest('Missing suggested file name');
+ }
+
+ $name = substr($name, 1);
+
+ if (empty($name)) {
+ // suggested name required
+ throw new BadRequest('Missing suggested file name');
+ }
+
+ // make sure the name is unique
+ $name = basename(\OC_Helper::buildNotExistingFileNameForView($parentPath, $name, $this->fileView));
+
+ $node->createFile($name, $request->getBodyAsStream());
+
+ list($parentUrl, ) = \Sabre\HTTP\URLUtil::splitPath($request->getUrl());
+
+ $response->setHeader('Content-Location', $parentUrl . '/' . rawurlencode($name));
+
+ // created
+ $response->setStatus(201);
+ return false;
+ }
+ }
}
diff --git a/apps/dav/lib/Connector/Sabre/IFrameTransportPlugin.php b/apps/dav/lib/Connector/Sabre/IFrameTransportPlugin.php
new file mode 100644
index 00000000000..af6e5a62a5e
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/IFrameTransportPlugin.php
@@ -0,0 +1,188 @@
+<?php
+/**
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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\Connector\Sabre;
+
+use Sabre\DAV\IFile;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\DAV\Exception\BadRequest;
+
+/**
+ * Plugin to receive Webdav PUT through POST,
+ * mostly used as a workaround for browsers that
+ * do not support PUT upload.
+ */
+class IFrameTransportPlugin extends \Sabre\DAV\ServerPlugin {
+
+ /**
+ * @var \Sabre\DAV\Server $server
+ */
+ private $server;
+
+ /**
+ * This initializes the plugin.
+ *
+ * @param \Sabre\DAV\Server $server
+ * @return void
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+ $this->server = $server;
+ $this->server->on('method:POST', [$this, 'handlePost']);
+ }
+
+ /**
+ * POST operation
+ *
+ * @param RequestInterface $request request object
+ * @param ResponseInterface $response response object
+ * @return null|false
+ */
+ public function handlePost(RequestInterface $request, ResponseInterface $response) {
+ try {
+ return $this->processUpload($request, $response);
+ } catch (\Sabre\DAV\Exception $e) {
+ $response->setStatus($e->getHTTPCode());
+ $response->setBody(['message' => $e->getMessage()]);
+ $this->convertResponse($response);
+ return false;
+ }
+ }
+
+ /**
+ * Wrap and send response in JSON format
+ *
+ * @param ResponseInterface $response response object
+ */
+ private function convertResponse(ResponseInterface $response) {
+ if (is_resource($response->getBody())) {
+ throw new BadRequest('Cannot request binary data with iframe transport');
+ }
+
+ $responseData = json_encode([
+ 'status' => $response->getStatus(),
+ 'headers' => $response->getHeaders(),
+ 'data' => $response->getBody(),
+ ]);
+
+ // IE needs this content type
+ $response->setHeader('Content-Type', 'text/plain');
+ $response->setHeader('Content-Length', strlen($responseData));
+ $response->setStatus(200);
+ $response->setBody($responseData);
+ }
+
+ /**
+ * Process upload
+ *
+ * @param RequestInterface $request request object
+ * @param ResponseInterface $response response object
+ * @return null|false
+ */
+ private function processUpload(RequestInterface $request, ResponseInterface $response) {
+ $queryParams = $request->getQueryParameters();
+
+ if (!isset($queryParams['_method'])) {
+ return null;
+ }
+
+ $method = $queryParams['_method'];
+ if ($method !== 'PUT' && $method !== 'POST') {
+ return null;
+ }
+
+ $contentType = $request->getHeader('Content-Type');
+ list($contentType) = explode(';', $contentType);
+ if ($contentType !== 'application/x-www-form-urlencoded'
+ && $contentType !== 'multipart/form-data'
+ ) {
+ return null;
+ }
+
+ if (!isset($_FILES['files'])) {
+ return null;
+ }
+
+ // TODO: move this to another plugin ?
+ if (!\OC::$CLI && !\OC::$server->getRequest()->passesCSRFCheck()) {
+ throw new BadRequest('Invalid CSRF token');
+ }
+
+ if ($_FILES) {
+ $file = current($_FILES);
+ } else {
+ return null;
+ }
+
+ if ($file['error'][0] !== 0) {
+ throw new BadRequest('Error during upload, code ' . $file['error'][0]);
+ }
+
+ if (!\OC::$CLI && !is_uploaded_file($file['tmp_name'][0])) {
+ return null;
+ }
+
+ if (count($file['tmp_name']) > 1) {
+ throw new BadRequest('Only a single file can be uploaded');
+ }
+
+ $postData = $request->getPostData();
+ if (isset($postData['headers'])) {
+ $headers = json_decode($postData['headers'], true);
+
+ // copy safe headers into the request
+ $allowedHeaders = [
+ 'If',
+ 'If-Match',
+ 'If-None-Match',
+ 'If-Modified-Since',
+ 'If-Unmodified-Since',
+ 'Authorization',
+ ];
+
+ foreach ($allowedHeaders as $allowedHeader) {
+ if (isset($headers[$allowedHeader])) {
+ $request->setHeader($allowedHeader, $headers[$allowedHeader]);
+ }
+ }
+ }
+
+ // MEGAHACK, because the Sabre File impl reads this property directly
+ $_SERVER['CONTENT_LENGTH'] = $file['size'][0];
+ $request->setHeader('Content-Length', $file['size'][0]);
+
+ $tmpFile = $file['tmp_name'][0];
+ $resource = fopen($tmpFile, 'r');
+
+ $request->setBody($resource);
+ $request->setMethod($method);
+
+ $this->server->invokeMethod($request, $response, false);
+
+ fclose($resource);
+ unlink($tmpFile);
+
+ $this->convertResponse($response);
+
+ return false;
+ }
+
+}
diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php
index 6d9f9b1bc8b..da541abc199 100644
--- a/apps/dav/lib/Connector/Sabre/ServerFactory.php
+++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php
@@ -114,6 +114,7 @@ class ServerFactory {
// FIXME: The following line is a workaround for legacy components relying on being able to send a GET to /
$server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin());
$server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger));
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\IFrameTransportPlugin());
$server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin());
// Some WebDAV clients do require Class 2 WebDAV support (locking), since
// we do not provide locking we emulate it using a fake locking plugin.
diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
index 282a5b2f626..43ca119abff 100644
--- a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
@@ -123,7 +123,7 @@ class FilesPluginTest extends TestCase {
* @param string $class
* @return \PHPUnit_Framework_MockObject_MockObject
*/
- private function createTestNode($class) {
+ private function createTestNode($class, $path = '/dummypath') {
$node = $this->getMockBuilder($class)
->disableOriginalConstructor()
->getMock();
@@ -134,7 +134,7 @@ class FilesPluginTest extends TestCase {
$this->tree->expects($this->any())
->method('getNodeForPath')
- ->with('/dummypath')
+ ->with($path)
->will($this->returnValue($node));
$node->expects($this->any())
@@ -547,4 +547,85 @@ class FilesPluginTest extends TestCase {
$this->assertEquals("false", $propFind->get(self::HAS_PREVIEW_PROPERTYNAME));
}
+
+ public function postCreateFileProvider() {
+ $baseUrl = 'http://example.com/owncloud/remote.php/webdav/subdir/';
+ return [
+ ['test.txt', 'some file.txt', 'some file.txt', $baseUrl . 'some%20file.txt'],
+ ['some file.txt', 'some file.txt', 'some file (2).txt', $baseUrl . 'some%20file%20%282%29.txt'],
+ ];
+ }
+
+ /**
+ * @dataProvider postCreateFileProvider
+ */
+ public function testPostWithAddMember($existingFile, $wantedName, $deduplicatedName, $expectedLocation) {
+ $request = $this->getMock('Sabre\HTTP\RequestInterface');
+ $response = $this->getMock('Sabre\HTTP\ResponseInterface');
+
+ $request->expects($this->any())
+ ->method('getUrl')
+ ->will($this->returnValue('http://example.com/owncloud/remote.php/webdav/subdir/&' . $wantedName));
+
+ $request->expects($this->any())
+ ->method('getPath')
+ ->will($this->returnValue('/subdir/&' . $wantedName));
+
+ $request->expects($this->once())
+ ->method('getBodyAsStream')
+ ->will($this->returnValue(fopen('data://text/plain,hello', 'r')));
+
+ $this->view->expects($this->any())
+ ->method('file_exists')
+ ->will($this->returnCallback(function($path) use ($existingFile) {
+ return ($path === '/subdir/' . $existingFile);
+ }));
+
+ $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory', '/subdir');
+
+ $node->expects($this->once())
+ ->method('createFile')
+ ->with($deduplicatedName, $this->isType('resource'));
+
+ $response->expects($this->once())
+ ->method('setStatus')
+ ->with(201);
+ $response->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Location', $expectedLocation);
+
+ $this->assertFalse($this->plugin->httpPost($request, $response));
+ }
+
+ public function testPostOnNonDirectory() {
+ $request = $this->getMock('Sabre\HTTP\RequestInterface');
+ $response = $this->getMock('Sabre\HTTP\ResponseInterface');
+
+ $request->expects($this->any())
+ ->method('getPath')
+ ->will($this->returnValue('/subdir/test.txt/&abc'));
+
+ $this->createTestNode('\OCA\DAV\Connector\Sabre\File', '/subdir/test.txt');
+
+ $this->assertNull($this->plugin->httpPost($request, $response));
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\BadRequest
+ */
+ public function testPostWithoutAddMember() {
+ $request = $this->getMock('Sabre\HTTP\RequestInterface');
+ $response = $this->getMock('Sabre\HTTP\ResponseInterface');
+
+ $request->expects($this->any())
+ ->method('getPath')
+ ->will($this->returnValue('/subdir/&'));
+
+ $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory', '/subdir');
+
+ $node->expects($this->never())
+ ->method('createFile');
+
+ $this->plugin->httpPost($request, $response);
+ }
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/IFrameTransportPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/IFrameTransportPluginTest.php
new file mode 100644
index 00000000000..485dd1b779e
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/IFrameTransportPluginTest.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
+
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+class IFrameTransportPluginTest extends \Test\TestCase {
+
+ /**
+ * @var \Sabre\DAV\Server
+ */
+ private $server;
+
+ /**
+ * @var \OCA\DAV\Connector\Sabre\IFrameTransportPlugin
+ */
+ private $plugin;
+
+ public function setUp() {
+ parent::setUp();
+ $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->plugin = new \OCA\DAV\Connector\Sabre\IFrameTransportPlugin();
+ $this->plugin->initialize($this->server);
+ }
+
+ public function tearDown() {
+ $_FILES = null;
+ unset($_SERVER['CONTENT_LENGTH']);
+ }
+
+ public function testPutConversion() {
+ $request = $this->getMock('Sabre\HTTP\RequestInterface');
+ $response = $this->getMock('Sabre\HTTP\ResponseInterface');
+
+ $request->expects($this->once())
+ ->method('getQueryParameters')
+ ->will($this->returnValue(['_method' => 'PUT']));
+
+ $postData = [
+ 'headers' => json_encode([
+ 'If-None-Match' => '*',
+ 'Disallowed-Header' => 'test',
+ ]),
+ ];
+
+ $request->expects($this->once())
+ ->method('getPostData')
+ ->will($this->returnValue($postData));
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('Content-Type')
+ ->will($this->returnValue('multipart/form-data'));
+
+ $tmpFileName = tempnam(sys_get_temp_dir(), 'tmpfile');
+ $fh = fopen($tmpFileName, 'w');
+ fwrite($fh, 'hello');
+ fclose($fh);
+
+ $_FILES = ['files' => [
+ 'error' => [0],
+ 'tmp_name' => [$tmpFileName],
+ 'size' => [5],
+ ]];
+
+ $request->expects($this->any())
+ ->method('setHeader')
+ ->withConsecutive(
+ ['If-None-Match', '*'],
+ ['Content-Length', 5]
+ );
+
+ $request->expects($this->once())
+ ->method('setMethod')
+ ->with('PUT');
+
+ $this->server->expects($this->once())
+ ->method('invokeMethod')
+ ->with($request, $response);
+
+ // response data before conversion
+ $response->expects($this->once())
+ ->method('getHeaders')
+ ->will($this->returnValue(['Test-Response-Header' => [123]]));
+
+ $response->expects($this->any())
+ ->method('getBody')
+ ->will($this->returnValue('test'));
+
+ $response->expects($this->once())
+ ->method('getStatus')
+ ->will($this->returnValue(201));
+
+ $responseBody = json_encode([
+ 'status' => 201,
+ 'headers' => ['Test-Response-Header' => [123]],
+ 'data' => 'test',
+ ]);
+
+ // response data after conversion
+ $response->expects($this->once())
+ ->method('setBody')
+ ->with($responseBody);
+
+ $response->expects($this->once())
+ ->method('setStatus')
+ ->with(200);
+
+ $response->expects($this->any())
+ ->method('setHeader')
+ ->withConsecutive(
+ ['Content-Type', 'text/plain'],
+ ['Content-Length', strlen($responseBody)]
+ );
+
+ $this->assertFalse($this->plugin->handlePost($request, $response));
+
+ $this->assertEquals(5, $_SERVER['CONTENT_LENGTH']);
+
+ $this->assertFalse(file_exists($tmpFileName));
+ }
+
+ public function testIgnoreNonPut() {
+ $request = $this->getMock('Sabre\HTTP\RequestInterface');
+ $response = $this->getMock('Sabre\HTTP\ResponseInterface');
+
+ $request->expects($this->once())
+ ->method('getQueryParameters')
+ ->will($this->returnValue(['_method' => 'PROPFIND']));
+
+ $this->server->expects($this->never())
+ ->method('invokeMethod')
+ ->with($request, $response);
+
+ $this->assertNull($this->plugin->handlePost($request, $response));
+ }
+
+ public function testIgnoreMismatchedContentType() {
+ $request = $this->getMock('Sabre\HTTP\RequestInterface');
+ $response = $this->getMock('Sabre\HTTP\ResponseInterface');
+
+ $request->expects($this->once())
+ ->method('getQueryParameters')
+ ->will($this->returnValue(['_method' => 'PUT']));
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('Content-Type')
+ ->will($this->returnValue('text/plain'));
+
+ $this->server->expects($this->never())
+ ->method('invokeMethod')
+ ->with($request, $response);
+
+ $this->assertNull($this->plugin->handlePost($request, $response));
+ }
+}