diff --git a/.gitignore b/.gitignore index 2c05782..d613452 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,9 @@ examples/fileDav/dub.selections.json .buildpath .settings/org.dsource.ddt.ide.core.prefs + +examples/fileDav/dub.selections.json + +examples/fileDav/dub.selections.json + +examples/fileDav/dub.selections.json diff --git a/README.md b/README.md index 2d011ff..b44cd1a 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,4 @@ auto router = new URLRouter; * Improve XML support (eg: change xml node format from "name:DAV:" to "{DAV:}name") * Add DB support * Add migration tools from https://github.com/Kozea/Radicale +* Maybe update the @ResourceProperty... Structs to something more general like @ResourceProperty!"%value"() diff --git a/dub.json b/dub.json index 4c938db..054b322 100644 --- a/dub.json +++ b/dub.json @@ -5,7 +5,7 @@ "authors": ["Szabo Bogdan"], "license": "MIT", "dependencies": { - "vibe-d": "~>0.7.22", + "vibe-d": "~>0.7.23", "tested": "~>0.9.2" }, diff --git a/examples/webDav/dub.json b/examples/webDav/dub.json index f22e65c..a38c500 100644 --- a/examples/webDav/dub.json +++ b/examples/webDav/dub.json @@ -4,7 +4,7 @@ "copyright": "Copyright © 2015, Szabo Bogdan", "authors": ["Szabo Bogdan"], "dependencies": { - "vibe-d": "~>0.7.19", + "vibe-d": "~>0.7.23", "vibe-dav": { "version": "~>0.2.0", "path": "../.." diff --git a/examples/webDav/public/calendar/admin/personal/6837CB82-AE37-4E57-9C89-D39D62BBBC01.ics b/examples/webDav/public/calendar/admin/personal/6837CB82-AE37-4E57-9C89-D39D62BBBC01.ics deleted file mode 100644 index 30c467a..0000000 --- a/examples/webDav/public/calendar/admin/personal/6837CB82-AE37-4E57-9C89-D39D62BBBC01.ics +++ /dev/null @@ -1,41 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Apple Inc.//Mac OS X 10.10.3//EN -CALSCALE:GREGORIAN -BEGIN:VTIMEZONE -TZID:Europe/Stockholm -BEGIN:DAYLIGHT -TZOFFSETFROM:+0100 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -DTSTART:19810329T020000 -TZNAME:CEST -TZOFFSETTO:+0200 -END:DAYLIGHT -BEGIN:STANDARD -TZOFFSETFROM:+0200 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -DTSTART:19961027T030000 -TZNAME:CET -TZOFFSETTO:+0100 -END:STANDARD -END:VTIMEZONE -BEGIN:VEVENT -TRANSP:OPAQUE -DTEND;TZID=Europe/Stockholm:20150408T170000 -PRIORITY:0 -UID:6837CB82-AE37-4E57-9C89-D39D62BBBC01 -DTSTAMP:20150407T131647Z -X-YAHOO-USER-STATUS:BUSY -X-YAHOO-USER-STATUS:BUSY -STATUS:CONFIRMED -SEQUENCE:0 -CLASS:PUBLIC -X-YAHOO-YID:szabobogdan -X-YAHOO-YID:szabobogdan -X-YAHOO-EVENT-STATUS:BUSY -X-YAHOO-EVENT-STATUS:BUSY -SUMMARY:test event -DTSTART;TZID=Europe/Stockholm:20150407T160000 -CREATED:20150407T131647Z -END:VEVENT -END:VCALENDAR diff --git a/examples/webDav/public/calendar/admin/personal/7B508AD5-FE51-4C7B-8E21-0104F5412923.ics b/examples/webDav/public/calendar/admin/personal/7B508AD5-FE51-4C7B-8E21-0104F5412923.ics deleted file mode 100644 index 8d3926d..0000000 --- a/examples/webDav/public/calendar/admin/personal/7B508AD5-FE51-4C7B-8E21-0104F5412923.ics +++ /dev/null @@ -1,32 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Apple Inc.//Mac OS X 10.10.3//EN -CALSCALE:GREGORIAN -BEGIN:VTIMEZONE -TZID:Europe/Stockholm -BEGIN:DAYLIGHT -TZOFFSETFROM:+0100 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -DTSTART:19810329T020000 -TZNAME:CEST -TZOFFSETTO:+0200 -END:DAYLIGHT -BEGIN:STANDARD -TZOFFSETFROM:+0200 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -DTSTART:19961027T030000 -TZNAME:CET -TZOFFSETTO:+0100 -END:STANDARD -END:VTIMEZONE -BEGIN:VEVENT -CREATED:20150407T194949Z -UID:7B508AD5-FE51-4C7B-8E21-0104F5412923 -DTEND;TZID=Europe/Stockholm:20150422T100000 -TRANSP:OPAQUE -SUMMARY:cucu -DTSTART;TZID=Europe/Stockholm:20150422T090000 -DTSTAMP:20150407T194949Z -SEQUENCE:0 -END:VEVENT -END:VCALENDAR diff --git a/examples/webDav/public/calendar/admin/personal/main.ics b/examples/webDav/public/calendar/admin/personal/main.ics deleted file mode 100644 index 2012da3..0000000 --- a/examples/webDav/public/calendar/admin/personal/main.ics +++ /dev/null @@ -1,77 +0,0 @@ -BEGIN:VCALENDAR -METHOD:PUBLISH -VERSION:2.0 -X-WR-CALNAME:Home -X-WR-CALDESC: -X-APPLE-CALENDAR-COLOR:#1BADF8 -X-WR-TIMEZONE:Europe/Bucharest -CALSCALE:GREGORIAN -BEGIN:VTIMEZONE -TZID:Europe/Bucharest -BEGIN:DAYLIGHT -TZOFFSETFROM:+0200 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -DTSTART:19970330T030000 -TZNAME:EEST -TZOFFSETTO:+0300 -END:DAYLIGHT -BEGIN:STANDARD -TZOFFSETFROM:+0300 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -DTSTART:19971026T040000 -TZNAME:EET -TZOFFSETTO:+0200 -END:STANDARD -END:VTIMEZONE -BEGIN:VEVENT -CREATED:20140214T081455Z -UID:489F8C95-B5B3-49E3-BA4D-0CE0F820E022 -DTEND;TZID=Europe/Bucharest:20140214T120000 -TRANSP:OPAQUE -SUMMARY:New Event -DTSTART;TZID=Europe/Bucharest:20140214T110000 -DTSTAMP:20140214T081455Z -SEQUENCE:1 -END:VEVENT -BEGIN:VEVENT -CREATED:20140912T181025Z -UID:BAF9FA4C-C758-4E73-87C2-847A879AC884 -DTEND;TZID=Europe/Bucharest:20200724T130000 -TRANSP:OPAQUE -SUMMARY:New Event -DTSTART;TZID=Europe/Bucharest:20200724T120000 -DTSTAMP:20140911T200404Z -LAST-MODIFIED:20140131T143908Z -SEQUENCE:0 -END:VEVENT -BEGIN:VEVENT -CREATED:20140912T181025Z -UID:349C7C65-CD2F-44B6-8467-FF9C22F58832 -RRULE:FREQ=WEEKLY;COUNT=1 -DTEND;TZID=Europe/Bucharest:20140212T130000 -TRANSP:OPAQUE -SUMMARY:New Event -DTSTART;TZID=Europe/Bucharest:20140212T120000 -DTSTAMP:20140911T200404Z -LAST-MODIFIED:20140201T082515Z -SEQUENCE:0 -BEGIN:VALARM -X-WR-ALARMUID:95787692-6F0A-48A3-B4FE-B4C4CB0540F7 -UID:95787692-6F0A-48A3-B4FE-B4C4CB0540F7 -TRIGGER:-PT30M -DESCRIPTION:Event reminder -ACTION:DISPLAY -END:VALARM -END:VEVENT -BEGIN:VEVENT -CREATED:20140912T181025Z -UID:8FC79E15-43C5-409A-B541-4DD21F2E9C89 -DTEND;TZID=Europe/Bucharest:20131230T130000 -TRANSP:OPAQUE -SUMMARY:Eveniment nou -DTSTART;TZID=Europe/Bucharest:20131230T120000 -DTSTAMP:20140911T200404Z -LAST-MODIFIED:20140131T124833Z -SEQUENCE:0 -END:VEVENT -END:VCALENDAR diff --git a/examples/webDav/public/principals/admin/calendars/Personal/4AF119BE-84A2-462C-9F8A-529F01454B18.ics b/examples/webDav/public/principals/admin/calendars/Personal/4AF119BE-84A2-462C-9F8A-529F01454B18.ics new file mode 100644 index 0000000..1824c02 --- /dev/null +++ b/examples/webDav/public/principals/admin/calendars/Personal/4AF119BE-84A2-462C-9F8A-529F01454B18.ics @@ -0,0 +1,32 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.10.3//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Bucharest +BEGIN:DAYLIGHT +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19970330T030000 +TZNAME:EEST +TZOFFSETTO:+0300 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0300 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19971026T040000 +TZNAME:EET +TZOFFSETTO:+0200 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20150426T104512Z +UID:4AF119BE-84A2-462C-9F8A-529F01454B18 +DTEND;TZID=Europe/Bucharest:20150415T100000 +TRANSP:OPAQUE +SUMMARY:New Event +DTSTART;TZID=Europe/Bucharest:20150415T090000 +DTSTAMP:20150426T104512Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/examples/webDav/source/app.d b/examples/webDav/source/app.d index bd0c876..2007a1d 100644 --- a/examples/webDav/source/app.d +++ b/examples/webDav/source/app.d @@ -4,28 +4,17 @@ * License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. * Copyright: Public Domain */ -import vibe.core.file; -import vibe.core.log; -import vibe.inet.message; -import vibe.inet.mimetypes; import vibe.http.router : URLRouter; import vibe.http.server; -import vibe.http.fileserver; import vibe.http.auth.basic_auth; import vibedav.filedav; +import vibedav.acldav; import vibedav.caldav; -import vibedav.user; -import vibedav.prop; -import vibedav.userhome; +import vibedav.syncdav; import core.time; -import std.conv : to; import std.stdio; -import std.file; -import std.path; -import std.digest.md; -import std.datetime; import std.functional : toDelegate; bool checkPassword(string user, string password) @@ -35,29 +24,21 @@ bool checkPassword(string user, string password) shared static this() { - writeln("Starting WebDav server."); + writeln("Start Kangal server"); auto router = new URLRouter; // now any request is matched and checked for authentication: - router.any("/calendar/*", performBasicAuth("Site Realm", toDelegate(&checkPassword))); + router.any("/principals/*", performBasicAuth("Site Realm", toDelegate(&checkPassword))); - auto userConnection = new BaseCalDavUserCollection; + auto dav = router.serveFileDav("", "public"); - alias factory = FileDavResourceFactory!( - "calendar", "public/calendar", - "", FileDavCollection, FileDavResource, - ":user/", FileDavCollection, FileDavResource, - ":user/inbox", FileDavCollection, FileDavResource, - ":user/outbox", FileDavCollection, FileDavResource, - ":user/personal", FileDavCalendarCollection, FileDavCalendarResource - ); - - router.serveFileDav!("/files/", "public/files/")(userConnection); - router.serveFileDav!factory(userConnection); + new ACLDavPlugin(dav); + new CalDavPlugin(dav); + new SyncDavPlugin(dav); auto settings = new HTTPServerSettings; settings.port = 8080; - settings.bindAddresses = ["::1", "127.0.0.1"]; + settings.bindAddresses = ["::1", "127.0.0.1", "192.168.0.13", "192.168.0.100"]; listenHTTP(settings, router); } diff --git a/source/vibedav/base.d b/source/vibedav/base.d index bd8cb87..fd9840d 100644 --- a/source/vibedav/base.d +++ b/source/vibedav/base.d @@ -57,6 +57,67 @@ class DavException : Exception { } } +struct DavReport { + string name; + string ns; +} + +string reportName(DavProp reportBody) { + if(reportBody[0].name == "?xml") + return reportBody[1].tagName ~ ":" ~ reportBody[1].namespace; + + return reportBody[0].tagName ~ ":" ~ reportBody[0].namespace; +} + +DavReport getReportProperty(T...)() { + static if(T.length == 0) + static assert(false, "There is no `@DavReport` attribute."); + else static if( is(typeof(T[0]) == DavReport) ) + return T[0]; + else + return getResourceProperty!(T[1..$]); +} + +bool hasDavReport(I)(string key) { + bool result = false; + + void keyExist(T...)() { + static if(T.length > 0) { + enum val = getReportProperty!(__traits(getAttributes, __traits(getMember, I, T[0]))); + enum staticKey = val.name ~ ":" ~ val.ns; + + if(staticKey == key) + result = true; + + keyExist!(T[1..$])(); + } + } + + keyExist!(__traits(allMembers, I))(); + + return result; +} + +void getDavReport(I)(I plugin, DavRequest request, DavResponse response) { + string key = request.content.reportName; + + void getProp(T...)() { + static if(T.length > 0) { + enum val = getReportProperty!(__traits(getAttributes, __traits(getMember, I, T[0]))); + enum staticKey = val.name ~ ":" ~ val.ns; + + + if(staticKey == key) { + __traits(getMember, plugin, T[0])(request, response); + } + + getProp!(T[1..$])(); + } + } + + getProp!(__traits(allMembers, I))(); +} + interface IDavResourceAccess { bool exists(URL url, string username); bool canCreateCollection(URL url, string username); @@ -71,6 +132,12 @@ interface IDavResourceAccess { } interface IDavPlugin : IDavResourceAccess { + + bool hasReport(URL url, string username, string name); + void report(DavRequest request, DavResponse response); + + void notice(string action, DavResource resource); + @property { IDav dav(); string name(); @@ -86,9 +153,10 @@ interface IDavPluginHub { abstract class BaseDavPlugin : IDavPlugin { - private IDav _dav; + protected IDav _dav; this(IDav dav) { + dav.registerPlugin(this); _dav = dav; } @@ -122,6 +190,18 @@ abstract class BaseDavPlugin : IDavPlugin { void bindResourcePlugins(DavResource resource) { } + bool hasReport(URL url, string username, string name) { + return false; + } + + void report(DavRequest request, DavResponse response) { + throw new DavException(HTTPStatus.internalServerError, "Can't get report."); + } + + void notice(string action, DavResource resource) { + + } + @property { IDav dav() { return _dav; @@ -146,6 +226,10 @@ interface IDav : IDavResourceAccess, IDavPluginHub { void copy(DavRequest request, DavResponse response); void report(DavRequest request, DavResponse response); + void notice(string action, DavResource resource); + + DavResource[] getResources(URL url, ulong depth, string username); + @property Path rootUrl(); } @@ -241,20 +325,23 @@ class Dav : IDav { void removeResource(URL url, string username) { foreach_reverse(plugin; plugins) - if(plugin.exists(url, username)) + if(plugin.exists(url, username)) { + notice("deleted", getResource(url, username)); return plugin.removeResource(url, username); + } throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found."); } DavResource getResource(URL url, string username) { - foreach_reverse(plugin; plugins) + foreach_reverse(plugin; plugins) { if(plugin.exists(url, username)) { auto res = plugin.getResource(url, username); bindResourcePlugins(res); return res; } + } throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found."); } @@ -292,6 +379,8 @@ class Dav : IDav { auto res = plugin.createCollection(url, username); bindResourcePlugins(res); + notice("created", res); + return res; } @@ -304,13 +393,14 @@ class Dav : IDav { auto res = plugin.createResource(url, username); bindResourcePlugins(res); + notice("created", res); + return res; } throw new DavException(HTTPStatus.methodNotAllowed, "No plugin available."); } - void bindResourcePlugins(DavResource resource) { foreach(plugin; plugins) plugin.bindResourcePlugins(resource); @@ -374,6 +464,36 @@ class Dav : IDav { response.flush; } + void proppatch(DavRequest request, DavResponse response) { + auto ifHeader = request.ifCondition; + response.statusCode = HTTPStatus.ok; + + DavStorage.locks.check(request.url, ifHeader); + DavResource resource = getResource(request.url, request.username); + + notice("changed", resource); + + auto xmlString = resource.propPatch(request.content); + + response.content = xmlString; + response.flush; + } + + void report(DavRequest request, DavResponse response) { + auto ifHeader = request.ifCondition; + + string report = ""; + + foreach_reverse(plugin; plugins) { + if(plugin.hasReport(request.url, request.username, request.content.reportName)) { + plugin.report(request, response); + return; + } + } + + throw new DavException(HTTPStatus.notFound, "There is no report."); + } + void lock(DavRequest request, DavResponse response) { auto ifHeader = request.ifCondition; @@ -464,6 +584,7 @@ class Dav : IDav { DavStorage.locks.check(request.url, request.ifCondition); resource.setContent(request.stream, request.contentLength); + notice("changed", resource); DavStorage.locks.setETag(resource.url, resource.eTag); @@ -472,19 +593,6 @@ class Dav : IDav { response.flush; } - void proppatch(DavRequest request, DavResponse response) { - auto ifHeader = request.ifCondition; - response.statusCode = HTTPStatus.ok; - - DavStorage.locks.check(request.url, ifHeader); - DavResource resource = getResource(request.url, request.username); - - auto xmlString = resource.propPatch(request.content); - - response.content = xmlString; - response.flush; - } - void mkcol(DavRequest request, DavResponse response) { auto ifHeader = request.ifCondition; @@ -498,6 +606,7 @@ class Dav : IDav { response.statusCode = HTTPStatus.created; createCollection(request.url, request.username); + notice("created", getResource(request.url, request.username)); response.flush; } @@ -533,10 +642,6 @@ class Dav : IDav { response.flush; } - void report(DavRequest request, DavResponse response) { - - } - void copy(DavRequest request, DavResponse response) { string username = request.username; @@ -608,9 +713,15 @@ class Dav : IDav { } localCopy(source, destination); + notice("changed", destination); response.flush; } + + void notice(string action, DavResource resource) { + foreach_reverse(plugin; plugins) + plugin.notice(action, resource); + } } /// Hook vibe.d requests to the right DAV method diff --git a/source/vibedav/caldav.d b/source/vibedav/caldav.d index a14adb8..99c8277 100644 --- a/source/vibedav/caldav.d +++ b/source/vibedav/caldav.d @@ -8,6 +8,7 @@ module vibedav.caldav; public import vibedav.base; import vibedav.filedav; +import vibedav.syncdav; import vibe.core.file; import vibe.http.server; @@ -53,7 +54,6 @@ interface ICalDavProperties { } } - interface ICalDavCollectionProperties { @property { @@ -85,21 +85,40 @@ interface ICalDavCollectionProperties { @ResourceProperty("max-attendees-per-instance", "urn:ietf:params:xml:ns:caldav") ulong maxAttendeesPerInstance(DavResource resource); + } +} - /* - - - - - */ +interface ICalDavResourceProperties { + + @property { + @ResourceProperty("calendar-data", "urn:ietf:params:xml:ns:caldav") + string calendarData(DavResource resource); } } +interface ICalDavReports { + @DavReport("free-busy-query", "urn:ietf:params:xml:ns:caldav") + void freeBusyQuery(DavRequest request, DavResponse response); + + @DavReport("calendar-query", "urn:ietf:params:xml:ns:caldav") + void calendarQuery(DavRequest request, DavResponse response); + + @DavReport("calendar-multiget", "urn:ietf:params:xml:ns:caldav") + void calendarMultiget(DavRequest request, DavResponse response); +} + interface ICalDavSchedulingProperties { // for Inbox // + + /* + + + + + */ } private bool matchPluginUrl(URL url, string username) { @@ -117,7 +136,7 @@ private bool matchPluginUrl(URL url, string username) { return false; } -class CalDavResourcePlugin : BaseDavResourcePlugin, ICalDavProperties, IDavReportSetProperties, IDavBindingProperties { +class CalDavDataPlugin : BaseDavResourcePlugin, ICalDavProperties, IDavReportSetProperties, IDavBindingProperties { string[] calendarHomeSet(DavResource resource) { @@ -201,6 +220,53 @@ class CalDavResourcePlugin : BaseDavResourcePlugin, ICalDavProperties, IDavRepor } +class CalDavResourcePlugin : BaseDavResourcePlugin, ICalDavResourceProperties { + string calendarData(DavResource resource) { + + auto content = resource.stream; + string data; + + while(!content.empty) { + auto leastSize = content.leastSize; + ubyte[] buf; + + buf.length = leastSize; + content.read(buf); + + data ~= buf; + } + + return data; + } + + override { + + bool canGetProperty(DavResource resource, string name) { + if(matchPluginUrl(resource.url, resource.username) && hasDavInterfaceProperty!ICalDavResourceProperties(name)) + return true; + + return false; + } + + DavProp property(DavResource resource, string name) { + if(!matchPluginUrl(resource.url, resource.username)) + throw new DavException(HTTPStatus.internalServerError, "Can't get property."); + + if(hasDavInterfaceProperty!ICalDavResourceProperties(name)) + return getDavInterfaceProperty!ICalDavResourceProperties(name, this, resource); + + throw new DavException(HTTPStatus.internalServerError, "Can't get property."); + } + } + + + @property { + string name() { + return "CalDavResourcePlugin"; + } + } +} + class CalDavCollectionPlugin : BaseDavResourcePlugin, ICalDavCollectionProperties { string calendarDescription(DavResource resource) { @@ -271,24 +337,86 @@ class CalDavCollectionPlugin : BaseDavResourcePlugin, ICalDavCollectionPropertie } } -class CalDavPlugin : BaseDavPlugin { +class CalDavPlugin : BaseDavPlugin, ICalDavReports { this(IDav dav) { super(dav); } + override { + + void bindResourcePlugins(DavResource resource) { - override void bindResourcePlugins(DavResource resource) { + if(!matchPluginUrl(resource.url, resource.username)) + return; + + resource.registerPlugin(new CalDavDataPlugin); + auto path = resource.url.path.toString.stripSlashes; - if(!matchPluginUrl(resource.url, resource.username)) - return; + if(resource.isCollection && path != "principals/" ~ resource.username ~ "/calendars") { + resource.resourceType ~= "calendar:urn:ietf:params:xml:ns:caldav"; + resource.registerPlugin(new CalDavCollectionPlugin); + } else if(!resource.isCollection && path.length > 4 && path[$-4..$].toLower == ".ics") { + resource.registerPlugin(new CalDavResourcePlugin); + } + } - resource.registerPlugin(new CalDavResourcePlugin); + bool hasReport(URL url, string username, string name) { + + if(!matchPluginUrl(url, username)) + return false; - if(resource.isCollection && resource.url.path.toString.stripSlashes != "principals/" ~ resource.username ~ "/calendars") { - resource.resourceType ~= "calendar:urn:ietf:params:xml:ns:caldav"; - resource.registerPlugin(new CalDavCollectionPlugin); + if(hasDavReport!ICalDavReports(name)) + return true; + + return false; } + + void report(DavRequest request, DavResponse response) { + if(!matchPluginUrl(request.url, request.username) || !hasDavReport!ICalDavReports(request.content.reportName)) + throw new DavException(HTTPStatus.internalServerError, "Can't get report."); + + getDavReport!ICalDavReports(this, request, response); + } + } + + void freeBusyQuery(DavRequest request, DavResponse response) { + throw new DavException(HTTPStatus.internalServerError, "Not Implemented"); + } + + void calendarQuery(DavRequest request, DavResponse response) { + throw new DavException(HTTPStatus.internalServerError, "Not Implemented"); + } + + void calendarMultiget(DavRequest request, DavResponse response) { + response.mimeType = "application/xml"; + response.statusCode = HTTPStatus.multiStatus; + auto reportData = request.content; + + bool[string] requestedProperties; + + foreach(name, p; reportData["calendar-multiget"]["prop"]) + requestedProperties[name] = true; + + DavResource[] list; + + auto hrefList = [ reportData["calendar-multiget"] ].getTagChilds("href"); + + HTTPStatus[string] resourceStatus; + + foreach(p; hrefList) { + string path = p.value; + + try { + list ~= dav.getResource(URL(path), request.username); + resourceStatus[path] = HTTPStatus.ok; + } catch(DavException e) { + resourceStatus[path] = e.status; + } + } + + response.setPropContent(list, requestedProperties, resourceStatus); + response.flush; } @property { diff --git a/source/vibedav/davresource.d b/source/vibedav/davresource.d index 55817cd..8a77b9c 100644 --- a/source/vibedav/davresource.d +++ b/source/vibedav/davresource.d @@ -688,7 +688,6 @@ class DavResource : IDavResourcePluginHub { foreach(prop; removeList) foreach(string key, p; prop) { auto status = removeProperty(key); - result ~= `` ~ p.toString ~ ``; result ~= `HTTP/1.1 ` ~ status.to!int.to!string ~ ` ` ~ status.to!string ~ ``; } @@ -743,7 +742,7 @@ class DavResource : IDavResourcePluginHub { return; } - throw new DavException(HTTPStatus.methodNotAllowed, "No plugin support."); + throw new DavException(HTTPStatus.methodNotAllowed, "setContent No plugin support."); } void setContent(InputStream content, ulong size) { @@ -755,7 +754,7 @@ class DavResource : IDavResourcePluginHub { return; } - throw new DavException(HTTPStatus.methodNotAllowed, "No plugin support."); + throw new DavException(HTTPStatus.methodNotAllowed, "setContent No plugin support."); } @property { @@ -764,7 +763,7 @@ class DavResource : IDavResourcePluginHub { if(plugin.canGetStream(this)) return plugin.stream(this); - throw new DavException(HTTPStatus.methodNotAllowed, "No plugin support."); + throw new DavException(HTTPStatus.methodNotAllowed, "stream No plugin support."); } pure nothrow bool isCollection() { diff --git a/source/vibedav/filedav.d b/source/vibedav/filedav.d index 9f2fa09..cac3037 100644 --- a/source/vibedav/filedav.d +++ b/source/vibedav/filedav.d @@ -313,16 +313,15 @@ class FileResourcePlugin : IDavResourcePlugin { } /// File Dav impplementation -class FileDav : IDavPlugin { +class FileDav : BaseDavPlugin { private { - IDav _dav; Path baseUrlPath; Path basePath; } this(IDav dav, Path baseUrlPath, Path basePath) { - _dav = dav; + super(dav); this.baseUrlPath = baseUrlPath; this.basePath = basePath; } @@ -347,87 +346,91 @@ class FileDav : IDavPlugin { return getFilePath(baseUrlPath, basePath, url); } - bool exists(URL url, string username) { - auto filePath = filePath(url); + override { + bool exists(URL url, string username) { + auto filePath = filePath(url); - return filePath.toString.exists; - } + return filePath.toString.exists; + } - bool canCreateResource(URL url, string username) { - return !exists(url, username); - } + bool canCreateResource(URL url, string username) { + return !exists(url, username); + } - bool canCreateCollection(URL url, string username) { - return !exists(url, username); - } + bool canCreateCollection(URL url, string username) { + return !exists(url, username); + } - void removeResource(URL url, string username) { - if(!exists(url, username)) - throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found."); + void removeResource(URL url, string username) { + if(!exists(url, username)) + throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found."); - auto filePath = filePath(url).toString; + auto filePath = filePath(url).toString; - if(filePath.isDir) - filePath.rmdirRecurse; - else - filePath.remove; + if(filePath.isDir) + filePath.rmdirRecurse; + else + filePath.remove; + } - } + DavResource getResource(URL url, string username) { + if(!exists(url, username)) + throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found."); - DavResource getResource(URL url, string username) { - if(!exists(url, username)) - throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found."); + auto filePath = filePath(url); - auto filePath = filePath(url); + DavResource resource = new DavResource(_dav, url); + resource.username = username; + setResourceProperties(resource); - DavResource resource = new DavResource(_dav, url); - resource.username = username; - setResourceProperties(resource); + return resource; + } - return resource; - } + DavResource createResource(URL url, string username) { + auto filePath = filePath(url).toString; + + File(filePath, "w"); - DavResource createResource(URL url, string username) { - auto filePath = filePath(url).toString; + return getResource(url, username); + } - File(filePath, "w"); + DavResource createCollection(URL url, string username) { + auto filePath = filePath(url); - return getResource(url, username); - } + if(filePath.toString.exists) + throw new DavException(HTTPStatus.methodNotAllowed, "Resource already exists."); - DavResource createCollection(URL url, string username) { - auto filePath = filePath(url); + filePath.toString.mkdirRecurse; - if(filePath.toString.exists) - throw new DavException(HTTPStatus.methodNotAllowed, "Resource already exists."); + return getResource(url, username); + } - filePath.toString.mkdirRecurse; + void bindResourcePlugins(DavResource resource) { + if(resource.isCollection) + resource.registerPlugin(new DirectoryResourcePlugin(baseUrlPath, basePath)); + else + resource.registerPlugin(new FileResourcePlugin(baseUrlPath, basePath)); - return getResource(url, username); - } + resource.registerPlugin(new ResourceCustomProperties); + resource.registerPlugin(new ResourceBasicProperties); + } - void bindResourcePlugins(DavResource resource) { - if(resource.isCollection) - resource.registerPlugin(new DirectoryResourcePlugin(baseUrlPath, basePath)); - else - resource.registerPlugin(new FileResourcePlugin(baseUrlPath, basePath)); - resource.registerPlugin(new ResourceCustomProperties); - resource.registerPlugin(new ResourceBasicProperties); + @property { + IDav dav() { + return _dav; + } + + string[] support(URL url, string username) { + return ["1", "2", "3"]; + } + } } @property { string name() { return "FileDav"; } - - IDav dav() { - return _dav; - } - - string[] support(URL url, string username) { - return ["1", "2", "3"]; - } } } diff --git a/source/vibedav/http.d b/source/vibedav/http.d index 67fa192..c1e5d08 100644 --- a/source/vibedav/http.d +++ b/source/vibedav/http.d @@ -109,6 +109,10 @@ struct DavResponse { _content = value; } + void content(DavProp value) { + content(value.toString); + } + void mimeType(string value) { response.headers["Content-Type"] = value; } @@ -162,23 +166,40 @@ struct DavResponse { response.writeRawBody(resource.stream); } - void setPropContent (DavResource[] list, bool[string] props) { + void setPropContent (DavResource[] list, bool[string] props, HTTPStatus[string] responseCodes = null) { statusCode = HTTPStatus.multiStatus; mimeType = "application/xml"; string str = ``; - auto response = parseXMLProp(``); + auto multistatus = new DavProp; + multistatus.addNamespace("d", "DAV:"); + multistatus.name = "d:multistatus"; foreach(item; list) - item.filterProps(response["d:multistatus"], props); + item.filterProps(multistatus, props); + + if(responseCodes !is null) { + foreach(string path, HTTPStatus status; responseCodes) { + DavProp element = new DavProp; + multistatus.addChild(element); + + element.name = "response"; + element.namespaceAttr = "DAV:"; + element[`d:href`] = path; + element[`d:status`] = "HTTP/1.1 " ~ status.to!int.to!string ~ " " ~ status.to!string; + } + } - _content = str ~ response.toString; + _content = str ~ multistatus.toString; } } /// The HTTP request wrapper struct DavRequest { - private HTTPServerRequest request; + private { + HTTPServerRequest request; + DavProp document; + } this(HTTPServerRequest req) { request = req; @@ -220,15 +241,15 @@ struct DavRequest { } DavProp content() { - DavProp document; + + if(document !is null) + return document; if(request.bodyReader is null) return document; string requestXml = cast(string)request.bodyReader.readAllUTF8; - debug writeln("requestXml:", requestXml); - if(requestXml.length > 0) { try document = requestXml.parseXMLProp; catch (DavPropException e) diff --git a/source/vibedav/syncdav.d b/source/vibedav/syncdav.d new file mode 100644 index 0000000..7431943 --- /dev/null +++ b/source/vibedav/syncdav.d @@ -0,0 +1,216 @@ +/** + * Authors: Szabo Bogdan + * Date: 4 23, 2015 + * License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + * Copyright: Public Domain + */ +module vibedav.syncdav; + +import std.datetime; + +import vibedav.base; +import vibedav.davresource; +import vibe.core.file; + +import vibe.http.server; + + +interface ISyncDavProperties { + @property { + + /// rfc6578 - 4 + @ResourceProperty("sync-token", "DAV:") + string syncToken(DavResource resource); + } +} + +interface ISyncDavReports { + + /// rfc6578 - 3.2 + @DavReport("sync-collection", "DAV:") + void syncCollection(DavRequest request, DavResponse response); +} + +class SyncDavDataPlugin : BaseDavResourcePlugin, ISyncDavProperties { + + private SyncDavPlugin _syncPlugin; + + this(SyncDavPlugin syncPlugin) { + _syncPlugin = syncPlugin; + } + + string syncToken(DavResource resource) { + return SyncDavPlugin.prefix ~ _syncPlugin.currentChangeNr.to!string; + } + + override { + bool canGetProperty(DavResource resource, string name) { + if(hasDavInterfaceProperty!ISyncDavProperties(name)) + return true; + + return false; + } + + DavProp property(DavResource resource, string name) { + if(hasDavInterfaceProperty!ISyncDavProperties(name)) + return getDavInterfaceProperty!ISyncDavProperties(name, this, resource); + + throw new DavException(HTTPStatus.internalServerError, "Can't get property."); + } + } + + @property + string name() { + return "SyncDavDataPlugin"; + } +} + +// todo: add ISyncDavReports +class SyncDavPlugin : BaseDavPlugin, ISyncDavReports { + enum string prefix = "http://vibedav/ns/sync/"; + + struct Change { + Path path; + string type; + SysTime time; + } + + private { + Change[] log; + ulong changeNr = 1; + } + + this(IDav dav) { + super(dav); + } + + private { + + ulong getToken(DavProp[] syncTokenList) { + if(syncTokenList.length == 0) + return 0; + + if(syncTokenList[0].tagName != "sync-token") + return 0; + + string value = syncTokenList[0].value; + + if(value.length == 0) + return 0; + + if(value.length <= prefix.length) + return 0; + + if(value[0..prefix.length] != prefix) + return 0; + + value = value[prefix.length..$]; + + try { + return value.to!ulong; + } catch(Exception e) { + throw new DavException(HTTPStatus.internalServerError, "invalid sync-token"); + } + } + + ulong getLevel(DavProp[] syncLevelList) { + if(syncLevelList.length == 0) + return 0; + + if(syncLevelList[0].name != "sync-level") + return 0; + + try { + return syncLevelList[0].value.to!ulong; + } catch(Exception e) { + throw new DavException(HTTPStatus.internalServerError, "invalid sync-level"); + } + } + + bool[string] getChangesFrom(ulong token) { + if(token > changeNr) + throw new DavException(HTTPStatus.forbidden, "Invalid token."); + + bool[string] wasRemoved; + + foreach(i; token..changeNr-1) { + auto change = log[i]; + + wasRemoved[change.path.toString] = (change.type == "deleted"); + } + + return wasRemoved; + } + } + + void syncCollection(DavRequest request, DavResponse response) { + response.mimeType = "application/xml"; + response.statusCode = HTTPStatus.multiStatus; + auto reportData = request.content; + + bool[string] requestedProperties; + HTTPStatus[string] responseCodes; + + foreach(name, p; reportData["sync-collection"]["prop"]) + requestedProperties[name] = true; + + auto syncTokenList = [ reportData["sync-collection"] ].getTagChilds("sync-token"); + auto syncLevelList = [ reportData["sync-collection"] ].getTagChilds("sync-level"); + + ulong token = getToken(syncTokenList); + ulong level = getLevel(syncLevelList); + + DavResource[] list; + auto changes = getChangesFrom(token); + + foreach(string path, bool wasRemoved; changes) { + if(wasRemoved) { + responseCodes[path] = HTTPStatus.notFound; + } else { + list ~= _dav.getResource(URL(path), request.username); + } + } + + response.setPropContent(list, requestedProperties, responseCodes); + response.flush; + } + + override { + bool hasReport(URL url, string username, string name) { + + if(hasDavReport!ISyncDavReports(name)) + return true; + + return false; + } + + void report(DavRequest request, DavResponse response) { + if(!hasDavReport!ISyncDavReports(request.content.reportName)) + throw new DavException(HTTPStatus.internalServerError, "Can't get report."); + + getDavReport!ISyncDavReports(this, request, response); + } + + void bindResourcePlugins(DavResource resource) { + if(resource.isCollection) + resource.registerPlugin(new SyncDavDataPlugin(this)); + } + + void notice(string action, DavResource resource) { + if(action == "created" || action == "deleted" || action == "changed") { + changeNr++; + log ~= Change(resource.url.path, action, Clock.currTime); + } + } + } + + @property { + ulong currentChangeNr() { + return changeNr; + } + + string name() { + return "SyncDavPlugin"; + } + } +}