1. Stored XSS in populateMetaPanel (viewer.js): IFC type and storey
names from model data were interpolated unescaped into innerHTML.
Added hesc() helper and applied it to all IFC-derived values.
2. sessionStorage CSS injection (viewer.html): viewerSnapshot value was
used unsanitised in style.backgroundImage. Now validated to start
with 'data:image/jpeg;base64,' before use.
3. Hostname-based SSRF bypass (service.py): _is_private_ip() only
blocked literal IP addresses; hostnames resolving to private
addresses bypassed the check. Now resolves all addresses via
socket.getaddrinfo() and rejects if any resolve to a private range.
Unresolvable hostnames are treated as private (fail-safe).
4. SSRF + bearer token exfiltration via BCF snapshot (bcf_api.py):
get_snapshot() forwarded the caller's Authorization token to the
preview service for an ifc:// URL stored in a Forgejo comment body
(attacker-controlled). Now parses and validates the stored URL
through _ssrf_check(), and uses the server-side token for the
ifc:// host instead of the caller's credential.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
applySelector() now returns the resolved GUIDs array in both the simple
(ThatOpen classifier → getGuid()) and complex (/select → GUIDs already
fetched) paths. generateBcf() reads currentGuids directly and builds the
full BCF 2.1 zip in the browser, including <Selection>/<Exceptions> in
the viewpoint component list.
The Python POST /bcf endpoint, BcfRequest model, and build_bcf()/
_viewpoint_xml()/_markup_xml() functions are removed — the browser had
all the data needed to do this itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Capture the WebGL canvas client-side before POSTing to /bcf, send the
base64 PNG as a new `snapshot` field, and embed it as snapshot.png in
the zip alongside viewpoint.bcfv. Also wires up the <Snapshot> element
in markup.bcf when a snapshot is present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Forgejo editor previews (non-contenteditable) were being modified by
linkifyBareIfcUrls and replaceIfcAnchors, corrupting the editor state.
Restrict both functions to .markup containers (rendered issue/wiki/README
content) so editor UI is never touched.
Forgejo's WYSIWYG editor uses a contenteditable div. Walking text nodes
inside it caused linkifyBareIfcUrls to split [label](ifc://...) by wrapping
just the URL in an anchor, which the editor serialised back as [label]().
Bare ifc:// URLs in markdown are now handled entirely client-side:
linkifyBareIfcUrls() walks rendered HTML text nodes and promotes bare
ifc:// strings to <a> elements, which replaceIfcAnchors() then converts
to preview figures. The [title](ifc://...) form was already handled by
Goldmark's standard link parser, so no Go extension is needed at all.
Removes ifc_url.go and ifc_url_test.go (the goldmark AST transformer
and renderer) from the repository.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
web-ifc disposes its IfcAPI after load so context filtering must happen
before the buffer reaches ifcLoader.load(). Pre-process the IFC-SPF buffer
to find IfcGeometricRepresentationSubContext entities with ContextIdentifier
'Clearance', then empty the items list of any IFCSHAPEREPRESENTATION that
references those contexts, so door-swing and equipment-clearance volumes
produce no geometry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Inline script at top of <body> creates the snapshot overlay immediately
on page load, before the deferred module script runs, eliminating the
black flash between navigation and overlay appearance
- Save a JPEG snapshot to sessionStorage before navigating to a new ref;
the overlay covers the blank WebGL canvas while the model reloads
- Fade the overlay out after two animation frames post-load so the canvas
has painted before the transition begins
- Reload on pageshow with persisted=true: bfcache restore left the WebGL
canvas blank and skipped the inline script; a fresh reload lets the
snapshot overlay work correctly
- Replace native <datalist> ref picker with a custom dropdown; the native
datalist won't open programmatically via .focus() in most browsers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the ifc:// URL is edited and Enter pressed, capture toRawUrl() of
the loaded model at load time. On any subsequent loadUrl() call, compare
against that snapshot: if the raw file URL is unchanged, call the new
applyViewChanges() function in-place instead of navigating to a new page.
applyViewChanges() diffs old vs new URL query params and applies only what
changed: camera position (with cameraControls.update(0) flush before
render), FOV/scale, clipping planes, selector/visibility, and query. A
full page reload now only happens when host, repo, ref, or path changes.
Fixes ifcurl-ntm.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add /query proxy rule to nginx config (was missing, caused 404)
- Fix populateMetaPanel: call groupData.get() instead of reading empty
groupData.map, and use ids.size (Set) not ids.length
- Update CLAUDE.md check #17 to reflect type counts now working
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
web-ifc's COORDINATE_TO_ORIGIN=true shifts all model geometry so the first
element lands at origin (≈12m shift for simple-ifc). GetCoordinationMatrix
returns identity despite the shift, so engine_fragment can't compensate.
Camera URLs are in IFC world coords (per spec §5), but the model was in
shifted coords, causing the JS viewer and Python renderer to disagree.
Set COORDINATE_TO_ORIGIN: false in ifcLoader.setup() so model geometry
stays in IFC world coords, matching what IfcOpenShell outputs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ifcurl.js: URL-pattern cases (PR diff, commit page) now run before
the DOM-based file-view case, which also matches on Forgejo diff pages
and was causing an early return that skipped PR diff rendering.
git.py: fetch_ifc now retries with a full ref fetch when an immutable
ref (commit hash) is not found in the cached clone — PR branch commits
are absent from the initial default-branch clone and need a fetch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
service.py: return 400 with a clear message when query= looks like a
bare property set name (starts with Pset_ or Qto_ but has no dot).
Catches the common mistake of writing Pset_WallCommon instead of
Pset_WallCommon.FireRating, giving feedback instead of silent empty results.
viewer.html: update query input placeholder and title to show the
expected formats (Name, Description for attributes; Pset_X.Prop for
property sets).
https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
viewer-url.js: parseIfcUrl returns query field; buildIfcUrl gains optional
fifth query param, included in the QS when non-empty.
viewer.html: add 'query' text field (label + input) in toolbar row 2
after the visibility selector. Add #query-panel (fixed bottom-centre)
with a scrollable table showing GlobalId | value rows, a header
displaying the query path, and a close button.
viewer.js: populate queryInput from parsed URL on load; include query
in buildIfcUrl() wrapper; wire queryInput into Enter-key and focus/blur
listeners. Add applyQuery(srcUrl, queryPath) which calls GET /query,
populates the table, and shows the panel. Called from main() after
applySelector when both selector and query params are present.
viewer-url.test.js: add tests for parseIfcUrl query field, buildIfcUrl
with query, and buildIfcUrl omitting empty query.
https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
viewer.html: add 'clash' option to the visibility <select> dropdown.
viewer.js: add clash branch in applySelector for both the simple
type-selector path and the complex server-side GUID path. Selected
elements are coloured 0xdc3232 (red, matching render.py's _CLASH_COLOR);
non-selected elements remain at their normal material colours, consistent
with highlight mode behaviour.
https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
Displays "Double-click model surface to place clip plane" in the status
bar when clip mode is toggled on, clears it when toggled off.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bundle OBCF (components-front) alongside OBC so Highlighter is sourced from the
correct package. Update click handler to use item.getGuid() — GlobalId is no
longer in the attributes map in fragments 3.4.x. Fix populateMetaPanel crash
(ClassificationGroupData now wraps ids in a .map property, not a plain object).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On /src/commit/{hash}/file.ifc pages, breadcrumb anchors match the
a[href*="/src/commit/"] selector before the Permalink anchor, returning
a directory URL that parseCommitHref rejects (no file path). Fix by
trying window.location.pathname first — it parses correctly when already
on a commit file URL — and only falling back to a Permalink anchor search
for branch/tag file views. Add a regression test for directory-level
commit hrefs returning null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract parseCommitHref, isSimpleTypeSelector, subtractIdMap, and
isCommitHash into a new viewer-util.js ES module so they can be tested
without a browser. Convert ifcurl.js from an IIFE to an ES module
importing from viewer-util.js; update footer.tmpl to type="module".
Add node:test suites for viewer-url.js (32 tests) and viewer-util.js
(28 tests). Add npm test script and document it in the build section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ifcurl.js: scan a[href^="ifc://"] at page load and replace with preview
figures — works with [title](ifc://...) syntax without any Go extension
- viewer.js: Issue button now emits [title](ifc://...) markdown link syntax
instead of a bare URL, so shared views render as previews without the patch
- README: document no-rebuild quick setup path; Go patch section is now optional
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
footer.tmpl now just loads /assets/ifcurl.js; all injection logic lives in
the new ifcurl.js file alongside the other viewer assets. Update deploy
instructions in both READMEs; fix incorrect /etc/forgejo path in top-level README.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
viewer.html was ~1250 lines of almost entirely JavaScript. The script
block is now a single <script type="module" src="./viewer.js"> tag,
leaving viewer.html as pure HTML/CSS (~238 lines).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Types panel: fix count by iterating groupData directly (groupData.map
was undefined — FragmentIdMap is the groupData value, not a wrapper)
- Clip planes: register OBC.Raycasters for the world before setupClipper
so clipper.create(world) can raycast against the model surface
- Ref navigation: ▾ button now navigates when input already has a value;
add change listener alongside input for cross-browser datalist coverage
- Orthographic projection: replace missing controls.setOrthoCamera() with
world.camera.projection.set("Orthographic") + threeOrtho scale fix;
refresh threeCamera reference after projection switch
- BCF export: capture rendererCanvas snapshot and include snapshot.png in
the BCF zip with a <Snapshot> reference in markup.bcf
Closes: ifcurl-07d, ifcurl-1pa, ifcurl-66h, ifcurl-xy9, ifcurl-uo9
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a permanent 'All storeys' row at the top of the Storeys list in the
model overview panel. It starts active (all visible) and clicking it
restores full model visibility. Clicking any storey row then back to
'All storeys' is now always possible, even after switching between storeys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move ⎘ copy-URL button immediately after the URL text box (ifcurl-2m9)
- Widen selector field from flex:1 to flex:2 (ifcurl-mts)
- Use window.location.origin for the new-issue link, removing hardcoded https:// (ifcurl-njq)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show the canvas snapshot on the current page (z-index 9999) immediately
before navigating, so the browser's between-pages blank is hidden behind
the static image. Navigate without delay. Remove the body fadeIn animation
that was making the snapshot on the new page invisible during the 0.2s
fade-in, causing the visible blank-then-snapshot sequence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- visibility=highlight: apply orange colour overlay to selected elements
(all others visible normally) using FragmentsManager.highlight() directly
— OBC.Highlighter is in @thatopen/components-front which is not installed
- visibility=ghost: dim non-selected elements to 15% grey opacity
- visibility=isolate: unchanged (hider.isolate)
- Add 'show' dropdown (highlight/ghost/isolate) that appears when a
selector is set; changing it reloads with the new visibility= param
- Add 'fit' button: calls cameraControls.fitToBox(model.box, true)
- Smooth load transition: capture canvas snapshot (preserveDrawingBuffer)
before navigation, display it as an overlay on the new page, fade it
out once the model has rendered
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously applySelector always called hider.isolate() which hid all
non-selected elements regardless of visibility mode. For highlight mode,
set up the Highlighter before applying the selector and use
highlightByID("select") instead so all elements remain visible with the
selection highlighted in orange.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ref input now has a datalist populated from the Forgejo branches+tags
API on load. A ▾ button clears and focuses the field to expose all refs.
Selecting a datalist option auto-navigates; pressing Enter in any structured
field now preserves camera/clip/fov/scale from the current URL so switching
refs keeps the same viewpoint.
Closes ifcurl-4rj.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New GET+POST /select endpoint resolves an ifc:// URL selector to a JSON
list of GlobalIds, with T3 caching. The viewer now detects non-type-name
selectors (property filters, Name=, pset notation) and falls back to a
/select round-trip, applying the returned GUIDs via getLocalIdsByGuids().
Closes ifcurl-ov6.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A new '☰' toolbar button opens a panel showing the IFC type breakdown
(sorted by element count) and storey list. Clicking a storey isolates
its elements via OBC.Hider; clicking again restores full visibility.
Closes ifcurl-dag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When an ifc:// URL uses a bare commit hash as the ref the toolbar now
shows an amber badge with the abbreviated hash and (after a Forgejo API
lookup) a '→ heads/main' button that reloads with the branch-tip ref.
Closes ifcurl-gy8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
three 0.177→0.184, web-ifc 0.0.75→0.0.77, @thatopen/components
3.3.1→3.4.2, @thatopen/fragments 3.3.1→3.4.3, + new camera-controls
3.1.2 peer dep. Reduces fast-xml-parser findings from 2 critical to
1 high + 1 low; remaining issues are upstream in @thatopen >= 3.3.3
with no fix released yet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace importmap pointing at esm.sh/unpkg with locally-bundled files
built from pinned npm packages (package.json + lockfile). Run
`cd forgejo && npm install && npm run build` to regenerate after a
version bump. Tracked in ifcurl-dkz: fast-xml-parser CVEs in
@thatopen/components 3.3.1 should be addressed in a follow-up upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Client-side BCF generation cannot resolve IFC GlobalIds from a selector
string. When a selector is present, POST to /bcf instead — the service
runs the selector through IfcOpenShell and writes resolved GUIDs into the
viewpoint. Fails visibly if unreachable; no silent fallback to a GUIDless
file. Also adds /bcf to the Nginx proxy block in forgejo/README.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- footer.tmpl: refactor to inject per-commit "View in 3D" links on the file
history page, alongside the existing file view page button
- ifc_url.go: add ')' to urlTerminators so bare ifc:// URLs followed by a
closing parenthesis are correctly terminated
- ifc_url_test.go: add tests for @ in query value, ) terminator, trailing
period, and SERVICE_TOKEN presence/absence in preview URL
- viewer.html: selector input placeholder and title attribute now document
that the browser viewer only supports type-name union selectors
- README: document viewer selector limitation vs full server-side behaviour
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /preview now accepts a token= query parameter, matching POST /preview
- Forgejo Go: add SERVICE_TOKEN config field; renderer appends it to <img src>
preview URLs so a machine-user token can authenticate private repos
- Go tests: verify token appears in preview URL when set, absent when unset
- viewer.html: BCF import via drag-and-drop or Import .bcf button in BCF row;
parses first viewpoint camera/clips/GUIDs and reloads with the viewpoint
applied to the current model context
- viewer-url.js, viewer.html: unescape %24 → $ in URL building so GlobalIds
with $ characters are human-readable in ifc:// URLs
- README: document SERVICE_TOKEN and private-repo preview configuration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ⎘ button to copy the current ifc:// URL to clipboard
- Add Issue button to open a pre-filled new issue on the git host
- Add click-to-identify via OBC.Highlighter: clicking an element shows
its IFC type, name, and GlobalId in a panel with a copy button
- Replace arrayBuffer() with ReadableStream loop showing fetch progress
as percentage (when Content-Length known) or MB transferred
- README: document viewer features, collaboration workflow, and that
private Forgejo repos work via same-origin session cookie
- CLAUDE.md: document that [attr=val] bracket selector syntax is invalid;
correct form is comma-separated (IfcWall, Name="...") and bare GlobalId
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pyproject.toml: add bcf to [service] dependencies (ifcurl-9rk)
- README.md: replace bogus GET /viewer row with POST /bcf (ifcurl-ttb)
- viewer-url.js: fix GitLab host detection (includes → startsWith) (ifcurl-3n4)
- git.py: call _evict_if_needed() after fetch, not only after clone (ifcurl-t1k)
- bcf.py: add description param to build_bcf(); markup.bcf now includes
<Description> when provided (ifcurl-7wg)
- service.py: pass request URL as description to build_bcf() (ifcurl-7wg)
- viewer.html: BCF export reads visibility= from URL and sets
DefaultVisibility accordingly (ifcurl-sto)
- viewer.html: applyCameraParam now switches to orthographic projection
and applies scale when scale= is present in URL (ifcurl-4l7)
- tests: 3 new tests for description field; update JS tests pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ifcurl/bcf.py: build_bcf() builds BCF 2.1 zips with perspective/
orthographic camera, clipping planes, component selection, and
isolate visibility mode
- ifcurl/service.py: /bcf POST endpoint parses ifc:// URL, resolves
selector → GUIDs when present, delegates to build_bcf()
- viewer.html: BCF toolbar button + collapsible title/comment form;
client-side zip generation via JSZip (esm.sh CDN) so the browser
does not need service access
- tests/test_bcf.py: 21 tests covering build_bcf() unit behaviour and
the /bcf endpoint including SSRF rejection and selector resolution
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
viewer.html, viewer-url.js, and footer.tmpl are intended for Forgejo
contribution and carry MIT headers matching Forgejo's per-file convention.
ThatOpen @thatopen/components (loaded from CDN) is also MIT-licensed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
parseIfcUrl, buildIfcUrl, toRawUrl extracted from viewer.html into
viewer-url.js so they can be imported and tested independently.
viewer.html imports from the module. 32 Node test-runner tests in
tests/test_viewer_url.mjs cover all three functions across GitHub,
GitLab, Forgejo/Gitea, SSH user@ prefix, and round-trip cases.
Caught a bug: + (selector union operator) was being URL-encoded as
%2B in buildIfcUrl; fixed by adding .replace(/%2B/g, "+") alongside
the existing comma fix, in both viewer-url.js and syncCameraUrl.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the raw renderer.clippingPlanes approach with OBC.Clipper:
URL-specified clip= planes are seeded as draggable SimplePlanes.
Toolbar '✂ clip' toggle + double-click on model surface to add planes;
'⊗ clips' button (shown when planes exist) to clear all. syncCameraUrl
now serialises current plane state back to the ifc:// URL on every
camera move or plane drag/create/delete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
applyClipPlanes() parses all repeatable clip= params (6 floats: point +
normal in IFC world coords), transforms each via toThree() into Three.js
space, and sets world.renderer.three.clippingPlanes. syncCameraUrl already
preserves clip= params when rebuilding the URL on camera movement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
applySelector() classifies elements via OBC.Classifier.byCategory(), then
maps the selector type names to matching categories using prefix matching
(so IfcWall also covers IfcWallStandardCase), and calls hider.isolate() to
show only matching elements. Supports simple type names and + union syntax.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a second toolbar row with labelled fields for repo, ref, path,
and selector. Editing any field rebuilds and reloads the ifc:// URL.
The raw URL in row 1 remains editable and copyable. Closes ifcurl-yju.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- toRawUrl() now handles SSH user@ prefix, GitHub (raw.githubusercontent.com),
GitLab (/-/raw/), and Forgejo/Gitea; strips heads/tags/ prefix for hosts
that don't use gitnamespace form
- FOV input in toolbar: shows current FOV, editing it updates the camera
and syncs the fov= param in the ifc:// URL
- applyCameraParam now restores fov= from URL when placing camera
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>