aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile19
-rw-r--r--lib/caldav/caldav.js101
-rw-r--r--lib/caldav/ics.js140
-rw-r--r--lib/caldav/responder.js209
-rw-r--r--lib/caldav/sax.js158
-rw-r--r--lib/caldav/sax/propstat.js30
-rw-r--r--lib/caldav/xhr.js111
-rw-r--r--package.json28
-rw-r--r--samples/calendar-data11
-rw-r--r--samples/get-props0
-rw-r--r--samples/resource-type10
-rw-r--r--samples/xml/calendar-multiget.xml63
-rw-r--r--samples/xml/prop-get.xml27
-rw-r--r--samples/xml/propstat-success.xml10
-rw-r--r--test.js104
-rw-r--r--test/caldav/ics_test.js15
-rw-r--r--test/caldav/sax/propstat_test.js27
-rw-r--r--test/caldav/sax_test.js93
-rw-r--r--test/helper.js16
20 files changed, 1174 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fd4f2b0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+.DS_Store
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..de8556a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,19 @@
+REPORTER := spec
+
+.PHONY: test
+test:
+ ./node_modules/mocha/bin/mocha \
+ --ui tdd \
+ --reporter $(REPORTER) \
+ --growl test/helper.js \
+ test/caldav/*_test.js
+
+.PHONY: watch
+FILES=
+watch:
+ ./node_modules/mocha/bin/mocha \
+ --ui tdd \
+ --reporter $(REPORTER) \
+ --watch \
+ --growl \
+ test/helper.js $(FILES)
diff --git a/lib/caldav/caldav.js b/lib/caldav/caldav.js
new file mode 100644
index 0000000..cf81358
--- /dev/null
+++ b/lib/caldav/caldav.js
@@ -0,0 +1,101 @@
+(function(global, module) {
+
+ /**
+ * Define a list of paths
+ * this will only be used in the browser.
+ */
+ var paths = {};
+
+
+ /**
+ * Exports object is a shim
+ * we use in the browser to
+ * create an object that will behave much
+ * like module.exports
+ */
+ function Exports(path) {
+ this.path = path;
+ }
+
+ Exports.prototype = {
+
+ /**
+ * Unified require between browser/node.
+ * Path is relative to this file so you
+ * will want to use it like this from any depth.
+ *
+ *
+ * var Leaf = ns.require('sub/leaf');
+ *
+ *
+ * @param {String} path path lookup relative to this file.
+ */
+ require: function exportRequire(path) {
+ if (typeof(window) === 'undefined') {
+ return require(require('path').join(__dirname, path));
+ } else {
+ return paths[path];
+ }
+ },
+
+ /**
+ * Maps exports to a file path.
+ */
+ set exports(val) {
+ return paths[this.path] = val;
+ },
+
+ get exports() {
+ return paths[this.path];
+ }
+ };
+
+ /**
+ * Module object constructor.
+ *
+ *
+ * var module = Module('sub/leaf');
+ * module.exports = function Leaf(){}
+ *
+ *
+ * @constructor
+ * @param {String} path file path.
+ */
+ function Module(path) {
+ return new Exports(path);
+ }
+
+ Module.require = Exports.prototype.require;
+ Module.exports = Module;
+ Module._paths = paths;
+
+
+ /**
+ * Reference self as exports
+ * which also happens to be the constructor
+ * so you can assign items to the namespace:
+ *
+ * //assign to Module.X
+ * //assume module.exports is Module
+ * module.exports.X = Foo; //Module.X === Foo;
+ * Module.exports('foo'); //creates module.exports object.
+ *
+ */
+ module.exports = Module;
+
+ /**
+ * In the browser assign
+ * to a global namespace
+ * obviously 'Module' would
+ * be whatever your global namespace is.
+ */
+ if (this.window)
+ window.CalDav = Module;
+
+}(
+ this,
+ (typeof(module) === 'undefined') ?
+ {} :
+ module
+));
+
diff --git a/lib/caldav/ics.js b/lib/caldav/ics.js
new file mode 100644
index 0000000..47d9151
--- /dev/null
+++ b/lib/caldav/ics.js
@@ -0,0 +1,140 @@
+/**
+@namespace
+*/
+(function(module, ns) {
+
+ // Iterate over all entries if x is an array, otherwise just call fn on x.
+ function ForAll(x, fn) {
+ if (!(x instanceof Array)) {
+ fn(x);
+ return;
+ }
+ for (var n = 0; n < x.length; ++n)
+ fn(x[n]);
+ }
+
+ /* Pattern for an individual entry: name:value */
+ var ENTRY = /^([A-Za-z0-9-]+)((?:;[A-Za-z0-9-]+=(?:"[^"]+"|[^";:,]+)(?:,(?:"[^"]+"|[^";:,]+))*)*):(.*)$/;
+ /* Pattern for an individual parameter: name=value[,value] */
+ var PARAM = /;([A-Za-z0-9-]+)=((?:"[^"]+"|[^";:,]+)(?:,(?:"[^"]+"|[^";:,]+))*)/g;
+ /* Pattern for an individual parameter value: value | "value" */
+ var PARAM_VALUE = /,?("[^"]+"|[^";:,]+)/g;
+
+ // Parse a calendar in iCal format.
+ function ParseICal (text, success, error) {
+ // Parse the text into an object graph
+ var lines = text.replace('\r', '').split('\n');
+ var tos = Object.create(null);
+ var stack = [tos];
+
+ // Parse parameters for an entry. Foramt: <param>=<pvalue>[;...]
+ function parseParams(params) {
+ var map = Object.create(null);
+ var param = PARAM.exec(params);
+ while (param) {
+ var values = [];
+ var value = PARAM_VALUE.exec(param[2]);
+ while (value) {
+ values.push(value[1].replace(/^"(.*)"$/, '$1'));
+ value = PARAM_VALUE.exec(param[2]);
+ }
+ map[param[1].toLowerCase()] = (values.length > 1 ? values : values[0]);
+ param = PARAM.exec(params);
+ }
+ return map;
+ }
+
+ // Add a property to the current object. If a property with the same name
+ // already exists, turn it into an array.
+ function add(prop, value, params) {
+ if (params)
+ value = { parameters: parseParams(params), value: value };
+ if (prop in tos) {
+ var previous = tos[prop];
+ if (previous instanceof Array) {
+ previous.push(value);
+ return;
+ }
+ value = [previous, value];
+ }
+ tos[prop] = value;
+ }
+
+ for (var n = 0; n < lines.length; ++n) {
+ var line = lines[n];
+ // check whether the line continues (next line stats with space or tab)
+ var nextLine;
+ while ((nextLine = lines[n+1]) && (nextLine[0] == ' ' || nextLine[0] == '\t')) {
+ line += nextLine.substr(1);
+ ++n;
+ continue;
+ }
+ // parse the entry, format is 'PROPERTY:VALUE'
+ var matches = ENTRY.exec(line);
+ if (!matches)
+ return error('invalid format');
+ var prop = matches[1].toLowerCase();
+ var params = matches[2];
+ var value = matches[3];
+ switch (prop) {
+ case 'begin':
+ var obj = Object.create(null);
+ add(value.toLowerCase(), obj);
+ stack.push(tos = obj);
+ break;
+ case 'end':
+ stack.pop();
+ tos = stack[stack.length - 1];
+ if (stack.length == 1) {
+ var cal = stack[0];
+ if (typeof cal.vcalendar != 'object' || cal.vcalendar instanceof Array)
+ return error('single vcalendar object expected');
+ return success(cal.vcalendar);
+ }
+ break;
+ default:
+ add(prop, value, params);
+ break;
+ }
+ }
+ return error('unexpected end of file');
+ }
+
+ function Value(v) {
+ return (typeof v !== 'object') ? v : v.value;
+ }
+
+ function Parameter(v, name) {
+ if (typeof v !== 'object')
+ return undefined;
+ return v.parameters[name];
+ }
+
+ // Parse a time specification.
+ function ParseDateTime(v) {
+ var dt = Value(v);
+ if (Parameter(v, 'VALUE') == 'DATE') {
+ // 20081202
+ return new Date(dt.substr(0, 4), dt.substr(4, 2), dt.substr(6, 2));
+ }
+ v = Value(v);
+ // 20120426T130000Z
+ var year = dt.substr(0, 4);
+ var month = dt.substr(4, 2) - 1;
+ var day = dt.substr(6, 2);
+ var hour = dt.substr(9, 2);
+ var min = dt.substr(11, 2);
+ var sec = dt.substr(13, 2);
+ if (dt[15] == 'Z')
+ return new Date(Date.UTC(year, month, day, hour, min, sec));
+ return new Date(year, month, day, hour, min, sec);
+ }
+
+ module.exports = ParseICal;
+
+}.apply(
+ this,
+ (this.CalDav) ?
+ [CalDav('ics'), CalDav] :
+ [module, require('./caldav')]
+));
diff --git a/lib/caldav/responder.js b/lib/caldav/responder.js
new file mode 100644
index 0000000..b4abd21
--- /dev/null
+++ b/lib/caldav/responder.js
@@ -0,0 +1,209 @@
+/**
+@namespace
+*/
+(function(module, ns) {
+
+ /**
+ * Constructor
+ *
+ * @param {Object} list of events to add onto responder.
+ */
+ function Responder(events) {
+ this._$events = Object.create(null);
+
+ if (typeof(events) !== 'undefined') {
+ this.addEventListener(events);
+ }
+ };
+
+ /**
+ * Stringifies request to websocket
+ *
+ *
+ * @param {String} command command name.
+ * @param {Object} data object to be sent over the wire.
+ * @return {String} json object.
+ */
+ Responder.stringify = function stringify(command, data) {
+ return JSON.stringify([command, data]);
+ };
+
+ /**
+ * Parses request from WebSocket.
+ *
+ * @param {String} json json string to translate.
+ * @return {Object} ex: { event: 'test', data: {} }.
+ */
+ Responder.parse = function parse(json) {
+ var data;
+ try {
+ data = (json.forEach) ? json : JSON.parse(json);
+ } catch (e) {
+ throw new Error("Could not parse json: '" + json + '"');
+ }
+
+ return {event: data[0], data: data[1]};
+ };
+
+ Responder.prototype = {
+ parse: Responder.parse,
+ stringify: Responder.stringify,
+
+ /**
+ * Events on this instance
+ *
+ * @type Object
+ */
+ _$events: null,
+
+ /**
+ * Recieves json string event and dispatches an event.
+ *
+ * @param {String|Object} json data object to respond to.
+ * @param {String} json.event event to emit.
+ * @param {Object} json.data data to emit with event.
+ * @param {Object} [params] option number of params to pass to emit.
+ * @return {Object} result of WebSocketCommon.parse.
+ */
+ respond: function respond(json) {
+ var event = Responder.parse(json),
+ args = Array.prototype.slice.call(arguments).slice(1);
+
+ args.unshift(event.data);
+ args.unshift(event.event);
+
+ this.emit.apply(this, args);
+
+ return event;
+ },
+
+ //TODO: Extract event emitter logic
+
+ /**
+ * Adds an event listener to this object.
+ *
+ *
+ * @param {String} type event name.
+ * @param {Function} callback event callback.
+ */
+ addEventListener: function addEventListener(type, callback) {
+ var event;
+
+ if (typeof(callback) === 'undefined' && typeof(type) === 'object') {
+ for (event in type) {
+ if (type.hasOwnProperty(event)) {
+ this.addEventListener(event, type[event]);
+ }
+ }
+
+ return this;
+ }
+
+ if (!(type in this._$events)) {
+ this._$events[type] = [];
+ }
+
+ this._$events[type].push(callback);
+
+ return this;
+ },
+
+ /**
+ * Adds an event listener which will
+ * only fire once and then remove itself.
+ *
+ *
+ * @param {String} type event name.
+ * @param {Function} callback fired when event is emitted.
+ */
+ once: function once(type, callback) {
+ var self = this;
+ function onceCb() {
+ self.removeEventListener(type, onceCb);
+ callback.apply(this, arguments);
+ }
+
+ this.addEventListener(type, onceCb);
+
+ return this;
+ },
+
+ /**
+ * Emits an event.
+ *
+ * Accepts any number of additional arguments to pass unto
+ * event listener.
+ *
+ * @param {String} eventName name of the event to emit.
+ * @param {Object} [arguments] additional arguments to pass.
+ */
+ emit: function emit() {
+ var args = Array.prototype.slice.call(arguments),
+ event = args.shift(),
+ eventList,
+ self = this;
+
+ if (event in this._$events) {
+ eventList = this._$events[event];
+
+ eventList.forEach(function(callback) {
+ callback.apply(self, args);
+ });
+ }
+
+ return this;
+ },
+
+ /**
+ * Removes all event listeners for a given event type
+ *
+ *
+ * @param {String} event event type to remove.
+ */
+ removeAllEventListeners: function removeAllEventListeners(name) {
+ if (name in this._$events) {
+ //reuse array
+ this._$events[name].length = 0;
+ }
+
+ return this;
+ },
+
+ /**
+ * Removes a single event listener from a given event type
+ * and callback function.
+ *
+ *
+ * @param {String} eventName event name.
+ * @param {Function} callback same instance of event handler.
+ */
+ removeEventListener: function removeEventListener(name, callback) {
+ var i, length, events;
+
+ if (!(name in this._$events)) {
+ return false;
+ }
+
+ events = this._$events[name];
+
+ for (i = 0, length = events.length; i < length; i++) {
+ if (events[i] && events[i] === callback) {
+ events.splice(i, 1);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ };
+
+ Responder.prototype.on = Responder.prototype.addEventListener;
+ module.exports = Responder;
+
+}.apply(
+ this,
+ (this.CalDav) ?
+ [CalDav('responder'), CalDav] :
+ [module, require('./caldav')]
+));
diff --git a/lib/caldav/sax.js b/lib/caldav/sax.js
new file mode 100644
index 0000000..28fa716
--- /dev/null
+++ b/lib/caldav/sax.js
@@ -0,0 +1,158 @@
+(function(module, ns) {
+
+ var sax = require('sax'),
+ Responder = ns.require('responder');
+
+ function Parser() {
+ var dispatch = [
+ 'onerror',
+ 'onopentag',
+ 'onclosetag',
+ 'ontext'
+ ];
+
+ this.parse = sax.parser(true, {
+ xmlns: true,
+ trim: true,
+ normalize: true,
+ lowercase: true
+ });
+
+ dispatch.forEach(function(event) {
+ this.parse[event] = this._dispatchEvent.bind(this, event);
+ }, this);
+
+ this.parse.onend = this.onend.bind(this);
+
+ this.depth = 0;
+ this.handles = {};
+
+ this.stack = [];
+ this.handlerStack = [];
+ this.tagStack = [];
+
+ this.current = this.root = {};
+ this.setParser(this);
+
+ Responder.call(this);
+ }
+
+ Parser.prototype = {
+
+ __proto__: Responder.prototype,
+
+ setParser: function(parse) {
+ this.currentParser = parse;
+ },
+
+ restoreParser: function() {
+ if ('oncomplete' in this.currentParser) {
+ this.currentParser.oncomplete.call(this);
+ }
+
+ var parser = this.handlerStack.pop();
+ this.setParser(parser || this);
+ },
+
+ _dispatchEvent: function(type, data) {
+
+ if (type === 'onopentag') {
+ data.tagSpec = data.uri + '/' + data.local;
+ }
+
+ if (type in this.currentParser) {
+ this.currentParser[type].call(this, data);
+ } else {
+ this[type](data);
+ }
+ },
+
+ addHandler: function(obj) {
+ this.handles[obj.tag] = obj;
+ },
+
+ checkHandler: function(handle) {
+ var handler,
+ handlers = this.currentParser.handles;
+
+ if (handle in handlers) {
+ handler = handlers[handle];
+ if (handler !== this.currentParser) {
+ return handler;
+ }
+ }
+
+ return false;
+ },
+
+ handleError: function() {
+ },
+
+ onopentag: function(data) {
+ var current = this.current,
+ name = data.local,
+ handler = this.checkHandler(data.tagSpec);
+
+ if (handler) {
+ this.handlerStack.push(this.currentParser);
+ this.setParser(handler);
+ return this._dispatchEvent('onopentag', data);
+ }
+
+ this.tagStack.push(data.tagSpec);
+ this.stack.push(this.current);
+
+ if (name in current) {
+ var next = {};
+
+ if (!(current[name] instanceof Array)) {
+ current[name] = [current[name]];
+ }
+
+ current[name].push(next);
+ this.current = next;
+ } else {
+ this.current = current[name] = {};
+ }
+ },
+
+ onend: function() {
+ this.emit('complete', this.root, this);
+ },
+
+ checkStackForHandler: function(restore) {
+ var stack = this.tagStack,
+ last = stack[stack.length - 1],
+ result;
+
+ result = last === this.currentParser.tag;
+
+ if (restore && result) {
+ this.restoreParser();
+ }
+
+ return result;
+ },
+
+ onclosetag: function() {
+ this.current = this.stack.pop();
+ this.tagStack.pop();
+ },
+
+ ontext: function(data) {
+ this.current.value = data;
+ },
+
+ write: function(data) {
+ return this.parse.write(data);
+ }
+ };
+
+ module.exports = Parser;
+
+}.apply(
+ this,
+ (this.CalDav) ?
+ [CalDav('xml_parser'), CalDav] :
+ [module, require('./caldav')]
+));
diff --git a/lib/caldav/sax/propstat.js b/lib/caldav/sax/propstat.js
new file mode 100644
index 0000000..0339f92
--- /dev/null
+++ b/lib/caldav/sax/propstat.js
@@ -0,0 +1,30 @@
+(function(module, ns) {
+ var Responder = ns.require('responder');
+
+ function Propstat(sax, complete) {
+
+ function onopen() {
+
+ }
+
+ function onclose() {
+
+ }
+
+ function ontext() {
+
+ }
+
+ sax.on('tagopen', onopen);
+ sax.on('tagclose', ontext);
+ sax.on('text', ontext);
+ }
+
+ module.exports = Propstat;
+
+}.apply(
+ this,
+ (this.CalDav) ?
+ [CalDav('sax/propstat'), CalDav] :
+ [module, require('../caldav')]
+));
diff --git a/lib/caldav/xhr.js b/lib/caldav/xhr.js
new file mode 100644
index 0000000..bfc3eeb
--- /dev/null
+++ b/lib/caldav/xhr.js
@@ -0,0 +1,111 @@
+/**
+@namespace
+*/
+(function(module, ns) {
+ var Native;
+
+ if (typeof(window) === 'undefined') {
+ Native = require('xmlhttprequest').XMLHttpRequest;
+ } else {
+ Native = window.XMLHttpRequest;
+ }
+
+ /**
+ * Creates a XHR wrapper.
+ * Depending on the platform this is loaded
+ * from the correct wrapper type will be used.
+ *
+ * Options are derived from properties on the prototype.
+ * See each property for its default value.
+ *
+ * @class
+ * @name CalDav.Xhr
+ * @param {Object} options options for xhr.
+ * @param {String} [options.method="GET"] any HTTP verb like 'GET' or 'POST'.
+ * @param {Boolean} [options.async] false will indicate
+ * a synchronous request.
+ * @param {Object} [options.headers] full of http headers.
+ * @param {Object} [options.data] post data.
+ */
+ function Xhr(options) {
+ var key;
+ if (typeof(options) === 'undefined') {
+ options = {};
+ }
+
+ for (key in options) {
+ if (options.hasOwnProperty(key)) {
+ this[key] = options[key];
+ }
+ }
+ }
+
+ Xhr.prototype = {
+ /** @scope CalDav.Xhr.prototype */
+
+ xhrClass: Native,
+ method: 'GET',
+ async: true,
+ waiting: false,
+ user: null,
+ password: null,
+
+ headers: {},
+ data: {},
+
+ _seralize: function _seralize() {
+ return this.data;
+ },
+
+ /**
+ * Aborts request if its in progress.
+ */
+ abort: function abort() {
+ if (this.xhr) {
+ this.xhr.abort();
+ }
+ },
+
+ /**
+ * Sends request to server.
+ *
+ * @param {Function} callback success/failure handler.
+ */
+ send: function send(callback) {
+ var header, xhr;
+
+ if (typeof(callback) === 'undefined') {
+ callback = this.callback;
+ }
+
+ xhr = this.xhr = new this.xhrClass();
+ xhr.open(this.method, this.url, this.async, this.user, this.password);
+
+ for (header in this.headers) {
+ if (this.headers.hasOwnProperty(header)) {
+ xhr.setRequestHeader(header, this.headers[header]);
+ }
+ }
+
+ xhr.onreadystatechange = function onReadyStateChange() {
+ var data;
+ if (xhr.readyState === 4) {
+ data = xhr.responseText;
+ this.waiting = false;
+ callback(data, xhr);
+ }
+ }.bind(this);
+
+ this.waiting = true;
+ xhr.send(this._seralize());
+ }
+ };
+
+ module.exports = Xhr;
+
+}.apply(
+ this,
+ (this.CalDav) ?
+ [CalDav('xhr'), CalDav] :
+ [module, require('./caldav')]
+));
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4c84623
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "caldav",
+ "version": "0.0.1",
+ "author": "",
+ "description": "",
+ "main": "lib/index.js",
+
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/.git"
+ },
+
+ "keywords": [
+ ""
+ ],
+ "dependencies" : {
+ "xmlhttprequest": "1.4.2"
+ },
+ "devDependencies": {
+ "mocha": "~1.1",
+ "chai": "~1.0",
+ "sax": "0.4.0"
+ },
+ "license": "MIT",
+ "engine": {
+ "node": ">=0.4"
+ }
+}
diff --git a/samples/calendar-data b/samples/calendar-data
new file mode 100644
index 0000000..1b9ed7b
--- /dev/null
+++ b/samples/calendar-data
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<C:calendar-query xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR"/>
+ </C:filter>
+</C:calendar-query>
diff --git a/samples/get-props b/samples/get-props
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/samples/get-props
diff --git a/samples/resource-type b/samples/resource-type
new file mode 100644
index 0000000..a6aa471
--- /dev/null
+++ b/samples/resource-type
@@ -0,0 +1,10 @@
+<?xml version="1.0" ?>
+<D:propfind xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:D="DAV:">
+ <D:prop>
+ <D:resourcetype/>
+ <D:owner/>
+ <D:supported-report-set/>
+ <C:supported-calendar-component-set/>
+ <CS:getctag/>
+ </D:prop>
+</D:propfind>
diff --git a/samples/xml/calendar-multiget.xml b/samples/xml/calendar-multiget.xml
new file mode 100644
index 0000000..9ef1aaf
--- /dev/null
+++ b/samples/xml/calendar-multiget.xml
@@ -0,0 +1,63 @@
+<D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>/dav/calmozilla1/Calendar/calmozilla1/6e4d147f-4d75-436f-a233-976fd06fd3f7.ics</D:href>
+ <D:propstat>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ <D:prop>
+ <C:calendar-data xmlns:C="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR
+ VERSION:2.0 PRODID:Zimbra-Calendar-Provider
+ BEGIN:VTIMEZONE TZID:Etc/GMT
+ BEGIN:STANDARD DTSTART:19710101T000000
+ TZOFFSETTO:-0000 TZOFFSETFROM:-0000
+ TZNAME:GMT END:STANDARD
+ END:VTIMEZONE BEGIN:VEVENT
+ UID:6e4d147f-4d75-436f-a233-976fd06fd3f7 SUMMARY:Foo
+ ORGANIZER:mailto:calmozilla1@yahoo.com DTSTART;TZID="Etc/GMT":20120629T143000
+ DTEND;TZID="Etc/GMT":20120629T150000 STATUS:CONFIRMED
+ CLASS:PUBLIC X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
+ TRANSP:OPAQUE X-MICROSOFT-DISALLOW-COUNTER:TRUE
+ DTSTAMP:20120616T221243Z SEQUENCE:0
+ END:VEVENT END:VCALENDAR</C:calendar-data>
+ <D:getetag>"2-2"</D:getetag>
+ </D:prop>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>/dav/calmozilla1/Calendar/calmozilla1/25b74da2-47d3-4ea8-be27-774fb13041b8.ics</D:href>
+ <D:propstat>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ <D:prop>
+ <C:calendar-data xmlns:C="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR
+ VERSION:2.0 PRODID:Zimbra-Calendar-Provider
+ BEGIN:VEVENT UID:25b74da2-47d3-4ea8-be27-774fb13041b8
+ SUMMARY:Bar ORGANIZER;CN=Sahaja Lal:mailto:calmozilla1@yahoo.com
+ DTSTART;VALUE=DATE:20120630 DTEND;VALUE=DATE:20120701
+ STATUS:CONFIRMED CLASS:PUBLIC
+ X-MICROSOFT-CDO-ALLDAYEVENT:TRUE X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
+ TRANSP:OPAQUE X-MICROSOFT-DISALLOW-COUNTER:TRUE
+ DTSTAMP:20120616T221255Z SEQUENCE:0
+ END:VEVENT END:VCALENDAR</C:calendar-data>
+ <D:getetag>"3-3"</D:getetag>
+ </D:prop>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>/dav/calmozilla1/Calendar/calmozilla1/a8c499cb-3cab-44d5-ab1e-18abc0f5286f.ics</D:href>
+ <D:propstat>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ <D:prop>
+ <C:calendar-data xmlns:C="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR
+ VERSION:2.0 PRODID:Zimbra-Calendar-Provider
+ BEGIN:VEVENT UID:a8c499cb-3cab-44d5-ab1e-18abc0f5286f
+ SUMMARY:Baz ORGANIZER;CN=Sahaja Lal:mailto:calmozilla1@yahoo.com
+ DTSTART;VALUE=DATE:20120628 DTEND;VALUE=DATE:20120629
+ STATUS:CONFIRMED CLASS:PUBLIC
+ X-MICROSOFT-CDO-ALLDAYEVENT:TRUE X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
+ TRANSP:OPAQUE X-MICROSOFT-DISALLOW-COUNTER:TRUE
+ DTSTAMP:20120616T221305Z SEQUENCE:0
+ END:VEVENT END:VCALENDAR</C:calendar-data>
+ <D:getetag>"4-4"</D:getetag>
+ </D:prop>
+ </D:propstat>
+ </D:response>
+</D:multistatus>
diff --git a/samples/xml/prop-get.xml b/samples/xml/prop-get.xml
new file mode 100644
index 0000000..72ee984
--- /dev/null
+++ b/samples/xml/prop-get.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<D:multistatus xmlns:D="DAV:">
+
+ <D:response>
+ <D:href>/calendar/dav/calmozilla1%40gmail.com/user/</D:href>
+ <D:propstat>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ <D:prop>
+ <D:principal-URL>
+ <D:href>/calendar/dav/calmozilla1@gmail.com/user/</D:href>
+ </D:principal-URL>
+ <D:resourcetype>
+ <D:principal />
+ <D:collection />
+ </D:resourcetype>
+ </D:prop>
+ </D:propstat>
+
+ <D:propstat>
+ <D:status>HTTP/1.1 404 Not Found</D:status>
+ <D:prop>
+ <A:current-user-principal xmlns:A="DAV:" />
+ </D:prop>
+ </D:propstat>
+ </D:response>
+
+</D:multistatus>
diff --git a/samples/xml/propstat-success.xml b/samples/xml/propstat-success.xml
new file mode 100644
index 0000000..93f27ec
--- /dev/null
+++ b/samples/xml/propstat-success.xml
@@ -0,0 +1,10 @@
+<D:status>HTTP/1.1 200 OK</D:status>
+<D:prop>
+ <D:principal-URL>
+ <D:href>/calendar/dav/calmozilla1@gmail.com/user/</D:href>
+ </D:principal-URL>
+ <D:resourcetype>
+ <D:principal />
+ <D:collection />
+ </D:resourcetype>
+</D:prop>
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..a0452e3
--- /dev/null
+++ b/test.js
@@ -0,0 +1,104 @@
+var XHR = require('./lib/caldav/xhr.js');
+var url, user, pass, domain;
+
+function getBody(file) {
+ return require('fs').readFileSync(
+ __dirname + '/samples/' + file, 'utf8'
+ );
+}
+
+domain = 'http://localdav.com';
+uri = '/calendars/admin';
+user = 'admin';
+pass = 'admin';
+
+function request(options) {
+ var defaults = {
+ user: user,
+ password: pass,
+ url: domain + uri + (options.uri || ''),
+ headers: {
+ Depth: 0
+ }
+ };
+
+ if (typeof(options) === 'undefined') {
+ options = {};
+ }
+
+ for (var key in options) {
+ if (options.hasOwnProperty(key)) {
+ defaults[key] = options[key];
+ }
+ }
+
+ return new XHR(defaults);
+}
+
+
+
+function getProps() {
+
+ var body = '<?xml version="1.0" encoding="utf-8" ?>' +
+ '<D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">' +
+ '<D:prop>' +
+ '<C:calendar-home-set/>' +
+ '<C:calendar-user-address-set/>' +
+ '<C:schedule-inbox-URL/>' +
+ '<C:schedule-outbox-URL/>' +
+ '</D:prop>' +
+ '</D:propfind>';
+
+
+ var xhr = request({
+ method: 'PROPFIND',
+ data: body,
+ headers: {
+ 'Depth': 0
+ }
+ });
+
+ xhr.send(function(text, xhr) {
+ console.log(xhr.responseText);
+ });
+}
+
+
+function getCalenders() {
+ var body = getBody('calendar-data');
+
+ var xhr = request({
+ url: domain + '/calendar/dav/james%40lightsofapollo.com/',
+ method: 'REPORT',
+ headers: {
+ Depth: 1
+ },
+ data: body
+ });
+
+ xhr.send(function(text) {
+ console.log(text);
+ });
+}
+
+
+function checkResource() {
+ var body = getBody('resource-type'),
+ xhr;
+
+ xhr = request({
+ uri: '/default',
+ method: 'PROPFIND',
+ data: body
+ });
+
+ xhr.send(function(text) {
+ console.log(text);
+ });
+
+}
+
+checkResource();
+
+//getCalenders();
+//getProps();
diff --git a/test/caldav/ics_test.js b/test/caldav/ics_test.js
new file mode 100644
index 0000000..3f821b4
--- /dev/null
+++ b/test/caldav/ics_test.js
@@ -0,0 +1,15 @@
+var fs = require('fs'),
+ ics = requireLib('ics'),
+ data = fs.readFileSync(__dirname + '/../../data/test.data', 'utf8');
+
+suite('caldav/ics', function() {
+
+ test('intiailizer', function() {
+ ics(data, function(data) {
+ console.log(data.vevent[0]);
+ }, function() {
+ console.log('y');
+ })
+ });
+
+});
diff --git a/test/caldav/sax/propstat_test.js b/test/caldav/sax/propstat_test.js
new file mode 100644
index 0000000..74360a7
--- /dev/null
+++ b/test/caldav/sax/propstat_test.js
@@ -0,0 +1,27 @@
+suite('caldav/sax/propstat', function() {
+ var stat = requireLib('sax/propstat'),
+ sax = requireLib('sax'),
+ subject;
+
+ var expected = {
+ status: 200,
+ 'principal-URL': '/calendar/dav/calmozilla1@gmail.com/user/',
+ 'resource-type': [
+ 'principal',
+ 'collection'
+ ]
+ };
+
+ test('propstat success', function(done) {
+ var parser = sax();
+
+ console.log(parser.on);
+ stat(parser, function(err, result) {
+ console.log(result);
+ done();
+ });
+
+ parser.write(loadSample('xml/propstat-success.xml')).close();
+ });
+
+});
diff --git a/test/caldav/sax_test.js b/test/caldav/sax_test.js
new file mode 100644
index 0000000..83d69c1
--- /dev/null
+++ b/test/caldav/sax_test.js
@@ -0,0 +1,93 @@
+var xml = requireLib('sax');
+
+suite('sax test', function() {
+
+ var data,
+ subject;
+
+ test('existance', function() {
+ var parser = new xml();
+
+ var StatusHandler = {
+ tag: 'DAV:/status',
+
+ onopentag: function(data) {
+ },
+
+ ontext: function(data) {
+ this.current.status = data.match(/([0-9]{3,3})/)[1];
+ },
+
+ onclosetag: function(data) {
+ this.restoreParser();
+ }
+ };
+
+ var ResourceTypeHandler = {
+ tag: 'DAV:/resourcetype',
+
+ onopentag: function(data) {
+ this.tagStack.push(data.tagSpec);
+
+ if (data.local === 'resourcetype') {
+ this.current.resourceTypes = [];
+ } else {
+ this.current.resourceTypes.push(data.local);
+ }
+ },
+
+ onclosetag: function(data) {
+ this.checkStackForHandler(true);
+ this.tagStack.pop();
+ }
+ };
+
+ var TextOnlyHandler = {
+ tag: 'DAV:/href',
+
+ onopentag: function(data) {
+ },
+
+ ontext: function(data) {
+ this.current.href = data;
+ },
+
+ onclosetag: function(data) {
+ this.restoreParser();
+ }
+ };
+
+ var ResponseHandler = {
+ tag: 'DAV:/response',
+ handles: {
+ 'DAV:/status': StatusHandler,
+ 'DAV:/resourcetype': ResourceTypeHandler,
+ 'DAV:/href': TextOnlyHandler,
+ 'DAV:/getetag': TextOnlyHandler
+ },
+
+ onclosetag: function(data) {
+ this.checkStackForHandler(true);
+ this.onclosetag(data);
+ },
+
+ oncomplete: function() {
+ this.emit('response', this.current, this);
+ }
+
+ };
+
+ parser.addHandler(ResponseHandler);
+
+ parser.on('response', function(data, context) {
+ console.log(JSON.stringify(data), '\n\n');
+ });
+
+ parser.once('complete', function(data, parser) {
+ console.log(JSON.stringify(data));
+ });
+
+ parser.write(data).close();
+ });
+
+});
diff --git a/test/helper.js b/test/helper.js
new file mode 100644
index 0000000..9f40311
--- /dev/null
+++ b/test/helper.js
@@ -0,0 +1,16 @@
+var chai = require('chai'),
+ fs = require('fs');
+
+chai.Assertion.includeStack = true;
+assert = chai.assert;
+
+loadSample = function(file, cb) {
+ var root = __dirname + '/../samples/';
+ fs.readFile(root + file, 'utf8', function(err, contents) {
+ cb(err, contents);
+ });
+};
+
+requireLib = function(lib) {
+ return require(__dirname + '/../lib/caldav/' + lib);
+};