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 }