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