aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--apps/comments/appinfo/app.php34
-rw-r--r--apps/comments/appinfo/info.xml16
-rw-r--r--apps/comments/css/comments.css51
-rw-r--r--apps/comments/js/app.js20
-rw-r--r--apps/comments/js/commentcollection.js110
-rw-r--r--apps/comments/js/commentmodel.js48
-rw-r--r--apps/comments/js/commentstabview.js236
-rw-r--r--apps/comments/js/filesplugin.js41
-rw-r--r--apps/comments/tests/js/commentscollectionSpec.js104
-rw-r--r--apps/comments/tests/js/commentstabviewSpec.js198
-rw-r--r--apps/dav/lib/comments/commentnode.php39
-rw-r--r--apps/dav/lib/comments/entitycollection.php57
-rw-r--r--apps/dav/lib/comments/entitytypecollection.php5
-rw-r--r--apps/dav/lib/comments/rootcollection.php1
-rw-r--r--apps/dav/tests/unit/comments/commentnode.php48
-rw-r--r--apps/dav/tests/unit/comments/entitycollection.php3
-rw-r--r--apps/dav/tests/unit/comments/entitytypecollection.php3
-rw-r--r--apps/files_external/appinfo/register_command.php3
-rw-r--r--apps/files_external/command/applicable.php157
-rw-r--r--apps/files_external/js/settings.js32
-rw-r--r--apps/files_external/l10n/fi_FI.js3
-rw-r--r--apps/files_external/l10n/fi_FI.json3
-rw-r--r--apps/files_external/l10n/he.js3
-rw-r--r--apps/files_external/l10n/he.json3
-rw-r--r--apps/files_external/l10n/it.js3
-rw-r--r--apps/files_external/l10n/it.json3
-rw-r--r--apps/files_external/l10n/pt_PT.js3
-rw-r--r--apps/files_external/l10n/pt_PT.json3
-rw-r--r--apps/files_external/l10n/ru.js3
-rw-r--r--apps/files_external/l10n/ru.json3
-rw-r--r--apps/files_external/lib/definitionparameter.php26
-rw-r--r--apps/files_external/tests/command/applicabletest.php168
-rw-r--r--apps/files_external/tests/command/commandtest.php104
-rw-r--r--apps/files_external/tests/definitionparameterttest.php24
-rw-r--r--apps/files_external/tests/js/settingsSpec.js48
-rw-r--r--apps/user_ldap/l10n/he.js5
-rw-r--r--apps/user_ldap/l10n/he.json5
-rw-r--r--build/integration/features/provisioning-v1.feature3
-rw-r--r--config/config.sample.php9
-rw-r--r--core/ajax/update.php6
-rw-r--r--core/command/upgrade.php6
-rw-r--r--core/js/js.js32
-rw-r--r--core/js/oc-backbone-webdav.js24
-rw-r--r--core/l10n/fr.js4
-rw-r--r--core/l10n/fr.json4
-rw-r--r--core/shipped.json1
-rw-r--r--core/templates/layout.user.php2
-rw-r--r--core/vendor/davclient.js/lib/client.js31
-rw-r--r--lib/private/comments/manager.php8
-rw-r--r--settings/l10n/fr.js1
-rw-r--r--settings/l10n/fr.json1
-rw-r--r--tests/karma.config.js12
53 files changed, 1676 insertions, 85 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/dav/lib/comments/commentnode.php b/apps/dav/lib/comments/commentnode.php
index eb26e350a42..a5d508dd198 100644
--- a/apps/dav/lib/comments/commentnode.php
+++ b/apps/dav/lib/comments/commentnode.php
@@ -26,12 +26,17 @@ use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\ILogger;
use OCP\IUserManager;
+use OCP\IUserSession;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\PropPatch;
class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
const NS_OWNCLOUD = 'http://owncloud.org/ns';
+ const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}isUnread';
+ const PROPERTY_NAME_MESSAGE = '{http://owncloud.org/ns}message';
+ const PROPERTY_NAME_ACTOR_DISPLAYNAME = '{http://owncloud.org/ns}actorDisplayName';
+
/** @var IComment */
public $comment;
@@ -47,18 +52,23 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
/** @var IUserManager */
protected $userManager;
+ /** @var IUserSession */
+ protected $userSession;
+
/**
* CommentNode constructor.
*
* @param ICommentsManager $commentsManager
* @param IComment $comment
* @param IUserManager $userManager
+ * @param IUserSession $userSession
* @param ILogger $logger
*/
public function __construct(
ICommentsManager $commentsManager,
IComment $comment,
IUserManager $userManager,
+ IUserSession $userSession,
ILogger $logger
) {
$this->commentsManager = $commentsManager;
@@ -74,6 +84,7 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
$this->properties[$name] = $getter;
}
$this->userManager = $userManager;
+ $this->userSession = $userSession;
}
/**
@@ -87,15 +98,17 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
'{http://owncloud.org/ns}parentId',
'{http://owncloud.org/ns}topmostParentId',
'{http://owncloud.org/ns}childrenCount',
- '{http://owncloud.org/ns}message',
'{http://owncloud.org/ns}verb',
'{http://owncloud.org/ns}actorType',
'{http://owncloud.org/ns}actorId',
- '{http://owncloud.org/ns}actorDisplayName',
'{http://owncloud.org/ns}creationDateTime',
'{http://owncloud.org/ns}latestChildDateTime',
'{http://owncloud.org/ns}objectType',
'{http://owncloud.org/ns}objectId',
+ // re-used property names are defined as constants
+ self::PROPERTY_NAME_MESSAGE,
+ self::PROPERTY_NAME_ACTOR_DISPLAYNAME,
+ self::PROPERTY_NAME_UNREAD
];
}
@@ -169,7 +182,7 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
*/
function propPatch(PropPatch $propPatch) {
// other properties than 'message' are read only
- $propPatch->handle('{'.self::NS_OWNCLOUD.'}message', [$this, 'updateComment']);
+ $propPatch->handle(self::PROPERTY_NAME_MESSAGE, [$this, 'updateComment']);
}
/**
@@ -201,8 +214,26 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
if($this->comment->getActorType() === 'users') {
$user = $this->userManager->get($this->comment->getActorId());
$displayName = is_null($user) ? null : $user->getDisplayName();
- $result['{' . self::NS_OWNCLOUD . '}actorDisplayName'] = $displayName;
+ $result[self::PROPERTY_NAME_ACTOR_DISPLAYNAME] = $displayName;
+ }
+
+ $unread = null;
+ $user = $this->userSession->getUser();
+ if(!is_null($user)) {
+ $readUntil = $this->commentsManager->getReadMark(
+ $this->comment->getObjectType(),
+ $this->comment->getObjectId(),
+ $user
+ );
+ if(is_null($readUntil)) {
+ $unread = 'true';
+ } else {
+ $unread = $this->comment->getCreationDateTime() > $readUntil;
+ // re-format for output
+ $unread = $unread ? 'true' : 'false';
+ }
}
+ $result[self::PROPERTY_NAME_UNREAD] = $unread;
return $result;
}
diff --git a/apps/dav/lib/comments/entitycollection.php b/apps/dav/lib/comments/entitycollection.php
index a93569f6e4f..a55a18c00c0 100644
--- a/apps/dav/lib/comments/entitycollection.php
+++ b/apps/dav/lib/comments/entitycollection.php
@@ -25,7 +25,9 @@ use OCP\Comments\ICommentsManager;
use OCP\Files\Folder;
use OCP\ILogger;
use OCP\IUserManager;
+use OCP\IUserSession;
use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\PropPatch;
/**
* Class EntityCollection
@@ -35,7 +37,9 @@ use Sabre\DAV\Exception\NotFound;
*
* @package OCA\DAV\Comments
*/
-class EntityCollection extends RootCollection {
+class EntityCollection extends RootCollection implements \Sabre\DAV\IProperties {
+ const PROPERTY_NAME_READ_MARKER = '{http://owncloud.org/ns}readMarker';
+
/** @var Folder */
protected $fileRoot;
@@ -51,6 +55,7 @@ class EntityCollection extends RootCollection {
* @param ICommentsManager $commentsManager
* @param Folder $fileRoot
* @param IUserManager $userManager
+ * @param IUserSession $userSession
* @param ILogger $logger
*/
public function __construct(
@@ -59,6 +64,7 @@ class EntityCollection extends RootCollection {
ICommentsManager $commentsManager,
Folder $fileRoot,
IUserManager $userManager,
+ IUserSession $userSession,
ILogger $logger
) {
foreach(['id', 'name'] as $property) {
@@ -73,6 +79,7 @@ class EntityCollection extends RootCollection {
$this->fileRoot = $fileRoot;
$this->logger = $logger;
$this->userManager = $userManager;
+ $this->userSession = $userSession;
}
/**
@@ -97,7 +104,13 @@ class EntityCollection extends RootCollection {
function getChild($name) {
try {
$comment = $this->commentsManager->get($name);
- return new CommentNode($this->commentsManager, $comment, $this->userManager, $this->logger);
+ return new CommentNode(
+ $this->commentsManager,
+ $comment,
+ $this->userManager,
+ $this->userSession,
+ $this->logger
+ );
} catch (\OCP\Comments\NotFoundException $e) {
throw new NotFound();
}
@@ -125,7 +138,13 @@ class EntityCollection extends RootCollection {
$comments = $this->commentsManager->getForObject($this->name, $this->id, $limit, $offset, $datetime);
$result = [];
foreach($comments as $comment) {
- $result[] = new CommentNode($this->commentsManager, $comment, $this->userManager, $this->logger);
+ $result[] = new CommentNode(
+ $this->commentsManager,
+ $comment,
+ $this->userManager,
+ $this->userSession,
+ $this->logger
+ );
}
return $result;
}
@@ -144,5 +163,37 @@ class EntityCollection extends RootCollection {
return false;
}
}
+
+ /**
+ * Sets the read marker to the specified date for the logged in user
+ *
+ * @param \DateTime $value
+ * @return bool
+ */
+ public function setReadMarker($value) {
+ $dateTime = new \DateTime($value);
+ $user = $this->userSession->getUser();
+ $this->commentsManager->setReadMark($this->name, $this->id, $dateTime, $user);
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ function propPatch(PropPatch $propPatch) {
+ $propPatch->handle(self::PROPERTY_NAME_READ_MARKER, [$this, 'setReadMarker']);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ function getProperties($properties) {
+ $marker = null;
+ $user = $this->userSession->getUser();
+ if(!is_null($user)) {
+ $marker = $this->commentsManager->getReadMark($this->name, $this->id, $user);
+ }
+ return [self::PROPERTY_NAME_READ_MARKER => $marker];
+ }
}
diff --git a/apps/dav/lib/comments/entitytypecollection.php b/apps/dav/lib/comments/entitytypecollection.php
index f49aac747c2..6bc42484207 100644
--- a/apps/dav/lib/comments/entitytypecollection.php
+++ b/apps/dav/lib/comments/entitytypecollection.php
@@ -25,6 +25,7 @@ use OCP\Comments\ICommentsManager;
use OCP\Files\Folder;
use OCP\ILogger;
use OCP\IUserManager;
+use OCP\IUserSession;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
@@ -51,6 +52,7 @@ class EntityTypeCollection extends RootCollection {
* @param ICommentsManager $commentsManager
* @param Folder $fileRoot
* @param IUserManager $userManager
+ * @param IUserSession $userSession
* @param ILogger $logger
*/
public function __construct(
@@ -58,6 +60,7 @@ class EntityTypeCollection extends RootCollection {
ICommentsManager $commentsManager,
Folder $fileRoot,
IUserManager $userManager,
+ IUserSession $userSession,
ILogger $logger
) {
$name = trim($name);
@@ -69,6 +72,7 @@ class EntityTypeCollection extends RootCollection {
$this->fileRoot = $fileRoot;
$this->logger = $logger;
$this->userManager = $userManager;
+ $this->userSession = $userSession;
}
/**
@@ -91,6 +95,7 @@ class EntityTypeCollection extends RootCollection {
$this->commentsManager,
$this->fileRoot,
$this->userManager,
+ $this->userSession,
$this->logger
);
}
diff --git a/apps/dav/lib/comments/rootcollection.php b/apps/dav/lib/comments/rootcollection.php
index aec8e655667..cda666f7162 100644
--- a/apps/dav/lib/comments/rootcollection.php
+++ b/apps/dav/lib/comments/rootcollection.php
@@ -98,6 +98,7 @@ class RootCollection implements ICollection {
$this->commentsManager,
$userFolder,
$this->userManager,
+ $this->userSession,
$this->logger
);
}
diff --git a/apps/dav/tests/unit/comments/commentnode.php b/apps/dav/tests/unit/comments/commentnode.php
index aa75e35d665..44ac54ae937 100644
--- a/apps/dav/tests/unit/comments/commentnode.php
+++ b/apps/dav/tests/unit/comments/commentnode.php
@@ -30,6 +30,7 @@ class CommentsNode extends \Test\TestCase {
protected $node;
protected $userManager;
protected $logger;
+ protected $userSession;
public function setUp() {
parent::setUp();
@@ -37,9 +38,16 @@ class CommentsNode extends \Test\TestCase {
$this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager');
$this->comment = $this->getMock('\OCP\Comments\IComment');
$this->userManager = $this->getMock('\OCP\IUserManager');
+ $this->userSession = $this->getMock('\OCP\IUserSession');
$this->logger = $this->getMock('\OCP\ILogger');
- $this->node = new CommentNode($this->commentsManager, $this->comment, $this->userManager, $this->logger);
+ $this->node = new CommentNode(
+ $this->commentsManager,
+ $this->comment,
+ $this->userManager,
+ $this->userSession,
+ $this->logger
+ );
}
public function testDelete() {
@@ -133,6 +141,7 @@ class CommentsNode extends \Test\TestCase {
$ns . 'latestChildDateTime' => new \DateTime('2016-01-12 18:48:00'),
$ns . 'objectType' => 'files',
$ns . 'objectId' => '1848',
+ $ns . 'isUnread' => null,
];
$this->comment->expects($this->once())
@@ -198,10 +207,45 @@ class CommentsNode extends \Test\TestCase {
$properties = $this->node->getProperties(null);
foreach($properties as $name => $value) {
- $this->assertTrue(isset($expected[$name]));
+ $this->assertTrue(array_key_exists($name, $expected));
$this->assertSame($expected[$name], $value);
unset($expected[$name]);
}
$this->assertTrue(empty($expected));
}
+
+ public function readCommentProvider() {
+ $creationDT = new \DateTime('2016-01-19 18:48:00');
+ $diff = new \DateInterval('PT2H');
+ $readDT1 = clone $creationDT; $readDT1->sub($diff);
+ $readDT2 = clone $creationDT; $readDT2->add($diff);
+ return [
+ [$creationDT, $readDT1, 'true'],
+ [$creationDT, $readDT2, 'false'],
+ [$creationDT, null, 'true'],
+ ];
+ }
+
+ /**
+ * @dataProvider readCommentProvider
+ * @param $expected
+ */
+ public function testGetPropertiesUnreadProperty($creationDT, $readDT, $expected) {
+ $this->comment->expects($this->any())
+ ->method('getCreationDateTime')
+ ->will($this->returnValue($creationDT));
+
+ $this->commentsManager->expects($this->once())
+ ->method('getReadMark')
+ ->will($this->returnValue($readDT));
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->will($this->returnValue($this->getMock('\OCP\IUser')));
+
+ $properties = $this->node->getProperties(null);
+
+ $this->assertTrue(array_key_exists(CommentNode::PROPERTY_NAME_UNREAD, $properties));
+ $this->assertSame($properties[CommentNode::PROPERTY_NAME_UNREAD], $expected);
+ }
}
diff --git a/apps/dav/tests/unit/comments/entitycollection.php b/apps/dav/tests/unit/comments/entitycollection.php
index 81442c7a873..5bf155f12ba 100644
--- a/apps/dav/tests/unit/comments/entitycollection.php
+++ b/apps/dav/tests/unit/comments/entitycollection.php
@@ -28,6 +28,7 @@ class EntityCollection extends \Test\TestCase {
protected $userManager;
protected $logger;
protected $collection;
+ protected $userSession;
public function setUp() {
parent::setUp();
@@ -35,6 +36,7 @@ class EntityCollection extends \Test\TestCase {
$this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager');
$this->folder = $this->getMock('\OCP\Files\Folder');
$this->userManager = $this->getMock('\OCP\IUserManager');
+ $this->userSession = $this->getMock('\OCP\IUserSession');
$this->logger = $this->getMock('\OCP\ILogger');
$this->collection = new \OCA\DAV\Comments\EntityCollection(
@@ -43,6 +45,7 @@ class EntityCollection extends \Test\TestCase {
$this->commentsManager,
$this->folder,
$this->userManager,
+ $this->userSession,
$this->logger
);
}
diff --git a/apps/dav/tests/unit/comments/entitytypecollection.php b/apps/dav/tests/unit/comments/entitytypecollection.php
index e8a88c4e2cb..f3aa2dbd71f 100644
--- a/apps/dav/tests/unit/comments/entitytypecollection.php
+++ b/apps/dav/tests/unit/comments/entitytypecollection.php
@@ -30,6 +30,7 @@ class EntityTypeCollection extends \Test\TestCase {
protected $userManager;
protected $logger;
protected $collection;
+ protected $userSession;
public function setUp() {
parent::setUp();
@@ -37,6 +38,7 @@ class EntityTypeCollection extends \Test\TestCase {
$this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager');
$this->folder = $this->getMock('\OCP\Files\Folder');
$this->userManager = $this->getMock('\OCP\IUserManager');
+ $this->userSession = $this->getMock('\OCP\IUserSession');
$this->logger = $this->getMock('\OCP\ILogger');
$this->collection = new \OCA\DAV\Comments\EntityTypeCollection(
@@ -44,6 +46,7 @@ class EntityTypeCollection extends \Test\TestCase {
$this->commentsManager,
$this->folder,
$this->userManager,
+ $this->userSession,
$this->logger
);
}
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/js/settings.js b/apps/files_external/js/settings.js
index 233215a4aa1..26df203091e 100644
--- a/apps/files_external/js/settings.js
+++ b/apps/files_external/js/settings.js
@@ -587,6 +587,19 @@ MountOptionsDropdown.prototype = {
var MountConfigListView = function($el, options) {
this.initialize($el, options);
};
+
+MountConfigListView.ParameterFlags = {
+ OPTIONAL: 1,
+ USER_PROVIDED: 2
+};
+
+MountConfigListView.ParameterTypes = {
+ TEXT: 0,
+ BOOLEAN: 1,
+ PASSWORD: 2,
+ HIDDEN: 3
+};
+
/**
* @memberOf OCA.External.Settings
*/
@@ -961,16 +974,15 @@ MountConfigListView.prototype = _.extend({
*/
writeParameterInput: function($td, parameter, placeholder, classes) {
var hasFlag = function(flag) {
- return placeholder.indexOf(flag) !== -1;
+ return (placeholder.flags & flag) === flag;
};
classes = $.isArray(classes) ? classes : [];
classes.push('added');
- if (placeholder.indexOf('&') === 0) {
+ if (hasFlag(MountConfigListView.ParameterFlags.OPTIONAL)) {
classes.push('optional');
- placeholder = placeholder.substring(1);
}
- if (hasFlag('@')) {
+ if (hasFlag(MountConfigListView.ParameterFlags.USER_PROVIDED)) {
if (this._isPersonal) {
classes.push('user_provided');
} else {
@@ -980,17 +992,13 @@ MountConfigListView.prototype = _.extend({
var newElement;
- var trimmedPlaceholder = placeholder;
- var flags = ['@', '*', '!', '#', '&']; // used to determine what kind of parameter
- while(flags.indexOf(trimmedPlaceholder[0]) !== -1) {
- trimmedPlaceholder = trimmedPlaceholder.substr(1);
- }
- if (hasFlag('*')) {
+ var trimmedPlaceholder = placeholder.value;
+ if (placeholder.type === MountConfigListView.ParameterTypes.PASSWORD) {
newElement = $('<input type="password" class="'+classes.join(' ')+'" data-parameter="'+parameter+'" placeholder="'+ trimmedPlaceholder+'" />');
- } else if (hasFlag('!')) {
+ } else if (placeholder.type === MountConfigListView.ParameterTypes.BOOLEAN) {
var checkboxId = _.uniqueId('checkbox_');
newElement = $('<input type="checkbox" id="'+checkboxId+'" class="'+classes.join(' ')+'" data-parameter="'+parameter+'" /><label for="'+checkboxId+'">'+ trimmedPlaceholder+'</label>');
- } else if (hasFlag('#')) {
+ } else if (placeholder.type === MountConfigListView.ParameterTypes.HIDDEN) {
newElement = $('<input type="hidden" class="'+classes.join(' ')+'" data-parameter="'+parameter+'" />');
} else {
newElement = $('<input type="text" class="'+classes.join(' ')+'" data-parameter="'+parameter+'" placeholder="'+ trimmedPlaceholder+'" />');
diff --git a/apps/files_external/l10n/fi_FI.js b/apps/files_external/l10n/fi_FI.js
index 5ee0dba896d..ec969586d6d 100644
--- a/apps/files_external/l10n/fi_FI.js
+++ b/apps/files_external/l10n/fi_FI.js
@@ -45,7 +45,10 @@ OC.L10N.register(
"Password" : "Salasana",
"Rackspace" : "Rackspace",
"API key" : "API-avain",
+ "Log-in credentials, save in database" : "Kirjautumistiedot, tallenna tietokantaan",
"Username and password" : "Käyttäjätunnus ja salasana",
+ "Log-in credentials, save in session" : "Kirjautumistiedot, tallenna istuntoon",
+ "User entered, store in database" : "Käyttäjän määrittämä, tallenna tietokantaan",
"RSA public key" : "Julkinen RSA-avain",
"Public key" : "Julkinen avain",
"Amazon S3" : "Amazon S3",
diff --git a/apps/files_external/l10n/fi_FI.json b/apps/files_external/l10n/fi_FI.json
index 5134563caef..8c39acff034 100644
--- a/apps/files_external/l10n/fi_FI.json
+++ b/apps/files_external/l10n/fi_FI.json
@@ -43,7 +43,10 @@
"Password" : "Salasana",
"Rackspace" : "Rackspace",
"API key" : "API-avain",
+ "Log-in credentials, save in database" : "Kirjautumistiedot, tallenna tietokantaan",
"Username and password" : "Käyttäjätunnus ja salasana",
+ "Log-in credentials, save in session" : "Kirjautumistiedot, tallenna istuntoon",
+ "User entered, store in database" : "Käyttäjän määrittämä, tallenna tietokantaan",
"RSA public key" : "Julkinen RSA-avain",
"Public key" : "Julkinen avain",
"Amazon S3" : "Amazon S3",
diff --git a/apps/files_external/l10n/he.js b/apps/files_external/l10n/he.js
index f5619a645a0..d963d796428 100644
--- a/apps/files_external/l10n/he.js
+++ b/apps/files_external/l10n/he.js
@@ -61,7 +61,10 @@ OC.L10N.register(
"Identity endpoint URL" : "זהות נתיב נקודת קצה",
"Rackspace" : "חץ אחורה",
"API key" : "מפתח API",
+ "Log-in credentials, save in database" : "אישורי התחברות, נשמרים במסד הנתונים",
"Username and password" : "שם משתמש וסיסמא",
+ "Log-in credentials, save in session" : "אישורי התחברות, נשמרים במידע שיחה - סשן",
+ "User entered, store in database" : "משתמש התחבר, נשמר במסד הנתונים",
"RSA public key" : "מפתח ציבורי RSA",
"Public key" : "מפתח ציבורי",
"Amazon S3" : "אמזון S3",
diff --git a/apps/files_external/l10n/he.json b/apps/files_external/l10n/he.json
index 1e6501262bd..fbdcc287495 100644
--- a/apps/files_external/l10n/he.json
+++ b/apps/files_external/l10n/he.json
@@ -59,7 +59,10 @@
"Identity endpoint URL" : "זהות נתיב נקודת קצה",
"Rackspace" : "חץ אחורה",
"API key" : "מפתח API",
+ "Log-in credentials, save in database" : "אישורי התחברות, נשמרים במסד הנתונים",
"Username and password" : "שם משתמש וסיסמא",
+ "Log-in credentials, save in session" : "אישורי התחברות, נשמרים במידע שיחה - סשן",
+ "User entered, store in database" : "משתמש התחבר, נשמר במסד הנתונים",
"RSA public key" : "מפתח ציבורי RSA",
"Public key" : "מפתח ציבורי",
"Amazon S3" : "אמזון S3",
diff --git a/apps/files_external/l10n/it.js b/apps/files_external/l10n/it.js
index 20c0e8b2e70..d9adaad833b 100644
--- a/apps/files_external/l10n/it.js
+++ b/apps/files_external/l10n/it.js
@@ -61,7 +61,10 @@ OC.L10N.register(
"Identity endpoint URL" : "URL endpoint delle identità",
"Rackspace" : "Rackspace",
"API key" : "Chiave API",
+ "Log-in credentials, save in database" : "Credenziali di accesso, salva nel database",
"Username and password" : "Nome utente e password",
+ "Log-in credentials, save in session" : "Credenziali di accesso, salva nella sessione",
+ "User entered, store in database" : "Digitate dall'utente, memorizza nel database",
"RSA public key" : "Chiave pubblica RSA",
"Public key" : "Chiave pubblica",
"Amazon S3" : "Amazon S3",
diff --git a/apps/files_external/l10n/it.json b/apps/files_external/l10n/it.json
index 1853e0eba62..b228cd76604 100644
--- a/apps/files_external/l10n/it.json
+++ b/apps/files_external/l10n/it.json
@@ -59,7 +59,10 @@
"Identity endpoint URL" : "URL endpoint delle identità",
"Rackspace" : "Rackspace",
"API key" : "Chiave API",
+ "Log-in credentials, save in database" : "Credenziali di accesso, salva nel database",
"Username and password" : "Nome utente e password",
+ "Log-in credentials, save in session" : "Credenziali di accesso, salva nella sessione",
+ "User entered, store in database" : "Digitate dall'utente, memorizza nel database",
"RSA public key" : "Chiave pubblica RSA",
"Public key" : "Chiave pubblica",
"Amazon S3" : "Amazon S3",
diff --git a/apps/files_external/l10n/pt_PT.js b/apps/files_external/l10n/pt_PT.js
index 5dc99fbfd4d..b84c399e34d 100644
--- a/apps/files_external/l10n/pt_PT.js
+++ b/apps/files_external/l10n/pt_PT.js
@@ -61,7 +61,10 @@ OC.L10N.register(
"Identity endpoint URL" : "Identidade URL endpoint",
"Rackspace" : "Rackspace",
"API key" : "Chave API",
+ "Log-in credentials, save in database" : "Credenciais de login, guardar na base de dados",
"Username and password" : "Nome de utilizador e palavra-passe",
+ "Log-in credentials, save in session" : "Credenciais de login, guardar na sessão",
+ "User entered, store in database" : "Utilizador introduzido, guardar na base de dados",
"RSA public key" : "Chave pública RSA",
"Public key" : "Chave pública",
"Amazon S3" : "Amazon S3",
diff --git a/apps/files_external/l10n/pt_PT.json b/apps/files_external/l10n/pt_PT.json
index 10728f875cf..ac65ca7744f 100644
--- a/apps/files_external/l10n/pt_PT.json
+++ b/apps/files_external/l10n/pt_PT.json
@@ -59,7 +59,10 @@
"Identity endpoint URL" : "Identidade URL endpoint",
"Rackspace" : "Rackspace",
"API key" : "Chave API",
+ "Log-in credentials, save in database" : "Credenciais de login, guardar na base de dados",
"Username and password" : "Nome de utilizador e palavra-passe",
+ "Log-in credentials, save in session" : "Credenciais de login, guardar na sessão",
+ "User entered, store in database" : "Utilizador introduzido, guardar na base de dados",
"RSA public key" : "Chave pública RSA",
"Public key" : "Chave pública",
"Amazon S3" : "Amazon S3",
diff --git a/apps/files_external/l10n/ru.js b/apps/files_external/l10n/ru.js
index 0c7708029a9..ceae42c7942 100644
--- a/apps/files_external/l10n/ru.js
+++ b/apps/files_external/l10n/ru.js
@@ -61,7 +61,10 @@ OC.L10N.register(
"Identity endpoint URL" : "Удостоверение конечной точки URL",
"Rackspace" : "Rackspace",
"API key" : "Ключ API",
+ "Log-in credentials, save in database" : "Учетные данные, хранить в базе данных",
"Username and password" : "Имя пользователя и пароль",
+ "Log-in credentials, save in session" : "Учетные данные, хранить в сессии",
+ "User entered, store in database" : "Введенные пользователем, хранить в базе данных",
"RSA public key" : "Открытый ключ RSA",
"Public key" : "Открытый ключ",
"Amazon S3" : "Amazon S3",
diff --git a/apps/files_external/l10n/ru.json b/apps/files_external/l10n/ru.json
index a20a3a91867..d0dab706ea9 100644
--- a/apps/files_external/l10n/ru.json
+++ b/apps/files_external/l10n/ru.json
@@ -59,7 +59,10 @@
"Identity endpoint URL" : "Удостоверение конечной точки URL",
"Rackspace" : "Rackspace",
"API key" : "Ключ API",
+ "Log-in credentials, save in database" : "Учетные данные, хранить в базе данных",
"Username and password" : "Имя пользователя и пароль",
+ "Log-in credentials, save in session" : "Учетные данные, хранить в сессии",
+ "User entered, store in database" : "Введенные пользователем, хранить в базе данных",
"RSA public key" : "Открытый ключ RSA",
"Public key" : "Открытый ключ",
"Amazon S3" : "Amazon S3",
diff --git a/apps/files_external/lib/definitionparameter.php b/apps/files_external/lib/definitionparameter.php
index dc7985837f5..27c6af0fcda 100644
--- a/apps/files_external/lib/definitionparameter.php
+++ b/apps/files_external/lib/definitionparameter.php
@@ -131,27 +131,11 @@ class DefinitionParameter implements \JsonSerializable {
* @return string
*/
public function jsonSerialize() {
- $prefix = '';
- switch ($this->getType()) {
- case self::VALUE_BOOLEAN:
- $prefix = '!';
- break;
- case self::VALUE_PASSWORD:
- $prefix = '*';
- break;
- case self::VALUE_HIDDEN:
- $prefix = '#';
- break;
- }
-
- if ($this->isFlagSet(self::FLAG_OPTIONAL)) {
- $prefix = '&' . $prefix;
- }
- if ($this->isFlagSet(self::FLAG_USER_PROVIDED)) {
- $prefix = '@' . $prefix;
- }
-
- return $prefix . $this->getText();
+ return [
+ 'value' => $this->getText(),
+ 'flags' => $this->getFlags(),
+ 'type' => $this->getType()
+ ];
}
public function isOptional() {
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/apps/files_external/tests/definitionparameterttest.php b/apps/files_external/tests/definitionparameterttest.php
index dc7c150ec96..e89058d5c78 100644
--- a/apps/files_external/tests/definitionparameterttest.php
+++ b/apps/files_external/tests/definitionparameterttest.php
@@ -27,18 +27,34 @@ class DefinitionParameterTest extends \Test\TestCase {
public function testJsonSerialization() {
$param = new Param('foo', 'bar');
- $this->assertEquals('bar', $param->jsonSerialize());
+ $this->assertEquals([
+ 'value' => 'bar',
+ 'flags' => 0,
+ 'type' => 0
+ ], $param->jsonSerialize());
$param->setType(Param::VALUE_BOOLEAN);
- $this->assertEquals('!bar', $param->jsonSerialize());
+ $this->assertEquals([
+ 'value' => 'bar',
+ 'flags' => 0,
+ 'type' => Param::VALUE_BOOLEAN
+ ], $param->jsonSerialize());
$param->setType(Param::VALUE_PASSWORD);
$param->setFlag(Param::FLAG_OPTIONAL);
- $this->assertEquals('&*bar', $param->jsonSerialize());
+ $this->assertEquals([
+ 'value' => 'bar',
+ 'flags' => Param::FLAG_OPTIONAL,
+ 'type' => Param::VALUE_PASSWORD
+ ], $param->jsonSerialize());
$param->setType(Param::VALUE_HIDDEN);
$param->setFlags(Param::FLAG_NONE);
- $this->assertEquals('#bar', $param->jsonSerialize());
+ $this->assertEquals([
+ 'value' => 'bar',
+ 'flags' => Param::FLAG_NONE,
+ 'type' => Param::VALUE_HIDDEN
+ ], $param->jsonSerialize());
}
public function validateValueProvider() {
diff --git a/apps/files_external/tests/js/settingsSpec.js b/apps/files_external/tests/js/settingsSpec.js
index 72b186edf06..6f5bb2a8e3e 100644
--- a/apps/files_external/tests/js/settingsSpec.js
+++ b/apps/files_external/tests/js/settingsSpec.js
@@ -58,8 +58,13 @@ describe('OCA.External.Settings tests', function() {
'identifier': '\\OC\\TestBackend',
'name': 'Test Backend',
'configuration': {
- 'field1': 'Display Name 1',
- 'field2': '&Display Name 2'
+ 'field1': {
+ 'value': 'Display Name 1'
+ },
+ 'field2': {
+ 'value': 'Display Name 2',
+ 'flags': 1
+ }
},
'authSchemes': {
'builtin': true,
@@ -70,8 +75,13 @@ describe('OCA.External.Settings tests', function() {
'identifier': '\\OC\\AnotherTestBackend',
'name': 'Another Test Backend',
'configuration': {
- 'field1': 'Display Name 1',
- 'field2': '&Display Name 2'
+ 'field1': {
+ 'value': 'Display Name 1'
+ },
+ 'field2': {
+ 'value': 'Display Name 2',
+ 'flags': 1
+ }
},
'authSchemes': {
'builtin': true,
@@ -82,12 +92,30 @@ describe('OCA.External.Settings tests', function() {
'identifier': '\\OC\\InputsTestBackend',
'name': 'Inputs test backend',
'configuration': {
- 'field_text': 'Text field',
- 'field_password': '*Password field',
- 'field_bool': '!Boolean field',
- 'field_hidden': '#Hidden field',
- 'field_text_optional': '&Text field optional',
- 'field_password_optional': '&*Password field optional'
+ 'field_text': {
+ 'value': 'Text field'
+ },
+ 'field_password': {
+ 'value': ',Password field',
+ 'type': 2
+ },
+ 'field_bool': {
+ 'value': 'Boolean field',
+ 'type': 1
+ },
+ 'field_hidden': {
+ 'value': 'Hidden field',
+ 'type': 3
+ },
+ 'field_text_optional': {
+ 'value': 'Text field optional',
+ 'flags': 1
+ },
+ 'field_password_optional': {
+ 'value': 'Password field optional',
+ 'flags': 1,
+ 'type': 2
+ }
},
'authSchemes': {
'builtin': true,
diff --git a/apps/user_ldap/l10n/he.js b/apps/user_ldap/l10n/he.js
index 836ae4a5712..56be7447e72 100644
--- a/apps/user_ldap/l10n/he.js
+++ b/apps/user_ldap/l10n/he.js
@@ -106,9 +106,14 @@ OC.L10N.register(
"Connection Settings" : "הגדרות התחברות",
"Configuration Active" : "תצורה פעילה",
"When unchecked, this configuration will be skipped." : "כאשר לא מסומן, נדלג על תצורה זו.",
+ "Backup (Replica) Host" : "גיבוי (העתק) שרת",
+ "Backup (Replica) Port" : "גיבוי (העתק) שער - פורט",
"Disable Main Server" : "ניטרול שרת עיקרי",
+ "Only connect to the replica server." : "חיבור רק להעתק שרת.",
+ "Turn off SSL certificate validation." : "כיבוי אימות אישורי אבטחה SSL.",
"in seconds. A change empties the cache." : "בשניות. שינוי מרוקן את המטמון.",
"Directory Settings" : "הגדרות תיקייה",
+ "Base User Tree" : "עץ משתמש בסיסי",
"in bytes" : "בבתים"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/user_ldap/l10n/he.json b/apps/user_ldap/l10n/he.json
index 39928da41bb..ca2ba2f91a6 100644
--- a/apps/user_ldap/l10n/he.json
+++ b/apps/user_ldap/l10n/he.json
@@ -104,9 +104,14 @@
"Connection Settings" : "הגדרות התחברות",
"Configuration Active" : "תצורה פעילה",
"When unchecked, this configuration will be skipped." : "כאשר לא מסומן, נדלג על תצורה זו.",
+ "Backup (Replica) Host" : "גיבוי (העתק) שרת",
+ "Backup (Replica) Port" : "גיבוי (העתק) שער - פורט",
"Disable Main Server" : "ניטרול שרת עיקרי",
+ "Only connect to the replica server." : "חיבור רק להעתק שרת.",
+ "Turn off SSL certificate validation." : "כיבוי אימות אישורי אבטחה SSL.",
"in seconds. A change empties the cache." : "בשניות. שינוי מרוקן את המטמון.",
"Directory Settings" : "הגדרות תיקייה",
+ "Base User Tree" : "עץ משתמש בסיסי",
"in bytes" : "בבתים"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
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 81a02efb3f7..bdbf3f42046 100644
--- a/config/config.sample.php
+++ b/config/config.sample.php
@@ -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/l10n/fr.js b/core/l10n/fr.js
index 68184dc4ddc..2f1ebc3a9cb 100644
--- a/core/l10n/fr.js
+++ b/core/l10n/fr.js
@@ -86,7 +86,7 @@ OC.L10N.register(
"Oct." : "Oct.",
"Nov." : "Nov.",
"Dec." : "Déc.",
- "<a href=\"{docUrl}\">There were problems with the code integrity check. More information…</a>" : "<a href=\"{docUrl}\">Il y a eu des problèmes avec la vérification d’intégrité du code. Plus d'infos...</a>",
+ "<a href=\"{docUrl}\">There were problems with the code integrity check. More information…</a>" : "<a href=\"{docUrl}\">Il y a eu des problèmes à la vérification d’intégrité du code. Plus d'infos...</a>",
"Settings" : "Paramètres",
"Saving..." : "Enregistrement…",
"seconds ago" : "à l'instant",
@@ -120,7 +120,6 @@ OC.L10N.register(
"Your web server is not yet set up properly to allow file synchronization because the WebDAV interface seems to be broken." : "Votre serveur web n'est pas correctement configuré pour la synchronisation de fichiers : l'interface WebDAV semble ne pas fonctionner.",
"Your web server is not set up properly to resolve \"{url}\". Further information can be found in our <a target=\"_blank\" href=\"{docLink}\">documentation</a>." : "La configuration du serveur web ne permet pas d'atteindre \"{url}\". Consultez la <a target=\"_blank\" href=\"{docLink}\">documentation</a> pour avoir plus d'informations à ce sujet.",
"This server has no working Internet connection. This means that some of the features like mounting external storage, notifications about updates or installation of third-party apps will not work. Accessing files remotely and sending of notification emails might not work, either. We suggest to enable Internet connection for this server if you want to have all features." : "Ce serveur ne peut se connecter à internet. Cela signifie que certaines fonctionnalités, telles que le montage de supports de stockage distants, les notifications de mises à jour ou l'installation d'applications tierces ne fonctionneront pas. L'accès aux fichiers à distance, ainsi que les notifications par mail peuvent aussi être indisponibles. Il est recommandé d'activer la connexion internet pour ce serveur si vous souhaitez disposer de l'ensemble des fonctionnalités offertes.",
- "Your data directory and your files are probably accessible from the Internet. The .htaccess file is not working. We strongly suggest that you configure your web server in a way that the data directory is no longer accessible or you move the data directory outside the web server document root." : "Votre dossier de données et vos fichiers sont probablement accessibles depuis internet. Le fichier .htaccess ne fonctionne pas. Nous vous recommandons vivement de configurer votre serveur web de façon à ce que ce dossier de données ne soit plus accessible, ou de le déplacer hors de la racine du serveur web.",
"No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our <a target=\"_blank\" href=\"{docLink}\">documentation</a>." : "Aucun cache mémoire n'est configuré. Si possible, configurez un cache pour augmenter les performances. Consultez la <a target=\"_blank\" href=\"{docLink}\">documentation</a> pour avoir plus d'informations à ce sujet.",
"/dev/urandom is not readable by PHP which is highly discouraged for security reasons. Further information can be found in our <a target=\"_blank\" href=\"{docLink}\">documentation</a>." : "/dev/urandom n'est pas lisible par PHP, ce qui est fortement déconseillé pour des raisons de sécurité. Consultez la <a target=\"_blank\" href=\"{docLink}\">documentation</a> pour avoir plus d'informations à ce sujet.",
"Your PHP version ({version}) is no longer <a target=\"_blank\" href=\"{phpLink}\">supported by PHP</a>. We encourage you to upgrade your PHP version to take advantage of performance and security updates provided by PHP." : "Votre version de PHP ({version}) <a target=\"_blank\" href=\"{phpLink}\">n'est plus prise en charge par les créateurs de PHP</a>. Nous vous recommandons de mettre à niveau votre installation de PHP pour bénéficier de meilleures performances et des mises à jour de sécurité fournies par PHP.",
@@ -128,6 +127,7 @@ OC.L10N.register(
"Memcached is configured as distributed cache, but the wrong PHP module \"memcache\" is installed. \\OC\\Memcache\\Memcached only supports \"memcached\" and not \"memcache\". See the <a target=\"_blank\" href=\"{wikiLink}\">memcached wiki about both modules</a>." : "\"memcached\" est configuré comme cache distribué, mais le module installé est \"memcache\". \\OC\\Memcache\\Memcached ne prend en charge que \"memcached\" et non \"memcache\". <a target=\"_blank\" href=\"{wikiLink}\">Consulter le wiki de memcached à propos de ces deux modules.</a>",
"Some files have not passed the integrity check. Further information on how to resolve this issue can be found in our <a target=\"_blank\" href=\"{docLink}\">documentation</a>. (<a href=\"{codeIntegrityDownloadEndpoint}\">List of invalid files…</a> / <a href=\"{rescanEndpoint}\">Rescan…</a>)" : "Des fichiers n'ont pas passé la vérification d’intégrité. \nConsultez la <a target=\"_blank\" href=\"{docLink}\">documentation</a> pour avoir plus d'informations sur comment résoudre ce problème.\n(<a target=\"_blank\" href=\"{codeIntegrityDownloadEndpoint}\">Liste des fichiers non valides…</a> / <a href=\"{rescanEndpoint}\">Relancer…</a>)",
"Error occurred while checking server setup" : "Une erreur s'est produite lors de la vérification de la configuration du serveur",
+ "Your data directory and your files are probably accessible from the Internet. The .htaccess file is not working. We strongly suggest that you configure your web server in a way that the data directory is no longer accessible or you move the data directory outside the web server document root." : "Votre dossier de données et vos fichiers sont probablement accessibles depuis internet. Le fichier .htaccess ne fonctionne pas. Nous vous recommandons vivement de configurer votre serveur web de façon à ce que ce dossier de données ne soit plus accessible, ou de le déplacer hors de la racine du serveur web.",
"The \"{header}\" HTTP header is not configured to equal to \"{expected}\". This is a potential security or privacy risk and we recommend adjusting this setting." : "L'en-tête HTTP \"{header}\" n'est pas configurée pour être égale à \"{expected}\" créant potentiellement un risque relié à la sécurité et à la vie privée. Il est donc recommandé d'ajuster ce paramètre.",
"The \"Strict-Transport-Security\" HTTP header is not configured to least \"{seconds}\" seconds. For enhanced security we recommend enabling HSTS as described in our <a href=\"{docUrl}\">security tips</a>." : "L'en-tête HTTP \"Strict-Transport-Security\" n'est pas configurée à \"{seconds}\" secondes. Pour renforcer la sécurité nous recommandons d'activer HSTS comme décrit dans notre <a href=\"{docUrl}\">Guide pour le renforcement et la sécurité</a>.",
"You are accessing this site via HTTP. We strongly suggest you configure your server to require using HTTPS instead as described in our <a href=\"{docUrl}\">security tips</a>." : "Vous accédez à ce site via HTTP. Nous vous recommandons fortement de configurer votre serveur pour forcer l'utilisation de HTTPS, comme expliqué dans notre <a href=\"{docUrl}\">Guide pour le renforcement et la sécurité</a>.",
diff --git a/core/l10n/fr.json b/core/l10n/fr.json
index b9a888268dc..df7cd0cf3c5 100644
--- a/core/l10n/fr.json
+++ b/core/l10n/fr.json
@@ -84,7 +84,7 @@
"Oct." : "Oct.",
"Nov." : "Nov.",
"Dec." : "Déc.",
- "<a href=\"{docUrl}\">There were problems with the code integrity check. More information…</a>" : "<a href=\"{docUrl}\">Il y a eu des problèmes avec la vérification d’intégrité du code. Plus d'infos...</a>",
+ "<a href=\"{docUrl}\">There were problems with the code integrity check. More information…</a>" : "<a href=\"{docUrl}\">Il y a eu des problèmes à la vérification d’intégrité du code. Plus d'infos...</a>",
"Settings" : "Paramètres",
"Saving..." : "Enregistrement…",
"seconds ago" : "à l'instant",
@@ -118,7 +118,6 @@
"Your web server is not yet set up properly to allow file synchronization because the WebDAV interface seems to be broken." : "Votre serveur web n'est pas correctement configuré pour la synchronisation de fichiers : l'interface WebDAV semble ne pas fonctionner.",
"Your web server is not set up properly to resolve \"{url}\". Further information can be found in our <a target=\"_blank\" href=\"{docLink}\">documentation</a>." : "La configuration du serveur web ne permet pas d'atteindre \"{url}\". Consultez la <a target=\"_blank\" href=\"{docLink}\">documentation</a> pour avoir plus d'informations à ce sujet.",
"This server has no working Internet connection. This means that some of the features like mounting external storage, notifications about updates or installation of third-party apps will not work. Accessing files remotely and sending of notification emails might not work, either. We suggest to enable Internet connection for this server if you want to have all features." : "Ce serveur ne peut se connecter à internet. Cela signifie que certaines fonctionnalités, telles que le montage de supports de stockage distants, les notifications de mises à jour ou l'installation d'applications tierces ne fonctionneront pas. L'accès aux fichiers à distance, ainsi que les notifications par mail peuvent aussi être indisponibles. Il est recommandé d'activer la connexion internet pour ce serveur si vous souhaitez disposer de l'ensemble des fonctionnalités offertes.",
- "Your data directory and your files are probably accessible from the Internet. The .htaccess file is not working. We strongly suggest that you configure your web server in a way that the data directory is no longer accessible or you move the data directory outside the web server document root." : "Votre dossier de données et vos fichiers sont probablement accessibles depuis internet. Le fichier .htaccess ne fonctionne pas. Nous vous recommandons vivement de configurer votre serveur web de façon à ce que ce dossier de données ne soit plus accessible, ou de le déplacer hors de la racine du serveur web.",
"No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our <a target=\"_blank\" href=\"{docLink}\">documentation</a>." : "Aucun cache mémoire n'est configuré. Si possible, configurez un cache pour augmenter les performances. Consultez la <a target=\"_blank\" href=\"{docLink}\">documentation</a> pour avoir plus d'informations à ce sujet.",
"/dev/urandom is not readable by PHP which is highly discouraged for security reasons. Further information can be found in our <a target=\"_blank\" href=\"{docLink}\">documentation</a>." : "/dev/urandom n'est pas lisible par PHP, ce qui est fortement déconseillé pour des raisons de sécurité. Consultez la <a target=\"_blank\" href=\"{docLink}\">documentation</a> pour avoir plus d'informations à ce sujet.",
"Your PHP version ({version}) is no longer <a target=\"_blank\" href=\"{phpLink}\">supported by PHP</a>. We encourage you to upgrade your PHP version to take advantage of performance and security updates provided by PHP." : "Votre version de PHP ({version}) <a target=\"_blank\" href=\"{phpLink}\">n'est plus prise en charge par les créateurs de PHP</a>. Nous vous recommandons de mettre à niveau votre installation de PHP pour bénéficier de meilleures performances et des mises à jour de sécurité fournies par PHP.",
@@ -126,6 +125,7 @@
"Memcached is configured as distributed cache, but the wrong PHP module \"memcache\" is installed. \\OC\\Memcache\\Memcached only supports \"memcached\" and not \"memcache\". See the <a target=\"_blank\" href=\"{wikiLink}\">memcached wiki about both modules</a>." : "\"memcached\" est configuré comme cache distribué, mais le module installé est \"memcache\". \\OC\\Memcache\\Memcached ne prend en charge que \"memcached\" et non \"memcache\". <a target=\"_blank\" href=\"{wikiLink}\">Consulter le wiki de memcached à propos de ces deux modules.</a>",
"Some files have not passed the integrity check. Further information on how to resolve this issue can be found in our <a target=\"_blank\" href=\"{docLink}\">documentation</a>. (<a href=\"{codeIntegrityDownloadEndpoint}\">List of invalid files…</a> / <a href=\"{rescanEndpoint}\">Rescan…</a>)" : "Des fichiers n'ont pas passé la vérification d’intégrité. \nConsultez la <a target=\"_blank\" href=\"{docLink}\">documentation</a> pour avoir plus d'informations sur comment résoudre ce problème.\n(<a target=\"_blank\" href=\"{codeIntegrityDownloadEndpoint}\">Liste des fichiers non valides…</a> / <a href=\"{rescanEndpoint}\">Relancer…</a>)",
"Error occurred while checking server setup" : "Une erreur s'est produite lors de la vérification de la configuration du serveur",
+ "Your data directory and your files are probably accessible from the Internet. The .htaccess file is not working. We strongly suggest that you configure your web server in a way that the data directory is no longer accessible or you move the data directory outside the web server document root." : "Votre dossier de données et vos fichiers sont probablement accessibles depuis internet. Le fichier .htaccess ne fonctionne pas. Nous vous recommandons vivement de configurer votre serveur web de façon à ce que ce dossier de données ne soit plus accessible, ou de le déplacer hors de la racine du serveur web.",
"The \"{header}\" HTTP header is not configured to equal to \"{expected}\". This is a potential security or privacy risk and we recommend adjusting this setting." : "L'en-tête HTTP \"{header}\" n'est pas configurée pour être égale à \"{expected}\" créant potentiellement un risque relié à la sécurité et à la vie privée. Il est donc recommandé d'ajuster ce paramètre.",
"The \"Strict-Transport-Security\" HTTP header is not configured to least \"{seconds}\" seconds. For enhanced security we recommend enabling HSTS as described in our <a href=\"{docUrl}\">security tips</a>." : "L'en-tête HTTP \"Strict-Transport-Security\" n'est pas configurée à \"{seconds}\" secondes. Pour renforcer la sécurité nous recommandons d'activer HSTS comme décrit dans notre <a href=\"{docUrl}\">Guide pour le renforcement et la sécurité</a>.",
"You are accessing this site via HTTP. We strongly suggest you configure your server to require using HTTPS instead as described in our <a href=\"{docUrl}\">security tips</a>." : "Vous accédez à ce site via HTTP. Nous vous recommandons fortement de configurer votre serveur pour forcer l'utilisation de HTTPS, comme expliqué dans notre <a href=\"{docUrl}\">Guide pour le renforcement et la sécurité</a>.",
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 = {
- '<': '&lt;',
- '>': '&gt;',
- '&': '&amp;',
- '"': '&quot;',
- "'": '&apos;'
+ '<': '&lt;',
+ '>': '&gt;',
+ '&': '&amp;',
+ '"': '&quot;',
+ "'": '&apos;'
};
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/comments/manager.php b/lib/private/comments/manager.php
index 23add3fd2c3..28bd3b0916a 100644
--- a/lib/private/comments/manager.php
+++ b/lib/private/comments/manager.php
@@ -629,9 +629,15 @@ class Manager implements ICommentsManager {
$affectedRows = $qb
->update('comments_read_markers')
->set('user_id', $values['user_id'])
- ->set('marker_datetime', $values['marker_datetime'], 'datetime')
+ ->set('marker_datetime', $values['marker_datetime'])
->set('object_type', $values['object_type'])
->set('object_id', $values['object_id'])
+ ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
+ ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
+ ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
+ ->setParameter('user_id', $user->getUID(), \PDO::PARAM_STR)
+ ->setParameter('object_type', $objectType, \PDO::PARAM_STR)
+ ->setParameter('object_id', $objectId, \PDO::PARAM_STR)
->execute();
if ($affectedRows > 0) {
diff --git a/settings/l10n/fr.js b/settings/l10n/fr.js
index 7f0034ae940..43a6ef4358e 100644
--- a/settings/l10n/fr.js
+++ b/settings/l10n/fr.js
@@ -205,6 +205,7 @@ OC.L10N.register(
"Experimental applications ahead" : "Attention! Applications expérimentales",
"Experimental apps are not checked for security issues, new or known to be unstable and under heavy development. Installing them can cause data loss or security breaches." : "Les applications expérimentales n'ont pas fait l'objet de tests de sécurité, sont encore en développement et peuvent être instables. Les installer peut causer des pertes de données ou des failles de sécurité. ",
"by %s" : "par %s",
+ "%s-licensed" : "Sous licence %s",
"Documentation:" : "Documentation :",
"User documentation" : "Documentation utilisateur",
"Admin documentation" : "Documentation administrateur",
diff --git a/settings/l10n/fr.json b/settings/l10n/fr.json
index b690dc5d8e6..9b5fbd02401 100644
--- a/settings/l10n/fr.json
+++ b/settings/l10n/fr.json
@@ -203,6 +203,7 @@
"Experimental applications ahead" : "Attention! Applications expérimentales",
"Experimental apps are not checked for security issues, new or known to be unstable and under heavy development. Installing them can cause data loss or security breaches." : "Les applications expérimentales n'ont pas fait l'objet de tests de sécurité, sont encore en développement et peuvent être instables. Les installer peut causer des pertes de données ou des failles de sécurité. ",
"by %s" : "par %s",
+ "%s-licensed" : "Sous licence %s",
"Documentation:" : "Documentation :",
"User documentation" : "Documentation utilisateur",
"Admin documentation" : "Documentation administrateur",
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...