1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 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) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a preferences page for displaying shares.
 26  * @constructor
 27  * @class
 28  * This class contains a {@link ZmSharingView}, which shows shares in two list views.
 29  *
 30  * @author Conrad Damon
 31  *
 32  * @param {DwtControl}	parent			the containing widget
 33  * @param {Object}	section			the page
 34  * @param {ZmPrefController}	controller		the prefs controller
 35  * 
 36  * @extends		ZmPreferencesPage
 37  * 
 38  * @private
 39  */
 40 ZmSharingPage = function(parent, section, controller) {
 41 	ZmPreferencesPage.apply(this, arguments);
 42 };
 43 
 44 ZmSharingPage.prototype = new ZmPreferencesPage;
 45 ZmSharingPage.prototype.constructor = ZmSharingPage;
 46 
 47 ZmSharingPage.prototype.isZmSharingPage = true;
 48 ZmSharingPage.prototype.toString = function () { return "ZmSharingPage"; };
 49 
 50 ZmSharingPage.prototype.getShares =
 51 function(type, owner, callback) {
 52 
 53 	var jsonObj = {GetShareInfoRequest:{_jsns:"urn:zimbraAccount"}};
 54 	var request = jsonObj.GetShareInfoRequest;
 55 	if (type && type != ZmShare.TYPE_ALL) {
 56 		request.grantee = {type:type};
 57 	}
 58 	if (owner) {
 59 		request.owner = {by:"name", _content:owner};
 60 	}
 61 	var respCallback = new AjxCallback(this, this._handleGetSharesResponse, [callback]);
 62 	appCtxt.getAppController().sendRequest({jsonObj:	jsonObj,
 63 											asyncMode:	true,
 64 											callback:	respCallback});
 65 };
 66 
 67 ZmSharingPage.prototype._handleGetSharesResponse =
 68 function(callback, result) {
 69 
 70 	var resp = result.getResponse().GetShareInfoResponse;
 71 	if (callback) {
 72 		callback.run(resp.share);
 73 	}
 74 };
 75 
 76 ZmSharingPage.prototype._createControls =
 77 function() {
 78 	ZmPreferencesPage.prototype._createControls.call(this);
 79 
 80 	this.view = new ZmSharingView({parent:this, pageId:this._htmlElId});
 81 	this.view.showMounts();
 82 	this.view.findShares();
 83 	this.view.showGrants();
 84 
 85 	if (appCtxt.multiAccounts && this._acAddrSelectList) {
 86 		this._acAddrSelectList.setActiveAccount(appCtxt.getActiveAccount());
 87 	}
 88 };
 89 
 90 ZmSharingPage.prototype.hasResetButton =
 91 function() {
 92 	return false;
 93 };
 94 
 95 
 96 /**
 97  * Creates a sharing view.
 98  * @constructor
 99  * @class
100  * <p>Manages a view composed of two sections. The first section is for showing information about
101  * folders shared with the user. The user can look for shares that came via their membership in
102  * a distribution list, or shares directly from a particular user. The shares are displayed in two
103  * lists: one for shares that have not been accepted, and one for shares that have been accepted,
104  * which have mountpoints.</p>
105  * <p>
106  * Internally, shares are standardized into ZmShare objects, with a few additional fields. Shares
107  * are converted into those from several different forms: share info JSON from GetShareInfoResponse,
108  * ZmShare's on folders that have been shared by the user, and folders that have been mounted by the
109  * user.
110  *
111  * @param {Hash}	params	a hash of parameters
112  * @param {ZmSharingPage}		params.parent	the owning prefs page
113  * @param {String}	params.pageId	the ID of prefs page's HTML element
114  * 
115  * @extends		DwtComposite
116  * 
117  * @private
118  */
119 ZmSharingView = function(params) {
120 
121 	DwtComposite.apply(this, arguments);
122 
123 	this._pageId = params.pageId;
124 	this._shareByKey = {};
125 	this._shareByDomId = {};
126 
127 	this._initialize();
128 	ZmFolderTree.createAllDeferredFolders();
129 };
130 
131 ZmSharingView.prototype = new DwtComposite;
132 ZmSharingView.prototype.constructor = ZmSharingView;
133 
134 ZmSharingView.ID_RADIO			= "radio";
135 ZmSharingView.ID_GROUP			= "group";
136 ZmSharingView.ID_USER			= "user";
137 ZmSharingView.ID_OWNER			= "owner";
138 ZmSharingView.ID_FIND_BUTTON	= "findButton";
139 ZmSharingView.ID_FOLDER_TYPE	= "folderType";
140 ZmSharingView.ID_SHARE_BUTTON	= "shareButton";
141 
142 ZmSharingView.PENDING	= "PENDING";
143 ZmSharingView.MOUNTED	= "MOUNTED";
144 
145 ZmSharingView.F_ACTIONS	= "ac";
146 ZmSharingView.F_FOLDER	= "fo";
147 ZmSharingView.F_ITEM	= "it";
148 ZmSharingView.F_OWNER	= "ow";
149 ZmSharingView.F_ROLE	= "ro";
150 ZmSharingView.F_TYPE	= "ty";
151 ZmSharingView.F_WITH	= "wi";
152 
153 ZmSharingView.prototype.toString = function() { return "ZmSharingView"; };
154 
155 /**
156  * Makes a request to the server for group shares or shares from a particular user.
157  *
158  * @param owner					[string]*		address of account to check for shares from
159  * @param userButtonClicked		[boolean]*		if true, user pressed "Find Shares" button
160  * 
161  * @private
162  */
163 ZmSharingView.prototype.findShares =
164 function(owner, userButtonClicked) {
165 
166 	var errorMsg;
167 	// check if button was actually clicked, since missing owner is fine when form
168 	// goes through rote validation on display
169 	if (userButtonClicked && !owner) {
170 		errorMsg = ZmMsg.sharingErrorOwnerMissing;
171 	} else if (!this._shareForm.validate(ZmSharingView.ID_OWNER)) {
172 		errorMsg = ZmMsg.sharingErrorOwnerSelf;
173 	}
174 	if (errorMsg) {
175 		appCtxt.setStatusMsg({msg: errorMsg, level: ZmStatusView.LEVEL_INFO});
176 		return;
177 	}
178 
179 	var respCallback = new AjxCallback(this, this.showPendingShares);
180 	var type = owner ? null : ZmShare.TYPE_GROUP;
181 	this._curOwner = owner;
182 	var shares = this.parent.getShares(type, owner, respCallback);
183 };
184 /**
185  * Displays a list of shares that have been accepted/mounted by the user.
186  * 
187  * @private
188  */
189 ZmSharingView.prototype.showMounts =
190 function() {
191 
192 	var folderTree = appCtxt.getFolderTree();
193 	var folders = folderTree && folderTree.asList({remoteOnly:true});
194 	if (!folders) { return; }
195 
196 	var ownerHash = {};
197 	for (var i = 0; i < folders.length; i++) {
198 		var folder = folders[i];
199 		if (folder.isMountpoint || folder.link) {
200 			if (folder.owner) {
201 				ownerHash[folder.owner] = true;
202 			}
203 		}
204 	}
205 
206 	var owners = AjxUtil.keys(ownerHash);
207 	if (owners.length > 0) {
208 		var jsonObj = {BatchRequest:{_jsns:"urn:zimbra", onerror:"continue"}};
209 		var br = jsonObj.BatchRequest;
210 		var requests = br.GetShareInfoRequest = [];
211 		for (var i = 0; i < owners.length; i++) {
212 			var req = {_jsns: "urn:zimbraAccount"};
213 			req.owner = {by:"name", _content:owners[i]};
214 			requests.push(req);
215 		}
216 
217 		var respCallback = new AjxCallback(this, this._handleResponseGetShares);
218 		appCtxt.getRequestMgr().sendRequest({jsonObj:jsonObj, asyncMode:true, callback:respCallback});
219 	}
220 };
221 
222 ZmSharingView.prototype._handleResponseGetShares =
223 function(result) {
224 
225 	var mounts = [];
226 	var resp = result.getResponse().BatchResponse.GetShareInfoResponse;
227 	for (var i = 0; i < resp.length; i++) {
228 		var shares = resp[i].share;
229 		if (!(shares && shares.length)) { continue; }
230 		for (var j = 0; j < shares.length; j++) {
231 			var share = ZmShare.getShareFromShareInfo(shares[j]);
232 			if (share.mounted) {
233 				mounts.push(share);
234 			}
235 		}
236 	}
237 
238 	mounts.sort(ZmSharingView.sortCompareShare);
239 	this._mountedShareListView.set(AjxVector.fromArray(mounts));
240 
241 };
242 
243 /**
244  * Displays shares that are pending (have not yet been mounted).
245  *
246  * @param shares	[array]		list of JSON share info objects from GetShareInfoResponse
247  * 
248  * @private
249  */
250 ZmSharingView.prototype.showPendingShares =
251 function(shares) {
252 
253 	var pending = [];
254 	if (shares && shares.length) {
255 		for (var i = 0; i < shares.length; i++) {
256 			// convert share info to ZmShare
257 			var share = ZmShare.getShareFromShareInfo(shares[i]);
258 			if (!share.mounted) {
259 				pending.push(share);
260 			}
261 		}
262 	}
263 	pending.sort(ZmSharingView.sortCompareShare);
264 	this._pendingShareListView.set(AjxVector.fromArray(pending));
265 };
266 
267 /**
268  * Displays grants (folders shared by the user) in a list view. Grants show up as shares
269  * in folders owned by the user.
270  * 
271  * @private
272  */
273 ZmSharingView.prototype.showGrants =
274 function() {
275 
276 	// the grant objects we get in the refresh block don't have grantee names,
277 	// so use GetFolder in a BatchRequest to get them
278 	var batchCmd = new ZmBatchCommand(true, null, true);
279 	var list = appCtxt.getFolderTree().asList();
280 	for (var i = 0; i < list.length; i++) {
281 		var folder = list[i];
282 		if (folder.shares && folder.shares.length) {
283 			for (var j = 0; j < folder.shares.length; j++) {
284 				var share = folder.shares[j];
285 				if (!(share.grantee && share.grantee.name)) {
286 					batchCmd.add(new AjxCallback(folder, folder.getFolder, [null, batchCmd]));
287 					break;
288 				}
289 			}
290 		}
291 	}
292 
293 	if (batchCmd._cmds.length) {
294 		var respCallback = new AjxCallback(this, this._handleResponseGetFolder);
295 		batchCmd.run(respCallback);
296 	} else {
297 		this._handleResponseGetFolder();
298 	}
299 };
300 
301 ZmSharingView.prototype._handleResponseGetFolder =
302 function() {
303 
304 	var shares = [], invalid = [];
305 	var list = appCtxt.getFolderTree().asList();
306 	for (var i = 0; i < list.length; i++) {
307 		var folder = list[i];
308 		if (folder.shares && folder.shares.length) {
309 			for (var j = 0; j < folder.shares.length; j++) {
310 				var share = ZmShare.getShareFromGrant(folder.shares[j]);
311 				if (share.invalid) {
312 					invalid.push(share);
313 				}
314 				shares.push(share);
315 			}
316 		}
317 	}
318 
319 	shares.sort(ZmSharingView.sortCompareGrant);
320 	this._grantListView.set(AjxVector.fromArray(shares));
321 
322 	// an invalid grant is one whose grantee has been removed from the system
323 	// if we have some, ask the user if it's okay to remove them
324 	if (invalid.length) {
325 		invalid.sort(ZmSharingView.sortCompareGrant);
326 		var msgDialog = appCtxt.getOkCancelMsgDialog();
327 		var list = [];
328 		for (var i = 0; i < invalid.length; i++) {
329 			var share = invalid[i];
330 			var path = share.link && share.link.path;
331 			if (path) {
332 				list.push(["<li>", AjxStringUtil.htmlEncode(path), "</li>"].join(""));
333 			}
334 		}
335 		list = AjxUtil.uniq(list);
336 		var listText = list.join("");
337 		msgDialog.setMessage(AjxMessageFormat.format(ZmMsg.granteeGone, listText));
338 		msgDialog.registerCallback(DwtDialog.OK_BUTTON, this._revokeGrantsOk, this, [msgDialog, invalid]);
339 		msgDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._revokeGrantsCancel, this, msgDialog);
340 		msgDialog.associateEnterWithButton(DwtDialog.OK_BUTTON);
341 		msgDialog.popup(null, DwtDialog.OK_BUTTON);
342 	}
343 };
344 
345 ZmSharingView.prototype._revokeGrantsOk =
346 function(dlg, invalid) {
347 
348 	var batchCmd = new ZmBatchCommand(true, null, true);
349 	var zids = {};
350 	for (var i = 0; i < invalid.length; i++) {
351 		var share = invalid[i];
352 		zids[share.grantee.id] = share.grantee.type;
353 	}
354 
355 	for (var zid in zids) {
356 		batchCmd.add(new AjxCallback(null, ZmShare.revokeOrphanGrants, [zid, zids[zid], null, batchCmd]));
357 	}
358 
359 	if (batchCmd._cmds.length) {
360 		batchCmd.run();
361 	}
362 
363 	dlg.popdown();
364 };
365 
366 ZmSharingView.prototype._revokeGrantsCancel =
367 function(dlg) {
368 	dlg.popdown();
369 };
370 
371 ZmSharingView._handleAcceptLink =
372 function(domId) {
373 
374 	var sharingView = appCtxt.getApp(ZmApp.PREFERENCES).getPrefController().getPrefsView().getView("SHARING").view;
375 	var share = sharingView._shareByDomId[domId];
376 	if (share) {
377 		appCtxt.getAcceptShareDialog().popup(share, share.grantor.email);
378 	}
379 	return false;
380 };
381 
382 ZmSharingView._handleShareAction =
383 function(domId, handler) {
384 
385 	var sharingView = appCtxt.getApp(ZmApp.PREFERENCES).getPrefController().getPrefsView().getView("SHARING").view;
386 	var share = sharingView._shareByDomId[domId];
387 	if (share) {
388 		var dlg = appCtxt.getFolderPropsDialog();
389 		return dlg[handler](null, share);
390 	}
391 };
392 
393 ZmSharingView.prototype._initialize =
394 function() {
395 
396 	// form for finding shares
397 	var params = {};
398 	params.parent = this;
399 	params.template = "prefs.Pages#ShareForm";
400 	params.form = {
401 		items: [
402 			{ id: ZmSharingView.ID_RADIO, type: "DwtRadioButtonGroup", onclick: this._onClick, items: [
403 			{ id: ZmSharingView.ID_GROUP, type: "DwtRadioButton", value: ZmSharingView.ID_GROUP, label: ZmMsg.showGroupShares, checked: true },
404 			{ id: ZmSharingView.ID_USER, type: "DwtRadioButton", value: ZmSharingView.ID_USER, label: ZmMsg.showUserShares }]},
405 			{ id: ZmSharingView.ID_OWNER, type: "ZmAddressInputField", validator: this._validateOwner, params: { singleBubble: true } },
406 			{ id: ZmSharingView.ID_FIND_BUTTON, type: "DwtButton", label: ZmMsg.findShares, onclick: this._onClick }
407 		]
408 	};
409 	this._shareForm = new DwtForm(params);
410 	var shareFormDiv = document.getElementById(this._pageId + "_shareForm");
411 	shareFormDiv.appendChild(this._shareForm.getHtmlElement());
412 
413 	// form for creating a new share
414 	var options = [];
415 	var orgTypes = [ZmOrganizer.FOLDER, ZmOrganizer.CALENDAR, ZmOrganizer.ADDRBOOK, 
416 					ZmOrganizer.TASKS, ZmOrganizer.BRIEFCASE];
417 	var orgKey = {};
418 	orgKey[ZmOrganizer.FOLDER]		= "mailFolder";
419 	orgKey[ZmOrganizer.TASKS]		= "tasksFolder";
420 	orgKey[ZmOrganizer.BRIEFCASE]	= "briefcase";
421 	for (var i = 0; i < orgTypes.length; i++) {
422 		var orgType = orgTypes[i];
423 		if (orgType) {
424 			var key = orgKey[orgType] || ZmOrganizer.MSG_KEY[orgType];
425 			options.push({id: orgType, value: orgType, label: ZmMsg[key]});
426 		}
427 	}
428 	params.template = "prefs.Pages#GrantForm";
429 	params.form = {
430 		items: [
431 			{ id: ZmSharingView.ID_FOLDER_TYPE, type: "DwtSelect", items: options},
432 			{ id: ZmSharingView.ID_SHARE_BUTTON, type: "DwtButton", label: ZmMsg.share, onclick: this._onClick }
433 		]
434 	};
435 	this._grantForm = new DwtForm(params);
436 	var grantFormDiv = document.getElementById(this._pageId + "_grantForm");
437 	grantFormDiv.appendChild(this._grantForm.getHtmlElement());
438 
439 	var folderTypeSelect = this._grantForm._items.folderType.control;
440 	folderTypeSelect.fixedButtonWidth();
441 
442 	// list views of shares and grants
443 	this._pendingShareListView = new ZmSharingListView({parent:this, type:ZmShare.SHARE,
444 		status:ZmSharingView.PENDING, sharingView:this, view:ZmId.VIEW_SHARE_PENDING});
445 	this._addListView(this._pendingShareListView, this._pageId + "_pendingShares");
446 	this._mountedShareListView = new ZmSharingListView({parent:this, type:ZmShare.SHARE,
447 		status:ZmSharingView.MOUNTED, sharingView:this, view:ZmId.VIEW_SHARE_MOUNTED});
448 	this._addListView(this._mountedShareListView, this._pageId + "_mountedShares");
449 	this._grantListView = new ZmSharingListView({parent:this, type:ZmShare.GRANT,
450 		sharingView:this, view:ZmId.VIEW_SHARE_GRANTS});
451 	this._addListView(this._grantListView, this._pageId + "_sharesBy");
452 
453 	// autocomplete
454 	if (appCtxt.get(ZmSetting.CONTACTS_ENABLED) || appCtxt.get(ZmSetting.GAL_ENABLED)) {
455 		var params = {
456 			parent:			appCtxt.getShell(),
457 			dataClass:		appCtxt.getAutocompleter(),
458 			matchValue:		ZmAutocomplete.AC_VALUE_EMAIL,
459 			separator:		"",
460 			keyUpCallback:	this._enterCallback.bind(this),
461 			contextId:		this.toString()
462 		};
463 		this._acAddrSelectList = new ZmAutocompleteListView(params);
464 		var inputCtrl = this._shareForm.getControl(ZmSharingView.ID_OWNER);
465 		this._acAddrSelectList.handle(inputCtrl.getInputElement(), inputCtrl._htmlElId);
466 		inputCtrl.setAutocompleteListView(this._acAddrSelectList);
467 	}
468 
469 	appCtxt.getFolderTree().addChangeListener(new AjxListener(this, this._folderTreeChangeListener));
470 };
471 
472 ZmSharingView.prototype._addListView =
473 function(listView, listViewDivId) {
474 	var listDiv = document.getElementById(listViewDivId);
475  	listDiv.appendChild(listView.getHtmlElement());
476 	listView.setUI(null, true); // renders headers and empty list
477 	listView._initialized = true;
478 };
479 
480 // make sure user is not looking for folders shared from their account
481 ZmSharingView.prototype._validateOwner =
482 function(value) {
483 	if (!value) { return true; }
484 	return (appCtxt.isMyAddress(value, true)) ? false: true;
485 };
486 
487 // Note that in the handler call, "this" is set to the form
488 ZmSharingView.prototype._onClick =
489 function(id) {
490 
491 	if (id == ZmSharingView.ID_FIND_BUTTON) {
492 		this.setValue(ZmSharingView.ID_USER, true, true);
493 		this.parent.findShares(this.getValue(ZmSharingView.ID_OWNER), true);
494 	} else if (id == ZmSharingView.ID_GROUP) {
495 		this.parent.findShares();
496 	} else if (id == ZmSharingView.ID_SHARE_BUTTON) {
497 		var orgType = this.getValue(ZmSharingView.ID_FOLDER_TYPE);
498 		this.parent._showChooser(orgType);
499 	}
500 };
501 
502 ZmSharingView.prototype._enterCallback =
503 function(ev) {
504 	var key = DwtKeyEvent.getCharCode(ev);
505 	if (DwtKeyEvent.IS_RETURN[key]) {
506 		this._onClick.call(this._shareForm, ZmSharingView.ID_FIND_BUTTON);
507 		return false;
508 	}
509 	return true;
510 };
511 
512 ZmSharingView.prototype._showChooser =
513 function(orgType) {
514 
515 	// In multi-account, sharing page gets its own choose-folder dialog since it 
516 	// only shows the active account's overview. Otherwise, we have to juggle
517 	// overviews with between single/multiple overview trees. Ugh.
518 	var dialog;
519 	if (appCtxt.multiAccounts) {
520 		if (!this._chooseFolderDialog) {
521 			AjxDispatcher.require("Extras");
522 			this._chooseFolderDialog = new ZmChooseFolderDialog(appCtxt.getShell());
523 		}
524 		dialog = this._chooseFolderDialog;
525 	} else {
526 		dialog = appCtxt.getChooseFolderDialog();
527 	}
528 
529 	var overviewId = dialog.getOverviewId(ZmOrganizer.APP[orgType]);
530 	if (appCtxt.multiAccounts) {
531 		overviewId = [overviewId, "-", this.toString(), "-", appCtxt.getActiveAccount().name].join("");
532 	}
533 	var omit = {};
534 	omit[ZmFolder.ID_TRASH] = true;
535 	var params = {
536 		treeIds: [orgType],
537 		overviewId: overviewId,
538 		title: ZmMsg.chooseFolder,
539 		skipReadOnly: true,
540 		skipRemote: true,
541 		omit: omit,
542 		hideNewButton: true,
543 		appName: ZmOrganizer.APP[orgType],
544 		noRootSelect: true,
545 		forceSingle: true
546 	};
547 	dialog.reset();
548 	dialog.registerCallback(DwtDialog.OK_BUTTON, this._folderSelectionCallback, this, [dialog]);
549 	dialog.popup(params);
550 };
551 
552 ZmSharingView.prototype._folderSelectionCallback =
553 function(chooserDialog, org) {
554 
555 	chooserDialog.popdown();
556 	var shareDialog = appCtxt.getSharePropsDialog();
557 	shareDialog.popup(ZmSharePropsDialog.NEW, org);
558 };
559 
560 /**
561  * Sorts shares in the following order:
562  *   1. by name of owner
563  *   2. by name of group it was shared with, if any
564  *   3. by path of shared folder
565  *   
566  * @private
567  */
568 ZmSharingView.sortCompareShare =
569 function(a, b) {
570 
571 	var ownerA = (a.grantor.name && a.grantor.name.toLowerCase()) || (a.grantor.email && a.grantor.email.toLowerCase()) || "";
572 	var ownerB = (b.grantor.name && b.grantor.name.toLowerCase()) || (b.grantor.email && b.grantor.email.toLowerCase()) || "";
573 	if (ownerA != ownerB) {
574 		return (ownerA > ownerB) ? 1 : -1;
575 	}
576 
577 	var groupA = (a.grantee.type == ZmShare.TYPE_GROUP) ? (a.grantee.name && a.grantee.name.toLowerCase()) : "";
578 	var groupB = (b.grantee.type == ZmShare.TYPE_GROUP) ? (b.grantee.name && b.grantee.name.toLowerCase()) : "";
579 	if (groupA != groupB) {
580 		if (!groupA && groupB) {
581 			return 1;
582 		} else if (groupA && !groupB) {
583 			return -1;
584 		} else {
585 			return (groupA > groupB) ? 1 : -1;
586 		}
587 	}
588 
589 	var pathA = (a.link.name && a.link.name.toLowerCase()) || "";
590 	var pathB = (b.link.name && b.link.name.toLowerCase()) || "";
591 	if (pathA != pathB) {
592 		return (pathA > pathB) ? 1 : -1;
593 	}
594 
595 	return 0;
596 };
597 
598 /**
599  * Sorts shares in the following order:
600  *   1. by name of who it was shared with
601  *   2. by path of shared folder
602  *   
603  * @private
604  */
605 ZmSharingView.sortCompareGrant =
606 function(a, b) {
607 
608 	var granteeA = (a.grantee && a.grantee.name && a.grantee.name.toLowerCase()) || "";
609 	var granteeB = (b.grantee && b.grantee.name && b.grantee.name.toLowerCase()) || "";
610 	if (granteeA != granteeB) {
611 		return (granteeA > granteeB) ? 1 : -1;
612 	}
613 
614 	var pathA = (a.link && a.link.name) || "";
615 	var pathB = (b.link && b.link.name) || "";
616 	if (pathA != pathB) {
617 		return (pathA > pathB) ? 1 : -1;
618 	}
619 
620 	return 0;
621 };
622 
623 ZmSharingView.prototype._folderTreeChangeListener =
624 function(ev) {
625 
626 	this._pendingShareListView._changeListener(ev);
627 	this._mountedShareListView._changeListener(ev);
628 	this._grantListView._changeListener(ev);
629 };
630 
631 /**
632  * Handle modifications to pending shares, which don't have an item to propagate
633  * changes through. The preferences app sends the notifications here.
634  *
635  * @param modifies		[hash]		notifications
636  * 
637  * @private
638  */
639 ZmSharingView.prototype.notifyModify =
640 function(modifies) {
641 
642 	for (var name in modifies) {
643 		if (name == "folder") {
644 			modifies = modifies.folder;
645 			for (var i = 0; i < modifies.length; i++) {
646 				var mod = modifies[i];
647 				var share = this._shareByKey[mod.id];
648 				var ev = new ZmEvent();
649 				if (share) {
650 					var parts = mod.id.split(":");
651 					share.zid = parts[0];
652 					share.rid = parts[1];
653 					ev.ersatz = true;
654 					ev.set(ZmEvent.E_MODIFY);
655 					var fields = {};
656 					if (mod.perm) {
657 						share.setPermissions(mod.perm);
658 						fields[ZmOrganizer.F_PERMS] = true;
659 					}
660 					if (mod.name) {
661 						fields[ZmOrganizer.F_RNAME] = true;
662 					}
663 					if (mod.l) {
664 						ev.set(ZmEvent.E_MOVE);
665 					}
666 					ev.setDetail("share", share);
667 					ev.setDetail("fields", fields);
668 					this._folderTreeChangeListener(ev);
669 					mod._handled = true;
670 				} else if (mod.id.indexOf(":") != -1) {
671 					ev.set(ZmEvent.E_CREATE);
672 				}
673 			}
674 		}
675 	}
676 };
677 
678 /**
679  * If we get a refresh block from the server, redraw all three list views.
680  *
681  * @param refresh	[object]	the refresh block JSON
682  * 
683  * @private
684  */
685 ZmSharingView.prototype.refresh =
686 function(refresh) {
687 	this.findShares(this._curOwner);
688 	this.showGrants();
689 };
690 
691 /**
692  * A list view that displays some form of shares, either with or by the user. The data
693  * is in the form of a list of ZmShare's.
694  *
695  * @param {Hash}	params	a hash of parameters
696  * @param	{constant}		params.type		the SHARE (shared with user) or GRANT (shared by user)
697  * @param	{ZmSharingView}		params.view		the owning view
698  * @param	{constant}		params.status	the pending or mounted
699  *       
700  * @extends		DwtListView
701  * 
702  * @private
703  */
704 ZmSharingListView = function(params) {
705 
706 	this.type = params.type;
707 	this.status = params.status;
708 	params.headerList = this._getHeaderList();
709 	DwtListView.call(this, params);
710 
711 	this.sharingView = params.sharingView;
712 	this._idMap = {};
713 };
714 
715 ZmSharingListView.prototype = new DwtListView;
716 ZmSharingListView.prototype.constructor = ZmSharingListView;
717 
718 ZmSharingListView.prototype.toString =
719 function() {
720 	return "ZmSharingListView";
721 };
722 
723 ZmSharingListView.prototype._getHeaderList =
724 function() {
725 
726 	var headerList = [];
727 	if (this.type == ZmShare.SHARE) {
728 		headerList.push(new DwtListHeaderItem({field:ZmSharingView.F_OWNER, text:ZmMsg.sharingOwner, width:ZmMsg.COLUMN_WIDTH_OWNER_SH}));
729 	} else if (this.type == ZmShare.GRANT) {
730 		headerList.push(new DwtListHeaderItem({field:ZmSharingView.F_WITH, text:ZmMsg.sharingWith, width:ZmMsg.COLUMN_WIDTH_WITH_SH}));
731 	}
732 	headerList.push(new DwtListHeaderItem({field:ZmSharingView.F_ITEM, text:ZmMsg.sharingItem}));
733 	headerList.push(new DwtListHeaderItem({field:ZmSharingView.F_TYPE, text:ZmMsg.sharingFolderType, width:ZmMsg.COLUMN_WIDTH_TYPE_SH}));
734 	headerList.push(new DwtListHeaderItem({field:ZmSharingView.F_ROLE, text:ZmMsg.sharingRole, width:ZmMsg.COLUMN_WIDTH_ROLE_SH}));
735 	if (this.type == ZmShare.SHARE) {
736 		if (this.status == ZmSharingView.PENDING) {
737 			headerList.push(new DwtListHeaderItem({field:ZmSharingView.F_ACTIONS, text:ZmMsg.actions, width:ZmMsg.COLUMN_WIDTH_ACTIONS_SH}));
738 		} else {
739 			headerList.push(new DwtListHeaderItem({field:ZmSharingView.F_FOLDER, text:ZmMsg.sharingFolder, width:ZmMsg.COLUMN_WIDTH_FOLDER_SH}));
740 		}
741 		headerList.push(new DwtListHeaderItem({field:ZmSharingView.F_WITH, text:ZmMsg.sharingWith, width:ZmMsg.COLUMN_WIDTH_WITH_SH}));
742 	} else {
743 		headerList.push(new DwtListHeaderItem({field:ZmSharingView.F_ACTIONS, text:ZmMsg.actions, width:ZmMsg.COLUMN_WIDTH_ACTIONS_SH}));
744 	}
745 
746 	return headerList;
747 };
748 
749 ZmSharingListView.prototype._getItemId =
750 function(item) {
751 
752 	var account = (item.type == ZmShare.SHARE) ? item.grantor && item.grantor.id :
753 													   item.grantee && item.grantee.id;
754 	var key = [account, item.link.id].join(":");
755 	var id = item.domId;
756 	if (!id) {
757 		id = Dwt.getNextId();
758 		item.domId = id;
759 		this.sharingView._shareByDomId[id] = item;
760 		this.sharingView._shareByKey[key] = item;
761 	}
762 
763 	return id;
764 };
765 
766 ZmSharingListView.prototype._getCellId =
767 function(item, field, params) {
768     var rowId = this._getItemId(item);
769     return [rowId, field].join("_");
770 };
771 
772 ZmSharingListView.prototype._getCellContents =
773 function(html, idx, item, field, colIdx, params) {
774 
775 	if (field == ZmSharingView.F_OWNER) {
776 		html[idx++] = AjxStringUtil.htmlEncode(item.grantor.name) || item.grantor.email;
777 	} else if (field == ZmSharingView.F_WITH) {
778 		var type = item.grantee.type;
779 		if (type == ZmShare.TYPE_PUBLIC) {
780 			html[idx++] = ZmMsg.shareWithPublic;
781 		} else if (type == ZmShare.TYPE_ALL) {
782 			html[idx++] = ZmMsg.shareWithAll;
783 		} else if (type == ZmShare.TYPE_GUEST) {
784 			html[idx++] = item.grantee.id;
785 		} else {
786 			html[idx++] = AjxStringUtil.htmlEncode(item.grantee.name);
787 		}
788 	} else if (field == ZmSharingView.F_ITEM) {
789 		html[idx++] = AjxStringUtil.htmlEncode(item.link.path);
790 	} else if (field == ZmSharingView.F_TYPE) {
791 		html[idx++] = (item.object && item.object.type) ? ZmMsg[ZmOrganizer.FOLDER_KEY[item.object.type]] :
792 					  									  ZmShare._getFolderType(item.link.view);
793 	} else if (field == ZmSharingView.F_ROLE) {
794 		var role = item.link.role || ZmShare._getRoleFromPerm(item.link.perm);
795 		html[idx++] = ZmShare.getRoleName(role);
796 	} else if (field == ZmSharingView.F_FOLDER) {
797 		html[idx++] = (item.mountpoint && item.mountpoint.path) || " ";
798 	} else if (field == ZmSharingView.F_ACTIONS) {
799 		if (this.type == ZmShare.SHARE) {
800 			var id = this._getItemId(item);
801             var linkId = [id, ZmShare.ACCEPT].join("_");
802 			html[idx++] = "<a href='javascript:;' id='" + linkId + "' onclick='ZmSharingView._handleAcceptLink(" + '"' + id + '"' + ");'>" + ZmMsg.accept + "</a>";
803 		} else {
804 			idx = this._addActionLinks(item, html, idx);
805 		}
806 	}
807 
808 	return (params && params.returnText) ? html.join("") : idx;
809 };
810 
811 ZmSharingListView.prototype._changeListener =
812 function(ev) {
813 
814 	var organizers = ev.getDetail("organizers") || [];
815 	var fields = ev.getDetail("fields") || {};
816 
817 	if (this.type == ZmShare.SHARE) {
818 		var share = ev.getDetail("share");
819 		if (!share) {
820 			var mtpt = organizers[0];
821 			if (!mtpt.link) { return; }
822 			var share = this.sharingView._shareByKey[[mtpt.zid, mtpt.rid].join(":")];
823 			share = ZmShare.getShareFromLink(mtpt, share);	// update share
824 		}
825 		if (!share) { return; }
826 		if (ev.event == ZmEvent.E_CREATE) {
827 			// share accepted, mountpoint created; move from pending to mounted list
828 			if (this.status == ZmSharingView.PENDING) {
829 				this.removeItem(share);
830 			} else if (this.status == ZmSharingView.MOUNTED) {
831 				var index = this._list && this._getIndex(share, this._list.getArray(), ZmSharingView.sortCompareShare);
832 				this.addItem(share, index, true);
833 			}
834 		} else if (ev.event == ZmEvent.E_MODIFY) {
835 			if ((this.status == ZmSharingView.PENDING && share.mounted) ||
836 				(this.status == ZmSharingView.MOUNTED && !share.mounted)) { return; }
837 			if (fields[ZmOrganizer.F_PERMS]) {
838 				var cell = document.getElementById(this._getCellId(share, ZmSharingView.F_ROLE));
839 				if (cell) {
840 					cell.innerHTML = this._getCellContents([], 0, share, ZmSharingView.F_ROLE, null, {returnText:true});
841 				}
842 			}
843 			if ((this.status == ZmSharingView.MOUNTED) && fields[ZmOrganizer.F_NAME]) {
844 				var cell = document.getElementById(this._getCellId(share, ZmSharingView.F_FOLDER));
845 				if (cell) {
846 					cell.innerHTML = this._getCellContents([], 0, share, ZmSharingView.F_FOLDER, null, {returnText:true});
847 				}
848 			}
849 		}
850 		// if a remote folder has been renamed or moved, rerun the search
851 		if (ev.event == ZmEvent.E_MOVE || fields[ZmOrganizer.F_RNAME]) {
852 			if (this.sharingView._curOwner) {
853 				this.sharingView.findShares(this.sharingView._curOwner);
854 			}
855 		}
856 	}
857 
858 	// Any change to a grant (including create or revoke) results in a wholesale replacement of
859 	// the folder's shares, so it's easiest to just redraw the list. Also check for folder rename.
860 	if (this.type == ZmShare.GRANT) {
861 		if ((ev.event = ZmEvent.E_MODIFY && fields[ZmOrganizer.F_SHARES]) ||
862 		    (ev.event = ZmEvent.E_MODIFY && fields[ZmOrganizer.F_NAME] && organizers[0].shares)) {
863 
864 			this.sharingView.showGrants();
865 		}
866 	}
867 };
868 
869 /**
870  * Adds links for editing, revoking, or resending a grant.
871  *
872  * @param share		[ZmShare]		share
873  * @param html		[array]			HTML content
874  * @param idx		[int]			index
875  * 
876  * @private
877  */
878 ZmSharingListView.prototype._addActionLinks =
879 function(share, html, idx) {
880 
881 	var type = share.grantee.type;
882 	var actions = ["edit", "revoke", "resend"];
883 	if (type == ZmShare.TYPE_ALL || type == ZmShare.TYPE_DOMAIN || !share.link.role) {
884 		html[idx++] = ZmMsg.configureWithAdmin;
885 		actions = [];
886 	}
887 
888 	var handlers = ["_handleEditShare", "_handleRevokeShare", "_handleResendShare"]; // handlers in ZmFolderPropsDialog
889 
890 	for (var i = 0; i < actions.length; i++) {
891 
892 		var action = actions[i];
893         var linkId = [share.domId, action].join("_");
894 		// public shares have no editable fields, and sent no mail
895 		if (share.isGuest() && action == "edit") { continue; }
896 		if ((share.isPublic() || share.invalid) && (action == "edit" || action == "resend")) { continue; }
897 
898 		html[idx++] = "<a href='javascript:;' id='" + linkId + "' onclick='ZmSharingView._handleShareAction(" + '"' + share.domId + '", "' + handlers[i] + '"' + ");'>" + ZmMsg[action] + "</a> ";
899 	}
900 
901 	return idx;
902 };
903 
904 /**
905  * Returns the position of the share in the given list using the given compare function.
906  *
907  * @param share			[ZmShare]		a share
908  * @param list			[array]			list of shares
909  * @param compareFunc	[function]		compare function
910  * 
911  * @private
912  */
913 ZmSharingListView.prototype._getIndex =
914 function(share, list, compareFunc) {
915 
916 	for (var i = 0; i < list.length; i++) {
917 		var result = compareFunc(share, list[i]);
918 		if (result == -1) {
919 			return i;
920 		}
921 	}
922 	return null;
923 };
924