1/* 2 * Copyright (c) 2015 3 * 4 * This file is licensed under the Affero General Public License version 3 5 * or later. 6 * 7 * See the COPYING-README file. 8 * 9 */ 10 11(function() { 12 if(!OC.Share) { 13 OC.Share = {}; 14 } 15 16 var TEMPLATE_BASE = 17 '<div class="resharerInfoView subView"></div>' + 18 '{{#if isSharingAllowed}}' + 19 '{{#if isLinkSharingAllowed}}' + 20 '<ul class="subTabHeaders">' + 21 ' <li class="subTabHeader selected subtab-localshare">{{localSharesLabel}}</li>' + 22 ' <li class="subTabHeader subtab-publicshare">{{publicSharesLabel}}</li>' + 23 '</ul>' + 24 '{{/if}}' + 25 '<div class="tabsContainer">' + 26 // TODO: this really should be a separate view class 27 ' <div class="localShareView tab" style="padding-left:0;padding-right:0;">' + 28 ' <label for="shareWith-{{cid}}" class="hidden-visually">{{shareLabel}}</label>' + 29 ' <div class="oneline">' + 30 ' <input id="shareWith-{{cid}}" class="shareWithField" type="text" placeholder="{{sharePlaceholder}}" />' + 31 ' <span class="shareWithLoading icon-loading-small hidden"></span>'+ 32 ' {{{remoteShareInfo}}}' + 33 ' </div>' + 34 ' <div class="shareeListView subView"></div>' + 35 ' <div class="shareeTreeUserGroupListView subView"></div>' + 36 ' </div>' + 37 ' <div class="linkShareView tab hidden" style="padding-left:0;padding-right:0;">' + 38 ' <div class="linkListView"></div>' + 39 ' <div class="shareeTreeLinkListView subView"></div>' + 40 ' </div>' + 41 '</div>' + 42 '{{else}}' + 43 '<div class="noSharingPlaceholder">{{noSharingPlaceholder}}</div>' + 44 '{{/if}}' + 45 '<div class="loading hidden" style="height: 50px"></div>'; 46 47 var TEMPLATE_REMOTE_SHARE_INFO = 48 '<a target="_blank" class="icon icon-info shareWithRemoteInfo hasTooltip" href="{{docLink}
48}" ' + 49 'title="{{tooltip}}"></a>'; 50 51 var TEMPLATE_AUTOCOMPLETE_ITEM = 52 '<li class="{{shareTypeClass}}">' + 53 '<a>' + 54 '<div class="share-autocomplete-item">' + 55 '{{#if showAvatar}}' + 56 '<div class="avatardiv"></div>' + 57 '{{/if}}' + 58 '<div class="autocomplete-item-text">' + 59 '<span class="autocomplete-item-displayname">{{displayName}}</span>' + 60 '{{#if additionalInfo}}' + 61 '<br/><span class="autocomplete-item-additional-info">({{additionalInfo}})</span>' + 62 '{{/if}}' + 63 '<br/><span class="autocomplete-item-typeInfo">{{typeInfo}}</span>' + 64 '</div>' + 65 '</div>' + 66 '</a>' + 67 '</li>'; 68 69 /** 70 * @class OCA.Share.ShareDialogView 71 * @member {OC.Share.ShareItemModel} model 72 * @member {jQuery} $el 73 * @memberof OCA.Sharing 74 * @classdesc 75 * 76 * Represents the GUI of the share dialogue 77 * 78 */ 79 var ShareDialogView = OC.Backbone.View.extend({ 80 /** @type {Object} **/ 81 _templates: {}, 82 83 /** @type {string} **/ 84 tagName: 'div', 85 86 /** @type {OC.Share.ShareConfigModel} **/ 87 configModel: undefined, 88 89 /** @type {object} **/ 90 resharerInfoView: undefined, 91 92 /** @type {object} **/ 93 linkShareView: undefined, 94 95 /** @type {object} **/ 96 shareeListView: undefined, 97 98 /** @type {string} **/ 99 batchActionSeparator: ',', 100 101 events: { 102 'input .shareWithField': 'onShareWithFieldChanged', 103 'click .subTabHeader': '_onClickTabHeader' 104 }, 105 106 initialize: function(options) { 107 var view = this; 108 109 this.model.on('fetchError', function() { 110 OC.Notification.showTemporary(t('core', 'Share details could not be loaded for this item.')); 111 }); 112 113 if(!_.isUndefined(options.configModel)) { 114 this.configModel = options.configModel; 115 } else { 116 throw 'missing OC.Share.ShareConfigModel'; 117 } 118 119 this.configModel.on('change:isRemoteShareAllowed', function() { 120 view.render(); 121 }); 122 this.model.on('change:permissions', function() { 123 view.render(); 124 }); 125 126 this.model.on('request', this._onRequest, this); 127 this.model.on('sync', this._onEndRequest, this); 128 129 var subViewOptions = { 130 model: this.model, 131 configModel: this.configModel 132 }; 133 134 var subViews = { 135 resharerInfoView: 'ShareDialogResharerInfoView', 136 shareeListView: 'ShareDialogShareeListView' 137 }; 138 139 for(var name in subViews) { 140 var className = subViews[name]; 141 this[name] = _.isUndefined(options[name]) 142 ? new OC.Share[className](subViewOptions) 143 : options[name]; 144 } 145 146 this.linkShareView = null; 147 148 _.bindAll(this, 149 'autocompleteHandler', 150 '_onSelectRecipient', 151 'onShareWithFieldChanged' 152 ); 153 154 OC.Plugins.attach('OC.Share.ShareDialogView', this); 155 }, 156 157 _onClickTabHeader: function(ev) { 158 var $target = $(ev.target); 159 this.$('.subTabHeaders .subTabHeader.selected').removeClass('selected'); 160 161 $target.addClass('selected'); 162 163 this.$('.localShareView').toggleClass('hidden', !$target.hasClass('subtab-localshare')); 164 165 var $linkShareView = this.$('.linkShareView'); 166 var $linkListView = this.$('.linkListView'); 167 if ($linkShareView.length) { 168 $linkShareView.toggleClass('hidden', !$target.hasClass('subtab-publicshare')); 169 170 if (!this.linkShareView) { 171 this.linkShareView = new OC.Share.ShareDialogLinkListView({ 172 collection: this.model.getLinkSharesCollection(), 173 // pass in the legacy stuff... 174 itemModel: this.model 175 }); 176 this.linkShareView.render(); 177 $linkListView.html(this.linkShareView.$el); 178 } 179 } 180 }, 181 182 onShareWithFieldChanged: function() { 183 var $el = this.$el.find('.shareWithField'); 184 if ($el.val().length < 2) { 185 $el.removeClass('error').tooltip('hide'); 186 } 187 }, 188 189 autocompleteHandler: function (search, response) { 190 var view = this; 191 var $loading = this.$el.find('.shareWithLoading'); 192 var trimmedSearch = search.term.trim(); 193 $loading.removeClass('hidden'); 194 $loading.addClass('inlineblock'); 195 $.get( 196 OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees', 197 { 198 format: 'json', 199 search: trimmedSearch, 200 perPage: 200, 201 itemType: view.model.get('itemType') 202 }, 203 function (result) { 204 $loading.addClass('hidden'); 205 $loading.removeClass('inlineblock'); 206 if (result.ocs.meta.statuscode == 100) { 207 var users = result.ocs.data.exact.users.concat(result.ocs.data.users); 208 var groups = result.ocs.data.exact.groups.concat(result.ocs.data.groups); 209 var remotes = result.ocs.data.exact.remotes.concat(result.ocs.data.remotes); 210 211 var usersLength; 212 var groupsLength; 213 var remotesLength; 214 215 var i, j; 216 217 //Filter out the current user 218 usersLength = users.length; 219 for (i = 0 ; i < usersLength; i++) { 220 if (users[i].value.shareWith === OC.currentUser) { 221 users.splice(i, 1); 222 break; 223 } 224 } 225 226 // Filter out the owner of the share 227 if (view.model.hasReshare()) { 228 usersLength = users.length; 229 for (i = 0 ; i < usersLength; i++) { 230 if (users[i].value.shareWith === view.model.getReshareOwner()) { 231 users.splice(i, 1); 232 break; 233 } 234 } 235 } 236 237 var shares = view.model.get('shares'); 238 var sharesLength = shares.length; 239 240 // Now filter out all sharees that are already shared with 241 for (i = 0; i < sharesLength; i++) { 242 var share = shares[i]; 243 244 if (share.share_type === OC.Share.SHARE_TYPE_USER) { 245 usersLength = users.length; 246 for (j = 0; j < usersLength; j++) { 247 if (users[j].value.shareWith === share.share_with) { 248 users.splice(j, 1); 249 break; 250 } 251 } 252 } else if (share.share_type === OC.Share.SHARE_TYPE_GROUP) { 253 groupsLength = groups.length; 254 for (j = 0; j < groupsLength; j++) { 255 if (groups[j].value.shareWith === share.share_with) { 256 groups.splice(j, 1); 257 break; 258 } 259 } 260 } else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE) { 261 remotesLength = remotes.length; 262 for (j = 0; j < remotesLength; j++) { 263 if (remotes[j].value.shareWith === share.share_with) { 264 remotes.splice(j, 1); 265 break; 266 } 267 } 268 } 269 } 270 271 var isBatch = trimmedSearch.indexOf(view.batchActionSeparator) !== -1; 272 var suggestions = users.concat(groups); 273 if (suggestions.length < 1 && !isBatch) { 274 suggestions = suggestions.concat(remotes); 275 } 276 277 if (isBatch) { 278 return view._getUsersForBatchAction(trimmedSearch).then(function (res) { 279 if (res.found.length) { 280 //Filter out the current user 281 for (i = 0; i < res.found.length; i++) { 282 if (res.found[i].shareWith === OC.currentUser) { 283 res.found.splice(i, 1); 284 continue; 285 } 286 } 287 } 288 if (res.found.length) { 289 // Build the label for the list of users to be batch-added 290 var labelArray = []; 291 for (i = 0; i < res.found.length; i++) { 292 labelArray.push(res.found[i].shareWith) 293 } 294 295 suggestions.push({ 296 batch: res.found, 297 failedBatch: res.notFound, 298 label: labelArray.join(', '), 299 value: {} 300 }); 301 } 302 303 $loading.addClass('hidden'); 304 $loading.removeClass('inlineblock'); 305 if (suggestions.length) { 306 $('.shareWithField').removeClass('error') 307 .tooltip('hide') 308 .autocomplete("option", "autoFocus", true); 309 return response(suggestions, result); 310 } 311
312 view._displayError(t('core', 'No users or groups found')); 313 return response(undefined, result); 314 }) 315 } 316 317 if (suggestions.length > 0) { 318 suggestions.sort(function (a, b) { 319 return OC.Util.naturalSortCompare(a.label, b.label); 320 }); 321 $('.shareWithField').removeClass('error') 322 .tooltip('hide') 323 .autocomplete("option", "autoFocus", true); 324 response(suggestions, result); 325 } else { 326 var title = t('core', 'No users or groups found for {search}', {search: $('.shareWithField').val()}); 327 if (!view.configModel.get('allowGroupSharing')) { 328 title = t('core', 'No users found for {search}', {search: $('.shareWithField').val()}); 329 } 330 var suggestStarts = OC.getCapabilities().files_sharing.search_min_length; 331 if (suggestStarts > $('.shareWithField').val().length) { 332 title = title + '. ' + n( 333 'core', 334 'Please enter at least {chars} character for suggestions', 335 'Please enter at least {chars} characters for suggestions', 336 suggestStarts, 337 {chars: suggestStarts} 338 ); 339 } 340 view._displayError(title); 341 response(undefined, result); 342 } 343 } else { 344 response(undefined, result); 345 } 346 } 347 ).fail(function() { 348 $loading.addClass('hidden'); 349 $loading.removeClass('inlineblock'); 350 OC.Notification.show(t('core', 'An error occurred. Please try again')); 351 window.setTimeout(OC.Notification.hide, 5000); 352 }); 353 }, 354 355 autocompleteRenderItem: function(ul, item) { 356 357 var text = item.label; 358 var typeInfo = t('core', 'User'); 359 360 if (item.batch) { 361 typeInfo = this._getBatchActionLabel(); 362 } 363 if (item.value.shareType === OC.Share.SHARE_TYPE_GROUP) { 364 typeInfo = t('core', 'Group'); 365 } 366 if (item.value.shareType === OC.Share.SHARE_TYPE_GUEST) { 367 typeInfo = t('core', 'Guest'); 368 } 369 if (item.value.shareType === OC.Share.SHARE_TYPE_REMOTE) { 370 if (item.value.server) { 371 typeInfo = t('core', 'At {server}', { 372 server: item.value.server 373 }); 374 } else { 375 typeInfo = t('core', 'Federated'); 376 } 377 } 378 if(item.value.userType === OC.User.USER_TYPE_GUEST){ 379 typeInfo = t('core', 'Guest'); 380 } 381 382 var template = this._getAutocompleteItemTemplate(); 383 var $el = $(template({ 384 showAvatar: this.configModel.areAvatarsEnabled(), 385 displayName: text, 386 typeInfo: typeInfo, 387 additionalInfo: item.value.shareWithAdditionalInfo, 388 shareTypeClass: (item.value.shareType === OC.Share.SHARE_TYPE_GROUP) ? 'group' : 'user' 389 })); 390 391 if(this.configModel.areAvatarsEnabled()) { 392 var $avatar = $el.find('.avatardiv'); 393 if (item.value.shareType === OC.Share.SHARE_TYPE_USER) { 394 $avatar.avatar(item.value.shareWith, 32, undefined, undefined, undefined, item.label); 395 } else { 396 $avatar.imageplaceholder(text, undefined, 32); 397 } 398 } 399 400 return $el.appendTo(ul); 401 }, 402 403 _onSelectRecipient: function(e, s) { 404 e.preventDefault(); 405 $(e.target).attr('disabled', true) 406 .val(s.item.label); 407 var $loading = this.$el.find('.shareWithLoading'); 408 $loading.removeClass('hidden') 409 .addClass('inlineblock'); 410 411 if (s.item.failedBatch && s.item.failedBatch.length) { 412 this._showFailedBatchSharees(s.item.failedBatch); 413 } 414 415 var shares = s.item.batch || [s.item.value]; 416 for (var i = 0; i < shares.length; i++) { 417 this.model.addShare(shares[i], { 418 success: function () { 419 $(e.target).val('').attr('disabled', false); 420 $loading.addClass('hidden').removeClass('inlineblock'); 421 }, error: function (obj, msg) { 422 OC.Notification.showTemporary(msg); 423 $(e.target).attr('disabled', false).autocomplete('search', $(e.target).val()); 424 $loading.addClass('hidden').removeClass('inlineblock'); 425 } 426 }); 427 } 428 }, 429 430 _toggleLoading: function(state) { 431 this._loading = state; 432 this.$el.find('.localShareView, .noSharingPlaceholder').toggleClass('hidden', state); 433 this.$el.find('.loading').toggleClass('hidden', !state); 434 }, 435 436 _onRequest: function() { 437 // only show the loading spinner for the first request (for now) 438 if (!this._loadingOnce) { 439 this._toggleLoading(true); 440 } 441 }, 442 443 _onEndRequest: function() { 444 var self = this; 445 this._toggleLoading(false); 446 if (!this._loadingOnce) { 447 this._loadingOnce = true; 448 // the first time, focus on the share field after the spinner disappeared 449 if (!bowser.msie) { 450 _.defer(function () { 451 self.$('.shareWithField').focus(); 452 }); 453 } 454 } 455 }, 456 457 render: function() { 458 var baseTemplate = this._getTemplate('base', TEMPLATE_BASE); 459 460 this.$el.html(baseTemplate({ 461 cid: this.cid, 462 shareLabel: t('core', 'Share'), 463 sharePlaceholder: this._renderSharePlaceholderPart(), 464 remoteShareInfo: this._renderRemoteShareInfoPart(), 465 isSharingAllowed: this.model.sharePermissionPossible(), 466 isLinkSharingAllowed: this.configModel.isShareWithLinkAllowed(), 467 localSharesLabel: t('core', 'User and Groups'), 468 publicSharesLabel: t('core', 'Public Links'), 469 noSharingPlaceholder: t('core', 'Sharing is not allowed') 470 })); 471 472 var $shareField = this.$el.find('.shareWithField'); 473 if ($shareField.length) { 474 $shareField.autocomplete({ 475 minLength: 1, 476 delay: 750, 477 focus: function(event) { 478 event.preventDefault(); 479 }, 480 source: this.autocompleteHandler, 481 select: this._onSelectRecipient, 482 appendTo: '#shareTabView' 483 }).data('ui-autocomplete')._renderItem = _.bind(this.autocompleteRenderItem, this); 484 } 485 486 this.resharerInfoView.$el = this.$el.find('.resharerInfoView'); 487 this.resharerInfoView.render(); 488 489 var resharingAllowed = this.model.sharePermissionPossible(); 490 if (!resharingAllowed || !this.configModel.isShareWithLinkAllowed()) { 491 this.$('.tabHeaders, .linkShareView').remove(); 492 } 493 494 this.shareeListView.$el = this.$el.find('.shareeListView'); 495 this.shareeListView.render(); 496 497 this.$el.find('.hasTooltip').tooltip(); 498 499 return this; 500 }, 501 502 _renderRemoteShareInfoPart: function() { 503 var remoteShareInfo = ''; 504 if(this.configModel.get('isRemoteShareAllowed')) { 505 var infoTemplate = this._getRemoteShareInfoTemplate(); 506 remoteShareInfo = infoTemplate({ 507 docLink: this.configModel.getFederatedShareDocLink(), 508 tooltip: t('core', 'Share with people on other ownClouds using the syntax [email protected]/owncloud') 509 }); 510 } 511 512 return remoteShareInfo; 513 }, 514 515 _renderSharePlaceholderPart: function () { 516 var sharePlaceholder = t('core', 'Share with usersâ¦'); 517 518 if (this.configModel.get('allowGroupSharing')) { 519 if (this.configModel.get('isRemoteShareAllowed')) { 520 sharePlaceholder = t('core', 'Share with users, groups or federated usersâ¦'); 521 } else { 522 sharePlaceholder = t('core', 'Share with users or groupsâ¦') 523 } 524 } else if (this.configModel.get('isRemoteShareAllowed')) { 525 sharePlaceholder = t('core', 'Share with users or federated usersâ¦'); 526 } 527 528 return sharePlaceholder; 529 }, 530 531 /** 532 * 533 * @param {string} key - an identifier for the template 534 * @param {string} template - the HTML to be compiled by Handlebars 535 * @returns {Function} from Handlebars 536 * @private 537 */ 538 _getTemplate: function (key, template) { 539 if (!this._templates[key]) { 540 this._templates[key] = Handlebars.compile(template); 541 } 542 return this._templates[key]; 543 }, 544 545 /** 546 * returns the info template for remote sharing 547 * 548 * @returns {Function} 549 * @private 550 */ 551 _getRemoteShareInfoTemplate: function() { 552 return this._getTemplate('remoteShareInfo', TEMPLATE_REMOTE_SHARE_INFO); 553 }, 554 555 /** 556 * Returns the autocomplete item template 557 * 558 * @returns {Function} 559 * @private 560 */ 561 _getAutocompleteItemTemplate: function() { 562 return this._getTemplate('autocompleteItem', TEMPLATE_AUTOCOMPLETE_ITEM); 563 }, 564 565 /** 566 * Shows all users with whom the batch share action failed. 567 * 568 * @param {Array.<String>} users 569 * @private 570 */ 571 _showFailedBatchSharees: function(users) { 572 var failedUsersStr = users.join(', '); 573 OC.Notification.show( 574 t('core', 'Could not be shared with the following users: {users}', {users: failedUsersStr}), 575 {type: 'error'} 576 ); 577 }, 578 579 /** 580 * Returns the label for the batch action 581 * 582 * @returns {string} 583 * @private 584 */ 585 _getBatchActionLabel: function() { 586 return t('core', 'Add multiple users'); 587 }, 588 589 /** 590 * Displays an error, e.g. when the autocomplete doesn't have results. 591 * 592 * @param {string} title - title of the error 593 * @private 594 */ 595 _displayError: function(title) { 596 $('.shareWithField').addClass('error') 597 .attr('data-original-title', title) 598 .tooltip('hide') 599 .tooltip({ 600 placement: 'bottom', 601 trigger: 'manual' 602 }) 603 .tooltip('fixTitle') 604 .tooltip('show'); 605 }, 606 607 /** 608 * Returns a promise which includes all fetched users for batch actions once resolved. 609 * 610 * @param {string} search - trimmed search term 611 * @returns {Promise} 612 * @private 613 */ 614 _getUsersForBatchAction: function(search) { 615 var view = this; 616 var foundUsers = []; 617 var notFound = []; 618 var promises = []; 619 var users = Array.from(new Set(search.replace(new RegExp(this.batchActionSeparator + '\\s+', 'g'), this.batchActionSeparator).split(this.batchActionSeparator))); 620 621 for (var i = 0; i < users.length; i++) { 622 if (!users[i] || users[i] === OC.currentUser) { 623 continue; 624 } 625 var user = users[i].trim(); 626 promises.push( 627 $.ajax({ 628 url: OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees', 629 contentType: 'application/json', 630 dataType: 'json', 631 data: { 632 format: 'json', 633 search: user, 634 perPage: 200, 635 itemType: view.model.get('itemType') 636 }, 637 context: { user: user } 638 }) 639 .done(function (result) { 640 if (result.ocs.data.exact.users.length) { 641 var addShare = true; 642 var shares = view.model.get('shares'); 643 if (!shares.length) { 644 foundUsers.push(result.ocs.data.exact.users[0].value); 645 return; 646 } 647 648 // filter out all sharees that are already shared with 649 for (j= 0; j < shares.length; j++) { 650 if (shares[j].share_type === OC.Share.SHARE_TYPE_USER 651 && result.ocs.data.exact.users[0].value.shareWith === shares[j].share_with) { 652 addShare = false;
653 break; 654 } 655 } 656 657 if (addShare) { 658 foundUsers.push(result.ocs.data.exact.users[0].value); 659 } 660 } else { 661 notFound.push(this.user); 662 } 663 }) 664 ); 665 } 666 667 return Promise.all(promises).then(function() { 668 return {found: foundUsers, notFound: notFound}; 669 }); 670 } 671 }); 672 673 OC.Share.ShareDialogView = ShareDialogView; 674 675})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.