diff options
Diffstat (limited to 'lib/caldav')
-rw-r--r-- | lib/caldav/caldav.js | 101 | ||||
-rw-r--r-- | lib/caldav/ical.js | 139 | ||||
-rw-r--r-- | lib/caldav/index.js | 18 | ||||
-rw-r--r-- | lib/caldav/request/abstract.js | 92 | ||||
-rw-r--r-- | lib/caldav/request/calendar_query.js | 57 | ||||
-rw-r--r-- | lib/caldav/request/index.js | 14 | ||||
-rw-r--r-- | lib/caldav/request/propfind.js | 69 | ||||
-rw-r--r-- | lib/caldav/resource_root.js | 14 | ||||
-rw-r--r-- | lib/caldav/responder.js | 208 | ||||
-rw-r--r-- | lib/caldav/sax.js | 220 | ||||
-rw-r--r-- | lib/caldav/sax/base.js | 73 | ||||
-rw-r--r-- | lib/caldav/sax/dav_response.js | 208 | ||||
-rw-r--r-- | lib/caldav/sax/index.js | 14 | ||||
-rw-r--r-- | lib/caldav/template.js | 120 | ||||
-rw-r--r-- | lib/caldav/templates/calendar_data.js | 107 | ||||
-rw-r--r-- | lib/caldav/templates/calendar_filter.js | 27 | ||||
-rw-r--r-- | lib/caldav/templates/index.js | 13 | ||||
-rw-r--r-- | lib/caldav/xhr.js | 118 |
18 files changed, 1612 insertions, 0 deletions
diff --git a/lib/caldav/caldav.js b/lib/caldav/caldav.js new file mode 100644 index 0000000..065b815 --- /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/ical.js b/lib/caldav/ical.js new file mode 100644 index 0000000..a7da546 --- /dev/null +++ b/lib/caldav/ical.js @@ -0,0 +1,139 @@ +(function(module, ns) { + // Credit: Andreas Gal - I removed the callback / xhr logic + + // Iterate over all entries if x is an array, otherwise just call fn on x. + + /* 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) { + // Parse the text into an object graph + var lines = text.replace(/\r/g, '').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) { + throw new 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) { + throw new Error('single vcalendar object expected'); + } + + return cal.vcalendar; + } + break; + default: + add(prop, value, params); + break; + } + } + throw new 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('ical'), Caldav] : + [module, require('./caldav')] +)); + diff --git a/lib/caldav/index.js b/lib/caldav/index.js new file mode 100644 index 0000000..b54a9ac --- /dev/null +++ b/lib/caldav/index.js @@ -0,0 +1,18 @@ +(function(module, ns) { + + var exports = module.exports; + + exports.Ical = ns.require('ical'); + exports.Responder = ns.require('responder'); + exports.Sax = ns.require('sax'); + exports.Template = ns.require('template'); + exports.Xhr = ns.require('xhr'); + exports.Request = ns.require('request'); + exports.Templates = ns.require('templates'); + +}.apply( + this, + (this.Caldav) ? + [Caldav, Caldav] : + [module, require('./caldav')] +)); diff --git a/lib/caldav/request/abstract.js b/lib/caldav/request/abstract.js new file mode 100644 index 0000000..04d2128 --- /dev/null +++ b/lib/caldav/request/abstract.js @@ -0,0 +1,92 @@ +(function(module, ns) { + + var SAX = ns.require('sax'); + var XHR = ns.require('xhr'); + + + /** + * Creates an (Web/Cal)Dav request. + * + * @param {String} url location of resource. + * @param {Object} options additional options for request. + */ + function Abstract(url, options) { + if (typeof(options) === 'undefined') { + options = {}; + } + + var key; + var xhrOptions = {}; + + if (typeof(url) === 'undefined' || !url) { + throw new Error('request requires a url'); + } + + xhrOptions.url = url; + + if ('password' in options) { + xhrOptions.password = options.password; + delete options.password; + } + + if ('user' in options) { + xhrOptions.user = options.user; + delete options.user; + } + + this.sax = new SAX(); + + for (key in options) { + if (options.hasOwnProperty(key)) { + this[key] = options[key]; + } + } + + this.xhr = new XHR(xhrOptions); + } + + Abstract.prototype = { + + _createPayload: function() { + return ''; + }, + + _processResult: function(req, callback) { + callback.call(this, null, this.sax.root, req); + }, + + /** + * Sends request to server. + * + * @param {Function} callback node style callback. + * Receives three arguments + * error, parsedData, xhr. + */ + send: function(callback) { + var self = this; + var req = this.xhr; + req.data = this._createPayload(); + + // in the future we may stream data somehow + req.send(function xhrResult() { + var xhr = req.xhr; + if (xhr.status > 199 && xhr.status < 300) { + // success + self.sax.write(xhr.responseText).close(); + self._processResult(req, callback); + } else { + // fail + callback(new Error('http error code: ' + xhr.status)); + } + }); + } + }; + + module.exports = Abstract; + +}.apply( + this, + (this.Caldav) ? + [Caldav('request/abstract'), Caldav] : + [module, require('../caldav')] +)); diff --git a/lib/caldav/request/calendar_query.js b/lib/caldav/request/calendar_query.js new file mode 100644 index 0000000..7b64d3f --- /dev/null +++ b/lib/caldav/request/calendar_query.js @@ -0,0 +1,57 @@ +(function(module, ns) { + + var Propfind = ns.require('request/propfind'); + var CalendarData = ns.require('templates/calendar_data'); + var CalendarFilter = ns.require('templates/calendar_filter'); + + /** + * Creates a calendar query request. + * + * Defaults to Depth of 1. + * + * @param {String} url location to make request. + * @param {Object} options options for calendar query. + */ + function CalendarQuery(url, options) { + Propfind.apply(this, arguments); + + this.xhr.headers['Depth'] = this.depth || 1; + this.xhr.method = 'REPORT'; + this.fields = new CalendarData(); + this.filters = new CalendarFilter(); + + this.template.rootTag = ['caldav', 'calendar-query']; + } + + CalendarQuery.prototype = { + __proto__: Propfind.prototype, + + _createPayload: function() { + var content; + var props; + + props = this._props.join(''); + + if (this.fields) { + props += this.fields.render(this.template); + } + + content = this.template.tag('prop', props); + + if (this.filters) { + content += this.filters.render(this.template); + } + + return this.template.render(content); + } + + }; + + module.exports = CalendarQuery; + +}.apply( + this, + (this.Caldav) ? + [Caldav('request/calendar_query'), Caldav] : + [module, require('../caldav')] +)); diff --git a/lib/caldav/request/index.js b/lib/caldav/request/index.js new file mode 100644 index 0000000..fc49491 --- /dev/null +++ b/lib/caldav/request/index.js @@ -0,0 +1,14 @@ +(function(module, ns) { + + module.exports = { + Abstract: ns.require('request/abstract'), + CalendarQuery: ns.require('request/calendar_query'), + Propfind: ns.require('request/propfind') + }; + +}.apply( + this, + (this.Caldav) ? + [Caldav('request'), Caldav] : + [module, require('../caldav')] +)); diff --git a/lib/caldav/request/propfind.js b/lib/caldav/request/propfind.js new file mode 100644 index 0000000..7202ea5 --- /dev/null +++ b/lib/caldav/request/propfind.js @@ -0,0 +1,69 @@ +(function(module, ns) { + + var Abstract = ns.require('request/abstract'), + Template = ns.require('template'), + DavResponse = ns.require('sax/dav_response'); + + /** + * Creates a propfind request. + * + * @param {String} url location to make request. + * @param {Object} options options for propfind. + */ + function Propfind(url, options) { + Abstract.apply(this, arguments); + + this.template = new Template('propfind'); + this._props = []; + this.sax.registerHandler( + 'DAV:/response', + DavResponse + ); + + this.xhr.headers['Depth'] = this.depth; + this.xhr.method = 'PROPFIND'; + } + + Propfind.prototype = { + __proto__: Abstract.prototype, + + depth: 0, + + /** + * Adds property to request. + * + * @param {String|Array} tagDesc tag description. + * @param {Object} [attr] optional tag attrs. + * @param {Obj} [content] optional content. + */ + prop: function(tagDesc, attr, content) { + this._props.push(this.template.tag(tagDesc, attr, content)); + }, + + _createPayload: function() { + var content = this.template.tag('prop', this._props.join('')); + return this.template.render(content); + }, + + _processResult: function(req, callback) { + if ('multistatus' in this.sax.root) { + callback(null, this.sax.root.multistatus, req); + } else { + //XXX: Improve error handling + callback( + new Error('unexpected xml result'), + this.sax.root, req + ); + } + } + + }; + + module.exports = Propfind; + +}.apply( + this, + (this.Caldav) ? + [Caldav('request/propfind'), Caldav] : + [module, require('../caldav')] +)); diff --git a/lib/caldav/resource_root.js b/lib/caldav/resource_root.js new file mode 100644 index 0000000..0cff9b7 --- /dev/null +++ b/lib/caldav/resource_root.js @@ -0,0 +1,14 @@ +(function(module, ns) { + + function ResourceRoot() { + + } + + module.exports = ResourceRoot; + +}.apply( + this, + (this.Caldav) ? + [Caldav('resource_root'), Caldav] : + [module, require('./caldav')] +)); diff --git a/lib/caldav/responder.js b/lib/caldav/responder.js new file mode 100644 index 0000000..6ba41c4 --- /dev/null +++ b/lib/caldav/responder.js @@ -0,0 +1,208 @@ +/** +@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..db3a486 --- /dev/null +++ b/lib/caldav/sax.js @@ -0,0 +1,220 @@ +(function(module, ns) { + + var Responder = ns.require('responder'); + + if (typeof(window) === 'undefined') { + Parser.sax = require('sax'); + } else { + Parser.sax = sax; + } + + /** + * Creates a parser object. + * + * @param {Object} baseHandler base sax handler. + */ + function Parser(baseHandler) { + var handler; + + var events = [ + 'ontext', + 'onopentag', + 'onclosetag', + 'onerror', + 'onend' + ]; + + if (typeof(baseHandler) !== 'undefined') { + handler = baseHandler; + } else { + handler = ns.require('sax/base'); + } + + this.stack = []; + this.handles = {}; + this._handlerStack = []; + this.tagStack = []; + this.root = this.current = {}; + + this.setHandler(handler); + + this._parse = Parser.sax.parser(true, { + xmlns: true, + trim: true, + normalize: false, + lowercase: true + }); + + events.forEach(function(event) { + this._parse[event] = this[event].bind(this); + }, this); + + Responder.call(this); + } + + Parser.prototype = { + + __proto__: Responder.prototype, + + /** + * Sets current handler, optionally adding + * previous one to the handlerStack. + * + * @param {Object} handler new handler. + * @param {Boolean} storeOriginal store old handler? + */ + setHandler: function(handler, storeOriginal) { + if (storeOriginal) { + this._handlerStack.push(this.handler); + } + + this.handler = handler; + }, + + /** + * Sets handler to previous one in the stack. + */ + restoreHandler: function() { + if (this._handlerStack.length) { + this.handler = this._handlerStack.pop(); + } + }, + + /** + * Registers a top level handler + * + * @param {String} tag xmlns uri/local tag name for example + * DAV:/a. + * + * @param {Object} handler new handler to use when tag is + * triggered. + */ + registerHandler: function(tag, handler) { + this.handles[tag] = handler; + }, + + /** + * Writes data into the parser. + * + * @param {String} chunk partial/complete chunk of xml. + */ + write: function(chunk) { + return this._parse.write(chunk); + }, + + get closed() { + return this._parse.closed; + }, + + /** + * Determines if given tagSpec has a specific handler. + * + * @param {String} tagSpec usual tag spec. + */ + getHandler: function(tagSpec) { + var handler; + var handlers = this.handler.handles; + + if (!handlers) { + handlers = this.handles; + } + + if (tagSpec in handlers) { + handler = handlers[tagSpec]; + + if (handler !== this.handler) { + return handler; + } + } + + return false; + }, + + _fireHandler: function(event, data) { + if (typeof(this.handler[event]) === 'function') { + this.handler[event].call(this, data, this.handler); + } + }, + + onopentag: function(data) { + var handle; + var stackData = { + local: data.local, + name: data.name + }; + + //build tagSpec for others to use. + data.tagSpec = data.uri + '/' + data.local; + + //add to stackData + stackData.tagSpec = data.tagSpec; + + // shortcut to the current tag object + this.currentTag = stackData; + + //determine if we need to switch to another + //handler object. + handle = this.getHandler(data.tagSpec); + + if (handle) { + //switch to new handler object + this.setHandler(handle, true); + stackData.handler = handle; + } + + this.tagStack.push(stackData); + this._fireHandler('onopentag', data); + }, + + //XXX: optimize later + get currentTag() { + return this.tagStack[this.tagStack.length - 1]; + }, + + onclosetag: function(data) { + var stack, handler; + + stack = this.currentTag; + + if (stack.handler) { + //fire oncomplete handler if available + this._fireHandler('oncomplete'); + } + + //fire the onclosetag event + this._fireHandler('onclosetag', data); + + if (stack.handler) { + //restore previous handler + this.restoreHandler(); + } + + //actually remove the stack tag + this.tagStack.pop(); + }, + + ontext: function(data) { + this._fireHandler('ontext', data); + }, + + onerror: function(data) { + //TODO: XXX implement handling of parsing errors. + //unlikely but possible if server goes down + //or there is some authentication issue that + //we miss. + this._fireHandler('onerror', data); + }, + + onend: function() { + this._fireHandler('onend', this.root); + } + }; + + module.exports = Parser; + +}.apply( + this, + (this.Caldav) ? + [Caldav('sax'), Caldav] : + [module, require('./caldav')] +)); diff --git a/lib/caldav/sax/base.js b/lib/caldav/sax/base.js new file mode 100644 index 0000000..a823aad --- /dev/null +++ b/lib/caldav/sax/base.js @@ -0,0 +1,73 @@ +(function(module, ns) { + + var Base = { + + name: 'base', + + tagField: 'local', + + /** + * Creates a new object with base as its prototype. + * Adds ._super to object as convenience prop to access + * the parents functions. + * + * @param {Object} obj function overrides. + * @return {Object} new object. + */ + create: function(obj) { + var key; + var child = Object.create(this); + + child._super = this; + + for (key in obj) { + if (obj.hasOwnProperty(key)) { + child[key] = obj[key]; + } + } + + return child; + }, + + onopentag: function(data, handler) { + var current = this.current; + var name = data[handler.tagField]; + + 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] = {}; + } + }, + + ontext: function(data) { + this.current.value = data; + }, + + onclosetag: function() { + this.current = this.stack.pop(); + }, + + onend: function() { + this.emit('complete', this.root); + } + }; + + module.exports = Base; + +}.apply( + this, + (this.Caldav) ? + [Caldav('sax/base'), Caldav] : + [module, require('../caldav')] +)); diff --git a/lib/caldav/sax/dav_response.js b/lib/caldav/sax/dav_response.js new file mode 100644 index 0000000..b2556a7 --- /dev/null +++ b/lib/caldav/sax/dav_response.js @@ -0,0 +1,208 @@ +(function(module, ns) { + + var HTTP_STATUS = /([0-9]{3,3})/; + + var Base = ns.require('sax/base'); + var ParseICal = ns.require('ical'); + + var TextHandler = Base.create({ + name: 'text', + + //don't add text only elements + //to the stack as objects + onopentag: null, + onclosetag: null, + + //add the value to the parent + //value where key is local tag name + //and value is the text. + ontext: function(data) { + var handler = this.handler; + this.current[this.currentTag[handler.tagField]] = data; + } + }); + + var CalendarDataHandler = Base.create({ + name: 'calendar data', + + //don't add text only elements + //to the stack as objects + onopentag: null, + onclosetag: null, + + //add the value to the parent + //value where key is local tag name + //and value is the text. + ontext: function(data) { + var handler = this.handler; + this.current[this.currentTag[handler.tagField]] = ParseICal(data); + } + }); + + + var HrefHandler = Base.create({ + name: 'href', + + //don't add text only elements + //to the stack as objects + onopentag: null, + onclosetag: null, + + onopentag: function() { + if (this.currentTag.handler === this.handler) { + this.stack.push(this.current); + this.current = null; + } + }, + + onclosetag: function() { + var current = this.currentTag; + var data; + + if (current.handler === this.handler) { + data = this.current; + + this.current = this.stack.pop(); + this.current[current.local] = data; + } + }, + + ontext: function(data) { + if (this.currentTag.local === 'href') { + this.current = data; + } + } + + }); + + var HttpStatusHandler = TextHandler.create({ + name: 'status', + + ontext: function(data, handler) { + var match = data.match(HTTP_STATUS); + + if (match) { + var handler = this.handler; + this.current[this.currentTag[handler.tagField]] = match[1]; + } else { + this._super.ontext.call(this, data, handler); + } + } + }); + + var ArrayHandler = Base.create({ + name: 'array', + + handles: { + 'DAV:/href': TextHandler + }, + + onopentag: function(data, handler) { + var last; + var tag = data[handler.tagField]; + var last = this.tagStack[this.tagStack.length - 1]; + + if (last.handler === handler) { + this.stack.push(this.current); + this.current = this.current[tag] = []; + } else { + this.current.push(tag); + } + }, + + ontext: null, + onclosetag: null, + + oncomplete: function() { + this.current = this.stack.pop(); + } + + }); + + var PropStatHandler = Base.create({ + name: 'propstat', + + handles: { + 'DAV:/href': TextHandler, + 'DAV:/status': HttpStatusHandler, + 'DAV:/resourcetype': ArrayHandler, + 'DAV:/principal-URL': HrefHandler, + 'urn:ietf:params:xml:ns:caldav/calendar-data': CalendarDataHandler, + 'DAV:/value': TextHandler, + 'urn:ietf:params:xml:ns:caldav/calendar-home-set': HrefHandler, + 'urn:ietf:params:xml:ns:caldav/calendar-user-address-set': HrefHandler + }, + + onopentag: function(data, handler) { + //orphan + if (data.tagSpec === 'DAV:/propstat') { + //blank slate propstat + if (!('propstat' in this.current)) { + this.current['propstat'] = {}; + } + + this.stack.push(this.current); + + //contents will be copied over later. + return this.current = {}; + } + + handler._super.onopentag.call(this, data, handler); + }, + + oncomplete: function() { + var propstat = this.stack[this.stack.length - 1]; + propstat = propstat.propstat; + var key; + var status = this.current.status; + var props = this.current.prop; + + delete this.current.status; + delete this.current.prop; + + for (key in props) { + if (props.hasOwnProperty(key)) { + propstat[key] = { + status: status, + value: props[key] + }; + } + } + } + }); + + var Response = Base.create({ + name: 'dav_response', + handles: { + 'DAV:/href': TextHandler, + 'DAV:/propstat': PropStatHandler + }, + + onopentag: function(data, handler) { + if (data.tagSpec === 'DAV:/response') { + this.stack.push(this.current); + return this.current = {}; + } + + handler._super.onopentag.call(this, data, handler._super); + }, + + oncomplete: function() { + var parent; + + if (this.current.href) { + parent = this.stack[this.stack.length - 1]; + parent[this.current.href] = this.current.propstat; + } + } + + }); + + module.exports = Response; + +}.apply( + this, + (this.Caldav) ? + [Caldav('sax/dav_response'), Caldav] : + [module, require('../caldav')] +)); diff --git a/lib/caldav/sax/index.js b/lib/caldav/sax/index.js new file mode 100644 index 0000000..2ba2f16 --- /dev/null +++ b/lib/caldav/sax/index.js @@ -0,0 +1,14 @@ +(function(module, ns) { + + module.exports = { + Abstract: ns.require('sax/abstract'), + CalendarQuery: ns.require('sax/dav_response') + }; + +}.apply( + this, + (this.Caldav) ? + [Caldav('sax'), Caldav] : + [module, require('../caldav')] +)); + diff --git a/lib/caldav/template.js b/lib/caldav/template.js new file mode 100644 index 0000000..2916ce6 --- /dev/null +++ b/lib/caldav/template.js @@ -0,0 +1,120 @@ +(function(module, ns) { + + function Template(root, rootAttrs) { + if (typeof(rootAttrs) === 'undefined') { + rootAttrs = {}; + } + + this.rootTag = root; + this.rootAttrs = rootAttrs; + this.activeNamespaces = {}; + } + + Template.prototype = { + + defaultNamespace: 'dav', + doctype: '<?xml version="1.0" encoding="UTF-8"?>', + + _nsId: 0, + + xmlns: { + dav: 'DAV:', + calserver: 'http://calendarserver.org/ns/', + ical: 'http://apple.com/ns/ical/', + caldav: 'urn:ietf:params:xml:ns:caldav' + }, + + render: function(content) { + var output = this.doctype; + output += this.tag(this.rootTag, this.rootAttrs, content); + + return output; + }, + + /** + * Registers an xml tag/namespace. + * + * @param {String} prefix xmlns. + * @param {String} tag tag name. + * @return {String} xml tag name. + */ + _registerTag: function(prefix, tag) { + if (prefix in this.xmlns) { + prefix = this.xmlns[prefix]; + } + + if (prefix in this.activeNamespaces) { + prefix = this.activeNamespaces[prefix]; + } else { + var alias = 'N' + this._nsId++; + this.activeNamespaces[prefix] = alias; + this.rootAttrs['xmlns:' + alias] = prefix; + prefix = alias; + } + + return prefix + ':' + tag; + }, + + /** + * Returns a xml string based on + * input. Registers given xmlns on main + * template. Always use this with render. + * + * @param {String|Array} tagDesc as a string defaults to + * .defaultNamespace an array + * takes a xmlns or an alias + * defined in .xmlns. + * + * @param {Object} [attrs] optional attributes. + * @param {String} content content of tag. + * @return {String} xml tag output. + */ + tag: function(tagDesc, attrs, content) { + + if (typeof(attrs) === 'string') { + content = attrs; + attrs = {}; + } + + if (typeof(content) === 'undefined') { + content = false; + } + + if (attrs && typeof(attrs.render) === 'function') { + content = attrs.render(this); + attrs = {}; + } + + if (typeof(tagDesc) === 'string') { + tagDesc = [this.defaultNamespace, tagDesc]; + } + + var fullTag = this._registerTag(tagDesc[0], tagDesc[1]); + var output = ''; + var key; + + output += '<' + fullTag + ''; + + for (key in attrs) { + output += ' ' + key + '="' + attrs[key] + '"'; + } + + if (content) { + output += '>' + content + '</' + fullTag + '>'; + } else { + output += ' />'; + } + + return output; + } + + }; + + module.exports = Template; + +}.apply( + this, + (this.Caldav) ? + [Caldav('template'), Caldav] : + [module, require('./caldav')] +)); diff --git a/lib/caldav/templates/calendar_data.js b/lib/caldav/templates/calendar_data.js new file mode 100644 index 0000000..2c24569 --- /dev/null +++ b/lib/caldav/templates/calendar_data.js @@ -0,0 +1,107 @@ +(function(module, ns) { + + function CalendarData() { + this._hasItems = false; + this.struct = {}; + } + + CalendarData.prototype = { + + rootName: 'calendar-data', + compName: 'comp', + propName: 'prop', + + /** + * Appends a list of fields + * to a given iCalendar field set. + * + * @param {String} type iCal fieldset (VTODO, VEVENT,...). + */ + select: function(type, list) { + var struct = this.struct; + this._hasItems = true; + + if (!(type in struct)) { + struct[type] = []; + } + + if (list instanceof Array) { + struct[type] = struct[type].concat(list); + } else { + struct[type] = list; + } + + return this; + }, + + /** + * Accepts an object full of arrays + * recuse when encountering another object. + */ + _renderFieldset: function(template, element) { + var tag; + var value; + var i; + var output = ''; + var elementOutput = ''; + + for (tag in element) { + value = element[tag]; + for (i = 0; i < value.length; i++) { + if (typeof(value[i]) === 'object') { + elementOutput += this._renderFieldset( + template, + value[i] + ); + } else { + elementOutput += template.tag( + ['caldav', this.propName], + { name: value[i] } + ); + } + } + output += template.tag( + ['caldav', this.compName], + { name: tag }, + elementOutput || null + ); + elementOutput = ''; + } + + return output; + }, + + /** + * Renders CalendarData with a template. + * + * @param {WebCals.Template} template calendar to render. + * @return {String} <calendardata /> xml output. + */ + render: function(template) { + if (!this._hasItems) { + return template.tag(['caldav', this.rootName]); + } + + var struct = this.struct; + var output = template.tag( + ['caldav', this.rootName], + template.tag( + ['caldav', this.compName], + { name: 'VCALENDAR' }, + this._renderFieldset(template, struct) + ) + ); + + return output; + } + }; + + + module.exports = CalendarData; + +}.apply( + this, + (this.Caldav) ? + [Caldav('templates/calendar_data'), Caldav] : + [module, require('../caldav')] +)); diff --git a/lib/caldav/templates/calendar_filter.js b/lib/caldav/templates/calendar_filter.js new file mode 100644 index 0000000..aebd9fe --- /dev/null +++ b/lib/caldav/templates/calendar_filter.js @@ -0,0 +1,27 @@ +(function(module, ns) { + + var CalendarData = ns.require('templates/calendar_data'); + + function CalendarFilter() { + CalendarData.call(this); + } + + CalendarFilter.prototype = { + + __proto__: CalendarData.prototype, + + add: CalendarData.prototype.select, + + compName: 'comp-filter', + rootName: 'filter' + }; + + module.exports = CalendarFilter; + +}.apply( + this, + (this.Caldav) ? + [Caldav('templates/calendar_filter'), Caldav] : + [module, require('../caldav')] +)); + diff --git a/lib/caldav/templates/index.js b/lib/caldav/templates/index.js new file mode 100644 index 0000000..4e06cfd --- /dev/null +++ b/lib/caldav/templates/index.js @@ -0,0 +1,13 @@ +(function(module, ns) { + + module.exports = { + CalendarData: ns.require('templates/calendar_data'), + CalendarFilter: ns.require('templates/calendar_filter') + }; + +}.apply( + this, + (this.Caldav) ? + [Caldav('templates'), Caldav] : + [module, require('../caldav')] +)); diff --git a/lib/caldav/xhr.js b/lib/caldav/xhr.js new file mode 100644 index 0000000..84fb18d --- /dev/null +++ b/lib/caldav/xhr.js @@ -0,0 +1,118 @@ +/** +@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, + url: null, + + headers: {}, + data: null, + + _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(); + + if (Xhr.authHack) { + xhr.open(this.method, this.url, this.async); + } else { + 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(null, xhr); + } + }.bind(this); + + this.waiting = true; + xhr.send(this._seralize()); + } + }; + + module.exports = Xhr; + +}.apply( + this, + (this.Caldav) ? + [Caldav('xhr'), Caldav] : + [module, require('./caldav')] +)); |