1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 ZmCalMgr = function(container) {
 25 	this._container = container;
 26 	this.clearCache();
 27 	
 28 	this._listeners = {};
 29 	this._folderNames = {};
 30 
 31 	this._listeners[ZmOperation.NEW_APPT] = new AjxListener(this, this._newApptAction);
 32 	this._listeners[ZmOperation.NEW_ALLDAY_APPT] = new AjxListener(this, this._newAllDayApptAction);
 33 	this._listeners[ZmOperation.SEARCH_MAIL] = new AjxListener(this, this._searchMailAction);
 34 };
 35 
 36 ZmCalMgr.prototype.toString =
 37 function() {
 38 	return "ZmCalMgr";
 39 };
 40 
 41 ZmCalMgr.prototype.clearCache =
 42 function() {
 43 	this._miniCalData = {};
 44 };
 45 
 46 ZmCalMgr.prototype._createMiniCalendar =
 47 function(date) {
 48 	date = date ? date : new Date();
 49 
 50 	var firstDayOfWeek = appCtxt.get(ZmSetting.CAL_FIRST_DAY_OF_WEEK) || 0;
 51 
 52     //todo: need to use server setting to decide the weekno standard
 53     var serverId = AjxTimezone.getServerId(AjxTimezone.DEFAULT);
 54     var useISO8601WeekNo = (serverId && serverId.indexOf("Europe")==0 && serverId != "Europe/London");
 55 
 56 	this._miniCalendar = new DwtCalendar({parent: this._container, posStyle:DwtControl.ABSOLUTE_STYLE,
 57 										  firstDayOfWeek: firstDayOfWeek, showWeekNumber: appCtxt.get(ZmSetting.CAL_SHOW_CALENDAR_WEEK), useISO8601WeekNo: useISO8601WeekNo});
 58 	this._miniCalendar.setDate(date);
 59 	this._miniCalendar.setScrollStyle(Dwt.CLIP);
 60 	this._miniCalendar.addSelectionListener(new AjxListener(this, this._miniCalSelectionListener));
 61 	this._miniCalendar.addActionListener(new AjxListener(this, this._miniCalActionListener));
 62 	this._miniCalendar.addDateRangeListener(new AjxListener(this, this._miniCalDateRangeListener));
 63 	this._miniCalendar.setMouseOverDayCallback(new AjxCallback(this, this._miniCalMouseOverDayCallback));
 64 	this._miniCalendar.setMouseOutDayCallback(new AjxCallback(this, this._miniCalMouseOutDayCallback));
 65 
 66 	var list = [];
 67 	if (appCtxt.get(ZmSetting.MAIL_ENABLED)) {
 68 		list.push("ZmMailMsg");
 69 		list.push("ZmConv");
 70 	}
 71 	if (appCtxt.get(ZmSetting.CONTACTS_ENABLED)) {
 72 		list.push("ZmContact");
 73 	}
 74 	this._miniCalDropTarget = new DwtDropTarget(list);
 75 	this._miniCalDropTarget.addDropListener(new AjxListener(this, this._miniCalDropTargetListener));
 76 	this._miniCalendar.setDropTarget(this._miniCalDropTarget);
 77 
 78 	var workingWeek = [];
 79 	for (var i = 0; i < 7; i++) {
 80 		var d = (i + firstDayOfWeek) % 7;
 81 		workingWeek[i] = (d > 0 && d < 6);
 82 	}
 83 	this._miniCalendar.setWorkingWeek(workingWeek);
 84 
 85 	// add mini-calendar to skin
 86 	var components = {};
 87 	components[ZmAppViewMgr.C_TREE_FOOTER] = this._miniCalendar;
 88 	appCtxt.getAppViewMgr().setViewComponents(ZmAppViewMgr.GLOBAL, components, true);
 89 	
 90 	var app = appCtxt.getApp(ZmApp.CALENDAR);
 91 	var show = app._active || appCtxt.get(ZmSetting.CAL_ALWAYS_SHOW_MINI_CAL);
 92 	this._miniCalendar.setSkipNotifyOnPage(show && !app._active);
 93 	if (!app._active) {
 94 		this._miniCalendar.setSelectionMode(DwtCalendar.DAY);
 95 	}
 96 
 97     this._dayRollTimer = null;
 98 };
 99 
100 
101 
102 ZmCalMgr.prototype.startDayRollTimer =
103 function(){
104   if(!this._dayRollTimer){
105     var curTime = new Date();
106     var rollTime = new Date();
107     rollTime.setHours(23,59,59,999);
108     var interval = rollTime.getTime() - curTime.getTime();
109     var dayRollAction = new AjxTimedAction(this,this._rollDay);
110     AjxTimedAction.scheduleAction(dayRollAction,interval);
111   }
112 }
113 
114 
115 ZmCalMgr.prototype._rollDay =
116 function(){
117     this._dayRollTimer = null;
118     this._miniCalendar.setDate(new Date(),true);
119     this.startDayRollTimer();
120 }
121 
122 ZmCalMgr.prototype._miniCalDropTargetListener =
123 function(ev) {
124 	var calController = this.getCalViewController();
125 	calController._miniCalDropTargetListener(ev);
126 };
127 
128 ZmCalMgr.prototype.getMiniCalendar = 
129 function() {
130 	if (!this._miniCalendar) {
131 		this._createMiniCalendar();
132 	}
133 	
134 	return this._miniCalendar;
135 };
136 
137 ZmCalMgr.prototype._refreshCallback =
138 function(list) {
139 	this.getReminderController()._refreshCallback(list);
140 };
141 
142 ZmCalMgr.prototype.getReminderController =
143 function() {
144 	if (!this._reminderController) {
145 		this._reminderController = new ZmReminderController(this, "appt");
146 	}
147 	return this._reminderController;
148 };
149 
150 ZmCalMgr.prototype._miniCalSelectionListener =
151 function(ev) {
152 	if (ev.item instanceof DwtCalendar) {
153 		var calController = this.getCalViewController();
154 		calController._handleLoadMiniCalSelection(ev);
155 	}
156 };
157 
158 ZmCalMgr.prototype._miniCalActionListener =
159 function(ev) {
160     if (appCtxt.isExternalAccount()) { return; }
161 	var mm = this._getMiniCalActionMenu();
162 	mm.__detail = ev.detail;
163 	mm.popup(0, ev.docX, ev.docY);
164 };
165 
166 ZmCalMgr.prototype._getMiniCalActionMenu =
167 function() {
168 	if (this._minicalMenu == null) {
169 
170 		this.postInitListeners();
171 
172 		var list = [ZmOperation.NEW_APPT, ZmOperation.NEW_ALLDAY_APPT, ZmOperation.SEP, ZmOperation.SEARCH_MAIL];
173 		//Zimlet hack
174 		var zimletOps = ZmZimlet.actionMenus ? ZmZimlet.actionMenus["ZmCalViewController"] : null;
175 		if (zimletOps && zimletOps.length) {
176 			for (var i = 0; i < zimletOps.length; i++) {
177 				var op = zimletOps[i];
178 				ZmOperation.defineOperation(null, op);
179 				list.push(op.id);
180 			}
181 		}
182 		var params = {parent: appCtxt.getShell(), menuItems:list};
183 		this._minicalMenu = new ZmActionMenu(params);
184 		list = this._minicalMenu.opList;
185 		var cnt = list.length;
186 		for(var ix=0; ix < cnt; ix++) {
187 			if(this._listeners[list[ix]]) {
188 				this._minicalMenu.addSelectionListener(list[ix], this._listeners[list[ix]]);
189 			}
190 		}
191 	}
192 	return this._minicalMenu;
193 };
194 
195 // Zimlet hack
196 ZmCalMgr.prototype.postInitListeners =
197 function () {
198 	if (ZmZimlet.listeners && ZmZimlet.listeners["ZmCalViewController"]) {
199 		for (var ix in ZmZimlet.listeners["ZmCalViewController"]) {
200 			if (ZmZimlet.listeners["ZmCalViewController"][ix] instanceof AjxListener) {
201 				this._listeners[ix] = ZmZimlet.listeners["ZmCalViewController"][ix];
202 			} else {
203 				this._listeners[ix] = new AjxListener(this, this._proxyListeners, [ZmZimlet.listeners["ZmCalViewController"][ix]]);
204 			}
205 		}
206 	}
207 };
208 
209 // Few zimlets might expect listeners from calendar view controller object
210 ZmCalMgr.prototype._proxyListeners =
211 function(zimletListener, event) {
212 	var calController = this.getCalViewController();
213 	return (new AjxListener(calController, zimletListener)).handleEvent(event);
214 };
215 
216 ZmCalMgr.prototype.isMiniCalCreated =
217 function() {
218 	return (this._miniCalendar != null);
219 };
220 
221 ZmCalMgr.prototype._miniCalDateRangeListener =
222 function(ev) { 
223 	var viewId = appCtxt.getCurrentViewId();
224 	if (viewId == ZmId.VIEW_CAL) {
225 		var calController = this.getCalViewController();
226 		calController._scheduleMaintenance(ZmCalViewController.MAINT_MINICAL);
227 	} else {
228 		this.highlightMiniCal();
229 	}
230 };
231 
232 ZmCalMgr.prototype._miniCalMouseOverDayCallback =
233 function(control, day) {
234 	this._currentMouseOverDay = day;
235 	var action = new AjxTimedAction(this, this._getDayToolTipOnDelay, [control, day]);
236 	AjxTimedAction.scheduleAction(action, 1000);
237 };
238 
239 ZmCalMgr.prototype.getCalViewController = 
240 function() {
241 	var calController = AjxDispatcher.run("GetCalController");
242 	calController._miniCalendar = this._miniCalendar;
243 	calController._minicalMenu = this._minicalMenu;
244 	calController._miniCalDropTarget = this._miniCalDropTarget;
245 	return calController;
246 };
247 
248 ZmCalMgr.prototype._miniCalMouseOutDayCallback =
249 function(control) {
250 	this._currentMouseOverDay = null;
251 };
252 
253 ZmCalMgr.prototype._getDayToolTipOnDelay =
254 function(control, day) {
255 	if (!this._currentMouseOverDay) { return; }
256 	if ((this._currentMouseOverDay.getDate() == day.getDate()) &&
257 		(this._currentMouseOverDay.getMonth() == day.getMonth()))
258 	{
259 		this._currentMouseOverDay = null;
260         var mouseEv = DwtShell.mouseEvent;
261         if(mouseEv && mouseEv.docX > 0 && mouseEv.docY > 0) {
262             var callback = new AjxCallback(this, this.showTooltip, [control, mouseEv.docX, mouseEv.docY]);
263             this.getCalViewController().getDayToolTipText(day, false, callback, true);
264         }
265 	}
266 };
267 
268 ZmCalMgr.prototype.showTooltip =
269 function(control, x, y, tooltipContent) {
270     control.setToolTipContent(tooltipContent);
271     if(x > 0 && y > 0) {
272         var shell = DwtShell.getShell(window);
273         var tooltip = shell.getToolTip();
274         tooltip.setContent(tooltipContent);
275         tooltip.popup(x, y);
276         control.__tooltipClosed = false;
277     }
278 };
279 
280 ZmCalMgr.prototype.getApptSummaries =
281 function(params) {
282 	var apptVec = this.setSearchParams(params);
283 
284 	if (apptVec != null && (apptVec instanceof AjxVector)) {
285 		return apptVec;
286 	}
287 
288 	// this array will hold a list of appts as we collect them from the server
289 	this._rawAppts = [];
290 
291 	if (params.callback) {
292 		this._search(params);
293 	} else {
294 		return this._search(params);
295 	}
296 };
297 
298 ZmCalMgr.prototype.setSearchParams =
299 function(params) {
300 	if (!(params.folderIds instanceof Array)) {
301 		params.folderIds = [params.folderIds];
302 	} else if (params.folderIds.length == 0) {
303 		var newVec = new AjxVector();
304 		if (params.callback) {
305 			params.callback.run(newVec);
306 		}
307 		return newVec;
308 	}
309 
310 	var folderIdMapper = {};
311 	var query = "";
312 	for (var i=0; i < params.folderIds.length; i++) {
313 		var fid = params.folderIds[i];
314 		var systemFolderId = appCtxt.getActiveAccount().isMain
315 			? fid : ZmOrganizer.getSystemId(fid);
316 
317 		// map remote folder ids into local ones while processing search since
318 		// server wont do it for us (see bug 7083)
319 		var folder = appCtxt.getById(systemFolderId);
320 		var rid = folder ? folder.getRemoteId() : systemFolderId;
321 		folderIdMapper[rid] = systemFolderId;
322 
323 		if (query.length) {
324 			query += " OR ";
325 		}
326 		var idText = AjxUtil.isNumeric(fid) ? fid : ['"', fid, '"'].join("");
327 		query += "inid:" + idText;
328 		
329 	}
330 	params.queryHint = query;
331     params.needToFetch = params.folderIds;
332 	params.folderIdMapper = folderIdMapper;
333 	params.offset = 0;
334 };
335 
336 ZmCalMgr.prototype._search =
337 function(params) {
338 	var jsonObj = {SearchRequest:{_jsns:"urn:zimbraMail"}};
339 	var request = jsonObj.SearchRequest;
340 
341 	this._setSoapParams(request, params);
342 
343     var calController = this.getCalViewController();
344     var apptCache     = calController.getApptCache();
345 
346     var accountName = appCtxt.multiAccounts ? appCtxt.accountList.mainAccount.name : null;
347 	if (params.callback) {
348 		appCtxt.getAppController().sendRequest({
349 			jsonObj: jsonObj,
350 			asyncMode: true,
351 			callback: (new AjxCallback(this, this._getApptSummariesResponse, [params])),
352             offlineCallback: apptCache.offlineSearchAppts(null, null, params),
353 			noBusyOverlay: params.noBusyOverlay,
354 			accountName: accountName
355 		});
356 	} else {
357 		var response = appCtxt.getAppController().sendRequest({jsonObj: jsonObj, accountName: accountName});
358 		var result = new ZmCsfeResult(response, false);
359 		return this._getApptSummariesResponse(params, result);
360 	}
361 };
362 
363 ZmCalMgr.prototype._setSoapParams =
364 function(request, params) {	
365 	request.sortBy = "none";
366 	request.limit = "500";
367 	request.calExpandInstStart = params.start;
368 	request.calExpandInstEnd = params.end;
369 	request.types = ZmSearch.TYPE[ZmItem.APPT];
370 	request.offset = params.offset;
371 
372 	var query = params.query;
373 	if (params.queryHint) {
374 		query = (query != null)
375 			? (query + " (" + params.queryHint + ")")
376 			: params.queryHint;
377 	}
378 	request.query = {_content:query};
379 };
380 
381 
382 ZmCalMgr.prototype._getApptSummariesResponse =
383 function(params, result) {
384 	// TODO: mark both as needing refresh?
385 	if (!result) { return; }
386 
387 	var callback = params.callback;
388 	var resp;
389 	try {
390 		resp = result.getResponse();
391 	} catch (ex) {
392 		if (callback) {
393 			callback.run(new AjxVector());
394 		}
395 		return;
396 	}
397 
398 	var searchResp = resp.SearchResponse;
399 	var newList = this.processSearchResponse(searchResp, params);
400 	if (newList == null) { return; }
401 
402 	if (callback) {
403 		callback.run(newList, params.query);
404 	} else {
405 		return newList;
406 	}
407 };
408 
409 ZmCalMgr.prototype.processSearchResponse = 
410 function(searchResp, params) {
411 	if(!searchResp) { return; }
412 
413 	if (searchResp && searchResp.appt && searchResp.appt.length) {
414 		this._rawAppts = this._rawAppts != null 
415 			? this._rawAppts.concat(searchResp.appt)
416 			: searchResp.appt;
417 
418 		// if "more" flag set, keep requesting more appts
419 		if (searchResp.more) {
420 			var lastAppt = searchResp.appt[searchResp.appt.length-1];
421 			if (lastAppt) {
422 				params.offset += 500;
423 				this._search(params);
424 				return;
425 			}
426 		}
427 	}
428 
429 	var newList = new AjxVector();
430 	if (this._rawAppts && this._rawAppts.length) {
431 		this._list = new ZmList(ZmItem.APPT);
432 		for (var i = 0; i < this._rawAppts.length; i++) {
433 			DBG.println(AjxDebug.DBG2, "appt[j]:" + this._rawAppts[i].name);
434 			var apptNode = this._rawAppts[i];
435 			var instances = apptNode ? apptNode.inst : null;
436 			if (instances) {
437 				var args = {list:this._list};
438 				for (var j = 0; j < instances.length; j++) {
439 					var appt = ZmCalBaseItem.createFromDom(apptNode, args, instances[j]);
440 					DBG.println(AjxDebug.DBG2, "lite appt :" + appt);
441 					if (appt) newList.add(appt);
442 				}
443 			}
444 		}
445 
446 	}
447 	return newList;
448 };
449 
450 ZmCalMgr.prototype.getCalendarName =
451 function(folderId) {
452 	var app = appCtxt.getApp(ZmApp.CALENDAR);
453 	return app.getCalendarName(folderId);
454 };
455 
456 // Mini calendar action menu listeners, calview controller is loaded and than
457 // event handling listener functions are called
458 ZmCalMgr.prototype._newApptAction =
459 function(ev) {
460 	var calController = this.getCalViewController();
461 	calController._newApptAction(ev);
462 };
463 
464 ZmCalMgr.prototype._newAllDayApptAction =
465 function(ev) {
466 	var calController = this.getCalViewController();
467 	calController._newAllDayApptAction(ev);
468 };
469 
470 ZmCalMgr.prototype._searchMailAction =
471 function(ev) {
472 	var calController = this.getCalViewController();
473 	calController._searchMailAction(ev);
474 };
475 
476 ZmCalMgr.prototype.getCheckedCalendarFolderIds =
477 function(localOnly) {
478 	var app = appCtxt.getApp(ZmApp.CALENDAR);
479 	return app.getCheckedCalendarFolderIds(localOnly);
480 };
481 
482 ZmCalMgr.prototype.getReminderCalendarFolderIds =
483 function() {
484 	var app = appCtxt.getApp(ZmApp.CALENDAR);
485 	return app.getReminderCalendarFolderIds();
486 };
487 
488 ZmCalMgr.prototype._handleError =
489 function(ex) {
490 	if (ex.code == 'mail.INVITE_OUT_OF_DATE' ||	ex.code == 'mail.NO_SUCH_APPT') {
491 		var msgDialog = appCtxt.getMsgDialog();
492 		msgDialog.setMessage(ZmMsg.apptOutOfDate, DwtMessageDialog.INFO_STYLE);
493 		msgDialog.popup();
494 		return true;
495 	}
496 	return false;
497 };
498 
499 ZmCalMgr.prototype.highlightMiniCal =
500 function() {
501 	this.getMiniCalCache()._getMiniCalData(this.getMiniCalendarParams());
502 };
503 
504 ZmCalMgr.prototype.getMiniCalendarParams =
505 function() {
506 	var dr = this.getMiniCalendar().getDateRange();
507 	return {
508 		start: dr.start.getTime(),
509 		end: dr.end.getTime(),
510 		fanoutAllDay: true,
511 		noBusyOverlay: true,
512 		folderIds: this.getCheckedCalendarFolderIds(),
513         tz: AjxTimezone.DEFAULT
514 	};
515 };
516 
517 ZmCalMgr.prototype.getMiniCalCache =
518 function() {
519 	if (!this._miniCalCache) {
520 		this._miniCalCache = new ZmMiniCalCache(this);
521 	}
522 	return this._miniCalCache;
523 };
524 
525 ZmCalMgr.prototype.getQuickReminderSearchTimeRange =
526 function() {
527 	var endOfDay = new Date();
528 	endOfDay.setHours(23,59,59,999);
529 
530 	var end = new Date(endOfDay.getTime());
531 
532 	var start = endOfDay;
533 	start.setHours(0,0,0, 0);
534 
535 	return { start: start.getTime(), end: end.getTime() };
536 };
537 
538 ZmCalMgr.prototype.showQuickReminder =
539 function() {
540     var params = this.getQuickReminderParams();
541     this.getApptSummaries(params);
542 };
543 
544 ZmCalMgr.prototype.getQuickReminderParams =
545 function() {
546 
547 	var timeRange = this.getQuickReminderSearchTimeRange();
548 	return {
549 		start: timeRange.start,
550 		end: timeRange.end,
551 		fanoutAllDay: false,
552 		folderIds: this.getCheckedCalendarFolderIds(true),
553 		callback: (new AjxCallback(this, this._quickReminderCallback)),
554 		includeReminders: true
555 	};
556 };
557 
558 ZmCalMgr.prototype._quickReminderCallback =
559 function(list) {
560     var newList = new AjxVector();
561     this._cacheMap = {};
562     var size = list.size();
563 
564     var currentTime  = (new Date()).getTime();
565 
566     for (var i = 0; i < size; i++) {
567         var appt = list.get(i);
568         var id = appt.id;
569         if (!this._cacheMap[id]) {
570             this._cacheMap[id] = appt;
571             if(appt.isAllDayEvent()) continue;
572             var diff = appt.getStartTime() - currentTime;
573             var isUpcomingEvent = (diff >= 0 && diff <= AjxDateUtil.MSEC_PER_HOUR)
574             if((currentTime >= appt.getStartTime() && currentTime <= appt.getEndTime()) || isUpcomingEvent) {
575                 appt.isUpcomingEvent = isUpcomingEvent;
576                 newList.add(appt);
577             }
578         }
579     }
580 
581     var qDlg = this.getQuickReminderDialog();
582     qDlg.initialize(newList);
583     qDlg.popup();
584 };
585 
586 
587 /**
588  * Gets the quick reminder dialog.
589  *
590  * @return	{ZmQuickReminderDialog}	the dialog
591  */
592 ZmCalMgr.prototype.getQuickReminderDialog =
593 function() {
594 	if (this._reminderDialog == null) {
595 		this._reminderDialog = new ZmQuickReminderDialog(appCtxt.getShell(), this, this._calController);
596 	}
597 	return this._reminderDialog;
598 };
599