From af8bb27575793892f7f684f3523f009d5106794c Mon Sep 17 00:00:00 2001
From: Ashish Narnoli <54370515+anarnoli@users.noreply.github.com>
Date: Tue, 16 May 2023 00:22:48 +0530
Subject: [PATCH] feat: query string dynamic app config (#3391)
* feat: query string dynamic app config
* fix: bug fix and couple of very small improvements
---
extensions/default/src/ViewerLayout/index.tsx | 24 ++++++++++---
.../docs/configuration/configurationFiles.md | 19 ++++++++++-
platform/viewer/public/config/default.js | 10 ++++++
platform/viewer/src/index.js | 34 ++++++++++++-------
platform/viewer/src/loadDynamicConfig.js | 23 +++++++++++++
.../viewer/src/routes/DataSourceWrapper.tsx | 1 +
.../viewer/src/routes/WorkList/WorkList.tsx | 11 ++++--
7 files changed, 101 insertions(+), 21 deletions(-)
create mode 100644 platform/viewer/src/loadDynamicConfig.js
diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx
index 30b8932a5..fc2ad3626 100644
--- a/extensions/default/src/ViewerLayout/index.tsx
+++ b/extensions/default/src/ViewerLayout/index.tsx
@@ -46,13 +46,27 @@ function ViewerLayout({
const onClickReturnButton = () => {
const { pathname } = location;
const dataSourceIdx = pathname.indexOf('/', 1);
- const search =
- dataSourceIdx === -1
- ? undefined
- : `datasources=${pathname.substring(dataSourceIdx + 1)}`;
+ // const search =
+ // dataSourceIdx === -1
+ // ? undefined
+ // : `datasources=${pathname.substring(dataSourceIdx + 1)}`;
+
+ // Todo: Handle parameters in a better way.
+ const query = new URLSearchParams(window.location.search);
+ const configUrl = query.get('configUrl');
+
+ const searchQuery = new URLSearchParams();
+ if (dataSourceIdx !== -1) {
+ searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1));
+ }
+
+ if (configUrl) {
+ searchQuery.append('configUrl', configUrl);
+ }
+
navigate({
pathname: '/',
- search,
+ search: decodeURIComponent(searchQuery.toString()),
});
};
diff --git a/platform/docs/docs/configuration/configurationFiles.md b/platform/docs/docs/configuration/configurationFiles.md
index d8cf8ded6..141b2c005 100644
--- a/platform/docs/docs/configuration/configurationFiles.md
+++ b/platform/docs/docs/configuration/configurationFiles.md
@@ -116,7 +116,24 @@ Here are a list of some options available:
if auth headers are used, a preflight request is required.
- `maxNumRequests`: The maximum number of requests to allow in parallel. It is an object with keys of `interaction`, `thumbnail`, and `prefetch`. You can specify a specific number for each type.
- `showLoadingIndicator`: (default to true), if set to false, the loading indicator will not be shown when navigating between studies.
-
+- `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.
+Points to consider while using `dangerouslyUseDynamicConfig`:
+ - User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`.
+ - Regex helps to avoid easy exploit. Dafault is `/.*/`. Setup your own regex to choose a specific source of configuration only.
+ - System administrators can return `cross-origin: same-origin` with OHIF files to disallow any loading from other origin. It will block read access to resources loaded from a different origin to avoid potential attack vector.
+ - Example config:
+ ```js
+ dangerouslyUseDynamicConfig: {
+ enabled: false,
+ regex: /.*/
+ }
+ ```
+ > Example 1, to allow numbers and letters in an absolute or sub-path only.
+`regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/`
+Example 2, to restricts to either hosptial.com or othersite.com.
+`regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/`
+Example usage:
+`http://localhost:3000/?configUrl=http://localhost:3000/config/example.json`
diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js
index 757bdc344..ff93f31a1 100644
--- a/platform/viewer/public/config/default.js
+++ b/platform/viewer/public/config/default.js
@@ -25,6 +25,16 @@ window.config = {
},
// filterQueryParam: false,
defaultDataSourceName: 'dicomweb',
+ /* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */
+ // dangerouslyUseDynamicConfig: {
+ // enabled: true,
+ // // regex will ensure valid configuration source and default is /.*/ which matches any character. To use this, setup your own regex to choose a specific source of configuration only.
+ // // Example 1, to allow numbers and letters in an absolute or sub-path only.
+ // // regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/
+ // // Example 2, to restricts to either hosptial.com or othersite.com.
+ // // regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/
+ // regex: /.*/,
+ // },
dataSources: [
{
friendlyName: 'dcmjs DICOMWeb Server',
diff --git a/platform/viewer/src/index.js b/platform/viewer/src/index.js
index 71d48e2ef..6a6c064bf 100644
--- a/platform/viewer/src/index.js
+++ b/platform/viewer/src/index.js
@@ -20,20 +20,28 @@ import {
modes as defaultModes,
extensions as defaultExtensions,
} from './pluginImports';
+import loadDynamicConfig from './loadDynamicConfig';
-/**
- * Combine our appConfiguration with installed extensions and modes.
- * In the future appConfiguration may contain modes added at runtime.
- * */
-const appProps = {
- config: window ? window.config : {},
- defaultExtensions,
- defaultModes,
-};
+loadDynamicConfig(window.config).then(config_json => {
+ // Reset Dynamic config if defined
+ if (config_json !== null) {
+ window.config = config_json;
+ }
-/** Create App */
-const app = React.createElement(App, appProps, null);
-/** Render */
-ReactDOM.render(app, document.getElementById('root'));
+ /**
+ * Combine our appConfiguration with installed extensions and modes.
+ * In the future appConfiguration may contain modes added at runtime.
+ * */
+ const appProps = {
+ config: window ? window.config : {},
+ defaultExtensions,
+ defaultModes,
+ };
+
+ /** Create App */
+ const app = React.createElement(App, appProps, null);
+ /** Render */
+ ReactDOM.render(app, document.getElementById('root'));
+});
export { history };
diff --git a/platform/viewer/src/loadDynamicConfig.js b/platform/viewer/src/loadDynamicConfig.js
new file mode 100644
index 000000000..f646eefef
--- /dev/null
+++ b/platform/viewer/src/loadDynamicConfig.js
@@ -0,0 +1,23 @@
+export default async config => {
+ const useDynamicConfig = config.dangerouslyUseDynamicConfig;
+
+ // Check if dangerouslyUseDynamicConfig enabled
+ if (useDynamicConfig?.enabled) {
+ // If enabled then get configUrl query-string
+ let query = new URLSearchParams(window.location.search);
+ let configUrl = query.get('configUrl');
+
+ if (configUrl) {
+ // validate regex
+ const regex = useDynamicConfig.regex;
+
+ if (configUrl.match(regex)) {
+ const response = await fetch(configUrl);
+ return response.json();
+ } else {
+ return null;
+ }
+ }
+ }
+ return null;
+};
diff --git a/platform/viewer/src/routes/DataSourceWrapper.tsx b/platform/viewer/src/routes/DataSourceWrapper.tsx
index d5357875e..026bba14c 100644
--- a/platform/viewer/src/routes/DataSourceWrapper.tsx
+++ b/platform/viewer/src/routes/DataSourceWrapper.tsx
@@ -170,6 +170,7 @@ function _getQueryFilterValues(query, queryLimit) {
// Offset...
offset:
Math.floor((pageNumber * resultsPerPage) / queryLimit) * (queryLimit - 1),
+ config: query.get('configUrl'),
};
// patientName: good
diff --git a/platform/viewer/src/routes/WorkList/WorkList.tsx b/platform/viewer/src/routes/WorkList/WorkList.tsx
index 7cbc57cf1..ee3cc0aac 100644
--- a/platform/viewer/src/routes/WorkList/WorkList.tsx
+++ b/platform/viewer/src/routes/WorkList/WorkList.tsx
@@ -345,12 +345,17 @@ function WorkList({
// mode.routeName
// mode.routes[x].path
// Don't specify default data source, and it should just be picked up... (this may not currently be the case)
- // How do we know which params to pass? Today, it's just StudyInstanceUIDs
+ // How do we know which params to pass? Today, it's just StudyInstanceUIDs and configUrl if exists
+ const query = new URLSearchParams();
+ if (filterValues.configUrl) {
+ query.append('configUrl', filterValues.configUrl);
+ }
+ query.append('StudyInstanceUIDs', studyInstanceUid);
return (