import co from 'co'; import url from 'url'; import { listCalendars, listCalendarObjects } from './calendars'; import { listAddressBooks, listVCards, getFullVcards } from './contacts'; import fuzzyUrlEquals from './fuzzy_url_equals'; import { Account } from './model'; import * as ns from './namespace'; import * as request from './request'; let debug = require('./debug')('dav:accounts'); let defaults = { accountType: 'caldav', loadCollections: true, loadObjects: false }; /** * rfc 6764. * * @param {dav.Account} account to find root url for. */ let serviceDiscovery = co.wrap(function *(account, options) { debug('Attempt service discovery.'); let endpoint = url.parse(account.server); endpoint.protocol = endpoint.protocol || 'http'; // TODO(gareth) https? let uri = url.format({ protocol: endpoint.protocol, host: endpoint.host, pathname: (!options.useProvidedPath ? '/.well-known/' + options.accountType : endpoint.pathname) }); let req = request.basic({ method: 'GET' }); try { let xhr = yield options.xhr.send(req, uri, { sandbox: options.sandbox }); if (xhr.status >= 300 && xhr.status < 400) { // http redirect. let location = xhr.getResponseHeader('Location'); if (typeof location === 'string' && location.length) { debug(`Discovery redirected to ${location}`); return url.format({ protocol: endpoint.protocol, host: endpoint.host, pathname: location }); } } } catch (error) { debug('Discovery failed... failover to the provided url'); } return endpoint.href; }); /** * rfc 5397. * * @param {dav.Account} account to get principal url for. */ let principalUrl = co.wrap(function *(account, options) { debug(`Fetch principal url from context path ${account.rootUrl}.`); let req = request.propfind({ props: [ { name: 'current-user-principal', namespace: ns.DAV } ], depth: 0, mergeResponses: true }); let res = yield options.xhr.send(req, account.rootUrl, { sandbox: options.sandbox }); let container = res.props; debug(`Received principal: ${container.currentUserPrincipal}`); return url.resolve(account.rootUrl, container.currentUserPrincipal); }); /** * @param {dav.Account} account to get home url for. */ let homeUrl = co.wrap(function *(account, options) { debug(`Fetch home url from principal url ${account.principalUrl}.`); let prop; if (options.accountType === 'caldav') { prop = { name: 'calendar-home-set', namespace: ns.CALDAV }; } else if (options.accountType === 'carddav') { prop = { name: 'addressbook-home-set', namespace: ns.CARDDAV }; } var req = request.propfind({ props: [ prop ] }); let responses = yield options.xhr.send(req, account.principalUrl, { sandbox: options.sandbox }); let response = responses.find(response => { return fuzzyUrlEquals(account.principalUrl, response.href); }); let container = response.props; let href; if (options.accountType === 'caldav') { debug(`Received home: ${container.calendarHomeSet}`); href = container.calendarHomeSet; } else if (options.accountType === 'carddav') { debug(`Received home: ${container.addressbookHomeSet}`); href = container.addressbookHomeSet; } return url.resolve(account.rootUrl, href); }); /** * Options: * * (String) accountType - one of 'caldav' or 'carddav'. Defaults to 'caldav'. * (Array.) filters - list of caldav filters to send with request. * (Boolean) loadCollections - whether or not to load dav collections. * (Boolean) loadObjects - whether or not to load dav objects. * (dav.Sandbox) sandbox - optional request sandbox. * (String) server - some url for server (needn't be base url). * (String) timezone - VTIMEZONE calendar object. * (dav.Transport) xhr - request sender. * * @return {Promise} a promise that will resolve with a dav.Account object. */ exports.createAccount = co.wrap(function *(options) { options = Object.assign({}, defaults, options); if (typeof options.loadObjects !== 'boolean') { options.loadObjects = options.loadCollections; } let account = new Account({ server: options.server, credentials: options.xhr.credentials }); account.rootUrl = yield serviceDiscovery(account, options); account.principalUrl = yield principalUrl(account, options); account.homeUrl = yield homeUrl(account, options); if (!options.loadCollections) { return account; } let key, loadCollections, loadObjects; if (options.accountType === 'caldav') { key = 'calendars'; loadCollections = listCalendars; loadObjects = listCalendarObjects; } else if (options.accountType === 'carddav') { key = 'addressBooks'; loadCollections = listAddressBooks; loadObjects = listVCards; } var collections = yield loadCollections(account, options); account[key] = collections; if (!options.loadObjects) { return account; } yield collections.map(co.wrap(function *(collection) { try { collection.objects = yield loadObjects(collection, options); } catch (error) { collection.error = error; } })); account[key] = account[key].filter(function(collection) { return !collection.error; }); return account; });