Compare commits

...

2 Commits

Author SHA1 Message Date
Alexandre
408eef8a0f fix(lint): make the Unicode-space regex actually match Unicode spaces (#3027)
The `Lint workflows` autofix job has failed on every scheduled run since at
least 2026-08-16, with `shfmt` reporting parse errors ("LitWord cannot be
followed by a word", "${ stmts;} is a mksh feature", ...) in ~70 shell scripts
that parse cleanly on a pristine checkout.

Root cause is the preceding "Fix non-printable Unicode spaces" step. Its regex
was written as `$'[\\u00A0\\u2002...]'`: the doubled backslash makes bash's
ANSI-C quoting emit the literal text ` `, and Perl has no `\u` codepoint
escape — `\u` is the titlecase-next-character operator, so the character class
degrades to the plain characters `0 2 3 5 7 8 9 A B F`. The step therefore
replaced those digits and letters with spaces in every text file in the repo,
which is what left the shell scripts unparseable. `shfmt` then exited 1 and the
job stopped before opening its autofix PR — the only reason the corruption was
never committed.

Switch to Perl's own `\x{...}` escape in a plain single-quoted string, so the
class holds the ten intended code points and nothing else.

Verified locally against the exact step body extracted from the workflow: on a
sample of the repo it now rewrites only the real U+202F occurrences (e.g.
`postgres_15/.../99-run.sh`, `birdnet-pi/DOCS.md`) and leaves all other text
untouched, and `shfmt v3.12.0 -w -i 4 -ci -bn -sr` over the whole repo exits 0
with no parse errors.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 09:50:12 +02:00
Alexandre
0997c302ae fix(filebrowser_quantum): make Download save the file in the iOS companion app (#3030)
* fix(filebrowser_quantum): make Download save the file in the iOS companion app

FileBrowser downloads by clicking an <a> that carries no download attribute
and letting the attachment response do the rest. The Home Assistant iOS
companion app is a WKWebView, where a download only happens when WebKit turns
a navigation action into a WKDownload -- which is what the download attribute
does, and the app hands the result 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 inside the ingress panel a plain attachment
navigation is simply rendered: a text file opens and shows its content with no
way to save it.

The ingress filter now adds the attribute, matched on the two exact download
endpoints so nothing else in the app is touched. Desktop browsers already
downloaded these and are unaffected, and an empty value keeps the filename the
server sends in Content-Disposition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(filebrowser_quantum): keep Open file a navigation, and narrow the claims

Review of #3030 found that the 'no preview available' fallback renders an
'Open file' link on the same download endpoint with inline=true
(views/files/Preview.vue), so a pathname-only match would have turned opening
a file into downloading it. Exclude inline=true.

Also narrows two overstated claims: the app's download manager is gated on
iOS 17, and the public-share sidebar downloads with window.open() rather than
an anchor, so it is not covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 09:49:09 +02:00
4 changed files with 61 additions and 25 deletions

View File

@@ -38,7 +38,7 @@ jobs:
run: |
set -euo pipefail
CHANGED_FILES=$(git diff --name-only "$DIFF_RANGE")
UNICODE_SPACES_REGEX=$'[\\u00A0\\u2002\\u2003\\u2007\\u2008\\u2009\\u202F\\u205F\\u3000\\u200B]'
UNICODE_SPACES_REGEX='[\x{00A0}\x{2002}\x{2003}\x{2007}\x{2008}\x{2009}\x{202F}\x{205F}\x{3000}\x{200B}]'
for file in $CHANGED_FILES; do
if [ -f "$file" ]; then
MIME_TYPE=$(file --mime-type -b "$file")
@@ -89,7 +89,7 @@ jobs:
- name: Fix non-printable Unicode spaces in all text files
run: |
set -euo pipefail
UNICODE_SPACES_REGEX=$'[\\u00A0\\u2002\\u2003\\u2007\\u2008\\u2009\\u202F\\u205F\\u3000\\u200B]'
UNICODE_SPACES_REGEX='[\x{00A0}\x{2002}\x{2003}\x{2007}\x{2008}\x{2009}\x{202F}\x{205F}\x{3000}\x{200B}]'
find . -type f ! -path "./.git/*" | while read -r file; do
MIME_TYPE=$(file --mime-type -b "$file")
if [[ "$MIME_TYPE" == text/* ]]; then

View File

@@ -1,4 +1,15 @@
## 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.
FileBrowser downloads by clicking a link that carries no `download`
attribute, and the app's WKWebView only turns a click into a real download
when that attribute is present, so inside the ingress panel the file was
simply rendered. The ingress filter now adds the attribute to FileBrowser's
own download link. "Open file" still opens, and the public-share sidebar's
own download button is not covered. Desktop browsers already downloaded
these and are unchanged, as is direct access on port 8071.
## 1.5.3.1 (2026-08-29)
- Fix "open parent directory" in Tools -> File Size Analyzer under Home
Assistant ingress. FileBrowser opened the parent folder in a new tab, which

View File

@@ -118,4 +118,4 @@ schema:
slug: filebrowser_quantum
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.5.3.1"
version: "1.5.3.2"

View File

@@ -13,34 +13,59 @@ server {
proxy_read_timeout 30m;
proxy_pass %%protocol%%://backend%%subpath%%;
# Tools -> File Size Analyzer (and the other tool views) open a result's
# parent folder with window.open(<absolute url>, '_blank'), because
# goToItem() takes its newTab argument from the context menu's
# showLimitedOptions flag, which those views always set. Behind ingress
# that popup lands on the raw /api/hassio_ingress/<token>/ url with no
# Home Assistant frontend around it to keep the ingress session alive,
# so the new tab answers 401 instead of showing the folder. Turn that
# popup into a navigation of the panel itself.
# Two things the ingress panel needs that a plain browser tab does not.
# Both are injected into the page's existing nonce-carrying inline script
# rather than next to <div id="app">: FileBrowser sends
# script-src 'self' 'nonce-<random>', so a standalone inline <script>
# would be blocked. If upstream ever drops that
# window.__pwaDeferredPrompt line the filter stops matching and both
# behaviours revert, which is the state before either fix.
#
# The context menu's "go to item" action, which search results and the
# tool views also offer, hardcodes the same new tab and is fixed too.
# 1. Opening a folder. The tool views (Tools -> File Size Analyzer and
# the others) always set the context menu's showLimitedOptions flag, and
# openParentFolder() hands that same flag to goToItem() as its newTab
# argument, so the folder is opened with window.open(<url>, '_blank').
# Behind ingress that popup lands on the raw /api/hassio_ingress/<token>/
# url with no Home Assistant frontend around it to keep the ingress
# session alive, so the new tab answers 401 instead of showing the
# folder. Turn it into a navigation of the panel itself. The context
# menu's "go to item" action, offered by search results and the tool
# views, hardcodes the same new tab and is fixed with it.
#
# Scoped to the two prefixes goToItem() builds, "files/" and
# "public/share/", so the window.open calls that download or preview a
# file (they go to api/resources/download) keep their own tab: sending
# an inline raw file to location.assign would replace the whole app.
# A link out of the add-on keeps its own tab as well, unless a user
# points a sidebar link (or a link inside a file open in the editor) at
# this same instance's files/ or public/share/ route, which then also
# opens in the panel. Same shape as the komga add-on's ingress filter.
# an inline raw file to location.assign would replace the whole app. A
# link out of the add-on keeps its own tab as well, unless a user points
# a sidebar link -- or a link inside a file open in the editor -- at this
# same instance's files/ or public/share/ route. Same shape as the komga
# add-on's ingress filter.
#
# Injected into the page's existing nonce-carrying inline script rather
# than next to <div id="app">: FileBrowser sends
# script-src 'self' 'nonce-<random>', so a standalone inline <script>
# would be blocked. If upstream ever drops that
# window.__pwaDeferredPrompt line the filter simply stops matching and
# the popup behaviour returns, which is the pre-fix state.
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)}})();";
# 2. Download. FileBrowser downloads a file by building an <a> 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.
#
# 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)}})();";
}
}