expose server version via PROPFIND on root collection and display in the UI for authenticated users
This commit is contained in:
@@ -168,6 +168,8 @@ def xml_propfind_response(
|
|||||||
props = []
|
props = []
|
||||||
# Should list all properties that can be retrieved by the code below
|
# Should list all properties that can be retrieved by the code below
|
||||||
props.append(xmlutils.make_clark("D:principal-collection-set"))
|
props.append(xmlutils.make_clark("D:principal-collection-set"))
|
||||||
|
if user:
|
||||||
|
props.append(xmlutils.make_clark("RADICALE:version"))
|
||||||
props.append(xmlutils.make_clark("D:current-user-principal"))
|
props.append(xmlutils.make_clark("D:current-user-principal"))
|
||||||
props.append(xmlutils.make_clark("D:current-user-privilege-set"))
|
props.append(xmlutils.make_clark("D:current-user-privilege-set"))
|
||||||
props.append(xmlutils.make_clark("D:supported-report-set"))
|
props.append(xmlutils.make_clark("D:supported-report-set"))
|
||||||
@@ -467,6 +469,11 @@ def xml_propfind_response(
|
|||||||
element.append(child_element)
|
element.append(child_element)
|
||||||
else:
|
else:
|
||||||
is404 = True
|
is404 = True
|
||||||
|
elif tag == xmlutils.make_clark("RADICALE:version"):
|
||||||
|
if user:
|
||||||
|
element.text = utils.package_version("radicale")
|
||||||
|
else:
|
||||||
|
is404 = True
|
||||||
else:
|
else:
|
||||||
human_tag = xmlutils.make_human_tag(tag)
|
human_tag = xmlutils.make_human_tag(tag)
|
||||||
tag_text = collection.get_meta(human_tag)
|
tag_text = collection.get_meta(human_tag)
|
||||||
|
|||||||
@@ -2310,6 +2310,40 @@ permissions: RrWw""")
|
|||||||
element = prop.find(xmlutils.make_clark("D:href"))
|
element = prop.find(xmlutils.make_clark("D:href"))
|
||||||
assert element is not None and element.text == "/user/"
|
assert element is not None and element.text == "/user/"
|
||||||
|
|
||||||
|
def test_version_exposure_authenticated(self) -> None:
|
||||||
|
"""Test if the version is returned for authenticated users."""
|
||||||
|
self.configure({"auth": {"type": "none"}})
|
||||||
|
status, responses = self.propfind("/", """\
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<propfind xmlns="DAV:" xmlns:R="http://radicale.org/ns/">
|
||||||
|
<prop>
|
||||||
|
<R:version />
|
||||||
|
</prop>
|
||||||
|
</propfind>""", login="user:")
|
||||||
|
assert status == 207
|
||||||
|
response = responses["/"]
|
||||||
|
assert not isinstance(response, int)
|
||||||
|
status_code, prop = response["RADICALE:version"]
|
||||||
|
assert status_code == 200
|
||||||
|
assert prop.text == utils.package_version("radicale")
|
||||||
|
|
||||||
|
def test_version_exposure_unauthenticated(self) -> None:
|
||||||
|
"""Test if the version is hidden for unauthenticated users."""
|
||||||
|
self.configure({"auth": {"type": "none"}})
|
||||||
|
# Without login, 'user' will be empty
|
||||||
|
status, responses = self.propfind("/", """\
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<propfind xmlns="DAV:" xmlns:R="http://radicale.org/ns/">
|
||||||
|
<prop>
|
||||||
|
<R:version />
|
||||||
|
</prop>
|
||||||
|
</propfind>""")
|
||||||
|
assert status == 207
|
||||||
|
response = responses["/"]
|
||||||
|
assert not isinstance(response, int)
|
||||||
|
status_code, _ = response["RADICALE:version"]
|
||||||
|
assert status_code == 404
|
||||||
|
|
||||||
def test_authentication(self) -> None:
|
def test_authentication(self) -> None:
|
||||||
"""Test if server sends authentication request."""
|
"""Test if server sends authentication request."""
|
||||||
self.configure({"auth": {"type": "htpasswd",
|
self.configure({"auth": {"type": "htpasswd",
|
||||||
|
|||||||
@@ -179,6 +179,17 @@ summary:hover {
|
|||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#logoutview .version {
|
||||||
|
float: right !important;
|
||||||
|
width: auto !important;
|
||||||
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
right: 25px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #999;
|
||||||
|
line-height: 2.2em;
|
||||||
|
}
|
||||||
|
|
||||||
#collectionsscene {
|
#collectionsscene {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<span data-name="user"></span>
|
<span data-name="user"></span>
|
||||||
<a href="#" class="green" data-name="refresh" title="Refresh">Refresh</a>
|
<a href="#" class="green" data-name="refresh" title="Refresh">Refresh</a>
|
||||||
<a href="#" class="red" data-name="logout" title="Logout">Logout</a>
|
<a href="#" class="red" data-name="logout" title="Logout">Logout</a>
|
||||||
|
<span data-name="version" class="version"></span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function get_principal(user, password, callback) {
|
|||||||
if (xml) {
|
if (xml) {
|
||||||
let principal_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|current-user-principal > *|href");
|
let principal_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|current-user-principal > *|href");
|
||||||
let displayname_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|displayname");
|
let displayname_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|displayname");
|
||||||
|
let version_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|version");
|
||||||
if (principal_element) {
|
if (principal_element) {
|
||||||
callback(new Collection(
|
callback(new Collection(
|
||||||
principal_element.textContent,
|
principal_element.textContent,
|
||||||
@@ -52,7 +53,8 @@ export function get_principal(user, password, callback) {
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
[]), null,);
|
[],
|
||||||
|
version_element ? version_element.textContent : ""), null,);
|
||||||
} else {
|
} else {
|
||||||
callback(null, "No valid XML received")
|
callback(null, "No valid XML received")
|
||||||
}
|
}
|
||||||
@@ -64,10 +66,11 @@ export function get_principal(user, password, callback) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
request.send('<?xml version="1.0" encoding="utf-8" ?>' +
|
request.send('<?xml version="1.0" encoding="utf-8" ?>' +
|
||||||
'<propfind xmlns="DAV:">' +
|
'<propfind xmlns="DAV:" xmlns:RADICALE="http://radicale.org/ns/">' +
|
||||||
'<prop>' +
|
'<prop>' +
|
||||||
'<current-user-principal />' +
|
'<current-user-principal />' +
|
||||||
'<displayname />' +
|
'<displayname />' +
|
||||||
|
'<RADICALE:version />' +
|
||||||
'</prop>' +
|
'</prop>' +
|
||||||
'</propfind>');
|
'</propfind>');
|
||||||
return request;
|
return request;
|
||||||
@@ -259,7 +262,7 @@ function _parse_collection(response, collection_href) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (href.endsWith("/") && href !== collection_href && type) {
|
if (href.endsWith("/") && href !== collection_href && type) {
|
||||||
return new Collection(href, type, displayname, description, sane_color, count, size, source, permissions);
|
return new Collection(href, type, displayname, description, sane_color, count, size, source, permissions, "");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,8 +123,9 @@ export class Collection {
|
|||||||
* @param {number} size
|
* @param {number} size
|
||||||
* @param {string} source
|
* @param {string} source
|
||||||
* @param {Array<string>} permissions
|
* @param {Array<string>} permissions
|
||||||
|
* @param {string} version
|
||||||
*/
|
*/
|
||||||
constructor(href, type, displayname, description, color, contentcount, size, source, permissions) {
|
constructor(href, type, displayname, description, color, contentcount, size, source, permissions, version = "") {
|
||||||
this.href = href;
|
this.href = href;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.displayname = displayname;
|
this.displayname = displayname;
|
||||||
@@ -134,6 +135,7 @@ export class Collection {
|
|||||||
this.contentcount = contentcount;
|
this.contentcount = contentcount;
|
||||||
this.size = size;
|
this.size = size;
|
||||||
this.permissions = permissions;
|
this.permissions = permissions;
|
||||||
|
this.version = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
has_permission(/** @type {string} */ permission) {
|
has_permission(/** @type {string} */ permission) {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export class LoginScene {
|
|||||||
this._logout_view = get_element_by_id("logoutview");
|
this._logout_view = get_element_by_id("logoutview");
|
||||||
this._logout_user_form = get_element(this._logout_view, "[data-name=user]");
|
this._logout_user_form = get_element(this._logout_view, "[data-name=user]");
|
||||||
this._logout_btn = get_element(this._logout_view, "[data-name=logout]");
|
this._logout_btn = get_element(this._logout_view, "[data-name=logout]");
|
||||||
|
this._logout_version_form = get_element(this._logout_view, "[data-name=version]");
|
||||||
this._refresh_btn = get_element(this._logout_view, "[data-name=refresh]");
|
this._refresh_btn = get_element(this._logout_view, "[data-name=refresh]");
|
||||||
|
|
||||||
this._user = "";
|
this._user = "";
|
||||||
@@ -91,6 +92,11 @@ export class LoginScene {
|
|||||||
pop_scene();
|
pop_scene();
|
||||||
} else if (principal_collection) {
|
} else if (principal_collection) {
|
||||||
this._logout_user_form.textContent = extract_title(principal_collection) + "'s Collections";
|
this._logout_user_form.textContent = extract_title(principal_collection) + "'s Collections";
|
||||||
|
if (principal_collection.version) {
|
||||||
|
this._logout_version_form.textContent = "Radicale v" + principal_collection.version;
|
||||||
|
} else {
|
||||||
|
this._logout_version_form.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
// clear error on successful login
|
// clear error on successful login
|
||||||
this._errorHandler.clearError();
|
this._errorHandler.clearError();
|
||||||
|
|||||||
Reference in New Issue
Block a user