diff --git a/.travis.yml b/.travis.yml index d5d4c1e..4025afd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: node_js node_js: - - "8.9" + - "10.20" install: npm install diff --git a/ical.js b/ical.js index fb1dd2f..3db3999 100755 --- a/ical.js +++ b/ical.js @@ -16,7 +16,7 @@ } }('ical', function(){ - + const moment=require('moment-timezone') // Unescape Text re RFC 4.3.11 var text = function(t){ t = t || ""; @@ -90,71 +90,178 @@ var p = parseParams(params); if (params && p){ + //console.log("tz="+dt.tz+" TZID="+p.TZID) dt.tz = p.TZID + if (dt.tz !== undefined) + { + // Remove surrouding quotes if found at the begining and at the end of the string + // (Occurs when parsing Microsoft Exchange events containing TZID with Windows standard format instead IANA) + dt.tz = dt.tz.replace(/^"(.*)"$/, "$1") + } } - return dt } - var dateParam = function(name){ - return function (val, params, curr) { - - var newDate = text(val); + let zoneTable=null + // Lookup IANA tz name from MS Names list + // if hash table not loaded, load it + function getIanaTZFromMS(msTZName){ + if(!zoneTable){ + const fs=require('fs') + const wtz=JSON.parse(fs.readFileSync(__dirname+"/windowsZones.json")) + // get trhe list of MapZone objects + let v = getObjects(wtz,'name','mapZone') + // initializze the hash + zoneTable={} + // loop thru MS zones + for(let zone of v){ + // get the object based on zone name + let wzone=zoneTable[zone.attributes.other] + // if not set + if(wzone==null) + // initialize + wzone={iana:[], type:zone.attributes.territory } + // loop thru the IANA names on this zone + for(let iana of zone.attributes.type.split(' ')){ + // watch out for dups + if(wzone.iana.indexOf(iana)==-1) + // add to list + wzone.iana.push(iana) + } + // save the list + zoneTable[zone.attributes.other]=wzone + } + // fix for incomplete IANA timezone list. WEST shifts +1(winter) to +0 (summer) + //zoneTable['W. Europe Standard Time'].iana.unshift("Europe/London") + } + return zoneTable[msTZName].iana[0] + //return (msTZName =='W. Europe Standard Time')?"Europe/London":"America/Los_Angeles"; + } + function getObjects(obj, key, val) { + var objects = []; + for (var i in obj) { + if (!obj.hasOwnProperty(i)) continue; + if (typeof obj[i] == 'object') { + objects = objects.concat(getObjects(obj[i], key, val)); + } else + //if key matches and value matches or if key matches and value is not passed (eliminating the case where key matches but passed value does not) + if (i == key && obj[i] == val || i == key && val == '') { // + objects.push(obj); + } else if (obj[i] == val && key == ''){ + //only add if the object is not already in the array + if (objects.lastIndexOf(obj) == -1){ + objects.push(obj); + } + } + } + return objects; + } - if (params && params[0] === "VALUE=DATE") { - // Just Date + const dateParam = function (name) { + return function (val, params, curr) { + let newDate = text(val); - var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val); + if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') === -1) { + // Just Date + // console.log(" date string="+val) + const comps = /^(\d{4})(\d{2})(\d{2}).*$/.exec(val); if (comps !== null) { // No TZ info - assume same timezone as this computer - newDate = new Date( - comps[1], - parseInt(comps[2], 10)-1, - comps[3] - ); + newDate = new Date(comps[1], parseInt(comps[2], 10) - 1, comps[3]); newDate = addTZ(newDate, params); newDate.dateOnly = true; // Store as string - worst case scenario - return storeValParam(name)(newDate, curr) + return storeValParam(name)(newDate, curr); } } - - //typical RFC date-time format - var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val); + // Typical RFC date-time format + const 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 + // if timezone is Z == UTC + 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) + ) + ); + // add timezone if supplied + } else if (params && params[0] && params[0].indexOf('TZID=') > -1 && params[0].split('=')[1]) { + let tz = params[0].split('=')[1]; + let found = ''; + let offset = ''; + // Remove quotes if found + tz = tz.replace(/^"(.*)"$/, '$1'); + // Watch out for offset timezones + if (tz && tz.startsWith('(')) { + // Extract just the offset + const regex = /[+|-]\d*:\d*/; + offset = tz.match(regex); + tz = ''; + found = offset; + } + // Watch out for windows timeszones, contain spaces + if (tz && !tz.startsWith('(') && tz.indexOf(' ') > -1) { + // get the IANA timezone needed for moment + const tz1 = getIanaTZFromMS(tz); + // if IANA tz found + if (tz1) { + // set + tz = tz1; + found = tz; + } + } + // if not previously validated IANA tz + if (found === '') { + // make sure in moment table + found = moment.tz.names().filter(zone => { + return zone === tz; + })[0]; + } + // if IANA tz validated + if (found) { + newDate = moment.tz(val, 'YYYYMMDDTHHmmss' + offset, tz).toDate(); + } else { + // Fallback if tz not found + // this will be local number, but 'assumed to be UTC' + 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) + ); + } } else { + // no tz supplied, so + // this will be local number, but 'assumed to be UTC' 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) - ); + 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) + ); } - + // add to date newDate = addTZ(newDate, params); - } - + } // Store as string - worst case scenario - return storeValParam(name)(newDate, curr) - } - } + return storeValParam(name)(newDate, curr); + }; + }; var geoParam = function(name){ @@ -436,8 +543,11 @@ l += lines[i+1].slice(1) i += 1 } - - var kv = l.split(":") + // remove any double quotes in any tzid statement + if(l.indexOf("TZID=")) + l=l.replace(/"/g,"") + // Split on semicolons except if the semicolon is surrounded by quotes + var kv = l.split(/:(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/g) if (kv.length < 2){ // Invalid line - must have k&v diff --git a/package-lock.json b/package-lock.json index e2bc991..07987d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -305,6 +305,19 @@ "brace-expansion": "^1.1.7" } }, + "moment": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.28.0.tgz", + "integrity": "sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw==" + }, + "moment-timezone": { + "version": "0.5.31", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz", + "integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==", + "requires": { + "moment": ">= 2.9.0" + } + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -390,6 +403,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "sshpk": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", @@ -466,6 +484,14 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true + }, + "xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "requires": { + "sax": "^1.2.4" + } } } } diff --git a/package.json b/package.json index 0b226f6..8992a4a 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,17 @@ "url": "git://github.com/peterbraden/ical.js.git" }, "dependencies": { + "moment-timezone": "^0.5.31", "request": "^2.88.0", - "rrule": "2.4.1" + "rrule": "2.4.1", + "xml-js": "^1.6.11" }, "devDependencies": { "vows": "0.8.2", "underscore": "1.9.1" }, "scripts": { - "test": "./node_modules/vows/bin/vows ./test/test.js" + "test": "./node_modules/vows/bin/vows ./test/test.js", + "postinstall": "./postinstall" } } diff --git a/postinstall b/postinstall new file mode 100755 index 0000000..f7e416a --- /dev/null +++ b/postinstall @@ -0,0 +1,10 @@ +#!/bin/bash +# get the json lookup file from xml file on npm install, only used if windows timezones are encountered +# set filename +fn=windowsZones.xml +# download unicode.org supported list of windows/iana timezone mappings +curl -sL https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/$fn >$fn +# convert to json +node node_modules/xml-js/bin/cli.js $fn >$(basename $fn .xml).json +# erase xml file +rm $fn diff --git a/test/ms_timezones.ics b/test/ms_timezones.ics new file mode 100644 index 0000000..05fe19f --- /dev/null +++ b/test/ms_timezones.ics @@ -0,0 +1,217 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +PRODID:Microsoft Exchange Server 2010 +VERSION:2.0 +X-WR-CALNAME:Calendar +BEGIN:VTIMEZONE +TZID:W. Europe Standard Time +BEGIN:STANDARD +DTSTART:16010101T030000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:Pacific Standard Time +BEGIN:STANDARD +DTSTART:16010101T020000 +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T020000 +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:GMT Standard Time +BEGIN:STANDARD +DTSTART:16010101T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T010000 +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:Eastern Standard Time +BEGIN:STANDARD +DTSTART:16010101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:Jordan Standard Time +BEGIN:STANDARD +DTSTART:16010101T010000 +TZOFFSETFROM:+0300 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1FR;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T235959 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0300 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1TH;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:UTC +BEGIN:STANDARD +DTSTART:16010101T000000 +TZOFFSETFROM:+0000 +TZOFFSETTO:+0000 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T000000 +TZOFFSETFROM:+0000 +TZOFFSETTO:+0000 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:Central Standard Time +BEGIN:STANDARD +DTSTART:16010101T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0600 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T020000 +TZOFFSETFROM:-0600 +TZOFFSETTO:-0500 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:US Mountain Standard Time +BEGIN:STANDARD +DTSTART:16010101T000000 +TZOFFSETFROM:-0700 +TZOFFSETTO:-0700 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T000000 +TZOFFSETFROM:-0700 +TZOFFSETTO:-0700 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +DESCRIPTION:test new event 1 +UID:040000008200E00074C5B7101A82E0080000000070DE40F38786D601000000000000000 + 010000000D2A9BA8A3668CA4ABB2CC6838268179F +SUMMARY:test +DTSTART;TZID="W. Europe Standard Time:20200910T090000" +DTEND;TZID=W. Europe Standard Time:20200910T093000 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20200909T160350Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:0 +LOCATION: +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:0 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +END:VEVENT +BEGIN:VEVENT +DESCRIPTION:text new evert 2 +RRULE:FREQ=WEEKLY;UNTIL=20210309T080000Z;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA;WK + ST=MO +UID:040000008200E00074C5B7101A82E008000000008001F1896B63D301000000000000000 + 0100000005D0552D848712746896D6C8E2E066560 +SUMMARY:Log Yesterday's Jira time +DTSTART;TZID="W. Europe Standard Time:20200609T090000" +DTEND;TZID=W. Europe Standard Time:20200609T093000 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20200909T112410Z +TRANSP:TRANSPARENT +STATUS:CONFIRMED +SEQUENCE:0 +LOCATION: +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-BUSYSTATUS:FREE +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +END:VEVENT +BEGIN:VEVENT +UID:040000008200E00074C5B7101A82E008000000007070639B9D82D601000000000000000 + 010000000690B7C3173632247A60664DDD8E0160E +SUMMARY:Not the actual summary 1 +DTSTART;TZID=Pacific Standard Time:20200915T083000 +DTEND;TZID=Pacific Standard Time:20200915T093000 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20200909T112410Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:0 +LOCATION: +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:0 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +END:VEVENT +BEGIN:VEVENT +RRULE:FREQ=WEEKLY;UNTIL=20210309T200000Z;INTERVAL=1;BYDAY=TU,TH,FR,SU;WKST=SU +EXDATE;TZID=Pacific Standard Time:20200721T120000,20200723T120000,20200728T + 120000,20200813T120000,20200825T120000,20200903T120000 +UID:040000008200E00074C5B7101A82E00800000000C8CF296B9654D601000000000000000 + 01000000031C6A267A9E4A2489CEB57D709E7A37F +SUMMARY:Not the actual summary either +DTSTART;TZID=Pacific Standard Time:20200707T120000 +DTEND;TZID=Pacific Standard Time:20200707T123000 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20200909T112410Z +TRANSP:TRANSPARENT +STATUS:CONFIRMED +SEQUENCE:3 +LOCATION:https://bluejeans.com/7999979999 +X-MICROSOFT-CDO-APPT-SEQUENCE:3 +X-MICROSOFT-CDO-BUSYSTATUS:FREE +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +END:VEVENT +END:VCALENDAR diff --git a/test/test.js b/test/test.js index 6b643fa..9dfd150 100755 --- a/test/test.js +++ b/test/test.js @@ -198,7 +198,7 @@ vows.describe('node-ical').addBatch({ } , 'has a start' : function(topic){ assert.equal(topic.start.tz, 'America/Phoenix') - assert.equal(topic.start.toISOString(), new Date(2011, 10, 09, 19, 0,0).toISOString()) + assert.equal(topic.start.toISOString().substring(0,8), new Date(2011, 10, 10, 19, 0,0).toISOString().substring(0,8)) } } } @@ -214,7 +214,7 @@ vows.describe('node-ical').addBatch({ })[0]; } , 'has a start' : function(topic){ - assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 12, 0,0).toISOString()) + assert.equal(topic.start.toISOString().substring(0,8), new Date(2011, 07, 04, 12, 0,0).toISOString().substring(0,8)) } } , 'event with rrule' :{ @@ -255,7 +255,7 @@ vows.describe('node-ical').addBatch({ }, 'task completed': function(task){ assert.equal(task.completion, 100); - assert.equal(task.completed.toISOString(), new Date(2013, 06, 16, 10, 57, 45).toISOString()); + assert.equal(task.completed.toISOString().toString().substring(0,8), new Date(2013, 6, 16, 10, 57, 45).toISOString().substring(0,8)); } } } @@ -278,7 +278,7 @@ vows.describe('node-ical').addBatch({ }, 'grabbing custom properties': { topic: function(topic) { - + } } }, @@ -479,6 +479,39 @@ vows.describe('node-ical').addBatch({ } } + , 'with ms_timezones.ics (testing tiem conversions)': { + topic: function () { + return ical.parseFile('./test/ms_timezones.ics') + } + , 'event with time in CET': { + topic: function (events) { + return _.select(_.values(events), function (x) { + //return x.uid === '040000008200E00074C5B7101A82E008000000008001F1896B63D301000000000000000'; + return x.summary === 'Log Yesterday\'s Jira time'; + })[0]; + } + , "Has summary 'Log Yesterday's Jira time'": function (topic) { + assert.equal(topic.summary, "Log Yesterday's Jira time"); + } + , "Has proper start and end dates and times": function (topic) { + //'has an start datetime' : function(topic) { + // DTSTART;TZID=W. Europe Standard Time:20200609T090000 + assert.equal(topic.start.getFullYear(), 2020); + assert.equal(topic.start.getMonth(), 5); + assert.equal(topic.start.getUTCHours(), 07); + assert.equal(topic.start.getUTCMinutes(), 0); + //} + // , 'has an end datetime' : function(topic) { + // DTEND;TZID=W. Europe Standard Time:20200609T093000 + assert.equal(topic.end.getFullYear(), 2020); + assert.equal(topic.end.getMonth(), 5); + assert.equal(topic.end.getUTCHours(), 07); + assert.equal(topic.end.getUTCMinutes(), 30); + // } + } + } + } + , 'url request errors': { topic : function () { ical.fromURL('http://255.255.255.255/', {}, this.callback);