1 	/*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 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) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * 
 27  * This file defines a folder tree.
 28  *
 29  */
 30 
 31 /**
 32  * Creates an empty folder tree.
 33  * @class
 34  * This class represents a tree of folders. It may be typed, in which case
 35  * the folders are all of that type, or untyped.
 36  * 
 37  * @author Conrad Damon
 38  * 
 39  * @param {constant}	type		the organizer type
 40  * 
 41  * @extends	ZmTree
 42  */
 43 ZmFolderTree = function(type) {
 44 	ZmTree.call(this, type);
 45 };
 46 
 47 ZmFolderTree.prototype = new ZmTree;
 48 ZmFolderTree.prototype.constructor = ZmFolderTree;
 49 
 50 
 51 // Consts
 52 ZmFolderTree.IS_PARSED = {};
 53 
 54 
 55 // Public Methods
 56 
 57 /**
 58  * Returns a string representation of the object.
 59  * 
 60  * @return		{String}		a string representation of the object
 61  */
 62 ZmFolderTree.prototype.toString =
 63 function() {
 64 	return "ZmFolderTree";
 65 };
 66 
 67 /**
 68  * Loads the folder or the zimlet tree.
 69  * 
 70  * @param	{Object}		rootObj		the root object
 71  * @param	{String}		elementType		the element type
 72  * @param	{ZmZimbraAccount}		account		the account
 73  */
 74 ZmFolderTree.prototype.loadFromJs =
 75 function(rootObj, elementType, account) {
 76 	this.root = (elementType == "zimlet")
 77 		? ZmZimlet.createFromJs(null, rootObj, this)
 78 		: ZmFolderTree.createFromJs(null, rootObj, this, elementType, null, account);
 79 };
 80 
 81 /**
 82  * Generic function for creating a folder. Handles any organizer type that comes
 83  * in the folder list.
 84  * 
 85  * @param {ZmFolder}	parent		the parent folder
 86  * @param {Object}	obj			the JSON with folder data
 87  * @param {ZmFolderTree}	tree			the containing tree
 88  * @param {String}	elementType		the type of containing JSON element
 89  * @param {Array}	path			the list of path elements
 90  * @param {ZmZimbraAccount}	account		the account this folder belongs to
 91  */
 92 ZmFolderTree.createFromJs =
 93 function(parent, obj, tree, elementType, path, account) {
 94 	if (!(obj && obj.id)) { return; }
 95 
 96 	var folder;
 97 	if (elementType == "search") {
 98 		var types;
 99 		var idParts = obj.id.split(":");
100 		// Suppress display of searches for the shared mailbox (See Bug 96090) - it will have an id
101 		// of the form 'uuid:id'.  Local searches will just have 'id'
102 		if (!idParts || (idParts.length <= 1)) {
103 			if (obj.types) {
104 				var t = obj.types.split(",");
105 				types = [];
106 				var mailEnabled = appCtxt.get(ZmSetting.MAIL_ENABLED);
107 				for (var i = 0; i < t.length; i++) {
108 					var type = ZmSearch.TYPE_MAP[t[i]];
109 					if (!type || (!mailEnabled && (type == ZmItem.CONV || type == ZmItem.MSG))) {
110 						continue;
111 					}
112 					types.push(type);
113 				}
114 				if (types.length == 0) {
115 					return null;
116 				}
117 			}
118 			DBG.println(AjxDebug.DBG2, "Creating SEARCH with id " + obj.id + " and name " + obj.name);
119 			var params = {
120 				id: obj.id,
121 				name: obj.name,
122 				parent: parent,
123 				tree: tree,
124 				numUnread: obj.u,
125 				query: obj.query,
126 				types: types,
127 				sortBy: obj.sortBy,
128 				account: account,
129 				color: obj.color,
130 				rgb: obj.rgb
131 			};
132 			folder = new ZmSearchFolder(params);
133 			ZmFolderTree._fillInFolder(folder, obj, path);
134 			ZmFolderTree._traverse(folder, obj, tree, (path || []), elementType, account);
135 		}
136 	} else {
137 		var type = obj.view
138 			? (ZmOrganizer.TYPE[obj.view])
139 			: (parent ? parent.type : ZmOrganizer.FOLDER);
140 
141 		if (!type) {
142 			DBG.println(AjxDebug.DBG1, "No known type for view " + obj.view);
143 			return;
144 		}
145 		// let's avoid deferring folders for offline since multi-account folder deferring is hairy
146 		var hasGrants = (obj.acl && obj.acl.grant && obj.acl.grant.length > 0);
147 		if (appCtxt.inStartup && ZmOrganizer.DEFERRABLE[type] && !appCtxt.isOffline) {
148 			var app = appCtxt.getApp(ZmOrganizer.APP[type]);
149 			var defParams = {
150 				type:			type,
151 				parent:			parent,
152 				obj:			obj,
153 				tree:			tree,
154 				path:			path,
155 				elementType:	elementType,
156 				account:		account
157 			};
158 			app.addDeferredFolder(defParams);
159 		} else {
160 			var pkg = ZmOrganizer.ORG_PACKAGE[type];
161 			if (pkg) {
162 				AjxDispatcher.require(pkg);
163 			}
164 			folder = ZmFolderTree.createFolder(type, parent, obj, tree, path, elementType, account);
165             if (appCtxt.isExternalAccount() && folder.isSystem() && folder.id != ZmOrganizer.ID_ROOT) { return; }
166 			ZmFolderTree._traverse(folder, obj, tree, (path || []), elementType, account);
167 		}
168 	}
169 
170 	return folder;
171 };
172 
173 ZmFolderTree.createAllDeferredFolders =
174 function() {
175 	var ac = appCtxt.getAppController();
176 	for (var appId in ZmApp.ORGANIZER) {
177 		var app = ac.getApp(appId);
178 		app.createDeferred();
179 	}
180 };
181 
182 /**
183  * @private
184  */
185 ZmFolderTree._traverse =
186 function(folder, obj, tree, path, elementType, account) {
187 
188 	var isRoot = (folder.nId == ZmOrganizer.ID_ROOT);
189 	if (obj.folder && obj.folder.length) {
190 		if (!isRoot) {
191 			path.push(obj.name);
192 		}
193 		for (var i = 0; i < obj.folder.length; i++) {
194 			var folderObj = obj.folder[i];
195 			var childFolder = ZmFolderTree.createFromJs(folder, folderObj, tree, (elementType || "folder"), path, account);
196 			if (folder && childFolder) {
197 				folder.children.add(childFolder);
198 			}
199 		}
200 		if (!isRoot) {
201 			path.pop();
202 		}
203 	}
204 	
205 	if (obj.search && obj.search.length) {
206 		if (!isRoot) {
207 			path.push(obj.name);
208 		}
209 		for (var i = 0; i < obj.search.length; i++) {
210 			var searchObj = obj.search[i];
211 			var childSearch = ZmFolderTree.createFromJs(folder, searchObj, tree, "search", path, account);
212 			if (childSearch) {
213 				folder.children.add(childSearch);
214 			}
215 		}
216 		if (!isRoot) {
217 			path.pop();
218 		}
219 	}
220 
221 	if (obj.link && obj.link.length) {
222 		for (var i = 0; i < obj.link.length; i++) {
223 			var link = obj.link[i];
224 			var childFolder = ZmFolderTree.createFromJs(folder, link, tree, "link", path, account);
225 			if (childFolder) {
226 				folder.children.add(childFolder);
227 			}
228 		}
229 	}
230 };
231 
232 /**
233  * Creates the folder.
234  * 
235  * @param {String}	type		the folder type
236  * @param {ZmFolder}	parent		the parent folder
237  * @param {Object}	obj			the JSON with folder data
238  * @param {ZmFolderTree}	tree			the containing tree
239  * @param {Array}	path			the list of path elements
240  * @param {String}	elementType		the type of containing JSON element
241  * @param {ZmZimbraAccount}	account		the account this folder belongs to
242  */
243 ZmFolderTree.createFolder =
244 function(type, parent, obj, tree, path, elementType, account) {
245 	var orgClass = eval(ZmOrganizer.ORG_CLASS[type]);
246 	if (!orgClass) { return null; }
247 
248 	DBG.println(AjxDebug.DBG2, "Creating " + type + " with id " + obj.id + " and name " + obj.name);
249 
250 	var params = {
251 		id: 		obj.id,
252 		name: 		obj.name,
253 		parent: 	parent,
254 		tree: 		tree,
255 		color: 		obj.color,
256 		rgb:		obj.rgb,
257 		owner: 		obj.owner,
258 		oname: 		obj.oname,
259 		zid: 		obj.zid,
260 		rid: 		obj.rid,
261 		restUrl: 	obj.rest,
262 		url: 		obj.url,
263 		numUnread: 	obj.u,
264 		numTotal: 	obj.n,
265 		sizeTotal: 	obj.s,
266 		perm: 		obj.perm,
267 		link: 		elementType == "link",
268 		broken: 	obj.broken,
269 		account:	account,
270         webOfflineSyncDays : obj.webOfflineSyncDays,
271         retentionPolicy: obj.retentionPolicy
272 	};
273 
274 	var folder = new orgClass(params);
275 	ZmFolderTree._fillInFolder(folder, obj, path);
276 	ZmFolderTree.IS_PARSED[type] = true;
277 
278 	return folder;
279 };
280 
281 /**
282  * @private
283  */
284 ZmFolderTree._fillInFolder =
285 function(folder, obj, path) {
286 	if (path && path.length) {
287 		folder.path = path.join("/");
288 	}
289 
290 	if (obj.f && folder._parseFlags) {
291 		folder._parseFlags(obj.f);
292 	}
293 
294 	folder._setSharesFromJs(obj);
295 };
296 
297 /**
298  * Gets the folder by type.
299  * 
300  * @param	{String}	type	the type
301  * @return	{ZmFolder}	the folder or <code>null</code> if not found
302  */
303 ZmFolderTree.prototype.getByType =
304 function(type) {
305 	return this.root ? this.root.getByType(type) : null;
306 };
307 
308 /**
309  * Gets the folder by path.
310  * 
311  * @param	{String}	path	the path
312  * @param	{Boolean}	useSystemName		<code>true</code> to use the system name
313  * @return	{ZmFolder}	the folder or <code>null</code> if not found
314  */
315 ZmFolderTree.prototype.getByPath =
316 function(path, useSystemName) {
317 	return this.root ? this.root.getByPath(path, useSystemName) : null;
318 };
319 
320 /**
321  * Handles a missing link by marking its organizer as not there, redrawing it in
322  * any tree views, and asking to delete it.
323  *
324  * @param {int}	organizerType		the type of organizer (constants defined in {@link ZmOrganizer})
325  * @param {String}	zid			the zid of the missing folder
326  * @param {String}	rid			the rid of the missing folder
327  * @return	{Boolean}	<code>true</code> if the error is handled
328  */
329 ZmFolderTree.prototype.handleNoSuchFolderError =
330 function(organizerType, zid, rid) {
331 	var items = this.getByType(organizerType);
332 
333 	var treeView;
334 	var handled = false;
335 	if (items) {
336 		for (var i = 0; i < items.length; i++) {
337 			if ((items[i].zid == zid) && (items[i].rid == rid)) {
338 				// Mark that the item is not there any more.
339 				items[i].noSuchFolder = true;
340 
341 				// Change its appearance in the tree.
342 				if (!treeView) {
343 					var overviewId = appCtxt.getAppController().getOverviewId();
344 					treeView = appCtxt.getOverviewController().getTreeView(overviewId, organizerType);
345 				}
346 				var node = treeView.getTreeItemById(items[i].id);
347 				node.setText(items[i].getName(true));
348 
349 				// Ask if it should be deleted now.
350 				this.handleDeleteNoSuchFolder(items[i]);
351 				handled = true;
352 			}
353 		}
354 	}
355 	return handled;
356 };
357 
358 /**
359  * Handles no such folder. The user will be notified that a linked organizer generated a "no such folder",
360  * error, giving the user a chance to delete the folder.
361  *
362  * @param {ZmOrganizer}	organizer	the organizer
363  */
364 ZmFolderTree.prototype.handleDeleteNoSuchFolder =
365 function(organizer) {
366 	var ds = appCtxt.getYesNoMsgDialog();
367 	ds.reset();
368 	ds.registerCallback(DwtDialog.YES_BUTTON, this._deleteOrganizerYesCallback, this, [organizer, ds]);
369 	ds.registerCallback(DwtDialog.NO_BUTTON, appCtxt.getAppController()._clearDialog, this, ds);
370 	var msg = AjxMessageFormat.format(ZmMsg.confirmDeleteMissingFolder, AjxStringUtil.htmlEncode(organizer.getName(false, 0, true)));
371 	ds.setMessage(msg, DwtMessageDialog.WARNING_STYLE);
372 	ds.popup();
373 };
374 
375 /**
376  * Handles the "Yes" button in the delete organizer dialog.
377  * 
378  * @param	{ZmOrganizer}	organizer		the organizer
379  * @param	{ZmDialog}		dialog		the dialog
380  */
381 ZmFolderTree.prototype._deleteOrganizerYesCallback =
382 function(organizer, dialog) {
383 	organizer._delete();
384 	appCtxt.getAppController()._clearDialog(dialog);
385 };
386 
387 /**
388  * Issues a <code><BatchRequest></code> of <code><GetFolderRequest></code>s for existing
389  * mountpoints that do not have permissions set.
390  *
391  * @param	{Hash}	params	a hash of parameters
392  * @param {int}	params.type			the {@link ZmItem} type constant
393  * @param {AjxCallback}	params.callback			the callback to trigger after fetching permissions
394  * @param {Boolean}	params.skipNotify		<code>true</code> to skip notify after fetching permissions
395  * @param {Array}	params.folderIds			the list of folder Id's to fetch permissions for
396  * @param {Boolean}	params.noBusyOverlay		<code>true</code> to not block the UI while fetching permissions
397  * @param {String}	params.accountName		the account to issue request under
398  */
399 ZmFolderTree.prototype.getPermissions =
400 function(params) {
401 	var needPerms = params.folderIds || this._getItemsWithoutPerms(params.type);
402 
403 	// build batch request to get all permissions at once
404 	if (needPerms.length > 0) {
405 		var soapDoc = AjxSoapDoc.create("BatchRequest", "urn:zimbra");
406 		soapDoc.setMethodAttribute("onerror", "continue");
407 
408 		var doc = soapDoc.getDoc();
409 		for (var j = 0; j < needPerms.length; j++) {
410 			var folderRequest = soapDoc.set("GetFolderRequest", null, null, "urn:zimbraMail");
411 			var folderNode = doc.createElement("folder");
412 			folderNode.setAttribute("l", needPerms[j]);
413 			folderRequest.appendChild(folderNode);
414 		}
415 
416 		var respCallback = new AjxCallback(this, this._handleResponseGetShares, [params.callback, params.skipNotify]);
417 		appCtxt.getRequestMgr().sendRequest({
418 			soapDoc: soapDoc, 
419 			asyncMode: true,
420 			callback: respCallback,
421 			noBusyOverlay: params.noBusyOverlay,
422 			accountName: params.accountName
423 		});
424 	} else {
425 		if (params.callback) {
426 			params.callback.run();
427 		}
428 	}
429 };
430 
431 /**
432  * @private
433  */
434 ZmFolderTree.prototype._getItemsWithoutPerms =
435 function(type) {
436 	var needPerms = [];
437 	var orgs = type ? [type] : [ZmOrganizer.FOLDER, ZmOrganizer.CALENDAR, ZmOrganizer.TASKS, ZmOrganizer.BRIEFCASE, ZmOrganizer.ADDRBOOK];
438 
439 	for (var j = 0; j < orgs.length; j++) {
440 		var org = orgs[j];
441 		if (!ZmFolderTree.IS_PARSED[org]) { continue; }
442 
443 		var items = this.getByType(org);
444 
445 		for (var i = 0; i < items.length; i++) {
446 			if (items[i].link && items[i].shares == null) {
447 				needPerms.push(items[i].id);
448 			}
449 		}
450 	}
451 
452 	return needPerms;
453 };
454 
455 /**
456  * @private
457  */
458 ZmFolderTree.prototype._handleResponseGetShares =
459 function(callback, skipNotify, result) {
460 	var batchResp = result.getResponse().BatchResponse;
461 	this._handleErrorGetShares(batchResp);
462 
463 	var resp = batchResp.GetFolderResponse;
464 	if (resp) {
465 		for (var i = 0; i < resp.length; i++) {
466 			var link = resp[i].link ? resp[i].link[0] : null;
467 			if (link) {
468 				var mtpt = appCtxt.getById(link.id);
469 				if (mtpt) {
470 					// update the mtpt perms with the updated link perms
471 					mtpt.perm = link.perm;
472                     if (link.n) mtpt.numTotal=link.n;
473                     if (link.u) mtpt.numUnread=link.u;
474 					mtpt._setSharesFromJs(link);
475 				}
476 
477 				if (link.folder && link.folder.length > 0) {
478 					var parent = appCtxt.getById(link.id);
479 					if (parent) {
480 						// TODO: only goes one level deep - should we recurse?
481 						for (var j = 0; j < link.folder.length; j++) {
482 							if (appCtxt.getById(link.folder[j].id)) { continue; }
483 							parent.notifyCreate(link.folder[j], "link", skipNotify);
484 						}
485 					}
486 				}
487 			}
488 		}
489 	}
490 
491 	if (callback) {
492 		callback.run();
493 	}
494 };
495 
496 /**
497  * Handles errors that come back from the GetShares batch request.
498  *
499  * @param {Array}	organizerTypes	the types of organizer (constants defined in {@link ZmOrganizer})
500  * @param {Object}	batchResp			the response
501  *
502  */
503 ZmFolderTree.prototype._handleErrorGetShares =
504 function(batchResp) {
505 	var faults = batchResp.Fault;
506 	if (faults) {
507 		var rids = [];
508 		var zids = [];
509 		for (var i = 0, length = faults.length; i < length; i++) {
510 			var ex = ZmCsfeCommand.faultToEx(faults[i]);
511 			if (ex.code == ZmCsfeException.MAIL_NO_SUCH_FOLDER) {
512 				var itemId = ex.data.itemId[0];
513 				var index = itemId.lastIndexOf(':');
514 				zids.push(itemId.substring(0, index));
515 				rids.push(itemId.substring(index + 1, itemId.length));
516 			}
517 		}
518 		if (zids.length) {
519 			this._markNoSuchFolder(zids, rids);
520 		}
521 	}
522 };
523 
524 /**
525  * Handles missing links by marking the organizers as not there
526  *
527  * @param {Array}	zids		the zids of the missing folders
528  * @param {Array}	rids		the rids of the missing folders. rids and zids must have the same length
529  *
530  */
531 ZmFolderTree.prototype._markNoSuchFolder =
532 function(zids, rids) {
533 	var treeData = appCtxt.getFolderTree();
534 	var items = treeData && treeData.root
535 		? treeData.root.children.getArray()
536 		: null;
537 
538 	for (var i = 0; i < items.length; i++) {
539 		for (var j = 0; j < rids.length; j++) {
540 			if ((items[i].zid == zids[j]) && (items[i].rid == rids[j])) {
541 				items[i].noSuchFolder = true;
542 			}
543 		}
544 	}
545 };
546 
547 /**
548  * @private
549  */
550 ZmFolderTree.prototype._sortFolder =
551 function(folder) {
552 	var children = folder.children;
553 	if (children && children.length) {
554 		children.sort(ZmFolder.sortCompare);
555 		for (var i = 0; i < children.length; i++)
556 			this._sortFolder(children[i]);
557 	}
558 };