1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 ZmShareSearchDialog = function(params) {
 25     // initialize params
 26     params.className = params.className || "ZmShareSearchDialog DwtDialog";
 27     params.title = ZmMsg.sharedFoldersAddTitle;
 28     params.standardButtons = [ ZmShareSearchDialog.ADD_BUTTON, DwtDialog.CANCEL_BUTTON ];
 29 	params.id = "ZmShareSearchDialog";
 30 	
 31     // setup auto-complete
 32     // NOTE: This needs to be done before default construction so
 33     // NOTE: that it is available when we initialize the email
 34     // NOTE: input field.
 35     var acparams = {
 36         dataClass:		  appCtxt.getAutocompleter(),
 37         matchValue:		  ZmAutocomplete.AC_VALUE_EMAIL,
 38         keyUpCallback:	  this._acKeyUpListener.bind(this),
 39 		contextId:		  this.toString(),
 40 		autocompleteType: "all"
 41     };
 42     this._acAddrSelectList = new ZmAutocompleteListView(acparams);
 43     
 44     // default construction
 45     DwtDialog.call(this, params);
 46 
 47     // set custom button label
 48     this.getButton(ZmShareSearchDialog.ADD_BUTTON).setText(ZmMsg.add);
 49 
 50     // insert form elements into tab group
 51     var tabGroup = this._tabGroup;
 52     tabGroup.addMemberBefore(this._form.getTabGroupMember(), tabGroup.getFirstMember());
 53 };
 54 ZmShareSearchDialog.prototype = new DwtDialog;
 55 ZmShareSearchDialog.prototype.constructor = ZmShareSearchDialog;
 56 
 57 ZmShareSearchDialog.prototype.isZmShareSearchDialog = true;
 58 ZmShareSearchDialog.prototype.toString = function() { return "ZmShareSearchDialog"; };
 59 
 60 //
 61 // Constants
 62 //
 63 
 64 ZmShareSearchDialog.ADD_BUTTON = DwtDialog.OK_BUTTON; //++DwtDialog.LAST_BUTTON;
 65 
 66 ZmShareSearchDialog._APP_TYPES = [ZmApp.MAIL, ZmApp.CONTACTS, ZmApp.CALENDAR, ZmApp.TASKS, ZmApp.BRIEFCASE];
 67 ZmShareSearchDialog._APP_KEY = {};
 68 ZmShareSearchDialog._APP_KEY[ZmApp.MAIL]		= "mailSharesOnly";
 69 ZmShareSearchDialog._APP_KEY[ZmApp.TASKS]		= "taskSharesOnly";
 70 ZmShareSearchDialog._APP_KEY[ZmApp.BRIEFCASE]	= "briefcaseSharesOnly";
 71 ZmShareSearchDialog._APP_KEY[ZmApp.CALENDAR]    = "calendarSharesOnly";
 72 ZmShareSearchDialog._APP_KEY[ZmApp.CONTACTS]    = "addrbookSharesOnly";
 73 
 74 //
 75 // Data
 76 //
 77 
 78 ZmShareSearchDialog.prototype.CONTENT_TEMPLATE = "share.Widgets#ZmShareSearchView";
 79 
 80 
 81 //
 82 // Public methods
 83 //
 84 
 85 ZmShareSearchDialog.prototype.getShares = function() {
 86     var treeView = this._form.getControl("TREE");
 87     var root = this._getNode(ZmOrganizer.ID_ROOT);
 88     var shares = [];
 89     this._collectShares(treeView, root, shares);
 90     return shares;
 91 };
 92 
 93 //
 94 // Protected methods
 95 //
 96 
 97 ZmShareSearchDialog.prototype._collectShares = function(treeView, node, shares) {
 98     if (node.shareInfo) {
 99         var treeItem = treeView.getTreeItemById(node.id);
100         // NOTE: Only collect shares that are checked *and* visible.
101         // NOTE: In other words, we should never mount a share that
102         // NOTE: is not visible even if the user had checked it before
103         // NOTE: applying a filter. Otherwise they would be left
104         // NOTE: wondering why it was mounted.
105         if (treeItem && treeItem.getChecked() && treeItem.getVisible()) {
106             shares.push(node.shareInfo);
107         }
108     }
109     else {
110         var children = node.children.getArray();
111         for (var i = 0; i < children.length; i++) {
112             this._collectShares(treeView, children[i], shares);
113         }
114     }
115 };
116 
117 ZmShareSearchDialog.prototype._filterResults = function() {
118     var treeView = this._form.getControl("TREE");
119     var root = this._getNode(ZmOrganizer.ID_ROOT);
120     var text = this._form.getValue("FILTER") || "";
121     this._filterNode(treeView, root, text.toLowerCase());
122 };
123 
124 ZmShareSearchDialog.prototype._filterNode = function(treeView, node, text) {
125 	var nodeItem = treeView.getTreeItemById(node.id);
126 	if (!nodeItem) {
127 		return false;
128 	}
129     // process children
130     var count = node.children.size();
131     var app = this._form.getValue("APP") || "";
132 	var matches = false;
133     if (count > 0) {
134 		//this node has children.
135         for (var i = 0; i < count; i++) {
136             var child = node.children.get(i);
137             matches = this._filterNode(treeView, child, text) || matches; //order is important! (need to call _filterNode always
138         }
139     }
140 	else {
141 		//this is a leaf node
142 		var isInfoNode = String(node.id).match(/^-/);
143 		var textMatches = !text || node.name.toLowerCase().indexOf(text) !== -1;
144 		var appMatches = !app || node.shareInfo && node.shareInfo.view === app;
145 		matches = !isInfoNode && textMatches && appMatches;
146 	}
147 	matches = matches || node.id == ZmOrganizer.ID_ROOT;
148 	nodeItem.setVisible(matches);
149 	return matches;
150 };
151 
152 ZmShareSearchDialog.prototype._createOrganizer = function(parent, id, name) {
153     // NOTE: The caller is responsible for adding the new node
154     // NOTE: to the parent's children.
155     return new ZmShareProxy({parent:parent,id:id,name:name,tree:(parent&&parent.tree)});
156 };
157 
158 ZmShareSearchDialog.prototype._resetTree = function() {
159     // create new tree
160     var tree = new ZmTree(ZmOrganizer.SHARE);
161     // NOTE: The root should never be seen
162     tree.root = this._createOrganizer(null, ZmOrganizer.ID_ROOT, "[Root]");
163 
164     // setup tree view
165     var treeView = this._form.getControl("TREE");
166     treeView.set({ dataTree: tree });
167     var treeItem = treeView.getTreeItemById(ZmOrganizer.ID_ROOT);
168     treeItem.setVisible(false, true);
169     treeItem.setExpanded(true);
170     treeItem.enableSelection(false);
171     treeItem.showCheckBox(false);
172 };
173 
174 // Fix for bug: 79402. Passing extra param for wide search.
175 ZmShareSearchDialog.prototype._doUserSearch = function(emails, isWideSearch) {
176     this._resetTree();
177     // collect unique email addresses
178     emails = emails.split(/\s*[;,]\s*/);
179     var emailMap = {};
180     for (var i = 0; i < emails.length; i++) {
181         var email = AjxStringUtil.trim(emails[i]);
182         if (!email) {
183             continue;
184         }
185         if (email === appCtxt.get(ZmSetting.USERNAME)) {
186             continue;
187         }
188         emailMap[email.toLowerCase()] = email;
189     }
190 
191     // build request
192     var requests = [], requestIdMap = {};
193     var i = 0;
194     for (var emailId in emailMap) {
195         // add request
196         requests.push({
197             _jsns: "urn:zimbraAccount",
198             requestId: i,
199             includeSelf: 0,
200             owner: { by: "name", _content: emailMap[emailId] }
201         });
202 
203         // add loading placeholder node
204         if (!this._loadingUserFormatter) {
205             this._loadingUserFormatter = new AjxMessageFormat(ZmMsg.sharedFoldersLoadingUser);
206         }
207         var text = this._loadingUserFormatter.format([email]);
208         var loadingId = [ZmShareProxy.ID_LOADING,Dwt.getNextId("share")].join(":");
209         this._appendInfoNode(ZmOrganizer.ID_ROOT, loadingId, AjxStringUtil.htmlEncode(text));
210 
211         // remember the placeholder nodes
212         emailMap[emailId] = loadingId;
213         requestIdMap[i] = loadingId;
214         i++;
215     }
216 
217     // Fix for bug: 79402. Replaces _doGroupSearch.
218     if (isWideSearch) {
219         this._appendInfoNode(ZmOrganizer.ID_ROOT, ZmShareProxy.ID_LOADING, ZmMsg.sharedFoldersLoading);
220 
221         requests.push({
222             _jsns: "urn:zimbraAccount",
223             includeSelf: 0
224         });
225     }
226 
227     // anything to do?
228     if (requests.length == 0) {
229         return;
230     }
231 
232     // perform user search
233     this._setSearching(true);
234     var params = {
235         jsonObj: {
236             BatchRequest: {
237                 _jsns: "urn:zimbra",
238                 GetShareInfoRequest: requests
239             }
240         },
241         asyncMode: true,
242         callback: new AjxCallback(this, this._handleUserSearchResults, [emailMap, requestIdMap]),
243         errorCallback: new AjxCallback(this, this._handleUserSearchError)
244     };
245     appCtxt.getAppController().sendRequest(params);
246 };
247 
248 ZmShareSearchDialog.prototype._setSearching = function(searching) {
249     this._form.setEnabled(!searching);
250 };
251 
252 ZmShareSearchDialog.prototype._handleUserSearchResults = function(emailMap, requestIdMap, resp) {
253     this._setSearching(false);
254 
255     // remove placeholder nodes
256     for (var email in emailMap) {
257         this._removeNode(emailMap[email]);
258     }
259 
260     // add nodes for results
261     var batchResponse = AjxUtil.get(resp.getResponse(), "BatchResponse");
262     var responses = AjxUtil.get(batchResponse, "GetShareInfoResponse");
263     if (responses) {
264         // get list of owners with their shares, in alphabetical order
265         var owners = {};
266         for (var i = 0; i < responses.length; i++) {
267             var response = responses[i];
268             this._addToOwnerMap(owners, response.share);
269         }
270         owners = AjxUtil.values(owners);
271         owners.sort(ZmShareSearchDialog.__byOwnerName);
272 
273         // add shares
274         this._appendShareNodes(owners);
275     }
276 
277     // apply current filter
278     this._filterResults();
279 
280     // handle errors
281     var faults = AjxUtil.get(batchResponse, "Fault");
282     if (faults) {
283         var treeView = this._form.getControl("TREE");
284         for (var i = 0; i < faults.length; i++) {
285             var fault = faults[i];
286 
287             // replace placeholder node with error node
288             var faultNodeId = ZmShareProxy.ID_ERROR;// TODO: create unique error item id
289             var loadingNode = this._getNode(requestIdMap[fault.requestId]);
290             var faultNode = this._createOrganizer(loadingNode.parent, faultNodeId, ZmMsg.sharedFoldersError);
291             treeView.replaceNode(faultNode, loadingNode);
292 
293             // set error message as tooltip
294             var treeItem = treeView.getTreeItemById(faultNodeId);
295             treeItem.showCheckBox(false);
296             treeItem.setToolTipContent(AjxStringUtil.htmlEncode(fault.Reason.Text));
297         }
298     }
299 };
300 
301 ZmShareSearchDialog.prototype._addToOwnerMap = function(owners, shares) {
302     if (!shares) return;
303 
304     for (var j = 0; j < shares.length; j++) {
305         var share = shares[j];
306         var owner = owners[share.ownerId];
307         if (!owner) {
308             owner = owners[share.ownerId] = {
309                 ownerId: share.ownerId,
310                 ownerName: share.ownerName || share.ownerEmail,
311                 ownerEmail: share.ownerEmail,
312                 shares: []
313             };
314         }
315         owner.shares.push(share);
316     }
317 };
318 
319 ZmShareSearchDialog.prototype._handleUserSearchError = function(resp) {
320     this._setSearching(false);
321     // TODO
322 };
323 
324 // node management
325 
326 ZmShareSearchDialog.prototype._getNode = function(id) {
327     var treeView = this._form.getControl("TREE");
328     var treeItem = treeView.getTreeItemById(id);
329     return treeItem && treeItem.getData(Dwt.KEY_OBJECT);
330 };
331 
332 ZmShareSearchDialog.prototype._removeNode = function(nodeId) {
333     var treeView = this._form.getControl("TREE");
334     treeView.removeNode(this._getNode(nodeId));
335 };
336 
337 ZmShareSearchDialog.prototype._appendChild = function(childNode, parentNode, checkable, tooltip) {
338     var treeView = this._form.getControl("TREE");
339     var treeItem = treeView.appendChild(childNode, parentNode, null, tooltip);
340     treeItem.setExpanded(true);
341     treeItem.enableSelection(false);
342     treeItem.showCheckBox(checkable);
343     treeItem.setVisible(false);   //filterResults will set visibility
344     return treeItem;
345 };
346 
347 ZmShareSearchDialog.prototype._appendShareNodes = function(owners) {
348 
349     // run through owners
350     for (var j = 0; j < owners.length; j++) {
351         // create parent node, if needed
352         var owner = owners[j];
353         var parentNode = this._getNode(owner.ownerId);
354         if (!parentNode) {
355             var root = this._getNode(ZmOrganizer.ID_ROOT);
356             parentNode = this._createOrganizer(root, owner.ownerId, owner.ownerName || owner.ownerEmail);
357             this._appendChild(parentNode, root);
358         }
359 
360         // add share nodes
361         var shares = owner.shares;
362         if (shares.length > 0) {
363             shares.sort(ZmShareSearchDialog.__byFolderPath);
364             for (var i = 0; i < shares.length; i++) {
365                 var share = shares[i];
366 				if (ZmFolder.HIDE_ID[share.folderId]) {
367 					continue;
368 				}
369                 var shareId = [share.ownerId,share.folderId].join(":");
370                 if (this._getNode(shareId) != null) continue;
371 
372                  // NOTE: strip the leading slash from folder path
373 				var folderPath = share.folderPath;
374                 var shareFullPathName = share.folderId == ZmOrganizer.ID_ROOT ? ZmMsg.allApplications : folderPath.substr(1);
375                 var shareNode = this._createOrganizer(parentNode, shareId, shareFullPathName);
376                 shareNode.shareInfo = share;
377 
378                 // augment share info
379                 share.icon = shareNode.getIcon();
380                 share.role = ZmShare.getRoleFromPerm(share.rights);
381                 share.roleName = ZmShare.getRoleName(share.role);
382                 share.roleActions = ZmShare.getRoleActions(share.role);
383                 share.normalizedOwnerName = share.ownerName || share.ownerEmail;
384                 share.normalizedGranteeName = share.granteeDisplayName || share.granteeName;
385                 share.normalizedFolderPath = shareFullPathName;
386 				share.name = folderPath.substr(folderPath.lastIndexOf("/") + 1);
387 				var ownerName = share.normalizedOwnerName;
388 				var indexOfAtSign = ownerName.indexOf('@');
389 				if (indexOfAtSign > -1) {
390 					ownerName = ownerName.substr(0, indexOfAtSign)
391 				}
392                 share.defaultMountpointName = ZmShare.getDefaultMountpointName(ownerName, share.name);
393 
394                 // set tooltip
395                 var tooltip = AjxTemplate.expand(shareNode.TOOLTIP_TEMPLATE, share);
396                 this._appendChild(shareNode, parentNode, true, tooltip);
397             }
398         }
399 
400         // no shares found
401         else {
402             this._appendInfoNode(parentNode, ZmShareProxy.ID_NONE_FOUND, ZmMsg.sharedFoldersNoneFound);
403         }
404     }
405 };
406 
407 ZmShareSearchDialog.prototype._appendInfoNode = function(parentId, id, text, tooltip) {
408     var parent = this._getNode(parentId);
409     var node = this._createOrganizer(parent, id, text);
410     return this._appendChild(node, parent, null, tooltip);
411 };
412 
413 // sorting
414 
415 ZmShareSearchDialog.__byOwnerName = AjxCallback.simpleClosure(AjxUtil.byStringProp, window, "ownerName");
416 ZmShareSearchDialog.__byFolderPath = AjxCallback.simpleClosure(AjxUtil.byStringProp, window, "folderPath");
417 
418 // auto-complete
419 
420 ZmShareSearchDialog.prototype._acKeyUpListener = function(event, aclv, result) {
421 	// TODO: Does anything need to be done here?
422 };
423 
424 //
425 // DwtDialog methods
426 //
427 
428 ZmShareSearchDialog.prototype.popup = function(organizerType, addCallback, cancelCallback) {
429     this.reset();
430     if (addCallback) this._buttonDesc[ZmShareSearchDialog.ADD_BUTTON].callback = addCallback;
431     if (cancelCallback) this._buttonDesc[DwtDialog.CANCEL_BUTTON].callback = cancelCallback;
432 
433     if (appCtxt.multiAccounts) {
434         var acct =   appCtxt.getActiveAccount() || appCtxt.accountList.mainAccount;
435         this._acAddrSelectList.setActiveAccount(acct);
436     }
437 
438     var form = this._form;
439     form.setValue("FILTER", "");
440     form.setValue("EMAIL", "");
441     form.setEnabled("SEARCH", false);   //disable search button by default
442     this._selectApplicationOption();
443     this._resetTree();
444     // Fix for bug: 79402. Do wide search.
445     this._doUserSearch("", true);
446 
447     DwtDialog.prototype.popup.call(this);
448 
449     form.getControl("EMAIL").focus();
450 };
451 
452 ZmShareSearchDialog.prototype.popdown = function() {
453 	if (this._acAddrSelectList) {
454 		this._acAddrSelectList.reset();
455 		this._acAddrSelectList.show(false);
456 	}
457 	DwtDialog.prototype.popdown.call(this);
458 };
459 
460 //
461 // DwtBaseDialog methods
462 //
463 
464 ZmShareSearchDialog.prototype._createHtmlFromTemplate = function(templateId, data) {
465     DwtDialog.prototype._createHtmlFromTemplate.apply(this, arguments);
466 
467     // create form
468     var params = {
469         parent: this,
470         className: "ZmShareSearchView",
471         form: {
472             template: this.CONTENT_TEMPLATE,
473             items: [
474                 { id: "FILTER", type: "DwtInputField", hint: ZmMsg.sharedFoldersFilterHint,
475                     onchange: "this.parent._filterResults()"
476                 },
477                 { id: "TREE", type: "ZmShareTreeView", style: DwtTree.CHECKEDITEM_STYLE },
478                 { id: "EMAIL", type: "DwtInputField", hint: ZmMsg.sharedFoldersUserSearchHint },
479                 { id: "SEARCH", type: "DwtButton", label: ZmMsg.searchInput,
480                     enabled: "get('EMAIL')", onclick: "this.parent._doUserSearch(get('EMAIL'))"
481                 },
482                 { id: "APP", type: "DwtSelect",  items: this._getAppOptions(), onchange: "this.parent._filterResults()"
483 
484                 }
485             ]
486         },
487 	    id: "ZmShareSearchView"
488     };
489     this._form = new DwtForm(params);
490 	this._form.setScrollStyle(DwtControl.CLIP);
491     this.setView(this._form);
492 
493     var inputEl = this._form.getControl("EMAIL").getInputElement();
494     var onkeyupHandlers = [inputEl.onkeyup];
495     if (this._acAddrSelectList) {
496         this._acAddrSelectList.handle(inputEl);
497         onkeyupHandlers.push(inputEl.onkeyup);
498     }
499     onkeyupHandlers.push(AjxCallback.simpleClosure(this._handleEmailEnter, this));
500 
501     var handler = AjxCallback.simpleClosure(ZmShareSearchDialog.__onKeyUp, window, onkeyupHandlers);
502     Dwt.setHandler(inputEl, DwtEvent.ONKEYUP, handler);
503 };
504 
505 ZmShareSearchDialog.__onKeyUp = function(handlers, htmlEvent) {
506     for (var i = 0; i < handlers.length; i++) {
507         handlers[i](htmlEvent);
508     }
509 };
510 
511 ZmShareSearchDialog.prototype._handleEmailEnter = function(htmlEvent) {
512     // TODO: on enter, run search
513     if (false) {
514         this._doUserSearch(this.getValue("EMAIL"));
515     }
516 };
517 
518 /**
519  * Gets the include applications options.
520  *
521  * @return	{Array}	an array of include shares options
522  */
523 ZmShareSearchDialog.prototype._getAppOptions = function() {
524 	var options = [];
525     options.push({value: "", label: ZmMsg.allApplications});
526     for (var i = 0; i < ZmShareSearchDialog._APP_TYPES.length; i++) {
527 		var appType = ZmShareSearchDialog._APP_TYPES[i];
528 	    var key = ZmShareSearchDialog._APP_KEY[appType];
529 	    var appEnabled = appCtxt.get(ZmApp.SETTING[appType]);
530 	    if (appEnabled) {
531 		    var shareKey = ZmApp.ORGANIZER[appType];
532 		    if (AjxUtil.isArray1(ZmOrganizer.VIEWS[shareKey])) {
533 				options.push({id: appType, value: ZmOrganizer.VIEWS[shareKey][0], label: ZmMsg[key]});
534 		    }
535 	    }
536 	}
537 
538     return options;
539 };
540 
541 ZmShareSearchDialog.prototype._selectApplicationOption = function() {
542   var activeApp = appCtxt.getCurrentApp();
543   var appSelect = this._form.getControl("APP");
544   var appOptions = this._getAppOptions();
545 
546   if (!activeApp || !appSelect || !appOptions)
547     return;
548 
549   for (var i=0; i<appOptions.length; i++) {
550       if (appOptions[i].hasOwnProperty('id') &&
551           appOptions[i].id == activeApp.getName()) {
552             appSelect.setSelectedValue(appOptions[i].value);
553             return;
554       }
555   }
556 
557 };
558