From bf055e2c080e1b68292a2e075582343bfe80a723 Mon Sep 17 00:00:00 2001 From: Apehaenger Date: Sat, 12 Jan 2019 19:19:46 +0100 Subject: [PATCH 1/8] Fixed wrong/ugly comma between resulting XML elements --- lib/template/address_book_query.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/template/address_book_query.js b/lib/template/address_book_query.js index 9545ecec..950129fa 100644 --- a/lib/template/address_book_query.js +++ b/lib/template/address_book_query.js @@ -1,10 +1,9 @@ import prop from './prop'; export default function addressBookQuery(object) { - return ` + return ` - ${object.props.map(prop)} + ${object.props.map(prop).join('\n ')} From f553b57ee7701c2f8c8b2506aff354add99d796a Mon Sep 17 00:00:00 2001 From: Apehaenger Date: Sat, 12 Jan 2019 19:22:47 +0100 Subject: [PATCH 2/8] Extends for property attributes and childs --- lib/template/prop.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/template/prop.js b/lib/template/prop.js index 4ba4830f..e331ed18 100644 --- a/lib/template/prop.js +++ b/lib/template/prop.js @@ -1,5 +1,7 @@ import * as ns from '../namespace'; +let debug = require('debug')('dav:prop'); + /** * @param {Object} filter looks like * @@ -34,9 +36,34 @@ import * as ns from '../namespace'; * }] * }] * } + * + * (Array.) item - property item to request. Has to look like: + * { name: '', namespace: } + * it optionally might also contain an array of attributes like: + * { ... attrs: [{name: '', value: ''}, ...] } + * as well as it also might contain an array of childs like: + * { ... childs: [{ name: 'prop', attrs: [{name: 'name', value: 'FN'}], namespace: ns.CARDDAV }, ...]} */ export default function prop(item) { - return `<${xmlnsPrefix(item.namespace)}:${item.name} />`; + debug('item %o', item); + + var ret = `<${xmlnsPrefix(item.namespace)}:${item.name}`; + if (item.attrs !== undefined) { + ret += ` ${item.attrs.map(attr).join(' ')}`; + } + + if (item.childs === undefined) { + return ret + ' />'; + } else { + ret += `>${item.childs.map(prop).join('')}`; + } + ret += ``; + + return ret; +} + +function attr(item) { + return `${item.name}="${item.value}"`; } function xmlnsPrefix(namespace) { From f06aa11caaf8306a9d311f58db2c624525bf3ed0 Mon Sep 17 00:00:00 2001 From: Apehaenger Date: Sun, 13 Jan 2019 18:04:08 +0100 Subject: [PATCH 3/8] Unify filter.js + prop.js Merged filter.js + prop.js together so that filter.js would become needless once/if use of (old) filter get adapted to (new) prop. --- lib/template/prop.js | 73 +++++++++++++------------------------------- 1 file changed, 21 insertions(+), 52 deletions(-) diff --git a/lib/template/prop.js b/lib/template/prop.js index e331ed18..75b815f1 100644 --- a/lib/template/prop.js +++ b/lib/template/prop.js @@ -3,67 +3,36 @@ 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.) item - property item to request. Has to look like: - * { name: '', namespace: } - * it optionally might also contain an array of attributes like: - * { ... attrs: [{name: '', value: ''}, ...] } - * as well as it also might contain an array of childs like: - * { ... childs: [{ name: 'prop', attrs: [{name: 'name', value: 'FN'}], namespace: ns.CARDDAV }, ...]} + * (Array.) item - property to request. Has to look like: + * { + * name: '', + * namespace: , + * attrs: {: '', ...}, (optional) + * value: , (optional) + * } */ export default function prop(item) { debug('item %o', item); - - var ret = `<${xmlnsPrefix(item.namespace)}:${item.name}`; - if (item.attrs !== undefined) { - ret += ` ${item.attrs.map(attr).join(' ')}`; + + if (item.value === undefined) { + return `<${xmlnsPrefix(item.namespace)}:${item.name}${formatAttrs(item.attrs)} />`; } - if (item.childs === undefined) { - return ret + ' />'; - } else { - ret += `>${item.childs.map(prop).join('')}`; + if (typeof item.value !== 'object') { + return `<${xmlnsPrefix(item.namespace)}:${item.name}${formatAttrs(item.attrs)}>${item.value}`; } - ret += ``; - return ret; + return `<${xmlnsPrefix(item.namespace)}:${item.name}${formatAttrs(item.attrs)}>${item.value.map(prop)}`; } -function attr(item) { - return `${item.name}="${item.value}"`; +function formatAttrs(attrs) { + if (typeof attrs !== 'object') { + return ''; + } + + return ' ' + Object.keys(attrs) + .map(attr => `${attr}="${attrs[attr]}"`) + .join(' '); } function xmlnsPrefix(namespace) { From ca252f3c476a893f5436f64f6fd8047dee060848 Mon Sep 17 00:00:00 2001 From: Apehaenger Date: Sun, 13 Jan 2019 18:06:10 +0100 Subject: [PATCH 4/8] Replaced wrong/ugly comma with beautified newline --- lib/template/propfind.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/template/propfind.js b/lib/template/propfind.js index dd5cfef5..78c19e6a 100644 --- a/lib/template/propfind.js +++ b/lib/template/propfind.js @@ -7,7 +7,7 @@ export default function propfind(object) { xmlns:ca="http://apple.com/ns/ical/" xmlns:d="DAV:"> - ${object.props.map(prop)} + ${object.props.map(prop).join('\n ')} `; } From 6198c263d8c226f8f3a0c8bf8851ad200c24dbfa Mon Sep 17 00:00:00 2001 From: Apehaenger Date: Sun, 13 Jan 2019 18:24:10 +0100 Subject: [PATCH 5/8] Added 'filters' and 'contents' options to listVCards --- lib/contacts.js | 17 +++++++++++++++-- lib/request.js | 6 +++++- lib/template/address_book_query.js | 5 +---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/contacts.js b/lib/contacts.js index 07c22317..47006373 100644 --- a/lib/contacts.js +++ b/lib/contacts.js @@ -73,18 +73,31 @@ export function createCard(addressBook, options) { /** * Options: * + * (dav.Transport) xhr - request sender. * (dav.Sandbox) sandbox - optional request sandbox. + * (Array.) filters - optional carddav filters. + * (Array.) 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 }); let responses = yield options.xhr.send(req, addressBook.url, { diff --git a/lib/request.js b/lib/request.js index 6c951c95..dbe84414 100644 --- a/lib/request.js +++ b/lib/request.js @@ -6,10 +6,14 @@ import * as template from './template/index'; * * (String) depth - optional value for Depth header. * (Array.) props - list of props to request. + * (Array.) filters - list of filters to send with request. */ export function addressBookQuery(options) { return collectionQuery( - template.addressBookQuery({ props: options.props || [] }), + template.addressBookQuery({ + props: options.props || [], + filters: options.filters || [] + }), { depth: options.depth } ); } diff --git a/lib/template/address_book_query.js b/lib/template/address_book_query.js index 950129fa..38dac925 100644 --- a/lib/template/address_book_query.js +++ b/lib/template/address_book_query.js @@ -5,11 +5,8 @@ export default function addressBookQuery(object) { ${object.props.map(prop).join('\n ')} - - - + ${object.filters.map(prop)} ` } From 3495971d29267d43153ba2caa4e17f88f319f3d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Ebeling?= Date: Sun, 13 Jan 2019 20:40:42 +0100 Subject: [PATCH 6/8] Removed wrong/ugly comma --- lib/template/prop.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/template/prop.js b/lib/template/prop.js index 75b815f1..15ae6d13 100644 --- a/lib/template/prop.js +++ b/lib/template/prop.js @@ -22,7 +22,7 @@ export default function prop(item) { return `<${xmlnsPrefix(item.namespace)}:${item.name}${formatAttrs(item.attrs)}>${item.value}`; } - return `<${xmlnsPrefix(item.namespace)}:${item.name}${formatAttrs(item.attrs)}>${item.value.map(prop)}`; + return `<${xmlnsPrefix(item.namespace)}:${item.name}${formatAttrs(item.attrs)}>${item.value.map(prop).join('')}`; } function formatAttrs(attrs) { From e183c0a5cd09f4b51b4414ab93aa843293a777a0 Mon Sep 17 00:00:00 2001 From: Apehaenger Date: Sun, 13 Jan 2019 22:30:26 +0100 Subject: [PATCH 7/8] Add contact tests for new filters and contents options --- test/integration/contacts_test.js | 49 ++++++++++++++++- test/integration/data/index.js | 3 +- test/integration/data/john_doe.vcf | 8 +++ test/unit/request/address_book_query_test.js | 56 ++++++++++++++++++++ 4 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 test/integration/data/john_doe.vcf diff --git a/test/integration/contacts_test.js b/test/integration/contacts_test.js index 685f7b4b..b2020113 100644 --- a/test/integration/contacts_test.js +++ b/test/integration/contacts_test.js @@ -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.'); @@ -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', @@ -181,6 +181,51 @@ suite('contacts', function() { assert.notStrictEqual(addressBook.syncToken, prevSyncToken, 'new token'); })); + test('#add 2nd vcard, #list specific Card', 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, 'john.doe@example.com', 'specific vcard look wrong one'); + })); + test('#deleteCard', co.wrap(function *() { let addressBook = addressBooks[0]; let objects = addressBook.objects; diff --git a/test/integration/data/index.js b/test/integration/data/index.js index 7d2ed03b..527c7114 100644 --- a/test/integration/data/index.js +++ b/test/integration/data/index.js @@ -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( diff --git a/test/integration/data/john_doe.vcf b/test/integration/data/john_doe.vcf new file mode 100644 index 00000000..7b5538ea --- /dev/null +++ b/test/integration/data/john_doe.vcf @@ -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:john.doe@example.com +REV:2019-01-13T19:12:32Z +END:VCARD diff --git a/test/unit/request/address_book_query_test.js b/test/unit/request/address_book_query_test.js index 31777efd..cf7a4eb6 100644 --- a/test/unit/request/address_book_query_test.js +++ b/test/unit/request/address_book_query_test.js @@ -45,6 +45,62 @@ 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(//) !== 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('') !== -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(/.*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 resolve with appropriate data structure', co.wrap(function *() { nockWrapper('http://127.0.0.1:1337') .intercept('/', 'REPORT') From 405c00e4c4a0a26b0d9b4c79ec3bb983df2772e9 Mon Sep 17 00:00:00 2001 From: Apehaenger Date: Sun, 27 Jan 2019 22:27:36 +0100 Subject: [PATCH 8/8] Test CARDDAV:limit --- lib/contacts.js | 3 ++- lib/request.js | 5 ++++- lib/template/address_book_query.js | 1 + test/integration/contacts_test.js | 13 ++++++++++++- test/unit/request/address_book_query_test.js | 17 +++++++++++++++++ 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/contacts.js b/lib/contacts.js index 47006373..dc1be8d8 100644 --- a/lib/contacts.js +++ b/lib/contacts.js @@ -97,7 +97,8 @@ export let listVCards = co.wrap(function *(addressBook, options) { { name: 'getetag', namespace: ns.DAV }, { name: 'address-data', namespace: ns.CARDDAV, value: options.contents } ], - filters: filters + filters: filters, + limits: options.limits }); let responses = yield options.xhr.send(req, addressBook.url, { diff --git a/lib/request.js b/lib/request.js index dbe84414..6143e923 100644 --- a/lib/request.js +++ b/lib/request.js @@ -7,12 +7,15 @@ import * as template from './template/index'; * (String) depth - optional value for Depth header. * (Array.) props - list of props to request. * (Array.) filters - list of filters to send with request. + * (Array.) 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 || [], - filters: options.filters || [] + filters: options.filters || [], + limits: options.limits || [] }), { depth: options.depth } ); diff --git a/lib/template/address_book_query.js b/lib/template/address_book_query.js index 38dac925..1692fec4 100644 --- a/lib/template/address_book_query.js +++ b/lib/template/address_book_query.js @@ -8,5 +8,6 @@ export default function addressBookQuery(object) { ${object.filters.map(prop)} + ${(object.limits?object.limits.map(prop):'')} ` } diff --git a/test/integration/contacts_test.js b/test/integration/contacts_test.js index b2020113..9189682a 100644 --- a/test/integration/contacts_test.js +++ b/test/integration/contacts_test.js @@ -181,7 +181,7 @@ suite('contacts', function() { assert.notStrictEqual(addressBook.syncToken, prevSyncToken, 'new token'); })); - test('#add 2nd vcard, #list specific Card', co.wrap(function *() { + test('#add 2nd vcard, #list specific Card, #limit result', co.wrap(function *() { let addressBook = addressBooks[0]; yield dav.createCard(addressBook, { filename: 'test2.vcf', @@ -224,6 +224,17 @@ suite('contacts', function() { let object = objects[0]; assert.instanceOf(object, dav.VCard); assert.include(object.addressData, 'john.doe@example.com', '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 *() { diff --git a/test/unit/request/address_book_query_test.js b/test/unit/request/address_book_query_test.js index cf7a4eb6..90f92a71 100644 --- a/test/unit/request/address_book_query_test.js +++ b/test/unit/request/address_book_query_test.js @@ -101,6 +101,23 @@ suite('request.addressBookQuery', function() { 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(/\s*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')