Skip to content

Commit

Permalink
fix: Add iCloud support (#32)
Browse files Browse the repository at this point in the history
* feat: ignore attributes and strip tag prefixes during xml parse
* fix: getAllEvents() for iCloud calendars
* test: add fixtures for iCloud calendar style responses
* docs: add 401 error hint to README.md
  • Loading branch information
moalo authored and jhnns committed Sep 6, 2019
1 parent 0b87d9b commit 902b08f
Show file tree
Hide file tree
Showing 7 changed files with 438 additions and 21 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,25 @@ config = {
user: "username",
pass: "password"
},
// example using baikal as CalDAV server
uri: "http://example.com/cal.php/calendars/<user name>/<calendar name>",
timeout: 20000
};
```

If the request fails with a `401 Unauthorized` you might need to send the authentication headers preemptive.
This can be done by setting the `auth.sendImmediately` config property to `true`.

```javascript
config = {
auth: {
user: "username",
pass: "password",
sendImmediately: true
},
uri: "http://example.com/cal.php/calendars/<user name>/<calendar name>"
};
```

## API

### scrapegoat.getCtag()
Expand Down
47 changes: 27 additions & 20 deletions lib/xml/parser.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
"use strict";

const { promisify } = require("util");
const { parseString } = require("xml2js");
const { parseString, processors } = require("xml2js");
const parseXMLString = promisify(parseString);
const ICAL = require("ical.js");
const moment = require("moment");

const PARSE_XML_CONFIG = { ignoreAttrs: true, tagNameProcessors: [processors.stripPrefix] };

// parse calendar object
function parseCalendarMultistatus(xml) {
return parseXMLString(xml).then((result) => {
return parseXMLString(xml, PARSE_XML_CONFIG).then((result) => {
const parsed = {};

if (!result["d:multistatus"] || !result["d:multistatus"]["d:response"]) {
if (!result.multistatus || !result.multistatus.response) {
return parsed;
}

parsed.href = result["d:multistatus"]["d:response"][0]["d:href"][0];
parsed.name = result["d:multistatus"]["d:response"][0]["d:propstat"][0]["d:prop"][0]["d:displayname"][0];
parsed.ctag = result["d:multistatus"]["d:response"][0]["d:propstat"][0]["d:prop"][0]["cs:getctag"][0];
parsed.href = result.multistatus.response[0].href[0];
parsed.name = result.multistatus.response[0].propstat[0].prop[0].displayname[0];
parsed.ctag = result.multistatus.response[0].propstat[0].prop[0].getctag[0];

return parsed;
});
Expand All @@ -28,22 +30,22 @@ function parseEventsMultistatus(xml) {
let parsed;
const formatted = [];

return parseXMLString(xml).then((result) => {
if (!result["d:multistatus"] || !result["d:multistatus"]["d:response"]) {
return parseXMLString(xml, PARSE_XML_CONFIG).then((result) => {
if (!result.multistatus || !result.multistatus.response) {
return formatted;
}

parsed = result["d:multistatus"]["d:response"];
parsed = result.multistatus.response;

// parse must not be undefined!
parsed.forEach((event) => {
// fix etag string (renders as '"[...]"', ugly xml2js objects (pew pew)
let etag = event["d:propstat"][0]["d:prop"][0]["d:getetag"][0];
let etag = event.propstat[0].prop[0].getetag[0];

etag = stripDoubleQuotes(etag);

formatted.push({
ics: event["d:href"][0],
ics: event.href[0],
etag
});
});
Expand Down Expand Up @@ -135,20 +137,25 @@ function parseEvents(xml) {
let parsed;
const formatted = [];

return parseXMLString(xml).then((result) => {
if (!result["d:multistatus"] || !result["d:multistatus"]["d:response"]) {
return parseXMLString(xml, PARSE_XML_CONFIG).then((result) => {
if (!result.multistatus || !result.multistatus.response) {
return formatted;
}

parsed = result["d:multistatus"]["d:response"];
parsed = result.multistatus.response;

parsed.forEach((event) => {
let etag = event["d:propstat"][0]["d:prop"][0]["d:getetag"][0];
let etag = event.propstat[0].prop[0].getetag[0];

etag = stripDoubleQuotes(etag);

let eventData = {};
const calendarData = event["d:propstat"][0]["d:prop"][0]["cal:calendar-data"][0];

if (!event.propstat[0].prop[0]["calendar-data"]) {
return;
}

const calendarData = event.propstat[0].prop[0]["calendar-data"][0];
const jcalData = ICAL.parse(calendarData);
const comp = new ICAL.Component(jcalData);
const vevents = comp.getAllSubcomponents("vevent");
Expand Down Expand Up @@ -189,7 +196,7 @@ function parseEvents(xml) {
if (modifiedOccurences.length === 0) {
// No events have been modified
formatted.push({
ics: event["d:href"][0],
ics: event.href[0],
etag,
data: getNormalOccurenceEventData(nextEvent, eventData, vevent)
});
Expand All @@ -200,14 +207,14 @@ function parseEvents(xml) {
const key = getModifiedOccuranceKey(nextOccuranceTime, modifiedOccurences) || 0;

formatted.push({
ics: event["d:href"][0],
ics: event.href[0],
etag,
data: getModifiedOccurenceEventData(vevents[key], eventData)
});
} else {
// Expand this event normally
formatted.push({
ics: event["d:href"][0],
ics: event.href[0],
etag,
data: getNormalOccurenceEventData(nextEvent, eventData, vevent)
});
Expand Down Expand Up @@ -235,7 +242,7 @@ function parseEvents(xml) {
};

formatted.push({
ics: event["d:href"][0],
ics: event.href[0],
etag,
data: eventData
});
Expand Down
45 changes: 45 additions & 0 deletions test/Calendar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ describe("Calendar", () => {
});
});

it("should return an object with information about the calendar (no namespace)", () => {
const response = fixtures.getCtagNoNamespaceResponse;
const request = Promise.resolve(response);
const Calendar = createCalendar(() => request);
const calendar = new Calendar(config);

return calendar
.getCtag()
.then((response) => {
expect(response).to.have.property("href", "/123456789/calendars/DEADB715-BEEF-47E1-A2B6-E1BA415C93AC/");
expect(response).to.have.property("name", "My Calendar");
expect(response).to.have.property("ctag", "FT=-@RU=1a5c7464-1234-1234-ba09-bb58b7adbac7@S=2012");
});
});
});

describe(".getEtags()", () => {
Expand Down Expand Up @@ -98,6 +112,21 @@ describe("Calendar", () => {
});
});

it("should return an array of object with etags of all events (no namespace)", () => {
const response = fixtures.getEtagsNoNamespaceResponse;
const request = Promise.resolve(response);
const Calendar = createCalendar(() => request);
const calendar = new Calendar(config);

return calendar
.getEtags()
.then((response) => {
expect(response).to.be.an("array");
expect(response).to.have.lengthOf(3);
expect(response[0]).to.have.property("ics");
expect(response[0]).to.have.property("etag");
});
});
});

describe(".getEvents()", () => {
Expand Down Expand Up @@ -223,6 +252,22 @@ describe("Calendar", () => {
});
});

it("should return an array of objects with all events in the calendar (no namespace)", () => {
const response = fixtures.getAllEventsNoNamespaceResponse;
const request = Promise.resolve(response);
const Calendar = createCalendar(() => request);
const calendar = new Calendar(config);

return calendar
.getAllEvents()
.then((response) => {
expect(response).to.be.an("array");
expect(response).to.have.lengthOf(2);
expect(response[0]).to.have.property("ics");
expect(response[0]).to.have.property("etag");
expect(response[0]).to.have.property("data");
});
});
});

describe(".getEventsByTime()", () => {
Expand Down
12 changes: 12 additions & 0 deletions test/fixtures/ctagNoNamespace.response.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<multistatus xmlns="DAV:">
<response xmlns="DAV:">
<href>/123456789/calendars/DEADB715-BEEF-47E1-A2B6-E1BA415C93AC/</href>
<propstat>
<prop>
<displayname xmlns="DAV:">My Calendar</displayname>
<getctag xmlns="http://calendarserver.org/ns/">FT=-@RU=1a5c7464-1234-1234-ba09-bb58b7adbac7@S=2012</getctag>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>
30 changes: 30 additions & 0 deletions test/fixtures/etagsNoNamespace.response.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0"?>
<multistatus xmlns="DAV:">
<response>
<href>/123456789/calendars/D1234567-B5E0-4444-B09E-9999FF89C4AF/</href>
<propstat>
<prop>
<getetag xmlns="DAV:">"C=2325@U=c0e471b8-9d6a-4e13-80e3-2e89dab8f4c4"</getetag>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/123456789/calendars/D1234567-B5E0-4444-B09E-9999FF89C4AF/A47C9E63-DC03-42B5-9DF4-2F4AA324EFEF.ics</href>
<propstat>
<prop>
<getetag xmlns="DAV:">"C=2329@U=c0e471b8-9d6a-4e13-80e3-2e89dab8f4c4"</getetag>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/123456789/calendars/D1234567-B5E0-4444-B09E-9999FF89C4AF/C047F711-544B-4752-BDFF-BF1230E2DD39.ics</href>
<propstat>
<prop>
<getetag xmlns="DAV:">"C=2328@U=c0e471b8-9d6a-4e13-80e3-2e89dab8f4c4"</getetag>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>
Loading

0 comments on commit 902b08f

Please sign in to comment.