1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2021 Osimis S.A., Belgium
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public License
9 * as published by the Free Software Foundation, either version 3 of
10 * the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this program. If not, see
19 * <http://www.gnu.org/licenses/>.
20 **/
21
22
23 #include "../PrecompiledHeaders.h"
24 #include "IWebDavBucket.h"
25
26 #include "HttpOutput.h"
27 #include "../OrthancException.h"
28 #include "../Toolbox.h"
29
30
GetNow()31 static boost::posix_time::ptime GetNow()
32 {
33 return boost::posix_time::second_clock::universal_time();
34 }
35
36
AddTrailingSlash(const std::string & s)37 static std::string AddTrailingSlash(const std::string& s)
38 {
39 if (s.empty() ||
40 s[s.size() - 1] != '/')
41 {
42 return s + '/';
43 }
44 else
45 {
46 return s;
47 }
48 }
49
50
51 namespace Orthanc
52 {
Resource(const std::string & displayName)53 IWebDavBucket::Resource::Resource(const std::string& displayName) :
54 displayName_(displayName),
55 hasModificationTime_(false),
56 creationTime_(GetNow()),
57 modificationTime_(GetNow())
58 {
59 if (displayName.empty() ||
60 displayName.find('/') != std::string::npos ||
61 displayName.find('\\') != std::string::npos ||
62 displayName.find('\0') != std::string::npos)
63 {
64 throw OrthancException(ErrorCode_ParameterOutOfRange,
65 "Bad resource name for WebDAV: " + displayName);
66 }
67 }
68
69
SetCreationTime(const boost::posix_time::ptime & t)70 void IWebDavBucket::Resource::SetCreationTime(const boost::posix_time::ptime& t)
71 {
72 if (t.is_special())
73 {
74 throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid date-time");
75 }
76 else
77 {
78 creationTime_ = t;
79
80 if (!hasModificationTime_)
81 {
82 modificationTime_ = t;
83 }
84 }
85 }
86
87
SetModificationTime(const boost::posix_time::ptime & t)88 void IWebDavBucket::Resource::SetModificationTime(const boost::posix_time::ptime& t)
89 {
90 if (t.is_special())
91 {
92 throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid date-time");
93 }
94 else
95 {
96 modificationTime_ = t;
97 hasModificationTime_ = true;
98 }
99 }
100
101
FormatInternal(pugi::xml_node & node,const std::string & href,const std::string & displayName,const boost::posix_time::ptime & creationTime,const boost::posix_time::ptime & modificationTime)102 static void FormatInternal(pugi::xml_node& node,
103 const std::string& href,
104 const std::string& displayName,
105 const boost::posix_time::ptime& creationTime,
106 const boost::posix_time::ptime& modificationTime)
107 {
108 node.set_name("D:response");
109
110 node.append_child("D:href").append_child(pugi::node_pcdata).set_value(href.c_str());
111
112 pugi::xml_node propstat = node.append_child("D:propstat");
113
114 static const HttpStatus status = HttpStatus_200_Ok;
115 std::string s = ("HTTP/1.1 " + boost::lexical_cast<std::string>(status) + " " +
116 std::string(EnumerationToString(status)));
117 propstat.append_child("D:status").append_child(pugi::node_pcdata).set_value(s.c_str());
118
119 pugi::xml_node prop = propstat.append_child("D:prop");
120 prop.append_child("D:displayname").append_child(pugi::node_pcdata).set_value(displayName.c_str());
121
122 // IMPORTANT: Adding the "Z" suffix is mandatory on Windows >= 7 (it indicates UTC)
123 assert(!creationTime.is_special());
124 s = boost::posix_time::to_iso_extended_string(creationTime) + "Z";
125 prop.append_child("D:creationdate").append_child(pugi::node_pcdata).set_value(s.c_str());
126
127 assert(!modificationTime.is_special());
128 s = boost::posix_time::to_iso_extended_string(modificationTime) + "Z";
129 prop.append_child("D:getlastmodified").append_child(pugi::node_pcdata).set_value(s.c_str());
130
131 #if 0
132 // Maybe used by davfs2
133 prop.append_child("D:quota-available-bytes");
134 prop.append_child("D:quota-used-bytes");
135 #endif
136
137 #if 0
138 prop.append_child("D:lockdiscovery");
139 pugi::xml_node lock = prop.append_child("D:supportedlock");
140
141 pugi::xml_node lockentry = lock.append_child("D:lockentry");
142 lockentry.append_child("D:lockscope").append_child("D:exclusive");
143 lockentry.append_child("D:locktype").append_child("D:write");
144
145 lockentry = lock.append_child("D:lockentry");
146 lockentry.append_child("D:lockscope").append_child("D:shared");
147 lockentry.append_child("D:locktype").append_child("D:write");
148 #endif
149 }
150
151
File(const std::string & displayName)152 IWebDavBucket::File::File(const std::string& displayName) :
153 Resource(displayName),
154 contentLength_(0),
155 mime_(MimeType_Binary)
156 {
157 }
158
159
Format(pugi::xml_node & node,const std::string & parentPath) const160 void IWebDavBucket::File::Format(pugi::xml_node& node,
161 const std::string& parentPath) const
162 {
163 std::string href;
164 Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName());
165 FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime());
166
167 pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop");
168 prop.append_child("D:resourcetype");
169
170 std::string s = boost::lexical_cast<std::string>(contentLength_);
171 prop.append_child("D:getcontentlength").append_child(pugi::node_pcdata).set_value(s.c_str());
172
173 s = EnumerationToString(mime_);
174 prop.append_child("D:getcontenttype").append_child(pugi::node_pcdata).set_value(s.c_str());
175 }
176
177
Format(pugi::xml_node & node,const std::string & parentPath) const178 void IWebDavBucket::Folder::Format(pugi::xml_node& node,
179 const std::string& parentPath) const
180 {
181 std::string href;
182 Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName());
183 FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime());
184
185 pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop");
186 prop.append_child("D:resourcetype").append_child("D:collection");
187
188 //prop.append_child("D:getcontenttype").append_child(pugi::node_pcdata).set_value("httpd/unix-directory");
189 }
190
191
~Collection()192 IWebDavBucket::Collection::~Collection()
193 {
194 for (std::list<Resource*>::iterator it = resources_.begin(); it != resources_.end(); ++it)
195 {
196 assert(*it != NULL);
197 delete(*it);
198 }
199 }
200
201
AddResource(Resource * resource)202 void IWebDavBucket::Collection::AddResource(Resource* resource) // Takes ownership
203 {
204 if (resource == NULL)
205 {
206 throw OrthancException(ErrorCode_NullPointer);
207 }
208 else
209 {
210 resources_.push_back(resource);
211 }
212 }
213
214
ListDisplayNames(std::set<std::string> & target)215 void IWebDavBucket::Collection::ListDisplayNames(std::set<std::string>& target)
216 {
217 for (std::list<Resource*>::iterator it = resources_.begin(); it != resources_.end(); ++it)
218 {
219 assert(*it != NULL);
220 target.insert((*it)->GetDisplayName());
221 }
222 }
223
224
Format(std::string & target,const std::string & parentPath) const225 void IWebDavBucket::Collection::Format(std::string& target,
226 const std::string& parentPath) const
227 {
228 pugi::xml_document doc;
229
230 pugi::xml_node root = doc.append_child("D:multistatus");
231 root.append_attribute("xmlns:D").set_value("DAV:");
232
233 {
234 pugi::xml_node self = root.append_child();
235
236 std::vector<std::string> tokens;
237 Toolbox::SplitUriComponents(tokens, parentPath);
238
239 std::string folder;
240 if (!tokens.empty())
241 {
242 folder = tokens.back();
243 }
244
245 std::string href;
246 Toolbox::UriEncode(href, Toolbox::FlattenUri(tokens) + "/");
247
248 boost::posix_time::ptime now = GetNow();
249 FormatInternal(self, href, folder, now, now);
250
251 pugi::xml_node prop = self.first_element_by_path("D:propstat/D:prop");
252 prop.append_child("D:resourcetype").append_child("D:collection");
253 }
254
255 for (std::list<Resource*>::const_iterator
256 it = resources_.begin(); it != resources_.end(); ++it)
257 {
258 assert(*it != NULL);
259 pugi::xml_node n = root.append_child();
260 (*it)->Format(n, parentPath);
261 }
262
263 pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
264 decl.append_attribute("version").set_value("1.0");
265 decl.append_attribute("encoding").set_value("UTF-8");
266
267 Toolbox::XmlToString(target, doc);
268 }
269
270
AnswerFakedProppatch(HttpOutput & output,const std::string & uri)271 void IWebDavBucket::AnswerFakedProppatch(HttpOutput& output,
272 const std::string& uri)
273 {
274 /**
275 * This is a fake implementation. The goal is to make happy the
276 * WebDAV clients that set properties (such as Windows >= 7).
277 **/
278
279 pugi::xml_document doc;
280
281 pugi::xml_node root = doc.append_child("D:multistatus");
282 root.append_attribute("xmlns:D").set_value("DAV:");
283
284 pugi::xml_node response = root.append_child("D:response");
285 response.append_child("D:href").append_child(pugi::node_pcdata).set_value(uri.c_str());
286
287 response.append_child("D:propstat");
288
289 pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
290 decl.append_attribute("version").set_value("1.0");
291 decl.append_attribute("encoding").set_value("UTF-8");
292
293 std::string s;
294 Toolbox::XmlToString(s, doc);
295
296 output.AddHeader("Content-Type", "application/xml");
297 output.SendStatus(HttpStatus_207_MultiStatus, s);
298 }
299
300
AnswerFakedLock(HttpOutput & output,const std::string & uri)301 void IWebDavBucket::AnswerFakedLock(HttpOutput& output,
302 const std::string& uri)
303 {
304 /**
305 * This is a fake implementation. No lock is actually
306 * created. The goal is to make happy the WebDAV clients
307 * that use locking (such as Windows >= 7).
308 **/
309
310 pugi::xml_document doc;
311
312 pugi::xml_node root = doc.append_child("D:prop");
313 root.append_attribute("xmlns:D").set_value("DAV:");
314
315 pugi::xml_node activelock = root.append_child("D:lockdiscovery").append_child("D:activelock");
316 activelock.append_child("D:locktype").append_child("D:write");
317 activelock.append_child("D:lockscope").append_child("D:exclusive");
318 activelock.append_child("D:depth").append_child(pugi::node_pcdata).set_value("0");
319 activelock.append_child("D:timeout").append_child(pugi::node_pcdata).set_value("Second-3599");
320
321 activelock.append_child("D:lockroot").append_child("D:href")
322 .append_child(pugi::node_pcdata).set_value(uri.c_str());
323 activelock.append_child("D:owner");
324
325 std::string token = Toolbox::GenerateUuid();
326 boost::erase_all(token, "-");
327 token = "opaquelocktoken:0x" + token;
328
329 activelock.append_child("D:locktoken").append_child("D:href").
330 append_child(pugi::node_pcdata).set_value(token.c_str());
331
332 pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
333 decl.append_attribute("version").set_value("1.0");
334 decl.append_attribute("encoding").set_value("UTF-8");
335
336 std::string s;
337 Toolbox::XmlToString(s, doc);
338
339 output.AddHeader("Lock-Token", token); // Necessary for davfs2
340 output.AddHeader("Content-Type", "application/xml");
341 output.SendStatus(HttpStatus_201_Created, s);
342 }
343
344
AnswerFakedUnlock(HttpOutput & output)345 void IWebDavBucket::AnswerFakedUnlock(HttpOutput& output)
346 {
347 output.SendStatus(HttpStatus_204_NoContent);
348 }
349 }
350