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