fix(uve): open file-asset links in a new tab instead of loading them as pages (#35504) - #36925
fix(uve): open file-asset links in a new tab instead of loading them as pages (#35504)#36925dsilvam wants to merge 10 commits into
Conversation
…as pages (#35504) handleInternalNav treated every same-host link as an HTMLPage and fed it to the Page API, so a link to a PDF resolved to a 404 "Page not found" in both edit and preview mode. Add an isAssetPath() predicate that mirrors the backend extension heuristic, and route hrefs resolving to a file asset to a new tab instead. Refs: #35504, FD #36746 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
zJaaal
left a comment
There was a problem hiding this comment.
Follow-up review on the heuristic. Three of these are behavior bugs, and two of them reintroduce the reported symptom on narrower inputs; the rest are nits.
What holds up: the three prefixes match web.xml exactly (/dotAsset/* L540, /dA/* L544, /contentAsset/* L563), so the case-sensitive startsWith is correct rather than an oversight (servlet url-patterns are case-sensitive). Placement after the external-host check and before isSamePageNavigation is right, and preventDefault() is what actually stops the iframe from navigating away too.
Also closing out my earlier question on pathname.slice(pathname.lastIndexOf('/') + 1): a pathname with no / is safe. lastIndexOf returns -1, so the slice is slice(0), the whole string. 'report.pdf' classifies as an asset and 'about' as a page. No fix needed, though it would be a cheap test case.
Review drafted by Claude (Claude Code) on behalf of @zJaaal.
…d URL (#35504) Applies review feedback on the isAssetPath heuristic: - Drop `htm` from PAGE_PATH_EXTENSIONS. VELOCITY_PAGE_EXTENSION ships as `html` with `dot` as the backend fallback; `htm` is an ordinary file asset in dotCMS, so a .htm upload was still routed to the Page API. - Accept digit-initial extensions (7z, 3gp). The URL-map slug guard only needs to reject all-digit trailing tokens, not digit-initial ones. - Open url.href rather than the raw href, which can be a relative attribute when the click lands on a child of the anchor, and add noopener since the host check compares hostname only. - Record that the backend resolves page vs file by identifier lookup, and that dotted page slugs are a known false positive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@dsilvam I pushed the review fixes straight to this branch as 6cfcf1e rather than leaving you to transcribe five threads. Shout if you would rather I had left them as comments and I will happily revert. All four findings applied, red-first: six tests failed before the change (three in
Tests: Verified locally: Still out of scope and untouched, as you flagged: the external-host branch missing Changes authored by Claude (Claude Code) on behalf of @zJaaal. |
|
Claude finished @zJaaal's task in 1m 45s —— View job SDK Compatibility Analysis
Result: No SDK breaking change detected. All 4 changed files live in Walked through each surface called out in this task:
The new No comment or label action taken, per instructions. |
…5504) The dotted-page-slug false positive is a deliberate choice, not an oversight. Reading a page as a file opens a new tab, which is visible and recoverable; reading a file as a page strands the editor on "Page not found", the defect this guards against. An extension allowlist would fix the false positive but invert that bias, so the test stays permissive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Locks in the documented trade so it stays a conscious choice: a page slug carrying a dot plus a short alpha token reads as a file asset. Flipping any of these to false means the bias was changed, which would send uncommon file extensions to the Page API instead, the failure the guard prevents. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Accepting this one, and making the acceptance explicit rather than implicit. Pushed 96b01be (JSDoc) and 76fc64c (test). First, the finding is real, so it is worth stating why rather than waving it through. The page The reason not to fix it is that the heuristic already fails in the correct direction, and the only real alternative would reverse that:
The permissive extension test is what buys the first failure mode. Swapping it for a known-extension allowlist would fix So, two changes instead of a fix:
it.each(['/store/product.detail', '/pages/about.us', '/docs/getting.started'])(
'should knowingly misread the page %s as a file asset',
(pathname) => {
expect(isAssetPath(pathname)).toBe(true);
}
);That turns the trade into a tripwire: anyone who "fixes" the false positive has to delete a test whose comment explains what they are giving up, which is the point where the allowlist bias gets reconsidered on purpose. The authoritative fix remains a backend round-trip per link click against Investigated and applied by Claude (Claude Code) on behalf of @zJaaal. |
Firefox raises "DOMException: The operation is insecure" when window.open is given a windowFeatures string from a gesture that originated in the sandboxed iframe, which is declared without allow-popups. The noopener argument added earlier turned a permitted tab-open into a rejected popup request, and because the throw happened before preventDefault(), the anchor's default action ran and navigated the iframe to the asset. - Call preventDefault() first, so the page under edit stays put whatever window.open does. - Drop the windowFeatures string, matching the pre-existing external-host branch, which Firefox accepts. - Guard the open call: an escaping throw would reach the RxJS subscriber driving this handler and kill the click listener for the whole session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or (#35504) window.open with a windowFeatures string makes Firefox classify the call as a popup request, and the iframe raising the gesture is sandboxed without allow-popups, so it threw "The operation is insecure". Dropping the string lost the opener guarantee, which is not acceptable. A rel="noopener" anchor is an ordinary tab navigation, so the sandbox permits it, and it severs the opener even for cross-origin targets, where assigning opener = null on a returned window would not be allowed. The host check above compares hostname only, so cross-origin is reachable. preventDefault() still runs first, so the iframe stays on the page under edit whatever the open does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds file-asset detection to UVE navigation so same-host assets open safely in a new tab instead of invoking the Page API.
Changes:
- Adds asset-path classification using delivery prefixes and extensions.
- Opens detected assets through a
noopeneranchor. - Adds utility and navigation tests.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
utils/index.ts |
Implements asset-path detection. |
utils/utils.spec.ts |
Tests path classification. |
edit-ema-editor.component.ts |
Routes asset links to a new tab. |
edit-ema-editor.component.spec.ts |
Tests asset navigation behavior. |
Suppressed comments (1)
core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts:1194
- The arbitrary 8-character cap leaves valid file extensions such as
.webmanifestclassified as pages, sohandleInternalNavstill sends those assets to the Page API. The backend heuristic accepts any nonempty extension, and the surrounding rationale explicitly aims not to break uncommon file types; remove the length cap while retaining the letter guard, and add a regression case.
const FILE_EXTENSION_PATTERN = /^(?=.*[a-z])[a-z0-9]{1,8}$/;
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…hor (#35504) Applies Copilot review feedback. - Drop `dot` from PAGE_PATH_EXTENSIONS. It is only the fallback that Config.getStringProperty("VELOCITY_PAGE_EXTENSION", "dot") reaches for when the property is absent, and dotmarketing-config.properties:91 ships it as `html`, so `dot` is never the active page extension. It is also the Word 97-2003 template extension, so listing it sent a real upload type to the Page API. This is the same bias already applied to `htm`. - Move the .dot case from the page table to the asset table. - Restructure #openInNewTab so the anchor is removed in a finally block. click() is the call that throws when the open is refused, so the old ordering stranded an anchor in the admin document on every failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Copilot review addressed in 0bb9493. Both inline findings applied; recording why the suppressed one was not. Applied — Applied — Declined — dropping the 8-character cap from
Happy to revisit if someone produces a case where an author actually links to a long-extension asset. Verified: Reviewed and applied by Claude (Claude Code) on behalf of @zJaaal. |
I applied my own comments, we are good from my side
Proposed Changes
isAssetPath()to the UVE utils — a predicate that distinguishes file-asset URLs from HTMLPage URLs. It matches dotCMS asset-delivery prefixes (/dA/,/dotAsset/,/contentAsset/) and otherwise mirrors the backend's own extension heuristic inIdentifier#setURI: no extension (or the configuredVELOCITY_PAGE_EXTENSION) means a page, any other real extension means a file.handleInternalNav— a same-host href that resolves to a file asset now opens in a new tab and callspreventDefault(), instead of being handed touveStore.pageLoad().Root cause
handleInternalNavsplit anchor clicks into exactly two buckets: different hostname → open a new tab; anything else →uveStore.pageLoad({ url: url.pathname, ... }). There was no check for whether the same-host target was actually an HTMLPage, so a link to a file asset (/dA/<inode>/fileAsset/doc.pdf,/application/files/doc.pdf) was fed to the Page API, which cannot resolve it. The editor then rendered its "Nothing Live Here Yet" / "Page not found" state.Because the
(internalNav)binding is unconditional, this affected Preview/Published mode as well as Edit mode — the linked issue is titled edit-mode-only, so please exercise both when testing.Checklist
Security note: the new branch passes the already-resolved same-origin
hreftowindow.open. The external-host branch above it is unchanged and still handles cross-origin links, so this does not widen what can be opened; it only changes how same-origin file links are handled. No new user input is parsed —isAssetPathreceives aURL.pathnamethat was already constructed upstream.Test coverage
utils.spec.ts— 19 cases onisAssetPath:/dA/.../report.pdf,/dA/with no extension,/dotAsset/,/contentAsset/,/application/files/report.pdf,.docx,.mp4,.tar.gz, uppercase.PDF/about-us/index,.html,.htm,.dot,/blog/,//blog/release-v1.2and/news/2024.10must stay pages — a naive extension check would read the trailing2/10as a file extension and break navigation to URL-map slugsedit-ema-editor.component.spec.ts— 3 cases onhandleInternalNav: a.pdflink and a/dA/link each open a new tab, callpreventDefault, and do not callpageLoad; an.htmllink still routes throughpageLoad.Full suite: 37/37 suites, 913 passed, 0 failures.
nx lint portlets-edit-ema-portletclean.Additional Info
Verified manually against a locally built image: clicking
/dA/<inode>/fileAsset/<name>.pdfin edit mode now opens the PDF in a new tab and leaves the editor on the page.Note for reviewers/QA: the Angular bundle ships from the separate
dotcms-core-webMaven module, so./mvnw install -pl :dotcms-core -DskipTestswithout--amwill silently test a stale frontend. Use./mvnw install -pl :dotcms-core --am -DskipTests.Two related items deliberately left out of scope:
edit-ema-editor.component.ts, theurl.hostname !== window.location.hostnamecase) opens a new tab but never callspreventDefault(), so an external link also navigates the iframe away. Same class of bug, one line, but unrelated to this issue./dA/would still be treated as a page. Not reachable through the reported flow; a fully authoritative fix would need a backend round-trip per link click.Refs: #35504, FD #36746
This PR fixes: #35504