diff --git a/radicale/web/internal_data/index.html b/radicale/web/internal_data/index.html
index baa242c7..cb813508 100644
--- a/radicale/web/internal_data/index.html
+++ b/radicale/web/internal_data/index.html
@@ -280,6 +280,14 @@
name="newshare_permissions">
+
+
+
+
WARNING: This allows everyone who knows the URL to make
+ changes.
Conversions
diff --git a/radicale/web/internal_data/js/models/collection.js b/radicale/web/internal_data/js/models/collection.js
index 1e51224c..05474aa8 100644
--- a/radicale/web/internal_data/js/models/collection.js
+++ b/radicale/web/internal_data/js/models/collection.js
@@ -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);
+ }
}
\ 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 b4d85f07..abdc2b82 100644
--- a/radicale/web/internal_data/js/scenes/CollectionsScene.js
+++ b/radicale/web/internal_data/js/scenes/CollectionsScene.js
@@ -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");
diff --git a/radicale/web/internal_data/js/scenes/CreateEditShareScene.js b/radicale/web/internal_data/js/scenes/CreateEditShareScene.js
index 4a0d68b3..f2f869ee 100644
--- a/radicale/web/internal_data/js/scenes/CreateEditShareScene.js
+++ b/radicale/web/internal_data/js/scenes/CreateEditShareScene.js
@@ -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() {
diff --git a/radicale/web/internal_data/js/scenes/ShareCollectionScene.js b/radicale/web/internal_data/js/scenes/ShareCollectionScene.js
index d8b71d08..676ab49d 100644
--- a/radicale/web/internal_data/js/scenes/ShareCollectionScene.js
+++ b/radicale/web/internal_data/js/scenes/ShareCollectionScene.js
@@ -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) {