Compare commits

..

7 Commits

Author SHA1 Message Date
Lennart
455b4c405f version 0.4.13 2025-07-10 21:39:28 +02:00
Lennart
2774d092ac propfind: Implement <include/>
Implements #95
2025-07-10 15:45:54 +02:00
Lennart
32b616fd75 xml serialize_to_string: Enable indentation 2025-07-10 15:45:07 +02:00
Lennart K
b02f7c427a minor refactoring 2025-07-10 10:51:59 +02:00
Lennart
eae8e7d768 version 0.4.12 2025-07-07 21:18:46 +02:00
Lennart
105718a4ca frontend: Add xml escaping to collection creation forms 2025-07-07 21:18:16 +02:00
Lennart
0e68f1bdce frontend: refactor collection list to allow for dialogs 2025-07-07 11:22:20 +02:00
26 changed files with 174 additions and 113 deletions

22
Cargo.lock generated
View File

@@ -2999,7 +2999,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -3042,7 +3042,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3080,7 +3080,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3112,7 +3112,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3137,7 +3137,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3163,7 +3163,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -3196,7 +3196,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -3214,7 +3214,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3229,7 +3229,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3263,7 +3263,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3284,7 +3284,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.4.11" version = "0.4.13"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.12", "thiserror 2.0.12",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.4.11" version = "0.4.13"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical" repository = "https://github.com/lennart-k/rustical"

View File

@@ -67,7 +67,7 @@ fn objects_response(
object, object,
principal: principal.to_owned(), principal: principal.to_owned(),
} }
.propfind(&path, prop, puri, user)?, .propfind(&path, prop, None, puri, user)?,
); );
} }

View File

@@ -39,7 +39,7 @@ pub async fn handle_sync_collection<C: CalendarStore>(
object, object,
principal: principal.to_owned(), principal: principal.to_owned(),
} }
.propfind(&path, &sync_collection.prop, puri, user)?, .propfind(&path, &sync_collection.prop, None, puri, user)?,
); );
} }

View File

@@ -69,7 +69,6 @@ impl Resource for CalendarObjectResource {
} }
fn get_displayname(&self) -> Option<&str> { fn get_displayname(&self) -> Option<&str> {
// TODO: Extract summary from object
None None
} }

View File

@@ -47,7 +47,7 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
let mut resp = Response::builder().status(StatusCode::OK); let mut resp = Response::builder().status(StatusCode::OK);
let hdrs = resp.headers_mut().unwrap(); let hdrs = resp.headers_mut().unwrap();
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap()); hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
let filename = format!("{}_{}.vcf", principal, addressbook_id); let filename = format!("{principal}_{addressbook_id}.vcf");
let filename = utf8_percent_encode(&filename, CONTROLS); let filename = utf8_percent_encode(&filename, CONTROLS);
hdrs.insert( hdrs.insert(
header::CONTENT_DISPOSITION, header::CONTENT_DISPOSITION,

View File

@@ -88,15 +88,8 @@ pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>(
} }
} }
match addr_store.insert_addressbook(addressbook).await { addr_store.insert_addressbook(addressbook).await?;
// TODO: The spec says we should return a mkcol-response. Ok(StatusCode::CREATED.into_response())
// However, it works without one but breaks on iPadOS when using an empty one :)
Ok(()) => Ok(StatusCode::CREATED.into_response()),
Err(err) => {
dbg!(err.to_string());
Err(err.into())
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -81,7 +81,7 @@ pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
object, object,
principal: principal.to_owned(), principal: principal.to_owned(),
} }
.propfind(&path, prop, puri, user)?, .propfind(&path, prop, None, puri, user)?,
); );
} }

View File

@@ -39,7 +39,7 @@ pub async fn handle_sync_collection<AS: AddressbookStore>(
object, object,
principal: principal.to_owned(), principal: principal.to_owned(),
} }
.propfind(&path, &sync_collection.prop, puri, user)?, .propfind(&path, &sync_collection.prop, None, puri, user)?,
); );
} }

View File

@@ -64,6 +64,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
} else { } else {
PropfindElement { PropfindElement {
prop: PropfindType::Allprop, prop: PropfindType::Allprop,
include: None,
} }
}; };
let propfind_member: PropfindElement<<<R::MemberType as Resource>::Prop as PropName>::Names> = let propfind_member: PropfindElement<<<R::MemberType as Resource>::Prop as PropName>::Names> =
@@ -72,6 +73,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
} else { } else {
PropfindElement { PropfindElement {
prop: PropfindType::Allprop, prop: PropfindType::Allprop,
include: None,
} }
}; };
@@ -82,13 +84,20 @@ pub(crate) async fn route_propfind<R: ResourceService>(
member_responses.push(member.propfind( member_responses.push(member.propfind(
&format!("{}/{}", path.trim_end_matches('/'), member.get_name()), &format!("{}/{}", path.trim_end_matches('/'), member.get_name()),
&propfind_member.prop, &propfind_member.prop,
propfind_member.include.as_ref(),
puri, puri,
principal, principal,
)?); )?);
} }
} }
let response = resource.propfind(path, &propfind_self.prop, puri, principal)?; let response = resource.propfind(
path,
&propfind_self.prop,
propfind_self.include.as_ref(),
puri,
principal,
)?;
Ok(MultistatusElement { Ok(MultistatusElement {
responses: vec![response], responses: vec![response],

View File

@@ -106,6 +106,7 @@ pub trait Resource: Clone + Send + 'static {
&self, &self,
path: &str, path: &str,
prop: &PropfindType<<Self::Prop as PropName>::Names>, prop: &PropfindType<<Self::Prop as PropName>::Names>,
include: Option<&PropElement<<Self::Prop as PropName>::Names>>,
principal_uri: &impl PrincipalUri, principal_uri: &impl PrincipalUri,
principal: &Self::Principal, principal: &Self::Principal,
) -> Result<ResponseElement<Self::Prop>, Self::Error> { ) -> Result<ResponseElement<Self::Prop>, Self::Error> {
@@ -115,36 +116,40 @@ pub trait Resource: Clone + Send + 'static {
path.push('/'); path.push('/');
} }
// TODO: Support include element let (mut props, mut invalid_props): (HashSet<<Self::Prop as PropName>::Names>, Vec<_>) =
let (props, invalid_props): (HashSet<<Self::Prop as PropName>::Names>, Vec<_>) = match prop match prop {
{ PropfindType::Propname => {
PropfindType::Propname => { let props = Self::list_props()
let props = Self::list_props() .into_iter()
.into_iter() .map(|(ns, tag)| (ns.map(NamespaceOwned::from), tag.to_string()))
.map(|(ns, tag)| (ns.map(NamespaceOwned::from), tag.to_string())) .collect_vec();
.collect_vec();
return Ok(ResponseElement { return Ok(ResponseElement {
href: path.to_owned(), href: path.to_owned(),
propstat: vec![PropstatWrapper::TagList(PropstatElement { propstat: vec![PropstatWrapper::TagList(PropstatElement {
prop: TagList::from(props), prop: TagList::from(props),
status: StatusCode::OK, status: StatusCode::OK,
})], })],
..Default::default() ..Default::default()
}); });
} }
PropfindType::Allprop => ( PropfindType::Allprop => (
Self::list_props() Self::list_props()
.iter() .iter()
.map(|(_ns, name)| <Self::Prop as PropName>::Names::from_str(name).unwrap()) .map(|(_ns, name)| <Self::Prop as PropName>::Names::from_str(name).unwrap())
.collect(), .collect(),
vec![], vec![],
), ),
PropfindType::Prop(PropElement(valid_tags, invalid_tags)) => ( PropfindType::Prop(PropElement(valid_tags, invalid_tags)) => (
valid_tags.iter().cloned().collect(), valid_tags.iter().cloned().collect(),
invalid_tags.to_owned(), invalid_tags.to_owned(),
), ),
}; };
if let Some(PropElement(valid_tags, invalid_tags)) = include {
props.extend(valid_tags.clone());
invalid_props.extend(invalid_tags.to_owned());
}
let prop_responses = props let prop_responses = props
.into_iter() .into_iter()

View File

@@ -11,10 +11,11 @@ use rustical_xml::XmlRootTag;
pub struct PropfindElement<PN: XmlDeserialize> { pub struct PropfindElement<PN: XmlDeserialize> {
#[xml(ty = "untagged")] #[xml(ty = "untagged")]
pub prop: PropfindType<PN>, pub prop: PropfindType<PN>,
#[xml(ns = "crate::namespace::NS_DAV")]
pub include: Option<PropElement<PN>>,
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
// pub struct PropElement<PN: XmlDeserialize = Propname>(#[xml(ty = "untagged", flatten)] pub Vec<PN>);
pub struct PropElement<PN: XmlDeserialize>( pub struct PropElement<PN: XmlDeserialize>(
// valid // valid
pub Vec<PN>, pub Vec<PN>,

View File

@@ -33,7 +33,13 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
out, out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><resourcetype><displayname xmlns=\"DAV:\"/><calendar-color xmlns=\"http://calendarserver.org/ns/\"/></resourcetype></document>" r#"<?xml version="1.0" encoding="utf-8"?>
<document>
<resourcetype>
<displayname xmlns="DAV:"/>
<calendar-color xmlns="http://calendarserver.org/ns/"/>
</resourcetype>
</document>"#
) )
} }
} }

View File

@@ -2,6 +2,7 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js'; import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav"; import { createClient } from "webdav";
import { escapeXml } from ".";
@customElement("create-addressbook-form") @customElement("create-addressbook-form")
export class CreateAddressbookForm extends LitElement { export class CreateAddressbookForm extends LitElement {
@@ -17,15 +18,15 @@ export class CreateAddressbookForm extends LitElement {
client = createClient("/carddav") client = createClient("/carddav")
@property() @property()
user: String = '' user: string = ''
@property() @property()
principal: String = '' principal: string = ''
@property() @property()
addr_id: String = '' addr_id: string = ''
@property() @property()
displayname: String = '' displayname: string = ''
@property() @property()
description: String = '' description: string = ''
dialog: Ref<HTMLDialogElement> = createRef() dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef() form: Ref<HTMLFormElement> = createRef()
@@ -79,14 +80,13 @@ export class CreateAddressbookForm extends LitElement {
alert("Empty displayname") alert("Empty displayname")
return return
} }
// TODO: Escape user input: There's not really a security risk here but would be nicer
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, { await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, {
data: ` data: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav"> <mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set> <set>
<prop> <prop>
<displayname>${this.displayname}</displayname> <displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CARD:addressbook-description>${this.description}</CARD:addressbook-description>` : ''} ${this.description ? `<CARD:addressbook-description>${escapeXml(this.description)}</CARD:addressbook-description>` : ''}
</prop> </prop>
</set> </set>
</mkcol> </mkcol>

View File

@@ -2,6 +2,7 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js'; import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav"; import { createClient } from "webdav";
import { escapeXml } from ".";
@customElement("create-calendar-form") @customElement("create-calendar-form")
export class CreateCalendarForm extends LitElement { export class CreateCalendarForm extends LitElement {
@@ -16,21 +17,21 @@ export class CreateCalendarForm extends LitElement {
client = createClient("/caldav") client = createClient("/caldav")
@property() @property()
user: String = '' user: string = ''
@property() @property()
principal: String = '' principal: string = ''
@property() @property()
cal_id: String = '' cal_id: string = ''
@property() @property()
displayname: String = '' displayname: string = ''
@property() @property()
description: String = '' description: string = ''
@property() @property()
color: String = '' color: string = ''
@property() @property()
isSubscription: boolean = false isSubscription: boolean = false
@property() @property()
subscriptionUrl: String = '' subscriptionUrl: string = ''
@property() @property()
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set() components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
@@ -123,12 +124,12 @@ export class CreateCalendarForm extends LitElement {
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/"> <mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set> <set>
<prop> <prop>
<displayname>${this.displayname}</displayname> <displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''} ${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''} ${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
${(this.isSubscription && this.subscriptionUrl) ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ''} ${(this.isSubscription && this.subscriptionUrl) ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ''}
<CAL:supported-calendar-component-set> <CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')} ${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${escapeXml(comp)}" />`).join('\n')}
</CAL:supported-calendar-component-set> </CAL:supported-calendar-component-set>
</prop> </prop>
</set> </set>

View File

@@ -0,0 +1,7 @@
export function escapeXml(unsafe: string): string {
return unsafe.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}

View File

@@ -1,6 +1,6 @@
import { i, x } from "./lit-z6_uA4GX.mjs"; import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-D0NJdseG.mjs"; import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n } from "./ref-CPp9J0V5.mjs"; import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs";
import { a as an } from "./webdav-D0R7xCzX.mjs"; import { a as an } from "./webdav-D0R7xCzX.mjs";
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -84,8 +84,8 @@ let CreateAddressbookForm = class extends i {
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav"> <mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set> <set>
<prop> <prop>
<displayname>${this.displayname}</displayname> <displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CARD:addressbook-description>${this.description}</CARD:addressbook-description>` : ""} ${this.description ? `<CARD:addressbook-description>${escapeXml(this.description)}</CARD:addressbook-description>` : ""}
</prop> </prop>
</set> </set>
</mkcol> </mkcol>

View File

@@ -1,6 +1,6 @@
import { i, x } from "./lit-z6_uA4GX.mjs"; import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-D0NJdseG.mjs"; import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n } from "./ref-CPp9J0V5.mjs"; import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs";
import { a as an } from "./webdav-D0R7xCzX.mjs"; import { a as an } from "./webdav-D0R7xCzX.mjs";
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -119,12 +119,12 @@ let CreateCalendarForm = class extends i {
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/"> <mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set> <set>
<prop> <prop>
<displayname>${this.displayname}</displayname> <displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ""} ${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""} ${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
${this.isSubscription && this.subscriptionUrl ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ""} ${this.isSubscription && this.subscriptionUrl ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ""}
<CAL:supported-calendar-component-set> <CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map((comp) => `<CAL:comp name="${comp}" />`).join("\n")} ${Array.from(this.components.keys()).map((comp) => `<CAL:comp name="${escapeXml(comp)}" />`).join("\n")}
</CAL:supported-calendar-component-set> </CAL:supported-calendar-component-set>
</prop> </prop>
</set> </set>

View File

@@ -122,7 +122,11 @@ const o = /* @__PURE__ */ new WeakMap(), n = e$1(class extends f {
this.rt(this.ct); this.rt(this.ct);
} }
}); });
function escapeXml(unsafe) {
return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
}
export { export {
escapeXml as a,
e, e,
n n
}; };

View File

@@ -205,10 +205,21 @@ ul.collection-list {
li.collection-list-item { li.collection-list-item {
list-style: none; list-style: none;
display: contents; display: block;
position: relative;
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 5%);
border: 2px solid var(--border-color);
border-radius: 12px;
margin: 12px 0;
box-shadow: 4px 2px 12px -6px black;
overflow: hidden;
a { a {
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 5%); position: absolute;
inset: 2px;
}
.inner {
display: grid; display: grid;
min-height: 80px; min-height: 80px;
height: fit-content; height: fit-content;
@@ -227,11 +238,15 @@ ul.collection-list {
text-decoration: none; text-decoration: none;
padding-left: 12px; padding-left: 12px;
border: 2px solid var(--border-color); position: relative;
border-radius: 12px; z-index: 1;
margin: 12px 0; pointer-events: none;
box-shadow: 4px 2px 12px -6px black;
overflow: hidden; a,
button {
pointer-events: all;
cursor: pointer;
}
.title { .title {
font-weight: bold; font-weight: bold;
@@ -283,10 +298,10 @@ ul.collection-list {
display: flex; display: flex;
gap: 12px; gap: 12px;
} }
}
&:hover { &:hover {
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%); background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
}
} }
} }
} }

View File

@@ -2,7 +2,8 @@
<ul class="collection-list"> <ul class="collection-list">
{% for (meta, addressbook) in addressbooks %} {% for (meta, addressbook) in addressbooks %}
<li class="collection-list-item"> <li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"> <a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
<div class="inner">
<span class="title"> <span class="title">
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%} {%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }} {{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
@@ -21,7 +22,7 @@
<div class="metadata"> <div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects {{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
</div> </div>
</a> </div>
</li> </li>
{% else %} {% else %}
You do not have any addressbooks yet You do not have any addressbooks yet
@@ -32,7 +33,8 @@
<ul class="collection-list"> <ul class="collection-list">
{% for (meta, addressbook) in deleted_addressbooks %} {% for (meta, addressbook) in deleted_addressbooks %}
<li class="collection-list-item"> <li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"> <a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
<div class="inner">
<span class="title"> <span class="title">
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%} {%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }} {{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
@@ -50,7 +52,7 @@
<div class="metadata"> <div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects {{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
</div> </div>
</a> </div>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -3,7 +3,8 @@
{% for (meta, calendar) in calendars %} {% for (meta, calendar) in calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %} {% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}"> <li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"> <a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"></a>
<div class="inner">
<span class="title"> <span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%} {%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }} {{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
@@ -31,7 +32,7 @@
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects {{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
</div> </div>
<div class="color-chip"></div> <div class="color-chip"></div>
</a> </div>
</li> </li>
{% else %} {% else %}
You do not have any calendars yet You do not have any calendars yet
@@ -43,7 +44,8 @@
{% for (meta, calendar) in deleted_calendars %} {% for (meta, calendar) in deleted_calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %} {% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}"> <li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"> <a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"></a>
<div class="inner">
<span class="title"> <span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%} {%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }} {{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
@@ -67,7 +69,7 @@
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects {{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
</div> </div>
<div class="color-chip"></div> <div class="color-chip"></div>
</a> </div>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -13,7 +13,7 @@ pub enum PrincipalType {
Resource, Resource,
Room, Room,
Unknown, Unknown,
// TODO: X-Name, IANA-token // X-Name, IANA-token
} }
impl TryFrom<&str> for PrincipalType { impl TryFrom<&str> for PrincipalType {

View File

@@ -43,7 +43,7 @@ pub trait XmlSerializeRoot {
fn serialize_to_string(&self) -> std::io::Result<String> { fn serialize_to_string(&self) -> std::io::Result<String> {
let mut buf: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into(); let mut buf: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
let mut writer = quick_xml::Writer::new(&mut buf); let mut writer = quick_xml::Writer::new_with_indent(&mut buf, b' ', 4);
self.serialize_root(&mut writer)?; self.serialize_root(&mut writer)?;
Ok(String::from_utf8_lossy(&buf).to_string()) Ok(String::from_utf8_lossy(&buf).to_string())
} }

View File

@@ -22,6 +22,11 @@ fn test_struct_value_tagged() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
out, out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<propfind><prop><test>asd</test></prop></propfind>" r#"<?xml version="1.0" encoding="utf-8"?>
<propfind>
<prop>
<test>asd</test>
</prop>
</propfind>"#
); );
} }

View File

@@ -71,7 +71,11 @@ fn test_struct_value_tagged() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
out, out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><href>okay</href><num>123</num></document>" r#"<?xml version="1.0" encoding="utf-8"?>
<document>
<href>okay</href>
<num>123</num>
</document>"#
); );
} }
@@ -91,7 +95,8 @@ fn test_struct_value_untagged() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
out, out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document>okays</document>" r#"<?xml version="1.0" encoding="utf-8"?>
<document>okays</document>"#
); );
} }
@@ -111,7 +116,11 @@ fn test_struct_vec() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
out, out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><href>okay</href><href>wow</href></document>" r#"<?xml version="1.0" encoding="utf-8"?>
<document>
<href>okay</href>
<href>wow</href>
</document>"#
); );
} }
@@ -141,7 +150,10 @@ fn test_struct_serialize_with() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
out, out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><href>OKAY</href></document>" r#"<?xml version="1.0" encoding="utf-8"?>
<document>
<href>OKAY</href>
</document>"#
); );
} }