diff --git a/ChangeLog b/ChangeLog index 5fb995b..24e942c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +0.3.25: + o fixed: recurring events with exceptions could be added multiple times. Deleting one of them deleted the server event. + o change: (BIG CHANGE!) Now using event timezone id, again. Solves issues with daylight saving times and recurrences. + Please test this carefully. In local tests with Google, ownclound and eGroupware results are better than before. + One issue remains, but this is a bug in the calendar app: + For events from a foreing timezone exceptions are not handled properly and the original occurence will + show in the calendar in addition to possible differing occurences or even though the occurence it has been delete completely. +2015-03-03: Achim Königs + +0.3.24: + o fixed issue with event exceptions not being upsynced properly and possibly lost afterwards. +2015-03-03: Achim Königs + 0.3.23: o App: stats and status display improvements o refresh Auth on 401 errors for OAuth and Digest Auth diff --git a/app/app/assistants/check-status-assistant.js b/app/app/assistants/check-status-assistant.js index aaafba5..7577bda 100644 --- a/app/app/assistants/check-status-assistant.js +++ b/app/app/assistants/check-status-assistant.js @@ -173,9 +173,9 @@ CheckStatusAssistant.prototype.processStatus = function (status) { this.upNumbersDisplay.innerHTML = "No uploads"; } if (stat.downloadTotal) { - this.upNumbersDisplay.innerHTML = "Downloading " + (stat.downloadsDone || 0) + " of " + stat.downloadTotal; + this.downNumbersDisplay.innerHTML = "Downloading " + (stat.downloadsDone || 0) + " of " + stat.downloadTotal; } else { - this.upNumbersDisplay.innerHTML = "No downloads"; + this.downNumbersDisplay.innerHTML = "No downloads"; } } } diff --git a/package/packageinfo.json b/package/packageinfo.json index 585f16b..7c5b2cb 100644 --- a/package/packageinfo.json +++ b/package/packageinfo.json @@ -2,7 +2,7 @@ "id": "org.webosports.cdav", "package_format_version": 2, "loc_name": "C+DAV synergy connector", - "version": "0.3.23", + "version": "0.3.25", "vendor": "WebOS Ports - Stefan Schmidt", "vendorurl": "www.webos-ports.org", "app": "org.webosports.cdav.app", diff --git a/service/javascript/assistants/addeventassistant.js b/service/javascript/assistants/addeventassistant.js deleted file mode 100644 index 0b7644c..0000000 --- a/service/javascript/assistants/addeventassistant.js +++ /dev/null @@ -1,103 +0,0 @@ -/*jslint node: true, nomen: true */ -/*global Log, DB, checkResult, fs, Future, servicePath, Kinds */ - -var iCal = require(servicePath + "/javascript/utils/iCal.js"); -var CalendarEventHandler = require(servicePath + "/javascript/utils/CalendarEventHandler.js"); - -var AddEventAssistant = function () { "use strict"; }; - -function removeIds(obj) { - "use strict"; - var key; - for (key in obj) { - if (obj.hasOwnProperty(key)) { - if (key === "_id") { - delete obj[key]; - } else if (typeof obj[key] === "object") { - obj[key] = removeIds(obj[key]); - } - } - } - return obj; -} - -function parseAndPut(ics, filename) { - "use strict"; - var future = new Future(); - future.nest(iCal.parseICal(ics)); - - future.then(function () { - var result = checkResult(future), objs = []; - Log.log("parse result: ", result); - - if (result.returnValue === true) { - objs.push(result.result); - result.result.remoteId = filename; - if (result.hasExceptions) { - future.nest(CalendarEventHandler.fillParentIds(filename, result.result, result.exceptions)); - objs = objs.concat(result.exceptions); - } else { - future.result = {returnValue: true}; - } - } - - objs.forEach(function (event) { - event._kind = Kinds.objects.calendarevent.id; - }); - - future.then(function idCB() { - checkResult(future); - Log.debug("Putting: ", objs); - future.nest(DB.put(objs)); - }); - }); - - return future; -} - -AddEventAssistant.prototype.run = function (outerFuture) { - "use strict"; - var args = this.controller.args, filename, parse = false; - - if (args.json) { - filename = args.json; - } else if (args.ics) { - filename = args.ics; - parse = true; - } - - if (filename) { - fs.readFile(filename, function (err, data) { - if (err) { - Log.log("Could not read ", args.json); - } else { - - if (parse) { - parseAndPut(data.toString("utf8"), filename).then(function (future) { - var result = checkResult(future); - Log.log("Parse & Put Result: ", result); - outerFuture.result = result; - }); - } else { - var obj = JSON.parse(data), objs; - - obj = removeIds(obj); - - if (obj.length >= 0) { - objs = obj; - } else { - objs = [obj]; - } - - DB.put(objs).then(function (future) { - var result = checkResult(future); - Log.log("Put result: ", result); - outerFuture.result = result; - }); - } - } - }); - } - - return outerFuture; -}; diff --git a/service/javascript/assistants/additemassistant.js b/service/javascript/assistants/additemassistant.js new file mode 100644 index 0000000..bdc79e7 --- /dev/null +++ b/service/javascript/assistants/additemassistant.js @@ -0,0 +1,117 @@ +/*jslint node: true, nomen: true */ +/*global Log, DB, checkResult, fs, Future, libPath, Kinds, iCal */ + +var vCard = require(libPath + "vCard.js"); +var CalendarEventHandler = require(libPath + "CalendarEventHandler.js"); + +var AddItemAssistant = function () { "use strict"; }; + +function removeIds(obj) { + "use strict"; + var key; + for (key in obj) { + if (obj.hasOwnProperty(key)) { + if (key === "_id") { + delete obj[key]; + } else if (typeof obj[key] === "object") { + obj[key] = removeIds(obj[key]); + } + } + } + return obj; +} + +function parseAndPut(data, filename, contact) { + "use strict"; + var future = new Future(); + + if (contact) { + future.nest(vCard.parseVCard({account: { name: "addItemAssistant", + kind: Kinds.objects.contact.id }, + vCard: data})); + } else { + future.nest(iCal.parseICal(data)); + + future.then(function () { + var result = checkResult(future), objs = []; + Log.log("parse result: ", result); + + if (result.returnValue === true) { + objs.push(result.result); + result.result.remoteId = filename; + if (result.hasExceptions) { + future.nest(CalendarEventHandler.fillParentIds(filename, result.result, result.exceptions)); + objs = objs.concat(result.exceptions); + } else { + future.result = {returnValue: true}; + } + } + + objs.forEach(function (event) { + event._kind = Kinds.objects.calendarevent.id; + }); + + future.then(function idCB() { + checkResult(future); + Log.debug("Putting: ", objs); + future.nest(DB.put(objs)); + }); + }); + } + + return future; +} + +AddItemAssistant.prototype.run = function (outerFuture) { + "use strict"; + var args = this.controller.args, filename, parse = false, contact = false; + + if (args.json) { + filename = args.json; + if (args.contact) { + contact = true; + } + } else if (args.ics) { + filename = args.ics; + parse = true; + } else if (args.vcf) { + filename = args.vcf; + parse = true; + contact = true; + } + + if (filename) { + fs.readFile(filename, function (err, data) { + if (err) { + Log.log("Could not read ", filename); + } else { + + if (parse) { + parseAndPut(data.toString("utf8"), filename, contact).then(function (future) { + var result = checkResult(future); + Log.log("Parse & Put Result: ", result); + outerFuture.result = result; + }); + } else { + var obj = JSON.parse(data), objs; + + obj = removeIds(obj); + + if (obj.length >= 0) { + objs = obj; + } else { + objs = [obj]; + } + + DB.put(objs).then(function (future) { + var result = checkResult(future); + Log.log("Put result: ", result); + outerFuture.result = result; + }); + } + } + }); + } + + return outerFuture; +}; diff --git a/service/javascript/assistants/checkcredentialsassistant.js b/service/javascript/assistants/checkcredentialsassistant.js index a10279c..86775db 100644 --- a/service/javascript/assistants/checkcredentialsassistant.js +++ b/service/javascript/assistants/checkcredentialsassistant.js @@ -1,9 +1,9 @@ /*jslint nomen: true, node: true */ -/*global DB, searchAccountConfig, Future, Log, UrlSchemes, Transport, checkResult, servicePath */ +/*global DB, searchAccountConfig, Future, Log, UrlSchemes, Transport, checkResult, libPath */ /*exported checkCredentialsAssistant*/ -var KeyStore = require(servicePath + "/javascript/utils/KeyStore.js"); -var Base64 = require(servicePath + "/javascript/utils/Base64.js"); -var AuthManager = require(servicePath + "/javascript/utils/AuthManager.js"); +var KeyStore = require(libPath + "KeyStore.js"); +var Base64 = require(libPath + "Base64.js"); +var AuthManager = require(libPath + "AuthManager.js"); /* Validate contact username/password */ var checkCredentialsAssistant = function () { "use strict"; }; diff --git a/service/javascript/assistants/ondeleteassistant.js b/service/javascript/assistants/ondeleteassistant.js index 0027486..0828ad0 100644 --- a/service/javascript/assistants/ondeleteassistant.js +++ b/service/javascript/assistants/ondeleteassistant.js @@ -1,8 +1,8 @@ /*jslint nomen: true, node: true */ -/*global Class, Future, Log, Sync, DB, checkResult, servicePath */ +/*global Class, Future, Log, Sync, DB, checkResult, libPath */ /*exported OnDelete*/ -var KeyStore = require(servicePath + "/javascript/utils/KeyStore.js"); +var KeyStore = require(libPath + "KeyStore.js"); var OnDelete = Class.create(Sync.DeleteAccountCommand, { run: function run(outerFuture) { diff --git a/service/javascript/assistants/serviceassistant.js b/service/javascript/assistants/serviceassistant.js index 9e7a23c..54b92e7 100644 --- a/service/javascript/assistants/serviceassistant.js +++ b/service/javascript/assistants/serviceassistant.js @@ -7,14 +7,13 @@ * run-js-service -d /media/cryptofs/apps/usr/palm/services/org.webosports.cdav.service/ */ /*jslint node: true */ -/*global Log, Class, searchAccountConfig, Transport, Sync, Future, Kinds, KindsCalendar, KindsContacts, KindsTasks, checkResult, lockCreateAssistant, servicePath, httpClient, PackageVersion, fs */ -/*exported ServiceAssistant, OnCredentialsChanged*/ +/*global Log, Class, searchAccountConfig, Transport, Sync, Future, Kinds, KindsCalendar, KindsContacts, KindsTasks, checkResult, lockCreateAssistant, libPath, httpClient, PackageVersion, fs, iCal */ +/*exported ServiceAssistant, OnCredentialsChanged */ -var iCal = require(servicePath + "/javascript/utils/iCal.js"); -var vCard = require(servicePath + "/javascript/utils/vCard.js"); -var AuthManager = require(servicePath + "/javascript/utils/AuthManager.js"); -var KeyStore = require(servicePath + "/javascript/utils/KeyStore.js"); -var Base64 = require(servicePath + "/javascript/utils/Base64.js"); +var vCard = require(libPath + "vCard.js"); +var AuthManager = require(libPath + "AuthManager.js"); +var KeyStore = require(libPath + "KeyStore.js"); +var Base64 = require(libPath + "Base64.js"); var ServiceAssistant = Transport.ServiceAssistantBuilder({ clientId: "", @@ -113,7 +112,7 @@ var ServiceAssistant = Transport.ServiceAssistantBuilder({ future.then(this, function () { var result = checkResult(future); - if (!result.iCal) { + if (!result.returnValue) { Log.debug("iCal init not ok."); } else { Log.debug("iCal initialized"); diff --git a/service/javascript/assistants/statusassistant.js b/service/javascript/assistants/statusassistant.js index b02603c..815ec28 100644 --- a/service/javascript/assistants/statusassistant.js +++ b/service/javascript/assistants/statusassistant.js @@ -1,7 +1,7 @@ /*jslint node: true, nomen: true */ -/*global servicePath, Log */ +/*global libPath, Log */ /*exported statusAssistant*/ -var SyncStatus = require(servicePath + "/javascript/utils/SyncStatus.js"); +var SyncStatus = require(libPath + "SyncStatus.js"); /* Validate contact username/password */ var statusAssistant = function () { "use strict"; }; diff --git a/service/javascript/assistants/syncassistant.js b/service/javascript/assistants/syncassistant.js index 0537bbe..55c96bf 100644 --- a/service/javascript/assistants/syncassistant.js +++ b/service/javascript/assistants/syncassistant.js @@ -3,15 +3,15 @@ * Description: Handles the remote to local data conversion for CalDav and CardDav */ /*jslint nomen: true, node: true */ -/*global Log, Class, Sync, Kinds, Future, CalDav, DB, PalmCall, Activity, checkResult, servicePath */ +/*global Log, Class, Sync, Kinds, Future, CalDav, DB, PalmCall, Activity, checkResult, libPath */ /*exported SyncAssistant */ -var vCard = require(servicePath + "/javascript/utils/vCard.js"); -var ETag = require(servicePath + "/javascript/utils/ETag.js"); -var ID = require(servicePath + "/javascript/utils/ID.js"); -var SyncKey = require(servicePath + "/javascript/utils/SyncKey.js"); -var CalendarEventHandler = require(servicePath + "/javascript/utils/CalendarEventHandler.js"); -var SyncStatus = require(servicePath + "/javascript/utils/SyncStatus.js"); +var vCard = require(libPath + "vCard.js"); +var ETag = require(libPath + "ETag.js"); +var ID = require(libPath + "ID.js"); +var SyncKey = require(libPath + "SyncKey.js"); +var CalendarEventHandler = require(libPath + "CalendarEventHandler.js"); +var SyncStatus = require(libPath + "SyncStatus.js"); var SyncAssistant = Class.create(Sync.SyncCommand, { run: function run(outerfuture, subscription) { @@ -112,7 +112,7 @@ var SyncAssistant = Class.create(Sync.SyncCommand, { this.SyncKey = new SyncKey(this.client, this.handler); this.$super(run)(future); - future.then(function syncCameBackCB() { + future.then(this, function syncCameBackCB() { var result = checkResult(future); Log.debug("Sync came back: ", result); if (args.syncOnEdit) { @@ -1259,11 +1259,7 @@ var SyncAssistant = Class.create(Sync.SyncCommand, { noReUpload = true; } - //before I did throw an error here. This prevented the sync from finishing and triggered upsync for this object on - //next occasion... if we only return an error here, probably we loose local changes. - //issue is that on error code 412 (i.e. something changed on server on this object) sync will NEVER finish - //and always will be triggered. - if (result.returnCode === 412) { + if (result.returnCode === 412 || result.returnCode === 409) { future.result = {returnValue: false, putError: true, msg: "Put object failed, because it was changed on server, too: " + JSON.stringify(result) + " for " + obj.uri, noReUpload: noReUpload }; } else { future.result = {returnValue: false, putError: true, msg: "Put object failed: " + JSON.stringify(result) + " for " + obj.uri, noReUpload: noReUpload }; diff --git a/service/javascript/prologue.js b/service/javascript/prologue.js index 2a6c65c..01df6b5 100644 --- a/service/javascript/prologue.js +++ b/service/javascript/prologue.js @@ -28,22 +28,25 @@ var fs = require("fs"); //required for own node modules and current vCard conver //node in webos is a bit picky about require paths. Really point it to the library here. var servicePath = fs.realpathSync("."); +var libPath = servicePath + "/javascript/utils/"; console.log("Service Path: " + servicePath); -var Log = require(servicePath + "/javascript/utils/Log.js"); +var Log = require(libPath + "Log.js"); Log.setFilename("/media/internal/.org.webosports.cdav.service.log"); -var CalDav = require(servicePath + "/javascript/utils/CalDav.js"); +var CalDav = require(libPath + "CalDav.js"); var nodejsMajorVersion = Number(process.version.match(/^v\d+\.(\d+)/)[1]); if (nodejsMajorVersion >= 4) { - var httpClient = require(servicePath + "/javascript/utils/httpClient.js"); + var httpClient = require(libPath + "httpClient.js"); } else { - var httpClient = require(servicePath + "/javascript/utils/httpClient_legacy.js"); + var httpClient = require(libPath + "httpClient_legacy.js"); } -var checkResult = require(servicePath + "/javascript/utils/checkResult.js"); +var checkResult = require(libPath + "checkResult.js"); var KindsModule = require(servicePath + "/javascript/kinds.js"); var Kinds = KindsModule.Kinds; var KindsCalendar = KindsModule.KindsCalendar; var KindsContacts = KindsModule.KindsContacts; +var iCal = require(libPath + "iCal.js"); + //load assistants: var SyncAssistant = require(servicePath + "/javascript/assistants/syncassistant.js"); diff --git a/service/javascript/utils/AuthManager.js b/service/javascript/utils/AuthManager.js index 8d9e627..01ea67e 100644 --- a/service/javascript/utils/AuthManager.js +++ b/service/javascript/utils/AuthManager.js @@ -9,9 +9,9 @@ * Can switch from basic auth to MD5 digest, if necessary. */ /*jslint node: true */ -/*global servicePath, checkResult, Future, CalDav, UrlSchemes, Log */ +/*global libPath, checkResult, Future, CalDav, UrlSchemes, Log */ -var OAuth = require(servicePath + "/javascript/utils/OAuth.js"); +var OAuth = require(libPath + "OAuth.js"); var urlParser; var AuthManager = (function () { diff --git a/service/javascript/utils/CalDav.js b/service/javascript/utils/CalDav.js index ed23fdb..1f13458 100644 --- a/service/javascript/utils/CalDav.js +++ b/service/javascript/utils/CalDav.js @@ -1,7 +1,7 @@ /*jslint node: true */ -/*global Log, httpClient, Future, checkResult, servicePath */ +/*global Log, httpClient, Future, checkResult, libPath */ -var AuthManager = require(servicePath + "/javascript/utils/AuthManager.js"); +var AuthManager = require(libPath + "AuthManager.js"); var CalDav = (function () { "use strict"; diff --git a/service/javascript/utils/CalendarEventHandler.js b/service/javascript/utils/CalendarEventHandler.js index 8212832..ed1fe2a 100644 --- a/service/javascript/utils/CalendarEventHandler.js +++ b/service/javascript/utils/CalendarEventHandler.js @@ -1,6 +1,5 @@ -/*global Log, DB, Kinds, checkResult, Future, servicePath */ - -var iCal = require(servicePath + "/javascript/utils/iCal.js"); +/*jslint node: true, nomen: true */ +/*global Log, DB, Kinds, checkResult, Future, iCal */ var CalendarEventHandler = (function () { "use strict"; @@ -150,7 +149,8 @@ var CalendarEventHandler = (function () { if (result.results.length > 0) { //have children! result.results.forEach(function (e, index) { - e.uId = parentEvent.uId; + e.uid = parentEvent.uid || parentEvent.uId; + e.relateTo = e.uid; e.remoteId = parentEvent.remoteId + "exception" + index; e.uri = parentEvent.uri; }); @@ -165,7 +165,7 @@ var CalendarEventHandler = (function () { } }); - future.then(this, function() { + future.then(this, function () { var result = checkResult(future); if (parentEvent) { @@ -184,35 +184,73 @@ var CalendarEventHandler = (function () { var entry = entries[entriesIndex], future = new Future(); if (entry.obj.rrule) { - Log.debug("Event has rrule, deleting child events.", entry); + Log.debug("Event has rrule, deleting child events for remoteId ", remoteId, " and entry ", entry); future.nest(findEventByRemoteId(remoteId)); future.then(function findByRemoteIdCB() { - Log.debug("Find event returned: "); - var result = checkResult(future); - if (result.returnValue && result.results && result.results[0]) { - future.nest(DB.merge( - { - from: Kinds.objects.calendarevent.id, - where: [ - { - prop: "parentId", - op: "=", - val: result.results[0]._id - } - ] - }, - { - "_del": true, - preventSync: true - } - )); + var result = checkResult(future), toMerge = [], parentId; + Log.debug("Find event returned: ", result); + result.results.forEach(function (res) { + if (res.parentId || res.relatedTo || res.parentDtstart) { + res._del = true; + res.preventSync = true; + toMerge.push(res); + } else { + Log.debug("Found partent with id ", res._id); + parentId = res._id; + } + }); + + Log.debug("To delete events: ", toMerge); + if (toMerge.length) { + future.nest(DB.merge(toMerge)); } else { future.result = { returnValue: false}; } + future.then(function delByRemoteIDCD() { + var result = checkResult(future); + Log.debug("Delete children by remoteId result: ", result); + if (parentId) { + Log.debug("Have parentId, use that to delete, too."); + future.nest(getChildren({_id: parentId})); + } else { + future.result = result; + } + }); + + //delete by parentId here, too, because webOS creates new events for exceptions that don't have + //the remoteId set and are not caught by the above method. This will happen during upsync + //and might provoke db errors on device. + //But if we don't do that here, we will have exceptions showing up to often. + future.then(function getChildrenCB() { + var result = checkResult(future); + if (parentId) { + Log.debug("Got children by parentId: ", result); + toMerge = []; + if (result.results && result.results.length) { + result.results.forEach(function (res) { + if (res.parentId || res.relatedTo || res.parentDtstart) { + res._del = true; + res.preventSync = true; + toMerge.push(res); + } + }); + } + + Log.debug("To delete events by parentId: ", toMerge); + if (toMerge.length) { + future.nest(DB.merge(toMerge)); + } else { + future.result = { returnValue: false}; + } + } else { + future.result = { returnValue: false}; + } + }); + future.then(function delChildrenCB() { var result = checkResult(future); - Log.debug("Delete children result: ", result); + Log.debug("Delete children by parentId result: ", result); future.result = {returnValue: true}; }); }); @@ -228,7 +266,7 @@ var CalendarEventHandler = (function () { //add the exceptions to the end of the entries, indicating that they are already downloaded. result.exceptions.forEach(function (event, index) { event.collectionId = entries[entriesIndex].collectionId; - event.uId = entries[entriesIndex].uId; + event.uid = entries[entriesIndex].uid || entries[entriesIndex].uId; event.remoteId = entries[entriesIndex].remoteId; entries.push({ alreadyDownloaded: true, diff --git a/service/javascript/utils/KeyStore.js b/service/javascript/utils/KeyStore.js index f73f1c9..022cb49 100644 --- a/service/javascript/utils/KeyStore.js +++ b/service/javascript/utils/KeyStore.js @@ -3,9 +3,9 @@ KeyStore - used to handle storage of authentication data within key manager. **************************************************/ /*jslint nomen: true, node: true */ -/*global PalmCall, Log, checkResult, servicePath */ +/*global PalmCall, Log, checkResult, libPath */ //this is taken from MojoSyncFramework example in PalmSDK -var Base64 = require(servicePath + "/javascript/utils/Base64.js"); +var Base64 = require(libPath + "Base64.js"); var KeyStore = (function () { "use strict"; diff --git a/service/javascript/utils/SyncStatus.js b/service/javascript/utils/SyncStatus.js index 1e6ad12..ced1e6a 100644 --- a/service/javascript/utils/SyncStatus.js +++ b/service/javascript/utils/SyncStatus.js @@ -59,6 +59,9 @@ var SyncStatus = (function () { if (!perAccountStatus[accountId]) { return; } + if (kindName) { + return perAccountStatus[accountId][field]; + } if (!perAccountStatus[accountId][kindName]) { return; } diff --git a/service/javascript/utils/iCal.js b/service/javascript/utils/iCal.js index 337df24..58b0105 100644 --- a/service/javascript/utils/iCal.js +++ b/service/javascript/utils/iCal.js @@ -1,7 +1,25 @@ /*jslint node: true, nomen: true */ -/*global Log, PalmCall, Calendar, Future, checkResult, servicePath */ +/*global Log, PalmCall, Calendar, Future, checkResult, libPath */ -var Quoting = require(servicePath + "/javascript/utils/Quoting.js"); +var Quoting = require(libPath + "Quoting.js"); +var Time = require(libPath + "iCalTimeHandling.js"); + +//from later node.js versions. If we fade out 2.x support, this can go away and be replaced by node.js util._extend method. +var extend = function (origin, add) { + "use strict"; + // Don't do anything if add isn't an object + if (!add || typeof add !== 'object') { + return origin; + } + + var keys = Object.keys(add), + i = keys.length; + while (i) { + i -= 1; + origin[keys[i]] = add[keys[i]]; + } + return origin; +}; // This is a small iCal to webOs event parser. // Its meant to be simple and has some deficiencies. @@ -83,149 +101,13 @@ var iCal = (function () { var dayToNum = { "SU": 0, "MO": 1, "TU": 2, "WE": 3, "TH": 4, "FR": 5, "SA": 6 }, numToDay = { "0": "SU", "1": "MO", "2": "TU", "3": "WE", "4": "TH", "5": "FR", "6": "SA"}, DATETIME = /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)$/, - DATE = /^(\d{4})(\d\d)(\d\d)$/, + DATE = /^(\d{4})(\d\d)(\d\d)$/; //DATE: yyyymmdd, time: hhmmss, if both are present they are divided by a T. A Z at the end is optional. //if only a Date is given (=> allDay), no letters are present and just 8 numbers should be given. //Usually the Z at the end of DATE-TIME should say that it's UTC. But I'm quite sure that most programs do this wrong... :( //there is a timezone property that could be set. //it could also be a comma seperated list of dates / date times. But we don't support that, yet.. ;) //used to try timeZone correction... - localTzId = "UTC", - TZManager = Calendar.TimezoneManager(), - shiftAllDay = true; - - function iCalTimeToWebOsTime(time, tz) { - var t = {offset: 0}, result, date, offset, ts2; - t.allDayCue = !DATETIME.test(time); - if (!tz || !tz.tzId) { - if (time.charAt(time.length - 1) === "Z") { - tz = {tzId: "UTC"}; - t.tzId = "UTC"; - } else { - tz = {tzId: localTzId}; - t.tzId = localTzId; - } - } else { - t.tzId = tz.tzId; - } - if (t.allDayCue) { - //only have DATE, add hours, minutes, and seconds - result = DATE.exec(time); - result.push(12); //use 12 here, so that time is in the middle of day and timezone changes, i.e. daylight saving times, won't spread all day events to multiple days. - result.push(0); - result.push(0); - } else { - //have date and time: - result = DATETIME.exec(time); - } - //look at tzId. Shift whole thing that we have all day events on the right day, no matter the TZ. - date = new Date(result[1], result[2] - 1, result[3], result[4], result[5], result[6]); - t.offset = date.getTimezoneOffset() * 60000; - if (localTzId === tz.tzId) { - //for times in the local tz, this will be ok in any case. - t.ts = date.getTime(); - } else if (tz.tzId === "UTC") { //got UTC time, we can easily correct that: - t.ts = Date.UTC(result[1], result[2] - 1, result[3], result[4], result[5], result[6]); //get UTC timestamp from UTC date values :) - if (t.allDayCue && shiftAllDay) { //move to 0:00 in local timeZone. - t.ts += date.getTimezoneOffset() * 60000; - } - } else { //this relies on a framework function from webOs. - ts2 = date.getTime(); - t.ts = TZManager.convertTime(ts2, t.tzId, localTzId); - if (t.allDayCue && shiftAllDay) { - offset = (t.ts - ts2) * 2; - t.ts -= offset; - } - } - return t; - } - - function webOsTimeToICal(time, allDay, tzId, addTZIDParam) { - var t = "", date, time2, offset; - tzId = "UTC"; //we can't provide VTIMEZONE entries that are needed if a TZID parameter is set, so let's just transfer everything to UTC, it's the savest bet. - //"Floating" time would be possible, too, but seems to be ignored by some servers (??) - - date = new Date(time); - if (!tzId) { - tzId = localTzId; //if nothing is specified, take "floating time" which means no timezone at all. - } - if (tzId === localTzId) { - t = date.getFullYear() + (date.getMonth() + 1 < 10 ? "0" : "") + (date.getMonth() + 1) + (date.getDate() < 10 ? "0" : "") + date.getDate(); - if (!allDay) { - t += "T" + (date.getHours() < 10 ? "0" : "") + date.getHours(); - t += (date.getMinutes() < 10 ? "0" : "") + date.getMinutes(); - t += (date.getSeconds() < 10 ? "0" : "") + date.getSeconds(); - } - } else if (tzId === "UTC") { - if (allDay && shiftAllDay) { - t = date.getFullYear() + (date.getMonth() + 1 < 10 ? "0" : "") + (date.getMonth() + 1) + (date.getDate() < 10 ? "0" : "") + date.getDate(); - } else { - t = date.getUTCFullYear() + (date.getUTCMonth() + 1 < 10 ? "0" : "") + (date.getUTCMonth() + 1) + (date.getUTCDate() < 10 ? "0" : "") + date.getUTCDate(); - if (!allDay) { - t += "T" + (date.getUTCHours() < 10 ? "0" : "") + date.getUTCHours(); - t += (date.getUTCMinutes() < 10 ? "0" : "") + date.getUTCMinutes(); - t += (date.getUTCSeconds() < 10 ? "0" : "") + date.getUTCSeconds(); - t += "Z"; //is a hint that time is in UTC. - } - } - } else { - time2 = TZManager.convertTime(time, localTzId, tzId); //convert local ts to target, now should be correct UTC TS, right? - if (allDay && shiftAllDay) { - offset = (time2 - time) * 2; - time2 -= offset; - } - t = webOsTimeToICal(time2, allDay, localTzId, addTZIDParam); - } - return t; - } - - function convertDurationIntoMicroseconds(duration) { - var signRegExp = /^([+\-])?PT?([0-9DHM]+)/gi, parts, sign, remaining, - weekRegExp = /(\d)+W/gi, - dayRegExp = /(\d)+D/gi, - hourRegExp = /(\d)+H/gi, - minuteRegExp = /(\d)+M/gi, - secondRegExp = /(\d)+S/gi, - offset = 0; - - parts = signRegExp.exec(duration); - if (parts) { - sign = parts[1] === "-" ? -1 : 1; - remaining = parts[2]; - - parts = weekRegExp.exec(remaining); - if (parts) { - offset += parseInt(parts[1], 10) * 86400000 * 7; - } - - parts = dayRegExp.exec(remaining); - if (parts) { - offset += parseInt(parts[1], 10) * 86400000; - } - - parts = hourRegExp.exec(remaining); - if (parts) { - offset += parseInt(parts[1], 10) * 3600000; - } - - parts = minuteRegExp.exec(remaining); - if (parts) { - offset += parseInt(parts[1], 10) * 60000; - } - - parts = secondRegExp.exec(remaining); - if (parts) { - offset += parseInt(parts[1], 10) * 1000; - } - - offset *= sign; - Log.log_icalDebug("Converted duration " + duration + " into offset " + offset); - return offset; - } - //else - Log.log("iCal.js======> DURATION DID NOT MATCH: ", duration); - return 0; - } function parseDATEARRAY(str, exdates) { var parts, times = [], i; @@ -274,7 +156,7 @@ var iCal = (function () { text += "INTERVAL=" + rr.interval + ";"; } if (rr.until) { - text += "UNTIL=" + webOsTimeToICal(rr.until, false, tzId) + ";"; + text += "UNTIL=" + Time.webOsTimeToICal(rr.until, false, tzId === "UTC") + ";"; } if (rr.wkst || rr.wkst === 0 || rr.wkst === "0") { text += "WKST=" + numToDay(rr.wkst) + ";"; @@ -740,6 +622,27 @@ var iCal = (function () { //UTC or floating time! event[transTime[lObj.key]] = {tzId: false, value: lObj.value}; } + + if (transTime[lObj.key] === "dtstart") { + event.allDay = !DATETIME.test(lObj.value); //if we only have DATE not DATETIME in dtstart, make event allday. + } + + } else if (lObj.key.indexOf("X-") === 0 && event.valid && !event.finished) { + if (lObj.key === "X-FUNAMBOL-ALLDAY") { + if (lObj.value === "1") { + event.allDay = true; + } else { + event.allDay = false; + } + } + if (lObj.key === "X-ALLDAYEVENT" || lObj.key === "X-MICROSOFT-CDO-ALLDAYEVENT") { + if (lObj.value.toLowerCase() === "true") { + event.allDay = true; + } else { + event.allDay = false; + } + } + event[lObj.key.toLowerCase()] = lObj.line; //keep X-* extensions in object. } else { //one of the more complex cases. switch (lObj.key) { case "ATTACH": //I still don't get why this is an array? @@ -791,21 +694,6 @@ var iCal = (function () { Log.log("WARNING: Parser only tested for iCal version 2.0, read: ", lObj.value); } break; - case "X-FUNAMBOL-ALLDAY": - if (lObj.value === "1") { - event.allDay = true; - } else { - event.allDay = false; - } - break; - case "X-ALLDAYEVENT": - case "X-MICROSOFT-CDO-ALLDAYEVENT": - if (lObj.value.toLowerCase() === "true") { - event.allDay = true; - } else { - event.allDay = false; - } - break; case "DURATION": //this can be specified instead of DTEND. event.duration = lObj.value; @@ -821,16 +709,6 @@ var iCal = (function () { return event; } - function addToExdates(exdates, stamp) { - var i; - for (i = 0; i < exdates.length; i += 1) { - if (exdates[i] === stamp) { - return; - } - } - exdates.push(stamp); - } - function tryToFillParentIds(events, exceptions) { var i, event, revent, parentdtstart; //try to fill "parent id" and parentdtstamp for exceptions to recurring dates. @@ -842,18 +720,11 @@ var iCal = (function () { //search for original event, should usually be the first event. Log.log_icalDebug("processing ", events.length, " events in search of parentids."); for (i = events.length - 1; i >= 0; i -= 1) { - if (!events[i].valid) { - Log.log_icalDebug("Event ", events[i], " was invalid."); - events.splice(i, 1); - } - if (events[i].rrule) { revent = events[i]; parentdtstart = revent.dtstart; events.splice(i, 1); //remove this event. - - delete revent.originalDtstart; //clean that up } } @@ -878,76 +749,74 @@ var iCal = (function () { event.parentDtstart = parentdtstart; event.relatedTo = revent.uid || revent.uId; - addToExdates(revent.exdates, event.originalDtstart); - exceptions.push(event); - - delete event.originalDtstart; //clean that up } return revent; } - function applyHacks(event) { - var i, val, start, diff, tz, tsStruct, date; - /*if (event.tzId && event.tzId !== "UTC") { - log_icalDebug("ERROR: Event was not specified in UTC. Can't currently handle anything else than UTC! Expect problems!!!! :("); - if(event.allDay) { //only mangling with time, if event is allday event. - event.tzId = UTC; + function isTimeStringInTimeStringArray(timestring, tsArray, field) { + var i, ts; + if (!tsArray || !timestring) { + return false; + } + ts = Time.iCalTimeToWebOsTime(timestring); + for (i = 0; i < tsArray.length; i += 1) { + if (field) { + if (Time.iCalTimeToWebOsTime(tsArray[i][field]) === ts) { + Log.log_icalDebug("Found ", tsArray[i][field], " for ", timestring); + return true; + } + } else { + if (Time.iCalTimeToWebOsTime(tsArray[i]) === ts) { + Log.log_icalDebug("Found ", tsArray[i], " for ", timestring); + return true; + } } - }*/ + } + + return false; + } + + function applyHacks(event, children) { + var i, val, start, diff, date, recc, ex, lastChar; //webOs does not support DATE-TIME as alarm trigger. Try to calculate a relative alarm from that... //issue: this does not work, if server and device are in different timezones. Then the offset from //server to GMT still exists... hm. for (i = 0; event.alarm && i < event.alarm.length; i += 1) { if (event.alarm[i].alarmTrigger.valueType === "DATETIME" || event.alarm[i].alarmTrigger.valueType === "DATE-TIME") { - tz = event.tz; - if (!tz) { - if (event.tzId) { - tz = { tzId: event.tzId }; - } - } //log_icalDebug("Calling iCalTimeToWebOsTime with " + event.alarm[i].alarmTrigger.value + " and " + {tzId: event.tzId}); - tsStruct = iCalTimeToWebOsTime(event.alarm[i].alarmTrigger.value, {tzId: event.tzId}); - val = tsStruct.ts; - val -= tsStruct.offset; - Log.log_icalDebug("Hacking alarm, got alarm TS: " + val); - Log.log_icalDebug("Value: " + event.alarm[i].alarmTrigger.value); - Log.log_icalDebug("Val: " + val); + val = Time.iCalTimeToWebOsTime(event.alarm[i].alarmTrigger.value); + Log.log_icalDebug("Hacking alarm, got alarm TS: ", val); + Log.log_icalDebug("Value: ", event.alarm[i].alarmTrigger.value); start = event.dtstart; - Log.log_icalDebug("Start is: " + start); - Log.log_icalDebug("start: " + start); + Log.log_icalDebug("Start is: ", start); date = new Date(start); - Log.log_icalDebug("Date: " + date); + Log.log_icalDebug("Date: ", date); diff = (val - start) / 60000; //now minutes. - Log.log_icalDebug("Diff: " + diff); - Log.log_icalDebug("Diff is " + diff); - if (event.allDay) { - diff += date.getTimezoneOffset(); //remedy allday hack. - Log.log_icalDebug("localized: " + diff); - } + Log.log_icalDebug("Diff: ", diff); if (diff < 0) { val = "-PT"; diff *= -1; } else { val = "PT"; } - Log.log_icalDebug("Diff after < 0: " + diff + " val: " + val); + Log.log_icalDebug("Diff after < 0: ", diff, " val: ", val); if (diff / 10080 >= 1) { //we have weeks. val += (diff / 10080).toFixed() + "W"; } else if (diff / 1440 >= 1) { //we have days. :) val += (diff / 1440).toFixed() + "D"; - Log.log_icalDebug("Day: " + val); + Log.log_icalDebug("Day: ", val); } else if (diff / 60 >= 1) { val += (diff / 60).toFixed() + "H"; - Log.log_icalDebug("Hour: " + val); + Log.log_icalDebug("Hour: ", val); } else { val += diff + "M"; - Log.log_icalDebug("Minutes: " + val + ", diff: " + diff); + Log.log_icalDebug("Minutes: ", val, ", diff: ", diff); } - Log.log_icalDebug("Val is: " + val); + Log.log_icalDebug("Val is: ", val); event.alarm[i].alarmTrigger.value = val; event.alarm[i].alarmTrigger.valueType = "DURATION"; Log.log_icalDebug("Hacked alarm to ", event.alarm[i]); @@ -962,143 +831,69 @@ var iCal = (function () { if (event.allDay) { //86400000 = one day. event.dtend -= 86399000; } - return event; - } - function convertTimestamps(events, index) { - var t, i, directTS = ["dtstart", "dtstamp", "dtend", "created", "lastModified"], makeAllDay = false, tzs = [localTzId], years = [], future = new Future(), event; - event = events[index]; - - if (!event || !event.valid) { - if (index >= events.length) { - future.result = {returnValue: true}; - return future; - } else { - future.nest(convertTimestamps(events, index + 1)); //try next event - return future; + //webOS interprets RFC5545 a bit different here than the rest of the world. + //it requires *every* exception to be listed in exdates. Even those + //that have an own child event with different settings. That's not + //what the rest of the world does. + //So we need to add all recurrenceIds of children to exdates array here + //or webOS displays the unchange recurrence AND the changed one. + if (event.rrule && children) { + Log.log_icalDebug("Have parent with children. Adding recurrenceIds to exdates: ", event, " and ", children); + if (!event.exdates) { + event.exdates = []; } - } - - makeAllDay = event.allDay; //keep allDay setting from possible other cues. - //log_icalDebug("Converting timestamps for " + event.subject); - - directTS.forEach(function buildYearsAndTZs(field) { - if (event[field]) { - if (event[field].tzId) { - tzs.push(event[field].tzId); - years.push(event[field].year); - } - event[field] = event[field].value; - } - }); - - event.originalDtstart = event.dtstart; //keep this for recurrence stuff - - Log.log_icalDebug("Got tzIds and years for tzmanager: ", tzs, " ", years); - if (years.length > 0) { - if (event.tzId) { - tzs.push(event.tzId); - } - if (event.tz && event.tz.tzId) { - tzs.push(event.tz.tzId); - } - - future.nest(TZManager.loadTimezones(tzs, years)); - } else { - future.result = {returnValue: true}; - } - - future.then(function () { - var result = checkResult(future); - Log.log_icalDebug("TZ Result: ", result); - if (result.returnValue) { - if (!event.tz) { - if (event.tzId) { - //log_icalDebug("Did not have tz, setting event.tzId " + event.tzId); - event.tz = { tzId: event.tzId }; - } - } - if (event.dtstart.indexOf("000000") !== -1 && - ((event.dtend.indexOf("235900") !== -1) || - (event.dtend.indexOf("235959") !== -1) || - (event.dtend.indexOf("000000") !== -1))) { - makeAllDay = true; - } - for (i = 0; i < directTS.length; i += 1) { - if (event[directTS[i]]) { - t = iCalTimeToWebOsTime(event[directTS[i]], event.tz); - event[directTS[i]] = t.ts; - if (directTS[i] === "dtstart") { - event.tzId = t.tzId; - event.allDay = t.allDayCue; + for (i = 0; i < children.length; i += 1) { + if (!children[i].recurrenceId) { + Log.log("========================== ERROR: child without reccurrenceId: ", children[i]); + } else { + //both can be either local or UTC => we can savely convert to webOS ts here. + if (!isTimeStringInTimeStringArray(children[i].recurrenceId, event.exdates)) { + recc = children[i].recurrenceId; + Log.log_icalDebug(recc, " missing in exdates ", event.exdates, ", adding."); + if (event.exdates.length) { + ex = event.exdates[0]; + lastChar = ex[ex.length - 1]; + if (lastChar !== recc[recc.length - 1]) { + recc = Time.webOsTimeToICal(Time.iCalTimeToWebOsTime(recc), event.allDay, lastChar === "Z" || lastChar === "z"); + Log.log_icalDebug("Modifyed time string to match those in exdates array: ", ex, " and ", recc); + } } + event.exdates.push(recc); } } - - if (!event.dtend && event.duration) { - event.dtend = event.dtstart + convertDurationIntoMicroseconds(event.duration); - Log.log_icalDebug("Created dtend " + event.dtend + " from " + event.duration + " and " + event.dtstart); - delete event.duration; - } - - if (event.rrule && event.rrule.until) { - t = iCalTimeToWebOsTime(event.rrule.until, event.tz); - event.rrule.until = t.ts; - event.rrule.untilOffset = t.offset; - } - - if (makeAllDay) { - event.allDay = true; - } - - applyHacks(event); - - future.nest(convertTimestamps(events, index + 1)); //try next event - } else { - future.result = result; } - }); - return future; - } + } - function removeHacks(event) { - //do NOT change original event here! return event; } - function fillYearsTZids(event, tzids, years) { - if (event.tzId && event.tzId !== localTzId && event.tzId !== "UTC") { - years.push(new Date(event.dtstart).getFullYear()); - years.push(new Date(event.dtend).getFullYear()); - if (event.lastModified) { - years.push(new Date(event.lastModified).getFullYear()); - } - if (event.created) { - years.push(new Date(event.created).getFullYear()); + function removeHacks(event, children) { + var i; + //webOS interprets RFC5545 a bit different here than the rest of the world. + //it requires *every* exception to be listed in exdates. Even those + //that have an own child event with different settings. That's not + //what the rest of the world does. + //So we need to remove all recurrenceIds of children from exdates array here + //or remote servers will do stupid things. + if (event.rrule && children) { + Log.log_icalDebug("Have parent with children. Removing recurrenceIds from exdates: ", event, " and ", children); + if (event.exdates) { //only necessary if we have exdates. + event.exdates = event.exdates.slice(0); //copy exdates array, to avoid changes in original event here. + for (i = event.exdates.length - 1; i >= 0; i -= 1) { + if (isTimeStringInTimeStringArray(event.exdates[i], children, "recurrenceId")) { + Log.log_icalDebug(event.exdates[i], " already in recurrenceIds, removing."); + event.exdates.splice(i, 1); + } else { + Log.log_icalDebug(event.exdates[i], " unique."); + } + } + } else { + Log.log_icalDebug("No exdates array in event, hack not applicabel"); } - tzids.push(event.tzId); - } - } - - function prepareTZManager(event, children) { - var future = new Future(), years = [], tzids = []; - - fillYearsTZids(event, tzids, years); - - if (children) { - children.forEach(function (e) { - fillYearsTZids(e, tzids, years); - }); - } - - if (years.length > 0) { - tzids.push(localTzId); - future = TZManager.loadTimezones(tzids, years); - } else { - future.result = {returnValue: true}; } - return future; + return event; } function generateICalIntern(event) { @@ -1120,7 +915,6 @@ var iCal = (function () { //"transp" : "TRANSP", //intentionally skip this to let server decide... //"tzId" : "TZID", //skip this. It's not used anyway by most, and we now transmit everything using UTC. "url" : "URL", - "recurrenceId" : "RECURRENCE-ID;VALUE=DATE-TIME", "aalarm" : "AALARM", "uid" : "UID" //try to sed uId. I hope it will be saved in DB although docs don't talk about it. ;) }; @@ -1168,7 +962,12 @@ var iCal = (function () { value += 43199000; } text.push(transTime[field] + - (allDay ? ";VALUE=DATE:" : ":") + webOsTimeToICal(value, allDay, event.tzId)); + (allDay ? ";VALUE=DATE:" : "") + + (event.tzId && event.tzId !== "UTC" ? ";TZID=" + event.tzId : "") + + ":" + + Time.webOsTimeToICal(value, allDay, event.tzId === "UTC")); + } else if (field.indexOf("x-") === 0 && typeof event[field] === "string") { + text.push(event[field]); } else { //more complex fields. switch (field) { case "attach": @@ -1176,11 +975,16 @@ var iCal = (function () { break; case "exdates": if (event.exdates.length > 0) { - text.push("EXDATE;VALUE=DATE-TIME:" + event.exdates.join(",")); + text.push("EXDATE" + (event.allDay ? "VALUE=DATE" : ";VALUE=DATE-TIME") + (event.tzId && event.tzId !== "UTC" ? ";TZID=" + event.tzId : "") + ":" + event.exdates.join(",")); } break; case "rdates": - text.push("RDATE:" + event.rdates.join(",")); + if (event.rdates.length > 0) { + text.push("RDATE" + (event.allDay ? "VALUE=DATE" : ";VALUE=DATE-TIME") + (event.tzId && event.tzId !== "UTC" ? ";TZID=" + event.tzId : "") + ":" + event.rdates.join(",")); + } + break; + case "recurrenceId": + text.push("RECURRENCE-ID" + (event.allDay ? "VALUE=DATE" : ";VALUE=DATE-TIME") + (event.tzId && event.recurrenceId.indexOf("Z") === -1 && event.tzId !== "UTC" ? ";TZID=" + event.tzId : "") + ":" + event.recurrenceId); break; case "alarm": text = buildALARM(event.alarm, text); @@ -1209,9 +1013,6 @@ var iCal = (function () { } } //field loop - text.push("X-FUNAMBOL-ALLDAY:" + (event.allDay ? "1" : "0")); - text.push("X-MICROSOFT-CDO-ALLDAYEVENT:" + (event.allDay ? "TRUE" : "FALSE")); - text.push("X-ALLDAYEVENT:" + (event.allDay ? "TRUE" : "FALSE")); text.push("END:VEVENT"); //lines "should not" be longer than 75 chars in icalendar spec. @@ -1292,25 +1093,35 @@ var iCal = (function () { //END:VEVENT read. Prepare for next event. if (event.finished) { + delete event.finished; events.push(event); event = getNewEvent(); } } + + for (i = events.length - 1; i >= 0; i -= 1) { + if (!events[i].valid) { + events.splice(i, 1); + } + delete events[i].valid; + } + Log.log_icalDebug("Parsing finished, event:", events); - convertTimestamps(events, 0).then(function (future) { + Time.normalizeToLocalTimezone(events).then(function (future) { var result = checkResult(future), exceptions = [], revent; if (result.returnValue) { revent = tryToFillParentIds(events, exceptions); - if (!revent || !revent.valid) { + if (!revent) { Log.log("VCALENDAR Object did not contain valid VEVENT."); outerFuture.result = {returnValue: false}; return; } - events.forEach(function (event) { - delete event.valid; + exceptions.forEach(function (event) { + applyHacks(event); }); - Log.log_icalDebug("After TZ conversion:", events); + applyHacks(revent, exceptions); + Log.log_icalDebug("After TZ conversion:", revent, " and ", exceptions); outerFuture.result = {returnValue: true, result: revent, hasExceptions: exceptions.length > 0, exceptions: exceptions}; } else { outerFuture.result = result; @@ -1325,12 +1136,12 @@ var iCal = (function () { * @param event the event object * @return future with the text in result.result */ - generateICal: function (event) { - var future; + generateICal: function (eventIn) { + var future, event; + event = extend({}, eventIn); removeHacks(event); - - future = prepareTZManager(event); + future = Time.normalizeToEventTimezone([event]); future.then(this, function () { checkResult(future); @@ -1347,19 +1158,21 @@ var iCal = (function () { * @param event the event object * @return future with the text in result.result */ - generateICalWithExceptions: function (event, children) { - var future; + generateICalWithExceptions: function (eventIn, childrenIn) { + var future, event, children = []; - if (!children) { - children = []; + event = extend({}, eventIn); + if (!childrenIn) { + childrenIn = []; } - - removeHacks(event); - children.forEach(function (e) { - removeHacks(e); + childrenIn.forEach(function (e) { + var event = extend({}, e); + removeHacks(event); + children.push(event); }); + removeHacks(event, children); - future = prepareTZManager(event, children); + future = Time.normalizeToEventTimezone([event].concat(children)); future.then(function tzManagerCB() { checkResult(future); @@ -1378,54 +1191,7 @@ var iCal = (function () { }, initialize: function () { - var future = new Future(); - - if (!this.haveSystemTime) { - Log.log_icalDebug("iCal: need systemTime!"); - future.nest(PalmCall.call("palm://com.palm.systemservice", "time/getSystemTime", { "subscribe": false})); - future.then(this, function palmCallReturn() { - var result = future.result; - if (result.timezone) { - localTzId = result.timezone; - Log.log_icalDebug("Got local timezone: " + localTzId); - } - this.haveSystemTime = true; - future.result = { returnValue: true }; - }); - } else { //already have system time. - future.result = { returnValue: true }; - } - - future.then(this, function initializeTZManager() { - if (!this.TZManagerInitialized) { - Log.log_icalDebug("iCal: init TZManager"); - future.nest(TZManager.setup()); - future.then(this, function tzManagerReturn() { - checkResult(future); - Log.log_icalDebug("TZManager initialized"); - this.TZManagerInitialized = true; - future.result = { returnValue: true }; - }); - } else { //TZManager already initialized. - future.result = { returnValue: true }; - } - }); - - future.then(this, function endInit() { - Log.log_icalDebug("iCal init checking " + this.haveSystemTime + " - " + this.TZManagerInitialized); - checkResult(future); - if (this.haveSystemTime && this.TZManagerInitialized) { - Log.log_icalDebug("iCal init finished"); - } - future.result = { iCal: true}; - }); - - future.onError(function () { - Log.log("Error in iCal.initialize:", future.exeption); - future.result = { returnValue: false }; - }); - - return future; + return Time.initialize(); } }; //end of public interface }()); diff --git a/service/javascript/utils/iCalTimeHandling.js b/service/javascript/utils/iCalTimeHandling.js new file mode 100644 index 0000000..f2b5b49 --- /dev/null +++ b/service/javascript/utils/iCalTimeHandling.js @@ -0,0 +1,371 @@ +/*jslint node: true */ +/*global Calendar, Log, Future, checkResult */ + +//Only for time and timezone handling. Ahrg. + +var Time = (function () { + "use strict"; + var DATETIME = /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)$/, + DATE = /^(\d{4})(\d\d)(\d\d)$/, + //DATE: yyyymmdd, time: hhmmss, if both are present they are divided by a T. A Z at the end is optional. + //if only a Date is given (=> allDay), no letters are present and just 8 numbers should be given. + //Usually the Z at the end of DATE-TIME should say that it's UTC. But I'm quite sure that most programs do this wrong... :( + //there is a timezone property that could be set. + //it could also be a comma seperated list of dates / date times. But we don't support that, yet.. ;) + //used to try timeZone correction... + TZManager = Calendar.TimezoneManager(), + TZManagerInitialized = false, + shiftAllDay = true; + + /** + * Converts iCal time string of format YYYYMMDDTHHMM(Z) into javascript timestamp (from local timezone or UTC of Z is present). + */ + function iCalTimeToWebOsTime(time) { + var t = 0, result, date, utc = time.charAt(time.length - 1) === "Z", + allDayCue = !DATETIME.test(time); + + if (allDayCue) { + //only have DATE, add hours, minutes, and seconds + result = DATE.exec(time); + result.push(12); //use 12 here, so that time is in the middle of day and timezone changes, i.e. daylight saving times, won't spread all day events to multiple days. + result.push(0); + result.push(0); + } else { + //have date and time: + result = DATETIME.exec(time); + } + //look at tzId. Shift whole thing that we have all day events on the right day, no matter the TZ. + date = new Date(result[1], result[2] - 1, result[3], result[4], result[5], result[6]); + if (!utc) { + //for times in the local tz, this will be ok in any case. + t = date.getTime(); + } else { //got UTC time, we can easily correct that: + t = Date.UTC(result[1], result[2] - 1, result[3], result[4], result[5], result[6]); //get UTC timestamp from UTC date values :) + if (allDayCue && shiftAllDay) { //move to 0:00 in local timeZone. + t += date.getTimezoneOffset() * 60000; + } + } + return t; + } + + /** + * Converts javascript ts into iCal time string of format YYYYMMDDTHHMM in local TZ. + * If allday param is true, only YYYYMMDD will be returned. + * If utc param is true, string will be in UTC and a "Z" will be appended. + */ + function webOsTimeToICal(time, allDay, utc) { + var t = "", date; + + date = new Date(time); + if (utc) { + if (allDay && shiftAllDay) { + t = date.getFullYear() + (date.getMonth() + 1 < 10 ? "0" : "") + (date.getMonth() + 1) + (date.getDate() < 10 ? "0" : "") + date.getDate(); + } else { + t = date.getUTCFullYear() + (date.getUTCMonth() + 1 < 10 ? "0" : "") + (date.getUTCMonth() + 1) + (date.getUTCDate() < 10 ? "0" : "") + date.getUTCDate(); + if (!allDay) { + t += "T" + (date.getUTCHours() < 10 ? "0" : "") + date.getUTCHours(); + t += (date.getUTCMinutes() < 10 ? "0" : "") + date.getUTCMinutes(); + t += (date.getUTCSeconds() < 10 ? "0" : "") + date.getUTCSeconds(); + t += "Z"; //is a hint that time is in UTC. + } + } + } else { + t = date.getFullYear() + (date.getMonth() + 1 < 10 ? "0" : "") + (date.getMonth() + 1) + (date.getDate() < 10 ? "0" : "") + date.getDate(); + if (!allDay) { + t += "T" + (date.getHours() < 10 ? "0" : "") + date.getHours(); + t += (date.getMinutes() < 10 ? "0" : "") + date.getMinutes(); + t += (date.getSeconds() < 10 ? "0" : "") + date.getSeconds(); + } + } + return t; + } + + /** + * Convert duration string (for example +P0D) into microseconds to add to javascript timestamps. + */ + function convertDurationIntoMicroseconds(duration) { + var signRegExp = /^([+\-])?PT?([0-9DHM]+)/gi, parts, sign, remaining, + weekRegExp = /(\d)+W/gi, + dayRegExp = /(\d)+D/gi, + hourRegExp = /(\d)+H/gi, + minuteRegExp = /(\d)+M/gi, + secondRegExp = /(\d)+S/gi, + offset = 0; + + parts = signRegExp.exec(duration); + if (parts) { + sign = parts[1] === "-" ? -1 : 1; + remaining = parts[2]; + + parts = weekRegExp.exec(remaining); + if (parts) { + offset += parseInt(parts[1], 10) * 86400000 * 7; + } + + parts = dayRegExp.exec(remaining); + if (parts) { + offset += parseInt(parts[1], 10) * 86400000; + } + + parts = hourRegExp.exec(remaining); + if (parts) { + offset += parseInt(parts[1], 10) * 3600000; + } + + parts = minuteRegExp.exec(remaining); + if (parts) { + offset += parseInt(parts[1], 10) * 60000; + } + + parts = secondRegExp.exec(remaining); + if (parts) { + offset += parseInt(parts[1], 10) * 1000; + } + + offset *= sign; + Log.log_icalDebug("Converted duration " + duration + " into offset " + offset); + return offset; + } + //else + Log.log("iCal.js======> DURATION DID NOT MATCH: ", duration); + return 0; + } + + /** + * fetchTimezone information for timezones in the event and years required by the event. + */ + function fetchTimezones(events) { + var timezones = [], + years = [], + tsFields = ["dtstart", "dtstamp", "dtend", "created", "lastModified"], + tsArrays = ["exdates", "rdates", "recurrenceId"]; + + events.forEach(function (event) { + var dtendYear, + dtstartYear, + dtend, + dtstart, + until; + + timezones.push(event.tzId || TZManager.timezone); + + tsFields.forEach(function (field) { + if (event[field]) { + if (typeof event[field] === "object") { + if (event[field].tzId) { + timezones.push(event[field].tzId); + } + if (event[field].year) { + Log.log_icalDebug("Adding year ", event[field].year, " from ", field, " which was object: ", event[field]); + years.push(event[field].year); + } + event[field] = iCalTimeToWebOsTime(event[field].value); //overwrite objects with ts here. + } else { + var date = new Date(event[field]); + Log.log_icalDebug("Adding year ", date.getFullYear(), " from ", field, " which was ts? ", event[field]); + years.push(date.getFullYear()); + } + } + }); + + tsArrays.forEach(function (field) { + var i, year; + if (typeof event[field] === "string") { + year = parseInt(event[field].substr(0, 4), 10); + Log.log_icalDebug("Adding year ", year, " from ", field, " which was string"); + years.push(year); + } else if (event[field] && event[field].length) { + for (i = 0; i < event[field].length; i += 1) { + if (typeof event[field][i] === "string") { + year = parseInt(event[field][i].substr(0, 4), 10); + Log.log_icalDebug("Adding year ", year, " from ", field, " which was array."); + years.push(year); + } + } + } + }); + + if (event.rrule && event.rrule.until) { + Log.log_icalDebug("fetchTimezones(): rrule: ", event.rrule); + until = new Date(event.rrule.until); + Log.log_icalDebug("Adding year ", until.getUTCFullYear(), " from rrule."); + years.push(until.getUTCFullYear()); + } + }); + + Log.log_icalDebug("fetchTimezones(): years: ", years, " for ", timezones); + + return TZManager.loadTimezones(timezones, years); + } + + function normalizeICalTimeString(value, source, target) { + if (value) { + var lastChar = value[value.length - 1], + oldDate, + newDate; + if (lastChar !== "Z" && lastChar !== "z") { + Log.log_icalDebug("Need to process, because of lastChar: ", lastChar); + oldDate = iCalTimeToWebOsTime(value); // new Date( event[field][i] ); + newDate = TZManager.convertTime(oldDate, source, target); + value = webOsTimeToICal(newDate, false, false); + Log.log_icalDebug(" ", oldDate, " (", new Date(oldDate).toDateString(), ") -> ", newDate, " (", new Date(newDate).toDateString(), ") value = ", value); + } + return value; + } + } + + /* + * normalize an array of timestamps (i.e. exdates or rdates) for the local timezone + */ + function normalizeToLocalTimezoneArray(event, field, direction) { + if (event[field]) { + var i, + oldDate, + newDate, + value, + lastChar, + source = event.tzId, + target = TZManager.timezone; + if (direction === "event") { + source = TZManager.timezone; + target = event.tzId; + } + + Log.log_icalDebug("----CONVERTING ", field, " TZ from ", source, " to ", target, "; ", event[field]); + + for (i = 0; i < event[field].length; i += 1) { + Log.log_icalDebug(" item ", i, ": ", event[field][i]); + value = event[field][i]; + event[field][i] = normalizeICalTimeString(value, source, target); + } + } + } + + /** + * Normalize events to local timezone or + * event in local timezone into the timezone specified by tzId. + */ + function normalizeToTimezone(events, direction) { + var future = fetchTimezones(events), + tsFields = ["dtstamp", "created", "lastModified"]; + + future.then(function (future) { + future.getResult(); + + Log.log_icalDebug("Processing ", events.length, " events.", events); + events.forEach(function (event) { + Log.log_icalDebug("normalizeTo", direction, "Timezone(): "); + var newDtend, oldVal, dt, source = event.tzId, target = TZManager.timezone; + if (direction === "event") { + target = event.tzId; + source = TZManager.timezone; + } + + if (event.dtstart) { + Log.log_icalDebug("----CONVERTING TZ from ", source, " to ", target); + oldVal = event.dtstart; + event.dtstart = TZManager.convertTime(event.dtstart, source, target); + Log.log_icalDebug(" ", oldVal, " -> ", event.dtstart); + } + + if (event.dtend) { + newDtend = TZManager.convertTime(event.dtend, source, target); + Log.log_icalDebug("----DTEND EXISTED ", event.dtend, "->", newDtend); + event.dtend = newDtend; + } else if (event.duration) { + event.dtend = event.dtstart + convertDurationIntoMicroseconds(event.duration); + Log.log_icalDebug("----Created dtend ", event.dtend, " from ", event.duration, " and ", event.dtstart); + delete event.duration; + } else if (event.dtstart && !event.recurrenceId) { + // dtend does not exist; if this is not an exception to another + // event, synthesize one at the end of the day + dt = new Date(event.dtstart); + dt.setHours(23); + dt.setMinutes(59); + dt.setSeconds(59); + newDtend = TZManager.convertTime(dt.getTime(), source, target); + Log.log_icalDebug("----DTEND DID NOT EXIST ", dt.getTime(), " -> ", newDtend); + event.dtend = newDtend; + } + + if (event.recurrenceId) { + oldVal = event.recurrenceId; + event.recurrenceId = normalizeICalTimeString(event.recurrenceId, source, target); + Log.log_icalDebug("----RECURRENCE-ID converted ", oldVal, " to ", event.recurrenceId); + } + + tsFields.forEach(function (field) { + var val = event[field]; + if (val) { + event[field] = TZManager.convertTime(val, source, target); + Log.log_icalDebug("----", field.toUpperCase(), " converted ", val, " to ", event[field]); + } + }); + + if (event.rrule && event.rrule.until) { + event.rrule.until = TZManager.convertTime( + event.rrule.until, + source, + target + ); + } + + normalizeToLocalTimezoneArray(event, "exdates", direction); + normalizeToLocalTimezoneArray(event, "rdates", direction); + }); + + future.result = {returnValue: true, events: events}; + }); + + return future; + } + + function normalizeToLocalTimezone(events) { + return normalizeToTimezone(events, "local"); + } + + function normalizeToEventTimezone(events) { + return normalizeToTimezone(events, "event"); + } + + return { + /** + * Normalize event to local timezone. + * Meant as post processing after ical parsing. + * Timestamp members need to be objects with this fields: + * { tzId: "", year: "", value: "YYYYMMDD(THHMM(Z))" (required) } + */ + normalizeToLocalTimezone: normalizeToLocalTimezone, + + /** + * Normalize local timestamps to event timezone + * Meant as pre processing before ical generation. + */ + normalizeToEventTimezone: normalizeToEventTimezone, + + convertDurationIntoMicroseconds: convertDurationIntoMicroseconds, + iCalTimeToWebOsTime: iCalTimeToWebOsTime, + webOsTimeToICal: webOsTimeToICal, + + initialize: function () { + var future = new Future(); + if (!TZManagerInitialized) { + Log.log_icalDebug("iCal: init TZManager"); + future.nest(TZManager.setup()); + future.then(this, function tzManagerReturn() { + checkResult(future); + Log.log_icalDebug("TZManager initialized"); + TZManagerInitialized = true; + future.result = { returnValue: true }; + }); + } else { //TZManager already initialized. + future.result = { returnValue: true }; + } + + return future; + } + + }; +}()); + +module.exports = Time; diff --git a/service/javascript/utils/vCard.js b/service/javascript/utils/vCard.js index 5f56784..6bf5ad7 100644 --- a/service/javascript/utils/vCard.js +++ b/service/javascript/utils/vCard.js @@ -1,10 +1,10 @@ /*jslint regexp: true, node: true, nomen: true, newcap: true */ -/*global Contacts, fs, Log, Future, servicePath, checkResult */ +/*global Contacts, fs, Log, Future, libPath, checkResult */ var path = require("path"); //required for vCard converter. -var Quoting = require(servicePath + "/javascript/utils/Quoting.js"); -var vCardReader = require(servicePath + "/javascript/utils/vCardReader.js"); -var vCardWriter = require(servicePath + "/javascript/utils/vCardWriter.js"); +var Quoting = require(libPath + "Quoting.js"); +var vCardReader = require(libPath + "vCardReader.js"); +var vCardWriter = require(libPath + "vCardWriter.js"); var vCard = (function () { "use strict"; diff --git a/service/javascript/utils/vCardWriter.js b/service/javascript/utils/vCardWriter.js index 544cf24..90239c0 100644 --- a/service/javascript/utils/vCardWriter.js +++ b/service/javascript/utils/vCardWriter.js @@ -1,7 +1,7 @@ /*jslint node: true */ -/*global Future, fs, Log, servicePath */ +/*global Future, fs, Log, libPath */ -var Quoting = require(servicePath + "/javascript/utils/Quoting.js"); +var Quoting = require(libPath + "Quoting.js"); var vCardWriter = function () { "use strict"; diff --git a/service/javascript/version.js b/service/javascript/version.js index 305e4e2..2d28e3e 100644 --- a/service/javascript/version.js +++ b/service/javascript/version.js @@ -1 +1 @@ -var PackageVersion = "0.3.23"; \ No newline at end of file +var PackageVersion = "0.3.25"; \ No newline at end of file diff --git a/service/services.json b/service/services.json index 4b55513..4523ebd 100644 --- a/service/services.json +++ b/service/services.json @@ -72,8 +72,8 @@ "public": true }, { - "name": "addEvent", - "assistant": "AddEventAssistant", + "name": "addItem", + "assistant": "AddItemAssistant", "public": true }, { diff --git a/service/sources.json b/service/sources.json index 1721cda..16ac39e 100644 --- a/service/sources.json +++ b/service/sources.json @@ -54,7 +54,7 @@ "source": "javascript/utils/accountConfigUtils.js" }, { - "source": "javascript/assistants/addeventassistant.js" + "source": "javascript/assistants/additemassistant.js" }, { "source": "javascript/assistants/checkcredentialsassistant.js" diff --git a/test-scripts/httpClient_proxy_test.js b/test-scripts/httpClient_proxy_test.js index 1eb1b39..5bf5397 100644 --- a/test-scripts/httpClient_proxy_test.js +++ b/test-scripts/httpClient_proxy_test.js @@ -3,7 +3,7 @@ var fs = require("fs"); var path = fs.realpathSync("."); -global.servicePath = path + "/../service"; +global.libPath = path + "/../service/javascript/utils/"; global.Log = require('./../service/javascript/utils/Log.js'); diff --git a/test-scripts/test-ctag-404.js b/test-scripts/test-ctag-404.js index 4ab94b7..ef0a448 100644 --- a/test-scripts/test-ctag-404.js +++ b/test-scripts/test-ctag-404.js @@ -2,7 +2,7 @@ var fs = require("fs"); var path = fs.realpathSync("."); -global.servicePath = path + "/../service"; +global.libPath = path + "/../service/javascript/utils/"; global.checkResult = require("./../service/javascript/utils/checkResult"); global.Log = require("./../service/javascript/utils/Log.js"); global.Future = require("./foundations/Future"); diff --git a/test-scripts/test-empty-ics.js b/test-scripts/test-empty-ics.js index 2b22ed3..83bc1fd 100644 --- a/test-scripts/test-empty-ics.js +++ b/test-scripts/test-empty-ics.js @@ -2,7 +2,7 @@ var fs = require("fs"); var path = fs.realpathSync("."); -global.servicePath = path + "/../service"; +global.libPath = path + "/../service/javascript/utils/"; global.Log = require("./../service/javascript/utils/Log.js"); global.Future = require("./foundations/Future"); global.checkResult = require("./../service/javascript/utils/checkResult.js"); @@ -39,4 +39,4 @@ var future = syncClient._downloadData("calendarevent", [{uri: "some-uir", etag: future.then(function checkFinish() { var result = checkResult(future); console.log("Result: ", result); -}); \ No newline at end of file +}); diff --git a/test-scripts/test-folder-parsing.js b/test-scripts/test-folder-parsing.js index f307fce..3f92bb8 100644 --- a/test-scripts/test-folder-parsing.js +++ b/test-scripts/test-folder-parsing.js @@ -2,7 +2,7 @@ var fs = require("fs"); var path = fs.realpathSync("."); -global.servicePath = path + "/../service"; +global.libPath = path + "/../service/javascript/utils/"; global.Log = require("./../service/javascript/utils/Log.js"); var CalDav = require("./../service/javascript/utils/CalDav.js"); var xml = require("./foundations/xml.js"); diff --git a/test-scripts/test-md5-digest.js b/test-scripts/test-md5-digest.js index 2037912..f650255 100644 --- a/test-scripts/test-md5-digest.js +++ b/test-scripts/test-md5-digest.js @@ -2,7 +2,7 @@ var fs = require("fs"); var path = fs.realpathSync("."); -global.servicePath = path + "/../service"; +global.libPath = path + "/../service/javascript/utils/"; global.Log = require("./../service/javascript/utils/Log.js"); global.xml = require("./foundations/xml.js"); global.CalDav = require("./../service/javascript/utils/CalDav.js"); diff --git a/test-scripts/test-no-ctag-but-etag.js b/test-scripts/test-no-ctag-but-etag.js index ad7bf68..4b6e12a 100644 --- a/test-scripts/test-no-ctag-but-etag.js +++ b/test-scripts/test-no-ctag-but-etag.js @@ -2,7 +2,7 @@ var fs = require("fs"); var path = fs.realpathSync("."); -global.servicePath = path + "/../service"; +global.libPath = path + "/../service/javascript/utils/"; global.checkResult = require("./../service/javascript/utils/checkResult"); global.Log = require("./../service/javascript/utils/Log.js"); global.Future = require("./foundations/Future");