diff options
author | Daniel Calviño Sánchez <danxuliu@gmail.com> | 2018-10-15 17:09:46 +0200 |
---|---|---|
committer | Daniel Calviño Sánchez <danxuliu@gmail.com> | 2018-11-02 13:30:18 +0100 |
commit | 376704e83413d093d596f1b9168daf24907189aa (patch) | |
tree | 0058f7763e7cdbd535400847843178655a54f97d /core | |
parent | adf80aa8b329cf08e3f21a1ed8f722ab066d868d (diff) | |
download | nextcloud-server-376704e83413d093d596f1b9168daf24907189aa.tar.gz nextcloud-server-376704e83413d093d596f1b9168daf24907189aa.zip |
Add "Password protect by Talk" to the menu of link shares
When Talk is enabled the menu for link shares now shows a checkbox to
protect the password by Talk (that is, to show the "Request password by
Talk" UI in the authentication page for the link share).
Although in e-mail shares protecting the share with a password and
protecting the password by Talk are mutually exclusive actions (as when
the password is set it is sent to the sharee, so it must be set again
when protecting it by Talk to be able to verify the identity of the
sharee), in the case of link shares protecting the password by Talk is
an additional step to protecting the share with a password (as just
setting the password does not disclose it to anyone). As such, the
checkbox is shown only when there is a password set for the link share
(even if the field itself for the password is not shown, like when they
are enforced in the settings).
Note that the icon set for the field, "icon-passwordtalk", does not
currently exist; it is the same used for e-mail shares, and it is needed
simply to get the right padding in the menu.
Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Diffstat (limited to 'core')
-rw-r--r-- | core/js/share/sharedialoglinkshareview_popover_menu.handlebars | 10 | ||||
-rw-r--r-- | core/js/sharedialoglinkshareview.js | 64 | ||||
-rw-r--r-- | core/js/shareitemmodel.js | 5 | ||||
-rw-r--r-- | core/js/sharetemplates.js | 29 | ||||
-rw-r--r-- | core/js/tests/specs/sharedialoglinkshareview.js | 113 | ||||
-rw-r--r-- | core/js/tests/specs/shareitemmodelSpec.js | 13 |
6 files changed, 222 insertions, 12 deletions
diff --git a/core/js/share/sharedialoglinkshareview_popover_menu.handlebars b/core/js/share/sharedialoglinkshareview_popover_menu.handlebars index cc951ce047d..59312bc70b0 100644 --- a/core/js/share/sharedialoglinkshareview_popover_menu.handlebars +++ b/core/js/share/sharedialoglinkshareview_popover_menu.handlebars @@ -62,6 +62,16 @@ </span> </li> {{/if}} + {{#if showPasswordByTalkCheckBox}} + <li> + <span class="shareOption menuitem"> + <span class="icon-loading-small hidden"></span> + <input type="checkbox" name="passwordByTalk" id="passwordByTalk-{{cid}}" class="checkbox passwordByTalkCheckbox" + {{#if isPasswordByTalkSet}}checked="checked"{{/if}} /> + <label for="passwordByTalk-{{cid}}">{{passwordByTalkLabel}}</label> + </span> + </li> + {{/if}} <li> <span class="menuitem"> <input id="expireDate-{{cid}}" type="checkbox" name="expirationDate" class="expireDate checkbox" diff --git a/core/js/sharedialoglinkshareview.js b/core/js/sharedialoglinkshareview.js index 4ea8c0fa153..e5af4ad1f17 100644 --- a/core/js/sharedialoglinkshareview.js +++ b/core/js/sharedialoglinkshareview.js @@ -54,6 +54,7 @@ 'focusout input.linkPassText': 'onPasswordEntered', 'keyup input.linkPassText': 'onPasswordKeyUp', 'change .showPasswordCheckbox': 'onShowPasswordClick', + 'change .passwordByTalkCheckbox': 'onPasswordByTalkChange', 'change .publicEditingCheckbox': 'onAllowPublicEditingChange', // copy link url 'click .linkText': 'onLinkTextClick', @@ -96,6 +97,37 @@ view.render(); }); + this.model.on('change:linkShares', function(model, linkShares) { + // The "Password protect by Talk" item is shown only when there + // is a password. Unfortunately there is no fine grained + // rendering of items in the link shares, so the whole view + // needs to be rendered again when the password of a share + // changes. + // Note that this event handler is concerned only about password + // changes; other changes in the link shares does not trigger + // a rendering, so the view must be rendered again as needed in + // those cases (for example, when a link share is removed). + + var previousLinkShares = model.previous('linkShares'); + if (previousLinkShares.length !== linkShares.length) { + return; + } + + var i; + for (i = 0; i < linkShares.length; i++) { + if (linkShares[i].id !== previousLinkShares[i].id) { + // A resorting should never happen, but just in case. + return; + } + + if (linkShares[i].password !== previousLinkShares[i].password) { + view.render(); + + return; + } + } + }); + if(!_.isUndefined(options.configModel)) { this.configModel = options.configModel; } else { @@ -343,6 +375,32 @@ }); }, + onPasswordByTalkChange: function(event) { + var $element = $(event.target); + var $li = $element.closest('li[data-share-id]'); + var shareId = $li.data('share-id'); + var $checkbox = $li.find('.passwordByTalkCheckbox'); + $checkbox.siblings('.icon-loading-small').removeClass('hidden').addClass('inlineblock'); + + var sendPasswordByTalk = false; + if($checkbox.is(':checked')) { + sendPasswordByTalk = true; + } + + this.model.saveLinkShare({ + sendPasswordByTalk: sendPasswordByTalk, + cid: shareId + }, { + success: function() { + $checkbox.siblings('.icon-loading-small').addClass('hidden').removeClass('inlineblock'); + }, + error: function(obj, msg) { + OC.Notification.showTemporary(t('core', 'Unable to toggle this option')); + $checkbox.siblings('.icon-loading-small').addClass('hidden').removeClass('inlineblock'); + } + }); + }, + onAllowPublicEditingChange: function(event) { var $element = $(event.target); var $li = $element.closest('li[data-share-id]'); @@ -790,6 +848,9 @@ expireDate = moment(share.expiration, 'YYYY-MM-DD').format('DD-MM-YYYY'); } + var isTalkEnabled = oc_appswebroots['spreed'] !== undefined; + var sendPasswordByTalk = share.sendPasswordByTalk; + var showHideDownloadCheckbox = !this.model.isFolder(); var hideDownload = share.hideDownload; @@ -816,6 +877,9 @@ passwordPlaceholder: isPasswordSet ? PASSWORD_PLACEHOLDER : PASSWORD_PLACEHOLDER_MESSAGE, isPasswordSet: isPasswordSet || isPasswordEnabledByDefault || isPasswordEnforced, showPasswordCheckBox: showPasswordCheckBox, + showPasswordByTalkCheckBox: isTalkEnabled && isPasswordSet, + passwordByTalkLabel: t('core', 'Password protect by Talk'), + isPasswordByTalkSet: sendPasswordByTalk, publicUploadRWChecked: publicUploadRWChecked, publicUploadRChecked: publicUploadRChecked, publicUploadWChecked: publicUploadWChecked, diff --git a/core/js/shareitemmodel.js b/core/js/shareitemmodel.js index 1bbdb2448ab..f4ac03e1c18 100644 --- a/core/js/shareitemmodel.js +++ b/core/js/shareitemmodel.js @@ -19,6 +19,7 @@ * @property {string} token * @property {bool} hideDownload * @property {string|null} password + * @property {bool} sendPasswordByTalk * @property {number} permissions * @property {Date} expiration * @property {number} stime share time @@ -141,6 +142,7 @@ hideDownload: false, password: '', passwordChanged: false, + sendPasswordByTalk: false, permissions: OC.PERMISSION_READ, expireDate: this.configModel.getDefaultExpirationDateString(), shareType: OC.Share.SHARE_TYPE_LINK @@ -873,7 +875,8 @@ // hide_download is returned as an int, so force it // to a boolean hideDownload: !!share.hide_download, - password: share.share_with + password: share.share_with, + sendPasswordByTalk: share.send_password_by_talk })); return share; diff --git a/core/js/sharetemplates.js b/core/js/sharetemplates.js index bd9886e6afd..6469e264d75 100644 --- a/core/js/sharetemplates.js +++ b/core/js/sharetemplates.js @@ -158,18 +158,30 @@ templates['sharedialoglinkshareview_popover_menu'] = template({"1":function(cont },"11":function(container,depth0,helpers,partials,data) { return "hidden"; },"13":function(container,depth0,helpers,partials,data) { - return "datepicker"; + var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; + + return " <li>\n <span class=\"shareOption menuitem\">\n <span class=\"icon-loading-small hidden\"></span>\n <input type=\"checkbox\" name=\"passwordByTalk\" id=\"passwordByTalk-" + + alias4(((helper = (helper = helpers.cid || (depth0 != null ? depth0.cid : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"cid","hash":{},"data":data}) : helper))) + + "\" class=\"checkbox passwordByTalkCheckbox\"\n " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.isPasswordByTalkSet : depth0),{"name":"if","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + " />\n <label for=\"passwordByTalk-" + + alias4(((helper = (helper = helpers.cid || (depth0 != null ? depth0.cid : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"cid","hash":{},"data":data}) : helper))) + + "\">" + + alias4(((helper = (helper = helpers.passwordByTalkLabel || (depth0 != null ? depth0.passwordByTalkLabel : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"passwordByTalkLabel","hash":{},"data":data}) : helper))) + + "</label>\n </span>\n </li>\n"; },"15":function(container,depth0,helpers,partials,data) { + return "datepicker"; +},"17":function(container,depth0,helpers,partials,data) { var helper; return container.escapeExpression(((helper = (helper = helpers.expireDate || (depth0 != null ? depth0.expireDate : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"expireDate","hash":{},"data":data}) : helper))); -},"17":function(container,depth0,helpers,partials,data) { +},"19":function(container,depth0,helpers,partials,data) { var helper; return container.escapeExpression(((helper = (helper = helpers.defaultExpireDate || (depth0 != null ? depth0.defaultExpireDate : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"defaultExpireDate","hash":{},"data":data}) : helper))); -},"19":function(container,depth0,helpers,partials,data) { - return "readonly"; },"21":function(container,depth0,helpers,partials,data) { + return "readonly"; +},"23":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; return " <li>\n <a href=\"#\" class=\"menuitem pop-up\" data-url=\"" @@ -193,6 +205,7 @@ templates['sharedialoglinkshareview_popover_menu'] = template({"1":function(cont + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.publicEditing : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.showHideDownloadCheckbox : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.showPasswordCheckBox : depth0),{"name":"if","hash":{},"fn":container.program(8, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.showPasswordByTalkCheckBox : depth0),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " <li>\n <span class=\"menuitem\">\n <input id=\"expireDate-" + alias4(((helper = (helper = helpers.cid || (depth0 != null ? depth0.cid : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"cid","hash":{},"data":data}) : helper))) + "\" type=\"checkbox\" name=\"expirationDate\" class=\"expireDate checkbox\"\n " @@ -216,15 +229,15 @@ templates['sharedialoglinkshareview_popover_menu'] = template({"1":function(cont + "</label>\n <!-- do not use the datepicker if enforced -->\n <input id=\"expirationDatePicker-" + alias4(((helper = (helper = helpers.cid || (depth0 != null ? depth0.cid : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"cid","hash":{},"data":data}) : helper))) + "\" class=\"" - + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.isExpirationEnforced : depth0),{"name":"unless","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.isExpirationEnforced : depth0),{"name":"unless","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\" type=\"text\"\n placeholder=\"" + alias4(((helper = (helper = helpers.expirationDatePlaceholder || (depth0 != null ? depth0.expirationDatePlaceholder : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"expirationDatePlaceholder","hash":{},"data":data}) : helper))) + "\" value=\"" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.hasExpireDate : depth0),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.program(17, data, 0),"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.hasExpireDate : depth0),{"name":"if","hash":{},"fn":container.program(17, data, 0),"inverse":container.program(19, data, 0),"data":data})) != null ? stack1 : "") + "\"\n data-max-date=\"" + alias4(((helper = (helper = helpers.maxDate || (depth0 != null ? depth0.maxDate : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"maxDate","hash":{},"data":data}) : helper))) + "\" " - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.isExpirationEnforced : depth0),{"name":"if","hash":{},"fn":container.program(19, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.isExpirationEnforced : depth0),{"name":"if","hash":{},"fn":container.program(21, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " />\n </span>\n </li>\n <li>\n <a href=\"#\" class=\"share-add\">\n <span class=\"icon-loading-small hidden\"></span>\n <span class=\"icon icon-edit\"></span>\n <span>" + alias4(((helper = (helper = helpers.addNoteLabel || (depth0 != null ? depth0.addNoteLabel : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"addNoteLabel","hash":{},"data":data}) : helper))) + "</span>\n <input type=\"button\" class=\"share-note-delete icon-delete " @@ -236,7 +249,7 @@ templates['sharedialoglinkshareview_popover_menu'] = template({"1":function(cont + "</textarea>\n <input type=\"submit\" class=\"icon-confirm share-note-submit\" value=\"\" id=\"add-note-" + alias4(((helper = (helper = helpers.shareId || (depth0 != null ? depth0.shareId : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"shareId","hash":{},"data":data}) : helper))) + "\" />\n </span>\n </li>\n" - + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.social : depth0),{"name":"each","hash":{},"fn":container.program(21, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.social : depth0),{"name":"each","hash":{},"fn":container.program(23, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " <li>\n <a href=\"#\" class=\"unshare\"><span class=\"icon-loading-small hidden\"></span><span class=\"icon icon-delete\"></span><span>" + alias4(((helper = (helper = helpers.unshareLinkLabel || (depth0 != null ? depth0.unshareLinkLabel : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"unshareLinkLabel","hash":{},"data":data}) : helper))) + "</span></a>\n </li>\n <li>\n <a href=\"#\" class=\"new-share\">\n <span class=\"icon-loading-small hidden\"></span>\n <span class=\"icon icon-add\"></span>\n <span>" diff --git a/core/js/tests/specs/sharedialoglinkshareview.js b/core/js/tests/specs/sharedialoglinkshareview.js index f5fe8725c03..c2d84fd2e87 100644 --- a/core/js/tests/specs/sharedialoglinkshareview.js +++ b/core/js/tests/specs/sharedialoglinkshareview.js @@ -235,4 +235,117 @@ describe('OC.Share.ShareDialogLinkShareView', function () { }); + describe('protect password by Talk', function () { + + var $passwordByTalkCheckbox; + var $workingIcon; + + beforeEach(function () { + // Needed to render the view + configModel.isShareWithLinkAllowed.returns(true); + + // "Enable" Talk + window.oc_appswebroots['spreed'] = window.oc_webroot + '/apps/files/'; + + shareModel.set({ + linkShares: [{ + id: 123, + password: 'password' + }] + }); + view.render(); + + $passwordByTalkCheckbox = view.$el.find('.passwordByTalkCheckbox'); + $workingIcon = $passwordByTalkCheckbox.prev('.icon-loading-small'); + + sinon.stub(shareModel, 'saveLinkShare'); + + expect($workingIcon.hasClass('hidden')).toBeTruthy(); + }); + + afterEach(function () { + shareModel.saveLinkShare.restore(); + }); + + it('is shown if Talk is enabled and there is a password set', function() { + expect($passwordByTalkCheckbox.length).toBeTruthy(); + }); + + it('is not shown if Talk is enabled but there is no password set', function() { + // Changing the password value also triggers the rendering + shareModel.set({ + linkShares: [{ + id: 123 + }] + }); + + $passwordByTalkCheckbox = view.$el.find('.passwordByTalkCheckbox'); + + expect($passwordByTalkCheckbox.length).toBeFalsy(); + }); + + it('is not shown if there is a password set but Talk is not enabled', function() { + // "Disable" Talk + delete window.oc_appswebroots['spreed']; + + view.render(); + + $passwordByTalkCheckbox = view.$el.find('.passwordByTalkCheckbox'); + + expect($passwordByTalkCheckbox.length).toBeFalsy(); + }); + + it('checkbox is checked when the setting is enabled', function () { + shareModel.set({ + linkShares: [{ + id: 123, + password: 'password', + sendPasswordByTalk: true + }] + }); + view.render(); + + $passwordByTalkCheckbox = view.$el.find('.passwordByTalkCheckbox'); + + expect($passwordByTalkCheckbox.is(':checked')).toEqual(true); + }); + + it('checkbox is not checked when the setting is disabled', function () { + expect($passwordByTalkCheckbox.is(':checked')).toEqual(false); + }); + + it('enables the setting if clicked when unchecked', function () { + // Simulate the click by checking the checkbox and then triggering + // the "change" event. + $passwordByTalkCheckbox.prop('checked', true); + $passwordByTalkCheckbox.change(); + + expect($workingIcon.hasClass('hidden')).toBeFalsy(); + expect(shareModel.saveLinkShare.withArgs({ sendPasswordByTalk: true, cid: 123 }).calledOnce).toBeTruthy(); + }); + + it('disables the setting if clicked when checked', function () { + shareModel.set({ + linkShares: [{ + id: 123, + password: 'password', + sendPasswordByTalk: true + }] + }); + view.render(); + + $passwordByTalkCheckbox = view.$el.find('.passwordByTalkCheckbox'); + $workingIcon = $passwordByTalkCheckbox.prev('.icon-loading-small'); + + // Simulate the click by unchecking the checkbox and then triggering + // the "change" event. + $passwordByTalkCheckbox.prop('checked', false); + $passwordByTalkCheckbox.change(); + + expect($workingIcon.hasClass('hidden')).toBeFalsy(); + expect(shareModel.saveLinkShare.withArgs({ sendPasswordByTalk: false, cid: 123 }).calledOnce).toBeTruthy(); + }); + + }); + }); diff --git a/core/js/tests/specs/shareitemmodelSpec.js b/core/js/tests/specs/shareitemmodelSpec.js index 3b4dc5a960f..e8016950094 100644 --- a/core/js/tests/specs/shareitemmodelSpec.js +++ b/core/js/tests/specs/shareitemmodelSpec.js @@ -169,7 +169,8 @@ describe('OC.Share.ShareItemModel', function() { storage: 1, token: 'tehtoken', uid_owner: 'root', - hide_download: 1 + hide_download: 1, + send_password_by_talk: true } ])); @@ -189,6 +190,7 @@ describe('OC.Share.ShareItemModel', function() { expect(linkShares.length).toEqual(1); var linkShare = linkShares[0]; expect(linkShare.hideDownload).toEqual(true); + expect(linkShare.sendPasswordByTalk).toEqual(true); // TODO: check more attributes }); @@ -293,7 +295,8 @@ describe('OC.Share.ShareItemModel', function() { storage: 1, token: 'tehtoken', uid_owner: 'root', - hide_download: 0 + hide_download: 0, + send_password_by_talk: false }, { displayname_owner: 'root', expiration: '2015-10-15 00:00:00', @@ -312,7 +315,8 @@ describe('OC.Share.ShareItemModel', function() { storage: 1, token: 'anothertoken', uid_owner: 'root', - hide_download: 1 + hide_download: 1, + send_password_by_talk: true }] )); OC.currentUser = 'root'; @@ -327,6 +331,7 @@ describe('OC.Share.ShareItemModel', function() { var linkShare = linkShares[0]; expect(linkShare.token).toEqual('tehtoken'); expect(linkShare.hideDownload).toEqual(false); + expect(linkShare.sendPasswordByTalk).toEqual(false); // TODO: check child too }); @@ -588,6 +593,7 @@ describe('OC.Share.ShareItemModel', function() { hideDownload: false, password: '', passwordChanged: false, + sendPasswordByTalk: false, permissions: OC.PERMISSION_READ, expireDate: '', shareType: OC.Share.SHARE_TYPE_LINK @@ -612,6 +618,7 @@ describe('OC.Share.ShareItemModel', function() { hideDownload: false, password: '', passwordChanged: false, + sendPasswordByTalk: false, permissions: OC.PERMISSION_READ, expireDate: '2015-07-24 00:00:00', shareType: OC.Share.SHARE_TYPE_LINK |