Merge pull request #11037 from nextcloud/trash-webui-dav

Use trashbin dav endpoint to list trash in webui
This commit is contained in:
Morris Jobke 2018-09-28 16:03:24 +02:00 committed by GitHub
commit d867f9f091
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 584 additions and 788 deletions

View File

@ -1,93 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @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/>
*
*/
use OCP\ILogger;
\OC_JSON::checkLoggedIn();
\OC_JSON::callCheck();
\OC::$server->getSession()->close();
$folder = isset($_POST['dir']) ? $_POST['dir'] : '/';
// "empty trash" command
if (isset($_POST['allfiles']) && (string)$_POST['allfiles'] === 'true'){
$deleteAll = true;
if ($folder === '/' || $folder === '') {
OCA\Files_Trashbin\Trashbin::deleteAll();
$list = array();
} else {
$list[] = $folder;
$folder = dirname($folder);
}
}
else {
$deleteAll = false;
$files = (string)$_POST['files'];
$list = json_decode($files);
}
$folder = rtrim($folder, '/') . '/';
$error = array();
$success = array();
$i = 0;
foreach ($list as $file) {
if ($folder === '/') {
$file = ltrim($file, '/');
$delimiter = strrpos($file, '.d');
$filename = substr($file, 0, $delimiter);
$timestamp = substr($file, $delimiter+2);
} else {
$filename = $folder . '/' . $file;
$timestamp = null;
}
OCA\Files_Trashbin\Trashbin::delete($filename, \OCP\User::getUser(), $timestamp);
if (OCA\Files_Trashbin\Trashbin::file_exists($filename, $timestamp)) {
$error[] = $filename;
\OCP\Util::writeLog('trashbin','can\'t delete ' . $filename . ' permanently.', ILogger::ERROR);
}
// only list deleted files if not deleting everything
else if (!$deleteAll) {
$success[$i]['filename'] = $file;
$success[$i]['timestamp'] = $timestamp;
$i++;
}
}
if ( $error ) {
$filelist = '';
foreach ( $error as $e ) {
$filelist .= $e.', ';
}
$l = \OC::$server->getL10N('files_trashbin');
$message = $l->t("Couldn't delete %s permanently", array(rtrim($filelist, ', ')));
\OC_JSON::error(array("data" => array("message" => $message,
"success" => $success, "error" => $error)));
} else {
\OC_JSON::success(array("data" => array("success" => $success)));
}

View File

@ -1,33 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Björn Schießle <bjoern@schiessle.org>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @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_JSON::checkLoggedIn();
\OC_JSON::callCheck();
\OC::$server->getSession()->close();
$trashStatus = OCA\Files_Trashbin\Trashbin::isEmpty(OCP\User::getUser());
\OC_JSON::success(array("data" => array("isEmpty" => $trashStatus)));

View File

@ -1,48 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Björn Schießle <bjoern@schiessle.org>
* @author Lukas Reschke <lukas@statuscode.ch>
* @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_JSON::checkLoggedIn();
\OC::$server->getSession()->close();
// Load the files
$dir = isset($_GET['dir']) ? (string)$_GET['dir'] : '';
$sortAttribute = isset($_GET['sort']) ? (string)$_GET['sort'] : 'name';
$sortDirection = isset($_GET['sortdirection']) ? ($_GET['sortdirection'] === 'desc') : false;
$data = array();
// make filelist
try {
$files = \OCA\Files_Trashbin\Helper::getTrashFiles($dir, \OCP\User::getUser(), $sortAttribute, $sortDirection);
} catch (Exception $e) {
http_response_code(404);
exit();
}
$encodedDir = \OCP\Util::encodePath($dir);
$data['permissions'] = 0;
$data['directory'] = $dir;
$data['files'] = \OCA\Files_Trashbin\Helper::formatFileInfos($files);
\OC_JSON::success(array('data' => $data));

View File

@ -1,98 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @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/>
*
*/
use OCP\ILogger;
\OC_JSON::checkLoggedIn();
\OC_JSON::callCheck();
\OC::$server->getSession()->close();
$dir = '/';
if (isset($_POST['dir'])) {
$dir = rtrim((string)$_POST['dir'], '/'). '/';
}
$allFiles = false;
if (isset($_POST['allfiles']) && (string)$_POST['allfiles'] === 'true') {
$allFiles = true;
$list = array();
$dirListing = true;
if ($dir === '' || $dir === '/') {
$dirListing = false;
}
foreach (OCA\Files_Trashbin\Helper::getTrashFiles($dir, \OCP\User::getUser()) as $file) {
$fileName = $file['name'];
if (!$dirListing) {
$fileName .= '.d' . $file['mtime'];
}
$list[] = $fileName;
}
} else {
$list = json_decode($_POST['files']);
}
$error = array();
$success = array();
$i = 0;
foreach ($list as $file) {
$path = $dir . '/' . $file;
if ($dir === '/') {
$file = ltrim($file, '/');
$delimiter = strrpos($file, '.d');
$filename = substr($file, 0, $delimiter);
$timestamp = substr($file, $delimiter+2);
} else {
$path_parts = pathinfo($file);
$filename = $path_parts['basename'];
$timestamp = null;
}
if ( !OCA\Files_Trashbin\Trashbin::restore($path, $filename, $timestamp) ) {
$error[] = $filename;
\OCP\Util::writeLog('trashbin', 'can\'t restore ' . $filename, ILogger::ERROR);
} else {
$success[$i]['filename'] = $file;
$success[$i]['timestamp'] = $timestamp;
$i++;
}
}
if ( $error ) {
$filelist = '';
foreach ( $error as $e ) {
$filelist .= $e.', ';
}
$l = OC::$server->getL10N('files_trashbin');
$message = $l->t("Couldn't restore %s", array(rtrim($filelist, ', ')));
\OC_JSON::error(array("data" => array("message" => $message,
"success" => $success, "error" => $error)));
} else {
\OC_JSON::success(array("data" => array("success" => $success)));
}

View File

@ -34,13 +34,3 @@ $application->registerRoutes($this, [
],
],
]);
$this->create('files_trashbin_ajax_delete', 'ajax/delete.php')
->actionInclude('files_trashbin/ajax/delete.php');
$this->create('files_trashbin_ajax_isEmpty', 'ajax/isEmpty.php')
->actionInclude('files_trashbin/ajax/isEmpty.php');
$this->create('files_trashbin_ajax_list', 'ajax/list.php')
->actionInclude('files_trashbin/ajax/list.php');
$this->create('files_trashbin_ajax_undelete', 'ajax/undelete.php')
->actionInclude('files_trashbin/ajax/undelete.php');

View File

@ -18,6 +18,7 @@ return array(
'OCA\\Files_Trashbin\\Expiration' => $baseDir . '/../lib/Expiration.php',
'OCA\\Files_Trashbin\\Helper' => $baseDir . '/../lib/Helper.php',
'OCA\\Files_Trashbin\\Hooks' => $baseDir . '/../lib/Hooks.php',
'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => $baseDir . '/../lib/Sabre/AbstractTrash.php',
'OCA\\Files_Trashbin\\Sabre\\ITrash' => $baseDir . '/../lib/Sabre/ITrash.php',
'OCA\\Files_Trashbin\\Sabre\\PropfindPlugin' => $baseDir . '/../lib/Sabre/PropfindPlugin.php',
'OCA\\Files_Trashbin\\Sabre\\RestoreFolder' => $baseDir . '/../lib/Sabre/RestoreFolder.php',

View File

@ -33,6 +33,7 @@ class ComposerStaticInitFiles_Trashbin
'OCA\\Files_Trashbin\\Expiration' => __DIR__ . '/..' . '/../lib/Expiration.php',
'OCA\\Files_Trashbin\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php',
'OCA\\Files_Trashbin\\Hooks' => __DIR__ . '/..' . '/../lib/Hooks.php',
'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrash.php',
'OCA\\Files_Trashbin\\Sabre\\ITrash' => __DIR__ . '/..' . '/../lib/Sabre/ITrash.php',
'OCA\\Files_Trashbin\\Sabre\\PropfindPlugin' => __DIR__ . '/..' . '/../lib/Sabre/PropfindPlugin.php',
'OCA\\Files_Trashbin\\Sabre\\RestoreFolder' => __DIR__ . '/..' . '/../lib/Sabre/RestoreFolder.php',

View File

@ -17,12 +17,21 @@ OCA.Trashbin = {};
*/
OCA.Trashbin.App = {
_initialized: false,
/** @type {OC.Files.Client} */
client: null,
initialize: function($el) {
initialize: function ($el) {
if (this._initialized) {
return;
}
this._initialized = true;
this.client = new OC.Files.Client({
host: OC.getHost(),
port: OC.getPort(),
root: OC.linkToRemoteBase('dav') + '/trashbin/' + OC.getCurrentUser().uid,
useHTTPS: OC.getProtocol() === 'https'
});
var urlParams = OC.Util.History.parseUrlQuery();
this.fileList = new OCA.Trashbin.FileList(
$('#app-content-trashbin'), {
@ -31,22 +40,24 @@ OCA.Trashbin.App = {
scrollTo: urlParams.scrollto,
config: OCA.Files.App.getFilesConfig(),
multiSelectMenu: [
{
name: 'restore',
displayName: t('files', 'Restore'),
iconClass: 'icon-history',
},
{
name: 'delete',
displayName: t('files', 'Delete'),
iconClass: 'icon-delete',
}
]
{
name: 'restore',
displayName: t('files', 'Restore'),
iconClass: 'icon-history',
},
{
name: 'delete',
displayName: t('files', 'Delete'),
iconClass: 'icon-delete',
}
],
client: this.client
}
);
},
_createFileActions: function() {
_createFileActions: function () {
var client = this.client;
var fileActions = new OCA.Files.FileActions();
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
var dir = context.fileList.getCurrentDirectory();
@ -62,17 +73,19 @@ OCA.Trashbin.App = {
mime: 'all',
permissions: OC.PERMISSION_READ,
iconClass: 'icon-history',
actionHandler: function(filename, context) {
actionHandler: function (filename, context) {
var fileList = context.fileList;
var tr = fileList.findFileEl(filename);
var deleteAction = tr.children("td.date").children(".action.delete");
deleteAction.removeClass('icon-delete').addClass('icon-loading-small');
$.post(OC.filePath('files_trashbin', 'ajax', 'undelete.php'), {
files: JSON.stringify([filename]),
dir: fileList.getCurrentDirectory()
},
_.bind(fileList._removeCallback, fileList)
);
fileList.showFileBusyState(tr, true);
var dir = context.fileList.getCurrentDirectory();
client.move(OC.joinPaths('trash', dir, filename), OC.joinPaths('restore', filename), true)
.then(
fileList._removeCallback.bind(fileList, [filename]),
function () {
fileList.showFileBusyState(tr, false);
OC.Notification.show(t('files_trashbin', 'Error while restoring file from trashbin'));
}
);
}
});
@ -82,33 +95,35 @@ OCA.Trashbin.App = {
mime: 'all',
permissions: OC.PERMISSION_READ,
iconClass: 'icon-delete',
render: function(actionSpec, isDefault, context) {
render: function (actionSpec, isDefault, context) {
var $actionLink = fileActions._makeActionLink(actionSpec, context);
$actionLink.attr('original-title', t('files_trashbin', 'Delete permanently'));
$actionLink.children('img').attr('alt', t('files_trashbin', 'Delete permanently'));
context.$file.find('td:last').append($actionLink);
return $actionLink;
},
actionHandler: function(filename, context) {
actionHandler: function (filename, context) {
var fileList = context.fileList;
$('.tipsy').remove();
var tr = fileList.findFileEl(filename);
var deleteAction = tr.children("td.date").children(".action.delete");
deleteAction.removeClass('icon-delete').addClass('icon-loading-small');
$.post(OC.filePath('files_trashbin', 'ajax', 'delete.php'), {
files: JSON.stringify([filename]),
dir: fileList.getCurrentDirectory()
},
_.bind(fileList._removeCallback, fileList)
);
fileList.showFileBusyState(tr, true);
var dir = context.fileList.getCurrentDirectory();
client.remove(OC.joinPaths('trash', dir, filename))
.then(
fileList._removeCallback.bind(fileList, [filename]),
function () {
fileList.showFileBusyState(tr, false);
OC.Notification.show(t('files_trashbin', 'Error while removing file from trashbin'));
}
);
}
});
return fileActions;
}
};
$(document).ready(function() {
$('#app-content-trashbin').one('show', function() {
$(document).ready(function () {
$('#app-content-trashbin').one('show', function () {
var App = OCA.Trashbin.App;
App.initialize($('#app-content-trashbin'));
// force breadcrumb init

View File

@ -9,6 +9,8 @@
*/
(function() {
var DELETED_REGEXP = new RegExp(/^(.+)\.d[0-9]+$/);
var FILENAME_PROP = '{http://nextcloud.org/ns}trashbin-filename';
var DELETION_TIME_PROP = '{http://nextcloud.org/ns}trashbin-deletion-time';
/**
* Convert a file name in the format filename.d12345 to the real file name.
@ -36,17 +38,30 @@
* @param [options] map of options
*/
var FileList = function($el, options) {
this.client = options.client;
this.initialize($el, options);
};
FileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
/** @lends OCA.Trashbin.FileList.prototype */ {
id: 'trashbin',
appName: t('files_trashbin', 'Deleted files'),
/** @type {OC.Files.Client} */
client: null,
/**
* @private
*/
initialize: function() {
this.client.addFileInfoParser(function(response, data) {
var props = response.propStat[0].properties;
return {
displayName: props[FILENAME_PROP],
mtime: parseInt(props[DELETION_TIME_PROP], 10) * 1000,
hasPreview: true,
path: data.path.substr(6), // remove leading /trash
}
});
var result = OCA.Files.FileList.prototype.initialize.apply(this, arguments);
this.$el.find('.undelete').click('click', _.bind(this._onClickRestoreSelected, this));
@ -91,23 +106,6 @@
return tr;
},
_renderRow: function(fileData, options) {
options = options || {};
// make a copy to avoid changing original object
fileData = _.extend({}, fileData);
var dir = this.getCurrentDirectory();
var dirListing = dir !== '' && dir !== '/';
// show deleted time as mtime
if (fileData.mtime) {
fileData.mtime = parseInt(fileData.mtime, 10);
}
if (!dirListing) {
fileData.displayName = fileData.name;
fileData.name = fileData.name + '.d' + Math.floor(fileData.mtime / 1000);
}
return OCA.Files.FileList.prototype._renderRow.call(this, fileData, options);
},
getAjaxUrl: function(action, params) {
var q = '';
if (params) {
@ -140,15 +138,10 @@
this.$el.find('#filestable th').toggleClass('hidden', !exists);
},
_removeCallback: function(result) {
if (result.status !== 'success') {
OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error'));
}
var files = result.data.success;
_removeCallback: function(files) {
var $el;
for (var i = 0; i < files.length; i++) {
$el = this.remove(OC.basename(files[i].filename), {updateSummary: false});
$el = this.remove(OC.basename(files[i]), {updateSummary: false});
this.fileSummary.remove({type: $el.attr('data-type'), size: $el.attr('data-size')});
}
this.fileSummary.update();
@ -158,97 +151,71 @@
_onClickRestoreSelected: function(event) {
event.preventDefault();
var self = this;
var allFiles = this.$el.find('.select-all').is(':checked');
var files = [];
var params = {};
this.fileMultiSelectMenu.toggleLoading('restore', true);
if (allFiles) {
this.showMask();
params = {
allfiles: true,
dir: this.getCurrentDirectory()
};
}
else {
files = _.pluck(this.getSelectedFiles(), 'name');
for (var i = 0; i < files.length; i++) {
var deleteAction = this.findFileEl(files[i]).children("td.date").children(".action.delete");
deleteAction.removeClass('icon-delete').addClass('icon-loading-small');
}
params = {
files: JSON.stringify(files),
dir: this.getCurrentDirectory()
};
var files = _.pluck(this.getSelectedFiles(), 'name');
for (var i = 0; i < files.length; i++) {
var tr = this.findFileEl(files[i]);
this.showFileBusyState(tr, true);
}
$.post(OC.filePath('files_trashbin', 'ajax', 'undelete.php'),
params,
function(result) {
if (allFiles) {
if (result.status !== 'success') {
OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error'));
this.fileMultiSelectMenu.toggleLoading('restore', true);
var restorePromises = files.map(function(file) {
return self.client.move(OC.joinPaths('trash', self.getCurrentDirectory(), file), OC.joinPaths('restore', file), true)
.then(
function() {
self._removeCallback([file]);
}
self.hideMask();
// simply remove all files
self.setFiles([]);
}
else {
self._removeCallback(result);
}
);
});
return Promise.all(restorePromises).then(
function() {
self.fileMultiSelectMenu.toggleLoading('restore', false);
},
function() {
OC.Notification.show(t('files_trashbin', 'Error while restoring files from trashbin'));
}
);
event.preventDefault();
},
_onClickDeleteSelected: function(event) {
event.preventDefault();
var self = this;
var allFiles = this.$el.find('.select-all').is(':checked');
var files = [];
var params = {};
if (allFiles) {
params = {
allfiles: true,
dir: this.getCurrentDirectory()
};
}
else {
files = _.pluck(this.getSelectedFiles(), 'name');
params = {
files: JSON.stringify(files),
dir: this.getCurrentDirectory()
};
var files = _.pluck(this.getSelectedFiles(), 'name');
for (var i = 0; i < files.length; i++) {
var tr = this.findFileEl(files[i]);
this.showFileBusyState(tr, true);
}
this.fileMultiSelectMenu.toggleLoading('delete', true);
if (allFiles) {
this.showMask();
}
else {
for (var i = 0; i < files.length; i++) {
var deleteAction = this.findFileEl(files[i]).children("td.date").children(".action.delete");
deleteAction.removeClass('icon-delete').addClass('icon-loading-small');
}
}
$.post(OC.filePath('files_trashbin', 'ajax', 'delete.php'),
params,
function(result) {
if (allFiles) {
if (result.status !== 'success') {
OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error'));
}
return this.client.remove(OC.joinPaths('trash', this.getCurrentDirectory()))
.then(
function() {
self.hideMask();
// simply remove all files
self.setFiles([]);
},
function() {
OC.Notification.show(t('files_trashbin', 'Error while emptying trashbin'));
}
else {
self._removeCallback(result);
}
);
} else {
this.fileMultiSelectMenu.toggleLoading('delete', true);
var deletePromises = files.map(function(file) {
return self.client.remove(OC.joinPaths('trash', self.getCurrentDirectory(), file))
.then(
function() {
self._removeCallback([file]);
}
);
});
return Promise.all(deletePromises).then(
function() {
self.fileMultiSelectMenu.toggleLoading('delete', false);
},
function() {
OC.Notification.show(t('files_trashbin', 'Error while removing files from trashbin'));
}
);
);
}
},
_onClickFile: function(event) {
@ -277,6 +244,13 @@
return true;
},
/**
* Returns list of webdav properties to request
*/
_getWebdavProperties: function() {
return [FILENAME_PROP, DELETION_TIME_PROP].concat(this.filesClient.getPropfindProperties());
},
/**
* Reloads the file list using ajax call
*
@ -290,39 +264,25 @@
if (this._reloadCall) {
this._reloadCall.abort();
}
this._reloadCall = $.ajax({
url: this.getAjaxUrl('list'),
data: {
dir : this.getCurrentDirectory(),
sort: this._sort,
sortdirection: this._sortDirection
this._reloadCall = this.client.getFolderContents(
'trash/' + this.getCurrentDirectory(), {
includeParent: false,
properties: this._getWebdavProperties()
}
});
);
var callBack = this.reloadCallback.bind(this);
return this._reloadCall.then(callBack, callBack);
},
reloadCallback: function(result) {
reloadCallback: function(status, result) {
delete this._reloadCall;
this.hideMask();
if (!result || result.status === 'error') {
// if the error is not related to folder we're trying to load, reload the page to handle logout etc
if (result.data.error === 'authentication_error' ||
result.data.error === 'token_expired' ||
result.data.error === 'application_not_enabled'
) {
OC.redirect(OC.generateUrl('apps/files'));
}
OC.Notification.show(result.data.message);
return false;
}
if (result.status === 401) {
if (status === 401) {
return false;
}
// Firewall Blocked request?
if (result.status === 403) {
if (status === 403) {
// Go home
this.changeDirectory('/');
OC.Notification.show(t('files', 'This operation is forbidden'));
@ -330,24 +290,24 @@
}
// Did share service die or something else fail?
if (result.status === 500) {
if (status === 500) {
// Go home
this.changeDirectory('/');
OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'));
return false;
}
if (result.status === 404) {
if (status === 404) {
// go back home
this.changeDirectory('/');
return false;
}
// aborted ?
if (result.status === 0){
if (status === 0){
return true;
}
this.setFiles(result.data.files);
this.setFiles(result);
return true;
},

View File

@ -0,0 +1,69 @@
<?php
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_Trashbin\Sabre;
use OCP\Files\FileInfo;
abstract class AbstractTrash implements ITrash {
/** @var FileInfo */
protected $data;
public function __construct(FileInfo $data) {
$this->data = $data;
}
public function getFilename(): string {
return $this->data->getName();
}
public function getDeletionTime(): int {
return $this->data->getMtime();
}
public function getFileId(): int {
return $this->data->getId();
}
public function getFileInfo(): FileInfo {
return $this->data;
}
public function getSize(): int {
return $this->data->getSize();
}
public function getLastModified(): int {
return $this->data->getMtime();
}
public function getContentType(): string {
return $this->data->getMimetype();
}
public function getETag(): string {
return $this->data->getEtag();
}
public function getName(): string {
return $this->data->getName();
}
}

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
*/
namespace OCA\Files_Trashbin\Sabre;
use OCP\Files\FileInfo;
interface ITrash {
public function restore(): bool;
@ -35,4 +37,6 @@ interface ITrash {
public function getSize();
public function getFileId(): int;
public function getFileInfo(): FileInfo;
}

View File

@ -25,6 +25,8 @@ declare(strict_types=1);
namespace OCA\Files_Trashbin\Sabre;
use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCP\Constants;
use OCP\IPreview;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
@ -39,7 +41,13 @@ class PropfindPlugin extends ServerPlugin {
/** @var Server */
private $server;
public function __construct() {
/** @var IPreview */
private $previewManager;
public function __construct(
IPreview $previewManager
) {
$this->previewManager = $previewManager;
}
public function initialize(Server $server) {
@ -54,11 +62,11 @@ class PropfindPlugin extends ServerPlugin {
return;
}
$propFind->handle(self::TRASHBIN_FILENAME, function() use ($node) {
$propFind->handle(self::TRASHBIN_FILENAME, function () use ($node) {
return $node->getFilename();
});
$propFind->handle(self::TRASHBIN_ORIGINAL_LOCATION, function() use ($node) {
$propFind->handle(self::TRASHBIN_ORIGINAL_LOCATION, function () use ($node) {
return $node->getOriginalLocation();
});
@ -73,6 +81,28 @@ class PropfindPlugin extends ServerPlugin {
$propFind->handle(FilesPlugin::FILEID_PROPERTYNAME, function () use ($node) {
return $node->getFileId();
});
$propFind->handle(FilesPlugin::PERMISSIONS_PROPERTYNAME, function () {
return 'GD'; // read + delete
});
$propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node) {
// add fake etag, it is only needed to identify the preview image
return $node->getLastModified();
});
$propFind->handle(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
// add fake etag, it is only needed to identify the preview image
return $node->getFileId();
});
$propFind->handle(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
return $this->previewManager->isAvailable($node->getFileInfo());
});
$propFind->handle(FilesPlugin::MOUNT_TYPE_PROPERTYNAME, function () {
return '';
});
}
}

View File

@ -27,16 +27,13 @@ use OCP\Files\FileInfo;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\IFile;
class TrashFile implements IFile, ITrash {
class TrashFile extends AbstractTrash implements IFile, ITrash {
/** @var string */
private $userId;
/** @var FileInfo */
private $data;
public function __construct(string $userId, FileInfo $data) {
$this->userId = $userId;
$this->data = $data;
parent::__construct($data);
}
public function put($data) {
@ -47,18 +44,6 @@ class TrashFile implements IFile, ITrash {
return $this->data->getStorage()->fopen($this->data->getInternalPath().'.d'.$this->getLastModified(), 'rb');
}
public function getContentType(): string {
return $this->data->getMimetype();
}
public function getETag(): string {
return $this->data->getEtag();
}
public function getSize(): int {
return $this->data->getSize();
}
public function delete() {
\OCA\Files_Trashbin\Trashbin::delete($this->data->getName(), $this->userId, $this->getLastModified());
}
@ -71,29 +56,11 @@ class TrashFile implements IFile, ITrash {
throw new Forbidden();
}
public function getLastModified(): int {
return $this->data->getMtime();
}
public function restore(): bool {
return \OCA\Files_Trashbin\Trashbin::restore($this->getName(), $this->data->getName(), $this->getLastModified());
}
public function getFilename(): string {
return $this->data->getName();
}
public function getOriginalLocation(): string {
return $this->data['extraData'];
}
public function getDeletionTime(): int {
return $this->getLastModified();
}
public function getFileId(): int {
return $this->data->getId();
}
}

View File

@ -28,16 +28,13 @@ use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\ICollection;
class TrashFolder implements ICollection, ITrash {
class TrashFolder extends AbstractTrash implements ICollection, ITrash {
/** @var string */
private $userId;
/** @var FileInfo */
private $data;
public function __construct(string $root, string $userId, FileInfo $data) {
$this->userId = $userId;
$this->data = $data;
parent::__construct($data);
}
public function createFile($name, $data = null) {
@ -100,31 +97,11 @@ class TrashFolder implements ICollection, ITrash {
throw new Forbidden();
}
public function getLastModified(): int {
return $this->data->getMtime();
}
public function restore(): bool {
return \OCA\Files_Trashbin\Trashbin::restore($this->getName(), $this->data->getName(), $this->getLastModified());
}
public function getFilename(): string {
return $this->data->getName();
}
public function getOriginalLocation(): string {
return $this->data['extraData'];
}
public function getDeletionTime(): int {
return $this->getLastModified();
}
public function getSize(): int {
return $this->data->getSize();
}
public function getFileId(): int {
return $this->data->getId();
}
}

View File

@ -27,16 +27,13 @@ use OCP\Files\FileInfo;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\IFile;
class TrashFolderFile implements IFile, ITrash {
class TrashFolderFile extends AbstractTrash implements IFile, ITrash {
/** @var string */
private $root;
/** @var string */
private $userId;
/** @var FileInfo */
private $data;
/** @var string */
private $location;
@ -46,8 +43,8 @@ class TrashFolderFile implements IFile, ITrash {
string $location) {
$this->root = $root;
$this->userId = $userId;
$this->data = $data;
$this->location = $location;
parent::__construct($data);
}
public function put($data) {
@ -58,51 +55,19 @@ class TrashFolderFile implements IFile, ITrash {
return $this->data->getStorage()->fopen($this->data->getInternalPath(), 'rb');
}
public function getContentType(): string {
return $this->data->getMimetype();
}
public function getETag(): string {
return $this->data->getEtag();
}
public function getSize(): int {
return $this->data->getSize();
}
public function delete() {
\OCA\Files_Trashbin\Trashbin::delete($this->root . '/' . $this->getName(), $this->userId, null);
}
public function getName(): string {
return $this->data->getName();
}
public function setName($name) {
throw new Forbidden();
}
public function getLastModified(): int {
return $this->data->getMtime();
}
public function restore(): bool {
return \OCA\Files_Trashbin\Trashbin::restore($this->root . '/' . $this->getName(), $this->data->getName(), null);
}
public function getFilename(): string {
return $this->data->getName();
}
public function getOriginalLocation(): string {
return $this->location . '/' . $this->getFilename();
}
public function getDeletionTime(): int {
return $this->getLastModified();
}
public function getFileId(): int {
return $this->data->getId();
}
}

View File

@ -28,7 +28,7 @@ use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\ICollection;
class TrashFolderFolder implements ICollection, ITrash {
class TrashFolderFolder extends AbstractTrash implements ICollection, ITrash {
/** @var string */
private $root;
@ -36,9 +36,6 @@ class TrashFolderFolder implements ICollection, ITrash {
/** @var string */
private $userId;
/** @var FileInfo */
private $data;
/** @var string */
private $location;
@ -48,8 +45,8 @@ class TrashFolderFolder implements ICollection, ITrash {
string $location) {
$this->root = $root;
$this->userId = $userId;
$this->data = $data;
$this->location = $location;
parent::__construct($data);
}
public function createFile($name, $data = null) {
@ -104,40 +101,15 @@ class TrashFolderFolder implements ICollection, ITrash {
\OCA\Files_Trashbin\Trashbin::delete($this->root . '/' . $this->getName(), $this->userId, null);
}
public function getName(): string {
return $this->data->getName();
}
public function setName($name) {
throw new Forbidden();
}
public function getLastModified(): int {
return $this->data->getMtime();
}
public function restore(): bool {
return \OCA\Files_Trashbin\Trashbin::restore($this->root . '/' . $this->getName(), $this->data->getName(), null);
}
public function getFilename(): string {
return $this->data->getName();
}
public function getOriginalLocation(): string {
return $this->location . '/' . $this->getFilename();
}
public function getDeletionTime(): int {
return $this->getLastModified();
}
public function getSize(): int {
return $this->data->getSize();
}
public function getFileId(): int {
return $this->data->getId();
}
}

View File

@ -1,31 +1,38 @@
/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
* ownCloud
*
* @author Vincent Petry
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
describe('OCA.Trashbin.FileList tests', function() {
var testFiles, alertStub, notificationStub, fileList;
describe('OCA.Trashbin.FileList tests', function () {
var testFiles, alertStub, notificationStub, fileList, client;
beforeEach(function() {
beforeEach(function () {
alertStub = sinon.stub(OC.dialogs, 'alert');
notificationStub = sinon.stub(OC.Notification, 'show');
client = new OC.Files.Client({
host: 'localhost',
port: 80,
root: '/remote.php/dav/trashbin/user',
useHTTPS: OC.getProtocol() === 'https'
});
// init parameters and test table elements
$('#testArea').append(
'<div id="app-content-trashbin">' +
@ -59,21 +66,24 @@ describe('OCA.Trashbin.FileList tests', function() {
testFiles = [{
id: 1,
type: 'file',
name: 'One.txt',
name: 'One.txt.d11111',
displayName: 'One.txt',
mtime: 11111000,
mimetype: 'text/plain',
etag: 'abc'
}, {
id: 2,
type: 'file',
name: 'Two.jpg',
name: 'Two.jpg.d22222',
displayName: 'Two.jpg',
mtime: 22222000,
mimetype: 'image/jpeg',
etag: 'def',
}, {
id: 3,
type: 'file',
name: 'Three.pdf',
name: 'Three.pdf.d33333',
displayName: 'Three.pdf',
mtime: 33333000,
mimetype: 'application/pdf',
etag: '123',
@ -81,7 +91,8 @@ describe('OCA.Trashbin.FileList tests', function() {
id: 4,
type: 'dir',
mtime: 99999000,
name: 'somedir',
name: 'somedir.d99999',
displayName: 'somedir',
mimetype: 'httpd/unix-directory',
etag: '456'
}];
@ -92,20 +103,21 @@ describe('OCA.Trashbin.FileList tests', function() {
$('#app-content-trashbin'), {
fileActions: fileActions,
multiSelectMenu: [{
name: 'restore',
displayName: t('files', 'Restore'),
iconClass: 'icon-history',
},
name: 'restore',
displayName: t('files', 'Restore'),
iconClass: 'icon-history',
},
{
name: 'delete',
displayName: t('files', 'Delete'),
iconClass: 'icon-delete',
}
]
],
client: client
}
);
});
afterEach(function() {
afterEach(function () {
testFiles = undefined;
fileList.destroy();
fileList = undefined;
@ -114,17 +126,17 @@ describe('OCA.Trashbin.FileList tests', function() {
notificationStub.restore();
alertStub.restore();
});
describe('Initialization', function() {
it('Sorts by mtime by default', function() {
describe('Initialization', function () {
it('Sorts by mtime by default', function () {
expect(fileList._sort).toEqual('mtime');
expect(fileList._sortDirection).toEqual('desc');
});
it('Always returns read and delete permission', function() {
it('Always returns read and delete permission', function () {
expect(fileList.getDirectoryPermissions()).toEqual(OC.PERMISSION_READ | OC.PERMISSION_DELETE);
});
});
describe('Breadcrumbs', function() {
beforeEach(function() {
describe('Breadcrumbs', function () {
beforeEach(function () {
var data = {
status: 'success',
data: {
@ -133,13 +145,13 @@ describe('OCA.Trashbin.FileList tests', function() {
}
};
fakeServer.respondWith(/\/index\.php\/apps\/files_trashbin\/ajax\/list.php\?dir=%2Fsubdir/, [
200, {
"Content-Type": "application/json"
},
JSON.stringify(data)
200, {
"Content-Type": "application/json"
},
JSON.stringify(data)
]);
});
it('links the breadcrumb to the trashbin view', function() {
it('links the breadcrumb to the trashbin view', function () {
fileList.changeDirectory('/subdir', false, true);
fakeServer.respond();
var $crumbs = fileList.$el.find('#controls .crumb');
@ -152,8 +164,8 @@ describe('OCA.Trashbin.FileList tests', function() {
.toEqual(OC.webroot + '/index.php/apps/files?view=trashbin&dir=/subdir');
});
});
describe('Rendering rows', function() {
it('renders rows with the correct data when in root', function() {
describe('Rendering rows', function () {
it('renders rows with the correct data when in root', function () {
// dir listing is false when in root
$('#dir').val('/');
fileList.setFiles(testFiles);
@ -174,7 +186,7 @@ describe('OCA.Trashbin.FileList tests', function() {
expect(fileList.findFileEl('One.txt.d11111')[0]).toEqual($tr[0]);
});
it('renders rows with the correct data when in root after calling setFiles with the same data set', function() {
it('renders rows with the correct data when in root after calling setFiles with the same data set', function () {
// dir listing is false when in root
$('#dir').val('/');
fileList.setFiles(testFiles);
@ -196,11 +208,14 @@ describe('OCA.Trashbin.FileList tests', function() {
expect(fileList.findFileEl('One.txt.d11111')[0]).toEqual($tr[0]);
});
it('renders rows with the correct data when in subdirectory', function() {
it('renders rows with the correct data when in subdirectory', function () {
// dir listing is true when in a subdir
$('#dir').val('/subdir');
fileList.setFiles(testFiles);
fileList.setFiles(testFiles.map(function (file) {
file.name = file.displayName;
return file;
}));
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(4);
@ -218,42 +233,42 @@ describe('OCA.Trashbin.FileList tests', function() {
expect(fileList.findFileEl('One.txt')[0]).toEqual($tr[0]);
});
it('does not render a size column', function() {
it('does not render a size column', function () {
expect(fileList.$el.find('tbody tr .filesize').length).toEqual(0);
});
});
describe('File actions', function() {
describe('Deleting single files', function() {
describe('File actions', function () {
describe('Deleting single files', function () {
// TODO: checks ajax call
// TODO: checks spinner
// TODO: remove item after delete
// TODO: bring back item if delete failed
});
describe('Restoring single files', function() {
describe('Restoring single files', function () {
// TODO: checks ajax call
// TODO: checks spinner
// TODO: remove item after restore
// TODO: bring back item if restore failed
});
});
describe('file previews', function() {
describe('file previews', function () {
// TODO: check that preview URL is going through files_trashbin
});
describe('loading file list', function() {
describe('loading file list', function () {
// TODO: check that ajax URL is going through files_trashbin
});
describe('breadcrumbs', function() {
describe('breadcrumbs', function () {
// TODO: test label + URL
});
describe('elementToFile', function() {
describe('elementToFile', function () {
var $tr;
beforeEach(function() {
beforeEach(function () {
fileList.setFiles(testFiles);
$tr = fileList.findFileEl('One.txt.d11111');
});
it('converts data attributes to file info structure', function() {
it('converts data attributes to file info structure', function () {
var fileInfo = fileList.elementToFile($tr);
expect(fileInfo.id).toEqual(1);
expect(fileInfo.name).toEqual('One.txt.d11111');
@ -265,8 +280,8 @@ describe('OCA.Trashbin.FileList tests', function() {
expect(fileInfo.type).toEqual('file');
});
});
describe('Global Actions', function() {
beforeEach(function() {
describe('Global Actions', function () {
beforeEach(function () {
fileList.setFiles(testFiles);
fileList.findFileEl('One.txt.d11111').find('input:checkbox').click();
fileList.findFileEl('Three.pdf.d33333').find('input:checkbox').click();
@ -274,12 +289,12 @@ describe('OCA.Trashbin.FileList tests', function() {
fileList.$el.find('.actions-selected').click();
});
afterEach(function() {
afterEach(function () {
fileList.$el.find('.actions-selected').click();
});
describe('Delete', function() {
it('Shows trashbin actions', function() {
describe('Delete', function () {
it('Shows trashbin actions', function () {
// visible because a few files were selected
expect($('.selectedActions').is(':visible')).toEqual(true);
expect($('.selectedActions .item-delete').is(':visible')).toEqual(true);
@ -301,99 +316,82 @@ describe('OCA.Trashbin.FileList tests', function() {
expect($('.selectedActions .item-delete').is(':visible')).toEqual(false);
expect($('.selectedActions .item-restore').is(':visible')).toEqual(false);
});
it('Deletes selected files when "Delete" clicked', function() {
it('Deletes selected files when "Delete" clicked', function () {
var request;
var $deleteLink = $('.selectedActions .filesSelectMenu .delete');
$deleteLink.click();
expect($deleteLink.find('.icon-loading-small').length).toEqual(1);
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(OC.webroot + '/index.php/apps/files_trashbin/ajax/delete.php');
expect(OC.parseQueryString(request.requestBody))
.toEqual({'dir': '/', files: '["One.txt.d11111","Three.pdf.d33333","somedir.d99999"]'});
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({
status: 'success',
data: {
success: [
{filename: 'One.txt.d11111'},
{filename: 'Three.pdf.d33333'},
{filename: 'somedir.d99999'}
]
}
})
);
expect($deleteLink.find('.icon-loading-small').length).toEqual(0);
expect(fileList.findFileEl('One.txt.d11111').length).toEqual(0);
expect(fileList.findFileEl('Three.pdf.d33333').length).toEqual(0);
expect(fileList.findFileEl('somedir.d99999').length).toEqual(0);
expect(fileList.findFileEl('Two.jpg.d22222').length).toEqual(1);
var promise = fileList._onClickDeleteSelected({
preventDefault: function () {
}
});
var files = ["One.txt.d11111", "Three.pdf.d33333", "somedir.d99999"];
expect(fakeServer.requests.length).toEqual(files.length);
for (var i = 0; i < files.length; i++) {
request = fakeServer.requests[i];
expect(request.url).toEqual(OC.webroot + '/remote.php/dav/trashbin/user/trash/' + files[i]);
request.respond(200);
}
return promise.then(function () {
expect(fileList.findFileEl('One.txt.d11111').length).toEqual(0);
expect(fileList.findFileEl('Three.pdf.d33333').length).toEqual(0);
expect(fileList.findFileEl('somedir.d99999').length).toEqual(0);
expect(fileList.findFileEl('Two.jpg.d22222').length).toEqual(1);
});
});
it('Deletes all files when all selected when "Delete" clicked', function() {
it('Deletes all files when all selected when "Delete" clicked', function () {
var request;
$('.select-all').click();
$('.selectedActions .filesSelectMenu .delete').click();
var promise = fileList._onClickDeleteSelected({
preventDefault: function () {
}
});
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(OC.webroot + '/index.php/apps/files_trashbin/ajax/delete.php');
expect(OC.parseQueryString(request.requestBody))
.toEqual({'dir': '/', allfiles: 'true'});
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({status: 'success'})
);
expect(fileList.isEmpty).toEqual(true);
expect(request.url).toEqual(OC.webroot + '/remote.php/dav/trashbin/user/trash');
request.respond(200);
return promise.then(function () {
expect(fileList.isEmpty).toEqual(true);
});
});
});
describe('Restore', function() {
it('Restores selected files when "Restore" clicked', function() {
describe('Restore', function () {
it('Restores selected files when "Restore" clicked', function () {
var request;
var $restoreLink = $('.selectedActions .filesSelectMenu .restore');
$restoreLink.click();
expect($restoreLink.find('.icon-loading-small').length).toEqual(1);
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(OC.webroot + '/index.php/apps/files_trashbin/ajax/undelete.php');
expect(OC.parseQueryString(request.requestBody))
.toEqual({'dir': '/', files: '["One.txt.d11111","Three.pdf.d33333","somedir.d99999"]'});
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({
status: 'success',
data: {
success: [
{filename: 'One.txt.d11111'},
{filename: 'Three.pdf.d33333'},
{filename: 'somedir.d99999'}
]
}
})
);
expect($restoreLink.find('.icon-loading-small').length).toEqual(0);
expect(fileList.findFileEl('One.txt.d11111').length).toEqual(0);
expect(fileList.findFileEl('Three.pdf.d33333').length).toEqual(0);
expect(fileList.findFileEl('somedir.d99999').length).toEqual(0);
expect(fileList.findFileEl('Two.jpg.d22222').length).toEqual(1);
var promise = fileList._onClickRestoreSelected({
preventDefault: function () {
}
});
var files = ["One.txt.d11111", "Three.pdf.d33333", "somedir.d99999"];
expect(fakeServer.requests.length).toEqual(files.length);
for (var i = 0; i < files.length; i++) {
request = fakeServer.requests[i];
expect(request.url).toEqual(OC.webroot + '/remote.php/dav/trashbin/user/trash/' + files[i]);
expect(request.requestHeaders.Destination).toEqual(OC.webroot + '/remote.php/dav/trashbin/user/restore/' + files[i]);
request.respond(200);
}
return promise.then(function() {
expect(fileList.findFileEl('One.txt.d11111').length).toEqual(0);
expect(fileList.findFileEl('Three.pdf.d33333').length).toEqual(0);
expect(fileList.findFileEl('somedir.d99999').length).toEqual(0);
expect(fileList.findFileEl('Two.jpg.d22222').length).toEqual(1);
});
});
it('Restores all files when all selected when "Restore" clicked', function() {
it('Restores all files when all selected when "Restore" clicked', function () {
var request;
$('.select-all').click();
$('.selectedActions .filesSelectMenu .restore').click();
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(OC.webroot + '/index.php/apps/files_trashbin/ajax/undelete.php');
expect(OC.parseQueryString(request.requestBody))
.toEqual({'dir': '/', allfiles: 'true'});
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({status: 'success'})
);
expect(fileList.isEmpty).toEqual(true);
var promise = fileList._onClickRestoreSelected({
preventDefault: function () {
}
});
var files = ["One.txt.d11111", "Two.jpg.d22222", "Three.pdf.d33333", "somedir.d99999"];
expect(fakeServer.requests.length).toEqual(files.length);
for (var i = 0; i < files.length; i++) {
request = fakeServer.requests[i];
expect(request.url).toEqual(OC.webroot + '/remote.php/dav/trashbin/user/trash/' + files[i]);
expect(request.requestHeaders.Destination).toEqual(OC.webroot + '/remote.php/dav/trashbin/user/restore/' + files[i]);
request.respond(200);
}
return promise.then(function() {
expect(fileList.isEmpty).toEqual(true);
});
});
});
});

View File

@ -1,10 +1,10 @@
default:
autoload:
'': %paths.base%/../features/bootstrap
'': "%paths.base%/../features/bootstrap"
suites:
default:
paths:
- %paths.base%/../features
- "%paths.base%/../features"
contexts:
- FeatureContext:
baseUrl: http://localhost:8080/ocs/
@ -27,7 +27,7 @@ default:
ocPath: ../../
federation:
paths:
- %paths.base%/../federation_features
- "%paths.base%/../federation_features"
contexts:
- FederationContext:
baseUrl: http://localhost:8080/ocs/
@ -37,7 +37,7 @@ default:
regular_user_password: 123456
capabilities:
paths:
- %paths.base%/../capabilities_features
- "%paths.base%/../capabilities_features"
contexts:
- CapabilitiesContext:
baseUrl: http://localhost:8080/ocs/
@ -47,7 +47,7 @@ default:
regular_user_password: 123456
sharees:
paths:
- %paths.base%/../sharees_features
- "%paths.base%/../sharees_features"
contexts:
- ShareesContext:
baseUrl: http://localhost:8080/ocs/
@ -57,7 +57,7 @@ default:
regular_user_password: 123456
setup:
paths:
- %paths.base%/../setup_features
- "%paths.base%/../setup_features"
contexts:
- SetupContext:
baseUrl: http://localhost:8080/ocs/
@ -67,7 +67,7 @@ default:
regular_user_password: 123456
filesdrop:
paths:
- %paths.base%/../filesdrop_features
- "%paths.base%/../filesdrop_features"
contexts:
- FilesDropContext:
baseUrl: http://localhost:8080
@ -77,7 +77,7 @@ default:
regular_user_password: 123456
ldap:
paths:
- %paths.base%/../ldap_features
- "%paths.base%/../ldap_features"
contexts:
- LDAPContext:
baseUrl: http://localhost:8080
@ -87,7 +87,7 @@ default:
regular_user_password: what_for
remoteapi:
paths:
- %paths.base%/../remoteapi_features
- "%paths.base%/../remoteapi_features"
contexts:
- FeatureContext:
baseUrl: http://localhost:8080/ocs/
@ -100,4 +100,4 @@ default:
extensions:
jarnaiz\JUnitFormatter\JUnitFormatterExtension:
filename: report.xml
outputDir: %paths.base%/../output/
outputDir: "%paths.base%/../output/"

View File

@ -20,8 +20,6 @@
*
*/
use GuzzleHttp\Client;
use GuzzleHttp\Message\ResponseInterface;
use PHPUnit\Framework\Assert;
require __DIR__ . '/../../vendor/autoload.php';
@ -30,16 +28,46 @@ require __DIR__ . '/../../vendor/autoload.php';
* Trashbin functions
*/
trait Trashbin {
use WebDav;
/**
* @When User :user empties trashbin
* @param string $user user
*/
public function emptyTrashbin($user) {
$this->asAn($user);
$body = new \Behat\Gherkin\Node\TableNode([['allfiles', 'true'], ['dir', '%2F']]);
$this->sendingToWithDirectUrl('POST', "/index.php/apps/files_trashbin/ajax/delete.php", $body);
$this->theHTTPStatusCodeShouldBe('200');
$client = $this->getSabreClient($user);
$response = $client->request('DELETE', $this->makeSabrePath($user, 'trash', 'trashbin'));
Assert::assertEquals(204, $response['statusCode']);
}
private function findFullTrashname($user, $name) {
$rootListing = $this->listTrashbinFolder($user, '/');
foreach ($rootListing as $href => $rootItem) {
if ($rootItem['{http://nextcloud.org/ns}trashbin-filename'] === $name) {
return basename($href);
}
}
return null;
}
/**
* Get the full /startofpath.dxxxx/rest/of/path from /startofpath/rest/of/path
*/
private function getFullTrashPath($user, $path) {
if ($path !== '' && $path !== '/') {
$parts = explode('/', $path);
$fullName = $this->findFullTrashname($user, $parts[1]);
if ($fullName === null) {
Assert::fail("cant find $path in trash");
return '/dummy_full_path_not_found';
}
$parts[1] = $fullName;
$path = implode('/', $parts);
}
return $path;
}
/**
@ -49,74 +77,92 @@ trait Trashbin {
* @param string $path path
* @return array response
*/
public function listTrashbinFolder($user, $path){
$this->asAn($user);
$params = '?dir=' . rawurlencode('/' . trim($path, '/'));
$this->sendingToWithDirectUrl('GET', '/index.php/apps/files_trashbin/ajax/list.php' . $params, null);
$this->theHTTPStatusCodeShouldBe('200');
public function listTrashbinFolder($user, $path) {
$path = $this->getFullTrashPath($user, $path);
$client = $this->getSabreClient($user);
$response = json_decode($this->response->getBody(), true);
return $response['data']['files'];
$results = $client->propfind($this->makeSabrePath($user, 'trash' . $path, 'trashbin'), [
'{http://nextcloud.org/ns}trashbin-filename',
'{http://nextcloud.org/ns}trashbin-original-location',
'{http://nextcloud.org/ns}trashbin-deletion-time'
], 1);
$results = array_filter($results, function (array $item) {
return isset($item['{http://nextcloud.org/ns}trashbin-filename']);
});
if ($path !== '' && $path !== '/') {
array_shift($results);
}
return $results;
}
/**
* @Then /^as "([^"]*)" the (file|folder|entry) "([^"]*)" exists in trash$/
* @Then /^user "([^"]*)" in trash folder "([^"]*)" should have the following elements$/
* @param string $user
* @param string $entryText
* @param string $path
* @param string $folder
* @param \Behat\Gherkin\Node\TableNode|null $expectedElements
*/
public function asTheFileOrFolderExistsInTrash($user, $entryText, $path) {
$path = trim($path, '/');
$sections = explode('/', $path, 2);
$firstEntry = $this->findFirstTrashedEntry($user, trim($sections[0], '/'));
Assert::assertNotNull($firstEntry);
// query was on the main element ?
if (count($sections) === 1) {
// already found, return
return;
}
$subdir = trim(dirname($sections[1]), '/');
if ($subdir !== '' && $subdir !== '.') {
$subdir = $firstEntry . '/' . $subdir;
} else {
$subdir = $firstEntry;
}
$listing = $this->listTrashbinFolder($user, $subdir);
$checkedName = basename($path);
$found = false;
foreach ($listing as $entry) {
if ($entry['name'] === $checkedName) {
$found = true;
break;
public function checkTrashContents($user, $folder, $expectedElements) {
$elementList = $this->listTrashbinFolder($user, $folder);
$trashContent = array_filter(array_map(function (array $item) {
return $item['{http://nextcloud.org/ns}trashbin-filename'];
}, $elementList));
if ($expectedElements instanceof \Behat\Gherkin\Node\TableNode) {
$elementRows = $expectedElements->getRows();
$elementsSimplified = $this->simplifyArray($elementRows);
foreach ($elementsSimplified as $expectedElement) {
$expectedElement = ltrim($expectedElement, '/');
if (array_search($expectedElement, $trashContent) === false) {
Assert::fail("$expectedElement" . " is not in trash listing");
}
}
}
Assert::assertTrue($found);
}
/**
* Finds the first trashed entry matching the given name
*
* @param string $name
* @return string|null real entry name with timestamp suffix or null if not found
* @Then /^as "([^"]*)" the (file|folder) "([^"]*)" exists in trash$/
* @param string $user
* @param string $type
* @param string $file
*/
private function findFirstTrashedEntry($user, $name) {
$listing = $this->listTrashbinFolder($user, '/');
foreach ($listing as $entry) {
if ($entry['name'] === $name) {
return $entry['name'] . '.d' . ((int)$entry['mtime'] / 1000);
}
public function checkTrashContains($user, $type, $file) {
$parent = dirname($file);
if ($parent === '.') {
$parent = '/';
}
$name = basename($file);
$elementList = $this->listTrashbinFolder($user, $parent);
$trashContent = array_filter(array_map(function (array $item) {
return $item['{http://nextcloud.org/ns}trashbin-filename'];
}, $elementList));
return null;
Assert::assertArraySubset([$name], array_values($trashContent));
}
/**
* @Then /^user "([^"]*)" in trash folder "([^"]*)" should have (\d+) elements?$/
* @param string $user
* @param string $folder
* @param \Behat\Gherkin\Node\TableNode|null $expectedElements
*/
public function checkTrashSize($user, $folder, $expectedCount) {
$elementList = $this->listTrashbinFolder($user, $folder);
Assert::assertEquals($expectedCount, count($elementList));
}
/**
* @When /^user "([^"]*)" in restores "([^"]*)" from trash$/
* @param string $user
* @param string $file
*/
public function restoreFromTrash($user, $file) {
$file = $this->getFullTrashPath($user, $file);
$url = $this->makeSabrePath($user, 'trash' . $file, 'trashbin');
$client = $this->getSabreClient($user);
$response = $client->request('MOVE', $url, null, [
'Destination' => $this->makeSabrePath($user, 'restore/' . basename($file), 'trashbin'),
]);
Assert::assertEquals(201, $response['statusCode']);
return;
}
}

View File

@ -423,8 +423,12 @@ trait WebDav {
return $parsedResponse;
}
public function makeSabrePath($user, $path) {
return $this->encodePath($this->getDavFilesPath($user) . $path);
public function makeSabrePath($user, $path, $type = 'files') {
if ($type === 'files') {
return $this->encodePath($this->getDavFilesPath($user) . $path);
} else {
return $this->encodePath($this->davPath . '/' . $type . '/' . $user . '/' . $path);
}
}
public function getSabreClient($user) {

View File

@ -1,7 +1,7 @@
Feature: sharing
Background:
Given using api version "1"
Given using old dav path
Given using new dav path
# See sharing-v1-part2.feature
@ -295,7 +295,7 @@ Feature: sharing
And user "user0" exists
And User "user0" deletes file "/textfile0.txt"
When User "user0" empties trashbin
Then the HTTP status code should be "200"
Then the HTTP status code should be "204"
Scenario: orphaned shares
Given As an "admin"
@ -392,4 +392,4 @@ Feature: sharing
And folder "/shared" of user "user0" is shared with user "user1"
When User "user1" moved file "/textfile0.txt" to "/shared/shared_file.txt"
Then as "user1" the file "/shared/shared_file.txt" exists
And as "user0" the file "/shared/shared_file.txt" exists
And as "user0" the file "/shared/shared_file.txt" exists

View File

@ -1,7 +1,7 @@
Feature: trashbin
Background:
Given using api version "1"
And using old dav path
And using new dav path
And As an "admin"
And app "files_trashbin" is enabled
@ -9,5 +9,73 @@ Feature: trashbin
Given As an "admin"
And user "user0" exists
When User "user0" deletes file "/textfile0.txt"
Then as "user0" the file "/textfile0.txt" exists in trash
Then user "user0" in trash folder "/" should have 1 element
And user "user0" in trash folder "/" should have the following elements
| textfile0.txt |
Scenario: clearing the trashbin
Given As an "admin"
And user "user0" exists
When User "user0" deletes file "/textfile0.txt"
And User "user0" empties trashbin
Then user "user0" in trash folder "/" should have 0 elements
Scenario: restoring file from trashbin
Given As an "admin"
And user "user0" exists
When User "user0" deletes file "/textfile0.txt"
And user "user0" in restores "/textfile0.txt" from trash
Then user "user0" in trash folder "/" should have 0 elements
And as "user0" the file "/textfile0.txt" exists
Scenario: deleting and restoring a folder
Given As an "admin"
And user "user0" exists
When User "user0" created a folder "/testfolder"
And User "user0" moves file "/textfile0.txt" to "/testfolder/textfile0.txt"
And as "user0" the file "/testfolder/textfile0.txt" exists
And User "user0" deletes file "/testfolder"
And user "user0" in trash folder "/" should have 1 element
And user "user0" in trash folder "/" should have the following elements
| testfolder |
And user "user0" in trash folder "/testfolder" should have 1 element
And user "user0" in trash folder "/testfolder" should have the following elements
| textfile0.txt |
And user "user0" in restores "/testfolder" from trash
Then user "user0" in trash folder "/" should have 0 elements
And as "user0" the file "/testfolder/textfile0.txt" exists
Scenario: deleting a file from a subfolder and restoring it moves it back to the subfolder
Given As an "admin"
And user "user0" exists
When User "user0" created a folder "/testfolder"
And User "user0" moves file "/textfile0.txt" to "/testfolder/textfile0.txt"
And as "user0" the file "/testfolder/textfile0.txt" exists
And User "user0" deletes file "/testfolder/textfile0.txt"
And user "user0" in trash folder "/" should have 1 element
And user "user0" in trash folder "/" should have the following elements
| textfile0.txt |
And user "user0" in restores "/textfile0.txt" from trash
Then user "user0" in trash folder "/" should have 0 elements
And as "user0" the file "/textfile0.txt" does not exist
And as "user0" the file "/testfolder/textfile0.txt" exists
Scenario: deleting and a folder and restoring a file inside it
Given As an "admin"
And user "user0" exists
When User "user0" created a folder "/testfolder"
And User "user0" moves file "/textfile0.txt" to "/testfolder/textfile0.txt"
And as "user0" the file "/testfolder/textfile0.txt" exists
And User "user0" deletes file "/testfolder"
And user "user0" in trash folder "/" should have 1 element
And user "user0" in trash folder "/" should have the following elements
| testfolder |
And user "user0" in trash folder "/testfolder" should have 1 element
And user "user0" in trash folder "/testfolder" should have the following elements
| textfile0.txt |
And user "user0" in restores "/testfolder/textfile0.txt" from trash
Then user "user0" in trash folder "/" should have 1 elements
And user "user0" in trash folder "/testfolder" should have 0 element
And as "user0" the file "/textfile0.txt" exists

View File

@ -61,6 +61,7 @@
}
this._client = new dav.Client(clientOptions);
this._client.xhrProvider = _.bind(this._xhrProvider, this);
this._fileInfoParsers = [];
};
Client.NS_OWNCLOUD = 'http://owncloud.org/ns';
@ -390,7 +391,7 @@
// extend the parsed data using the custom parsers
_.each(this._fileInfoParsers, function(parserFunction) {
_.extend(data, parserFunction(response) || {});
_.extend(data, parserFunction(response, data) || {});
});
return new FileInfo(data);