Orthanc/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp
2025-06-23 19:07:37 +05:30

352 lines
11 KiB
C++

/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2023 Osimis S.A., Belgium
* Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
* Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/>.
**/
#include "../PrecompiledHeaders.h"
#include "IWebDavBucket.h"
#include "HttpOutput.h"
#include "../OrthancException.h"
#include "../Toolbox.h"
static boost::posix_time::ptime GetNow()
{
return boost::posix_time::second_clock::universal_time();
}
static std::string AddTrailingSlash(const std::string& s)
{
if (s.empty() ||
s[s.size() - 1] != '/')
{
return s + '/';
}
else
{
return s;
}
}
namespace Orthanc
{
IWebDavBucket::Resource::Resource(const std::string& displayName) :
displayName_(displayName),
hasModificationTime_(false),
creationTime_(GetNow()),
modificationTime_(GetNow())
{
if (displayName.empty() ||
displayName.find('/') != std::string::npos ||
displayName.find('\\') != std::string::npos ||
displayName.find('\0') != std::string::npos)
{
throw OrthancException(ErrorCode_ParameterOutOfRange,
"Bad resource name for WebDAV: " + displayName);
}
}
void IWebDavBucket::Resource::SetCreationTime(const boost::posix_time::ptime& t)
{
if (t.is_special())
{
throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid date-time");
}
else
{
creationTime_ = t;
if (!hasModificationTime_)
{
modificationTime_ = t;
}
}
}
void IWebDavBucket::Resource::SetModificationTime(const boost::posix_time::ptime& t)
{
if (t.is_special())
{
throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid date-time");
}
else
{
modificationTime_ = t;
hasModificationTime_ = true;
}
}
static void 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)
{
node.set_name("D:response");
node.append_child("D:href").append_child(pugi::node_pcdata).set_value(href.c_str());
pugi::xml_node propstat = node.append_child("D:propstat");
static const HttpStatus status = HttpStatus_200_Ok;
std::string s = ("HTTP/1.1 " + boost::lexical_cast<std::string>(status) + " " +
std::string(EnumerationToString(status)));
propstat.append_child("D:status").append_child(pugi::node_pcdata).set_value(s.c_str());
pugi::xml_node prop = propstat.append_child("D:prop");
prop.append_child("D:displayname").append_child(pugi::node_pcdata).set_value(displayName.c_str());
// IMPORTANT: Adding the "Z" suffix is mandatory on Windows >= 7 (it indicates UTC)
assert(!creationTime.is_special());
s = boost::posix_time::to_iso_extended_string(creationTime) + "Z";
prop.append_child("D:creationdate").append_child(pugi::node_pcdata).set_value(s.c_str());
assert(!modificationTime.is_special());
s = boost::posix_time::to_iso_extended_string(modificationTime) + "Z";
prop.append_child("D:getlastmodified").append_child(pugi::node_pcdata).set_value(s.c_str());
#if 0
// Maybe used by davfs2
prop.append_child("D:quota-available-bytes");
prop.append_child("D:quota-used-bytes");
#endif
#if 0
prop.append_child("D:lockdiscovery");
pugi::xml_node lock = prop.append_child("D:supportedlock");
pugi::xml_node lockentry = lock.append_child("D:lockentry");
lockentry.append_child("D:lockscope").append_child("D:exclusive");
lockentry.append_child("D:locktype").append_child("D:write");
lockentry = lock.append_child("D:lockentry");
lockentry.append_child("D:lockscope").append_child("D:shared");
lockentry.append_child("D:locktype").append_child("D:write");
#endif
}
IWebDavBucket::File::File(const std::string& displayName) :
Resource(displayName),
contentLength_(0),
mime_(MimeType_Binary)
{
}
void IWebDavBucket::File::Format(pugi::xml_node& node,
const std::string& parentPath) const
{
std::string href;
Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName());
FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime());
pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop");
prop.append_child("D:resourcetype");
std::string s = boost::lexical_cast<std::string>(contentLength_);
prop.append_child("D:getcontentlength").append_child(pugi::node_pcdata).set_value(s.c_str());
s = EnumerationToString(mime_);
prop.append_child("D:getcontenttype").append_child(pugi::node_pcdata).set_value(s.c_str());
}
void IWebDavBucket::Folder::Format(pugi::xml_node& node,
const std::string& parentPath) const
{
std::string href;
Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName());
FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime());
pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop");
prop.append_child("D:resourcetype").append_child("D:collection");
//prop.append_child("D:getcontenttype").append_child(pugi::node_pcdata).set_value("httpd/unix-directory");
}
IWebDavBucket::Collection::~Collection()
{
for (std::list<Resource*>::iterator it = resources_.begin(); it != resources_.end(); ++it)
{
assert(*it != NULL);
delete(*it);
}
}
void IWebDavBucket::Collection::AddResource(Resource* resource) // Takes ownership
{
if (resource == NULL)
{
throw OrthancException(ErrorCode_NullPointer);
}
else
{
resources_.push_back(resource);
}
}
void IWebDavBucket::Collection::ListDisplayNames(std::set<std::string>& target)
{
for (std::list<Resource*>::iterator it = resources_.begin(); it != resources_.end(); ++it)
{
assert(*it != NULL);
target.insert((*it)->GetDisplayName());
}
}
void IWebDavBucket::Collection::Format(std::string& target,
const std::string& parentPath) const
{
pugi::xml_document doc;
pugi::xml_node root = doc.append_child("D:multistatus");
root.append_attribute("xmlns:D").set_value("DAV:");
{
pugi::xml_node self = root.append_child();
std::vector<std::string> tokens;
Toolbox::SplitUriComponents(tokens, parentPath);
std::string folder;
if (!tokens.empty())
{
folder = tokens.back();
}
std::string href;
Toolbox::UriEncode(href, Toolbox::FlattenUri(tokens) + "/");
boost::posix_time::ptime now = GetNow();
FormatInternal(self, href, folder, now, now);
pugi::xml_node prop = self.first_element_by_path("D:propstat/D:prop");
prop.append_child("D:resourcetype").append_child("D:collection");
}
for (std::list<Resource*>::const_iterator
it = resources_.begin(); it != resources_.end(); ++it)
{
assert(*it != NULL);
pugi::xml_node n = root.append_child();
(*it)->Format(n, parentPath);
}
pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
decl.append_attribute("version").set_value("1.0");
decl.append_attribute("encoding").set_value("UTF-8");
Toolbox::XmlToString(target, doc);
}
void IWebDavBucket::AnswerFakedProppatch(HttpOutput& output,
const std::string& uri)
{
/**
* This is a fake implementation. The goal is to make happy the
* WebDAV clients that set properties (such as Windows >= 7).
**/
pugi::xml_document doc;
pugi::xml_node root = doc.append_child("D:multistatus");
root.append_attribute("xmlns:D").set_value("DAV:");
pugi::xml_node response = root.append_child("D:response");
response.append_child("D:href").append_child(pugi::node_pcdata).set_value(uri.c_str());
response.append_child("D:propstat");
pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
decl.append_attribute("version").set_value("1.0");
decl.append_attribute("encoding").set_value("UTF-8");
std::string s;
Toolbox::XmlToString(s, doc);
output.AddHeader("Content-Type", "application/xml");
output.SendStatus(HttpStatus_207_MultiStatus, s);
}
void IWebDavBucket::AnswerFakedLock(HttpOutput& output,
const std::string& uri)
{
/**
* This is a fake implementation. No lock is actually
* created. The goal is to make happy the WebDAV clients
* that use locking (such as Windows >= 7).
**/
pugi::xml_document doc;
pugi::xml_node root = doc.append_child("D:prop");
root.append_attribute("xmlns:D").set_value("DAV:");
pugi::xml_node activelock = root.append_child("D:lockdiscovery").append_child("D:activelock");
activelock.append_child("D:locktype").append_child("D:write");
activelock.append_child("D:lockscope").append_child("D:exclusive");
activelock.append_child("D:depth").append_child(pugi::node_pcdata).set_value("0");
activelock.append_child("D:timeout").append_child(pugi::node_pcdata).set_value("Second-3599");
activelock.append_child("D:lockroot").append_child("D:href")
.append_child(pugi::node_pcdata).set_value(uri.c_str());
activelock.append_child("D:owner");
std::string token = Toolbox::GenerateUuid();
boost::erase_all(token, "-");
token = "opaquelocktoken:0x" + token;
activelock.append_child("D:locktoken").append_child("D:href").
append_child(pugi::node_pcdata).set_value(token.c_str());
pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
decl.append_attribute("version").set_value("1.0");
decl.append_attribute("encoding").set_value("UTF-8");
std::string s;
Toolbox::XmlToString(s, doc);
output.AddHeader("Lock-Token", token); // Necessary for davfs2
output.AddHeader("Content-Type", "application/xml");
output.SendStatus(HttpStatus_201_Created, s);
}
void IWebDavBucket::AnswerFakedUnlock(HttpOutput& output)
{
output.SendStatus(HttpStatus_204_NoContent);
}
}