Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/contacts extension limit (based on pull request #141) #143

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
18 changes: 16 additions & 2 deletions lib/contacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,32 @@ export function createCard(addressBook, options) {
/**
* Options:
*
* (dav.Transport) xhr - request sender.
* (dav.Sandbox) sandbox - optional request sandbox.
* (Array.<Object>) filters - optional carddav filters.
* (Array.<Object>) contents - optional contents of address object.
*/
export let listVCards = co.wrap(function *(addressBook, options) {
debug(`Doing REPORT on address book ${addressBook.url} which belongs to
${addressBook.account.credentials.username}`);

/* According to http://stackoverflow.com/questions/23742568/google-carddav-api-addressbook-multiget-returns-400-bad-request,
* Google's CardDAV server requires a filter element. I don't think all addressbook-query calls need a filter in the spec though?
*/
let filters = options.filters || [{
name: 'prop-filter',
attrs: { name: 'FN' },
namespace: ns.CARDDAV
}];

var req = request.addressBookQuery({
depth: 1,
props: [
{ name: 'getetag', namespace: ns.DAV },
{ name: 'address-data', namespace: ns.CARDDAV }
]
{ name: 'address-data', namespace: ns.CARDDAV, value: options.contents }
],
filters: filters,
limits: options.limits
});

let responses = yield options.xhr.send(req, addressBook.url, {
Expand Down
9 changes: 8 additions & 1 deletion lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ import * as template from './template/index';
*
* (String) depth - optional value for Depth header.
* (Array.<Object>) props - list of props to request.
* (Array.<Object>) filters - list of filters to send with request.
* (Array.<Object>) limits - list of limits to send with request.
* I.e. [{ name: 'limit', namespace: ns.CARDDAV, value: [{name: 'nresults', value: 20, namespace: ns.CARDDAV }] }]
*/
export function addressBookQuery(options) {
return collectionQuery(
template.addressBookQuery({ props: options.props || [] }),
template.addressBookQuery({
props: options.props || [],
filters: options.filters || [],
limits: options.limits || []
}),
{ depth: options.depth }
);
}
Expand Down
11 changes: 4 additions & 7 deletions lib/template/address_book_query.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import prop from './prop';

export default function addressBookQuery(object) {
return `<card:addressbook-query xmlns:card="urn:ietf:params:xml:ns:carddav"
xmlns:d="DAV:">
return `<card:addressbook-query xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:d="DAV:">
<d:prop>
${object.props.map(prop)}
${object.props.map(prop).join('\n ')}
</d:prop>
<!-- According to http://stackoverflow.com/questions/23742568/google-carddav-api-addressbook-multiget-returns-400-bad-request,
Google's CardDAV server requires a filter element. I don't think all addressbook-query calls need a filter in the spec though? -->
<card:filter>
<card:prop-filter name="FN">
</card:prop-filter>
${object.filters.map(prop)}
</card:filter>
${(object.limits?object.limits.map(prop):'')}
</card:addressbook-query>`
}
64 changes: 30 additions & 34 deletions lib/template/prop.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,38 @@
import * as ns from '../namespace';

let debug = require('debug')('dav:prop');

/**
* @param {Object} filter looks like
*
* {
* type: 'comp-filter',
* attrs: {
* name: 'VCALENDAR'
* }
* }
*
* Or maybe
*
* {
* type: 'time-range',
* attrs: {
* start: '20060104T000000Z',
* end: '20060105T000000Z'
* }
* }
*
* You can nest them like so:
*
* {
* type: 'comp-filter',
* attrs: { name: 'VCALENDAR' },
* children: [{
* type: 'comp-filter',
* attrs: { name: 'VEVENT' },
* children: [{
* type: 'time-range',
* attrs: { start: '20060104T000000Z', end: '20060105T000000Z' }
* }]
* }]
* }
* (Array.<Object>) item - property to request. Has to look like:
* {
* name: '<property name like getetag|address-data|prop|...>',
* namespace: <namespace like ns.DAV|ns.CARDDAV|...>,
* attrs: {<attribute name>: '<attribute value>', ...}, (optional)
* value: <property value or array of further property childs>, (optional)
* }
*/
export default function prop(item) {
return `<${xmlnsPrefix(item.namespace)}:${item.name} />`;
debug('item %o', item);

if (item.value === undefined) {
return `<${xmlnsPrefix(item.namespace)}:${item.name}${formatAttrs(item.attrs)} />`;
}

if (typeof item.value !== 'object') {
return `<${xmlnsPrefix(item.namespace)}:${item.name}${formatAttrs(item.attrs)}>${item.value}</${xmlnsPrefix(item.namespace)}:${item.name}>`;
}

return `<${xmlnsPrefix(item.namespace)}:${item.name}${formatAttrs(item.attrs)}>${item.value.map(prop).join('')}</${xmlnsPrefix(item.namespace)}:${item.name}>`;
}

function formatAttrs(attrs) {
if (typeof attrs !== 'object') {
return '';
}

return ' ' + Object.keys(attrs)
.map(attr => `${attr}="${attrs[attr]}"`)
.join(' ');
}

function xmlnsPrefix(namespace) {
Expand Down
2 changes: 1 addition & 1 deletion lib/template/propfind.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function propfind(object) {
xmlns:ca="http://apple.com/ns/ical/"
xmlns:d="DAV:">
<d:prop>
${object.props.map(prop)}
${object.props.map(prop).join('\n ')}
</d:prop>
</d:propfind>`;
}
60 changes: 58 additions & 2 deletions test/integration/contacts_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as dav from '../../lib';
let debug = dav.debug('dav:contacts_test');

suite('contacts', function() {
let addressBooks, xhr;
let account, addressBooks, xhr;

setup(co.wrap(function *() {
debug('Create account.');
Expand All @@ -19,7 +19,7 @@ suite('contacts', function() {
})
);

let account = yield dav.createAccount({
account = yield dav.createAccount({
server: 'http://127.0.0.1:8888/',
xhr: xhr,
accountType: 'carddav',
Expand Down Expand Up @@ -181,6 +181,62 @@ suite('contacts', function() {
assert.notStrictEqual(addressBook.syncToken, prevSyncToken, 'new token');
}));

test('#add 2nd vcard, #list specific Card, #limit result', co.wrap(function *() {
let addressBook = addressBooks[0];
yield dav.createCard(addressBook, {
filename: 'test2.vcf',
data: data.johnDoe,
xhr: xhr
});

let updated = yield dav.syncCarddavAccount(account, {
syncMethod: 'basic',
xhr: xhr
});

addressBooks = updated.addressBooks;
addressBook = account.addressBooks[0];
let objects = addressBook.objects;
debug('%i addressBook(s) with %i vcard(s)', addressBooks.length, objects.length);
assert.isArray(objects);
assert.lengthOf(objects, 2, '2 vcards expected');

objects = yield dav.listVCards(addressBook, {
xhr: xhr,
/* AH20190113 - Content filter seem not to be supported < sabre-io 3.2
contents: [{ name: 'prop', attrs: {name: 'FN'}, namespace: dav.ns.CARDDAV},
{ name: 'prop', attrs: {name: 'N'}, namespace: dav.ns.CARDDAV}],*/
filters: [{
name: 'prop-filter',
attrs: { name: 'FN' },
namespace: dav.ns.CARDDAV,
value: [{
name: 'text-match',
attrs: { collation: 'i;unicode-casemap', 'match-type': 'contains' },
value: 'John',
namespace: dav.ns.CARDDAV
}]
}]
});

assert.isArray(objects);
assert.lengthOf(objects, 1);
let object = objects[0];
assert.instanceOf(object, dav.VCard);
assert.include(object.addressData, '[email protected]', 'specific vcard look wrong one');

objects = yield dav.listVCards(addressBook, {
xhr: xhr,
limits: [{
name: 'limit', namespace: dav.ns.CARDDAV,
value: [{name: 'nresults', value: 1, namespace: dav.ns.CARDDAV }] }
]
});

assert.isArray(objects);
assert.lengthOf(objects, 1, 'wrong num of limit(ed) result');
}));

test('#deleteCard', co.wrap(function *() {
let addressBook = addressBooks[0];
let objects = addressBook.objects;
Expand Down
3 changes: 2 additions & 1 deletion test/integration/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export default docs;

[
{ name: 'bastille_day_party', fmt: 'ics' },
{ name: 'forrest_gump', fmt: 'vcf' }
{ name: 'forrest_gump', fmt: 'vcf' },
{ name: 'john_doe', fmt: 'vcf' }
].forEach(function(file) {
let camelCase = camelize(file.name);
docs[camelCase] = fs.readFileSync(
Expand Down
8 changes: 8 additions & 0 deletions test/integration/data/john_doe.vcf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
BEGIN:VCARD
VERSION:3.0
N:Doe;John;Mr.
FN:John
TEL;TYPE=HOME,VOICE:(666) 123-4567
EMAIL;TYPE=PREF,INTERNET:[email protected]
REV:2019-01-13T19:12:32Z
END:VCARD
73 changes: 73 additions & 0 deletions test/unit/request/address_book_query_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,79 @@ suite('request.addressBookQuery', function() {
yield mock.verify(send);
}));

test('should add specific contents to report body', co.wrap(function *() {
let mock = nockWrapper('http://127.0.0.1:1337')
.matchRequestBody('/principals/admin/', 'REPORT', body => {
return body.match(/<card:prop\s+name="FN"\s*\/>/) !== null;
});

let req = addressBookQuery({
props: [ {
name: 'address-data',
namespace: ns.CARDDAV,
value: [{ name: 'prop', attrs: {name: 'FN'}, namespace: ns.CARDDAV}]
}],
});

let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/');
yield mock.verify(send);
}));

test('should add specified props to report body', co.wrap(function *() {
let mock = nockWrapper('http://127.0.0.1:1337')
.matchRequestBody('/principals/admin/', 'REPORT', body => {
return body.indexOf('<d:catdog />') !== -1;
});

let req = addressBookQuery({
props: [ { name: 'catdog', namespace: ns.DAV } ]
});

let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/');
yield mock.verify(send);
}));

test('should add specified filters to report body', co.wrap(function *() {
let mock = nockWrapper('http://127.0.0.1:1337')
.matchRequestBody('/principals/admin/', 'REPORT', body => {
return body.match(/<card:prop-filter\s+name="FN">.*<card:text-match\s+collation="i;unicode-casemap"\s+match-type="contains">John Doe<\/card:text-match>/) !== null;
});

let req = addressBookQuery({
filters: [{
name: 'prop-filter',
attrs: { name: 'FN' },
namespace: ns.CARDDAV,
value: [{
name: 'text-match',
attrs: { collation: 'i;unicode-casemap', 'match-type': 'contains' },
value: 'John Doe',
namespace: ns.CARDDAV
}]
}]
});

let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/');
yield mock.verify(send);
}));

test('should add specified limit to report body', co.wrap(function *() {
let mock = nockWrapper('http://127.0.0.1:1337')
.matchRequestBody('/principals/admin/', 'REPORT', body => {
return body.match(/<card:limit>\s*<card:nresults>12<\/card:nresults>\s*<\/card:limit>/) !== null;
});

let req = addressBookQuery({
limits: [{
name: 'limit', namespace: ns.CARDDAV,
value: [{name: 'nresults', value: 12, namespace: ns.CARDDAV }] }
]
});

let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/');
yield mock.verify(send);
}));

test('should resolve with appropriate data structure', co.wrap(function *() {
nockWrapper('http://127.0.0.1:1337')
.intercept('/', 'REPORT')
Expand Down