1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 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) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a calendar tree controller.
 26  * @constructor
 27  * @class
 28  * This class manages the calendar tree controller.
 29  *
 30  * @author Parag Shah
 31  *
 32  * @extends		ZmTreeController
 33  */
 34 ZmCalendarTreeController = function() {
 35 
 36 	ZmTreeController.call(this, ZmOrganizer.CALENDAR);
 37 
 38 	this._listeners[ZmOperation.NEW_CALENDAR]			= this._newListener.bind(this);
 39 	this._listeners[ZmOperation.ADD_EXTERNAL_CALENDAR]	= this._addExternalCalendarListener.bind(this);
 40 	this._listeners[ZmOperation.CHECK_ALL]				= this._checkAllListener.bind(this);
 41 	this._listeners[ZmOperation.CLEAR_ALL]				= this._clearAllListener.bind(this);
 42 	this._listeners[ZmOperation.DETACH_WIN]				= this._detachListener.bind(this);
 43 	this._listeners[ZmOperation.SHARE_CALENDAR]			= this._shareCalListener.bind(this);
 44     this._listeners[ZmOperation.MOVE]					= this._moveListener.bind(this);
 45 	this._listeners[ZmOperation.RECOVER_DELETED_ITEMS]	= this._recoverListener.bind(this);
 46 
 47 	this._eventMgrs = {};
 48 };
 49 
 50 ZmCalendarTreeController.prototype = new ZmTreeController;
 51 ZmCalendarTreeController.prototype.constructor = ZmCalendarTreeController;
 52 
 53 ZmCalendarTreeController.prototype.isZmCalendarTreeController = true;
 54 ZmCalendarTreeController.prototype.toString = function() { return "ZmCalendarTreeController"; };
 55 
 56 ZmCalendarTreeController.prototype._initializeActionMenus = function() {
 57 	ZmTreeController.prototype._initializeActionMenus.call(this);
 58 
 59 	var ops = this._getRemoteActionMenuOps();
 60 	if (!this._remoteActionMenu && ops) {
 61 		var args = [this._shell, ops];
 62 		this._remoteActionMenu = new AjxCallback(this, this._createActionMenu, args);
 63 	}
 64 
 65 }
 66 
 67 ZmCalendarTreeController.prototype._treeListener =
 68 function(ev) {
 69 
 70     ZmTreeController.prototype._treeListener.call(this, ev);
 71 
 72 	if(ev.detail == DwtTree.ITEM_EXPANDED){
 73         var calItem = ev.item;
 74         var calendar = calItem.getData(Dwt.KEY_OBJECT);
 75         if(calendar && calendar.isRemote() && calendar.isMountpoint){
 76             this._fixupTreeNode(calItem, calendar, calItem._tree);
 77         }
 78 	}
 79 };
 80 
 81 // Public methods
 82 
 83 /**
 84  * Displays the tree of this type.
 85  *
 86  * @param {Hash}	params		a hash of parameters
 87  * @param	{constant}	params.overviewId		the overview ID
 88  * @param	{Boolean}	params.showUnread		if <code>true</code>, unread counts will be shown
 89  * @param	{Object}	params.omit				a hash of organizer IDs to ignore
 90  * @param	{Object}	params.include			a hash of organizer IDs to include
 91  * @param	{Boolean}	params.forceCreate		if <code>true</code>, tree view will be created
 92  * @param	{String}	params.app				the app that owns the overview
 93  * @param	{Boolean}	params.hideEmpty		if <code>true</code>, don't show header if there is no data
 94  * @param	{Boolean}	params.noTooltips	if <code>true</code>, don't show tooltips for tree items
 95  */
 96 ZmCalendarTreeController.prototype.show = function(params) {
 97 	params.include = params.include || {};
 98     params.include[ZmFolder.ID_TRASH] = true;
 99     params.showUnread = false;
100     return ZmFolderTreeController.prototype.show.call(this, params);
101 };
102 
103 /**
104  * Gets all calendars.
105  * 
106  * @param	{String}	overviewId		the overview id
107  * @param   {boolean}   includeTrash    True to include trash, if checked.
108  * @return	{Array}		an array of {@link ZmCalendar} objects
109  */
110 ZmCalendarTreeController.prototype.getCalendars =
111 function(overviewId, includeTrash) {
112 	var calendars = [];
113 	var items = this._getItems(overviewId);
114 	for (var i = 0; i < items.length; i++) {
115 		var item = items[i];
116 		if (item._isSeparator) { continue; }
117 	    var calendar = item.getData(Dwt.KEY_OBJECT);
118         if (calendar) {
119             if (calendar.id == ZmOrganizer.ID_TRASH && !includeTrash) continue;
120 			calendars.push(calendar);
121         }
122 	}
123 
124 	return calendars;
125 };
126 
127 /**
128  * Gets the owned calendars.
129  * 
130  * @param	{String}	overviewId		the overview id
131  * @param	{String}	owner		the owner
132  * @return	{Array}		an array of {@link ZmCalendar} objects
133  */
134 ZmCalendarTreeController.prototype.getOwnedCalendars =
135 function(overviewId, owner) {
136 	var calendars = [];
137 	var items = this._getItems(overviewId);
138 	for (var i = 0; i < items.length; i++) {
139 		var item = items[i];
140 		if (!item || item._isSeparator) { continue; }
141 		var calendar = item.getData(Dwt.KEY_OBJECT);
142 		if (calendar && calendar.getOwner() == owner) {
143 			calendars.push(calendar);
144 		}
145 	}
146 
147 	return calendars;
148 };
149 
150 ZmCalendarTreeController.prototype.addSelectionListener =
151 function(overviewId, listener) {
152 	// Each overview gets its own event manager
153 	if (!this._eventMgrs[overviewId]) {
154 		this._eventMgrs[overviewId] = new AjxEventMgr;
155 		// Each event manager has its own selection event to avoid multi-threaded
156 		// collisions
157 		this._eventMgrs[overviewId]._selEv = new DwtSelectionEvent(true);
158 	}
159 	this._eventMgrs[overviewId].addListener(DwtEvent.SELECTION, listener);
160 };
161 
162 ZmCalendarTreeController.prototype.removeSelectionListener =
163 function(overviewId, listener) {
164 	if (this._eventMgrs[overviewId]) {
165 		this._eventMgrs[overviewId].removeListener(DwtEvent.SELECTION, listener);
166 	}
167 };
168 
169 // Protected methods
170 
171 ZmCalendarTreeController.prototype.resetOperations = 
172 function(actionMenu, type, id) {
173 	if (actionMenu && !appCtxt.isWebClientOffline()) {
174 		var calendar = appCtxt.getById(id);
175 		var nId;
176 		if (calendar) {
177 			nId = calendar.nId;
178             var isShareVisible = (!calendar.link || calendar.isAdmin()) && nId != ZmFolder.ID_TRASH;
179             if (appCtxt.isOffline) {
180                 var acct = calendar.getAccount();
181                 isShareVisible = !acct.isMain && acct.isZimbraAccount;
182             }
183 			actionMenu.enable(ZmOperation.SHARE_CALENDAR, isShareVisible);
184 			actionMenu.enable(ZmOperation.SYNC, calendar.isFeed());
185 		} else {
186 			nId = ZmOrganizer.normalizeId(id);
187 		}
188 		var isTrash = (nId == ZmFolder.ID_TRASH);
189 		actionMenu.enable(ZmOperation.DELETE_WITHOUT_SHORTCUT, (nId != ZmOrganizer.ID_CALENDAR && nId != ZmOrganizer.ID_TRASH));
190 		this.setVisibleIfExists(actionMenu, ZmOperation.EMPTY_FOLDER, nId == ZmFolder.ID_TRASH);
191 		var hasContent = ((calendar.numTotal > 0) || (calendar.children && (calendar.children.size() > 0)));
192 		actionMenu.enable(ZmOperation.EMPTY_FOLDER,hasContent);
193 
194         var moveItem = actionMenu.getItemById(ZmOperation.KEY_ID,ZmOperation.MOVE);
195 
196 		var rootId = ZmOrganizer.getSystemId(ZmOrganizer.ID_ROOT);
197 		if (id == rootId) {
198 			var items = this._getItems(this._actionedOverviewId);
199 			var foundChecked = false;
200 			var foundUnchecked = false;
201 			for (var i = 0; i < items.length; i++) {
202 				var item = items[i];
203 				if (item._isSeparator) continue;
204 				item.getChecked() ? foundChecked = true : foundUnchecked = true;
205 			}
206 			actionMenu.enable(ZmOperation.CHECK_ALL, foundUnchecked);
207 			actionMenu.enable(ZmOperation.CLEAR_ALL, foundChecked);
208 		}
209 
210 		this._enableRecoverDeleted(actionMenu, isTrash);
211 
212 		// we always enable sharing in case we're in multi-mbox mode
213 		this._resetButtonPerSetting(actionMenu, ZmOperation.SHARE_CALENDAR, appCtxt.get(ZmSetting.SHARING_ENABLED));
214 		this._resetButtonPerSetting(actionMenu, ZmOperation.FREE_BUSY_LINK, appCtxt.getActiveAccount().isZimbraAccount);
215 
216         var fbLinkMenuItem = actionMenu.getMenuItem(ZmOperation.FREE_BUSY_LINK);
217         if (fbLinkMenuItem){
218             //setting up free busy link submenu
219             actionMenu._fbLinkSubMenu = actionMenu._fbLinkSubMenu || this._getFreeBusySubMenu(actionMenu, calendar.restUrl);
220 
221             fbLinkMenuItem.setMenu(actionMenu._fbLinkSubMenu);
222         }
223 
224         actionMenu.enable(ZmOperation.NEW_CALENDAR, !isTrash && !appCtxt.isExternalAccount() && !appCtxt.isWebClientOffline());
225 
226     }
227 };
228 
229 ZmCalendarTreeController.prototype._getFreeBusySubMenu =
230 function(actionMenu, restUrl){
231         var subMenuItems = [ZmOperation.SEND_FB_HTML,ZmOperation.SEND_FB_ICS,ZmOperation.SEND_FB_ICS_EVENT];
232         var params = {parent:actionMenu, menuItems:subMenuItems};
233 	    var subMenu = new ZmActionMenu(params);
234         for(var s=0;s<subMenuItems.length;s++){
235             subMenu.addSelectionListener(subMenuItems[s], this._freeBusyLinkListener.bind(this, subMenuItems[s], restUrl) );
236         }
237         return subMenu;
238 }
239 
240 ZmCalendarTreeController.prototype._detachListener =
241 function(ev){
242 	var folder = this._getActionedOrganizer(ev);
243     if (!folder){
244         return;
245     }
246     var acct = folder.getAccount();
247     var noRemote = true;  // noRemote is to achieve a restUrl that points to user's mailbox instead of the shared calendar owner's mailbox
248     var url = folder.getRestUrl(acct, noRemote);
249     if (url) {
250 		window.open(url+".html?tz=" + AjxTimezone.DEFAULT, "_blank");
251 	}
252 };
253 
254 ZmCalendarTreeController.prototype._freeBusyLinkListener =
255 function(op, restUrl, ev){
256 	var inNewWindow = false;
257 	var app = appCtxt.getApp(ZmApp.CALENDAR);
258 	if (app) {
259 		inNewWindow = app._inNewWindow(ev);
260 	}
261 	restUrl = restUrl || appCtxt.get(ZmSetting.REST_URL);
262 	if (restUrl) {
263 	   restUrl += op === ZmOperation.SEND_FB_ICS_EVENT ? "?fmt=ifb&fbfmt=event" : op === ZmOperation.SEND_FB_ICS ? "?fmt=ifb" : "?fmt=freebusy";
264 	}
265 	var params = {
266 		action: ZmOperation.NEW_MESSAGE, 
267 		inNewWindow: inNewWindow,
268 		msg: (new ZmMailMsg()),
269 		extraBodyText: restUrl
270 	};
271 	AjxDispatcher.run("Compose", params);
272 };
273 
274 ZmCalendarTreeController.prototype._recoverListener =
275 function(ev) {
276 	appCtxt.getDumpsterDialog().popup(this._getSearchFor(), this._getSearchTypes());
277 };
278 
279 ZmCalendarTreeController.prototype._getSearchFor =
280 function(ev) {
281 	return ZmItem.APPT;
282 };
283 
284 ZmCalendarTreeController.prototype._getSearchTypes =
285 function(ev) {
286 	return [ZmItem.APPT];
287 };
288 
289 // Returns a list of desired header action menu operations
290 ZmCalendarTreeController.prototype._getHeaderActionMenuOps =
291 function() {
292     var ops = [];
293     if (appCtxt.getCurrentApp().containsWritableFolder()) {
294         ops.push(ZmOperation.NEW_CALENDAR,
295                     ZmOperation.ADD_EXTERNAL_CALENDAR,
296                     ZmOperation.CHECK_ALL,
297                     ZmOperation.CLEAR_ALL,
298                     ZmOperation.SEP,
299                     ZmOperation.FREE_BUSY_LINK);
300     }
301     else {
302         ops.push(ZmOperation.CHECK_ALL,
303                 ZmOperation.CLEAR_ALL);
304     }
305 
306 	ops.push(ZmOperation.FIND_SHARES);
307 
308 	return ops;
309 };
310 
311 
312 // Returns a list of desired remote shared mailbox action menu operations
313 ZmCalendarTreeController.prototype._getRemoteActionMenuOps = function() {
314 	return [ZmOperation.NEW_CALENDAR,
315 			ZmOperation.ADD_EXTERNAL_CALENDAR,
316 			ZmOperation.FREE_BUSY_LINK];
317 };
318 
319 // Returns a list of desired action menu operations
320 ZmCalendarTreeController.prototype._getActionMenuOps = function() {
321 
322     if (appCtxt.getCurrentApp().containsWritableFolder()) {
323         return [
324             ZmOperation.NEW_CALENDAR,
325 	        ZmOperation.SYNC,
326 	        ZmOperation.EMPTY_FOLDER,
327 	        ZmOperation.RECOVER_DELETED_ITEMS,
328             ZmOperation.SHARE_CALENDAR,
329 	        ZmOperation.MOVE,
330             ZmOperation.DELETE_WITHOUT_SHORTCUT,
331             ZmOperation.EDIT_PROPS,
332             ZmOperation.DETACH_WIN
333         ];
334     }
335     else {
336         return [
337             ZmOperation.EDIT_PROPS,
338             ZmOperation.DETACH_WIN
339         ];
340     }
341 };
342 
343 ZmCalendarTreeController.prototype.getItemActionMenu = function(ev, item) {
344 	var actionMenu = null;
345 	if (item.isRemoteRoot()) {
346 		actionMenu = this._getRemoteActionMenu();
347 	} else {
348 		actionMenu = ZmTreeController.prototype.getItemActionMenu.apply(this, arguments);
349 	}
350 	return actionMenu;
351 }
352 
353 ZmCalendarTreeController.prototype._getRemoteActionMenu = function() {
354 	if (this._remoteActionMenu instanceof AjxCallback) {
355 		var callback = this._remoteActionMenu;
356 		this._remoteActionMenu = callback.run();
357 	}
358 	return this._remoteActionMenu;
359 };
360 
361 ZmCalendarTreeController.prototype._getActionMenu =
362 function(ev) {
363 	var organizer = ev.item.getData(Dwt.KEY_OBJECT);
364 	if (organizer.type != this.type &&
365         organizer.nId != ZmOrganizer.ID_TRASH) {
366         return null;
367     }
368 	var menu = ZmTreeController.prototype._getActionMenu.apply(this, arguments);
369     if (appCtxt.isWebClientOffline())  {
370         menu.enableAll(false);
371     } else {
372         var isTrash = organizer.nId == ZmOrganizer.ID_TRASH;
373         //bug 67531: "Move" Option should be disabled for the default calendar
374         var isCalendar = organizer.nId == ZmOrganizer.ID_CALENDAR;
375         menu.enableAll(!isTrash);
376         menu.enable(ZmOperation.MOVE, !isCalendar && !isTrash);
377         menu.enable(ZmOperation.EMPTY_FOLDER, isTrash);
378         var menuItem = menu.getMenuItem(ZmOperation.EMPTY_FOLDER);
379         if (menuItem) {
380             menuItem.setText(isTrash ? ZmMsg.emptyTrash : ZmMsg.emptyFolder);
381         }
382     }
383     return menu;
384 };
385 
386 // Method that is run when a tree item is left-clicked
387 ZmCalendarTreeController.prototype._itemClicked =
388 function(organizer) {
389 	if ((organizer.type != ZmOrganizer.CALENDAR) && !organizer.isRemoteRoot()) {
390         if (organizer._showFoldersCallback) {
391             organizer._showFoldersCallback.run();
392             return;
393         }
394 
395         if (organizer.nId == ZmOrganizer.ID_TRASH) {
396 			return;
397 		}
398 
399 		var appId = ZmOrganizer.APP[organizer.type];
400 		var app = appId && appCtxt.getApp(appId);
401 		if (app) {
402 			var callback = new AjxCallback(this, this._postActivateApp, [organizer, app]);
403 			appCtxt.getAppController().activateApp(appId, null, callback);
404 		}
405 		else {
406 			appCtxt.setStatusMsg({
407 				msg:	AjxMessageFormat.format(ZmMsg.appUnknown, [appId]),
408 				level:	ZmStatusView.LEVEL_WARNING
409 			});
410 		}
411 	}
412 };
413 
414 ZmCalendarTreeController.prototype._postActivateApp =
415 function(organizer, app) {
416 	var controller = appCtxt.getOverviewController();
417 	var overviewId = app.getOverviewId();
418 	var treeId = organizer.type;
419 	var treeView = controller.getTreeView(overviewId, treeId);
420 	if (treeView) {
421 		treeView.setSelected(organizer);
422 	}
423 };
424 
425 // Handles a drop event
426 ZmCalendarTreeController.prototype._dropListener =
427 function(ev) {
428 	var data = ev.srcData.data;
429     var dropFolder = ev.targetControl.getData(Dwt.KEY_OBJECT);
430 
431 	var appts = (!(data instanceof Array)) ? [data] : data;
432 	var isShiftKey = (ev.shiftKey || ev.uiEvent.shiftKey);
433 
434 	if (ev.action == DwtDropEvent.DRAG_ENTER) {
435 
436         var type = ev.targetControl.getData(ZmTreeView.KEY_TYPE);
437 
438         if(data instanceof ZmCalendar){
439              ev.doIt = dropFolder.mayContain(data, type) && !data.isSystem();
440         }
441 		else if (!(appts[0] instanceof ZmAppt)) {
442 			ev.doIt = false;
443 		}
444 		else if (this._dropTgt.isValidTarget(data)) {
445 			var type = ev.targetControl.getData(ZmTreeView.KEY_TYPE);
446 			ev.doIt = dropFolder.mayContain(data, type);
447 
448 			var action;
449 			// walk thru the array and find out what action is allowed
450 			for (var i = 0; i < appts.length; i++) {
451 				if (appts[i] instanceof ZmItem) {
452 					action |= appts[i].getDefaultDndAction(isShiftKey);
453 				}
454 			}
455 
456 			if (((action & ZmItem.DND_ACTION_COPY) != 0)) {
457 				var plusDiv = (appts.length == 1)
458 					? ev.dndProxy.firstChild.nextSibling
459 					: ev.dndProxy.firstChild.nextSibling.nextSibling;
460 
461 				Dwt.setVisibility(plusDiv, true);
462 			}
463 		}
464 	}
465 	else if (ev.action == DwtDropEvent.DRAG_DROP) {
466 		var ctlr = ev.srcData.controller;
467 		var cc = AjxDispatcher.run("GetCalController");
468         if (!isShiftKey && cc.isMovingBetwAccounts(appts, dropFolder.id)) {
469             var dlg = appCtxt.getMsgDialog();
470             dlg.setMessage(ZmMsg.orgChange, DwtMessageDialog.WARNING_STYLE);
471             dlg.popup();
472 		} else {
473             if (data instanceof ZmCalendar) {
474                 this._doMove(data, dropFolder);
475             } else {
476                 ctlr._doMove(appts, dropFolder, null, isShiftKey);
477             }
478 		}
479 	}
480 };
481 
482 ZmCalendarTreeController.prototype._dropToRemoteFolder =
483 function(name) {
484     appCtxt.setStatusMsg(AjxMessageFormat.format(ZmMsg.calStatusUpdate, name));
485 }
486 
487 ZmCalendarTreeController.prototype._changeOrgCallback =
488 function(controller, dialog, appts, dropFolder) {
489 	dialog.popdown();
490     if(!dropFolder.noSuchFolder){
491 	    controller._doMove(appts, dropFolder, null, false);
492     }
493     else{
494         var dialog = appCtxt.getMsgDialog();
495         var msg = AjxMessageFormat.format(ZmMsg.noFolderExists, dropFolder.name);
496         dialog.setMessage(msg);
497         dialog.popup();
498     }
499 };
500 
501 /*
502 * Returns a "New Calendar" dialog.
503 */
504 ZmCalendarTreeController.prototype._getNewDialog =
505 function() {
506     return appCtxt.getNewCalendarDialog();
507 };
508 
509 ZmCalendarTreeController.prototype._newCallback =
510 function(params) {
511     // For a calendar, set the parent folder (params.l) if specified
512     var folder = this._pendingActionData instanceof ZmOrganizer ? this._pendingActionData :
513         (this._pendingActionData && this._pendingActionData.organizer);
514     if (folder) {
515         params.l = folder.id;
516     }
517     ZmTreeController.prototype._newCallback.call(this, params);
518 };
519 
520 
521 
522 /*
523 * Returns an "External Calendar" dialog.
524 */
525 ZmCalendarTreeController.prototype.getExternalCalendarDialog =
526 function() {
527     if(!this._externalCalendarDialog) {
528         AjxDispatcher.require(["MailCore", "CalendarCore", "Calendar", "CalendarAppt"]);
529 	    this._externalCalendarDialog = new ZmExternalCalendarDialog({parent: this._shell, controller: this});
530     }
531     return this._externalCalendarDialog;
532 };
533 
534 // Listener callbacks
535 
536 /*
537 * Listener to handle new external calendar.
538 */
539 ZmCalendarTreeController.prototype._addExternalCalendarListener =
540 function() {
541 	var dialog = this.getExternalCalendarDialog();
542     dialog.popup();
543 };
544 
545 
546 ZmCalendarTreeController.prototype._changeListener =
547 function(ev, treeView, overviewId) {
548 	ZmTreeController.prototype._changeListener.call(this, ev, treeView, overviewId);
549 
550 	if (ev.type != this.type || ev.handled) { return; }
551 
552 	var fields = ev.getDetail("fields") || {};
553 	if (ev.event == ZmEvent.E_CREATE ||
554 		ev.event == ZmEvent.E_DELETE ||
555 		(ev.event == ZmEvent.E_MODIFY && fields[ZmOrganizer.F_FLAGS]))
556 	{
557 		var aCtxt = appCtxt.isChildWindow ? parentAppCtxt : appCtxt;
558 		var controller = aCtxt.getApp(ZmApp.CALENDAR).getCalController();
559 		controller._updateCheckedCalendars();
560 
561 		// if calendar is deleted, notify will initiate the refresh action
562 		if (ev.event != ZmEvent.E_DELETE) {
563             var calIds = controller.getCheckedCalendarFolderIds();
564             AjxDebug.println(AjxDebug.CALENDAR, "tree change listener refreshing calendar event '" + ev.event + "' with checked folder ids " + calIds.join(","));
565 			controller._refreshAction(true);
566 			ev.handled = true;
567 		}
568     }
569 };
570 
571 ZmCalendarTreeController.prototype._treeViewListener =
572 function(ev) {
573 	// handle item(s) clicked
574 	if (ev.detail == DwtTree.ITEM_CHECKED) {
575 		var overviewId = ev.item.getData(ZmTreeView.KEY_ID);
576 		var calendar = ev.item.getData(Dwt.KEY_OBJECT);
577 
578 		//checkbox event may not be propagated to close action menu
579 		if (this._getActionMenu(ev)) {
580 			this._getActionMenu(ev).popdown();
581 		}
582 
583 		// notify listeners of selection
584 		if (this._eventMgrs[overviewId]) {
585 			this._eventMgrs[overviewId].notifyListeners(DwtEvent.SELECTION, ev);
586 		}
587 		return;
588 	}
589 
590 	// default processing
591 	ZmTreeController.prototype._treeViewListener.call(this, ev);
592 };
593 
594 ZmCalendarTreeController.prototype._checkAllListener =
595 function(ev) {
596 	this._setAllChecked(ev, true);
597 };
598 
599 ZmCalendarTreeController.prototype._clearAllListener =
600 function(ev) {
601 	this._setAllChecked(ev, false);
602 };
603 
604 ZmCalendarTreeController.prototype._shareCalListener =
605 function(ev) {
606 	this._pendingActionData = this._getActionedOrganizer(ev);
607 	appCtxt.getSharePropsDialog().popup(ZmSharePropsDialog.NEW, this._pendingActionData);
608 };
609 
610 ZmCalendarTreeController.prototype._deleteListener =
611 function(ev) {
612 	var organizer = this._getActionedOrganizer(ev);
613     if (organizer.isInTrash()) {
614         var callback = new AjxCallback(this, this._deleteListener2, [organizer]);
615         var message = AjxMessageFormat.format(ZmMsg.confirmDeleteCalendar, AjxStringUtil.htmlEncode(organizer.name));
616 
617         appCtxt.getConfirmationDialog().popup(message, callback);
618     }
619     else {
620         this._doMove(organizer, appCtxt.getById(ZmFolder.ID_TRASH));
621     }
622 };
623 
624 ZmCalendarTreeController.prototype._deleteListener2 =
625 function(organizer) {
626 	this._doDelete(organizer);
627 };
628 
629 /**
630  * Empties a folder.
631  * It removes all the items in the folder except sub-folders.
632  * If the folder is Trash, it empties even the sub-folders.
633  * A warning dialog will be shown before any folder is emptied.
634  *
635  * @param {DwtUiEvent}		ev		the UI event
636  *
637  * @private
638  */
639 ZmCalendarTreeController.prototype._emptyListener =
640 function(ev) {
641 	this._getEmptyShieldWarning(ev);
642 };
643 
644 ZmCalendarTreeController.prototype._notifyListeners =
645 function(overviewId, type, items, detail, srcEv, destEv) {
646 	if (this._eventMgrs[overviewId] &&
647 		this._eventMgrs[overviewId].isListenerRegistered(type))
648 	{
649 		if (srcEv) DwtUiEvent.copy(destEv, srcEv);
650 		destEv.items = items;
651 		if (items.length == 1) destEv.item = items[0];
652 		destEv.detail = detail;
653 		this._eventMgrs[overviewId].notifyListeners(type, destEv);
654 	}
655 };
656 
657 ZmCalendarTreeController.prototype._getItems =
658 function(overviewId) {
659 	var treeView = this.getTreeView(overviewId);
660 	if (treeView) {
661 		var account = appCtxt.multiAccounts ? treeView._overview.account : null;
662 		if (!appCtxt.get(ZmSetting.CALENDAR_ENABLED, null, account)) { return []; }
663 
664 		var rootId = ZmOrganizer.getSystemId(ZmOrganizer.ID_ROOT, account);
665 		var root = treeView.getTreeItemById(rootId);
666 		if (root) {
667 			var totalItems = [];
668 			this._getSubItems(root, totalItems);
669 			return totalItems;
670 		}
671 	}
672 	return [];
673 };
674 
675 ZmCalendarTreeController.prototype._getSubItems =
676 function(root, totalItems) {
677 	if (!root || (root && root._isSeparator)) { return; }
678 
679 	var items = root.getItems();
680     //items is an array
681 	for (var i = 0; i < items.length; i++) {
682 		var item = items[i];
683 		if (item && !item._isSeparator) {
684 			totalItems.push(item);
685 			this._getSubItems(item, totalItems);
686 		}
687 	}
688 };
689 
690 ZmCalendarTreeController.prototype._setAllChecked =
691 function(ev, checked) {
692 	var overviewId = this._actionedOverviewId;
693 	var items = this._getItems(overviewId);
694 	var checkedItems = [];
695 	var item, organizer;
696 	for (var i = 0;  i < items.length; i++) {
697 		item = items[i];
698 		if (item._isSeparator) { continue; }
699 		organizer = item.getData(Dwt.KEY_OBJECT);
700 		if (!organizer || organizer.type != ZmOrganizer.CALENDAR) { continue; }
701 		item.setChecked(checked);
702 		checkedItems.push(item);
703 	}
704 
705 	// notify listeners of selection
706 	if (checkedItems.length && this._eventMgrs[overviewId]) {
707 		this._notifyListeners(overviewId, DwtEvent.SELECTION, checkedItems, DwtTree.ITEM_CHECKED, ev, this._eventMgrs[overviewId]._selEv);
708 	}
709 };
710 
711 ZmCalendarTreeController.prototype._createTreeView =
712 function(params) {
713 	if (params.treeStyle == null) {
714 		params.treeStyle = DwtTree.CHECKEDITEM_STYLE;
715 	}
716     params.showUnread = false;
717 	return new ZmTreeView(params);
718 };
719 
720 ZmCalendarTreeController.prototype._postSetup =
721 function(overviewId, account) {
722 	ZmTreeController.prototype._postSetup.apply(this, arguments);
723 
724 	// bug: 43067 - remove the default calendar since its only a place holder
725 	// for caldav based accounts
726 	if (account && account.isCalDavBased()) {
727 		var treeView = this.getTreeView(overviewId);
728 		var calendarId = ZmOrganizer.getSystemId(ZmOrganizer.ID_CALENDAR, account);
729 		var treeItem = treeView.getTreeItemById(calendarId);
730 		treeItem.dispose();
731 	}
732 };
733 
734 /**
735  * Pops up the appropriate "New ..." dialog.
736  *
737  * @param {DwtUiEvent}	ev		the UI event
738  * @param {ZmZimbraAccount}	account	used by multi-account mailbox (optional)
739  *
740  * @private
741  */
742 ZmCalendarTreeController.prototype._newListener =
743 function(ev, account, isExternalCalendar) {
744 	this._pendingActionData = this._getActionedOrganizer(ev);
745 	var newDialog = this._getNewDialog();
746 
747     // Fix for Bug: 85158 and regression due to Bug: 82811
748     // Pass a flag isExternalCalendar from ZmExternalCalendarDialog::_nextButtonListener to help decide creating external calendar or local calendar
749     if (isExternalCalendar && this._extCalData) {
750         var iCalData = this._extCalData.iCal;
751         newDialog.setICalData(iCalData);
752         newDialog.setTitle(ZmMsg.addExternalCalendar);
753         newDialog.getButton(ZmNewCalendarDialog.BACK_BUTTON).setVisibility(true);
754     }
755     else {
756         newDialog.setTitle(ZmMsg.createNewCalendar);
757         newDialog.getButton(ZmNewCalendarDialog.BACK_BUTTON).setVisibility(false);
758     }
759 	if (!this._newCb) {
760 		this._newCb = new AjxCallback(this, this._newCallback);
761 	}
762 	if (this._pendingActionData && !appCtxt.getById(this._pendingActionData.id)) {
763 		this._pendingActionData = appCtxt.getFolderTree(account).root;
764 	}
765 
766 	if (!account && appCtxt.multiAccounts) {
767 		var ov = this._opc.getOverview(this._actionedOverviewId);
768 		account = ov && ov.account;
769 	}
770 
771 	ZmController.showDialog(newDialog, this._newCb, this._pendingActionData, account);
772 
773 	newDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, newDialog);
774 };
775 
776 ZmCalendarTreeController.prototype.setExternalCalendarData =
777 function(extCalData) {
778     this._extCalData = extCalData;
779 };
780 
781 ZmCalendarTreeController.prototype._clearDialog =
782 function(dialog) {
783     ZmTreeController.prototype._clearDialog.apply(this, arguments);
784     if(this._externalCalendarDialog) {
785         this._externalCalendarDialog.popdown();
786     }
787 };
788 
789 ZmCalendarTreeController.prototype.createDataSourceErrorCallback =
790 function(response) {
791     appCtxt.setStatusMsg(ZmMsg.addExternalCalendarError);
792 };
793 
794 ZmCalendarTreeController.prototype.createDataSourceCallback =
795 function(response) {
796     var dsResponse = response.getResponse(),
797         sourceId =  dsResponse && dsResponse.caldav ? dsResponse.caldav[0].id : "",
798         jsonObj,
799         params;
800     if(sourceId) {
801         jsonObj = {
802             ImportDataRequest : {
803                 _jsns : "urn:zimbraMail",
804                 caldav : {
805                     id : sourceId
806                 }
807             }
808         };
809         params = {
810               soapDoc: jsonObj,
811               asyncMode: false
812             };
813         appCtxt.getAppController().sendRequest(params);
814     }
815 
816     appCtxt.setStatusMsg(ZmMsg.addExternalCalendarSuccess);
817     return response;
818 };
819 
820 ZmCalendarTreeController.POLLING_INTERVAL = "1m";
821 ZmCalendarTreeController.CONN_TYPE_CLEARTEXT = "cleartext";
822 ZmCalendarTreeController.CONN_TYPE_SSL = "ssl";
823 ZmCalendarTreeController.SSL_PORT = "443";
824 ZmCalendarTreeController.GOOGLE_CALDAV_SERVER = "www.google.com";
825 ZmCalendarTreeController.ALT_GOOGLE_CALDAV_SERVER = "apidata.googleusercontent.com";
826 ZmCalendarTreeController.DATA_SOURCE_ATTR_YAHOO = "p:/principals/users/_USERNAME_";
827 ZmCalendarTreeController.DATA_SOURCE_ATTR = "p:/calendar/dav/_USERNAME_/user";
828 
829 ZmCalendarTreeController.prototype.createDataSource =
830 function(organizer, errorCallback) {
831     var calDav = this._extCalData && this._extCalData.calDav ? this._extCalData.calDav : null;
832     if(!calDav) { return; }
833 
834     var url,
835         port,
836         urlComponents,
837         hostUrl,
838         jsonObj,
839         connType = ZmCalendarTreeController.CONN_TYPE_CLEARTEXT,
840         dsa = ZmCalendarTreeController.DATA_SOURCE_ATTR;
841 
842 
843     hostUrl = calDav.hostUrl;
844     urlComponents = AjxStringUtil.parseURL(hostUrl);
845 	url = urlComponents.domain;
846 	port = urlComponents.port || ZmCalendarTreeController.SSL_PORT;    	
847 	dsa = urlComponents.path ? "p:" + urlComponents.path : ZmCalendarTreeController.DATA_SOURCE_ATTR;
848     
849 
850     if(port == ZmCalendarTreeController.SSL_PORT) {
851         connType = ZmCalendarTreeController.CONN_TYPE_SSL;
852     }
853 
854     if (calDav.hostUrl.indexOf(ZmCalendarTreeController.GOOGLE_CALDAV_SERVER) === -1 
855     	&& calDav.hostUrl.indexOf(ZmCalendarTreeController.ALT_GOOGLE_CALDAV_SERVER) === -1) { // Not google url
856         dsa = ZmCalendarTreeController.DATA_SOURCE_ATTR_YAHOO;
857     }
858 
859     jsonObj = {
860         CreateDataSourceRequest : {
861             _jsns : "urn:zimbraMail",
862             caldav : {
863                 name : organizer.name,
864                 pollingInterval : ZmCalendarTreeController.POLLING_INTERVAL,
865                 isEnabled : "1",
866                 l : organizer.nId,
867                 host : url,
868                 port : port,
869                 connectionType : connType,
870                 username : calDav.userName,
871                 password : calDav.password,
872                 a : {
873                     n : "zimbraDataSourceAttribute",
874                     _content : dsa
875                 }
876             }
877         }
878     };
879 
880     this._extCalData = null;
881     delete this._extCalData;
882     var accountName = (appCtxt.multiAccounts ? appCtxt.accountList.mainAccount.name : null);
883 
884     var params = {
885             jsonObj: jsonObj,
886             asyncMode: true,
887             sensitive: true,
888             callback: new AjxCallback(this, this.createDataSourceCallback),
889             errorCallback: new AjxCallback(this, this.createDataSourceErrorCallback),
890             accountName: accountName
891         };
892     appCtxt.getAppController().sendRequest(params);
893 };
894