1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 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) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * This file defines a base calendar item. 27 * 28 */ 29 30 /** 31 * @class 32 * This class represents the base calendar item. 33 * 34 * @param {constant} type the item type 35 * @param {ZmList} list the list 36 * @param {String} id the id 37 * @param {String} folderId the folder id 38 * @extends ZmItem 39 */ 40 ZmCalBaseItem = function(type, list, id, folderId) { 41 if (arguments.length == 0) { return; } 42 43 ZmItem.call(this, type, id, list); 44 45 this.id = id || -1; 46 this.uid = -1; // iCal uid of appt 47 this.folderId = folderId || this._getDefaultFolderId(); 48 this.fragment = ""; 49 this.name = ""; 50 this.allDayEvent = "0"; 51 this.startDate = null; 52 this.endDate = null; 53 this.timezone = AjxTimezone.getServerId(AjxTimezone.DEFAULT); 54 this.alarm = false; 55 this.alarmData = null; 56 this.isException = false; 57 this.recurring = false; 58 this.priority = null; 59 this.ptst = null; // participant status 60 this.status = ZmCalendarApp.STATUS_CONF; 61 this._reminderMinutes = 0; 62 this.otherAttendees = false; 63 }; 64 65 ZmCalBaseItem.prototype = new ZmItem; 66 ZmCalBaseItem.prototype.constructor = ZmCalBaseItem; 67 /** 68 * Returns a string representation of the object. 69 * 70 * @return {String} a string representation of the object 71 */ 72 ZmCalBaseItem.prototype.toString = 73 function() { 74 return "ZmCalBaseItem"; 75 }; 76 77 78 // consts 79 /** 80 * Defines the "person" resource type. 81 */ 82 ZmCalBaseItem.PERSON = "PERSON"; 83 /** 84 * Defines the "optional person" resource type. 85 */ 86 ZmCalBaseItem.OPTIONAL_PERSON = "OPT_PERSON"; 87 /** 88 * Defines the "group" resource type. 89 */ 90 ZmCalBaseItem.GROUP = "GROUP"; 91 /** 92 * Defines the "location" resource type. 93 */ 94 ZmCalBaseItem.LOCATION = "LOCATION"; 95 /** 96 * Defines the "equipment" resource type. 97 */ 98 ZmCalBaseItem.EQUIPMENT = "EQUIPMENT"; 99 ZmCalBaseItem.FORWARD = "FORWARD"; 100 101 /** 102 * Defines the "accept" participant status. 103 */ 104 ZmCalBaseItem.PSTATUS_ACCEPT = "AC"; // vevent, vtodo 105 /** 106 * Defines the "declined" participant status. 107 */ 108 ZmCalBaseItem.PSTATUS_DECLINED = "DE"; // vevent, vtodo 109 /** 110 * Defines the "deferred" participant status. 111 */ 112 ZmCalBaseItem.PSTATUS_DEFERRED = "DF"; // vtodo [outlook] 113 /** 114 * Defines the "delegated" participant status. 115 */ 116 ZmCalBaseItem.PSTATUS_DELEGATED = "DG"; // vevent, vtodo 117 /** 118 * Defines the "needs action" participant status. 119 */ 120 ZmCalBaseItem.PSTATUS_NEEDS_ACTION = "NE"; // vevent, vtodo 121 /** 122 * Defines the "completed" participant status. 123 */ 124 ZmCalBaseItem.PSTATUS_COMPLETED = "CO"; // vtodo 125 /** 126 * Defines the "tentative" participant status. 127 */ 128 ZmCalBaseItem.PSTATUS_TENTATIVE = "TE"; // vevent, vtodo 129 /** 130 * Defines the "waiting" participant status. 131 */ 132 ZmCalBaseItem.PSTATUS_WAITING = "WA"; // vtodo [outlook] 133 134 ZmCalBaseItem.FBA_TO_PTST = { 135 B: ZmCalBaseItem.PSTATUS_ACCEPT, 136 F: ZmCalBaseItem.PSTATUS_DECLINED, 137 T: ZmCalBaseItem.PSTATUS_TENTATIVE 138 }; 139 140 ZmCalBaseItem._pstatusString = { 141 NE: ZmMsg._new, 142 TE: ZmMsg.tentative, 143 AC: ZmMsg.accepted, 144 DE: ZmMsg.declined, 145 DG: ZmMsg.delegated 146 }; 147 148 /** 149 * Compares two appointments by start time and duration. 150 * 151 * @param {ZmCalBaseItem} a an appointment 152 * @param {ZmCalBaseItem} b an appointment 153 * @return {int} 1 if start time "a" is after "b" or duration "a" is shorter than "b"; 1 if start time "b" is after "a" or duration "b" is shorter than "a"; 0 if both are the same 154 */ 155 ZmCalBaseItem.compareByTimeAndDuration = 156 function(a, b) { 157 if (a.getStartTime() > b.getStartTime()) return 1; 158 if (a.getStartTime() < b.getStartTime()) return -1; 159 if (a.getDuration() < b.getDuration()) return 1; 160 if (a.getDuration() > b.getDuration()) return -1; 161 return 0; 162 }; 163 164 /** 165 * Creates the item from the DOM. 166 * 167 * @private 168 */ 169 ZmCalBaseItem.createFromDom = 170 function(apptNode, args, instNode) { 171 var appt = new ZmCalBaseItem(ZmItem.APPT, args.list); 172 appt._loadFromDom(apptNode, (instNode || {})); 173 return appt; 174 }; 175 176 /** 177 * Gets the name (the "subject"). 178 * 179 * @return {String} the name 180 */ 181 ZmCalBaseItem.prototype.getName = function() { return this.name || ""; }; // name (aka Subject) of appt 182 183 /** 184 * Gets the end time. 185 * 186 * @return {Date} the end time 187 */ 188 ZmCalBaseItem.prototype.getEndTime = function() { return this.endDate.getTime(); }; // end time in ms 189 190 /** 191 * Gets the start time. 192 * 193 * @return {Date} the start time 194 */ 195 ZmCalBaseItem.prototype.getStartTime = function() { return this.startDate.getTime(); }; // start time in ms 196 197 /** 198 * Gets the alarm instance start time 199 * 200 * @return {Date} the alarmInst time 201 */ 202 ZmCalBaseItem.prototype.getAlarmInstStart = function() { return this._alarmInstStart; }; // alarm inst time in ms 203 204 /** 205 * Gets the duration. 206 * 207 * @return {int} the duration (in milliseconds) 208 */ 209 ZmCalBaseItem.prototype.getDuration = function() { return this.getEndTime() - this.getStartTime(); } // duration in ms 210 /** 211 * Gets the location. 212 * 213 * @return {String} the location 214 */ 215 ZmCalBaseItem.prototype.getLocation = function() { return this.location || ""; }; 216 /** 217 * Checks if the item is an all day event. 218 * 219 * @return {Boolean} <code>true</code> if all day event 220 */ 221 ZmCalBaseItem.prototype.isAllDayEvent = function() { return this.allDayEvent == "1"; }; 222 223 /** 224 * Gets the participant status as a string. 225 * 226 * @return {String} the participant status 227 */ 228 ZmCalBaseItem.prototype.getParticipantStatusStr = 229 function() { 230 return ZmCalBaseItem._pstatusString[this.ptst]; 231 }; 232 233 /** 234 * Gets the unique id for this item. 235 * 236 * @param {Boolean} useStartTime if <code>true</code>, use the start time 237 * @return {String} the unique id 238 */ 239 ZmCalBaseItem.prototype.getUniqueId = 240 function(useStartTime) { 241 if (useStartTime) { 242 if (!this._startTimeUniqId) { 243 this._startTimeUniqId = this.id + "_" + this.getStartTime(); 244 } 245 return this._startTimeUniqId; 246 } else { 247 if (this._uniqId == null) { 248 this._uniqId = Dwt.getNextId(); 249 } 250 return (this.id + "_" + this._uniqId); 251 } 252 }; 253 254 /** 255 * Checks if this item is multi-day. 256 * 257 * @return {Boolean} <code>true</code> if start date and end date are on different days 258 * 259 * @see #getStartTime 260 * @see #getEndTime 261 */ 262 ZmCalBaseItem.prototype.isMultiDay = 263 function() { 264 var start = this.startDate; 265 var end = this.endDate; 266 267 if(!start && !end) { return false; } 268 269 if(!start) { return false; } 270 271 if (end.getHours() == 0 && end.getMinutes() == 0 && end.getSeconds() == 0) { 272 // if end is the beginning of day, then disregard that it 273 // technically crossed a day boundary for the purpose of 274 // determining if it is a multi-day appt 275 end = new Date(end.getTime() - 2 * AjxDateUtil.MSEC_PER_HOUR); 276 } 277 278 return (start.getDate() != end.getDate()) || 279 (start.getMonth() != end.getMonth()) || 280 (start.getFullYear() != end.getFullYear()); 281 }; 282 283 /** 284 * Gets the duration text. 285 * 286 * @param {Boolean} emptyAllDay if <code>true</code>, return empty string if all day event 287 * @param {Boolean} startOnly if <code>true</code>, use start date only 288 * @param {Boolean} getSimpleText if <code>true</code>, use the modified representation for duration where: 289 * 1. For one day all day event we show only "All day" before event name and omit the Date information 290 * 2. For multiday all day event we just show final start/end date and omit time information and other words. 291 * 3. For appt that entirely falls in one day we omit day and just show time. 292 * 4. For multiday appt we show final start/end date&time 293 * @return {String} the duration text 294 */ 295 ZmCalBaseItem.prototype.getDurationText = 296 function(emptyAllDay, startOnly, getSimpleText) { 297 var isAllDay = this.isAllDayEvent(); 298 var isMultiDay = this.isMultiDay(); 299 var pattern; 300 301 if (isAllDay) { 302 if (emptyAllDay) return ""; 303 304 var start = this.startDate; 305 var end = new Date(this.endDate.getTime() - (isMultiDay ? 2 * AjxDateUtil.MSEC_PER_HOUR : 0)); 306 307 if (getSimpleText) { 308 if (isMultiDay) { 309 pattern = ZmMsg.apptTimeAllDayMultiCondensed; 310 } 311 else { 312 return ZmMsg.allDay; 313 } 314 } 315 else { 316 pattern = isMultiDay ? ZmMsg.apptTimeAllDayMulti : ZmMsg.apptTimeAllDay; 317 } 318 return AjxMessageFormat.format(pattern, [start, end]); 319 } 320 321 if (startOnly) { 322 return ZmCalBaseItem._getTTHour(this.startDate); 323 } 324 325 if (getSimpleText) { 326 pattern = isMultiDay ? ZmMsg.apptTimeInstanceMultiCondensed : ZmMsg.apptTimeInstanceCondensed; 327 } 328 else { 329 pattern = isMultiDay ? ZmMsg.apptTimeInstanceMulti : ZmMsg.apptTimeInstance; 330 } 331 332 return AjxMessageFormat.format(pattern, [this.getDateInLocalTimezone(this.startDate), this.getDateInLocalTimezone(this.endDate), ""]); 333 }; 334 335 /** 336 * Checks if alarm is in range (based on current time). 337 * 338 * @return {Boolean} <code>true</code> if the alarm is in range 339 */ 340 ZmCalBaseItem.prototype.isAlarmInRange = 341 function() { 342 if (!this.alarmData) { return false; } 343 344 var alarmData = this.alarmData[0]; 345 346 if (!alarmData) { return false; } 347 348 this._nextAlarmTime = this.adjustMS(alarmData.nextAlarm, this.tzo); 349 this._alarmInstStart = this.adjustMS(alarmData.alarmInstStart, this.tzo); 350 351 var currentTime = (new Date()).getTime(); 352 353 return (currentTime >= this._nextAlarmTime); 354 }; 355 356 /** 357 * Adjusts milliseconds. 358 * 359 * @param {int} s the seconds 360 * @param {int} tzo the timezone offset 361 * @return {int} the resulting milliseconds 362 */ 363 ZmCalBaseItem.prototype.adjustMS = 364 function(s, tzo) { 365 var adjustMs = this.isAllDayEvent() ? (tzo + new Date(s).getTimezoneOffset()*60*1000) : 0; 366 return parseInt(s, 10) + adjustMs; 367 }; 368 369 /** 370 * Checks if this is an alarm instance. 371 * 372 * @return {Boolean} <code>true</code> if this is an alarm instance 373 */ 374 ZmCalBaseItem.prototype.isAlarmInstance = 375 function() { 376 var alarmData = this.alarmData ? this.alarmData[0] : null; 377 378 if (!alarmData || 379 !alarmData.alarmInstStart || 380 !this.startDate) { 381 return false; 382 } 383 this._alarmInstStart = this.adjustMS(alarmData.alarmInstStart, this.tzo); 384 return (this._alarmInstStart == this.startDate.getTime()); 385 }; 386 387 /** 388 * Checks if this item has alarm data. 389 * 390 * @return {Boolean} <code>true</code> if item has alarm data 391 */ 392 ZmCalBaseItem.prototype.hasAlarmData = 393 function() { 394 return (this.alarmData != null); 395 }; 396 397 /** 398 * @private 399 */ 400 ZmCalBaseItem.prototype._loadFromDom = 401 function(calItemNode, instNode) { 402 403 this.uid = calItemNode.uid; 404 this.folderId = calItemNode.l || this._getDefaultFolderId(); 405 this.invId = calItemNode.invId; 406 this.isException = instNode.ex; 407 this.id = calItemNode.id; 408 this.name = this._getAttr(calItemNode, instNode, "name"); 409 this.fragment = this._getAttr(calItemNode, instNode, "fr"); 410 this.status = this._getAttr(calItemNode, instNode, "status"); 411 this.ptst = this._getAttr(calItemNode, instNode, "ptst"); 412 413 this.allDayEvent = (instNode.allDay || calItemNode.allDay) ? "1" : "0"; 414 this.organizer = calItemNode.or && calItemNode.or.a; 415 this.isOrg = this._getAttr(calItemNode, instNode, "isOrg"); 416 this.transparency = this._getAttr(calItemNode, instNode, "transp"); 417 418 if (instNode.allDay == false) { 419 this.allDayEvent = "0"; 420 } 421 422 this.alarm = this._getAttr(calItemNode, instNode, "alarm"); 423 this.alarmData = this._getAttr(calItemNode, instNode, "alarmData"); 424 if (!this.alarmData && this.isException) { 425 this.alarmData = calItemNode.alarmData; 426 } 427 this.priority = parseInt(this._getAttr(calItemNode, instNode, "priority")); 428 429 this.recurring = instNode.recur != null ? instNode.recur : calItemNode.recur; // TEST for null since recur can be FALSE 430 this.ridZ = this.recurring && instNode && instNode.ridZ; 431 432 this.fba = this._getAttr(calItemNode, instNode, "fba"); 433 434 var sd = instNode.s !=null ? instNode.s : calItemNode.inst && calItemNode.inst.length > 0 && calItemNode.inst[0].s; 435 if (sd) { 436 var tzo = this.tzo = instNode.tzo != null ? instNode.tzo : calItemNode.tzo; 437 var adjustMs = this.isAllDayEvent() ? (tzo + new Date(sd).getTimezoneOffset()*60*1000) : 0; 438 var startTime = parseInt(sd,10) + adjustMs; 439 this.startDate = new Date(startTime); 440 this.uniqStartTime = this.startDate.getTime(); 441 } 442 443 var dur = this._getAttr(calItemNode, instNode, "dur"); 444 if (dur) { 445 var endTime = startTime + (parseInt(dur)); 446 this.endDate = new Date(endTime); 447 } 448 449 this.otherAttendees = this._getAttr(calItemNode, instNode, "otherAtt"); 450 this.location = this._getAttr(calItemNode, instNode, "loc"); 451 }; 452 453 /** 454 * @private 455 */ 456 ZmCalBaseItem.prototype._getDefaultFolderId = 457 function() { 458 return ZmOrganizer.ID_CALENDAR; 459 }; 460 461 /** 462 * @private 463 */ 464 ZmCalBaseItem.prototype._getAttr = 465 function(calItem, inst, name) { 466 return inst[name] != null ? inst[name] : inst.ex ? null : calItem[name]; 467 }; 468 469 /** 470 * @private 471 */ 472 ZmCalBaseItem.prototype._addLocationToRequest = 473 function(inv) { 474 inv.loc = this.getLocation(); 475 }; 476 477 /** 478 * @private 479 */ 480 ZmCalBaseItem._getTTHour = 481 function(d) { 482 var formatter = AjxDateFormat.getTimeInstance(AjxDateFormat.SHORT); 483 return formatter.format(d); 484 }; 485 486 487 ZmCalBaseItem.prototype.getReminderLocation = 488 function() { 489 return (this.alarmData[0].loc || ""); 490 }; 491 492 /** 493 * Gets the reminder name. 494 * 495 * @return {String} the reminder name or empty string if not set 496 */ 497 ZmCalBaseItem.prototype.getReminderName = 498 function() { 499 return (this.alarmData[0].name || ""); 500 }; 501 502 /** 503 * Gets alarm info 504 * 505 * @return {Object} the alarm information 506 */ 507 ZmCalBaseItem.prototype.getAlarmData = 508 function() { 509 return this.alarmData; 510 } 511 512 /** 513 * Checks if the alarm is old (based on current time). 514 * 515 * @return {Boolean} <code>true</code> if the alarm is old 516 */ 517 ZmCalBaseItem.prototype.isAlarmOld = 518 function() { 519 if (!this.alarmData) { return false; } 520 521 var alarmData = this.alarmData[0]; 522 this._nextAlarmTime = alarmData.nextAlarm; 523 this._alarmInstStart = alarmData.alarmInstStart; 524 525 var currentTime = (new Date()).getTime(); 526 527 var diff = (currentTime - this._nextAlarmTime); 528 529 //reminder controller takes 1 minute interval for house keeping schedule 530 //if the diff is greater than 2 minutes (safer deadline) mark the alarm as old 531 if(diff > 2*60*1000) { 532 return true; 533 } 534 return false; 535 }; 536 537 ZmCalBaseItem.prototype.getRestUrl = 538 function() { 539 // return REST URL as seen by server 540 if (this.restUrl) { 541 return this.restUrl; 542 } 543 544 // if server doesn't tell us what URL to use, do our best to generate 545 var organizer = appCtxt.getById(this.folderId); 546 var url = organizer 547 ? ([organizer.getRestUrl(), "/?id=", AjxStringUtil.urlComponentEncode(this.id || this.invId)].join("")) 548 : null; 549 550 DBG.println(AjxDebug.DBG3, "NO REST URL FROM SERVER. GENERATED URL: " + url); 551 552 return url; 553 }; 554 555 ZmCalBaseItem.prototype.getDateInLocalTimezone = 556 function(date) { 557 var apptTZ = this.getTimezone(); 558 var localTZ = AjxTimezone.getServerId(AjxTimezone.DEFAULT); 559 if(apptTZ != localTZ) { 560 var offset1 = AjxTimezone.getOffset(AjxTimezone.DEFAULT, date); 561 var offset2 = AjxTimezone.getOffset(AjxTimezone.getClientId(apptTZ), date); 562 return new Date(date.getTime() + (offset1 - offset2)*60*1000); 563 } 564 return date; 565 }; 566 567