Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
3329 lines
99 KiB
JavaScript
3329 lines
99 KiB
JavaScript
/**
|
|
* alameda 0.2.0 Copyright (c) 2011-2014, The Dojo Foundation All Rights Reserved.
|
|
* Available via the MIT or new BSD license.
|
|
* see: http://github.com/requirejs/alameda for details
|
|
*/
|
|
//Going sloppy to avoid 'use strict' string cost, but strict practices should
|
|
//be followed.
|
|
/*jslint sloppy: true, nomen: true, regexp: true */
|
|
/*global setTimeout, process, document, navigator, importScripts,
|
|
setImmediate */
|
|
|
|
var requirejs, require, define;
|
|
(function (global, undef) {
|
|
var prim, topReq, dataMain, src, subPath,
|
|
bootstrapConfig = requirejs || require,
|
|
hasOwn = Object.prototype.hasOwnProperty,
|
|
contexts = {},
|
|
queue = [],
|
|
currDirRegExp = /^\.\//,
|
|
urlRegExp = /^\/|\:|\?|\.js$/,
|
|
commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
|
|
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
|
|
jsSuffixRegExp = /\.js$/;
|
|
|
|
if (typeof requirejs === 'function') {
|
|
return;
|
|
}
|
|
|
|
function hasProp(obj, prop) {
|
|
return hasOwn.call(obj, prop);
|
|
}
|
|
|
|
function getOwn(obj, prop) {
|
|
return obj && hasProp(obj, prop) && obj[prop];
|
|
}
|
|
|
|
/**
|
|
* Cycles over properties in an object and calls a function for each
|
|
* property value. If the function returns a truthy value, then the
|
|
* iteration is stopped.
|
|
*/
|
|
function eachProp(obj, func) {
|
|
var prop;
|
|
for (prop in obj) {
|
|
if (hasProp(obj, prop)) {
|
|
if (func(obj[prop], prop)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simple function to mix in properties from source into target,
|
|
* but only if target does not already have a property of the same name.
|
|
*/
|
|
function mixin(target, source, force, deepStringMixin) {
|
|
if (source) {
|
|
eachProp(source, function (value, prop) {
|
|
if (force || !hasProp(target, prop)) {
|
|
if (deepStringMixin && typeof value === 'object' && value &&
|
|
!Array.isArray(value) && typeof value !== 'function' &&
|
|
!(value instanceof RegExp)) {
|
|
|
|
if (!target[prop]) {
|
|
target[prop] = {};
|
|
}
|
|
mixin(target[prop], value, force, deepStringMixin);
|
|
} else {
|
|
target[prop] = value;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
return target;
|
|
}
|
|
|
|
//Allow getting a global that expressed in
|
|
//dot notation, like 'a.b.c'.
|
|
function getGlobal(value) {
|
|
if (!value) {
|
|
return value;
|
|
}
|
|
var g = global;
|
|
value.split('.').forEach(function (part) {
|
|
g = g[part];
|
|
});
|
|
return g;
|
|
}
|
|
|
|
//START prim 0.0.6
|
|
/**
|
|
* Changes from baseline prim
|
|
* - removed UMD registration
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
var waitingId, nextTick,
|
|
waiting = [];
|
|
|
|
function callWaiting() {
|
|
waitingId = 0;
|
|
var w = waiting;
|
|
waiting = [];
|
|
while (w.length) {
|
|
w.shift()();
|
|
}
|
|
}
|
|
|
|
function asyncTick(fn) {
|
|
waiting.push(fn);
|
|
if (!waitingId) {
|
|
waitingId = setTimeout(callWaiting, 0);
|
|
}
|
|
}
|
|
|
|
function syncTick(fn) {
|
|
fn();
|
|
}
|
|
|
|
function isFunObj(x) {
|
|
var type = typeof x;
|
|
return type === 'object' || type === 'function';
|
|
}
|
|
|
|
//Use setImmediate.bind() because attaching it (or setTimeout directly
|
|
//to prim will result in errors. Noticed first on IE10,
|
|
//issue requirejs/alameda#2)
|
|
nextTick = typeof setImmediate === 'function' ? setImmediate.bind() :
|
|
(typeof process !== 'undefined' && process.nextTick ?
|
|
process.nextTick : (typeof setTimeout !== 'undefined' ?
|
|
asyncTick : syncTick));
|
|
|
|
function notify(ary, value) {
|
|
prim.nextTick(function () {
|
|
ary.forEach(function (item) {
|
|
item(value);
|
|
});
|
|
});
|
|
}
|
|
|
|
function callback(p, ok, yes) {
|
|
if (p.hasOwnProperty('v')) {
|
|
prim.nextTick(function () {
|
|
yes(p.v);
|
|
});
|
|
} else {
|
|
ok.push(yes);
|
|
}
|
|
}
|
|
|
|
function errback(p, fail, no) {
|
|
if (p.hasOwnProperty('e')) {
|
|
prim.nextTick(function () {
|
|
no(p.e);
|
|
});
|
|
} else {
|
|
fail.push(no);
|
|
}
|
|
}
|
|
|
|
prim = function prim(fn) {
|
|
var promise, f,
|
|
p = {},
|
|
ok = [],
|
|
fail = [];
|
|
|
|
function makeFulfill() {
|
|
var f, f2,
|
|
called = false;
|
|
|
|
function fulfill(v, prop, listeners) {
|
|
if (called) {
|
|
return;
|
|
}
|
|
called = true;
|
|
|
|
if (promise === v) {
|
|
called = false;
|
|
f.reject(new TypeError('value is same promise'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var then = v && v.then;
|
|
if (isFunObj(v) && typeof then === 'function') {
|
|
f2 = makeFulfill();
|
|
then.call(v, f2.resolve, f2.reject);
|
|
} else {
|
|
p[prop] = v;
|
|
notify(listeners, v);
|
|
}
|
|
} catch (e) {
|
|
called = false;
|
|
f.reject(e);
|
|
}
|
|
}
|
|
|
|
f = {
|
|
resolve: function (v) {
|
|
fulfill(v, 'v', ok);
|
|
},
|
|
reject: function(e) {
|
|
fulfill(e, 'e', fail);
|
|
}
|
|
};
|
|
return f;
|
|
}
|
|
|
|
f = makeFulfill();
|
|
|
|
promise = {
|
|
then: function (yes, no) {
|
|
var next = prim(function (nextResolve, nextReject) {
|
|
|
|
function finish(fn, nextFn, v) {
|
|
try {
|
|
if (fn && typeof fn === 'function') {
|
|
v = fn(v);
|
|
nextResolve(v);
|
|
} else {
|
|
nextFn(v);
|
|
}
|
|
} catch (e) {
|
|
nextReject(e);
|
|
}
|
|
}
|
|
|
|
callback(p, ok, finish.bind(undefined, yes, nextResolve));
|
|
errback(p, fail, finish.bind(undefined, no, nextReject));
|
|
|
|
});
|
|
return next;
|
|
},
|
|
|
|
catch: function (no) {
|
|
return promise.then(null, no);
|
|
}
|
|
};
|
|
|
|
try {
|
|
fn(f.resolve, f.reject);
|
|
} catch (e) {
|
|
f.reject(e);
|
|
}
|
|
|
|
return promise;
|
|
};
|
|
|
|
prim.resolve = function (value) {
|
|
return prim(function (yes) {
|
|
yes(value);
|
|
});
|
|
};
|
|
|
|
prim.reject = function (err) {
|
|
return prim(function (yes, no) {
|
|
no(err);
|
|
});
|
|
};
|
|
|
|
prim.cast = function (x) {
|
|
// A bit of a weak check, want "then" to be a function,
|
|
// but also do not want to trigger a getter if accessing
|
|
// it. Good enough for now.
|
|
if (isFunObj(x) && 'then' in x) {
|
|
return x;
|
|
} else {
|
|
return prim(function (yes, no) {
|
|
if (x instanceof Error) {
|
|
no(x);
|
|
} else {
|
|
yes(x);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
prim.all = function (ary) {
|
|
return prim(function (yes, no) {
|
|
var count = 0,
|
|
length = ary.length,
|
|
result = [];
|
|
|
|
function resolved(i, v) {
|
|
result[i] = v;
|
|
count += 1;
|
|
if (count === length) {
|
|
yes(result);
|
|
}
|
|
}
|
|
|
|
ary.forEach(function (item, i) {
|
|
prim.cast(item).then(function (v) {
|
|
resolved(i, v);
|
|
}, function (err) {
|
|
no(err);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
prim.nextTick = nextTick;
|
|
}());
|
|
//END prim
|
|
|
|
function newContext(contextName) {
|
|
var req, main, makeMap, callDep, handlers, checkingLater, load, context,
|
|
defined = {},
|
|
waiting = {},
|
|
config = {
|
|
//Defaults. Do not set a default for map
|
|
//config to speed up normalize(), which
|
|
//will run faster if there is no default.
|
|
waitSeconds: 7,
|
|
baseUrl: './',
|
|
paths: {},
|
|
bundles: {},
|
|
pkgs: {},
|
|
shim: {},
|
|
config: {}
|
|
},
|
|
mapCache = {},
|
|
requireDeferreds = [],
|
|
deferreds = {},
|
|
calledDefine = {},
|
|
calledPlugin = {},
|
|
loadCount = 0,
|
|
startTime = (new Date()).getTime(),
|
|
errCount = 0,
|
|
trackedErrors = {},
|
|
urlFetched = {},
|
|
bundlesMap = {};
|
|
|
|
/**
|
|
* Trims the . and .. from an array of path segments.
|
|
* It will keep a leading path segment if a .. will become
|
|
* the first path segment, to help with module name lookups,
|
|
* which act like paths, but can be remapped. But the end result,
|
|
* all paths that use this function should look normalized.
|
|
* NOTE: this method MODIFIES the input array.
|
|
* @param {Array} ary the array of path segments.
|
|
*/
|
|
function trimDots(ary) {
|
|
var i, part, length = ary.length;
|
|
for (i = 0; i < length; i++) {
|
|
part = ary[i];
|
|
if (part === '.') {
|
|
ary.splice(i, 1);
|
|
i -= 1;
|
|
} else if (part === '..') {
|
|
if (i === 1 && (ary[2] === '..' || ary[0] === '..')) {
|
|
//End of the line. Keep at least one non-dot
|
|
//path segment at the front so it can be mapped
|
|
//correctly to disk. Otherwise, there is likely
|
|
//no path mapping for a path starting with '..'.
|
|
//This can still fail, but catches the most reasonable
|
|
//uses of ..
|
|
break;
|
|
} else if (i > 0) {
|
|
ary.splice(i - 1, 2);
|
|
i -= 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a relative module name, like ./something, normalize it to
|
|
* a real name that can be mapped to a path.
|
|
* @param {String} name the relative name
|
|
* @param {String} baseName a real name that the name arg is relative
|
|
* to.
|
|
* @param {Boolean} applyMap apply the map config to the value. Should
|
|
* only be done if this normalization is for a dependency ID.
|
|
* @returns {String} normalized name
|
|
*/
|
|
function normalize(name, baseName, applyMap) {
|
|
var pkgMain, mapValue, nameParts, i, j, nameSegment, lastIndex,
|
|
foundMap, foundI, foundStarMap, starI,
|
|
baseParts = baseName && baseName.split('/'),
|
|
normalizedBaseParts = baseParts,
|
|
map = config.map,
|
|
starMap = map && map['*'];
|
|
|
|
//Adjust any relative paths.
|
|
if (name && name.charAt(0) === '.') {
|
|
//If have a base name, try to normalize against it,
|
|
//otherwise, assume it is a top-level require that will
|
|
//be relative to baseUrl in the end.
|
|
if (baseName) {
|
|
//Convert baseName to array, and lop off the last part,
|
|
//so that . matches that 'directory' and not name of the baseName's
|
|
//module. For instance, baseName of 'one/two/three', maps to
|
|
//'one/two/three.js', but we want the directory, 'one/two' for
|
|
//this normalization.
|
|
normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
|
|
name = name.split('/');
|
|
lastIndex = name.length - 1;
|
|
|
|
// If wanting node ID compatibility, strip .js from end
|
|
// of IDs. Have to do this here, and not in nameToUrl
|
|
// because node allows either .js or non .js to map
|
|
// to same file.
|
|
if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
|
|
name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
|
|
}
|
|
|
|
name = normalizedBaseParts.concat(name);
|
|
trimDots(name);
|
|
name = name.join('/');
|
|
} else if (name.indexOf('./') === 0) {
|
|
// No baseName, so this is ID is resolved relative
|
|
// to baseUrl, pull off the leading dot.
|
|
name = name.substring(2);
|
|
}
|
|
}
|
|
|
|
//Apply map config if available.
|
|
if (applyMap && map && (baseParts || starMap)) {
|
|
nameParts = name.split('/');
|
|
|
|
outerLoop: for (i = nameParts.length; i > 0; i -= 1) {
|
|
nameSegment = nameParts.slice(0, i).join('/');
|
|
|
|
if (baseParts) {
|
|
//Find the longest baseName segment match in the config.
|
|
//So, do joins on the biggest to smallest lengths of baseParts.
|
|
for (j = baseParts.length; j > 0; j -= 1) {
|
|
mapValue = getOwn(map, baseParts.slice(0, j).join('/'));
|
|
|
|
//baseName segment has config, find if it has one for
|
|
//this name.
|
|
if (mapValue) {
|
|
mapValue = getOwn(mapValue, nameSegment);
|
|
if (mapValue) {
|
|
//Match, update name to the new value.
|
|
foundMap = mapValue;
|
|
foundI = i;
|
|
break outerLoop;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Check for a star map match, but just hold on to it,
|
|
//if there is a shorter segment match later in a matching
|
|
//config, then favor over this star map.
|
|
if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) {
|
|
foundStarMap = getOwn(starMap, nameSegment);
|
|
starI = i;
|
|
}
|
|
}
|
|
|
|
if (!foundMap && foundStarMap) {
|
|
foundMap = foundStarMap;
|
|
foundI = starI;
|
|
}
|
|
|
|
if (foundMap) {
|
|
nameParts.splice(0, foundI, foundMap);
|
|
name = nameParts.join('/');
|
|
}
|
|
}
|
|
|
|
// If the name points to a package's name, use
|
|
// the package main instead.
|
|
pkgMain = getOwn(config.pkgs, name);
|
|
|
|
return pkgMain ? pkgMain : name;
|
|
}
|
|
|
|
function makeShimExports(value) {
|
|
function fn() {
|
|
var ret;
|
|
if (value.init) {
|
|
ret = value.init.apply(global, arguments);
|
|
}
|
|
return ret || (value.exports && getGlobal(value.exports));
|
|
}
|
|
return fn;
|
|
}
|
|
|
|
function takeQueue(anonId) {
|
|
var i, id, args, shim;
|
|
for (i = 0; i < queue.length; i += 1) {
|
|
//Peek to see if anon
|
|
if (typeof queue[i][0] !== 'string') {
|
|
if (anonId) {
|
|
queue[i].unshift(anonId);
|
|
anonId = undef;
|
|
} else {
|
|
//Not our anon module, stop.
|
|
break;
|
|
}
|
|
}
|
|
args = queue.shift();
|
|
id = args[0];
|
|
i -= 1;
|
|
|
|
if (!hasProp(defined, id) && !hasProp(waiting, id)) {
|
|
if (hasProp(deferreds, id)) {
|
|
main.apply(undef, args);
|
|
} else {
|
|
waiting[id] = args;
|
|
}
|
|
}
|
|
}
|
|
|
|
//if get to the end and still have anonId, then could be
|
|
//a shimmed dependency.
|
|
if (anonId) {
|
|
shim = getOwn(config.shim, anonId) || {};
|
|
main(anonId, shim.deps || [], shim.exportsFn);
|
|
}
|
|
}
|
|
|
|
function makeRequire(relName, topLevel) {
|
|
var req = function (deps, callback, errback, alt) {
|
|
var name, cfg;
|
|
|
|
if (topLevel) {
|
|
takeQueue();
|
|
}
|
|
|
|
if (typeof deps === "string") {
|
|
if (handlers[deps]) {
|
|
return handlers[deps](relName);
|
|
}
|
|
//Just return the module wanted. In this scenario, the
|
|
//deps arg is the module name, and second arg (if passed)
|
|
//is just the relName.
|
|
//Normalize module name, if it contains . or ..
|
|
name = makeMap(deps, relName, true).id;
|
|
if (!hasProp(defined, name)) {
|
|
throw new Error('Not loaded: ' + name);
|
|
}
|
|
return defined[name];
|
|
} else if (deps && !Array.isArray(deps)) {
|
|
//deps is a config object, not an array.
|
|
cfg = deps;
|
|
deps = undef;
|
|
|
|
if (Array.isArray(callback)) {
|
|
//callback is an array, which means it is a dependency list.
|
|
//Adjust args if there are dependencies
|
|
deps = callback;
|
|
callback = errback;
|
|
errback = alt;
|
|
}
|
|
|
|
if (topLevel) {
|
|
//Could be a new context, so call returned require
|
|
return req.config(cfg)(deps, callback, errback);
|
|
}
|
|
}
|
|
|
|
//Support require(['a'])
|
|
callback = callback || function () {};
|
|
|
|
//Simulate async callback;
|
|
prim.nextTick(function () {
|
|
//Grab any modules that were defined after a
|
|
//require call.
|
|
takeQueue();
|
|
main(undef, deps || [], callback, errback, relName);
|
|
});
|
|
|
|
return req;
|
|
};
|
|
|
|
req.isBrowser = typeof document !== 'undefined' &&
|
|
typeof navigator !== 'undefined';
|
|
|
|
req.nameToUrl = function (moduleName, ext, skipExt) {
|
|
var paths, syms, i, parentModule, url,
|
|
parentPath, bundleId,
|
|
pkgMain = getOwn(config.pkgs, moduleName);
|
|
|
|
if (pkgMain) {
|
|
moduleName = pkgMain;
|
|
}
|
|
|
|
bundleId = getOwn(bundlesMap, moduleName);
|
|
|
|
if (bundleId) {
|
|
return req.nameToUrl(bundleId, ext, skipExt);
|
|
}
|
|
|
|
//If a colon is in the URL, it indicates a protocol is used and it is just
|
|
//an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?)
|
|
//or ends with .js, then assume the user meant to use an url and not a module id.
|
|
//The slash is important for protocol-less URLs as well as full paths.
|
|
if (urlRegExp.test(moduleName)) {
|
|
//Just a plain path, not module name lookup, so just return it.
|
|
//Add extension if it is included. This is a bit wonky, only non-.js things pass
|
|
//an extension, this method probably needs to be reworked.
|
|
url = moduleName + (ext || '');
|
|
} else {
|
|
//A module that needs to be converted to a path.
|
|
paths = config.paths;
|
|
|
|
syms = moduleName.split('/');
|
|
//For each module name segment, see if there is a path
|
|
//registered for it. Start with most specific name
|
|
//and work up from it.
|
|
for (i = syms.length; i > 0; i -= 1) {
|
|
parentModule = syms.slice(0, i).join('/');
|
|
|
|
parentPath = getOwn(paths, parentModule);
|
|
if (parentPath) {
|
|
//If an array, it means there are a few choices,
|
|
//Choose the one that is desired
|
|
if (Array.isArray(parentPath)) {
|
|
parentPath = parentPath[0];
|
|
}
|
|
syms.splice(0, i, parentPath);
|
|
break;
|
|
}
|
|
}
|
|
|
|
//Join the path parts together, then figure out if baseUrl is needed.
|
|
url = syms.join('/');
|
|
url += (ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js'));
|
|
url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url;
|
|
}
|
|
|
|
return config.urlArgs ? url +
|
|
((url.indexOf('?') === -1 ? '?' : '&') +
|
|
config.urlArgs) : url;
|
|
};
|
|
|
|
/**
|
|
* Converts a module name + .extension into an URL path.
|
|
* *Requires* the use of a module name. It does not support using
|
|
* plain URLs like nameToUrl.
|
|
*/
|
|
req.toUrl = function (moduleNamePlusExt) {
|
|
var ext,
|
|
index = moduleNamePlusExt.lastIndexOf('.'),
|
|
segment = moduleNamePlusExt.split('/')[0],
|
|
isRelative = segment === '.' || segment === '..';
|
|
|
|
//Have a file extension alias, and it is not the
|
|
//dots from a relative path.
|
|
if (index !== -1 && (!isRelative || index > 1)) {
|
|
ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length);
|
|
moduleNamePlusExt = moduleNamePlusExt.substring(0, index);
|
|
}
|
|
|
|
return req.nameToUrl(normalize(moduleNamePlusExt, relName), ext, true);
|
|
};
|
|
|
|
req.defined = function (id) {
|
|
return hasProp(defined, makeMap(id, relName, true).id);
|
|
};
|
|
|
|
req.specified = function (id) {
|
|
id = makeMap(id, relName, true).id;
|
|
return hasProp(defined, id) || hasProp(deferreds, id);
|
|
};
|
|
|
|
return req;
|
|
}
|
|
|
|
function resolve(name, d, value) {
|
|
if (name) {
|
|
defined[name] = value;
|
|
if (requirejs.onResourceLoad) {
|
|
requirejs.onResourceLoad(context, d.map, d.deps);
|
|
}
|
|
}
|
|
d.finished = true;
|
|
d.resolve(value);
|
|
}
|
|
|
|
function reject(d, err) {
|
|
d.finished = true;
|
|
d.rejected = true;
|
|
d.reject(err);
|
|
}
|
|
|
|
function makeNormalize(relName) {
|
|
return function (name) {
|
|
return normalize(name, relName, true);
|
|
};
|
|
}
|
|
|
|
function defineModule(d) {
|
|
var name = d.map.id,
|
|
ret = d.factory.apply(defined[name], d.values);
|
|
|
|
if (name) {
|
|
// Favor return value over exports. If node/cjs in play,
|
|
// then will not have a return value anyway. Favor
|
|
// module.exports assignment over exports object.
|
|
if (ret === undef) {
|
|
if (d.cjsModule) {
|
|
ret = d.cjsModule.exports;
|
|
} else if (d.usingExports) {
|
|
ret = defined[name];
|
|
}
|
|
}
|
|
} else {
|
|
//Remove the require deferred from the list to
|
|
//make cycle searching faster. Do not need to track
|
|
//it anymore either.
|
|
requireDeferreds.splice(requireDeferreds.indexOf(d), 1);
|
|
}
|
|
resolve(name, d, ret);
|
|
}
|
|
|
|
//This method is attached to every module deferred,
|
|
//so the "this" in here is the module deferred object.
|
|
function depFinished(val, i) {
|
|
if (!this.rejected && !this.depDefined[i]) {
|
|
this.depDefined[i] = true;
|
|
this.depCount += 1;
|
|
this.values[i] = val;
|
|
if (!this.depending && this.depCount === this.depMax) {
|
|
defineModule(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
function makeDefer(name) {
|
|
var d = {};
|
|
d.promise = prim(function (resolve, reject) {
|
|
d.resolve = resolve;
|
|
d.reject = reject;
|
|
});
|
|
d.map = name ? makeMap(name, null, true) : {};
|
|
d.depCount = 0;
|
|
d.depMax = 0;
|
|
d.values = [];
|
|
d.depDefined = [];
|
|
d.depFinished = depFinished;
|
|
if (d.map.pr) {
|
|
//Plugin resource ID, implicitly
|
|
//depends on plugin. Track it in deps
|
|
//so cycle breaking can work
|
|
d.deps = [makeMap(d.map.pr)];
|
|
}
|
|
return d;
|
|
}
|
|
|
|
function getDefer(name) {
|
|
var d;
|
|
if (name) {
|
|
d = hasProp(deferreds, name) && deferreds[name];
|
|
if (!d) {
|
|
d = deferreds[name] = makeDefer(name);
|
|
}
|
|
} else {
|
|
d = makeDefer();
|
|
requireDeferreds.push(d);
|
|
}
|
|
return d;
|
|
}
|
|
|
|
function makeErrback(d, name) {
|
|
return function (err) {
|
|
if (!d.rejected) {
|
|
if (!err.dynaId) {
|
|
err.dynaId = 'id' + (errCount += 1);
|
|
err.requireModules = [name];
|
|
}
|
|
reject(d, err);
|
|
}
|
|
};
|
|
}
|
|
|
|
function waitForDep(depMap, relName, d, i) {
|
|
d.depMax += 1;
|
|
|
|
//Do the fail at the end to catch errors
|
|
//in the then callback execution.
|
|
callDep(depMap, relName).then(function (val) {
|
|
d.depFinished(val, i);
|
|
}, makeErrback(d, depMap.id)).catch(makeErrback(d, d.map.id));
|
|
}
|
|
|
|
function makeLoad(id) {
|
|
var fromTextCalled;
|
|
function load(value) {
|
|
//Protect against older plugins that call load after
|
|
//calling load.fromText
|
|
if (!fromTextCalled) {
|
|
resolve(id, getDefer(id), value);
|
|
}
|
|
}
|
|
|
|
load.error = function (err) {
|
|
getDefer(id).reject(err);
|
|
};
|
|
|
|
load.fromText = function (text, textAlt) {
|
|
/*jslint evil: true */
|
|
var d = getDefer(id),
|
|
map = makeMap(makeMap(id).n),
|
|
plainId = map.id;
|
|
|
|
fromTextCalled = true;
|
|
|
|
//Set up the factory just to be a return of the value from
|
|
//plainId.
|
|
d.factory = function (p, val) {
|
|
return val;
|
|
};
|
|
|
|
//As of requirejs 2.1.0, support just passing the text, to reinforce
|
|
//fromText only being called once per resource. Still
|
|
//support old style of passing moduleName but discard
|
|
//that moduleName in favor of the internal ref.
|
|
if (textAlt) {
|
|
text = textAlt;
|
|
}
|
|
|
|
//Transfer any config to this other module.
|
|
if (hasProp(config.config, id)) {
|
|
config.config[plainId] = config.config[id];
|
|
}
|
|
|
|
try {
|
|
req.exec(text);
|
|
} catch (e) {
|
|
reject(d, new Error('fromText eval for ' + plainId +
|
|
' failed: ' + e));
|
|
}
|
|
|
|
//Execute any waiting define created by the plainId
|
|
takeQueue(plainId);
|
|
|
|
//Mark this as a dependency for the plugin
|
|
//resource
|
|
d.deps = [map];
|
|
waitForDep(map, null, d, d.deps.length);
|
|
};
|
|
|
|
return load;
|
|
}
|
|
|
|
load = typeof importScripts === 'function' ?
|
|
function (map) {
|
|
var url = map.url;
|
|
if (urlFetched[url]) {
|
|
return;
|
|
}
|
|
urlFetched[url] = true;
|
|
|
|
//Ask for the deferred so loading is triggered.
|
|
//Do this before loading, since loading is sync.
|
|
getDefer(map.id);
|
|
importScripts(url);
|
|
takeQueue(map.id);
|
|
} :
|
|
function (map) {
|
|
var script,
|
|
id = map.id,
|
|
url = map.url;
|
|
|
|
if (urlFetched[url]) {
|
|
return;
|
|
}
|
|
urlFetched[url] = true;
|
|
|
|
script = document.createElement('script');
|
|
script.setAttribute('data-requiremodule', id);
|
|
script.type = config.scriptType || 'text/javascript';
|
|
script.charset = 'utf-8';
|
|
script.async = true;
|
|
|
|
loadCount += 1;
|
|
|
|
script.addEventListener('load', function () {
|
|
loadCount -= 1;
|
|
takeQueue(id);
|
|
}, false);
|
|
script.addEventListener('error', function () {
|
|
loadCount -= 1;
|
|
var err,
|
|
pathConfig = getOwn(config.paths, id),
|
|
d = getOwn(deferreds, id);
|
|
if (pathConfig && Array.isArray(pathConfig) && pathConfig.length > 1) {
|
|
script.parentNode.removeChild(script);
|
|
//Pop off the first array value, since it failed, and
|
|
//retry
|
|
pathConfig.shift();
|
|
d.map = makeMap(id);
|
|
load(d.map);
|
|
} else {
|
|
err = new Error('Load failed: ' + id + ': ' + script.src);
|
|
err.requireModules = [id];
|
|
getDefer(id).reject(err);
|
|
}
|
|
}, false);
|
|
|
|
script.src = url;
|
|
|
|
document.head.appendChild(script);
|
|
};
|
|
|
|
function callPlugin(plugin, map, relName) {
|
|
plugin.load(map.n, makeRequire(relName), makeLoad(map.id), {});
|
|
}
|
|
|
|
callDep = function (map, relName) {
|
|
var args, bundleId,
|
|
name = map.id,
|
|
shim = config.shim[name];
|
|
|
|
if (hasProp(waiting, name)) {
|
|
args = waiting[name];
|
|
delete waiting[name];
|
|
main.apply(undef, args);
|
|
} else if (!hasProp(deferreds, name)) {
|
|
if (map.pr) {
|
|
//If a bundles config, then just load that file instead to
|
|
//resolve the plugin, as it is built into that bundle.
|
|
if ((bundleId = getOwn(bundlesMap, name))) {
|
|
map.url = req.nameToUrl(bundleId);
|
|
load(map);
|
|
} else {
|
|
return callDep(makeMap(map.pr)).then(function (plugin) {
|
|
//Redo map now that plugin is known to be loaded
|
|
var newMap = makeMap(name, relName, true),
|
|
newId = newMap.id,
|
|
shim = getOwn(config.shim, newId);
|
|
|
|
//Make sure to only call load once per resource. Many
|
|
//calls could have been queued waiting for plugin to load.
|
|
if (!hasProp(calledPlugin, newId)) {
|
|
calledPlugin[newId] = true;
|
|
if (shim && shim.deps) {
|
|
req(shim.deps, function () {
|
|
callPlugin(plugin, newMap, relName);
|
|
});
|
|
} else {
|
|
callPlugin(plugin, newMap, relName);
|
|
}
|
|
}
|
|
return getDefer(newId).promise;
|
|
});
|
|
}
|
|
} else if (shim && shim.deps) {
|
|
req(shim.deps, function () {
|
|
load(map);
|
|
});
|
|
} else {
|
|
load(map);
|
|
}
|
|
}
|
|
|
|
return getDefer(name).promise;
|
|
};
|
|
|
|
//Turns a plugin!resource to [plugin, resource]
|
|
//with the plugin being undefined if the name
|
|
//did not have a plugin prefix.
|
|
function splitPrefix(name) {
|
|
var prefix,
|
|
index = name ? name.indexOf('!') : -1;
|
|
if (index > -1) {
|
|
prefix = name.substring(0, index);
|
|
name = name.substring(index + 1, name.length);
|
|
}
|
|
return [prefix, name];
|
|
}
|
|
|
|
/**
|
|
* Makes a name map, normalizing the name, and using a plugin
|
|
* for normalization if necessary. Grabs a ref to plugin
|
|
* too, as an optimization.
|
|
*/
|
|
makeMap = function (name, relName, applyMap) {
|
|
if (typeof name !== 'string') {
|
|
return name;
|
|
}
|
|
|
|
var plugin, url, parts, prefix, result,
|
|
cacheKey = name + ' & ' + (relName || '') + ' & ' + !!applyMap;
|
|
|
|
parts = splitPrefix(name);
|
|
prefix = parts[0];
|
|
name = parts[1];
|
|
|
|
if (!prefix && hasProp(mapCache, cacheKey)) {
|
|
return mapCache[cacheKey];
|
|
}
|
|
|
|
if (prefix) {
|
|
prefix = normalize(prefix, relName, applyMap);
|
|
plugin = hasProp(defined, prefix) && defined[prefix];
|
|
}
|
|
|
|
//Normalize according
|
|
if (prefix) {
|
|
if (plugin && plugin.normalize) {
|
|
name = plugin.normalize(name, makeNormalize(relName));
|
|
} else {
|
|
name = normalize(name, relName, applyMap);
|
|
}
|
|
} else {
|
|
name = normalize(name, relName, applyMap);
|
|
parts = splitPrefix(name);
|
|
prefix = parts[0];
|
|
name = parts[1];
|
|
|
|
url = req.nameToUrl(name);
|
|
}
|
|
|
|
//Using ridiculous property names for space reasons
|
|
result = {
|
|
id: prefix ? prefix + '!' + name : name, //fullName
|
|
n: name,
|
|
pr: prefix,
|
|
url: url
|
|
};
|
|
|
|
if (!prefix) {
|
|
mapCache[cacheKey] = result;
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
handlers = {
|
|
require: function (name) {
|
|
return makeRequire(name);
|
|
},
|
|
exports: function (name) {
|
|
var e = defined[name];
|
|
if (typeof e !== 'undefined') {
|
|
return e;
|
|
} else {
|
|
return (defined[name] = {});
|
|
}
|
|
},
|
|
module: function (name) {
|
|
return {
|
|
id: name,
|
|
uri: '',
|
|
exports: handlers.exports(name),
|
|
config: function () {
|
|
return getOwn(config.config, name) || {};
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
function breakCycle(d, traced, processed) {
|
|
var id = d.map.id;
|
|
|
|
traced[id] = true;
|
|
if (!d.finished && d.deps) {
|
|
d.deps.forEach(function (depMap) {
|
|
var depId = depMap.id,
|
|
dep = !hasProp(handlers, depId) && getDefer(depId);
|
|
|
|
//Only force things that have not completed
|
|
//being defined, so still in the registry,
|
|
//and only if it has not been matched up
|
|
//in the module already.
|
|
if (dep && !dep.finished && !processed[depId]) {
|
|
if (hasProp(traced, depId)) {
|
|
d.deps.forEach(function (depMap, i) {
|
|
if (depMap.id === depId) {
|
|
d.depFinished(defined[depId], i);
|
|
}
|
|
});
|
|
} else {
|
|
breakCycle(dep, traced, processed);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
processed[id] = true;
|
|
}
|
|
|
|
function check(d) {
|
|
var err,
|
|
notFinished = [],
|
|
waitInterval = config.waitSeconds * 1000,
|
|
//It is possible to disable the wait interval by using waitSeconds of 0.
|
|
expired = waitInterval && (startTime + waitInterval) < (new Date()).getTime();
|
|
|
|
if (loadCount === 0) {
|
|
//If passed in a deferred, it is for a specific require call.
|
|
//Could be a sync case that needs resolution right away.
|
|
//Otherwise, if no deferred, means a nextTick and all
|
|
//waiting require deferreds should be checked.
|
|
if (d) {
|
|
if (!d.finished) {
|
|
breakCycle(d, {}, {});
|
|
}
|
|
} else if (requireDeferreds.length) {
|
|
requireDeferreds.forEach(function (d) {
|
|
breakCycle(d, {}, {});
|
|
});
|
|
}
|
|
}
|
|
|
|
//If still waiting on loads, and the waiting load is something
|
|
//other than a plugin resource, or there are still outstanding
|
|
//scripts, then just try back later.
|
|
if (expired) {
|
|
//If wait time expired, throw error of unloaded modules.
|
|
eachProp(deferreds, function (d) {
|
|
if (!d.finished) {
|
|
notFinished.push(d.map.id);
|
|
}
|
|
});
|
|
err = new Error('Timeout for modules: ' + notFinished);
|
|
err.requireModules = notFinished;
|
|
req.onError(err);
|
|
} else if (loadCount || requireDeferreds.length) {
|
|
//Something is still waiting to load. Wait for it, but only
|
|
//if a later check is not already scheduled.
|
|
if (!checkingLater) {
|
|
checkingLater = true;
|
|
prim.nextTick(function () {
|
|
checkingLater = false;
|
|
check();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
//Used to break out of the promise try/catch chains.
|
|
function delayedError(e) {
|
|
prim.nextTick(function () {
|
|
if (!e.dynaId || !trackedErrors[e.dynaId]) {
|
|
trackedErrors[e.dynaId] = true;
|
|
req.onError(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
main = function (name, deps, factory, errback, relName) {
|
|
//Only allow main calling once per module.
|
|
if (name && hasProp(calledDefine, name)) {
|
|
return;
|
|
}
|
|
calledDefine[name] = true;
|
|
|
|
var d = getDefer(name);
|
|
|
|
//This module may not have dependencies
|
|
if (deps && !Array.isArray(deps)) {
|
|
//deps is not an array, so probably means
|
|
//an object literal or factory function for
|
|
//the value. Adjust args.
|
|
factory = deps;
|
|
deps = [];
|
|
}
|
|
|
|
d.promise.catch(errback || delayedError);
|
|
|
|
//Use name if no relName
|
|
relName = relName || name;
|
|
|
|
//Call the factory to define the module, if necessary.
|
|
if (typeof factory === 'function') {
|
|
|
|
if (!deps.length && factory.length) {
|
|
//Remove comments from the callback string,
|
|
//look for require calls, and pull them into the dependencies,
|
|
//but only if there are function args.
|
|
factory
|
|
.toString()
|
|
.replace(commentRegExp, '')
|
|
.replace(cjsRequireRegExp, function (match, dep) {
|
|
deps.push(dep);
|
|
});
|
|
|
|
//May be a CommonJS thing even without require calls, but still
|
|
//could use exports, and module. Avoid doing exports and module
|
|
//work though if it just needs require.
|
|
//REQUIRES the function to expect the CommonJS variables in the
|
|
//order listed below.
|
|
deps = (factory.length === 1 ?
|
|
['require'] :
|
|
['require', 'exports', 'module']).concat(deps);
|
|
}
|
|
|
|
//Save info for use later.
|
|
d.factory = factory;
|
|
d.deps = deps;
|
|
|
|
d.depending = true;
|
|
deps.forEach(function (depName, i) {
|
|
var depMap;
|
|
deps[i] = depMap = makeMap(depName, relName, true);
|
|
depName = depMap.id;
|
|
|
|
//Fast path CommonJS standard dependencies.
|
|
if (depName === "require") {
|
|
d.values[i] = handlers.require(name);
|
|
} else if (depName === "exports") {
|
|
//CommonJS module spec 1.1
|
|
d.values[i] = handlers.exports(name);
|
|
d.usingExports = true;
|
|
} else if (depName === "module") {
|
|
//CommonJS module spec 1.1
|
|
d.values[i] = d.cjsModule = handlers.module(name);
|
|
} else if (depName === undefined) {
|
|
d.values[i] = undefined;
|
|
} else {
|
|
waitForDep(depMap, relName, d, i);
|
|
}
|
|
});
|
|
d.depending = false;
|
|
|
|
//Some modules just depend on the require, exports, modules, so
|
|
//trigger their definition here if so.
|
|
if (d.depCount === d.depMax) {
|
|
defineModule(d);
|
|
}
|
|
} else if (name) {
|
|
//May just be an object definition for the module. Only
|
|
//worry about defining if have a module name.
|
|
resolve(name, d, factory);
|
|
}
|
|
|
|
startTime = (new Date()).getTime();
|
|
|
|
if (!name) {
|
|
check(d);
|
|
}
|
|
};
|
|
|
|
req = makeRequire(null, true);
|
|
|
|
/*
|
|
* Just drops the config on the floor, but returns req in case
|
|
* the config return value is used.
|
|
*/
|
|
req.config = function (cfg) {
|
|
if (cfg.context && cfg.context !== contextName) {
|
|
return newContext(cfg.context).config(cfg);
|
|
}
|
|
|
|
//Since config changed, mapCache may not be valid any more.
|
|
mapCache = {};
|
|
|
|
//Make sure the baseUrl ends in a slash.
|
|
if (cfg.baseUrl) {
|
|
if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
|
|
cfg.baseUrl += '/';
|
|
}
|
|
}
|
|
|
|
//Save off the paths and packages since they require special processing,
|
|
//they are additive.
|
|
var primId,
|
|
shim = config.shim,
|
|
objs = {
|
|
paths: true,
|
|
bundles: true,
|
|
config: true,
|
|
map: true
|
|
};
|
|
|
|
eachProp(cfg, function (value, prop) {
|
|
if (objs[prop]) {
|
|
if (!config[prop]) {
|
|
config[prop] = {};
|
|
}
|
|
mixin(config[prop], value, true, true);
|
|
} else {
|
|
config[prop] = value;
|
|
}
|
|
});
|
|
|
|
//Reverse map the bundles
|
|
if (cfg.bundles) {
|
|
eachProp(cfg.bundles, function (value, prop) {
|
|
value.forEach(function (v) {
|
|
if (v !== prop) {
|
|
bundlesMap[v] = prop;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
//Merge shim
|
|
if (cfg.shim) {
|
|
eachProp(cfg.shim, function (value, id) {
|
|
//Normalize the structure
|
|
if (Array.isArray(value)) {
|
|
value = {
|
|
deps: value
|
|
};
|
|
}
|
|
if ((value.exports || value.init) && !value.exportsFn) {
|
|
value.exportsFn = makeShimExports(value);
|
|
}
|
|
shim[id] = value;
|
|
});
|
|
config.shim = shim;
|
|
}
|
|
|
|
//Adjust packages if necessary.
|
|
if (cfg.packages) {
|
|
cfg.packages.forEach(function (pkgObj) {
|
|
var location, name;
|
|
|
|
pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj;
|
|
|
|
name = pkgObj.name;
|
|
location = pkgObj.location;
|
|
if (location) {
|
|
config.paths[name] = pkgObj.location;
|
|
}
|
|
|
|
//Save pointer to main module ID for pkg name.
|
|
//Remove leading dot in main, so main paths are normalized,
|
|
//and remove any trailing .js, since different package
|
|
//envs have different conventions: some use a module name,
|
|
//some use a file name.
|
|
config.pkgs[name] = pkgObj.name + '/' + (pkgObj.main || 'main')
|
|
.replace(currDirRegExp, '')
|
|
.replace(jsSuffixRegExp, '');
|
|
});
|
|
}
|
|
|
|
//If want prim injected, inject it now.
|
|
primId = config.definePrim;
|
|
if (primId) {
|
|
waiting[primId] = [primId, [], function () { return prim; }];
|
|
}
|
|
|
|
//If a deps array or a config callback is specified, then call
|
|
//require with those args. This is useful when require is defined as a
|
|
//config object before require.js is loaded.
|
|
if (cfg.deps || cfg.callback) {
|
|
req(cfg.deps, cfg.callback);
|
|
}
|
|
|
|
return req;
|
|
};
|
|
|
|
req.onError = function (err) {
|
|
throw err;
|
|
};
|
|
|
|
context = {
|
|
id: contextName,
|
|
defined: defined,
|
|
waiting: waiting,
|
|
config: config,
|
|
deferreds: deferreds
|
|
};
|
|
|
|
contexts[contextName] = context;
|
|
|
|
return req;
|
|
}
|
|
|
|
requirejs = topReq = newContext('_');
|
|
|
|
if (typeof require !== 'function') {
|
|
require = topReq;
|
|
}
|
|
|
|
/**
|
|
* Executes the text. Normally just uses eval, but can be modified
|
|
* to use a better, environment-specific call. Only used for transpiling
|
|
* loader plugins, not for plain JS modules.
|
|
* @param {String} text the text to execute/evaluate.
|
|
*/
|
|
topReq.exec = function (text) {
|
|
/*jslint evil: true */
|
|
return eval(text);
|
|
};
|
|
|
|
topReq.contexts = contexts;
|
|
|
|
define = function () {
|
|
queue.push([].slice.call(arguments, 0));
|
|
};
|
|
|
|
define.amd = {
|
|
jQuery: true
|
|
};
|
|
|
|
if (bootstrapConfig) {
|
|
topReq.config(bootstrapConfig);
|
|
}
|
|
|
|
//data-main support.
|
|
if (topReq.isBrowser && !contexts._.config.skipDataMain) {
|
|
dataMain = document.querySelectorAll('script[data-main]')[0];
|
|
dataMain = dataMain && dataMain.getAttribute('data-main');
|
|
if (dataMain) {
|
|
//Strip off any trailing .js since dataMain is now
|
|
//like a module name.
|
|
dataMain = dataMain.replace(jsSuffixRegExp, '');
|
|
|
|
if (!bootstrapConfig || !bootstrapConfig.baseUrl) {
|
|
//Pull off the directory of data-main for use as the
|
|
//baseUrl.
|
|
src = dataMain.split('/');
|
|
dataMain = src.pop();
|
|
subPath = src.length ? src.join('/') + '/' : './';
|
|
|
|
topReq.config({baseUrl: subPath});
|
|
}
|
|
|
|
topReq([dataMain]);
|
|
}
|
|
}
|
|
}(this));
|
|
|
|
define("ext/alameda", function(){});
|
|
|
|
// this code is from test-agent might use native dom events
|
|
// or something else in the future to replace this.
|
|
define('common/responder',['require','exports','module'],function(require, exports, module) {
|
|
'use strict';
|
|
|
|
/**
|
|
* @param {Object} list of events to add onto responder.
|
|
*/
|
|
function Responder(events) {
|
|
this._$events = Object.create(null);
|
|
|
|
// Buffer for emitWhenListener that hangs onto events dispatched for topics
|
|
// no one is currently listening for. Inspired by email's function
|
|
// of the same name.
|
|
this.buffer = {};
|
|
|
|
if (typeof(events) !== 'undefined') {
|
|
this.addEventListener(events);
|
|
}
|
|
}
|
|
module.exports = Responder;
|
|
|
|
/**
|
|
* 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 data;
|
|
};
|
|
|
|
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);
|
|
var args = Array.prototype.slice.call(arguments).slice(1);
|
|
this.emit.apply(this, event.concat(args));
|
|
return event;
|
|
},
|
|
|
|
/**
|
|
* Adds an event listener to this object.
|
|
*
|
|
* @param {String} type event name.
|
|
* @param {Function} callback event callback.
|
|
*/
|
|
addEventListener: function addEventListener(type, callback) {
|
|
var event;
|
|
|
|
if (typeof(callback) === 'undefined' && typeof(type) === 'object') {
|
|
for (event in type) {
|
|
if (type.hasOwnProperty(event)) {
|
|
this.addEventListener(event, type[event]);
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
if (!(type in this._$events)) {
|
|
this._$events[type] = [];
|
|
}
|
|
|
|
this._$events[type].push(callback);
|
|
return this.flushTopicBuffer(type);
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
/*jshint validthis:true */
|
|
self.removeEventListener(type, onceCb);
|
|
callback.apply(this, arguments);
|
|
}
|
|
|
|
this.addEventListener(type, onceCb);
|
|
return this.flushTopicBuffer(type);
|
|
},
|
|
|
|
flushTopicBuffer: function flushTopicBuffer(topic) {
|
|
if (!(topic in this.buffer)) {
|
|
// Nothing to flush.
|
|
return this;
|
|
}
|
|
|
|
this.buffer[topic].forEach(args => {
|
|
args.unshift(topic);
|
|
this.emit.apply(this, args);
|
|
});
|
|
|
|
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) {
|
|
if (typeof(callback) === 'object' && callback.handleEvent) {
|
|
callback.handleEvent({ type: event, data: args });
|
|
} else {
|
|
callback.apply(self, args);
|
|
}
|
|
});
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
emitWhenListener: function emitWhenListener() {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
var event = args.shift();
|
|
|
|
if (event in this._$events && this._$events[event].length) {
|
|
// Someone is already listening for this event, so this is just
|
|
// a regular old emit.
|
|
return this.emit.apply(this, arguments);
|
|
}
|
|
|
|
if (!(event in this.buffer)) {
|
|
this.buffer[event] = [];
|
|
}
|
|
|
|
// Now just push the call info onto the topic buffer.
|
|
this.buffer[event].push(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;
|
|
Responder.prototype.off = Responder.prototype.removeEventListener;
|
|
|
|
});
|
|
|
|
define('common/debug',['require','exports','module'],function(require, exports, module) {
|
|
'use strict';
|
|
|
|
module.exports = function(name) {
|
|
return function() {
|
|
var args = Array.prototype.slice.call(arguments).map(JSON.stringify);
|
|
args.unshift('[calendar] ');
|
|
args.unshift(name);
|
|
console.log.apply(console, args);
|
|
};
|
|
};
|
|
|
|
});
|
|
|
|
define('worker/thread',['require','exports','module','common/responder','common/debug'],function(require, exports, module) {
|
|
'use strict';
|
|
|
|
var Responder = require('common/responder');
|
|
var debug = require('common/debug')('worker/thread');
|
|
|
|
function Thread(worker) {
|
|
Responder.call(this);
|
|
this.worker = worker;
|
|
this.roles = {};
|
|
|
|
this._initEvents();
|
|
}
|
|
module.exports = Thread;
|
|
|
|
Thread.prototype = {
|
|
__proto__: Responder.prototype,
|
|
|
|
send: function() {
|
|
this.worker.postMessage(Array.prototype.slice.call(arguments));
|
|
},
|
|
|
|
addRole: function(name) {
|
|
this.roles[name] = new Responder();
|
|
},
|
|
|
|
_remoteEmitter: function(id) {
|
|
var self = this;
|
|
return {
|
|
emit: function emitRemote() {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
self.worker.postMessage([id + ' stream'].concat(args));
|
|
}
|
|
};
|
|
},
|
|
|
|
_initEvents: function() {
|
|
var self = this;
|
|
|
|
debug('Will listen for messages from the main thread...');
|
|
this.on('_dispatch', function(data) {
|
|
// data.id
|
|
// data.type
|
|
// data.role
|
|
// data.payload
|
|
var callback = self._requestCallback.bind(
|
|
self, data.id
|
|
);
|
|
|
|
if (data.role) {
|
|
if (data.role in self.roles) {
|
|
if (data.type && data.type === 'stream') {
|
|
self.roles[data.role].respond(
|
|
data.payload,
|
|
self._remoteEmitter(data.id),
|
|
callback
|
|
);
|
|
} else {
|
|
self.roles[data.role].respond(
|
|
data.payload, callback
|
|
);
|
|
}
|
|
} else {
|
|
// TODO: respond with error
|
|
debug('ERROR: ' + data.role + ' is not available.');
|
|
}
|
|
} else {
|
|
self.respond(data.payload, callback);
|
|
}
|
|
});
|
|
},
|
|
|
|
_wrapError: function(err) {
|
|
var errorObject = {};
|
|
|
|
errorObject.stack = err.stack || '';
|
|
errorObject.message = err.message || err.toString();
|
|
errorObject.type = err.type || 'Error';
|
|
errorObject.constructorName = err.constructor.name || 'Error';
|
|
|
|
if (err.name) {
|
|
errorObject.name = err.name;
|
|
}
|
|
|
|
if (err.code) {
|
|
errorObject.code = err.code;
|
|
}
|
|
|
|
return errorObject;
|
|
},
|
|
|
|
_requestCallback: function(id) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
args.unshift(id + ' end');
|
|
|
|
if (args[1] instanceof Error) {
|
|
args[1] = this._wrapError(args[1]);
|
|
}
|
|
|
|
this.worker.postMessage(args);
|
|
},
|
|
|
|
console: function console(name) {
|
|
return {
|
|
log: function() {
|
|
return postMessage(['log', {
|
|
name: name,
|
|
message: Array.prototype.slice.call(arguments).join(', ')
|
|
}]);
|
|
},
|
|
error: function() {
|
|
return postMessage(['error', {
|
|
name: name,
|
|
message: Array.prototype.slice.call(arguments).join(', ')
|
|
}]);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
});
|
|
|
|
/**
|
|
* The way that the current caldav library works,
|
|
* we need to preconfigure some global state :/.
|
|
*/
|
|
define('worker/initialize',['require','exports','module','./thread','common/debug'],function(require, exports, module) {
|
|
'use strict';
|
|
|
|
var Thread = require('./thread');
|
|
var debug = require('common/debug')('worker/initialize');
|
|
|
|
self.window = self;
|
|
|
|
module.exports = function() {
|
|
var thread = self.thread = new Thread(window);
|
|
thread.addRole('caldav');
|
|
window.console = new thread.console('worker');
|
|
require(['service/caldav'], (CaldavService) => {
|
|
// Lazily loaded so that we can prime environment first.
|
|
debug('Will create new caldav service...');
|
|
self.caldav = new CaldavService(thread.roles.caldav);
|
|
});
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
require.config({
|
|
baseUrl: '/js',
|
|
waitSeconds: 60,
|
|
paths: {
|
|
shared: '/shared/js'
|
|
},
|
|
shim: {
|
|
'ext/caldav': {
|
|
deps: ['worker/initialize'],
|
|
exports: 'Caldav'
|
|
},
|
|
'ext/ical': {
|
|
deps: ['worker/initialize'],
|
|
exports: 'ICAL'
|
|
}
|
|
}
|
|
});
|
|
|
|
self.addEventListener('message', function onMessage(msg) {
|
|
if (typeof(caldav) !== 'undefined') {
|
|
return self.thread.respond(msg.data);
|
|
}
|
|
|
|
// Try again in a little bit since the worker may not be ready...
|
|
setTimeout(function() {
|
|
onMessage(msg);
|
|
}, 10);
|
|
});
|
|
|
|
require(['worker/initialize'], initialize => initialize());
|
|
|
|
define("caldav_worker", function(){});
|
|
|
|
define('service/ical_recur_expansion',['require','exports','module','ext/ical'],function(require, exports, module) {
|
|
'use strict';
|
|
|
|
var ICAL = require('ext/ical');
|
|
|
|
module.exports = {
|
|
/**
|
|
* Maximum iterations must be > 0 && < Infinity.
|
|
* Lower values are probably better as we can show progress
|
|
* for multiple events rather then complete one long recurring
|
|
* event after another...
|
|
*/
|
|
forEachLimit: 200,
|
|
|
|
_isDone: function(last, sent, max) {
|
|
if (last && max && last.compare(max) >= 0) {
|
|
return true;
|
|
} else if (sent < this.forEachLimit) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Iterates through a recur expansion instance.
|
|
* Gracefully handles existing iterators (including failures).
|
|
* Will fallback to complete re-expansion when necessary.
|
|
*
|
|
* NOTE: This method intentionally does not accept the "forEach"
|
|
* function as the final argument to indicate it does not follow
|
|
* the NodeJS example...
|
|
*
|
|
* minDate is always exclusive
|
|
* maxDate is not strict and may include one occurrence beyond.
|
|
*
|
|
* @param {ICAL.Event} event complete event.
|
|
* @param {Null|Object} iterator or nothing.
|
|
* @param {Function} forEach receives [nextDate].
|
|
* @param {ICAL.Time|Null} minDate minimum time (defaults to start).
|
|
* @param {ICAL.Time|Null} maxDate maximum date (defaults to none).
|
|
* @return {ICAL.RecurExpansion} iterator.
|
|
*/
|
|
forEach: function(event, iterator, each, min, max) {
|
|
// if there is no iterator create one...
|
|
if (!iterator) {
|
|
return this._beginIteration(event, each, min, max);
|
|
}
|
|
|
|
var iter;
|
|
|
|
try {
|
|
iter = this._resumeIteration(event, iterator, each, min, max);
|
|
} catch (e) {
|
|
console.error('Iteration Error: ' + e.toString());
|
|
iter = this._beginIteration(event, each, min, max);
|
|
}
|
|
|
|
return iter;
|
|
},
|
|
|
|
_resumeIteration: function(event, iterator, each, min, max) {
|
|
if (!(iterator instanceof ICAL.RecurExpansion)) {
|
|
iterator = new ICAL.RecurExpansion(iterator);
|
|
}
|
|
|
|
this._iterate(event, iterator, each, min, max);
|
|
return iterator;
|
|
},
|
|
|
|
_beginIteration: function(event, each, min, max) {
|
|
var iterator = event.iterator();
|
|
this._iterate(event, iterator, each, min, max);
|
|
|
|
return iterator;
|
|
},
|
|
|
|
_iterate: function(event, iterator, each, min, max) {
|
|
// keep track of the iterations
|
|
var sent = 0;
|
|
var current;
|
|
|
|
do {
|
|
current = iterator.next();
|
|
|
|
if (!current) {
|
|
break;
|
|
}
|
|
|
|
if (!min || current.compare(min) > 0) {
|
|
// sent should be inside the loop to guard against
|
|
// the possibility that the resume functionality breaking.
|
|
sent++;
|
|
each(current);
|
|
}
|
|
|
|
} while (!this._isDone(current, sent, max));
|
|
}
|
|
};
|
|
|
|
});
|
|
|
|
define('common/presets',{
|
|
"google": {
|
|
"providerType": "Caldav",
|
|
"group": "remote",
|
|
"authenticationType": "oauth2",
|
|
"apiCredentials": {
|
|
"tokenUrl": "https://accounts.google.com/o/oauth2/token",
|
|
"authorizationUrl": "https://accounts.google.com/o/oauth2/auth",
|
|
"user_info": {
|
|
"url": "https://www.googleapis.com/oauth2/v3/userinfo",
|
|
"field": "email"
|
|
},
|
|
"client_secret": "jQTKlOhF-RclGaGJot3HIcVf",
|
|
"client_id": "605300196874-1ki833poa7uqabmh3hq6u1onlqlsi54h.apps.googleusercontent.com",
|
|
"scope": "https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/userinfo.email",
|
|
"redirect_uri": "https://oauth.gaiamobile.org/authenticated"
|
|
},
|
|
"options": {
|
|
"domain": "https://apidata.googleusercontent.com",
|
|
"entrypoint": "/caldav/v2/",
|
|
"providerType": "Caldav"
|
|
}
|
|
},
|
|
"yahoo": {
|
|
"providerType": "Caldav",
|
|
"group": "remote",
|
|
"options": {
|
|
"domain": "https://caldav.calendar.yahoo.com",
|
|
"entrypoint": "/",
|
|
"providerType": "Caldav",
|
|
"user": "@yahoo.com",
|
|
"usernameType": "email"
|
|
}
|
|
},
|
|
"caldav": {
|
|
"providerType": "Caldav",
|
|
"group": "remote",
|
|
"options": {
|
|
"domain": "",
|
|
"entrypoint": "",
|
|
"providerType": "Caldav"
|
|
}
|
|
},
|
|
"local": {
|
|
"singleUse": true,
|
|
"providerType": "Local",
|
|
"group": "local",
|
|
"options": {
|
|
"providerType": "Local"
|
|
}
|
|
}
|
|
});
|
|
define('common/extend',['require','exports','module'],function(require, exports, module) {
|
|
'use strict';
|
|
|
|
module.exports = function(target, input) {
|
|
for (var key in input) {
|
|
if (hasOwnProperty.call(input, key)) {
|
|
target[key] = input[key];
|
|
}
|
|
}
|
|
|
|
return target;
|
|
};
|
|
|
|
});
|
|
|
|
// uuid.js
|
|
//
|
|
// Copyright (c) 2010-2012 Robert Kieffer
|
|
// MIT License - http://opensource.org/licenses/mit-license.php
|
|
|
|
(function() {
|
|
var _global = this;
|
|
|
|
// Unique ID creation requires a high quality random # generator. We feature
|
|
// detect to determine the best RNG source, normalizing to a function that
|
|
// returns 128-bits of randomness, since that's what's usually required
|
|
var _rng;
|
|
|
|
// Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html
|
|
//
|
|
// Moderately fast, high quality
|
|
if (typeof(_global.require) == 'function' && typeof(module) != 'undefined' && module.exports) {
|
|
try {
|
|
var _rb = _global.require('crypto').randomBytes;
|
|
_rng = _rb && function() {return _rb(16);};
|
|
} catch(e) {}
|
|
}
|
|
|
|
if (!_rng && _global.crypto && crypto.getRandomValues) {
|
|
// WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
|
|
//
|
|
// Moderately fast, high quality
|
|
var _rnds8 = new Uint8Array(16);
|
|
_rng = function whatwgRNG() {
|
|
crypto.getRandomValues(_rnds8);
|
|
return _rnds8;
|
|
};
|
|
}
|
|
|
|
if (!_rng) {
|
|
// Math.random()-based (RNG)
|
|
//
|
|
// If all else fails, use Math.random(). It's fast, but is of unspecified
|
|
// quality.
|
|
var _rnds = new Array(16);
|
|
_rng = function() {
|
|
for (var i = 0, r; i < 16; i++) {
|
|
if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
|
|
_rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
|
|
}
|
|
|
|
return _rnds;
|
|
};
|
|
}
|
|
|
|
// Buffer class to use
|
|
var BufferClass = typeof(_global.Buffer) == 'function' ? _global.Buffer : Array;
|
|
|
|
// Maps for number <-> hex string conversion
|
|
var _byteToHex = [];
|
|
var _hexToByte = {};
|
|
for (var i = 0; i < 256; i++) {
|
|
_byteToHex[i] = (i + 0x100).toString(16).substr(1);
|
|
_hexToByte[_byteToHex[i]] = i;
|
|
}
|
|
|
|
// **`parse()` - Parse a UUID into it's component bytes**
|
|
function parse(s, buf, offset) {
|
|
var i = (buf && offset) || 0, ii = 0;
|
|
|
|
buf = buf || [];
|
|
s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
|
|
if (ii < 16) { // Don't overflow!
|
|
buf[i + ii++] = _hexToByte[oct];
|
|
}
|
|
});
|
|
|
|
// Zero out remaining bytes if string was short
|
|
while (ii < 16) {
|
|
buf[i + ii++] = 0;
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
// **`unparse()` - Convert UUID byte array (ala parse()) into a string**
|
|
function unparse(buf, offset) {
|
|
var i = offset || 0, bth = _byteToHex;
|
|
return bth[buf[i++]] + bth[buf[i++]] +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] +
|
|
bth[buf[i++]] + bth[buf[i++]] +
|
|
bth[buf[i++]] + bth[buf[i++]];
|
|
}
|
|
|
|
// **`v1()` - Generate time-based UUID**
|
|
//
|
|
// Inspired by https://github.com/LiosK/UUID.js
|
|
// and http://docs.python.org/library/uuid.html
|
|
|
|
// random #'s we need to init node and clockseq
|
|
var _seedBytes = _rng();
|
|
|
|
// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
|
|
var _nodeId = [
|
|
_seedBytes[0] | 0x01,
|
|
_seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
|
|
];
|
|
|
|
// Per 4.2.2, randomize (14 bit) clockseq
|
|
var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
|
|
|
|
// Previous uuid creation time
|
|
var _lastMSecs = 0, _lastNSecs = 0;
|
|
|
|
// See https://github.com/broofa/node-uuid for API details
|
|
function v1(options, buf, offset) {
|
|
var i = buf && offset || 0;
|
|
var b = buf || [];
|
|
|
|
options = options || {};
|
|
|
|
var clockseq = options.clockseq != null ? options.clockseq : _clockseq;
|
|
|
|
// UUID timestamps are 100 nano-second units since the Gregorian epoch,
|
|
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
|
|
// time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
|
|
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
|
|
var msecs = options.msecs != null ? options.msecs : new Date().getTime();
|
|
|
|
// Per 4.2.1.2, use count of uuid's generated during the current clock
|
|
// cycle to simulate higher resolution clock
|
|
var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1;
|
|
|
|
// Time since last uuid creation (in msecs)
|
|
var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
|
|
|
|
// Per 4.2.1.2, Bump clockseq on clock regression
|
|
if (dt < 0 && options.clockseq == null) {
|
|
clockseq = clockseq + 1 & 0x3fff;
|
|
}
|
|
|
|
// Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
|
|
// time interval
|
|
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) {
|
|
nsecs = 0;
|
|
}
|
|
|
|
// Per 4.2.1.2 Throw error if too many uuids are requested
|
|
if (nsecs >= 10000) {
|
|
throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
|
|
}
|
|
|
|
_lastMSecs = msecs;
|
|
_lastNSecs = nsecs;
|
|
_clockseq = clockseq;
|
|
|
|
// Per 4.1.4 - Convert from unix epoch to Gregorian epoch
|
|
msecs += 12219292800000;
|
|
|
|
// `time_low`
|
|
var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
|
|
b[i++] = tl >>> 24 & 0xff;
|
|
b[i++] = tl >>> 16 & 0xff;
|
|
b[i++] = tl >>> 8 & 0xff;
|
|
b[i++] = tl & 0xff;
|
|
|
|
// `time_mid`
|
|
var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
|
|
b[i++] = tmh >>> 8 & 0xff;
|
|
b[i++] = tmh & 0xff;
|
|
|
|
// `time_high_and_version`
|
|
b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
|
|
b[i++] = tmh >>> 16 & 0xff;
|
|
|
|
// `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
|
|
b[i++] = clockseq >>> 8 | 0x80;
|
|
|
|
// `clock_seq_low`
|
|
b[i++] = clockseq & 0xff;
|
|
|
|
// `node`
|
|
var node = options.node || _nodeId;
|
|
for (var n = 0; n < 6; n++) {
|
|
b[i + n] = node[n];
|
|
}
|
|
|
|
return buf ? buf : unparse(b);
|
|
}
|
|
|
|
// **`v4()` - Generate random UUID**
|
|
|
|
// See https://github.com/broofa/node-uuid for API details
|
|
function v4(options, buf, offset) {
|
|
// Deprecated - 'format' argument, as supported in v1.2
|
|
var i = buf && offset || 0;
|
|
|
|
if (typeof(options) == 'string') {
|
|
buf = options == 'binary' ? new BufferClass(16) : null;
|
|
options = null;
|
|
}
|
|
options = options || {};
|
|
|
|
var rnds = options.random || (options.rng || _rng)();
|
|
|
|
// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
|
|
rnds[6] = (rnds[6] & 0x0f) | 0x40;
|
|
rnds[8] = (rnds[8] & 0x3f) | 0x80;
|
|
|
|
// Copy bytes to buffer, if provided
|
|
if (buf) {
|
|
for (var ii = 0; ii < 16; ii++) {
|
|
buf[i + ii] = rnds[ii];
|
|
}
|
|
}
|
|
|
|
return buf || unparse(rnds);
|
|
}
|
|
|
|
// Export public API
|
|
var uuid = v4;
|
|
uuid.v1 = v1;
|
|
uuid.v4 = v4;
|
|
uuid.parse = parse;
|
|
uuid.unparse = unparse;
|
|
uuid.BufferClass = BufferClass;
|
|
|
|
if (typeof define === 'function' && define.amd) {
|
|
// Publish as AMD module
|
|
define('ext/uuid',[],function() {return uuid;});
|
|
} else if (typeof(module) != 'undefined' && module.exports) {
|
|
// Publish as node.js module
|
|
module.exports = uuid;
|
|
} else {
|
|
// Publish as global (in browsers)
|
|
var _previousRoot = _global.uuid;
|
|
|
|
// **`noConflict()` - (browser only) to reset global 'uuid' var**
|
|
uuid.noConflict = function() {
|
|
_global.uuid = _previousRoot;
|
|
return uuid;
|
|
};
|
|
|
|
_global.uuid = uuid;
|
|
}
|
|
}).call(this);
|
|
|
|
define('service/caldav',['require','exports','module','ext/caldav','ext/ical','./ical_recur_expansion','common/presets','common/responder','common/debug','common/extend','ext/uuid'],function(require, exports, module) {
|
|
'use strict';
|
|
|
|
var Caldav = require('ext/caldav');
|
|
var ICAL = require('ext/ical');
|
|
var IcalRecurExpansion = require('./ical_recur_expansion');
|
|
var Presets = require('common/presets');
|
|
var Responder = require('common/responder');
|
|
var debug = require('common/debug')('service/caldav');
|
|
var extend = require('common/extend');
|
|
var uuid = require('ext/uuid');
|
|
|
|
/* TODO: ugly hack to enable system XHR fix upstream in Caldav lib */
|
|
var xhrOpts = {
|
|
/** system is required for cross domain XHR */
|
|
mozSystem: true,
|
|
/** mozAnon is required to avoid system level popups on 401 status */
|
|
mozAnon: true,
|
|
/** enables use of mozilla only streaming api's when available */
|
|
useMozChunkedText: true
|
|
};
|
|
|
|
Caldav.Xhr.prototype.globalXhrOptions = xhrOpts;
|
|
|
|
function Service(service) {
|
|
Responder.call(this);
|
|
|
|
this.service = service;
|
|
this._initEvents();
|
|
}
|
|
module.exports = Service;
|
|
|
|
Service.prototype = {
|
|
__proto__: Responder.prototype,
|
|
|
|
/**
|
|
* See: http://tools.ietf.org/html/rfc5545#section-3.7.3
|
|
*/
|
|
icalProductId: '-//Mozilla//FirefoxOS',
|
|
|
|
/**
|
|
* See: http://tools.ietf.org/html/rfc5545#section-3.7.4
|
|
*/
|
|
icalVersion: '2.0',
|
|
|
|
_initEvents: function() {
|
|
var events = [
|
|
'noop',
|
|
'getAccount',
|
|
'findCalendars',
|
|
'getCalendar',
|
|
'streamEvents',
|
|
'expandComponents',
|
|
'expandRecurringEvent',
|
|
'deleteEvent',
|
|
'updateEvent',
|
|
'createEvent'
|
|
];
|
|
|
|
debug('Will listen for worker events...');
|
|
events.forEach(function(e) {
|
|
this.service.on(e, this);
|
|
}, this);
|
|
},
|
|
|
|
handleEvent: function(e) {
|
|
debug('Worker will fulfill', e.type, 'request...');
|
|
this[e.type].apply(this, e.data);
|
|
},
|
|
|
|
/**
|
|
* Builds an Caldav connection from an account model object.
|
|
*/
|
|
_createConnection: function(account) {
|
|
var params = extend({}, account);
|
|
var preset = Presets[account.preset];
|
|
|
|
if (
|
|
preset &&
|
|
preset.authenticationType &&
|
|
preset.apiCredentials
|
|
) {
|
|
switch (preset.authenticationType) {
|
|
case 'oauth2':
|
|
params.httpHandler = 'oauth2';
|
|
// shallow copy the apiCredentials on the preset
|
|
params.apiCredentials = extend({}, preset.apiCredentials);
|
|
// the url in this case will always be tokenUrl
|
|
params.apiCredentials.url = preset.apiCredentials.tokenUrl;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var connection = new Caldav.Connection(params);
|
|
return connection;
|
|
},
|
|
|
|
_requestHome: function(connection, url) {
|
|
return new Caldav.Request.CalendarHome(
|
|
connection,
|
|
{ url: url }
|
|
);
|
|
},
|
|
|
|
_requestCalendars: function(connection, url) {
|
|
var Finder = Caldav.Request.Resources;
|
|
var Resource = Caldav.Resources.Calendar;
|
|
|
|
var req = new Finder(connection, { url: url });
|
|
|
|
req.addResource('calendar', Resource);
|
|
req.prop(['ical', 'calendar-color']);
|
|
req.prop(['caldav', 'calendar-description']);
|
|
req.prop('current-user-privilege-set');
|
|
req.prop('displayname');
|
|
req.prop('resourcetype');
|
|
req.prop(['calserver', 'getctag']);
|
|
|
|
return req;
|
|
},
|
|
|
|
_requestEvents: function(connection, cal, options) {
|
|
var Resource = Caldav.Resources.Calendar;
|
|
var remoteCal = new Resource(connection, cal);
|
|
var query = remoteCal.createQuery();
|
|
|
|
query.prop('getetag');
|
|
|
|
// only return VEVENT & VTIMEZONE
|
|
var filterQuery = query.filter.setComp('VCALENDAR');
|
|
var filterEvent = filterQuery.comp('VEVENT');
|
|
|
|
if (options && options.startDate) {
|
|
// convert startDate to unix ical time.
|
|
var icalDate = new ICAL.Time();
|
|
|
|
// ical uses seconds not milliseconds
|
|
icalDate.fromUnixTime(options.startDate.valueOf() / 1000);
|
|
filterEvent.setTimeRange({ start: icalDate.toICALString() });
|
|
}
|
|
|
|
// include only the VEVENT/VTIMEZONE in the data
|
|
query.data.setComp('VCALENDAR');
|
|
|
|
return query;
|
|
},
|
|
|
|
noop: function(callback) {
|
|
callback({ ready: true });
|
|
},
|
|
|
|
getAccount: function(account, callback) {
|
|
var url = account.entrypoint;
|
|
debug('Fetching account from:', url);
|
|
|
|
var connection = this._createConnection(account);
|
|
var request = this._requestHome(connection, url);
|
|
debug('Will issue calendar home request.');
|
|
return request.send(function(err, data) {
|
|
if (err) {
|
|
debug('Error sending home request:', err);
|
|
return callback(err);
|
|
}
|
|
|
|
debug('Received data:', data);
|
|
var result = {};
|
|
|
|
if (data.url) {
|
|
result.calendarHome = data.url;
|
|
}
|
|
|
|
if (connection.oauth) {
|
|
result.oauth = connection.oauth;
|
|
}
|
|
|
|
if (connection.user) {
|
|
result.user = connection.user;
|
|
}
|
|
|
|
callback(null, result);
|
|
});
|
|
},
|
|
|
|
_formatCalendar: function(cal) {
|
|
var result = Object.create(null);
|
|
|
|
result.id = cal.url;
|
|
result.url = cal.url;
|
|
result.name = cal.name;
|
|
result.color = cal.color;
|
|
result.description = cal.description;
|
|
result.syncToken = cal.ctag;
|
|
result.privilegeSet = cal.privilegeSet;
|
|
|
|
return result;
|
|
},
|
|
|
|
findCalendars: function(account, callback) {
|
|
var self = this;
|
|
var url = account.calendarHome;
|
|
var connection = this._createConnection(account);
|
|
|
|
var request = this._requestCalendars(
|
|
connection,
|
|
url
|
|
);
|
|
|
|
request.send(function(err, data) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var calendars = data.calendar;
|
|
var results = {};
|
|
var key;
|
|
var item;
|
|
|
|
for (key in calendars) {
|
|
if (calendars.hasOwnProperty(key)) {
|
|
item = calendars[key];
|
|
var formattedCal = self._formatCalendar(
|
|
item
|
|
);
|
|
|
|
// If privilegeSet is not present we will assume full permissions.
|
|
// Its highly unlikey that it is missing however.
|
|
if (('privilegeSet' in formattedCal) &&
|
|
(formattedCal.privilegeSet.indexOf('read') === -1)) {
|
|
|
|
// skip calendars without read permissions
|
|
continue;
|
|
}
|
|
|
|
results[key] = formattedCal;
|
|
}
|
|
}
|
|
callback(null, results);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Formats an alarm trigger
|
|
* Returns the relative time for that trigger
|
|
*
|
|
* @param {ICAL.Property} trigger property.
|
|
* @param {ICAL.Date} start date.
|
|
*/
|
|
_formatTrigger: function(trigger, startDate) {
|
|
var alarmTrigger;
|
|
if (trigger.type == 'duration') {
|
|
alarmTrigger = trigger.getFirstValue().toSeconds();
|
|
} else {
|
|
// Type is date-time
|
|
alarmTrigger = trigger
|
|
.getFirstValue()
|
|
.subtractDate(startDate)
|
|
.toSeconds();
|
|
}
|
|
|
|
return alarmTrigger;
|
|
},
|
|
|
|
/**
|
|
* Formats an already parsed ICAL.Event instance.
|
|
* Expects event to already contain exceptions, etc..
|
|
*
|
|
* @param {String} etag etag.
|
|
* @param {String} url caldav url.
|
|
* @param {String} ical raw ical string.
|
|
* @param {ICAL.Event} event ical event.
|
|
*/
|
|
_formatEvent: function(etag, url, ical, event) {
|
|
var self = this;
|
|
var exceptions = null;
|
|
var key;
|
|
|
|
if (event.exceptions) {
|
|
exceptions = [];
|
|
for (key in event.exceptions) {
|
|
exceptions.push(this._formatEvent(
|
|
etag,
|
|
url,
|
|
ical,
|
|
event.exceptions[key]
|
|
));
|
|
}
|
|
|
|
if (!exceptions.length) {
|
|
exceptions = null;
|
|
}
|
|
}
|
|
|
|
var rid = event.recurrenceId;
|
|
|
|
if (rid) {
|
|
rid = this.formatICALTime(rid);
|
|
}
|
|
|
|
var resultAlarms = [];
|
|
var alarms = event.component.getAllSubcomponents('valarm');
|
|
alarms.forEach(function(instance) {
|
|
var action = instance.getFirstPropertyValue('action');
|
|
if (action && action === 'DISPLAY') {
|
|
var triggers = instance.getAllProperties('trigger');
|
|
var i = 0;
|
|
var len = triggers.length;
|
|
|
|
for (; i < len; i++) {
|
|
|
|
var trigger = triggers[i];
|
|
|
|
resultAlarms.push({
|
|
action: action,
|
|
trigger: self._formatTrigger(trigger, event.startDate)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
var result = {
|
|
alarms: resultAlarms,
|
|
syncToken: etag,
|
|
url: url,
|
|
id: event.uid,
|
|
title: event.summary,
|
|
recurrenceId: rid,
|
|
isRecurring: event.isRecurring(),
|
|
description: event.description,
|
|
location: event.location,
|
|
start: this.formatICALTime(event.startDate),
|
|
end: this.formatICALTime(event.endDate),
|
|
exceptions: exceptions
|
|
};
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Find and parse the display alarms for an event.
|
|
*
|
|
* @param {Object} details details for specific instance.
|
|
*/
|
|
_displayAlarms: function(details) {
|
|
var event = details.item;
|
|
var comp = event.component;
|
|
var alarms = comp.getAllSubcomponents('valarm');
|
|
var result = [];
|
|
|
|
var self = this;
|
|
alarms.forEach(function(instance) {
|
|
var action = instance.getFirstPropertyValue('action');
|
|
if (action && action === 'DISPLAY') {
|
|
// lets just assume we might have multiple triggers
|
|
var triggers = instance.getAllProperties('trigger');
|
|
var i = 0;
|
|
var len = triggers.length;
|
|
|
|
for (; i < len; i++) {
|
|
result.push({
|
|
action: action,
|
|
trigger: self._formatTrigger(triggers[i], event.startDate)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Takes an ICAL.Time object and converts it
|
|
* into the storage format familiar to the calendar app.
|
|
*
|
|
* var time = new ICAL.Time({
|
|
* year: 2012,
|
|
* month: 1,
|
|
* day: 1,
|
|
* zone: 'PST'
|
|
* });
|
|
*
|
|
* // time is converted to a MS
|
|
* // then its UTC offset is added
|
|
* // so the time is at UTC (offset 0) then the
|
|
* // offset is associated with that time.
|
|
*
|
|
* var output = {
|
|
* utc: ms,
|
|
* offset: (+|-)ms,
|
|
* // zone can mostly be ignored except
|
|
* // in the case where the event is "floating"
|
|
* // in time and we need to convert the utc value
|
|
* // to the current local time.
|
|
* tzid: ''
|
|
* };
|
|
*/
|
|
formatICALTime: function(time) {
|
|
var zone = time.zone;
|
|
var offset = time.utcOffset() * 1000;
|
|
var utc = time.toUnixTime() * 1000;
|
|
|
|
utc += offset;
|
|
|
|
var result = {
|
|
tzid: zone.tzid,
|
|
// from seconds to ms
|
|
offset: offset,
|
|
// from seconds to ms
|
|
utc: utc
|
|
};
|
|
|
|
if (time.isDate) {
|
|
result.isDate = true;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Formats a given time/date into a ICAL.Time instance.
|
|
* Suitable for converting the output of formatICALTime back
|
|
* into a similar representation of the original.
|
|
*
|
|
* Once a time instance goes through this method it should _not_
|
|
* be modified as the DST information is lost (offset is preserved).
|
|
*
|
|
* @param {ICAL.Time|Object} time formatted ical time
|
|
* or output of formatICALTime.
|
|
*/
|
|
formatInputTime: function(time) {
|
|
if (time instanceof ICAL.Time) {
|
|
return time;
|
|
}
|
|
|
|
var utc = time.utc;
|
|
var tzid = time.tzid;
|
|
var offset = time.offset;
|
|
var result;
|
|
|
|
if (tzid === ICAL.Timezone.localTimezone.tzid) {
|
|
result = new ICAL.Time();
|
|
result.fromUnixTime(utc / 1000);
|
|
result.zone = ICAL.Timezone.localTimezone;
|
|
} else {
|
|
result = new ICAL.Time();
|
|
result.fromUnixTime((utc - offset) / 1000);
|
|
result.zone = ICAL.Timezone.utcTimezone;
|
|
}
|
|
|
|
if (time.isDate) {
|
|
result.isDate = true;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Parse an ical data/string into primary
|
|
* event and exceptions.
|
|
*
|
|
* It is assumed there is only one primary event
|
|
* (does not have a RECURRENCE-ID) in the ical content.
|
|
*
|
|
* @param {Object|String|ICAL.Event} ical vcalendar chunk (and exceptions).
|
|
* @param {Function} callback node style callback [err, primary event].
|
|
*/
|
|
parseEvent: function(ical, callback) {
|
|
if (ical instanceof ICAL.Event) {
|
|
callback(null, ical);
|
|
return;
|
|
}
|
|
|
|
var parser = new ICAL.ComponentParser();
|
|
var primaryEvent;
|
|
var exceptions = [];
|
|
|
|
parser.ontimezone = function(zone) {
|
|
var id = zone.tzid;
|
|
|
|
if (!ICAL.TimezoneService.has(id)) {
|
|
ICAL.TimezoneService.register(id, zone);
|
|
}
|
|
};
|
|
|
|
parser.onevent = function(item) {
|
|
if (item.isRecurrenceException()) {
|
|
exceptions.push(item);
|
|
} else {
|
|
primaryEvent = item;
|
|
}
|
|
};
|
|
|
|
parser.oncomplete = function() {
|
|
if (!primaryEvent) {
|
|
//TODO: in the error handling pass we need to define
|
|
// path to log this information so we can determine
|
|
// the cause of failures.
|
|
callback(new Error('ical parse error'));
|
|
return;
|
|
}
|
|
exceptions.forEach(primaryEvent.relateException, primaryEvent);
|
|
callback(null, primaryEvent);
|
|
};
|
|
|
|
//XXX: Right now ICAL.js is all sync so we
|
|
// can catch the errors this way in the future
|
|
// onerror will replace this.
|
|
try {
|
|
parser.process(ical);
|
|
} catch (e) {
|
|
callback(e);
|
|
}
|
|
},
|
|
|
|
_defaultMaxDate: function() {
|
|
var now = new Date();
|
|
|
|
return new ICAL.Time({
|
|
year: now.getFullYear(),
|
|
// three months in advance
|
|
// +1 because js months are zero based
|
|
month: now.getMonth() + 6,
|
|
day: now.getDate()
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Expands a list recurring events by their component.
|
|
*
|
|
* It is expected for this function to receive an array
|
|
* of items each structured as a icalComponent.
|
|
*
|
|
* [
|
|
* { ical: '...', lastRecurrenceId: '..', iterator: '...' },
|
|
* ...
|
|
* ]
|
|
*
|
|
* @param {Array.<icalComponent>} components list of icalComponents.
|
|
* @param {Calendar.Responder} stream to emit events.
|
|
* @param {Object} options list of options.
|
|
* @param {Object} options.maxDate maximum date to expand to.
|
|
* @param {Function} callback only sends an error if fatal.
|
|
*/
|
|
expandComponents: function(components, options, stream, callback) {
|
|
var pending = components.length;
|
|
|
|
function next() {
|
|
if (!(--pending)) {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
|
|
components.forEach(function(component) {
|
|
var ical = component.ical;
|
|
var localOpts = {
|
|
maxDate: options.maxDate,
|
|
iterator: component.iterator
|
|
};
|
|
|
|
if (component.lastRecurrenceId) {
|
|
localOpts.minDate = component.lastRecurrenceId;
|
|
}
|
|
|
|
// expand each component
|
|
this.expandRecurringEvent(ical, localOpts, stream,
|
|
function(err, iter, lastRecurId, uid) {
|
|
|
|
if (err) {
|
|
stream.emit('error', err);
|
|
next();
|
|
return;
|
|
}
|
|
|
|
stream.emit('component', {
|
|
eventId: uid,
|
|
lastRecurrenceId: lastRecurId,
|
|
ical: ical,
|
|
iterator: iter
|
|
});
|
|
|
|
next();
|
|
});
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* Options:
|
|
*
|
|
* - iterator: (ICAL.RecurExpander) optional recurrence expander
|
|
* used to resume the iterator state for existing events.
|
|
*
|
|
* - maxDate: if instance ends after this date stop expansion.
|
|
*
|
|
*
|
|
* Returns:
|
|
*
|
|
* [
|
|
* {
|
|
* start: { offset: inMS, utc: ms },
|
|
* endDate: // same format as start,
|
|
* recurrenceId: // id of specific recurrence.
|
|
* uid: // uid of event
|
|
* isException: // true when is exception to usual rule.
|
|
* },
|
|
* //...
|
|
* ]
|
|
*/
|
|
expandRecurringEvent: function(component, options, stream, callback) {
|
|
var self = this;
|
|
var maxDate;
|
|
var minDate = null;
|
|
var now;
|
|
|
|
if (options.minDate) {
|
|
minDate = this.formatInputTime(options.minDate);
|
|
}
|
|
|
|
if (options.maxDate) {
|
|
maxDate = this.formatInputTime(options.maxDate);
|
|
}
|
|
|
|
if (!('now' in options)) {
|
|
options.now = ICAL.Time.now();
|
|
}
|
|
|
|
now = options.now;
|
|
|
|
// convert to rich ical event
|
|
this.parseEvent(component, function(err, event) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var iter = IcalRecurExpansion.forEach(
|
|
event,
|
|
options.iterator,
|
|
occuranceHandler,
|
|
minDate,
|
|
maxDate
|
|
);
|
|
|
|
function occuranceHandler(next) {
|
|
var details = event.getOccurrenceDetails(next);
|
|
var inFuture = details.endDate.compare(now);
|
|
|
|
debug('alarm time',
|
|
event.summary,
|
|
'will add ' + String(inFuture),
|
|
'start:', details.startDate.toJSDate().toString(),
|
|
'end:', details.endDate.toJSDate().toString(),
|
|
'now:', now.toJSDate().toString());
|
|
|
|
var occurrence = {
|
|
start: self.formatICALTime(details.startDate),
|
|
end: self.formatICALTime(details.endDate),
|
|
recurrenceId: self.formatICALTime(next),
|
|
eventId: details.item.uid,
|
|
isException: details.item.isRecurrenceException()
|
|
};
|
|
|
|
// only set alarms for those dates in the future...
|
|
if (inFuture >= 0) {
|
|
var alarms = self._displayAlarms(details);
|
|
if (alarms) {
|
|
occurrence.alarms = alarms;
|
|
}
|
|
}
|
|
|
|
stream.emit('occurrence', occurrence);
|
|
}
|
|
|
|
var lastRecurrence;
|
|
|
|
if (iter.complete) {
|
|
// when the iterator is complete
|
|
// last recurrence is false.
|
|
// We use this to signify the end
|
|
// of the iteration cycle.
|
|
lastRecurrence = false;
|
|
} else {
|
|
// its very important all times used
|
|
// for comparison are based on the recurrence id
|
|
// and not the start date as those can change
|
|
// with exceptions...
|
|
lastRecurrence = self.formatICALTime(
|
|
iter.last
|
|
);
|
|
}
|
|
|
|
callback(
|
|
null,
|
|
iter.toJSON(),
|
|
lastRecurrence,
|
|
event.uid
|
|
);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Handle a single caldav event response.
|
|
*
|
|
* @param {String} url location of event.
|
|
* @param {Object} response caldav response object.
|
|
* @param {Calendar.Responder} responder event emitter.
|
|
* @param {Function} callback node style callback fired after event parsing.
|
|
*/
|
|
_handleCaldavEvent: function(url, response, stream, callback) {
|
|
var self = this;
|
|
var etag = response.getetag;
|
|
var event = response['calendar-data'];
|
|
|
|
if (event.status != 200) {
|
|
callback(new Error('non 200 status code "' + url + '"'));
|
|
return;
|
|
}
|
|
|
|
// process event
|
|
var ical = event.value;
|
|
this.parseEvent(ical, function(err, event) {
|
|
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var result = self._formatEvent(etag.value, url, ical, event);
|
|
stream.emit('event', result);
|
|
|
|
var options = {
|
|
maxDate: self._defaultMaxDate(),
|
|
now: ICAL.Time.now()
|
|
};
|
|
|
|
self.expandRecurringEvent(event, options, stream,
|
|
function(err, iter, lastRecurrenceId) {
|
|
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
if (!event.isRecurring()) {
|
|
stream.emit('component', {
|
|
eventId: result.id,
|
|
isRecurring: false,
|
|
ical: ical
|
|
});
|
|
} else {
|
|
stream.emit('component', {
|
|
eventId: result.id,
|
|
lastRecurrenceId: lastRecurrenceId,
|
|
ical: ical,
|
|
iterator: iter
|
|
});
|
|
}
|
|
|
|
callback(null);
|
|
});
|
|
});
|
|
},
|
|
|
|
streamEvents: function(account, calendar, options, stream, callback) {
|
|
var self = this;
|
|
var hasCompleted = false;
|
|
var connection = this._createConnection(account);
|
|
|
|
var cache = options.cached;
|
|
|
|
// we don't need to pass this around anywhere.
|
|
//delete options.cache;
|
|
|
|
var request = this._requestEvents(connection, calendar, options);
|
|
var pending = 0;
|
|
|
|
function next(err) {
|
|
if (err) {
|
|
try {
|
|
stream.emit('error', err);
|
|
} catch (e) {
|
|
console.error('Failed to transport err:', err.toString(), err.stack);
|
|
}
|
|
}
|
|
|
|
if (!(--pending) && hasCompleted) {
|
|
callback(null);
|
|
}
|
|
}
|
|
|
|
function handleResponse(url, data) {
|
|
if (!data || !data['calendar-data']) {
|
|
// throw some error;
|
|
console.error('Could not sync: ', url);
|
|
return;
|
|
}
|
|
var etag = data.getetag.value;
|
|
if (url in cache) {
|
|
// don't need to track this for missing events.
|
|
if (etag !== cache[url].syncToken) {
|
|
pending++;
|
|
self._handleCaldavEvent(url, data, stream, next);
|
|
}
|
|
|
|
delete cache[url];
|
|
} else {
|
|
pending++;
|
|
self._handleCaldavEvent(url, data, stream, next);
|
|
}
|
|
}
|
|
|
|
request.sax.on('DAV:/response', handleResponse);
|
|
|
|
request.send(function(err) {
|
|
hasCompleted = true;
|
|
// when the request is completed stop listening
|
|
// for sax events.
|
|
request.sax.removeEventListener(
|
|
'DAV:/response', handleResponse
|
|
);
|
|
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
if (!pending) {
|
|
var missing = [];
|
|
|
|
for (var url in cache) {
|
|
missing.push(cache[url].id);
|
|
}
|
|
|
|
// send missing events
|
|
stream.emit('missingEvents', missing);
|
|
|
|
// notify the requester that we have completed.
|
|
callback();
|
|
}
|
|
});
|
|
},
|
|
|
|
_assetRequest: function(connection, url) {
|
|
return new Caldav.Request.Asset(connection, url);
|
|
},
|
|
|
|
deleteEvent: function(account, calendar, event, callback) {
|
|
var connection = this._createConnection(account);
|
|
|
|
var req = this._assetRequest(connection, event.url);
|
|
|
|
req.delete({}, function(err) {
|
|
callback(err);
|
|
});
|
|
},
|
|
|
|
addAlarms: function(component, alarms, account) {
|
|
alarms = alarms || [];
|
|
|
|
for (var i = 0; i < alarms.length; i++) {
|
|
|
|
var valarm = new ICAL.Component('valarm');
|
|
|
|
// valarm details
|
|
valarm.addPropertyWithValue('action', alarms[i].action);
|
|
valarm.addPropertyWithValue('description', 'This is an event reminder');
|
|
var trigger = valarm.addPropertyWithValue('trigger',
|
|
ICAL.Duration.fromSeconds(
|
|
alarms[i].trigger
|
|
)
|
|
);
|
|
trigger.setParameter('relative', 'START');
|
|
component.addSubcomponent(valarm);
|
|
|
|
// Check if we need to mirror the VALARM onto email
|
|
if (this.mirrorAlarms(account)) {
|
|
valarm = new ICAL.Component('valarm');
|
|
valarm.addPropertyWithValue('action', 'EMAIL');
|
|
valarm.addPropertyWithValue('description',
|
|
'This is an event reminder');
|
|
valarm.addPropertyWithValue('ATTENDEE', account.user);
|
|
trigger = valarm.addPropertyWithValue('trigger',
|
|
ICAL.Duration.fromSeconds(
|
|
alarms[i].trigger
|
|
)
|
|
);
|
|
trigger.setParameter('relative', 'START');
|
|
component.addSubcomponent(valarm);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update absolute alarm times when the startDate changes.
|
|
*
|
|
* @param {ICAL.Time} originalDate of the event.
|
|
* @param {ICAL.Event} event to update.
|
|
*/
|
|
adjustAbsoluteAlarms: function(originalDate, event) {
|
|
var alarms = event.component.getAllSubcomponents('valarm');
|
|
|
|
alarms.forEach(function(alarm) {
|
|
var trigger = alarm.getFirstProperty('trigger');
|
|
var value = trigger.getValues()[0].clone();
|
|
|
|
// absolute time
|
|
if (value instanceof ICAL.Time) {
|
|
// find absolute time difference
|
|
var diff = value.subtractDateTz(originalDate);
|
|
trigger.setValue(diff);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Yahoo needs us to mirror all alarms as EMAIL alarms
|
|
*/
|
|
mirrorAlarms: function(account) {
|
|
return account && account.domain === 'https://caldav.calendar.yahoo.com';
|
|
},
|
|
|
|
createEvent: function(account, calendar, event, callback) {
|
|
var connection = this._createConnection(account);
|
|
var vcalendar = new ICAL.Component('vcalendar');
|
|
var icalEvent = new ICAL.Event();
|
|
|
|
// vcalendar details
|
|
vcalendar.addPropertyWithValue('prodid', this.icalProductId);
|
|
vcalendar.addPropertyWithValue('version', this.icalVersion);
|
|
|
|
// text fields
|
|
icalEvent.uid = uuid();
|
|
icalEvent.summary = event.title;
|
|
icalEvent.description = event.description;
|
|
icalEvent.location = event.location;
|
|
icalEvent.sequence = 1;
|
|
|
|
// time fields
|
|
icalEvent.startDate = this.formatInputTime(event.start);
|
|
icalEvent.endDate = this.formatInputTime(event.end);
|
|
|
|
// alarms
|
|
this.addAlarms(icalEvent.component, event.alarms, account);
|
|
|
|
vcalendar.addSubcomponent(icalEvent.component);
|
|
|
|
var url = calendar.url + icalEvent.uid + '.ics';
|
|
var req = this._assetRequest(connection, url);
|
|
|
|
event.id = icalEvent.uid;
|
|
event.url = url;
|
|
event.icalComponent = vcalendar.toString();
|
|
|
|
req.put({}, event.icalComponent, function(err, data, xhr) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var token = xhr.getResponseHeader('Etag');
|
|
event.syncToken = token;
|
|
// TODO: error handling
|
|
callback(err, event);
|
|
});
|
|
|
|
},
|
|
|
|
/**
|
|
* Updates a single caldav event.
|
|
* Will handle updating both primary events and exceptions.
|
|
*
|
|
* @param {Object} account full account details.
|
|
* @param {Object} calendar full calendar details.
|
|
* @param {Object} eventDetails details to update the event.
|
|
* unmodified parsed ical component. (VCALENDAR).
|
|
*/
|
|
updateEvent: function(account, calendar, eventDetails, callback) {
|
|
var connection = this._createConnection(account);
|
|
|
|
var icalComponent = eventDetails.icalComponent;
|
|
var event = eventDetails.event;
|
|
|
|
var self = this;
|
|
var req = this._assetRequest(connection, event.url);
|
|
|
|
// parse event
|
|
this.parseEvent(icalComponent, function(err, icalEvent) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var target = icalEvent;
|
|
var vcalendar = icalEvent.component.parent;
|
|
var originalStartDate = target.startDate;
|
|
|
|
// find correct event
|
|
if (event.recurrenceId) {
|
|
var rid = self.formatInputTime(event.recurrenceId);
|
|
rid = rid.toString();
|
|
if (icalEvent.exceptions[rid]) {
|
|
target = icalEvent.exceptions[rid];
|
|
}
|
|
}
|
|
|
|
// vcalendar pieces
|
|
vcalendar.updatePropertyWithValue('prodid', self.icalProductId);
|
|
|
|
// text fields
|
|
target.summary = event.title;
|
|
target.description = event.description;
|
|
target.location = event.location;
|
|
|
|
if (!target.sequence) {
|
|
target.sequence = 0;
|
|
}
|
|
|
|
target.sequence = parseInt(target.sequence, 10) + 1;
|
|
|
|
// time fields
|
|
target.startDate = self.formatInputTime(
|
|
event.start
|
|
);
|
|
|
|
target.endDate = self.formatInputTime(
|
|
event.end
|
|
);
|
|
|
|
// adjust absolute alarm time ( we do this before adding/changing our
|
|
// new alarm times )
|
|
self.adjustAbsoluteAlarms(originalStartDate, target);
|
|
|
|
// We generally want to remove all 'DISPLAY' alarms
|
|
// UNLESS we are dealing with a YAHOO account
|
|
// Then we overwrite all alarms
|
|
var alarms = target.component.getAllSubcomponents('valarm');
|
|
alarms.forEach(function(alarm) {
|
|
var action = alarm.getFirstPropertyValue('action');
|
|
if (action === 'DISPLAY' || self.mirrorAlarms(account)) {
|
|
target.component.removeSubcomponent(alarm);
|
|
}
|
|
});
|
|
self.addAlarms(target.component, event.alarms, account);
|
|
|
|
var vcal = target.component.parent.toString();
|
|
event.icalComponent = vcal;
|
|
|
|
req.put({}, vcal, function(err, data, xhr) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var token = xhr.getResponseHeader('Etag');
|
|
event.syncToken = token;
|
|
// TODO: error handling
|
|
callback(err, event);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
});
|
|
|