Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
4095 lines
100 KiB
JavaScript
4095 lines
100 KiB
JavaScript
/* sax js - LICENSE: https://github.com/isaacs/sax-js/blob/master/LICENSE */
|
|
// wrapper for non-node envs
|
|
;(function (sax) {
|
|
|
|
sax.parser = function (strict, opt) { return new SAXParser(strict, opt) }
|
|
sax.SAXParser = SAXParser
|
|
sax.SAXStream = SAXStream
|
|
sax.createStream = createStream
|
|
|
|
// When we pass the MAX_BUFFER_LENGTH position, start checking for buffer overruns.
|
|
// When we check, schedule the next check for MAX_BUFFER_LENGTH - (max(buffer lengths)),
|
|
// since that's the earliest that a buffer overrun could occur. This way, checks are
|
|
// as rare as required, but as often as necessary to ensure never crossing this bound.
|
|
// Furthermore, buffers are only tested at most once per write(), so passing a very
|
|
// large string into write() might have undesirable effects, but this is manageable by
|
|
// the caller, so it is assumed to be safe. Thus, a call to write() may, in the extreme
|
|
// edge case, result in creating at most one complete copy of the string passed in.
|
|
// Set to Infinity to have unlimited buffers.
|
|
sax.MAX_BUFFER_LENGTH = 64 * 1024
|
|
|
|
var buffers = [
|
|
"comment", "sgmlDecl", "textNode", "tagName", "doctype",
|
|
"procInstName", "procInstBody", "entity", "attribName",
|
|
"attribValue", "cdata", "script"
|
|
]
|
|
|
|
sax.EVENTS = // for discoverability.
|
|
[ "text"
|
|
, "processinginstruction"
|
|
, "sgmldeclaration"
|
|
, "doctype"
|
|
, "comment"
|
|
, "attribute"
|
|
, "opentag"
|
|
, "closetag"
|
|
, "opencdata"
|
|
, "cdata"
|
|
, "closecdata"
|
|
, "error"
|
|
, "end"
|
|
, "ready"
|
|
, "script"
|
|
, "opennamespace"
|
|
, "closenamespace"
|
|
]
|
|
|
|
function SAXParser (strict, opt) {
|
|
if (!(this instanceof SAXParser)) return new SAXParser(strict, opt)
|
|
|
|
var parser = this
|
|
clearBuffers(parser)
|
|
parser.q = parser.c = ""
|
|
parser.bufferCheckPosition = sax.MAX_BUFFER_LENGTH
|
|
parser.opt = opt || {}
|
|
parser.opt.lowercase = parser.opt.lowercase || parser.opt.lowercasetags;
|
|
parser.looseCase = parser.opt.lowercase ? "toLowerCase" : "toUpperCase"
|
|
parser.tags = []
|
|
parser.closed = parser.closedRoot = parser.sawRoot = false
|
|
parser.tag = parser.error = null
|
|
parser.strict = !!strict
|
|
parser.noscript = !!(strict || parser.opt.noscript)
|
|
parser.state = S.BEGIN
|
|
parser.ENTITIES = Object.create(sax.ENTITIES)
|
|
parser.attribList = []
|
|
|
|
// namespaces form a prototype chain.
|
|
// it always points at the current tag,
|
|
// which protos to its parent tag.
|
|
if (parser.opt.xmlns) parser.ns = Object.create(rootNS)
|
|
|
|
// mostly just for error reporting
|
|
parser.position = parser.line = parser.column = 0
|
|
emit(parser, "onready")
|
|
|
|
return this;
|
|
}
|
|
|
|
function checkBufferLength (parser) {
|
|
var maxAllowed = Math.max(sax.MAX_BUFFER_LENGTH, 10)
|
|
, maxActual = 0
|
|
for (var i = 0, l = buffers.length; i < l; i ++) {
|
|
var len = parser[buffers[i]].length
|
|
if (len > maxAllowed) {
|
|
// Text/cdata nodes can get big, and since they're buffered,
|
|
// we can get here under normal conditions.
|
|
// Avoid issues by emitting the text node now,
|
|
// so at least it won't get any bigger.
|
|
switch (buffers[i]) {
|
|
case "textNode":
|
|
closeText(parser)
|
|
break
|
|
|
|
case "cdata":
|
|
emitNode(parser, "oncdata", parser.cdata)
|
|
parser.cdata = ""
|
|
break
|
|
|
|
case "script":
|
|
emitNode(parser, "onscript", parser.script)
|
|
parser.script = ""
|
|
break
|
|
|
|
default:
|
|
error(parser, "Max buffer length exceeded: "+buffers[i])
|
|
}
|
|
}
|
|
maxActual = Math.max(maxActual, len)
|
|
}
|
|
// schedule the next check for the earliest possible buffer overrun.
|
|
parser.bufferCheckPosition = (sax.MAX_BUFFER_LENGTH - maxActual)
|
|
+ parser.position
|
|
}
|
|
|
|
function clearBuffers (parser) {
|
|
for (var i = 0, l = buffers.length; i < l; i ++) {
|
|
parser[buffers[i]] = ""
|
|
}
|
|
}
|
|
|
|
SAXParser.prototype =
|
|
{ end: function () { end(this) }
|
|
, write: write
|
|
, resume: function () { this.error = null; return this }
|
|
, close: function () { return this.write(null) }
|
|
}
|
|
|
|
var Stream = function () {}
|
|
|
|
var streamWraps = sax.EVENTS.filter(function (ev) {
|
|
return ev !== "error" && ev !== "end"
|
|
})
|
|
|
|
function createStream (strict, opt) {
|
|
return new SAXStream(strict, opt)
|
|
}
|
|
|
|
function SAXStream (strict, opt) {
|
|
if (!(this instanceof SAXStream)) return new SAXStream(strict, opt)
|
|
|
|
Stream.apply(me)
|
|
|
|
this._parser = new SAXParser(strict, opt)
|
|
this.writable = true
|
|
this.readable = true
|
|
|
|
|
|
var me = this
|
|
|
|
this._parser.onend = function () {
|
|
me.emit("end")
|
|
}
|
|
|
|
this._parser.onerror = function (er) {
|
|
me.emit("error", er)
|
|
|
|
// if didn't throw, then means error was handled.
|
|
// go ahead and clear error, so we can write again.
|
|
me._parser.error = null
|
|
}
|
|
|
|
streamWraps.forEach(function (ev) {
|
|
Object.defineProperty(me, "on" + ev, {
|
|
get: function () { return me._parser["on" + ev] },
|
|
set: function (h) {
|
|
if (!h) {
|
|
me.removeAllListeners(ev)
|
|
return me._parser["on"+ev] = h
|
|
}
|
|
me.on(ev, h)
|
|
return null;
|
|
},
|
|
enumerable: true,
|
|
configurable: false
|
|
})
|
|
})
|
|
|
|
return this;
|
|
}
|
|
|
|
SAXStream.prototype = Object.create(Stream.prototype,
|
|
{ constructor: { value: SAXStream } })
|
|
|
|
SAXStream.prototype.write = function (data) {
|
|
this._parser.write(data.toString())
|
|
this.emit("data", data)
|
|
return true
|
|
}
|
|
|
|
SAXStream.prototype.end = function (chunk) {
|
|
if (chunk && chunk.length) this._parser.write(chunk.toString())
|
|
this._parser.end()
|
|
return true
|
|
}
|
|
|
|
SAXStream.prototype.on = function (ev, handler) {
|
|
var me = this
|
|
if (!me._parser["on"+ev] && streamWraps.indexOf(ev) !== -1) {
|
|
me._parser["on"+ev] = function () {
|
|
var args = arguments.length === 1 ? [arguments[0]]
|
|
: Array.apply(null, arguments)
|
|
args.splice(0, 0, ev)
|
|
me.emit.apply(me, args)
|
|
}
|
|
}
|
|
|
|
return Stream.prototype.on.call(me, ev, handler)
|
|
}
|
|
|
|
|
|
|
|
// character classes and tokens
|
|
var whitespace = "\r\n\t "
|
|
// this really needs to be replaced with character classes.
|
|
// XML allows all manner of ridiculous numbers and digits.
|
|
, number = "0124356789"
|
|
, letter = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
// (Letter | "_" | ":")
|
|
, nameStart = letter+"_:"
|
|
, nameBody = nameStart+number+"-."
|
|
, quote = "'\""
|
|
, entity = number+letter+"#"
|
|
, attribEnd = whitespace + ">"
|
|
, CDATA = "[CDATA["
|
|
, DOCTYPE = "DOCTYPE"
|
|
, XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
|
|
, XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/"
|
|
, rootNS = { xml: XML_NAMESPACE, xmlns: XMLNS_NAMESPACE }
|
|
|
|
// turn all the string character sets into character class objects.
|
|
whitespace = charClass(whitespace)
|
|
number = charClass(number)
|
|
letter = charClass(letter)
|
|
nameStart = charClass(nameStart)
|
|
nameBody = charClass(nameBody)
|
|
quote = charClass(quote)
|
|
entity = charClass(entity)
|
|
attribEnd = charClass(attribEnd)
|
|
|
|
function charClass (str) {
|
|
return str.split("").reduce(function (s, c) {
|
|
s[c] = true
|
|
return s
|
|
}, {})
|
|
}
|
|
|
|
function is (charclass, c) {
|
|
return charclass[c]
|
|
}
|
|
|
|
function not (charclass, c) {
|
|
return !charclass[c]
|
|
}
|
|
|
|
var S = 0
|
|
sax.STATE =
|
|
{ BEGIN : S++
|
|
, TEXT : S++ // general stuff
|
|
, TEXT_ENTITY : S++ // & and such.
|
|
, OPEN_WAKA : S++ // <
|
|
, SGML_DECL : S++ // <!BLARG
|
|
, SGML_DECL_QUOTED : S++ // <!BLARG foo "bar
|
|
, DOCTYPE : S++ // <!DOCTYPE
|
|
, DOCTYPE_QUOTED : S++ // <!DOCTYPE "//blah
|
|
, DOCTYPE_DTD : S++ // <!DOCTYPE "//blah" [ ...
|
|
, DOCTYPE_DTD_QUOTED : S++ // <!DOCTYPE "//blah" [ "foo
|
|
, COMMENT_STARTING : S++ // <!-
|
|
, COMMENT : S++ // <!--
|
|
, COMMENT_ENDING : S++ // <!-- blah -
|
|
, COMMENT_ENDED : S++ // <!-- blah --
|
|
, CDATA : S++ // <![CDATA[ something
|
|
, CDATA_ENDING : S++ // ]
|
|
, CDATA_ENDING_2 : S++ // ]]
|
|
, PROC_INST : S++ // <?hi
|
|
, PROC_INST_BODY : S++ // <?hi there
|
|
, PROC_INST_QUOTED : S++ // <?hi "there
|
|
, PROC_INST_ENDING : S++ // <?hi "there" ?
|
|
, OPEN_TAG : S++ // <strong
|
|
, OPEN_TAG_SLASH : S++ // <strong /
|
|
, ATTRIB : S++ // <a
|
|
, ATTRIB_NAME : S++ // <a foo
|
|
, ATTRIB_NAME_SAW_WHITE : S++ // <a foo _
|
|
, ATTRIB_VALUE : S++ // <a foo=
|
|
, ATTRIB_VALUE_QUOTED : S++ // <a foo="bar
|
|
, ATTRIB_VALUE_UNQUOTED : S++ // <a foo=bar
|
|
, ATTRIB_VALUE_ENTITY_Q : S++ // <foo bar="""
|
|
, ATTRIB_VALUE_ENTITY_U : S++ // <foo bar="
|
|
, CLOSE_TAG : S++ // </a
|
|
, CLOSE_TAG_SAW_WHITE : S++ // </a >
|
|
, SCRIPT : S++ // <script> ...
|
|
, SCRIPT_ENDING : S++ // <script> ... <
|
|
}
|
|
|
|
sax.ENTITIES =
|
|
{ "apos" : "'"
|
|
, "quot" : "\""
|
|
, "amp" : "&"
|
|
, "gt" : ">"
|
|
, "lt" : "<"
|
|
}
|
|
|
|
for (var S in sax.STATE) sax.STATE[sax.STATE[S]] = S
|
|
|
|
// shorthand
|
|
S = sax.STATE
|
|
|
|
function emit (parser, event, data) {
|
|
parser[event] && parser[event](data)
|
|
}
|
|
|
|
function emitNode (parser, nodeType, data) {
|
|
if (parser.textNode) closeText(parser)
|
|
emit(parser, nodeType, data)
|
|
}
|
|
|
|
function closeText (parser) {
|
|
parser.textNode = textopts(parser.opt, parser.textNode)
|
|
if (parser.textNode) emit(parser, "ontext", parser.textNode)
|
|
parser.textNode = ""
|
|
}
|
|
|
|
function textopts (opt, text) {
|
|
if (opt.trim) text = text.trim()
|
|
if (opt.normalize) text = text.replace(/\s+/g, " ")
|
|
return text
|
|
}
|
|
|
|
function error (parser, er) {
|
|
closeText(parser)
|
|
er += "\nLine: "+parser.line+
|
|
"\nColumn: "+parser.column+
|
|
"\nChar: "+parser.c
|
|
er = new Error(er)
|
|
parser.error = er
|
|
emit(parser, "onerror", er)
|
|
return parser
|
|
}
|
|
|
|
function end (parser) {
|
|
if (parser.state !== S.TEXT) error(parser, "Unexpected end")
|
|
closeText(parser)
|
|
parser.c = ""
|
|
parser.closed = true
|
|
emit(parser, "onend")
|
|
SAXParser.call(parser, parser.strict, parser.opt)
|
|
return parser
|
|
}
|
|
|
|
function strictFail (parser, message) {
|
|
if (parser.strict) error(parser, message)
|
|
}
|
|
|
|
function newTag (parser) {
|
|
if (!parser.strict) parser.tagName = parser.tagName[parser.looseCase]()
|
|
var parent = parser.tags[parser.tags.length - 1] || parser
|
|
, tag = parser.tag = { name : parser.tagName, attributes : {} }
|
|
|
|
// will be overridden if tag contails an xmlns="foo" or xmlns:foo="bar"
|
|
if (parser.opt.xmlns) tag.ns = parent.ns
|
|
parser.attribList.length = 0
|
|
}
|
|
|
|
function qname (name) {
|
|
var i = name.indexOf(":")
|
|
, qualName = i < 0 ? [ "", name ] : name.split(":")
|
|
, prefix = qualName[0]
|
|
, local = qualName[1]
|
|
|
|
// <x "xmlns"="http://foo">
|
|
if (name === "xmlns") {
|
|
prefix = "xmlns"
|
|
local = ""
|
|
}
|
|
|
|
return { prefix: prefix, local: local }
|
|
}
|
|
|
|
function attrib (parser) {
|
|
if (!parser.strict) parser.attribName = parser.attribName[parser.looseCase]()
|
|
if (parser.opt.xmlns) {
|
|
var qn = qname(parser.attribName)
|
|
, prefix = qn.prefix
|
|
, local = qn.local
|
|
|
|
if (prefix === "xmlns") {
|
|
// namespace binding attribute; push the binding into scope
|
|
if (local === "xml" && parser.attribValue !== XML_NAMESPACE) {
|
|
strictFail( parser
|
|
, "xml: prefix must be bound to " + XML_NAMESPACE + "\n"
|
|
+ "Actual: " + parser.attribValue )
|
|
} else if (local === "xmlns" && parser.attribValue !== XMLNS_NAMESPACE) {
|
|
strictFail( parser
|
|
, "xmlns: prefix must be bound to " + XMLNS_NAMESPACE + "\n"
|
|
+ "Actual: " + parser.attribValue )
|
|
} else {
|
|
var tag = parser.tag
|
|
, parent = parser.tags[parser.tags.length - 1] || parser
|
|
if (tag.ns === parent.ns) {
|
|
tag.ns = Object.create(parent.ns)
|
|
}
|
|
tag.ns[local] = parser.attribValue
|
|
}
|
|
}
|
|
|
|
// defer onattribute events until all attributes have been seen
|
|
// so any new bindings can take effect; preserve attribute order
|
|
// so deferred events can be emitted in document order
|
|
parser.attribList.push([parser.attribName, parser.attribValue])
|
|
} else {
|
|
// in non-xmlns mode, we can emit the event right away
|
|
parser.tag.attributes[parser.attribName] = parser.attribValue
|
|
emitNode( parser
|
|
, "onattribute"
|
|
, { name: parser.attribName
|
|
, value: parser.attribValue } )
|
|
}
|
|
|
|
parser.attribName = parser.attribValue = ""
|
|
}
|
|
|
|
function openTag (parser, selfClosing) {
|
|
if (parser.opt.xmlns) {
|
|
// emit namespace binding events
|
|
var tag = parser.tag
|
|
|
|
// add namespace info to tag
|
|
var qn = qname(parser.tagName)
|
|
tag.prefix = qn.prefix
|
|
tag.local = qn.local
|
|
tag.uri = tag.ns[qn.prefix] || qn.prefix
|
|
|
|
if (tag.prefix && !tag.uri) {
|
|
strictFail(parser, "Unbound namespace prefix: "
|
|
+ JSON.stringify(parser.tagName))
|
|
}
|
|
|
|
var parent = parser.tags[parser.tags.length - 1] || parser
|
|
if (tag.ns && parent.ns !== tag.ns) {
|
|
Object.keys(tag.ns).forEach(function (p) {
|
|
emitNode( parser
|
|
, "onopennamespace"
|
|
, { prefix: p , uri: tag.ns[p] } )
|
|
})
|
|
}
|
|
|
|
// handle deferred onattribute events
|
|
for (var i = 0, l = parser.attribList.length; i < l; i ++) {
|
|
var nv = parser.attribList[i]
|
|
var name = nv[0]
|
|
, value = nv[1]
|
|
, qualName = qname(name)
|
|
, prefix = qualName.prefix
|
|
, local = qualName.local
|
|
, uri = tag.ns[prefix] || ""
|
|
, a = { name: name
|
|
, value: value
|
|
, prefix: prefix
|
|
, local: local
|
|
, uri: uri
|
|
}
|
|
|
|
// if there's any attributes with an undefined namespace,
|
|
// then fail on them now.
|
|
if (prefix && prefix != "xmlns" && !uri) {
|
|
strictFail(parser, "Unbound namespace prefix: "
|
|
+ JSON.stringify(prefix))
|
|
a.uri = prefix
|
|
}
|
|
parser.tag.attributes[name] = a
|
|
emitNode(parser, "onattribute", a)
|
|
}
|
|
parser.attribList.length = 0
|
|
}
|
|
|
|
// process the tag
|
|
parser.sawRoot = true
|
|
parser.tags.push(parser.tag)
|
|
emitNode(parser, "onopentag", parser.tag)
|
|
if (!selfClosing) {
|
|
// special case for <script> in non-strict mode.
|
|
if (!parser.noscript && parser.tagName.toLowerCase() === "script") {
|
|
parser.state = S.SCRIPT
|
|
} else {
|
|
parser.state = S.TEXT
|
|
}
|
|
parser.tag = null
|
|
parser.tagName = ""
|
|
}
|
|
parser.attribName = parser.attribValue = ""
|
|
parser.attribList.length = 0
|
|
}
|
|
|
|
function closeTag (parser) {
|
|
if (!parser.tagName) {
|
|
strictFail(parser, "Weird empty close tag.")
|
|
parser.textNode += "</>"
|
|
parser.state = S.TEXT
|
|
return
|
|
}
|
|
// first make sure that the closing tag actually exists.
|
|
// <a><b></c></b></a> will close everything, otherwise.
|
|
var t = parser.tags.length
|
|
var tagName = parser.tagName
|
|
if (!parser.strict) tagName = tagName[parser.looseCase]()
|
|
var closeTo = tagName
|
|
while (t --) {
|
|
var close = parser.tags[t]
|
|
if (close.name !== closeTo) {
|
|
// fail the first time in strict mode
|
|
strictFail(parser, "Unexpected close tag")
|
|
} else break
|
|
}
|
|
|
|
// didn't find it. we already failed for strict, so just abort.
|
|
if (t < 0) {
|
|
strictFail(parser, "Unmatched closing tag: "+parser.tagName)
|
|
parser.textNode += "</" + parser.tagName + ">"
|
|
parser.state = S.TEXT
|
|
return
|
|
}
|
|
parser.tagName = tagName
|
|
var s = parser.tags.length
|
|
while (s --> t) {
|
|
var tag = parser.tag = parser.tags.pop()
|
|
parser.tagName = parser.tag.name
|
|
emitNode(parser, "onclosetag", parser.tagName)
|
|
|
|
var x = {}
|
|
for (var i in tag.ns) x[i] = tag.ns[i]
|
|
|
|
var parent = parser.tags[parser.tags.length - 1] || parser
|
|
if (parser.opt.xmlns && tag.ns !== parent.ns) {
|
|
// remove namespace bindings introduced by tag
|
|
Object.keys(tag.ns).forEach(function (p) {
|
|
var n = tag.ns[p]
|
|
emitNode(parser, "onclosenamespace", { prefix: p, uri: n })
|
|
})
|
|
}
|
|
}
|
|
if (t === 0) parser.closedRoot = true
|
|
parser.tagName = parser.attribValue = parser.attribName = ""
|
|
parser.attribList.length = 0
|
|
parser.state = S.TEXT
|
|
}
|
|
|
|
function parseEntity (parser) {
|
|
var entity = parser.entity.toLowerCase()
|
|
, num
|
|
, numStr = ""
|
|
if (parser.ENTITIES[entity]) return parser.ENTITIES[entity]
|
|
if (entity.charAt(0) === "#") {
|
|
if (entity.charAt(1) === "x") {
|
|
entity = entity.slice(2)
|
|
num = parseInt(entity, 16)
|
|
numStr = num.toString(16)
|
|
} else {
|
|
entity = entity.slice(1)
|
|
num = parseInt(entity, 10)
|
|
numStr = num.toString(10)
|
|
}
|
|
}
|
|
entity = entity.replace(/^0+/, "")
|
|
if (numStr.toLowerCase() !== entity) {
|
|
strictFail(parser, "Invalid character entity")
|
|
return "&"+parser.entity + ";"
|
|
}
|
|
return String.fromCharCode(num)
|
|
}
|
|
|
|
function write (chunk) {
|
|
var parser = this
|
|
if (this.error) throw this.error
|
|
if (parser.closed) return error(parser,
|
|
"Cannot write after close. Assign an onready handler.")
|
|
if (chunk === null) return end(parser)
|
|
var i = 0, c = ""
|
|
while ((parser.c = c = chunk.charAt(i++))) {
|
|
parser.position ++
|
|
if (c === "\n") {
|
|
parser.line ++
|
|
parser.column = 0
|
|
} else parser.column ++
|
|
switch (parser.state) {
|
|
|
|
case S.BEGIN:
|
|
if (c === "<") parser.state = S.OPEN_WAKA
|
|
else if (not(whitespace,c)) {
|
|
// have to process this as a text node.
|
|
// weird, but happens.
|
|
strictFail(parser, "Non-whitespace before first tag.")
|
|
parser.textNode = c
|
|
parser.state = S.TEXT
|
|
}
|
|
continue
|
|
|
|
case S.TEXT:
|
|
if (parser.sawRoot && !parser.closedRoot) {
|
|
var starti = i-1
|
|
while (c && c!=="<" && c!=="&") {
|
|
c = chunk.charAt(i++)
|
|
if (c) {
|
|
parser.position ++
|
|
if (c === "\n") {
|
|
parser.line ++
|
|
parser.column = 0
|
|
} else parser.column ++
|
|
}
|
|
}
|
|
parser.textNode += chunk.substring(starti, i-1)
|
|
}
|
|
if (c === "<") parser.state = S.OPEN_WAKA
|
|
else {
|
|
if (not(whitespace, c) && (!parser.sawRoot || parser.closedRoot))
|
|
strictFail("Text data outside of root node.")
|
|
if (c === "&") parser.state = S.TEXT_ENTITY
|
|
else parser.textNode += c
|
|
}
|
|
continue
|
|
|
|
case S.SCRIPT:
|
|
// only non-strict
|
|
if (c === "<") {
|
|
parser.state = S.SCRIPT_ENDING
|
|
} else parser.script += c
|
|
continue
|
|
|
|
case S.SCRIPT_ENDING:
|
|
if (c === "/") {
|
|
emitNode(parser, "onscript", parser.script)
|
|
parser.state = S.CLOSE_TAG
|
|
parser.script = ""
|
|
parser.tagName = ""
|
|
} else {
|
|
parser.script += "<" + c
|
|
parser.state = S.SCRIPT
|
|
}
|
|
continue
|
|
|
|
case S.OPEN_WAKA:
|
|
// either a /, ?, !, or text is coming next.
|
|
if (c === "!") {
|
|
parser.state = S.SGML_DECL
|
|
parser.sgmlDecl = ""
|
|
} else if (is(whitespace, c)) {
|
|
// wait for it...
|
|
} else if (is(nameStart,c)) {
|
|
parser.startTagPosition = parser.position - 1
|
|
parser.state = S.OPEN_TAG
|
|
parser.tagName = c
|
|
} else if (c === "/") {
|
|
parser.startTagPosition = parser.position - 1
|
|
parser.state = S.CLOSE_TAG
|
|
parser.tagName = ""
|
|
} else if (c === "?") {
|
|
parser.state = S.PROC_INST
|
|
parser.procInstName = parser.procInstBody = ""
|
|
} else {
|
|
strictFail(parser, "Unencoded <")
|
|
parser.textNode += "<" + c
|
|
parser.state = S.TEXT
|
|
}
|
|
continue
|
|
|
|
case S.SGML_DECL:
|
|
if ((parser.sgmlDecl+c).toUpperCase() === CDATA) {
|
|
emitNode(parser, "onopencdata")
|
|
parser.state = S.CDATA
|
|
parser.sgmlDecl = ""
|
|
parser.cdata = ""
|
|
} else if (parser.sgmlDecl+c === "--") {
|
|
parser.state = S.COMMENT
|
|
parser.comment = ""
|
|
parser.sgmlDecl = ""
|
|
} else if ((parser.sgmlDecl+c).toUpperCase() === DOCTYPE) {
|
|
parser.state = S.DOCTYPE
|
|
if (parser.doctype || parser.sawRoot) strictFail(parser,
|
|
"Inappropriately located doctype declaration")
|
|
parser.doctype = ""
|
|
parser.sgmlDecl = ""
|
|
} else if (c === ">") {
|
|
emitNode(parser, "onsgmldeclaration", parser.sgmlDecl)
|
|
parser.sgmlDecl = ""
|
|
parser.state = S.TEXT
|
|
} else if (is(quote, c)) {
|
|
parser.state = S.SGML_DECL_QUOTED
|
|
parser.sgmlDecl += c
|
|
} else parser.sgmlDecl += c
|
|
continue
|
|
|
|
case S.SGML_DECL_QUOTED:
|
|
if (c === parser.q) {
|
|
parser.state = S.SGML_DECL
|
|
parser.q = ""
|
|
}
|
|
parser.sgmlDecl += c
|
|
continue
|
|
|
|
case S.DOCTYPE:
|
|
if (c === ">") {
|
|
parser.state = S.TEXT
|
|
emitNode(parser, "ondoctype", parser.doctype)
|
|
parser.doctype = true // just remember that we saw it.
|
|
} else {
|
|
parser.doctype += c
|
|
if (c === "[") parser.state = S.DOCTYPE_DTD
|
|
else if (is(quote, c)) {
|
|
parser.state = S.DOCTYPE_QUOTED
|
|
parser.q = c
|
|
}
|
|
}
|
|
continue
|
|
|
|
case S.DOCTYPE_QUOTED:
|
|
parser.doctype += c
|
|
if (c === parser.q) {
|
|
parser.q = ""
|
|
parser.state = S.DOCTYPE
|
|
}
|
|
continue
|
|
|
|
case S.DOCTYPE_DTD:
|
|
parser.doctype += c
|
|
if (c === "]") parser.state = S.DOCTYPE
|
|
else if (is(quote,c)) {
|
|
parser.state = S.DOCTYPE_DTD_QUOTED
|
|
parser.q = c
|
|
}
|
|
continue
|
|
|
|
case S.DOCTYPE_DTD_QUOTED:
|
|
parser.doctype += c
|
|
if (c === parser.q) {
|
|
parser.state = S.DOCTYPE_DTD
|
|
parser.q = ""
|
|
}
|
|
continue
|
|
|
|
case S.COMMENT:
|
|
if (c === "-") parser.state = S.COMMENT_ENDING
|
|
else parser.comment += c
|
|
continue
|
|
|
|
case S.COMMENT_ENDING:
|
|
if (c === "-") {
|
|
parser.state = S.COMMENT_ENDED
|
|
parser.comment = textopts(parser.opt, parser.comment)
|
|
if (parser.comment) emitNode(parser, "oncomment", parser.comment)
|
|
parser.comment = ""
|
|
} else {
|
|
parser.comment += "-" + c
|
|
parser.state = S.COMMENT
|
|
}
|
|
continue
|
|
|
|
case S.COMMENT_ENDED:
|
|
if (c !== ">") {
|
|
strictFail(parser, "Malformed comment")
|
|
// allow <!-- blah -- bloo --> in non-strict mode,
|
|
// which is a comment of " blah -- bloo "
|
|
parser.comment += "--" + c
|
|
parser.state = S.COMMENT
|
|
} else parser.state = S.TEXT
|
|
continue
|
|
|
|
case S.CDATA:
|
|
if (c === "]") parser.state = S.CDATA_ENDING
|
|
else parser.cdata += c
|
|
continue
|
|
|
|
case S.CDATA_ENDING:
|
|
if (c === "]") parser.state = S.CDATA_ENDING_2
|
|
else {
|
|
parser.cdata += "]" + c
|
|
parser.state = S.CDATA
|
|
}
|
|
continue
|
|
|
|
case S.CDATA_ENDING_2:
|
|
if (c === ">") {
|
|
if (parser.cdata) emitNode(parser, "oncdata", parser.cdata)
|
|
emitNode(parser, "onclosecdata")
|
|
parser.cdata = ""
|
|
parser.state = S.TEXT
|
|
} else if (c === "]") {
|
|
parser.cdata += "]"
|
|
} else {
|
|
parser.cdata += "]]" + c
|
|
parser.state = S.CDATA
|
|
}
|
|
continue
|
|
|
|
case S.PROC_INST:
|
|
if (c === "?") parser.state = S.PROC_INST_ENDING
|
|
else if (is(whitespace, c)) parser.state = S.PROC_INST_BODY
|
|
else parser.procInstName += c
|
|
continue
|
|
|
|
case S.PROC_INST_BODY:
|
|
if (!parser.procInstBody && is(whitespace, c)) continue
|
|
else if (c === "?") parser.state = S.PROC_INST_ENDING
|
|
else if (is(quote, c)) {
|
|
parser.state = S.PROC_INST_QUOTED
|
|
parser.q = c
|
|
parser.procInstBody += c
|
|
} else parser.procInstBody += c
|
|
continue
|
|
|
|
case S.PROC_INST_ENDING:
|
|
if (c === ">") {
|
|
emitNode(parser, "onprocessinginstruction", {
|
|
name : parser.procInstName,
|
|
body : parser.procInstBody
|
|
})
|
|
parser.procInstName = parser.procInstBody = ""
|
|
parser.state = S.TEXT
|
|
} else {
|
|
parser.procInstBody += "?" + c
|
|
parser.state = S.PROC_INST_BODY
|
|
}
|
|
continue
|
|
|
|
case S.PROC_INST_QUOTED:
|
|
parser.procInstBody += c
|
|
if (c === parser.q) {
|
|
parser.state = S.PROC_INST_BODY
|
|
parser.q = ""
|
|
}
|
|
continue
|
|
|
|
case S.OPEN_TAG:
|
|
if (is(nameBody, c)) parser.tagName += c
|
|
else {
|
|
newTag(parser)
|
|
if (c === ">") openTag(parser)
|
|
else if (c === "/") parser.state = S.OPEN_TAG_SLASH
|
|
else {
|
|
if (not(whitespace, c)) strictFail(
|
|
parser, "Invalid character in tag name")
|
|
parser.state = S.ATTRIB
|
|
}
|
|
}
|
|
continue
|
|
|
|
case S.OPEN_TAG_SLASH:
|
|
if (c === ">") {
|
|
openTag(parser, true)
|
|
closeTag(parser)
|
|
} else {
|
|
strictFail(parser, "Forward-slash in opening tag not followed by >")
|
|
parser.state = S.ATTRIB
|
|
}
|
|
continue
|
|
|
|
case S.ATTRIB:
|
|
// haven't read the attribute name yet.
|
|
if (is(whitespace, c)) continue
|
|
else if (c === ">") openTag(parser)
|
|
else if (c === "/") parser.state = S.OPEN_TAG_SLASH
|
|
else if (is(nameStart, c)) {
|
|
parser.attribName = c
|
|
parser.attribValue = ""
|
|
parser.state = S.ATTRIB_NAME
|
|
} else strictFail(parser, "Invalid attribute name")
|
|
continue
|
|
|
|
case S.ATTRIB_NAME:
|
|
if (c === "=") parser.state = S.ATTRIB_VALUE
|
|
else if (is(whitespace, c)) parser.state = S.ATTRIB_NAME_SAW_WHITE
|
|
else if (is(nameBody, c)) parser.attribName += c
|
|
else strictFail(parser, "Invalid attribute name")
|
|
continue
|
|
|
|
case S.ATTRIB_NAME_SAW_WHITE:
|
|
if (c === "=") parser.state = S.ATTRIB_VALUE
|
|
else if (is(whitespace, c)) continue
|
|
else {
|
|
strictFail(parser, "Attribute without value")
|
|
parser.tag.attributes[parser.attribName] = ""
|
|
parser.attribValue = ""
|
|
emitNode(parser, "onattribute",
|
|
{ name : parser.attribName, value : "" })
|
|
parser.attribName = ""
|
|
if (c === ">") openTag(parser)
|
|
else if (is(nameStart, c)) {
|
|
parser.attribName = c
|
|
parser.state = S.ATTRIB_NAME
|
|
} else {
|
|
strictFail(parser, "Invalid attribute name")
|
|
parser.state = S.ATTRIB
|
|
}
|
|
}
|
|
continue
|
|
|
|
case S.ATTRIB_VALUE:
|
|
if (is(whitespace, c)) continue
|
|
else if (is(quote, c)) {
|
|
parser.q = c
|
|
parser.state = S.ATTRIB_VALUE_QUOTED
|
|
} else {
|
|
strictFail(parser, "Unquoted attribute value")
|
|
parser.state = S.ATTRIB_VALUE_UNQUOTED
|
|
parser.attribValue = c
|
|
}
|
|
continue
|
|
|
|
case S.ATTRIB_VALUE_QUOTED:
|
|
if (c !== parser.q) {
|
|
if (c === "&") parser.state = S.ATTRIB_VALUE_ENTITY_Q
|
|
else parser.attribValue += c
|
|
continue
|
|
}
|
|
attrib(parser)
|
|
parser.q = ""
|
|
parser.state = S.ATTRIB
|
|
continue
|
|
|
|
case S.ATTRIB_VALUE_UNQUOTED:
|
|
if (not(attribEnd,c)) {
|
|
if (c === "&") parser.state = S.ATTRIB_VALUE_ENTITY_U
|
|
else parser.attribValue += c
|
|
continue
|
|
}
|
|
attrib(parser)
|
|
if (c === ">") openTag(parser)
|
|
else parser.state = S.ATTRIB
|
|
continue
|
|
|
|
case S.CLOSE_TAG:
|
|
if (!parser.tagName) {
|
|
if (is(whitespace, c)) continue
|
|
else if (not(nameStart, c)) strictFail(parser,
|
|
"Invalid tagname in closing tag.")
|
|
else parser.tagName = c
|
|
}
|
|
else if (c === ">") closeTag(parser)
|
|
else if (is(nameBody, c)) parser.tagName += c
|
|
else {
|
|
if (not(whitespace, c)) strictFail(parser,
|
|
"Invalid tagname in closing tag")
|
|
parser.state = S.CLOSE_TAG_SAW_WHITE
|
|
}
|
|
continue
|
|
|
|
case S.CLOSE_TAG_SAW_WHITE:
|
|
if (is(whitespace, c)) continue
|
|
if (c === ">") closeTag(parser)
|
|
else strictFail("Invalid characters in closing tag")
|
|
continue
|
|
|
|
case S.TEXT_ENTITY:
|
|
case S.ATTRIB_VALUE_ENTITY_Q:
|
|
case S.ATTRIB_VALUE_ENTITY_U:
|
|
switch(parser.state) {
|
|
case S.TEXT_ENTITY:
|
|
var returnState = S.TEXT, buffer = "textNode"
|
|
break
|
|
|
|
case S.ATTRIB_VALUE_ENTITY_Q:
|
|
var returnState = S.ATTRIB_VALUE_QUOTED, buffer = "attribValue"
|
|
break
|
|
|
|
case S.ATTRIB_VALUE_ENTITY_U:
|
|
var returnState = S.ATTRIB_VALUE_UNQUOTED, buffer = "attribValue"
|
|
break
|
|
}
|
|
if (c === ";") {
|
|
parser[buffer] += parseEntity(parser)
|
|
parser.entity = ""
|
|
parser.state = returnState
|
|
}
|
|
else if (is(entity, c)) parser.entity += c
|
|
else {
|
|
strictFail("Invalid character entity")
|
|
parser[buffer] += "&" + parser.entity + c
|
|
parser.entity = ""
|
|
parser.state = returnState
|
|
}
|
|
continue
|
|
|
|
default:
|
|
throw new Error(parser, "Unknown state: " + parser.state)
|
|
}
|
|
} // while
|
|
// cdata blocks can get very big under normal conditions. emit and move on.
|
|
// if (parser.state === S.CDATA && parser.cdata) {
|
|
// emitNode(parser, "oncdata", parser.cdata)
|
|
// parser.cdata = ""
|
|
// }
|
|
if (parser.position >= parser.bufferCheckPosition) checkBufferLength(parser)
|
|
return parser
|
|
}
|
|
|
|
})(typeof exports === "undefined" ? this.sax = {} : exports)
|
|
|
|
;
|
|
/* caldav.js - https://github.com/mozilla-b2g/caldav */
|
|
(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 = {
|
|
|
|
/**
|
|
* Associate and register module.
|
|
*
|
|
* @param {String} path uri associated with module.
|
|
* @param {Object} object module object.
|
|
*/
|
|
register: function(path, object) {
|
|
paths[path] = object;
|
|
},
|
|
|
|
/**
|
|
* 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 {
|
|
if (path in paths) {
|
|
return paths[path];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Maps exports to a file path.
|
|
*/
|
|
set exports(val) {
|
|
this.register(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.register = Exports.prototype.register;
|
|
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
|
|
));
|
|
|
|
/**
|
|
@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 (Object.hasOwnProperty.call(type, 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')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
Errors = {};
|
|
|
|
/**
|
|
* Errors typically are for front-end routing purposes so the important
|
|
* part really is just the name and (maybe) the symbol... These are really
|
|
* intended to be consumed by name... So once a name has been assigned it
|
|
* should never be modified.
|
|
*/
|
|
[
|
|
{ symbol: 'Authentication', name: 'authentication' },
|
|
{ symbol: 'InvalidEntrypoint', name: 'invalid-entrypoint' },
|
|
{ symbol: 'ServerFailure', name: 'server-failure' },
|
|
{ symbol: 'Unknown', name: 'unknown' }
|
|
].forEach(function createError(def) {
|
|
var obj = Errors[def.symbol] = function(message) {
|
|
this.message = message;
|
|
this.name = 'caldav-' + def.name;
|
|
|
|
try {
|
|
throw new Error();
|
|
} catch (e) {
|
|
this.stack = e.stack;
|
|
}
|
|
};
|
|
|
|
// just so instanceof Error works
|
|
obj.prototype = Object.create(Error.prototype);
|
|
});
|
|
|
|
module.exports = Errors;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('errors'), Caldav] :
|
|
[module, require('./caldav')]
|
|
));
|
|
|
|
// Copyright Joyent, Inc. and other Node contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
// persons to whom the Software is furnished to do so, subject to the
|
|
// following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included
|
|
// in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
// Query String Utilities
|
|
|
|
(function(module, ns) {
|
|
|
|
var QueryString = {};
|
|
|
|
QueryString.escape = function(str) {
|
|
return encodeURIComponent(str);
|
|
};
|
|
|
|
var stringifyPrimitive = function(v) {
|
|
switch (typeof v) {
|
|
case 'string':
|
|
return v;
|
|
|
|
case 'boolean':
|
|
return v ? 'true' : 'false';
|
|
|
|
case 'number':
|
|
return isFinite(v) ? v : '';
|
|
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
|
|
QueryString.stringify = QueryString.encode = function(obj, sep, eq, name) {
|
|
sep = sep || '&';
|
|
eq = eq || '=';
|
|
if (obj === null) {
|
|
obj = undefined;
|
|
}
|
|
|
|
if (typeof obj === 'object') {
|
|
return Object.keys(obj).map(function(k) {
|
|
var ks = QueryString.escape(stringifyPrimitive(k)) + eq;
|
|
if (Array.isArray(obj[k])) {
|
|
return obj[k].map(function(v) {
|
|
return ks + QueryString.escape(stringifyPrimitive(v));
|
|
}).join(sep);
|
|
} else {
|
|
return ks + QueryString.escape(stringifyPrimitive(obj[k]));
|
|
}
|
|
}).join(sep);
|
|
|
|
}
|
|
|
|
if (!name) return '';
|
|
return QueryString.escape(stringifyPrimitive(name)) + eq +
|
|
QueryString.escape(stringifyPrimitive(obj));
|
|
};
|
|
|
|
module.exports = QueryString;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('querystring'), Caldav] :
|
|
[module, require('./caldav')]
|
|
));
|
|
(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',
|
|
'oncdata',
|
|
'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);
|
|
},
|
|
|
|
close: function() {
|
|
this._parse.close();
|
|
},
|
|
|
|
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;
|
|
|
|
//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);
|
|
},
|
|
|
|
oncdata: function(data) {
|
|
this._fireHandler('oncdata', 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')]
|
|
));
|
|
(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')]
|
|
));
|
|
/**
|
|
@namespace
|
|
*/
|
|
(function(module, ns) {
|
|
var Template = ns.require('template');
|
|
|
|
/**
|
|
* Builds a node of a calendar-data or filter xml element.
|
|
*
|
|
* @param {QueryBuilder} builder instance.
|
|
* @param {String} name component/prop name (like RRULE/VEVENT).
|
|
* @param {Boolean} isProp is this node a property tag?
|
|
*/
|
|
function Node(builder, name, isProp) {
|
|
this.name = name;
|
|
this.builder = builder;
|
|
this.isProp = !!isProp;
|
|
|
|
this.comps = Object.create(null);
|
|
this.props = Object.create(null);
|
|
}
|
|
|
|
Node.prototype = {
|
|
|
|
/**
|
|
* Hook for adding custom node content.
|
|
* (for unsupported or custom filters)
|
|
*
|
|
* Usually you never want to use this.
|
|
*
|
|
* @type {Null|Array}
|
|
*/
|
|
content: null,
|
|
|
|
/**
|
|
* Appends custom string content into node.
|
|
*
|
|
* @param {String} string content.
|
|
*/
|
|
appendString: function(string) {
|
|
if (!this.content) {
|
|
this.content = [];
|
|
}
|
|
|
|
if (typeof(string) !== 'string') {
|
|
string = string.toString();
|
|
}
|
|
|
|
this.content.push(string);
|
|
},
|
|
|
|
_timeRange: null,
|
|
|
|
/**
|
|
* Adds a time range element to the node.
|
|
*
|
|
* Example:
|
|
*
|
|
* var node;
|
|
*
|
|
* // key/values not validated or converted
|
|
* // but directly piped into the time-range element.
|
|
* node.setTimeRange({
|
|
* start: '20060104T000000Z',
|
|
* end: '20060105T000000Z'
|
|
* });
|
|
*
|
|
* // when null removes element
|
|
* node.setTimeRange(null);
|
|
*
|
|
* @param {Object|Null} range time range or null to remove.
|
|
*/
|
|
setTimeRange: function(range) {
|
|
this._timeRange = range;
|
|
},
|
|
|
|
/**
|
|
* Removes a property from the output.
|
|
* @param {String} name prop.
|
|
*/
|
|
removeProp: function(name) {
|
|
delete this.props[name];
|
|
},
|
|
|
|
/**
|
|
* Removes a component from the output.
|
|
*
|
|
* @param {String} name comp.
|
|
*/
|
|
removeComp: function(name) {
|
|
delete this.comps[name];
|
|
},
|
|
|
|
_addNodes: function(type, nodes) {
|
|
// return value when is array
|
|
var result = this;
|
|
|
|
if (!Array.isArray(nodes)) {
|
|
// clear out the return value as we will
|
|
// now use the first node.
|
|
result = null;
|
|
}
|
|
|
|
nodes = (Array.isArray(nodes)) ? nodes : [nodes];
|
|
|
|
var idx = 0;
|
|
var len = nodes.length;
|
|
var name;
|
|
var node;
|
|
|
|
for (; idx < len; idx++) {
|
|
name = nodes[idx];
|
|
node = new Node(this.builder, name, type === 'props');
|
|
this[type][name] = node;
|
|
}
|
|
|
|
// when we where not given an array of nodes
|
|
// assume we want one specific one so set that
|
|
// as the return value.
|
|
if (!result)
|
|
result = node;
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Adds one or more props.
|
|
* If property already exists will not add
|
|
* duplicates but return the existing property.
|
|
*
|
|
* @param {String|Array[String]} prop one or more properties to add.
|
|
* @return {Node|Self} returns a node or self when given an array.
|
|
*/
|
|
prop: function(prop) {
|
|
return this._addNodes('props', prop);
|
|
},
|
|
|
|
/**
|
|
* Adds one or more comp.
|
|
* If comp already exists will not add
|
|
* duplicates but return the existing comp.
|
|
*
|
|
* @param {String|Array[String]} comp one or more components to add.
|
|
* @return {Node|Self} returns a node or self when given an array.
|
|
*/
|
|
comp: function(comp) {
|
|
return this._addNodes('comps', comp);
|
|
},
|
|
|
|
xmlAttributes: function() {
|
|
return { name: this.name };
|
|
},
|
|
|
|
/**
|
|
* Transform tree into a string.
|
|
*
|
|
* NOTE: order is not preserved at all here.
|
|
* It is highly unlikely that order is a
|
|
* factor for calendar-data or filter
|
|
* but this is fair warning for other uses.
|
|
*/
|
|
toString: function() {
|
|
var content = '';
|
|
var key;
|
|
var template = this.builder.template;
|
|
|
|
if (this._timeRange) {
|
|
content += template.tag(
|
|
['caldav', 'time-range'],
|
|
this._timeRange
|
|
);
|
|
}
|
|
|
|
// render out children
|
|
for (key in this.props) {
|
|
content += this.props[key].toString();
|
|
}
|
|
|
|
for (key in this.comps) {
|
|
content += this.comps[key].toString();
|
|
}
|
|
|
|
if (this.content) {
|
|
content += this.content.join('');
|
|
}
|
|
|
|
// determine the tag name
|
|
var tag;
|
|
if (this.isProp) {
|
|
tag = this.builder.propTag;
|
|
} else {
|
|
tag = this.builder.compTag;
|
|
}
|
|
|
|
// build the xml element and return it.
|
|
return template.tag(
|
|
tag,
|
|
this.xmlAttributes(),
|
|
content
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Query builder can be used to build xml document fragments
|
|
* for calendar-data & calendar-filter.
|
|
* (and any other xml document with a similar structure)
|
|
*
|
|
* Options:
|
|
* - template: (Caldav.Template instance)
|
|
* - tag: container tag (like 'calendar-data')
|
|
* - attributes: attributes for root
|
|
* - compTag: name of comp[onent] tag name (like 'comp')
|
|
* - propTag: name of property tag (like 'prop')
|
|
*
|
|
* @param {Object} options query builder options.
|
|
*/
|
|
function QueryBuilder(options) {
|
|
if (!options)
|
|
options = {};
|
|
|
|
if (!(options.template instanceof Template)) {
|
|
throw new TypeError(
|
|
'.template must be an instance' +
|
|
' of Caldav.Template given "' + options.template + '"'
|
|
);
|
|
}
|
|
|
|
for (var key in options) {
|
|
if (options.hasOwnProperty(key)) {
|
|
this[key] = options[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
QueryBuilder.prototype = {
|
|
tag: ['caldav', 'calendar-data'],
|
|
|
|
compTag: ['caldav', 'comp'],
|
|
|
|
propTag: ['caldav', 'prop'],
|
|
|
|
attributes: null,
|
|
|
|
_limitRecurrenceSet: null,
|
|
|
|
/**
|
|
* Adds the recurrence set limit child to the query.
|
|
* Directly maps to the caldav 'limit-recurrence-set' element.
|
|
*
|
|
* Examples:
|
|
*
|
|
* var builder;
|
|
*
|
|
* // no validation or formatting is done.
|
|
* builder.setRecurrenceSetLimit({
|
|
* start: '20060103T000000Z',
|
|
* end: '20060103T000000Z'
|
|
* });
|
|
*
|
|
* // use null to clear value
|
|
* builder.setRecurrenceSetLimit(null);
|
|
*
|
|
* @param {Object|Null} limit see above.
|
|
*/
|
|
setRecurrenceSetLimit: function(limit) {
|
|
this._limitRecurrenceSet = limit;
|
|
},
|
|
|
|
/**
|
|
* @param {String} name component name (like VCALENDAR).
|
|
* @return {QueryBuilder.Node} node instance.
|
|
*/
|
|
setComp: function(name) {
|
|
return this._compRoot = new Node(this, name);
|
|
},
|
|
|
|
/**
|
|
* Returns the root node of the document fragment.
|
|
*/
|
|
getComp: function() {
|
|
return this._compRoot;
|
|
},
|
|
|
|
toString: function() {
|
|
var content = '';
|
|
var comp = this.getComp();
|
|
|
|
if (this._limitRecurrenceSet) {
|
|
content += this.template.tag(
|
|
['caldav', 'limit-recurrence-set'],
|
|
this._limitRecurrenceSet
|
|
);
|
|
}
|
|
|
|
if (comp) {
|
|
content += comp.toString();
|
|
}
|
|
|
|
return this.template.tag(
|
|
this.tag,
|
|
this.attributes,
|
|
content
|
|
);
|
|
}
|
|
|
|
};
|
|
|
|
QueryBuilder.Node = Node;
|
|
module.exports = QueryBuilder;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('query_builder'), Caldav] :
|
|
[module, require('./caldav')]
|
|
));
|
|
|
|
/**
|
|
@namespace
|
|
*/
|
|
(function(module, ns) {
|
|
var Native;
|
|
var Errors = ns.require('errors');
|
|
|
|
if (typeof(window) === 'undefined') {
|
|
Native = require('xmlhttprequest').XMLHttpRequest;
|
|
} else {
|
|
Native = window.XMLHttpRequest;
|
|
}
|
|
|
|
function determineHttpStatusError(status) {
|
|
var message = 'Cannot handle request due to server response';
|
|
var err = 'Unknown';
|
|
|
|
if (status === 500)
|
|
err = 'ServerFailure';
|
|
|
|
if (status === 401)
|
|
err = 'Authentication';
|
|
|
|
return new Errors[err](message);
|
|
}
|
|
|
|
/**
|
|
* 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 (Object.hasOwnProperty.call(options, key)) {
|
|
this[key] = options[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
Xhr.prototype = {
|
|
globalXhrOptions: null,
|
|
xhrClass: Native,
|
|
xhr: null,
|
|
method: 'GET',
|
|
async: true,
|
|
waiting: false,
|
|
user: null,
|
|
password: null,
|
|
url: null,
|
|
streaming: true,
|
|
validateStatus: false,
|
|
|
|
headers: {},
|
|
data: null,
|
|
|
|
_serialize: function _serialize() {
|
|
return this.data;
|
|
},
|
|
|
|
/**
|
|
* @param {String} user basic auth user.
|
|
* @param {String} password basic auth pass.
|
|
* @return {String} basic auth token.
|
|
*/
|
|
_credentials: function(user, pass) {
|
|
// this code should never run in nodejs.
|
|
return 'Basic ' + window.btoa(
|
|
user + ':' + pass
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Aborts the request if it has already been sent.
|
|
* @param {Function=} cb An optional callback function.
|
|
*/
|
|
abort: function(cb) {
|
|
if (this.waiting) {
|
|
this.xhr.abort();
|
|
this.waiting = false;
|
|
}
|
|
|
|
if (cb !== undefined) {
|
|
cb();
|
|
}
|
|
},
|
|
|
|
_buildXHR: function(callback) {
|
|
var header;
|
|
|
|
if (typeof(callback) === 'undefined') {
|
|
callback = this.callback;
|
|
}
|
|
|
|
this.xhr = new this.xhrClass(
|
|
this.globalXhrOptions ? this.globalXhrOptions : undefined);
|
|
|
|
// This hack is in place due to some platform
|
|
// bug in gecko when using mozSystem xhr
|
|
// the credentials only seem to work as expected
|
|
// when constructing them manually.
|
|
if (!this.globalXhrOptions || !this.globalXhrOptions.mozSystem) {
|
|
this.xhr.open(
|
|
this.method, this.url, this.async, this.user, this.password);
|
|
} else {
|
|
this.xhr.open(this.method, this.url, this.async);
|
|
this.xhr.setRequestHeader('Authorization', this._credentials(
|
|
this.user,
|
|
this.password
|
|
));
|
|
}
|
|
|
|
var useMozChunkedText = false;
|
|
if (
|
|
this.streaming &&
|
|
this.globalXhrOptions &&
|
|
this.globalXhrOptions.useMozChunkedText
|
|
) {
|
|
useMozChunkedText = true;
|
|
this.xhr.responseType = 'moz-chunked-text';
|
|
}
|
|
|
|
for (header in this.headers) {
|
|
if (Object.hasOwnProperty.call(this.headers, header)) {
|
|
this.xhr.setRequestHeader(header, this.headers[header]);
|
|
}
|
|
}
|
|
|
|
|
|
var hasProgressEvents = false;
|
|
|
|
// check for progress event support.
|
|
if (this.streaming) {
|
|
if ('onprogress' in this.xhr) {
|
|
hasProgressEvents = true;
|
|
var last = 0;
|
|
|
|
if (useMozChunkedText) {
|
|
this.xhr.onprogress = (function onChunkedProgress(event) {
|
|
if (this.ondata) {
|
|
this.ondata(this.xhr.responseText);
|
|
}
|
|
}.bind(this));
|
|
} else {
|
|
this.xhr.onprogress = (function onProgress(event) {
|
|
var chunk = this.xhr.responseText.substr(last, event.loaded);
|
|
last = event.loaded;
|
|
if (this.ondata) {
|
|
this.ondata(chunk);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
}
|
|
}
|
|
|
|
this.xhr.onreadystatechange = (function onReadyStateChange() {
|
|
var data;
|
|
if (this.xhr.readyState === 4) {
|
|
data = this.xhr.responseText;
|
|
|
|
// emulate progress events for node...
|
|
// this really lame we should probably just
|
|
// use a real http request for node but this
|
|
// will let us do some testing via node for now.
|
|
if (!hasProgressEvents && this.ondata) {
|
|
this.ondata(data);
|
|
}
|
|
|
|
this.waiting = false;
|
|
|
|
if (
|
|
!this.validateStatus ||
|
|
(
|
|
this.xhr.status > 199 &&
|
|
this.xhr.status < 300
|
|
)
|
|
) {
|
|
return callback(null, this.xhr);
|
|
}
|
|
|
|
callback(determineHttpStatusError(this.xhr.status), this.xhr);
|
|
}
|
|
}.bind(this));
|
|
|
|
this.waiting = true;
|
|
return this.xhr;
|
|
},
|
|
|
|
/**
|
|
* Sends request to server.
|
|
*
|
|
* @param {Function} callback success/failure handler.
|
|
*/
|
|
send: function send(callback) {
|
|
var xhr = this._buildXHR(callback);
|
|
xhr.send(this._serialize());
|
|
return xhr;
|
|
}
|
|
};
|
|
|
|
module.exports = Xhr;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('xhr'), Caldav] :
|
|
[module, require('./caldav')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
var XHR = ns.require('xhr');
|
|
var QueryString = ns.require('querystring');
|
|
|
|
var REQUIRED_CREDENTIALS = [
|
|
'client_secret',
|
|
'client_id',
|
|
'redirect_uri',
|
|
'url'
|
|
];
|
|
|
|
/**
|
|
* Given a string (directly from xhr.responseText usually) format and create
|
|
* an oauth authorization server response.
|
|
*
|
|
* @param {String} resp raw response from http server.
|
|
* @return {Object} formatted version of response.
|
|
*/
|
|
function formatResponse(resp) {
|
|
resp = JSON.parse(resp);
|
|
|
|
// replace the oauth details
|
|
if (resp.access_token) {
|
|
resp.issued_at = Date.now();
|
|
}
|
|
|
|
return resp;
|
|
}
|
|
|
|
/**
|
|
* Sends XHR object's request and handles common JSON parsing issues.
|
|
*/
|
|
function sendRequest(xhr, callback) {
|
|
return xhr.send(function(err, request) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
var result;
|
|
try {
|
|
result = formatResponse(request.responseText);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
callback(err, result, request);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Private helper for issuing a POST http request the given endpoint.
|
|
* The body of the HTTP request is a x-www-form-urlencoded request.
|
|
*
|
|
*
|
|
* @param {String} url endpoint of server.
|
|
* @param {Object} requestData object representation of form data.
|
|
*/
|
|
function post(url, requestData, callback) {
|
|
var xhr = new XHR({
|
|
url: url,
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
data: QueryString.stringify(requestData),
|
|
method: 'POST',
|
|
streaming: false
|
|
});
|
|
|
|
return sendRequest(xhr, callback);
|
|
}
|
|
|
|
/**
|
|
* Creates an OAuth authentication handler. The logic here is designed to
|
|
* handle the cases after the user initially authenticates and we either have
|
|
* a "code" or "refresh_token".
|
|
*
|
|
*
|
|
* var oauthClient = new OAuth(
|
|
* {
|
|
* url: 'https://accounts.google.com/o/oauth2/token',
|
|
* client_secret: '',
|
|
* client_id: '',
|
|
* redirect_uri: '',
|
|
* // optional user_info option
|
|
* user_info: {
|
|
* url: 'https://www.googleapis.com/oauth2/v3/userinfo',
|
|
* field: 'email'
|
|
* }
|
|
* }
|
|
* );
|
|
*
|
|
*/
|
|
function OAuth(credentials) {
|
|
this.apiCredentials = {};
|
|
|
|
for (var key in credentials) {
|
|
this.apiCredentials[key] = credentials[key];
|
|
}
|
|
|
|
REQUIRED_CREDENTIALS.forEach(function(type) {
|
|
if (!(type in this.apiCredentials)) {
|
|
throw new Error('.apiCredentials.' + type + ' : must be available.');
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
OAuth.prototype = {
|
|
|
|
/**
|
|
* Basic API credentials for oauth operations.
|
|
*
|
|
* Required properties:
|
|
*
|
|
* - url
|
|
* - client_id
|
|
* - client_secret
|
|
* - redirect_uri
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
apiCredentials: null,
|
|
|
|
/**
|
|
* Private helper for requesting user info... Unlike other methods this
|
|
* is unrelated to core rfc6749 functionality.
|
|
*
|
|
* NOTE: Really brittle as it will not refresh tokens must be called
|
|
* directly after authorization with a fresh access_token.
|
|
*
|
|
*
|
|
* @param {Object} oauth result of a previous oauth response
|
|
* (must contain valid access_token).
|
|
*
|
|
* @param {Function} callback called with [err, userProperty].
|
|
*/
|
|
_requestUserInfo: function(oauth, callback) {
|
|
var apiCredentials = this.apiCredentials;
|
|
var url = apiCredentials.user_info.url;
|
|
var field = apiCredentials.user_info.field;
|
|
var authorization = oauth.token_type + ' ' + oauth.access_token;
|
|
|
|
var xhr = new XHR({
|
|
headers: {
|
|
Authorization: authorization
|
|
},
|
|
url: url,
|
|
streaming: false
|
|
});
|
|
|
|
sendRequest(xhr, function(err, json) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
/* json is an object so this should not explode */
|
|
callback(err, json[field]);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Given a code from the user sign in flow get the refresh token &
|
|
* access_token.
|
|
*/
|
|
authenticateCode: function(code, callback) {
|
|
var apiCredentials = this.apiCredentials;
|
|
|
|
if (!code) {
|
|
return setTimeout(function() {
|
|
callback(new Error('code must be given'));
|
|
});
|
|
}
|
|
|
|
var self = this;
|
|
function handleResponse(err, result) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
if (!apiCredentials.user_info) {
|
|
return callback(null, result);
|
|
}
|
|
|
|
// attempt fetching user details
|
|
self._requestUserInfo(result, function(err, user) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
result.user = user;
|
|
callback(null, result);
|
|
});
|
|
}
|
|
|
|
return post(
|
|
apiCredentials.url,
|
|
{
|
|
code: code,
|
|
client_id: apiCredentials.client_id,
|
|
client_secret: apiCredentials.client_secret,
|
|
redirect_uri: apiCredentials.redirect_uri,
|
|
grant_type: 'authorization_code'
|
|
},
|
|
handleResponse
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Refresh api keys and tokens related to those keys.
|
|
*
|
|
* @param {String} refreshToken token for refreshing oauth credentials
|
|
* (refresh_token per rfc6749).
|
|
*/
|
|
refreshToken: function(refreshToken, callback) {
|
|
var apiCredentials = this.apiCredentials;
|
|
|
|
if (!refreshToken) {
|
|
throw new Error('invalid refresh token given: "' + refreshToken + '"');
|
|
}
|
|
|
|
return post(
|
|
apiCredentials.url,
|
|
{
|
|
refresh_token: refreshToken,
|
|
client_id: apiCredentials.client_id,
|
|
client_secret: apiCredentials.client_secret,
|
|
grant_type: 'refresh_token'
|
|
},
|
|
callback
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Soft verification of tokens... Ensures access_token is available and is
|
|
* not expired.
|
|
*
|
|
* @param {Object} oauth details.
|
|
* @return {Boolean} true when looks valid.
|
|
*/
|
|
accessTokenValid: function(oauth) {
|
|
return !!(
|
|
oauth &&
|
|
oauth.access_token &&
|
|
oauth.expires_in &&
|
|
oauth.issued_at &&
|
|
(Date.now() < (oauth.issued_at + oauth.expires_in))
|
|
);
|
|
}
|
|
|
|
};
|
|
|
|
|
|
module.exports = OAuth;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('oauth2'), Caldav] :
|
|
[module, require('./caldav')]
|
|
));
|
|
|
|
|
|
|
|
(function(module, ns) {
|
|
|
|
var XHR = ns.require('xhr');
|
|
|
|
function BasicAuth(connection, options) {
|
|
// create a clone of options
|
|
var clone = Object.create(null);
|
|
|
|
if (typeof(options) !== 'undefined') {
|
|
for (var key in options) {
|
|
clone[key] = options[key];
|
|
}
|
|
}
|
|
|
|
clone.password = connection.password || clone.password;
|
|
clone.user = connection.user || clone.user;
|
|
|
|
XHR.call(this, clone);
|
|
}
|
|
|
|
BasicAuth.prototype = {
|
|
__proto__: XHR.prototype,
|
|
validateStatus: true
|
|
};
|
|
|
|
|
|
module.exports = BasicAuth;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('http/basic_auth'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
|
|
(function(module, ns) {
|
|
|
|
var XHR = ns.require('xhr');
|
|
var QueryString = ns.require('querystring');
|
|
var Connection = ns.require('connection');
|
|
var OAuth = ns.require('oauth2');
|
|
|
|
/**
|
|
* Creates an XHR like object given a connection and a set of options
|
|
* (passed directly to the superclass)
|
|
*
|
|
* @param {Caldav.Connection} connection used for apiCredentials.
|
|
* @param {Object} options typical XHR options.
|
|
*/
|
|
function Oauth2(connection, options) {
|
|
if (
|
|
!connection ||
|
|
!connection.oauth ||
|
|
(
|
|
!connection.oauth.code &&
|
|
!connection.oauth.refresh_token
|
|
)
|
|
) {
|
|
throw new Error('connection .oauth must have code or refresh_token');
|
|
}
|
|
|
|
this.connection = connection;
|
|
|
|
this.oauth =
|
|
new OAuth(connection.apiCredentials);
|
|
|
|
// create a clone of options
|
|
var clone = Object.create(null);
|
|
|
|
if (typeof(options) !== 'undefined') {
|
|
for (var key in options) {
|
|
clone[key] = options[key];
|
|
}
|
|
}
|
|
|
|
XHR.call(this, clone);
|
|
}
|
|
|
|
Oauth2.prototype = {
|
|
__proto__: XHR.prototype,
|
|
|
|
validateStatus: true,
|
|
|
|
_sendXHR: function(xhr) {
|
|
xhr.setRequestHeader(
|
|
'Authorization', 'Bearer ' + this.connection.oauth.access_token
|
|
);
|
|
|
|
xhr.send(this._serialize());
|
|
return xhr;
|
|
},
|
|
|
|
_updateConnection: function(credentials) {
|
|
var oauth = this.connection.oauth;
|
|
var update = { oauth: credentials };
|
|
|
|
if (oauth.refresh_token && !credentials.refresh_token)
|
|
credentials.refresh_token = oauth.refresh_token;
|
|
|
|
if (credentials.user) {
|
|
update.user = credentials.user;
|
|
delete credentials.user;
|
|
}
|
|
|
|
return this.connection.update(update);
|
|
},
|
|
|
|
send: function(callback) {
|
|
var xhr = this._buildXHR(callback);
|
|
var oauth = this.connection.oauth;
|
|
|
|
// everything is fine just send
|
|
if (this.oauth.accessTokenValid(oauth)) {
|
|
return this._sendXHR(xhr);
|
|
}
|
|
|
|
var handleTokenUpdates = (function handleTokenUpdates(err, credentials) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
this._updateConnection(credentials);
|
|
return this._sendXHR(xhr);
|
|
}.bind(this));
|
|
|
|
if (oauth.code) {
|
|
this.oauth.authenticateCode(oauth.code, handleTokenUpdates);
|
|
|
|
// it should be impossible to have both code and refresh_token
|
|
// but we return as a guard
|
|
return xhr;
|
|
}
|
|
|
|
if (oauth.refresh_token) {
|
|
this.oauth.refreshToken(oauth.refresh_token, handleTokenUpdates);
|
|
return xhr;
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
|
|
module.exports = Oauth2;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('http/oauth2'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
|
|
|
|
(function(module, ns) {
|
|
|
|
var Caldav;
|
|
if (typeof(window) !== 'undefined') {
|
|
Caldav = window.Caldav;
|
|
} else {
|
|
Caldav = ns.require('caldav');
|
|
}
|
|
var XHR = ns.require('xhr');
|
|
|
|
/**
|
|
* Connection objects contain
|
|
* general information to be reused
|
|
* across XHR requests.
|
|
*
|
|
* Also handles normalization of path details.
|
|
*/
|
|
function Connection(options) {
|
|
if (typeof(options) === 'undefined') {
|
|
options = {};
|
|
}
|
|
|
|
var key;
|
|
|
|
for (key in options) {
|
|
if (Object.hasOwnProperty.call(options, key)) {
|
|
this[key] = options[key];
|
|
}
|
|
}
|
|
|
|
var domain = options.domain;
|
|
|
|
if (domain) {
|
|
if (domain.substr(-1) === '/') {
|
|
this.domain = domain.substr(0, domain.length - 1);
|
|
}
|
|
}
|
|
|
|
var httpHandler = this.httpHandler || 'basic_auth';
|
|
if (typeof(httpHandler) !== 'object') {
|
|
this.httpHandler = Caldav.require('http/' + httpHandler);
|
|
}
|
|
}
|
|
|
|
Connection.prototype = {
|
|
/**
|
|
* Default username for requests.
|
|
*/
|
|
user: '',
|
|
|
|
/**
|
|
* Default passwords for requests.
|
|
*/
|
|
password: '',
|
|
|
|
/**
|
|
* Default domain for requests.
|
|
*/
|
|
domain: '',
|
|
|
|
/**
|
|
* Creates new XHR request based on default
|
|
* options for connection.
|
|
*
|
|
* @return {Caldav.Xhr} http request set with default options.
|
|
*/
|
|
request: function(options) {
|
|
if (options) {
|
|
if (options.url && options.url.indexOf('http') !== 0) {
|
|
var url = options.url;
|
|
if (url.substr(0, 1) !== '/') {
|
|
url = '/' + url;
|
|
}
|
|
options.url = this.domain + url;
|
|
}
|
|
}
|
|
|
|
return new this.httpHandler(this, options);
|
|
},
|
|
|
|
/**
|
|
* Update properties on this connection and trigger a "update" event.
|
|
*
|
|
*
|
|
* connection.onupdate = function() {
|
|
* // do stuff
|
|
* };
|
|
*
|
|
* connection.update({
|
|
* user: 'foobar'
|
|
* });
|
|
*
|
|
*
|
|
* @param {Object} newProperties to shallow copy onto connection.
|
|
*/
|
|
update: function(newProperties) {
|
|
if (newProperties) {
|
|
for (var key in newProperties) {
|
|
if (Object.prototype.hasOwnProperty.call(newProperties, key)) {
|
|
this[key] = newProperties[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.onupdate) {
|
|
this.onupdate();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
};
|
|
|
|
module.exports = Connection;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('connection'), Caldav] :
|
|
[module, require('./caldav')]
|
|
));
|
|
(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 (Object.hasOwnProperty.call(obj, 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')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
var Base = ns.require('sax/base');
|
|
|
|
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) {
|
|
return CalendarDataHandler.ondata.call(this, data);
|
|
},
|
|
|
|
// Servers can also stash ical data inside of an xml cdata.
|
|
oncdata: function(data) {
|
|
return CalendarDataHandler.ondata.call(this, data);
|
|
},
|
|
});
|
|
|
|
CalendarDataHandler.ondata = function(data) {
|
|
var handler = this.handler;
|
|
this.current[this.currentTag[handler.tagField]] =
|
|
CalendarDataHandler.parseICAL(data);
|
|
};
|
|
|
|
/**
|
|
* Default ical parser handler.
|
|
*
|
|
* XXX: Feels a little hacky but works...
|
|
*/
|
|
CalendarDataHandler.parseICAL = function(input) {
|
|
return input;
|
|
};
|
|
|
|
module.exports = CalendarDataHandler;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('sax/calendar_data_handler'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
var HTTP_STATUS = /([0-9]{3,3})/;
|
|
|
|
var Base = ns.require('sax/base');
|
|
var CalendarDataHandler = ns.require('sax/calendar_data_handler');
|
|
|
|
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 HrefHandler = Base.create({
|
|
name: 'href',
|
|
|
|
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) {
|
|
handler = this.handler;
|
|
this.current[this.currentTag[handler.tagField]] = match[1];
|
|
} else {
|
|
this._super.ontext.call(this, data, handler);
|
|
}
|
|
}
|
|
});
|
|
|
|
var PrivilegeSet = Base.create({
|
|
name: 'PrivilegeSet',
|
|
|
|
onopentag: function(data) {
|
|
if (this.currentTag.handler === this.handler) {
|
|
this.stack.push(this.current);
|
|
this.current = [];
|
|
} else {
|
|
if (data.tagSpec !== 'DAV:/privilege') {
|
|
this.current.push(data.local);
|
|
}
|
|
}
|
|
},
|
|
|
|
onclosetag: function(data) {
|
|
var current = this.currentTag;
|
|
|
|
if (current.handler === this.handler) {
|
|
data = this.current;
|
|
|
|
this.current = this.stack.pop();
|
|
this.current[current.local] = data;
|
|
}
|
|
},
|
|
|
|
ontext: null
|
|
|
|
});
|
|
|
|
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 && 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,
|
|
'http://calendarserver.org/ns//getctag': TextHandler,
|
|
'DAV:/status': HttpStatusHandler,
|
|
'DAV:/resourcetype': ArrayHandler,
|
|
'DAV:/current-user-privilege-set': PrivilegeSet,
|
|
'urn:ietf:params:xml:ns:caldav/calendar-data': CalendarDataHandler,
|
|
'DAV:/value': TextHandler,
|
|
'DAV:/owner': HrefHandler,
|
|
'DAV:/getetag': TextHandler,
|
|
'DAV:/displayname': TextHandler,
|
|
'urn:ietf:params:xml:ns:caldav/calendar-home-set': HrefHandler,
|
|
'urn:ietf:params:xml:ns:caldav/calendar-timezone': TextHandler,
|
|
'http://apple.com/ns/ical//calendar-color': TextHandler,
|
|
'urn:ietf:params:xml:ns:caldav/calendar-description': TextHandler
|
|
},
|
|
|
|
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);
|
|
return null;
|
|
},
|
|
|
|
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 (Object.hasOwnProperty.call(props, 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);
|
|
return null;
|
|
},
|
|
|
|
oncomplete: function() {
|
|
var parent;
|
|
|
|
if (this.current.href) {
|
|
this.emit(
|
|
'DAV:/response',
|
|
this.current.href,
|
|
this.current.propstat
|
|
);
|
|
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')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
var SAX = ns.require('sax');
|
|
var XHR = ns.require('xhr');
|
|
|
|
/**
|
|
* Creates an (Web/Cal)Dav request.
|
|
*
|
|
* @param {Caldav.Connection} connection connection details.
|
|
* @param {Object} options additional options for request.
|
|
*/
|
|
function Abstract(connection, options) {
|
|
if (typeof(options) === 'undefined') {
|
|
options = {};
|
|
}
|
|
|
|
var key;
|
|
var xhrOptions = {};
|
|
|
|
this.sax = new SAX();
|
|
|
|
for (key in options) {
|
|
if (Object.hasOwnProperty.call(options, key)) {
|
|
this[key] = options[key];
|
|
}
|
|
}
|
|
|
|
if (!connection) {
|
|
throw new Error('must pass connection object');
|
|
}
|
|
|
|
this.connection = connection;
|
|
|
|
this.xhr = this.connection.request({
|
|
url: this.url,
|
|
headers: { 'Content-Type': 'text/xml' }
|
|
});
|
|
}
|
|
|
|
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.
|
|
* @return {Caldav.Xhr} The xhr request so that the caller
|
|
* has a chance to abort the request.
|
|
*/
|
|
send: function(callback) {
|
|
var self = this;
|
|
var req = this.xhr;
|
|
req.data = this._createPayload();
|
|
|
|
req.ondata = function xhrOnData(chunk) {
|
|
self.sax.write(chunk);
|
|
};
|
|
|
|
// in the future we may stream data somehow
|
|
req.send(function xhrResult(err, xhr) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self.sax.close();
|
|
return self._processResult(req, callback);
|
|
});
|
|
|
|
return req;
|
|
}
|
|
};
|
|
|
|
module.exports = Abstract;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('request/abstract'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
var XHR = ns.require('xhr');
|
|
|
|
/**
|
|
* Creates an Http request for a single webdav resource.
|
|
* Thin wrapper over http/xhr each public method has the same
|
|
* signature with similar options:
|
|
*
|
|
* // the intent is that after a larger calendar query
|
|
* // the urls are stored and can be used to modify the
|
|
* // calendar resources.
|
|
* var asset = new Caldav.Request.Asset(con, 'someurl');
|
|
*
|
|
* asset.get({ etag: 'foo'}, function(err, data) {
|
|
* });
|
|
*
|
|
* asset.put({ etag: 'foo' }, body, function(err, data) {
|
|
*
|
|
* });
|
|
*
|
|
* asset.delete(function() {
|
|
*
|
|
* });
|
|
*
|
|
* @param {Caldav.Connection} connection connection details.
|
|
* @param {String} url assert url.
|
|
*/
|
|
function Asset(connection, url) {
|
|
if (!connection) {
|
|
throw new Error('must pass connection object');
|
|
}
|
|
this.connection = connection;
|
|
this.url = url;
|
|
}
|
|
|
|
Asset.prototype = {
|
|
|
|
contentType: 'text/calendar',
|
|
|
|
_buildRequest: function(method, options) {
|
|
var headers = {
|
|
'Content-Type': this.contentType
|
|
};
|
|
|
|
if (options && options.contentType) {
|
|
headers['Content-Type'] = options.contentType;
|
|
}
|
|
|
|
if (options && options.etag) {
|
|
headers['If-None-Match'] = options.etag;
|
|
}
|
|
|
|
return this.connection.request({
|
|
url: this.url,
|
|
headers: headers,
|
|
method: method
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Find a single calendar asset.
|
|
* This method should only be used to either
|
|
* confirm a put or delete request.
|
|
*
|
|
* Calendar query is far more suited for fetching
|
|
* large amounts of calendar data.
|
|
*
|
|
* Options:
|
|
* - etag: used to issue a 'If-Not-Match'
|
|
*
|
|
* @param {Object} [options] calendar options.
|
|
* @param {Function} callback node style [err, data, xhr].
|
|
* @return {Caldav.Xhr} The underlying xhr request so that the caller
|
|
* has a chance to abort the request.
|
|
*/
|
|
get: function(options, callback) {
|
|
if (typeof(options) === 'function') {
|
|
callback = options;
|
|
options = null;
|
|
}
|
|
|
|
var req = this._buildRequest('GET', options);
|
|
|
|
return req.send(function(err, xhr) {
|
|
callback(err, xhr.responseText, xhr);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Adds or modifies a single calendar resource.
|
|
*
|
|
* @param {Object} [options] see get.
|
|
* @param {String} data post content.
|
|
* @param {Function} callback node style [err, data, xhr].
|
|
* @return {Caldav.Xhr} The underlying xhr request so that the caller
|
|
* has a chance to abort the request.
|
|
*/
|
|
put: function(options, data, callback) {
|
|
if (typeof(options) === 'string') {
|
|
data = options;
|
|
options = null;
|
|
}
|
|
|
|
if (typeof(data) === 'function') {
|
|
callback = data;
|
|
data = null;
|
|
}
|
|
|
|
var req = this._buildRequest('PUT', options);
|
|
req.data = data;
|
|
|
|
return req.send(function(err, xhr) {
|
|
callback(err, xhr.responseText, xhr);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Deletes a calendar resource
|
|
*
|
|
* @param {Object} [options] see get.
|
|
* @param {Function} callback node style [err, data, xhr].
|
|
* @return {Caldav.Xhr} The underlying xhr request so that the caller
|
|
* has a chance to abort the request.
|
|
*/
|
|
delete: function(options, callback) {
|
|
if (typeof(options) === 'function') {
|
|
callback = options;
|
|
options = null;
|
|
}
|
|
|
|
var req = this._buildRequest('DELETE', options);
|
|
|
|
return req.send(function(err, xhr) {
|
|
callback(err, xhr.responseText, xhr);
|
|
});
|
|
}
|
|
};
|
|
|
|
module.exports = Asset;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('request/asset'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
(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'] = 0;
|
|
this.xhr.method = 'PROPFIND';
|
|
}
|
|
|
|
Propfind.prototype = {
|
|
__proto__: Abstract.prototype,
|
|
|
|
get depth() {
|
|
return this.xhr.headers.Depth;
|
|
},
|
|
|
|
set depth(val) {
|
|
this.xhr.headers.Depth = val;
|
|
},
|
|
|
|
/**
|
|
* 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));
|
|
},
|
|
|
|
/**
|
|
* Removes property from request.
|
|
* Must use same arguments as 'prop' to remove prop.
|
|
*
|
|
* @param {String|Array} tagDesc tag description.
|
|
* @param {Object} [attr] optional tag attrs.
|
|
* @param {Obj} [content] optional content.
|
|
*/
|
|
removeProp: function(tagDesc, attr, content) {
|
|
var prop = this.template.tag(tagDesc, attr, content);
|
|
var idx = this._props.indexOf(prop);
|
|
|
|
if (idx !== -1) {
|
|
this._props.splice(idx, 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Checks if prop has been added to the request.
|
|
*
|
|
* @param {String|Array} tagDesc tag description.
|
|
* @param {Object} [attr] optional tag attrs.
|
|
* @param {Obj} [content] optional content.
|
|
*/
|
|
hasProp: function(tagDesc, attr, content) {
|
|
var prop = this.template.tag(tagDesc, attr, content);
|
|
return this._props.indexOf(prop) !== -1;
|
|
},
|
|
|
|
_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')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
var Propfind = ns.require('request/propfind');
|
|
var Builder = ns.require('query_builder');
|
|
|
|
/**
|
|
* Creates a calendar query request.
|
|
*
|
|
* Defaults to Depth of 1.
|
|
*
|
|
* @param {CalDav.Connection} connection connection object.
|
|
* @param {Object} options options for calendar query.
|
|
*/
|
|
function CalendarQuery(connection, options) {
|
|
Propfind.apply(this, arguments);
|
|
|
|
this.xhr.headers['Depth'] = this.depth || 1;
|
|
this.xhr.method = 'REPORT';
|
|
|
|
this.template.rootTag = ['caldav', 'calendar-query'];
|
|
|
|
this.data = new Builder({
|
|
template: this.template
|
|
});
|
|
|
|
this.filter = new Builder({
|
|
template: this.template,
|
|
tag: ['caldav', 'filter'],
|
|
propTag: ['caldav', 'prop-filter'],
|
|
compTag: ['caldav', 'comp-filter']
|
|
});
|
|
}
|
|
|
|
CalendarQuery.prototype = {
|
|
__proto__: Propfind.prototype,
|
|
|
|
_createPayload: function() {
|
|
var content;
|
|
var props;
|
|
|
|
props = this._props.join('');
|
|
|
|
if (this.data) {
|
|
props += this.data.toString();
|
|
}
|
|
|
|
content = this.template.tag('prop', props);
|
|
|
|
if (this.filter) {
|
|
content += this.filter.toString();
|
|
}
|
|
|
|
return this.template.render(content);
|
|
}
|
|
|
|
};
|
|
|
|
module.exports = CalendarQuery;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('request/calendar_query'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
var RequestErrors = ns.require('errors');
|
|
|
|
/**
|
|
* Creates a propfind request.
|
|
*
|
|
* @param {Caldav.Connection} connection connection details.
|
|
* @param {Object} options options for propfind.
|
|
*/
|
|
function CalendarHome(connection, options) {
|
|
var key;
|
|
|
|
if (typeof(options) === 'undefined') {
|
|
options = {};
|
|
}
|
|
|
|
for (key in options) {
|
|
if (Object.hasOwnProperty.call(options, key)) {
|
|
this[key] = options[key];
|
|
}
|
|
}
|
|
|
|
this.connection = connection;
|
|
}
|
|
|
|
function findProperty(name, data, single) {
|
|
var url, results = [], prop;
|
|
|
|
for (url in data) {
|
|
if (Object.hasOwnProperty.call(data, url)) {
|
|
if (name in data[url]) {
|
|
prop = data[url][name];
|
|
if (prop.status === '200') {
|
|
results.push(data[url][name].value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!results.length)
|
|
return false;
|
|
|
|
if (typeof(single) !== 'undefined' && single) {
|
|
return results[0];
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
CalendarHome.prototype = {
|
|
|
|
Propfind: ns.require('request/propfind'),
|
|
|
|
/**
|
|
* @return {Caldav.Xhr} The underlying xhr request so that the caller
|
|
* has a chance to abort the request.
|
|
*/
|
|
_findPrincipal: function(url, callback) {
|
|
var find = new this.Propfind(this.connection, {
|
|
url: url
|
|
});
|
|
|
|
find.prop('current-user-principal');
|
|
find.prop('principal-URL');
|
|
|
|
return find.send(function(err, data) {
|
|
var principal;
|
|
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
// some fairly dumb allowances
|
|
principal =
|
|
findProperty('current-user-principal', data, true) ||
|
|
findProperty('principal-URL', data, true);
|
|
|
|
if (!principal) {
|
|
return callback(new Errors.InvalidEntrypoint(
|
|
'both current-user-principal and principal-URL are missing'
|
|
));
|
|
}
|
|
|
|
// per http://tools.ietf.org/html/rfc6638 we get unauthenticated
|
|
if ('unauthenticated' in principal) {
|
|
return callback(
|
|
new Errors.Authentication('caldav response is unauthenticated')
|
|
);
|
|
}
|
|
|
|
// we might have both principal.href & unauthenticated
|
|
if (principal.href) {
|
|
return callback(null, principal.href);
|
|
}
|
|
|
|
callback(
|
|
new Errors.InvalidEntrypoint('no useful location information found')
|
|
);
|
|
});
|
|
},
|
|
|
|
_findCalendarHome: function(url, callback) {
|
|
var details = {};
|
|
var find = new this.Propfind(this.connection, {
|
|
url: url
|
|
});
|
|
|
|
find.prop(['caldav', 'calendar-home-set']);
|
|
|
|
return find.send(function(err, data) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
details = {
|
|
url: findProperty('calendar-home-set', data, true)
|
|
};
|
|
|
|
callback(null, details);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Starts request to find calendar home url
|
|
*
|
|
* @param {Function} callback node style where second argument
|
|
* are the details of the home calendar.
|
|
* @return {Caldav.Xhr} The underlying xhr request so that the caller
|
|
* has a chance to abort the request.
|
|
*/
|
|
send: function(callback) {
|
|
var self = this;
|
|
return self._findPrincipal(self.url, function(err, url) {
|
|
|
|
if (!url) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
self._findCalendarHome(url, function(err, details) {
|
|
callback(err, details);
|
|
});
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
module.exports = CalendarHome;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('request/calendar_home'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
var Propfind = ns.require('request/propfind');
|
|
|
|
function Resources(connection, options) {
|
|
Propfind.apply(this, arguments);
|
|
|
|
this._resources = {};
|
|
this.depth = 1;
|
|
}
|
|
|
|
Resources.prototype = {
|
|
__proto__: Propfind.prototype,
|
|
|
|
addResource: function(name, handler) {
|
|
this._resources[name] = handler;
|
|
},
|
|
|
|
_processResult: function(req, callback) {
|
|
var results = {};
|
|
var url;
|
|
var root;
|
|
var collection;
|
|
var self = this;
|
|
var resources;
|
|
|
|
if ('multistatus' in this.sax.root) {
|
|
root = this.sax.root.multistatus;
|
|
|
|
for (url in root) {
|
|
collection = root[url];
|
|
resources = collection.resourcetype;
|
|
if (resources.value.forEach) {
|
|
|
|
resources.value.forEach(function(type) {
|
|
if (type in self._resources) {
|
|
|
|
if (!(type in results)) {
|
|
results[type] = {};
|
|
}
|
|
|
|
results[type][url] = new self._resources[type](
|
|
self.connection,
|
|
collection
|
|
);
|
|
|
|
results[type][url].url = url;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
callback(null, results, req);
|
|
|
|
} else {
|
|
//XXX: Improve error handling
|
|
callback(
|
|
new Error('unexpected xml result'),
|
|
this.sax.root, req
|
|
);
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
module.exports = Resources;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('request/resources'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
module.exports = {
|
|
BasicAuth: ns.require('http/basic_auth'),
|
|
OAuth2: ns.require('http/oauth2')
|
|
};
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('http'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
|
|
(function(module, ns) {
|
|
|
|
module.exports = {
|
|
Abstract: ns.require('request/abstract'),
|
|
CalendarQuery: ns.require('request/calendar_query'),
|
|
Propfind: ns.require('request/propfind'),
|
|
CalendarHome: ns.require('request/calendar_home'),
|
|
Resources: ns.require('request/resources'),
|
|
Asset: ns.require('request/asset')
|
|
};
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('request'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
module.exports = {
|
|
Base: ns.require('sax/base'),
|
|
CalendarDataHandler: ns.require('sax/calendar_data_handler'),
|
|
DavResponse: ns.require('sax/dav_response')
|
|
};
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('sax'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
|
|
/**
|
|
@namespace
|
|
*/
|
|
(function(module, ns) {
|
|
|
|
var CalendarQuery = ns.require('request/calendar_query');
|
|
|
|
/**
|
|
* Represents a (Web/Cal)Dav resource type.
|
|
*
|
|
* @param {Caldav.Connection} connection connection details.
|
|
* @param {Object} options public options on prototype.
|
|
*/
|
|
function Calendar(connection, options) {
|
|
if (typeof(options) === 'undefined') {
|
|
options = {};
|
|
}
|
|
|
|
if (options.url) {
|
|
this.url = options.url;
|
|
}
|
|
|
|
this.connection = connection;
|
|
this.updateFromServer(options);
|
|
}
|
|
|
|
Calendar.prototype = {
|
|
|
|
_map: {
|
|
'displayname': 'name',
|
|
|
|
'calendar-color': 'color',
|
|
|
|
'calendar-description': 'description',
|
|
|
|
'getctag': 'ctag',
|
|
'getetag': 'etag',
|
|
'getlastmodified': 'lastmodified',
|
|
|
|
'resourcetype': {
|
|
field: 'resourcetype',
|
|
defaults: []
|
|
},
|
|
|
|
'current-user-privilege-set': {
|
|
field: 'privilegeSet',
|
|
defaults: []
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
* location of calendar resource
|
|
*/
|
|
url: null,
|
|
|
|
/**
|
|
* displayname as defined by webdav spec
|
|
* Maps to: displayname
|
|
*/
|
|
name: null,
|
|
|
|
/**
|
|
* color of calendar as defined by ical spec
|
|
* Maps to: calendar-color
|
|
*/
|
|
color: null,
|
|
|
|
/**
|
|
* description of calendar as described by caldav spec
|
|
* Maps to: calendar-description
|
|
*/
|
|
description: null,
|
|
|
|
/**
|
|
* change tag (as defined by calendarserver spec)
|
|
* used to determine if a change has occurred to this
|
|
* calendar resource.
|
|
*
|
|
* Maps to: getctag
|
|
*/
|
|
ctag: null,
|
|
|
|
/**
|
|
* Resource types of this resource will
|
|
* always contain 'calendar'
|
|
*
|
|
* Maps to: resourcetype
|
|
*
|
|
* @type Array
|
|
*/
|
|
resourcetype: null,
|
|
|
|
/**
|
|
* Set of privileges available to the user.
|
|
*
|
|
* Maps to: current-user-privilege-set
|
|
*/
|
|
privilegeSet: null,
|
|
|
|
/**
|
|
* Updates calendar details from server.
|
|
*/
|
|
updateFromServer: function(options) {
|
|
var key;
|
|
var defaultTo;
|
|
var mapName;
|
|
var value;
|
|
var descriptor;
|
|
|
|
if (typeof(options) === 'undefined') {
|
|
options = {};
|
|
}
|
|
|
|
for (key in options) {
|
|
if (Object.hasOwnProperty.call(options, key)) {
|
|
if (key in this._map) {
|
|
descriptor = this._map[key];
|
|
value = options[key];
|
|
|
|
if (typeof(descriptor) === 'object') {
|
|
defaultTo = descriptor.defaults;
|
|
mapName = descriptor.field;
|
|
} else {
|
|
defaultTo = '';
|
|
mapName = descriptor;
|
|
}
|
|
|
|
if (value.status !== '200') {
|
|
this[mapName] = defaultTo;
|
|
} else {
|
|
this[mapName] = value.value;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Creates a query request for this calendar resource.
|
|
*
|
|
* @return {CalDav.Request.CalendarQuery} query object.
|
|
*/
|
|
createQuery: function() {
|
|
return new CalendarQuery(this.connection, {
|
|
url: this.url
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
module.exports = Calendar;
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('resources/calendar'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
(function(module, ns) {
|
|
|
|
module.exports = {
|
|
Calendar: ns.require('resources/calendar')
|
|
};
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav('resources'), Caldav] :
|
|
[module, require('../caldav')]
|
|
));
|
|
|
|
(function(module, ns) {
|
|
|
|
var exports = module.exports;
|
|
|
|
exports.Responder = ns.require('responder');
|
|
exports.Sax = ns.require('sax');
|
|
exports.Template = ns.require('template');
|
|
exports.QueryBuilder = ns.require('query_builder');
|
|
exports.Xhr = ns.require('xhr');
|
|
exports.Request = ns.require('request');
|
|
exports.Connection = ns.require('connection');
|
|
exports.Resources = ns.require('resources');
|
|
exports.Http = ns.require('http');
|
|
exports.OAuth2 = ns.require('oauth2');
|
|
exports.Errors = ns.require('errors');
|
|
|
|
}.apply(
|
|
this,
|
|
(this.Caldav) ?
|
|
[Caldav, Caldav] :
|
|
[module, require('./caldav')]
|
|
));
|
|
|
|
define("ext/caldav", (function (global) {
|
|
return function () {
|
|
var ret, fn;
|
|
return ret || global.Caldav;
|
|
};
|
|
}(this)));
|
|
|