# HG changeset patch # User Alain Mazy # Date 1562142666 -7200 # Node ID 9ea218c90057c6b1226c3884232682a1e4ca0335 # Parent ca3ac0f210d64f2af04dce672620ccd1c2159e9b# Parent 0013818bf6d4cfa6b43733b1d4720e66d8f21984 merge diff -r ca3ac0f210d6 -r 9ea218c90057 Core/DicomNetworking/Internals/CommandDispatcher.cpp --- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -459,7 +459,7 @@ // The global variable "numberOfDcmAllStorageSOPClassUIDs" is // only published if DCMTK >= 3.6.2: // https://bitbucket.org/sjodogne/orthanc/issues/137 - assert(count == numberOfDcmAllStorageSOPClassUIDs); + assert(static_cast(count) == numberOfDcmAllStorageSOPClassUIDs); #endif cond = ASC_acceptContextsWithPreferredTransferSyntaxes( diff -r ca3ac0f210d6 -r 9ea218c90057 Core/DicomNetworking/Internals/FindScp.cpp --- a/Core/DicomNetworking/Internals/FindScp.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/DicomNetworking/Internals/FindScp.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -83,6 +83,7 @@ #include "../../PrecompiledHeaders.h" #include "FindScp.h" +#include "../../DicomFormat/DicomArray.h" #include "../../DicomParsing/FromDcmtkBridge.h" #include "../../DicomParsing/ToDcmtkBridge.h" #include "../../Logging.h" diff -r ca3ac0f210d6 -r 9ea218c90057 Core/DicomParsing/DicomWebJsonVisitor.cpp --- a/Core/DicomParsing/DicomWebJsonVisitor.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/DicomParsing/DicomWebJsonVisitor.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -558,9 +558,9 @@ tokens.size() > 1 && tokens[0].empty()) { - std::string s = tokens[1]; - tokens.clear(); - tokens.push_back(s); + // Specific character set with code extension: Remove the + // first element from the vector of encodings + tokens.erase(tokens.begin()); } node[KEY_VALUE] = Json::arrayValue; diff -r ca3ac0f210d6 -r 9ea218c90057 Core/DicomParsing/FromDcmtkBridge.cpp --- a/Core/DicomParsing/FromDcmtkBridge.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -2312,8 +2312,15 @@ if (c != NULL) // This case corresponds to the empty string { - std::string s(c); - utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions); + if (element.getTag() == DCM_SpecificCharacterSet) + { + utf8.assign(c); + } + else + { + std::string s(c); + utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions); + } } std::string newValue; diff -r ca3ac0f210d6 -r 9ea218c90057 Core/DicomParsing/ParsedDicomFile.cpp --- a/Core/DicomParsing/ParsedDicomFile.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -915,12 +915,23 @@ { std::string patientId, studyUid, seriesUid, instanceUid; - if (!GetTagValue(patientId, DICOM_TAG_PATIENT_ID) || - !GetTagValue(studyUid, DICOM_TAG_STUDY_INSTANCE_UID) || + if (!GetTagValue(patientId, DICOM_TAG_PATIENT_ID)) + { + /** + * If "PatientID" is absent, be tolerant by considering it + * equals the empty string, then proceed. In Orthanc <= 1.5.6, + * an exception "Bad file format" was generated. + * https://groups.google.com/d/msg/orthanc-users/aphG_h1AHVg/rfOTtTPTAgAJ + * https://bitbucket.org/sjodogne/orthanc/commits/4c45e018bd3de3cfa21d6efc6734673aaaee4435 + **/ + patientId.clear(); + } + + if (!GetTagValue(studyUid, DICOM_TAG_STUDY_INSTANCE_UID) || !GetTagValue(seriesUid, DICOM_TAG_SERIES_INSTANCE_UID) || !GetTagValue(instanceUid, DICOM_TAG_SOP_INSTANCE_UID)) { - throw OrthancException(ErrorCode_BadFileFormat, "missing PatientID, StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID"); + throw OrthancException(ErrorCode_BadFileFormat, "missing StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID"); } return DicomInstanceHasher(patientId, studyUid, seriesUid, instanceUid); diff -r ca3ac0f210d6 -r 9ea218c90057 Core/HttpServer/HttpServer.cpp --- a/Core/HttpServer/HttpServer.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/HttpServer/HttpServer.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -483,6 +483,7 @@ //chunkStore.Print(); + // TODO - Refactor using class "MultipartStreamReader" try { FindIterator last; @@ -841,6 +842,10 @@ ct->second.size() >= MULTIPART_FORM_LENGTH && !memcmp(ct->second.c_str(), MULTIPART_FORM, MULTIPART_FORM_LENGTH)) { + /** + * The user uses the "upload" form of Orthanc Explorer, for + * file uploads through a HTML form. + **/ status = ParseMultipartForm(body, connection, headers, ct->second, server.GetChunkStore()); isMultipartForm = true; } diff -r ca3ac0f210d6 -r 9ea218c90057 Core/HttpServer/HttpToolbox.cpp --- a/Core/HttpServer/HttpToolbox.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/HttpServer/HttpToolbox.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -225,25 +225,15 @@ } - bool HttpToolbox::SimpleGet(std::string& result, - IHttpHandler& handler, - RequestOrigin origin, - const std::string& uri) - { - IHttpHandler::Arguments headers; // No HTTP header - return SimpleGet(result, handler, origin, uri, headers); - } - - static bool SimplePostOrPut(std::string& result, IHttpHandler& handler, RequestOrigin origin, HttpMethod method, const std::string& uri, const void* bodyData, - size_t bodySize) + size_t bodySize, + const IHttpHandler::Arguments& httpHeaders) { - IHttpHandler::Arguments headers; // No HTTP header IHttpHandler::GetArguments getArguments; // No GET argument for POST/PUT UriComponents curi; @@ -253,7 +243,7 @@ HttpOutput http(stream, false /* no keep alive */); if (handler.Handle(http, origin, LOCALHOST, "", method, curi, - headers, getArguments, bodyData, bodySize)) + httpHeaders, getArguments, bodyData, bodySize)) { stream.GetOutput(result); return true; @@ -270,9 +260,10 @@ RequestOrigin origin, const std::string& uri, const void* bodyData, - size_t bodySize) + size_t bodySize, + const IHttpHandler::Arguments& httpHeaders) { - return SimplePostOrPut(result, handler, origin, HttpMethod_Post, uri, bodyData, bodySize); + return SimplePostOrPut(result, handler, origin, HttpMethod_Post, uri, bodyData, bodySize, httpHeaders); } @@ -281,26 +272,27 @@ RequestOrigin origin, const std::string& uri, const void* bodyData, - size_t bodySize) + size_t bodySize, + const IHttpHandler::Arguments& httpHeaders) { - return SimplePostOrPut(result, handler, origin, HttpMethod_Put, uri, bodyData, bodySize); + return SimplePostOrPut(result, handler, origin, HttpMethod_Put, uri, bodyData, bodySize, httpHeaders); } bool HttpToolbox::SimpleDelete(IHttpHandler& handler, RequestOrigin origin, - const std::string& uri) + const std::string& uri, + const IHttpHandler::Arguments& httpHeaders) { UriComponents curi; Toolbox::SplitUriComponents(curi, uri); - IHttpHandler::Arguments headers; // No HTTP header IHttpHandler::GetArguments getArguments; // No GET argument for DELETE StringHttpOutput stream; HttpOutput http(stream, false /* no keep alive */); return handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Delete, curi, - headers, getArguments, NULL /* no body for DELETE */, 0); + httpHeaders, getArguments, NULL /* no body for DELETE */, 0); } } diff -r ca3ac0f210d6 -r 9ea218c90057 Core/HttpServer/HttpToolbox.h --- a/Core/HttpServer/HttpToolbox.h Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/HttpServer/HttpToolbox.h Wed Jul 03 10:31:06 2019 +0200 @@ -64,11 +64,6 @@ static bool SimpleGet(std::string& result, IHttpHandler& handler, RequestOrigin origin, - const std::string& uri); - - static bool SimpleGet(std::string& result, - IHttpHandler& handler, - RequestOrigin origin, const std::string& uri, const IHttpHandler::Arguments& httpHeaders); @@ -77,17 +72,20 @@ RequestOrigin origin, const std::string& uri, const void* bodyData, - size_t bodySize); + size_t bodySize, + const IHttpHandler::Arguments& httpHeaders); static bool SimplePut(std::string& result, IHttpHandler& handler, RequestOrigin origin, const std::string& uri, const void* bodyData, - size_t bodySize); + size_t bodySize, + const IHttpHandler::Arguments& httpHeaders); static bool SimpleDelete(IHttpHandler& handler, RequestOrigin origin, - const std::string& uri); + const std::string& uri, + const IHttpHandler::Arguments& httpHeaders); }; } diff -r ca3ac0f210d6 -r 9ea218c90057 Core/HttpServer/MultipartStreamReader.cpp --- a/Core/HttpServer/MultipartStreamReader.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/HttpServer/MultipartStreamReader.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -175,7 +175,7 @@ HttpHeaders headers; ParseHeaders(headers, start, headersMatcher_.GetMatchBegin()); - size_t contentLength; + size_t contentLength = 0; if (!LookupHeaderSizeValue(contentLength, headers, "content-length")) { if (boundaryMatcher_.Apply(headersMatcher_.GetMatchEnd(), corpusEnd)) diff -r ca3ac0f210d6 -r 9ea218c90057 Core/Lua/LuaContext.cpp --- a/Core/Lua/LuaContext.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/Lua/LuaContext.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -36,6 +36,7 @@ #include "../Logging.h" #include "../OrthancException.h" +#include "../Toolbox.h" #include #include @@ -156,7 +157,7 @@ } Json::Value json; - that.GetJson(json, 1, keepStrings); + that.GetJson(json, state, 1, keepStrings); Json::FastWriter writer; std::string s = writer.write(json); @@ -208,27 +209,21 @@ return true; } - - void LuaContext::SetHttpHeaders(lua_State *state, int top) - { - this->httpClient_.ClearHeaders(); // always reset headers in case they have been set in a previous request - - if (lua_gettop(state) >= top) - { - Json::Value headers; - this->GetJson(headers, top, true); + - Json::Value::Members members = headers.getMemberNames(); + void LuaContext::SetHttpHeaders(int top) + { + std::map headers; + GetDictionaryArgument(headers, lua_, top, false /* keep key case as provided by Lua script */); + + httpClient_.ClearHeaders(); // always reset headers in case they have been set in a previous request - for (Json::Value::Members::const_iterator - it = members.begin(); it != members.end(); ++it) - { - this->httpClient_.AddHeader(*it, headers[*it].asString()); - } + for (std::map::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + httpClient_.AddHeader(it->first, it->second); } - - } - + } int LuaContext::CallHttpGet(lua_State *state) @@ -237,8 +232,8 @@ // Check the types of the arguments int nArgs = lua_gettop(state); - if ((nArgs < 1 || nArgs > 2) || // check args count - !lua_isstring(state, 1)) // URL is a string + if (nArgs < 1 || nArgs > 2 || // check args count + !lua_isstring(state, 1)) // URL is a string { LOG(ERROR) << "Lua: Bad parameters to HttpGet()"; lua_pushnil(state); @@ -250,7 +245,7 @@ that.httpClient_.SetMethod(HttpMethod_Get); that.httpClient_.SetUrl(url); that.httpClient_.GetBody().clear(); - that.SetHttpHeaders(state, 2); + that.SetHttpHeaders(2); // Do the HTTP GET request if (!that.AnswerHttpQuery(state)) @@ -283,7 +278,7 @@ const char* url = lua_tostring(state, 1); that.httpClient_.SetMethod(method); that.httpClient_.SetUrl(url); - that.SetHttpHeaders(state, 3); + that.SetHttpHeaders(3); if (nArgs >= 2 && !lua_isnil(state, 2)) { @@ -345,7 +340,7 @@ that.httpClient_.SetMethod(HttpMethod_Delete); that.httpClient_.SetUrl(url); that.httpClient_.GetBody().clear(); - that.SetHttpHeaders(state, 2); + that.SetHttpHeaders(2); // Do the HTTP DELETE request std::string s; @@ -434,10 +429,11 @@ void LuaContext::GetJson(Json::Value& result, + lua_State* state, int top, bool keepStrings) { - if (lua_istable(lua_, top)) + if (lua_istable(state, top)) { Json::Value tmp = Json::objectValue; bool isArray = true; @@ -448,19 +444,19 @@ // Push another reference to the table on top of the stack (so we know // where it is, and this function can work for negative, positive and // pseudo indices - lua_pushvalue(lua_, top); + lua_pushvalue(state, top); // stack now contains: -1 => table - lua_pushnil(lua_); + lua_pushnil(state); // stack now contains: -1 => nil; -2 => table - while (lua_next(lua_, -2)) + while (lua_next(state, -2)) { // stack now contains: -1 => value; -2 => key; -3 => table // copy the key so that lua_tostring does not modify the original - lua_pushvalue(lua_, -2); + lua_pushvalue(state, -2); // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table - std::string key(lua_tostring(lua_, -1)); + std::string key(lua_tostring(state, -1)); Json::Value v; - GetJson(v, -2, keepStrings); + GetJson(v, state, -2, keepStrings); tmp[key] = v; @@ -479,13 +475,13 @@ } // pop value + copy of key, leaving original key - lua_pop(lua_, 2); + lua_pop(state, 2); // stack now contains: -1 => key; -2 => table } // stack now contains: -1 => table (when lua_next returns 0 it pops the key // but does not push anything.) // Pop table - lua_pop(lua_, 1); + lua_pop(state, 1); // Stack is now the same as it was on entry to this function @@ -502,20 +498,20 @@ result = tmp; } } - else if (lua_isnil(lua_, top)) + else if (lua_isnil(state, top)) { result = Json::nullValue; } else if (!keepStrings && - lua_isboolean(lua_, top)) + lua_isboolean(state, top)) { - result = lua_toboolean(lua_, top) ? true : false; + result = lua_toboolean(state, top) ? true : false; } else if (!keepStrings && - lua_isnumber(lua_, top)) + lua_isnumber(state, top)) { // Convert to "int" if truncation does not loose precision - double value = static_cast(lua_tonumber(lua_, top)); + double value = static_cast(lua_tonumber(state, top)); int truncated = static_cast(value); if (std::abs(value - static_cast(truncated)) <= @@ -528,15 +524,15 @@ result = value; } } - else if (lua_isstring(lua_, top)) + else if (lua_isstring(state, top)) { // Caution: The "lua_isstring()" case must be the last, since // Lua can convert most types to strings by default. - result = std::string(lua_tostring(lua_, top)); + result = std::string(lua_tostring(state, top)); } - else if (lua_isboolean(lua_, top)) + else if (lua_isboolean(state, top)) { - result = lua_toboolean(lua_, top) ? true : false; + result = lua_toboolean(state, top) ? true : false; } else { @@ -653,4 +649,33 @@ lua_pop(state, 1); return value; } + + + void LuaContext::GetDictionaryArgument(std::map& target, + lua_State* state, + int top, + bool keyToLowerCase) + { + target.clear(); + + if (lua_gettop(state) >= top) + { + Json::Value headers; + GetJson(headers, state, top, true); + + Json::Value::Members members = headers.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + std::string key = members[i]; + + if (keyToLowerCase) + { + Toolbox::ToLowerCase(key); + } + + target[key] = headers[members[i]].asString(); + } + } + } } diff -r ca3ac0f210d6 -r 9ea218c90057 Core/Lua/LuaContext.h --- a/Core/Lua/LuaContext.h Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/Lua/LuaContext.h Wed Jul 03 10:31:06 2019 +0200 @@ -87,11 +87,12 @@ void ExecuteInternal(std::string* output, const std::string& command); - void GetJson(Json::Value& result, - int top, - bool keepStrings); + static void GetJson(Json::Value& result, + lua_State* state, + int top, + bool keepStrings); - void SetHttpHeaders(lua_State* state, int top); + void SetHttpHeaders(int top); public: LuaContext(); @@ -136,5 +137,10 @@ const char* name); void PushJson(const Json::Value& value); + + static void GetDictionaryArgument(std::map& target, + lua_State* state, + int top, + bool keyToLowerCase); }; } diff -r ca3ac0f210d6 -r 9ea218c90057 Core/Lua/LuaFunctionCall.cpp --- a/Core/Lua/LuaFunctionCall.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/Lua/LuaFunctionCall.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -134,7 +134,7 @@ bool keepStrings) { ExecuteInternal(1); - context_.GetJson(result, lua_gettop(context_.lua_), keepStrings); + context_.GetJson(result, context_.lua_, lua_gettop(context_.lua_), keepStrings); } diff -r ca3ac0f210d6 -r 9ea218c90057 Core/WebServiceParameters.cpp --- a/Core/WebServiceParameters.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/WebServiceParameters.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -289,13 +289,22 @@ { if (!IsReservedKey(*it)) { - if (peer[*it].type() != Json::stringValue) + switch (peer[*it].type()) { - throw OrthancException(ErrorCode_BadFileFormat); - } - else - { - userProperties_[*it] = peer[*it].asString(); + case Json::stringValue: + userProperties_[*it] = peer[*it].asString(); + break; + + case Json::booleanValue: + userProperties_[*it] = peer[*it].asBool() ? "1" : "0"; + break; + + case Json::intValue: + userProperties_[*it] = boost::lexical_cast(peer[*it].asInt()); + break; + + default: + throw OrthancException(ErrorCode_BadFileFormat); } } } @@ -402,6 +411,33 @@ return true; } } + + + bool WebServiceParameters::GetBooleanUserProperty(const std::string& key, + bool defaultValue) const + { + Dictionary::const_iterator found = userProperties_.find(key); + + if (found == userProperties_.end()) + { + return defaultValue; + } + else if (found->second == "0" || + found->second == "false") + { + return false; + } + else if (found->second == "1" || + found->second == "true") + { + return true; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "Bad value for a Boolean user property in the parameters " + "of a Web service: Property \"" + key + "\" equals: " + found->second); + } + } bool WebServiceParameters::IsAdvancedFormatNeeded() const diff -r ca3ac0f210d6 -r 9ea218c90057 Core/WebServiceParameters.h --- a/Core/WebServiceParameters.h Wed Jul 03 10:28:17 2019 +0200 +++ b/Core/WebServiceParameters.h Wed Jul 03 10:31:06 2019 +0200 @@ -162,7 +162,10 @@ void ListUserProperties(std::set& target) const; bool LookupUserProperty(std::string& value, - const std::string& key) const; + const std::string& key) const; + + bool GetBooleanUserProperty(const std::string& key, + bool defaultValue) const; bool IsAdvancedFormatNeeded() const; diff -r ca3ac0f210d6 -r 9ea218c90057 NEWS --- a/NEWS Wed Jul 03 10:28:17 2019 +0200 +++ b/NEWS Wed Jul 03 10:31:06 2019 +0200 @@ -2,9 +2,14 @@ =============================== + +Version 1.5.7 (2019-06-25) +========================== + REST API -------- +* API version has been upgraded to 3 * "/modalities/{id}/query": New argument "Normalize" can be set to "false" to bypass the automated correction of outgoing C-FIND queries * Reporting of "ParentResources" in "DicomModalityStore" and "DicomModalityStore" jobs @@ -26,9 +31,12 @@ * Allow the serialization of signed 16bpp images in PAM format * HTTP header "Accept-Encoding" is honored for streams without built-in support for compression * The default HTTP timeout is now 60 seconds (instead of 10 seconds in previous versions) +* Allow anonymizing/modifying instances without the PatientID tag +* Fix issue #106 (Unable to export preview as jpeg from Lua script) * Fix issue #136 (C-FIND request fails when found DICOM file does not have certain tags) * Fix issue #137 (C-STORE fails for unknown SOP Class although server is configured to accept any) * Fix issue #138 (POST to modalities/{name} accepts invalid characters) +* Fix issue #141 (/tools/create-dicom removes non-ASCII characters from study description) Version 1.5.6 (2019-03-01) diff -r ca3ac0f210d6 -r 9ea218c90057 OrthancServer/LuaScripting.cpp --- a/OrthancServer/LuaScripting.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/OrthancServer/LuaScripting.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -279,9 +279,9 @@ // Check the types of the arguments int nArgs = lua_gettop(state); - if ((nArgs != 1 && nArgs != 2) || + if (nArgs < 1 || nArgs > 3 || !lua_isstring(state, 1) || // URI - (nArgs == 2 && !lua_isboolean(state, 2))) // Restrict to built-in API? + (nArgs >= 2 && !lua_isboolean(state, 2))) // Restrict to built-in API? { LOG(ERROR) << "Lua: Bad parameters to RestApiGet()"; lua_pushnil(state); @@ -291,11 +291,14 @@ const char* uri = lua_tostring(state, 1); bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false); + std::map headers; + LuaContext::GetDictionaryArgument(headers, state, 3, true /* HTTP header key to lower case */); + try { std::string result; if (HttpToolbox::SimpleGet(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), - RequestOrigin_Lua, uri)) + RequestOrigin_Lua, uri, headers)) { lua_pushlstring(state, result.c_str(), result.size()); return 1; @@ -325,10 +328,10 @@ // Check the types of the arguments int nArgs = lua_gettop(state); - if ((nArgs != 2 && nArgs != 3) || + if (nArgs < 2 || nArgs > 4 || !lua_isstring(state, 1) || // URI !lua_isstring(state, 2) || // Body - (nArgs == 3 && !lua_isboolean(state, 3))) // Restrict to built-in API? + (nArgs >= 3 && !lua_isboolean(state, 3))) // Restrict to built-in API? { LOG(ERROR) << "Lua: Bad parameters to " << (isPost ? "RestApiPost()" : "RestApiPut()"); lua_pushnil(state); @@ -340,14 +343,17 @@ const char* bodyData = lua_tolstring(state, 2, &bodySize); bool builtin = (nArgs == 3 ? lua_toboolean(state, 3) != 0 : false); + std::map headers; + LuaContext::GetDictionaryArgument(headers, state, 4, true /* HTTP header key to lower case */); + try { std::string result; if (isPost ? HttpToolbox::SimplePost(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), - RequestOrigin_Lua, uri, bodyData, bodySize) : + RequestOrigin_Lua, uri, bodyData, bodySize, headers) : HttpToolbox::SimplePut(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), - RequestOrigin_Lua, uri, bodyData, bodySize)) + RequestOrigin_Lua, uri, bodyData, bodySize, headers)) { lua_pushlstring(state, result.c_str(), result.size()); return 1; @@ -391,9 +397,9 @@ // Check the types of the arguments int nArgs = lua_gettop(state); - if ((nArgs != 1 && nArgs != 2) || + if (nArgs < 1 || nArgs > 3 || !lua_isstring(state, 1) || // URI - (nArgs == 2 && !lua_isboolean(state, 2))) // Restrict to built-in API? + (nArgs >= 2 && !lua_isboolean(state, 2))) // Restrict to built-in API? { LOG(ERROR) << "Lua: Bad parameters to RestApiDelete()"; lua_pushnil(state); @@ -403,10 +409,13 @@ const char* uri = lua_tostring(state, 1); bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false); + std::map headers; + LuaContext::GetDictionaryArgument(headers, state, 3, true /* HTTP header key to lower case */); + try { if (HttpToolbox::SimpleDelete(serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), - RequestOrigin_Lua, uri)) + RequestOrigin_Lua, uri, headers)) { lua_pushboolean(state, 1); return 1; diff -r ca3ac0f210d6 -r 9ea218c90057 OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -518,11 +518,8 @@ } else if (tag["Type"] == "String") { - std::string value = tag["Value"].asString(); - - bool hasCodeExtensions; - Encoding encoding = dicom.DetectEncoding(hasCodeExtensions); - dicom.ReplacePlainString(*it, Toolbox::ConvertFromUtf8(value, encoding)); + std::string value = tag["Value"].asString(); // This is an UTF-8 value (as it comes from JSON) + dicom.ReplacePlainString(*it, value); } } } diff -r ca3ac0f210d6 -r 9ea218c90057 Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Plugins/Engine/OrthancPlugins.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -1948,8 +1948,10 @@ handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); } + std::map httpHeaders; + std::string result; - if (HttpToolbox::SimpleGet(result, *handler, RequestOrigin_Plugins, p.uri)) + if (HttpToolbox::SimpleGet(result, *handler, RequestOrigin_Plugins, p.uri, httpHeaders)) { CopyToMemoryBuffer(*p.target, result); } @@ -2013,10 +2015,12 @@ handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); } + std::map httpHeaders; + std::string result; if (isPost ? - HttpToolbox::SimplePost(result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize) : - HttpToolbox::SimplePut (result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize)) + HttpToolbox::SimplePost(result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize, httpHeaders) : + HttpToolbox::SimplePut (result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize, httpHeaders)) { CopyToMemoryBuffer(*p.target, result); } @@ -2041,7 +2045,9 @@ handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); } - if (!HttpToolbox::SimpleDelete(*handler, RequestOrigin_Plugins, uri)) + std::map httpHeaders; + + if (!HttpToolbox::SimpleDelete(*handler, RequestOrigin_Plugins, uri, httpHeaders)) { throw OrthancException(ErrorCode_UnknownResource); } diff -r ca3ac0f210d6 -r 9ea218c90057 Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Wed Jul 03 10:28:17 2019 +0200 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Wed Jul 03 10:31:06 2019 +0200 @@ -25,6 +25,7 @@ * - Possibly register a callback to filter incoming HTTP requests using OrthancPluginRegisterIncomingHttpRequestFilter2(). * - Possibly register a callback to unserialize jobs using OrthancPluginRegisterJobsUnserializer(). * - Possibly register a callback to refresh its metrics using OrthancPluginRegisterRefreshMetricsCallback(). + * - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback(). * -# void OrthancPluginFinalize(): * This function is invoked by Orthanc during its shutdown. The plugin * must free all its memory. @@ -4055,6 +4056,7 @@ * @param username The username (can be NULL if no password protection). * @param password The password (can be NULL if no password protection). * @return 0 if success, or the error code if failure. + * @ingroup Toolbox **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpGet( OrthancPluginContext* context, @@ -4092,6 +4094,7 @@ * @param username The username (can be NULL if no password protection). * @param password The password (can be NULL if no password protection). * @return 0 if success, or the error code if failure. + * @ingroup Toolbox **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpPost( OrthancPluginContext* context, @@ -4133,6 +4136,7 @@ * @param username The username (can be NULL if no password protection). * @param password The password (can be NULL if no password protection). * @return 0 if success, or the error code if failure. + * @ingroup Toolbox **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpPut( OrthancPluginContext* context, @@ -4170,6 +4174,7 @@ * @param username The username (can be NULL if no password protection). * @param password The password (can be NULL if no password protection). * @return 0 if success, or the error code if failure. + * @ingroup Toolbox **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpDelete( OrthancPluginContext* context, @@ -5539,6 +5544,7 @@ * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards. * @return 0 if success, or the error code if failure. * @see OrthancPluginCallPeerApi() + * @ingroup Toolbox **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpClient( OrthancPluginContext* context, @@ -6801,36 +6807,114 @@ - - - - - - - - - - - - - typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddHeader) (void* answer, - const char* key, - const char* value); - - typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddChunk) (void* answer, - const void* data, - uint32_t size); - + /** + * @brief Callback executed when a HTTP header is received during a chunked transfer. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, as soon as it + * receives one HTTP header from the answer of the remote HTTP + * server. + * + * @see OrthancPluginChunkedHttpClient() + * @param answer The user payload, as provided by the calling plugin. + * @param key The key of the HTTP header. + * @param value The value of the HTTP header. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddHeader) ( + void* answer, + const char* key, + const char* value); + + + /** + * @brief Callback executed when an answer chunk is received during a chunked transfer. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, as soon as it + * receives one data chunk from the answer of the remote HTTP + * server. + * + * @see OrthancPluginChunkedHttpClient() + * @param answer The user payload, as provided by the calling plugin. + * @param data The content of the data chunk. + * @param size The size of the data chunk. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddChunk) ( + void* answer, + const void* data, + uint32_t size); + + + /** + * @brief Callback to know whether the request body is entirely read during a chunked transfer + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, while reading + * the body of a POST or PUT request. The plugin must answer "1" as + * soon as the body is entirely read: The "request" data structure + * must act as an iterator. + * + * @see OrthancPluginChunkedHttpClient() + * @param request The user payload, as provided by the calling plugin. + * @return "1" if the body is over, or "0" if there is still data to be read. + * @ingroup Toolbox + **/ typedef uint8_t (*OrthancPluginChunkedClientRequestIsDone) (void* request); + + /** + * @brief Callback to advance in the request body during a chunked transfer + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, while reading + * the body of a POST or PUT request. This function asks the plugin + * to advance to the next chunk of data of the request body: The + * "request" data structure must act as an iterator. + * + * @see OrthancPluginChunkedHttpClient() + * @param request The user payload, as provided by the calling plugin. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientRequestNext) (void* request); + + /** + * @brief Callback to read the current chunk of the request body during a chunked transfer + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, while reading + * the body of a POST or PUT request. The plugin must provide the + * content of the current chunk of data of the request body. + * + * @see OrthancPluginChunkedHttpClient() + * @param request The user payload, as provided by the calling plugin. + * @return The content of the current request chunk. + * @ingroup Toolbox + **/ typedef const void* (*OrthancPluginChunkedClientRequestGetChunkData) (void* request); + + /** + * @brief Callback to read the size of the current request chunk during a chunked transfer + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, while reading + * the body of a POST or PUT request. The plugin must provide the + * size of the current chunk of data of the request body. + * + * @see OrthancPluginChunkedHttpClient() + * @param request The user payload, as provided by the calling plugin. + * @return The size of the current request chunk. + * @ingroup Toolbox + **/ typedef uint32_t (*OrthancPluginChunkedClientRequestGetChunkSize) (void* request); - typedef struct { void* answer; @@ -6856,6 +6940,57 @@ uint8_t pkcs11; } _OrthancPluginChunkedHttpClient; + + /** + * @brief Issue a HTTP call, using chunked HTTP transfers. + * + * Make a HTTP call to the given URL using chunked HTTP + * transfers. The request body is provided as an iterator over data + * chunks. The answer is provided as a sequence of function calls + * with the individual HTTP headers and answer chunks. + * + * Contrarily to OrthancPluginHttpClient() that entirely stores the + * request body and the answer body in memory buffers, this function + * uses chunked HTTP transfers. This results in a lower memory + * consumption. Pay attention to the fact that Orthanc servers with + * version <= 1.5.6 do not support chunked transfers: You must use + * OrthancPluginHttpClient() if contacting such older servers. + * + * The HTTP request will be done accordingly to the global + * configuration of Orthanc (in particular, the options "HttpProxy", + * "HttpTimeout", "HttpsVerifyPeers", "HttpsCACertificates", and + * "Pkcs11" will be taken into account). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answer The user payload for the answer body. It will be provided to the callbacks for the answer. + * @param answerAddChunk Callback function to report a data chunk from the answer body. + * @param answerAddHeader Callback function to report an HTTP header sent by the remote server. + * @param httpStatus The HTTP status after the execution of the request (out argument). + * @param method HTTP method to be used. + * @param url The URL of interest. + * @param headersCount The number of HTTP headers. + * @param headersKeys Array containing the keys of the HTTP headers (can be NULL if no header). + * @param headersValues Array containing the values of the HTTP headers (can be NULL if no header). + * @param request The user payload containing the request body, and acting as an iterator. + * It will be provided to the callbacks for the request. + * @param requestIsDone Callback function to tell whether the request body is entirely read. + * @param requestChunkData Callback function to get the content of the current data chunk of the request body. + * @param requestChunkSize Callback function to get the size of the current data chunk of the request body. + * @param requestNext Callback function to advance to the next data chunk of the request body. + * @param username The username (can be NULL if no password protection). + * @param password The password (can be NULL if no password protection). + * @param timeout Timeout in seconds (0 for default timeout). + * @param certificateFile Path to the client certificate for HTTPS, in PEM format + * (can be NULL if no client certificate or if not using HTTPS). + * @param certificateKeyFile Path to the key of the client certificate for HTTPS, in PEM format + * (can be NULL if no client certificate or if not using HTTPS). + * @param certificateKeyPassword Password to unlock the key of the client certificate + * (can be NULL if no client certificate or if not using HTTPS). + * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginHttpClient() + * @ingroup Toolbox + **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginChunkedHttpClient( OrthancPluginContext* context, void* answer, @@ -6913,23 +7048,86 @@ + /** + * @brief Opaque structure that reads the content of a HTTP request body during a chunked HTTP transfer. + * @ingroup Callback + **/ typedef struct _OrthancPluginServerChunkedRequestReader_t OrthancPluginServerChunkedRequestReader; - /* POST and PUT must share the same reader */ + + + /** + * @brief Callback to create a reader to handle incoming chunked HTTP transfers. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP server that supports chunked HTTP transfers. This + * callback is only invoked if the HTTP method is POST or PUT. The + * callback must create an user-specific "reader" object that will + * be fed with the body of the incoming body. + * + * @see OrthancPluginRegisterChunkedRestCallback() + * @param reader Memory location that must be filled with the newly-created reader. + * @param url The URI that is accessed. + * @param request The body of the HTTP request. Note that "body" and "bodySize" are not used. + * @return 0 if success, or the error code if failure. + **/ typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderFactory) ( - OrthancPluginServerChunkedRequestReader** reader, /* out, for POST/PUT only */ + OrthancPluginServerChunkedRequestReader** reader, const char* url, - const OrthancPluginHttpRequest* request); /* body and bodySize are not used */ - + const OrthancPluginHttpRequest* request); + + + /** + * @brief Callback invoked whenever a new data chunk is available during a chunked transfer. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP server that supports chunked HTTP transfers. This callback + * is invoked as soon as a new data chunk is available for the request body. + * + * @see OrthancPluginRegisterChunkedRestCallback() + * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback. + * @param data The content of the data chunk. + * @param size The size of the data chunk. + * @return 0 if success, or the error code if failure. + **/ typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderAddChunk) ( OrthancPluginServerChunkedRequestReader* reader, const void* data, uint32_t size); + + /** + * @brief Callback invoked whenever the request body is entirely received. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP server that supports chunked HTTP transfers. This + * callback is invoked as soon as the full body of the HTTP request + * is available. The plugin can then send its answer thanks to the + * provided "output" object. + * + * @see OrthancPluginRegisterChunkedRestCallback() + * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback. + * @param output The HTTP connection to the client application. + * @return 0 if success, or the error code if failure. + **/ typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderExecute) ( OrthancPluginServerChunkedRequestReader* reader, OrthancPluginRestOutput* output); + + /** + * @brief Callback invoked to release the resources associated with an incoming HTTP chunked transfer. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP server that supports chunked HTTP transfers. This + * callback is invoked to release all the resources allocated by the + * given reader. Note that this function might be invoked even if + * the entire body was not read, to deal with client error or + * disconnection. + * + * @see OrthancPluginRegisterChunkedRestCallback() + * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback. + **/ typedef void (*OrthancPluginServerChunkedRequestReaderFinalize) ( OrthancPluginServerChunkedRequestReader* reader); @@ -6945,6 +7143,36 @@ OrthancPluginServerChunkedRequestReaderFinalize finalize; } _OrthancPluginChunkedRestCallback; + + /** + * @brief Register a REST callback to handle chunked HTTP transfers. + * + * This function registers a REST callback against a regular + * expression for a URI. This function must be called during the + * initialization of the plugin, i.e. inside the + * OrthancPluginInitialize() public function. + * + * Contrarily to OrthancPluginRegisterRestCallback(), the callbacks + * will NOT be invoked in mutual exclusion, so it is up to the + * plugin to implement the required locking mechanisms. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param pathRegularExpression Regular expression for the URI. May contain groups. + * @param getHandler The callback function to handle REST calls using the GET HTTP method. + * @param postHandler The callback function to handle REST calls using the GET POST method. + * @param deleteHandler The callback function to handle REST calls using the GET DELETE method. + * @param putHandler The callback function to handle REST calls using the GET PUT method. + * @param addChunk The callback invoked when a new chunk is available for the request body of a POST or PUT call. + * @param execute The callback invoked once the entire body of a POST or PUT call is read. + * @param finalize The callback invoked to release the resources associated with a POST or PUT call. + * @see OrthancPluginRegisterRestCallbackNoLock() + * + * @note + * The regular expression is case sensitive and must follow the + * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html). + * + * @ingroup Callbacks + **/ ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterChunkedRestCallback( OrthancPluginContext* context, const char* pathRegularExpression, @@ -6981,11 +7209,26 @@ const char* privateCreator; } _OrthancPluginGetTagName; + /** + * @brief Returns the symbolic name of a DICOM tag. + * + * This function makes a lookup to the dictionary of DICOM tags that + * are known to Orthanc, and returns the symbolic name of a DICOM tag. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param group The group of the tag. + * @param element The element of the tag. + * @param privateCreator For private tags, the name of the private creator (can be NULL). + * @return NULL in the case of an error, or a newly allocated string + * containing the path. This string must be freed by + * OrthancPluginFreeString(). + * @ingroup Toolbox + **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginGetTagName( OrthancPluginContext* context, uint16_t group, uint16_t element, - const char* privateCreator /* can be NULL */) + const char* privateCreator) { char* result; diff -r ca3ac0f210d6 -r 9ea218c90057 Plugins/Samples/Common/OrthancPluginCppWrapper.cpp --- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Jul 03 10:28:17 2019 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Jul 03 10:31:06 2019 +0200 @@ -33,6 +33,7 @@ #include "OrthancPluginCppWrapper.h" +#include #include #include #include @@ -333,13 +334,10 @@ void OrthancString::Assign(char* str) { - if (str == NULL) + Clear(); + + if (str != NULL) { - ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); - } - else - { - Clear(); str_ = str; } } @@ -2050,6 +2048,11 @@ std::string OrthancJob::Submit(OrthancJob* job, int priority) { + if (job == NULL) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer); + } + OrthancPluginJob* orthanc = Create(job); char* id = OrthancPluginSubmitJob(GetGlobalContext(), orthanc, priority); @@ -2069,6 +2072,70 @@ return tmp; } } + + + void OrthancJob::SubmitAndWait(Json::Value& result, + OrthancJob* job /* takes ownership */, + int priority) + { + std::string id = Submit(job, priority); + + for (;;) + { + boost::this_thread::sleep(boost::posix_time::milliseconds(100)); + + Json::Value status; + if (!RestApiGet(status, "/jobs/" + id, false) || + !status.isMember("State") || + status["State"].type() != Json::stringValue) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InexistentItem); + } + + const std::string state = status["State"].asString(); + if (state == "Success") + { + if (status.isMember("Content")) + { + result = status["Content"]; + } + else + { + result = Json::objectValue; + } + + return; + } + else if (state == "Running") + { + continue; + } + else if (!status.isMember("ErrorCode") || + status["ErrorCode"].type() != Json::intValue) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InternalError); + } + else + { + if (!status.isMember("ErrorDescription") || + status["ErrorDescription"].type() != Json::stringValue) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt()); + } + else + { +#if HAS_ORTHANC_EXCEPTION == 1 + throw Orthanc::OrthancException(static_cast(status["ErrorCode"].asInt()), + status["ErrorDescription"].asString()); +#else + LogError("Exception while executing the job: " + status["ErrorDescription"].asString()); + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt()); +#endif + } + } + } + } + #endif @@ -2218,7 +2285,8 @@ method_(OrthancPluginHttpMethod_Get), timeout_(0), pkcs11_(false), - chunkedBody_(NULL) + chunkedBody_(NULL), + allowChunkedTransfers_(true) { } @@ -2598,35 +2666,63 @@ } -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 void HttpClient::Execute(IAnswer& answer) { - if (chunkedBody_ != NULL) +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 + if (allowChunkedTransfers_) { - ExecuteWithStream(httpStatus_, answer, *chunkedBody_); + if (chunkedBody_ != NULL) + { + ExecuteWithStream(httpStatus_, answer, *chunkedBody_); + } + else + { + MemoryRequestBody wrapper(fullBody_); + ExecuteWithStream(httpStatus_, answer, wrapper); + } + + return; } - else +#endif + + // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked + // transfers are disabled. This results in higher memory usage + // (all chunks from the answer body are sent at once) + + HttpHeaders answerHeaders; + std::string answerBody; + Execute(answerHeaders, answerBody); + + for (HttpHeaders::const_iterator it = answerHeaders.begin(); + it != answerHeaders.end(); ++it) { - MemoryRequestBody wrapper(fullBody_); - ExecuteWithStream(httpStatus_, answer, wrapper); + answer.AddHeader(it->first, it->second); + } + + if (!answerBody.empty()) + { + answer.AddChunk(answerBody.c_str(), answerBody.size()); } } -#endif void HttpClient::Execute(HttpHeaders& answerHeaders /* out */, std::string& answerBody /* out */) { #if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 - MemoryAnswer answer; - Execute(answer); - answerHeaders = answer.GetHeaders(); - answer.GetBody().Flatten(answerBody); - -#else - // Compatibility mode for Orthanc SDK <= 1.5.6. This results in - // higher memory usage (all chunks from the body request are sent - // at once) + if (allowChunkedTransfers_) + { + MemoryAnswer answer; + Execute(answer); + answerHeaders = answer.GetHeaders(); + answer.GetBody().Flatten(answerBody); + return; + } +#endif + + // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked + // transfers are disabled. This results in higher memory usage + // (all chunks from the request body are sent at once) if (chunkedBody_ != NULL) { @@ -2647,7 +2743,6 @@ { ExecuteWithoutStream(httpStatus_, answerHeaders, answerBody, fullBody_); } -#endif } diff -r ca3ac0f210d6 -r 9ea218c90057 Plugins/Samples/Common/OrthancPluginCppWrapper.h --- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed Jul 03 10:28:17 2019 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed Jul 03 10:31:06 2019 +0200 @@ -768,6 +768,10 @@ static std::string Submit(OrthancJob* job /* takes ownership */, int priority); + + static void SubmitAndWait(Json::Value& result, + OrthancJob* job /* takes ownership */, + int priority); }; #endif @@ -810,7 +814,7 @@ virtual bool ReadNextChunk(std::string& chunk) = 0; }; -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 + class IAnswer : public boost::noncopyable { public: @@ -824,7 +828,6 @@ virtual void AddChunk(const void* data, size_t size) = 0; }; -#endif private: @@ -842,7 +845,8 @@ std::string certificateKeyPassword_; bool pkcs11_; std::string fullBody_; - IRequestBody* chunkedBody_; + IRequestBody* chunkedBody_; + bool allowChunkedTransfers_; #if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 void ExecuteWithStream(uint16_t& httpStatus, // out @@ -920,9 +924,19 @@ void SetBody(IRequestBody& body); -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 + // This function can be used to disable chunked transfers if the + // remote server is Orthanc with a version <= 1.5.6. + void SetChunkedTransfersAllowed(bool allow) + { + allowChunkedTransfers_ = allow; + } + + bool IsChunkedTransfersAllowed() const + { + return allowChunkedTransfers_; + } + void Execute(IAnswer& answer); -#endif void Execute(HttpHeaders& answerHeaders /* out */, std::string& answerBody /* out */); @@ -1067,9 +1081,6 @@ Internals::ChunkedRequestReaderExecute, Internals::ChunkedRequestReaderFinalize); #else - LogWarning("Performance warning: The plugin was compiled against a pre-1.5.7 version " - "of the Orthanc SDK. Multipart transfers will be entirely stored in RAM."); - OrthancPluginRegisterRestCallbackNoLock( GetGlobalContext(), uri.c_str(), Internals::ChunkedRestCompatibility); diff -r ca3ac0f210d6 -r 9ea218c90057 Resources/CMake/OrthancFrameworkParameters.cmake --- a/Resources/CMake/OrthancFrameworkParameters.cmake Wed Jul 03 10:28:17 2019 +0200 +++ b/Resources/CMake/OrthancFrameworkParameters.cmake Wed Jul 03 10:31:06 2019 +0200 @@ -17,7 +17,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 "2") +set(ORTHANC_API_VERSION "3") ##################################################################### diff -r ca3ac0f210d6 -r 9ea218c90057 Resources/DownloadOrthancFramework.cmake --- a/Resources/DownloadOrthancFramework.cmake Wed Jul 03 10:28:17 2019 +0200 +++ b/Resources/DownloadOrthancFramework.cmake Wed Jul 03 10:31:06 2019 +0200 @@ -108,6 +108,8 @@ set(ORTHANC_FRAMEWORK_MD5 "cfc437e0687ae4bd725fd93dc1f08bc4") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.6") set(ORTHANC_FRAMEWORK_MD5 "3c29de1e289b5472342947168f0105c0") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.7") + set(ORTHANC_FRAMEWORK_MD5 "e1b76f01116d9b5d4ac8cc39980560e3") endif() endif() endif() diff -r ca3ac0f210d6 -r 9ea218c90057 Resources/OldBuildInstructions.txt --- a/Resources/OldBuildInstructions.txt Wed Jul 03 10:28:17 2019 +0200 +++ b/Resources/OldBuildInstructions.txt Wed Jul 03 10:31:06 2019 +0200 @@ -133,6 +133,18 @@ libpng-devel sqlite-devel libuuid-devel openssl-devel \ lua-devel mercurial patch tar +Using static linking with Civetweb (tested with Orthanc 1.5.7): + +# cmake -DSTATIC_BUILD=ON \ + -DSTANDALONE_BUILD=ON \ + -DUSE_LEGACY_JSONCPP=ON \ + -DUSE_LEGACY_LIBICU=ON \ + -DBOOST_LOCALE_BACKEND=icu \ + -DCMAKE_BUILD_TYPE=Debug \ + ~/Orthanc + +Using Mongoose (untested): + # cmake -DALLOW_DOWNLOADS=ON \ -DUSE_SYSTEM_JSONCPP=OFF \ -DUSE_SYSTEM_CIVETWEB=OFF \