# HG changeset patch # User Alain Mazy # Date 1648034591 -3600 # Node ID 501411a67f10ddb42e26163e726659a480c6a0d7 # Parent 1b76853e17979d7555b417ead98892a1a9b9dee9# Parent 94edc2c897684c4729fe7021d292e91c137fa4b9 merge diff -r 1b76853e1797 -r 501411a67f10 NEWS --- a/NEWS Wed Mar 23 11:56:28 2022 +0100 +++ b/NEWS Wed Mar 23 12:23:11 2022 +0100 @@ -1,10 +1,11 @@ Pending changes in the mainline =============================== - General ------- +* Improved DICOM authorization checks when multiple modalities are + declared with the same AET. * New configuration "ExtraMainDicomTags" to store more tags in the Index DB to speed up, e.g, building C-Find, dicom-web or tools/find answers * New sample plugin: "DbOptimizer" that will re-construct the DB/Storage @@ -39,13 +40,18 @@ * new field "MainDicomTags" in the /system route response to list the tags that are saved in DB +Plugins +------- + +* New function in the SDK: "OrthancPluginRegisterWebDavCollection()" + to map a WebDAV virtual filesystem into the REST API of Orthanc. Documentation ------------- -* Removed the "LimitJobs" configuration that is not used anymore since the new - JobEngine has been introduced (in Orthanc 1.4.0). The pending list of jobs - is unlimited. +* Removed the "LimitJobs" configuration that is not used anymore since + the new JobEngine has been introduced (in Orthanc 1.4.0). The + pending list of jobs is unlimited. Version 1.10.0 (2022-02-23) diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/Resources/CMake/EmscriptenParameters.cmake --- a/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake Wed Mar 23 12:23:11 2022 +0100 @@ -25,10 +25,23 @@ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s BINARYEN_TRAP_MODE='\"${EMSCRIPTEN_TRAP_MODE}\"'") endif() +# If "-O3" is used (the default in "Release" mode), this results in a +# too large memory consumption in "wasm-opt", at least in Emscripten +# 3.1.7, which ultimately crashes the compiler. So we force "-O2" +# (this also has the advantage of speeding up the build): +set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG") + # "DISABLE_EXCEPTION_CATCHING" is a "compile+link" option. HOWEVER, # setting it inside "WASM_FLAGS" creates link errors, at least with # side modules. TODO: Understand why set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0") + +# "-Wno-unused-command-line-argument" is used to avoid annoying +# warnings about setting WASM, FETCH and ASSERTIONS, which was +# required for earlier versions of emsdk: +# https://groups.google.com/g/emscripten-discuss/c/VX4enWfadUE +set(WASM_FLAGS "${WASM_FLAGS} -Wno-unused-command-line-argument") + #set(WASM_FLAGS "${WASM_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0") if (EMSCRIPTEN_TARGET_MODE STREQUAL "wasm") diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Wed Mar 23 12:23:11 2022 +0100 @@ -2660,18 +2660,10 @@ l++; } - if (l == length) - { - // Not a null-terminated plain string - action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr); - } - else - { - std::string ignored; - std::string s(reinterpret_cast(data), l); - action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr, - Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions)); - } + std::string ignored; + std::string s(reinterpret_cast(data), l); + action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr, + Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions)); } else { @@ -3293,6 +3285,49 @@ IDicomPathVisitor::Apply(visitor, dataset, path); return visitor.HasFound(); } + + + bool FromDcmtkBridge::LookupStringValue(std::string& target, + DcmDataset& dataset, + const DicomTag& key) + { + DcmTagKey dcmkey(key.GetGroup(), key.GetElement()); + + const char* str = NULL; + const Uint8* data = NULL; + unsigned long size = 0; + + if (dataset.findAndGetString(dcmkey, str).good() && + str != NULL) + { + target.assign(str); + return true; + } + else if (dataset.findAndGetUint8Array(dcmkey, data, &size).good() && + data != NULL && + size > 0) + { + /** + * This special case is necessary for borderline DICOM files + * that have DICOM tags have the "UN" value representation. New + * in Orthanc 1.10.1. + * https://groups.google.com/g/orthanc-users/c/86fobx3ZyoM/m/KBG17un6AQAJ + **/ + unsigned long l = 0; + while (l < size && + data[l] != 0) + { + l++; + } + + target.assign(reinterpret_cast(data), l); + return true; + } + else + { + return false; + } + } } diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Wed Mar 23 12:23:11 2022 +0100 @@ -292,5 +292,9 @@ DcmDataset& dataset, const DicomPath& path, size_t sequenceIndex); + + static bool LookupStringValue(std::string& target, + DcmDataset& dataset, + const DicomTag& key); }; } diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp --- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp Wed Mar 23 12:23:11 2022 +0100 @@ -75,13 +75,11 @@ } DcmDataset& dataset = *dicom.getDataset(); - - const char* v = NULL; - if (dataset.findAndGetString(DCM_SOPInstanceUID, v).good() && - v != NULL) + std::string s; + if (FromDcmtkBridge::LookupStringValue(s, dataset, DICOM_TAG_SOP_INSTANCE_UID)) { - return std::string(v); + return s; } else { diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp --- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Wed Mar 23 12:23:11 2022 +0100 @@ -76,6 +76,8 @@ #include "Internals/DicomImageDecoder.h" #include "ToDcmtkBridge.h" +#include "../Images/Image.h" +#include "../Images/ImageProcessing.h" #include "../Images/PamReader.h" #include "../Logging.h" #include "../OrthancException.h" @@ -1918,6 +1920,175 @@ } + void ParsedDicomFile::ListOverlays(std::set& groups) const + { + DcmDataset& dataset = *const_cast(*this).GetDcmtkObject().getDataset(); + + // "Repeating Groups shall only be allowed in the even Groups (6000-601E,eeee)" + // https://dicom.nema.org/medical/dicom/2021e/output/chtml/part05/sect_7.6.html + + for (uint16_t group = 0x6000; group <= 0x601e; group += 2) + { + if (dataset.tagExists(DcmTagKey(group, 0x0010))) + { + groups.insert(group); + } + } + } + + + static unsigned int Ceiling(unsigned int a, + unsigned int b) + { + if (a % b == 0) + { + return a / b; + } + else + { + return a / b + 1; + } + } + + + ImageAccessor* ParsedDicomFile::DecodeOverlay(int& originX, + int& originY, + uint16_t group) const + { + // https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.9.2.html + + DcmDataset& dataset = *const_cast(*this).GetDcmtkObject().getDataset(); + + Uint16 rows, columns, bitsAllocated, bitPosition; + const Sint16* origin = NULL; + unsigned long originSize = 0; + DcmElement* overlayElement = NULL; + Uint8* overlayData = NULL; + + if (dataset.findAndGetUint16(DcmTagKey(group, 0x0010), rows).good() && + dataset.findAndGetUint16(DcmTagKey(group, 0x0011), columns).good() && + dataset.findAndGetSint16Array(DcmTagKey(group, 0x0050), origin, &originSize).good() && + origin != NULL && + originSize == 2 && + dataset.findAndGetUint16(DcmTagKey(group, 0x0100), bitsAllocated).good() && + bitsAllocated == 1 && + dataset.findAndGetUint16(DcmTagKey(group, 0x0102), bitPosition).good() && + bitPosition == 0 && + dataset.findAndGetElement(DcmTagKey(group, 0x3000), overlayElement).good() && + overlayElement != NULL && + overlayElement->getUint8Array(overlayData).good() && + overlayData != NULL) + { + /** + * WARNING - It might seem easier to use + * "dataset.findAndGetUint8Array()" that directly gives the size + * of the overlay data (using the "count" parameter), instead of + * "dataset.findAndGetElement()". Unfortunately, this does *not* + * work with Emscripten/WebAssembly, that reports a "count" that + * is half the number of bytes, presumably because of + * discrepancies in the way sizeof are computed inside DCMTK. + * The method "getLengthField()" reports the correct number of + * bytes, even if targeting WebAssembly. + **/ + + unsigned int expectedSize = Ceiling(rows * columns, 8); + if (overlayElement->getLengthField() < expectedSize) + { + throw OrthancException(ErrorCode_CorruptedFile, "Overlay doesn't have a valid number of bits"); + } + + originX = origin[1]; + originY = origin[0]; + + std::unique_ptr overlay(new Image(Orthanc::PixelFormat_Grayscale8, columns, rows, false)); + + unsigned int posBit = 0; + for (int y = 0; y < rows; y++) + { + uint8_t* target = reinterpret_cast(overlay->GetRow(y)); + + for (int x = 0; x < columns; x++) + { + uint8_t source = overlayData[posBit / 8]; + uint8_t mask = 1 << (posBit % 8); + + *target = ((source & mask) ? 255 : 0); + + target++; + posBit++; + } + } + + return overlay.release(); + } + else + { + throw OrthancException(ErrorCode_CorruptedFile, "Invalid overlay"); + } + } + + + ImageAccessor* ParsedDicomFile::DecodeAllOverlays(int& originX, + int& originY) const + { + std::set groups; + ListOverlays(groups); + + if (groups.empty()) + { + originX = 0; + originY = 0; + return new Image(PixelFormat_Grayscale8, 0, 0, false); + } + else + { + std::set::const_iterator it = groups.begin(); + assert(it != groups.end()); + + std::unique_ptr result(DecodeOverlay(originX, originY, *it)); + assert(result.get() != NULL); + ++it; + + int right = originX + static_cast(result->GetWidth()); + int bottom = originY + static_cast(result->GetHeight()); + + while (it != groups.end()) + { + int ox, oy; + std::unique_ptr overlay(DecodeOverlay(ox, oy, *it)); + assert(overlay.get() != NULL); + + int mergedX = std::min(originX, ox); + int mergedY = std::min(originY, oy); + right = std::max(right, ox + static_cast(overlay->GetWidth())); + bottom = std::max(bottom, oy + static_cast(overlay->GetHeight())); + + assert(right >= mergedX && bottom >= mergedY); + unsigned int width = static_cast(right - mergedX); + unsigned int height = static_cast(bottom - mergedY); + + std::unique_ptr merged(new Image(PixelFormat_Grayscale8, width, height, false)); + ImageProcessing::Set(*merged, 0); + + ImageAccessor a; + merged->GetRegion(a, originX - mergedX, originY - mergedY, result->GetWidth(), result->GetHeight()); + ImageProcessing::Maximum(a, *result); + + merged->GetRegion(a, ox - mergedX, oy - mergedY, overlay->GetWidth(), overlay->GetHeight()); + ImageProcessing::Maximum(a, *overlay); + + originX = mergedX; + originY = mergedY; + result.reset(merged.release()); + + ++it; + } + + return result.release(); + } + } + + #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore void ParsedDicomFile::DatasetToJson(Json::Value& target, diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h --- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Wed Mar 23 12:23:11 2022 +0100 @@ -300,5 +300,14 @@ void GetRescale(double& rescaleIntercept, double& rescaleSlope, unsigned int frame) const; + + void ListOverlays(std::set& groups) const; + + ImageAccessor* DecodeOverlay(int& originX, + int& originY, + uint16_t group) const; + + ImageAccessor* DecodeAllOverlays(int& originX, + int& originY) const; }; } diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/Sources/HttpServer/IWebDavBucket.h --- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.h Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.h Wed Mar 23 12:23:11 2022 +0100 @@ -117,8 +117,6 @@ return mime_; } - void SetCreated(bool created); - virtual void Format(pugi::xml_node& node, const std::string& parentPath) const ORTHANC_OVERRIDE; }; diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/Sources/Images/ImageProcessing.cpp --- a/OrthancFramework/Sources/Images/ImageProcessing.cpp Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp Wed Mar 23 12:23:11 2022 +0100 @@ -2964,4 +2964,78 @@ throw OrthancException(ErrorCode_NotImplemented); } } + + + template + static void ApplyImageOntoImage(Functor f, + ImageAccessor& image /* inout */, + const ImageAccessor& other) + { + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + if (width != other.GetWidth() || + height != other.GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize); + } + else if (image.GetFormat() != other.GetFormat() || + GetBytesPerPixel(image.GetFormat()) != sizeof(PixelType)) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + else + { + for (unsigned int y = 0; y < height; y++) + { + PixelType* p = reinterpret_cast(image.GetRow(y)); + const PixelType* q = reinterpret_cast(other.GetConstRow(y)); + + for (unsigned int x = 0; x < width; x++, p++, q++) + { + f(*p, *q); + } + } + } + } + + + namespace + { + // For older version of gcc, templated functors cannot be defined + // as types internal to functions, hence the anonymous namespace + + struct MaximumFunctor + { + void operator() (uint8_t& a, const uint8_t& b) + { + a = std::max(a, b); + } + + void operator() (uint16_t& a, const uint16_t& b) + { + a = std::max(a, b); + } + }; + } + + + void ImageProcessing::Maximum(ImageAccessor& image, + const ImageAccessor& other) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + ApplyImageOntoImage(MaximumFunctor(), image, other); + return; + + case PixelFormat_Grayscale16: + ApplyImageOntoImage(MaximumFunctor(), image, other); + return; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } } diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/Sources/Images/ImageProcessing.h --- a/OrthancFramework/Sources/Images/ImageProcessing.h Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/Sources/Images/ImageProcessing.h Wed Mar 23 12:23:11 2022 +0100 @@ -218,5 +218,8 @@ static void ConvertJpegYCbCrToRgb(ImageAccessor& image /* inplace */); static void SwapEndianness(ImageAccessor& image /* inplace */); + + static void Maximum(ImageAccessor& image /* inout */, + const ImageAccessor& other); }; } diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/Sources/SystemToolbox.cpp --- a/OrthancFramework/Sources/SystemToolbox.cpp Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Wed Mar 23 12:23:11 2022 +0100 @@ -817,6 +817,14 @@ { return MimeType_Ico; } + else if (extension == ".gz") + { + return MimeType_Gzip; + } + else if (extension == ".zip") + { + return MimeType_Zip; + } // Default type else diff -r 1b76853e1797 -r 501411a67f10 OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp --- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Wed Mar 23 12:23:11 2022 +0100 @@ -49,7 +49,6 @@ #include "../Sources/Images/ImageBuffer.h" #include "../Sources/Images/ImageProcessing.h" #include "../Sources/Images/PngReader.h" -#include "../Sources/Images/PngWriter.h" #include "../Sources/Logging.h" #include "../Sources/OrthancException.h" diff -r 1b76853e1797 -r 501411a67f10 OrthancServer/Plugins/Engine/OrthancPlugins.cpp --- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Wed Mar 23 12:23:11 2022 +0100 @@ -39,6 +39,7 @@ #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h" #include "../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h" +#include "../../../OrthancFramework/Sources/HttpServer/HttpServer.h" #include "../../../OrthancFramework/Sources/HttpServer/HttpToolbox.h" #include "../../../OrthancFramework/Sources/Images/Image.h" #include "../../../OrthancFramework/Sources/Images/ImageProcessing.h" @@ -75,6 +76,344 @@ namespace Orthanc { + class OrthancPlugins::WebDavCollection : public IWebDavBucket + { + private: + PluginsErrorDictionary& errorDictionary_; + std::string uri_; + OrthancPluginWebDavIsExistingFolderCallback isExistingFolder_; + OrthancPluginWebDavListFolderCallback listFolder_; + OrthancPluginWebDavRetrieveFileCallback retrieveFile_; + OrthancPluginWebDavStoreFileCallback storeFile_; + OrthancPluginWebDavCreateFolderCallback createFolder_; + OrthancPluginWebDavDeleteItemCallback deleteItem_; + void* payload_; + + class PathHelper : public boost::noncopyable + { + private: + std::vector items_; + + public: + explicit PathHelper(const std::vector& path) + { + items_.resize(path.size()); + for (size_t i = 0; i < path.size(); i++) + { + items_[i] = path[i].c_str(); + } + } + + uint32_t GetSize() const + { + return static_cast(items_.size()); + } + + const char* const* GetItems() const + { + return (items_.empty() ? NULL : &items_[0]); + } + }; + + + static MimeType ParseMimeType(const char* mimeType) + { + MimeType mime; + if (LookupMimeType(mime, mimeType)) + { + return mime; + } + else + { + LOG(WARNING) << "Unknown MIME type in plugin: " << mimeType; + return MimeType_Binary; + } + } + + static OrthancPluginErrorCode AddFile( + OrthancPluginWebDavCollection* collection, + const char* displayName, + uint64_t contentSize, + const char* mimeType, + const char* creationTime) + { + try + { + std::unique_ptr f(new File(displayName)); + f->SetCreationTime(boost::posix_time::from_iso_string(creationTime)); + f->SetContentLength(contentSize); + + if (mimeType == NULL || + std::string(mimeType).empty()) + { + f->SetMimeType(SystemToolbox::AutodetectMimeType(displayName)); + } + else + { + f->SetMimeType(ParseMimeType(mimeType)); + } + + reinterpret_cast(collection)->AddResource(f.release()); + return OrthancPluginErrorCode_Success; + } + catch (OrthancException& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } + + static OrthancPluginErrorCode AddFolder( + OrthancPluginWebDavCollection* collection, + const char* displayName, + const char* creationTime) + { + try + { + std::unique_ptr f(new Folder(displayName)); + f->SetCreationTime(boost::posix_time::from_iso_string(creationTime)); + reinterpret_cast(collection)->AddResource(f.release()); + return OrthancPluginErrorCode_Success; + } + catch (OrthancException& e) + { + return static_cast(e.GetErrorCode()); + } + catch (boost::bad_lexical_cast&) + { + LOG(ERROR) << "Presumably ill-formed date in the plugin"; + return OrthancPluginErrorCode_ParameterOutOfRange; + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } + + + class ContentTarget : public boost::noncopyable + { + private: + bool isSent_; + MimeType& mime_; + std::string& content_; + boost::posix_time::ptime& modificationTime_; + + public: + ContentTarget(const std::string& displayName, + MimeType& mime, + std::string& content, + boost::posix_time::ptime& modificationTime) : + isSent_(false), + mime_(mime), + content_(content), + modificationTime_(modificationTime) + { + mime = SystemToolbox::AutodetectMimeType(displayName); + } + + bool IsSent() const + { + return isSent_; + } + + static OrthancPluginErrorCode RetrieveFile( + OrthancPluginWebDavCollection* collection, + const void* data, + uint64_t size, + const char* mimeType, + const char* creationTime) + { + ContentTarget& target = *reinterpret_cast(collection); + + if (target.isSent_) + { + return OrthancPluginErrorCode_BadSequenceOfCalls; + } + else + { + try + { + target.isSent_ = true; + + if (mimeType != NULL && + !std::string(mimeType).empty()) + { + target.mime_ = ParseMimeType(mimeType); + } + + target.content_.assign(reinterpret_cast(data), size); + target.modificationTime_ = boost::posix_time::from_iso_string(creationTime); + return OrthancPluginErrorCode_Success; + } + catch (Orthanc::OrthancException& e) + { + return static_cast(e.GetErrorCode()); + } + catch (boost::bad_lexical_cast&) + { + LOG(ERROR) << "Presumably ill-formed date in the plugin"; + return OrthancPluginErrorCode_ParameterOutOfRange; + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } + } + }; + + + public: + WebDavCollection(PluginsErrorDictionary& errorDictionary, + const _OrthancPluginRegisterWebDavCollection& p) : + errorDictionary_(errorDictionary), + uri_(p.uri), + isExistingFolder_(p.isExistingFolder), + listFolder_(p.listFolder), + retrieveFile_(p.retrieveFile), + storeFile_(p.storeFile), + createFolder_(p.createFolder), + deleteItem_(p.deleteItem), + payload_(p.payload) + { + } + + const std::string& GetUri() const + { + return uri_; + } + + virtual bool IsExistingFolder(const std::vector& path) + { + PathHelper helper(path); + + uint8_t isExisting; + OrthancPluginErrorCode code = isExistingFolder_(&isExisting, helper.GetSize(), helper.GetItems(), payload_); + + if (code == OrthancPluginErrorCode_Success) + { + return (isExisting != 0); + } + else + { + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + + virtual bool ListCollection(Collection& collection, + const std::vector& path) + { + PathHelper helper(path); + + uint8_t isExisting; + OrthancPluginErrorCode code = listFolder_(&isExisting, reinterpret_cast(&collection), + AddFile, AddFolder, helper.GetSize(), helper.GetItems(), payload_); + + if (code == OrthancPluginErrorCode_Success) + { + return (isExisting != 0); + } + else + { + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + + virtual bool GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& modificationTime, + const std::vector& path) + { + PathHelper helper(path); + + ContentTarget target(path.back(), mime, content, modificationTime); + OrthancPluginErrorCode code = retrieveFile_( + reinterpret_cast(&target), + ContentTarget::RetrieveFile, helper.GetSize(), helper.GetItems(), payload_); + + if (code == OrthancPluginErrorCode_Success) + { + return target.IsSent(); + } + else + { + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + + virtual bool StoreFile(const std::string& content, + const std::vector& path) + { + PathHelper helper(path); + + uint8_t isReadOnly; + OrthancPluginErrorCode code = storeFile_(&isReadOnly, helper.GetSize(), helper.GetItems(), + content.empty() ? NULL : content.c_str(), content.size(), payload_); + + if (code == OrthancPluginErrorCode_Success) + { + return (isReadOnly != 0); + } + else + { + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + + virtual bool CreateFolder(const std::vector& path) + { + PathHelper helper(path); + + uint8_t isReadOnly; + OrthancPluginErrorCode code = createFolder_(&isReadOnly, helper.GetSize(), helper.GetItems(), payload_); + + if (code == OrthancPluginErrorCode_Success) + { + return (isReadOnly != 0); + } + else + { + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + + virtual bool DeleteItem(const std::vector& path) + { + PathHelper helper(path); + + uint8_t isReadOnly; + OrthancPluginErrorCode code = deleteItem_(&isReadOnly, helper.GetSize(), helper.GetItems(), payload_); + + if (code == OrthancPluginErrorCode_Success) + { + return (isReadOnly != 0); + } + else + { + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + + virtual void Start() + { + } + + virtual void Stop() + { + } + }; + + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, const void* data, size_t size) @@ -1164,6 +1503,7 @@ typedef std::list RefreshMetricsCallbacks; typedef std::list StorageCommitmentScpCallbacks; typedef std::map Properties; + typedef std::list WebDavCollections; PluginsManager manager_; @@ -1184,6 +1524,7 @@ OrthancPluginReceivedInstanceCallback receivedInstanceCallback_; // New in Orthanc 1.10.0 RefreshMetricsCallbacks refreshMetricsCallbacks_; StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_; + WebDavCollections webDavCollections_; // New in Orthanc 1.10.1 std::unique_ptr storageArea_; std::set authorizationTokens_; @@ -1768,7 +2109,13 @@ it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it) { delete *it; - } + } + + for (PImpl::WebDavCollections::iterator it = pimpl_->webDavCollections_.begin(); + it != pimpl_->webDavCollections_.end(); ++it) + { + delete *it; + } } @@ -5265,6 +5612,15 @@ return true; } + case _OrthancPluginService_RegisterWebDavCollection: + { + CLOG(INFO, PLUGINS) << "Plugin has registered a WebDAV collection"; + const _OrthancPluginRegisterWebDavCollection& p = + *reinterpret_cast(parameters); + pimpl_->webDavCollections_.push_back(new WebDavCollection(GetErrorDictionary(), p)); + return true; + } + default: { // This service is unknown to the Orthanc plugin engine @@ -5862,4 +6218,22 @@ boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); return pimpl_->maxDatabaseRetries_; } + + + void OrthancPlugins::RegisterWebDavCollections(HttpServer& target) + { + boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); + + while (!pimpl_->webDavCollections_.empty()) + { + WebDavCollection* collection = pimpl_->webDavCollections_.front(); + assert(collection != NULL); + + UriComponents components; + Toolbox::SplitUriComponents(components, collection->GetUri()); + target.Register(components, collection); + + pimpl_->webDavCollections_.pop_front(); + } + } } diff -r 1b76853e1797 -r 501411a67f10 OrthancServer/Plugins/Engine/OrthancPlugins.h --- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Wed Mar 23 12:23:11 2022 +0100 @@ -62,6 +62,7 @@ namespace Orthanc { + class HttpServer; class ServerContext; class OrthancPlugins : @@ -89,6 +90,7 @@ class DicomInstanceFromCallback; class DicomInstanceFromBuffer; class DicomInstanceFromTranscoded; + class WebDavCollection; void RegisterRestCallback(const void* parameters, bool lock); @@ -394,6 +396,8 @@ bool IsValidAuthorizationToken(const std::string& token) const; unsigned int GetMaxDatabaseRetries() const; + + void RegisterWebDavCollections(HttpServer& target); }; } diff -r 1b76853e1797 -r 501411a67f10 OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed Mar 23 12:23:11 2022 +0100 @@ -30,6 +30,7 @@ * - Possibly register a callback to keep/discard/modify incoming DICOM instances using OrthancPluginRegisterReceivedInstanceCallback(). * - Possibly register a custom transcoder for DICOM images using OrthancPluginRegisterTranscoderCallback(). * - Possibly register a callback to discard instances received through DICOM C-STORE using OrthancPluginRegisterIncomingCStoreInstanceFilter(). + * - Possibly register a callback to branch a WebDAV virtual filesystem using OrthancPluginRegisterWebDavCollection(). * -# void OrthancPluginFinalize(): * This function is invoked by Orthanc during its shutdown. The plugin * must free all its memory. @@ -119,7 +120,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 10 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 0 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 1 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -466,6 +467,7 @@ _OrthancPluginService_RegisterStorageArea2 = 1016, /* New in Orthanc 1.9.0 */ _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017, /* New in Orthanc 1.10.0 */ _OrthancPluginService_RegisterReceivedInstanceCallback = 1018, /* New in Orthanc 1.10.0 */ + _OrthancPluginService_RegisterWebDavCollection = 1019, /* New in Orthanc 1.10.1 */ /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -1065,7 +1067,7 @@ /** * @brief Opaque structure that represents the HTTP connection to the client application. - * @ingroup Callback + * @ingroup Callbacks **/ typedef struct _OrthancPluginRestOutput_t OrthancPluginRestOutput; @@ -1372,7 +1374,7 @@ * @param headersKeys The keys of the HTTP headers (always converted to low-case). * @param headersValues The values of the HTTP headers. * @return 0 if forbidden access, 1 if allowed access, -1 if error. - * @ingroup Callback + * @ingroup Callbacks * @deprecated Please instead use OrthancPluginIncomingHttpRequestFilter2() **/ typedef int32_t (*OrthancPluginIncomingHttpRequestFilter) ( @@ -1408,7 +1410,7 @@ * @param getArgumentsKeys The keys of the GET arguments (only for the GET HTTP method). * @param getArgumentsValues The values of the GET arguments (only for the GET HTTP method). * @return 0 if forbidden access, 1 if allowed access, -1 if error. - * @ingroup Callback + * @ingroup Callbacks **/ typedef int32_t (*OrthancPluginIncomingHttpRequestFilter2) ( OrthancPluginHttpMethod method, @@ -7410,7 +7412,7 @@ /** * @brief Opaque structure that reads the content of a HTTP request body during a chunked HTTP transfer. - * @ingroup Callback + * @ingroup Callbacks **/ typedef struct _OrthancPluginServerChunkedRequestReader_t OrthancPluginServerChunkedRequestReader; @@ -7705,7 +7707,7 @@ * @param factory Factory function that creates the handler object * for incoming storage commitment requests. * @param destructor Destructor function to destroy the handler object. - * @param lookup Callback method to get the status of one DICOM instance. + * @param lookup Callback function to get the status of one DICOM instance. * @return 0 if success, other value if error. * @ingroup DicomCallbacks **/ @@ -7747,7 +7749,7 @@ * * @param instance The received DICOM instance. * @return 0 to discard the instance, 1 to store the instance, -1 if error. - * @ingroup Callback + * @ingroup Callbacks **/ typedef int32_t (*OrthancPluginIncomingDicomInstanceFilter) ( const OrthancPluginDicomInstance* instance); @@ -7809,7 +7811,7 @@ * DIMSE status to be sent by the C-STORE SCP of Orthanc * @param instance The received DICOM instance. * @return 0 to discard the instance, 1 to store the instance, -1 if error. - * @ingroup Callback + * @ingroup Callbacks **/ typedef int32_t (*OrthancPluginIncomingCStoreInstanceFilter) ( uint16_t* dimseStatus /* out */, @@ -7876,7 +7878,7 @@ * @return `OrthancPluginReceivedInstanceAction_KeepAsIs` to accept the instance as is,
* `OrthancPluginReceivedInstanceAction_Modify` to store the modified DICOM contained in `modifiedDicomBuffer`,
* `OrthancPluginReceivedInstanceAction_Discard` to tell Orthanc to discard the instance. - * @ingroup Callback + * @ingroup Callbacks **/ typedef OrthancPluginReceivedInstanceAction (*OrthancPluginReceivedInstanceCallback) ( OrthancPluginMemoryBuffer64* modifiedDicomBuffer, @@ -8474,7 +8476,7 @@ /** - * @brief Generate a token to grant full access to the REST API of Orthanc + * @brief Generate a token to grant full access to the REST API of Orthanc. * * This function generates a token that can be set in the HTTP * header "Authorization" so as to grant full access to the REST API @@ -8729,6 +8731,279 @@ } + + /** + * @brief Opaque structure that represents a WebDAV collection. + * @ingroup Callbacks + **/ + typedef struct _OrthancPluginWebDavCollection_t OrthancPluginWebDavCollection; + + + /** + * @brief Declare a file while returning the content of a folder. + * + * This function declares a file while returning the content of a + * WebDAV folder. + * + * @param collection Context of the collection. + * @param name Base name of the file. + * @param dateTime The date and time of creation of the file. + * Check out the documentation of OrthancPluginWebDavRetrieveFile() for more information. + * @param size Size of the file. + * @param mimeType The MIME type of the file. If empty or set to `NULL`, + * Orthanc will do a best guess depending on the file extension. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavAddFile) ( + OrthancPluginWebDavCollection* collection, + const char* name, + uint64_t size, + const char* mimeType, + const char* dateTime); + + + /** + * @brief Declare a subfolder while returning the content of a folder. + * + * This function declares a subfolder while returning the content of a + * WebDAV folder. + * + * @param collection Context of the collection. + * @param name Base name of the subfolder. + * @param dateTime The date and time of creation of the subfolder. + * Check out the documentation of OrthancPluginWebDavRetrieveFile() for more information. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavAddFolder) ( + OrthancPluginWebDavCollection* collection, + const char* name, + const char* dateTime); + + + /** + * @brief Retrieve the content of a file. + * + * This function is used to forward the content of a file from a + * WebDAV collection, to the core of Orthanc. + * + * @param collection Context of the collection. + * @param data Content of the file. + * @param size Size of the file. + * @param mimeType The MIME type of the file. If empty or set to `NULL`, + * Orthanc will do a best guess depending on the file extension. + * @param dateTime The date and time of creation of the file. + * It must be formatted as an ISO string of form + * `YYYYMMDDTHHMMSS,fffffffff` where T is the date-time + * separator. It must be expressed in UTC (it is the responsibility + * of the plugin to do the possible timezone + * conversions). Internally, this string will be parsed using + * `boost::posix_time::from_iso_string()`. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavRetrieveFile) ( + OrthancPluginWebDavCollection* collection, + const void* data, + uint64_t size, + const char* mimeType, + const char* dateTime); + + + /** + * @brief Callback for testing the existence of a folder. + * + * Signature of a callback function that tests whether the given + * path in the WebDAV collection exists and corresponds to a folder. + * + * @param isExisting Pointer to a Boolean that must be set to `1` if the folder exists, or `0` otherwise. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavIsExistingFolderCallback) ( + uint8_t* isExisting, /* out */ + uint32_t pathSize, + const char* const* pathItems, + void* payload); + + + /** + * @brief Callback for listing the content of a folder. + * + * Signature of a callback function that lists the content of a + * folder in the WebDAV collection. The callback must call the + * provided `addFile()` and `addFolder()` functions to emit the + * content of the folder. + * + * @param isExisting Pointer to a Boolean that must be set to `1` if the folder exists, or `0` otherwise. + * @param collection Context to be provided to `addFile()` and `addFolder()` functions. + * @param addFile Function to add a file to the list. + * @param addFolder Function to add a folder to the list. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavListFolderCallback) ( + uint8_t* isExisting, /* out */ + OrthancPluginWebDavCollection* collection, + OrthancPluginWebDavAddFile addFile, + OrthancPluginWebDavAddFolder addFolder, + uint32_t pathSize, + const char* const* pathItems, + void* payload); + + + /** + * @brief Callback for retrieving the content of a file. + * + * Signature of a callback function that retrieves the content of a + * file in the WebDAV collection. The callback must call the + * provided `retrieveFile()` function to emit the actual content of + * the file. + * + * @param collection Context to be provided to `retrieveFile()` function. + * @param retrieveFile Function to return the content of the file. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavRetrieveFileCallback) ( + OrthancPluginWebDavCollection* collection, + OrthancPluginWebDavRetrieveFile retrieveFile, + uint32_t pathSize, + const char* const* pathItems, + void* payload); + + + /** + * @brief Callback to store a file. + * + * Signature of a callback function that stores a file into the + * WebDAV collection. + * + * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param data Content of the file to be stored. + * @param size Size of the file to be stored. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavStoreFileCallback) ( + uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + const void* data, + uint64_t size, + void* payload); + + + /** + * @brief Callback to create a folder. + * + * Signature of a callback function that creates a folder in the + * WebDAV collection. + * + * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavCreateFolderCallback) ( + uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + void* payload); + + + /** + * @brief Callback to remove a file or a folder. + * + * Signature of a callback function that removes a file or a folder + * from the WebDAV collection. + * + * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavDeleteItemCallback) ( + uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + void* payload); + + + typedef struct + { + const char* uri; + OrthancPluginWebDavIsExistingFolderCallback isExistingFolder; + OrthancPluginWebDavListFolderCallback listFolder; + OrthancPluginWebDavRetrieveFileCallback retrieveFile; + OrthancPluginWebDavStoreFileCallback storeFile; + OrthancPluginWebDavCreateFolderCallback createFolder; + OrthancPluginWebDavDeleteItemCallback deleteItem; + void* payload; + } _OrthancPluginRegisterWebDavCollection; + + /** + * @brief Register a WebDAV virtual filesystem. + * + * This function maps a WebDAV collection onto the given URI in the + * REST API of Orthanc. This function must be called during the + * initialization of the plugin, i.e. inside the + * OrthancPluginInitialize() public function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param uri URI where to map the WebDAV collection (must start with a `/` character). + * @param isExistingFolder Callback method to test for the existence of a folder. + * @param listFolder Callback method to list the content of a folder. + * @param retrieveFile Callback method to retrieve the content of a file. + * @param storeFile Callback method to store a file into the WebDAV collection. + * @param createFolder Callback method to create a folder. + * @param deleteItem Callback method to delete a file or a folder. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterWebDavCollection( + OrthancPluginContext* context, + const char* uri, + OrthancPluginWebDavIsExistingFolderCallback isExistingFolder, + OrthancPluginWebDavListFolderCallback listFolder, + OrthancPluginWebDavRetrieveFileCallback retrieveFile, + OrthancPluginWebDavStoreFileCallback storeFile, + OrthancPluginWebDavCreateFolderCallback createFolder, + OrthancPluginWebDavDeleteItemCallback deleteItem, + void* payload) + { + _OrthancPluginRegisterWebDavCollection params; + params.uri = uri; + params.isExistingFolder = isExistingFolder; + params.listFolder = listFolder; + params.retrieveFile = retrieveFile; + params.storeFile = storeFile; + params.createFolder = createFolder; + params.deleteItem = deleteItem; + params.payload = payload; + + return context->InvokeService(context, _OrthancPluginService_RegisterWebDavCollection, ¶ms); + } + + #ifdef __cplusplus } #endif diff -r 1b76853e1797 -r 501411a67f10 OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp --- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Mar 23 12:23:11 2022 +0100 @@ -2534,7 +2534,7 @@ } catch (...) { - return OrthancPluginErrorCode_InternalError; + return OrthancPluginErrorCode_Plugin; } } } @@ -3556,4 +3556,238 @@ } } #endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static std::vector WebDavConvertPath(uint32_t pathSize, + const char* const* pathItems) + { + std::vector result(pathSize); + + for (uint32_t i = 0; i < pathSize; i++) + { + result[i] = pathItems[i]; + } + + return result; + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavIsExistingFolder(uint8_t* isExisting, + uint32_t pathSize, + const char* const* pathItems, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + *isExisting = (that.IsExistingFolder(WebDavConvertPath(pathSize, pathItems)) ? 1 : 0); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavListFolder(uint8_t* isExisting, + OrthancPluginWebDavCollection* collection, + OrthancPluginWebDavAddFile addFile, + OrthancPluginWebDavAddFolder addFolder, + uint32_t pathSize, + const char* const* pathItems, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + std::list files; + std::list subfolders; + + if (!that.ListFolder(files, subfolders, WebDavConvertPath(pathSize, pathItems))) + { + *isExisting = 0; + } + else + { + *isExisting = 1; + + for (std::list::const_iterator + it = files.begin(); it != files.end(); ++it) + { + OrthancPluginErrorCode code = addFile( + collection, it->GetName().c_str(), it->GetContentSize(), + it->GetMimeType().c_str(), it->GetDateTime().c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + return code; + } + } + + for (std::list::const_iterator it = + subfolders.begin(); it != subfolders.end(); ++it) + { + OrthancPluginErrorCode code = addFolder( + collection, it->GetName().c_str(), it->GetDateTime().c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + return code; + } + } + } + + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavRetrieveFile(OrthancPluginWebDavCollection* collection, + OrthancPluginWebDavRetrieveFile retrieveFile, + uint32_t pathSize, + const char* const* pathItems, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + std::string content, mime, dateTime; + + if (that.GetFile(content, mime, dateTime, WebDavConvertPath(pathSize, pathItems))) + { + return retrieveFile(collection, content.empty() ? NULL : content.c_str(), + content.size(), mime.c_str(), dateTime.c_str()); + } + else + { + // Inexisting file + return OrthancPluginErrorCode_Success; + } + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavStoreFileCallback(uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + const void* data, + uint64_t size, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + *isReadOnly = (that.StoreFile(WebDavConvertPath(pathSize, pathItems), data, size) ? 1 : 0); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavCreateFolderCallback(uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + *isReadOnly = (that.CreateFolder(WebDavConvertPath(pathSize, pathItems)) ? 1 : 0); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavDeleteItemCallback(uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + *isReadOnly = (that.DeleteItem(WebDavConvertPath(pathSize, pathItems)) ? 1 : 0); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + void IWebDavCollection::Register(const std::string& uri, + IWebDavCollection& collection) + { + OrthancPluginErrorCode code = OrthancPluginRegisterWebDavCollection( + GetGlobalContext(), uri.c_str(), WebDavIsExistingFolder, WebDavListFolder, WebDavRetrieveFile, + WebDavStoreFileCallback, WebDavCreateFolderCallback, WebDavDeleteItemCallback, &collection); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif } diff -r 1b76853e1797 -r 501411a67f10 OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h --- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed Mar 23 12:23:11 2022 +0100 @@ -115,6 +115,12 @@ # define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP 0 #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 10, 1) +# define HAS_ORTHANC_PLUGIN_WEBDAV 1 +#else +# define HAS_ORTHANC_PLUGIN_WEBDAV 0 +#endif + namespace OrthancPlugins @@ -1257,4 +1263,107 @@ const std::string& transferSyntax); #endif }; + + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + class IWebDavCollection : public boost::noncopyable + { + public: + class FileInfo + { + private: + std::string name_; + uint64_t contentSize_; + std::string mime_; + std::string dateTime_; + + public: + FileInfo(const std::string& name, + uint64_t contentSize, + const std::string& dateTime) : + name_(name), + contentSize_(contentSize), + dateTime_(dateTime) + { + } + + const std::string& GetName() const + { + return name_; + } + + uint64_t GetContentSize() const + { + return contentSize_; + } + + void SetMimeType(const std::string& mime) + { + mime_ = mime; + } + + const std::string& GetMimeType() const + { + return mime_; + } + + const std::string& GetDateTime() const + { + return dateTime_; + } + }; + + class FolderInfo + { + private: + std::string name_; + std::string dateTime_; + + public: + FolderInfo(const std::string& name, + const std::string& dateTime) : + name_(name), + dateTime_(dateTime) + { + } + + const std::string& GetName() const + { + return name_; + } + + const std::string& GetDateTime() const + { + return dateTime_; + } + }; + + virtual ~IWebDavCollection() + { + } + + virtual bool IsExistingFolder(const std::vector& path) = 0; + + virtual bool ListFolder(std::list& files, + std::list& subfolders, + const std::vector& path) = 0; + + virtual bool GetFile(std::string& content /* out */, + std::string& mime /* out */, + std::string& dateTime /* out */, + const std::vector& path) = 0; + + virtual bool StoreFile(const std::vector& path, + const void* data, + size_t size) = 0; + + virtual bool CreateFolder(const std::vector& path) = 0; + + virtual bool DeleteItem(const std::vector& path) = 0; + + static void Register(const std::string& uri, + IWebDavCollection& collection); + }; +#endif } diff -r 1b76853e1797 -r 501411a67f10 OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt Wed Mar 23 12:23:11 2022 +0100 @@ -0,0 +1,42 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2022 Osimis S.A., Belgium +# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +cmake_minimum_required(VERSION 2.8) + +project(WebDavFilesystem) + +SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") +SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") + +SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") +SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") + +include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake) +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/BoostConfiguration.cmake) + +add_definitions(-DHAS_ORTHANC_EXCEPTION=0) + +add_library(WebDavFilesystem SHARED + Plugin.cpp + ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp + ${JSONCPP_SOURCES} + ${BOOST_SOURCES} + ) diff -r 1b76853e1797 -r 501411a67f10 OrthancServer/Plugins/Samples/WebDavFilesystem/Plugin.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/WebDavFilesystem/Plugin.cpp Wed Mar 23 12:23:11 2022 +0100 @@ -0,0 +1,390 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2022 Osimis S.A., Belgium + * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../Common/OrthancPluginCppWrapper.h" + +#include + + +class Resource : public boost::noncopyable +{ +private: + boost::posix_time::ptime dateTime_; + +public: + Resource() : + dateTime_(boost::posix_time::second_clock::universal_time()) + { + } + + virtual ~Resource() + { + } + + const boost::posix_time::ptime& GetDateTime() const + { + return dateTime_; + } + + virtual bool IsFolder() const = 0; + + virtual Resource* LookupPath(const std::vector& path) = 0; +}; + + +class File : public Resource +{ +private: + std::string content_; + +public: + File(const void* data, + size_t size) : + content_(reinterpret_cast(data), size) + { + } + + const std::string& GetContent() const + { + return content_; + } + + virtual bool IsFolder() const + { + return false; + } + + virtual Resource* LookupPath(const std::vector& path) + { + if (path.empty()) + { + return this; + } + else + { + return NULL; + } + } +}; + + +class Folder : public Resource +{ +private: + typedef std::map Content; + + Content content_; + +public: + virtual ~Folder() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + virtual bool IsFolder() const + { + return true; + } + + virtual Resource* LookupPath(const std::vector& path) + { + if (path.empty()) + { + return this; + } + else + { + Content::const_iterator found = content_.find(path[0]); + if (found == content_.end()) + { + return NULL; + } + else + { + std::vector childPath(path.size() - 1); + + for (size_t i = 0; i < childPath.size(); i++) + { + childPath[i] = path[i + 1]; + } + + return found->second->LookupPath(childPath); + } + } + } + + void ListContent(std::list& files, + std::list& subfolders) const + { + for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(it->second != NULL); + + const std::string dateTime = boost::posix_time::to_iso_string(it->second->GetDateTime()); + + if (it->second->IsFolder()) + { + subfolders.push_back(OrthancPlugins::IWebDavCollection::FolderInfo(it->first, dateTime)); + } + else + { + const File& f = dynamic_cast(*it->second); + files.push_back(OrthancPlugins::IWebDavCollection::FileInfo(it->first, f.GetContent().size(), dateTime)); + } + } + } + + void StoreFile(const std::string& name, + File* f) + { + std::unique_ptr protection(f); + + if (content_.find(name) != content_.end()) + { + OrthancPlugins::LogError("Already existing: " + name); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadRequest); + } + else + { + content_[name] = protection.release(); + } + } + + void CreateSubfolder(const std::string& name) + { + if (content_.find(name) != content_.end()) + { + OrthancPlugins::LogError("Already existing: " + name); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadRequest); + } + else + { + content_[name] = new Folder; + } + } + + void DeleteItem(const std::string& name) + { + Content::iterator found = content_.find(name); + + if (found == content_.end()) + { + OrthancPlugins::LogError("Cannot delete inexistent path: " + name); + ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentItem); + } + else + { + assert(found->second != NULL); + delete found->second; + content_.erase(found); + } + } +}; + + +class WebDavFilesystem : public OrthancPlugins::IWebDavCollection +{ +private: + boost::mutex mutex_; + std::unique_ptr root_; + + static std::vector GetParentPath(const std::vector& path) + { + if (path.empty()) + { + OrthancPlugins::LogError("Empty path"); + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } + else + { + std::vector p(path.size() - 1); + + for (size_t i = 0; i < p.size(); i++) + { + p[i] = path[i]; + } + + return p; + } + } + +public: + WebDavFilesystem() : + root_(new Folder) + { + } + + virtual bool IsExistingFolder(const std::vector& path) + { + boost::mutex::scoped_lock lock(mutex_); + + Resource* resource = root_->LookupPath(path); + return (resource != NULL && + resource->IsFolder()); + } + + virtual bool ListFolder(std::list& files, + std::list& subfolders, + const std::vector& path) + { + boost::mutex::scoped_lock lock(mutex_); + + Resource* resource = root_->LookupPath(path); + if (resource != NULL && + resource->IsFolder()) + { + dynamic_cast(*resource).ListContent(files, subfolders); + return true; + } + else + { + return false; + } + } + + virtual bool GetFile(std::string& content /* out */, + std::string& mime /* out */, + std::string& dateTime /* out */, + const std::vector& path) + { + boost::mutex::scoped_lock lock(mutex_); + + Resource* resource = root_->LookupPath(path); + if (resource != NULL && + !resource->IsFolder()) + { + const File& file = dynamic_cast(*resource); + content = file.GetContent(); + mime = ""; // Let the Orthanc core autodetect the MIME type + dateTime = boost::posix_time::to_iso_string(file.GetDateTime()); + return true; + } + else + { + return false; + } + } + + virtual bool StoreFile(const std::vector& path, + const void* data, + size_t size) + { + boost::mutex::scoped_lock lock(mutex_); + + Resource* parent = root_->LookupPath(GetParentPath(path)); + if (parent != NULL && + parent->IsFolder()) + { + dynamic_cast(*parent).StoreFile(path.back(), new File(data, size)); + return true; + } + else + { + return false; + } + } + + virtual bool CreateFolder(const std::vector& path) + { + boost::mutex::scoped_lock lock(mutex_); + + Resource* parent = root_->LookupPath(GetParentPath(path)); + if (parent != NULL && + parent->IsFolder()) + { + dynamic_cast(*parent).CreateSubfolder(path.back()); + return true; + } + else + { + return false; + } + } + + virtual bool DeleteItem(const std::vector& path) + { + boost::mutex::scoped_lock lock(mutex_); + + Resource* parent = root_->LookupPath(GetParentPath(path)); + if (parent != NULL && + parent->IsFolder()) + { + dynamic_cast(*parent).DeleteItem(path.back()); + return true; + } + else + { + return false; + } + } +}; + + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + OrthancPlugins::SetGlobalContext(c); + OrthancPluginLogWarning(c, "WebDAV plugin is initializing"); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 0) + { + char info[1024]; + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + c->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(c, info); + return -1; + } + + static WebDavFilesystem filesystem; + OrthancPlugins::IWebDavCollection::Register("/webdav-plugin", filesystem); + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "WebDAV plugin is finalizing"); + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "webdav-sample"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return "0.0"; + } +} diff -r 1b76853e1797 -r 501411a67f10 OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Wed Mar 23 11:56:28 2022 +0100 +++ b/OrthancServer/Sources/main.cpp Wed Mar 23 12:23:11 2022 +0100 @@ -430,29 +430,53 @@ } else { - // If there are multiple modalities with the same AET, consider the one matching this IP + // If there are multiple modalities with the same AET, consider the one matching this IP + // or check if the operation is allowed for all modalities + bool allowedForAllModalities = true; + for (std::list::const_iterator it = modalities.begin(); it != modalities.end(); ++it) { - if (it->GetHost() == remoteIp) + if (it->IsRequestAllowed(type)) { - if (it->IsRequestAllowed(type)) + if (checkIp && + it->GetHost() == remoteIp) { return true; } - else - { - ReportDisallowedCommand(remoteIp, remoteAet, type); - return false; - } + } + else + { + allowedForAllModalities = false; } } - LOG(WARNING) << "DICOM authorization rejected for AET " << remoteAet - << " on IP " << remoteIp << ": " << modalities.size() - << " modalites found with this AET in configuration option " - << "\"DicomModalities\", but none of them matches the IP"; - return false; + if (allowedForAllModalities) + { + return true; + } + else + { + ReportDisallowedCommand(remoteIp, remoteAet, type); + + if (checkIp) + { + LOG(WARNING) << "DICOM authorization rejected for AET " << remoteAet + << " on IP " << remoteIp << ": " << modalities.size() + << " modalites found with this AET in configuration option " + << "\"DicomModalities\", but the operation is allowed for none " + << "of them matching the IP"; + } + else + { + LOG(WARNING) << "DICOM authorization rejected for AET " << remoteAet + << " on IP " << remoteIp << ": " << modalities.size() + << " modalites found with this AET in configuration option " + << "\"DicomModalities\", but the operation is not allowed for" + << "all of them"; + } + return false; + } } } } @@ -1157,6 +1181,13 @@ } } +#if ORTHANC_ENABLE_PLUGINS == 1 + if (plugins != NULL) + { + plugins->RegisterWebDavCollections(httpServer); + } +#endif + MyHttpExceptionFormatter exceptionFormatter(httpDescribeErrors, plugins); httpServer.SetIncomingHttpRequestFilter(httpFilter); diff -r 1b76853e1797 -r 501411a67f10 TODO --- a/TODO Wed Mar 23 11:56:28 2022 +0100 +++ b/TODO Wed Mar 23 12:23:11 2022 +0100 @@ -28,8 +28,10 @@ * (1) Accept extra DICOM tags dictionaries in the DCMTK format '.dic' (easier to use than declare them in the Orthanc configuration file). Even the standard dictionaries could be overriden by these custom dictionaries. -* provide more flexibility wrt Dicom TLS ciphers and TLS version: +* Provide more flexibility wrt Dicom TLS ciphers and TLS version: https://groups.google.com/g/orthanc-users/c/X4IhmXCSr7I/m/EirawAFcBwAJ +* Provide a configuration option to index additional tags as "MainDicomTags": + https://groups.google.com/g/orthanc-users/c/04oPNMHMzfE/m/kgHCroPWAQAJ ============================