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, &params);
   }
 
+
+  /**
+   * @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, &params);
+  }
+
+
+  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