934 lines
34 KiB
C++
934 lines
34 KiB
C++
/**
|
|
* Orthanc - A Lightweight, RESTful DICOM Store
|
|
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
|
* Department, University Hospital of Liege, Belgium
|
|
* Copyright (C) 2017-2024 Osimis S.A., Belgium
|
|
* Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
|
* Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
|
*
|
|
* This program is free software: you can redistribute it and/or
|
|
* modify it under the terms of the GNU Affero 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
|
|
* Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
**/
|
|
|
|
#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
|
|
|
|
#include <Logging.h>
|
|
#include <SystemToolbox.h>
|
|
#include <Toolbox.h>
|
|
#include <SerializationToolbox.h>
|
|
|
|
#include <EmbeddedResources.h>
|
|
|
|
#define ORTHANC_PLUGIN_NAME "orthanc-explorer-2"
|
|
|
|
// we are using Orthanc 1.11.0 API (RequestedTags in tools/find)
|
|
#define ORTHANC_CORE_MINIMAL_MAJOR 1
|
|
#define ORTHANC_CORE_MINIMAL_MINOR 11
|
|
#define ORTHANC_CORE_MINIMAL_REVISION 0
|
|
|
|
|
|
std::unique_ptr<OrthancPlugins::OrthancConfiguration> orthancFullConfiguration_;
|
|
Json::Value pluginJsonConfiguration_;
|
|
std::string oe2BaseUrl_;
|
|
|
|
Json::Value pluginsConfiguration_;
|
|
bool hasUserProfile_ = false;
|
|
bool openInOhifV3IsExplicitelyDisabled = false;
|
|
bool enableShares_ = false;
|
|
bool isReadOnly_ = false;
|
|
std::string customCssPath_;
|
|
std::string theme_ = "light";
|
|
std::string customLogoPath_;
|
|
std::string customLogoUrl_;
|
|
std::string customFavIconPath_;
|
|
std::string customTitle_;
|
|
|
|
enum CustomFilesPath
|
|
{
|
|
CustomFilesPath_Logo,
|
|
CustomFilesPath_FavIcon
|
|
};
|
|
|
|
|
|
template <enum Orthanc::EmbeddedResources::DirectoryResourceId folder>
|
|
void ServeEmbeddedFolder(OrthancPluginRestOutput* output,
|
|
const char* url,
|
|
const OrthancPluginHttpRequest* request)
|
|
{
|
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
|
|
|
if (request->method != OrthancPluginHttpMethod_Get)
|
|
{
|
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
|
}
|
|
else
|
|
{
|
|
std::string path = "/" + std::string(request->groups[0]);
|
|
Orthanc::MimeType mimeType = Orthanc::SystemToolbox::AutodetectMimeType(path);
|
|
const char* mime = Orthanc::EnumerationToString(mimeType);
|
|
|
|
std::string fileContent;
|
|
Orthanc::EmbeddedResources::GetDirectoryResource(fileContent, folder, path.c_str());
|
|
|
|
const char* resource = fileContent.size() ? fileContent.c_str() : NULL;
|
|
OrthancPluginAnswerBuffer(context, output, resource, fileContent.size(), mime);
|
|
}
|
|
}
|
|
|
|
template <enum Orthanc::EmbeddedResources::FileResourceId file, Orthanc::MimeType mime>
|
|
void ServeEmbeddedFile(OrthancPluginRestOutput* output,
|
|
const char* url,
|
|
const OrthancPluginHttpRequest* request)
|
|
{
|
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
|
|
|
if (request->method != OrthancPluginHttpMethod_Get)
|
|
{
|
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
|
}
|
|
else
|
|
{
|
|
std::string s;
|
|
Orthanc::EmbeddedResources::GetFileResource(s, file);
|
|
|
|
if (file == Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX && theme_ != "light")
|
|
{
|
|
boost::replace_all(s, "data-bs-theme=\"light\"", "data-bs-theme=\"" + theme_ + "\"");
|
|
}
|
|
|
|
const char* resource = s.size() ? s.c_str() : NULL;
|
|
OrthancPluginAnswerBuffer(context, output, resource, s.size(), Orthanc::EnumerationToString(mime));
|
|
}
|
|
}
|
|
|
|
template <enum CustomFilesPath customFile>
|
|
void ServeCustomFile(OrthancPluginRestOutput* output,
|
|
const char* url,
|
|
const OrthancPluginHttpRequest* request)
|
|
{
|
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
|
|
|
if (request->method != OrthancPluginHttpMethod_Get)
|
|
{
|
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
|
}
|
|
else
|
|
{
|
|
std::string fileContent;
|
|
std::string customFileContent;
|
|
std::string customFilePath;
|
|
if (customFile == CustomFilesPath_FavIcon)
|
|
{
|
|
customFilePath = customFavIconPath_;
|
|
}
|
|
else if (customFile == CustomFilesPath_Logo)
|
|
{
|
|
customFilePath = customLogoPath_;
|
|
}
|
|
else
|
|
{
|
|
throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
|
|
}
|
|
|
|
Orthanc::SystemToolbox::ReadFile(fileContent, customFilePath);
|
|
Orthanc::MimeType mimeType = Orthanc::SystemToolbox::AutodetectMimeType(customFilePath);
|
|
|
|
// include an ETag for correct cache handling
|
|
OrthancPlugins::OrthancString md5;
|
|
size_t size = fileContent.size();
|
|
md5.Assign(OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), fileContent.c_str(), size));
|
|
|
|
std::string etag = "\"" + std::string(md5.GetContent()) + "\"";
|
|
OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "ETag", etag.c_str());
|
|
OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Cache-Control", "no-cache");
|
|
|
|
OrthancPluginAnswerBuffer(context, output, fileContent.c_str(), size, Orthanc::EnumerationToString(mimeType));
|
|
}
|
|
}
|
|
|
|
// serves either the default CSS or a custom file CSS
|
|
void ServeCustomCss(OrthancPluginRestOutput* output,
|
|
const char* url,
|
|
const OrthancPluginHttpRequest* request)
|
|
{
|
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
|
|
|
if (request->method != OrthancPluginHttpMethod_Get)
|
|
{
|
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
|
}
|
|
else
|
|
{
|
|
std::string cssFileContent;
|
|
|
|
if (strstr(url, "custom.css") != NULL)
|
|
{
|
|
if (theme_ == "dark")
|
|
{
|
|
Orthanc::EmbeddedResources::GetFileResource(cssFileContent, Orthanc::EmbeddedResources::DEFAULT_CSS_DARK);
|
|
}
|
|
else
|
|
{
|
|
Orthanc::EmbeddedResources::GetFileResource(cssFileContent, Orthanc::EmbeddedResources::DEFAULT_CSS_LIGHT);
|
|
}
|
|
|
|
if (!customCssPath_.empty())
|
|
{ // append the custom CSS
|
|
std::string customCssFileContent;
|
|
Orthanc::SystemToolbox::ReadFile(customCssFileContent, customCssPath_);
|
|
cssFileContent += "\n/* Appending the custom CSS */\n" + customCssFileContent;
|
|
}
|
|
}
|
|
|
|
const char* resource = cssFileContent.size() ? cssFileContent.c_str() : NULL;
|
|
size_t size = cssFileContent.size();
|
|
|
|
// include an ETag for correct cache handling
|
|
OrthancPlugins::OrthancString md5;
|
|
md5.Assign(OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), resource, size));
|
|
|
|
std::string etag = "\"" + std::string(md5.GetContent()) + "\"";
|
|
OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "ETag", etag.c_str());
|
|
OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Cache-Control", "no-cache");
|
|
|
|
OrthancPluginAnswerBuffer(context, output, resource, size, Orthanc::EnumerationToString(Orthanc::MimeType_Css));
|
|
}
|
|
}
|
|
|
|
|
|
void RedirectRoot(OrthancPluginRestOutput* output,
|
|
const char* url,
|
|
const OrthancPluginHttpRequest* request)
|
|
{
|
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
|
|
|
if (request->method != OrthancPluginHttpMethod_Get)
|
|
{
|
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
|
}
|
|
else
|
|
{
|
|
for (uint32_t i = 0; i < request->headersCount; ++i)
|
|
{
|
|
OrthancPlugins::LogError(std::string(request->headersKeys[i]) + " : " + request->headersValues[i]);
|
|
}
|
|
|
|
std::string oe2BaseApp = oe2BaseUrl_ + "app/";
|
|
OrthancPluginRedirect(context, output, &(oe2BaseApp.c_str()[1])); // remove the first '/' to make a relative redirect !
|
|
}
|
|
}
|
|
|
|
void MergeJson(Json::Value &a, const Json::Value &b) {
|
|
|
|
if (!a.isObject() || !b.isObject())
|
|
{
|
|
return;
|
|
}
|
|
|
|
Json::Value::Members members = b.getMemberNames();
|
|
|
|
for (size_t i = 0; i < members.size(); i++)
|
|
{
|
|
std::string key = members[i];
|
|
|
|
if (!a[key].isNull() && a[key].type() == Json::objectValue && b[key].type() == Json::objectValue)
|
|
{
|
|
MergeJson(a[key], b[key]);
|
|
}
|
|
else
|
|
{
|
|
// const std::string& val = b[key].asString();
|
|
a[key] = b[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void ReadConfiguration()
|
|
{
|
|
orthancFullConfiguration_.reset(new OrthancPlugins::OrthancConfiguration);
|
|
|
|
// read default configuration
|
|
std::string defaultConfigurationFileContent;
|
|
Orthanc::EmbeddedResources::GetFileResource(defaultConfigurationFileContent, Orthanc::EmbeddedResources::DEFAULT_CONFIGURATION);
|
|
|
|
Json::Value defaultConfiguration;
|
|
OrthancPlugins::ReadJsonWithoutComments(defaultConfiguration, defaultConfigurationFileContent);
|
|
pluginJsonConfiguration_ = defaultConfiguration["OrthancExplorer2"];
|
|
|
|
if (orthancFullConfiguration_->IsSection("OrthancExplorer2"))
|
|
{
|
|
OrthancPlugins::OrthancConfiguration pluginConfiguration(false);
|
|
orthancFullConfiguration_->GetSection(pluginConfiguration, "OrthancExplorer2");
|
|
|
|
Json::Value jsonConfig = pluginConfiguration.GetJson();
|
|
|
|
// backward compatibility
|
|
if (jsonConfig.isMember("UiOptions"))
|
|
{
|
|
// fix typo from version 0.7.0
|
|
if (jsonConfig["UiOptions"].isMember("EnableAnonimization") && !jsonConfig["UiOptions"].isMember("EnableAnonymization"))
|
|
{
|
|
LOG(WARNING) << "You are still using the 'UiOptions.EnableAnonimization' configuration that has a typo. You should use 'UiOptions.EnableAnonymization' instead.";
|
|
jsonConfig["UiOptions"]["EnableAnonymization"] = jsonConfig["UiOptions"]["EnableAnonimization"];
|
|
}
|
|
|
|
if (jsonConfig["UiOptions"].isMember("StudyListEmptyIfNoSearch") && !jsonConfig["UiOptions"].isMember("StudyListContentIfNoSearch"))
|
|
{
|
|
if (jsonConfig["UiOptions"]["StudyListEmptyIfNoSearch"].asBool() == true)
|
|
{
|
|
LOG(WARNING) << "You are still using the 'UiOptions.StudyListEmptyIfNoSearch' configuration that is now deprecated a typo. You should use 'UiOptions.StudyListContentIfNoSearch' instead.";
|
|
jsonConfig["UiOptions"]["StudyListContentIfNoSearch"] = "empty";
|
|
}
|
|
}
|
|
|
|
openInOhifV3IsExplicitelyDisabled = jsonConfig["UiOptions"].isMember("EnableOpenInOhifViewer3") && jsonConfig["UiOptions"]["EnableOpenInOhifViewer3"].asBool() == false;
|
|
}
|
|
|
|
MergeJson(pluginJsonConfiguration_, jsonConfig);
|
|
|
|
if (jsonConfig.isMember("CustomCssPath") && jsonConfig["CustomCssPath"].isString())
|
|
{
|
|
customCssPath_ = jsonConfig["CustomCssPath"].asString();
|
|
if (!Orthanc::SystemToolbox::IsExistingFile(customCssPath_))
|
|
{
|
|
LOG(ERROR) << "Unable to accesss the 'CustomCssPath': " << customCssPath_;
|
|
throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile);
|
|
}
|
|
}
|
|
|
|
if (jsonConfig.isMember("CustomLogoPath") && jsonConfig["CustomLogoPath"].isString())
|
|
{
|
|
customLogoPath_ = jsonConfig["CustomLogoPath"].asString();
|
|
if (!Orthanc::SystemToolbox::IsExistingFile(customLogoPath_))
|
|
{
|
|
LOG(ERROR) << "Unable to accesss the 'CustomLogoPath': " << customLogoPath_;
|
|
throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile);
|
|
}
|
|
}
|
|
|
|
if (jsonConfig.isMember("CustomLogoUrl") && jsonConfig["CustomLogoUrl"].isString())
|
|
{
|
|
customLogoUrl_ = jsonConfig["CustomLogoUrl"].asString();
|
|
}
|
|
|
|
if (jsonConfig.isMember("Theme") && jsonConfig["Theme"].isString() && jsonConfig["Theme"].asString() == "dark")
|
|
{
|
|
theme_ = "dark";
|
|
}
|
|
|
|
if (jsonConfig.isMember("CustomFavIconPath") && jsonConfig["CustomFavIconPath"].isString())
|
|
{
|
|
customFavIconPath_ = jsonConfig["CustomFavIconPath"].asString();
|
|
if (!Orthanc::SystemToolbox::IsExistingFile(customFavIconPath_))
|
|
{
|
|
LOG(ERROR) << "Unable to accesss the 'CustomFavIconPath': " << customFavIconPath_;
|
|
throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile);
|
|
}
|
|
}
|
|
|
|
if (jsonConfig.isMember("CustomTitle") && jsonConfig["CustomTitle"].isString())
|
|
{
|
|
customTitle_ = jsonConfig["CustomTitle"].asString();
|
|
}
|
|
}
|
|
|
|
enableShares_ = pluginJsonConfiguration_["UiOptions"]["EnableShares"].asBool(); // we are sure that the value exists since it is in the default configuration file
|
|
|
|
isReadOnly_ = orthancFullConfiguration_->GetBooleanValue("ReadOnly", false);
|
|
}
|
|
|
|
bool GetPluginConfiguration(Json::Value& jsonPluginConfiguration, const std::string& sectionName)
|
|
{
|
|
if (orthancFullConfiguration_->IsSection(sectionName))
|
|
{
|
|
OrthancPlugins::OrthancConfiguration pluginConfiguration(false);
|
|
orthancFullConfiguration_->GetSection(pluginConfiguration, sectionName);
|
|
|
|
jsonPluginConfiguration = pluginConfiguration.GetJson();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
bool IsPluginEnabledInConfiguration(const std::string& sectionName, const std::string& enableValueName, bool defaultValue)
|
|
{
|
|
if (orthancFullConfiguration_->IsSection(sectionName))
|
|
{
|
|
OrthancPlugins::OrthancConfiguration pluginConfiguration(false);
|
|
orthancFullConfiguration_->GetSection(pluginConfiguration, sectionName);
|
|
|
|
return pluginConfiguration.GetBooleanValue(enableValueName, defaultValue);
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
Json::Value GetPluginInfo(const std::string& pluginName)
|
|
{
|
|
Json::Value pluginInfo;
|
|
|
|
OrthancPlugins::RestApiGet(pluginInfo, "/plugins/" + pluginName, false);
|
|
|
|
return pluginInfo;
|
|
}
|
|
|
|
Json::Value GetKeycloakConfiguration()
|
|
{
|
|
if (pluginJsonConfiguration_.isMember("Keycloak"))
|
|
{
|
|
const Json::Value& keyCloakSection = pluginJsonConfiguration_["Keycloak"];
|
|
if (keyCloakSection.isMember("Enable") && keyCloakSection["Enable"].asBool() == true)
|
|
{
|
|
return pluginJsonConfiguration_["Keycloak"];
|
|
}
|
|
}
|
|
|
|
return Json::nullValue;
|
|
}
|
|
|
|
Json::Value GetPluginsConfiguration(bool& hasUserProfile)
|
|
{
|
|
Json::Value pluginsConfiguration;
|
|
Json::Value pluginList;
|
|
|
|
Orthanc::UriComponents components;
|
|
Orthanc::Toolbox::SplitUriComponents(components, oe2BaseUrl_);
|
|
std::string pluginUriPrefix = ""; // the RootUri is provided relative to Orthanc Explorer /app/explorer.html -> we need to correct this !
|
|
for (size_t i = 0; i < components.size(); i++)
|
|
{
|
|
pluginUriPrefix += "../";
|
|
}
|
|
|
|
OrthancPlugins::RestApiGet(pluginList, "/plugins", false);
|
|
|
|
for (Json::Value::ArrayIndex i = 0; i < pluginList.size(); i++)
|
|
{
|
|
Json::Value pluginConfiguration;
|
|
std::string pluginName = pluginList[i].asString();
|
|
|
|
if (pluginName == "explorer.js")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Json::Value pluginInfo = GetPluginInfo(pluginName);
|
|
|
|
if (pluginInfo.isMember("RootUri") && pluginInfo["RootUri"].asString().size() > 0)
|
|
{
|
|
pluginInfo["RootUri"] = pluginUriPrefix + pluginInfo["RootUri"].asString();
|
|
}
|
|
|
|
pluginsConfiguration[pluginName] = pluginInfo;
|
|
pluginsConfiguration[pluginName]["Enabled"] = true; // we assume that unknown plugins are enabled (if they are loaded by Orthanc)
|
|
|
|
if (pluginName == "authorization")
|
|
{
|
|
pluginConfiguration = Json::nullValue;
|
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "Authorization")
|
|
&& (pluginConfiguration.isMember("WebService")
|
|
|| pluginConfiguration.isMember("WebServiceRootUrl")
|
|
|| pluginConfiguration.isMember("WebServiceUserProfileUrl")
|
|
|| pluginConfiguration.isMember("WebServiceTokenValidationUrl")
|
|
|| pluginConfiguration.isMember("WebServiceTokenCreationBaseUrl")
|
|
|| pluginConfiguration.isMember("WebServiceTokenDecoderUrl"));
|
|
hasUserProfile = GetPluginConfiguration(pluginConfiguration, "Authorization") && (pluginConfiguration.isMember("WebServiceUserProfileUrl") || pluginConfiguration.isMember("WebServiceRootUrl"));
|
|
|
|
if (!pluginConfiguration.isMember("CheckedLevel") || pluginConfiguration["CheckedLevel"].asString() != "studies")
|
|
{
|
|
LOG(WARNING) << "When using OE2 and the authorization plugin together, you must set 'Authorization.CheckedLevel' to 'studies'. Unless you are using this orthanc only to generate tokens.";
|
|
}
|
|
}
|
|
else if (pluginName == "AWS S3 Storage")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "AwsS3Storage");
|
|
}
|
|
else if (pluginName == "Azure Blob Storage")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "AzureBlobStorage");
|
|
}
|
|
else if (pluginName == "connectivity-checks")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
|
}
|
|
else if (pluginName == "ohif")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
|
std::string ohifDataSource = "dicom-web";
|
|
if (GetPluginConfiguration(pluginConfiguration, "OHIF"))
|
|
{
|
|
if (pluginConfiguration.isMember("DataSource") && pluginConfiguration["DataSource"].asString() == "dicom-json")
|
|
{
|
|
ohifDataSource = "dicom-json";
|
|
}
|
|
}
|
|
pluginsConfiguration[pluginName]["DataSource"] = ohifDataSource;
|
|
}
|
|
else if (pluginName == "delayed-deletion")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("DelayedDeletion", "Enable", false);
|
|
}
|
|
else if (pluginName == "dicom-web")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("DicomWeb", "Enable", false);
|
|
}
|
|
else if (pluginName == "gdcm")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Gdcm", "Enable", true);
|
|
}
|
|
else if (pluginName == "Google Cloud Storage")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "GoogleCloudStorage");
|
|
}
|
|
else if (pluginName == "mysql-index")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("MySQL", "EnableIndex", false);
|
|
}
|
|
else if (pluginName == "mysql-storage")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("MySQL", "EnableStorage", false);
|
|
}
|
|
else if (pluginName == "odbc-index")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Odbc", "EnableIndex", false);
|
|
}
|
|
else if (pluginName == "odbc-storage")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Odbc", "EnableStorage", false);
|
|
}
|
|
else if (pluginName == "postgresql-index")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("PostgreSQL", "EnableIndex", false);
|
|
}
|
|
else if (pluginName == "postgresql-storage")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("PostgreSQL", "EnableStorage", false);
|
|
}
|
|
else if (pluginName == "osimis-web-viewer")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "WebViewer");
|
|
}
|
|
else if (pluginName == "python")
|
|
{
|
|
std::string notUsed;
|
|
pluginsConfiguration[pluginName]["Enabled"] = orthancFullConfiguration_->LookupStringValue(notUsed, "PythonScript");
|
|
}
|
|
else if (pluginName == "serve-folders")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "ServeFolders");
|
|
}
|
|
else if (pluginName == "stone-webviewer")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
|
}
|
|
else if (pluginName == "volview")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
|
}
|
|
else if (pluginName == "tcia")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Tcia", "Enable", false);
|
|
}
|
|
else if (pluginName == "transfers")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
|
}
|
|
else if (pluginName == "web-viewer")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
|
}
|
|
else if (pluginName == "worklists")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Worklists", "Enable", false);
|
|
}
|
|
else if (pluginName == "wsi")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
|
}
|
|
else if (pluginName == "multitenant-dicom")
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = false;
|
|
Json::Value pluginConfiguration;
|
|
if (GetPluginConfiguration(pluginConfiguration, "MultitenantDicom"))
|
|
{
|
|
pluginsConfiguration[pluginName]["Enabled"] = pluginConfiguration.isMember("Servers") && pluginConfiguration["Servers"].isArray() && pluginConfiguration["Servers"].size() > 0;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return pluginsConfiguration;
|
|
}
|
|
|
|
void UpdateUiOptions(Json::Value& uiOption, const std::list<std::string>& permissions, const std::string& anyOfPermissions)
|
|
{
|
|
std::vector<std::string> permissionsVector;
|
|
Orthanc::Toolbox::TokenizeString(permissionsVector, anyOfPermissions, '|');
|
|
|
|
bool hasPermission = false;
|
|
|
|
for (size_t i = 0; i < permissionsVector.size(); ++i)
|
|
{
|
|
hasPermission |= std::find(permissions.begin(), permissions.end(), permissionsVector[i]) != permissions.end();
|
|
}
|
|
|
|
uiOption = uiOption.asBool() && hasPermission;
|
|
}
|
|
|
|
void GetOE2Configuration(OrthancPluginRestOutput* output,
|
|
const char* /*url*/,
|
|
const OrthancPluginHttpRequest* request)
|
|
{
|
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
|
|
|
if (request->method != OrthancPluginHttpMethod_Get)
|
|
{
|
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
|
}
|
|
else
|
|
{
|
|
Json::Value oe2Configuration;
|
|
|
|
oe2Configuration["Plugins"] = pluginsConfiguration_;
|
|
oe2Configuration["UiOptions"] = pluginJsonConfiguration_["UiOptions"];
|
|
|
|
// if OHIF has not been explicitely disabled in the config and if the plugin is loaded, enable it
|
|
if (!openInOhifV3IsExplicitelyDisabled && pluginsConfiguration_.isMember("ohif"))
|
|
{
|
|
oe2Configuration["UiOptions"]["EnableOpenInOhifViewer3"] = true;
|
|
}
|
|
|
|
Json::Value tokens = pluginJsonConfiguration_["Tokens"];
|
|
if (!tokens.isMember("RequiredForLinks"))
|
|
{
|
|
tokens["RequiredForLinks"] = hasUserProfile_;
|
|
}
|
|
|
|
oe2Configuration["Tokens"] = tokens;
|
|
|
|
oe2Configuration["HasCustomLogo"] = !customLogoPath_.empty() || !customLogoUrl_.empty();
|
|
if (!customLogoUrl_.empty())
|
|
{
|
|
oe2Configuration["CustomLogoUrl"] = customLogoUrl_;
|
|
}
|
|
|
|
if (!customTitle_.empty())
|
|
{
|
|
oe2Configuration["CustomTitle"] = customTitle_;
|
|
}
|
|
|
|
Json::Value& uiOptions = oe2Configuration["UiOptions"];
|
|
|
|
if (hasUserProfile_)
|
|
{
|
|
{// get the available-labels from the auth plugin (and the auth-service)
|
|
std::map<std::string, std::string> headers;
|
|
OrthancPlugins::GetHttpHeaders(headers, request);
|
|
|
|
uiOptions["EnablePermissionsEdition"] = false;
|
|
|
|
Json::Value rolesConfig;
|
|
if (OrthancPlugins::RestApiGet(rolesConfig, "/auth/settings/roles", headers, true))
|
|
{
|
|
if (rolesConfig.isObject() && rolesConfig.isMember("available-labels"))
|
|
{
|
|
LOG(INFO) << "Overriding \"AvailableLabels\" in UiOptions with the values from the auth-service";
|
|
uiOptions["AvailableLabels"] = rolesConfig["available-labels"];
|
|
}
|
|
|
|
LOG(INFO) << rolesConfig.toStyledString();
|
|
|
|
// if the auth-service is not fully configured, disable permissions edition
|
|
if (rolesConfig.isObject() && rolesConfig.isMember("roles") && rolesConfig["roles"].isObject() && rolesConfig["roles"].size() > 0)
|
|
{
|
|
uiOptions["EnablePermissionsEdition"] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
{// get the user profile from the auth plugin (and the auth-service)
|
|
std::map<std::string, std::string> headers;
|
|
OrthancPlugins::GetHttpHeaders(headers, request);
|
|
|
|
Json::Value userProfile;
|
|
OrthancPlugins::RestApiGet(userProfile, "/auth/user/profile", headers, true);
|
|
|
|
// modify the UiOptions based on the user profile
|
|
std::list<std::string> permissions;
|
|
Orthanc::SerializationToolbox::ReadListOfStrings(permissions, userProfile, "permissions");
|
|
|
|
LOG(INFO) << "Overriding \"Enable...\" in UiOptions with the permissions from the auth-service for this user-profile";
|
|
|
|
UpdateUiOptions(uiOptions["EnableStudyList"], permissions, "all|view");
|
|
UpdateUiOptions(uiOptions["EnableViewerQuickButton"], permissions, "all|view");
|
|
UpdateUiOptions(uiOptions["EnableReportQuickButton"], permissions, "all|view");
|
|
UpdateUiOptions(uiOptions["EnableUpload"], permissions, "all|upload");
|
|
UpdateUiOptions(uiOptions["EnableAddSeries"], permissions, "all|upload");
|
|
UpdateUiOptions(uiOptions["EnableDicomModalities"], permissions, "all|q-r-remote-modalities");
|
|
UpdateUiOptions(uiOptions["EnableDeleteResources"], permissions, "all|delete");
|
|
UpdateUiOptions(uiOptions["EnableDownloadZip"], permissions, "all|download");
|
|
UpdateUiOptions(uiOptions["EnableDownloadDicomDir"], permissions, "all|download");
|
|
UpdateUiOptions(uiOptions["EnableDownloadDicomFile"], permissions, "all|download");
|
|
UpdateUiOptions(uiOptions["EnableModification"], permissions, "all|modify");
|
|
UpdateUiOptions(uiOptions["EnableAnonymization"], permissions, "all|anonymize");
|
|
UpdateUiOptions(uiOptions["EnableSendTo"], permissions, "all|send");
|
|
UpdateUiOptions(uiOptions["EnableApiViewMenu"], permissions, "all|api-view");
|
|
UpdateUiOptions(uiOptions["EnableSettings"], permissions, "all|settings");
|
|
UpdateUiOptions(uiOptions["EnableShares"], permissions, "all|share");
|
|
UpdateUiOptions(uiOptions["EnableEditLabels"], permissions, "all|edit-labels");
|
|
UpdateUiOptions(uiOptions["EnablePermissionsEdition"], permissions, "admin-permissions");
|
|
|
|
// the Legacy UI is not available with user profile since it would not refresh the tokens
|
|
uiOptions["EnableLinkToLegacyUi"] = false;
|
|
|
|
oe2Configuration["Profile"] = userProfile;
|
|
}
|
|
|
|
}
|
|
|
|
// disable operations on read only systems
|
|
if (isReadOnly_)
|
|
{
|
|
uiOptions["EnableUpload"] = false;
|
|
uiOptions["EnableAddSeries"] = false;
|
|
uiOptions["EnableDeleteResources"] = false;
|
|
uiOptions["EnableModification"] = false;
|
|
uiOptions["EnableAnonymization"] = false;
|
|
uiOptions["EnableEditLabels"] = false;
|
|
uiOptions["EnablePermissionsEdition"] = false;
|
|
}
|
|
|
|
|
|
oe2Configuration["Keycloak"] = GetKeycloakConfiguration();
|
|
std::string answer = oe2Configuration.toStyledString();
|
|
OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
|
|
}
|
|
}
|
|
|
|
void GetOE2PreLoginConfiguration(OrthancPluginRestOutput* output,
|
|
const char* /*url*/,
|
|
const OrthancPluginHttpRequest* request)
|
|
{
|
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
|
|
|
if (request->method != OrthancPluginHttpMethod_Get)
|
|
{
|
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
|
}
|
|
else
|
|
{
|
|
Json::Value oe2Configuration;
|
|
oe2Configuration["Keycloak"] = GetKeycloakConfiguration();
|
|
|
|
std::string answer = oe2Configuration.toStyledString();
|
|
OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
|
|
}
|
|
}
|
|
|
|
|
|
static bool DisplayPerformanceWarning(OrthancPluginContext* context)
|
|
{
|
|
(void) DisplayPerformanceWarning; // Disable warning about unused function
|
|
OrthancPluginLogWarning(context, "Performance warning in Orthanc Explorer 2: "
|
|
"Non-release build, runtime debug assertions are turned on");
|
|
return true;
|
|
}
|
|
|
|
|
|
static void CheckRootUrlIsValid(const std::string& value, const std::string& name, bool allowEmpty)
|
|
{
|
|
if (allowEmpty && value.size() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (value.size() < 1 ||
|
|
value[0] != '/' ||
|
|
value[value.size() - 1] != '/')
|
|
{
|
|
OrthancPlugins::LogError("Orthanc-Explorer 2: '" + name + "' configuration shall start with a '/' and end with a '/': " + value);
|
|
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
|
}
|
|
}
|
|
|
|
OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
|
|
OrthancPluginResourceType resourceType,
|
|
const char* resourceId)
|
|
{
|
|
try
|
|
{
|
|
if (changeType == OrthancPluginChangeType_OrthancStarted)
|
|
{
|
|
// this can not be performed during plugin initialization because it is accessing the DB -> must be done when Orthanc has just started
|
|
pluginsConfiguration_ = GetPluginsConfiguration(hasUserProfile_);
|
|
}
|
|
}
|
|
catch (Orthanc::OrthancException& e)
|
|
{
|
|
LOG(ERROR) << "Exception: " << e.What();
|
|
return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
|
|
}
|
|
|
|
return OrthancPluginErrorCode_Success;
|
|
}
|
|
|
|
|
|
extern "C"
|
|
{
|
|
ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
|
|
{
|
|
assert(DisplayPerformanceWarning(context));
|
|
|
|
OrthancPlugins::SetGlobalContext(context);
|
|
|
|
Orthanc::Logging::InitializePluginContext(context);
|
|
|
|
Orthanc::Logging::EnableInfoLevel(true);
|
|
|
|
|
|
/* Check the version of the Orthanc core */
|
|
if (!OrthancPlugins::CheckMinimalOrthancVersion(ORTHANC_CORE_MINIMAL_MAJOR,
|
|
ORTHANC_CORE_MINIMAL_MINOR,
|
|
ORTHANC_CORE_MINIMAL_REVISION))
|
|
{
|
|
OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_CORE_MINIMAL_MAJOR,
|
|
ORTHANC_CORE_MINIMAL_MINOR,
|
|
ORTHANC_CORE_MINIMAL_REVISION);
|
|
return -1;
|
|
}
|
|
|
|
OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Advanced User Interface for Orthanc");
|
|
|
|
try
|
|
{
|
|
ReadConfiguration();
|
|
|
|
if (pluginJsonConfiguration_["Enable"].asBool())
|
|
{
|
|
oe2BaseUrl_ = pluginJsonConfiguration_["Root"].asString();
|
|
|
|
CheckRootUrlIsValid(oe2BaseUrl_, "Root", false);
|
|
|
|
OrthancPlugins::LogWarning("Root URI to the Orthanc-Explorer 2 application: " + oe2BaseUrl_);
|
|
|
|
|
|
OrthancPlugins::RegisterRestCallback
|
|
<ServeCustomCss>
|
|
(oe2BaseUrl_ + "app/customizable/custom.css", true);
|
|
|
|
if (!customLogoPath_.empty())
|
|
{
|
|
OrthancPlugins::RegisterRestCallback
|
|
<ServeCustomFile<CustomFilesPath_Logo> >
|
|
(oe2BaseUrl_ + "app/customizable/custom-logo", true);
|
|
}
|
|
|
|
// we need to mix the "routing" between the server and the frontend (vue-router)
|
|
// first part are the files that are 'static files' that must be served by the backend
|
|
OrthancPlugins::RegisterRestCallback
|
|
<ServeEmbeddedFolder<Orthanc::EmbeddedResources::WEB_APPLICATION_ASSETS> >
|
|
(oe2BaseUrl_ + "app/assets/(.*)", true);
|
|
OrthancPlugins::RegisterRestCallback
|
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX, Orthanc::MimeType_Html> >
|
|
(oe2BaseUrl_ + "app/index.html", true);
|
|
OrthancPlugins::RegisterRestCallback
|
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX_LANDING, Orthanc::MimeType_Html> >
|
|
(oe2BaseUrl_ + "app/token-landing.html", true);
|
|
OrthancPlugins::RegisterRestCallback
|
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX_RETRIEVE_AND_VIEW, Orthanc::MimeType_Html> >
|
|
(oe2BaseUrl_ + "app/retrieve-and-view.html", true);
|
|
|
|
if (customFavIconPath_.empty())
|
|
{
|
|
OrthancPlugins::RegisterRestCallback
|
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_FAVICON, Orthanc::MimeType_Ico> >
|
|
(oe2BaseUrl_ + "app/favicon.ico", true);
|
|
}
|
|
else
|
|
{
|
|
OrthancPlugins::RegisterRestCallback
|
|
<ServeCustomFile<CustomFilesPath_FavIcon> >
|
|
(oe2BaseUrl_ + "app/favicon.ico", true);
|
|
}
|
|
// second part are all the routes that are actually handled by vue-router and that are actually returning the same file (index.html)
|
|
OrthancPlugins::RegisterRestCallback
|
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX, Orthanc::MimeType_Html> >
|
|
(oe2BaseUrl_ + "app/(.*)", true);
|
|
OrthancPlugins::RegisterRestCallback
|
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX, Orthanc::MimeType_Html> >
|
|
(oe2BaseUrl_ + "app", true);
|
|
|
|
OrthancPlugins::RegisterRestCallback<GetOE2Configuration>(oe2BaseUrl_ + "api/configuration", true);
|
|
OrthancPlugins::RegisterRestCallback<GetOE2PreLoginConfiguration>(oe2BaseUrl_ + "api/pre-login-configuration", true);
|
|
|
|
std::string pluginRootUri = oe2BaseUrl_ + "app/";
|
|
OrthancPlugins::SetRootUri(ORTHANC_PLUGIN_NAME, pluginRootUri.c_str());
|
|
|
|
if (pluginJsonConfiguration_["IsDefaultOrthancUI"].asBool())
|
|
{
|
|
OrthancPlugins::RegisterRestCallback<RedirectRoot>("/", true);
|
|
}
|
|
|
|
OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
|
|
|
|
{
|
|
std::string explorer;
|
|
Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER);
|
|
|
|
std::map<std::string, std::string> dictionary;
|
|
dictionary["OE2_BASE_URL"] = oe2BaseUrl_.substr(1, oe2BaseUrl_.size() - 2); // Remove heading and trailing slashes
|
|
std::string explorerConfigured = Orthanc::Toolbox::SubstituteVariables(explorer, dictionary);
|
|
|
|
OrthancPluginExtendOrthancExplorer(OrthancPlugins::GetGlobalContext(), explorerConfigured.c_str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OrthancPlugins::LogWarning("Orthanc Explorer 2 plugin is disabled");
|
|
}
|
|
}
|
|
catch (Orthanc::OrthancException& e)
|
|
{
|
|
OrthancPlugins::LogError("Exception while initializing the Orthanc-Explorer 2 plugin: " +
|
|
std::string(e.What()));
|
|
return -1;
|
|
}
|
|
catch (...)
|
|
{
|
|
OrthancPlugins::LogError("Exception while initializing the Orthanc-Explorer 2 plugin");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
ORTHANC_PLUGINS_API void OrthancPluginFinalize()
|
|
{
|
|
}
|
|
|
|
|
|
ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
|
|
{
|
|
return ORTHANC_PLUGIN_NAME;
|
|
}
|
|
|
|
|
|
ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
|
|
{
|
|
return ORTHANC_OE2_VERSION;
|
|
}
|
|
}
|