diff --git a/filebrowser_quantum/CHANGELOG.md b/filebrowser_quantum/CHANGELOG.md index dc4370c679..ae30e3fe9c 100644 --- a/filebrowser_quantum/CHANGELOG.md +++ b/filebrowser_quantum/CHANGELOG.md @@ -1,4 +1,18 @@ +## 1.5.3.3 (2026-08-31) +- Complete the iOS companion app fix from 1.5.3.2. The "no preview available" + screen -- what you get for a `.zip`, `.bin` or anything else FileBrowser + cannot render -- offers its Download and "Open file" buttons as new-tab + links, and so does the share list in settings. The app hands every new tab + to an external browser, which carries no ingress session, so those answered + 401. Those two buttons and the settings share links now stay in the panel + when running in the companion app, and open a new tab as before in a normal + browser -- as does a cmd/ctrl/shift-click anywhere, which is left alone. + Download also saves the file instead of displaying it, which 1.5.3.2 only + fixed for the download button in the file list. Links the app opens without + a link element, such as the public-share sidebar download, are still + affected. + ## 1.5.3.2 (2026-08-30) - Fix Download in the Home Assistant iOS companion app (iOS 17 and later), where a file opened and showed its content with no way to save it. diff --git a/filebrowser_quantum/config.yaml b/filebrowser_quantum/config.yaml index 11ab783109..deeae059b0 100644 --- a/filebrowser_quantum/config.yaml +++ b/filebrowser_quantum/config.yaml @@ -118,4 +118,4 @@ schema: slug: filebrowser_quantum udev: true url: https://github.com/alexbelgium/hassio-addons -version: "1.5.3.2" +version: "1.5.3.3" diff --git a/filebrowser_quantum/rootfs/etc/nginx/servers/ingress.conf b/filebrowser_quantum/rootfs/etc/nginx/servers/ingress.conf index c884b54327..bec1083c04 100644 --- a/filebrowser_quantum/rootfs/etc/nginx/servers/ingress.conf +++ b/filebrowser_quantum/rootfs/etc/nginx/servers/ingress.conf @@ -41,31 +41,72 @@ server { # same instance's files/ or public/share/ route. Same shape as the komga # add-on's ingress filter. # - # 2. Download. FileBrowser downloads a file by building an with no - # download attribute, clicking it, and letting the attachment response do - # the rest. The Home Assistant iOS companion app is a WKWebView, and a - # download happens there only when WebKit turns a navigation *action* - # into a WKDownload, which is what the download attribute does -- the app - # hands that to its own download manager - # (WebViewController+WebKitDelegates.swift, navigationAction:didBecome - # download:). Its response policy delegate returns .allow for every - # sub-frame and never returns .download, so a plain attachment navigation - # inside the ingress panel is just rendered: a text file opens and shows - # its content with no way to save it. Adding the attribute makes the same - # click a real download. Desktop browsers already downloaded these and - # are unaffected, and an empty value keeps the filename the server sends - # in Content-Disposition. Matched on the two exact download endpoints, - # minus inline=true: the "no preview available" fallback renders an - # "Open file" link on the same endpoint with that parameter - # (views/files/Preview.vue), and it is meant to open, not save. Only - # programmatic clicks pass through here -- a person clicking a link never - # calls HTMLAnchorElement.prototype.click -- so this reaches - # FileBrowser's own hidden download anchor and nothing a user clicks. + # 2. Saving and opening a file. FileBrowser downloads by clicking an + # that carries no download attribute -- a hidden one it builds itself + # (api/resources.js), and two visible ones in the "no preview available" + # fallback (views/files/Preview.vue), which are also target="_blank". + # Neither works in the Home Assistant companion app: # - # Not covered: the public-share sidebar downloads with window.open() - # rather than an anchor, and the app's download manager is gated on - # iOS 17 (WebViewController+WebKitDelegates.swift). - sub_filter "window.__pwaDeferredPrompt = null;" "window.__pwaDeferredPrompt = null;(function(){var o=window.open;window.open=function(u,n,f){try{var b=(window.globalVars||{}).baseURL;if(u&&n==='_blank'&&!f&&b){if(b.slice(-1)!=='/')b+='/';var t=new URL(u,location.href);if((t.protocol==='http:'||t.protocol==='https:')&&t.origin===location.origin&&(t.pathname.indexOf(b+'files/')===0||t.pathname.indexOf(b+'public/share/')===0)){location.assign(t.href);return window}}}catch(e){}return o.apply(window,arguments)};var c=HTMLAnchorElement.prototype.click;HTMLAnchorElement.prototype.click=function(){try{var b=(window.globalVars||{}).baseURL;if(b&&this.href&&!this.hasAttribute('download')){if(b.slice(-1)!=='/')b+='/';var t=new URL(this.href,location.href);if(t.origin===location.origin&&t.searchParams.get('inline')!=='true'&&(t.pathname===b+'api/resources/download'||t.pathname===b+'public/api/resources/download')){this.download=''}}}catch(e){}return c.apply(this,arguments)}})();"; + # - A download happens in its WKWebView only when WebKit turns a + # navigation *action* into a WKDownload, which is what the download + # attribute does; the app hands that to its own download manager + # (WebViewController+WebKitDelegates.swift, navigationAction:didBecome + # download:, iOS 17+). Its response policy delegate returns .allow for + # every sub-frame and never returns .download, so a plain attachment + # navigation in the ingress panel is just rendered: the file opens and + # shows its content with no way to save it. + # - target="_blank" is worse. The app has no navigationAction policy + # delegate, so every new-window request reaches createWebViewWith, + # which hands the url to an external browser. That browser carries no + # ingress session cookie, so Home Assistant answers 401. + # + # One capturing click listener covers both, and covers the hidden anchor + # too: a programmatic .click() on an anchor that is in the document + # dispatches through it exactly like a real one -- same event, button 0, + # no modifiers -- and the capture phase runs before the link is followed. + # It replaces an earlier wrapper around HTMLAnchorElement.prototype.click, + # which reached the hidden anchor but not the two the user clicks. The + # wrapper would also have caught a click on a *detached* anchor, which + # this cannot; both of FileBrowser's download paths append theirs to the + # body first (api/resources.js), so nothing is lost today. + # + # Only unmodified primary clicks are touched. A cmd/ctrl/shift-click is + # an explicit request for a separate context and is left alone, which + # matters on desktop and in the Mac Catalyst app. + # + # Downloads gain the attribute everywhere -- desktop browsers already + # saved these on a plain click, and an empty value keeps the filename the + # server sends in Content-Disposition. One behaviour does change there: + # if the endpoint answers with an error the body is saved as a file + # instead of being shown, because the decision is made before the + # response exists. Matched on the two exact download endpoints minus + # inline=true, which is the fallback's "Open file" link and is meant to + # open rather than save. That branch drops target="_blank" in every + # browser too: with the attribute present a same-origin link downloads + # and never opens a tab (measured), so it changes nothing here, but it + # stops the companion app from taking its new-window path before it + # considers the download -- an ordering that cannot be tested from + # outside iOS. + # + # Dropping target="_blank" from links that are *not* downloads is limited + # to the companion app, identified by the Mobile/HomeAssistant marker it + # appends to the user agent + # (HAAPI.swift, applicationNameForUserAgent -- it covers iPhone, iPad and + # Mac Catalyst). In a real browser a new tab is the better behaviour and + # is left alone; in the app it is a 401. It applies to any same-origin + # link below the add-on's own base path, which in practice is the + # "Open file" button and the share links in settings + # (views/settings/Shares.vue). Links to other origins keep their new tab, + # and only an exact target="_blank" is matched, so named windows, _parent + # and _top are untouched. + # + # Not covered, and still 401 in the app: anything the app opens with + # window.open() rather than an anchor, which is the public-share sidebar + # download and absolute sidebar links (components/sidebar/Links.vue); and + # links inside a document FileBrowser renders in its own iframe -- the + # pdf viewer, the srcdoc markdown/html preview, OnlyOffice -- because a + # listener on this document never sees another document's clicks. + sub_filter "window.__pwaDeferredPrompt = null;" "window.__pwaDeferredPrompt = null;(function(){var o=window.open;window.open=function(u,n,f){try{var b=(window.globalVars||{}).baseURL;if(u&&n==='_blank'&&!f&&b){if(b.slice(-1)!=='/')b+='/';var t=new URL(u,location.href);if((t.protocol==='http:'||t.protocol==='https:')&&t.origin===location.origin&&(t.pathname.indexOf(b+'files/')===0||t.pathname.indexOf(b+'public/share/')===0)){location.assign(t.href);return window}}}catch(e){}return o.apply(window,arguments)};document.addEventListener('click',function(e){try{if(e.button||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)return;var a=e.target&&e.target.closest?e.target.closest('a'):null;if(!a||!a.href)return;var b=(window.globalVars||{}).baseURL;if(!b)return;if(b.slice(-1)!=='/')b+='/';var t=new URL(a.href,location.href);if(t.origin!==location.origin)return;if(!a.hasAttribute('download')&&t.searchParams.get('inline')!=='true'&&(t.pathname===b+'api/resources/download'||t.pathname===b+'public/api/resources/download')){a.download='';a.removeAttribute('target');return}if(a.target==='_blank'&&t.pathname.indexOf(b)===0&&navigator.userAgent.indexOf('Mobile/HomeAssistant')!==-1){a.removeAttribute('target')}}catch(err){}},true)})();"; } }