diff options
26 files changed, 1409 insertions, 40 deletions
diff --git a/.gitignore b/.gitignore index 237f0f44e81..2e42105ad83 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # ignore all apps except core ones /apps*/* +!/apps/comments !/apps/dav !/apps/files !/apps/federation diff --git a/apps/comments/appinfo/app.php b/apps/comments/appinfo/app.php new file mode 100644 index 00000000000..c6f36567c51 --- /dev/null +++ b/apps/comments/appinfo/app.php @@ -0,0 +1,34 @@ +<?php +/** + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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/> + * + */ + +$eventDispatcher = \OC::$server->getEventDispatcher(); +$eventDispatcher->addListener( + 'OCA\Files::loadAdditionalScripts', + function() { + \OCP\Util::addScript('oc-backbone-webdav'); + \OCP\Util::addScript('comments', 'app'); + \OCP\Util::addScript('comments', 'commentmodel'); + \OCP\Util::addScript('comments', 'commentcollection'); + \OCP\Util::addScript('comments', 'commentstabview'); + \OCP\Util::addScript('comments', 'filesplugin'); + \OCP\Util::addStyle('comments', 'comments'); + } +); diff --git a/apps/comments/appinfo/info.xml b/apps/comments/appinfo/info.xml new file mode 100644 index 00000000000..550c79448cf --- /dev/null +++ b/apps/comments/appinfo/info.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<info> + <id>comments</id> + <name>Comments</name> + <description>Files app plugin to add comments to files</description> + <licence>AGPL</licence> + <author>Arthur Shiwon, Vincent Petry</author> + <default_enable/> + <version>0.1</version> + <dependencies> + <owncloud min-version="9.0" max-version="9.0" /> + </dependencies> + <documentation> + <user>user-comments</user> + </documentation> +</info> diff --git a/apps/comments/css/comments.css b/apps/comments/css/comments.css new file mode 100644 index 00000000000..c1624dcc57b --- /dev/null +++ b/apps/comments/css/comments.css @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +#commentsTabView .newCommentForm { + margin-bottom: 20px; +} + +#commentsTabView .newCommentForm .message { + width: 90%; + resize: none; +} + +#commentsTabView .newCommentForm .submitLoading { + background-position: left; +} + +#commentsTabView .comment { + margin-bottom: 30px; +} + +#commentsTabView .comment .avatar { + width: 28px; + height: 28px; + line-height: 28px; +} + +#commentsTabView .authorRow>div { + display: inline-block; + vertical-align: middle; +} + +#commentsTabView .comment .authorRow { + margin-bottom: 5px; + position: relative; +} + +#commentsTabView .comment .author { + font-weight: bold; +} + +#commentsTabView .comment .date { + position: absolute; + right: 0; +} diff --git a/apps/comments/js/app.js b/apps/comments/js/app.js new file mode 100644 index 00000000000..547059393a5 --- /dev/null +++ b/apps/comments/js/app.js @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com> + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + if (!OCA.Comments) { + /** + * @namespace + */ + OCA.Comments = {}; + } + +})(); + diff --git a/apps/comments/js/commentcollection.js b/apps/comments/js/commentcollection.js new file mode 100644 index 00000000000..d10e5e00865 --- /dev/null +++ b/apps/comments/js/commentcollection.js @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OC, OCA) { + + var NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** + * @class OCA.Comments.CommentCollection + * @classdesc + * + * Collection of comments assigned to a file + * + */ + var CommentCollection = OC.Backbone.Collection.extend( + /** @lends OCA.Comments.CommentCollection.prototype */ { + + sync: OC.Backbone.davSync, + + model: OCA.Comments.CommentModel, + + _objectType: 'files', + _objectId: null, + + _endReached: false, + _limit : 20, + + initialize: function(models, options) { + options = options || {}; + if (options.objectType) { + this._objectType = options.objectType; + } + if (options.objectId) { + this._objectId = options.objectId; + } + }, + + url: function() { + return OC.linkToRemote('dav') + '/comments/' + + encodeURIComponent(this._objectType) + '/' + + encodeURIComponent(this._objectId) + '/'; + }, + + setObjectId: function(objectId) { + this._objectId = objectId; + }, + + hasMoreResults: function() { + return !this._endReached; + }, + + reset: function() { + this._endReached = false; + return OC.Backbone.Collection.prototype.reset.apply(this, arguments); + }, + + /** + * Fetch the next set of results + */ + fetchNext: function(options) { + var self = this; + if (!this.hasMoreResults()) { + return null; + } + + var body = '<?xml version="1.0" encoding="utf-8" ?>\n' + + '<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">\n' + + // load one more so we know there is more + ' <oc:limit>' + (this._limit + 1) + '</oc:limit>\n' + + ' <oc:offset>' + this.length + '</oc:offset>\n' + + '</oc:filter-comments>\n'; + + options = options || {}; + var success = options.success; + options = _.extend({ + remove: false, + data: body, + davProperties: CommentCollection.prototype.model.prototype.davProperties, + success: function(resp) { + if (resp.length <= self._limit) { + // no new entries, end reached + self._endReached = true; + } else { + // remove last entry, for next page load + resp = _.initial(resp); + } + if (!self.set(resp, options)) { + return false; + } + if (success) { + success.apply(null, arguments); + } + self.trigger('sync', 'REPORT', self, options); + } + }, options); + + return this.sync('REPORT', this, options); + } + }); + + OCA.Comments.CommentCollection = CommentCollection; +})(OC, OCA); + diff --git a/apps/comments/js/commentmodel.js b/apps/comments/js/commentmodel.js new file mode 100644 index 00000000000..b945f71fdd2 --- /dev/null +++ b/apps/comments/js/commentmodel.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OC, OCA) { + var NS_OWNCLOUD = 'http://owncloud.org/ns'; + /** + * @class OCA.Comments.CommentModel + * @classdesc + * + * Comment + * + */ + var CommentModel = OC.Backbone.Model.extend( + /** @lends OCA.Comments.CommentModel.prototype */ { + sync: OC.Backbone.davSync, + + defaults: { + actorType: 'users', + objectType: 'files' + }, + + davProperties: { + 'id': '{' + NS_OWNCLOUD + '}id', + 'message': '{' + NS_OWNCLOUD + '}message', + 'actorType': '{' + NS_OWNCLOUD + '}actorType', + 'actorId': '{' + NS_OWNCLOUD + '}actorId', + 'actorDisplayName': '{' + NS_OWNCLOUD + '}actorDisplayName', + 'creationDateTime': '{' + NS_OWNCLOUD + '}creationDateTime', + 'objectType': '{' + NS_OWNCLOUD + '}objectType', + 'objectId': '{' + NS_OWNCLOUD + '}objectId' + }, + + parse: function(data) { + // TODO: parse non-string values + return data; + } + }); + + OCA.Comments.CommentModel = CommentModel; +})(OC, OCA); + diff --git a/apps/comments/js/commentstabview.js b/apps/comments/js/commentstabview.js new file mode 100644 index 00000000000..463ac2d76ef --- /dev/null +++ b/apps/comments/js/commentstabview.js @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2016 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OC, OCA) { + var TEMPLATE = + '<div class="newCommentRow comment">' + + ' <div class="authorRow">' + + ' {{#if avatarEnabled}}' + + ' <div class="avatar" data-username="{{userId}}"></div>' + + ' {{/if}}' + + ' <div class="author">{{userDisplayName}}</div>' + + ' </div>' + + ' <form class="newCommentForm">' + + ' <textarea class="message" placeholder="{{newMessagePlaceholder}}"></textarea>' + + ' <input class="submit" type="submit" value="{{submitText}}" />' + + ' <div class="submitLoading icon-loading-small hidden"></div>'+ + ' </form>' + + ' <ul class="comments">' + + ' </ul>' + + '</div>' + + '<div class="empty hidden">{{emptyResultLabel}}</div>' + + '<input type="button" class="showMore hidden" value="{{moreLabel}}"' + + ' name="show-more" id="show-more" />' + + '<div class="loading hidden" style="height: 50px"></div>'; + + var COMMENT_TEMPLATE = + '<li class="comment">' + + ' <div class="authorRow">' + + ' {{#if avatarEnabled}}' + + ' <div class="avatar" data-username="{{actorId}}"> </div>' + + ' {{/if}}' + + ' <div class="author">{{actorDisplayName}}</div>' + + ' <div class="date has-tooltip" title="{{altDate}}">{{date}}</div>' + + ' </div>' + + ' <div class="message">{{{formattedMessage}}}</div>' + + '</li>'; + + /** + * @memberof OCA.Comments + */ + var CommentsTabView = OCA.Files.DetailTabView.extend( + /** @lends OCA.Comments.CommentsTabView.prototype */ { + id: 'commentsTabView', + className: 'tab commentsTabView', + + events: { + 'submit .newCommentForm': '_onSubmitComment', + 'click .showMore': '_onClickShowMore' + }, + + initialize: function() { + OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments); + this.collection = new OCA.Comments.CommentCollection(); + this.collection.on('request', this._onRequest, this); + this.collection.on('sync', this._onEndRequest, this); + this.collection.on('add', this._onAddModel, this); + + this._avatarsEnabled = !!OC.config.enable_avatars; + + // TODO: error handling + _.bindAll(this, '_onSubmitComment'); + }, + + template: function(params) { + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE); + } + var currentUser = OC.getCurrentUser(); + return this._template(_.extend({ + avatarEnabled: this._avatarsEnabled, + userId: currentUser.uid, + userDisplayName: currentUser.displayName, + newMessagePlaceholder: t('comments', 'Type in a new comment...'), + submitText: t('comments', 'Post') + }, params)); + }, + + commentTemplate: function(params) { + if (!this._commentTemplate) { + this._commentTemplate = Handlebars.compile(COMMENT_TEMPLATE); + } + return this._commentTemplate(_.extend({ + avatarEnabled: this._avatarsEnabled + }, params)); + }, + + getLabel: function() { + return t('comments', 'Comments'); + }, + + setFileInfo: function(fileInfo) { + if (fileInfo) { + this.render(); + this.collection.setObjectId(fileInfo.id); + // reset to first page + this.collection.reset([], {silent: true}); + this.nextPage(); + } else { + this.render(); + this.collection.reset(); + } + }, + + render: function() { + this.$el.html(this.template({ + emptyResultLabel: t('comments', 'No other comments available'), + moreLabel: t('comments', 'More comments...') + })); + this.$el.find('.has-tooltip').tooltip(); + this.$container = this.$el.find('ul.comments'); + this.$el.find('.avatar').avatar(OC.getCurrentUser().uid, 28); + this.delegateEvents(); + }, + + _formatItem: function(commentModel) { + var timestamp = new Date(commentModel.get('creationDateTime')).getTime(); + var data = _.extend({ + date: OC.Util.relativeModifiedDate(timestamp), + altDate: OC.Util.formatDate(timestamp), + formattedMessage: this._formatMessage(commentModel.get('message')) + }, commentModel.attributes); + return data; + }, + + _toggleLoading: function(state) { + this._loading = state; + this.$el.find('.loading').toggleClass('hidden', !state); + }, + + _onRequest: function() { + this._toggleLoading(true); + this.$el.find('.showMore').addClass('hidden'); + }, + + _onEndRequest: function() { + this._toggleLoading(false); + this.$el.find('.empty').toggleClass('hidden', !!this.collection.length); + this.$el.find('.showMore').toggleClass('hidden', !this.collection.hasMoreResults()); + }, + + _onAddModel: function(model, collection, options) { + var $el = $(this.commentTemplate(this._formatItem(model))); + if (!_.isUndefined(options.at) && collection.length > 1) { + this.$container.find('li').eq(options.at).before($el); + } else { + this.$container.append($el); + } + + this._postRenderItem($el); + }, + + _postRenderItem: function($el) { + $el.find('.has-tooltip').tooltip(); + if(this._avatarsEnabled) { + $el.find('.avatar').each(function() { + var $this = $(this); + $this.avatar($this.attr('data-username'), 28); + }); + } + }, + + /** + * Convert a message to be displayed in HTML, + * converts newlines to <br> tags. + */ + _formatMessage: function(message) { + return escapeHTML(message).replace(/\n/g, '<br/>'); + }, + + nextPage: function() { + if (this._loading || !this.collection.hasMoreResults()) { + return; + } + + this.collection.fetchNext(); + }, + + _onClickShowMore: function(ev) { + ev.preventDefault(); + this.nextPage(); + }, + + _onSubmitComment: function(e) { + var $form = $(e.target); + var currentUser = OC.getCurrentUser(); + var $submit = $form.find('.submit'); + var $loading = $form.find('.submitLoading'); + var $textArea = $form.find('textarea'); + var message = $textArea.val().trim(); + e.preventDefault(); + + if (!message.length) { + return; + } + + $textArea.prop('disabled', true); + $submit.addClass('hidden'); + $loading.removeClass('hidden'); + + this.collection.create({ + actorId: currentUser.uid, + actorDisplayName: currentUser.displayName, + actorType: 'users', + verb: 'comment', + message: $textArea.val(), + creationDateTime: (new Date()).getTime() + }, { + at: 0, + success: function() { + $submit.removeClass('hidden'); + $loading.addClass('hidden'); + $textArea.val('').prop('disabled', false); + }, + error: function(msg) { + $submit.removeClass('hidden'); + $loading.addClass('hidden'); + $textArea.prop('disabled', false); + + OC.Notification.showTemporary(msg); + } + }); + + return false; + } + }); + + OCA.Comments.CommentsTabView = CommentsTabView; +})(OC, OCA); + diff --git a/apps/comments/js/filesplugin.js b/apps/comments/js/filesplugin.js new file mode 100644 index 00000000000..c8d91e0ede3 --- /dev/null +++ b/apps/comments/js/filesplugin.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com> + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + OCA.Comments = _.extend({}, OCA.Comments); + if (!OCA.Comments) { + /** + * @namespace + */ + OCA.Comments = {}; + } + + /** + * @namespace + */ + OCA.Comments.FilesPlugin = { + allowedLists: [ + 'files', + 'favorites' + ], + + attach: function(fileList) { + if (this.allowedLists.indexOf(fileList.id) < 0) { + return; + } + + fileList.registerTabView(new OCA.Comments.CommentsTabView('commentsTabView')); + } + }; + +})(); + +OC.Plugins.register('OCA.Files.FileList', OCA.Comments.FilesPlugin); + diff --git a/apps/comments/tests/js/commentscollectionSpec.js b/apps/comments/tests/js/commentscollectionSpec.js new file mode 100644 index 00000000000..0dc68cc167c --- /dev/null +++ b/apps/comments/tests/js/commentscollectionSpec.js @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016 + * + * This file is licensed under the Affero General Public License comment 3 + * or later. + * + * See the COPYING-README file. + * + */ +describe('OCA.Comments.CommentCollection', function() { + var CommentCollection = OCA.Comments.CommentCollection; + var collection, syncStub; + var comment1, comment2, comment3; + + beforeEach(function() { + syncStub = sinon.stub(CommentCollection.prototype, 'sync'); + collection = new CommentCollection(); + collection.setObjectId(5); + + comment1 = { + id: 1, + actorType: 'users', + actorId: 'user1', + actorDisplayName: 'User One', + objectType: 'files', + objectId: 5, + message: 'First', + creationDateTime: Date.UTC(2016, 1, 3, 10, 5, 0) + }; + comment2 = { + id: 2, + actorType: 'users', + actorId: 'user2', + actorDisplayName: 'User Two', + objectType: 'files', + objectId: 5, + message: 'Second\nNewline', + creationDateTime: Date.UTC(2016, 1, 3, 10, 0, 0) + }; + comment3 = { + id: 3, + actorType: 'users', + actorId: 'user3', + actorDisplayName: 'User Three', + objectType: 'files', + objectId: 5, + message: 'Third', + creationDateTime: Date.UTC(2016, 1, 3, 5, 0, 0) + }; + }); + afterEach(function() { + syncStub.restore(); + }); + + it('fetches the next page', function() { + collection._limit = 2; + collection.fetchNext(); + + expect(syncStub.calledOnce).toEqual(true); + expect(syncStub.lastCall.args[0]).toEqual('REPORT'); + var options = syncStub.lastCall.args[2]; + expect(options.remove).toEqual(false); + + var parser = new DOMParser(); + var doc = parser.parseFromString(options.data, "application/xml"); + expect(doc.getElementsByTagNameNS('http://owncloud.org/ns', 'limit')[0].textContent).toEqual('3'); + expect(doc.getElementsByTagNameNS('http://owncloud.org/ns', 'offset')[0].textContent).toEqual('0'); + + syncStub.yieldTo('success', [comment1, comment2, comment3]); + + expect(collection.length).toEqual(2); + expect(collection.hasMoreResults()).toEqual(true); + + collection.fetchNext(); + + expect(syncStub.calledTwice).toEqual(true); + options = syncStub.lastCall.args[2]; + doc = parser.parseFromString(options.data, "application/xml"); + expect(doc.getElementsByTagNameNS('http://owncloud.org/ns', 'limit')[0].textContent).toEqual('3'); + expect(doc.getElementsByTagNameNS('http://owncloud.org/ns', 'offset')[0].textContent).toEqual('2'); + + syncStub.yieldTo('success', [comment3]); + + expect(collection.length).toEqual(3); + expect(collection.hasMoreResults()).toEqual(false); + + collection.fetchNext(); + + // no further requests + expect(syncStub.calledTwice).toEqual(true); + }); + it('resets page counted when calling reset', function() { + collection.fetchNext(); + + syncStub.yieldTo('success', [comment1]); + + expect(collection.hasMoreResults()).toEqual(false); + + collection.reset(); + + expect(collection.hasMoreResults()).toEqual(true); + }); +}); + diff --git a/apps/comments/tests/js/commentstabviewSpec.js b/apps/comments/tests/js/commentstabviewSpec.js new file mode 100644 index 00000000000..0fb5eec0653 --- /dev/null +++ b/apps/comments/tests/js/commentstabviewSpec.js @@ -0,0 +1,198 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2016 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 +* comment 3 of the License, or any later comment. +* +* 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.Comments.CommentsTabView tests', function() { + var view, fileInfoModel; + var fetchStub; + var testComments; + var clock; + + beforeEach(function() { + clock = sinon.useFakeTimers(Date.UTC(2016, 1, 3, 10, 5, 9)); + fetchStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'fetchNext'); + view = new OCA.Comments.CommentsTabView(); + fileInfoModel = new OCA.Files.FileInfoModel({ + id: 5, + name: 'One.txt', + mimetype: 'text/plain', + permissions: 31, + path: '/subdir', + size: 123456789, + etag: 'abcdefg', + mtime: Date.UTC(2016, 1, 0, 0, 0, 0) + }); + view.render(); + var comment1 = new OCA.Comments.CommentModel({ + id: 1, + actorType: 'users', + actorId: 'user1', + actorDisplayName: 'User One', + objectType: 'files', + objectId: 5, + message: 'First', + creationDateTime: Date.UTC(2016, 1, 3, 10, 5, 0) + }); + var comment2 = new OCA.Comments.CommentModel({ + id: 2, + actorType: 'users', + actorId: 'user2', + actorDisplayName: 'User Two', + objectType: 'files', + objectId: 5, + message: 'Second\nNewline', + creationDateTime: Date.UTC(2016, 1, 3, 10, 0, 0) + }); + + testComments = [comment1, comment2]; + }); + afterEach(function() { + view.remove(); + view = undefined; + fetchStub.restore(); + clock.restore(); + }); + describe('rendering', function() { + it('reloads matching comments when setting file info model', function() { + view.setFileInfo(fileInfoModel); + expect(fetchStub.calledOnce).toEqual(true); + }); + + it('renders loading icon while fetching comments', function() { + view.setFileInfo(fileInfoModel); + view.collection.trigger('request'); + + expect(view.$el.find('.loading').length).toEqual(1); + expect(view.$el.find('.comments li').length).toEqual(0); + }); + + it('renders comments', function() { + + view.setFileInfo(fileInfoModel); + view.collection.set(testComments); + + var $comments = view.$el.find('.comments>li'); + expect($comments.length).toEqual(2); + var $item = $comments.eq(0); + expect($item.find('.author').text()).toEqual('User One'); + expect($item.find('.date').text()).toEqual('seconds ago'); + expect($item.find('.message').text()).toEqual('First'); + + $item = $comments.eq(1); + expect($item.find('.author').text()).toEqual('User Two'); + expect($item.find('.date').text()).toEqual('5 minutes ago'); + expect($item.find('.message').html()).toEqual('Second<br>Newline'); + }); + }); + describe('more comments', function() { + var hasMoreResultsStub; + + beforeEach(function() { + view.collection.set(testComments); + hasMoreResultsStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'hasMoreResults'); + }); + afterEach(function() { + hasMoreResultsStub.restore(); + }); + + it('shows "More comments" button when more comments are available', function() { + hasMoreResultsStub.returns(true); + view.collection.trigger('sync'); + + expect(view.$el.find('.showMore').hasClass('hidden')).toEqual(false); + }); + it('does not show "More comments" button when more comments are available', function() { + hasMoreResultsStub.returns(false); + view.collection.trigger('sync'); + + expect(view.$el.find('.showMore').hasClass('hidden')).toEqual(true); + }); + it('fetches and appends the next page when clicking the "More" button', function() { + hasMoreResultsStub.returns(true); + + expect(fetchStub.notCalled).toEqual(true); + + view.$el.find('.showMore').click(); + + expect(fetchStub.calledOnce).toEqual(true); + }); + it('appends comment to the list when added to collection', function() { + var comment3 = new OCA.Comments.CommentModel({ + id: 3, + actorType: 'users', + actorId: 'user3', + actorDisplayName: 'User Three', + objectType: 'files', + objectId: 5, + message: 'Third', + creationDateTime: Date.UTC(2016, 1, 3, 5, 0, 0) + }); + + view.collection.add(comment3); + + expect(view.$el.find('.comments>li').length).toEqual(3); + + var $item = view.$el.find('.comments>li').eq(2); + expect($item.find('.author').text()).toEqual('User Three'); + expect($item.find('.date').text()).toEqual('5 hours ago'); + expect($item.find('.message').html()).toEqual('Third'); + }); + }); + describe('posting comments', function() { + var createStub; + var currentUserStub; + + beforeEach(function() { + view.collection.set(testComments); + createStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'create'); + currentUserStub = sinon.stub(OC, 'getCurrentUser'); + currentUserStub.returns({ + uid: 'testuser', + displayName: 'Test User' + }); + }); + afterEach(function() { + createStub.restore(); + currentUserStub.restore(); + }); + + it('creates a new comment when clicking post button', function() { + view.$el.find('.message').val('New message'); + view.$el.find('form').submit(); + + expect(createStub.calledOnce).toEqual(true); + expect(createStub.lastCall.args[0]).toEqual({ + actorId: 'testuser', + actorDisplayName: 'Test User', + actorType: 'users', + verb: 'comment', + message: 'New message', + creationDateTime: Date.UTC(2016, 1, 3, 10, 5, 9) + }); + }); + it('does not create a comment if the field is empty', function() { + view.$el.find('.message').val(' '); + view.$el.find('form').submit(); + + expect(createStub.notCalled).toEqual(true); + }); + + }); +}); diff --git a/apps/files_external/appinfo/register_command.php b/apps/files_external/appinfo/register_command.php index d85906e3831..929becce77a 100644 --- a/apps/files_external/appinfo/register_command.php +++ b/apps/files_external/appinfo/register_command.php @@ -23,12 +23,14 @@ use OCA\Files_External\Command\ListCommand; use OCA\Files_External\Command\Config; use OCA\Files_External\Command\Option; +use OCA\Files_External\Command\Applicable; use OCA\Files_External\Command\Import; use OCA\Files_External\Command\Export; use OCA\Files_External\Command\Delete; $userManager = OC::$server->getUserManager(); $userSession = OC::$server->getUserSession(); +$groupManager = OC::$server->getGroupManager(); $app = \OC_Mount_Config::$app; @@ -41,6 +43,7 @@ $backendService = $app->getContainer()->query('OCA\Files_External\Service\Backen $application->add(new ListCommand($globalStorageService, $userStorageService, $userSession, $userManager)); $application->add(new Config($globalStorageService)); $application->add(new Option($globalStorageService)); +$application->add(new Applicable($globalStorageService, $userManager, $groupManager)); $application->add(new Import($globalStorageService, $userStorageService, $userSession, $userManager, $importLegacyStorageService, $backendService)); $application->add(new Export($globalStorageService, $userStorageService, $userSession, $userManager)); $application->add(new Delete($globalStorageService, $userStorageService, $userSession, $userManager)); diff --git a/apps/files_external/command/applicable.php b/apps/files_external/command/applicable.php new file mode 100644 index 00000000000..7e6c99d2915 --- /dev/null +++ b/apps/files_external/command/applicable.php @@ -0,0 +1,157 @@ +<?php +/** + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\Files_External\Command; + +use OC\Core\Command\Base; +use OCA\Files_external\Lib\StorageConfig; +use OCA\Files_external\NotFoundException; +use OCA\Files_external\Service\GlobalStoragesService; +use OCP\IGroupManager; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Applicable extends Base { + /** + * @var GlobalStoragesService + */ + protected $globalService; + + /** + * @var IUserManager + */ + private $userManager; + + /** + * @var IGroupManager + */ + private $groupManager; + + function __construct( + GlobalStoragesService $globalService, + IUserManager $userManager, + IGroupManager $groupManager + ) { + parent::__construct(); + $this->globalService = $globalService; + $this->userManager = $userManager; + $this->groupManager = $groupManager; + } + + protected function configure() { + $this + ->setName('files_external:applicable') + ->setDescription('Manage applicable users and groups for a mount') + ->addArgument( + 'mount_id', + InputArgument::REQUIRED, + 'The id of the mount to edit' + )->addOption( + 'add-user', + null, + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'user to add as applicable' + )->addOption( + 'remove-user', + null, + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'user to remove as applicable' + )->addOption( + 'add-group', + null, + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'group to add as applicable' + )->addOption( + 'remove-group', + null, + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'group to remove as applicable' + )->addOption( + 'remove-all', + null, + InputOption::VALUE_NONE, + 'Set the mount to be globally applicable' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $mountId = $input->getArgument('mount_id'); + try { + $mount = $this->globalService->getStorage($mountId); + } catch (NotFoundException $e) { + $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts</error>'); + return 404; + } + + if ($mount->getType() === StorageConfig::MOUNT_TYPE_PERSONAl) { + $output->writeln('<error>Can\'t change applicables on personal mounts</error>'); + return 1; + } + + $addUsers = $input->getOption('add-user'); + $removeUsers = $input->getOption('remove-user'); + $addGroups = $input->getOption('add-group'); + $removeGroups = $input->getOption('remove-group'); + + $applicableUsers = $mount->getApplicableUsers(); + $applicableGroups = $mount->getApplicableGroups(); + + if ((count($addUsers) + count($removeUsers) + count($addGroups) + count($removeGroups) > 0) || $input->getOption('remove-all')) { + foreach ($addUsers as $addUser) { + if (!$this->userManager->userExists($addUser)) { + $output->writeln('<error>User "' . $addUser . '" not found</error>'); + return 404; + } + } + foreach ($addGroups as $addGroup) { + if (!$this->groupManager->groupExists($addGroup)) { + $output->writeln('<error>Group "' . $addGroup . '" not found</error>'); + return 404; + } + } + + if ($input->getOption('remove-all')) { + $applicableUsers = []; + $applicableGroups = []; + } else { + $applicableUsers = array_unique(array_merge($applicableUsers, $addUsers)); + $applicableUsers = array_values(array_diff($applicableUsers, $removeUsers)); + $applicableGroups = array_unique(array_merge($applicableGroups, $addGroups)); + $applicableGroups = array_values(array_diff($applicableGroups, $removeGroups)); + } + $mount->setApplicableUsers($applicableUsers); + $mount->setApplicableGroups($applicableGroups); + $this->globalService->updateStorage($mount); + } + + $this->writeArrayInOutputFormat($input, $output, [ + 'users' => $applicableUsers, + 'groups' => $applicableGroups + ]); + } +} diff --git a/apps/files_external/tests/command/applicabletest.php b/apps/files_external/tests/command/applicabletest.php new file mode 100644 index 00000000000..64d41f6f245 --- /dev/null +++ b/apps/files_external/tests/command/applicabletest.php @@ -0,0 +1,168 @@ +<?php +/** + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\Files_External\Tests\Command; + +use OCA\Files_External\Command\Applicable; + +class ApplicableTest extends CommandTest { + private function getInstance($storageService) { + /** @var \OCP\IUserManager|\PHPUnit_Framework_MockObject_MockObject $userManager */ + $userManager = $this->getMock('\OCP\IUserManager'); + /** @var \OCP\IGroupManager|\PHPUnit_Framework_MockObject_MockObject $groupManager */ + $groupManager = $this->getMock('\OCP\IGroupManager'); + + $userManager->expects($this->any()) + ->method('userExists') + ->will($this->returnValue(true)); + + $groupManager->expects($this->any()) + ->method('groupExists') + ->will($this->returnValue(true)); + + return new Applicable($storageService, $userManager, $groupManager); + } + + public function testListEmpty() { + $mount = $this->getMount(1, '', ''); + + $storageService = $this->getGlobalStorageService([$mount]); + $command = $this->getInstance($storageService); + + $input = $this->getInput($command, [ + 'mount_id' => 1 + ], [ + 'output' => 'json' + ]); + + $result = json_decode($this->executeCommand($command, $input), true); + + $this->assertEquals(['users' => [], 'groups' => []], $result); + } + + public function testList() { + $mount = $this->getMount(1, '', '', '', [], [], ['test', 'asd']); + + $storageService = $this->getGlobalStorageService([$mount]); + $command = $this->getInstance($storageService); + + $input = $this->getInput($command, [ + 'mount_id' => 1 + ], [ + 'output' => 'json' + ]); + + $result = json_decode($this->executeCommand($command, $input), true); + + $this->assertEquals(['users' => ['test', 'asd'], 'groups' => []], $result); + } + + public function testAddSingle() { + $mount = $this->getMount(1, '', '', '', [], [], []); + + $storageService = $this->getGlobalStorageService([$mount]); + $command = $this->getInstance($storageService); + + $input = $this->getInput($command, [ + 'mount_id' => 1 + ], [ + 'output' => 'json', + 'add-user' => ['foo'] + ]); + + $this->executeCommand($command, $input); + + $this->assertEquals(['foo'], $mount->getApplicableUsers()); + } + + public function testAddDuplicate() { + $mount = $this->getMount(1, '', '', '', [], [], ['foo']); + + $storageService = $this->getGlobalStorageService([$mount]); + $command = $this->getInstance($storageService); + + $input = $this->getInput($command, [ + 'mount_id' => 1 + ], [ + 'output' => 'json', + 'add-user' => ['foo', 'bar'] + ]); + + $this->executeCommand($command, $input); + + $this->assertEquals(['foo', 'bar'], $mount->getApplicableUsers()); + } + + public function testRemoveSingle() { + $mount = $this->getMount(1, '', '', '', [], [], ['foo', 'bar']); + + $storageService = $this->getGlobalStorageService([$mount]); + $command = $this->getInstance($storageService); + + $input = $this->getInput($command, [ + 'mount_id' => 1 + ], [ + 'output' => 'json', + 'remove-user' => ['bar'] + ]); + + $this->executeCommand($command, $input); + + $this->assertEquals(['foo'], $mount->getApplicableUsers()); + } + + public function testRemoveNonExisting() { + $mount = $this->getMount(1, '', '', '', [], [], ['foo', 'bar']); + + $storageService = $this->getGlobalStorageService([$mount]); + $command = $this->getInstance($storageService); + + $input = $this->getInput($command, [ + 'mount_id' => 1 + ], [ + 'output' => 'json', + 'remove-user' => ['bar', 'asd'] + ]); + + $this->executeCommand($command, $input); + + $this->assertEquals(['foo'], $mount->getApplicableUsers()); + } + + public function testRemoveAddRemove() { + $mount = $this->getMount(1, '', '', '', [], [], ['foo', 'bar']); + + $storageService = $this->getGlobalStorageService([$mount]); + $command = $this->getInstance($storageService); + + $input = $this->getInput($command, [ + 'mount_id' => 1 + ], [ + 'output' => 'json', + 'remove-user' => ['bar', 'asd'], + 'add-user' => ['test'] + ]); + + $this->executeCommand($command, $input); + + $this->assertEquals(['foo', 'test'], $mount->getApplicableUsers()); + } +} diff --git a/apps/files_external/tests/command/commandtest.php b/apps/files_external/tests/command/commandtest.php new file mode 100644 index 00000000000..9a0afbd3681 --- /dev/null +++ b/apps/files_external/tests/command/commandtest.php @@ -0,0 +1,104 @@ +<?php +/** + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\Files_External\Tests\Command; + +use OCA\Files_external\Lib\StorageConfig; +use OCA\Files_external\NotFoundException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\Input; +use Symfony\Component\Console\Output\BufferedOutput; +use Test\TestCase; + +abstract class CommandTest extends TestCase { + /** + * @param StorageConfig[] $mounts + * @return \OCA\Files_external\Service\GlobalStoragesService|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getGlobalStorageService(array $mounts = []) { + $mock = $this->getMockBuilder('OCA\Files_external\Service\GlobalStoragesService') + ->disableOriginalConstructor() + ->getMock(); + + $this->bindMounts($mock, $mounts); + + return $mock; + } + + /** + * @param \PHPUnit_Framework_MockObject_MockObject $mock + * @param StorageConfig[] $mounts + */ + protected function bindMounts(\PHPUnit_Framework_MockObject_MockObject $mock, array $mounts) { + $mock->expects($this->any()) + ->method('getStorage') + ->will($this->returnCallback(function ($id) use ($mounts) { + foreach ($mounts as $mount) { + if ($mount->getId() === $id) { + return $mount; + } + } + throw new NotFoundException(); + })); + } + + /** + * @param $id + * @param $mountPoint + * @param $backendClass + * @param string $applicableIdentifier + * @param array $config + * @param array $options + * @param array $users + * @param array $groups + * @return StorageConfig + */ + protected function getMount($id, $mountPoint, $backendClass, $applicableIdentifier = 'password::password', $config = [], $options = [], $users = [], $groups = []) { + $mount = new StorageConfig($id); + + $mount->setMountPoint($mountPoint); + $mount->setBackendOptions($config); + $mount->setMountOptions($options); + $mount->setApplicableUsers($users); + $mount->setApplicableGroups($groups); + + return $mount; + } + + protected function getInput(Command $command, array $arguments = [], array $options = []) { + $input = new ArrayInput([]); + $input->bind($command->getDefinition()); + foreach ($arguments as $key => $value) { + $input->setArgument($key, $value); + } + foreach ($options as $key => $value) { + $input->setOption($key, $value); + } + return $input; + } + + protected function executeCommand(Command $command, Input $input) { + $output = new BufferedOutput(); + $this->invokePrivate($command, 'execute', [$input, $output]); + return $output->fetch(); + } +} diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 4d4d5361647..3b8633b872a 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -282,8 +282,9 @@ Feature: provisioning Then the OCS status code should be "100" And the HTTP status code should be "200" And apps returned are - | files | + | comments | | dav | + | files | | files_sharing | | files_trashbin | | files_versions | diff --git a/config/config.sample.php b/config/config.sample.php index 3f6ae4bc3ec..bdbf3f42046 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -9,8 +9,8 @@ * consider important for your instance to your working ``config.php``, and * apply configuration options that are pertinent for your instance. * - * This file is used to generate the config documentation. Please consider - * following requirements of the current parser: + * This file is used to generate the configuration documentation. + * Please consider following requirements of the current parser: * * all comments need to start with `/**` and end with ` *\/` - each on their * own line * * add a `@see CONFIG_INDEX` to copy a previously described config option @@ -488,16 +488,16 @@ $CONFIG = array( * to ``true``. This verifies that the ``.htaccess`` file is writable and works. * If it is not, then any options controlled by ``.htaccess``, such as large * file uploads, will not work. It also runs checks on the ``data/`` directory, - * which verifies that it can't be accessed directly through the web server. + * which verifies that it can't be accessed directly through the Web server. */ 'check_for_working_htaccess' => true, /** - * In certain environments it is desired to have a read-only config file. + * In certain environments it is desired to have a read-only configuration file. * When this switch is set to ``true`` ownCloud will not verify whether the * configuration is writable. However, it will not be possible to configure - * all options via the web-interface. Furthermore, when updating ownCloud - * it is required to make the config file writable again for the update + * all options via the Web interface. Furthermore, when updating ownCloud + * it is required to make the configuration file writable again for the update * process. */ 'config_is_read_only' => false, @@ -660,9 +660,9 @@ $CONFIG = array( * Use the ``apps_paths`` parameter to set the location of the Apps directory, * which should be scanned for available apps, and where user-specific apps * should be installed from the Apps store. The ``path`` defines the absolute - * file system path to the app folder. The key ``url`` defines the HTTP web path - * to that folder, starting from the ownCloud web root. The key ``writable`` - * indicates if a web server can write files to that folder. + * file system path to the app folder. The key ``url`` defines the HTTP Web path + * to that folder, starting from the ownCloud webroot. The key ``writable`` + * indicates if a Web server can write files to that folder. */ 'apps_paths' => array( array( @@ -999,7 +999,7 @@ $CONFIG = array( /** - * All other config options + * All other configuration options */ /** @@ -1011,8 +1011,8 @@ $CONFIG = array( ), /** - * sqlite3 journal mode can be specified using this config parameter - can be - * 'WAL' or 'DELETE' see for more details https://www.sqlite.org/wal.html + * sqlite3 journal mode can be specified using this configuration parameter - + * can be 'WAL' or 'DELETE' see for more details https://www.sqlite.org/wal.html */ 'sqlite.journal_mode' => 'DELETE', @@ -1038,7 +1038,7 @@ $CONFIG = array( * restricted, or if external storages which do not support streaming are in * use. * - * The web server user must have write access to this directory. + * The Web server user must have write access to this directory. */ 'tempdirectory' => '/tmp/owncloudtemp', @@ -1104,7 +1104,7 @@ $CONFIG = array( 'filesystem_check_changes' => 0, /** - * All css and js files will be served by the web server statically in one js + * All css and js files will be served by the Web server statically in one js * file and one css file if this is set to ``true``. This improves performance. */ 'asset-pipeline.enabled' => false, @@ -1115,7 +1115,7 @@ $CONFIG = array( * will be stored in a subdirectory of this directory named 'assets'. The * server *must* be configured to serve that directory as $WEBROOT/assets. * You will only likely need to change this if the main ownCloud directory - * is not writeable by the web server in your configuration. + * is not writeable by the Web server in your configuration. */ 'assetdirectory' => '/var/www/owncloud', @@ -1196,6 +1196,15 @@ $CONFIG = array( 'debug' => false, /** + * Skips the migration test during upgrades + * + * If this is set to true the migration test are deactivated during upgrade. + * This is only recommended in installations where upgrade tests are run in + * advance with the same data on a test system. + */ +'update.skip-migration-test' => false, + +/** * This entry is just here to show a warning in case somebody copied the sample * configuration. DO NOT ADD THIS SWITCH TO YOUR CONFIGURATION! * diff --git a/core/ajax/update.php b/core/ajax/update.php index 4d8fe19f168..15daff4e1de 100644 --- a/core/ajax/update.php +++ b/core/ajax/update.php @@ -50,6 +50,12 @@ if (OC::checkUpgrade(false)) { \OC::$server->getIntegrityCodeChecker(), $logger ); + + if ($config->getSystemValue('update.skip-migration-test', false)) { + $eventSource->send('success', (string)$l->t('Migration tests are skipped - "update.skip-migration-test" is activated in config.php')); + $updater->setSimulateStepEnabled(false); + } + $incompatibleApps = []; $disabledThirdPartyApps = []; diff --git a/core/command/upgrade.php b/core/command/upgrade.php index c45984d7a30..2123efdfd38 100644 --- a/core/command/upgrade.php +++ b/core/command/upgrade.php @@ -99,6 +99,12 @@ class Upgrade extends Command { $updateStepEnabled = true; $skip3rdPartyAppsDisable = false; + if ($this->config->getSystemValue('update.skip-migration-test', false)) { + $output->writeln( + '<info>"skip-migration-test" is activated via config.php</info>' + ); + $simulateStepEnabled = false; + } if ($input->getOption('skip-migration-test')) { $simulateStepEnabled = false; } diff --git a/core/js/js.js b/core/js/js.js index 43ea269c203..bc8c51e40d3 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -82,6 +82,12 @@ var OC={ webroot:oc_webroot, appswebroots:(typeof oc_appswebroots !== 'undefined') ? oc_appswebroots:false, + /** + * Currently logged in user or null if none + * + * @type String + * @deprecated use {@link OC.getCurrentUser} instead + */ currentUser:(typeof oc_current_user!=='undefined')?oc_current_user:false, config: window.oc_config, appConfig: window.oc_appconfig || {}, @@ -272,6 +278,23 @@ var OC={ }, /** + * Returns the currently logged in user or null if there is no logged in + * user (public page mode) + * + * @return {OC.CurrentUser} user spec + * @since 9.0.0 + */ + getCurrentUser: function() { + if (_.isUndefined(this._currentUserDisplayName)) { + this._currentUserDisplayName = document.getElementsByTagName('head')[0].getAttribute('data-user-displayname'); + } + return { + uid: this.currentUser, + displayName: this._currentUserDisplayName + }; + }, + + /** * get the absolute path to an image file * if no extension is given for the image, it will automatically decide * between .png and .svg based on what the browser supports @@ -690,6 +713,15 @@ var OC={ }; /** + * Current user attributes + * + * @typedef {Object} OC.CurrentUser + * + * @property {String} uid user id + * @property {String} displayName display name + */ + +/** * @namespace OC.Plugins */ OC.Plugins = { diff --git a/core/js/oc-backbone-webdav.js b/core/js/oc-backbone-webdav.js index 7c32116f011..ba678a32fcf 100644 --- a/core/js/oc-backbone-webdav.js +++ b/core/js/oc-backbone-webdav.js @@ -76,6 +76,11 @@ * @param {Object} davProperties properties mapping */ function parsePropFindResult(result, davProperties) { + if (_.isArray(result)) { + return _.map(result, function(subResult) { + return parsePropFindResult(subResult, davProperties); + }); + } var props = { href: result.href }; @@ -87,7 +92,7 @@ for (var key in propStat.properties) { var propKey = key; - if (davProperties[key]) { + if (key in davProperties) { propKey = davProperties[key]; } props[propKey] = propStat.properties[key]; @@ -151,15 +156,10 @@ if (isSuccessStatus(response.status)) { if (_.isFunction(options.success)) { var propsMapping = _.invert(options.davProperties); - var results; + var results = parsePropFindResult(response.body, propsMapping); if (options.depth > 0) { - results = _.map(response.body, function(data) { - return parsePropFindResult(data, propsMapping); - }); // discard root entry results.shift(); - } else { - results = parsePropFindResult(response.body, propsMapping); } options.success(results); @@ -217,7 +217,13 @@ options.success(responseJson); return; } - options.success(result.body); + // if multi-status, parse + if (result.status === 207) { + var propsMapping = _.invert(options.davProperties); + options.success(parsePropFindResult(result.body, propsMapping)); + } else { + options.success(result.body); + } } }); } @@ -249,7 +255,7 @@ * DAV transport */ function davSync(method, model, options) { - var params = {type: methodMap[method]}; + var params = {type: methodMap[method] || method}; var isCollection = (model instanceof Backbone.Collection); if (method === 'update' && (model.usePUT || (model.collection && model.collection.usePUT))) { diff --git a/core/shipped.json b/core/shipped.json index 5dd8700bf1a..5f995326625 100644 --- a/core/shipped.json +++ b/core/shipped.json @@ -3,6 +3,7 @@ "activity", "admin_audit", "encryption", + "comments", "dav", "enterprise_key", "external", diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index 7fe67159bb5..7905f5b7f3a 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -2,7 +2,7 @@ <!--[if lte IE 8]><html class="ng-csp ie ie8 lte9 lte8" data-placeholder-focus="false" lang="<?php p($_['language']); ?>" ><![endif]--> <!--[if IE 9]><html class="ng-csp ie ie9 lte9" data-placeholder-focus="false" lang="<?php p($_['language']); ?>" ><![endif]--> <!--[if (gt IE 9)|!(IE)]><!--><html class="ng-csp" data-placeholder-focus="false" lang="<?php p($_['language']); ?>" ><!--<![endif]--> - <head data-user="<?php p($_['user_uid']); ?>" data-requesttoken="<?php p($_['requesttoken']); ?>" + <head data-user="<?php p($_['user_uid']); ?>" data-user-displayname="<?php p($_['user_displayname']); ?>" data-requesttoken="<?php p($_['requesttoken']); ?>" <?php if ($_['updateAvailable']): ?> data-update-version="<?php p($_['updateVersion']); ?>" data-update-link="<?php p($_['updateLink']); ?>" <?php endif; ?> diff --git a/core/vendor/davclient.js/lib/client.js b/core/vendor/davclient.js/lib/client.js index 1a73c7db020..89c11516a38 100644 --- a/core/vendor/davclient.js/lib/client.js +++ b/core/vendor/davclient.js/lib/client.js @@ -1,17 +1,17 @@ if (typeof dav == 'undefined') { dav = {}; }; dav._XML_CHAR_MAP = { - '<': '<', - '>': '>', - '&': '&', - '"': '"', - "'": ''' + '<': '<', + '>': '>', + '&': '&', + '"': '"', + "'": ''' }; dav._escapeXml = function(s) { - return s.replace(/[<>&"']/g, function (ch) { - return dav._XML_CHAR_MAP[ch]; - }); + return s.replace(/[<>&"']/g, function (ch) { + return dav._XML_CHAR_MAP[ch]; + }); }; dav.Client = function(options) { @@ -79,17 +79,16 @@ dav.Client.prototype = { return this.request('PROPFIND', url, headers, body).then( function(result) { - var resultBody = this.parseMultiStatus(result.body); if (depth===0) { return { status: result.status, - body: resultBody[0], + body: result.body[0], xhr: result.xhr }; } else { return { status: result.status, - body: resultBody, + body: result.body, xhr: result.xhr }; } @@ -161,6 +160,7 @@ dav.Client.prototype = { */ request : function(method, url, headers, body) { + var self = this; var xhr = this.xhrProvider(); if (this.userName) { @@ -182,8 +182,13 @@ dav.Client.prototype = { return; } + var resultBody = xhr.response; + if (xhr.status === 207) { + resultBody = self.parseMultiStatus(xhr.response); + } + fulfill({ - body: xhr.response, + body: resultBody, status: xhr.status, xhr: xhr }); @@ -238,7 +243,7 @@ dav.Client.prototype = { } } - return content || propNode.textContent || propNode.text; + return content || propNode.textContent || propNode.text || ''; }, /** diff --git a/lib/private/files/config/usermountcache.php b/lib/private/files/config/usermountcache.php index 7d7b03fbc06..a2da3e9f528 100644 --- a/lib/private/files/config/usermountcache.php +++ b/lib/private/files/config/usermountcache.php @@ -74,7 +74,7 @@ class UserMountCache implements IUserMountCache { public function registerMounts(IUser $user, array $mounts) { // filter out non-proper storages coming from unit tests $mounts = array_filter($mounts, function (IMountPoint $mount) { - return $mount->getStorage()->getCache(); + return $mount->getStorage() && $mount->getStorage()->getCache(); }); /** @var ICachedMountInfo[] $newMounts */ $newMounts = array_map(function (IMountPoint $mount) use ($user) { diff --git a/tests/karma.config.js b/tests/karma.config.js index 467b270b350..4a7a9ad236e 100644 --- a/tests/karma.config.js +++ b/tests/karma.config.js @@ -83,6 +83,18 @@ module.exports = function(config) { testFiles: ['apps/files_versions/tests/js/**/*.js'] }, { + name: 'comments', + srcFiles: [ + // need to enforce loading order... + 'apps/comments/js/app.js', + 'apps/comments/js/commentmodel.js', + 'apps/comments/js/commentcollection.js', + 'apps/comments/js/commentstabview.js', + 'apps/comments/js/filesplugin' + ], + testFiles: ['apps/comments/tests/js/**/*.js'] + }, + { name: 'systemtags', srcFiles: [ // need to enforce loading order... |