1 /**
2  * Authors: Szabo Bogdan <szabobogdan@yahoo.com>
3  * Date: 4 23, 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.syncdav;
8 
9 import std.datetime;
10 
11 import vibedav.base;
12 import vibedav.davresource;
13 import vibe.core.file;
14 import std.conv;
15 
16 import vibe.http.server;
17 
18 
19 interface ISyncDavProperties {
20 	@property {
21 
22 		/// rfc6578 - 4
23 		@ResourceProperty("sync-token", "DAV:")
24 		string syncToken(DavResource resource);
25 	}
26 }
27 
28 interface ISyncDavReports {
29 
30 	/// rfc6578 - 3.2
31 	@DavReport("sync-collection", "DAV:")
32 	void syncCollection(DavRequest request, DavResponse response);
33 }
34 
35 class SyncDavDataPlugin : BaseDavResourcePlugin, ISyncDavProperties {
36 
37 	private SyncDavPlugin _syncPlugin;
38 
39 	this(SyncDavPlugin syncPlugin) {
40 		_syncPlugin = syncPlugin;
41 	}
42 
43 	string syncToken(DavResource resource) {
44 		return SyncDavPlugin.prefix ~ _syncPlugin.currentChangeNr.to!string;
45 	}
46 
47 	override {
48 		bool canGetProperty(DavResource resource, string name) {
49 			if(hasDavInterfaceProperty!ISyncDavProperties(name))
50 				return true;
51 
52 			return false;
53 		}
54 
55 		DavProp property(DavResource resource, string name) {
56 			if(hasDavInterfaceProperty!ISyncDavProperties(name))
57 				return getDavInterfaceProperty!ISyncDavProperties(name, this, resource);
58 
59 			throw new DavException(HTTPStatus.internalServerError, "Can't get property.");
60 		}
61 	}
62 
63 	@property
64 	string name() {
65 		return "SyncDavDataPlugin";
66 	}
67 }
68 
69 // todo: add ISyncDavReports
70 class SyncDavPlugin : BaseDavPlugin, ISyncDavReports {
71 	enum string prefix = "http://vibedav/ns/sync/";
72 
73 	struct Change {
74 		Path path;
75 		NoticeAction type;
76 		SysTime time;
77 	}
78 
79 	private {
80 		Change[] log;
81 		ulong changeNr = 1;
82 	}
83 
84 	this(IDav dav) {
85 		super(dav);
86 	}
87 
88 	protected {
89 
90 		ulong getToken(DavProp[] syncTokenList) {
91 			if(syncTokenList.length == 0)
92 				return 0;
93 
94 			if(syncTokenList[0].tagName != "sync-token")
95 				return 0;
96 
97 			string value = syncTokenList[0].value;
98 
99 			if(value.length == 0)
100 				return 0;
101 
102 			if(value.length <= prefix.length)
103 				return 0;
104 
105 			if(value[0..prefix.length] != prefix)
106 				return 0;
107 
108 			value = value[prefix.length..$];
109 
110 			try {
111 				return value.to!ulong;
112 			} catch(Exception e) {
113 				throw new DavException(HTTPStatus.internalServerError, "invalid sync-token");
114 			}
115 		}
116 
117 		ulong getLevel(DavProp[] syncLevelList) {
118 			if(syncLevelList.length == 0)
119 				return 0;
120 
121 			if(syncLevelList[0].name != "sync-level")
122 				return 0;
123 
124 			try {
125 				return syncLevelList[0].value.to!ulong;
126 			} catch(Exception e) {
127 				throw new DavException(HTTPStatus.internalServerError, "invalid sync-level");
128 			}
129 		}
130 
131 		bool[string] getChangesFrom(ulong token) {
132 			if(token > changeNr)
133 				throw new DavException(HTTPStatus.forbidden, "Invalid token.");
134 
135 			bool[string] wasRemoved;
136 
137 			foreach(i; token..changeNr-1) {
138 				auto change = log[i];
139 
140 				wasRemoved[change.path.toString] = (change.type == NoticeAction.deleted);
141 			}
142 
143 			return wasRemoved;
144 		}
145 	}
146 
147 	void syncCollection(DavRequest request, DavResponse response) {
148 		response.mimeType = "application/xml";
149 		response.statusCode = HTTPStatus.multiStatus;
150 		auto reportData = request.content;
151 
152 		bool[string] requestedProperties;
153 		HTTPStatus[string] responseCodes;
154 
155 		foreach(name, p; reportData["sync-collection"]["prop"])
156 			requestedProperties[name] = true;
157 
158 		auto syncTokenList = [ reportData["sync-collection"] ].getTagChilds("sync-token");
159 		auto syncLevelList = [ reportData["sync-collection"] ].getTagChilds("sync-level");
160 
161 		ulong token = getToken(syncTokenList);
162 		ulong level = getLevel(syncLevelList);
163 
164 		DavResource[] list;
165 		auto changes = getChangesFrom(token);
166 
167 		foreach(string path, bool wasRemoved; changes) {
168 			if(wasRemoved) {
169 				responseCodes[path] = HTTPStatus.notFound;
170 			} else {
171 				list ~= _dav.getResource(URL(path), request.username);
172 			}
173 		}
174 
175 		response.setPropContent(list, requestedProperties, responseCodes);
176 		response.flush;
177 	}
178 
179 	override {
180 		bool hasReport(URL url, string username, string name) {
181 
182 			if(hasDavReport!ISyncDavReports(name))
183 				return true;
184 
185 			return false;
186 		}
187 
188 		void report(DavRequest request, DavResponse response) {
189 			if(!hasDavReport!ISyncDavReports(request.content.reportName))
190 				throw new DavException(HTTPStatus.internalServerError, "Can't get report.");
191 
192 			getDavReport!ISyncDavReports(this, request, response);
193 		}
194 
195 		void bindResourcePlugins(DavResource resource) {
196 			if(resource.isCollection)
197 				resource.registerPlugin(new SyncDavDataPlugin(this));
198 		}
199 
200 		void notice(NoticeAction action, DavResource resource) {
201 			changeNr++;
202 			log ~= Change(resource.url.path, action, Clock.currTime);
203 		}
204 	}
205 
206 	@property {
207 		ulong currentChangeNr() {
208 			return changeNr;
209 		}
210 
211 		string name() {
212 			return "SyncDavPlugin";
213 		}
214 	}
215 }