changeset 5388:c026301eee9e

merged Orthanc-1.12.1 back to mainline (todo file was commited in the wrong branch)
author Alain Mazy <am@osimis.io>
date Wed, 20 Sep 2023 09:02:31 +0200
parents 579f541fb9ce (diff) b074943fca35 (current diff)
children 0e5e675b9750
files TODO
diffstat 62 files changed, 839 insertions(+), 186 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Sep 19 10:56:09 2023 +0200
+++ b/NEWS	Wed Sep 20 09:02:31 2023 +0200
@@ -1,6 +1,29 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* Housekeeper plugin:
+  - Update to rebuild the cache of the DicomWeb plugin when updating to DicomWeb 1.15.
+  - New trigger configuration: "DicomWebCacheChange"
+  - Fixed reading the triggers configuration.
+
+REST API
+--------
+
+* API version upgraded to 22
+* Added a route to delete completed jobs from history: DELETE /jobs/{id}
+
+Maintenance
+-----------
+
+* Fix unit test PngWriter.Color16Pattern on big-endian architectures,
+  as suggested by Etienne Mollier: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1041813
+* Prevent the leak of the full path of the source files in the binaries
+* Fix loading of DCMTK dictionary in the MultitenantDicom plugin when built dynamically:
+  https://discourse.orthanc-server.org/t/dimse-failure-using-multitenant-plugin/3665
+
 
 Version 1.12.1 (2023-07-04)
 ===========================
--- a/OrthancFramework/Resources/CMake/Compiler.cmake	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Resources/CMake/Compiler.cmake	Wed Sep 20 09:02:31 2023 +0200
@@ -263,3 +263,24 @@
   # preceding batches. https://cmake.org/Bug/view.php?id=14874
   set(CMAKE_CXX_ARCHIVE_APPEND "<CMAKE_AR> <LINK_FLAGS> q <TARGET> <OBJECTS>")
 endif()
+
+
+# This function defines macro "__ORTHANC_FILE__" as a replacement to
+# macro "__FILE__", as the latter leaks the full path of the source
+# files in the binaries
+# https://stackoverflow.com/questions/8487986/file-macro-shows-full-path
+# https://twitter.com/wget42/status/1676877802375634944?s=20
+function(DefineSourceBasenameForTarget targetname)
+  # Microsoft Visual Studio is extremely slow if using
+  # "set_property()", we only enable this feature for gcc and clang
+  if (CMAKE_COMPILER_IS_GNUCXX OR
+      CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+    get_target_property(source_files "${targetname}" SOURCES)
+    foreach(sourcefile ${source_files})
+      get_filename_component(basename "${sourcefile}" NAME)
+      set_property(
+        SOURCE "${sourcefile}" APPEND
+        PROPERTY COMPILE_DEFINITIONS "__ORTHANC_FILE__=\"${basename}\"")
+    endforeach()
+  endif()
+endfunction()
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Wed Sep 20 09:02:31 2023 +0200
@@ -153,9 +153,11 @@
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.2")
         set(ORTHANC_FRAMEWORK_MD5 "ede3de356493a8868545f8cb4b8bc8b5")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.3")
-        set(ORTHANC_FRAMEWORK_MD5 "5c1b11009d782f248739919db6bf7f7a")
+        set(ORTHANC_FRAMEWORK_MD5 "f941c0f5771db7616e7b7961026a60e2")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.0")
         set(ORTHANC_FRAMEWORK_MD5 "d32a0cde03b6eb603d8dd2b33d38bf1b")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.1")
+        set(ORTHANC_FRAMEWORK_MD5 "8a435140efc8ff4a01d8242f092f21de")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
@@ -179,6 +181,9 @@
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "b2e08d83e21d")
         # WSI 1.1 (framework pre-1.10.0), to remove "-std=c++11"
         set(ORTHANC_FRAMEWORK_MD5 "2eaa073cbb4b44ffba199ad93393b2b1")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "daf4807631c5")
+        # DICOMweb 1.15 (framework pre-1.12.2)
+        set(ORTHANC_FRAMEWORK_MD5 "c644aff2817306b3207c98c92e43f35f")
       endif()
     endif()
   endif()
--- a/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Wed Sep 20 09:02:31 2023 +0200
@@ -109,7 +109,7 @@
     endif()
 
     set_property(
-      SOURCE ${CURL_SOURCES}
+      SOURCE ${CURL_SOURCES} APPEND
       PROPERTY COMPILE_DEFINITIONS "HAVE_CONFIG_H=1;OS=\"${TMP_OS}\""
       )
    
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Sep 20 09:02:31 2023 +0200
@@ -24,7 +24,7 @@
 #####################################################################
 
 # Version of the build, should always be "mainline" except in release branches
-set(ORTHANC_VERSION "1.12.1")
+set(ORTHANC_VERSION "mainline")
 
 # Version of the database schema. History:
 #   * Orthanc 0.1.0 -> Orthanc 0.3.0 = no versioning
@@ -38,7 +38,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "21")
+set(ORTHANC_API_VERSION "22")
 
 
 #####################################################################
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Wed Sep 20 09:02:31 2023 +0200
@@ -250,6 +250,12 @@
     "Name": "MainDicomTagsMultiplyDefined",
     "Description": "A main DICOM Tag has been defined multiple times for the same resource level"
   }, 
+  {
+    "Code": 45, 
+    "HttpStatus": 403, 
+    "Name": "ForbiddenAccess", 
+    "Description": "Access to a resource is forbidden"
+  }, 
 
 
 
--- a/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Wed Sep 20 09:02:31 2023 +0200
@@ -122,7 +122,7 @@
 with open(path, 'r') as f:
     a = f.read()
 
-e = filter(lambda x: 'SQLite' in x and x['SQLite'], ERRORS)
+e = list(filter(lambda x: 'SQLite' in x and x['SQLite'], ERRORS))
 s = ',\n'.join(map(lambda x: '      ErrorCode_%s' % x['Name'], e))
 a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
 
--- a/OrthancFramework/Resources/Patches/civetweb-1.14.patch	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Resources/Patches/civetweb-1.14.patch	Wed Sep 20 09:02:31 2023 +0200
@@ -1,6 +1,38 @@
 diff -urEb civetweb-1.14.orig/src/civetweb.c civetweb-1.14/src/civetweb.c
---- civetweb-1.14.orig/src/civetweb.c	2021-06-21 17:42:52.343136123 +0200
-+++ civetweb-1.14/src/civetweb.c	2021-06-21 17:43:11.623158128 +0200
+--- civetweb-1.14.orig/src/civetweb.c	2023-07-06 15:48:01.163703913 +0200
++++ civetweb-1.14/src/civetweb.c	2023-07-06 15:48:51.207843938 +0200
+@@ -567,7 +567,7 @@
+ #if (_MSC_VER < 1300)
+ #define STRX(x) #x
+ #define STR(x) STRX(x)
+-#define __func__ __FILE__ ":" STR(__LINE__)
++#define __func__ __ORTHANC_FILE__ ":" STR(__LINE__)
+ #define strtoull(x, y, z) ((unsigned __int64)_atoi64(x))
+ #define strtoll(x, y, z) (_atoi64(x))
+ #else
+@@ -1450,14 +1450,14 @@
+ }
+ 
+ 
+-#define mg_malloc(a) mg_malloc_ex(a, NULL, __FILE__, __LINE__)
+-#define mg_calloc(a, b) mg_calloc_ex(a, b, NULL, __FILE__, __LINE__)
+-#define mg_realloc(a, b) mg_realloc_ex(a, b, NULL, __FILE__, __LINE__)
+-#define mg_free(a) mg_free_ex(a, __FILE__, __LINE__)
+-
+-#define mg_malloc_ctx(a, c) mg_malloc_ex(a, c, __FILE__, __LINE__)
+-#define mg_calloc_ctx(a, b, c) mg_calloc_ex(a, b, c, __FILE__, __LINE__)
+-#define mg_realloc_ctx(a, b, c) mg_realloc_ex(a, b, c, __FILE__, __LINE__)
++#define mg_malloc(a) mg_malloc_ex(a, NULL, __ORTHANC_FILE__, __LINE__)
++#define mg_calloc(a, b) mg_calloc_ex(a, b, NULL, __ORTHANC_FILE__, __LINE__)
++#define mg_realloc(a, b) mg_realloc_ex(a, b, NULL, __ORTHANC_FILE__, __LINE__)
++#define mg_free(a) mg_free_ex(a, __ORTHANC_FILE__, __LINE__)
++
++#define mg_malloc_ctx(a, c) mg_malloc_ex(a, c, __ORTHANC_FILE__, __LINE__)
++#define mg_calloc_ctx(a, b, c) mg_calloc_ex(a, b, c, __ORTHANC_FILE__, __LINE__)
++#define mg_realloc_ctx(a, b, c) mg_realloc_ex(a, b, c, __ORTHANC_FILE__, __LINE__)
+ 
+ 
+ #else /* USE_SERVER_STATS */
 @@ -1774,6 +1774,7 @@
  #if !defined(OPENSSL_API_3_0)
  #define OPENSSL_API_3_0
--- a/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -133,7 +133,7 @@
 
 if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
   set_property(
-    SOURCE ${PROTOBUF_COMPILER_SOURCES}
+    SOURCE ${PROTOBUF_COMPILER_SOURCES} APPEND
     PROPERTY COMPILE_DEFINITIONS "HAVE_PTHREAD=1"
     )
 endif()
--- a/OrthancFramework/Resources/ProtocolBuffers/ProtobufLibrary.cmake	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Resources/ProtocolBuffers/ProtobufLibrary.cmake	Wed Sep 20 09:02:31 2023 +0200
@@ -138,7 +138,7 @@
 
 if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
   set_property(
-    SOURCE ${PROTOBUF_LIBRARY_SOURCES}
+    SOURCE ${PROTOBUF_LIBRARY_SOURCES} APPEND
     PROPERTY COMPILE_DEFINITIONS "HAVE_PTHREAD=1"
     )
 endif()
--- a/OrthancFramework/SharedLibrary/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/SharedLibrary/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -257,6 +257,8 @@
     ${ORTHANC_DICOM_SOURCES}
     )
 
+  DefineSourceBasenameForTarget(OrthancFramework)
+
   # CMake does not natively handle SIDE_MODULE, and believes that
   # Emscripten produces a ".js" file (whereas it creates only the
   # ".wasm"). Create a dummy ".js" for target to work.
@@ -273,6 +275,8 @@
       DllMain.cpp
       )
 
+    DefineSourceBasenameForTarget(OrthancFramework)
+
     # By default, hide all the symbols
     set_target_properties(OrthancFramework PROPERTIES C_VISIBILITY_PRESET hidden)
     set_target_properties(OrthancFramework PROPERTIES CXX_VISIBILITY_PRESET hidden)
@@ -301,10 +305,14 @@
       ${ORTHANC_DICOM_SOURCES}
       )
 
+    DefineSourceBasenameForTarget(OrthancFramework)
+
     # Add the "-fPIC" option to use the static library from Orthanc
     # plugins (the latter being shared libraries)
     set_property(TARGET OrthancFramework PROPERTY POSITION_INDEPENDENT_CODE ON)
   endif()
+
+  DefineSourceBasenameForTarget(OrthancFramework)
 endif()
 
 
--- a/OrthancFramework/Sources/Enumerations.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/Enumerations.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -199,6 +199,9 @@
       case ErrorCode_MainDicomTagsMultiplyDefined:
         return "A main DICOM Tag has been defined multiple times for the same resource level";
 
+      case ErrorCode_ForbiddenAccess:
+        return "Access to a resource is forbidden";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
@@ -2278,6 +2281,9 @@
       case ErrorCode_Revision:
         return HttpStatus_409_Conflict;
 
+      case ErrorCode_ForbiddenAccess:
+        return HttpStatus_403_Forbidden;
+
       case ErrorCode_CreateDicomNotString:
         return HttpStatus_400_BadRequest;
 
--- a/OrthancFramework/Sources/Enumerations.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Wed Sep 20 09:02:31 2023 +0200
@@ -148,6 +148,7 @@
     ErrorCode_DatabaseCannotSerialize = 42    /*!< Database could not serialize access due to concurrent update, the transaction should be retried */,
     ErrorCode_Revision = 43    /*!< A bad revision number was provided, which might indicate conflict between multiple writers */,
     ErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
+    ErrorCode_ForbiddenAccess = 45    /*!< Access to a resource is forbidden */,
     ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -122,8 +122,16 @@
                                  size_t size,
                                  FileContentType type)
   {
-    LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
-              << "\" type (size: " << (size / (1024 * 1024) + 1) << "MB)";
+    if (size > (1024 * 1024))
+    {
+      LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+                << "\" type (size: " << (size / (1024 * 1024) + 1) << "MB)";
+    }
+    else
+    {
+      LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+                << "\" type (size: " << (size / 1024 + 1) << "KB)";
+    }
 
     boost::filesystem::path path;
     
--- a/OrthancFramework/Sources/JobsEngine/IJob.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/JobsEngine/IJob.h	Wed Sep 20 09:02:31 2023 +0200
@@ -66,5 +66,9 @@
     // This function can only be called if the job has reached its
     // "success" state
     virtual bool DeleteOutput(const std::string& key) = 0;
+
+    // This function can only be called if the job has reached its
+    // "success" state
+    virtual void DeleteAllOutputs() {}
   };
 }
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -649,6 +649,42 @@
   }
 
 
+  bool JobsRegistry::DeleteJobInfo(const std::string& id)
+  {
+    LOG(INFO) << "Deleting job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job to delete: " << id;
+      return false;
+    }
+    else
+    {
+      for (CompletedJobs::iterator it = completedJobs_.begin();
+           it != completedJobs_.end(); ++it)
+      {
+        if (*it == found->second)
+        {
+          found->second->GetJob().DeleteAllOutputs();
+          delete found->second;
+          
+          completedJobs_.erase(it);
+          jobsIndex_.erase(id);
+          return true;
+        }
+      }
+
+      LOG(WARNING) << "Can not delete a job that is not complete: " << id;
+      return false;
+    }
+  }
+
+
   bool JobsRegistry::GetJobOutput(std::string& output,
                                   MimeType& mime,
                                   std::string& filename,
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Wed Sep 20 09:02:31 2023 +0200
@@ -147,6 +147,8 @@
     bool GetJobInfo(JobInfo& target,
                     const std::string& id);
 
+    bool DeleteJobInfo(const std::string& id);
+
     bool GetJobOutput(std::string& output,
                       MimeType& mime,
                       std::string& filename,
--- a/OrthancFramework/Sources/Logging.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/Logging.h	Wed Sep 20 09:02:31 2023 +0200
@@ -157,15 +157,25 @@
 #  define VLOG(unused)          ::Orthanc::Logging::NullStream()
 #  define CLOG(level, category) ::Orthanc::Logging::NullStream()
 #else /* ORTHANC_ENABLE_LOGGING == 1 */
-#  define LOG(level)     ::Orthanc::Logging::InternalLogger     \
-  (::Orthanc::Logging::LogLevel_ ## level,                      \
-   ::Orthanc::Logging::LogCategory_GENERIC, __FILE__, __LINE__)
-#  define VLOG(unused)   ::Orthanc::Logging::InternalLogger     \
-  (::Orthanc::Logging::LogLevel_TRACE,                          \
-   ::Orthanc::Logging::LogCategory_GENERIC, __FILE__, __LINE__)
+
+#if !defined(__ORTHANC_FILE__)
+#  if defined(_MSC_VER)
+#    pragma message("Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries")
+#  else
+#    warning Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries
+#  endif
+#  define __ORTHANC_FILE__ __FILE__
+#endif
+
+#  define LOG(level)     ::Orthanc::Logging::InternalLogger             \
+  (::Orthanc::Logging::LogLevel_ ## level,                              \
+   ::Orthanc::Logging::LogCategory_GENERIC, __ORTHANC_FILE__, __LINE__)
+#  define VLOG(unused)   ::Orthanc::Logging::InternalLogger             \
+  (::Orthanc::Logging::LogLevel_TRACE,                                  \
+   ::Orthanc::Logging::LogCategory_GENERIC, __ORTHANC_FILE__, __LINE__)
 #  define CLOG(level, category) ::Orthanc::Logging::InternalLogger      \
   (::Orthanc::Logging::LogLevel_ ## level,                              \
-   ::Orthanc::Logging::LogCategory_ ## category, __FILE__, __LINE__)
+   ::Orthanc::Logging::LogCategory_ ## category, __ORTHANC_FILE__, __LINE__)
 #endif
 
 
--- a/OrthancFramework/Sources/SQLite/Connection.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/SQLite/Connection.h	Wed Sep 20 09:02:31 2023 +0200
@@ -46,7 +46,16 @@
 #include <string>
 #include <map>
 
-#define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__FILE__, __LINE__)
+#if !defined(__ORTHANC_FILE__)
+#  if defined(_MSC_VER)
+#    pragma message("Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries")
+#  else
+#    warning Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries
+#  endif
+#  define __ORTHANC_FILE__ __FILE__
+#endif
+
+#define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__)
 
 namespace Orthanc
 {
--- a/OrthancFramework/Sources/SerializationToolbox.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/SerializationToolbox.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -170,31 +170,52 @@
 
   
   void SerializationToolbox::ReadArrayOfStrings(std::vector<std::string>& target,
-                                                const Json::Value& value,
+                                                const Json::Value& valueObject,
                                                 const std::string& field)
   {
-    if (value.type() != Json::objectValue ||
-        !value.isMember(field.c_str()) ||
-        value[field.c_str()].type() != Json::arrayValue)
+    if (valueObject.type() != Json::objectValue ||
+        !valueObject.isMember(field.c_str()) ||
+        valueObject[field.c_str()].type() != Json::arrayValue)
     {
       throw OrthancException(ErrorCode_BadFileFormat,
-                             "List of strings expected in field: " + field);
+                            "List of strings expected in field: " + field);
     }
 
-    const Json::Value& arr = value[field.c_str()];
+    const Json::Value& arr = valueObject[field.c_str()];
 
-    target.resize(arr.size());
+    try
+    {
+      ReadArrayOfStrings(target, arr);
+    }
+    catch (OrthancException& ex)
+    {  // more detailed error
+      throw OrthancException(ErrorCode_BadFileFormat,
+                              "List of strings expected in field: " + field);
+    }
+  }
+
 
-    for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++)
+  void SerializationToolbox::ReadArrayOfStrings(std::vector<std::string>& target,
+                                                const Json::Value& valueArray)
+  {
+    if (valueArray.type() != Json::arrayValue)
     {
-      if (arr[i].type() != Json::stringValue)
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "List of strings expected");
+    }
+
+    target.resize(valueArray.size());
+
+    for (Json::Value::ArrayIndex i = 0; i < valueArray.size(); i++)
+    {
+      if (valueArray[i].type() != Json::stringValue)
       {
         throw OrthancException(ErrorCode_BadFileFormat,
-                               "List of strings expected in field: " + field);
+                               "List of strings expected");
       }
       else
       {
-        target[i] = arr[i].asString();
+        target[i] = valueArray[i].asString();
       }
     }
   }
@@ -216,11 +237,25 @@
   
 
   void SerializationToolbox::ReadSetOfStrings(std::set<std::string>& target,
-                                              const Json::Value& value,
+                                              const Json::Value& valueObject,
                                               const std::string& field)
   {
     std::vector<std::string> tmp;
-    ReadArrayOfStrings(tmp, value, field);
+    ReadArrayOfStrings(tmp, valueObject, field);
+
+    target.clear();
+    for (size_t i = 0; i < tmp.size(); i++)
+    {
+      target.insert(tmp[i]);
+    }
+  }
+
+
+  void SerializationToolbox::ReadSetOfStrings(std::set<std::string>& target,
+                                              const Json::Value& valueArray)
+  {
+    std::vector<std::string> tmp;
+    ReadArrayOfStrings(tmp, valueArray);
 
     target.clear();
     for (size_t i = 0; i < tmp.size(); i++)
@@ -379,24 +414,38 @@
   }
 
 
-  void SerializationToolbox::WriteSetOfStrings(Json::Value& target,
+  void SerializationToolbox::WriteSetOfStrings(Json::Value& targetObject,
                                                const std::set<std::string>& values,
                                                const std::string& field)
   {
-    if (target.type() != Json::objectValue ||
-        target.isMember(field.c_str()))
+    if (targetObject.type() != Json::objectValue ||
+        targetObject.isMember(field.c_str()))
     {
       throw OrthancException(ErrorCode_BadFileFormat);
     }
 
-    Json::Value& value = target[field];
+    Json::Value& targetArray = targetObject[field];
+
+    targetArray = Json::arrayValue;
+
+    WriteSetOfStrings(targetArray, values);
+  }
+
 
-    value = Json::arrayValue;
+  void SerializationToolbox::WriteSetOfStrings(Json::Value& targetArray,
+                                               const std::set<std::string>& values)
+  {
+    if (targetArray.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    targetArray.clear();
 
     for (std::set<std::string>::const_iterator it = values.begin();
          it != values.end(); ++it)
     {
-      value.append(*it);
+      targetArray.append(*it);
     }
   }
 
--- a/OrthancFramework/Sources/SerializationToolbox.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/SerializationToolbox.h	Wed Sep 20 09:02:31 2023 +0200
@@ -60,17 +60,23 @@
                             const std::string& field);
 
     static void ReadArrayOfStrings(std::vector<std::string>& target,
-                                   const Json::Value& value,
+                                   const Json::Value& valueObject,
                                    const std::string& field);
 
+    static void ReadArrayOfStrings(std::vector<std::string>& target,
+                                   const Json::Value& valueArray);
+
     static void ReadListOfStrings(std::list<std::string>& target,
                                   const Json::Value& value,
                                   const std::string& field);
 
     static void ReadSetOfStrings(std::set<std::string>& target,
-                                 const Json::Value& value,
+                                 const Json::Value& valueObject,
                                  const std::string& field);
 
+    static void ReadSetOfStrings(std::set<std::string>& target,
+                                 const Json::Value& valueArray);
+
     static void ReadSetOfTags(std::set<DicomTag>& target,
                               const Json::Value& value,
                               const std::string& field);
@@ -91,10 +97,13 @@
                                    const std::list<std::string>& values,
                                    const std::string& field);
 
-    static void WriteSetOfStrings(Json::Value& target,
+    static void WriteSetOfStrings(Json::Value& targetObject,
                                   const std::set<std::string>& values,
                                   const std::string& field);
 
+    static void WriteSetOfStrings(Json::Value& targetArray,
+                                  const std::set<std::string>& values);
+
     static void WriteSetOfTags(Json::Value& target,
                                const std::set<DicomTag>& tags,
                                const std::string& field);
--- a/OrthancFramework/Sources/Toolbox.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -496,6 +496,20 @@
       result[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16));
     }
   }
+
+  void Toolbox::ComputeMD5(std::string& result,
+                           const std::set<std::string>& data)
+  {
+    std::string s;
+
+    for (std::set<std::string>::const_iterator it = data.begin(); it != data.end(); ++it)
+    {
+      s += *it;
+    }
+
+    ComputeMD5(result, s);
+  }
+
 #endif
 
 
@@ -1035,10 +1049,10 @@
     return result;
   }
 
-
-  void Toolbox::TokenizeString(std::vector<std::string>& result,
+  static void TokenizeStringInternal(std::vector<std::string>& result,
                                const std::string& value,
-                               char separator)
+                               char separator,
+                               bool includeEmptyStrings)
   {
     size_t countSeparators = 0;
     
@@ -1068,7 +1082,41 @@
       }
     }
 
-    result.push_back(currentItem);
+    if (includeEmptyStrings || !currentItem.empty())
+    {
+      result.push_back(currentItem);
+    }
+  }
+
+
+  void Toolbox::TokenizeString(std::vector<std::string>& result,
+                               const std::string& value,
+                               char separator)
+  {
+    TokenizeStringInternal(result, value, separator, true);
+  }
+
+
+  void Toolbox::SplitString(std::set<std::string>& result,
+                            const std::string& value,
+                            char separator)
+  {
+    result.clear();
+
+    std::vector<std::string> temp;
+    TokenizeStringInternal(temp, value, separator, false);
+    for (size_t i = 0; i < temp.size(); ++i)
+    {
+      result.insert(temp[i]);
+    }
+  }
+
+
+  void Toolbox::SplitString(std::vector<std::string>& result,
+                            const std::string& value,
+                            char separator)
+  {
+    TokenizeStringInternal(result, value, separator, false);
   }
 
 
--- a/OrthancFramework/Sources/Toolbox.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/Sources/Toolbox.h	Wed Sep 20 09:02:31 2023 +0200
@@ -128,6 +128,9 @@
     static void ComputeMD5(std::string& result,
                            const void* data,
                            size_t size);
+
+    static void ComputeMD5(std::string& result,
+                           const std::set<std::string>& data);
 #endif
 
     static void ComputeSHA1(std::string& result,
@@ -183,10 +186,21 @@
 
     static std::string WildcardToRegularExpression(const std::string& s);
 
+    // TokenizeString result might contain empty strings (not SplitString)
     static void TokenizeString(std::vector<std::string>& result,
                                const std::string& source,
                                char separator);
 
+    // SplitString result won't contain empty strings (compared to TokenizeString)
+    static void SplitString(std::vector<std::string>& result,
+                            const std::string& source,
+                            char separator);
+
+    // SplitString result won't contain empty strings (compared to TokenizeString)
+    static void SplitString(std::set<std::string>& result,
+                            const std::string& source,
+                            char separator);
+
     static void JoinStrings(std::string& result,
                             const std::set<std::string>& source,
                             const char* separator);
@@ -245,6 +259,22 @@
       }
     }
 
+    // returns true if all element of 'needles' are found in 'haystack'
+    template <typename T> static void GetIntersection(std::set<T>& target, const std::set<T>& a, const std::set<T>& b)
+    {
+      target.clear();
+
+      for (typename std::set<T>::const_iterator it = a.begin();
+            it != a.end(); ++it)
+      {
+        if (b.count(*it) > 0)
+        {
+          target.insert(*it);
+        }
+      }
+    }
+
+
 #if ORTHANC_ENABLE_PUGIXML == 1
     static void JsonToXml(std::string& target,
                           const Json::Value& source,
--- a/OrthancFramework/UnitTestsSources/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/UnitTestsSources/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -109,6 +109,8 @@
   ${GOOGLE_TEST_SOURCES}
   )
 
+DefineSourceBasenameForTarget(UnitTests)
+
 target_link_libraries(UnitTests ${ORTHANC_FRAMEWORK_LIBRARIES})
 
 install(TARGETS UnitTests
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -381,6 +381,21 @@
   ASSERT_EQ("8b1a9953c4611296a827abf8c47804d7", s);
   Toolbox::ComputeMD5(s, "");
   ASSERT_EQ("d41d8cd98f00b204e9800998ecf8427e", s);
+
+  Toolbox::ComputeMD5(s, "aaabbbccc");
+  ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s);
+
+  std::set<std::string> set;
+
+  Toolbox::ComputeMD5(s, set);
+  ASSERT_EQ("d41d8cd98f00b204e9800998ecf8427e", s);  // empty set same as empty string
+
+  set.insert("bbb");
+  set.insert("ccc");
+  set.insert("aaa");
+
+  Toolbox::ComputeMD5(s, set);
+  ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); // set md5 same as string with the values sorted
 }
 
 TEST(Toolbox, ComputeSHA1)
@@ -706,6 +721,82 @@
   ASSERT_EQ("", t[3]);
 }
 
+TEST(Toolbox, SplitString)
+{
+  {
+    std::set<std::string> result;
+    Toolbox::SplitString(result, "", ';');
+    ASSERT_EQ(0u, result.size());
+  }
+
+  {
+    std::set<std::string> result;
+    Toolbox::SplitString(result, "a", ';');
+    ASSERT_EQ(1u, result.size());
+    ASSERT_TRUE(result.end() != result.find("a"));
+  }
+
+  {
+    std::set<std::string> result;
+    Toolbox::SplitString(result, "a;b", ';');
+    ASSERT_EQ(2u, result.size());
+    ASSERT_TRUE(result.end() != result.find("a"));
+    ASSERT_TRUE(result.end() != result.find("b"));
+  }
+
+  {
+    std::set<std::string> result;
+    Toolbox::SplitString(result, "a;b;", ';');
+    ASSERT_EQ(2u, result.size());
+    ASSERT_TRUE(result.end() != result.find("a"));
+    ASSERT_TRUE(result.end() != result.find("b"));
+  }
+
+  {
+    std::set<std::string> result;
+    Toolbox::SplitString(result, "a;a", ';');
+    ASSERT_EQ(1u, result.size());
+    ASSERT_TRUE(result.end() != result.find("a"));
+  }
+
+  {
+    std::vector<std::string> result;
+    Toolbox::SplitString(result, "", ';');
+    ASSERT_EQ(0u, result.size());
+  }
+
+  {
+    std::vector<std::string> result;
+    Toolbox::SplitString(result, "a", ';');
+    ASSERT_EQ(1u, result.size());
+    ASSERT_EQ("a", result[0]);
+  }
+
+  {
+    std::vector<std::string> result;
+    Toolbox::SplitString(result, "a;b", ';');
+    ASSERT_EQ(2u, result.size());
+    ASSERT_EQ("a", result[0]);
+    ASSERT_EQ("b", result[1]);
+  }
+
+  {
+    std::vector<std::string> result;
+    Toolbox::SplitString(result, "a;b;", ';');
+    ASSERT_EQ(2u, result.size());
+    ASSERT_EQ("a", result[0]);
+    ASSERT_EQ("b", result[1]);
+  }
+
+  {
+    std::vector<std::string> result;
+    Toolbox::TokenizeString(result, "a;a", ';');
+    ASSERT_EQ(2u, result.size());
+    ASSERT_EQ("a", result[0]);
+    ASSERT_EQ("a", result[1]);
+  }
+}
+
 TEST(Toolbox, Enumerations)
 {
   ASSERT_EQ(Encoding_Utf8, StringToEncoding(EnumerationToString(Encoding_Utf8)));
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -3479,6 +3479,7 @@
 }
 
 
+#if ORTHANC_SANDBOXED != 1
 TEST(ParsedDicomFile, DISABLED_InjectEmptyPixelData2)
 {
   static const char* PIXEL_DATA = "7FE00010";
@@ -3526,7 +3527,7 @@
     }
   }
 }
-
+#endif
 
 
 
--- a/OrthancFramework/UnitTestsSources/ImageTests.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/UnitTestsSources/ImageTests.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -33,10 +33,11 @@
 #include "../Sources/Images/ImageProcessing.h"
 #include "../Sources/Images/JpegReader.h"
 #include "../Sources/Images/JpegWriter.h"
+#include "../Sources/Images/PamReader.h"
+#include "../Sources/Images/PamWriter.h"
 #include "../Sources/Images/PngReader.h"
 #include "../Sources/Images/PngWriter.h"
-#include "../Sources/Images/PamReader.h"
-#include "../Sources/Images/PamWriter.h"
+#include "../Sources/OrthancException.h"
 #include "../Sources/Toolbox.h"
 
 #if ORTHANC_SANDBOXED != 1
@@ -96,14 +97,33 @@
     uint8_t *p = &image[0] + y * pitch;
     for (unsigned int x = 0; x < width; x++, p += 8)
     {
-      p[0] = (y % 8 == 0) ? 255 : 0;
-      p[1] = (y % 8 == 1) ? 255 : 0;
-      p[2] = (y % 8 == 2) ? 255 : 0;
-      p[3] = (y % 8 == 3) ? 255 : 0;
-      p[4] = (y % 8 == 4) ? 255 : 0;
-      p[5] = (y % 8 == 5) ? 255 : 0;
-      p[6] = (y % 8 == 6) ? 255 : 0;
-      p[7] = (y % 8 == 7) ? 255 : 0;
+      switch (Orthanc::Toolbox::DetectEndianness())
+      {
+        case Orthanc::Endianness_Little:
+          p[0] = (y % 8 == 0) ? 255 : 0;
+          p[1] = (y % 8 == 1) ? 255 : 0;
+          p[2] = (y % 8 == 2) ? 255 : 0;
+          p[3] = (y % 8 == 3) ? 255 : 0;
+          p[4] = (y % 8 == 4) ? 255 : 0;
+          p[5] = (y % 8 == 5) ? 255 : 0;
+          p[6] = (y % 8 == 6) ? 255 : 0;
+          p[7] = (y % 8 == 7) ? 255 : 0;
+          break;
+
+        case Orthanc::Endianness_Big:
+          p[0] = (y % 8 == 1) ? 255 : 0;
+          p[1] = (y % 8 == 0) ? 255 : 0;
+          p[2] = (y % 8 == 3) ? 255 : 0;
+          p[3] = (y % 8 == 2) ? 255 : 0;
+          p[4] = (y % 8 == 5) ? 255 : 0;
+          p[5] = (y % 8 == 4) ? 255 : 0;
+          p[6] = (y % 8 == 7) ? 255 : 0;
+          p[7] = (y % 8 == 6) ? 255 : 0;
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
     }
   }
 
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -280,6 +280,48 @@
   }
 }
 
+TEST(Toolbox, GetSetIntersection)
+{
+  {
+    std::set<int> target;
+    std::set<int> a;
+    std::set<int> b;
+
+    Toolbox::GetIntersection(target, a, b);
+    ASSERT_EQ(0u, target.size());
+  }
+
+  {
+    std::set<int> target;
+    std::set<int> a;
+    std::set<int> b;
+
+    a.insert(1);
+    b.insert(1);
+
+    Toolbox::GetIntersection(target, a, b);
+    ASSERT_EQ(1u, target.size());
+    ASSERT_EQ(1u, target.count(1));
+  }
+
+  {
+    std::set<int> target;
+    std::set<int> a;
+    std::set<int> b;
+
+    a.insert(1);
+    a.insert(2);
+    b.insert(2);
+
+    Toolbox::GetIntersection(target, a, b);
+    ASSERT_EQ(1u, target.size());
+    ASSERT_EQ(0u, target.count(1));
+    ASSERT_EQ(1u, target.count(2));
+  }
+
+}
+
+
 TEST(Toolbox, JoinStrings)
 {
   {
--- a/OrthancServer/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -379,6 +379,8 @@
   ${AUTOGENERATED_SOURCES}
   )
 
+DefineSourceBasenameForTarget(CoreLibrary)
+
 add_dependencies(CoreLibrary AutogeneratedTarget)
 
 if (LIBICU_LIBRARIES)
@@ -394,8 +396,11 @@
   add_custom_command(
     COMMAND
     ${PROTOC_EXECUTABLE} ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto --cpp_out=${AUTOGENERATED_DIR} -I${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc
+    COMMAND
+    ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/Resources/PreventProtobufDirectoryLeaks.py ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.cc
     DEPENDS
     ProtobufCompiler
+    ${CMAKE_SOURCE_DIR}/Resources/PreventProtobufDirectoryLeaks.py
     ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto
     OUTPUT
     ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.cc
@@ -420,6 +425,8 @@
   ${ORTHANC_SERVER_SOURCES}
   )
 
+DefineSourceBasenameForTarget(ServerLibrary)
+
 # Ensure autogenerated code is built before building ServerLibrary
 add_dependencies(ServerLibrary CoreLibrary OrthancDatabaseProtobuf)
 
@@ -428,6 +435,8 @@
   ${ORTHANC_RESOURCES}
   )
 
+DefineSourceBasenameForTarget(Orthanc)
+
 target_link_libraries(Orthanc ServerLibrary CoreLibrary ${DCMTK_LIBRARIES})
 
 if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
@@ -458,6 +467,8 @@
   ${BOOST_EXTENDED_SOURCES}
   )
 
+DefineSourceBasenameForTarget(UnitTests)
+
 target_link_libraries(UnitTests
   ServerLibrary
   CoreLibrary
@@ -506,6 +517,8 @@
     ${PLUGINS_DEPENDENCIES_SOURCES}
     )
 
+  DefineSourceBasenameForTarget(PluginsDependencies)
+
   # Add the "-fPIC" option as this static library must be embedded
   # inside shared libraries (important on UNIX)
   set_target_properties(
@@ -546,6 +559,8 @@
     ${SERVE_FOLDERS_RESOURCES}
     )
 
+  DefineSourceBasenameForTarget(ServeFolders)
+
   target_link_libraries(ServeFolders PluginsDependencies)
 
   set_target_properties(
@@ -594,6 +609,8 @@
     ${MODALITY_WORKLISTS_RESOURCES}
     )
 
+  DefineSourceBasenameForTarget(ModalityWorklists)
+
   target_link_libraries(ModalityWorklists PluginsDependencies)
 
   set_target_properties(
@@ -655,7 +672,9 @@
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp
     ${CONNECTIVITY_CHECKS_RESOURCES}
     )
-  
+
+  DefineSourceBasenameForTarget(ConnectivityChecks)
+
   target_link_libraries(ConnectivityChecks PluginsDependencies)
   
   set_target_properties(
@@ -708,6 +727,8 @@
     ${DELAYED_DELETION_RESOURCES}
     )
   
+  DefineSourceBasenameForTarget(DelayedDeletion)
+
   target_link_libraries(DelayedDeletion PluginsDependencies)
   
   set_target_properties(
@@ -755,6 +776,8 @@
     ${HOUSEKEEPER_RESOURCES}
     )
   
+  DefineSourceBasenameForTarget(Housekeeper)
+
   target_link_libraries(Housekeeper PluginsDependencies)
   
   set_target_properties(
@@ -820,6 +843,8 @@
     ${MULTITENANT_DICOM_RESOURCES}
     )
   
+  DefineSourceBasenameForTarget(MultitenantDicom)
+
   target_link_libraries(MultitenantDicom PluginsDependencies ${DCMTK_LIBRARIES})
   
   set_target_properties(
@@ -865,6 +890,7 @@
   endif()
 
   add_executable(OrthancRecoverCompressedFile ${RECOVER_COMPRESSED_SOURCES})
+  DefineSourceBasenameForTarget(OrthancRecoverCompressedFile)
 
   target_link_libraries(OrthancRecoverCompressedFile CoreLibrary)
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Sep 20 09:02:31 2023 +0200
@@ -245,6 +245,7 @@
     OrthancPluginErrorCode_DatabaseCannotSerialize = 42    /*!< Database could not serialize access due to concurrent update, the transaction should be retried */,
     OrthancPluginErrorCode_Revision = 43    /*!< A bad revision number was provided, which might indicate conflict between multiple writers */,
     OrthancPluginErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
+    OrthancPluginErrorCode_ForbiddenAccess = 45    /*!< Access to a resource is forbidden */,
     OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
--- a/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -25,3 +25,5 @@
 include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
 
 add_library(AutomatedJpeg2kCompression SHARED Plugin.cpp)
+
+DefineSourceBasenameForTarget(AutomatedJpeg2kCompression)
--- a/OrthancServer/Plugins/Samples/Basic/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/Basic/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -25,3 +25,5 @@
 include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
 
 add_library(PluginTest SHARED Plugin.c)
+
+DefineSourceBasenameForTarget(PluginTest)
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -1670,15 +1670,16 @@
       return true;
     }
 
+#ifdef _MSC_VER
+#define ORTHANC_SCANF sscanf_s
+#else
+#define ORTHANC_SCANF sscanf
+#endif
+
     // Parse the version
-    int aa, bb, cc;
-    if (
-#ifdef _MSC_VER
-      sscanf_s
-#else
-      sscanf
-#endif
-      (version, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 ||
+    int aa, bb, cc = 0;
+    if ((ORTHANC_SCANF(version, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 &&
+         ORTHANC_SCANF(version, "%4d.%4d", &aa, &bb) != 2) ||
       aa < 0 ||
       bb < 0 ||
       cc < 0)
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -77,6 +77,8 @@
   Plugin.cpp
   )
 
+DefineSourceBasenameForTarget(ConnectivityChecks)
+
 set_target_properties(
   ConnectivityChecks PROPERTIES 
   VERSION ${PLUGIN_VERSION} 
--- a/OrthancServer/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -25,3 +25,5 @@
 include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
 
 add_library(PluginTest SHARED Plugin.cpp)
+
+DefineSourceBasenameForTarget(PluginTest)
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -78,6 +78,8 @@
   Plugin.cpp
   )
 
+DefineSourceBasenameForTarget(DelayedDeletion)
+
 set_target_properties(
   DelayedDeletion PROPERTIES 
   VERSION ${PLUGIN_VERSION} 
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -45,7 +45,7 @@
 static bool triggerOnMainDicomTagsChange_ = true;
 static bool triggerOnUnnecessaryDicomAsJsonFiles_ = true;
 static bool triggerOnIngestTranscodingChange_ = true;
-
+static bool triggerOnDicomWebCacheChange_ = true;
 
 struct RunningPeriod
 {
@@ -166,6 +166,7 @@
   std::string seriesMainDicomTagsSignature;
   std::string instancesMainDicomTagsSignature;
   std::string ingestTranscoding;
+  std::string dicomWebVersion;
   bool storageCompressionEnabled;
 
   DbConfiguration()
@@ -186,6 +187,7 @@
     seriesMainDicomTagsSignature.clear();
     instancesMainDicomTagsSignature.clear();
     ingestTranscoding.clear();
+    dicomWebVersion.clear();
   }
 
   void ToJson(Json::Value& target)
@@ -210,6 +212,7 @@
       target["OrthancVersion"] = orthancVersion;
       target["StorageCompressionEnabled"] = storageCompressionEnabled;
       target["IngestTranscoding"] = ingestTranscoding;
+      target["DicomWebVersion"] = dicomWebVersion;
     }
   }
 
@@ -218,6 +221,14 @@
     if (!source.isNull())
     {
       orthancVersion = source["OrthancVersion"].asString();
+      if (source.isMember("DicomWebVersion"))
+      {
+        dicomWebVersion = source["DicomWebVersion"].asString();
+      }
+      else
+      {
+        dicomWebVersion = "1.14"; // the first change that requires processing has been introduced between 1.14 & 1.15
+      }
 
       const Json::Value& signatures = source["MainDicomTagsSignature"];
       patientsMainDicomTagsSignature = signatures["Patient"].asString();
@@ -321,6 +332,7 @@
     pluginStatus_.lastTimeStarted = boost::date_time::not_a_date_time;
     
     pluginStatus_.lastProcessedConfiguration.orthancVersion = "1.9.0"; // when we don't know, we assume some files were stored with Orthanc 1.9.0 (last version saving the dicom-as-json files)
+    pluginStatus_.lastProcessedConfiguration.dicomWebVersion = "1.14"; // the first change that requires processing has been introduced between 1.14 & 1.15
 
     // default main dicom tags signature are the one from Orthanc 1.4.2 (last time the list was changed):
     pluginStatus_.lastProcessedConfiguration.patientsMainDicomTagsSignature = "0010,0010;0010,0020;0010,0030;0010,0040;0010,1000";
@@ -360,12 +372,19 @@
   configuration.ingestTranscoding = systemInfo["IngestTranscoding"].asString();
 
   configuration.orthancVersion = OrthancPlugins::GetGlobalContext()->orthancVersion;
+
+  Json::Value pluginInfo;
+  if (OrthancPlugins::RestApiGet(pluginInfo, "/plugins/dicom-web", false))
+  {
+    configuration.dicomWebVersion = pluginInfo["Version"].asString();
+  }
 }
 
-static void CheckNeedsProcessing(bool& needsReconstruct, bool& needsReingest, const DbConfiguration& current, const DbConfiguration& last)
+static void CheckNeedsProcessing(bool& needsReconstruct, bool& needsReingest, bool& needsDicomWebCaching, const DbConfiguration& current, const DbConfiguration& last)
 {
   needsReconstruct = false;
   needsReingest = false;
+  needsDicomWebCaching = false;
 
   if (!last.IsDefined())
   {
@@ -474,9 +493,37 @@
     }
   }
 
+  if (!current.dicomWebVersion.empty())
+  {
+    if (last.dicomWebVersion.empty())
+    {
+      if (triggerOnDicomWebCacheChange_)
+      {
+        OrthancPlugins::LogWarning("Housekeeper: DicomWEB plugin is enabled and the housekeeper has never run, you might miss series metadata cache -> will perform housekeeping");
+      }
+      needsDicomWebCaching = triggerOnDicomWebCacheChange_;
+    }
+    else
+    {
+      const char* lastDicomWebVersion = last.dicomWebVersion.c_str();
+
+      if (!OrthancPlugins::CheckMinimalVersion(lastDicomWebVersion, 1, 15, 0))
+      {
+        if (triggerOnDicomWebCacheChange_)
+        {
+          OrthancPlugins::LogWarning("Housekeeper: DicomWEB plugin might miss series metadata cache -> will perform housekeeping");
+          needsDicomWebCaching = true;
+        }
+        else
+        {
+          OrthancPlugins::LogWarning("Housekeeper: DicomWEB plugin might miss series metadata cache but the trigger has been disabled");
+        }
+      }
+    }
+  }
 }
 
-static bool ProcessChanges(bool needsReconstruct, bool needsReingest, const DbConfiguration& currentDbConfiguration)
+static bool ProcessChanges(bool needsReconstruct, bool needsReingest, bool needsDicomWebCaching, const DbConfiguration& currentDbConfiguration)
 {
   Json::Value changes;
 
@@ -498,12 +545,22 @@
       if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed
       {
         Json::Value result;
-        Json::Value request;
-        if (needsReingest)
+
+        if (needsReconstruct)
         {
-          request["ReconstructFiles"] = true;
+          Json::Value request;
+          if (needsReingest)
+          {
+            request["ReconstructFiles"] = true;
+          }
+          OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
         }
-        OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
+
+        if (needsDicomWebCaching)
+        {
+          Json::Value request;
+          OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/update-dicomweb-cache", request, true);
+        }
       }
 
       {
@@ -554,13 +611,14 @@
   bool needsReingest = false;
   bool needsFullProcessing = false;
   bool needsProcessing = false;
+  bool needsDicomWebCaching = false;
 
   {
     boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
 
     // compare with last full processed configuration
-    CheckNeedsProcessing(needsReconstruct, needsReingest, currentDbConfiguration, pluginStatus_.lastProcessedConfiguration);
-    needsFullProcessing = needsReconstruct || needsReingest;
+    CheckNeedsProcessing(needsReconstruct, needsReingest, needsDicomWebCaching, currentDbConfiguration, pluginStatus_.lastProcessedConfiguration);
+    needsFullProcessing = needsReconstruct || needsReingest || needsDicomWebCaching;
     needsProcessing = needsFullProcessing;
 
       // if a processing was in progress, check if the config has changed since
@@ -570,9 +628,10 @@
 
       bool needsReconstruct2 = false;
       bool needsReingest2 = false;
+      bool needsDicomWebCaching2 = false;
 
-      CheckNeedsProcessing(needsReconstruct2, needsReingest2, currentDbConfiguration, pluginStatus_.currentlyProcessingConfiguration);
-      needsFullProcessing = needsReconstruct2 || needsReingest2;  // if the configuration has changed compared to the config being processed, we need a full processing again
+      CheckNeedsProcessing(needsReconstruct2, needsReingest2, needsDicomWebCaching2, currentDbConfiguration, pluginStatus_.currentlyProcessingConfiguration);
+      needsFullProcessing = needsReconstruct2 || needsReingest2 || needsDicomWebCaching2;  // if the configuration has changed compared to the config being processed, we need a full processing again
     }
   }
 
@@ -621,7 +680,7 @@
   {
     if (runningPeriods_.isInPeriod())
     {
-      completed = ProcessChanges(needsReconstruct, needsReingest, currentDbConfiguration);
+      completed = ProcessChanges(needsReconstruct, needsReingest, needsDicomWebCaching, currentDbConfiguration);
       SaveStatusInDb();
       
       if (!completed)
@@ -775,7 +834,8 @@
             "Triggers" : {
               "StorageCompressionChange": true,
               "MainDicomTagsChange": true,
-              "UnnecessaryDicomAsJsonFiles": true
+              "UnnecessaryDicomAsJsonFiles": true,
+              "DicomWebCacheChange": true   // new in 1.12.2
             }
 
           }
@@ -789,11 +849,14 @@
 
       if (housekeeper.GetJson().isMember("Triggers"))
       {
-        triggerOnStorageCompressionChange_ = housekeeper.GetBooleanValue("StorageCompressionChange", true);
+        OrthancPlugins::OrthancConfiguration triggers;
+        housekeeper.GetSection(triggers, "Triggers");
+        triggerOnStorageCompressionChange_ = triggers.GetBooleanValue("StorageCompressionChange", true);
 
-        triggerOnMainDicomTagsChange_ = housekeeper.GetBooleanValue("MainDicomTagsChange", true);
-        triggerOnUnnecessaryDicomAsJsonFiles_ = housekeeper.GetBooleanValue("UnnecessaryDicomAsJsonFiles", true);
-        triggerOnIngestTranscodingChange_ = housekeeper.GetBooleanValue("IngestTranscodingChange", true);
+        triggerOnMainDicomTagsChange_ = triggers.GetBooleanValue("MainDicomTagsChange", true);
+        triggerOnUnnecessaryDicomAsJsonFiles_ = triggers.GetBooleanValue("UnnecessaryDicomAsJsonFiles", true);
+        triggerOnIngestTranscodingChange_ = triggers.GetBooleanValue("IngestTranscodingChange", true);
+        triggerOnDicomWebCacheChange_ = triggers.GetBooleanValue("DicomWebCacheChange", true);
       }
 
       if (housekeeper.GetJson().isMember("Schedule"))
@@ -802,7 +865,7 @@
       }
 
       OrthancPluginRegisterOnChangeCallback(c, OnChangeCallback);
-      OrthancPluginRegisterRestCallback(c, "/housekeeper/status", GetPluginStatus);   // for bacward compatiblity with version 1.11.0
+      OrthancPluginRegisterRestCallback(c, "/housekeeper/status", GetPluginStatus);   // for backward compatiblity with version 1.11.0
       OrthancPluginRegisterRestCallback(c, "/plugins/housekeeper/status", GetPluginStatus);
     }
     else
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -55,6 +55,8 @@
   ${AUTOGENERATED_SOURCES}
   )
 
+DefineSourceBasenameForTarget(MultitenantDicom)
+
 target_link_libraries(MultitenantDicom ${DCMTK_LIBRARIES})
 
 message("Setting the version of the plugin to ${ORTHANC_PLUGIN_VERSION}")
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.h	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.h	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/Plugin.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/Plugin.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
@@ -120,6 +119,10 @@
       return -1;
     }
 
+    // For static builds, this is not required - but does not harm - because the dictionary seems to be shared between the Orthanc Core and the plugin.
+    // For dynamic builds, this is however required.  See https://discourse.orthanc-server.org/t/dimse-failure-using-multitenant-plugin/3665
+    Orthanc::FromDcmtkBridge::InitializeDictionary(false /* loadPrivateDictionary */);
+
     /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */
     dcmDisableGethostbyaddr.set(OFTrue);
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/PluginEnumerations.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginEnumerations.h	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.h	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h	Wed Sep 20 09:02:31 2023 +0200
@@ -6,18 +6,17 @@
  * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
+ * General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
 
--- a/OrthancServer/Plugins/Samples/Sanitizer/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/Sanitizer/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -51,6 +51,8 @@
     ${ORTHANC_DICOM_SOURCES}
     )
 
+DefineSourceBasenameForTarget(Sanitizer)
+
 target_link_libraries(Sanitizer ${DCMTK_LIBRARIES})
 
 
--- a/OrthancServer/Plugins/Samples/WebSkeleton/CMakeLists.txt	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/CMakeLists.txt	Wed Sep 20 09:02:31 2023 +0200
@@ -32,3 +32,5 @@
 add_library(WebSkeleton SHARED 
   ${AUTOGENERATED_SOURCES}
   )
+
+DefineSourceBasenameForTarget(WebSkeleton)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/PreventProtobufDirectoryLeaks.py	Wed Sep 20 09:02:31 2023 +0200
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2023 Osimis S.A., Belgium
+# Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import sys
+
+if len(sys.argv) != 2:
+    raise Exception('Bad number of arguments in %s' % sys.argv[0])
+
+with open(sys.argv[1], 'r') as f:
+    s = f.read()
+
+s = s.replace('__FILE__', '__ORTHANC_FILE__')
+
+s = """
+#if !defined(__ORTHANC_FILE__)
+#  define __ORTHANC_FILE__ __FILE__
+#endif
+""" + s
+
+with open(sys.argv[1], 'w') as f:
+    f.write(s)
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -52,6 +52,12 @@
 #include <dcmtk/dcmnet/diutil.h>  // For DCM_dcmnetLogger
 
 #if ORTHANC_ENABLE_PLUGINS == 1
+#  if defined(__ORTHANC_FILE__)
+//   Prevents the system-wide Google Protobuf library from leaking the
+//   full path of this source file
+#    undef __FILE__
+#    define __FILE__ __ORTHANC_FILE__
+#  endif
 #  include <google/protobuf/any.h>
 #endif
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -729,6 +729,34 @@
     }
   }
 
+  static void DeleteJobInfo(RestApiDeleteCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("Jobs")
+        .SetSummary("Delete a job from history")
+        .SetDescription("Delete the job from the jobs history.  Only a completed job can be deleted. "
+                        "If the job has not run or not completed yet, you must cancel it first. "
+                        "If the job has outputs, all outputs will be deleted as well. ")
+        .SetUriArgument("id", "Identifier of the job of interest");
+      return;
+    }
+
+    std::string job = call.GetUriComponent("id", "");
+
+    if (OrthancRestApi::GetContext(call).GetJobsEngine().
+        GetRegistry().DeleteJobInfo(job))
+    {
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentItem,
+                             "No job found with this id: " + job);
+    }
+  }
+
 
   static void GetJobOutput(RestApiGetCall& call)
   {
@@ -1138,6 +1166,7 @@
 
     Register("/jobs", ListJobs);
     Register("/jobs/{id}", GetJobInfo);
+    Register("/jobs/{id}", DeleteJobInfo);
     Register("/jobs/{id}/cancel", ApplyJobAction<JobAction_Cancel>);
     Register("/jobs/{id}/pause", ApplyJobAction<JobAction_Pause>);
     Register("/jobs/{id}/resubmit", ApplyJobAction<JobAction_Resubmit>);
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -1482,4 +1482,9 @@
       return false;
     }
   }
+
+  void ArchiveJob::DeleteAllOutputs()
+  {
+    DeleteOutput("archive");
+  }
 }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Wed Sep 20 09:02:31 2023 +0200
@@ -122,5 +122,7 @@
                            const std::string& key) ORTHANC_OVERRIDE;
 
     virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE;
+
+    virtual void DeleteAllOutputs() ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/main.cpp	Tue Sep 19 10:56:09 2023 +0200
+++ b/OrthancServer/Sources/main.cpp	Wed Sep 20 09:02:31 2023 +0200
@@ -819,6 +819,7 @@
     PrintErrorCode(ErrorCode_DatabaseCannotSerialize, "Database could not serialize access due to concurrent update, the transaction should be retried");
     PrintErrorCode(ErrorCode_Revision, "A bad revision number was provided, which might indicate conflict between multiple writers");
     PrintErrorCode(ErrorCode_MainDicomTagsMultiplyDefined, "A main DICOM Tag has been defined multiple times for the same resource level");
+    PrintErrorCode(ErrorCode_ForbiddenAccess, "Access to a resource is forbidden");
     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
--- a/README	Tue Sep 19 10:56:09 2023 +0200
+++ b/README	Wed Sep 20 09:02:31 2023 +0200
@@ -38,6 +38,14 @@
 * Cross-compilation for Windows under GNU/Linux, with MinGW.
 
 
+Contributing
+------------
+
+Instructions for contributing to the Orthanc project are included in
+the Orthanc Book:
+https://book.orthanc-server.com/developers/repositories.html
+
+
 Licensing
 ---------
 
--- a/TODO	Tue Sep 19 10:56:09 2023 +0200
+++ b/TODO	Wed Sep 20 09:02:31 2023 +0200
@@ -115,15 +115,20 @@
   https://groups.google.com/g/orthanc-users/c/r20kDb0axms/m/2tzbQzYJAgAJ
 * save more details in jobs e.g: the resources being sent/exported ...
   https://groups.google.com/g/orthanc-users/c/rDDusFG5Lco/m/TzTUjWXLAQAJ
+  https://discourse.orthanc-server.org/t/some-confusion-about-jobs-function/3887
 * allow filtering/ordering on the /jobs route:
   https://groups.google.com/g/orthanc-users/c/hsZ1jng5rIg/m/8xZL2C1VBgAJ
+* add an "AutoDeleteIfSuccessful": false option when creating jobs 
+  https://discourse.orthanc-server.org/t/job-history-combined-with-auto-forwarding/3729/10
 * Also implement a GET variant of /tools/create-archive + sibling routes
   in which resources & transcode options are provided as get arguments.
   https://groups.google.com/g/orthanc-users/c/PmaRZ609ztA/m/JdwXvIBKAQAJ
 * Allow skiping automatic conversion of color-space in transcoding/decoding.
   The patch that was initialy provided was breaking the IngestTranscoding.
   https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533/9
-
+* Implement a 'commit' route to force the Stable status earlier.
+  https://discourse.orthanc-server.org/t/expediting-stability-of-a-dicom-study-new-api-endpoint/1684
+  
 
 ---------
 Long-term
@@ -173,6 +178,7 @@
 * Support multiple specific character sets (cf. "SCSH32" in orthanc-tests)
   - http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
   - Japanese test: http://dicom.nema.org/MEDICAL/dicom/2017c/output/chtml/part05/sect_H.3.2.html
+  https://discourse.orthanc-server.org/t/garbled-characters-when-i-uploaded-japanese-patient-name/3204/5
 * Support Supplementary Kanji set (ISO 2022 IR 159)
 * Create DICOM files with multibyte encodings (Korean, JapaneseKanji, SimplifiedChinese)
 
@@ -191,8 +197,6 @@
     - ModalitiesInStudy
     - all computed counters at series/study/patient level
     - RequestAttributesSequence (sequence that must be included in all DicomWeb QIDO-RS for series)
-* Investigate increase of CPU consumption while idle after anonymize/delete
-  (https://discourse.orthanc-server.org/t/onchange-callbacks-and-cpu-loads/3534)
 
 * Long-shot & not sure it is even feasible at all: try to reduce memory usage by implementing streaming
   when receiving DICOM instances from the Rest API or from DICOM and store files directly to disk as they
@@ -274,8 +278,6 @@
 Code refactoring
 ================
 
-* Use Semaphore::Locker everywhere (instead of explicit
-  Release() and Acquire())
 * Avoid direct calls to FromDcmtkBridge (make most of its 
   methods private), go through ParsedDicomFile wherever possible