Add per-user CardDAV properties + fix per-user CalDAV properties (fixes #2058)

This commit is contained in:
mdecimus
2025-10-02 10:07:22 +02:00
parent d0aef5d5a2
commit 98deb17482
13 changed files with 187 additions and 129 deletions

View File

@@ -80,4 +80,14 @@ impl DavResources {
account_acls
}
pub fn document_ids(&self, is_container: bool) -> impl Iterator<Item = u32> {
self.resources.iter().filter_map(move |resource| {
if resource.is_container() == is_container {
Some(resource.document_id)
} else {
None
}
})
}
}

View File

@@ -100,7 +100,7 @@ impl CalendarMkColRequestHandler for Server {
let mut prop_stat = PropStatBuilder::default();
is_mkcalendar = mkcol.is_mkcalendar;
if !self.apply_calendar_properties(
account_id,
access_token,
&mut calendar,
false,
mkcol.props,

View File

@@ -47,7 +47,7 @@ pub(crate) trait CalendarPropPatchRequestHandler: Sync + Send {
fn apply_calendar_properties(
&self,
account_id: u32,
access_token: &AccessToken,
calendar: &mut Calendar,
is_update: bool,
properties: Vec<DavPropertyValue>,
@@ -149,7 +149,7 @@ impl CalendarPropPatchRequestHandler for Server {
// Remove properties
if !request.set_first && !request.remove.is_empty() {
remove_calendar_properties(
account_id,
access_token,
&mut new_calendar,
std::mem::take(&mut request.remove),
&mut items,
@@ -158,7 +158,7 @@ impl CalendarPropPatchRequestHandler for Server {
// Set properties
is_success = self.apply_calendar_properties(
account_id,
access_token,
&mut new_calendar,
true,
request.set,
@@ -168,7 +168,7 @@ impl CalendarPropPatchRequestHandler for Server {
// Remove properties
if is_success && !request.remove.is_empty() {
remove_calendar_properties(
account_id,
access_token,
&mut new_calendar,
request.remove,
&mut items,
@@ -238,7 +238,7 @@ impl CalendarPropPatchRequestHandler for Server {
fn apply_calendar_properties(
&self,
account_id: u32,
access_token: &AccessToken,
calendar: &mut Calendar,
is_update: bool,
properties: Vec<DavPropertyValue>,
@@ -250,7 +250,7 @@ impl CalendarPropPatchRequestHandler for Server {
match (&property.property, property.value) {
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
if name.len() <= self.core.groupware.live_property_size {
calendar.preferences_mut(account_id).name = name;
calendar.preferences_mut(access_token).name = name;
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
@@ -266,7 +266,7 @@ impl CalendarPropPatchRequestHandler for Server {
DavValue::String(name),
) => {
if name.len() <= self.core.groupware.live_property_size {
calendar.preferences_mut(account_id).description = Some(name);
calendar.preferences_mut(access_token).description = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
@@ -298,13 +298,14 @@ impl CalendarPropPatchRequestHandler for Server {
);
has_errors = true;
} else {
calendar.preferences_mut(account_id).time_zone = Timezone::Custom(ical);
calendar.preferences_mut(access_token).time_zone = Timezone::Custom(ical);
items.insert_ok(property.property);
}
}
(DavProperty::CalDav(CalDavProperty::TimezoneId), DavValue::String(tz_id)) => {
if let Ok(tz) = Tz::from_str(&tz_id) {
calendar.preferences_mut(account_id).time_zone = Timezone::IANA(tz.as_id());
calendar.preferences_mut(access_token).time_zone =
Timezone::IANA(tz.as_id());
items.insert_ok(property.property);
} else {
items.insert_precondition_failed_with_description(
@@ -472,7 +473,7 @@ fn remove_event_properties(
}
fn remove_calendar_properties(
account_id: u32,
access_token: &AccessToken,
calendar: &mut Calendar,
properties: Vec<DavProperty>,
items: &mut PropStatBuilder,
@@ -480,12 +481,12 @@ fn remove_calendar_properties(
for property in properties {
match &property {
DavProperty::CalDav(CalDavProperty::CalendarDescription) => {
calendar.preferences_mut(account_id).description = None;
calendar.preferences_mut(access_token).description = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::CalDav(CalDavProperty::CalendarTimezone)
| DavProperty::CalDav(CalDavProperty::TimezoneId) => {
calendar.preferences_mut(account_id).time_zone = Timezone::Default;
calendar.preferences_mut(access_token).time_zone = Timezone::Default;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::DeadProperty(dead) => {

View File

@@ -18,7 +18,7 @@ use dav_proto::{Depth, RequestHeaders};
use groupware::{
DestroyArchive,
cache::GroupwareCache,
contact::{AddressBook, ContactCard},
contact::{AddressBook, AddressBookPreferences, ContactCard},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
@@ -759,10 +759,17 @@ async fn copy_container(
.caused_by(trc::location!())?;
}
let preference = book.preferences.into_iter().next().unwrap();
book.name = new_name.to_string();
book.subscribers.clear();
book.acls.clear();
book.is_default = false;
book.preferences = vec![AddressBookPreferences {
account_id: to_account_id,
name: preference.name,
description: preference.description,
sort_order: 0,
is_default: false,
}];
let is_overwrite = to_document_id.is_some();
let to_document_id = if let Some(to_document_id) = to_document_id {

View File

@@ -89,7 +89,13 @@ impl CardMkColRequestHandler for Server {
let mut return_prop_stat = None;
if let Some(mkcol) = request {
let mut prop_stat = PropStatBuilder::default();
if !self.apply_addressbook_properties(&mut book, false, mkcol.props, &mut prop_stat) {
if !self.apply_addressbook_properties(
access_token,
&mut book,
false,
mkcol.props,
&mut prop_stat,
) {
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
MkColResponse::new(prop_stat.build())
.with_namespace(Namespace::CardDav)

View File

@@ -45,6 +45,7 @@ pub(crate) trait CardPropPatchRequestHandler: Sync + Send {
fn apply_addressbook_properties(
&self,
access_token: &AccessToken,
address_book: &mut AddressBook,
is_update: bool,
properties: Vec<DavPropertyValue>,
@@ -146,6 +147,7 @@ impl CardPropPatchRequestHandler for Server {
// Remove properties
if !request.set_first && !request.remove.is_empty() {
remove_addressbook_properties(
access_token,
&mut new_book,
std::mem::take(&mut request.remove),
&mut items,
@@ -153,12 +155,22 @@ impl CardPropPatchRequestHandler for Server {
}
// Set properties
is_success =
self.apply_addressbook_properties(&mut new_book, true, request.set, &mut items);
is_success = self.apply_addressbook_properties(
access_token,
&mut new_book,
true,
request.set,
&mut items,
);
// Remove properties
if is_success && !request.remove.is_empty() {
remove_addressbook_properties(&mut new_book, request.remove, &mut items);
remove_addressbook_properties(
access_token,
&mut new_book,
request.remove,
&mut items,
);
}
if is_success {
@@ -224,6 +236,7 @@ impl CardPropPatchRequestHandler for Server {
fn apply_addressbook_properties(
&self,
access_token: &AccessToken,
address_book: &mut AddressBook,
is_update: bool,
properties: Vec<DavPropertyValue>,
@@ -235,7 +248,7 @@ impl CardPropPatchRequestHandler for Server {
match (&property.property, property.value) {
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
if name.len() <= self.core.groupware.live_property_size {
address_book.display_name = Some(name);
address_book.preferences_mut(access_token).name = name;
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
@@ -251,7 +264,7 @@ impl CardPropPatchRequestHandler for Server {
DavValue::String(name),
) => {
if name.len() <= self.core.groupware.live_property_size {
address_book.description = Some(name);
address_book.preferences_mut(access_token).description = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
@@ -418,6 +431,7 @@ fn remove_card_properties(
}
fn remove_addressbook_properties(
access_token: &AccessToken,
book: &mut AddressBook,
properties: Vec<DavProperty>,
items: &mut PropStatBuilder,
@@ -425,13 +439,13 @@ fn remove_addressbook_properties(
for property in properties {
match &property {
DavProperty::CardDav(CardDavProperty::AddressbookDescription) => {
book.description = None;
book.preferences_mut(access_token).description = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::WebDav(WebDavProperty::DisplayName) => {
/*DavProperty::WebDav(WebDavProperty::DisplayName) => {
book.display_name = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
}*/
DavProperty::DeadProperty(dead) => {
book.dead_properties.remove_element(dead);
items.insert_with_status(property, StatusCode::NO_CONTENT);

View File

@@ -8,6 +8,7 @@ use calcard::{
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
vcard::{VCardParameterName, VCardVersion},
};
use common::auth::AccessToken;
use dav_proto::{
Depth, RequestHeaders, Return,
schema::{
@@ -418,13 +419,15 @@ impl<'x> ArchivedResource<'x> {
}
}
pub fn display_name(&self, account_id: u32) -> Option<&str> {
pub fn display_name(&self, access_token: &AccessToken) -> Option<&str> {
match self {
ArchivedResource::Calendar(archive) => {
Some(archive.inner.preferences(account_id).name.as_str())
Some(archive.inner.preferences(access_token).name.as_str())
}
ArchivedResource::CalendarEvent(archive) => archive.inner.display_name.as_deref(),
ArchivedResource::AddressBook(archive) => archive.inner.display_name.as_deref(),
ArchivedResource::AddressBook(archive) => {
Some(archive.inner.preferences(access_token).name.as_str())
}
ArchivedResource::ContactCard(archive) => archive.inner.display_name.as_deref(),
ArchivedResource::FileNode(archive) => archive.inner.display_name.as_deref(),
ArchivedResource::CalendarScheduling(_)

View File

@@ -408,7 +408,6 @@ impl PropFindRequestHandler for Server {
PropFind::Prop(items) => items.clone(),
};
let view_as_id = access_token.primary_id();
let is_scheduling = collection_container == Collection::CalendarScheduling;
'outer: for item in paths {
let account_id = item.account_id;
@@ -491,7 +490,7 @@ impl PropFindRequestHandler for Server {
));
}
WebDavProperty::DisplayName => {
if let Some(name) = archive.display_name(view_as_id) {
if let Some(name) = archive.display_name(access_token) {
fields.push(DavPropertyValue::new(
property.clone(),
DavValue::String(name.to_string()),
@@ -777,11 +776,17 @@ impl PropFindRequestHandler for Server {
(
CardDavProperty::AddressbookDescription,
ArchivedResource::AddressBook(book),
) if book.inner.description.is_some() => {
fields.push(DavPropertyValue::new(
property.clone(),
book.inner.description.as_ref().unwrap().to_string(),
));
) => {
if let Some(desc) =
book.inner.preferences(access_token).description.as_deref()
{
fields.push(DavPropertyValue::new(
property.clone(),
desc.to_string(),
));
} else {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
}
}
(
CardDavProperty::SupportedAddressData,
@@ -844,7 +849,7 @@ impl PropFindRequestHandler for Server {
) => {
if let Some(desc) = calendar
.inner
.preferences(account_id)
.preferences(access_token)
.description
.as_deref()
{
@@ -861,7 +866,7 @@ impl PropFindRequestHandler for Server {
ArchivedResource::Calendar(calendar),
) => {
if let ArchivedTimezone::Custom(tz) =
&calendar.inner.preferences(account_id).time_zone
&calendar.inner.preferences(access_token).time_zone
{
fields.push(DavPropertyValue::new(
property.clone(),
@@ -873,7 +878,7 @@ impl PropFindRequestHandler for Server {
}
(CalDavProperty::TimezoneId, ArchivedResource::Calendar(calendar)) => {
if let ArchivedTimezone::IANA(tz) =
&calendar.inner.preferences(account_id).time_zone
&calendar.inner.preferences(access_token).time_zone
{
fields.push(DavPropertyValue::new(
property.clone(),

View File

@@ -7,7 +7,7 @@
use crate::{
cache::calcard::{build_scheduling_resources, path_from_scheduling, resource_from_scheduling},
calendar::{Calendar, CalendarEvent, CalendarPreferences},
contact::{AddressBook, ContactCard},
contact::{AddressBook, AddressBookPreferences, ContactCard},
file::FileNode,
};
use ahash::AHashSet;
@@ -327,17 +327,19 @@ impl GroupwareCache for Server {
.await?;
AddressBook {
name: name.clone(),
display_name: format!(
"{} ({})",
self.core
.groupware
.default_addressbook_display_name
.as_ref()
.unwrap_or(name),
account_name
)
.into(),
is_default: true,
preferences: vec![AddressBookPreferences {
account_id,
name: format!(
"{} ({})",
self.core
.groupware
.default_addressbook_display_name
.as_ref()
.unwrap_or(name),
account_name
),
..Default::default()
}],
..Default::default()
}
.insert(access_token, account_id, document_id, &mut batch)?;

View File

@@ -12,9 +12,9 @@ pub mod itip;
pub mod storage;
use calcard::icalendar::ICalendar;
use common::DavName;
use common::{DavName, auth::AccessToken};
use dav_proto::schema::request::DeadProperty;
use types::acl::{Acl, AclGrant};
use types::acl::AclGrant;
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
@@ -154,55 +154,12 @@ pub enum Timezone {
Default,
}
pub enum CalendarRight {
ReadFreeBusy,
ReadItems,
WriteAll,
WriteOwn,
UpdatePrivate,
RSVP,
Share,
Delete,
}
impl TryFrom<Acl> for CalendarRight {
type Error = Acl;
fn try_from(value: Acl) -> Result<Self, Self::Error> {
match value {
Acl::SchedulingReadFreeBusy => Ok(CalendarRight::ReadFreeBusy),
Acl::ReadItems => Ok(CalendarRight::ReadItems),
Acl::Modify => Ok(CalendarRight::WriteAll),
Acl::ModifyItemsOwn => Ok(CalendarRight::WriteOwn),
Acl::ModifyPrivateProperties => Ok(CalendarRight::UpdatePrivate),
Acl::SchedulingReply => Ok(CalendarRight::RSVP),
Acl::Administer => Ok(CalendarRight::Share),
Acl::Delete => Ok(CalendarRight::Delete),
_ => Err(value),
}
}
}
impl From<CalendarRight> for Acl {
fn from(value: CalendarRight) -> Self {
match value {
CalendarRight::ReadFreeBusy => Acl::SchedulingReadFreeBusy,
CalendarRight::ReadItems => Acl::ReadItems,
CalendarRight::WriteAll => Acl::Modify,
CalendarRight::WriteOwn => Acl::ModifyItemsOwn,
CalendarRight::UpdatePrivate => Acl::ModifyPrivateProperties,
CalendarRight::RSVP => Acl::SchedulingReply,
CalendarRight::Share => Acl::Administer,
CalendarRight::Delete => Acl::Delete,
}
}
}
impl Calendar {
pub fn preferences(&self, account_id: u32) -> &CalendarPreferences {
pub fn preferences(&self, access_token: &AccessToken) -> &CalendarPreferences {
if self.preferences.len() == 1 {
&self.preferences[0]
} else {
let account_id = access_token.primary_id();
self.preferences
.iter()
.find(|p| p.account_id == account_id)
@@ -211,10 +168,11 @@ impl Calendar {
}
}
pub fn preferences_mut(&mut self, account_id: u32) -> &mut CalendarPreferences {
pub fn preferences_mut(&mut self, access_token: &AccessToken) -> &mut CalendarPreferences {
if self.preferences.len() == 1 {
&mut self.preferences[0]
} else {
let account_id = access_token.primary_id();
let idx = self
.preferences
.iter()
@@ -226,10 +184,11 @@ impl Calendar {
}
impl ArchivedCalendar {
pub fn preferences(&self, account_id: u32) -> &ArchivedCalendarPreferences {
pub fn preferences(&self, access_token: &AccessToken) -> &ArchivedCalendarPreferences {
if self.preferences.len() == 1 {
&self.preferences[0]
} else {
let account_id = access_token.primary_id();
self.preferences
.iter()
.find(|p| p.account_id == account_id)

View File

@@ -21,8 +21,14 @@ impl IndexableObject for AddressBook {
},
IndexValue::Quota {
used: self.dead_properties.size() as u32
+ self.display_name.as_ref().map_or(0, |n| n.len() as u32)
+ self.description.as_ref().map_or(0, |n| n.len() as u32)
+ self
.preferences
.iter()
.map(|p| {
p.name.len() as u32
+ p.description.as_ref().map_or(0, |n| n.len() as u32)
})
.sum::<u32>()
+ self.name.len() as u32,
},
IndexValue::LogContainer {
@@ -46,8 +52,14 @@ impl IndexableObject for &ArchivedAddressBook {
},
IndexValue::Quota {
used: self.dead_properties.size() as u32
+ self.display_name.as_ref().map_or(0, |n| n.len() as u32)
+ self.description.as_ref().map_or(0, |n| n.len() as u32)
+ self
.preferences
.iter()
.map(|p| {
p.name.len() as u32
+ p.description.as_ref().map_or(0, |n| n.len() as u32)
})
.sum::<u32>()
+ self.name.len() as u32,
},
IndexValue::LogContainer {

View File

@@ -8,9 +8,9 @@ pub mod index;
pub mod storage;
use calcard::vcard::VCard;
use common::DavName;
use common::{DavName, auth::AccessToken};
use dav_proto::schema::request::DeadProperty;
use types::acl::{Acl, AclGrant};
use types::acl::AclGrant;
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
@@ -18,10 +18,7 @@ use types::acl::{Acl, AclGrant};
#[rkyv(derive(Debug))]
pub struct AddressBook {
pub name: String,
pub display_name: Option<String>,
pub description: Option<String>,
pub sort_order: u32,
pub is_default: bool,
pub preferences: Vec<AddressBookPreferences>,
pub subscribers: Vec<u32>,
pub dead_properties: DeadProperty,
pub acls: Vec<AclGrant>,
@@ -29,11 +26,16 @@ pub struct AddressBook {
pub modified: i64,
}
pub enum AddressBookRight {
Read,
Write,
Share,
Delete,
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
#[rkyv(derive(Debug))]
pub struct AddressBookPreferences {
pub account_id: u32,
pub name: String,
pub description: Option<String>,
pub sort_order: u32,
pub is_default: bool,
}
#[derive(
@@ -49,27 +51,46 @@ pub struct ContactCard {
pub size: u32,
}
impl TryFrom<Acl> for AddressBookRight {
type Error = Acl;
impl AddressBook {
pub fn preferences(&self, access_token: &AccessToken) -> &AddressBookPreferences {
if self.preferences.len() == 1 {
&self.preferences[0]
} else {
let account_id = access_token.primary_id();
self.preferences
.iter()
.find(|p| p.account_id == account_id)
.or_else(|| self.preferences.first())
.unwrap()
}
}
fn try_from(value: Acl) -> Result<Self, Self::Error> {
match value {
Acl::Read => Ok(AddressBookRight::Read),
Acl::Modify => Ok(AddressBookRight::Write),
Acl::Administer => Ok(AddressBookRight::Share),
Acl::Delete => Ok(AddressBookRight::Delete),
_ => Err(value),
pub fn preferences_mut(&mut self, access_token: &AccessToken) -> &mut AddressBookPreferences {
if self.preferences.len() == 1 {
&mut self.preferences[0]
} else {
let account_id = access_token.primary_id();
let idx = self
.preferences
.iter()
.position(|p| p.account_id == account_id)
.unwrap_or(0);
&mut self.preferences[idx]
}
}
}
impl From<AddressBookRight> for Acl {
fn from(value: AddressBookRight) -> Self {
match value {
AddressBookRight::Read => Acl::Read,
AddressBookRight::Write => Acl::Modify,
AddressBookRight::Share => Acl::Administer,
AddressBookRight::Delete => Acl::Delete,
impl ArchivedAddressBook {
pub fn preferences(&self, access_token: &AccessToken) -> &ArchivedAddressBookPreferences {
if self.preferences.len() == 1 {
&self.preferences[0]
} else {
let account_id = access_token.primary_id();
self.preferences
.iter()
.find(|p| p.account_id == account_id)
.or_else(|| self.preferences.first())
.unwrap()
}
}
}

View File

@@ -46,6 +46,24 @@ const LOCK_WAIT_TIME_ACCOUNT: u64 = 3 * 60;
const LOCK_WAIT_TIME_CORE: u64 = 5 * 60;
const LOCK_RETRY_TIME: Duration = Duration::from_secs(30);
/*
pub struct AddressBook {
pub name: String,
pub display_name: Option<String>,
pub description: Option<String>,
pub sort_order: u32,
pub is_default: bool,
pub subscribers: Vec<u32>,
pub dead_properties: DeadProperty,
pub acls: Vec<AclGrant>,
pub created: i64,
pub modified: i64,
}
*/
pub async fn try_migrate(server: &Server) -> trc::Result<()> {
if let Some(version) = std::env::var("FORCE_MIGRATE_QUEUE")
.ok()