Opening remote files in LibreOffice on macOS¶
This guide walks through giving your users a one-click "Open in LibreOffice" button that hands off any remote URL — WebDAV, plain HTTPS, whatever LibreOffice can read — from the browser to LibreOffice on macOS. The recipe is generic; this page lives in the webdav module's docs because that's where the question usually arises, but nothing below is WebDAV-specific until you reach the troubleshooting notes at the very end.
Why a custom URL scheme is needed¶
LibreOffice ships with two URL schemes registered for browser
handover — vnd.sun.star.webdav and vnd.libreoffice.command.
On macOS they no longer register correctly with Launch Services
in recent LO builds (see TDF#158260). Clicking a
link with one of those schemes either does nothing or opens the
Finder.
The supported workaround is to register your own URL scheme
(e.g. libreoffice:), point Launch Services at a small
AppleScript application you ship to your users, and have that app
shell out to the soffice binary directly. The browser → app →
soffice handover then bypasses the broken LS path entirely.
The flow¶
flowchart LR
B["Browser<br/>user clicks<br/>libreoffice:https://..."]
L["Launch Services<br/>routes the scheme<br/>to the registered app"]
H["Helper.app<br/>on open location:<br/>1. strip libreoffice: prefix<br/>2. shell out to soffice<br/>3. activate LibreOffice<br/>4. quit"]
O["LibreOffice<br/>opens the document"]
B --> L --> H -->|soffice <url>| O
Step 1 — Write the AppleScript handler¶
Open Script Editor (/Applications/Utilities/Script Editor.app)
and paste:
on open location theURL
do shell script "echo $(date) URL=" & quoted form of theURL & " >> /tmp/lo-handler.log"
try
-- Strip the "libreoffice:" prefix. theURL arrives as
-- "libreoffice:https://host/path"; we want
-- "https://host/path".
set AppleScript's text item delimiters to "http"
set cleanURL to "http" & text item 2 of theURL
set AppleScript's text item delimiters to ""
-- 1. Spawn soffice directly so the URL hits its arg
-- parser and the WebDAV file actually opens. This is
-- NOT `open -a 'LibreOffice' --args URL` because that
-- launches LO via Launch Services and the URL never
-- reaches soffice's argv — LO opens to its splash
-- screen instead of the file.
do shell script "/Applications/LibreOffice.app/Contents/MacOS/soffice " & quoted form of cleanURL & " > /dev/null 2>&1 &"
-- 2. Tiny settle delay before the retry loop starts.
-- Without this, the first few `tell to activate`
-- calls fire while soffice is still in its splash /
-- initial-window-setup phase, causing a brief visual
-- flash as the splash window gets focused then yields
-- to the doc window. 100ms lets soffice get past
-- the splash phase before activation requests start —
-- activate then sticks on the doc window directly.
-- Not on the critical path of "how long does it take
-- to come to front" (the retry loop still adapts), but
-- cosmetically nicer.
delay 0.1
-- 3. Retry-loop activation, not a fixed delay.
-- Each iteration:
-- (a) check if the soffice process exists (it
-- spawns within ~100-300ms),
-- (b) `tell application "LibreOffice" to activate`
-- — early iterations no-op silently while LO
-- has no activatable window; the iteration
-- after LO's window appears is the one where
-- the activate sticks,
-- (c) check `frontmost of process "soffice"`. If
-- true, exit early.
-- Adapts to whatever activation latency the machine
-- actually needs — typically 1-3 iterations
-- (~100-1500ms) vs a fixed delay that would have to
-- be tuned for the worst case.
--
-- Why this works inside the helper app:
-- macOS focus-stealing prevention allows activation
-- requests from the "frontmost recent" app. The
-- helper IS the frontmost app at the moment
-- `on open location` fires (Launch Services just
-- activated it to handle the URL), so its
-- `tell to activate` calls have the focus privilege.
-- The same code run from a terminal `osascript`
-- wouldn't work — different calling context, no
-- focus privilege.
set maxAttempts to 50
repeat with i from 1 to maxAttempts
try
tell application "System Events"
if exists process "soffice" then
tell application "LibreOffice" to activate
if frontmost of process "soffice" is true then
exit repeat
end if
end if
end tell
end try
delay 0.1
end repeat
quit
on error
set AppleScript's text item delimiters to ""
end try
end open location
A few notes on the script:
on open locationis the AppleScript event Launch Services dispatches when an app handles a URL scheme. The full URL is passed in astheURL.- The scheme-stripping trick relies on the wrapped URL starting
with
http(eitherhttp://orhttps://). Splitting on"http"and grabbing the second item drops the prefix scheme cleanly. - Two operations. First spawn soffice with the URL so LO actually opens the WebDAV file (the URL has to hit soffice's argv, see "what doesn't work" below). Then a retry loop brings LO forward.
process "soffice", notprocess "LibreOffice". The literal executable name on disk issoffice(LO is a port with its historical Linux binary name). When you ask System Events forname of process X, you get"soffice". Bothprocess "LibreOffice"andprocess "soffice"will resolve to the same process via Launch Services fuzzy lookup, but the literal name property issoffice, and filters likeevery process whose name is "X"are strict equality. Usingsofficeeverywhere keeps it unambiguous.- Why retry loop, not
open -a 'LibreOffice'. Both work, but the retry loop is adaptive — exits as soon as LO is actually frontmost.open -ais one-shot; you'd need a fixeddelay Ntuned for the worst case, wasting time on the common case. With Automation perms granted to the helper (one-time prompt), the retry loop is pure AppleScript and has a real ready signal. - Required Automation permissions (granted on first run via
macOS prompt, then cached forever): the helper app needs to
control
LibreOffice(for thetell to activate) andSystem Events(for thefrontmostcheck). Visible at System Settings → Privacy & Security → Automation → [HelperApp]. If both targets aren't checked there, the outertry ... end tryswallows the permission errors and the loop runs allmaxAttemptsiterations silently — visible as a multi-second hang with no activation. - What does NOT work (every other approach tried during
development):
tell application "LibreOffice" to activateas a single call (no retry) — fires before LO has an activatable window and is silently dropped.set frontmost of process "soffice" to truevia System Events — focus-stealing prevention suppresses it. Stays suppressed even in a retry loop because the suppression is policy, not race.count of documentsvia the LO AppleScript bridge — the dictionary is sparsely populated on macOS LO builds and reads as empty even when documents are loaded.count of windows of process "soffice"via System Events — LO's editor windows don't register the way native macOS apps' windows do.lsof -c soffice | grep <fname>polling for the WebDAV file's fd — soffice keeps the file in memory (no temp write), so the filename never appears in lsof output. An earlier version of this script tried it; the loop always timed out at 8s and fell through to activate.open -a 'LibreOffice' --args URL— Launch Services launches LO but--argsdoesn't reach soffice's argv; LO opens to its splash screen, not the file.open -a 'LibreOffice' URL— Launch Services interprets the URL as a "thing to open" and routes it through the default URL handler (browser), not LO.
Step 2 — Save as an application¶
In Script Editor:
- File → Export…
- File Format: Application
- Uncheck Stay open after run handler
- Save as e.g.
LibreOfficeUrlHandler.appsomewhere durable (a permanent home like/Applications/, not~/Desktop).
The resulting .app is a normal AppleScript bundle. Its
Contents/Info.plist is the file we need to tweak next.
Step 3 — Register the URL scheme in Info.plist¶
Right-click LibreOfficeUrlHandler.app → Show Package Contents →
Contents/Info.plist. Open it in a text editor and add the
following keys inside the top-level <dict>:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.libreoffice</string>
<key>CFBundleURLSchemes</key>
<array>
<string>libreoffice</string>
</array>
</dict>
</array>
The CFBundleURLSchemes array is the source of truth — any
string you put there becomes a URL scheme this app will handle.
You can register multiple schemes by adding more <string>
entries.
While you're in there, also add LSUIElement — the helper opens,
shells out and quits in under two seconds, so its Dock icon
showing during that window is visual noise. With this key it
stays hidden from the Dock, Cmd+Tab, and the menu bar:
<key>LSUIElement</key>
<true/>
Optional but recommended: pin a stable bundle identifier so Launch Services has a clean anchor across rebuilds of the app:
<key>CFBundleIdentifier</key>
<string>com.example.libreoffice</string>
Step 4 — Force Launch Services to re-scan¶
macOS caches Launch Services bindings. After editing Info.plist
(or moving the .app), nudge LS to rescan:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister \
-f /Applications/LibreOfficeUrlHandler.app
Then verify the scheme is bound to your app:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister \
-dump | grep -A1 -B1 'libreoffice'
You should see your .app listed as the handler.
Step 5 — Generate the link from your application¶
Wrap the actual remote URL with your custom scheme prefix when rendering the "Open in LibreOffice" button:
$remote_url = 'https://example.test/path/to/file.odt';
$lo_url = 'libreoffice:' . $remote_url;
$build['button'] = [
'#type' => 'link',
'#title' => $this->t('Open in LibreOffice'),
'#url' => \Drupal\Core\Url::fromUri($lo_url),
];
When the user clicks the link, the browser hands libreoffice:…
off to Launch Services, which launches LibreOfficeUrlHandler.app, which
shells out to soffice with the unwrapped https://… URL.
Step 6 — Test¶
- Visit a page that renders the "Open in LibreOffice" button.
- Click it. The first time, macOS may prompt "Allow this website to open LibreOfficeUrlHandler?" — click Allow.
- LO should foreground (or launch if not running) with the remote document open.
Troubleshooting¶
The browser does nothing when I click the link¶
- Run the
lsregister -dump | grep libreofficecheck from Step 4. If your app is not listed, the binding never took effect: re-runlsregister -f /Applications/LibreOfficeUrlHandler.app. - Some browsers (Safari, Chrome) ask for confirmation on the first launch of an unknown scheme. Open the browser's notification / permissions panel and re-allow.
- If you see "Safari can't open the page because the address is
invalid", the URL is malformed — check that the actual
remote URL embedded after
libreoffice:is correctly escaped in the HTML.
LibreOffice opens but doesn't come to the front¶
The soffice spawn succeeded (LO is running, file loaded) but the retry-loop activation didn't bring it forward. Possible causes:
- Automation permissions not granted to the helper app. The
retry loop calls
tell application "LibreOffice" to activateAND queriesfrontmost of process "soffice"via System Events — both need Automation permission for the helper app. The outertry ... end trysilently swallows permission- denial errors, so missing perms manifest as the loop running all 50 iterations without activation (a multi-second hang with no LO frontmost). Check System Settings → Privacy & Security → Automation → [HelperApp] — bothLibreOfficeandSystem Eventsshould be checked. macOS prompts for these on the helper's first run; if you denied or never saw the prompt, toggle them on manually. process "LibreOffice"instead ofprocess "soffice". System Events identifies LO's process by its executable name, which issoffice. The literalprocess "X"selector is fuzzy (both names work) butfrontmost of process "X"can return inconsistent results depending on macOS version. Useprocess "soffice"for the System Events check, which matches the literalname of processproperty.delay 0.1was removed. The 100ms settle delay between the soffice spawn and the retry loop suppresses a visible flash where the activate briefly lands on LO's splash window before the doc window appears. Not a correctness issue (the retry loop still adapts to slower machines), but the script flashes the splash before settling on the document window without it.maxAttemptsis too low for a slow machine. Default is 50 iterations × 100ms = 5s. On very slow machines or with large WebDAV files, LO may take longer than 5s to be activatable. Raise to 100 (10s).
LibreOffice doesn't open the file¶
LO came to front but to its splash screen or "Recent Documents" window, not to the file.
- The script is using
open -a 'LibreOffice' URLor--args URL. Neither passes the URL to soffice's argv. The correct invocation isdo shell script "/Applications/LibreOffice.app/Contents/MacOS/soffice URL &"— direct binary call, URL as a command-line argument. - The URL was malformed. Check the entry in
/tmp/lo-handler.logagainst what the formatter rendered in the HTML. - The WebDAV layer rejected the request. Check
/admin/reports/dblogon the Drupal side, filter to thewebdavchannel.
Edit the script, re-export the app, re-run lsregister -f after
any change.
soffice doesn't pick up credentials in the URL¶
LO does not parse https://user:pw@host/file.odt from the
CLI. Verified by running soffice https://admin:pw@host/file.odt
in a terminal — the file does not open at all.
Embed credentials in the URL path instead, e.g.
/<uid>-<hmac>/file.odt, and have your server unpack them
server-side. The browser → AppleScript → soffice chain passes the
path verbatim, so the auth segment survives.
LO complains about an untrusted certificate¶
LibreOffice has its own NSS trust store (a copy of Mozilla's
NSS bundled with LO, not the macOS keychain). Tools like
mkcert -install import your local CA into the system keychain
and Firefox, but not into LO.
To trust your local CA from LO:
- Tools → Options → LibreOffice → Security → Certificate, click Certificate Path….
- Point LO at a Mozilla NSS folder that already trusts your CA
(typically a Firefox profile directory under
~/Library/Application Support/Firefox/Profiles/<random>.default-release/). - Or set the
MOZILLA_CERTIFICATE_FOLDERenvironment variable insoffice's startup environment.
soffice runs but the file save fails with 403 (WebDAV-specific)¶
If you're serving the file over WebDAV, two failure modes are worth checking on the server side:
- Query-string auth tokens are stripped by LO before
PUT— this is RFC-compliant (query strings aren't part of the resource identifier). Put any auth token in the URL path instead. The auth survives every verb that way. - Lock tokens are URL-keyed. If your server rewrites the URL
between
LOCKandPUT, the lock token from the LOCK response won't match the resource path onPUTand the server returns 423/412/409. Keep the URL identical across the WebDAV session.