Bug 1546606 - Refactor CalDAV request handling. r=pmorris
MozReview-Commit-ID: 81oND0JtEFK
This commit is contained in:
Родитель
d7ac1094d1
Коммит
e4cb801505
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -6,8 +6,7 @@ var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|||
var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
|
||||
var { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
|
||||
|
||||
var xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
var MIME_TEXT_XML = "text/xml; charset=utf-8";
|
||||
ChromeUtils.import("resource:///modules/caldav/calDavRequest.jsm");
|
||||
|
||||
/**
|
||||
* This is a handler for the etag request in calDavCalendar.js' getUpdatedItem.
|
||||
|
@ -176,7 +175,9 @@ etagsHandler.prototype = {
|
|||
if (this.calendar.verboseLogging()) {
|
||||
this.logXML += aValue;
|
||||
}
|
||||
this.currentResponse[this.tag] += aValue;
|
||||
if (this.tag) {
|
||||
this.currentResponse[this.tag] += aValue;
|
||||
}
|
||||
},
|
||||
|
||||
startDocument() {
|
||||
|
@ -312,7 +313,7 @@ webDavSyncHandler.prototype = {
|
|||
doWebDAVSync() {
|
||||
if (this.calendar.mDisabled) {
|
||||
// check if maybe our calendar has become available
|
||||
this.calendar.setupAuthentication(this.changeLogListener);
|
||||
this.calendar.checkDavResourceType(this.changeLogListener);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -323,7 +324,7 @@ webDavSyncHandler.prototype = {
|
|||
}
|
||||
|
||||
let queryXml =
|
||||
xmlHeader +
|
||||
XML_HEADER +
|
||||
'<sync-collection xmlns="DAV:">' +
|
||||
syncTokenString +
|
||||
"<sync-level>1</sync-level>" +
|
||||
|
@ -339,32 +340,32 @@ webDavSyncHandler.prototype = {
|
|||
cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml);
|
||||
}
|
||||
cal.LOG("CalDAV: webdav-sync Token: " + this.calendar.mWebdavSyncToken);
|
||||
this.calendar.sendHttpRequest(
|
||||
let request = new LegacySAXRequest(
|
||||
this.calendar.session,
|
||||
this.calendar,
|
||||
requestUri,
|
||||
queryXml,
|
||||
MIME_TEXT_XML,
|
||||
null,
|
||||
this,
|
||||
channel => {
|
||||
// The depth header adheres to an older version of the webdav-sync
|
||||
// spec and has been replaced by the <sync-level> tag above.
|
||||
// Unfortunately some servers still depend on the depth header,
|
||||
// therefore we send both (yuck).
|
||||
channel.setRequestHeader("Depth", "1", false);
|
||||
|
||||
channel.requestMethod = "REPORT";
|
||||
return this;
|
||||
},
|
||||
() => {
|
||||
// Something went wrong with the OAuth token, notify failure
|
||||
if (this.calendar.isCached && this.changeLogListener) {
|
||||
this.changeLogListener.onResult(
|
||||
{ status: Cr.NS_ERROR_NOT_AVAILABLE },
|
||||
Cr.NS_ERROR_NOT_AVAILABLE
|
||||
);
|
||||
}
|
||||
},
|
||||
false
|
||||
}
|
||||
);
|
||||
|
||||
request.commit().catch(() => {
|
||||
// Something went wrong with the OAuth token, notify failure
|
||||
if (this.calendar.isCached && this.changeLogListener) {
|
||||
this.changeLogListener.onResult(
|
||||
{ status: Cr.NS_ERROR_NOT_AVAILABLE },
|
||||
Cr.NS_ERROR_NOT_AVAILABLE
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -713,7 +714,7 @@ multigetSyncHandler.prototype = {
|
|||
doMultiGet() {
|
||||
if (this.calendar.mDisabled) {
|
||||
// check if maybe our calendar has become available
|
||||
this.calendar.setupAuthentication(this.changeLogListener);
|
||||
this.calendar.checkDavResourceType(this.changeLogListener);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -728,7 +729,7 @@ multigetSyncHandler.prototype = {
|
|||
}
|
||||
|
||||
let queryXml =
|
||||
xmlHeader +
|
||||
XML_HEADER +
|
||||
'<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">' +
|
||||
"<D:prop>" +
|
||||
"<D:getetag/>" +
|
||||
|
@ -741,27 +742,28 @@ multigetSyncHandler.prototype = {
|
|||
if (this.calendar.verboseLogging()) {
|
||||
cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml);
|
||||
}
|
||||
this.calendar.sendHttpRequest(
|
||||
let request = new LegacySAXRequest(
|
||||
this.calendar.session,
|
||||
this.calendar,
|
||||
requestUri,
|
||||
queryXml,
|
||||
MIME_TEXT_XML,
|
||||
null,
|
||||
this,
|
||||
channel => {
|
||||
channel.requestMethod = "REPORT";
|
||||
channel.setRequestHeader("Depth", "1", false);
|
||||
return this;
|
||||
},
|
||||
() => {
|
||||
// Something went wrong with the OAuth token, notify failure
|
||||
if (this.calendar.isCached && this.changeLogListener) {
|
||||
this.changeLogListener.onResult(
|
||||
{ status: Cr.NS_ERROR_NOT_AVAILABLE },
|
||||
Cr.NS_ERROR_NOT_AVAILABLE
|
||||
);
|
||||
}
|
||||
},
|
||||
false
|
||||
}
|
||||
);
|
||||
|
||||
request.commit().catch(() => {
|
||||
// Something went wrong with the OAuth token, notify failure
|
||||
if (this.calendar.isCached && this.changeLogListener) {
|
||||
this.changeLogListener.onResult(
|
||||
{ status: Cr.NS_ERROR_NOT_AVAILABLE },
|
||||
Cr.NS_ERROR_NOT_AVAILABLE
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -859,8 +861,8 @@ multigetSyncHandler.prototype = {
|
|||
/**
|
||||
* @see nsISAXErrorHandler
|
||||
*/
|
||||
fatalError() {
|
||||
cal.WARN("CalDAV: Fatal Error doing multiget for " + this.calendar.name);
|
||||
fatalError(error) {
|
||||
cal.WARN("CalDAV: Fatal Error doing multiget for " + this.calendar.name + ": " + error);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -870,7 +872,9 @@ multigetSyncHandler.prototype = {
|
|||
if (this.calendar.verboseLogging()) {
|
||||
this.logXML += aValue;
|
||||
}
|
||||
this.currentResponse[this.tag] += aValue;
|
||||
if (this.tag) {
|
||||
this.currentResponse[this.tag] += aValue;
|
||||
}
|
||||
},
|
||||
|
||||
startDocument() {
|
||||
|
@ -930,7 +934,6 @@ multigetSyncHandler.prototype = {
|
|||
resp.status.indexOf(" 404") > 0
|
||||
) {
|
||||
if (this.calendar.mHrefIndex[resp.href]) {
|
||||
this.changeCount++;
|
||||
this.calendar.deleteTargetCalendarItem(resp.href);
|
||||
} else {
|
||||
cal.LOG("CalDAV: skipping unfound deleted item : " + resp.href);
|
||||
|
@ -952,7 +955,6 @@ multigetSyncHandler.prototype = {
|
|||
oldEtag = null;
|
||||
}
|
||||
if (!oldEtag || oldEtag != resp.getetag) {
|
||||
this.changeCount++;
|
||||
this.calendar.addTargetCalendarItem(
|
||||
resp.href,
|
||||
resp.calendardata,
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,345 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
ChromeUtils.import("resource://gre/modules/Timer.jsm");
|
||||
|
||||
ChromeUtils.import("resource:///modules/OAuth2.jsm");
|
||||
|
||||
ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
|
||||
|
||||
/**
|
||||
* Session and authentication tools for the caldav provider
|
||||
*/
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["CalDavSession"]; /* exported CalDavSession */
|
||||
|
||||
const OAUTH_GRACE_TIME = 30 * 1000;
|
||||
|
||||
/**
|
||||
* Authentication provider for Google's OAuth.
|
||||
*/
|
||||
class CalDavGoogleOAuth extends OAuth2 {
|
||||
/**
|
||||
* Constructs a new Google OAuth autentication provider
|
||||
*
|
||||
* @param {String} sessionId The session id, used in the password manager
|
||||
* @param {String} name The user-readable description of this session
|
||||
*/
|
||||
constructor(sessionId, name) {
|
||||
super(OAUTH_BASE_URI, OAUTH_SCOPE, OAUTH_CLIENT_ID, OAUTH_HASH);
|
||||
|
||||
this.id = sessionId;
|
||||
this.pwMgrId = "Google CalDAV v2";
|
||||
this.requestWindowTitle = cal.l10n.getAnyString(
|
||||
"global",
|
||||
"commonDialogs",
|
||||
"EnterUserPasswordFor2",
|
||||
[name]
|
||||
);
|
||||
|
||||
this.requestWindowFeatures = "chrome,private,centerscreen,width=430,height=600";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the token has expired, or will expire within the grace time
|
||||
*/
|
||||
get tokenExpired() {
|
||||
let now = new Date().getTime();
|
||||
return this.tokenExpires - OAUTH_GRACE_TIME < now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the refresh token from the password manager. The token is cached.
|
||||
*/
|
||||
get refreshToken() {
|
||||
if (!this._refreshToken) {
|
||||
let pass = { value: null };
|
||||
try {
|
||||
let origin = "oauth:" + this.id;
|
||||
cal.auth.passwordManagerGet(this.id, pass, origin, this.pwMgrId);
|
||||
} catch (e) {
|
||||
// User might have cancelled the master password prompt, thats ok
|
||||
if (e.result != Cr.NS_ERROR_ABORT) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this._refreshToken = pass.value;
|
||||
}
|
||||
return this._refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the refresh token in the password manager
|
||||
* @param {String} aVal The value to set
|
||||
*/
|
||||
set refreshToken(aVal) {
|
||||
try {
|
||||
let origin = "oauth:" + this.id;
|
||||
if (aVal) {
|
||||
cal.auth.passwordManagerSave(this.id, aVal, origin, this.pwMgrId);
|
||||
} else {
|
||||
cal.auth.passwordManagerRemove(this.id, origin, this.pwMgrId);
|
||||
}
|
||||
} catch (e) {
|
||||
// User might have cancelled the master password prompt, thats ok
|
||||
if (e.result != Cr.NS_ERROR_ABORT) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return (this._refreshToken = aVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the calendar window to appear.
|
||||
*
|
||||
* This is a workaround for bug 901329: If the calendar window isn't loaded yet the master
|
||||
* password prompt will show just the buttons and possibly hang. If we postpone until the window
|
||||
* is loaded, all is well.
|
||||
*
|
||||
* @return {Promise} A promise resolved without value when the window is loaded
|
||||
*/
|
||||
waitForCalendarWindow() {
|
||||
return new Promise(resolve => {
|
||||
// eslint-disable-next-line func-names, require-jsdoc
|
||||
function postpone() {
|
||||
let win = cal.window.getCalendarWindow();
|
||||
if (!win || win.document.readyState != "complete") {
|
||||
setTimeout(postpone, 0);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
setTimeout(postpone, 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Promisified version of |connect|, using all means necessary to gracefully display the
|
||||
* authentication prompt.
|
||||
*
|
||||
* @param {Boolean} aWithUI If UI should be shown for authentication
|
||||
* @param {Boolean} aRefresh Force refresh the token TODO default false
|
||||
* @return {Promise} A promise resolved when the OAuth process is completed
|
||||
*/
|
||||
promiseConnect(aWithUI = true, aRefresh = true) {
|
||||
return this.waitForCalendarWindow().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let self = this;
|
||||
let asyncprompter = Cc["@mozilla.org/messenger/msgAsyncPrompter;1"].getService(
|
||||
Ci.nsIMsgAsyncPrompter
|
||||
);
|
||||
asyncprompter.queueAsyncAuthPrompt(this.id, false, {
|
||||
onPromptStartAsync(callback) {
|
||||
this.onPromptAuthAvailable(callback);
|
||||
},
|
||||
|
||||
onPromptAuthAvailable(callback) {
|
||||
self.connect(
|
||||
() => {
|
||||
if (callback) {
|
||||
callback.onAuthResult(true);
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
() => {
|
||||
if (callback) {
|
||||
callback.onAuthResult(false);
|
||||
}
|
||||
reject();
|
||||
},
|
||||
aWithUI,
|
||||
aRefresh
|
||||
);
|
||||
},
|
||||
onPromptCanceled: reject,
|
||||
onPromptStart() {},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the given channel for an OAuth request
|
||||
*
|
||||
* @param {nsIChannel} aChannel The channel to prepare
|
||||
*/
|
||||
async prepareRequest(aChannel) {
|
||||
if (!this.accessToken || this.tokenExpired) {
|
||||
// The token has expired, we need to reauthenticate first
|
||||
cal.LOG("CalDAV: OAuth token expired or empty, refreshing");
|
||||
await this.promiseConnect();
|
||||
}
|
||||
|
||||
let hdr = "Bearer " + this.accessToken;
|
||||
aChannel.setRequestHeader("Authorization", hdr, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the redirect, copying the auth header to the new channel
|
||||
*
|
||||
* @param {nsIChannel} aOldChannel The old channel that is being redirected
|
||||
* @param {nsIChannel} aNewChannel The new channel to prepare
|
||||
*/
|
||||
async prepareRedirect(aOldChannel, aNewChannel) {
|
||||
try {
|
||||
let hdrValue = aOldChannel.getRequestHeader("WWW-Authenticate");
|
||||
if (hdrValue) {
|
||||
aNewChannel.setRequestHeader("WWW-Authenticate", hdrValue, false);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
|
||||
// The header could possibly not be availible, ignore that
|
||||
// case but throw otherwise
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for OAuth auth errors and restart the request without a token if necessary
|
||||
*
|
||||
* @param {CalDavResponse} aResponse The response to inspect for completion
|
||||
* @return {Promise} A promise resolved when complete, with
|
||||
* CalDavSession.RESTART_REQUEST or null
|
||||
*/
|
||||
async completeRequest(aResponse) {
|
||||
// Check for OAuth errors
|
||||
let wwwauth = aResponse.getHeader("WWW-Authenticate");
|
||||
if (this.oauth && wwwauth && wwwauth.startsWith("Bearer") && wwwauth.includes("error=")) {
|
||||
this.oauth.accessToken = null;
|
||||
|
||||
return CalDavSession.RESTART_REQUEST;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A session for the caldav provider. Two or more calendars can share a session if they have the
|
||||
* same auth credentials.
|
||||
*/
|
||||
class CalDavSession {
|
||||
QueryInterface(aIID) {
|
||||
return cal.generateClassQI(this, aIID, [Ci.nsIInterfaceRequestor]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant returned by |completeRequest| when the request should be restarted
|
||||
* @return {Number} The constant
|
||||
*/
|
||||
static get RESTART_REQUEST() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new caldav session
|
||||
*
|
||||
* @param {String} aSessionId The session id, used in the password manager
|
||||
* @param {String} aName The user-readable description of this session
|
||||
*/
|
||||
constructor(aSessionId, aName) {
|
||||
this.id = aSessionId;
|
||||
this.name = aName;
|
||||
|
||||
// There is only one right now, but for better separation this is ready for more oauth hosts
|
||||
/* eslint-disable object-curly-newline */
|
||||
this.authAdapters = {
|
||||
"apidata.googleusercontent.com": new CalDavGoogleOAuth(aSessionId, aName),
|
||||
};
|
||||
/* eslint-enable object-curly-newline */
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement nsIInterfaceRequestor. The base class has no extra interfaces, but a subclass of
|
||||
* the session may.
|
||||
*
|
||||
* @param {nsIIDRef} aIID The IID of the interface being requested
|
||||
* @return {?*} Either this object QI'd to the IID, or null.
|
||||
* Components.returnCode is set accordingly.
|
||||
*/
|
||||
getInterface(aIID) {
|
||||
try {
|
||||
// Try to query the this object for the requested interface but don't
|
||||
// throw if it fails since that borks the network code.
|
||||
return this.QueryInterface(aIID);
|
||||
} catch (e) {
|
||||
Components.returnCode = e;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the auth adapter for the given host in case it exists. This allows delegating auth
|
||||
* preparation based on the host, e.g. for OAuth.
|
||||
*
|
||||
* @param {String} aHost The host to check the auth adapter for
|
||||
* @param {String} aMethod The method to call
|
||||
* @param {...*} aArgs Remaining args specific to the adapted method
|
||||
* @return {*} Return value specific to the adapter method
|
||||
*/
|
||||
async _callAdapter(aHost, aMethod, ...aArgs) {
|
||||
let adapter = this.authAdapters[aHost] || null;
|
||||
if (adapter) {
|
||||
return adapter[aMethod](...aArgs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the channel for a request, e.g. setting custom authentication headers
|
||||
*
|
||||
* @param {nsIChannel} aChannel The channel to prepare
|
||||
* @return {Promise} A promise resolved when the preparations are complete
|
||||
*/
|
||||
async prepareRequest(aChannel) {
|
||||
return this._callAdapter(aChannel.URI.host, "prepareRequest", aChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the given new channel for a redirect, e.g. copying headers.
|
||||
*
|
||||
* @param {nsIChannel} aOldChannel The old channel that is being redirected
|
||||
* @param {nsIChannel} aNewChannel The new channel to prepare
|
||||
* @return {Promise} A promise resolved when the preparations are complete
|
||||
*/
|
||||
async prepareRedirect(aOldChannel, aNewChannel) {
|
||||
return this._callAdapter(aNewChannel.URI.host, "prepareRedirect", aOldChannel, aNewChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the request based on the results from the response. Allows restarting the session if
|
||||
* |CalDavSession.RESTART_REQUEST| is returned.
|
||||
*
|
||||
* @param {CalDavResponse} aResponse The response to inspect for completion
|
||||
* @return {Promise} A promise resolved when complete, with
|
||||
* CalDavSession.RESTART_REQUEST or null
|
||||
*/
|
||||
async completeRequest(aResponse) {
|
||||
return this._callAdapter(aResponse.request.uri.host, "completeRequest", aResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Before you spend time trying to find out what this means, please note that
|
||||
// doing so and using the information WILL cause Google to revoke Lightning's
|
||||
// privileges, which means not one Lightning user will be able to connect to
|
||||
// Google Calendar via CalDAV. This will cause unhappy users all around which
|
||||
// means that the Lightning developers will have to spend more time with user
|
||||
// support, which means less time for features, releases and bugfixes. For a
|
||||
// paid developer this would actually mean financial harm.
|
||||
//
|
||||
// Do you really want all of this to be your fault? Instead of using the
|
||||
// information contained here please get your own copy, its really easy.
|
||||
/* eslint-disable */
|
||||
// prettier-ignore
|
||||
(zqdx=>{zqdx["\x65\x76\x61\x6C"](zqdx["\x41\x72\x72\x61\x79"]["\x70\x72\x6F\x74"+
|
||||
"\x6F\x74\x79\x70\x65"]["\x6D\x61\x70"]["\x63\x61\x6C\x6C"]("uijt/PBVUI`CBTF`VS"+
|
||||
"J>#iuuqt;00bddpvout/hpphmf/dpn0p0#<uijt/PBVUI`TDPQF>#iuuqt;00xxx/hpphmfbqjt/dp"+
|
||||
"n0bvui0dbmfoebs#<uijt/PBVUI`DMJFOU`JE>#831674:95649/bqqt/hpphmfvtfsdpoufou/dpn"+
|
||||
"#<uijt/PBVUI`IBTI>#zVs7YVgyvsbguj7s8{1TTfJR#<",_=>zqdx["\x53\x74\x72\x69\x6E"+
|
||||
"\x67"]["\x66\x72\x6F\x6D\x43\x68\x61\x72\x43\x6F\x64\x65"](_["\x63\x68\x61\x72"+
|
||||
"\x43\x6F\x64\x65\x41\x74"](0)-1),this)[""+"\x6A\x6F\x69\x6E"](""))})["\x63\x61"+
|
||||
"\x6C\x6C"]((this),Components["\x75\x74\x69\x6c\x73"]["\x67\x65\x74\x47\x6c\x6f"+
|
||||
"\x62\x61\x6c\x46\x6f\x72\x4f\x62\x6a\x65\x63\x74"](this))
|
||||
/* eslint-enable */
|
|
@ -0,0 +1,108 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* Various utility functions for the caldav provider
|
||||
*/
|
||||
|
||||
/* exported xmlns, tagsToXmlns, caldavNSUnresolver, caldavNSResolver, caldavXPath,
|
||||
* caldavXPathFirst */
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"xmlns",
|
||||
"tagsToXmlns",
|
||||
"caldavNSUnresolver",
|
||||
"caldavNSResolver",
|
||||
"caldavXPath",
|
||||
"caldavXPathFirst",
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates an xmlns string with the requested namespace prefixes
|
||||
*
|
||||
* @param {...String} aRequested The requested namespace prefixes
|
||||
* @return {String} An xmlns string that can be inserted into xml documents
|
||||
*/
|
||||
function xmlns(...aRequested) {
|
||||
let namespaces = [];
|
||||
for (let namespace of aRequested) {
|
||||
let nsUri = caldavNSResolver(namespace);
|
||||
if (namespace) {
|
||||
namespaces.push(`xmlns:${namespace}='${nsUri}'`);
|
||||
}
|
||||
}
|
||||
|
||||
return namespaces.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to gather namespaces from QNames or namespace prefixes, plus a few extra for the
|
||||
* remaining request.
|
||||
*
|
||||
* @param {...String} aTags Either QNames, or just namespace prefixes to be resolved.
|
||||
* @return {String} The complete namespace string
|
||||
*/
|
||||
function tagsToXmlns(...aTags) {
|
||||
let namespaces = new Set(aTags.map(tag => tag.split(":")[0]));
|
||||
return xmlns(...namespaces.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the namespace URI to one of the prefixes used in our codebase
|
||||
*
|
||||
* @param {String} aNamespace The namespace URI to resolve
|
||||
* @return {?String} The namespace prefix we use
|
||||
*/
|
||||
function caldavNSUnresolver(aNamespace) {
|
||||
const prefixes = {
|
||||
"http://apple.com/ns/ical/": "A",
|
||||
"DAV:": "D",
|
||||
"urn:ietf:params:xml:ns:caldav": "C",
|
||||
"http://calendarserver.org/ns/": "CS",
|
||||
};
|
||||
return prefixes[aNamespace] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the namespace URI from one of the prefixes used in our codebase
|
||||
*
|
||||
* @param {String} aPrefix The namespace prefix we use
|
||||
* @return {?String} The namespace URI for the prefix
|
||||
*/
|
||||
function caldavNSResolver(aPrefix) {
|
||||
/* eslint-disable id-length */
|
||||
const namespaces = {
|
||||
A: "http://apple.com/ns/ical/",
|
||||
D: "DAV:",
|
||||
C: "urn:ietf:params:xml:ns:caldav",
|
||||
CS: "http://calendarserver.org/ns/",
|
||||
};
|
||||
/* eslint-enable id-length */
|
||||
|
||||
return namespaces[aPrefix] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an xpath expression on the given node, using the caldav namespace resolver
|
||||
*
|
||||
* @param {Element} aNode The context node to search from
|
||||
* @param {String} aExpr The XPath expression to search for
|
||||
* @param {?XPathResult} aType (optional) Force a result type, must be an XPathResult constant
|
||||
* @return {Element[]} Array of found elements
|
||||
*/
|
||||
function caldavXPath(aNode, aExpr, aType) {
|
||||
return cal.xml.evalXPath(aNode, aExpr, caldavNSResolver, aType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an xpath expression on the given node, using the caldav namespace resolver. Returns the first
|
||||
* result.
|
||||
*
|
||||
* @param {Element} aNode The context node to search from
|
||||
* @param {String} aExpr The XPath expression to search for
|
||||
* @param {?XPathResult} aType (optional) Force a result type, must be an XPathResult constant
|
||||
* @return {?Element} The found element, or null.
|
||||
*/
|
||||
function caldavXPathFirst(aNode, aExpr, aType) {
|
||||
return cal.xml.evalXPathFirst(aNode, aExpr, caldavNSResolver, aType);
|
||||
}
|
|
@ -13,6 +13,12 @@ XPCOM_MANIFESTS += [
|
|||
'components.conf',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.caldav += [
|
||||
'modules/calDavRequest.jsm',
|
||||
'modules/calDavSession.jsm',
|
||||
'modules/calDavUtils.jsm',
|
||||
]
|
||||
|
||||
# These files go in components so they can be packaged correctly.
|
||||
FINAL_TARGET_FILES.components += [
|
||||
'calDavRequestHandlers.js',
|
||||
|
|
|
@ -196,26 +196,36 @@ function ics_unfoldline(aLine) {
|
|||
*/
|
||||
function dedent(strings, ...values) {
|
||||
let parts = [];
|
||||
|
||||
// Perform variable interpolation
|
||||
let minIndent = Infinity;
|
||||
for (let [i, string] of strings.entries()) {
|
||||
parts.push(string);
|
||||
if (i < values.length) {
|
||||
parts.push(values[i]);
|
||||
let innerparts = string.split("\n");
|
||||
if (i == 0) {
|
||||
innerparts.shift();
|
||||
}
|
||||
if (i == strings.length - 1) {
|
||||
innerparts.pop();
|
||||
}
|
||||
for (let [j, ip] of innerparts.entries()) {
|
||||
let match = ip.match(/^(\s*)\S*/);
|
||||
if (j != 0) {
|
||||
minIndent = Math.min(minIndent, match[1].length);
|
||||
}
|
||||
}
|
||||
parts.push(innerparts);
|
||||
}
|
||||
let lines = parts.join("").split("\n");
|
||||
|
||||
// The first and last line is empty as in above example.
|
||||
lines.shift();
|
||||
lines.pop();
|
||||
|
||||
let minIndent = lines.reduce((min, line) => {
|
||||
let match = line.match(/^(\s*)\S*/);
|
||||
return Math.min(min, match[1].length);
|
||||
}, Infinity);
|
||||
|
||||
return lines.map(line => line.substr(minIndent)).join("\n");
|
||||
return parts
|
||||
.map((part, i) => {
|
||||
return (
|
||||
part
|
||||
.map((line, j) => {
|
||||
return j == 0 && i > 0 ? line : line.substr(minIndent);
|
||||
})
|
||||
.join("\n") + (i < values.length ? values[i] : "")
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -280,3 +290,27 @@ function do_calendar_startup(callback) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monkey patch the function with the name x on obj and overwrite it with func.
|
||||
* The first parameter of this function is the original function that can be
|
||||
* called at any time.
|
||||
*
|
||||
* @param obj The object the function is on.
|
||||
* @param name The string name of the function.
|
||||
* @param func The function to monkey patch with.
|
||||
*/
|
||||
function monkeyPatch(obj, x, func) {
|
||||
let old = obj[x];
|
||||
obj[x] = function() {
|
||||
let parent = old.bind(obj);
|
||||
let args = Array.from(arguments);
|
||||
args.unshift(parent);
|
||||
try {
|
||||
return func.apply(obj, args);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,890 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
ChromeUtils.import("resource://testing-common/httpd.js");
|
||||
ChromeUtils.import("resource://testing-common/MockRegistrar.jsm");
|
||||
|
||||
ChromeUtils.import("resource:///modules/caldav/calDavSession.jsm");
|
||||
ChromeUtils.import("resource:///modules/caldav/calDavRequest.jsm");
|
||||
ChromeUtils.import("resource:///modules/caldav/calDavUtils.jsm");
|
||||
|
||||
Components.utils.importGlobalProperties(["URL"]);
|
||||
|
||||
class LowerMap extends Map {
|
||||
get(key) {
|
||||
return super.get(key.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
var gServer;
|
||||
|
||||
var MockConflictPrompt = {
|
||||
_origFunc: null,
|
||||
overwrite: false,
|
||||
register: function() {
|
||||
if (!this._origFunc) {
|
||||
this._origFunc = cal.provider.promptOverwrite;
|
||||
cal.provider.promptOverwrite = (aMode, aItem) => {
|
||||
return this.overwrite;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
unregister: function() {
|
||||
if (this._origFunc) {
|
||||
cal.provider.promptOverwrite = this._origFunc;
|
||||
this._origFunc = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
class MockAlertsService {
|
||||
QueryInterface(aIID) {
|
||||
return cal.generateClassQI(this, aIID, [Ci.nsIAlertsService]);
|
||||
}
|
||||
showAlertNotification() {}
|
||||
}
|
||||
|
||||
function replaceAlertsService() {
|
||||
let originalAlertsServiceCID = MockRegistrar.register(
|
||||
"@mozilla.org/alerts-service;1",
|
||||
MockAlertsService
|
||||
);
|
||||
registerCleanupFunction(() => {
|
||||
MockRegistrar.unregister(originalAlertsServiceCID);
|
||||
});
|
||||
}
|
||||
|
||||
var gMockCalendar = { name: "xpcshell" };
|
||||
|
||||
class CalDavServer {
|
||||
constructor(calendarId) {
|
||||
this.server = new HttpServer();
|
||||
this.calendarId = calendarId;
|
||||
this.session = new CalDavSession(this.calendarId, "xpcshell");
|
||||
this.serverRequests = {};
|
||||
|
||||
this.server.registerPrefixHandler(
|
||||
"/principals/",
|
||||
this.router.bind(this, this.principals.bind(this))
|
||||
);
|
||||
this.server.registerPrefixHandler(
|
||||
"/calendars/",
|
||||
this.router.bind(this, this.calendars.bind(this))
|
||||
);
|
||||
this.server.registerPrefixHandler(
|
||||
"/requests/",
|
||||
this.router.bind(this, this.requests.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.server.start(-1);
|
||||
registerCleanupFunction(() => this.server.stop(() => {}));
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.serverRequests = {};
|
||||
}
|
||||
|
||||
uri(path) {
|
||||
let base = Services.io.newURI(`http://localhost:${this.server.identity.primaryPort}/`);
|
||||
return Services.io.newURI(path, null, base);
|
||||
}
|
||||
|
||||
router(nextHandler, request, response) {
|
||||
try {
|
||||
let method = request.method;
|
||||
let parameters = new Map(request.queryString.split("&").map(part => part.split("=", 2)));
|
||||
let available = request.bodyInputStream.available();
|
||||
let body =
|
||||
available > 0 ? NetUtil.readInputStreamToString(request.bodyInputStream, available) : null;
|
||||
|
||||
let headers = new LowerMap();
|
||||
for (let hdr of XPCOMUtils.IterSimpleEnumerator(request.headers, Ci.nsISupportsString)) {
|
||||
headers.set(hdr.data, request.getHeader(hdr.data));
|
||||
}
|
||||
|
||||
return nextHandler(request, response, method, headers, parameters, body);
|
||||
} catch (e) {
|
||||
info("Server Error: " + e.fileName + ":" + e.lineNumber + ": " + e + "\n");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
resetClient(client) {
|
||||
MockConflictPrompt.unregister();
|
||||
cal.getCalendarManager().unregisterCalendar(client);
|
||||
}
|
||||
|
||||
waitForLoad(aCalendar) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let observer = cal.createAdapter(Components.interfaces.calIObserver, {
|
||||
onLoad: function() {
|
||||
let uncached = aCalendar.wrappedJSObject.mUncachedCalendar.wrappedJSObject;
|
||||
aCalendar.removeObserver(observer);
|
||||
|
||||
if (Components.isSuccessCode(uncached._lastStatus)) {
|
||||
resolve(aCalendar);
|
||||
} else {
|
||||
reject(uncached._lastMessage);
|
||||
}
|
||||
},
|
||||
});
|
||||
aCalendar.addObserver(observer);
|
||||
});
|
||||
}
|
||||
|
||||
getClient() {
|
||||
let uri = this.uri("/calendars/xpcshell/events");
|
||||
let calmgr = cal.getCalendarManager();
|
||||
let client = calmgr.createCalendar("caldav", uri);
|
||||
let uclient = client.wrappedJSObject;
|
||||
client.name = "xpcshell";
|
||||
client.setProperty("cache.enabled", true);
|
||||
|
||||
// Make sure we catch the last error message in case sync fails
|
||||
monkeyPatch(uclient, "replayChangesOn", (protofunc, aListener) => {
|
||||
protofunc({
|
||||
onResult: function(operation, detail) {
|
||||
uclient._lastStatus = operation.status;
|
||||
uclient._lastMessage = detail;
|
||||
aListener.onResult(operation, detail);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
calmgr.registerCalendar(client);
|
||||
|
||||
let cachedCalendar = calmgr.getCalendarById(client.id);
|
||||
return this.waitForLoad(cachedCalendar);
|
||||
}
|
||||
|
||||
principals(request, response, method, headers, parameters, body) {
|
||||
this.serverRequests.principals = { method, headers, parameters, body };
|
||||
|
||||
if (method == "REPORT" && request.path == "/principals/") {
|
||||
response.setHeader("Content-Type", "application/xml");
|
||||
response.write(dedent`
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:multistatus xmlns:D="DAV:" xmlns:B="http://BigCorp.com/ns/">
|
||||
<D:response>
|
||||
<D:href>http://www.example.com/users/jdoe</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:displayname>John Doe</D:displayname>
|
||||
<B:department>Widget Sales</B:department>
|
||||
<B:phone>234-4567</B:phone>
|
||||
<B:office>209</B:office>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<B:salary/>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 403 Forbidden</D:status>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
<D:response>
|
||||
<D:href>http://www.example.com/users/zsmith</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:displayname>Zygdoebert Smith</D:displayname>
|
||||
<B:department>Gadget Sales</B:department>
|
||||
<B:phone>234-7654</B:phone>
|
||||
<B:office>114</B:office>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<B:salary/>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 403 Forbidden</D:status>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>
|
||||
`);
|
||||
response.setStatusLine(null, 207, "Multistatus");
|
||||
} else if (method == "PROPFIND" && request.path == "/principals/xpcshell/user/") {
|
||||
response.setHeader("Content-Type", "application/xml");
|
||||
response.write(dedent`
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:response>
|
||||
<D:href>${this.uri("/principals/xpcshell/user").spec}</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<C:calendar-home-set>
|
||||
<D:href>${this.uri("/calendars/xpcshell/user/").spec}</D:href>
|
||||
</C:calendar-home-set>
|
||||
<C:calendar-user-address-set>
|
||||
<D:href>mailto:xpcshell@example.com</D:href>
|
||||
</C:calendar-user-address-set>
|
||||
<C:schedule-inbox-URL>
|
||||
<D:href>${this.uri("/calendars/xpcshell/inbox").spec}/</D:href>
|
||||
</C:schedule-inbox-URL>
|
||||
<C:schedule-outbox-URL>
|
||||
<D:href>${this.uri("/calendars/xpcshell/outbox").spec}</D:href>
|
||||
</C:schedule-outbox-URL>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>
|
||||
`);
|
||||
response.setStatusLine(null, 207, "Multistatus");
|
||||
}
|
||||
}
|
||||
|
||||
calendars(request, response, method, headers, parameters, body) {
|
||||
this.serverRequests.calendars = { method, headers, parameters, body };
|
||||
|
||||
if (
|
||||
method == "PROPFIND" &&
|
||||
request.path.startsWith("/calendars/xpcshell/events") &&
|
||||
headers.get("depth") == 0
|
||||
) {
|
||||
response.setHeader("Content-Type", "application/xml");
|
||||
response.write(dedent`
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:multistatus ${xmlns("D", "C", "CS")} xmlns:R="http://www.foo.bar/boxschema/">
|
||||
<D:response>
|
||||
<D:href>${request.path}</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:resourcetype>
|
||||
<D:collection/>
|
||||
<C:calendar/>
|
||||
</D:resourcetype>
|
||||
<R:plain-text-prop>hello, world</R:plain-text-prop>
|
||||
<D:principal-collection-set>
|
||||
<D:href>${this.uri("/principals/").spec}</D:href>
|
||||
<D:href>${this.uri("/principals/subthing/").spec}</D:href>
|
||||
</D:principal-collection-set>
|
||||
<D:current-user-principal>
|
||||
<D:href>${this.uri("/principals/xpcshell/user").spec}</D:href>
|
||||
</D:current-user-principal>
|
||||
<D:supported-report-set>
|
||||
<D:supported-report>
|
||||
<D:report>
|
||||
<D:principal-property-search/>
|
||||
</D:report>
|
||||
</D:supported-report>
|
||||
<D:supported-report>
|
||||
<D:report>
|
||||
<C:calendar-multiget/>
|
||||
</D:report>
|
||||
</D:supported-report>
|
||||
<D:supported-report>
|
||||
<D:report>
|
||||
<D:sync-collection/>
|
||||
</D:report>
|
||||
</D:supported-report>
|
||||
</D:supported-report-set>
|
||||
<C:supported-calendar-component-set>
|
||||
<C:comp name="VEVENT"/>
|
||||
<C:comp name="VTODO"/>
|
||||
</C:supported-calendar-component-set>
|
||||
<C:schedule-inbox-URL>
|
||||
<D:href>${this.uri("/calendars/xpcshell/inbox").spec}</D:href>
|
||||
</C:schedule-inbox-URL>
|
||||
<C:schedule-outbox-URL>
|
||||
${this.uri("/calendars/xpcshell/outbox").spec}
|
||||
</C:schedule-outbox-URL>
|
||||
<CS:getctag>1413647159-1007960</CS:getctag>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<R:obscure-thing-not-found/>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 404 Not Found</D:status>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>
|
||||
`);
|
||||
response.setStatusLine(null, 207, "Multistatus");
|
||||
} else if (method == "POST" && request.path == "/calendars/xpcshell/outbox") {
|
||||
response.setHeader("Content-Type", "application/xml");
|
||||
response.write(dedent`
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:schedule-response ${xmlns("D", "C")}>
|
||||
<D:response>
|
||||
<D:href>mailto:recipient1@example.com</D:href>
|
||||
<D:request-status>2.0;Success</D:request-status>
|
||||
</D:response>
|
||||
<D:response>
|
||||
<D:href>mailto:recipient2@example.com</D:href>
|
||||
<D:request-status>2.0;Success</D:request-status>
|
||||
</D:response>
|
||||
</C:schedule-response>
|
||||
`);
|
||||
response.setStatusLine(null, 200, "OK");
|
||||
} else if (method == "POST" && request.path == "/calendars/xpcshell/outbox2") {
|
||||
response.setHeader("Content-Type", "application/xml");
|
||||
response.write(dedent`
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:schedule-response ${xmlns("D", "C")}>
|
||||
<D:response>
|
||||
<D:recipient>
|
||||
<D:href>mailto:recipient1@example.com</D:href>
|
||||
</D:recipient>
|
||||
<D:request-status>2.0;Success</D:request-status>
|
||||
<C:calendar-data>
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
|
||||
VERSION:2.0
|
||||
METHOD:REQUEST
|
||||
BEGIN:VFREEBUSY
|
||||
DTSTART;VALUE=DATE:20180102
|
||||
DTEND;VALUE=DATE:20180126
|
||||
ORGANIZER:mailto:xpcshell@example.com
|
||||
ATTENDEE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL:mail
|
||||
to:recipient@example.com
|
||||
FREEBUSY;FBTYPE=FREE:20180103/20180117
|
||||
FREEBUSY;FBTYPE=BUSY:20180118/P7D
|
||||
END:VFREEBUSY
|
||||
END:VCALENDAR
|
||||
</C:calendar-data>
|
||||
</D:response>
|
||||
</C:schedule-response>
|
||||
`);
|
||||
response.setStatusLine(null, 200, "OK");
|
||||
} else if (method == "OPTIONS" && request.path == "/calendars/xpcshell/") {
|
||||
response.setHeader(
|
||||
"DAV",
|
||||
"1, 2, 3, access-control, extended-mkcol, resource-sharing, calendar-access, calendar-auto-schedule, calendar-query-extended, calendar-availability, calendarserver-sharing, inbox-availability"
|
||||
);
|
||||
response.setStatusLine(null, 200, "OK");
|
||||
} else if (method == "REPORT" && request.path == "/calendars/xpcshell/events/") {
|
||||
response.setHeader("Content-Type", "application/xml");
|
||||
let bodydom = cal.xml.parseString(body);
|
||||
let report = bodydom.documentElement.localName;
|
||||
if (report == "sync-collection") {
|
||||
response.write(dedent`
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:multistatus ${xmlns("D")}>
|
||||
<D:response>
|
||||
<D:href>${this.uri("/calendars/xpcshell/events/test.ics").spec}</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:getcontenttype>text/calendar; charset=utf-8; component=VEVENT</D:getcontenttype>
|
||||
<D:getetag>"2decee6ffb701583398996bfbdacb8eec53edf94"</D:getetag>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>
|
||||
`);
|
||||
} else if (report == "calendar-multiget") {
|
||||
let event = cal.createEvent();
|
||||
event.startDate = cal.dtz.now();
|
||||
event.endDate = cal.dtz.now();
|
||||
response.write(dedent`
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:multistatus ${xmlns("D", "C")}>
|
||||
<D:response>
|
||||
<D:href>${this.uri("/calendars/xpcshell/events/test.ics").spec}</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:getetag>"2decee6ffb701583398996bfbdacb8eec53edf94"</D:getetag>
|
||||
<C:calendar-data>${event.icalString}</C:calendar-data>
|
||||
</D:prop>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>
|
||||
`);
|
||||
}
|
||||
response.setStatusLine(null, 207, "Multistatus");
|
||||
} else {
|
||||
console.log("XXX: " + method, request.path, [...headers.entries()]);
|
||||
}
|
||||
}
|
||||
|
||||
requests(request, response, method, headers, parameters, body) {
|
||||
// ["", "requests", "generic"] := /requests/generic
|
||||
let parts = request.path.split("/");
|
||||
let id = parts[2];
|
||||
let status = parseInt(parts[3] || "", 10) || 200;
|
||||
|
||||
if (id == "redirected") {
|
||||
response.setHeader("Location", "/requests/redirected-target", false);
|
||||
status = 302;
|
||||
} else if (id == "dav") {
|
||||
response.setHeader("DAV", "1, calendar-schedule, calendar-auto-schedule");
|
||||
}
|
||||
|
||||
this.serverRequests[id] = { method, headers, parameters, body };
|
||||
|
||||
for (let [hdr, value] of headers.entries()) {
|
||||
response.setHeader(hdr, "response-" + value, false);
|
||||
}
|
||||
|
||||
response.setHeader("Content-Type", "application/xml");
|
||||
response.write(`<response id="${id}">xpc</response>`);
|
||||
response.setStatusLine(null, status, null);
|
||||
}
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
Preferences.set("calendar.debug.log", true);
|
||||
Preferences.set("calendar.debug.log.verbose", true);
|
||||
cal.console.maxLogLevel = "debug";
|
||||
replaceAlertsService();
|
||||
|
||||
// TODO: make do_calendar_startup to work with this test and replace the startup code here
|
||||
do_get_profile();
|
||||
do_test_pending();
|
||||
|
||||
cal.getCalendarManager().startup({
|
||||
onResult: function() {
|
||||
gServer = new CalDavServer("xpcshell@example.com");
|
||||
gServer.start();
|
||||
cal.getTimezoneService().startup({
|
||||
onResult: function() {
|
||||
run_next_test();
|
||||
do_test_finished();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
add_task(async function test_caldav_session() {
|
||||
gServer.reset();
|
||||
|
||||
let prepared = 0;
|
||||
let redirected = 0;
|
||||
let completed = 0;
|
||||
let restart = false;
|
||||
|
||||
gServer.session.authAdapters.localhost = {
|
||||
async prepareRequest(aChannel) {
|
||||
prepared++;
|
||||
},
|
||||
|
||||
async prepareRedirect(aOldChannel, aNewChannel) {
|
||||
redirected++;
|
||||
},
|
||||
|
||||
async completeRequest(aResponse) {
|
||||
completed++;
|
||||
if (restart) {
|
||||
restart = false;
|
||||
return CalDavSession.RESTART_REQUEST;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
// First a simple request
|
||||
let uri = gServer.uri("/requests/session");
|
||||
let request = new GenericRequest(gServer.session, gMockCalendar, "HEAD", uri);
|
||||
await request.commit();
|
||||
|
||||
equal(prepared, 1);
|
||||
equal(redirected, 0);
|
||||
equal(completed, 1);
|
||||
|
||||
// Now a redirect
|
||||
prepared = redirected = completed = 0;
|
||||
|
||||
uri = gServer.uri("/requests/redirected");
|
||||
request = new GenericRequest(gServer.session, gMockCalendar, "HEAD", uri);
|
||||
await request.commit();
|
||||
|
||||
equal(prepared, 1);
|
||||
equal(redirected, 1);
|
||||
equal(completed, 1);
|
||||
|
||||
// Now with restarting the request
|
||||
prepared = redirected = completed = 0;
|
||||
restart = true;
|
||||
|
||||
uri = gServer.uri("/requests/redirected");
|
||||
request = new GenericRequest(gServer.session, gMockCalendar, "HEAD", uri);
|
||||
await request.commit();
|
||||
|
||||
equal(prepared, 2);
|
||||
equal(redirected, 2);
|
||||
equal(completed, 2);
|
||||
});
|
||||
|
||||
/**
|
||||
* This test covers both GenericRequest and the base class CalDavRequest/CalDavResponse
|
||||
*/
|
||||
add_task(async function test_generic_request() {
|
||||
gServer.reset();
|
||||
let uri = gServer.uri("/requests/generic");
|
||||
let headers = { "X-Hdr": "exists" };
|
||||
let request = new GenericRequest(
|
||||
gServer.session,
|
||||
gMockCalendar,
|
||||
"PUT",
|
||||
uri,
|
||||
headers,
|
||||
"<body>xpc</body>",
|
||||
"text/plain"
|
||||
);
|
||||
|
||||
strictEqual(request.uri.spec, uri.spec);
|
||||
strictEqual(request.session.id, gServer.session.id);
|
||||
strictEqual(request.calendar, gMockCalendar);
|
||||
strictEqual(request.uploadData, "<body>xpc</body>");
|
||||
strictEqual(request.contentType, "text/plain");
|
||||
strictEqual(request.response, null);
|
||||
strictEqual(request.getHeader("X-Hdr"), null); // Only works after commit
|
||||
|
||||
let response = await request.commit();
|
||||
|
||||
ok(!!request.response);
|
||||
equal(request.getHeader("X-Hdr"), "exists");
|
||||
|
||||
equal(response.uri.spec, uri.spec);
|
||||
ok(!response.redirected);
|
||||
equal(response.status, 200);
|
||||
equal(response.statusCategory, 2);
|
||||
ok(response.ok);
|
||||
ok(!response.clientError);
|
||||
ok(!response.conflict);
|
||||
ok(!response.notFound);
|
||||
ok(!response.serverError);
|
||||
equal(response.text, '<response id="generic">xpc</response>');
|
||||
equal(response.xml.documentElement.localName, "response");
|
||||
equal(response.getHeader("X-Hdr"), "response-exists");
|
||||
|
||||
let serverResult = gServer.serverRequests.generic;
|
||||
|
||||
equal(serverResult.method, "PUT");
|
||||
equal(serverResult.headers.get("x-hdr"), "exists");
|
||||
equal(serverResult.headers.get("content-type"), "text/plain");
|
||||
equal(serverResult.body, "<body>xpc</body>");
|
||||
});
|
||||
|
||||
add_task(async function test_generic_redirected_request() {
|
||||
gServer.reset();
|
||||
let uri = gServer.uri("/requests/redirected");
|
||||
let headers = {
|
||||
Depth: 1,
|
||||
Originator: "o",
|
||||
Recipient: "r",
|
||||
"If-None-Match": "*",
|
||||
"If-Match": "123",
|
||||
};
|
||||
let request = new GenericRequest(
|
||||
gServer.session,
|
||||
gMockCalendar,
|
||||
"PUT",
|
||||
uri,
|
||||
headers,
|
||||
"<body>xpc</body>",
|
||||
"text/plain"
|
||||
);
|
||||
|
||||
let response = await request.commit();
|
||||
|
||||
ok(response.redirected);
|
||||
equal(response.status, 200);
|
||||
equal(response.text, '<response id="redirected-target">xpc</response>');
|
||||
equal(response.xml.documentElement.getAttribute("id"), "redirected-target");
|
||||
|
||||
ok(gServer.serverRequests.redirected);
|
||||
ok(gServer.serverRequests["redirected-target"]);
|
||||
|
||||
let results = gServer.serverRequests.redirected;
|
||||
equal(results.headers.get("Depth"), 1);
|
||||
equal(results.headers.get("Originator"), "o");
|
||||
equal(results.headers.get("Recipient"), "r");
|
||||
equal(results.headers.get("If-None-Match"), "*");
|
||||
equal(results.headers.get("If-Match"), "123");
|
||||
|
||||
results = gServer.serverRequests["redirected-target"];
|
||||
equal(results.headers.get("Depth"), 1);
|
||||
equal(results.headers.get("Originator"), "o");
|
||||
equal(results.headers.get("Recipient"), "r");
|
||||
equal(results.headers.get("If-None-Match"), "*");
|
||||
equal(results.headers.get("If-Match"), "123");
|
||||
|
||||
equal(response.lastRedirectStatus, 302);
|
||||
});
|
||||
|
||||
add_task(async function test_item_request() {
|
||||
gServer.reset();
|
||||
let uri = gServer.uri("/requests/item/201");
|
||||
let icalString = "BEGIN:VEVENT\r\nUID:123\r\nEND:VEVENT";
|
||||
let componentString = `BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\n${icalString}\r\nEND:VCALENDAR\r\n`;
|
||||
let request = new ItemRequest(
|
||||
gServer.session,
|
||||
gMockCalendar,
|
||||
uri,
|
||||
cal.createEvent(icalString),
|
||||
"*"
|
||||
);
|
||||
let response = await request.commit();
|
||||
|
||||
equal(response.status, 201);
|
||||
ok(response.ok);
|
||||
|
||||
let serverResult = gServer.serverRequests.item;
|
||||
|
||||
equal(serverResult.method, "PUT");
|
||||
equal(serverResult.body, componentString);
|
||||
equal(serverResult.headers.get("If-None-Match"), "*");
|
||||
ok(!serverResult.headers.has("If-Match"));
|
||||
|
||||
// Now the same with 204 No Content and an etag
|
||||
gServer.reset();
|
||||
uri = gServer.uri("/requests/item/204");
|
||||
request = new ItemRequest(
|
||||
gServer.session,
|
||||
gMockCalendar,
|
||||
uri,
|
||||
cal.createEvent(icalString),
|
||||
"123123"
|
||||
);
|
||||
response = await request.commit();
|
||||
|
||||
equal(response.status, 204);
|
||||
ok(response.ok);
|
||||
|
||||
serverResult = gServer.serverRequests.item;
|
||||
|
||||
equal(serverResult.method, "PUT");
|
||||
equal(serverResult.body, componentString);
|
||||
equal(serverResult.headers.get("If-Match"), "123123");
|
||||
ok(!serverResult.headers.has("If-None-Match"));
|
||||
|
||||
// Now the same with 200 OK and no etag
|
||||
gServer.reset();
|
||||
uri = gServer.uri("/requests/item/200");
|
||||
request = new ItemRequest(gServer.session, gMockCalendar, uri, cal.createEvent(icalString));
|
||||
response = await request.commit();
|
||||
|
||||
equal(response.status, 200);
|
||||
ok(response.ok);
|
||||
|
||||
serverResult = gServer.serverRequests.item;
|
||||
|
||||
equal(serverResult.method, "PUT");
|
||||
equal(serverResult.body, componentString);
|
||||
ok(!serverResult.headers.has("If-Match"));
|
||||
ok(!serverResult.headers.has("If-None-Match"));
|
||||
});
|
||||
|
||||
add_task(async function test_delete_item_request() {
|
||||
gServer.reset();
|
||||
let uri = gServer.uri("/requests/deleteitem");
|
||||
let request = new DeleteItemRequest(gServer.session, gMockCalendar, uri, "*");
|
||||
|
||||
strictEqual(request.uploadData, null);
|
||||
strictEqual(request.contentType, null);
|
||||
|
||||
let response = await request.commit();
|
||||
|
||||
equal(response.status, 200);
|
||||
ok(response.ok);
|
||||
|
||||
let serverResult = gServer.serverRequests.deleteitem;
|
||||
|
||||
equal(serverResult.method, "DELETE");
|
||||
equal(serverResult.headers.get("If-Match"), "*");
|
||||
ok(!serverResult.headers.has("If-None-Match"));
|
||||
|
||||
// Now the same with no etag, and a (valid) 404 response
|
||||
gServer.reset();
|
||||
uri = gServer.uri("/requests/deleteitem/404");
|
||||
request = new DeleteItemRequest(gServer.session, gMockCalendar, uri);
|
||||
response = await request.commit();
|
||||
|
||||
equal(response.status, 404);
|
||||
ok(response.ok);
|
||||
|
||||
serverResult = gServer.serverRequests.deleteitem;
|
||||
|
||||
equal(serverResult.method, "DELETE");
|
||||
ok(!serverResult.headers.has("If-Match"));
|
||||
ok(!serverResult.headers.has("If-None-Match"));
|
||||
});
|
||||
|
||||
add_task(async function test_propfind_request() {
|
||||
gServer.reset();
|
||||
let uri = gServer.uri("/calendars/xpcshell/events");
|
||||
let props = [
|
||||
"D:principal-collection-set",
|
||||
"D:current-user-principal",
|
||||
"D:supported-report-set",
|
||||
"C:supported-calendar-component-set",
|
||||
"C:schedule-inbox-URL",
|
||||
"C:schedule-outbox-URL",
|
||||
"R:obscure-thing-not-found",
|
||||
];
|
||||
let request = new PropfindRequest(gServer.session, gMockCalendar, uri, props);
|
||||
let response = await request.commit();
|
||||
|
||||
equal(response.status, 207);
|
||||
ok(response.ok);
|
||||
|
||||
let results = gServer.serverRequests.calendars;
|
||||
|
||||
ok(
|
||||
results.body.match(/<D:prop>\s*<D:principal-collection-set\/>\s*<D:current-user-principal\/>/)
|
||||
);
|
||||
|
||||
equal(Object.keys(response.data).length, 1);
|
||||
ok(!!response.data[uri.filePath]);
|
||||
ok(!!response.firstProps);
|
||||
|
||||
let resprops = response.firstProps;
|
||||
|
||||
deepEqual(resprops["D:principal-collection-set"], [
|
||||
gServer.uri("/principals/").spec,
|
||||
gServer.uri("/principals/subthing/").spec,
|
||||
]);
|
||||
equal(resprops["D:current-user-principal"], gServer.uri("/principals/xpcshell/user").spec);
|
||||
|
||||
deepEqual(
|
||||
[...resprops["D:supported-report-set"].values()],
|
||||
["D:principal-property-search", "C:calendar-multiget", "D:sync-collection"]
|
||||
);
|
||||
|
||||
deepEqual([...resprops["C:supported-calendar-component-set"].values()], ["VEVENT", "VTODO"]);
|
||||
equal(resprops["C:schedule-inbox-URL"], gServer.uri("/calendars/xpcshell/inbox").spec);
|
||||
equal(resprops["C:schedule-outbox-URL"], gServer.uri("/calendars/xpcshell/outbox").spec);
|
||||
strictEqual(resprops["R:obscure-thing-not-found"], null);
|
||||
equal(resprops["R:plain-text-prop"], "hello, world");
|
||||
});
|
||||
|
||||
add_task(async function test_davheader_request() {
|
||||
gServer.reset();
|
||||
let uri = gServer.uri("/requests/dav");
|
||||
let request = new DAVHeaderRequest(gServer.session, gMockCalendar, uri);
|
||||
let response = await request.commit();
|
||||
|
||||
let serverResult = gServer.serverRequests.dav;
|
||||
|
||||
equal(serverResult.method, "OPTIONS");
|
||||
deepEqual([...response.features], ["calendar-schedule", "calendar-auto-schedule"]);
|
||||
strictEqual(response.version, 1);
|
||||
});
|
||||
|
||||
add_task(async function test_propsearch_request() {
|
||||
gServer.reset();
|
||||
let uri = gServer.uri("/principals/");
|
||||
let props = ["D:displayname", "B:department", "B:phone", "B:office"];
|
||||
let request = new PrincipalPropertySearchRequest(
|
||||
gServer.session,
|
||||
gMockCalendar,
|
||||
uri,
|
||||
"doE",
|
||||
"D:displayname",
|
||||
props
|
||||
);
|
||||
let response = await request.commit();
|
||||
|
||||
equal(response.status, 207);
|
||||
ok(response.ok);
|
||||
|
||||
equal(response.data["http://www.example.com/users/jdoe"]["D:displayname"], "John Doe");
|
||||
|
||||
ok(gServer.serverRequests.principals.body.includes("<D:match>doE</D:match>"));
|
||||
ok(gServer.serverRequests.principals.body.match(/<D:prop>\s*<D:displayname\/>\s*<\/D:prop>/));
|
||||
ok(
|
||||
gServer.serverRequests.principals.body.match(/<D:prop>\s*<D:displayname\/>\s*<B:department\/>/)
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_outbox_request() {
|
||||
gServer.reset();
|
||||
let icalString = "BEGIN:VEVENT\r\nUID:123\r\nEND:VEVENT";
|
||||
let uri = gServer.uri("/calendars/xpcshell/outbox");
|
||||
let request = new OutboxRequest(
|
||||
gServer.session,
|
||||
gMockCalendar,
|
||||
uri,
|
||||
"xpcshell@example.com",
|
||||
["recipient1@example.com", "recipient2@example.com"],
|
||||
"REPLY",
|
||||
cal.createEvent(icalString)
|
||||
);
|
||||
let response = await request.commit();
|
||||
|
||||
equal(response.status, 200);
|
||||
ok(response.ok);
|
||||
|
||||
let results = gServer.serverRequests.calendars;
|
||||
|
||||
ok(results.body.includes("METHOD:REPLY"));
|
||||
equal(results.method, "POST");
|
||||
equal(results.headers.get("Originator"), "xpcshell@example.com");
|
||||
equal(results.headers.get("Recipient"), "recipient1@example.com, recipient2@example.com");
|
||||
});
|
||||
|
||||
add_task(async function test_freebusy_request() {
|
||||
gServer.reset();
|
||||
let uri = gServer.uri("/calendars/xpcshell/outbox2");
|
||||
let request = new FreeBusyRequest(
|
||||
gServer.session,
|
||||
gMockCalendar,
|
||||
uri,
|
||||
"mailto:xpcshell@example.com",
|
||||
"mailto:recipient@example.com",
|
||||
cal.createDateTime("20180101"),
|
||||
cal.createDateTime("20180201")
|
||||
);
|
||||
|
||||
let response = await request.commit();
|
||||
|
||||
equal(response.status, 200);
|
||||
ok(response.ok);
|
||||
|
||||
let results = gServer.serverRequests.calendars;
|
||||
equal(
|
||||
ics_unfoldline(
|
||||
results.body
|
||||
.replace(/\r\n/g, "\n")
|
||||
.replace(/(UID|DTSTAMP):[^\n]+\n/g, "")
|
||||
.trim()
|
||||
),
|
||||
dedent`
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
|
||||
VERSION:2.0
|
||||
METHOD:REQUEST
|
||||
BEGIN:VFREEBUSY
|
||||
DTSTART;VALUE=DATE:20180101
|
||||
DTEND;VALUE=DATE:20180201
|
||||
ORGANIZER:mailto:xpcshell@example.com
|
||||
ATTENDEE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL:mailto:recipient@example.com
|
||||
END:VFREEBUSY
|
||||
END:VCALENDAR
|
||||
`
|
||||
);
|
||||
equal(results.method, "POST");
|
||||
equal(results.headers.get("Content-Type"), "text/calendar; charset=utf-8");
|
||||
equal(results.headers.get("Originator"), "mailto:xpcshell@example.com");
|
||||
equal(results.headers.get("Recipient"), "mailto:recipient@example.com");
|
||||
|
||||
let first = response.firstRecipient;
|
||||
strictEqual(first, response.data["mailto:recipient1@example.com"]);
|
||||
|
||||
equal(first.status, "2.0;Success");
|
||||
deepEqual(first.intervals.map(interval => interval.type), ["UNKNOWN", "FREE", "BUSY", "UNKNOWN"]);
|
||||
deepEqual(
|
||||
first.intervals.map(interval => interval.begin.icalString + ":" + interval.end.icalString),
|
||||
["20180101:20180102", "20180103:20180117", "20180118:20180125", "20180126:20180201"]
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_caldav_client() {
|
||||
let client = await gServer.getClient();
|
||||
let pclient = cal.async.promisifyCalendar(client);
|
||||
|
||||
let items = await pclient.getAllItems();
|
||||
equal(items.length, 1);
|
||||
});
|
|
@ -19,6 +19,7 @@
|
|||
[test_bug668222.js]
|
||||
[test_bug759324.js]
|
||||
[test_calmgr.js]
|
||||
[test_caldav_requests.js]
|
||||
[test_itip_utils.js]
|
||||
[test_data_bags.js]
|
||||
[test_datetime.js]
|
||||
|
|
Загрузка…
Ссылка в новой задаче