From e0017c9bf03518da2c45342fb13f84e7f48698b2 Mon Sep 17 00:00:00 2001 From: Max Berger Date: Sun, 22 Mar 2026 16:16:27 +0100 Subject: [PATCH] WebUI: Use Authentication header instead of URL --- integ_tests/test_x_remote_user.py | 34 +++++++++++++++---- radicale/web/internal_data/js/api/common.js | 20 ++++++++--- .../js/scenes/CollectionsScene.js | 25 ++++++++++++++ 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/integ_tests/test_x_remote_user.py b/integ_tests/test_x_remote_user.py index f41f7836..2bbd46b7 100644 --- a/integ_tests/test_x_remote_user.py +++ b/integ_tests/test_x_remote_user.py @@ -153,11 +153,33 @@ def test_create_collection_works( create_collection(page, radicale_server) - # Verify that the new collection exists afterwards - # By default it's called "Untitled Collection" or similar? - # Let's check what create_collection does. - # It clicks .fabcontainer a[data-name="new"] and then #createcollectionscene button[data-name="submit"] - expect(page.locator("article:not(.hidden)")).to_have_count(1) - # The title might be the HREF if no display name is set expect(page.locator("article:not(.hidden) .title")).to_be_visible() + + +def test_download_works_with_remote_user( + context: BrowserContext, page: Page, radicale_server: str +) -> None: + """Test downloading a collection with remote user.""" + context.set_extra_http_headers({"X-Remote-User": "admin"}) + page.goto(radicale_server) + + # Wait for auto-login + expect( + page.locator( + '#logoutview span[data-name="user"]', has_text="admin's Collections" + ) + ).to_be_visible() + + create_collection(page, radicale_server) + + # Start waiting for the download + with page.expect_download() as download_info: + # Perform the action that initiates download + page.hover("article:not(.hidden)") + page.click('article:not(.hidden) a[data-name="download"]') + + download = download_info.value + assert download.suggested_filename.endswith( + ".ics" + ) or download.suggested_filename.endswith(".vcf") diff --git a/radicale/web/internal_data/js/api/common.js b/radicale/web/internal_data/js/api/common.js index e17f8e9d..1f249936 100644 --- a/radicale/web/internal_data/js/api/common.js +++ b/radicale/web/internal_data/js/api/common.js @@ -32,6 +32,18 @@ export function to_error_message(request) { return request.status + " " + request.statusText; } +/** + * @param {?string} user + * @param {?string} password + * @returns {?string} + */ +export function get_auth_header(user, password) { + if (user !== null && password !== null && password !== "") { + return 'Basic ' + btoa(user + ':' + encodeURIComponent(password)); + } + return null; +} + /** * @param {string} method * @param {string} url @@ -41,10 +53,10 @@ export function to_error_message(request) { */ export function create_request(method, url, user, password) { let request = new XMLHttpRequest(); - if (user !== null && password !== null) { - request.open(method, url, true, user, encodeURIComponent(password)); - } else { - request.open(method, url, true); + request.open(method, url, true); + let auth = get_auth_header(user, password); + if (auth !== null) { + request.setRequestHeader('Authorization', auth); } return request; } \ No newline at end of file diff --git a/radicale/web/internal_data/js/scenes/CollectionsScene.js b/radicale/web/internal_data/js/scenes/CollectionsScene.js index 9c6296a0..c53245cb 100644 --- a/radicale/web/internal_data/js/scenes/CollectionsScene.js +++ b/radicale/web/internal_data/js/scenes/CollectionsScene.js @@ -20,6 +20,7 @@ */ import { delete_collection } from "../api/api.js"; +import { get_auth_header } from "../api/common.js"; import { SERVER } from "../constants.js"; import { Collection, CollectionType } from "../models/collection.js"; import { collectionsCache } from "../utils/collections_cache.js"; @@ -219,6 +220,30 @@ export class CollectionsScene { let href = SERVER + collection.href; url_form.value = href; download_btn.href = href; + download_btn.onclick = function (event) { + event.preventDefault(); + let auth = get_auth_header(user, password); + let headers = auth ? { 'Authorization': auth } : {}; + fetch(href, { + headers: headers + }).then(function (response) { + if (response.ok) { + return response.blob(); + } + throw new Error("Download failed: " + response.statusText); + }).then(function (blob) { + let url = window.URL.createObjectURL(blob); + let a = document.createElement("a"); + a.href = url; + a.download = (collection.displayname || collection.href).replace(/\/+$/, "") + (collection.type === CollectionType.ADDRESSBOOK ? ".vcf" : ".ics"); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + })["catch"](function (error) { + errorHandler.setError(error.message); + }); + }; if (collection.type == CollectionType.WEBCAL) { download_btn.parentElement.classList.add("hidden"); }