1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 ZmFreeBusyCache = function(controller) {
 25 	this._controller = controller;
 26 	this.clearCache();
 27 };
 28 
 29 ZmFreeBusyCache.STATUS_UNKNOWN = 'n';
 30 ZmFreeBusyCache.STATUS_TENTATIVE = 't';
 31 ZmFreeBusyCache.STATUS_BUSY = 'b';
 32 ZmFreeBusyCache.STATUS_OUT = 'u';
 33 ZmFreeBusyCache.STATUS_FREE = 'f';
 34 
 35 ZmFreeBusyCache.STATUS_WORKING_HOURS = 'f';
 36 ZmFreeBusyCache.STATUS_NON_WORKING_HOURS = 'u';
 37 ZmFreeBusyCache.STATUS_UNKNOWN = 'n';
 38 
 39 ZmFreeBusyCache.prototype.toString =
 40 function() {
 41 	return "ZmFreeBusyCache";
 42 };
 43 
 44 ZmFreeBusyCache.prototype.clearCache =
 45 function() {
 46     DBG.println("clearing free busy cache");
 47     this._schedule = {};
 48     this._workingHrs = {};
 49 };
 50 
 51 ZmFreeBusyCache.prototype.getFreeBusyKey =
 52 function(startTime, id) {
 53     return startTime + "-" + id;
 54 };
 55 
 56 ZmFreeBusyCache.prototype.getWorkingHoursKey =
 57 function(id, day) {
 58     return id + "-" + day;
 59 };
 60 
 61 //filter free busy slots for given time from compressed/accumulated free busy response that got cached already
 62 ZmFreeBusyCache.prototype.getFreeBusySlot =
 63 function(startTime, endTime, id, excludeTimeSlots) {
 64     var slotDate = new Date(startTime);
 65     slotDate.setHours(0, 0, 0, 0);
 66 
 67     var fbSlots = this._schedule[id] || [];
 68     var fbResult = {id: id};
 69 
 70     //free busy response is always merged
 71     var usr, searchRange, newSearchIsInRange;
 72     for(var i= fbSlots.length; --i >= 0;) {
 73         usr = fbSlots[i];
 74         searchRange = usr.searchRange;
 75 
 76         if(searchRange) {
 77             newSearchIsInRange = (startTime >= searchRange.startTime && endTime <= searchRange.endTime);
 78             if(!newSearchIsInRange) continue;
 79         }
 80 
 81         if (usr.n) this._addFBInfo(usr.n, id, ZmFreeBusyCache.STATUS_UNKNOWN, startTime, endTime, fbResult, excludeTimeSlots);
 82         if (usr.t) this._addFBInfo(usr.t, id, ZmFreeBusyCache.STATUS_TENTATIVE, startTime, endTime, fbResult, excludeTimeSlots);
 83         if (usr.b) this._addFBInfo(usr.b, id, ZmFreeBusyCache.STATUS_BUSY, startTime, endTime, fbResult, excludeTimeSlots);
 84         if (usr.u) this._addFBInfo(usr.u, id, ZmFreeBusyCache.STATUS_OUT, startTime, endTime, fbResult, excludeTimeSlots);
 85         if (usr.f) this._addFBInfo(usr.f, id, ZmFreeBusyCache.STATUS_FREE, startTime, endTime, fbResult, excludeTimeSlots);
 86     }
 87 
 88     return fbResult;
 89 };
 90 
 91 ZmFreeBusyCache.prototype._addFBInfo =
 92 function(slots, id, status, startTime, endTime, fbResult, excludeTimeSlots) {
 93 
 94     if(!fbResult[status]) fbResult[status] = [];
 95 
 96     for (var i = 0; i < slots.length; i++) {
 97         var fbSlot;
 98         if(slots[i].s >= startTime && slots[i].e  <= endTime) {
 99             fbSlot = {s: slots[i].s, e: slots[i].e};
100         }else if(startTime >= slots[i].s && endTime  <= slots[i].e) {
101             fbSlot = {s: startTime, e: endTime};
102         }else if(startTime >= slots[i].s && startTime  <= slots[i].e) {
103             fbSlot = {s: startTime, e: slots[i].e};
104         }else if(endTime >= slots[i].s && endTime  <= slots[i].e) {
105             fbSlot = {s: slots[i].s, e: endTime};
106         }
107 
108         if(fbSlot) {
109             if(excludeTimeSlots && status != ZmFreeBusyCache.STATUS_FREE && status != ZmFreeBusyCache.STATUS_UNKNOWN) {
110                 this._addByExcludingTime(excludeTimeSlots, fbSlot, fbResult, status);
111             }else {
112                 fbResult[status].push(fbSlot);
113             }
114         }
115     };
116 
117     if(fbResult[status].length == 0) fbResult[status] = null;
118 };
119 
120 ZmFreeBusyCache.prototype._addByExcludingTime =
121 function(excludeTimeSlots, fbSlot, fbResult, status) {
122     var startTime =  excludeTimeSlots.s;
123     var endTime =  excludeTimeSlots.e;
124     var newFBSlot;
125 
126     if(fbSlot.s == startTime && fbSlot.e == endTime) {
127         return;
128     }
129 
130     if(fbSlot.s < startTime && fbSlot.e > endTime) {
131         fbResult[status].push({s: fbSlot.s, e: startTime});
132         newFBSlot = {s: endTime, e: fbSlot.e};
133     }else if(fbSlot.s < startTime && fbSlot.e >= startTime) {
134         newFBSlot = {s: fbSlot.s, e: startTime};
135     }else if(fbSlot.s <= endTime && fbSlot.e > endTime) {
136         newFBSlot = {s: endTime, e: fbSlot.e};
137     }else if(fbSlot.s <= startTime && fbSlot.e <= startTime) {
138         newFBSlot = {s: fbSlot.s, e: fbSlot.e};
139     }else if(fbSlot.s >= endTime && fbSlot.e >= endTime) {
140         newFBSlot = {s: fbSlot.s, e: fbSlot.e};
141     }
142 
143     if(newFBSlot) {
144         fbResult[status].push(newFBSlot);
145     }
146 };
147 
148 ZmFreeBusyCache.prototype.getFreeBusyInfo =
149 function(params) {
150 
151     var requiredEmails = [], freeBusyKey, emails = params.emails, fbSlot;
152     for (var i = emails.length; --i >= 0;) {
153         freeBusyKey = params.startTime + "";
154         //check local cache
155         var entryExists = false
156         if(this._schedule[emails[i]]) {
157             var fbSlots = this.getFreeBusySlot(params.startTime, params.endTime, emails[i]);
158             if(fbSlots.f || fbSlots.u || fbSlots.b || fbSlots.t || fbSlots.n) entryExists = true;            
159         };
160         if(!entryExists) requiredEmails.push(emails[i]);
161     }
162 
163     var fbCallback = new AjxCallback(this, this._handleResponseFreeBusy, [params]);
164     var fbErrorCallback = new AjxCallback(this, this._handleErrorFreeBusy, [params]);
165 
166     if(requiredEmails.length) {
167 	    return this._getFreeBusyInfo(params.startTime,
168                                      params.endTime,
169                                      requiredEmails.join(","),
170                                      fbCallback,
171                                      fbErrorCallback,
172                                      params.noBusyOverlay,
173                                      params.account,
174                                      params.excludedId);
175     }else {
176         if(params.callback) {
177             params.callback.run();
178         }
179         return null;
180     }
181 
182 };
183 
184 //cache free busy response in user-id -> slots hash map
185 ZmFreeBusyCache.prototype. _handleResponseFreeBusy =
186 function(params, result) {
187 
188     var freeBusyKey;
189 	var args = result.getResponse().GetFreeBusyResponse.usr || [];
190     for (var i = 0; i < args.length; i++) {
191 		var usr = args[i];
192         var id = usr.id;
193         if (!id) {
194             continue;
195         }
196         if(!this._schedule[id]) {
197             this._schedule[id] = [];
198         }
199 
200         usr.searchRange = {startTime: params.startTime,  endTime: params.endTime};
201         this._schedule[id].push(usr);
202     };
203 
204     if(params.callback) {
205         params.callback.run(result);
206     }
207 };
208 
209 ZmFreeBusyCache.prototype._handleErrorFreeBusy =
210 function(params, result) {
211     if(params.errorCallback) {
212         params.errorCallback.run(result);
213     }
214 };
215 
216 ZmFreeBusyCache.prototype._getFreeBusyInfo =
217 function(startTime, endTime, emailList, callback, errorCallback, noBusyOverlay, acct, excludedId) {
218 	var soapDoc = AjxSoapDoc.create("GetFreeBusyRequest", "urn:zimbraMail");
219 	soapDoc.setMethodAttribute("s", startTime);
220 	soapDoc.setMethodAttribute("e", endTime);
221 	soapDoc.setMethodAttribute("uid", emailList);
222 	if (excludedId) {
223 		soapDoc.setMethodAttribute("excludeUid", excludedId);
224 	}
225 
226 	return appCtxt.getAppController().sendRequest({
227 		soapDoc: soapDoc,
228 		asyncMode: true,
229 		callback: callback,
230 		errorCallback: errorCallback,
231 		noBusyOverlay: noBusyOverlay,
232 		accountName: (acct ? acct.name : null)
233 	});
234 };
235 
236 //working hrs related code
237 ZmFreeBusyCache.prototype.getWorkingHours =
238 function(params) {
239 
240     var requiredEmails = [], whKey, emails = params.emails || [];
241     for (var i = emails.length; --i >= 0;) {
242         whKey = this.getWorkingHoursKey(emails[i], (new Date(params.startTime)).getDay());
243         //check local cache
244         if(!this._workingHrs[whKey]) requiredEmails.push(emails[i]);
245     }
246 
247     var fbCallback = new AjxCallback(this, this._handleResponseWorkingHrs, [params]);
248     var fbErrorCallback = new AjxCallback(this, this._handleErrorWorkingHrs, [params]);
249 
250     if(requiredEmails.length) {
251 	    return this._getWorkingHours(params.startTime,
252                                      params.endTime,
253                                      requiredEmails.join(","),
254                                      fbCallback,
255                                      fbErrorCallback,
256                                      params.noBusyOverlay,
257                                      params.account);
258     }else {
259         if(params.callback) {
260             params.callback.run();
261         }
262         return null;
263     }
264 
265 };
266 
267 ZmFreeBusyCache.prototype._handleResponseWorkingHrs =
268 function(params, result) {
269 
270     var freeBusyKey;
271 	var args = result.getResponse().GetWorkingHoursResponse.usr || [];
272     for (var i = 0; i < args.length; i++) {
273 		var usr = args[i];
274         var id = usr.id;
275         if (!id) {
276             continue;
277         }
278         this._addWorkingHrInfo(params.startTime, params.endTime, usr);
279     };
280 
281     if(params.callback) {
282         params.callback.run(result);
283     }
284 };
285 
286 ZmFreeBusyCache.prototype._addWorkingHrInfo =
287 function(startTime, endTime, usr) {
288     var id = usr.id;
289     if (usr.f) this._addWorkingHrSlot(usr.f, id, ZmFreeBusyCache.STATUS_WORKING_HOURS);
290     if (usr.u) this._addWorkingHrSlot(usr.u, id, ZmFreeBusyCache.STATUS_NON_WORKING_HOURS);
291     if (usr.n) this._addWorkingHrSlot(usr.n, id, ZmFreeBusyCache.STATUS_UNKNOWN);
292 };
293 
294 ZmFreeBusyCache.prototype._addWorkingHrSlot =
295 function(slots, id, status) {
296     var slotTime, slotDate, whKey, whSlots;
297     for (var i = 0; i < slots.length; i++) {
298         slotTime = slots[i].s;
299         slotDate = new Date(slotTime);
300         whKey = this.getWorkingHoursKey(id, slotDate.getDay());
301         whSlots = this._workingHrs[whKey];
302         if(!whSlots) {
303             this._workingHrs[whKey] = whSlots = {id: id};
304         }
305 
306         if(!whSlots[status]) {
307             whSlots[status] = [];
308         }
309         whSlots[status].push(slots[i]);
310 
311         //unknown working hours slots will be compressed on server response (will send one accumulated slots)
312         if(status == ZmFreeBusyCache.STATUS_UNKNOWN) {
313             this._workingHrs[id] = whSlots;
314         }
315     };
316 };
317 
318 
319 ZmFreeBusyCache.prototype._handleErrorWorkingHrs =
320 function(params, result) {
321     if(params.errorCallback) {
322         params.errorCallback.run(result);
323     }
324 };
325 
326 ZmFreeBusyCache.prototype._getWorkingHours =
327 function(startTime, endTime, emailList, callback, errorCallback, noBusyOverlay, acct) {
328     var soapDoc = AjxSoapDoc.create("GetWorkingHoursRequest", "urn:zimbraMail");
329     soapDoc.setMethodAttribute("s", startTime);
330     soapDoc.setMethodAttribute("e", endTime);
331     soapDoc.setMethodAttribute("name", emailList);
332 
333     return appCtxt.getAppController().sendRequest({
334         soapDoc: soapDoc,
335         asyncMode: true,
336         callback: callback,
337         errorCallback: errorCallback,
338         noBusyOverlay: noBusyOverlay,
339         accountName: (acct ? acct.name : null)
340     });
341 };
342 
343 ZmFreeBusyCache.prototype.getWorkingHrsSlot =
344 function(startTime, endTime, id) {
345     var whKey = this.getWorkingHoursKey(id, (new Date(startTime)).getDay());
346     var whSlots = this._workingHrs[whKey] || {};
347     var whResult = {id: id};
348 
349     //handle the case where the working hours are not available and slot dates are accumulated
350     var unknownSlots = this._workingHrs[id];
351     if(unknownSlots) {
352         return unknownSlots;
353     }
354 
355     if(whSlots[ZmFreeBusyCache.STATUS_WORKING_HOURS]) whResult[ZmFreeBusyCache.STATUS_WORKING_HOURS] = whSlots[ZmFreeBusyCache.STATUS_WORKING_HOURS];
356     if(whSlots[ZmFreeBusyCache.STATUS_NON_WORKING_HOURS]) whResult[ZmFreeBusyCache.STATUS_NON_WORKING_HOURS] = whSlots[ZmFreeBusyCache.STATUS_NON_WORKING_HOURS];
357     if(whSlots[ZmFreeBusyCache.STATUS_UNKNOWN]) whResult[ZmFreeBusyCache.STATUS_UNKNOWN] = whSlots[ZmFreeBusyCache.STATUS_UNKNOWN];
358 
359     if(!whResult[ZmFreeBusyCache.STATUS_WORKING_HOURS] && !whResult[ZmFreeBusyCache.STATUS_NON_WORKING_HOURS] && !whResult[ZmFreeBusyCache.STATUS_UNKNOWN]) return null;
360     return whResult;        
361 };
362 
363 ZmFreeBusyCache.prototype._addWHInfo =
364 function(slots, id, status, startTime, endTime, whResult) {
365 
366     if(!whResult[status]) whResult[status] = [];
367 
368     for (var i = 0; i < slots.length; i++) {
369         if(startTime >= slots[i].s && endTime <= slots[i].e) {
370             whResult[status].push({s: startTime, e: endTime});
371         }else if(slots[i].s >= startTime && slots[i].e <= endTime) {
372             whResult[status].push({s: slots[i].s, e: slots[i].e});
373         }else if(slots[i].s >= startTime && slots[i].s  <= endTime) {
374             whResult[status].push({s: slots[i].s, e: endTime});
375         }else if(slots[i].e >= startTime && slots[i].e  <= endTime) {
376             whResult[status].push({s: startTime, e: slots[i].e});
377         }
378     };
379 
380     if(whResult[status].length == 0) whResult[status] = null;
381 };
382 
383 /**
384  * converts working hours in different time base to required or current time base
385  * this is done due to the fact that working hrs pattern repeat everyweek and
386  * working hours are not fetched for every date change to optimize client code
387  * @param slot  {object} working hrs slot with start and end time in milliseconds
388  * @param relativeDate {date} optional date object relative to which the slot timings are converted
389  */
390 ZmFreeBusyCache.prototype.convertWorkingHours =
391 function(slot, relativeDate) {
392     relativeDate = relativeDate || new Date();
393     var slotStartDate = new Date(slot.s);
394     var slotEndDate = new Date(slot.e);
395     var dur = slot.e - slot.s;
396     slot.s = (new Date(relativeDate.getTime())).setHours(slotStartDate.getHours(), slotStartDate.getMinutes(), 0, 0);
397     slot.e = slot.s + dur;
398 };
399