diff --git a/modules/default/calendar/vendor/ical.js/ical.js b/modules/default/calendar/vendor/ical.js/ical.js
index 8f0c532b..024625b7 100644
--- a/modules/default/calendar/vendor/ical.js/ical.js
+++ b/modules/default/calendar/vendor/ical.js/ical.js
@@ -33,9 +33,9 @@
for (var i = 0; i
-1){
var segs = p[i].split('=');
-
+
out[segs[0]] = parseValue(segs.slice(1).join('='));
-
+
}
}
return out || sp
@@ -44,7 +44,7 @@
var parseValue = function(val){
if ('TRUE' === val)
return true;
-
+
if ('FALSE' === val)
return false;
@@ -55,75 +55,52 @@
return val;
}
- var storeParam = function(name){
- return function(val, params, curr){
- var data;
- if (params && params.length && !(params.length==1 && params[0]==='CHARSET=utf-8')){
- data = {params:parseParams(params), val:text(val)}
- }
- else
- data = text(val)
+ var storeValParam = function (name) {
+ return function (val, curr) {
+ var current = curr[name];
+ if (Array.isArray(current)) {
+ current.push(val);
+ return curr;
+ }
- var current = curr[name];
- if (Array.isArray(current)){
- current.push(data);
- return curr;
- }
+ if (current != null) {
+ curr[name] = [current, val];
+ return curr;
+ }
- if (current != null){
- curr[name] = [current, data];
- return curr;
+ curr[name] = val;
+ return curr
}
-
- curr[name] = data;
- return curr
- }
}
- var addTZ = function(dt, params){
+ var storeParam = function (name) {
+ return function (val, params, curr) {
+ var data;
+ if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) {
+ data = { params: parseParams(params), val: text(val) }
+ }
+ else
+ data = text(val)
+
+ return storeValParam(name)(data, curr);
+ }
+ }
+
+ var addTZ = function (dt, params) {
var p = parseParams(params);
- if (params && p && dt){
+ if (params && p){
dt.tz = p.TZID
}
return dt
}
- var parseTimestamp = function(val){
- //typical RFC date-time format
- var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
- if (comps !== null) {
- if (comps[7] == 'Z'){ // GMT
- return new Date(Date.UTC(
- parseInt(comps[1], 10),
- parseInt(comps[2], 10)-1,
- parseInt(comps[3], 10),
- parseInt(comps[4], 10),
- parseInt(comps[5], 10),
- parseInt(comps[6], 10 )
- ));
- // TODO add tz
- } else {
- return new Date(
- parseInt(comps[1], 10),
- parseInt(comps[2], 10)-1,
- parseInt(comps[3], 10),
- parseInt(comps[4], 10),
- parseInt(comps[5], 10),
- parseInt(comps[6], 10)
- );
- }
- }
-
- return undefined;
- }
-
var dateParam = function(name){
- return function(val, params, curr){
+ return function (val, params, curr) {
+
+ var newDate = text(val);
- // Store as string - worst case scenario
- storeParam(name)(val, undefined, curr)
if (params && params[0] === "VALUE=DATE") {
// Just Date
@@ -131,47 +108,54 @@
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val);
if (comps !== null) {
// No TZ info - assume same timezone as this computer
- curr[name] = new Date(
+ newDate = new Date(
comps[1],
parseInt(comps[2], 10)-1,
comps[3]
);
- curr[name] = addTZ(curr[name], params);
- return curr;
+ newDate = addTZ(newDate, params);
+ newDate.dateOnly = true;
+
+ // Store as string - worst case scenario
+ return storeValParam(name)(newDate, curr)
}
}
- curr[name] = []
- val.split(',').forEach(function(val){
- var newDate = parseTimestamp(val);
- curr[name].push(addTZ(newDate, params));
- });
- if (curr[name].length === 0){
- delete curr[name];
- } else if (curr[name].length === 1){
- curr[name] = curr[name][0];
- }
+ //typical RFC date-time format
+ var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
+ if (comps !== null) {
+ if (comps[7] == 'Z'){ // GMT
+ newDate = new Date(Date.UTC(
+ parseInt(comps[1], 10),
+ parseInt(comps[2], 10)-1,
+ parseInt(comps[3], 10),
+ parseInt(comps[4], 10),
+ parseInt(comps[5], 10),
+ parseInt(comps[6], 10 )
+ ));
+ // TODO add tz
+ } else {
+ newDate = new Date(
+ parseInt(comps[1], 10),
+ parseInt(comps[2], 10)-1,
+ parseInt(comps[3], 10),
+ parseInt(comps[4], 10),
+ parseInt(comps[5], 10),
+ parseInt(comps[6], 10)
+ );
+ }
- return curr;
+ newDate = addTZ(newDate, params);
}
+
+
+ // Store as string - worst case scenario
+ return storeValParam(name)(newDate, curr)
+ }
}
- var exdateParam = function(name){
- return function(val, params, curr){
- var date = dateParam(name)(val, params, curr);
- if (date.exdates === undefined) {
- date.exdates = [];
- }
- if (Array.isArray(date.exdate)){
- date.exdates = date.exdates.concat(date.exdate);
- } else {
- date.exdates.push(date.exdate);
- }
- return date;
- }
- }
var geoParam = function(name){
return function(val, params, curr){
@@ -195,7 +179,52 @@
}
}
- var addFBType = function(fb, params){
+ // EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4").
+ // The EXDATE entry itself can also contain a comma-separated list, so we make sure to parse each date out separately.
+ // There can also be more than one EXDATE entries in a calendar record.
+ // Since there can be multiple dates, we create an array of them. The index into the array is the ISO string of the date itself, for ease of use.
+ // i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception.
+ // NOTE: This specifically uses date only, and not time. This is to avoid a few problems:
+ // 1. The ISO string with time wouldn't work for "floating dates" (dates without timezones).
+ // ex: "20171225T060000" - this is supposed to mean 6 AM in whatever timezone you're currently in
+ // 2. Daylight savings time potentially affects the time you would need to look up
+ // 3. Some EXDATE entries in the wild seem to have times different from the recurrence rule, but are still excluded by calendar programs. Not sure how or why.
+ // These would fail any sort of sane time lookup, because the time literally doesn't match the event. So we'll ignore time and just use date.
+ // ex: DTSTART:20170814T140000Z
+ // RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
+ // EXDATE:20171219T060000
+ // Even though "T060000" doesn't match or overlap "T1400000Z", it's still supposed to be excluded? Odd. :(
+ // TODO: See if this causes any problems with events that recur multiple times a day.
+ var exdateParam = function (name) {
+ return function (val, params, curr) {
+ var separatorPattern = /\s*,\s*/g;
+ curr[name] = curr[name] || [];
+ var dates = val ? val.split(separatorPattern) : [];
+ dates.forEach(function (entry) {
+ var exdate = new Array();
+ dateParam(name)(entry, params, exdate);
+
+ if (exdate[name])
+ {
+ if (typeof exdate[name].toISOString === 'function') {
+ curr[name][exdate[name].toISOString().substring(0, 10)] = exdate[name];
+ } else {
+ console.error("No toISOString function in exdate[name]", exdate[name]);
+ }
+ }
+ }
+ )
+ return curr;
+ }
+ }
+
+ // RECURRENCE-ID is the ID of a specific recurrence within a recurrence rule.
+ // TODO: It's also possible for it to have a range, like "THISANDPRIOR", "THISANDFUTURE". This isn't currently handled.
+ var recurrenceParam = function (name) {
+ return dateParam(name);
+ }
+
+ var addFBType = function (fb, params) {
var p = parseParams(params);
if (params && p){
@@ -239,7 +268,7 @@
//scan all high level object in curr and drop all strings
var key,
obj;
-
+
for (key in curr) {
if(curr.hasOwnProperty(key)) {
obj = curr[key];
@@ -248,14 +277,93 @@
}
}
}
-
+
return curr
}
-
+
var par = stack.pop()
if (curr.uid)
- par[curr.uid] = curr
+ {
+ // If this is the first time we run into this UID, just save it.
+ if (par[curr.uid] === undefined)
+ {
+ par[curr.uid] = curr;
+ }
+ else
+ {
+ // If we have multiple ical entries with the same UID, it's either going to be a
+ // modification to a recurrence (RECURRENCE-ID), and/or a significant modification
+ // to the entry (SEQUENCE).
+
+ // TODO: Look into proper sequence logic.
+
+ if (curr.recurrenceid === undefined)
+ {
+ // If we have the same UID as an existing record, and it *isn't* a specific recurrence ID,
+ // not quite sure what the correct behaviour should be. For now, just take the new information
+ // and merge it with the old record by overwriting only the fields that appear in the new record.
+ var key;
+ for (key in curr) {
+ par[curr.uid][key] = curr[key];
+ }
+
+ }
+ }
+
+ // If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id.
+ // To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences
+ // array. If it exists, then use the data from the calendar object in the recurrence instead of the parent
+ // for that day.
+
+ // NOTE: Sometimes the RECURRENCE-ID record will show up *before* the record with the RRULE entry. In that
+ // case, what happens is that the RECURRENCE-ID record ends up becoming both the parent record and an entry
+ // in the recurrences array, and then when we process the RRULE entry later it overwrites the appropriate
+ // fields in the parent record.
+
+ if (curr.recurrenceid != null)
+ {
+
+ // TODO: Is there ever a case where we have to worry about overwriting an existing entry here?
+
+ // Create a copy of the current object to save in our recurrences array. (We *could* just do par = curr,
+ // except for the case that we get the RECURRENCE-ID record before the RRULE record. In that case, we
+ // would end up with a shared reference that would cause us to overwrite *both* records at the point
+ // that we try and fix up the parent record.)
+ var recurrenceObj = new Object();
+ var key;
+ for (key in curr) {
+ recurrenceObj[key] = curr[key];
+ }
+
+ if (recurrenceObj.recurrences != undefined) {
+ delete recurrenceObj.recurrences;
+ }
+
+
+ // If we don't have an array to store recurrences in yet, create it.
+ if (par[curr.uid].recurrences === undefined) {
+ par[curr.uid].recurrences = new Array();
+ }
+
+ // Save off our cloned recurrence object into the array, keyed by date but not time.
+ // We key by date only to avoid timezone and "floating time" problems (where the time isn't associated with a timezone).
+ // TODO: See if this causes a problem with events that have multiple recurrences per day.
+ if (typeof curr.recurrenceid.toISOString === 'function') {
+ par[curr.uid].recurrences[curr.recurrenceid.toISOString().substring(0,10)] = recurrenceObj;
+ } else {
+ console.error("No toISOString function in curr.recurrenceid", curr.recurrenceid);
+ }
+ }
+
+ // One more specific fix - in the case that an RRULE entry shows up after a RECURRENCE-ID entry,
+ // let's make sure to clear the recurrenceid off the parent field.
+ if ((par[curr.uid].rrule != undefined) && (par[curr.uid].recurrenceid != undefined))
+ {
+ delete par[curr.uid].recurrenceid;
+ }
+
+ }
else
par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID
@@ -277,6 +385,11 @@
, 'COMPLETED': dateParam('completed')
, 'CATEGORIES': categoriesParam('categories')
, 'FREEBUSY': freebusyParam('freebusy')
+ , 'DTSTAMP': dateParam('dtstamp')
+ , 'CREATED': dateParam('created')
+ , 'LAST-MODIFIED': dateParam('lastmodified')
+ , 'RECURRENCE-ID': recurrenceParam('recurrenceid')
+
},
@@ -292,7 +405,7 @@
name = name.substring(2);
return (storeParam(name))(val, params, ctx, stack, line);
}
-
+
return storeParam(name.toLowerCase())(val, params, ctx);
},