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