caldav-adapter 9.2.0 → 9.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/common/xml-encode.js +24 -0
- package/index.js +18 -10
- package/package.json +7 -2
- package/routes/calendar/calendar/calendar-multiget.js +5 -1
- package/routes/calendar/calendar/delete.js +1 -1
- package/routes/calendar/calendar/propfind.js +11 -0
- package/routes/calendar/calendar/put.js +19 -1
- package/routes/calendar/calendar/report.js +4 -1
- package/routes/calendar/calendar/sync-collection.js +18 -1
- package/routes/calendar/calendar.js +11 -4
- package/routes/calendar/scheduling.js +14 -6
- package/routes/calendar/user/propfind.js +10 -0
- package/routes/principal/mkcalendar.js +57 -34
- package/routes/principal/propfind.js +24 -7
- package/routes/principal/report.js +6 -0
- package/.commitlintrc.js +0 -3
- package/.editorconfig +0 -9
- package/.gitattributes +0 -1
- package/.github/workflows/ci.yml +0 -24
- package/.husky/commit-msg +0 -4
- package/.husky/pre-commit +0 -4
- package/.lintstagedrc.js +0 -5
- package/.prettierrc.js +0 -5
- package/.remarkignore +0 -1
- package/.remarkrc.js +0 -3
- package/.xo-config.js +0 -8
- package/test/event-response.test.js +0 -383
- package/test/proppatch.test.js +0 -1254
- package/test/scheduling.test.js +0 -697
- package/test/test.js +0 -41
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared XML entity encoding utility
|
|
3
|
+
* Centralizes the XML encoding logic to avoid duplication across handlers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Encode special characters for XML content to prevent parsing errors
|
|
8
|
+
* @param {string} str - String to encode
|
|
9
|
+
* @returns {string} - XML-safe encoded string
|
|
10
|
+
*/
|
|
11
|
+
function encodeXMLEntities(str) {
|
|
12
|
+
if (typeof str !== 'string') {
|
|
13
|
+
return str;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return str
|
|
17
|
+
.replaceAll('&', '&') // Must be first to avoid double-encoding
|
|
18
|
+
.replaceAll('<', '<')
|
|
19
|
+
.replaceAll('>', '>')
|
|
20
|
+
.replaceAll('"', '"')
|
|
21
|
+
.replaceAll("'", ''');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { encodeXMLEntities };
|
package/index.js
CHANGED
|
@@ -14,7 +14,8 @@ const defaults = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
module.exports = function (options) {
|
|
17
|
-
|
|
17
|
+
// avoid mutating shared `defaults` object
|
|
18
|
+
options = { ...defaults, ...options };
|
|
18
19
|
|
|
19
20
|
const log = winston({ ...options, label: 'index' });
|
|
20
21
|
|
|
@@ -49,10 +50,11 @@ module.exports = function (options) {
|
|
|
49
50
|
const fillParameters = function (ctx) {
|
|
50
51
|
ctx.state.params = {};
|
|
51
52
|
|
|
53
|
+
// use ctx.path instead of ctx.url to avoid query string matching
|
|
52
54
|
let regex;
|
|
53
|
-
if (calendarRegex.regexp.test(ctx.
|
|
55
|
+
if (calendarRegex.regexp.test(ctx.path)) {
|
|
54
56
|
regex = calendarRegex;
|
|
55
|
-
} else if (principalRegex.regexp.test(ctx.
|
|
57
|
+
} else if (principalRegex.regexp.test(ctx.path)) {
|
|
56
58
|
regex = principalRegex;
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -60,7 +62,7 @@ module.exports = function (options) {
|
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
const captures = ctx.
|
|
65
|
+
const captures = ctx.path.match(regex.regexp);
|
|
64
66
|
for (let i = 0; i < regex.keys.length; i++) {
|
|
65
67
|
let captured = captures[i + 1];
|
|
66
68
|
if (typeof captured === 'string') {
|
|
@@ -134,13 +136,18 @@ module.exports = function (options) {
|
|
|
134
136
|
};
|
|
135
137
|
|
|
136
138
|
return async function (ctx, next) {
|
|
139
|
+
// use 301 permanent redirect per RFC 6764 Section 5
|
|
137
140
|
if (
|
|
138
|
-
ctx.
|
|
141
|
+
ctx.path.toLowerCase() === '/.well-known/caldav' &&
|
|
139
142
|
!options.disableWellKnown
|
|
140
|
-
)
|
|
141
|
-
|
|
143
|
+
) {
|
|
144
|
+
ctx.status = 301;
|
|
145
|
+
ctx.redirect(rootRoute);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
142
148
|
|
|
143
|
-
|
|
149
|
+
// use ctx.path instead of ctx.url
|
|
150
|
+
if (!rootRegexp.test(ctx.path)) {
|
|
144
151
|
await next();
|
|
145
152
|
return;
|
|
146
153
|
}
|
|
@@ -157,9 +164,10 @@ module.exports = function (options) {
|
|
|
157
164
|
await parseBody(ctx);
|
|
158
165
|
log.verbose('REQUEST BODY', ctx?.request?.body || '<empty>');
|
|
159
166
|
|
|
160
|
-
|
|
167
|
+
// use ctx.path instead of ctx.url
|
|
168
|
+
if (calendarRegex.regexp.test(ctx.path)) {
|
|
161
169
|
await calendarRoutes(ctx);
|
|
162
|
-
} else if (principalRegex.regexp.test(ctx.
|
|
170
|
+
} else if (principalRegex.regexp.test(ctx.path)) {
|
|
163
171
|
await principalRoutes(ctx);
|
|
164
172
|
} else {
|
|
165
173
|
ctx.redirect(principalRoute);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caldav-adapter",
|
|
3
3
|
"description": "CalDAV server for Node.js and Koa. Modernized and maintained for Forward Email.",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.3.0",
|
|
5
5
|
"author": "Sanders DeNardi and Forward Email LLC",
|
|
6
6
|
"contributors": [
|
|
7
7
|
"Sanders DeNardi <sedenardi@gmail.com> (http://www.sandersdenardi.com/)",
|
|
@@ -41,6 +41,11 @@
|
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=18"
|
|
43
43
|
},
|
|
44
|
+
"files": [
|
|
45
|
+
"index.js",
|
|
46
|
+
"common",
|
|
47
|
+
"routes"
|
|
48
|
+
],
|
|
44
49
|
"homepage": "https://github.com/forwardemail/caldav-adapter",
|
|
45
50
|
"keywords": [
|
|
46
51
|
"caldav",
|
|
@@ -63,7 +68,7 @@
|
|
|
63
68
|
},
|
|
64
69
|
"scripts": {
|
|
65
70
|
"lint": "xo --fix && remark . -qfo && fixpack",
|
|
66
|
-
"prepare": "husky install",
|
|
71
|
+
"prepare": "husky install > /dev/null 2>&1 || true",
|
|
67
72
|
"pretest": "npm run lint",
|
|
68
73
|
"test": "npm run test-coverage",
|
|
69
74
|
"test-coverage": "cross-env NODE_ENV=test nyc ava"
|
|
@@ -36,7 +36,11 @@ module.exports = function (options) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const hrefParts = href.split('/');
|
|
39
|
-
const
|
|
39
|
+
const lastPart = hrefParts.at(-1);
|
|
40
|
+
// Only strip .ics extension if present, to avoid corrupting eventIds
|
|
41
|
+
const eventId = lastPart.endsWith('.ics')
|
|
42
|
+
? lastPart.slice(0, -4)
|
|
43
|
+
: lastPart;
|
|
40
44
|
const event = await options.data.getEvent(ctx, {
|
|
41
45
|
eventId,
|
|
42
46
|
principalId: ctx.state.params.principalId,
|
|
@@ -46,6 +46,17 @@ module.exports = function (options) {
|
|
|
46
46
|
const resp = await calendarResponse(ctx, calendar);
|
|
47
47
|
const resps = [resp];
|
|
48
48
|
|
|
49
|
+
//
|
|
50
|
+
// Performance: check Depth header — if Depth:0 the client only
|
|
51
|
+
// wants calendar-level properties, not the event listing.
|
|
52
|
+
// This avoids loading any events from the database.
|
|
53
|
+
//
|
|
54
|
+
const depth = ctx.get('depth') || 'infinity';
|
|
55
|
+
if (depth === '0') {
|
|
56
|
+
const ms = multistatus(resps);
|
|
57
|
+
return build(ms);
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
const { children } = xml.getWithChildren(
|
|
50
61
|
'/D:propfind/D:prop',
|
|
51
62
|
ctx.request.xml
|
|
@@ -69,6 +69,23 @@ module.exports = function (options) {
|
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
//
|
|
73
|
+
// RFC 7232 Section 3.1: If-Match
|
|
74
|
+
// Validate ETag to prevent silent concurrent overwrites.
|
|
75
|
+
//
|
|
76
|
+
const ifMatch = ctx.get('if-match');
|
|
77
|
+
if (ifMatch && ifMatch !== '*') {
|
|
78
|
+
const currentETag = options.data.getETag(ctx, existing);
|
|
79
|
+
const clientETag = ifMatch.replace(/^"/, '').replace(/"$/, '');
|
|
80
|
+
const serverETag = currentETag.replace(/^"/, '').replace(/"$/, '');
|
|
81
|
+
if (clientETag !== serverETag) {
|
|
82
|
+
log.warn('if-match ETag mismatch, precondition failed');
|
|
83
|
+
ctx.status = 412;
|
|
84
|
+
ctx.body = preconditionFail(ctx.url, 'if-match');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
const updateObject = await options.data.updateEvent(ctx, {
|
|
73
90
|
eventId: ctx.state.params.eventId,
|
|
74
91
|
principalId: ctx.state.params.principalId,
|
|
@@ -78,7 +95,7 @@ module.exports = function (options) {
|
|
|
78
95
|
log.debug('event updated');
|
|
79
96
|
|
|
80
97
|
/* https://tools.ietf.org/html/rfc4791#section-5.3.2 */
|
|
81
|
-
ctx.status =
|
|
98
|
+
ctx.status = 204;
|
|
82
99
|
ctx.set('ETag', options.data.getETag(ctx, updateObject));
|
|
83
100
|
} else {
|
|
84
101
|
const newObject = await options.data.createEvent(ctx, {
|
|
@@ -91,6 +108,7 @@ module.exports = function (options) {
|
|
|
91
108
|
/* https://tools.ietf.org/html/rfc4791#section-5.3.2 */
|
|
92
109
|
ctx.status = 201;
|
|
93
110
|
ctx.set('ETag', options.data.getETag(ctx, newObject));
|
|
111
|
+
ctx.set('Location', ctx.url);
|
|
94
112
|
}
|
|
95
113
|
};
|
|
96
114
|
|
|
@@ -40,7 +40,10 @@ module.exports = function (options) {
|
|
|
40
40
|
const rootAction = rootActions[rootTag];
|
|
41
41
|
log.debug(`report ${rootAction ? 'hit' : 'miss'}: ${rootTag}`);
|
|
42
42
|
if (!rootAction) {
|
|
43
|
-
return
|
|
43
|
+
// RFC 3253 Section 3.6: unsupported report type should return 403
|
|
44
|
+
ctx.status = 403;
|
|
45
|
+
ctx.body = notFound(ctx.url);
|
|
46
|
+
return;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
const { responses, other } = await rootAction(ctx, calendar);
|
|
@@ -12,6 +12,20 @@ module.exports = function (options) {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
return async function (ctx, calendar) {
|
|
15
|
+
// RFC 6578 Section 3.2 - parse the client's sync-token
|
|
16
|
+
// to enable incremental sync instead of always returning all events
|
|
17
|
+
const syncTokenNodes = xml.get(
|
|
18
|
+
'/D:sync-collection/D:sync-token',
|
|
19
|
+
ctx.request.xml
|
|
20
|
+
);
|
|
21
|
+
let clientSyncToken = null;
|
|
22
|
+
if (syncTokenNodes && syncTokenNodes.length > 0) {
|
|
23
|
+
const tokenText = syncTokenNodes[0].textContent;
|
|
24
|
+
if (tokenText && tokenText.trim() !== '') {
|
|
25
|
+
clientSyncToken = tokenText.trim();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
const { children } = xml.getWithChildren(
|
|
16
30
|
'/D:sync-collection/D:prop',
|
|
17
31
|
ctx.request.xml
|
|
@@ -20,12 +34,15 @@ module.exports = function (options) {
|
|
|
20
34
|
return child.localName === 'calendar-data';
|
|
21
35
|
});
|
|
22
36
|
|
|
37
|
+
// Pass the client's sync-token to the data layer so it can
|
|
38
|
+
// return only events changed since that token
|
|
23
39
|
const events = await options.data.getEventsForCalendar(ctx, {
|
|
24
40
|
principalId: ctx.state.params.principalId,
|
|
25
41
|
calendarId: options.data.getCalendarId(ctx, calendar),
|
|
26
42
|
user: ctx.state.user,
|
|
27
43
|
fullData,
|
|
28
|
-
showDeleted: true
|
|
44
|
+
showDeleted: true,
|
|
45
|
+
syncToken: clientSyncToken
|
|
29
46
|
});
|
|
30
47
|
const { responses } = await eventResponse(ctx, events, calendar, children);
|
|
31
48
|
|
|
@@ -39,10 +39,13 @@ module.exports = function (options) {
|
|
|
39
39
|
const method = ctx.method.toLowerCase();
|
|
40
40
|
const { calendarId } = ctx.state.params;
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
// use exact calendarId match instead of URL substring
|
|
43
|
+
// to prevent routing collisions with calendars named "inbox" or "outbox"
|
|
44
|
+
if (
|
|
45
|
+
calendarId &&
|
|
46
|
+
(calendarId.toLowerCase() === 'inbox' ||
|
|
47
|
+
calendarId.toLowerCase() === 'outbox')
|
|
48
|
+
) {
|
|
46
49
|
log.debug('Routing to scheduling handler', { url: ctx.url, method });
|
|
47
50
|
return scheduling.route(ctx);
|
|
48
51
|
}
|
|
@@ -80,9 +83,13 @@ module.exports = function (options) {
|
|
|
80
83
|
try {
|
|
81
84
|
if (typeof calMethods[method].exec === 'function') {
|
|
82
85
|
setMultistatusResponse(ctx);
|
|
86
|
+
// pass calendar object via ctx.state to avoid
|
|
87
|
+
// redundant getCalendar() calls inside handlers
|
|
88
|
+
ctx.state.calendar = calendar;
|
|
83
89
|
ctx.body = await calMethods[method].exec(ctx, calendar);
|
|
84
90
|
} else if (typeof calMethods[method] === 'function') {
|
|
85
91
|
setMultistatusResponse(ctx);
|
|
92
|
+
ctx.state.calendar = calendar;
|
|
86
93
|
ctx.body = await calMethods[method](ctx, calendar);
|
|
87
94
|
} else {
|
|
88
95
|
log.warn(`method handler not found: ${method}`);
|
|
@@ -152,13 +152,17 @@ module.exports = function (options) {
|
|
|
152
152
|
async function handleItipRequest(ctx, body) {
|
|
153
153
|
log.debug('Processing iTIP request');
|
|
154
154
|
|
|
155
|
+
// RFC 5545 Section 3.1: unfold long content lines before parsing
|
|
156
|
+
// Folded lines start with CRLF followed by a single whitespace character
|
|
157
|
+
const unfolded = body.replaceAll(/\r\n[ \t]/g, '');
|
|
158
|
+
|
|
155
159
|
// Extract METHOD from the iCalendar data
|
|
156
|
-
const methodMatch =
|
|
160
|
+
const methodMatch = unfolded.match(/method:([a-z]+)/i);
|
|
157
161
|
const method = methodMatch ? methodMatch[1].toUpperCase() : 'REQUEST';
|
|
158
162
|
|
|
159
163
|
// Extract attendees
|
|
160
164
|
const attendeeMatches =
|
|
161
|
-
|
|
165
|
+
unfolded.match(/attendee[^:]*:mailto:([^\r\n]+)/gi) || [];
|
|
162
166
|
const attendees = attendeeMatches
|
|
163
167
|
.map((match) => {
|
|
164
168
|
const email = match.match(/mailto:([^\r\n]+)/i);
|
|
@@ -327,11 +331,15 @@ module.exports = function (options) {
|
|
|
327
331
|
*/
|
|
328
332
|
async route(ctx) {
|
|
329
333
|
const method = ctx.method.toLowerCase();
|
|
330
|
-
const url = ctx.url.toLowerCase();
|
|
331
334
|
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
+
// Use calendarId from route params for reliable inbox/outbox detection
|
|
336
|
+
// instead of URL substring matching which can collide with calendar names
|
|
337
|
+
const calId = (
|
|
338
|
+
(ctx.state.params && ctx.state.params.calendarId) ||
|
|
339
|
+
''
|
|
340
|
+
).toLowerCase();
|
|
341
|
+
const isInbox = calId === 'inbox';
|
|
342
|
+
const isOutbox = calId === 'outbox';
|
|
335
343
|
|
|
336
344
|
if (method === 'options') {
|
|
337
345
|
if (isOutbox) {
|
|
@@ -47,6 +47,16 @@ module.exports = function (options) {
|
|
|
47
47
|
response(ctx.url, props.length > 0 ? status[200] : status[404], props)
|
|
48
48
|
];
|
|
49
49
|
|
|
50
|
+
//
|
|
51
|
+
// Performance: Depth:0 means the client only wants the collection
|
|
52
|
+
// itself, not its children (individual calendars).
|
|
53
|
+
//
|
|
54
|
+
const depth = ctx.get('depth') || 'infinity';
|
|
55
|
+
if (depth === '0') {
|
|
56
|
+
const ms = multistatus(responses);
|
|
57
|
+
return build(ms);
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
const calendars = await options.data.getCalendarsForPrincipal(ctx, {
|
|
51
61
|
principalId: ctx.state.params.principalId,
|
|
52
62
|
user: ctx.state.user
|
|
@@ -1,57 +1,80 @@
|
|
|
1
|
-
const _ = require('lodash');
|
|
2
1
|
const xml = require('../../common/xml');
|
|
3
2
|
|
|
4
3
|
// TODO: need to implement tests for MKCALENDAR
|
|
5
4
|
// <https://github.com/sabre-io/dav/blob/da8c1f226f1c053849540a189262274ef6809d1c/tests/Sabre/CalDAV/PluginTest.php#L142-L428>
|
|
5
|
+
|
|
6
|
+
function parseSupportedComponents(node) {
|
|
7
|
+
const comps = [];
|
|
8
|
+
if (!node.childNodes) return comps;
|
|
9
|
+
for (const comp of node.childNodes) {
|
|
10
|
+
if (comp.localName !== 'comp') continue;
|
|
11
|
+
const name = comp.getAttribute ? comp.getAttribute('name') : null;
|
|
12
|
+
if (name) comps.push(name.toUpperCase());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return comps;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
module.exports = function (options) {
|
|
7
19
|
return async function (ctx) {
|
|
8
|
-
const { children } = xml.getWithChildren(
|
|
9
|
-
'/CAL:mkcalendar/D:set/D:prop',
|
|
10
|
-
ctx.request.xml
|
|
11
|
-
);
|
|
12
|
-
|
|
13
20
|
const calendar = {};
|
|
14
|
-
for (const child of children) {
|
|
15
|
-
if (!child.localName || !child.textContent) continue;
|
|
16
|
-
switch (child.localName) {
|
|
17
|
-
case 'displayname': {
|
|
18
|
-
calendar.name = child.textContent;
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
// RFC 4791 Section 5.3.1: the request body is optional
|
|
23
|
+
if (ctx.request.xml) {
|
|
24
|
+
const { children } = xml.getWithChildren(
|
|
25
|
+
'/CAL:mkcalendar/D:set/D:prop',
|
|
26
|
+
ctx.request.xml
|
|
27
|
+
);
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
for (const child of children) {
|
|
30
|
+
if (!child.localName || !child.textContent) continue;
|
|
31
|
+
switch (child.localName) {
|
|
32
|
+
case 'displayname': {
|
|
33
|
+
calendar.name = child.textContent;
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
case 'calendar-description': {
|
|
39
|
+
calendar.description = child.textContent;
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
case 'calendar-timezone': {
|
|
45
|
+
calendar.timezone = child.textContent;
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case 'calendar-color': {
|
|
51
|
+
calendar.color = child.textContent;
|
|
52
|
+
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'calendar-order': {
|
|
57
|
+
calendar.order = Number.parseInt(child.textContent, 10);
|
|
58
|
+
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
40
61
|
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
case 'supported-calendar-component-set': {
|
|
63
|
+
const comps = parseSupportedComponents(child);
|
|
64
|
+
if (comps.length > 0) {
|
|
65
|
+
calendar.supportedComponents = comps;
|
|
66
|
+
}
|
|
43
67
|
|
|
44
|
-
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
// No default
|
|
45
71
|
}
|
|
46
|
-
// No default
|
|
47
72
|
}
|
|
48
73
|
}
|
|
49
74
|
|
|
50
|
-
//
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
err.xml = ctx.request.body;
|
|
54
|
-
throw err;
|
|
75
|
+
// Extract calendarId from URL if available
|
|
76
|
+
if (ctx.state.params && ctx.state.params.calendarId) {
|
|
77
|
+
calendar.calendarId = ctx.state.params.calendarId;
|
|
55
78
|
}
|
|
56
79
|
|
|
57
80
|
// TODO: we may need to implement this similar workaround
|
|
@@ -11,15 +11,32 @@ const commonTags = require('../../common/tags');
|
|
|
11
11
|
module.exports = function (options) {
|
|
12
12
|
const tags = commonTags(options);
|
|
13
13
|
return async function (ctx) {
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// RFC 4918 Section 9.1 says an empty PROPFIND body
|
|
15
|
+
// MUST be treated as an allprop request, not rejected with 400
|
|
16
|
+
let children = [];
|
|
17
|
+
if (ctx.request.xml) {
|
|
18
|
+
({ children } = xml.getWithChildren(
|
|
19
|
+
'/D:propfind/D:prop',
|
|
20
|
+
ctx.request.xml
|
|
21
|
+
));
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
// If no properties requested (allprop), return a default set
|
|
25
|
+
// Each entry must include namespaceURI so getResponse() can resolve the tag handler
|
|
26
|
+
const dav = 'DAV:';
|
|
27
|
+
const cal = 'urn:ietf:params:xml:ns:caldav';
|
|
28
|
+
if (children.length === 0) {
|
|
29
|
+
children = [
|
|
30
|
+
{ namespaceURI: dav, localName: 'displayname' },
|
|
31
|
+
{ namespaceURI: dav, localName: 'resourcetype' },
|
|
32
|
+
{ namespaceURI: dav, localName: 'current-user-principal' },
|
|
33
|
+
{ namespaceURI: dav, localName: 'current-user-privilege-set' },
|
|
34
|
+
{ namespaceURI: cal, localName: 'calendar-home-set' },
|
|
35
|
+
{ namespaceURI: cal, localName: 'calendar-user-address-set' },
|
|
36
|
+
{ namespaceURI: cal, localName: 'schedule-inbox-URL' },
|
|
37
|
+
{ namespaceURI: cal, localName: 'schedule-outbox-URL' }
|
|
38
|
+
];
|
|
39
|
+
}
|
|
23
40
|
|
|
24
41
|
const actions = _.map(children, async (child) => {
|
|
25
42
|
return tags.getResponse({
|
|
@@ -4,6 +4,12 @@ const winston = require('../../common/winston');
|
|
|
4
4
|
module.exports = function (options) {
|
|
5
5
|
const log = winston({ ...options, label: 'principal/report' });
|
|
6
6
|
return async function (ctx) {
|
|
7
|
+
// guard against null/missing XML body
|
|
8
|
+
if (!ctx.request.xml || !ctx.request.xml.documentElement) {
|
|
9
|
+
ctx.status = 400;
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
const rootTag = ctx.request.xml.documentElement.localName;
|
|
8
14
|
if (rootTag === 'principal-search-property-set') {
|
|
9
15
|
log.debug('principal-search-property-set');
|
package/.commitlintrc.js
DELETED
package/.editorconfig
DELETED
package/.gitattributes
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
* text=auto eol=lf
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
on:
|
|
3
|
-
- push
|
|
4
|
-
- pull_request
|
|
5
|
-
jobs:
|
|
6
|
-
build:
|
|
7
|
-
runs-on: ${{ matrix.os }}
|
|
8
|
-
strategy:
|
|
9
|
-
matrix:
|
|
10
|
-
os:
|
|
11
|
-
- ubuntu-latest
|
|
12
|
-
node_version:
|
|
13
|
-
- 18
|
|
14
|
-
name: Node ${{ matrix.node_version }} on ${{ matrix.os }}
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
- name: Setup node
|
|
18
|
-
uses: actions/setup-node@v4
|
|
19
|
-
with:
|
|
20
|
-
node-version: ${{ matrix.node_version }}
|
|
21
|
-
- name: Install dependencies
|
|
22
|
-
run: npm install
|
|
23
|
-
- name: Run tests
|
|
24
|
-
run: npm run test
|
package/.husky/commit-msg
DELETED
package/.husky/pre-commit
DELETED
package/.lintstagedrc.js
DELETED
package/.prettierrc.js
DELETED
package/.remarkignore
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
test/snapshots/**/*.md
|
package/.remarkrc.js
DELETED