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>
<input type="radio" data-name="permissions" id="newshare_attr_permissions_rw"
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 data-name="conversions" class="hidden">
<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 {
/**
@@ -123,4 +135,11 @@ export class Collection {
this.size = size;
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 { 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 { ErrorHandler } from "../utils/error.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 can_share = collection.permissions && (
collection.permissions.includes("RADICALE:share-map") ||
collection.permissions.includes("RADICALE:share-token")
);
let can_share = collection.has_permission(Permission.SHARE_MAP) || collection.has_permission(Permission.SHARE_TOKEN);
if (share_option) {
if (can_share) {
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 { CollectionType } from "../models/collection.js";
import { CollectionType, Permission } from "../models/collection.js";
import { collectionsCache } from "../utils/collections_cache.js";
import { ErrorHandler } from "../utils/error.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._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._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_container = get_element(this._html_scene, "[data-name=conversions_container]");
@@ -103,16 +106,32 @@ export class CreateEditShareScene {
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() {
let conversion = this._get_selected_conversion();
if (conversion != "none") {
this._permissions_ro_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._hidden_checkbox.checked = false;
} else {
this._permissions_ro_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") {
this._map_validator.validate();
@@ -140,7 +159,10 @@ export class CreateEditShareScene {
let enabled_by_owner = is_conversion ? true : this._enabled_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");
if (this._shareType === "token") {
let allowPropertiesWrite = this._properties_write_allow.checked;
if (allowPropertiesWrite) {
permissions = permissions + "P";
} else {
permissions = permissions + "p";
}
/** @type {string} */ let conversion_value = conversion;
@@ -221,6 +243,12 @@ export class CreateEditShareScene {
this._cancel_btn.onclick = () => this._oncancel();
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"));
title.textContent = this._edit ? "Edit Share" : "New Share";
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._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";
collectionsCache.getServerFeatures(this._user, this._password, this._errorHandler.setError, (features) => {
@@ -363,6 +403,7 @@ export class CreateEditShareScene {
this._sharemapfields.classList.add("hidden");
this._errorHandler.clearError();
}
this._on_permissions_change();
}
hide() {

View File

@@ -24,7 +24,7 @@ import {
delete_share_by_token,
reload_sharing_list,
} from "../api/sharing.js";
import { Collection } from "../models/collection.js";
import { Collection, Permission } from "../models/collection.js";
import { ErrorHandler } from "../utils/error.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();
let can_share_by_token = this._collection.permissions && this._collection.permissions.includes("RADICALE:share-token");
let can_share_by_map = this._collection.permissions && this._collection.permissions.includes("RADICALE:share-map");
let can_share_by_token = this._collection.has_permission(Permission.SHARE_TOKEN);
let can_share_by_map = this._collection.has_permission(Permission.SHARE_MAP);
if (can_share_by_token) {
if (this._share_by_token_btn) {