UI/Sharing: Allow explicit setting of write-properties permission

This commit is contained in:
Max Berger
2026-04-11 21:51:26 +02:00
parent 11ecb8c672
commit a5780f2dc5
5 changed files with 75 additions and 10 deletions

View File

@@ -280,6 +280,14 @@
name="newshare_permissions"><label for="newshare_attr_permissions_ro">Readonly</label> name="newshare_permissions"><label for="newshare_attr_permissions_ro">Readonly</label>
<input type="radio" data-name="permissions" id="newshare_attr_permissions_rw" <input type="radio" data-name="permissions" id="newshare_attr_permissions_rw"
name="newshare_permissions" /><label for="newshare_attr_permissions_rw">Read/Write</label> name="newshare_permissions" /><label for="newshare_attr_permissions_rw">Read/Write</label>
<br />
<input type="radio" data-name="properties_write" id="newshare_attr_properties_write_deny"
name="newshare_properties_write"><label for="newshare_attr_properties_write_deny">Deny</label>
<input type="radio" data-name="properties_write" id="newshare_attr_properties_write_allow"
name="newshare_properties_write"><label for="newshare_attr_properties_write_allow">Allow Properties
write</label>
<p data-name="token_write_warning" class="red hidden">WARNING: This allows everyone who knows the URL to make
changes.</p>
</details> </details>
<details data-name="conversions" class="hidden"> <details data-name="conversions" class="hidden">
<summary>Conversions</summary> <summary>Conversions</summary>

View File

@@ -99,6 +99,18 @@ export class CollectionType {
} }
} }
export class Permission {
// Private Fields
static #_WRITE_PROPERTIES = "D:write-properties";
static #_SHARE_MAP = "RADICALE:share-map";
static #_SHARE_TOKEN = "RADICALE:share-token";
// Accessors for "get" functions only (no "set" functions)
static get WRITE_PROPERTIES() { return this.#_WRITE_PROPERTIES; }
static get SHARE_MAP() { return this.#_SHARE_MAP; }
static get SHARE_TOKEN() { return this.#_SHARE_TOKEN; }
}
export class Collection { export class Collection {
/** /**
@@ -123,4 +135,11 @@ export class Collection {
this.size = size; this.size = size;
this.permissions = permissions; this.permissions = permissions;
} }
has_permission(/** @type {string} */ permission) {
if (!this.permissions) {
return false;
}
return this.permissions.includes(permission);
}
} }

View File

@@ -21,7 +21,7 @@
import { delete_collection } from "../api/api.js"; import { delete_collection } from "../api/api.js";
import { get_auth_header } from "../api/common.js"; import { get_auth_header } from "../api/common.js";
import { Collection, CollectionType } from "../models/collection.js"; import { Collection, CollectionType, Permission } from "../models/collection.js";
import { collectionsCache } from "../utils/collections_cache.js"; import { collectionsCache } from "../utils/collections_cache.js";
import { ErrorHandler } from "../utils/error.js"; import { ErrorHandler } from "../utils/error.js";
import { bytesToHumanReadable, get_element, get_element_by_id } from "../utils/misc.js"; import { bytesToHumanReadable, get_element, get_element_by_id } from "../utils/misc.js";
@@ -189,10 +189,7 @@ export class CollectionsScene {
}); });
let share_option = get_element(node, "[data-name=shareoption]"); let share_option = get_element(node, "[data-name=shareoption]");
let can_share = collection.permissions && ( let can_share = collection.has_permission(Permission.SHARE_MAP) || collection.has_permission(Permission.SHARE_TOKEN);
collection.permissions.includes("RADICALE:share-map") ||
collection.permissions.includes("RADICALE:share-token")
);
if (share_option) { if (share_option) {
if (can_share) { if (can_share) {
share_option.classList.remove("hidden"); share_option.classList.remove("hidden");

View File

@@ -20,7 +20,7 @@
*/ */
import { Share, add_share_by_map, add_share_by_token, get_property_key, update_share_by_map, update_share_by_token } from "../api/sharing.js"; import { Share, add_share_by_map, add_share_by_token, get_property_key, update_share_by_map, update_share_by_token } from "../api/sharing.js";
import { CollectionType } from "../models/collection.js"; import { CollectionType, Permission } from "../models/collection.js";
import { collectionsCache } from "../utils/collections_cache.js"; import { collectionsCache } from "../utils/collections_cache.js";
import { ErrorHandler } from "../utils/error.js"; import { ErrorHandler } from "../utils/error.js";
import { FormValidator, validate_href, validate_non_empty, validate_not_empty_or_equals } from "../utils/form_validator.js"; import { FormValidator, validate_href, validate_non_empty, validate_not_empty_or_equals } from "../utils/form_validator.js";
@@ -56,6 +56,9 @@ export class CreateEditShareScene {
this._hidden_checkbox = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=hidden]")); this._hidden_checkbox = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=hidden]"));
this._permissions_ro_radio = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_permissions_ro")); this._permissions_ro_radio = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_permissions_ro"));
this._permissions_rw_radio = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_permissions_rw")); this._permissions_rw_radio = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_permissions_rw"));
this._properties_write_allow = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_properties_write_allow"));
this._properties_write_deny = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_properties_write_deny"));
this._token_write_warning = /** @type {HTMLElement} */ (get_element(this._html_scene, "[data-name=token_write_warning]"));
this._conversions_details = /** @type {HTMLDetailsElement} */ (get_element(this._html_scene, "[data-name=conversions]")); this._conversions_details = /** @type {HTMLDetailsElement} */ (get_element(this._html_scene, "[data-name=conversions]"));
this._conversions_container = get_element(this._html_scene, "[data-name=conversions_container]"); this._conversions_container = get_element(this._html_scene, "[data-name=conversions_container]");
@@ -103,16 +106,32 @@ export class CreateEditShareScene {
return checked ? checked.value : "none"; return checked ? checked.value : "none";
} }
_on_permissions_change() {
if (this._shareType === "token") {
if (this._permissions_rw_radio.checked || this._properties_write_allow.checked) {
this._token_write_warning.classList.remove("hidden");
} else {
this._token_write_warning.classList.add("hidden");
}
} else {
this._token_write_warning.classList.add("hidden");
}
}
_on_conversion_change() { _on_conversion_change() {
let conversion = this._get_selected_conversion(); let conversion = this._get_selected_conversion();
if (conversion != "none") { if (conversion != "none") {
this._permissions_ro_radio.disabled = true; this._permissions_ro_radio.disabled = true;
this._permissions_rw_radio.disabled = true; this._permissions_rw_radio.disabled = true;
this._properties_write_allow.disabled = true;
this._properties_write_deny.disabled = true;
this._enabled_checkbox.checked = true; this._enabled_checkbox.checked = true;
this._hidden_checkbox.checked = false; this._hidden_checkbox.checked = false;
} else { } else {
this._permissions_ro_radio.disabled = false; this._permissions_ro_radio.disabled = false;
this._permissions_rw_radio.disabled = false; this._permissions_rw_radio.disabled = false;
this._properties_write_allow.disabled = false;
this._properties_write_deny.disabled = false;
} }
if (this._shareType === "map") { if (this._shareType === "map") {
this._map_validator.validate(); this._map_validator.validate();
@@ -140,7 +159,10 @@ export class CreateEditShareScene {
let enabled_by_owner = is_conversion ? true : this._enabled_checkbox.checked; let enabled_by_owner = is_conversion ? true : this._enabled_checkbox.checked;
let hidden_by_owner = is_conversion ? false : this._hidden_checkbox.checked; let hidden_by_owner = is_conversion ? false : this._hidden_checkbox.checked;
let permissions = is_conversion ? "r" : (this._permissions_rw_radio.checked ? "rw" : "r"); let permissions = is_conversion ? "r" : (this._permissions_rw_radio.checked ? "rw" : "r");
if (this._shareType === "token") { let allowPropertiesWrite = this._properties_write_allow.checked;
if (allowPropertiesWrite) {
permissions = permissions + "P";
} else {
permissions = permissions + "p"; permissions = permissions + "p";
} }
/** @type {string} */ let conversion_value = conversion; /** @type {string} */ let conversion_value = conversion;
@@ -221,6 +243,12 @@ export class CreateEditShareScene {
this._cancel_btn.onclick = () => this._oncancel(); this._cancel_btn.onclick = () => this._oncancel();
this._form.onsubmit = () => this._onsubmit(); this._form.onsubmit = () => this._onsubmit();
let onChangeCallback = () => this._on_permissions_change();
this._permissions_ro_radio.addEventListener("change", onChangeCallback);
this._permissions_rw_radio.addEventListener("change", onChangeCallback);
this._properties_write_allow.addEventListener("change", onChangeCallback);
this._properties_write_deny.addEventListener("change", onChangeCallback);
/** @type {HTMLHeadingElement} */ let title = /** @type {HTMLHeadingElement} */ (get_element(this._html_scene, "h1")); /** @type {HTMLHeadingElement} */ let title = /** @type {HTMLHeadingElement} */ (get_element(this._html_scene, "h1"));
title.textContent = this._edit ? "Edit Share" : "New Share"; title.textContent = this._edit ? "Edit Share" : "New Share";
this._submit_btn.textContent = this._edit ? "Save" : "Create"; this._submit_btn.textContent = this._edit ? "Save" : "Create";
@@ -230,6 +258,18 @@ export class CreateEditShareScene {
this._enabled_checkbox.checked = (this._edit && this._share && this._share.EnabledByOwner !== null) ? this._share.EnabledByOwner : true; this._enabled_checkbox.checked = (this._edit && this._share && this._share.EnabledByOwner !== null) ? this._share.EnabledByOwner : true;
this._hidden_checkbox.checked = (this._edit && this._share && this._share.HiddenByOwner !== null) ? this._share.HiddenByOwner : false; this._hidden_checkbox.checked = (this._edit && this._share && this._share.HiddenByOwner !== null) ? this._share.HiddenByOwner : false;
let hasWriteProperties = this._collection.has_permission(Permission.WRITE_PROPERTIES);
if (this._edit || this._shareType === "map") {
if (hasWriteProperties) {
this._properties_write_allow.checked = true;
} else {
this._properties_write_deny.checked = true;
}
} else {
this._properties_write_deny.checked = true;
}
let initial_conversion = (this._edit && this._share && this._share.Conversion) ? this._share.Conversion : "none"; let initial_conversion = (this._edit && this._share && this._share.Conversion) ? this._share.Conversion : "none";
collectionsCache.getServerFeatures(this._user, this._password, this._errorHandler.setError, (features) => { collectionsCache.getServerFeatures(this._user, this._password, this._errorHandler.setError, (features) => {
@@ -363,6 +403,7 @@ export class CreateEditShareScene {
this._sharemapfields.classList.add("hidden"); this._sharemapfields.classList.add("hidden");
this._errorHandler.clearError(); this._errorHandler.clearError();
} }
this._on_permissions_change();
} }
hide() { hide() {

View File

@@ -24,7 +24,7 @@ import {
delete_share_by_token, delete_share_by_token,
reload_sharing_list, reload_sharing_list,
} from "../api/sharing.js"; } from "../api/sharing.js";
import { Collection } from "../models/collection.js"; import { Collection, Permission } from "../models/collection.js";
import { ErrorHandler } from "../utils/error.js"; import { ErrorHandler } from "../utils/error.js";
import { get_element, get_element_by_id } from "../utils/misc.js"; import { get_element, get_element_by_id } from "../utils/misc.js";
@@ -88,8 +88,8 @@ export class ShareCollectionScene {
}); });
this._cancel_btn.onclick = () => this._oncancel(); this._cancel_btn.onclick = () => this._oncancel();
let can_share_by_token = this._collection.permissions && this._collection.permissions.includes("RADICALE:share-token"); let can_share_by_token = this._collection.has_permission(Permission.SHARE_TOKEN);
let can_share_by_map = this._collection.permissions && this._collection.permissions.includes("RADICALE:share-map"); let can_share_by_map = this._collection.has_permission(Permission.SHARE_MAP);
if (can_share_by_token) { if (can_share_by_token) {
if (this._share_by_token_btn) { if (this._share_by_token_btn) {