Skip to content

Commit

Permalink
Merge pull request #32250 from owncloud/feature/webdav-locking-frontend
Browse files Browse the repository at this point in the history
Feature/webdav locking frontend
  • Loading branch information
Vincent Petry authored Dec 20, 2018
2 parents 92d68bb + 7cf5737 commit 5182ad0
Show file tree
Hide file tree
Showing 21 changed files with 2,146 additions and 4 deletions.
9 changes: 9 additions & 0 deletions .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,15 @@ matrix:
OWNCLOUD_LOG: true
INSTALL_TESTING-APP: true

- PHP_VERSION: 7.1
TEST_SUITE: selenium
BEHAT_SUITE: webUIWebdavLocks
DB_TYPE: mariadb
USE_SERVER: true
INSTALL_SERVER: true
CHOWN_SERVER: true
OWNCLOUD_LOG: true

# caldav test
- PHP_VERSION: 7.1
TEST_SUITE: caldav
Expand Down
3 changes: 3 additions & 0 deletions apps/files/js/file-upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,9 @@ OC.Uploader.prototype = _.extend({
OC.Notification.show(t('files', 'Target folder does not exist any more'), {type: 'error'});
}
self.cancelUploads();
} else if (status === 423) {
// not enough space
OC.Notification.show(t('files', 'The file {file} is currently locked, please try again later', {file: upload.getFileName()}), {type: 'error'});
} else if (status === 507) {
// not enough space
OC.Notification.show(t('files', 'Not enough free space'), {type: 'error'});
Expand Down
21 changes: 18 additions & 3 deletions apps/files/js/filelist.js
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,10 @@
OC.Notification.show(t('files', 'Could not move "{file}", target exists',
{file: fileName}, null, {escape: false}), {type: 'error'}
);
} else if (status === 423) {
OC.Notification.show(t('files', 'Could not move "{file}" because either the file or the target are locked.',
{file: fileName, message: result.message}), {type: 'error'}
);
} else if (result != null && typeof result.message !== "undefined") {
OC.Notification.show(t('files', 'Could not move "{file}": {message}',
{file: fileName, message: result.message}), {type: 'error'}
Expand Down Expand Up @@ -2132,6 +2136,11 @@
type: 'error'
}
);
} else if (status === 423) {
// restore the item to its previous state
OC.Notification.show(t('files', 'The file "{fileName}" is locked and can not be renamed.',
{fileName: oldName}), {type: 'error'}
);
} else {
// restore the item to its previous state
OC.Notification.show(t('files', 'Could not rename "{fileName}"',
Expand Down Expand Up @@ -2461,9 +2470,15 @@
removeFromList(file);
} else {
// only reset the spinner for that one file
OC.Notification.show(t('files', 'Error deleting file "{fileName}".',
{fileName: file}), {type: 'error'}
);
if (status === 423) {
OC.Notification.show(t('files', 'The file "{fileName}" is locked and cannot be deleted.',
{fileName: file}), {type: 'error'}
);
} else {
OC.Notification.show(t('files', 'Error deleting file "{fileName}".',
{fileName: file}), {type: 'error'}
);
}
var deleteAction = self.findFileEl(file).find('.action.delete');
deleteAction.removeClass('icon-loading-small').addClass('icon-delete');
self.showFileBusyState(files, false);
Expand Down
165 changes: 165 additions & 0 deletions apps/files/js/filelockplugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright (c) 2018 Thomas Müller <thomas.mueller@tmit.eu>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/

(function(OCA) {
var NS_DAV = OC.Files.Client.NS_DAV;

var TEMPLATE_LOCK_STATUS_ACTION =
'<a class="action action-lock-status permanent" title="{{message}}" href="#">' +
'<span class="icon icon-lock-open" />' +
'</a>';

/**
* Parses an XML lock node
*
* @param {Node} xmlvalue node to parse
* @return {Object} parsed values in associative array
*/
function parseLockNode(xmlvalue) {
return {
lockscope: getChildNodeLocalName(xmlvalue.getElementsByTagNameNS(NS_DAV, 'lockscope')[0]),
locktype: getChildNodeLocalName(xmlvalue.getElementsByTagNameNS(NS_DAV, 'locktype')[0]),
lockroot: getHrefNodeContents(xmlvalue.getElementsByTagNameNS(NS_DAV, 'lockroot')[0]),
// string, as it can also be "infinite"
depth: xmlvalue.getElementsByTagNameNS(NS_DAV, 'depth')[0].textContent,
timeout: xmlvalue.getElementsByTagNameNS(NS_DAV, 'timeout')[0].textContent,
locktoken: getHrefNodeContents(xmlvalue.getElementsByTagNameNS(NS_DAV, 'locktoken')[0]),
owner: xmlvalue.getElementsByTagNameNS(NS_DAV, 'owner')[0].textContent
};
}

function getHrefNodeContents(node) {
var nodes = node.getElementsByTagNameNS(NS_DAV, 'href');
if (!nodes.length) {
return null;
}
return nodes[0].textContent;
}

/**
* Filter out text nodes from a list of XML nodes
*
* @param {Array.<Node>} nodes nodes to filter
* @return {Array.<Node>} filtered array of nodes
*/
function getChildNodeLocalName(node) {
for (var i = 0; i < node.childNodes.length; i++) {
// skip pure text nodes
if (node.childNodes[i].nodeType === 1) {
return node.childNodes[i].localName;
}
}
return null;
}

OCA.Files = OCA.Files || {};

/**
* @namespace OCA.Files.LockPlugin
*/
OCA.Files.LockPlugin = {

/**
* @param fileList
*/
attach: function(fileList) {
this._extendFileActions(fileList);

var oldCreateRow = fileList._createRow;
fileList._createRow = function(fileData) {
var $tr = oldCreateRow.apply(this, arguments);
if (fileData.activeLocks) {
$tr.attr('data-activelocks', JSON.stringify(fileData.activeLocks));
}
return $tr;
};
var oldElementToFile = fileList.elementToFile;
fileList.elementToFile = function($el) {
var fileInfo = oldElementToFile.apply(this, arguments);
var activeLocks = $el.attr('data-activelocks');
if (_.isUndefined(activeLocks)) {
activeLocks = '[]';
}
fileInfo.activeLocks = JSON.parse(activeLocks);
return fileInfo;
};

var oldGetWebdavProperties = fileList._getWebdavProperties;
fileList._getWebdavProperties = function() {
var props = oldGetWebdavProperties.apply(this, arguments);
props.push('{DAV:}lockdiscovery');
return props;
};

var lockTab = new OCA.Files.LockTabView('lockTabView', {order: -20});
fileList.registerTabView(lockTab);

fileList.filesClient.addFileInfoParser(function(response) {
var data = {};
var props = response.propStat[0].properties;
var activeLocks = props['{DAV:}lockdiscovery'];
if (!_.isUndefined(activeLocks) && activeLocks !== '') {
data.activeLocks = _.chain(activeLocks).filter(function(xmlvalue) {
return (xmlvalue.namespaceURI === NS_DAV && xmlvalue.nodeName.split(':')[1] === 'activelock');
}).map(function(xmlvalue) {
return parseLockNode(xmlvalue);
}).value();

}
return data;
});


},

/**
* @param fileList
* @private
*/
_extendFileActions: function(fileList) {
var self = this;
fileList.fileActions.registerAction({
name: 'lock-status',
displayName: t('files', 'Lock status'),
mime: 'all',
permissions: OC.PERMISSION_READ,
type: OCA.Files.FileActions.TYPE_INLINE,
render: function(actionSpec, isDefault, context) {
var $file = context.$file;
var isLocked = $file.data('activelocks');
if (isLocked && isLocked.length > 0) {
var $actionLink = $(self.renderLink());
context.$file.find('a.name>span.fileactions').append($actionLink);
return $actionLink;
}
return '';
},
actionHandler: function(fileName) {
fileList.showDetailsView(fileName, 'lockTabView');
}
});

},

renderLink: function () {
if (!this._template) {
this._template = Handlebars.compile(TEMPLATE_LOCK_STATUS_ACTION);
}
return this._template({
message: t('files', 'This resource is locked. Click to see more details.')
});
}

};

})(OCA);

OC.Plugins.register('OCA.Files.FileList', OCA.Files.LockPlugin);

124 changes: 124 additions & 0 deletions apps/files/js/locktabview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2018
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/

(function () {
var TEMPLATE =
'<ul class="locks"></ul>' +
'<div class="clear-float"></div>' +
'{{#each locks}}' +
'<div class="lock-entry" data-index="{{index}}">' +
'<div style="display: inline;">{{displayText}}</div>' +
// TODO: no inline css
'<a href="#" class="unlock" style="float: right" title="{{unlockLabel}}">' +
'<span class="icon icon-lock-open" style="display: block" /></a>' +
'</div>' +
'{{else}}' +
'<div class="empty">{{emptyResultLabel}}</div>' +
'{{/each}}' +
'';

function formatLocks(locks) {
var client = OC.Files.getClient();

return _.map(locks, function(lock, index) {
var path = client.getRelativePath(lock.lockroot) || lock.lockroot;

// TODO: what if user in root doesn't match ?

return {
index: index,
displayText: t('files', '{owner} has locked this resource via {path}', {owner: lock.owner, path: path}),
locktoken: lock.locktoken,
lockroot: lock.lockroot
};
});
}

/**
* @memberof OCA.Files
*/
var LockTabView = OCA.Files.DetailTabView.extend(
/** @lends OCA.Files.LockTabView.prototype */ {
id: 'lockTabView',
className: 'tab lockTabView',

events: {
'click a.unlock': '_onClickUnlock'
},

_onClickUnlock: function (event) {
var self = this;
var $target = $(event.target).closest('.lock-entry');
var lockIndex = parseInt($target.attr('data-index'), 10);

var currentLock = this.model.get('activeLocks')[lockIndex];

// FIXME: move to FileInfoModel
this.model._filesClient.getClient().request('UNLOCK',
currentLock.lockroot,
{
'Lock-Token': currentLock.locktoken
}).then(function (result) {
if (result.status === 204) {
// implicit clone of array else backbone doesn't fire change event
var locks = _.without(self.model.get('activeLocks') || [], currentLock);
self.model.set('activeLocks', locks);
self.render();
}
else if (result.status === 403) {
OC.Notification.show(t('files', 'Could not unlock, please contact the lock owner {owner}', {owner: currentLock.owner}));
} else {
// TODO: add more information
OC.Notification.show(t('files', 'Unlock failed with status {status}', {status: result.status}));
}
});
},

getLabel: function () {
return t('files', 'Locks');
},

template: function (data) {
if (!this._template) {
this._template = Handlebars.compile(TEMPLATE);
}

return this._template(data);
},

/**
* Renders this details view
*/
render: function () {
if (!this.model) {
return;
}
this.$el.html(this.template({
emptyResultLabel: t('files', 'Resource is not locked'),
locks: formatLocks(this.model.get('activeLocks'))
}));
},

/**
* Returns whether the current tab is able to display
* the given file info, for example based on mime type.
*
* @param {OCA.Files.FileInfoModel} fileInfo file info model
* @return {bool} whether to display this tab
*/
canDisplay: function(fileInfo) {
// don't display if no lock is set
return fileInfo && fileInfo.get('activeLocks') && fileInfo.get('activeLocks').length > 0;
}
});

OCA.Files.LockTabView = LockTabView;
})();

2 changes: 2 additions & 0 deletions apps/files/lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,14 @@ public function index($dir = '', $view = '', $fileid = null, $details = null) {
\OCP\Util::addScript('files', 'favoritesfilelist');
\OCP\Util::addScript('files', 'tagsplugin');
\OCP\Util::addScript('files', 'favoritesplugin');
\OCP\Util::addScript('files', 'filelockplugin');

\OCP\Util::addScript('files', 'detailfileinfoview');
\OCP\Util::addScript('files', 'detailtabview');
\OCP\Util::addScript('files', 'mainfileinfodetailview');
\OCP\Util::addScript('files', 'detailsview');
\OCP\Util::addStyle('files', 'detailsView');
\OCP\Util::addScript('files', 'locktabview');

\OC_Util::addVendorScript('core', 'handlebars/handlebars');

Expand Down
Loading

0 comments on commit 5182ad0

Please sign in to comment.