Mercurial > hg > orthanc
changeset 6247:02c3f861b6e6 sql-opti
merged default -> sql-opti
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Mon, 14 Jul 2025 16:13:22 +0200 |
parents | e64c3ae969e4 (current diff) d70e4de0c847 (diff) |
children | afc746c090f6 |
files | |
diffstat | 81 files changed, 1657 insertions(+), 304 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Fri Jun 27 15:00:33 2025 +0200 +++ b/NEWS Mon Jul 14 16:13:22 2025 +0200 @@ -4,25 +4,58 @@ General ------- -* Lua: new "SetStableStatus" function. - +* Lua: new "SetStableStatus()" function. + +REST API +-------- + +* When creating a job, a "UserData" field can be added to the payload. + This data will travel along with the job and will be available in the + "/jobs/{jobId}" route. +* Added new metrics: + - "orthanc_available_dicom_threads" that displays the minimum + number of DICOM threads that were available during the last 10 seconds. + - "orthanc_available_http_threads" that displays the minimum + number of HTTP threads that were available during the last 10 seconds. + It is basically the opposite of "orthanc_rest_api_active_requests" but + it is more convenient to configure alerts on. "orthanc_rest_api_active_requests" + also monitors the internal requests coming from plugins while "orthanc_available_http_threads" + monitors the requests received by the external HTTP server. +* Fixed the "orthanc_rest_api_active_requests" metrix that was not + reset after 10 seconds. Plugin SDK ---------- -* Added new function OrthancPluginSetStableStatus to e.g force the - stabilization of a study from a plugin. - +* Added new primitives: + - "OrthancPluginSetStableStatus()" to force the stabilization of a + DICOM resource from a plugin. + - "OrthancPluginRegisterHttpAuthentication()" to install a custom + callback to authenticate HTTP requests. + - "OrthancPluginRecordAuditLog()" to record an audit log in DB, provided + that the DB plugin supports it. +* The OrthancPluginHttpRequest structure provides the payload of + the possible HTTP authentication callback. +* OrthancPluginCallRestApi() now also returns the body of DELETE requests: + https://discourse.orthanc-server.org/t/response-to-plugin-from-orthanc-api-delete-endpoint/6022 Plugins ------- * Housekeeper plugin: - - new "ForceReconstructFiles": If "Force" is set to true, forces - the "ReconstructFiles" option when reconstructing resources even - if the plugin did not detect any changes in the configuration that - should trigger a Reconstruct. - + - new "ForceReconstructFiles" option: If set to true, forces the + "ReconstructFiles" option when reconstructing resources, even if + the plugin did not detect any changes in the configuration that + should trigger a reconstruct. + +Maintenance +----------- + +* For security, if the "RegisteredUsers" configuration option is present + but empty, Orthanc does not create the default user "orthanc" anymore. +* Added new CMake option "-DBUILD_UNIT_TESTS=ON" to disable the building of unit tests. +* Fix handling of backslashes in DICOM elements if encoding is ISO_IR 13. +* Fix initialization of ICU. Version 1.12.8 (2025-06-13)
--- a/OrthancFramework/SharedLibrary/CMakeLists.txt Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/SharedLibrary/CMakeLists.txt Mon Jul 14 16:13:22 2025 +0200 @@ -45,7 +45,8 @@ # adds CMAKE_INSTALL_PREFIX to the include_directories(), which causes # issues if re-building the shared library after install! set(ORTHANC_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" CACHE PATH "") -SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") +set(BUILD_UNIT_TESTS ON CACHE BOOL "Whether to build the unit tests (new in Orthanc 1.12.9)") +set(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") set(BUILD_SHARED_LIBRARY ON CACHE BOOL "Whether to build a shared library instead of a static library") set(ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES "" CACHE STRING "Additional libraries to link against, separated by whitespaces, typically needed if building the static library (a common minimal value is \"boost_filesystem boost_iostreams boost_locale boost_regex boost_thread jsoncpp pugixml uuid\")") @@ -75,7 +76,6 @@ set(ENABLE_DCMTK ON) set(ENABLE_DCMTK_TRANSCODING ON) -set(ENABLE_GOOGLE_TEST ON) set(ENABLE_JPEG ON) set(ENABLE_LOCALE ON) set(ENABLE_LUA ON) @@ -83,6 +83,13 @@ set(ENABLE_PUGIXML ON) set(ENABLE_ZLIB ON) +if (BUILD_UNIT_TESTS) + set(ENABLE_GOOGLE_TEST ON) +else() + set(ENABLE_GOOGLE_TEST OFF) +endif() + + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or asm.js set(BOOST_LOCALE_BACKEND "libiconv") @@ -504,7 +511,8 @@ ## Compile the unit tests ##################################################################### -if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") +if (BUILD_UNIT_TESTS AND + NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") include(ExternalProject) if (CMAKE_TOOLCHAIN_FILE) @@ -544,6 +552,7 @@ -DUNIT_TESTS_WITH_HTTP_CONNEXIONS:BOOL=${UNIT_TESTS_WITH_HTTP_CONNEXIONS} -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE:BOOL=${USE_GOOGLE_TEST_DEBIAN_PACKAGE} -DUSE_SYSTEM_GOOGLE_TEST:BOOL=${USE_SYSTEM_GOOGLE_TEST} + -DBUILD_UNIT_TESTS:BOOL=${BUILD_UNIT_TESTS} -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -89,7 +89,7 @@ } - DicomServer::DicomServer() : + DicomServer::DicomServer(MetricsRegistry& metricsRegistry) : pimpl_(new PImpl), checkCalledAet_(true), aet_("ANY-SCP"), @@ -105,6 +105,7 @@ worklistRequestHandlerFactory_(NULL), storageCommitmentFactory_(NULL), applicationEntityFilter_(NULL), + metricsRegistry_(metricsRegistry), useDicomTls_(false), maximumPduLength_(ASC_DEFAULTMAXPDU), remoteCertificateRequired_(true), @@ -432,7 +433,7 @@ CLOG(INFO, DICOM) << "The embedded DICOM server will use " << threadsCount_ << " threads"; - pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, "DICOM-")); + pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, "DICOM-", metricsRegistry_, "orthanc_available_dicom_threads")); pimpl_->thread_ = boost::thread(ServerThread, this, maximumPduLength_, useDicomTls_); }
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.h Mon Jul 14 16:13:22 2025 +0200 @@ -47,6 +47,8 @@ namespace Orthanc { + class MetricsRegistry; + class DicomServer : public boost::noncopyable { public: @@ -83,6 +85,7 @@ IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_; IStorageCommitmentRequestHandlerFactory* storageCommitmentFactory_; IApplicationEntityFilter* applicationEntityFilter_; + MetricsRegistry& metricsRegistry_; // New in Orthanc 1.9.0 for DICOM TLS bool useDicomTls_; @@ -100,7 +103,7 @@ bool useDicomTls); public: - DicomServer(); + explicit DicomServer(MetricsRegistry& metricsRegistry); ~DicomServer();
--- a/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -167,7 +167,8 @@ { if (s != NULL) { - result = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions); + const bool skipBacklashes = true; // cf. "ISO_IR 13": In this method, the VR will never be UT, ST, or LT + result = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions, skipBacklashes); } return true;
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -632,8 +632,8 @@ { target.SetValueInternal(element->getTag().getGTag(), element->getTag().getETag(), - ConvertLeafElement(*element, DicomToJsonFlags_Default, - maxStringLength, encoding, hasCodeExtensions, ignoreTagLength)); + ConvertLeafElement(*element, DicomToJsonFlags_Default, maxStringLength, encoding, + hasCodeExtensions, ignoreTagLength, Convert(element->getVR()))); } else { @@ -694,7 +694,8 @@ unsigned int maxStringLength, Encoding encoding, bool hasCodeExtensions, - const std::set<DicomTag>& ignoreTagLength) + const std::set<DicomTag>& ignoreTagLength, + ValueRepresentation vr) { if (!element.isLeaf()) { @@ -714,7 +715,7 @@ else { const std::string s(c); - const std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions); + const std::string utf8 = Toolbox::ConvertDicomStringToUtf8(s, encoding, hasCodeExtensions, Convert(element.getVR())); return CreateValueFromUtf8String(GetTag(element), utf8, maxStringLength, ignoreTagLength); } } @@ -782,7 +783,7 @@ // "SpecificCharacterSet" tag, if present. This branch is // new in Orthanc 1.9.1 (cf. DICOM CP 246). const std::string s(reinterpret_cast<const char*>(data), length); - const std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions); + const std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions, Convert(element.getVR())); return CreateValueFromUtf8String(GetTag(element), utf8, maxStringLength, ignoreTagLength); } } @@ -1102,7 +1103,7 @@ { // The "0" below lets "LeafValueToJson()" take care of "TooLong" values std::unique_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement - (element, flags, 0, encoding, hasCodeExtensions, ignoreTagLength)); + (element, flags, 0, encoding, hasCodeExtensions, ignoreTagLength, Convert(element.getVR()))); if (ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end()) { @@ -2594,7 +2595,7 @@ element->getString(c).good() && c != NULL) { - std::string a = Toolbox::ConvertToUtf8(c, source, hasSourceCodeExtensions); + std::string a = Toolbox::ConvertToUtf8(c, source, hasSourceCodeExtensions, Convert(element->getVR())); std::string b = Toolbox::ConvertFromUtf8(a, target); element->putString(b.c_str()); } @@ -2848,7 +2849,7 @@ else { std::string s(c); - utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions); + utf8 = Toolbox::ConvertDicomStringToUtf8(s, encoding, hasCodeExtensions, FromDcmtkBridge::Convert(element.getVR())); } } @@ -2924,8 +2925,8 @@ std::string ignored; std::string s(reinterpret_cast<const char*>(data), l); - action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr, - Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions)); + std::string utf8 = Toolbox::ConvertDicomStringToUtf8(s, encoding, hasCodeExtensions, FromDcmtkBridge::Convert(element.getVR())); + action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr, utf8); } else {
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Mon Jul 14 16:13:22 2025 +0200 @@ -192,7 +192,8 @@ unsigned int maxStringLength, Encoding encoding, bool hasCodeExtensions, - const std::set<DicomTag>& ignoreTagLength); + const std::set<DicomTag>& ignoreTagLength, + ValueRepresentation vr); static void ExtractHeaderAsJson(Json::Value& target, DcmMetaInfo& header,
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -893,7 +893,7 @@ std::set<DicomTag> tmp; std::unique_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement (*element, DicomToJsonFlags_Default, - 0, encoding, hasCodeExtensions, tmp)); + 0, encoding, hasCodeExtensions, tmp, FromDcmtkBridge::Convert(element->getVR()))); if (v.get() == NULL || v->IsNull())
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -128,7 +128,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& arguments, const void* /*bodyData*/, - size_t /*bodySize*/) + size_t /*bodySize*/, + const std::string& authenticationPayload /* ignored */) { if (!Toolbox::IsChildUri(pimpl_->baseUri_, uri)) {
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h Mon Jul 14 16:13:22 2025 +0200 @@ -50,7 +50,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload /* ignored */) ORTHANC_OVERRIDE { return false; } @@ -64,7 +65,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& arguments, const void* /*bodyData*/, - size_t /*bodySize*/) ORTHANC_OVERRIDE; + size_t /*bodySize*/, + const std::string& authenticationPayload /* ignored */) ORTHANC_OVERRIDE; bool IsListDirectoryContent() const {
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -32,6 +32,7 @@ #include "../Logging.h" #include "../OrthancException.h" #include "../TemporaryFile.h" +#include "../MetricsRegistry.h" #include "HttpToolbox.h" #include "IHttpHandler.h" #include "MultipartStreamReader.h" @@ -348,6 +349,7 @@ bool isJQueryUploadChunk_; std::string jqueryUploadFileName_; size_t jqueryUploadFileSize_; + const std::string& authenticationPayload_; void HandleInternal(const MultipartStreamReader::HttpHeaders& headers, const void* part, @@ -358,7 +360,7 @@ HttpToolbox::GetArguments getArguments; if (!handler_.Handle(fakeOutput, RequestOrigin_RestApi, remoteIp_.c_str(), username_.c_str(), - HttpMethod_Post, uri_, headers, getArguments, part, size)) + HttpMethod_Post, uri_, headers, getArguments, part, size, authenticationPayload_)) { throw OrthancException(ErrorCode_UnknownResource); } @@ -370,14 +372,16 @@ const std::string& remoteIp, const std::string& username, const UriComponents& uri, - const MultipartStreamReader::HttpHeaders& headers) : + const MultipartStreamReader::HttpHeaders& headers, + const std::string& authenticationPayload) : handler_(handler), chunkStore_(chunkStore), remoteIp_(remoteIp), username_(username), uri_(uri), isJQueryUploadChunk_(false), - jqueryUploadFileSize_(0) // Dummy initialization + jqueryUploadFileSize_(0), // Dummy initialization + authenticationPayload_(authenticationPayload) { typedef HttpToolbox::Arguments::const_iterator Iterator; @@ -470,9 +474,10 @@ const UriComponents& uri, const std::map<std::string, std::string>& headers, const std::string& body, - const std::string& boundary) + const std::string& boundary, + const std::string& authenticationPayload) { - MultipartFormDataHandler handler(GetHandler(), pimpl_->chunkStore_, remoteIp, username, uri, headers); + MultipartFormDataHandler handler(GetHandler(), pimpl_->chunkStore_, remoteIp, username, uri, headers, authenticationPayload); MultipartStreamReader reader(boundary); reader.SetHandler(handler); @@ -621,7 +626,7 @@ enum AccessMode { - AccessMode_Forbidden, + AccessMode_Unauthorized, AccessMode_AuthorizationToken, AccessMode_RegisteredUser }; @@ -657,7 +662,7 @@ } } - return AccessMode_Forbidden; + return AccessMode_Unauthorized; } @@ -1146,7 +1151,42 @@ } #endif /* ORTHANC_ENABLE_PUGIXML == 1 */ - + + std::string HttpServer::GetRelativePathToRoot(const std::string& uri) + { + if (uri.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (uri == "/") + { + return "./"; + } + + std::string path; + + if (uri[uri.size() - 1] == '/') + { + path = "../"; + } + else + { + path = "./"; + } + + UriComponents components; + Toolbox::SplitUriComponents(components, uri); + + for (size_t i = 1; i < components.size(); i++) + { + path += "../"; + } + + return path; + } + + static void InternalCallback(HttpOutput& output /* out */, HttpMethod& method /* out */, HttpServer& server, @@ -1155,6 +1195,8 @@ { bool localhost; + MetricsRegistry::AvailableResourcesDecounter counter(server.GetAvailableHttpThreadsMetrics()); + #if ORTHANC_ENABLE_MONGOOSE == 1 static const long LOCALHOST = (127ll << 24) + 1ll; localhost = (request->remote_ip == LOCALHOST); @@ -1200,18 +1242,10 @@ HttpToolbox::ParseGetArguments(argumentsGET, request->query_string); } - + AccessMode accessMode = IsAccessGranted(server, headers); - // Authenticate this connection - if (server.IsAuthenticationEnabled() && - accessMode == AccessMode_Forbidden) - { - output.SendUnauthorized(server.GetRealm()); // 401 error - return; - } - #if ORTHANC_ENABLE_MONGOOSE == 1 // Apply the filter, if it is installed char remoteIp[24]; @@ -1235,6 +1269,55 @@ requestUri = ""; } + + const IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); + + // Authenticate this connection + std::string authenticationPayload; + std::string redirection; + IIncomingHttpRequestFilter::AuthenticationStatus status; + + if (filter == NULL) + { + status = IIncomingHttpRequestFilter::AuthenticationStatus_BuiltIn; + } + else + { + status = filter->CheckAuthentication(authenticationPayload, redirection, requestUri, remoteIp, headers, argumentsGET); + } + + switch (status) + { + case IIncomingHttpRequestFilter::AuthenticationStatus_BuiltIn: + // This was the only behavior available in Orthanc <= 1.12.8 + if (server.IsAuthenticationEnabled() && + accessMode == AccessMode_Unauthorized) + { + output.SendUnauthorized(server.GetRealm()); // 401 error + return; + } + break; + + case IIncomingHttpRequestFilter::AuthenticationStatus_Granted: + break; + + case IIncomingHttpRequestFilter::AuthenticationStatus_Redirect: + output.Redirect(Toolbox::JoinUri(HttpServer::GetRelativePathToRoot(requestUri), redirection)); + return; + + case IIncomingHttpRequestFilter::AuthenticationStatus_Unauthorized: + output.SendStatus(HttpStatus_401_Unauthorized); + return; + + case IIncomingHttpRequestFilter::AuthenticationStatus_Forbidden: + output.SendStatus(HttpStatus_403_Forbidden); + return; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // Decompose the URI into its components UriComponents uri; try @@ -1300,10 +1383,9 @@ // filter. In the case of an authorization bearer token, grant // full access to the API. - assert(accessMode == AccessMode_Forbidden || // Could be the case if "!server.IsAuthenticationEnabled()" + assert(accessMode == AccessMode_Unauthorized || // Could be the case if "!server.IsAuthenticationEnabled()" accessMode == AccessMode_RegisteredUser); - IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); if (filter != NULL && !filter->IsAllowed(filterMethod, requestUri, remoteIp, username.c_str(), headers, argumentsGET)) @@ -1339,7 +1421,7 @@ if (method == HttpMethod_Post || method == HttpMethod_Put) { - PostDataStatus status; + PostDataStatus postStatus; bool isMultipartForm = false; @@ -1356,10 +1438,10 @@ **/ isMultipartForm = true; - status = ReadBodyToString(body, connection, headers); - if (status == PostDataStatus_Success) + postStatus = ReadBodyToString(body, connection, headers); + if (postStatus == PostDataStatus_Success) { - server.ProcessMultipartFormData(remoteIp, username, uri, headers, body, boundary); + server.ProcessMultipartFormData(remoteIp, username, uri, headers, body, boundary, authenticationPayload); output.SendStatus(HttpStatus_200_Ok); return; } @@ -1372,7 +1454,7 @@ if (server.HasHandler()) { found = server.GetHandler().CreateChunkedRequestReader - (stream, RequestOrigin_RestApi, remoteIp, username.c_str(), method, uri, headers); + (stream, RequestOrigin_RestApi, remoteIp, username.c_str(), method, uri, headers, authenticationPayload); } if (found) @@ -1382,20 +1464,20 @@ throw OrthancException(ErrorCode_InternalError); } - status = ReadBodyToStream(*stream, connection, headers); + postStatus = ReadBodyToStream(*stream, connection, headers); - if (status == PostDataStatus_Success) + if (postStatus == PostDataStatus_Success) { stream->Execute(output); } } else { - status = ReadBodyToString(body, connection, headers); + postStatus = ReadBodyToString(body, connection, headers); } } - switch (status) + switch (postStatus) { case PostDataStatus_NoLength: output.SendStatus(HttpStatus_411_LengthRequired); @@ -1421,7 +1503,7 @@ server.HasHandler()) { found = server.GetHandler().Handle(output, RequestOrigin_RestApi, remoteIp, username.c_str(), - method, uri, headers, argumentsGET, body.c_str(), body.size()); + method, uri, headers, argumentsGET, body.c_str(), body.size(), authenticationPayload); } if (!found) @@ -1583,7 +1665,7 @@ } - HttpServer::HttpServer() : + HttpServer::HttpServer(MetricsRegistry& metricsRegistry) : pimpl_(new PImpl), handler_(NULL), remoteAllowed_(false), @@ -1601,7 +1683,10 @@ realm_(ORTHANC_REALM), threadsCount_(50), // Default value in mongoose/civetweb tcpNoDelay_(true), - requestTimeout_(30) // Default value in mongoose/civetweb (30 seconds) + requestTimeout_(30), // Default value in mongoose/civetweb (30 seconds) + availableHttpThreadsMetrics_(metricsRegistry, + "orthanc_available_http_threads_count", + MetricsUpdatePolicy_MinOver10Seconds) { #if ORTHANC_ENABLE_MONGOOSE == 1 CLOG(INFO, HTTP) << "This Orthanc server uses Mongoose as its embedded HTTP server"; @@ -2118,6 +2203,7 @@ Stop(); threadsCount_ = threads; + availableHttpThreadsMetrics_.SetInitialValue(threadsCount_); CLOG(INFO, HTTP) << "The embedded HTTP server will use " << threads << " threads"; }
--- a/OrthancFramework/Sources/HttpServer/HttpServer.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpServer.h Mon Jul 14 16:13:22 2025 +0200 @@ -50,6 +50,7 @@ #include "IIncomingHttpRequestFilter.h" +#include "../MetricsRegistry.h" #include <list> #include <map> @@ -114,6 +115,7 @@ unsigned int threadsCount_; bool tcpNoDelay_; unsigned int requestTimeout_; // In seconds + MetricsRegistry::SharedMetrics availableHttpThreadsMetrics_; #if ORTHANC_ENABLE_PUGIXML == 1 WebDavBuckets webDavBuckets_; @@ -122,7 +124,7 @@ bool IsRunning() const; public: - HttpServer(); + explicit HttpServer(MetricsRegistry& metricsRegistry); ~HttpServer(); @@ -225,6 +227,14 @@ const UriComponents& uri, const std::map<std::string, std::string>& headers, const std::string& body, - const std::string& boundary); + const std::string& boundary, + const std::string& authenticationPayload); + + MetricsRegistry::SharedMetrics& GetAvailableHttpThreadsMetrics() + { + return availableHttpThreadsMetrics_; + } + + static std::string GetRelativePathToRoot(const std::string& uri); }; }
--- a/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -230,7 +230,8 @@ const std::string& uri, const Arguments& httpHeaders) { - return (IHttpHandler::SimpleDelete(NULL, handler, origin, uri, httpHeaders) == HttpStatus_200_Ok); + std::string ignoredBody; + return (IHttpHandler::SimpleDelete(ignoredBody, NULL, handler, origin, uri, httpHeaders) == HttpStatus_200_Ok); } #endif }
--- a/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -49,7 +49,7 @@ HttpOutput http(stream, false /* assume no keep-alive */, 0); if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, - httpHeaders, getArguments, NULL /* no body for GET */, 0)) + httpHeaders, getArguments, NULL /* no body for GET */, 0, "" /* no authentication payload */)) { if (stream.GetStatus() == HttpStatus_200_Ok) { @@ -89,7 +89,7 @@ HttpOutput http(stream, false /* assume no keep-alive */, 0); if (handler.Handle(http, origin, LOCALHOST, "", method, curi, - httpHeaders, getArguments, bodyData, bodySize)) + httpHeaders, getArguments, bodyData, bodySize, "" /* no authentication payload */)) { stream.GetBody(answerBody); @@ -133,7 +133,8 @@ } - HttpStatus IHttpHandler::SimpleDelete(HttpToolbox::Arguments* answerHeaders, + HttpStatus IHttpHandler::SimpleDelete(std::string& answerBody, + HttpToolbox::Arguments* answerHeaders, IHttpHandler& handler, RequestOrigin origin, const std::string& uri, @@ -148,8 +149,10 @@ HttpOutput http(stream, false /* assume no keep-alive */, 0); if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Delete, curi, - httpHeaders, getArguments, NULL /* no body for DELETE */, 0)) + httpHeaders, getArguments, NULL /* no body for DELETE */, 0, "" /* no authentication payload */)) { + stream.GetBody(answerBody); + if (answerHeaders != NULL) { stream.GetHeaders(*answerHeaders, true /* convert key to lower case */);
--- a/OrthancFramework/Sources/HttpServer/IHttpHandler.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.h Mon Jul 14 16:13:22 2025 +0200 @@ -72,7 +72,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) = 0; + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) = 0; virtual bool Handle(HttpOutput& output, RequestOrigin origin, @@ -83,7 +84,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) = 0; + size_t bodySize, + const std::string& authenticationPayload) = 0; /** @@ -116,7 +118,8 @@ size_t bodySize, const HttpToolbox::Arguments& httpHeaders); - static HttpStatus SimpleDelete(HttpToolbox::Arguments* answerHeaders /* out */, + static HttpStatus SimpleDelete(std::string& answerBody /* out */, + HttpToolbox::Arguments* answerHeaders /* out */, IHttpHandler& handler, RequestOrigin origin, const std::string& uri,
--- a/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h Mon Jul 14 16:13:22 2025 +0200 @@ -31,18 +31,36 @@ class IIncomingHttpRequestFilter : public boost::noncopyable { public: + enum AuthenticationStatus + { + AuthenticationStatus_BuiltIn, // Use the default HTTP authentication built in Orthanc + AuthenticationStatus_Granted, // Let the REST callback process the request + AuthenticationStatus_Unauthorized, // 401 HTTP status + AuthenticationStatus_Forbidden, // 403 HTTP status + AuthenticationStatus_Redirect // 307 HTTP status + }; + virtual ~IIncomingHttpRequestFilter() { } // New in Orthanc 1.8.1 - virtual bool IsValidBearerToken(const std::string& token) = 0; + virtual bool IsValidBearerToken(const std::string& token) const = 0; + + // This method corresponds to HTTP authentication + HTTP authorization + virtual AuthenticationStatus CheckAuthentication(std::string& customPayload /* out: payload to provide to "IsAllowed()" */, + std::string& redirection /* out: path relative to the root */, + const char* uri, + const char* ip, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::GetArguments& getArguments) const = 0; + // This method corresponds to HTTP authorization alone virtual bool IsAllowed(HttpMethod method, const char* uri, const char* ip, const char* username, const HttpToolbox::Arguments& httpHeaders, - const HttpToolbox::GetArguments& getArguments) = 0; + const HttpToolbox::GetArguments& getArguments) const = 0; }; }
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -48,7 +48,20 @@ return s; } } - + +static std::string AddLeadingSlash(const std::string& s) +{ + if (s.empty() || + s[0] != '/') + { + return std::string("/") + s; + } + else + { + return s; + } +} + namespace Orthanc { @@ -163,7 +176,14 @@ const std::string& parentPath) const { std::string href; - Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName()); + std::vector<std::string> pathTokens; + + Toolbox::SplitString(pathTokens, parentPath, '/'); + pathTokens.push_back(GetDisplayName()); + + Toolbox::UriEncode(href, pathTokens); + href = AddLeadingSlash(href); + FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime()); pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop"); @@ -181,7 +201,14 @@ const std::string& parentPath) const { std::string href; - Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName()); + std::vector<std::string> pathTokens; + + Toolbox::SplitString(pathTokens, parentPath, '/'); + pathTokens.push_back(GetDisplayName()); + + Toolbox::UriEncode(href, pathTokens); + href = AddLeadingSlash(href); + FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime()); pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop"); @@ -245,7 +272,8 @@ } std::string href; - Toolbox::UriEncode(href, Toolbox::FlattenUri(tokens) + "/"); + Toolbox::UriEncode(href, tokens); + href = AddTrailingSlash(AddLeadingSlash(href)); boost::posix_time::ptime now = GetNow(); FormatInternal(self, href, folder, now, now);
--- a/OrthancFramework/Sources/JobsEngine/IJob.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/IJob.h Mon Jul 14 16:13:22 2025 +0200 @@ -76,5 +76,7 @@ // This function can only be called if the job has reached its // "success" state virtual void DeleteAllOutputs() {} + + virtual bool GetUserData(Json::Value& userData) const = 0; }; }
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -190,6 +190,11 @@ target["CreationTime"] = boost::posix_time::to_iso_string(creationTime_); target["EffectiveRuntime"] = static_cast<double>(runtime_.total_milliseconds()) / 1000.0; target["Progress"] = boost::math::iround(status_.GetProgress() * 100.0f); + + if (status_.HasUserData()) + { + target["UserData"] = status_.GetUserData(); + } target["Type"] = status_.GetJobType(); target["Content"] = status_.GetPublicContent();
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobStatus.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -59,7 +59,8 @@ job.GetJobType(jobType_); job.GetPublicContent(publicContent_); - + job.GetUserData(userData_); + hasSerialized_ = job.Serialize(serialized_); }
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobStatus.h Mon Jul 14 16:13:22 2025 +0200 @@ -38,6 +38,7 @@ Json::Value serialized_; bool hasSerialized_; std::string details_; + Json::Value userData_; public: JobStatus(); @@ -87,5 +88,15 @@ { return details_; } + + bool HasUserData() const + { + return !userData_.isNull(); + } + + const Json::Value& GetUserData() const + { + return userData_; + } }; }
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -43,6 +43,7 @@ static const char* RUNTIME = "Runtime"; static const char* ERROR_CODE = "ErrorCode"; static const char* ERROR_DETAILS = "ErrorDetails"; + static const char* USER_DATA = "UserData"; class JobsRegistry::JobHandler : public boost::noncopyable @@ -296,6 +297,13 @@ target[ERROR_CODE] = static_cast<int>(lastStatus_.GetErrorCode()); target[ERROR_DETAILS] = lastStatus_.GetDetails(); + // New in Orthanc 1.12.9 + Json::Value userData; + if (job_->GetUserData(userData)) + { + target[USER_DATA] = userData; + } + return true; } else
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h Mon Jul 14 16:13:22 2025 +0200 @@ -139,5 +139,10 @@ } void AwakeTrailingSleep(); + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + return false; + } }; }
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -235,7 +235,7 @@ static const char* KEY_POSITION = "Position"; static const char* KEY_TYPE = "Type"; static const char* KEY_COMMANDS = "Commands"; - + static const char* KEY_USER_DATA = "UserData"; void SetOfCommandsJob::GetPublicContent(Json::Value& value) const { @@ -254,6 +254,7 @@ target[KEY_PERMISSIVE] = permissive_; target[KEY_POSITION] = static_cast<unsigned int>(position_); target[KEY_DESCRIPTION] = description_; + target[KEY_USER_DATA] = userData_; target[KEY_COMMANDS] = Json::arrayValue; Json::Value& tmp = target[KEY_COMMANDS]; @@ -280,7 +281,13 @@ permissive_ = SerializationToolbox::ReadBoolean(source, KEY_PERMISSIVE); position_ = SerializationToolbox::ReadUnsignedInteger(source, KEY_POSITION); description_ = SerializationToolbox::ReadString(source, KEY_DESCRIPTION); - + + // new in 1.12.9 + if (source.isMember(KEY_USER_DATA)) + { + userData_ = source[KEY_USER_DATA]; + } + if (!source.isMember(KEY_COMMANDS) || source[KEY_COMMANDS].type() != Json::arrayValue) {
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h Mon Jul 14 16:13:22 2025 +0200 @@ -63,6 +63,7 @@ bool permissive_; size_t position_; std::string description_; + Json::Value userData_; public: SetOfCommandsJob(); @@ -116,5 +117,21 @@ { return false; } + + void SetUserData(const Json::Value& userData) + { + userData_ = userData; + } + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + if (!userData_.isNull()) + { + userData = userData_; + return true; + } + return false; + } + }; }
--- a/OrthancFramework/Sources/MetricsRegistry.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/MetricsRegistry.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -38,7 +38,7 @@ return boost::posix_time::microsec_clock::universal_time(); } - namespace + namespace MetricsRegistryInternals { template <typename T> class TimestampedValue : public boost::noncopyable @@ -47,6 +47,8 @@ boost::posix_time::ptime time_; bool hasValue_; T value_; + bool hasNextValue_; // for min and max values over period, we need to store the next value + T nextValue_; void SetValue(const T& value, const boost::posix_time::ptime& now) @@ -89,8 +91,25 @@ public: explicit TimestampedValue() : hasValue_(false), - value_(0) + value_(0), + hasNextValue_(false), + nextValue_(0) + { + } + + int GetPeriodDuration(const MetricsUpdatePolicy& policy) { + switch (policy) + { + case MetricsUpdatePolicy_MaxOver10Seconds: + case MetricsUpdatePolicy_MinOver10Seconds: + return 10; + case MetricsUpdatePolicy_MaxOver1Minute: + case MetricsUpdatePolicy_MinOver1Minute: + return 60; + default: + throw OrthancException(ErrorCode_InternalError); + } } void Update(const T& value, @@ -105,30 +124,54 @@ break; case MetricsUpdatePolicy_MaxOver10Seconds: - if (IsLargerOverPeriod(value, 10, now)) + case MetricsUpdatePolicy_MaxOver1Minute: + if (IsLargerOverPeriod(value, GetPeriodDuration(policy), now)) { SetValue(value, now); } - break; - - case MetricsUpdatePolicy_MaxOver1Minute: - if (IsLargerOverPeriod(value, 60, now)) + else { - SetValue(value, now); + hasNextValue_ = true; + nextValue_ = value; } break; case MetricsUpdatePolicy_MinOver10Seconds: - if (IsSmallerOverPeriod(value, 10, now)) + case MetricsUpdatePolicy_MinOver1Minute: + if (IsSmallerOverPeriod(value, GetPeriodDuration(policy), now)) { SetValue(value, now); } + else + { + hasNextValue_ = true; + nextValue_ = value; + } break; + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + void Refresh(const MetricsUpdatePolicy& policy) + { + const boost::posix_time::ptime now = GetNow(); + + switch (policy) + { + case MetricsUpdatePolicy_Directly: + // nothing to do + break; + + case MetricsUpdatePolicy_MaxOver10Seconds: + case MetricsUpdatePolicy_MinOver10Seconds: + case MetricsUpdatePolicy_MaxOver1Minute: case MetricsUpdatePolicy_MinOver1Minute: - if (IsSmallerOverPeriod(value, 60, now)) + // if the min/max value is older than the period, get the latest value + if ((now - time_).total_seconds() > GetPeriodDuration(policy) /* old value has expired */ && hasNextValue_) { - SetValue(value, now); + SetValue(nextValue_, now); + hasNextValue_ = false; } break; @@ -217,13 +260,17 @@ virtual const boost::posix_time::ptime& GetTime() const = 0; virtual std::string FormatValue() const = 0; + + virtual void Refresh() = 0; + + virtual void SetInitialValue(int64_t value) = 0; }; class MetricsRegistry::FloatItem : public Item { private: - TimestampedValue<float> value_; + MetricsRegistryInternals::TimestampedValue<float> value_; public: explicit FloatItem(MetricsUpdatePolicy policy) : @@ -265,13 +312,23 @@ { return boost::lexical_cast<std::string>(value_.GetValue()); } + + virtual void Refresh() ORTHANC_OVERRIDE + { + value_.Refresh(GetPolicy()); + } + + virtual void SetInitialValue(int64_t value) ORTHANC_OVERRIDE + { + value_.Update(value, MetricsUpdatePolicy_Directly); + } }; class MetricsRegistry::IntegerItem : public Item { private: - TimestampedValue<int64_t> value_; + MetricsRegistryInternals::TimestampedValue<int64_t> value_; public: explicit IntegerItem(MetricsUpdatePolicy policy) : @@ -313,6 +370,16 @@ { return boost::lexical_cast<std::string>(value_.GetValue()); } + + virtual void Refresh() ORTHANC_OVERRIDE + { + value_.Refresh(GetPolicy()); + } + + virtual void SetInitialValue(int64_t value) ORTHANC_OVERRIDE + { + value_.Update(value, MetricsUpdatePolicy_Directly); + } }; @@ -432,6 +499,16 @@ } } + void MetricsRegistry::SetInitialValue(const std::string& name, + int64_t value) + { + if (enabled_) + { + boost::mutex::scoped_lock lock(mutex_); + GetItemInternal(name, MetricsUpdatePolicy_Directly, MetricsDataType_Integer).SetInitialValue(value); + } + } + MetricsUpdatePolicy MetricsRegistry::GetUpdatePolicy(const std::string& metrics) { @@ -494,6 +571,8 @@ { boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH; + it->second->Refresh(); + std::string line = (it->first + " " + it->second->FormatValue() + " " + boost::lexical_cast<std::string>(diff.total_milliseconds()) + "\n"); @@ -513,6 +592,7 @@ name_(name), value_(0) { + registry_.Register(name, policy, MetricsDataType_Integer); } void MetricsRegistry::SharedMetrics::Add(int64_t delta) @@ -522,6 +602,12 @@ registry_.SetIntegerValue(name_, value_); } + void MetricsRegistry::SharedMetrics::SetInitialValue(int64_t value) + { + boost::mutex::scoped_lock lock(mutex_); + value_ = value; + registry_.SetInitialValue(name_, value_); + } MetricsRegistry::ActiveCounter::ActiveCounter(MetricsRegistry::SharedMetrics &metrics) : metrics_(metrics) @@ -535,6 +621,18 @@ } + MetricsRegistry::AvailableResourcesDecounter::AvailableResourcesDecounter(MetricsRegistry::SharedMetrics &metrics) : + metrics_(metrics) + { + metrics_.Add(-1); + } + + MetricsRegistry::AvailableResourcesDecounter::~AvailableResourcesDecounter() + { + metrics_.Add(1); + } + + void MetricsRegistry::Timer::Start() { if (registry_.IsEnabled())
--- a/OrthancFramework/Sources/MetricsRegistry.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/MetricsRegistry.h Mon Jul 14 16:13:22 2025 +0200 @@ -106,6 +106,9 @@ SetIntegerValue(name, value, MetricsUpdatePolicy_Directly); } + void SetInitialValue(const std::string& name, + int64_t value); + void IncrementIntegerValue(const std::string& name, int64_t delta); @@ -131,6 +134,8 @@ MetricsUpdatePolicy policy); void Add(int64_t delta); + + void SetInitialValue(int64_t value); }; @@ -146,6 +151,17 @@ }; + class ORTHANC_PUBLIC AvailableResourcesDecounter : public boost::noncopyable + { + private: + SharedMetrics& metrics_; + + public: + explicit AvailableResourcesDecounter(SharedMetrics& metrics); + + ~AvailableResourcesDecounter(); + }; + class ORTHANC_PUBLIC Timer : public boost::noncopyable { private:
--- a/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -29,6 +29,8 @@ #include "../Compatibility.h" #include "../OrthancException.h" #include "../Logging.h" +#include "../MetricsRegistry.h" + namespace Orthanc { @@ -41,6 +43,7 @@ SharedMessageQueue& queue_; boost::thread thread_; std::string name_; + MetricsRegistry::SharedMetrics& availableWorkers_; static void WorkerThread(Worker* that) { @@ -51,8 +54,11 @@ try { std::unique_ptr<IDynamicObject> obj(that->queue_.Dequeue(100)); + if (obj.get() != NULL) { + MetricsRegistry::AvailableResourcesDecounter counter(that->availableWorkers_); + IRunnableBySteps& runnable = *dynamic_cast<IRunnableBySteps*>(obj.get()); bool wishToContinue = runnable.Step(); @@ -86,10 +92,12 @@ public: Worker(const bool& globalContinue, SharedMessageQueue& queue, - const std::string& name) : + const std::string& name, + MetricsRegistry::SharedMetrics& availableWorkers) : continue_(globalContinue), queue_(queue), - name_(name) + name_(name), + availableWorkers_(availableWorkers) { thread_ = boost::thread(WorkerThread, this); } @@ -107,11 +115,23 @@ bool continue_; std::vector<Worker*> workers_; SharedMessageQueue queue_; + MetricsRegistry::SharedMetrics availableWorkers_; + + public: + PImpl(MetricsRegistry& metricsRegistry, const char* availableWorkersMetricsName) : + continue_(false), + availableWorkers_(metricsRegistry, availableWorkersMetricsName, MetricsUpdatePolicy_MinOver10Seconds) + { + } }; - RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers, const std::string& name) : pimpl_(new PImpl) + RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers, + const std::string& name, + MetricsRegistry& metricsRegistry, + const char* availableWorkersMetricsName) : + pimpl_(new PImpl(metricsRegistry, availableWorkersMetricsName)) { pimpl_->continue_ = true; @@ -121,11 +141,12 @@ } pimpl_->workers_.resize(countWorkers); + pimpl_->availableWorkers_.Add(countWorkers); // mark all workers as available for (size_t i = 0; i < countWorkers; i++) { std::string workerName = name + boost::lexical_cast<std::string>(i); - pimpl_->workers_[i] = new PImpl::Worker(pimpl_->continue_, pimpl_->queue_, workerName); + pimpl_->workers_[i] = new PImpl::Worker(pimpl_->continue_, pimpl_->queue_, workerName, pimpl_->availableWorkers_); } }
--- a/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h Mon Jul 14 16:13:22 2025 +0200 @@ -30,6 +30,8 @@ namespace Orthanc { + class MetricsRegistry; + class RunnableWorkersPool : public boost::noncopyable { private: @@ -39,7 +41,7 @@ void Stop(); public: - explicit RunnableWorkersPool(size_t countWorkers, const std::string& name); + explicit RunnableWorkersPool(size_t countWorkers, const std::string& name, MetricsRegistry& metricsRegistry, const char* availableWorkersMetricsName); ~RunnableWorkersPool();
--- a/OrthancFramework/Sources/RestApi/RestApi.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/RestApi/RestApi.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -775,7 +775,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) { return false; } @@ -790,7 +791,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) + size_t bodySize, + const std::string& authenticationPayload) { RestApiOutput wrappedOutput(output, method);
--- a/OrthancFramework/Sources/RestApi/RestApi.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/RestApi/RestApi.h Mon Jul 14 16:13:22 2025 +0200 @@ -46,7 +46,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE; + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; virtual bool Handle(HttpOutput& output, RequestOrigin origin, @@ -57,7 +58,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) ORTHANC_OVERRIDE; + size_t bodySize, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; void Register(const std::string& path, RestApiGetCall::Handler handler);
--- a/OrthancFramework/Sources/Toolbox.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/Toolbox.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -37,8 +37,10 @@ # error Cannot access the version of JsonCpp #endif -#if !defined(ORTHANC_ENABLE_ICU) +#if (ORTHANC_ENABLE_LOCALE == 1) && (BOOST_LOCALE_WITH_ICU == 1) # define ORTHANC_ENABLE_ICU 1 +#else +# define ORTHANC_ENABLE_ICU 0 #endif @@ -148,21 +150,22 @@ } -#if defined(ORTHANC_STATIC_ICU) - -# if (ORTHANC_STATIC_ICU == 1) && (ORTHANC_ENABLE_ICU == 1) +#if ORTHANC_ENABLE_ICU == 1 + +# if ORTHANC_STATIC_ICU == 1 # if !defined(ORTHANC_FRAMEWORK_INCLUDE_RESOURCES) || (ORTHANC_FRAMEWORK_INCLUDE_RESOURCES == 1) # include <OrthancFrameworkResources.h> # endif +# include "Compression/GzipCompressor.h" # endif -# if (ORTHANC_STATIC_ICU == 1 && ORTHANC_ENABLE_LOCALE == 1) -# include <unicode/udata.h> -# include <unicode/uloc.h> -# include "Compression/GzipCompressor.h" +# include <unicode/udata.h> +# include <unicode/uloc.h> +# include <unicode/uclean.h> static std::string globalIcuData_; +# if ORTHANC_STATIC_ICU == 1 extern "C" { // This is dummy content for the "icudt58_dat" (resp. "icudt63_dat") @@ -170,15 +173,18 @@ // (resp. "icudt63l_dat.c") file that contains a huge C array. In // Orthanc, this array is compressed using gzip and attached as a // resource, then uncompressed during the launch of Orthanc by - // static function "InitializeIcu()". + // static function "InitializeIcu()". WARNING: Do NOT do this if + // dynamically linking against libicu! struct { double bogus; uint8_t *bytes; } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; } - -# if defined(__LSB_VERSION__) +# endif + +# if defined(__LSB_VERSION__) + extern "C" { /** @@ -193,9 +199,9 @@ **/ char *tzname[2] = { (char *) "GMT", (char *) "GMT" }; } -# endif # endif + #endif @@ -698,7 +704,7 @@ return "GB18030"; case Encoding_Thai: -#if BOOST_LOCALE_WITH_ICU == 1 +#if ORTHANC_ENABLE_ICU == 1 return "tis620.2533"; #else return "TIS620.2533-0"; @@ -724,7 +730,8 @@ // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2 std::string Toolbox::ConvertToUtf8(const std::string& source, Encoding sourceEncoding, - bool hasCodeExtensions) + bool hasCodeExtensions, + bool skipBackslashes) { #if ORTHANC_STATIC_ICU == 1 # if ORTHANC_ENABLE_ICU == 0 @@ -760,7 +767,26 @@ else { const char* encoding = GetBoostLocaleEncoding(sourceEncoding); - s = boost::locale::conv::to_utf<char>(source, encoding, boost::locale::conv::skip); + + if (skipBackslashes) + { + /** + * This is to deal with the fact that in Japanese coding + * (ISO_IR 13), backslashes will be converted to the Yen + * character. + **/ + std::vector<std::string> tokens; + TokenizeString(tokens, source, '\\'); + for (size_t i = 0; i < tokens.size(); i++) + { + tokens[i] = boost::locale::conv::to_utf<char>(tokens[i], encoding, boost::locale::conv::skip); + } + JoinStrings(s, tokens, "\\"); + } + else + { + s = boost::locale::conv::to_utf<char>(source, encoding, boost::locale::conv::skip); + } } if (hasCodeExtensions) @@ -830,6 +856,50 @@ #endif +#if ORTHANC_ENABLE_LOCALE == 1 + std::string Toolbox::ConvertDicomStringToUtf8(const std::string& source, + Encoding sourceEncoding, + bool hasCodeExtensions, + ValueRepresentation vr) + { + /** + * This method was added in Orthanc 1.12.9, as a consequence of: + * https://discourse.orthanc-server.org/t/issue-with-special-characters-when-scans-where-uploaded-with-specificcharacterset-dicom-tag-value-as-iso-ir-13/5962 + * + * From the DICOM standard: "Two character codes of the + * single-byte character sets invoked in the GL area of the code + * table, 02/00 and 05/12, have special significance in the DICOM + * Standard. The character SPACE, represented by bit combination + * 02/00, shall be used for the padding of Data Element Values + * that are character strings. The Graphic Character represented + * by the bit combination 05/12, "\" (BACKSLASH) (reverse solidus) + * in the repertoire ISO-IR 6, shall only be used in character + * strings with Value Representations of UT, ST and LT (see + * Section 6.2). Otherwise the character code 05/12 is used as a + * separator for multi-valued Data Elements (see Section + * 6.4). [...] When the Value of Specific Character Set + * (0008,0005) is either "ISO_IR 13" or "ISO 2022 IR 13", the + * graphic character represented by the bit combination 05/12 is a + * "Â¥" (YEN SIGN) in the character set of ISO-IR 14." + * https://www.dicomstandard.org/standards/view/data-structures-and-encoding + * + * This description implies that if "sourceEncoding" (which is + * derived from the value of the DICOM Specific Character Set) + * corresponds "ISO_IR 13" or "ISO 2022 IR 13", AND if the value + * representation is *not* UT, ST, or LT, then backslashes should + * be ignored during the conversion to UTF-8. + **/ + + const bool skipBackslashes = (sourceEncoding == Encoding_Japanese && + vr != ValueRepresentation_UnlimitedText && // UT + vr != ValueRepresentation_ShortText && // ST + vr != ValueRepresentation_LongText); // LT + + return ConvertToUtf8(source, sourceEncoding, hasCodeExtensions, skipBackslashes); + } +#endif + + static bool IsAsciiCharacter(uint8_t c) { return (c != 0 && @@ -1482,10 +1552,25 @@ c == '-' || c == '_' || c == '.' || - c == '~' || - c == '/'); + c == '~'); } + // in this version, each path token is uri encoded separately and then all parts are joined with "/" + void Toolbox::UriEncode(std::string& target, + const std::vector<std::string>& pathTokens) + { + std::vector<std::string> uriEncodedPathTokens; + for (std::vector<std::string>::const_iterator it = pathTokens.begin(); it != pathTokens.end(); ++it) + { + std::string encodedPathToken; + Toolbox::UriEncode(encodedPathToken, *it); + uriEncodedPathTokens.push_back(encodedPathToken); + } + + Toolbox::JoinStrings(target, uriEncodedPathTokens, "/"); + } + + void Toolbox::UriEncode(std::string& target, const std::string& source) { @@ -1772,15 +1857,24 @@ // TODO - The data table must be swapped (uint16_t) throw OrthancException(ErrorCode_NotImplemented); } - - // "First-use of ICU from a single thread before the - // multi-threaded use of ICU begins", to make sure everything is - // properly initialized (should not be mandatory in our - // case). We let boost handle calls to "u_init()" and "u_cleanup()". - // http://userguide.icu-project.org/design#TOC-ICU-Initialization-and-Termination - uloc_getDefault(); } #endif + +#if (ORTHANC_ENABLE_ICU == 1) + UErrorCode status = U_ZERO_ERROR; + u_init(&status); + + if (U_FAILURE(status)) + { + throw OrthancException(ErrorCode_InternalError, "Cannot initialize ICU: " + std::string(u_errorName(status))); + } + + // "First-use of ICU from a single thread before the + // multi-threaded use of ICU begins", to make sure everything is + // properly initialized (should not be mandatory in our case). + // http://userguide.icu-project.org/design#TOC-ICU-Initialization-and-Termination + uloc_getDefault(); +#endif } void Toolbox::InitializeGlobalLocale(const char* locale) @@ -1850,6 +1944,10 @@ void Toolbox::FinalizeGlobalLocale() { globalLocale_.reset(); + +#if (ORTHANC_ENABLE_ICU == 1) + u_cleanup(); +#endif }
--- a/OrthancFramework/Sources/Toolbox.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/Sources/Toolbox.h Mon Jul 14 16:13:22 2025 +0200 @@ -186,7 +186,13 @@ #if ORTHANC_ENABLE_LOCALE == 1 static std::string ConvertToUtf8(const std::string& source, Encoding sourceEncoding, - bool hasCodeExtensions); + bool hasCodeExtensions, + bool skipBackslashes /* was always "false" in Orthanc <= 1.12.8 */); + + static std::string ConvertDicomStringToUtf8(const std::string& source, + Encoding sourceEncoding, + bool hasCodeExtensions, + ValueRepresentation vr); static std::string ConvertFromUtf8(const std::string& source, Encoding targetEncoding); @@ -320,6 +326,9 @@ static void UriEncode(std::string& target, const std::string& source); + static void UriEncode(std::string& target, + const std::vector<std::string>& pathTokens); + static std::string GetJsonStringField(const ::Json::Value& json, const std::string& key, const std::string& defaultValue);
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -49,6 +49,7 @@ #endif #include <ctype.h> +#include <boost/thread.hpp> using namespace Orthanc; @@ -499,7 +500,7 @@ ASSERT_EQ("&abc", Toolbox::ConvertToAscii(s)); // Open in Emacs, then save with UTF-8 encoding, then "hexdump -C" - std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1, false); + std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1, false, false); ASSERT_EQ(15u, utf8.size()); ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[0])); ASSERT_EQ(0xa0, static_cast<unsigned char>(utf8[1])); @@ -526,8 +527,8 @@ std::string s((char*) &latin1[0], sizeof(latin1) / sizeof(char)); - ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1, false), Encoding_Latin1)); - ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8, false)); + ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1, false, false), Encoding_Latin1)); + ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8, false, false)); } @@ -1278,7 +1279,7 @@ Toolbox::UriEncode(s, t); ASSERT_EQ(t, s); - Toolbox::UriEncode(s, "!#$&'()*+,/:;=?@[]"); ASSERT_EQ("%21%23%24%26%27%28%29%2A%2B%2C/%3A%3B%3D%3F%40%5B%5D", s); + Toolbox::UriEncode(s, "!#$&'()*+,/:;=?@[]"); ASSERT_EQ("%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D", s); Toolbox::UriEncode(s, "%"); ASSERT_EQ("%25", s); // Encode characters from UTF-8. This is the test string from the @@ -1425,6 +1426,25 @@ #if ORTHANC_SANDBOXED != 1 + +void GetValuesDico(std::map<std::string, std::string>& values, MetricsRegistry& m) +{ + values.clear(); + + std::string s; + m.ExportPrometheusText(s); + + std::vector<std::string> t; + Toolbox::TokenizeString(t, s, '\n'); + + for (size_t i = 0; i < t.size() - 1; i++) + { + std::vector<std::string> v; + Toolbox::TokenizeString(v, t[i], ' '); + values[v[0]] = v[1]; + } +} + TEST(MetricsRegistry, Basic) { { @@ -1572,6 +1592,63 @@ ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("c")); ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c")); } + + { + std::map<std::string, std::string> values; + + MetricsRegistry mr; + + { + MetricsRegistry::SharedMetrics max10(mr, "shared_max10", MetricsUpdatePolicy_MaxOver10Seconds); + + { + MetricsRegistry::ActiveCounter c1(max10); + MetricsRegistry::ActiveCounter c2(max10); + GetValuesDico(values, mr); + ASSERT_EQ("2", values["shared_max10"]); + } + + GetValuesDico(values, mr); + ASSERT_EQ("2", values["shared_max10"]); + + // { // Uncomment to test max values going back to latest values after expiration of the 10 seconds period + // boost::this_thread::sleep(boost::posix_time::milliseconds(12000)); + + // GetValuesDico(values, mr); + // ASSERT_EQ("0", values["shared_max10"]); + // } + } + + { + MetricsRegistry::SharedMetrics min10(mr, "shared_min10", MetricsUpdatePolicy_MinOver10Seconds); + min10.SetInitialValue(10); + + GetValuesDico(values, mr); + ASSERT_EQ("10", values["shared_min10"]); + + { + MetricsRegistry::AvailableResourcesDecounter c1(min10); + MetricsRegistry::AvailableResourcesDecounter c2(min10); + GetValuesDico(values, mr); + ASSERT_EQ("8", values["shared_min10"]); + } + + GetValuesDico(values, mr); + ASSERT_EQ("8", values["shared_min10"]); + + // { + // // Uncomment to test min values going back to latest values after expiration of the 10 seconds period + // boost::this_thread::sleep(boost::posix_time::milliseconds(12000)); + + // GetValuesDico(values, mr); + // ASSERT_EQ("10", values["shared_min10"]); + // } + + min10.SetInitialValue(5); + GetValuesDico(values, mr); + ASSERT_EQ("5", values["shared_min10"]); + } + } } #endif
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -264,7 +264,7 @@ { std::string source(testEncodingsEncoded[i]); std::string expected(testEncodingsExpected[i]); - std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false); + std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false, false); //std::cout << EnumerationToString(testEncodings[i]) << std::endl; EXPECT_EQ(expected, s); } @@ -334,7 +334,7 @@ ParsedDicomFile f(true); f.SetEncoding(testEncodings[i]); - std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false); + std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false, false); f.Insert(DICOM_TAG_PATIENT_NAME, s, false, ""); f.SaveToMemoryBuffer(dicom); } @@ -571,7 +571,7 @@ ASSERT_FALSE(hasCodeExtensions); } - Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false); + Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false, false); f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent, ""); Json::Value v; @@ -1172,7 +1172,7 @@ // Sanity check to test the proper behavior of "EncodingTests.py" std::string encoded = Toolbox::ConvertFromUtf8(testEncodingsExpected[i], testEncodings[i]); ASSERT_STREQ(testEncodingsEncoded[i], encoded.c_str()); - std::string decoded = Toolbox::ConvertToUtf8(encoded, testEncodings[i], false); + std::string decoded = Toolbox::ConvertToUtf8(encoded, testEncodings[i], false, false); ASSERT_STREQ(testEncodingsExpected[i], decoded.c_str()); if (testEncodings[i] != Encoding_Chinese) @@ -1181,7 +1181,7 @@ // test against Chinese, it is normal that it does not correspond to UTF8 const std::string tmp = Toolbox::ConvertToUtf8( - Toolbox::ConvertFromUtf8(utf8, testEncodings[i]), testEncodings[i], false); + Toolbox::ConvertFromUtf8(utf8, testEncodings[i]), testEncodings[i], false, false); ASSERT_STREQ(testEncodingsExpected[i], tmp.c_str()); } }
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -137,6 +137,10 @@ { return false; } + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + return false; + } };
--- a/OrthancFramework/UnitTestsSources/RestApiTests.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -38,6 +38,7 @@ #include "../Sources/OrthancException.h" #include "../Sources/RestApi/RestApiHierarchy.h" #include "../Sources/WebServiceParameters.h" +#include "../Sources/MetricsRegistry.h" #include <ctype.h> #include <boost/lexical_cast.hpp> @@ -1289,7 +1290,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) ORTHANC_OVERRIDE { return false; } @@ -1303,7 +1305,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) ORTHANC_OVERRIDE + size_t bodySize, + const std::string& authenticationPayload) ORTHANC_OVERRIDE { printf("received %d\n", static_cast<int>(bodySize)); @@ -1330,9 +1333,9 @@ TEST(HttpClient, DISABLED_Issue156_Slow) { // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=156 - + MetricsRegistry dummyRegistry; TotoServer handler; - HttpServer server; + HttpServer server(dummyRegistry); server.SetPortNumber(5000); server.Register(handler); server.Start(); @@ -1359,8 +1362,9 @@ TEST(HttpClient, DISABLED_Issue156_Crash) { + MetricsRegistry dummyRegistry; TotoServer handler; - HttpServer server; + HttpServer server(dummyRegistry); server.SetPortNumber(5000); server.Register(handler); server.Start(); @@ -1384,3 +1388,14 @@ server.Stop(); } #endif + + +TEST(HttpServer, GetRelativePathToRoot) +{ + ASSERT_THROW(HttpServer::GetRelativePathToRoot(""), OrthancException); + ASSERT_EQ("./", HttpServer::GetRelativePathToRoot("/")); + ASSERT_EQ("./", HttpServer::GetRelativePathToRoot("/system")); + ASSERT_EQ("../", HttpServer::GetRelativePathToRoot("/system/")); + ASSERT_EQ("./../../", HttpServer::GetRelativePathToRoot("/a/b/system")); + ASSERT_EQ("../../../", HttpServer::GetRelativePathToRoot("/a/b/system/")); +}
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -407,4 +407,32 @@ ASSERT_EQ("8.59Gbps", Toolbox::GetHumanTransferSpeed(false, 1024*1024*1024, 1000000000)); ASSERT_EQ("1.00GB in 1.00s = 8.59Gbps", Toolbox::GetHumanTransferSpeed(true, 1024*1024*1024, 1000000000)); ASSERT_EQ("976.56KB in 1.00s = 8.00Mbps", Toolbox::GetHumanTransferSpeed(true, 1000*1000, 1000000000)); -} \ No newline at end of file +} + +TEST(Toolbox, DISABLED_JapaneseBackslashes) +{ + std::string s = Orthanc::Toolbox::ConvertToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, false); + ASSERT_EQ("ORIGINAL\302\245PRIMARY", s); // NB: The Yen symbol is encoded as 0xC2 0xA5 in UTF-8 + + s = Orthanc::Toolbox::ConvertToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, true); + ASSERT_EQ("ORIGINAL\\PRIMARY", s); + + s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, ValueRepresentation_PersonName); + ASSERT_EQ("ORIGINAL\\PRIMARY", s); + + // Backslashes should only be interpreted as the Yen symbol if VR is ST, LT, or UL + s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, ValueRepresentation_ShortText); + ASSERT_EQ("ORIGINAL\302\245PRIMARY", s); + + s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, ValueRepresentation_LongText); + ASSERT_EQ("ORIGINAL\302\245PRIMARY", s); + + s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, ValueRepresentation_UnlimitedText); + ASSERT_EQ("ORIGINAL\302\245PRIMARY", s); + + s = Orthanc::Toolbox::ConvertToUtf8("ORIGINAL\\PRIMARY", Encoding_Latin1, false, false); + ASSERT_EQ("ORIGINAL\\PRIMARY", s); + + s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Latin1, false, ValueRepresentation_ShortText); + ASSERT_EQ("ORIGINAL\\PRIMARY", s); +}
--- a/OrthancServer/CMakeLists.txt Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/CMakeLists.txt Mon Jul 14 16:13:22 2025 +0200 @@ -63,6 +63,7 @@ SET(BUILD_HOUSEKEEPER ON CACHE BOOL "Whether to build the Housekeeper plugin") SET(BUILD_DELAYED_DELETION ON CACHE BOOL "Whether to build the DelayedDeletion plugin") SET(BUILD_MULTITENANT_DICOM ON CACHE BOOL "Whether to build the MultitenantDicom plugin") +SET(BUILD_UNIT_TESTS ON CACHE BOOL "Whether to build the unit tests (new in Orthanc 1.12.9)") SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins") SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") @@ -76,6 +77,10 @@ set(ENABLE_PROTOBUF_COMPILER ON) endif() +if (NOT BUILD_UNIT_TESTS) + set(ENABLE_GOOGLE_TEST OFF) +endif() + include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake) include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) @@ -162,37 +167,39 @@ ) -set(ORTHANC_FRAMEWORK_UNIT_TESTS - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/DicomMapTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FileStorageTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FrameworkTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ImageTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/JobsTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/LoggingTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/LuaTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/RestApiTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/SQLiteTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/StreamTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ToolboxTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ZipTests.cpp - ) +if (BUILD_UNIT_TESTS) + set(ORTHANC_FRAMEWORK_UNIT_TESTS + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/DicomMapTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FileStorageTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FrameworkTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ImageTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/JobsTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/LoggingTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/LuaTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/RestApiTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/SQLiteTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/StreamTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ToolboxTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ZipTests.cpp + ) -set(ORTHANC_SERVER_UNIT_TESTS - ${CMAKE_SOURCE_DIR}/UnitTestsSources/DatabaseLookupTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/LuaServerTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/UnitTestsMain.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/VersionsTests.cpp - ) + set(ORTHANC_SERVER_UNIT_TESTS + ${CMAKE_SOURCE_DIR}/UnitTestsSources/DatabaseLookupTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/LuaServerTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/UnitTestsMain.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/VersionsTests.cpp + ) +endif() if (ENABLE_PLUGINS) @@ -211,9 +218,11 @@ ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsManager.cpp ) - list(APPEND ORTHANC_SERVER_UNIT_TESTS - ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp - ) + if (BUILD_UNIT_TESTS) + list(APPEND ORTHANC_SERVER_UNIT_TESTS + ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp + ) + endif() endif() @@ -334,16 +343,19 @@ endif() -if (UNIT_TESTS_WITH_HTTP_CONNEXIONS) - add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1) +if (BUILD_UNIT_TESTS) + add_definitions(-DORTHANC_BUILD_UNIT_TESTS=1) + if (UNIT_TESTS_WITH_HTTP_CONNEXIONS) + add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1) + else() + add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=0) + endif() else() - add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=0) + add_definitions(-DORTHANC_BUILD_UNIT_TESTS=1) endif() add_definitions( - -DORTHANC_BUILD_UNIT_TESTS=1 - # Macros for the plugins -DHAS_ORTHANC_EXCEPTION=0 ) @@ -371,9 +383,11 @@ "PrecompiledHeadersServer.h" "${CMAKE_SOURCE_DIR}/Sources/PrecompiledHeadersServer.cpp" ORTHANC_SERVER_SOURCES ORTHANC_SERVER_PCH) - ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( - "PrecompiledHeadersUnitTests.h" "${CMAKE_SOURCE_DIR}/UnitTestsSources/PrecompiledHeadersUnitTests.cpp" - ORTHANC_SERVER_UNIT_TESTS ORTHANC_UNIT_TESTS_PCH) + if (BUILD_UNIT_TESTS) + ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( + "PrecompiledHeadersUnitTests.h" "${CMAKE_SOURCE_DIR}/UnitTestsSources/PrecompiledHeadersUnitTests.cpp" + ORTHANC_SERVER_UNIT_TESTS ORTHANC_UNIT_TESTS_PCH) + endif() endif() @@ -477,22 +491,24 @@ ## Build the unit tests ##################################################################### -add_executable(UnitTests - ${GOOGLE_TEST_SOURCES} - ${ORTHANC_UNIT_TESTS_PCH} - ${ORTHANC_FRAMEWORK_UNIT_TESTS} - ${ORTHANC_SERVER_UNIT_TESTS} - ${BOOST_EXTENDED_SOURCES} - ) +if (BUILD_UNIT_TESTS) + add_executable(UnitTests + ${GOOGLE_TEST_SOURCES} + ${ORTHANC_UNIT_TESTS_PCH} + ${ORTHANC_FRAMEWORK_UNIT_TESTS} + ${ORTHANC_SERVER_UNIT_TESTS} + ${BOOST_EXTENDED_SOURCES} + ) -DefineSourceBasenameForTarget(UnitTests) + DefineSourceBasenameForTarget(UnitTests) -target_link_libraries(UnitTests - ServerLibrary - CoreLibrary - ${DCMTK_LIBRARIES} - ${GOOGLE_TEST_LIBRARIES} - ) + target_link_libraries(UnitTests + ServerLibrary + CoreLibrary + ${DCMTK_LIBRARIES} + ${GOOGLE_TEST_LIBRARIES} + ) +endif() #####################################################################
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -1511,6 +1511,16 @@ { throw OrthancException(ErrorCode_NotImplemented); // Not supported } + + virtual void RecordAuditLog(const std::string& userId, + ResourceType resourceType, + const std::string& resourceId, + const std::string& action, + const void* logData, + size_t logDataSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } };
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -1123,6 +1123,17 @@ { throw OrthancException(ErrorCode_NotImplemented); // Not supported } + + virtual void RecordAuditLog(const std::string& userId, + ResourceType resourceType, + const std::string& resourceId, + const std::string& action, + const void* logData, + size_t logDataSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + };
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -2051,6 +2051,39 @@ throw OrthancException(ErrorCode_InternalError); } } + + virtual void RecordAuditLog(const std::string& userId, + ResourceType resourceType, + const std::string& resourceId, + const std::string& action, + const void* logData, + size_t logDataSize) ORTHANC_OVERRIDE + { + // In protobuf, bytes "may contain any arbitrary sequence of bytes no longer than 2^32" + // https://protobuf.dev/programming-guides/proto3/ + if (logDataSize > std::numeric_limits<uint32_t>::max()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + if (database_.GetDatabaseCapabilities().HasAuditLogsSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_record_audit_log()->set_user_id(userId); + request.mutable_record_audit_log()->set_resource_type(Convert(resourceType)); + request.mutable_record_audit_log()->set_resource_id(resourceId); + request.mutable_record_audit_log()->set_action(action); + request.mutable_record_audit_log()->set_log_data(logData, logDataSize); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_ENQUEUE_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + }; @@ -2144,6 +2177,7 @@ dbCapabilities_.SetKeyValueStoresSupport(systemInfo.supports_key_value_stores()); dbCapabilities_.SetQueuesSupport(systemInfo.supports_queues()); dbCapabilities_.SetAttachmentCustomDataSupport(systemInfo.has_attachment_custom_data()); + dbCapabilities_.SetAuditLogsSupport(systemInfo.supports_audit_logs()); } open_ = true;
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -1739,6 +1739,7 @@ WebDavCollections webDavCollections_; // New in Orthanc 1.10.1 std::unique_ptr<StorageAreaFactory> storageArea_; std::set<std::string> authorizationTokens_; + OrthancPluginHttpAuthentication httpAuthentication_; // New in Orthanc 1.12.9 boost::recursive_mutex restCallbackInvokationMutex_; boost::shared_mutex restCallbackRegistrationMutex_; // New in Orthanc 1.9.0 @@ -1770,6 +1771,7 @@ findCallback_(NULL), worklistCallback_(NULL), receivedInstanceCallback_(NULL), + httpAuthentication_(NULL), argc_(1), argv_(NULL), databaseServerIdentifier_(databaseServerIdentifier), @@ -2280,6 +2282,8 @@ sizeof(int32_t) != sizeof(OrthancPluginLogCategory) || sizeof(int32_t) != sizeof(OrthancPluginStoreStatus) || sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin) || + sizeof(int32_t) != sizeof(OrthancPluginStableStatus) || + sizeof(int32_t) != sizeof(OrthancPluginHttpAuthenticationStatus) || // From OrthancCDatabasePlugin.h sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || @@ -2351,6 +2355,11 @@ std::vector<const char*>& values, const HttpToolbox::Arguments& arguments) { + if (static_cast<uint32_t>(arguments.size()) != arguments.size()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + keys.resize(arguments.size()); values.resize(arguments.size()); @@ -2362,6 +2371,8 @@ values[pos] = it->second.c_str(); pos++; } + + assert(pos == arguments.size()); } @@ -2369,6 +2380,11 @@ std::vector<const char*>& values, const HttpToolbox::GetArguments& arguments) { + if (static_cast<uint32_t>(arguments.size()) != arguments.size()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + keys.resize(arguments.size()); values.resize(arguments.size()); @@ -2455,7 +2471,8 @@ public: HttpRequestConverter(const RestCallbackMatcher& matcher, HttpMethod method, - const HttpToolbox::Arguments& headers) + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) { memset(&converted_, 0, sizeof(OrthancPluginHttpRequest)); @@ -2498,6 +2515,14 @@ converted_.headersKeys = &headersKeys_[0]; converted_.headersValues = &headersValues_[0]; } + + converted_.authenticationPayload = authenticationPayload.empty() ? NULL : authenticationPayload.c_str(); + converted_.authenticationPayloadSize = static_cast<uint32_t>(authenticationPayload.size()); + + if (converted_.authenticationPayloadSize != authenticationPayload.size()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } } void SetGetArguments(const HttpToolbox::GetArguments& getArguments) @@ -2569,7 +2594,8 @@ HttpMethod method, const UriComponents& uri, const HttpToolbox::Arguments& headers, - const HttpToolbox::GetArguments& getArguments) + const HttpToolbox::GetArguments& getArguments, + const std::string& authenticationPayload) { RestCallbackMatcher matcher(uri); @@ -2618,7 +2644,7 @@ } else { - HttpRequestConverter converter(matcher, method, headers); + HttpRequestConverter converter(matcher, method, headers, authenticationPayload); converter.SetGetArguments(getArguments); PImpl::PluginHttpOutput pluginOutput(output); @@ -2644,7 +2670,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) + size_t bodySize, + const std::string& authenticationPayload) { RestCallbackMatcher matcher(uri); @@ -2665,12 +2692,12 @@ if (callback == NULL) { // Callback not found, try to find a chunked callback - return HandleChunkedGetDelete(output, method, uri, headers, getArguments); + return HandleChunkedGetDelete(output, method, uri, headers, getArguments, authenticationPayload); } CLOG(INFO, PLUGINS) << "Delegating HTTP request to plugin for URI: " << matcher.GetFlatUri(); - HttpRequestConverter converter(matcher, method, headers); + HttpRequestConverter converter(matcher, method, headers, authenticationPayload); converter.SetGetArguments(getArguments); converter.GetRequest().body = bodyData; converter.GetRequest().bodySize = bodySize; @@ -3082,6 +3109,26 @@ } + void OrthancPlugins::RegisterHttpAuthentication(const void* parameters) + { + const _OrthancPluginHttpAuthentication& p = + *reinterpret_cast<const _OrthancPluginHttpAuthentication*>(parameters); + + boost::unique_lock<boost::shared_mutex> lock(pimpl_->incomingHttpRequestFilterMutex_); + + if (pimpl_->httpAuthentication_ == NULL) + { + CLOG(INFO, PLUGINS) << "Plugin has registered a callback to authenticate incoming HTTP requests"; + pimpl_->httpAuthentication_ = p.callback; + } + else + { + throw OrthancException(ErrorCode_Plugin, + "Only one plugin can register a callback to authenticate incoming HTTP requests"); + } + } + + void OrthancPlugins::AnswerBuffer(const void* parameters) { const _OrthancPluginAnswerBuffer& p = @@ -3385,7 +3432,8 @@ std::map<std::string, std::string> httpHeaders; - ThrowOnHttpError(IHttpHandler::SimpleDelete(NULL, *handler, RequestOrigin_Plugins, uri, httpHeaders)); + std::string bodyIgnored; + ThrowOnHttpError(IHttpHandler::SimpleDelete(bodyIgnored, NULL, *handler, RequestOrigin_Plugins, uri, httpHeaders)); } @@ -4174,7 +4222,7 @@ case OrthancPluginHttpMethod_Delete: status = IHttpHandler::SimpleDelete( - &answerHeaders, *handler, RequestOrigin_Plugins, p.uri, headers); + answerBody, &answerHeaders, *handler, RequestOrigin_Plugins, p.uri, headers); break; default: @@ -4190,8 +4238,7 @@ } PluginMemoryBuffer32 tmpBody; - if (p.method != OrthancPluginHttpMethod_Delete && - p.answerBody != NULL) + if (p.answerBody != NULL) { tmpBody.Assign(answerBody); } @@ -4721,6 +4768,18 @@ } } + void OrthancPlugins::ApplyRecordAuditLog(const _OrthancPluginRecordAuditLog& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + if (!lock.GetContext().GetIndex().HasAuditLogsSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support audit logs"); + } + + lock.GetContext().GetIndex().RecordAuditLog(parameters.userId, Plugins::Convert(parameters.resourceType), parameters.resourceId, parameters.action, parameters.logData, parameters.logDataSize); + } + void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params) { std::unique_ptr<IDicomInstance> target; @@ -5851,6 +5910,13 @@ return true; } + case _OrthancPluginService_RecordAuditLog: + { + const _OrthancPluginRecordAuditLog& p = *reinterpret_cast<const _OrthancPluginRecordAuditLog*>(parameters); + ApplyRecordAuditLog(p); + return true; + } + default: return false; } @@ -5932,6 +5998,10 @@ RegisterStorageCommitmentScpCallback(parameters); return true; + case _OrthancPluginService_RegisterHttpAuthentication: + RegisterHttpAuthentication(parameters); + return true; + case _OrthancPluginService_RegisterStorageArea: case _OrthancPluginService_RegisterStorageArea2: case _OrthancPluginService_RegisterStorageArea3: @@ -6634,7 +6704,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) { if (method != HttpMethod_Post && method != HttpMethod_Put) @@ -6690,7 +6761,7 @@ { CLOG(INFO, PLUGINS) << "Delegating chunked HTTP request to plugin for URI: " << matcher.GetFlatUri(); - HttpRequestConverter converter(matcher, method, headers); + HttpRequestConverter converter(matcher, method, headers, authenticationPayload); converter.GetRequest().body = NULL; converter.GetRequest().bodySize = 0; @@ -6816,4 +6887,64 @@ pimpl_->webDavCollections_.pop_front(); } } + + + IIncomingHttpRequestFilter::AuthenticationStatus OrthancPlugins::CheckAuthentication( + std::string& customPayload, + std::string& redirection, + const char* uri, + const char* ip, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::GetArguments& getArguments) const + { + boost::shared_lock<boost::shared_mutex> lock(pimpl_->incomingHttpRequestFilterMutex_); + + if (pimpl_->httpAuthentication_ == NULL) + { + return IIncomingHttpRequestFilter::AuthenticationStatus_BuiltIn; // Use the default authentication of Orthanc + } + else + { + std::vector<const char*> headersKeys, headersValues; + ArgumentsToPlugin(headersKeys, headersValues, httpHeaders); + + std::vector<const char*> getKeys, getValues; + ArgumentsToPlugin(getKeys, getValues, getArguments); + + OrthancPluginHttpAuthenticationStatus status = OrthancPluginHttpAuthenticationStatus_Unauthorized; + PluginMemoryBuffer32 payloadBuffer; + PluginMemoryBuffer32 redirectionBuffer; + OrthancPluginErrorCode code = pimpl_->httpAuthentication_( + &status, payloadBuffer.GetObject(), redirectionBuffer.GetObject(), uri, ip, + headersKeys.size(), headersKeys.empty() ? NULL : &headersKeys[0], headersValues.empty() ? NULL : &headersValues[0], + getKeys.size(), getKeys.empty() ? NULL : &getKeys[0], getValues.empty() ? NULL : &getValues[0]); + + if (code != OrthancPluginErrorCode_Success) + { + throw OrthancException(static_cast<ErrorCode>(code)); + } + else + { + switch (status) + { + case OrthancPluginHttpAuthenticationStatus_Granted: + payloadBuffer.MoveToString(customPayload); + return IIncomingHttpRequestFilter::AuthenticationStatus_Granted; + + case OrthancPluginHttpAuthenticationStatus_Unauthorized: + return IIncomingHttpRequestFilter::AuthenticationStatus_Unauthorized; + + case OrthancPluginHttpAuthenticationStatus_Forbidden: + return IIncomingHttpRequestFilter::AuthenticationStatus_Forbidden; + + case OrthancPluginHttpAuthenticationStatus_Redirect: + redirectionBuffer.MoveToString(redirection); + return IIncomingHttpRequestFilter::AuthenticationStatus_Redirect; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } + } }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Mon Jul 14 16:13:22 2025 +0200 @@ -106,7 +106,8 @@ HttpMethod method, const UriComponents& uri, const HttpToolbox::Arguments& headers, - const HttpToolbox::GetArguments& getArguments); + const HttpToolbox::GetArguments& getArguments, + const std::string& authenticationPayload); void RegisterOnStoredInstanceCallback(const void* parameters); @@ -138,6 +139,8 @@ void RegisterStorageCommitmentScpCallback(const void* parameters); + void RegisterHttpAuthentication(const void* parameters); + void AnswerBuffer(const void* parameters); void Redirect(const void* parameters); @@ -246,6 +249,8 @@ void ApplySetStableStatus(const _OrthancPluginSetStableStatus& parameters); + void ApplyRecordAuditLog(const _OrthancPluginRecordAuditLog& parameters); + void ComputeHash(_OrthancPluginService service, const void* parameters); @@ -289,7 +294,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) ORTHANC_OVERRIDE; + size_t bodySize, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; virtual bool InvokeService(SharedLibrary& plugin, _OrthancPluginService service, @@ -397,7 +403,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE; + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; // New in Orthanc 1.6.0 IStorageCommitmentFactory::ILookupHandler* CreateStorageCommitment( @@ -414,6 +421,14 @@ unsigned int GetMaxDatabaseRetries() const; void RegisterWebDavCollections(HttpServer& target); + + IIncomingHttpRequestFilter::AuthenticationStatus CheckAuthentication( + std::string& customPayload, + std::string& redirection, + const char* uri, + const char* ip, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::GetArguments& getArguments) const; }; }
--- a/OrthancServer/Plugins/Engine/PluginsJob.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsJob.h Mon Jul 14 16:13:22 2025 +0200 @@ -83,6 +83,12 @@ // TODO return false; } + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + return false; + } + }; }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Mon Jul 14 16:13:22 2025 +0200 @@ -31,6 +31,7 @@ * - 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(). + * - Possibly register a callback to authenticate HTTP requests using OrthancPluginRegisterHttpAuthentication(). * -# <tt>void OrthancPluginFinalize()</tt>: * This function is invoked by Orthanc during its shutdown. The plugin * must free all its memory. @@ -418,6 +419,23 @@ **/ const char* const* headersValues; + + /* -------------------------------------------------- + New in version 1.12.9 + -------------------------------------------------- */ + + /** + * @brief If a HTTP authentication callback is registered, the + * content of the custom payload generated by the callback. + **/ + const void* authenticationPayload; + + /** + * @brief The size of the custom authentication payload (0 if no + * authentication callback is registered). + **/ + uint32_t authenticationPayloadSize; + } OrthancPluginHttpRequest; @@ -484,6 +502,7 @@ _OrthancPluginService_DequeueValue = 58, /* New in Orthanc 1.12.8 */ _OrthancPluginService_GetQueueSize = 59, /* New in Orthanc 1.12.8 */ _OrthancPluginService_SetStableStatus = 60, /* New in Orthanc 1.12.9 */ + _OrthancPluginService_RecordAuditLog = 61, /* New in Orthanc 1.12.9 */ /* Registration of callbacks */ _OrthancPluginService_RegisterRestCallback = 1000, @@ -507,6 +526,7 @@ _OrthancPluginService_RegisterReceivedInstanceCallback = 1018, /* New in Orthanc 1.10.0 */ _OrthancPluginService_RegisterWebDavCollection = 1019, /* New in Orthanc 1.10.1 */ _OrthancPluginService_RegisterStorageArea3 = 1020, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_RegisterHttpAuthentication = 1021, /* New in Orthanc 1.12.9 */ /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -1159,6 +1179,7 @@ _OrthancPluginStoreStatus_INTERNAL = 0x7fffffff } OrthancPluginStoreStatus; + /** * The supported modes to remove an element from a queue. **/ @@ -1170,8 +1191,9 @@ _OrthancPluginQueueOrigin_INTERNAL = 0x7fffffff } OrthancPluginQueueOrigin; - /** - * The "stable" status of a resource. + + /** + * The "Stable" status of a resource. **/ typedef enum { @@ -1183,6 +1205,20 @@ /** + * The status related to the authentication of a HTTP request. + **/ + typedef enum + { + OrthancPluginHttpAuthenticationStatus_Granted = 0, /*!< The authentication has been granted */ + OrthancPluginHttpAuthenticationStatus_Unauthorized = 1, /*!< The authentication has failed (401 HTTP status) */ + OrthancPluginHttpAuthenticationStatus_Forbidden = 2, /*!< The authorization has failed (403 HTTP status) */ + OrthancPluginHttpAuthenticationStatus_Redirect = 3, /*!< Redirect to another path (e.g. for login, 307 HTTP status) */ + + _OrthancPluginHttpAuthenticationStatus_INTERNAL = 0x7fffffff + } OrthancPluginHttpAuthenticationStatus; + + + /** * @brief A 32-bit memory buffer allocated by the core system of Orthanc. * * A memory buffer allocated by the core system of Orthanc. When the @@ -2153,7 +2189,9 @@ sizeof(int32_t) != sizeof(OrthancPluginLogLevel) || sizeof(int32_t) != sizeof(OrthancPluginLogCategory) || sizeof(int32_t) != sizeof(OrthancPluginStoreStatus) || - sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin)) + sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin) || + sizeof(int32_t) != sizeof(OrthancPluginStableStatus) || + sizeof(int32_t) != sizeof(OrthancPluginHttpAuthenticationStatus)) { /* Mismatch in the size of the enumerations */ return 0; @@ -10311,13 +10349,18 @@ } _OrthancPluginSetStableStatus; /** - * @brief Change the "Stable" status of a resource. - * Forcing a resource to "Stable" if it is currently "Unstable" will change - * its Stable status AND trigger a new Stable change which will also trigger - * listener callbacks. - * Forcing a resource to "Stable" if it is already "Stable" is a no-op. - * Forcing a resource to "Unstable" will change its Stable status to "Unstable" - * AND reset its stabilization period, no matter of its initial state. + * @brief Change the "Stable" status of a resource. + * + * Forcing a resource to "Stable" if it is currently "Unstable" will + * change its Stable status AND trigger a new Stable change which + * will also trigger listener callbacks. + * + * Forcing a resource to "Stable" if it is already "Stable" is a + * no-op. + * + * Forcing a resource to "Unstable" will change its "Stable" status + * to "Unstable" AND reset its stabilization period, no matter its + * initial state. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param statusHasChanged Wheter the status has changed (1) or not (0) during the execution of this command. @@ -10339,10 +10382,142 @@ return context->InvokeService(context, _OrthancPluginService_SetStableStatus, ¶ms); } + + /** + * @brief Callback to authenticate a HTTP request. + * + * Signature of a callback function that authenticates every incoming HTTP. + * + * @param status The output status of the authentication. + * @param customPayload If status is `OrthancPluginHttpAuthenticationStatus_Granted`, + * a custom payload that will be provided to the HTTP authorization callback. + * @param redirection If status is `OrthancPluginHttpAuthenticationStatus_Redirect`, + * a buffer filled with the path where to redirect the user (typically, a login page). + * The path is relative to the root of the Web server of Orthanc. + * @param uri The URI of interest (without the possible GET arguments). + * @param ip The IP address of the HTTP client. + * @param headersCount The number of HTTP headers. + * @param headersKeys The keys of the HTTP headers (always converted to low-case). + * @param headersValues The values of the HTTP headers. + * @param getCount For a GET request, the number of GET parameters. + * @param getKeys For a GET request, the keys of the GET parameters. + * @param getValues For a GET request, the values of the GET parameters. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginHttpAuthentication) ( + OrthancPluginHttpAuthenticationStatus* status, /* out */ + OrthancPluginMemoryBuffer* customPayload, /* out */ + OrthancPluginMemoryBuffer* redirection, /* out */ + const char* uri, + const char* ip, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues, + uint32_t getCount, + const char* const* getKeys, + const char* const* getValues); + + + typedef struct + { + OrthancPluginHttpAuthentication callback; + } _OrthancPluginHttpAuthentication; + + /** + * @brief Register a callback to handle HTTP authentication (and + * possibly HTTP authorization). + * + * This function installs a callback that is executed for each + * incoming HTTP request to handle HTTP authentication. At most one + * plugin can register such a callback. This gives the opportunity + * to the plugin to validate access tokens (such as a JWT), possibly + * redirecting the user to a login page. The authentication callback + * can generate a custom payload that will be provided to the + * subsequent REST handling callback. + * + * This HTTP authentication callback can notably be used if some + * resource in the REST API must be available for public access, if + * the "RemoteAccessAllowed" configuration option is set to "true". + * + * In addition, the callback can handle HTTP authorization + * simultaneously with HTTP authentication, by reporting the + * "OrthancPluginHttpAuthenticationStatus_Forbidden" status. This + * corresponds to the behavior of callbacks installed using + * OrthancPluginRegisterIncomingHttpRequestFilter2(), but the latter + * callbacks do not provide access to the authentication payload. + * + * If one plugin installs a HTTP authentication callback, the + * built-in HTTP authentication of Orthanc is disabled. This means + * that the "RegisteredUsers" and "AuthenticationEnabled" + * configuration options of Orthanc are totally ignored. In + * addition, tokens generated by + * OrthancPluginGenerateRestApiAuthorizationToken() become + * ineffective. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The HTTP authentication callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterHttpAuthentication( + OrthancPluginContext* context, + OrthancPluginHttpAuthentication callback) + { + _OrthancPluginHttpAuthentication params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterHttpAuthentication, ¶ms); + } + + + typedef struct + { + const char* userId; + OrthancPluginResourceType resourceType; + const char* resourceId; + const char* action; + const void* logData; + uint32_t logDataSize; + } _OrthancPluginRecordAuditLog; + + + /** + * @brief Record an audit log + * + * Record an audit log (provided that a database plugin provides the feature). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param userId A string uniquely identifying the user or entity that is executing the action on the resource. + * @param resourceType The type of the resource this log relates to. + * @param resourceId The resource this log relates to. + * @param action The action that is performed on the resource. + * @param logData A pointer to custom log data. + * @param logDataSize The size of custom log data. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRecordAuditLog( + OrthancPluginContext* context, + const char* userId, + OrthancPluginResourceType resourceType, + const char* resourceId, + const char* action, + const void* logData, + uint32_t logDataSize) + { + _OrthancPluginRecordAuditLog m; + m.userId = userId; + m.resourceType = resourceType; + m.resourceId = resourceId; + m.action = action; + m.logData = logData; + m.logDataSize = logDataSize; + context->InvokeService(context, _OrthancPluginService_RecordAuditLog, &m); + } + + #ifdef __cplusplus } #endif /** @} */ -
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Mon Jul 14 16:13:22 2025 +0200 @@ -175,6 +175,7 @@ bool supports_key_value_stores = 10; // New in Orthanc 1.12.8 bool supports_queues = 11; // New in Orthanc 1.12.8 bool has_attachment_custom_data = 12; // New in Orthanc 1.12.8 + bool supports_audit_logs = 13; // New in Orthanc 1.12.9 } } @@ -339,6 +340,7 @@ OPERATION_GET_QUEUE_SIZE = 59; // New in Orthanc 1.12.8 OPERATION_GET_ATTACHMENT_CUSTOM_DATA = 60; // New in Orthanc 1.12.8 OPERATION_SET_ATTACHMENT_CUSTOM_DATA = 61; // New in Orthanc 1.12.8 + OPERATION_RECORD_AUDIT_LOG = 62; // New in Orthanc 1.12.9 } @@ -1095,6 +1097,20 @@ } } +message RecordAuditLog { + message Request { + string user_id = 1; + ResourceType resource_type = 2; + string resource_id = 3; + string action = 4; + bytes log_data = 5; + } + + message Response { + } +} + + message TransactionRequest { sfixed64 transaction = 1; TransactionOperation operation = 2; @@ -1161,6 +1177,7 @@ GetQueueSize.Request get_queue_size = 159; GetAttachmentCustomData.Request get_attachment_custom_data = 160; SetAttachmentCustomData.Request set_attachment_custom_data = 161; + RecordAuditLog.Request record_audit_log = 162; } message TransactionResponse { @@ -1226,6 +1243,7 @@ GetQueueSize.Response get_queue_size = 159; GetAttachmentCustomData.Response get_attachment_custom_data = 160; SetAttachmentCustomData.Response set_attachment_custom_data = 161; + RecordAuditLog.Response record_audit_log = 162; } enum RequestType {
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -266,6 +266,16 @@ #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void MemoryBuffer::AssignJson(const Json::Value& value) + { + std::string s; + WriteFastJson(s, value); + Assign(s); + } +#endif + + void MemoryBuffer::Assign(OrthancPluginMemoryBuffer& other) { Clear(); @@ -4212,6 +4222,11 @@ { path_ += "?" + getArguments; } + + if (request->bodySize > 0 && request->body != NULL) + { + requestBody_.assign(reinterpret_cast<const char*>(request->body), request->bodySize); + } } #endif
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Mon Jul 14 16:13:22 2025 +0200 @@ -231,6 +231,10 @@ void Assign(const std::string& s); #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void AssignJson(const Json::Value& value); +#endif + // This transfers ownership from "other" to "this" void Assign(OrthancPluginMemoryBuffer& other);
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -27,7 +27,24 @@ * dictionary. **/ -#define ORTHANC_ENABLE_ICU 0 +#if BOOST_LOCALE_WITH_ICU == 1 +# undef BOOST_LOCALE_WITH_ICU +# if ORTHANC_STATIC_ICU == 1 +# include <unicode/udata.h> + +// Define an empty ICU dictionary for static builds +extern "C" +{ + struct + { + double bogus; + uint8_t *bytes; + } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; +} + +# endif +#endif + #include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp" #include "../../../../OrthancFramework/Sources/Enumerations.cpp"
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/OrthancFrameworkDependencies.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/OrthancFrameworkDependencies.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -27,7 +27,24 @@ * dictionary. **/ -#define ORTHANC_ENABLE_ICU 0 +#if BOOST_LOCALE_WITH_ICU == 1 +# undef BOOST_LOCALE_WITH_ICU +# if ORTHANC_STATIC_ICU == 1 +# include <unicode/udata.h> + +// Define an empty ICU dictionary for static builds +extern "C" +{ + struct + { + double bogus; + uint8_t *bytes; + } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; +} + +# endif +#endif + #include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp" #include "../../../../OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp"
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -93,7 +93,7 @@ labelsStoreLevels_.insert(Orthanc::ResourceType_Instance); } - server_.reset(new Orthanc::DicomServer); + server_.reset(new Orthanc::DicomServer(dummyMetricsRegistry_)); { OrthancPlugins::OrthancConfiguration globalConfig;
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h Mon Jul 14 16:13:22 2025 +0200 @@ -27,6 +27,7 @@ #include "PluginEnumerations.h" #include "../../../../OrthancFramework/Sources/DicomNetworking/DicomServer.h" +#include "../../../../OrthancFramework/Sources/MetricsRegistry.h" #include <boost/thread/mutex.hpp> @@ -59,6 +60,7 @@ bool isStrictAet_; DicomFilter filter_; std::unique_ptr<Orthanc::DicomServer> server_; + Orthanc::MetricsRegistry dummyMetricsRegistry_; public: explicit MultitenantDicomServer(const Json::Value& serverConfig);
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -77,6 +77,7 @@ #include "../../../../OrthancFramework/Sources/Images/PngReader.cpp" #include "../../../../OrthancFramework/Sources/Images/PngWriter.cpp" #include "../../../../OrthancFramework/Sources/Logging.cpp" +#include "../../../../OrthancFramework/Sources/MetricsRegistry.cpp" #include "../../../../OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp" #include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp" #include "../../../../OrthancFramework/Sources/OrthancException.cpp"
--- a/OrthancServer/Resources/Configuration.json Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Resources/Configuration.json Mon Jul 14 16:13:22 2025 +0200 @@ -307,10 +307,11 @@ // The list of the registered users. Because Orthanc uses HTTP // Basic Authentication, the passwords are stored as plain text. - "RegisteredUsers" : { - // "alice" : "alicePassword" - }, - + /** + "RegisteredUsers" : { + // "alice" : "alicePassword" + }, + **/ /**
--- a/OrthancServer/Resources/RunCppCheck-2.17.0.sh Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Resources/RunCppCheck-2.17.0.sh Mon Jul 14 16:13:22 2025 +0200 @@ -9,7 +9,7 @@ fi cat <<EOF > /tmp/cppcheck-suppressions.txt -nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321 +nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:322 stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1525 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
--- a/OrthancServer/Resources/RunCppCheck.sh Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Resources/RunCppCheck.sh Mon Jul 14 16:13:22 2025 +0200 @@ -12,7 +12,7 @@ constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp -nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321 +nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:322 stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1525 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74 @@ -36,7 +36,7 @@ assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391 -assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3058 +assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3066 assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:286 assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454 EOF
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Mon Jul 14 16:13:22 2025 +0200 @@ -59,6 +59,7 @@ bool hasAttachmentCustomDataSupport_; bool hasKeyValueStoresSupport_; bool hasQueuesSupport_; + bool hasAuditLogsSupport_; public: Capabilities() : @@ -72,7 +73,8 @@ hasExtendedChanges_(false), hasAttachmentCustomDataSupport_(false), hasKeyValueStoresSupport_(false), - hasQueuesSupport_(false) + hasQueuesSupport_(false), + hasAuditLogsSupport_(false) { } @@ -185,6 +187,17 @@ { return hasQueuesSupport_; } + + void SetAuditLogsSupport(bool value) + { + hasAuditLogsSupport_ = value; + } + + bool HasAuditLogsSupport() const + { + return hasAuditLogsSupport_; + } + }; @@ -469,6 +482,15 @@ // New in Orthanc 1.12.8, for statistics only virtual uint64_t GetQueueSize(const std::string& queueId) = 0; + + // New in Orthanc 1.12.9 + virtual void RecordAuditLog(const std::string& userId, + ResourceType resourceType, + const std::string& resourceId, + const std::string& action, + const void* logData, + size_t logDataSize) = 0; + };
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -2340,6 +2340,17 @@ s.Step(); return s.ColumnInt64(0); } + + virtual void RecordAuditLog(const std::string& userId, + ResourceType resourceType, + const std::string& resourceId, + const std::string& action, + const void* logData, + size_t logDataSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + }; @@ -2587,6 +2598,7 @@ dbCapabilities_.SetKeyValueStoresSupport(true); dbCapabilities_.SetQueuesSupport(true); dbCapabilities_.SetAttachmentCustomDataSupport(true); + dbCapabilities_.SetAuditLogsSupport(false); db_.Open(path); } @@ -2604,6 +2616,7 @@ dbCapabilities_.SetKeyValueStoresSupport(true); dbCapabilities_.SetQueuesSupport(true); dbCapabilities_.SetAttachmentCustomDataSupport(true); + dbCapabilities_.SetAuditLogsSupport(false); db_.OpenInMemory(); }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -3226,6 +3226,12 @@ return db_.GetDatabaseCapabilities().HasQueuesSupport(); } + bool StatelessDatabaseOperations::HasAuditLogsSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasAuditLogsSupport(); + } + void StatelessDatabaseOperations::ExecuteCount(uint64_t& count, const FindRequest& request) { @@ -3755,4 +3761,48 @@ throw OrthancException(ErrorCode_BadSequenceOfCalls); } } + + void StatelessDatabaseOperations::RecordAuditLog(const std::string& userId, + ResourceType resourceType, + const std::string& resourceId, + const std::string& action, + const void* logData, + size_t logDataSize) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& userId_; + ResourceType resourceType_; + const std::string& resourceId_; + const std::string& action_; + const void* logData_; + size_t logDataSize_; + + public: + Operations(const std::string& userId, + ResourceType resourceType, + const std::string& resourceId, + const std::string& action, + const void* logData, + size_t logDataSize) : + userId_(userId), + resourceType_(resourceType), + resourceId_(resourceId), + action_(action), + logData_(logData), + logDataSize_(logDataSize) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.RecordAuditLog(userId_, resourceType_, resourceId_, action_, logData_, logDataSize_); + } + }; + + Operations operations(userId, resourceType, resourceId, action, logData, logDataSize); + Apply(operations); + } + }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Mon Jul 14 16:13:22 2025 +0200 @@ -491,6 +491,17 @@ { return transaction_.SetAttachmentCustomData(attachmentUuid, customData, customDataSize); } + + void RecordAuditLog(const std::string& userId, + ResourceType resourceType, + const std::string& resourceId, + const std::string& action, + const void* logData, + size_t logDataSize) + { + return transaction_.RecordAuditLog(userId, resourceType, resourceId, action, logData, logDataSize); + } + }; @@ -620,7 +631,9 @@ bool HasKeyValueStoresSupport(); bool HasQueuesSupport(); - + + bool HasAuditLogsSupport(); + void GetExportedResources(Json::Value& target, int64_t since, uint32_t limit); @@ -879,5 +892,12 @@ const std::string& GetValue() const; }; + + void RecordAuditLog(const std::string& userId, + ResourceType resourceType, + const std::string& resourceId, + const std::string& action, + const void* logData, + size_t logDataSize); }; }
--- a/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -51,7 +51,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& arguments, const void* /*bodyData*/, - size_t /*bodySize*/) + size_t /*bodySize*/, + const std::string& /*authenticationPayload*/) { if (!Toolbox::IsChildUri(baseUri_, uri)) {
--- a/OrthancServer/Sources/EmbeddedResourceHttpHandler.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.h Mon Jul 14 16:13:22 2025 +0200 @@ -47,7 +47,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) ORTHANC_OVERRIDE { return false; } @@ -61,6 +62,7 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& arguments, const void* /*bodyData*/, - size_t /*bodySize*/) ORTHANC_OVERRIDE; + size_t /*bodySize*/, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Sources/LuaScripting.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/LuaScripting.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -552,7 +552,8 @@ try { - if (IHttpHandler::SimpleDelete(NULL, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), + std::string bodyIgnored; + if (IHttpHandler::SimpleDelete(bodyIgnored, NULL, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), RequestOrigin_Lua, uri, headers) == HttpStatus_200_Ok) { lua_pushboolean(state, 1);
--- a/OrthancServer/Sources/OrthancConfiguration.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -707,32 +707,46 @@ } - bool OrthancConfiguration::SetupRegisteredUsers(HttpServer& httpServer) const + OrthancConfiguration::RegisteredUsersStatus OrthancConfiguration::SetupRegisteredUsers(HttpServer& httpServer) const { + static const char* const REGISTERED_USERS = "RegisteredUsers"; + httpServer.ClearUsers(); - if (!json_.isMember("RegisteredUsers")) + if (!json_.isMember(REGISTERED_USERS)) { - return false; + return RegisteredUsersStatus_NoConfiguration; } - - const Json::Value& users = json_["RegisteredUsers"]; - if (users.type() != Json::objectValue) + else { - throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users"); - } + const Json::Value& users = json_[REGISTERED_USERS]; + if (users.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users"); + } - bool hasUser = false; - Json::Value::Members usernames = users.getMemberNames(); - for (size_t i = 0; i < usernames.size(); i++) - { - const std::string& username = usernames[i]; - std::string password = users[username].asString(); - httpServer.RegisterUser(username.c_str(), password.c_str()); - hasUser = true; + bool hasUser = false; + Json::Value::Members usernames = users.getMemberNames(); + for (size_t i = 0; i < usernames.size(); i++) + { + const std::string& username = usernames[i]; + + if (users[username].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users"); + } + else + { + std::string password = users[username].asString(); + httpServer.RegisterUser(username.c_str(), password.c_str()); + hasUser = true; + } + } + + return (hasUser ? + RegisteredUsersStatus_HasUser : + RegisteredUsersStatus_NoUser); } - - return hasUser; }
--- a/OrthancServer/Sources/OrthancConfiguration.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.h Mon Jul 14 16:13:22 2025 +0200 @@ -49,6 +49,14 @@ class OrthancConfiguration : public boost::noncopyable { + public: + enum RegisteredUsersStatus + { + RegisteredUsersStatus_NoConfiguration, // There is no "RegisteredUsers" section in the configuration file + RegisteredUsersStatus_NoUser, // The "RegisteredUsers" section is present, but declares no user + RegisteredUsersStatus_HasUser // The "RegisteredUsers" section is present and contains at least 1 user + }; + private: typedef std::map<std::string, RemoteModalityParameters> Modalities; typedef std::map<std::string, WebServiceParameters> Peers; @@ -198,9 +206,8 @@ void GetListOfOrthancPeers(std::set<std::string>& target) const; unsigned int GetDicomLossyTranscodingQuality() const; - - // Returns "true" iff. at least one user is registered - bool SetupRegisteredUsers(HttpServer& httpServer) const; + + RegisteredUsersStatus SetupRegisteredUsers(HttpServer& httpServer) const; std::string InterpretStringParameterAsPath(const std::string& parameter) const;
--- a/OrthancServer/Sources/OrthancHttpHandler.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/OrthancHttpHandler.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -36,7 +36,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) { if (method != HttpMethod_Post && method != HttpMethod_Put) @@ -47,7 +48,7 @@ for (Handlers::const_iterator it = handlers_.begin(); it != handlers_.end(); ++it) { if ((*it)->CreateChunkedRequestReader - (target, origin, remoteIp, username, method, uri, headers)) + (target, origin, remoteIp, username, method, uri, headers, authenticationPayload)) { if (target.get() == NULL) { @@ -71,12 +72,13 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) + size_t bodySize, + const std::string& authenticationPayload) { for (Handlers::const_iterator it = handlers_.begin(); it != handlers_.end(); ++it) { if ((*it)->Handle(output, origin, remoteIp, username, method, uri, - headers, getArguments, bodyData, bodySize)) + headers, getArguments, bodyData, bodySize, authenticationPayload)) { return true; }
--- a/OrthancServer/Sources/OrthancHttpHandler.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/OrthancHttpHandler.h Mon Jul 14 16:13:22 2025 +0200 @@ -46,7 +46,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE; + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; virtual bool Handle(HttpOutput& output, RequestOrigin origin, @@ -57,7 +58,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) ORTHANC_OVERRIDE; + size_t bodySize, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; void Register(IHttpHandler& handler, bool isOrthancRestApi);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -297,13 +297,14 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) + size_t bodySize, + const std::string& authenticationPayload) { MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_rest_api_duration_ms"); MetricsRegistry::ActiveCounter counter(activeRequests_); return RestApi::Handle(output, origin, remoteIp, username, method, - uri, headers, getArguments, bodyData, bodySize); + uri, headers, getArguments, bodyData, bodySize, authenticationPayload); } @@ -324,6 +325,7 @@ static const char* KEY_PRIORITY = "Priority"; static const char* KEY_SYNCHRONOUS = "Synchronous"; static const char* KEY_ASYNCHRONOUS = "Asynchronous"; + static const char* KEY_USER_DATA = "UserData"; bool OrthancRestApi::IsSynchronousJobRequest(bool isDefaultSynchronous, @@ -441,6 +443,11 @@ job->SetPermissive(false); } + if (body.isMember(KEY_USER_DATA)) + { + job->SetUserData(body[KEY_USER_DATA]); + } + SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body); } @@ -467,6 +474,11 @@ job->SetPermissive(false); } + if (body.isMember(KEY_USER_DATA)) + { + job->SetUserData(body[KEY_USER_DATA]); + } + SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body); } @@ -492,7 +504,9 @@ DocumentSubmitGenericJob(call); call.GetDocumentation() .SetRequestField(KEY_PERMISSIVE, RestApiCallDocumentation::Type_Boolean, - "If `true`, ignore errors during the individual steps of the job.", false); + "If `true`, ignore errors during the individual steps of the job.", false) + .SetRequestField(KEY_USER_DATA, RestApiCallDocumentation::Type_JsonObject, + "User data that will travel along with the job.", false); }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h Mon Jul 14 16:13:22 2025 +0200 @@ -78,7 +78,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) ORTHANC_OVERRIDE; + size_t bodySize, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; const bool& LeaveBarrierFlag() const {
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -44,7 +44,8 @@ static const char* const KEY_TRANSCODE = "Transcode"; static const char* const KEY_LOSSY_QUALITY = "LossyQuality"; static const char* const KEY_FILENAME = "Filename"; - + static const char* const KEY_USER_DATA = "UserData"; + static const char* const GET_TRANSCODE = "transcode"; static const char* const GET_LOSSY_QUALITY = "lossy-quality"; static const char* const GET_FILENAME = "filename"; @@ -124,6 +125,7 @@ int& priority, /* out */ unsigned int& loaderThreads, /* out */ std::string& filename, /* out */ + Json::Value& userData, /* out */ const Json::Value& body, /* in */ const bool defaultExtended /* in */, const std::string& defaultFilename /* in */) @@ -174,6 +176,12 @@ filename = defaultFilename; } + if (body.type() == Json::objectValue && + body.isMember(KEY_USER_DATA) && body[KEY_USER_DATA].isString()) + { + userData = body[KEY_USER_DATA].asString(); + } + { OrthancConfiguration::ReaderLock lock; loaderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, 0); // New in Orthanc 1.10.0 @@ -548,6 +556,8 @@ "(including file extension)", false) .SetRequestField("Priority", RestApiCallDocumentation::Type_Number, "In asynchronous mode, the priority of the job. The higher the value, the higher the priority.", false) + .SetRequestField(KEY_USER_DATA, RestApiCallDocumentation::Type_JsonObject, + "In asynchronous mode, user data that will be attached to the job.", false) .AddAnswerType(MimeType_Zip, "In synchronous mode, the ZIP file containing the archive") .AddAnswerType(MimeType_Json, "In asynchronous mode, information about the job that has been submitted to " "generate the archive: https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs") @@ -593,9 +603,10 @@ unsigned int loaderThreads; std::string filename; unsigned int lossyQuality; + Json::Value userData; GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality, - priority, loaderThreads, filename, body, DEFAULT_IS_EXTENDED, "Archive.zip"); + priority, loaderThreads, filename, userData, body, DEFAULT_IS_EXTENDED, "Archive.zip"); std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, ResourceType_Patient)); AddResourcesOfInterest(*job, body); @@ -607,6 +618,7 @@ } job->SetLoaderThreads(loaderThreads); + job->SetUserData(userData); SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename); } @@ -783,8 +795,10 @@ unsigned int loaderThreads; std::string filename; unsigned int lossyQuality; + Json::Value userData; + GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality, - priority, loaderThreads, filename, body, false /* by default, not extented */, id + ".zip"); + priority, loaderThreads, filename, userData, body, false /* by default, not extented */, id + ".zip"); std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, LEVEL)); job->AddResource(id, true, LEVEL); @@ -796,6 +810,7 @@ } job->SetLoaderThreads(loaderThreads); + job->SetUserData(userData); SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename); }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -97,6 +97,7 @@ static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges"; static const char* const HAS_KEY_VALUE_STORES = "HasKeyValueStores"; static const char* const HAS_QUEUES = "HasQueues"; + static const char* const HAS_AUDITS_LOGS = "HasAuditLogs"; static const char* const HAS_EXTENDED_FIND = "HasExtendedFind"; static const char* const READ_ONLY = "ReadOnly"; @@ -215,6 +216,7 @@ result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport(); result[CAPABILITIES][HAS_KEY_VALUE_STORES] = OrthancRestApi::GetIndex(call).HasKeyValueStoresSupport(); result[CAPABILITIES][HAS_QUEUES] = OrthancRestApi::GetIndex(call).HasQueuesSupport(); + result[CAPABILITIES][HAS_AUDITS_LOGS] = OrthancRestApi::GetIndex(call).HasAuditLogsSupport(); call.GetOutput().AnswerJson(result); }
--- a/OrthancServer/Sources/Search/DatabaseLookup.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -111,8 +111,8 @@ std::set<DicomTag> ignoreTagLength; std::unique_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement - (*element, DicomToJsonFlags_None, - 0, encoding, hasCodeExtensions, ignoreTagLength)); + (*element, DicomToJsonFlags_None, 0, encoding, hasCodeExtensions, + ignoreTagLength, FromDcmtkBridge::Convert(element->getVR()))); // WARNING: Also modify "HierarchicalMatcher::Setup()" if modifying this code if (value.get() == NULL ||
--- a/OrthancServer/Sources/Search/HierarchicalMatcher.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/Search/HierarchicalMatcher.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -114,8 +114,8 @@ std::set<DicomTag> ignoreTagLength; std::unique_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement - (*element, DicomToJsonFlags_None, - 0, encoding, hasCodeExtensions, ignoreTagLength)); + (*element, DicomToJsonFlags_None, 0, encoding, hasCodeExtensions, + ignoreTagLength, FromDcmtkBridge::Convert(element->getVR()))); // WARNING: Also modify "DatabaseLookup::IsMatch()" if modifying this code if (value.get() == NULL ||
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h Mon Jul 14 16:13:22 2025 +0200 @@ -58,6 +58,7 @@ bool enableExtendedSopClass_; std::string description_; std::string filename_; + Json::Value userData_; boost::shared_ptr<ZipWriterIterator> writer_; size_t currentStep_; @@ -137,5 +138,20 @@ virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE; virtual void DeleteAllOutputs() ORTHANC_OVERRIDE; + + void SetUserData(const Json::Value& userData) + { + userData_ = userData; + } + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + if (!userData_.isNull()) + { + userData = userData_; + return true; + } + return false; + } }; }
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -367,6 +367,7 @@ static const char* KEY_PARENT_RESOURCES = "ParentResources"; static const char* KEY_DESCRIPTION = "Description"; static const char* KEY_PERMISSIVE = "Permissive"; + static const char* KEY_USER_DATA = "UserData"; static const char* KEY_CURRENT_STEP = "CurrentStep"; static const char* KEY_TYPE = "Type"; static const char* KEY_INSTANCES = "Instances"; @@ -402,6 +403,7 @@ target[KEY_TYPE] = type; target[KEY_PERMISSIVE] = permissive_; + target[KEY_USER_DATA] = userData_; target[KEY_CURRENT_STEP] = static_cast<unsigned int>(currentStep_); target[KEY_DESCRIPTION] = description_; target[KEY_KEEP_SOURCE] = keepSource_; @@ -456,6 +458,17 @@ { currentStep_ = static_cast<ThreadedJobStep>(SerializationToolbox::ReadUnsignedInteger(source, KEY_CURRENT_STEP)); } + + if (source.isMember(KEY_PERMISSIVE)) + { + SerializationToolbox::ReadBoolean(source, KEY_PERMISSIVE); + } + + // new in 1.12.9 + if (source.isMember(KEY_USER_DATA)) + { + userData_ = source[KEY_USER_DATA]; + } } @@ -538,6 +551,27 @@ return description_; } + + void ThreadedSetOfInstancesJob::SetUserData(const Json::Value& userData) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + userData_ = userData; + } + + bool ThreadedSetOfInstancesJob::GetUserData(Json::Value& userData) const + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + if (!userData_.isNull()) + { + userData = userData_; + return true; + } + return false; + } + + void ThreadedSetOfInstancesJob::SetErrorCode(ErrorCode errorCode) { boost::recursive_mutex::scoped_lock lock(mutex_);
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h Mon Jul 14 16:13:22 2025 +0200 @@ -62,6 +62,7 @@ bool permissive_; ThreadedJobStep currentStep_; std::string description_; + Json::Value userData_; size_t workersCount_; ServerContext& context_; @@ -170,5 +171,8 @@ return context_; } + void SetUserData(const Json::Value& userData); + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Sources/main.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/Sources/main.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -534,7 +534,7 @@ { } - virtual bool IsValidBearerToken(const std::string& token) ORTHANC_OVERRIDE + virtual bool IsValidBearerToken(const std::string& token) const ORTHANC_OVERRIDE { #if ORTHANC_ENABLE_PLUGINS == 1 return (plugins_ != NULL && @@ -549,7 +549,7 @@ const char* ip, const char* username, const HttpToolbox::Arguments& httpHeaders, - const HttpToolbox::GetArguments& getArguments) ORTHANC_OVERRIDE + const HttpToolbox::GetArguments& getArguments) const ORTHANC_OVERRIDE { #if ORTHANC_ENABLE_PLUGINS == 1 if (plugins_ != NULL && @@ -570,24 +570,24 @@ switch (method) { - case HttpMethod_Get: - call.PushString("GET"); - break; + case HttpMethod_Get: + call.PushString("GET"); + break; - case HttpMethod_Put: - call.PushString("PUT"); - break; + case HttpMethod_Put: + call.PushString("PUT"); + break; - case HttpMethod_Post: - call.PushString("POST"); - break; + case HttpMethod_Post: + call.PushString("POST"); + break; - case HttpMethod_Delete: - call.PushString("DELETE"); - break; + case HttpMethod_Delete: + call.PushString("DELETE"); + break; - default: - return true; + default: + return true; } call.PushString(uri); @@ -604,6 +604,23 @@ return true; } + + virtual AuthenticationStatus CheckAuthentication(std::string& customPayload /* out: payload to provide to "IsAllowed()" */, + std::string& redirection /* out: path relative to the root */, + const char* uri, + const char* ip, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::GetArguments& getArguments) const ORTHANC_OVERRIDE + { +#if ORTHANC_ENABLE_PLUGINS == 1 + if (plugins_ != NULL) + { + return plugins_->CheckAuthentication(customPayload, redirection, uri, ip, httpHeaders, getArguments); + } +#endif + + return AuthenticationStatus_BuiltIn; + } }; @@ -1032,7 +1049,7 @@ else { MyIncomingHttpRequestFilter httpFilter(context, plugins); - HttpServer httpServer; + HttpServer httpServer(context.GetMetricsRegistry()); bool httpDescribeErrors; #if ORTHANC_ENABLE_MONGOOSE == 1 @@ -1090,12 +1107,16 @@ httpServer.SetAuthenticationEnabled(false); } - bool hasUsers = lock.GetConfiguration().SetupRegisteredUsers(httpServer); + OrthancConfiguration::RegisteredUsersStatus status = lock.GetConfiguration().SetupRegisteredUsers(httpServer); + assert(status == OrthancConfiguration::RegisteredUsersStatus_NoConfiguration || + status == OrthancConfiguration::RegisteredUsersStatus_NoUser || + status == OrthancConfiguration::RegisteredUsersStatus_HasUser); if (httpServer.IsAuthenticationEnabled() && - !hasUsers) + status != OrthancConfiguration::RegisteredUsersStatus_HasUser) { - if (httpServer.IsRemoteAccessAllowed()) + if (httpServer.IsRemoteAccessAllowed() && + status == OrthancConfiguration::RegisteredUsersStatus_NoConfiguration) { /** * Starting with Orthanc 1.5.8, if no user is explicitly @@ -1117,7 +1138,8 @@ else { LOG(WARNING) << "HTTP authentication is enabled, but no user is declared, " - << "check the value of configuration option \"RegisteredUsers\""; + << "check the value of configuration option \"RegisteredUsers\" " + << "if you cannot access Orthanc as expected"; } } @@ -1274,7 +1296,7 @@ ModalitiesFromConfiguration modalities; // Setup the DICOM server - DicomServer dicomServer; + DicomServer dicomServer(context.GetMetricsRegistry()); dicomServer.SetRemoteModalities(modalities); dicomServer.SetStoreRequestHandlerFactory(serverFactory); dicomServer.SetMoveRequestHandlerFactory(serverFactory);
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -141,6 +141,10 @@ { return false; } + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + return false; + } }; @@ -148,7 +152,6 @@ { private: bool trailingStepDone_; - protected: virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE { @@ -207,6 +210,10 @@ { s = "DummyInstancesJob"; } + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + return false; + } };
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp Fri Jun 27 15:00:33 2025 +0200 +++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp Mon Jul 14 16:13:22 2025 +0200 @@ -194,7 +194,7 @@ const unsigned char raw[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 }; std::string latin1((char*) &raw[0], sizeof(raw) / sizeof(char)); - std::string utf8 = Toolbox::ConvertToUtf8(latin1, Encoding_Latin1, false); + std::string utf8 = Toolbox::ConvertToUtf8(latin1, Encoding_Latin1, false, false); ParsedDicomFile dicom(false); dicom.SetEncoding(Encoding_Latin1); @@ -516,7 +516,6 @@ int main(int argc, char **argv) { Logging::Initialize(); - Toolbox::InitializeGlobalLocale(NULL); SetGlobalVerbosity(Verbosity_Verbose); Toolbox::DetectEndianness(); SystemToolbox::MakeDirectory("UnitTestsResults");
--- a/TODO Fri Jun 27 15:00:33 2025 +0200 +++ b/TODO Mon Jul 14 16:13:22 2025 +0200 @@ -141,6 +141,9 @@ require ffmpeg or a similar library -> can not be done in the Orthanc core. -> keep it for a python plugin -> or require the payload to include rows/columns/cinerate/... +* When creating e.g a PDF series with /tools/create-dicom with a ParentStudy, + the new series does not have the InstitutionName copied from the parent study. + Can be tested with "Add PDF" in OE2. * (1) In the /studies/{id}/anonymize route, add an option to remove secondary captures. They usually contains Patient info in the image. The SOPClassUID might be used to identify such secondary