changeset 5482:4b51cf06b697 pg-transactions

merge mainline -> pg-transactions
author Alain Mazy <am@osimis.io>
date Thu, 21 Dec 2023 16:13:00 +0100
parents dceed5e3d6a9 (current diff) f7ac06300f86 (diff)
children 26877f4b306f
files OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Sources/ServerIndex.cpp
diffstat 20 files changed, 191 insertions(+), 123 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Dec 15 17:15:43 2023 +0100
+++ b/NEWS	Thu Dec 21 16:13:00 2023 +0100
@@ -1,73 +1,67 @@
 Pending changes in the mainline
 ===============================
 
+
+Version 1.12.2 (2023-12-19)
+===========================
+
 General
 -------
 
 * Performance:
   - Allow multiple plugins to use the plugin SDK at the same time.  In previous versions,
     functions like instance transcoding or instance reading where mutually exclusive.
-    This can bring some significant improvements particularly in viewers.
+    This can bring some significant improvements, especially in viewers.
   - Optimized the StorageCache to prevent loading the same file multiple times if
     multiple users request the same file at the same time.
   - The StorageCache is now also storing transcoded instances that have been requested by /file?transcode=...
     that is now used by the DICOMweb plugin.  This speeds up retrieval of transcoded frames through WADO-RS.
   - Now displaying timings when reading from/writing to disk in the verbose logs.
-* 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.
-* HTTP Compression:
+* HTTP compression:
   - The default value of the "HttpCompressionEnabled" is now false by default.  This reduces
     the Orthanc overall CPU usage and latency.  This is suitable for setups with large  
     bandwidth network like LAN.
   - When "HttpCompressionEnabled" is true, only the content that is clearly identified as
     compressible is compressed (JSON, XML, HTML, text, ...).  DICOM files are never
     compressed over HTTP.  In prior versions, all content types were compressed.
-    This notably greatly improve loading time of large DICOM 
+    This notably greatly improves loading time of large DICOM 
     files through WADO-RS e.g in StoneViewer when working on large bandwidth networks.
   - When "HttpCompressionEnabled" is true, content < 2KB are never compressed.
 * Logs:
   - Each line of log now contains the name of the thread that is logging the message.
-    A new '--logs-no-thread' command line option can be used to get back to the previous behavior to
+    A new "--logs-no-thread" command line option can be used to get back to the previous behavior to
     keep backward compatibility.
 
-
-Bug Fixes
----------
-
-* Solved a deadlock related to the Job Engine events and plugins.  Job events are now pushed
-  into a queue to be handled asynchronously by plugins.
-* Zip of studies whose PatientName and PatientID did not contain any ASCII character are now valid.
-
-
 REST API
 --------
 
 * API version upgraded to 22
 * Added a route to delete completed jobs from history: DELETE /jobs/{id}
-* added a "transcode" option to the /file route:
+* Added a "transcode" option to the /file route:
   e.g: /instances/../file?transcode=1.2.840.10008.1.2.4.80
 * now accepting GET requests on these 3 routes to create archive/media:
   /tools/create-archive?resources=..,..2&transcode=1.2.840.10008.1.2.4.80
   /tools/create-media?resources=..,..2&transcode=1.2.840.10008.1.2.4.80
   /tools/create-media-extended?resources=..,..2&transcode=1.2.840.10008.1.2.4.80
-* All 'expand' GET arguments now accepts expand=true and expand=false values.
+* All "expand" GET arguments now accepts "expand=true" and "expand=false" values.
   The /studies/../instances and sibling routes are the only whose expand is true if not specified.
-  These routes now accepts expand=false to simply list the child resources ids.
+  These routes now accepts "expand=false" to simply list the child resources ids.
 * In /tools/metrics-prometheus:
-  - 'orthanc_dicom_cache_size' renamed into 'orthanc_dicom_cache_size_mb'
-  - added 'orthanc_storage_cache_count' and 'orthanc_storage_cache_size_mb'
-
+  - "orthanc_dicom_cache_size" renamed as "orthanc_dicom_cache_size_mb"
+  - added "orthanc_storage_cache_count" and "orthanc_storage_cache_size_mb"
 
 Plugins
 -------
 
+* 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.
+  - Introduced a "sleep" to lower CPU usage when idle.
 * Plugins are now allowed to modify/delete private metadata/attachments
   (i.e. whose identifiers are < 1024)
 * Added "OrthancPluginSetCurrentThreadName()" in the plugin SDK.
 
-
 Maintenance
 -----------
 
@@ -76,24 +70,26 @@
 * 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
-* Housekeeper: Introduced a 'sleep' to lower CPU usage when idle.
 * Support multiple values in SpecificCharacterSet in C-Find answers:
   https://discourse.orthanc-server.org/t/c-find-fails-on-unknown-specific-character-set-iso-2022-ir-6-iso-2022-ir-100/3947
 * When exporting a study archive, make sure to use the PatientName from the study and not from the patient
   in case of PatientID collision.
 * DICOM C-Store:
-  - Avoid some unneccessary renegotiation of DICOM association.
+  - Avoid some unnecessary renegotiation of DICOM association.
   - Force renegotiation in case no presentation context were accepted in previous association (we have
     observed PACS that were not consistent in the accepted presentation contexts)
   - Improved logging
-* Upgraded dependencies for static builds:
-  - boost 1.83.0
-* Upgraded minizip library to stay away from CVE-2023-45853 although Orthanc is likely not affected since zip
+* Solved a deadlock related to the Job Engine events and plugins.  Job events are now pushed
+  into a queue to be handled asynchronously by plugins.
+* ZIP of studies whose PatientName and PatientID did not contain any ASCII character are now valid.
+* Upgraded minizip library to stay away from CVE-2023-45853 although Orthanc is likely not affected since ZIP
   filenames are based on DICOM Tag values whose length is limited in size.
   Great thanks to James Addison for notifying us about the vulnerability and patch to apply !
 * Fix XSS in Orthanc error reporting (as reported by Sébastien Doria, Vumetric Cybersecurity) by:
-  - always including a 'Content-Type' header in HTTP responses with a body.
-  - always including 'X-Content-Type-Options: nosniff'
+  - always including a "Content-Type" header in HTTP responses with a body.
+  - always including "X-Content-Type-Options: nosniff"
+* Upgraded dependencies for static builds:
+  - boost 1.83.0
 
 
 Version 1.12.1 (2023-07-04)
--- a/OrthancFramework/Resources/CMake/Compiler.cmake	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancFramework/Resources/CMake/Compiler.cmake	Thu Dec 21 16:13:00 2023 +0100
@@ -124,12 +124,15 @@
     ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
     ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
 
-  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND
+  if (# NOT ${CMAKE_SYSTEM_VERSION} STREQUAL "LinuxStandardBase" AND
+      NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND
       NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
     # The "--no-undefined" linker flag makes the shared libraries
     # (plugins ModalityWorklists and ServeFolders) fail to compile on
     # OpenBSD, and make the PostgreSQL plugin complain about missing
-    # "environ" global variable in FreeBSD
+    # "environ" global variable in FreeBSD. Furthermore, on Linux
+    # Standard Base running on Debian 12, the "-Wl,--no-undefined"
+    # breaks the compilation (added after Orthanc 1.12.2).
     set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined")
     set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
   endif()
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Thu Dec 21 16:13:00 2023 +0100
@@ -158,6 +158,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "d32a0cde03b6eb603d8dd2b33d38bf1b")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.1")
         set(ORTHANC_FRAMEWORK_MD5 "8a435140efc8ff4a01d8242f092f21de")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.2")
+        set(ORTHANC_FRAMEWORK_MD5 "d2476b9e796e339ac320b5333489bdb3")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.h	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.h	Thu Dec 21 16:13:00 2023 +0100
@@ -48,7 +48,7 @@
   class ORTHANC_PUBLIC MemoryStringCache : public boost::noncopyable
   {
   public:
-    class Accessor : public boost::noncopyable
+    class ORTHANC_PUBLIC Accessor : public boost::noncopyable
     {
     protected:
       MemoryStringCache& cache_;
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Thu Dec 21 16:13:00 2023 +0100
@@ -37,6 +37,7 @@
 #include "FromDcmtkBridge.h"
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../Toolbox.h"
 
 #include <dcmtk/dcmdata/dcdeftag.h>
 #include <dcmtk/dcmjpeg/djrploss.h>  // for DJ_RPLossy
@@ -83,12 +84,33 @@
     return lossyQuality_;
   }
 
+  bool TryTranscode(std::vector<std::string>& failureReasons, /* out */
+                    DicomTransferSyntax& selectedSyntax, /* out*/
+                    DcmFileFormat& dicom, /* in/out */
+                    const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                    DicomTransferSyntax trySyntax)
+  {
+    if (allowedSyntaxes.find(trySyntax) != allowedSyntaxes.end())
+    {
+      if (FromDcmtkBridge::Transcode(dicom, trySyntax, NULL))
+      {
+        selectedSyntax = trySyntax;
+        return true;
+      }
+
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(trySyntax));
+    }
+    return false;
+  }
 
   bool DcmtkTranscoder::InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
-                                         DcmFileFormat& dicom,
+                                         std::string& failureReason /* out */,
+                                         DcmFileFormat& dicom, /* in/out */
                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
                                          bool allowNewSopInstanceUid) 
   {
+    std::vector<std::string> failureReasons;
+
     if (dicom.getDataset() == NULL)
     {
       throw OrthancException(ErrorCode_InternalError);
@@ -109,62 +131,75 @@
       // No transcoding is needed
       return true;
     }
-      
-    if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianImplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianImplicit, NULL))
+    
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_LittleEndianImplicit))
+    {
+      return true;
+    }
+
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_LittleEndianExplicit))
     {
-      selectedSyntax = DicomTransferSyntax_LittleEndianImplicit;
+      return true;
+    }
+
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_BigEndianExplicit))
+    {
+      return true;
+    }
+
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_DeflatedLittleEndianExplicit))
+    {
       return true;
     }
 
-    if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianExplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianExplicit, NULL))
-    {
-      selectedSyntax = DicomTransferSyntax_LittleEndianExplicit;
-      return true;
-    }
-      
-    if (allowedSyntaxes.find(DicomTransferSyntax_BigEndianExplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_BigEndianExplicit, NULL))
-    {
-      selectedSyntax = DicomTransferSyntax_BigEndianExplicit;
-      return true;
-    }
-
-    if (allowedSyntaxes.find(DicomTransferSyntax_DeflatedLittleEndianExplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL))
-    {
-      selectedSyntax = DicomTransferSyntax_DeflatedLittleEndianExplicit;
-      return true;
-    }
 
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end() &&
-        allowNewSopInstanceUid &&
-        (!hasBitsStored || bitsStored == 8))
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end())
     {
-      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-      DJ_RPLossy parameters(lossyQuality_);
-        
-      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, &parameters))
+      if (!allowNewSopInstanceUid)
+      {
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess1) + " without generating new SOPInstanceUID");
+      }
+      else if (hasBitsStored && bitsStored != 8)
+      {
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess1) + " if BitsStored != 8");
+      }
+      else
       {
-        selectedSyntax = DicomTransferSyntax_JPEGProcess1;
-        return true;
+        // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+        DJ_RPLossy parameters(lossyQuality_);
+          
+        if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, &parameters))
+        {
+          selectedSyntax = DicomTransferSyntax_JPEGProcess1;
+          return true;
+        }
+        failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess1));
       }
     }
 #endif
       
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end() &&
-        allowNewSopInstanceUid &&
-        (!hasBitsStored || bitsStored <= 12))
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end())
     {
-      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-      DJ_RPLossy parameters(lossyQuality_);
-      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, &parameters))
+      if (!allowNewSopInstanceUid)
+      {
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess2_4) + " without generating new SOPInstanceUID");
+      }
+      else if (hasBitsStored && bitsStored > 12)
       {
-        selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
-        return true;
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess2_4) + " if BitsStored != 8");
+      }
+      else
+      {
+        // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+        DJ_RPLossy parameters(lossyQuality_);
+        if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, &parameters))
+        {
+          selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
+          return true;
+        }
+        failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess2_4));
       }
     }
 #endif
@@ -180,6 +215,7 @@
         selectedSyntax = DicomTransferSyntax_JPEGProcess14;
         return true;
       }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess14));
     }
 #endif
       
@@ -194,6 +230,7 @@
         selectedSyntax = DicomTransferSyntax_JPEGProcess14SV1;
         return true;
       }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess14SV1));
     }
 #endif
       
@@ -213,6 +250,7 @@
         selectedSyntax = DicomTransferSyntax_JPEGLSLossless;
         return true;
       }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGLSLossless));
     }
 #endif
       
@@ -233,9 +271,11 @@
         selectedSyntax = DicomTransferSyntax_JPEGLSLossy;
         return true;
       }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGLSLossy));
     }
 #endif
 
+    Orthanc::Toolbox::JoinStrings(failureReason, failureReasons, ", ");
     return false;
   }
 
@@ -285,28 +325,27 @@
       return false;
     }
 
+    std::string failureReason;
+    std::string s;
+    for (std::set<DicomTransferSyntax>::const_iterator
+            it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it)
     {
-      std::string s;
-      for (std::set<DicomTransferSyntax>::const_iterator
-             it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it)
+      if (!s.empty())
       {
-        if (!s.empty())
-        {
-          s += ", ";
-        }
-
-        s += GetTransferSyntaxUid(*it);
+        s += ", ";
       }
 
-      if (s.empty())
-      {
-        s = "<none>";
-      }
-      
-      LOG(INFO) << "DCMTK transcoding from " << GetTransferSyntaxUid(sourceSyntax)
-                << " to one of: " << s;
+      s += GetTransferSyntaxUid(*it);
     }
 
+    if (s.empty())
+    {
+      s = "<none>";
+    }
+
+    LOG(INFO) << "DCMTK transcoding from " << GetTransferSyntaxUid(sourceSyntax)
+              << " to one of: " << s;
+
 #if !defined(NDEBUG)
     const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
 #endif
@@ -319,7 +358,7 @@
       target.AcquireBuffer(source);
       return true;
     }
-    else if (InplaceTranscode(targetSyntax, source.GetParsed(),
+    else if (InplaceTranscode(targetSyntax, failureReason, source.GetParsed(),
                               allowedSyntaxes, allowNewSopInstanceUid))
     {   
       // Sanity check
@@ -347,6 +386,8 @@
     else
     {
       // Cannot transcode
+      LOG(WARNING) << "DCMTK was unable to transcode from " << GetTransferSyntaxUid(sourceSyntax)
+                   << " to one of: " << s << " " << failureReason;
       return false;
     }
   }
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Thu Dec 21 16:13:00 2023 +0100
@@ -41,6 +41,7 @@
     unsigned int  lossyQuality_;
     
     bool InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
+                          std::string& failureReason /* out */,
                           DcmFileFormat& dicom,
                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
                           bool allowNewSopInstanceUid);
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Thu Dec 21 16:13:00 2023 +0100
@@ -44,6 +44,8 @@
 #  endif
 #endif
 
+static const std::string X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options";
+
 
 namespace Orthanc
 {
@@ -58,7 +60,8 @@
     contentLength_(0),
     contentPosition_(0),
     keepAlive_(isKeepAlive),
-    keepAliveTimeout_(keepAliveTimeout)
+    keepAliveTimeout_(keepAliveTimeout),
+    hasXContentTypeOptions_(false)
   {
   }
 
@@ -142,6 +145,11 @@
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
 
+    if (header == X_CONTENT_TYPE_OPTIONS)
+    {
+      hasXContentTypeOptions_ = true;
+    }
+
     headers_.push_back(header + ": " + value + "\r\n");
   }
 
@@ -178,9 +186,6 @@
 
     if (state_ == State_WritingHeader)
     {
-      // always include this header to prevent MIME Confusion attacks: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-content-type-options
-      AddHeader("X-Content-Type-Options", "nosniff");
-
       // Send the HTTP header before writing the body
 
       stream_.OnHttpStatusReceived(status_);
@@ -220,6 +225,13 @@
         s += *it;
       }
 
+      if (!hasXContentTypeOptions_)
+      {
+        // Always include this header to prevent MIME Confusion attacks:
+        // https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-content-type-options
+        s += X_CONTENT_TYPE_OPTIONS + ": nosniff\r\n";
+      }
+
       if (status_ != HttpStatus_200_Ok)
       {
         hasContentLength_ = false;
@@ -369,8 +381,8 @@
 
     if (messageSize > 0)
     {
-      // we assume that the body always contains a json description of the error
-      stateMachine_.SetContentType("application/json");
+      // Assume that the body always contains a textual description of the error
+      stateMachine_.SetContentType("text/plain");
     }
 
     stateMachine_.SendBody(message, messageSize);
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.h	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h	Thu Dec 21 16:13:00 2023 +0100
@@ -64,6 +64,7 @@
       bool keepAlive_;
       unsigned int keepAliveTimeout_;
       std::list<std::string> headers_;
+      bool hasXContentTypeOptions_;
 
       std::string multipartBoundary_;
       std::string multipartContentType_;
--- a/OrthancFramework/Sources/Images/Font.cpp	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancFramework/Sources/Images/Font.cpp	Thu Dec 21 16:13:00 2023 +0100
@@ -37,6 +37,7 @@
 #include "Image.h"
 #include "ImageProcessing.h"
 
+#include <cassert>
 #include <stdio.h>
 #include <memory>
 #include <boost/lexical_cast.hpp>
--- a/OrthancFramework/Sources/Images/PamWriter.cpp	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancFramework/Sources/Images/PamWriter.cpp	Thu Dec 21 16:13:00 2023 +0100
@@ -28,6 +28,7 @@
 #include "../OrthancException.h"
 #include "../Toolbox.h"
 
+#include <cassert>
 #include <boost/lexical_cast.hpp>
 
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Dec 21 16:13:00 2023 +0100
@@ -120,7 +120,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     12
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  1
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  2
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -9359,21 +9359,22 @@
   }
 
 
-/**
-   * @brief Sets the name of the current thread
-   *
-   * This function sets the name of the thread that is calling it.
-   * This name is used in the logs.  This function shall be called only from threads that
-   * the plugin has created itself.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param threadName The name of the current thread.  A Thread name can not be larger than 16 characters.
+  /**
+   * @brief Set the name of the current thread.
+   *
+   * This function gives a name to the thread that is calling this
+   * function. This name is used in the Orthanc logs. This function
+   * must only be called from threads that the plugin has created
+   * itself.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param threadName The name of the current thread. A thread name cannot be longer than 16 characters.
    * @return 0 if success, other value if error.
    * @ingroup Toolbox
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetCurrentThreadName(
-    OrthancPluginContext*                        context,
-    const char*                                  threadName)
+    OrthancPluginContext*  context,
+    const char*            threadName)
   {
     return context->InvokeService(context, _OrthancPluginService_SetCurrentThreadName, threadName);
   }
--- a/OrthancServer/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Thu Dec 21 16:13:00 2023 +0100
@@ -224,7 +224,7 @@
 
 cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w')
 
-print os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+print(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 
 cpp.write("""
 #include "%s.h"
--- a/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Thu Dec 21 16:13:00 2023 +0100
@@ -33,7 +33,7 @@
     "${AUTOGENERATED_DIR}/EmbeddedResources.h"
     "${AUTOGENERATED_DIR}/EmbeddedResources.cpp"
     COMMAND 
-    python
+    ${PYTHON_EXECUTABLE}
     "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py"
     "${AUTOGENERATED_DIR}/EmbeddedResources"
     STATIC_RESOURCES
--- a/OrthancServer/Resources/Configuration.json	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancServer/Resources/Configuration.json	Thu Dec 21 16:13:00 2023 +0100
@@ -51,7 +51,9 @@
   // In "Reject" mode, the sender will receive a 0xA700 DIMSE status code
   // if the instance was sent through C-Store, a 507 HTTP status code
   // if using the REST API and a 0xA700 Failure reason when using
-  // DicomWeb Stow-RS 
+  // DicomWeb Stow-RS.
+  // Note: this value is taken into account only if you have set 
+  // a MaximumStorageSize != 0 or a MaximumPatientCount != 0
   // Allowed values: "Recycle", "Reject"
   // (new in Orthanc 1.11.2)
   "MaximumStorageMode" : "Recycle",
--- a/OrthancServer/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancServer/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Thu Dec 21 16:13:00 2023 +0100
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
--- a/OrthancServer/Resources/Samples/ImportDicomFiles/OrthancImport.py	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancServer/Resources/Samples/ImportDicomFiles/OrthancImport.py	Thu Dec 21 16:13:00 2023 +0100
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Dec 21 16:13:00 2023 +0100
@@ -342,8 +342,10 @@
     {
       call.GetDocumentation()
         .SetTag("Patients")
-        .SetSummary("Protect one patient against recycling")
-        .SetDescription("Check out configuration options `MaximumStorageSize` and `MaximumPatientCount`")
+        .SetSummary("Protect/Unprotect a patient against recycling")
+        .SetDescription("Protects a patient by sending `1` or `true` in the payload request. "
+                        "Unprotects a patient by sending `0` or `false` in the payload requests. "
+                        "More info: https://orthanc.uclouvain.be/book/faq/features.html#recycling-protection")
         .SetUriArgument("id", "Orthanc identifier of the patient of interest");
       return;
     }
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Thu Dec 21 16:13:00 2023 +0100
@@ -40,6 +40,7 @@
 
 #include "DatabaseConstraint.h"
 
+#include <cassert>
 #include <boost/lexical_cast.hpp>
 #include <list>
 
--- a/OrthancServer/Sources/ServerIndex.cpp	Fri Dec 15 17:15:43 2023 +0100
+++ b/OrthancServer/Sources/ServerIndex.cpp	Thu Dec 21 16:13:00 2023 +0100
@@ -417,11 +417,17 @@
       
       if (mode == MaxStorageMode_Recycle)
       {
-        LOG(WARNING) << "Maximum Storage mode: Recycle";
+        if (maximumStorageSize_ > 0 || maximumPatients_ > 0)
+        {
+          LOG(WARNING) << "Maximum Storage mode: Recycle";
+        }
       }
       else
       {
-        LOG(WARNING) << "Maximum Storage mode: Reject";
+        if (maximumStorageSize_ > 0 || maximumPatients_ > 0)
+        {
+          LOG(WARNING) << "Maximum Storage mode: Reject";
+        }
       }
     }
 
--- a/TODO	Fri Dec 15 17:15:43 2023 +0100
+++ b/TODO	Thu Dec 21 16:13:00 2023 +0100
@@ -138,11 +138,9 @@
   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.
+  This might require a DCMTK decoding plugin ?
   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