Skip to content

OHIF 同步與資料源設定調整:回傳 series 選擇並改走 3001 proxy#5902

Closed
nancy4358 wants to merge 1 commit into
OHIF:masterfrom
nancy4358:dev-Nancy
Closed

OHIF 同步與資料源設定調整:回傳 series 選擇並改走 3001 proxy#5902
nancy4358 wants to merge 1 commit into
OHIF:masterfrom
nancy4358:dev-Nancy

Conversation

@nancy4358
Copy link
Copy Markdown

@nancy4358 nancy4358 commented Mar 17, 2026

Context

Changes & Results

Testing

Checklist

PR

  • [] My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • [] My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • [] The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • [] OS:
  • [] Node version:
  • [] Browser:

Greptile Summary

This PR adds an iframe-to-parent communication bridge that forwards the active OHIF selection (study UID and series UID) to an embedding parent application, and redirects local DICOMWeb traffic through a custom proxy on port 3001. While the core idea of syncing selection state to a parent frame is sound, the implementation has several notable issues that need addressing before merging.

Key changes:

  • local_orthanc.js: Replaces standard http://localhost/dicom-web Orthanc endpoints with a hardcoded personal proxy at http://127.0.0.1:3001/api/orthanc/...this will break local Orthanc setup for all other contributors
  • index.js: Adds setupParentSelectionBridge() which monkey-patches window.history and polls every 1.2 s to send OHIF_SELECTION messages to a parent iframe; the setInterval is never cleared
  • Mode.tsx: Adds a useEffect that subscribes to viewport grid/display-set events and posts richer OHIF_SELECTION messages (including seriesInstanceUID) — but creates a duplicate message stream conflicting with the one in index.js

Issues found:

  • local_orthanc.js hardcodes a non-standard proxy that is specific to one developer's environment and will break other contributors' setups
  • Both index.js and Mode.tsx independently emit type: 'OHIF_SELECTION' messages, so the parent app receives duplicate and potentially conflicting events on every navigation
  • The setInterval fallback in index.js is never cleared — a permanent resource leak
  • postMessage uses '*' as the target origin in both files, which can leak DICOM study UIDs to any malicious host page embedding the viewer
  • The message payload in Mode.tsx uses plural field names (studyInstanceUIDs, seriesInstanceUIDs) but sets them to single strings, creating an inconsistent API contract for the parent consumer

Confidence Score: 1/5

  • Not safe to merge — the config change breaks the shared local dev setup, and the dual-bridge design creates duplicate messages with a security concern.
  • Three files each have at least one blocking issue: local_orthanc.js commits a personal proxy URL that will break other contributors' environments; index.js introduces a permanent uncleared interval and duplicates the message type already emitted by Mode.tsx; and both JavaScript files use postMessage('*') which leaks DICOM metadata to any embedding origin. None of the checklist items in the PR template are ticked, and no context, screenshots, or testing instructions are provided.
  • platform/app/public/config/local_orthanc.js (breaks shared config), platform/app/src/index.js (resource leak + duplicate messages + security), platform/app/src/routes/Mode/Mode.tsx (security + naming inconsistency)

Important Files Changed

Filename Overview
platform/app/src/index.js Adds setupParentSelectionBridge() to forward navigation events to an iframe parent via postMessage. Three issues: the setInterval fallback is never cleared (resource leak), the target origin is '*' (information disclosure), and it duplicates the OHIF_SELECTION message type that Mode.tsx now also emits with richer data.
platform/app/src/routes/Mode/Mode.tsx Adds a useEffect that subscribes to viewport/display-set changes and posts the active study+series selection to a parent iframe via postMessage. The subscription cleanup is correct. Main concerns are '*' target origin and confusingly plural field names holding single string values.
platform/app/public/config/local_orthanc.js Replaces the standard http://localhost/dicom-web DICOMWeb endpoints with a personal proxy at http://127.0.0.1:3001/api/orthanc/.... This breaks the shared local dev configuration for all other contributors who do not have this proxy running.

Sequence Diagram

sequenceDiagram
    participant Parent as Parent App (iframe host)
    participant IndexJS as index.js bridge<br/>(setupParentSelectionBridge)
    participant ModeRoute as Mode.tsx useEffect<br/>(viewport subscription)
    participant VGS as viewportGridService
    participant DSS as displaySetService

    Note over IndexJS: Runs once at bootstrap.<br/>Patches pushState/replaceState.<br/>Starts setInterval(1200ms) — never cleared.

    Parent->>IndexJS: (embeds OHIF in iframe)
    IndexJS-->>Parent: postMessage OHIF_SELECTION<br/>{studyInstanceUIDs, href} target='*'

    Note over ModeRoute: Runs after ExtensionDependenciesLoaded<br/>+ studyInstanceUIDs are set.

    ModeRoute->>VGS: subscribe(ACTIVE_VIEWPORT_ID_CHANGED)
    ModeRoute->>VGS: subscribe(GRID_STATE_CHANGED)
    ModeRoute->>DSS: subscribe(DISPLAY_SETS_CHANGED)
    ModeRoute->>VGS: getState() → activeViewport
    VGS-->>ModeRoute: gridState
    ModeRoute->>DSS: getDisplaySetByUID(displaySetUID)
    DSS-->>ModeRoute: displaySet {StudyInstanceUID, SeriesInstanceUID}
    ModeRoute-->>Parent: postMessage OHIF_SELECTION<br/>{studyInstanceUIDs (single string),<br/>seriesInstanceUIDs (single string), href}<br/>target='*'

    Note over Parent: Receives TWO OHIF_SELECTION messages<br/>per navigation — first from index.js,<br/>then from Mode.tsx.
Loading

Last reviewed commit: b911efb

Greptile also left 6 inline comments on this PR.

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 17, 2026

Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit b911efb
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/69b9018b0bc4450008f03426
😎 Deploy Preview https://deploy-preview-5902--ohif-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment thread platform/app/src/index.js

emitSelection();
// Poll as fallback for navigation paths that do not trigger history events.
setInterval(emitSelection, 1200);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 setInterval never cleared — resource leak

The interval started here has no corresponding clearInterval. Because setupParentSelectionBridge runs once at app bootstrap and the return value of setInterval is discarded, this interval fires every 1200 ms for the entire lifetime of the page with no way to stop it. Even though the emitSelection guard (lastPayload) keeps it cheap when nothing changes, it still represents a permanent timer that cannot be cleaned up.

Suggested change
setInterval(emitSelection, 1200);
const intervalId = setInterval(emitSelection, 1200);
// Return a cleanup function so callers can stop the interval if needed.
return () => {
clearInterval(intervalId);
window.removeEventListener('popstate', emitSelection);
window.removeEventListener('hashchange', emitSelection);
};

Comment thread platform/app/src/index.js
}

lastPayload = payload;
window.parent.postMessage(JSON.parse(payload), '*');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unnecessary JSON.stringifyJSON.parse round-trip

payload is already serialised to a string so it can be compared cheaply with lastPayload. On line 46 it is immediately deserialised back to an object before being passed to postMessage. postMessage performs its own structured-clone serialisation, so the double round-trip is redundant and slightly wasteful.

Suggested change
window.parent.postMessage(JSON.parse(payload), '*');
window.parent.postMessage(
{ type: 'OHIF_SELECTION', studyInstanceUIDs, href: window.location.href },
'*'
);

Comment thread platform/app/src/index.js
Comment on lines +36 to +46
type: 'OHIF_SELECTION',
studyInstanceUIDs,
href: window.location.href,
});

if (payload === lastPayload) {
return;
}

lastPayload = payload;
window.parent.postMessage(JSON.parse(payload), '*');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 postMessage target origin is '*' — information disclosure risk

Using '*' as the target origin means this message (containing studyInstanceUIDs and the full href) will be delivered to any page that embeds the viewer in an iframe, regardless of its origin. An attacker who social-engineers a user into visiting a malicious page embedding this viewer would receive DICOM study UIDs silently.

Best practice is to accept a configurable allowedParentOrigin from window.config and pass it as the target origin:

window.parent.postMessage(payload, window.config?.allowedParentOrigin ?? '*');

The same concern applies to Mode.tsx line 241.

Comment thread platform/app/src/index.js
Comment on lines +23 to +70
function setupParentSelectionBridge() {
// Bridge OHIF selection state to embedding parent app (iframe host).
if (!window.parent || window.parent === window) {
return;
}

let lastPayload = '';

const emitSelection = () => {
try {
const params = new URLSearchParams(window.location.search || '');
const studyInstanceUIDs = params.get('StudyInstanceUIDs') || '';
const payload = JSON.stringify({
type: 'OHIF_SELECTION',
studyInstanceUIDs,
href: window.location.href,
});

if (payload === lastPayload) {
return;
}

lastPayload = payload;
window.parent.postMessage(JSON.parse(payload), '*');
} catch (error) {
// Ignore bridge errors to avoid impacting viewer rendering.
}
};

const wrapHistoryMethod = methodName => {
const original = window.history[methodName];
window.history[methodName] = function (...args) {
const result = original.apply(this, args);
setTimeout(emitSelection, 0);
return result;
};
};

wrapHistoryMethod('pushState');
wrapHistoryMethod('replaceState');

window.addEventListener('popstate', emitSelection);
window.addEventListener('hashchange', emitSelection);

emitSelection();
// Poll as fallback for navigation paths that do not trigger history events.
setInterval(emitSelection, 1200);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Duplicate OHIF_SELECTION messages from two independent senders

setupParentSelectionBridge() in index.js (URL-only, no series info) and the new useEffect in Mode.tsx (rich viewport selection with series UID) both post messages with type: 'OHIF_SELECTION' to the parent window. Any given navigation event will likely trigger both handlers, causing the parent app to receive two messages in quick succession — the first with only studyInstanceUIDs, then a second (possibly more accurate) one overwriting it with seriesInstanceUIDs as well.

If Mode.tsx's richer handler is the intended canonical source of truth, the URL-based index.js bridge probably no longer needs to emit OHIF_SELECTION. Consider giving the index.js bridge a distinct type (e.g. OHIF_NAVIGATION) or removing its emission entirely now that Mode.tsx covers the same use-case with richer data.

Comment on lines +234 to +242
window.parent.postMessage(
{
type: 'OHIF_SELECTION',
studyInstanceUIDs: studyInstanceUID,
seriesInstanceUIDs: seriesInstanceUID,
href: window.location.href,
},
'*'
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Plural field name contains a single UID — confusing contract for parent app

The message field studyInstanceUIDs holds the value of studyInstanceUID (a single string), and seriesInstanceUIDs holds a single series UID string. Using plural names for singular values creates an ambiguous API contract for the parent app consumer, who may reasonably expect an array.

Consider either renaming to the singular form or always wrapping the values in an array to keep the contract consistent with what index.js sends:

Suggested change
window.parent.postMessage(
{
type: 'OHIF_SELECTION',
studyInstanceUIDs: studyInstanceUID,
seriesInstanceUIDs: seriesInstanceUID,
href: window.location.href,
},
'*'
);
window.parent.postMessage(
{
type: 'OHIF_SELECTION',
studyInstanceUID: studyInstanceUID,
seriesInstanceUID: seriesInstanceUID,
href: window.location.href,
},
'*'
);

Comment on lines +22 to +24
wadoUriRoot: 'http://127.0.0.1:3001/api/orthanc/wado',
qidoRoot: 'http://127.0.0.1:3001/api/orthanc/dicom-web',
wadoRoot: 'http://127.0.0.1:3001/api/orthanc/dicom-web',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Hardcoded personal proxy breaks shared config for all other developers

local_orthanc.js is the shared, out-of-the-box local development configuration used by every contributor who runs OHIF against a local Orthanc instance. Replacing the standard http://localhost/dicom-web with http://127.0.0.1:3001/api/orthanc/... hard-wires this to a custom liverct-server proxy that only exists in one developer's environment.

Anyone cloning the repo and running the default local setup will immediately hit connection failures with no Orthanc data loading. This change should either:

  • Be kept in a local, gitignored override file (e.g. local_orthanc.local.js), or
  • Document and upstream the proxy layer as a required dependency alongside the usual Orthanc setup instructions.

@nancy4358 nancy4358 closed this by deleting the head repository Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant