1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2007, 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) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a recurrence object.
 26  * @class
 27  * This class represents a recurrence pattern.
 28  * 
 29  * @param	{ZmCalItem}	calItem		the calendar item
 30  * 
 31  */
 32 ZmRecurrence = function(calItem) {
 33 	this._startDate 			= (calItem && calItem.startDate) ? calItem.startDate : (new Date());
 34 
 35 	// initialize all params (listed alphabetically)
 36 	this.repeatCustom			= "0";  										// 1|0
 37 	this.repeatCustomCount		= 1; 											// ival
 38 	this.repeatCustomDayOfWeek	= "SU"; 										// (DAY|WEEKDAY|WEEKEND) | (SU|MO|TU|WE|TH|FR|SA)
 39 	this.repeatBySetPos	= "1";
 40 	this.repeatCustomMonthDay	= this._startDate.getDate();
 41 	this.repeatCustomType		= "S"; 											// (S)pecific, (O)rdinal
 42 	this.repeatEnd				= null;
 43 	this.repeatEndCount			= 1; 											// maps to "count" (when there is no end date specified)
 44 	this.repeatEndDate			= null; 										// maps to "until"
 45 	this.repeatEndType			= "N";
 46 	this.repeatMonthlyDayList	= null; 										// list of numbers representing days (usually, just one day)
 47 	this.repeatType				= ZmRecurrence.NONE;							// maps to "freq"
 48 	this.repeatWeekday			= false; 										// set to true if freq = "DAI" and custom repeats every weekday
 49 	this.repeatWeeklyDays		= [];	 										// SU|MO|TU|WE|TH|FR|SA
 50 	this.repeatYearlyMonthsList	= 1; 											// list of numbers representing months (usually, just one month)
 51 
 52     this._cancelRecurIds        = {};                                           //list of recurIds to be excluded
 53 };
 54 
 55 ZmRecurrence.prototype.toString =
 56 function() {
 57 	return "ZmRecurrence";
 58 };
 59 
 60 /**
 61  * Defines the "none" recurrence.
 62  */
 63 ZmRecurrence.NONE		= "NON";
 64 /**
 65  * Defines the "daily" recurrence.
 66  */
 67 ZmRecurrence.DAILY		= "DAI";
 68 /**
 69  * Defines the "weekly" recurrence.
 70  */
 71 ZmRecurrence.WEEKLY		= "WEE";
 72 /**
 73  * Defines the "monthly" recurrence.
 74  */
 75 ZmRecurrence.MONTHLY	= "MON";
 76 /**
 77  * Defines the "yearly" recurrence.
 78  */
 79 ZmRecurrence.YEARLY		= "YEA";
 80 
 81 /**
 82  * Defines the "day" week day selection.
 83  */
 84 ZmRecurrence.RECURRENCE_DAY = -1;
 85 /**
 86  * Defines the "weekend" week day selection.
 87  */
 88 ZmRecurrence.RECURRENCE_WEEKEND = -2;
 89 /**
 90  * Defines the "weekday" week day selection.
 91  */
 92 ZmRecurrence.RECURRENCE_WEEKDAY = -3
 93 
 94 ZmRecurrence.prototype.setJson =
 95 function(inv) {
 96 	if (this.repeatType == ZmRecurrence.NONE) {
 97         return;
 98     }
 99 
100 	var recur = inv.recur = {},
101         add = recur.add = {},
102         rule = add.rule = {},
103         interval = rule.interval = {},
104         until,
105         bwd,
106         bmd,
107         c,
108         i,
109         day,
110         wkDay,
111         bysetpos,
112         bm;
113 
114 	rule.freq = this.repeatType;
115 	interval.ival = this.repeatCustomCount;
116 
117 	if (this.repeatEndDate != null && this.repeatEndType == "D") {
118 		until = rule.until = {};
119 		until.d = AjxDateUtil.getServerDate(this.repeatEndDate);
120 	}
121     else if (this.repeatEndType == "A"){
122 		c = rule.count = {};
123 		c.num = this.repeatEndCount;
124 	}
125 
126 	if (this.repeatCustom != "1") {
127         this.setExcludes(recur);
128 		return;
129     }
130 
131 	if (this.repeatType == ZmRecurrence.DAILY) {
132         if (this.repeatWeekday) {
133 			// TODO: for now, handle "every weekday" as M-F
134 			//       eventually, needs to be localized work week days
135 			bwd = rule.byday = {};
136             wkDay = bwd.wkday = [];
137 			for (i = 0; i < ZmCalItem.SERVER_WEEK_DAYS.length; i++) {
138 				day = ZmCalItem.SERVER_WEEK_DAYS[i];
139 				if (day == "SA" || day == "SU") {
140 					continue;
141                 }
142 				wkDay.push({
143                     day : day
144                 });
145 			}
146 		}
147 	}
148     else if (this.repeatType == ZmRecurrence.WEEKLY) {
149         bwd = rule.byday = {};
150         wkDay = bwd.wkday = [];
151 		for (i = 0; i < this.repeatWeeklyDays.length; ++i) {
152             wkDay.push({
153                 day : this.repeatWeeklyDays[i]
154             });
155 		}
156 	}
157 	else if (this.repeatType == ZmRecurrence.MONTHLY) {
158 		if (this.repeatCustomType == "S") {
159 			bmd = rule.bymonthday = {};
160 			bmd.modaylist = this.repeatMonthlyDayList.join(",");
161 		}
162         else {
163 			bwd = rule.byday = {};
164             bwd.wkday = [];
165             if (this.repeatCustomDays) {
166                 for (i=0; i < this.repeatCustomDays.length; i++) {
167                     wkDay = {};
168                     wkDay.day = this.repeatCustomDays[i];
169                     if (this.repeatCustomOrdinal) {
170                         wkDay.ordwk = this.repeatCustomOrdinal;
171                     }
172                     bwd.wkday.push(wkDay);
173                 }
174             }
175 
176             if (this.repeatCustomOrdinal == null) {
177                 bysetpos = rule.bysetpos = {};
178                 bysetpos.poslist = this.repeatBySetPos;
179             }
180         }
181     }
182 	else if (this.repeatType == ZmRecurrence.YEARLY) {
183 		bm = rule.bymonth = {};
184 		bm.molist = this.repeatYearlyMonthsList;
185 		if (this.repeatCustomType == "O") {
186 			bwd = rule.byday = {};
187             bwd.wkday = [];
188             if(this.repeatCustomDays) {
189                 for(i=0; i < this.repeatCustomDays.length; i++) {
190                     wkDay = {};
191                     wkDay.day = this.repeatCustomDays[i];
192                     if (this.repeatCustomOrdinal) {
193                         wkDay.ordwk = this.repeatCustomOrdinal;
194                     }
195                     bwd.wkday.push(wkDay);
196                 }
197             }
198 
199             if(this.repeatCustomOrdinal == null) {
200                 bysetpos = rule.bysetpos = {};
201                 bysetpos.poslist = this.repeatBySetPos;
202             }
203 
204         } else {
205 			bmd = rule.bymonthday = {};
206 			bmd.modaylist = this.repeatCustomMonthDay;
207 		}
208 
209 	}
210 
211     this.setExcludes(recur);
212 };
213 
214 ZmRecurrence.prototype.setExcludes =
215 function(recur) {
216     if (!this._cancelRecurIds) {
217         return;
218     }
219 
220     var exclude,
221         dates,
222         i,
223         ridZ,
224         dtval,
225         s;
226 
227     for (i in this._cancelRecurIds) {
228 
229         if (!this._cancelRecurIds[i]) {
230             continue;
231         }
232 
233         if (!exclude && !dates) {
234             exclude = recur.exclude = {};
235             dates = exclude.dates = {};
236             // Fix for bug: 77998, 84054. Object was missing child element dtval as per soap doc.
237             dates.dtval = [];
238         }
239 
240         ridZ = i;
241         dtval = {};
242         s = dtval.s = {};
243         s.d = ridZ;
244         // dtval should hold list of timestamps for conflicting appointments.
245         dates.dtval.push(dtval);
246     }
247 };
248 
249 /**
250  * Gets the recurrence blurb.
251  * 
252  * @return	{String}	the blurb text
253  */
254 ZmRecurrence.prototype.getBlurb =
255 function() {
256 	if (this.repeatType == ZmRecurrence.NONE)
257 		return "";
258 
259 	var every = [];
260 	switch (this.repeatType) {
261 		case ZmRecurrence.DAILY: {
262 			if (this.repeatCustom == "1" && this.repeatWeekday) {
263 				every.push(ZmMsg.recurDailyEveryWeekday);
264 			} else if (this.repeatCustomCount == 1) {
265 				every.push(ZmMsg.recurDailyEveryDay);
266 			} else {
267 				var formatter = new AjxMessageFormat(ZmMsg.recurDailyEveryNumDays);
268 				every.push(formatter.format(this.repeatCustomCount));
269 			}
270 			break;
271 		}
272 		case ZmRecurrence.WEEKLY: {
273 			if (this.repeatCustomCount == 1 && this.repeatWeeklyDays.length == 1) {
274 				var dayofweek = AjxUtil.indexOf(ZmCalItem.SERVER_WEEK_DAYS, this.repeatWeeklyDays[0]);
275 				var date = new Date();
276 				date.setDate(date.getDate() - date.getDay() + dayofweek);
277 
278 				var formatter = new AjxMessageFormat(ZmMsg.recurWeeklyEveryWeekday);
279 				every.push(formatter.format(date));
280 			} else {
281 				var weekdays = [];
282 				for (var i = 0; i < this.repeatWeeklyDays.length; i++) {
283 					var dayofweek = AjxUtil.indexOf(ZmCalItem.SERVER_WEEK_DAYS, this.repeatWeeklyDays[i]);
284 					var date = new Date();
285 					date.setDate(date.getDate() - date.getDay() + dayofweek);
286 					weekdays.push(date);
287 				}
288 
289 				var formatter = new AjxMessageFormat(ZmMsg.recurWeeklyEveryNumWeeksDate);
290 				every.push(formatter.format([ this.repeatCustomCount, weekdays, "" ]));
291 			}
292 			break;
293 		}
294 		case ZmRecurrence.MONTHLY: {
295 			if (this.repeatCustomType == "S") {
296 				var count = Number(this.repeatCustomCount);
297 				var date = Number(this.repeatMonthlyDayList[0]);
298 
299 				var formatter = new AjxMessageFormat(ZmMsg.recurMonthlyEveryNumMonthsDate);
300 				every.push(formatter.format([ date, count ]));
301 			} else {
302 				var ordinal = Number(this.repeatCustomOrdinal);
303                 var bysetpos = Number(this.repeatBySetPos);                                
304                 var dayofweek = AjxUtil.indexOf(ZmCalItem.SERVER_WEEK_DAYS, this.repeatCustomDayOfWeek);
305 				var day = new Date();
306 				day.setDate(day.getDate() - day.getDay() + dayofweek);
307                 var count = Number(this.repeatCustomCount);
308 
309                 var days = this.repeatCustomDays.join(",");
310                 var workWeekDays = ZmCalItem.SERVER_WEEK_DAYS.slice(1,6).join(","); 
311                 var weekEndDays = [ZmCalItem.SERVER_WEEK_DAYS[AjxDateUtil.SUNDAY], ZmCalItem.SERVER_WEEK_DAYS[AjxDateUtil.SATURDAY]].join(",");
312 
313                 //if both values are present and unequal give preference to repeatBySetPos
314                 if (this.repeatCustomOrdinal != null &&
315                     this.repeatBySetPos != null &&
316                     this.repeatCustomOrdinal != this.repeatBySetPos) {
317                     this.repeatCustomOrdinal = this.repeatBySetPos;
318                 }
319 
320                 if((ZmCalItem.SERVER_WEEK_DAYS.join(",") == days) || (workWeekDays == days) || (weekEndDays == days)) {
321                     var formatter = new AjxMessageFormat(ZmMsg.recurMonthlyEveryNumMonthsWeekDays);
322                     var dayType = -1;
323                     if(workWeekDays == days) {
324                         dayType = 1;
325                     }else if(weekEndDays == days) {
326                         dayType = 0;
327                     }
328                     every.push(formatter.format([ bysetpos || ordinal, dayType, count ]));
329                 }else {
330                     var day = new Date();
331                     day.setDate(day.getDate() - day.getDay() + dayofweek);
332                     var formatter = new AjxMessageFormat(ZmMsg.recurMonthlyEveryNumMonthsNumDay);
333                     every.push(formatter.format([ bysetpos || ordinal, day, count ]));
334                 }
335 			}
336 			break;
337 		}
338 		case ZmRecurrence.YEARLY: {
339 			if (this.repeatCustomType == "S") {
340 				var month = new Date();
341 				month.setMonth(Number(this.repeatYearlyMonthsList) - 1);
342 				var day = Number(this.repeatCustomMonthDay);
343 
344 				var formatter = new AjxMessageFormat(ZmMsg.recurYearlyEveryDate);
345 				every.push(formatter.format([ month, day ]));
346 			} else {
347 				var ordinal = Number(this.repeatCustomOrdinal);
348                 var bysetpos = Number(this.repeatBySetPos);                
349                 var dayofweek = AjxUtil.indexOf(ZmCalItem.SERVER_WEEK_DAYS, this.repeatCustomDayOfWeek);
350                 var month = new Date();
351                 month.setMonth(Number(this.repeatYearlyMonthsList)-1);
352 
353                 var days = this.repeatCustomDays.join(",");
354                 var workWeekDays = ZmCalItem.SERVER_WEEK_DAYS.slice(1,6).join(",");
355                 var weekEndDays = [ZmCalItem.SERVER_WEEK_DAYS[AjxDateUtil.SUNDAY], ZmCalItem.SERVER_WEEK_DAYS[AjxDateUtil.SATURDAY]].join(",");
356 
357                 if((ZmCalItem.SERVER_WEEK_DAYS.join(",") == days) || (workWeekDays == days) || (weekEndDays == days)) {
358                     var formatter = new AjxMessageFormat(ZmMsg.recurYearlyEveryMonthWeekDays);
359                     var dayType = -1;
360                     if(workWeekDays == days) {
361                         dayType = 1;
362                     }else if(weekEndDays == days) {
363                         dayType = 0;
364                     }
365                     every.push(formatter.format([ bysetpos || ordinal, dayType, month ]));
366                 }else {
367 
368                     var day = new Date();
369                     day.setDate(day.getDate() - day.getDay() + dayofweek);
370                     var formatter = new AjxMessageFormat(ZmMsg.recurYearlyEveryMonthNumDay);
371                     every.push(formatter.format([ bysetpos || ordinal, day, month ]));
372                 }
373             }
374 			break;
375 		}
376 	}
377 
378 	// start
379 	var start = [];
380 	var formatter = new AjxMessageFormat(ZmMsg.recurStart);
381 	start.push(formatter.format(this._startDate));
382 
383 	// end
384 	var end = [];
385 	switch (this.repeatEndType) {
386 		case "N": {
387 			end.push(ZmMsg.recurEndNone);
388 			break;
389 		}
390 		case "A": {
391 			formatter = new AjxMessageFormat(ZmMsg.recurEndNumber);
392 			end.push(formatter.format(this.repeatEndCount));
393 			break;
394 		}
395 		case "D": {
396 			formatter = new AjxMessageFormat(ZmMsg.recurEndByDate);
397 			end.push(formatter.format(this.repeatEndDate));
398 			break;
399 		}
400 	}
401 
402 	// join all three together
403 	if (every.length > 0) {
404 		every.push(".  ");
405 	}
406 	if (end.length > 0) {
407 		end.push(".  ");
408 	}
409 	formatter = new AjxMessageFormat(ZmMsg.recurBlurb);
410 	return formatter.format([ every.join(""), end.join(""), start.join("") ]);
411 };
412 
413 ZmRecurrence.prototype.parse =
414 function(recurRules) {
415 	// bug 16513: This array never gets cleaned.
416 	// TODO: Maybe this needs a flag so it doesn't reparse?
417 	this.repeatWeeklyDays = [];
418 
419 	for (var k = 0; k < recurRules.length ; ++k) {
420 		var adds = recurRules[k].add;
421 		if (!adds) continue;
422 
423 		this.repeatYearlyMonthsList = this._startDate.getMonth() + 1;
424 		for (var i = 0; i < adds.length; ++i) {
425 			var rules = adds[i].rule;
426 			if (!rules) continue;
427 
428 			for (var j = 0; j < rules.length; ++j) {
429 				var rule = rules[j];
430 				if (rule.freq) {
431 					this.repeatType = rule.freq.substring(0,3);
432 					if (rule.interval && rule.interval[0].ival) {
433 						this.repeatCustomCount = parseInt(rule.interval[0].ival);
434 						this.repeatCustom = "1";
435 					}
436 				}
437 				if (rule.bymonth) {
438 					this.repeatYearlyMonthsList = rule.bymonth[0].molist;
439 					this.repeatCustom = "1";
440 				}
441 				if (rule.bymonthday) {
442 					if (this.repeatType == ZmRecurrence.YEARLY) {
443 						this.repeatCustomMonthDay = rule.bymonthday[0].modaylist;
444 						this.repeatCustomType = "S";
445 					} else if (this.repeatType == ZmRecurrence.MONTHLY){
446 						this.repeatMonthlyDayList = rule.bymonthday[0].modaylist.split(",");
447 					}
448 					this.repeatCustom = "1";
449 				}
450 				if (rule.byday && rule.byday[0] && rule.byday[0].wkday) {
451 					this.repeatCustom = "1";
452 					var wkday = rule.byday[0].wkday;
453 					if (this.repeatType == ZmRecurrence.WEEKLY || (this.repeatType == ZmRecurrence.DAILY && wkday.length == 5)) {
454 						this.repeatWeekday = this.repeatType == ZmRecurrence.DAILY;
455 						for (var x = 0; x < wkday.length; ++x) {
456 							this.repeatWeeklyDays.push(wkday[x].day);
457 						}
458 					} else {
459 						this.repeatCustomDayOfWeek = wkday[0].day;
460                         var days = [];
461                         for(var i = 0; i < wkday.length; i++) {
462                             days.push(wkday[i].day);
463                         }
464                         this.repeatCustomDays = days;                        
465                         this.repeatCustomOrdinal = wkday[0].ordwk;
466                         this.repeatBySetPos  = (rule.bysetpos && (rule.bysetpos.length > 0)) ? rule.bysetpos[0].poslist : null;
467                         //ical sends only ordwk in recurrence rule, we follow outlook behavior in setting repeatbysetpos instead of ordwk
468                         if(this.repeatBySetPos == null && this.repeatCustomOrdinal) {
469                             this.repeatBySetPos  = this.repeatCustomOrdinal; 
470                         }
471                         this.repeatCustomType = "O";
472 					}
473 				}
474 				if (rule.until) {
475 					this.repeatEndType = "D";
476 					this.repeatEndDate = AjxDateUtil.parseServerDateTime(rule.until[0].d);
477 				} else if (rule.count) {
478 					this.repeatEndType = "A";
479 					this.repeatEndCount = rule.count[0].num;
480 				}
481 			}
482 		}
483 	}
484 };
485 
486 ZmRecurrence.prototype.setRecurrenceStartTime =
487 function(startTime) {
488 	
489 	this._startDate.setTime(startTime);
490     this.repeatCustomMonthDay	= this._startDate.getDate();    
491 	
492 	if (this.repeatType == ZmRecurrence.NONE) return;
493 
494 	//if (this.repeatCustom != "0")
495 		//return;
496 
497  	if (this.repeatType == ZmRecurrence.WEEKLY) {
498 		this.repeatWeeklyDays = [ZmCalItem.SERVER_WEEK_DAYS[this._startDate.getDay()]];
499 	} else if (this.repeatType == ZmRecurrence.MONTHLY) {
500 		this.repeatMonthlyDayList = [this._startDate.getDate()];
501     } else if (this.repeatType == ZmRecurrence.YEARLY) {
502 		this.repeatYearlyMonthsList = this._startDate.getMonth() + 1;	
503 	}
504 };
505 
506 ZmRecurrence.prototype.setRecurrenceRules =
507 function(recurRules, startDate) {
508 
509     if (recurRules)
510         this.parse(recurRules);    
511 
512     if(!startDate) return;
513 
514     if (this.repeatWeeklyDays == null) {
515         this.repeatWeeklyDays = [ZmCalItem.SERVER_WEEK_DAYS[startDate.getDay()]];
516     }
517     if (this.repeatMonthlyDayList == null) {
518         this.repeatMonthlyDayList = [startDate.getDate()];
519     }
520 
521 };
522 
523 ZmRecurrence.prototype.addCancelRecurId =
524 function(ridZ) {
525     this._cancelRecurIds[ridZ] = true;        
526 };
527 
528 ZmRecurrence.prototype.resetCancelRecurIds =
529 function(   ) {
530     this._cancelRecurIds = {};
531 };
532 
533 ZmRecurrence.prototype.isInstanceCanceled =
534 function(ridZ) {
535     return this._cancelRecurIds[ridZ];
536 };
537 
538 ZmRecurrence.prototype.removeCancelRecurId =
539 function(ridZ) {
540     this._cancelRecurIds[ridZ] = null;
541 };
542 
543 ZmRecurrence.prototype.parseExcludeInfo =
544 function(recurInfo) {
545 
546     if (!recurInfo) { return; }
547 
548     for(var i=0; i < recurInfo.length; i++) {
549         var excludeInfo = (recurInfo[i] && recurInfo[i].exclude) ? recurInfo[i].exclude : null;
550         if(!excludeInfo) continue;
551         for(var j=0; j < excludeInfo.length; j++) {
552             var datesInfo = excludeInfo[j].dates;
553             if(datesInfo) this._parseExcludeDates(datesInfo);
554         }
555     }
556 
557 };
558 
559 ZmRecurrence.prototype._parseExcludeDates =
560 function(datesInfo) {
561     for(var j=0; j < datesInfo.length; j++) {
562 
563         var dtval = datesInfo[j].dtval;
564         if(!dtval) continue;
565 
566         for(var k=0; k < dtval.length; k++) {
567             var dinfo = dtval[k];
568             var excludeDate = (dinfo && dinfo.s) ? dinfo.s[0].d : null;
569             if(excludeDate) this.addCancelRecurId(excludeDate);
570         }
571     }
572 };
573 
574