0
0
Fork 0
mirror of https://github.com/nextcloud/server.git synced 2025-03-16 17:24:10 +00:00

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>
This commit is contained in:
Vincent Petry 2015-12-16 17:35:53 +01:00 committed by Roeland Jago Douma
parent 4d01f23978
commit 59c5be1cc5
No known key found for this signature in database
GPG key ID: 1E152838F164D13B
16 changed files with 1516 additions and 802 deletions

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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.

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -1,283 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Clark Tomlinson <fallen013@gmail.com>
* @author Florian Pritz <bluewind@xinu.at>
* @author Frank Karlitschek <frank@karlitschek.de>
* @author Individual IT Services <info@individual-it.net>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Luke Policinski <lpolicinski@gmail.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roman Geber <rgeber@owncloudapps.com>
* @author TheSFReader <TheSFReader@gmail.com>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <pvince81@owncloud.com>
*
* @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/>
*
*/
\OC::$server->getSession()->close();
// Firefox and Konqueror tries to download application/json for me. --Arthur
OCP\JSON::setContentTypeHeader('text/plain');
// If a directory token is sent along check if public upload is permitted.
// If not, check the login.
// If no token is sent along, rely on login only
$errorCode = null;
$errorFileName = null;
$l = \OC::$server->getL10N('files');
if (empty($_POST['dirToken'])) {
// The standard case, files are uploaded through logged in users :)
OCP\JSON::checkLoggedIn();
$dir = isset($_POST['dir']) ? (string)$_POST['dir'] : '';
if (!$dir || empty($dir) || $dir === false) {
OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('Unable to set upload directory.')))));
die();
}
} else {
// TODO: ideally this code should be in files_sharing/ajax/upload.php
// and the upload/file transfer code needs to be refactored into a utility method
// that could be used there
\OC_User::setIncognitoMode(true);
$publicDirectory = !empty($_POST['subdir']) ? (string)$_POST['subdir'] : '/';
$linkItem = OCP\Share::getShareByToken((string)$_POST['dirToken']);
if ($linkItem === false) {
OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('Invalid Token')))));
die();
}
if (!($linkItem['permissions'] & \OCP\Constants::PERMISSION_CREATE)) {
OCP\JSON::checkLoggedIn();
} else {
// resolve reshares
$rootLinkItem = OCP\Share::resolveReShare($linkItem);
OCP\JSON::checkUserExists($rootLinkItem['uid_owner']);
// Setup FS with owner
OC_Util::tearDownFS();
OC_Util::setupFS($rootLinkItem['uid_owner']);
// The token defines the target directory (security reasons)
$path = \OC\Files\Filesystem::getPath($linkItem['file_source']);
if($path === null) {
OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('Unable to set upload directory.')))));
die();
}
$dir = sprintf(
"/%s/%s",
$path,
$publicDirectory
);
if (!$dir || empty($dir) || $dir === false) {
OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('Unable to set upload directory.')))));
die();
}
$dir = rtrim($dir, '/');
}
}
OCP\JSON::callCheck();
// get array with current storage stats (e.g. max file size)
$storageStats = \OCA\Files\Helper::buildFileStorageStatistics($dir);
if (!isset($_FILES['files'])) {
OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('No file was uploaded. Unknown error')), $storageStats)));
exit();
}
foreach ($_FILES['files']['error'] as $error) {
if ($error != 0) {
$errors = array(
UPLOAD_ERR_OK => $l->t('There is no error, the file uploaded with success'),
UPLOAD_ERR_INI_SIZE => $l->t('The uploaded file exceeds the upload_max_filesize directive in php.ini: ')
. OC::$server->getIniWrapper()->getNumeric('upload_max_filesize'),
UPLOAD_ERR_FORM_SIZE => $l->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
UPLOAD_ERR_PARTIAL => $l->t('The uploaded file was only partially uploaded'),
UPLOAD_ERR_NO_FILE => $l->t('No file was uploaded'),
UPLOAD_ERR_NO_TMP_DIR => $l->t('Missing a temporary folder'),
UPLOAD_ERR_CANT_WRITE => $l->t('Failed to write to disk'),
);
$errorMessage = $errors[$error];
\OC::$server->getLogger()->alert("Upload error: $error - $errorMessage", array('app' => 'files'));
OCP\JSON::error(array('data' => array_merge(array('message' => $errorMessage), $storageStats)));
exit();
}
}
$files = $_FILES['files'];
$error = false;
$maxUploadFileSize = $storageStats['uploadMaxFilesize'];
$maxHumanFileSize = OCP\Util::humanFileSize($maxUploadFileSize);
$totalSize = 0;
$isReceivedShare = \OC::$server->getRequest()->getParam('isReceivedShare', false) === 'true';
// defer quota check for received shares
if (!$isReceivedShare && $storageStats['freeSpace'] >= 0) {
foreach ($files['size'] as $size) {
$totalSize += $size;
}
}
if ($maxUploadFileSize >= 0 and $totalSize > $maxUploadFileSize) {
OCP\JSON::error(array('data' => array('message' => $l->t('Not enough storage available'),
'uploadMaxFilesize' => $maxUploadFileSize,
'maxHumanFilesize' => $maxHumanFileSize)));
exit();
}
$result = array();
if (\OC\Files\Filesystem::isValidPath($dir) === true) {
$fileCount = count($files['name']);
for ($i = 0; $i < $fileCount; $i++) {
if (isset($_POST['resolution'])) {
$resolution = $_POST['resolution'];
} else {
$resolution = null;
}
if(isset($_POST['dirToken'])) {
// If it is a read only share the resolution will always be autorename
$shareManager = \OC::$server->getShareManager();
$share = $shareManager->getShareByToken((string)$_POST['dirToken']);
if (!($share->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
$resolution = 'autorename';
}
}
// target directory for when uploading folders
$relativePath = '';
if(!empty($_POST['file_directory'])) {
$relativePath = '/'.$_POST['file_directory'];
}
// $path needs to be normalized - this failed within drag'n'drop upload to a sub-folder
if ($resolution === 'autorename') {
// append a number in brackets like 'filename (2).ext'
$target = OCP\Files::buildNotExistingFileName($dir . $relativePath, $files['name'][$i]);
} else {
$target = \OC\Files\Filesystem::normalizePath($dir . $relativePath.'/'.$files['name'][$i]);
}
// relative dir to return to the client
if (isset($publicDirectory)) {
// path relative to the public root
$returnedDir = $publicDirectory . $relativePath;
} else {
// full path
$returnedDir = $dir . $relativePath;
}
$returnedDir = \OC\Files\Filesystem::normalizePath($returnedDir);
$exists = \OC\Files\Filesystem::file_exists($target);
if ($exists) {
$updatable = \OC\Files\Filesystem::isUpdatable($target);
}
if ( ! $exists || ($updatable && $resolution === 'replace' ) ) {
// upload and overwrite file
try
{
if (is_uploaded_file($files['tmp_name'][$i]) and \OC\Files\Filesystem::fromTmpFile($files['tmp_name'][$i], $target)) {
// updated max file size after upload
$storageStats = \OCA\Files\Helper::buildFileStorageStatistics($dir);
$meta = \OC\Files\Filesystem::getFileInfo($target);
if ($meta === false) {
$error = $l->t('The target folder has been moved or deleted.');
$errorCode = 'targetnotfound';
} else {
$data = \OCA\Files\Helper::formatFileInfo($meta);
$data['status'] = 'success';
$data['originalname'] = $files['name'][$i];
$data['uploadMaxFilesize'] = $maxUploadFileSize;
$data['maxHumanFilesize'] = $maxHumanFileSize;
$data['permissions'] = $meta['permissions'];
$data['directory'] = $returnedDir;
$result[] = $data;
}
} else {
$error = $l->t('Upload failed. Could not find uploaded file');
$errorFileName = $files['name'][$i];
}
} catch(Exception $ex) {
$error = $ex->getMessage();
}
} else {
// file already exists
$meta = \OC\Files\Filesystem::getFileInfo($target);
if ($meta === false) {
$error = $l->t('Upload failed. Could not get file info.');
} else {
$data = \OCA\Files\Helper::formatFileInfo($meta);
if ($updatable) {
$data['status'] = 'existserror';
} else {
$data['status'] = 'readonly';
}
$data['originalname'] = $files['name'][$i];
$data['uploadMaxFilesize'] = $maxUploadFileSize;
$data['maxHumanFilesize'] = $maxHumanFileSize;
$data['permissions'] = $meta['permissions'];
$data['directory'] = $returnedDir;
$result[] = $data;
}
}
}
} else {
$error = $l->t('Invalid directory.');
}
if ($error === false) {
// Do not leak file information if it is a read-only share
if(isset($_POST['dirToken'])) {
$shareManager = \OC::$server->getShareManager();
$share = $shareManager->getShareByToken((string)$_POST['dirToken']);
if (!($share->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
$newResults = [];
foreach($result as $singleResult) {
$fileName = $singleResult['originalname'];
$newResults['filename'] = $fileName;
$newResults['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileName);
}
$result = $newResults;
}
}
OCP\JSON::encodedPrint($result);
} else {
OCP\JSON::error(array(array('data' => array_merge(array(
'message' => $error,
'code' => $errorCode,
'filename' => $errorFileName
), $storageStats))));
}

View file

@ -75,24 +75,12 @@ $application->registerRoutes(
/** @var $this \OC\Route\Router */
$this->create('files_ajax_delete', 'ajax/delete.php')
->actionInclude('files/ajax/delete.php');
$this->create('files_ajax_download', 'ajax/download.php')
->actionInclude('files/ajax/download.php');
$this->create('files_ajax_getstoragestats', 'ajax/getstoragestats.php')
->actionInclude('files/ajax/getstoragestats.php');
$this->create('files_ajax_list', 'ajax/list.php')
->actionInclude('files/ajax/list.php');
$this->create('files_ajax_move', 'ajax/move.php')
->actionInclude('files/ajax/move.php');
$this->create('files_ajax_newfile', 'ajax/newfile.php')
->actionInclude('files/ajax/newfile.php');
$this->create('files_ajax_newfolder', 'ajax/newfolder.php')
->actionInclude('files/ajax/newfolder.php');
$this->create('files_ajax_rename', 'ajax/rename.php')
->actionInclude('files/ajax/rename.php');
$this->create('files_ajax_upload', 'ajax/upload.php')
->actionInclude('files/ajax/upload.php');
$this->create('download', 'download{file}')
->requirements(array('file' => '.*'))

View file

@ -93,6 +93,7 @@
direction: $('#defaultFileSortingDirection').val()
},
config: this._filesConfig,
enableUpload: true
}
);
this.files.initialize();

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,7 @@
* @param {Object} [options.dragOptions] drag options, disabled by default
* @param {Object} [options.folderDropOptions] folder drop options, disabled by default
* @param {boolean} [options.detailsViewEnabled=true] whether to enable details view
* @param {boolean} [options.enableUpload=false] whether to enable uploader
* @param {OC.Files.Client} [options.filesClient] files client to use
*/
var FileList = function($el, options) {
@ -188,6 +189,11 @@
_dragOptions: null,
_folderDropOptions: null,
/**
* @type OC.Uploader
*/
_uploader: null,
/**
* Initialize the file list and its components
*
@ -328,8 +334,6 @@
this.$el.find('.selectedActions a').tooltip({placement:'top'});
this.setupUploadEvents();
this.$container.on('scroll', _.bind(this._onScroll, this));
if (options.scrollTo) {
@ -338,6 +342,20 @@
});
}
if (options.enableUpload) {
// TODO: auto-create this element
var $uploadEl = this.$el.find('#file_upload_start');
if ($uploadEl.exists()) {
this._uploader = new OC.Uploader($uploadEl, {
fileList: this,
filesClient: this.filesClient,
dropZone: $('#content')
});
this.setupUploadEvents(this._uploader);
}
}
OC.Plugins.attach('OCA.Files.FileList', this);
},
@ -1420,7 +1438,10 @@
return;
}
this._setCurrentDir(targetDir, changeUrl, fileId);
return this.reload().then(function(success){
// discard finished uploads list, we'll get it through a regular reload
this._uploads = {};
this.reload().then(function(success){
if (!success) {
self.changeDirectory(currentDir, true);
}
@ -1660,6 +1681,24 @@
return OCA.Files.Files.getDownloadUrl(files, dir || this.getCurrentDirectory(), isDir);
},
getUploadUrl: function(fileName, dir) {
if (_.isUndefined(dir)) {
dir = this.getCurrentDirectory();
}
var pathSections = dir.split('/');
if (!_.isUndefined(fileName)) {
pathSections.push(fileName);
}
var encodedPath = '';
_.each(pathSections, function(section) {
if (section !== '') {
encodedPath += '/' + encodeURIComponent(section);
}
});
return OC.linkToRemoteBase('webdav') + encodedPath;
},
/**
* Generates a preview URL based on the URL space.
* @param urlSpec attributes for the URL
@ -2121,19 +2160,11 @@
)
.done(function() {
// TODO: error handling / conflicts
self.filesClient.getFileInfo(
targetPath, {
properties: self._getWebdavProperties()
}
)
.then(function(status, data) {
self.add(data, {animate: true, scrollTo: true});
deferred.resolve(status, data);
})
.fail(function(status) {
OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name}));
deferred.reject(status);
});
self.addAndFetchFileInfo(targetPath, '', {scrollTo: true}).then(function(status, data) {
deferred.resolve(status, data);
}, function() {
OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name}));
});
})
.fail(function(status) {
if (status === 412) {
@ -2174,32 +2205,19 @@
var targetPath = this.getCurrentDirectory() + '/' + name;
this.filesClient.createDirectory(targetPath)
.done(function(createStatus) {
self.filesClient.getFileInfo(
targetPath, {
properties: self._getWebdavProperties()
}
)
.done(function(status, data) {
self.add(data, {animate: true, scrollTo: true});
deferred.resolve(status, data);
})
.fail(function() {
OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: name}));
deferred.reject(createStatus);
});
.done(function() {
self.addAndFetchFileInfo(targetPath, '', {scrollTo:true}).then(function(status, data) {
deferred.resolve(status, data);
}, function() {
OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: name}));
});
})
.fail(function(createStatus) {
// method not allowed, folder might exist already
if (createStatus === 405) {
self.filesClient.getFileInfo(
targetPath, {
properties: self._getWebdavProperties()
}
)
// add it to the list, for completeness
self.addAndFetchFileInfo(targetPath, '', {scrollTo:true})
.done(function(status, data) {
// add it to the list, for completeness
self.add(data, {animate: true, scrollTo: true});
OC.Notification.showTemporary(
t('files', 'Could not create folder "{dir}" because it already exists', {dir: name})
);
@ -2221,6 +2239,60 @@
return promise;
},
/**
* Add file into the list by fetching its information from the server first.
*
* If the given directory does not match the current directory, nothing will
* be fetched.
*
* @param {String} fileName file name
* @param {String} [dir] optional directory, defaults to the current one
* @param {Object} options same options as #add
* @return {Promise} promise that resolves with the file info, or an
* already resolved Promise if no info was fetched. The promise rejects
* if the file was not found or an error occurred.
*
* @since 9.0
*/
addAndFetchFileInfo: function(fileName, dir, options) {
var self = this;
var deferred = $.Deferred();
if (_.isUndefined(dir)) {
dir = this.getCurrentDirectory();
} else {
dir = dir || '/';
}
var targetPath = OC.joinPaths(dir, fileName);
if ((OC.dirname(targetPath) || '/') !== this.getCurrentDirectory()) {
// no need to fetch information
deferred.resolve();
return deferred.promise();
}
var addOptions = _.extend({
animate: true,
scrollTo: false
}, options || {});
this.filesClient.getFileInfo(targetPath, {
properties: this._getWebdavProperties()
})
.then(function(status, data) {
// remove first to avoid duplicates
self.remove(data.name);
self.add(data, addOptions);
deferred.resolve(status, data);
})
.fail(function(status) {
OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name}));
deferred.reject(status);
});
return deferred.promise();
},
/**
* Returns whether the given file name exists in the list
*
@ -2590,18 +2662,16 @@
/**
* Setup file upload events related to the file-upload plugin
*/
setupUploadEvents: function() {
setupUploadEvents: function($uploadEl) {
var self = this;
// handle upload events
var fileUploadStart = this.$el;
var delegatedElement = '#file_upload_start';
self._uploads = {};
// detect the progress bar resize
fileUploadStart.on('resized', this._onResize);
$uploadEl.on('resized', this._onResize);
fileUploadStart.on('fileuploaddrop', delegatedElement, function(e, data) {
OC.Upload.log('filelist handle fileuploaddrop', e, data);
$uploadEl.on('fileuploaddrop', function(e, data) {
self._uploader.log('filelist handle fileuploaddrop', e, data);
if (self.$el.hasClass('hidden')) {
// do not upload to invisible lists
@ -2664,13 +2734,8 @@
}
}
});
fileUploadStart.on('fileuploadadd', function(e, data) {
OC.Upload.log('filelist handle fileuploadadd', e, data);
//finish delete if we are uploading a deleted file
if (self.deleteFiles && self.deleteFiles.indexOf(data.files[0].name)!==-1) {
self.finishDelete(null, true); //delete file before continuing
}
$uploadEl.on('fileuploadadd', function(e, data) {
self._uploader.log('filelist handle fileuploadadd', e, data);
// add ui visualization to existing folder
if (data.context && data.context.data('type') === 'dir') {
@ -2692,126 +2757,57 @@
}
}
if (!data.targetDir) {
data.targetDir = self.getCurrentDirectory();
}
});
/*
* when file upload done successfully add row to filelist
* update counter when uploading to sub folder
*/
fileUploadStart.on('fileuploaddone', function(e, data) {
OC.Upload.log('filelist handle fileuploaddone', e, data);
$uploadEl.on('fileuploaddone', function(e, data) {
self._uploader.log('filelist handle fileuploaddone', e, data);
var response;
if (typeof data.result === 'string') {
response = data.result;
} else {
// fetch response from iframe
response = data.result[0].body.innerText;
var status = data.jqXHR.status;
if (status < 200 || status >= 300) {
// error was handled in OC.Uploads already
return;
}
var result = JSON.parse(response);
if (typeof result[0] !== 'undefined' && result[0].status === 'success') {
var file = result[0];
var size = 0;
if (data.context && data.context.data('type') === 'dir') {
// update upload counter ui
var uploadText = data.context.find('.uploadtext');
var currentUploads = parseInt(uploadText.attr('currentUploads'), 10);
currentUploads -= 1;
uploadText.attr('currentUploads', currentUploads);
var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads);
if (currentUploads === 0) {
self.showFileBusyState(uploadText.closest('tr'), false);
uploadText.text(translatedText);
uploadText.hide();
} else {
uploadText.text(translatedText);
}
// update folder size
size = parseInt(data.context.data('size'), 10);
size += parseInt(file.size, 10);
data.context.attr('data-size', size);
data.context.find('td.filesize').text(humanFileSize(size));
} else {
// only append new file if uploaded into the current folder
if (file.directory !== self.getCurrentDirectory()) {
// Uploading folders actually uploads a list of files
// for which the target directory (file.directory) might lie deeper
// than the current directory
var fileDirectory = file.directory.replace('/','').replace(/\/$/, "");
var currentDirectory = self.getCurrentDirectory().replace('/','').replace(/\/$/, "") + '/';
if (currentDirectory !== '/') {
// abort if fileDirectory does not start with current one
if (fileDirectory.indexOf(currentDirectory) !== 0) {
return;
}
// remove the current directory part
fileDirectory = fileDirectory.substr(currentDirectory.length);
}
// only take the first section of the path
fileDirectory = fileDirectory.split('/');
var fd;
// if the first section exists / is a subdir
if (fileDirectory.length) {
fileDirectory = fileDirectory[0];
// See whether it is already in the list
fd = self.findFileEl(fileDirectory);
if (fd.length === 0) {
var dir = {
name: fileDirectory,
type: 'dir',
mimetype: 'httpd/unix-directory',
permissions: file.permissions,
size: 0,
id: file.parentId
};
fd = self.add(dir, {insert: true});
}
// update folder size
size = parseInt(fd.attr('data-size'), 10);
size += parseInt(file.size, 10);
fd.attr('data-size', size);
fd.find('td.filesize').text(OC.Util.humanFileSize(size));
}
return;
}
// add as stand-alone row to filelist
size = t('files', 'Pending');
if (data.files[0].size>=0) {
size=data.files[0].size;
}
//should the file exist in the list remove it
self.remove(file.name);
// create new file context
data.context = self.add(file, {animate: true});
}
var upload = self._uploader.getUpload(data);
var fileName = upload.getFileName();
var fetchInfoPromise = self.addAndFetchFileInfo(fileName, upload.getFullPath());
if (!self._uploads) {
self._uploads = {};
}
if (OC.isSamePath(OC.dirname(upload.getFullPath() + '/'), self.getCurrentDirectory())) {
self._uploads[fileName] = fetchInfoPromise;
}
});
fileUploadStart.on('fileuploadstop', function() {
OC.Upload.log('filelist handle fileuploadstop');
$uploadEl.on('fileuploadcreatedfolder', function(e, fullPath) {
self.addAndFetchFileInfo(OC.basename(fullPath), OC.dirname(fullPath));
});
$uploadEl.on('fileuploadstop', function() {
self._uploader.log('filelist handle fileuploadstop');
//cleanup uploading to a dir
var uploadText = self.$fileList.find('tr .uploadtext');
self.showFileBusyState(uploadText.closest('tr'), false);
uploadText.fadeOut();
uploadText.attr('currentUploads', 0);
// prepare list of uploaded file names in the current directory
// and discard the other ones
var promises = _.values(self._uploads);
var fileNames = _.keys(self._uploads);
self._uploads = [];
// as soon as all info is fetched
$.when.apply($, promises).then(function() {
// highlight uploaded files
self.highlightFiles(fileNames);
});
self.updateStorageStatistics();
});
fileUploadStart.on('fileuploadfail', function(e, data) {
OC.Upload.log('filelist handle fileuploadfail', e, data);
$uploadEl.on('fileuploadfail', function(e, data) {
self._uploader.log('filelist handle fileuploadfail', e, data);
self._uploads = [];
//if user pressed cancel hide upload chrome
if (data.errorThrown === 'abort') {

View file

@ -226,17 +226,6 @@
// TODO: move file list related code (upload) to OCA.Files.FileList
$('#file_action_panel').attr('activeAction', false);
// Triggers invisible file input
$('#upload a').on('click', function() {
$(this).parent().children('#file_upload_start').trigger('click');
return false;
});
// Trigger cancelling of file upload
$('#uploadprogresswrapper .stop').on('click', function() {
OC.Upload.cancelUploads();
});
// drag&drop support using jquery.fileupload
// TODO use OC.dialogs
$(document).bind('drop dragover', function (e) {

View file

@ -75,8 +75,7 @@
</table>
<input type="hidden" name="dir" id="dir" value="" />
<div class="hiddenuploadfield">
<input type="file" id="file_upload_start" class="hiddenuploadfield" name="files[]"
data-url="<?php print_unescaped(OCP\Util::linkTo('files', 'ajax/upload.php')); ?>" />
<input type="file" id="file_upload_start" class="hiddenuploadfield" name="files[]" />
</div>
<div id="editor"></div><!-- FIXME Do not use this div in your app! It is deprecated and will be removed in the future! -->
<div id="uploadsize-message" title="<?php p($l->t('Upload too large'))?>">

View file

@ -19,11 +19,11 @@
*
*/
/* global FileList */
describe('OC.Upload tests', function() {
var $dummyUploader;
var testFile;
var uploader;
var failStub;
beforeEach(function() {
testFile = {
@ -46,59 +46,64 @@ describe('OC.Upload tests', function() {
'</div>'
);
$dummyUploader = $('#file_upload_start');
uploader = new OC.Uploader($dummyUploader);
failStub = sinon.stub();
$dummyUploader.on('fileuploadfail', failStub);
});
afterEach(function() {
delete window.file_upload_param;
$dummyUploader = undefined;
failStub = undefined;
});
describe('Adding files for upload', function() {
var params;
var failStub;
beforeEach(function() {
params = OC.Upload.init();
failStub = sinon.stub();
$dummyUploader.on('fileuploadfail', failStub);
});
afterEach(function() {
params = undefined;
failStub = undefined;
});
/**
* Add file for upload
* @param file file data
*/
function addFile(file) {
return params.add.call(
/**
* Add file for upload
* @param {Array.<File>} files array of file data to simulate upload
* @return {Array.<Object>} array of uploadinfo or null if add() returned false
*/
function addFiles(uploader, files) {
return _.map(files, function(file) {
var jqXHR = {status: 200};
var uploadInfo = {
originalFiles: files,
files: [file],
jqXHR: jqXHR,
response: sinon.stub.returns(jqXHR),
submit: sinon.stub()
};
if (uploader.fileUploadParam.add.call(
$dummyUploader[0],
{},
{
originalFiles: {},
files: [file]
});
}
uploadInfo
)) {
return uploadInfo;
}
return null;
});
}
describe('Adding files for upload', function() {
it('adds file when size is below limits', function() {
var result = addFile(testFile);
expect(result).toEqual(true);
var result = addFiles(uploader, [testFile]);
expect(result[0]).not.toEqual(null);
expect(result[0].submit.calledOnce).toEqual(true);
});
it('adds file when free space is unknown', function() {
var result;
$('#free_space').val(-2);
result = addFile(testFile);
result = addFiles(uploader, [testFile]);
expect(result).toEqual(true);
expect(result[0]).not.toEqual(null);
expect(result[0].submit.calledOnce).toEqual(true);
expect(failStub.notCalled).toEqual(true);
});
it('does not add file if it exceeds upload limit', function() {
var result;
$('#upload_limit').val(1000);
result = addFile(testFile);
result = addFiles(uploader, [testFile]);
expect(result).toEqual(false);
expect(result[0]).toEqual(null);
expect(failStub.calledOnce).toEqual(true);
expect(failStub.getCall(0).args[1].textStatus).toEqual('sizeexceedlimit');
expect(failStub.getCall(0).args[1].errorThrown).toEqual(
@ -109,9 +114,9 @@ describe('OC.Upload tests', function() {
var result;
$('#free_space').val(1000);
result = addFile(testFile);
result = addFiles(uploader, [testFile]);
expect(result).toEqual(false);
expect(result[0]).toEqual(null);
expect(failStub.calledOnce).toEqual(true);
expect(failStub.getCall(0).args[1].textStatus).toEqual('notenoughspace');
expect(failStub.getCall(0).args[1].errorThrown).toEqual(
@ -120,12 +125,10 @@ describe('OC.Upload tests', function() {
});
});
describe('Upload conflicts', function() {
var oldFileList;
var conflictDialogStub;
var callbacks;
var fileList;
beforeEach(function() {
oldFileList = FileList;
$('#testArea').append(
'<div id="tableContainer">' +
'<table id="filestable">' +
@ -145,74 +148,56 @@ describe('OC.Upload tests', function() {
'</table>' +
'</div>'
);
FileList = new OCA.Files.FileList($('#tableContainer'));
fileList = new OCA.Files.FileList($('#tableContainer'));
FileList.add({name: 'conflict.txt', mimetype: 'text/plain'});
FileList.add({name: 'conflict2.txt', mimetype: 'text/plain'});
fileList.add({name: 'conflict.txt', mimetype: 'text/plain'});
fileList.add({name: 'conflict2.txt', mimetype: 'text/plain'});
conflictDialogStub = sinon.stub(OC.dialogs, 'fileexists');
callbacks = {
onNoConflicts: sinon.stub()
};
uploader = new OC.Uploader($dummyUploader, {
fileList: fileList
});
});
afterEach(function() {
conflictDialogStub.restore();
FileList.destroy();
FileList = oldFileList;
fileList.destroy();
});
it('does not show conflict dialog when no client side conflict', function() {
var selection = {
// yes, the format of uploads is weird...
uploads: [
{files: [{name: 'noconflict.txt'}]},
{files: [{name: 'noconflict2.txt'}]}
]
};
OC.Upload.checkExistingFiles(selection, callbacks);
var result = addFiles(uploader, [{name: 'noconflict.txt'}, {name: 'noconflict2.txt'}]);
expect(conflictDialogStub.notCalled).toEqual(true);
expect(callbacks.onNoConflicts.calledOnce).toEqual(true);
expect(callbacks.onNoConflicts.calledWith(selection)).toEqual(true);
expect(result[0].submit.calledOnce).toEqual(true);
expect(result[1].submit.calledOnce).toEqual(true);
});
it('shows conflict dialog when no client side conflict', function() {
var selection = {
// yes, the format of uploads is weird...
uploads: [
{files: [{name: 'conflict.txt'}]},
{files: [{name: 'conflict2.txt'}]},
{files: [{name: 'noconflict.txt'}]}
]
};
var deferred = $.Deferred();
conflictDialogStub.returns(deferred.promise());
deferred.resolve();
OC.Upload.checkExistingFiles(selection, callbacks);
var result = addFiles(uploader, [
{name: 'conflict.txt'},
{name: 'conflict2.txt'},
{name: 'noconflict.txt'}
]);
expect(conflictDialogStub.callCount).toEqual(3);
expect(conflictDialogStub.getCall(1).args[0])
.toEqual({files: [ { name: 'conflict.txt' } ]});
expect(conflictDialogStub.getCall(1).args[0].getFileName())
.toEqual('conflict.txt');
expect(conflictDialogStub.getCall(1).args[1])
.toEqual({ name: 'conflict.txt', mimetype: 'text/plain', directory: '/' });
expect(conflictDialogStub.getCall(1).args[2]).toEqual({ name: 'conflict.txt' });
// yes, the dialog must be called several times...
expect(conflictDialogStub.getCall(2).args[0]).toEqual({
files: [ { name: 'conflict2.txt' } ]
});
expect(conflictDialogStub.getCall(2).args[0].getFileName()).toEqual('conflict2.txt');
expect(conflictDialogStub.getCall(2).args[1])
.toEqual({ name: 'conflict2.txt', mimetype: 'text/plain', directory: '/' });
expect(conflictDialogStub.getCall(2).args[2]).toEqual({ name: 'conflict2.txt' });
expect(callbacks.onNoConflicts.calledOnce).toEqual(true);
expect(callbacks.onNoConflicts.calledWith({
uploads: [
{files: [{name: 'noconflict.txt'}]}
]
})).toEqual(true);
expect(result[0].submit.calledOnce).toEqual(false);
expect(result[1].submit.calledOnce).toEqual(false);
expect(result[2].submit.calledOnce).toEqual(true);
});
});
});

View file

@ -159,7 +159,8 @@ describe('OCA.Files.FileList tests', function() {
pageSizeStub = sinon.stub(OCA.Files.FileList.prototype, 'pageSize').returns(20);
fileList = new OCA.Files.FileList($('#app-content-files'), {
filesClient: filesClient,
config: filesConfig
config: filesConfig,
enableUpload: true
});
});
afterEach(function() {
@ -2441,7 +2442,7 @@ describe('OCA.Files.FileList tests', function() {
deferredInfo.resolve(
200,
new FileInfo({
new FileInfo({
path: '/subdir',
name: 'test.txt',
mimetype: 'text/plain'
@ -2501,12 +2502,70 @@ describe('OCA.Files.FileList tests', function() {
// TODO: error cases
// TODO: unique name cases
});
describe('addAndFetchFileInfo', function() {
var getFileInfoStub;
var getFileInfoDeferred;
beforeEach(function() {
getFileInfoDeferred = $.Deferred();
getFileInfoStub = sinon.stub(OC.Files.Client.prototype, 'getFileInfo');
getFileInfoStub.returns(getFileInfoDeferred.promise());
});
afterEach(function() {
getFileInfoStub.restore();
});
it('does not fetch if the given folder is not the current one', function() {
var promise = fileList.addAndFetchFileInfo('testfile.txt', '/another');
expect(getFileInfoStub.notCalled).toEqual(true);
expect(promise.state()).toEqual('resolved');
});
it('fetches info when folder is the current one', function() {
fileList.addAndFetchFileInfo('testfile.txt', '/subdir');
expect(getFileInfoStub.calledOnce).toEqual(true);
expect(getFileInfoStub.getCall(0).args[0]).toEqual('/subdir/testfile.txt');
});
it('adds file data to list when fetching is done', function() {
fileList.addAndFetchFileInfo('testfile.txt', '/subdir');
getFileInfoDeferred.resolve(200, {
name: 'testfile.txt',
size: 100
});
expect(fileList.findFileEl('testfile.txt').attr('data-size')).toEqual('100');
});
it('replaces file data to list when fetching is done', function() {
fileList.addAndFetchFileInfo('testfile.txt', '/subdir', {replace: true});
fileList.add({
name: 'testfile.txt',
size: 95
});
getFileInfoDeferred.resolve(200, {
name: 'testfile.txt',
size: 100
});
expect(fileList.findFileEl('testfile.txt').attr('data-size')).toEqual('100');
});
it('resolves promise with file data when fetching is done', function() {
var promise = fileList.addAndFetchFileInfo('testfile.txt', '/subdir', {replace: true});
getFileInfoDeferred.resolve(200, {
name: 'testfile.txt',
size: 100
});
expect(promise.state()).toEqual('resolved');
promise.then(function(status, data) {
expect(status).toEqual(200);
expect(data.name).toEqual('testfile.txt');
expect(data.size).toEqual(100);
});
});
});
/**
* Test upload mostly by testing the code inside the event handlers
* that were registered on the magic upload object
*/
describe('file upload', function() {
var $uploader;
var uploadData;
beforeEach(function() {
// note: this isn't the real blueimp file uploader from jquery.fileupload
@ -2514,14 +2573,52 @@ describe('OCA.Files.FileList tests', function() {
// test the response of the handlers
$uploader = $('#file_upload_start');
fileList.setFiles(testFiles);
// simulate data structure from jquery.upload
uploadData = {
files: [{
name: 'upload.txt'
}]
};
});
afterEach(function() {
$uploader = null;
uploadData = null;
});
describe('enableupload', function() {
it('sets up uploader when enableUpload is true', function() {
expect(fileList._uploader).toBeDefined();
});
it('does not sets up uploader when enableUpload is false', function() {
fileList.destroy();
fileList = new OCA.Files.FileList($('#app-content-files'), {
filesClient: filesClient
});
expect(fileList._uploader).toBeFalsy();
});
});
describe('adding files for upload', function() {
/**
* Simulate add event on the given target
*
* @return event object including the result
*/
function addFile(data) {
var ev = new $.Event('fileuploadadd', {});
// using triggerHandler instead of trigger so we can pass
// extra data
$uploader.triggerHandler(ev, data || {});
return ev;
}
it('sets target dir to the current directory', function() {
addFile(uploadData);
expect(uploadData.targetDir).toEqual('/subdir');
});
});
describe('dropping external files', function() {
var uploadData;
/**
* Simulate drop event on the given target
@ -2540,17 +2637,6 @@ describe('OCA.Files.FileList tests', function() {
return ev;
}
beforeEach(function() {
// simulate data structure from jquery.upload
uploadData = {
files: [{
relativePath: 'fileToUpload.txt'
}]
};
});
afterEach(function() {
uploadData = null;
});
it('drop on a tr or crumb outside file list does not trigger upload', function() {
var $anotherTable = $('<table><tbody><tr><td>outside<div class="crumb">crumb</div></td></tr></table>');
var ev;
@ -2574,12 +2660,14 @@ describe('OCA.Files.FileList tests', function() {
ev = dropOn(fileList.$fileList.find('th:first'), uploadData);
expect(ev.result).not.toEqual(false);
expect(uploadData.targetDir).toEqual('/subdir');
});
it('drop on an element on the table container triggers upload', function() {
var ev;
ev = dropOn($('#app-content-files'), uploadData);
expect(ev.result).not.toEqual(false);
expect(uploadData.targetDir).toEqual('/subdir');
});
it('drop on an element inside the table does not trigger upload if no upload permission', function() {
$('#permissions').val(0);
@ -2603,6 +2691,7 @@ describe('OCA.Files.FileList tests', function() {
ev = dropOn(fileList.findFileEl('One.txt').find('td:first'), uploadData);
expect(ev.result).not.toEqual(false);
expect(uploadData.targetDir).toEqual('/subdir');
});
it('drop on a folder row inside the table triggers upload to target folder', function() {
var ev;
@ -2635,6 +2724,97 @@ describe('OCA.Files.FileList tests', function() {
expect(fileList.findFileEl('afile.txt').find('.uploadtext').length).toEqual(0);
});
});
describe('after folder creation due to folder upload', function() {
it('fetches folder info', function() {
var fetchInfoStub = sinon.stub(fileList, 'addAndFetchFileInfo');
var ev = new $.Event('fileuploadcreatedfolder', {});
$uploader.triggerHandler(ev, '/subdir/newfolder');
expect(fetchInfoStub.calledOnce).toEqual(true);
expect(fetchInfoStub.getCall(0).args[0]).toEqual('newfolder');
expect(fetchInfoStub.getCall(0).args[1]).toEqual('/subdir');
fetchInfoStub.restore();
});
});
describe('after upload', function() {
var fetchInfoStub;
beforeEach(function() {
fetchInfoStub = sinon.stub(fileList, 'addAndFetchFileInfo');
});
afterEach(function() {
fetchInfoStub.restore();
});
function createUpload(name, dir) {
var data = {
files: [{
name: name
}],
upload: {
getFileName: sinon.stub().returns(name),
getFullPath: sinon.stub().returns(dir)
},
jqXHR: {
status: 200
}
}
return data;
}
/**
* Simulate add event on the given target
*
* @return event object including the result
*/
function addFile(data) {
var ev = new $.Event('fileuploaddone', {});
// using triggerHandler instead of trigger so we can pass
// extra data
var deferred = $.Deferred();
fetchInfoStub.returns(deferred.promise());
$uploader.triggerHandler(ev, data || {});
return deferred;
}
it('fetches file info', function() {
addFile(createUpload('upload.txt', '/subdir'));
expect(fetchInfoStub.calledOnce).toEqual(true);
expect(fetchInfoStub.getCall(0).args[0]).toEqual('upload.txt');
expect(fetchInfoStub.getCall(0).args[1]).toEqual('/subdir');
});
it('highlights all uploaded files after all fetches are done', function() {
var highlightStub = sinon.stub(fileList, 'highlightFiles');
var def1 = addFile(createUpload('upload.txt', '/subdir'));
var def2 = addFile(createUpload('upload2.txt', '/subdir'));
var def3 = addFile(createUpload('upload3.txt', '/another'));
$uploader.triggerHandler(new $.Event('fileuploadstop'));
expect(highlightStub.notCalled).toEqual(true);
def1.resolve();
expect(highlightStub.notCalled).toEqual(true);
def2.resolve();
def3.resolve();
expect(highlightStub.calledOnce).toEqual(true);
expect(highlightStub.getCall(0).args[0]).toEqual(['upload.txt', 'upload2.txt']);
highlightStub.restore();
});
it('queries storage stats', function() {
var statStub = sinon.stub(fileList, 'updateStorageStatistics');
addFile(createUpload('upload.txt', '/subdir'));
expect(statStub.notCalled).toEqual(true);
$uploader.triggerHandler(new $.Event('fileuploadstop'));
expect(statStub.calledOnce).toEqual(true);
statStub.restore();
});
});
});
describe('Handling errors', function () {
var deferredList;

View file

@ -72,7 +72,8 @@ OCA.Sharing.PublicApp = {
folderDropOptions: folderDropOptions,
fileActions: fileActions,
detailsViewEnabled: false,
filesClient: filesClient
filesClient: filesClient,
enableUpload: true
}
);
this.files = OCA.Files.Files;
@ -170,6 +171,30 @@ OCA.Sharing.PublicApp = {
return OC.generateUrl('/s/' + token + '/download') + '?' + OC.buildQueryString(params);
};
this.fileList.getUploadUrl = function(fileName, dir) {
if (_.isUndefined(dir)) {
dir = this.getCurrentDirectory();
}
var pathSections = dir.split('/');
if (!_.isUndefined(fileName)) {
pathSections.push(fileName);
}
var encodedPath = '';
_.each(pathSections, function(section) {
if (section !== '') {
encodedPath += '/' + encodeURIComponent(section);
}
});
var base = '';
if (!this._uploader.isXHRUpload()) {
// also add auth in URL due to POST workaround
base = OC.getProtocol() + '://' + token + '@' + OC.getHost() + (OC.getPort() ? ':' + OC.getPort() : '');
}
return base + OC.getRootPath() + '/public.php/webdav' + encodedPath;
};
this.fileList.getAjaxUrl = function (action, params) {
params = params || {};
params.t = token;
@ -203,20 +228,12 @@ OCA.Sharing.PublicApp = {
OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
};
var file_upload_start = $('#file_upload_start');
file_upload_start.on('fileuploadadd', function (e, data) {
var fileDirectory = '';
if (typeof data.files[0].relativePath !== 'undefined') {
fileDirectory = data.files[0].relativePath;
this.fileList._uploader.on('fileuploadadd', function(e, data) {
if (!data.headers) {
data.headers = {};
}
// Add custom data to the upload handler
data.formData = {
requesttoken: $('#publicUploadRequestToken').val(),
dirToken: $('#dirToken').val(),
subdir: data.targetDir || self.fileList.getCurrentDirectory(),
file_directory: fileDirectory
};
data.headers.Authorization = 'Basic ' + btoa(token + ':');
});
// do not allow sharing from the public page

View file

@ -87,10 +87,18 @@ describe('OCA.Sharing.PublicApp tests', function() {
});
it('Uses public webdav endpoint', function() {
App._initialized = false;
fakeServer.restore();
window.fakeServer = sinon.fakeServer.create();
// uploader function messes up with fakeServer
var uploaderDetectStub = sinon.stub(OC.Uploader.prototype, '_supportAjaxUploadWithProgress');
App.initialize($('#preview'));
expect(fakeServer.requests.length).toEqual(1);
expect(fakeServer.requests[0].method).toEqual('PROPFIND');
expect(fakeServer.requests[0].url).toEqual('https://example.com:9876/owncloud/public.php/webdav/subdir');
expect(fakeServer.requests[0].requestHeaders.Authorization).toEqual('Basic c2g0dG9rOm51bGw=');
uploaderDetectStub.restore();
});
describe('Download Url', function() {
@ -118,5 +126,20 @@ describe('OCA.Sharing.PublicApp tests', function() {
.toEqual(OC.webroot + '/index.php/apps/files_sharing/ajax/test.php?a=1&b=x%20y&t=sh4tok');
});
});
describe('Upload Url', function() {
var fileList;
beforeEach(function() {
fileList = App.fileList;
});
it('returns correct upload URL', function() {
expect(fileList.getUploadUrl('some file.txt'))
.toEqual('/owncloud/public.php/webdav/subdir/some%20file.txt');
});
it('returns correct upload URL with specified dir', function() {
expect(fileList.getUploadUrl('some file.txt', 'sub'))
.toEqual('/owncloud/public.php/webdav/sub/some%20file.txt');
});
});
});
});