1 /**
2  * Authors: Szabo Bogdan <szabobogdan@yahoo.com>
3  * Date: 2 25, 2015
4  * License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
5  * Copyright: Public Domain
6  */
7 module vibedav.plugins.caldav;
8 
9 public import vibedav.base;
10 import vibedav.plugins.filedav;
11 import vibedav.plugins.syncdav;
12 
13 import vibe.core.file;
14 import vibe.http.server;
15 import vibe.inet.mimetypes;
16 import vibe.inet.message;
17 
18 import std.conv : to;
19 import std.algorithm;
20 import std.file;
21 import std.path;
22 import std.digest.md;
23 import std.datetime;
24 import std..string;
25 import std.stdio;
26 import std.typecons;
27 import std.uri;
28 import std.uuid;
29 
30 private bool matchPluginUrl(Path path, string username) {
31 	if(path.length < 2) {
32 		return false;
33 	}
34 
35 	if(path[0] != "principals") {
36 		return false;
37 	}
38 
39 	if(path[1] != username) {
40 		return false;
41 	}
42 
43 	return true;
44 }
45 
46 interface ICalDavProperties {
47 	@property {
48 
49 		/// rfc4791 - 6.2.1
50 		@ResourceProperty("calendar-home-set", "urn:ietf:params:xml:ns:caldav")
51 		@ResourcePropertyTagText("href", "DAV:")
52 		string[] calendarHomeSet(DavResource resource);
53 
54 		///rfc6638 - 2.1.1
55 		@ResourceProperty("schedule-outbox-URL", "urn:ietf:params:xml:ns:caldav")
56 		@ResourcePropertyTagText("href", "DAV:")
57 		string scheduleOutboxURL(DavResource resource);
58 
59 		/// rfc6638 - 2.2.1
60 		@ResourceProperty("schedule-inbox-URL", "urn:ietf:params:xml:ns:caldav")
61 		@ResourcePropertyTagText("href", "DAV:")
62 		string scheduleInboxURL(DavResource resource);
63 
64 		/// rfc6638 - 2.4.1
65 		@ResourceProperty("calendar-user-address-set", "urn:ietf:params:xml:ns:caldav")
66 		@ResourcePropertyTagText("href", "DAV:")
67 		string[] calendarUserAddressSet(DavResource resource);
68 	}
69 }
70 
71 interface ICalDavCollectionProperties {
72 
73 	@property {
74 		@ResourceProperty("calendar-description", "urn:ietf:params:xml:ns:caldav")
75 		string calendarDescription(DavResource resource);
76 
77 		@ResourceProperty("calendar-timezone", "urn:ietf:params:xml:ns:caldav")
78 		TimeZone calendarTimezone(DavResource resource);
79 
80 		@ResourceProperty("supported-calendar-component-set", "urn:ietf:params:xml:ns:caldav")
81 		@ResourcePropertyValueAttr("comp", "urn:ietf:params:xml:ns:caldav", "name")
82 		string[] supportedCalendarComponentSet(DavResource resource);
83 
84 		@ResourceProperty("supported-calendar-data", "urn:ietf:params:xml:ns:caldav")
85 		@ResourcePropertyTagAttributes("calendar-data", "urn:ietf:params:xml:ns:caldav")
86 		string[string][] supportedCalendarData(DavResource resource);
87 
88 		@ResourceProperty("max-resource-size", "urn:ietf:params:xml:ns:caldav")
89 		ulong maxResourceSize(DavResource resource);
90 
91 		@ResourceProperty("min-date-time", "urn:ietf:params:xml:ns:caldav")
92 		SysTime minDateTime(DavResource resource);
93 
94 		@ResourceProperty("max-date-time", "urn:ietf:params:xml:ns:caldav")
95 		SysTime maxDateTime(DavResource resource);
96 
97 		@ResourceProperty("max-instances", "urn:ietf:params:xml:ns:caldav")
98 		ulong maxInstances(DavResource resource);
99 
100 		@ResourceProperty("max-attendees-per-instance", "urn:ietf:params:xml:ns:caldav")
101 		ulong maxAttendeesPerInstance(DavResource resource);
102 	}
103 }
104 
105 interface ICalDavResourceProperties {
106 
107 	@property {
108 		@ResourceProperty("calendar-data", "urn:ietf:params:xml:ns:caldav")
109 		string calendarData(DavResource resource);
110 	}
111 }
112 
113 interface ICalDavReports {
114 	@DavReport("free-busy-query", "urn:ietf:params:xml:ns:caldav")
115 	void freeBusyQuery(DavRequest request, DavResponse response);
116 
117 	@DavReport("calendar-query", "urn:ietf:params:xml:ns:caldav")
118 	void calendarQuery(DavRequest request, DavResponse response);
119 
120 	@DavReport("calendar-multiget", "urn:ietf:params:xml:ns:caldav")
121 	void calendarMultiget(DavRequest request, DavResponse response);
122 }
123 
124 interface ICalDavSchedulingProperties {
125 
126 	// for Inbox
127 	//<schedule-default-calendar-URL xmlns="urn:ietf:params:xml:ns:caldav" />
128 
129 	/*
130     <default-alarm-vevent-date xmlns="urn:ietf:params:xml:ns:caldav" />
131     <default-alarm-vevent-datetime xmlns="urn:ietf:params:xml:ns:caldav" />
132     <supported-calendar-component-sets xmlns="urn:ietf:params:xml:ns:caldav" />
133     <schedule-calendar-transp xmlns="urn:ietf:params:xml:ns:caldav" />
134     <calendar-free-busy-set xmlns="urn:ietf:params:xml:ns:caldav" />*/
135 }
136 
137 class CalDavDataPlugin : BaseDavResourcePlugin, ICalDavProperties, IDavReportSetProperties, IDavBindingProperties {
138 
139 	string[] calendarHomeSet(DavResource resource) {
140 
141 		if(matchPluginUrl(resource.path, resource.username))
142 			return [ "/" ~ resource.rootURL ~"principals/" ~ resource.username ~ "/calendars/" ];
143 
144 		throw new DavException(HTTPStatus.notFound, "not found");
145 	}
146 
147 	string scheduleOutboxURL(DavResource resource) {
148 		if(matchPluginUrl(resource.path, resource.username))
149 			return "/" ~ resource.rootURL ~"principals/" ~ resource.username ~ "/outbox/";
150 
151 		throw new DavException(HTTPStatus.notFound, "not found");
152 	}
153 
154 	string scheduleInboxURL(DavResource resource) {
155 		if(matchPluginUrl(resource.path, resource.username))
156 			return "/" ~ resource.rootURL ~"principals/" ~ resource.username ~ "/inbox/";
157 
158 		throw new DavException(HTTPStatus.notFound, "not found");
159 	}
160 
161 	string[] calendarUserAddressSet(DavResource resource) {
162 		if(matchPluginUrl(resource.path, resource.username))
163 			return [ "mailto:" ~ resource.username ~ "@local.com" ];
164 
165 		throw new DavException(HTTPStatus.notFound, "not found");
166 	}
167 
168 	string[] supportedReportSet(DavResource resource) {
169 		if(matchPluginUrl(resource.path, resource.username))
170 			return ["free-busy-query:urn:ietf:params:xml:ns:caldav", "calendar-query:urn:ietf:params:xml:ns:caldav", "calendar-multiget:urn:ietf:params:xml:ns:caldav"];
171 
172 		return [];
173 	}
174 
175 	string resourceId(DavResource resource) {
176 		return resource.eTag;
177 	}
178 
179 	override {
180 		bool canGetProperty(DavResource resource, string name) {
181 			if(!matchPluginUrl(resource.path, resource.username))
182 				return false;
183 
184 			if(hasDavInterfaceProperty!ICalDavProperties(name))
185 				return true;
186 
187 			if(hasDavInterfaceProperty!IDavReportSetProperties(name))
188 				return true;
189 
190 			if(hasDavInterfaceProperty!IDavBindingProperties(name))
191 				return true;
192 
193 			return false;
194 		}
195 
196 		DavProp property(DavResource resource, string name) {
197 			if(!matchPluginUrl(resource.path, resource.username))
198 				throw new DavException(HTTPStatus.internalServerError, "Can't get property.");
199 
200 			if(hasDavInterfaceProperty!ICalDavProperties(name))
201 				return getDavInterfaceProperty!ICalDavProperties(name, this, resource);
202 
203 			if(hasDavInterfaceProperty!IDavReportSetProperties(name))
204 				return getDavInterfaceProperty!IDavReportSetProperties(name, this, resource);
205 
206 			if(hasDavInterfaceProperty!IDavBindingProperties(name))
207 				return getDavInterfaceProperty!IDavBindingProperties(name, this, resource);
208 
209 			throw new DavException(HTTPStatus.internalServerError, "Can't get property.");
210 		}
211 	}
212 
213 	@property {
214 		string name() {
215 			return "CalDavDataPlugin";
216 		}
217 	}
218 }
219 
220 class CalDavResourcePlugin : BaseDavResourcePlugin, ICalDavResourceProperties {
221 	string calendarData(DavResource resource) {
222 
223 		auto content = resource.stream;
224 		string data;
225 
226 		while(!content.empty) {
227 			auto leastSize = content.leastSize;
228 			ubyte[] buf;
229 
230 			buf.length = leastSize;
231 			content.read(buf);
232 
233 			data ~= buf;
234 		}
235 
236 		return data;
237 	}
238 
239 	override {
240 
241 		bool canGetProperty(DavResource resource, string name) {
242 			if(matchPluginUrl(resource.path, resource.username) && hasDavInterfaceProperty!ICalDavResourceProperties(name))
243 				return true;
244 
245 			return false;
246 		}
247 
248 		DavProp property(DavResource resource, string name) {
249 			if(!matchPluginUrl(resource.path, resource.username))
250 				throw new DavException(HTTPStatus.internalServerError, "Can't get property.");
251 
252 			if(hasDavInterfaceProperty!ICalDavResourceProperties(name))
253 				return getDavInterfaceProperty!ICalDavResourceProperties(name, this, resource);
254 
255 			throw new DavException(HTTPStatus.internalServerError, "Can't get property.");
256 		}
257 	}
258 
259 
260 	@property {
261 		string name() {
262 			return "CalDavResourcePlugin";
263 		}
264 	}
265 }
266 
267 class CalDavCollectionPlugin : BaseDavResourcePlugin, ICalDavCollectionProperties {
268 
269 	string calendarDescription(DavResource resource) {
270 		return resource.name;
271 	}
272 
273 	TimeZone calendarTimezone(DavResource resource) {
274 		TimeZone t;
275 		return t;
276 	}
277 
278 	string[] supportedCalendarComponentSet(DavResource resource) {
279 		return ["VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE", "VALARM"];
280 	}
281 
282 	string[string][] supportedCalendarData(DavResource resource) {
283 		string[string][] list;
284 
285 		list ~= [["content-type": "text/calendar", "version": "2.0"]];
286 
287 		return list;
288 	}
289 
290 	ulong maxResourceSize(DavResource resource) {
291 		return ulong.max;
292 	}
293 
294 	SysTime minDateTime(DavResource resource) {
295 		return SysTime.min;
296 	}
297 
298 	SysTime maxDateTime(DavResource resource) {
299 		return SysTime.max;
300 	}
301 
302 	ulong maxInstances(DavResource resource) {
303 		return ulong.max;
304 	}
305 
306 	ulong maxAttendeesPerInstance(DavResource resource) {
307 		return ulong.max;
308 	}
309 
310 	override {
311 
312 		bool canGetProperty(DavResource resource, string name) {
313 			if(matchPluginUrl(resource.path, resource.username) && hasDavInterfaceProperty!ICalDavCollectionProperties(name))
314 				return true;
315 
316 			return false;
317 		}
318 
319 		DavProp property(DavResource resource, string name) {
320 			if(!matchPluginUrl(resource.path, resource.username))
321 				throw new DavException(HTTPStatus.internalServerError, "Can't get property.");
322 
323 			if(hasDavInterfaceProperty!ICalDavCollectionProperties(name))
324 				return getDavInterfaceProperty!ICalDavCollectionProperties(name, this, resource);
325 
326 			throw new DavException(HTTPStatus.internalServerError, "Can't get property.");
327 		}
328 	}
329 
330 	@property {
331 		string name() {
332 			return "CalDavCollectionPlugin";
333 		}
334 	}
335 }
336 
337 class CalDavPlugin : BaseDavPlugin, ICalDavReports {
338 
339 	this(IDav dav) {
340 		super(dav);
341 	}
342 
343 	bool isCalendarsCollection(Path path, string username) {
344 		if(!matchPluginUrl(path, username))
345 			return false;
346 
347 		return path.length == 3 && path[2] == "calendars";
348 	}
349 
350 	bool isPrincipalCollection(Path path, string username) {
351 		if(!matchPluginUrl(path, username))
352 			return false;
353 
354 		return path.length == 2;
355 	}
356 
357 	override {
358 		bool exists(URL url, string username) {
359 			return isCalendarsCollection(dav.path(url), username);
360 		}
361 
362 		Path[] childList(URL url, string username) {
363 			if (isPrincipalCollection(dav.path(url), username)) {
364 				return [ Path("principals/" ~ username ~ "/calendars/") ];
365 			}
366 
367 			return [];
368 		}
369 
370 		DavResource getResource(URL url, string username) {
371 			if(isCalendarsCollection(dav.path(url), username)) {
372 				DavResource resource = super.getResource(url, username);
373 				resource.resourceType ~= "collection:DAV:";
374 
375 				return resource;
376 			}
377 
378 			throw new DavException(HTTPStatus.internalServerError, "Can't get resource.");
379 		}
380 
381 		void bindResourcePlugins(DavResource resource) {
382 			if(!matchPluginUrl(resource.path, resource.username))
383 				return;
384 
385 			resource.registerPlugin(new CalDavDataPlugin);
386 			auto path = resource.url.path.toString.stripSlashes;
387 
388 			if(resource.isCollection && path != "principals/" ~ resource.username ~ "/calendars") {
389 				resource.resourceType ~= "calendar:urn:ietf:params:xml:ns:caldav";
390 				resource.registerPlugin(new CalDavCollectionPlugin);
391 			} else if(!resource.isCollection && path.length > 4 && path[$-4..$].toLower == ".ics") {
392 				resource.registerPlugin(new CalDavResourcePlugin);
393 			}
394 		}
395 
396 		bool hasReport(URL url, string username, string name) {
397 
398 			if(!matchPluginUrl(dav.path(url), username))
399 				return false;
400 
401 			if(hasDavReport!ICalDavReports(name))
402 				return true;
403 
404 			return false;
405 		}
406 
407 		void report(DavRequest request, DavResponse response) {
408 			if(!matchPluginUrl(dav.path(request.url), request.username) || !hasDavReport!ICalDavReports(request.content.reportName))
409 				throw new DavException(HTTPStatus.internalServerError, "Can't get report.");
410 
411 			getDavReport!ICalDavReports(this, request, response);
412 		}
413 	}
414 
415 	void freeBusyQuery(DavRequest request, DavResponse response) {
416 		throw new DavException(HTTPStatus.internalServerError, "Not Implemented");
417 	}
418 
419 	void calendarQuery(DavRequest request, DavResponse response) {
420 		throw new DavException(HTTPStatus.internalServerError, "Not Implemented");
421 	}
422 
423 	void calendarMultiget(DavRequest request, DavResponse response) {
424 		response.mimeType = "application/xml";
425 		response.statusCode = HTTPStatus.multiStatus;
426 		auto reportData = request.content;
427 
428 		bool[string] requestedProperties;
429 
430 		foreach(name, p; reportData["calendar-multiget"]["prop"])
431 			requestedProperties[name] = true;
432 
433 		DavResource[] list;
434 
435 		auto hrefList = [ reportData["calendar-multiget"] ].getTagChilds("href");
436 
437 		HTTPStatus[string] resourceStatus;
438 
439 		foreach(p; hrefList) {
440 			string path = p.value;
441 
442 			try {
443 				list ~= dav.getResource(URL(path), request.username);
444 				resourceStatus[path] = HTTPStatus.ok;
445 			} catch(DavException e) {
446 				resourceStatus[path] = e.status;
447 			}
448 		}
449 
450 		response.setPropContent(list, requestedProperties, resourceStatus);
451 		response.flush;
452 	}
453 
454 	@property {
455 
456 		string name() {
457 			return "CalDavPlugin";
458 		}
459 
460 		override string[] support(URL url, string username) {
461 			if(matchPluginUrl(dav.path(url), username))
462 				return [ "calendar-access" ];
463 
464 			return [];
465 		}
466 	}
467 }