Compare commits

..

1 Commits

Author SHA1 Message Date
Lennart
cc384b6124 Merge pull request #97 from lennart-k/feature/sharing
Fix issues with group collections
2025-07-18 14:14:23 +02:00
163 changed files with 5831 additions and 5228 deletions

20
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Rust CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose --workspace

View File

@@ -1,57 +0,0 @@
name: "CICD"
on: [push, pull_request]
permissions:
contents: read
pull-requests: write
env:
CARGO_TERM_COLOR: always
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- run: rustup update
- name: Checkout sources
uses: actions/checkout@v4
- run: cargo check
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- run: rustup update
- name: Checkout sources
uses: actions/checkout@v4
- run: cargo test --all-features --verbose --workspace
coverage:
name: Test Coverage
runs-on: ubuntu-latest
steps:
- run: rustup update
- name: Install tarpaulin
run: cargo install cargo-tarpaulin
- name: Checkout sources
uses: actions/checkout@v4
- name: Run tarpaulin
run: cargo tarpaulin --workspace --all-features --exclude xml_derive --coveralls ${{ secrets.COVERALLS_REPO_TOKEN }}
lints:
name: Lints
runs-on: ubuntu-latest
steps:
- run: rustup update
- run: rustup component add rustfmt clippy
- name: Checkout sources
uses: actions/checkout@v4
- name: Run cargo fmt
run: cargo fmt --all -- --check
- name: Run cargo clippy
run: cargo clippy -- -D warnings

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ?\n WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "46ae176a06e314492f661c28436d6370883052c854da43475d7ced60cf8326e3"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO calendars (principal, id, displayname, description, \"order\", color, subscription_url, timezone, timezone_id, push_topic, comp_event, comp_todo, comp_journal)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "5132ee8198f155242aa332a10019c48ec334884bcf7841c8aa03fd5eb11351d9"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO calendars (principal, id, displayname, description, \"order\", color, subscription_url, timezone_id, push_topic, comp_event, comp_todo, comp_journal)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "60b940ff493e7c0fcb2ffe8ae97172c6444525ffeec21b194bd7443d11d06113"
}

View File

@@ -39,39 +39,44 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "timezone_id", "name": "timezone",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "deleted_at", "name": "timezone_id",
"ordinal": 8, "ordinal": 8,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 9,
"type_info": "Datetime" "type_info": "Datetime"
}, },
{ {
"name": "subscription_url", "name": "subscription_url",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "push_topic",
"ordinal": 10, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "comp_event", "name": "push_topic",
"ordinal": 11, "ordinal": 11,
"type_info": "Bool" "type_info": "Text"
}, },
{ {
"name": "comp_todo", "name": "comp_event",
"ordinal": 12, "ordinal": 12,
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"name": "comp_journal", "name": "comp_todo",
"ordinal": 13, "ordinal": 13,
"type_info": "Bool" "type_info": "Bool"
},
{
"name": "comp_journal",
"ordinal": 14,
"type_info": "Bool"
} }
], ],
"parameters": { "parameters": {
@@ -88,6 +93,7 @@
true, true,
true, true,
true, true,
true,
false, false,
false, false,
false, false,

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT principal, id, displayname, \"order\", description, color, timezone_id, deleted_at, synctoken, subscription_url, push_topic, comp_event, comp_todo, comp_journal\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL", "query": "SELECT *\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -14,14 +14,14 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "displayname", "name": "synctoken",
"ordinal": 2, "ordinal": 2,
"type_info": "Text" "type_info": "Integer"
}, },
{ {
"name": "order", "name": "displayname",
"ordinal": 3, "ordinal": 3,
"type_info": "Integer" "type_info": "Text"
}, },
{ {
"name": "description", "name": "description",
@@ -29,49 +29,54 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "color", "name": "order",
"ordinal": 5, "ordinal": 5,
"type_info": "Text" "type_info": "Integer"
}, },
{ {
"name": "timezone_id", "name": "color",
"ordinal": 6, "ordinal": 6,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "deleted_at", "name": "timezone",
"ordinal": 7, "ordinal": 7,
"type_info": "Datetime"
},
{
"name": "synctoken",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "subscription_url",
"ordinal": 9,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "push_topic", "name": "timezone_id",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "subscription_url",
"ordinal": 10, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "comp_event", "name": "push_topic",
"ordinal": 11, "ordinal": 11,
"type_info": "Bool" "type_info": "Text"
}, },
{ {
"name": "comp_todo", "name": "comp_event",
"ordinal": 12, "ordinal": 12,
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"name": "comp_journal", "name": "comp_todo",
"ordinal": 13, "ordinal": 13,
"type_info": "Bool" "type_info": "Bool"
},
{
"name": "comp_journal",
"ordinal": 14,
"type_info": "Bool"
} }
], ],
"parameters": { "parameters": {
@@ -80,13 +85,14 @@
"nullable": [ "nullable": [
false, false,
false, false,
false,
true,
true, true,
false, false,
true, true,
true, true,
true, true,
true, true,
false,
true, true,
false, false,
false, false,
@@ -94,5 +100,5 @@
false false
] ]
}, },
"hash": "27ac68a4eea40c1cac663cad034028cf6c373354b29e3a5290c18f58101913cd" "hash": "cce62f7829bd688cd8c7928b587bc31f0e50865c214b1df113350bea2c254237"
} }

View File

@@ -39,39 +39,44 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "timezone_id", "name": "timezone",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "deleted_at", "name": "timezone_id",
"ordinal": 8, "ordinal": 8,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 9,
"type_info": "Datetime" "type_info": "Datetime"
}, },
{ {
"name": "subscription_url", "name": "subscription_url",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "push_topic",
"ordinal": 10, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "comp_event", "name": "push_topic",
"ordinal": 11, "ordinal": 11,
"type_info": "Bool" "type_info": "Text"
}, },
{ {
"name": "comp_todo", "name": "comp_event",
"ordinal": 12, "ordinal": 12,
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"name": "comp_journal", "name": "comp_todo",
"ordinal": 13, "ordinal": 13,
"type_info": "Bool" "type_info": "Bool"
},
{
"name": "comp_journal",
"ordinal": 14,
"type_info": "Bool"
} }
], ],
"parameters": { "parameters": {
@@ -88,6 +93,7 @@
true, true,
true, true,
true, true,
true,
false, false,
false, false,
false, false,

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ?\n WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 14
},
"nullable": []
},
"hash": "d65c9c40606e59dd816a51b9b9ac60fd2ff81aaa358fcc038134e9a68ba45ad7"
}

1124
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,9 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.9.12" version = "0.4.13"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
documentation = "https://lennart-k.github.io/rustical/"
repository = "https://github.com/lennart-k/rustical" repository = "https://github.com/lennart-k/rustical"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
@@ -17,7 +16,7 @@ description.workspace = true
repository.workspace = true repository.workspace = true
license.workspace = true license.workspace = true
resolver = "2" resolver = "2"
publish = true publish = false
[features] [features]
debug = ["opentelemetry"] debug = ["opentelemetry"]
@@ -49,7 +48,7 @@ rand_core = { version = "0.9", features = ["std"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
regex = "1.10" regex = "1.10"
lazy_static = "1.5" lazy_static = "1.5"
rstest = "0.26" rstest = "0.25"
rstest_reuse = "0.7" rstest_reuse = "0.7"
sha2 = "0.10" sha2 = "0.10"
tokio = { version = "1", features = [ tokio = { version = "1", features = [
@@ -62,7 +61,7 @@ tokio = { version = "1", features = [
url = "2.5" url = "2.5"
base64 = "0.22" base64 = "0.22"
thiserror = "2.0" thiserror = "2.0"
quick-xml = { version = "0.38" } quick-xml = { version = "0.37" }
rust-embed = "8.5" rust-embed = "8.5"
tower-sessions = "0.14" tower-sessions = "0.14"
futures-core = "0.3.31" futures-core = "0.3.31"
@@ -96,12 +95,8 @@ strum = "0.27"
strum_macros = "0.27" strum_macros = "0.27"
serde_json = { version = "1.0", features = ["raw_value"] } serde_json = { version = "1.0", features = ["raw_value"] }
sqlx-sqlite = { version = "0.8", features = ["bundled"] } sqlx-sqlite = { version = "0.8", features = ["bundled"] }
ical = { git = "https://github.com/lennart-k/ical-rs", features = [ ical = { version = "0.11", features = ["generator", "serde"] }
"generator", toml = "0.8"
"serde",
"chrono-tz",
] }
toml = "0.9"
tower = "0.5" tower = "0.5"
tower-http = { version = "0.6", features = [ tower-http = { version = "0.6", features = [
"trace", "trace",
@@ -131,7 +126,7 @@ syn = { version = "2.0", features = ["full"] }
quote = "1.0" quote = "1.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"
heck = "0.5" heck = "0.5"
darling = "0.21" darling = "0.20"
reqwest = { version = "0.12", features = [ reqwest = { version = "0.12", features = [
"rustls-tls", "rustls-tls",
"charset", "charset",
@@ -140,7 +135,6 @@ reqwest = { version = "0.12", features = [
openidconnect = "4.0" openidconnect = "4.0"
clap = { version = "4.5", features = ["derive", "env"] } clap = { version = "4.5", features = ["derive", "env"] }
matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "f0591d13" } matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "f0591d13" }
vtimezones-rs = "0.2"
ece = { version = "2.3", default-features = false, features = [ ece = { version = "2.3", default-features = false, features = [
"backend-openssl", "backend-openssl",
] } ] }
@@ -164,15 +158,15 @@ async-trait = { workspace = true }
uuid.workspace = true uuid.workspace = true
axum.workspace = true axum.workspace = true
opentelemetry = { version = "0.31", optional = true } opentelemetry = { version = "0.30", optional = true }
opentelemetry-otlp = { version = "0.31", optional = true, features = [ opentelemetry-otlp = { version = "0.30", optional = true, features = [
"grpc-tonic", "grpc-tonic",
] } ] }
opentelemetry_sdk = { version = "0.31", features = [ opentelemetry_sdk = { version = "0.30", features = [
"rt-tokio", "rt-tokio",
], optional = true } ], optional = true }
opentelemetry-semantic-conventions = { version = "0.31", optional = true } opentelemetry-semantic-conventions = { version = "0.30", optional = true }
tracing-opentelemetry = { version = "0.32", optional = true } tracing-opentelemetry = { version = "0.31", optional = true }
tracing-subscriber = { version = "0.3", features = [ tracing-subscriber = { version = "0.3", features = [
"env-filter", "env-filter",
"fmt", "fmt",

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM rust:1.90-alpine AS chef FROM --platform=$BUILDPLATFORM rust:1.88-alpine AS chef
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG BUILDPLATFORM ARG BUILDPLATFORM
@@ -45,5 +45,4 @@ CMD ["/usr/local/bin/rustical"]
ENV RUSTICAL_DATA_STORE__SQLITE__DB_URL=/var/lib/rustical/db.sqlite3 ENV RUSTICAL_DATA_STORE__SQLITE__DB_URL=/var/lib/rustical/db.sqlite3
LABEL org.opencontainers.image.authors="Lennart K github.com/lennart-k" LABEL org.opencontainers.image.authors="Lennart K github.com/lennart-k"
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
EXPOSE 4000 EXPOSE 4000

View File

@@ -12,6 +12,3 @@ docs:
docs-dev: docs-dev:
mkdocs serve mkdocs serve
coverage:
cargo tarpaulin --workspace --exclude xml_derive

View File

@@ -4,23 +4,21 @@ a CalDAV/CardDAV server
> [!WARNING] > [!WARNING]
RustiCal is under **active development**! RustiCal is under **active development**!
While I've been successfully using RustiCal productively for some months now and there seems to be a growing user base, While I've been successfully using RustiCal productively for a few weeks now,
you'd still be one of the first testers so expect bugs and rough edges. you'd still be one of the first testers so expect bugs and rough edges.
If you still want to use it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
## Features ## Features
- easy to backup, everything saved in one SQLite database - easy to backup, everything saved in one SQLite database
- also export feature in the frontend - also export feature in the frontend
- Import your existing calendars in the frontend - [WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5
- **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5
- lightweight (the container image contains only one binary) - lightweight (the container image contains only one binary)
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks) - adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
- deleted calendars are recoverable - deleted calendars are recoverable
- Nextcloud login flow (In DAVx5 you can login through the Nextcloud flow and automatically generate an app token) - Nextcloud login flow (In DAVx5 you can login through the Nextcloud flow and automatically generate an app token)
- Apple configuration profiles (skip copy-pasting passwords and instead generate the configuration in the frontend) - Apple configuration profiles (skip copy-pasting passwords and instead generate the configuration in the frontend)
- **OpenID Connect** support (with option to disable password login) - OpenID Connect support (with option to disable password login)
- Group-based **sharing**
## Getting Started ## Getting Started

View File

@@ -7,7 +7,6 @@ accepted = [
"CDLA-Permissive-2.0", "CDLA-Permissive-2.0",
"Zlib", "Zlib",
"AGPL-3.0", "AGPL-3.0",
"GPL-3.0",
"MPL-2.0", "MPL-2.0",
] ]
workarounds = ["ring", "chrono", "rustls"] workarounds = ["ring", "chrono", "rustls"]

View File

@@ -1,22 +0,0 @@
services:
rustical:
image: ghcr.io/lennart-k/rustical:latest
restart: unless-stopped
environment:
RUSTICAL_FRONTEND__ALLOW_PASSWORD_LOGIN: "false"
RUSTICAL_OIDC__NAME: "Authelia"
RUSTICAL_OIDC__ISSUER: "https://auth.example.com"
RUSTICAL_OIDC__CLIENT_ID: "{{ rustical_oidc_client_id }}"
RUSTICAL_OIDC__CLIENT_SECRET: "{{ rustical_oidc_client_secret }}"
RUSTICAL_OIDC__CLAIM_USERID: "preferred_username"
RUSTICAL_OIDC__SCOPES: '["openid", "profile", "groups"]'
RUSTICAL_OIDC__REQUIRE_GROUP: "app:rustical" # optional
RUSTICAL_OIDC__ALLOW_SIGN_UP: "true"
volumes:
- data:/var/lib/rustical
# Here you probably want to you expose instead
ports:
- 4000:4000
volumes:
data:

View File

@@ -11,7 +11,6 @@ publish = false
rustical_store_sqlite = { workspace = true, features = ["test"] } rustical_store_sqlite = { workspace = true, features = ["test"] }
rstest.workspace = true rstest.workspace = true
async-std.workspace = true async-std.workspace = true
serde_json.workspace = true
[dependencies] [dependencies]
axum.workspace = true axum.workspace = true
@@ -43,4 +42,3 @@ headers.workspace = true
tower-http.workspace = true tower-http.workspace = true
strum.workspace = true strum.workspace = true
strum_macros.workspace = true strum_macros.workspace = true
vtimezones-rs.workspace = true

View File

@@ -8,7 +8,7 @@ use http::{HeaderValue, Method, StatusCode, header};
use ical::generator::{Emitter, IcalCalendarBuilder}; use ical::generator::{Emitter, IcalCalendarBuilder};
use ical::property::Property; use ical::property::Property;
use percent_encoding::{CONTROLS, utf8_percent_encode}; use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_ical::{CalendarObjectComponent, EventObject}; use rustical_ical::{CalendarObjectComponent, EventObject, JournalObject, TodoObject};
use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal}; use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
use std::collections::HashMap; use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
@@ -32,76 +32,58 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }
let calendar = cal_store
.get_calendar(&principal, &calendar_id, true)
.await?;
let mut timezones = HashMap::new(); let mut timezones = HashMap::new();
let mut vtimezones = HashMap::new();
let objects = cal_store.get_objects(&principal, &calendar_id).await?; let objects = cal_store.get_objects(&principal, &calendar_id).await?;
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0") let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
.gregorian() .gregorian()
.prodid("RustiCal"); .prodid("RustiCal");
if let Some(displayname) = calendar.meta.displayname { if calendar.displayname.is_some() {
ical_calendar_builder = ical_calendar_builder.set(Property { ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALNAME".to_owned(), name: "X-WR-CALNAME".to_owned(),
value: Some(displayname), value: calendar.displayname,
params: None, params: None,
}); });
} }
if let Some(description) = calendar.meta.description { if calendar.description.is_some() {
ical_calendar_builder = ical_calendar_builder.set(Property { ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALDESC".to_owned(), name: "X-WR-CALDESC".to_owned(),
value: Some(description), value: calendar.description,
params: None, params: None,
}); });
} }
if let Some(timezone_id) = calendar.timezone_id { if calendar.timezone_id.is_some() {
ical_calendar_builder = ical_calendar_builder.set(Property { ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-TIMEZONE".to_owned(), name: "X-WR-TIMEZONE".to_owned(),
value: Some(timezone_id), value: calendar.timezone_id,
params: None, params: None,
}); });
} }
let mut ical_calendar = ical_calendar_builder.build();
for object in &objects { for object in &objects {
vtimezones.extend(object.get_vtimezones());
match object.get_data() { match object.get_data() {
CalendarObjectComponent::Event( CalendarObjectComponent::Event(EventObject {
EventObject { event,
event, timezones: object_timezones,
timezones: object_timezones, ..
.. }) => {
},
overrides,
) => {
timezones.extend(object_timezones); timezones.extend(object_timezones);
ical_calendar_builder = ical_calendar_builder.add_event(event.clone()); ical_calendar.events.push(event.clone());
for _override in overrides {
ical_calendar_builder =
ical_calendar_builder.add_event(_override.event.clone());
}
} }
CalendarObjectComponent::Todo(todo, overrides) => { CalendarObjectComponent::Todo(TodoObject { todo, .. }) => {
ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone()); ical_calendar.todos.push(todo.clone());
for _override in overrides {
ical_calendar_builder = ical_calendar_builder.add_todo(_override.clone());
}
} }
CalendarObjectComponent::Journal(journal, overrides) => { CalendarObjectComponent::Journal(JournalObject { journal, .. }) => {
ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone()); ical_calendar.journals.push(journal.clone());
for _override in overrides {
ical_calendar_builder = ical_calendar_builder.add_journal(_override.clone());
}
} }
} }
} }
for vtimezone in vtimezones.into_values() {
ical_calendar_builder = ical_calendar_builder.add_tz(vtimezone.to_owned());
}
let ical_calendar = ical_calendar_builder
.build()
.map_err(|parser_error| Error::IcalError(parser_error.into()))?;
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/calendar").unwrap()); hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());

View File

@@ -1,110 +0,0 @@
use crate::Error;
use crate::calendar::CalendarResourceService;
use axum::{
extract::{Path, State},
response::{IntoResponse, Response},
};
use http::StatusCode;
use ical::{
generator::Emitter,
parser::{Component, ComponentMut},
};
use rustical_dav::header::Overwrite;
use rustical_ical::{CalendarObject, CalendarObjectType};
use rustical_store::{
Calendar, CalendarMetadata, CalendarStore, SubscriptionStore, auth::Principal,
};
use std::io::BufReader;
use tracing::instrument;
#[instrument(skip(resource_service))]
pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
Path((principal, cal_id)): Path<(String, String)>,
user: Principal,
State(resource_service): State<CalendarResourceService<C, S>>,
Overwrite(overwrite): Overwrite,
body: String,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(Error::Unauthorized);
}
let mut parser = ical::IcalParser::new(BufReader::new(body.as_bytes()));
let mut cal = parser
.next()
.expect("input must contain calendar")
.unwrap()
.mutable();
if parser.next().is_some() {
return Err(rustical_ical::Error::InvalidData(
"multiple calendars, only one allowed".to_owned(),
)
.into());
}
// Extract calendar metadata
let displayname = cal
.get_property("X-WR-CALNAME")
.and_then(|prop| prop.value.to_owned());
let description = cal
.get_property("X-WR-CALDESC")
.and_then(|prop| prop.value.to_owned());
let timezone_id = cal
.get_property("X-WR-TIMEZONE")
.and_then(|prop| prop.value.to_owned());
// These properties should not appear in the expanded calendar objects
cal.remove_property("X-WR-CALNAME");
cal.remove_property("X-WR-CALDESC");
cal.remove_property("X-WR-TIMEZONE");
let cal = cal.verify().unwrap();
// Make sure timezone is valid
if let Some(timezone_id) = timezone_id.as_ref() {
assert!(
vtimezones_rs::VTIMEZONES.contains_key(timezone_id),
"Invalid calendar timezone id"
);
}
// Extract necessary component types
let mut cal_components = vec![];
if !cal.events.is_empty() {
cal_components.push(CalendarObjectType::Event);
}
if !cal.journals.is_empty() {
cal_components.push(CalendarObjectType::Journal);
}
if !cal.todos.is_empty() {
cal_components.push(CalendarObjectType::Todo);
}
let expanded_cals = cal.expand_calendar();
// Janky way to convert between IcalCalendar and CalendarObject
let objects = expanded_cals
.into_iter()
.map(|cal| cal.generate())
.map(CalendarObject::from_ics)
.collect::<Result<Vec<_>, _>>()?;
let new_cal = Calendar {
principal,
id: cal_id,
meta: CalendarMetadata {
displayname,
order: 0,
description,
color: None,
},
timezone_id,
deleted_at: None,
synctoken: 0,
subscription_url: None,
push_topic: uuid::Uuid::new_v4().to_string(),
components: cal_components,
};
let cal_store = resource_service.cal_store;
cal_store
.import_calendar(new_cal, objects, overwrite)
.await?;
Ok(StatusCode::OK.into_response())
}

View File

@@ -4,11 +4,10 @@ use crate::calendar::prop::SupportedCalendarComponentSet;
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use http::{Method, StatusCode}; use http::{Method, StatusCode};
use ical::IcalParser;
use rustical_dav::xml::HrefElement; use rustical_dav::xml::HrefElement;
use rustical_ical::CalendarObjectType; use rustical_ical::CalendarObjectType;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
use rustical_store::{Calendar, CalendarMetadata, CalendarStore, SubscriptionStore}; use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag}; use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument; use tracing::instrument;
@@ -46,7 +45,7 @@ pub struct PropElement {
} }
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
#[xml(root = "mkcalendar")] #[xml(root = b"mkcalendar")]
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
struct MkcalendarRequest { struct MkcalendarRequest {
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
@@ -54,7 +53,7 @@ struct MkcalendarRequest {
} }
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
#[xml(root = "mkcol")] #[xml(root = b"mkcol")]
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
struct MkcolRequest { struct MkcolRequest {
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
@@ -83,42 +82,15 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
request.displayname = None request.displayname = None
} }
let timezone_id = if let Some(tzid) = request.calendar_timezone_id {
Some(tzid)
} else if let Some(tz) = request.calendar_timezone {
// TODO: Proper error (calendar-timezone precondition)
let calendar = IcalParser::new(tz.as_bytes())
.next()
.ok_or(rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
))?
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
let timezone = calendar
.timezones
.first()
.ok_or(rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
))?;
let timezone: chrono_tz::Tz = timezone
.try_into()
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
Some(timezone.name().to_owned())
} else {
None
};
let calendar = Calendar { let calendar = Calendar {
id: cal_id.to_owned(), id: cal_id.to_owned(),
principal: principal.to_owned(), principal: principal.to_owned(),
meta: CalendarMetadata { order: request.calendar_order.unwrap_or(0),
order: request.calendar_order.unwrap_or(0), displayname: request.displayname,
displayname: request.displayname, timezone: request.calendar_timezone,
color: request.calendar_color, timezone_id: request.calendar_timezone_id,
description: request.calendar_description, color: request.calendar_color,
}, description: request.calendar_description,
timezone_id,
deleted_at: None, deleted_at: None,
synctoken: 0, synctoken: 0,
subscription_url: request.source.map(|href| href.href), subscription_url: request.source.map(|href| href.href),

View File

@@ -1,5 +1,4 @@
pub mod get; pub mod get;
pub mod import;
pub mod mkcalendar; pub mod mkcalendar;
pub mod post; pub mod post;
pub mod report; pub mod report;

View File

@@ -1,7 +1,7 @@
use crate::calendar_object::CalendarObjectPropWrapperName; use crate::{Error, calendar_object::CalendarObjectPropWrapperName};
use rustical_dav::xml::PropfindType; use rustical_dav::xml::PropfindType;
use rustical_ical::{CalendarObject, UtcDateTime}; use rustical_ical::{CalendarObject, UtcDateTime};
use rustical_store::calendar_store::CalendarQuery; use rustical_store::{CalendarStore, calendar_store::CalendarQuery};
use rustical_xml::XmlDeserialize; use rustical_xml::XmlDeserialize;
use std::ops::Deref; use std::ops::Deref;
@@ -16,42 +16,36 @@ pub(crate) struct TimeRangeElement {
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
#[allow(dead_code)] #[allow(dead_code)]
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.3 struct ParamFilterElement {
pub struct ParamFilterElement {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) is_not_defined: Option<()>, is_not_defined: Option<()>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) text_match: Option<TextMatchElement>, text_match: Option<TextMatchElement>,
#[xml(ty = "attr")] #[xml(ty = "attr")]
pub(crate) name: String, name: String,
} }
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct TextMatchElement { struct TextMatchElement {
#[xml(ty = "attr")] #[xml(ty = "attr")]
pub(crate) collation: String, collation: String,
#[xml(ty = "attr")] #[xml(ty = "attr")]
// "yes" or "no", default: "no" negate_collation: String,
pub(crate) negate_condition: Option<String>,
} }
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
#[allow(dead_code)] #[allow(dead_code)]
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
pub(crate) struct PropFilterElement { pub(crate) struct PropFilterElement {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) is_not_defined: Option<()>, is_not_defined: Option<()>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) time_range: Option<TimeRangeElement>, time_range: Option<TimeRangeElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) text_match: Option<TextMatchElement>, text_match: Option<TextMatchElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)] #[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
pub(crate) param_filter: Vec<ParamFilterElement>, param_filter: Vec<ParamFilterElement>,
#[xml(ty = "attr")]
pub(crate) name: String,
} }
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
@@ -67,7 +61,7 @@ pub(crate) struct CompFilterElement {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)] #[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
pub(crate) comp_filter: Vec<CompFilterElement>, pub(crate) comp_filter: Vec<CompFilterElement>,
#[xml(ty = "attr")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV", ty = "attr")]
pub(crate) name: String, pub(crate) name: String,
} }
@@ -116,17 +110,19 @@ impl CompFilterElement {
// TODO: Implement prop-filter (and comp-filter?) at some point // TODO: Implement prop-filter (and comp-filter?) at some point
if let Some(time_range) = &self.time_range { if let Some(time_range) = &self.time_range {
if let Some(start) = &time_range.start if let Some(start) = &time_range.start {
&& let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None) if let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None) {
&& start.deref() > &last_occurence.utc() if start.deref() > &last_occurence.utc() {
{ return false;
return false; }
};
} }
if let Some(end) = &time_range.end if let Some(end) = &time_range.end {
&& let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None) if let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None) {
&& end.deref() < &first_occurence.utc() if end.deref() < &first_occurence.utc() {
{ return false;
return false; }
};
} }
} }
true true
@@ -154,15 +150,15 @@ impl From<&FilterElement> for CalendarQuery {
for comp_filter in comp_filter_vcalendar.comp_filter.iter() { for comp_filter in comp_filter_vcalendar.comp_filter.iter() {
// A calendar object cannot contain both VEVENT and VTODO, so we only have to handle // A calendar object cannot contain both VEVENT and VTODO, so we only have to handle
// whatever we get first // whatever we get first
if matches!(comp_filter.name.as_str(), "VEVENT" | "VTODO") if matches!(comp_filter.name.as_str(), "VEVENT" | "VTODO") {
&& let Some(time_range) = &comp_filter.time_range if let Some(time_range) = &comp_filter.time_range {
{ let start = time_range.start.as_ref().map(|start| start.date_naive());
let start = time_range.start.as_ref().map(|start| start.date_naive()); let end = time_range.end.as_ref().map(|end| end.date_naive());
let end = time_range.end.as_ref().map(|end| end.date_naive()); return CalendarQuery {
return CalendarQuery { time_start: start,
time_start: start, time_end: end,
time_end: end, };
}; }
} }
} }
Default::default() Default::default()
@@ -192,3 +188,18 @@ impl From<&CalendarQueryRequest> for CalendarQuery {
.unwrap_or_default() .unwrap_or_default()
} }
} }
pub async fn get_objects_calendar_query<C: CalendarStore>(
cal_query: &CalendarQueryRequest,
principal: &str,
cal_id: &str,
store: &C,
) -> Result<Vec<CalendarObject>, Error> {
let mut objects = store
.calendar_query(principal, cal_id, cal_query.into())
.await?;
if let Some(filter) = &cal_query.filter {
objects.retain(|object| filter.matches(object));
}
Ok(objects)
}

View File

@@ -1,120 +0,0 @@
use crate::Error;
use rustical_ical::CalendarObject;
use rustical_store::CalendarStore;
mod elements;
pub(crate) use elements::*;
pub async fn get_objects_calendar_query<C: CalendarStore>(
cal_query: &CalendarQueryRequest,
principal: &str,
cal_id: &str,
store: &C,
) -> Result<Vec<CalendarObject>, Error> {
let mut objects = store
.calendar_query(principal, cal_id, cal_query.into())
.await?;
if let Some(filter) = &cal_query.filter {
objects.retain(|object| filter.matches(object));
}
Ok(objects)
}
#[cfg(test)]
mod tests {
use rustical_dav::xml::PropElement;
use rustical_xml::XmlDocument;
use crate::{
calendar::methods::report::{
ReportRequest,
calendar_query::{
CalendarQueryRequest, CompFilterElement, FilterElement, ParamFilterElement,
PropFilterElement, TextMatchElement,
},
},
calendar_object::{CalendarObjectPropName, CalendarObjectPropWrapperName},
};
#[test]
fn calendar_query_7_8_7() {
const INPUT: &str = r#"
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop xmlns:D="DAV:">
<D:getetag/>
<C:calendar-data/>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="ATTENDEE">
<C:text-match collation="i;ascii-casemap">mailto:lisa@example.com</C:text-match>
<C:param-filter name="PARTSTAT">
<C:text-match collation="i;ascii-casemap">NEEDS-ACTION</C:text-match>
</C:param-filter>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
let report = ReportRequest::parse_str(INPUT).unwrap();
let calendar_query: CalendarQueryRequest =
if let ReportRequest::CalendarQuery(query) = report {
query
} else {
panic!()
};
assert_eq!(
calendar_query,
CalendarQueryRequest {
prop: rustical_dav::xml::PropfindType::Prop(PropElement(
vec![
CalendarObjectPropWrapperName::CalendarObject(
CalendarObjectPropName::Getetag,
),
CalendarObjectPropWrapperName::CalendarObject(
CalendarObjectPropName::CalendarData(Default::default())
),
],
vec![]
)),
filter: Some(FilterElement {
comp_filter: CompFilterElement {
is_not_defined: None,
time_range: None,
prop_filter: vec![],
comp_filter: vec![CompFilterElement {
prop_filter: vec![PropFilterElement {
name: "ATTENDEE".to_owned(),
text_match: Some(TextMatchElement {
collation: "i;ascii-casemap".to_owned(),
negate_condition: None
}),
is_not_defined: None,
param_filter: vec![ParamFilterElement {
is_not_defined: None,
name: "PARTSTAT".to_owned(),
text_match: Some(TextMatchElement {
collation: "i;ascii-casemap".to_owned(),
negate_condition: None
}),
}],
time_range: None
}],
comp_filter: vec![],
is_not_defined: None,
name: "VEVENT".to_owned(),
time_range: None
}],
name: "VCALENDAR".to_owned()
}
}),
timezone: None,
timezone_id: None
}
)
}
}

View File

@@ -4,6 +4,3 @@ pub mod resource;
mod service; mod service;
pub use service::CalendarResourceService; pub use service::CalendarResourceService;
#[cfg(test)]
pub mod tests;

View File

@@ -3,7 +3,6 @@ use crate::Error;
use crate::calendar::prop::ReportMethod; use crate::calendar::prop::ReportMethod;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use derive_more::derive::{From, Into}; use derive_more::derive::{From, Into};
use ical::IcalParser;
use rustical_dav::extensions::{ use rustical_dav::extensions::{
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp, CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
}; };
@@ -16,7 +15,7 @@ use rustical_store::Calendar;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
use rustical_xml::{EnumVariants, PropName}; use rustical_xml::{EnumVariants, PropName};
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use serde::Deserialize; use std::str::FromStr;
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarPropName")] #[xml(unit_variants_ident = "CalendarPropName")]
@@ -35,7 +34,7 @@ pub enum CalendarProp {
CalendarTimezoneId(Option<String>), CalendarTimezoneId(Option<String>),
#[xml(ns = "rustical_dav::namespace::NS_ICAL")] #[xml(ns = "rustical_dav::namespace::NS_ICAL")]
CalendarOrder(Option<i64>), CalendarOrder(Option<i64>),
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
SupportedCalendarComponentSet(SupportedCalendarComponentSet), SupportedCalendarComponentSet(SupportedCalendarComponentSet),
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)] #[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
SupportedCalendarData(SupportedCalendarData), SupportedCalendarData(SupportedCalendarData),
@@ -63,7 +62,7 @@ pub enum CalendarPropWrapper {
Common(CommonPropertiesProp), Common(CommonPropertiesProp),
} }
#[derive(Clone, Debug, From, Into, Deserialize)] #[derive(Clone, Debug, From, Into)]
pub struct CalendarResource { pub struct CalendarResource {
pub cal: Calendar, pub cal: Calendar,
pub read_only: bool, pub read_only: bool,
@@ -128,15 +127,13 @@ impl Resource for CalendarResource {
Ok(match prop { Ok(match prop {
CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop { CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop {
CalendarPropName::CalendarColor => { CalendarPropName::CalendarColor => {
CalendarProp::CalendarColor(self.cal.meta.color.clone()) CalendarProp::CalendarColor(self.cal.color.clone())
} }
CalendarPropName::CalendarDescription => { CalendarPropName::CalendarDescription => {
CalendarProp::CalendarDescription(self.cal.meta.description.clone()) CalendarProp::CalendarDescription(self.cal.description.clone())
} }
CalendarPropName::CalendarTimezone => { CalendarPropName::CalendarTimezone => {
CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| { CalendarProp::CalendarTimezone(self.cal.timezone.clone())
vtimezones_rs::VTIMEZONES.get(tzid).map(|tz| tz.to_string())
}))
} }
// chrono_tz uses the IANA database // chrono_tz uses the IANA database
CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet( CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet(
@@ -146,7 +143,7 @@ impl Resource for CalendarResource {
CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone()) CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone())
} }
CalendarPropName::CalendarOrder => { CalendarPropName::CalendarOrder => {
CalendarProp::CalendarOrder(Some(self.cal.meta.order)) CalendarProp::CalendarOrder(Some(self.cal.order))
} }
CalendarPropName::SupportedCalendarComponentSet => { CalendarPropName::SupportedCalendarComponentSet => {
CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into()) CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into())
@@ -187,56 +184,32 @@ impl Resource for CalendarResource {
match prop { match prop {
CalendarPropWrapper::Calendar(prop) => match prop { CalendarPropWrapper::Calendar(prop) => match prop {
CalendarProp::CalendarColor(color) => { CalendarProp::CalendarColor(color) => {
self.cal.meta.color = color; self.cal.color = color;
Ok(()) Ok(())
} }
CalendarProp::CalendarDescription(description) => { CalendarProp::CalendarDescription(description) => {
self.cal.meta.description = description; self.cal.description = description;
Ok(()) Ok(())
} }
CalendarProp::CalendarTimezone(timezone) => { CalendarProp::CalendarTimezone(timezone) => {
if let Some(tz) = timezone { // TODO: Ensure that timezone-id is also updated
// TODO: Proper error (calendar-timezone precondition) self.cal.timezone = timezone;
let calendar = IcalParser::new(tz.as_bytes())
.next()
.ok_or(rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
))?
.map_err(|_| {
rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
)
})?;
let timezone =
calendar
.timezones
.first()
.ok_or(rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
))?;
let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| {
rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
})?;
self.cal.timezone_id = Some(timezone.name().to_owned());
}
Ok(()) Ok(())
} }
CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly),
CalendarProp::CalendarTimezoneId(timezone_id) => { CalendarProp::CalendarTimezoneId(timezone_id) => {
if let Some(tzid) = &timezone_id if let Some(tzid) = &timezone_id {
&& !vtimezones_rs::VTIMEZONES.contains_key(tzid) // Validate timezone id
{ chrono_tz::Tz::from_str(tzid).map_err(|_| {
return Err(rustical_dav::Error::BadRequest(format!( rustical_dav::Error::BadRequest(format!("Invalid timezone-id: {tzid}"))
"Invalid timezone-id: {tzid}" })?;
))); // TODO: Ensure that timezone is also updated (For now hope that clients play nice)
} }
self.cal.timezone_id = timezone_id; self.cal.timezone_id = timezone_id;
Ok(()) Ok(())
} }
CalendarProp::CalendarOrder(order) => { CalendarProp::CalendarOrder(order) => {
self.cal.meta.order = order.unwrap_or_default(); self.cal.order = order.unwrap_or_default();
Ok(()) Ok(())
} }
CalendarProp::SupportedCalendarComponentSet(comp_set) => { CalendarProp::SupportedCalendarComponentSet(comp_set) => {
@@ -264,20 +237,24 @@ impl Resource for CalendarResource {
match prop { match prop {
CalendarPropWrapperName::Calendar(prop) => match prop { CalendarPropWrapperName::Calendar(prop) => match prop {
CalendarPropName::CalendarColor => { CalendarPropName::CalendarColor => {
self.cal.meta.color = None; self.cal.color = None;
Ok(()) Ok(())
} }
CalendarPropName::CalendarDescription => { CalendarPropName::CalendarDescription => {
self.cal.meta.description = None; self.cal.description = None;
Ok(()) Ok(())
} }
CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => { CalendarPropName::CalendarTimezone => {
self.cal.timezone_id = None; self.cal.timezone = None;
Ok(()) Ok(())
} }
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
CalendarPropName::CalendarTimezoneId => {
self.cal.timezone_id = None;
Ok(())
}
CalendarPropName::CalendarOrder => { CalendarPropName::CalendarOrder => {
self.cal.meta.order = 0; self.cal.order = 0;
Ok(()) Ok(())
} }
CalendarPropName::SupportedCalendarComponentSet => { CalendarPropName::SupportedCalendarComponentSet => {
@@ -300,10 +277,10 @@ impl Resource for CalendarResource {
} }
fn get_displayname(&self) -> Option<&str> { fn get_displayname(&self) -> Option<&str> {
self.cal.meta.displayname.as_deref() self.cal.displayname.as_deref()
} }
fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> { fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> {
self.cal.meta.displayname = name; self.cal.displayname = name;
Ok(()) Ok(())
} }
@@ -328,15 +305,3 @@ impl Resource for CalendarResource {
)) ))
} }
} }
#[cfg(test)]
mod tests {
#[test]
fn test_tzdb_version() {
// Ensure that both chrono_tz and vzic_rs use the same tzdb version
assert_eq!(
chrono_tz::IANA_TZDB_VERSION,
vtimezones_rs::IANA_TZDB_VERSION
);
}
}

View File

@@ -1,5 +1,4 @@
use crate::calendar::methods::get::route_get; use crate::calendar::methods::get::route_get;
use crate::calendar::methods::import::route_import;
use crate::calendar::methods::mkcalendar::route_mkcalendar; use crate::calendar::methods::mkcalendar::route_mkcalendar;
use crate::calendar::methods::post::route_post; use crate::calendar::methods::post::route_post;
use crate::calendar::methods::report::route_report_calendar; use crate::calendar::methods::report::route_report_calendar;
@@ -52,7 +51,7 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
type Principal = Principal; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, webdav-push"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy, webdav-push";
async fn get_resource( async fn get_resource(
&self, &self,
@@ -139,13 +138,6 @@ impl<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarResourceSer
}) })
} }
fn import() -> Option<rustical_dav::resource::MethodFunction<Self>> {
Some(|state, req| {
let mut service = Handler::with_state(route_import::<C, S>, state);
Box::pin(Service::call(&mut service, req))
})
}
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
{ {
Some(|state, req| { Some(|state, req| {

View File

@@ -1,222 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<href>/caldav/principal/user/calendar/</href>
<propstat>
<prop>
<calendar-color xmlns="http://apple.com/ns/ical/"/>
<calendar-description xmlns="urn:ietf:params:xml:ns:caldav"/>
<calendar-timezone xmlns="urn:ietf:params:xml:ns:caldav"/>
<timezone-service-set xmlns="urn:ietf:params:xml:ns:caldav"/>
<calendar-timezone-id xmlns="urn:ietf:params:xml:ns:caldav"/>
<calendar-order xmlns="http://apple.com/ns/ical/"/>
<supported-calendar-component-set xmlns="urn:ietf:params:xml:ns:caldav"/>
<supported-calendar-data xmlns="urn:ietf:params:xml:ns:caldav"/>
<max-resource-size xmlns="DAV:"/>
<supported-report-set xmlns="DAV:"/>
<source xmlns="http://calendarserver.org/ns/"/>
<min-date-time xmlns="urn:ietf:params:xml:ns:caldav"/>
<max-date-time xmlns="urn:ietf:params:xml:ns:caldav"/>
<sync-token xmlns="DAV:"/>
<getctag xmlns="http://calendarserver.org/ns/"/>
<transports xmlns="https://bitfire.at/webdav-push"/>
<topic xmlns="https://bitfire.at/webdav-push"/>
<supported-triggers xmlns="https://bitfire.at/webdav-push"/>
<resourcetype xmlns="DAV:"/>
<displayname xmlns="DAV:"/>
<current-user-principal xmlns="DAV:"/>
<current-user-privilege-set xmlns="DAV:"/>
<owner xmlns="DAV:"/>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<?xml version="1.0" encoding="utf-8"?>
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<href>/caldav/principal/user/calendar/</href>
<propstat>
<prop>
<CAL:calendar-timezone>BEGIN:VCALENDAR
PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
LAST-MODIFIED:20250723T190331Z
X-LIC-LOCATION:Europe/Berlin
X-PROLEPTIC-TZNAME:LMT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+005328
TZOFFSETTO:+0100
DTSTART:18930401T000000
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19160430T230000
RDATE:19400401T020000
RDATE:19430329T020000
RDATE:19460414T020000
RDATE:19470406T030000
RDATE:19480418T020000
RDATE:19490410T020000
RDATE:19800406T020000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19161001T010000
RDATE:19421102T030000
RDATE:19431004T030000
RDATE:19441002T030000
RDATE:19451118T030000
RDATE:19461007T030000
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19170416T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T010000Z
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19170917T030000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T010000Z
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19440403T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T010000Z
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEMT
TZOFFSETFROM:+0200
TZOFFSETTO:+0300
DTSTART:19450524T020000
RDATE:19470511T030000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0300
TZOFFSETTO:+0200
DTSTART:19450924T030000
RDATE:19470629T030000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0100
TZOFFSETTO:+0100
DTSTART:19460101T000000
RDATE:19800101T000000
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19471005T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19491002T010000Z
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19800928T030000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T010000Z
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
END:VCALENDAR
</CAL:calendar-timezone>
<CAL:timezone-service-set>
<href>https://www.iana.org/time-zones</href>
</CAL:timezone-service-set>
<CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
<calendar-order xmlns="http://apple.com/ns/ical/">0</calendar-order>
<CAL:supported-calendar-component-set>
<CAL:comp name="VEVENT"/>
<CAL:comp name="VTODO"/>
</CAL:supported-calendar-component-set>
<CAL:supported-calendar-data>
<CAL:calendar-data content-type="text/calendar" version="2.0"/>
</CAL:supported-calendar-data>
<max-resource-size>10000000</max-resource-size>
<supported-report-set>
<supported-report>
<report>
<CAL:calendar-query/>
</report>
</supported-report>
<supported-report>
<report>
<CAL:calendar-multiget/>
</report>
</supported-report>
<supported-report>
<report>
<sync-collection/>
</report>
</supported-report>
</supported-report-set>
<CAL:min-date-time>-2621430101T000000Z</CAL:min-date-time>
<CAL:max-date-time>+2621421231T235959Z</CAL:max-date-time>
<sync-token>github.com/lennart-k/rustical/ns/12</sync-token>
<CS:getctag>github.com/lennart-k/rustical/ns/12</CS:getctag>
<PUSH:transports>
<PUSH:web-push/>
</PUSH:transports>
<PUSH:topic>b28b41e9-8801-4fc5-ae29-8efb5fadeb36</PUSH:topic>
<PUSH:supported-triggers>
<PUSH:content-update>
<depth>1</depth>
</PUSH:content-update>
<PUSH:property-update>
<depth>1</depth>
</PUSH:property-update>
</PUSH:supported-triggers>
<resourcetype>
<collection/>
<CAL:calendar/>
</resourcetype>
<displayname>Calendar</displayname>
<current-user-principal>
<href>/caldav/principal/user/</href>
</current-user-principal>
<current-user-privilege-set>
<privilege>
<read/>
</privilege>
<privilege>
<read-acl/>
</privilege>
<privilege>
<read-current-user-privilege-set/>
</privilege>
</current-user-privilege-set>
<owner>
<href>/caldav/principal/user/</href>
</owner>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>

View File

@@ -1,11 +0,0 @@
[
{
"id": "user",
"displayname": null,
"principal_type": "individual",
"password": null,
"memberships": [
"group"
]
}
]

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<propfind xmlns="DAV:"><propname/></propfind>
<?xml version="1.0" encoding="UTF-8"?>
<propfind xmlns="DAV:"><allprop/></propfind>

View File

@@ -1,42 +0,0 @@
[
{
"cal": {
"principal": "user",
"id": "calendar",
"displayname": "Calendar",
"order": 0,
"description": null,
"color": null,
"timezone_id": "Europe/Berlin",
"deleted_at": null,
"synctoken": 12,
"subscription_url": null,
"push_topic": "b28b41e9-8801-4fc5-ae29-8efb5fadeb36",
"components": [
"VEVENT",
"VTODO"
]
},
"read_only": true
},
{
"cal": {
"principal": "user",
"id": "calendar",
"displayname": "Calendar",
"order": 0,
"description": null,
"color": null,
"timezone_id": "Europe/Berlin",
"deleted_at": null,
"synctoken": 12,
"subscription_url": null,
"push_topic": "b28b41e9-8801-4fc5-ae29-8efb5fadeb36",
"components": [
"VEVENT",
"VTODO"
]
},
"read_only": true
}
]

View File

@@ -1,47 +0,0 @@
use crate::{CalDavPrincipalUri, calendar::resource::CalendarResource};
use rustical_dav::resource::Resource;
use rustical_store::auth::Principal;
use rustical_xml::XmlSerializeRoot;
use serde_json::from_str;
#[tokio::test]
async fn test_propfind() {
let requests: Vec<_> = include_str!("./test_files/propfind.requests")
.trim()
.split("\n\n")
.collect();
let principals: Vec<Principal> =
from_str(include_str!("./test_files/propfind.principals.json")).unwrap();
let resources: Vec<CalendarResource> =
from_str(include_str!("./test_files/propfind.resources.json")).unwrap();
let outputs: Vec<_> = include_str!("./test_files/propfind.outputs")
.trim()
.split("\n\n")
.collect();
for principal in principals {
for ((request, resource), &expected_output) in requests.iter().zip(&resources).zip(&outputs)
{
let propfind = CalendarResource::parse_propfind(request).unwrap();
let response = resource
.propfind(
&format!("/caldav/principal/{}/{}", principal.id, resource.cal.id),
&propfind.prop,
propfind.include.as_ref(),
&CalDavPrincipalUri("/caldav"),
&principal,
)
.unwrap();
let expected_output = expected_output.trim();
let output = response
.serialize_to_string()
.unwrap()
.trim()
.replace("\r\n", "\n");
println!("{output}");
println!("{}, {} \n\n\n", output.len(), expected_output.len());
assert_eq!(output, expected_output);
}
}
}

View File

@@ -11,7 +11,7 @@ use rustical_ical::CalendarObject;
use rustical_store::CalendarStore; use rustical_store::CalendarStore;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
use std::str::FromStr; use std::str::FromStr;
use tracing::{debug, error, instrument}; use tracing::instrument;
#[instrument(skip(cal_store))] #[instrument(skip(cal_store))]
pub async fn get_event<C: CalendarStore>( pub async fn get_event<C: CalendarStore>(
@@ -78,21 +78,12 @@ pub async fn put_event<C: CalendarStore>(
true true
}; };
let object = match CalendarObject::from_ics(body.clone()) { let object = match CalendarObject::from_ics(object_id, body) {
Ok(obj) => obj, Ok(obj) => obj,
Err(_) => { Err(_) => {
debug!("invalid calendar data:\n{body}");
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData)); return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
} }
}; };
if object.get_id() != object_id {
error!(
"Calendar object UID and file name not matching: UID={}, filename={}",
object.get_id(),
object_id
);
return Err(Error::PreconditionFailed(Precondition::MatchingUid));
}
cal_store cal_store
.put_object(principal, calendar_id, object, overwrite) .put_object(principal, calendar_id, object, overwrite)
.await?; .await?;

View File

@@ -12,8 +12,6 @@ pub enum Precondition {
#[error("valid-calendar-data")] #[error("valid-calendar-data")]
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
ValidCalendarData, ValidCalendarData,
#[error("matching-uid")]
MatchingUid,
} }
impl IntoResponse for Precondition { impl IntoResponse for Precondition {
@@ -85,12 +83,6 @@ impl Error {
impl IntoResponse for Error { impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
if matches!(
self.status_code(),
StatusCode::INTERNAL_SERVER_ERROR | StatusCode::PRECONDITION_FAILED
) {
error!("{self}");
}
(self.status_code(), self.to_string()).into_response() (self.status_code(), self.to_string()).into_response()
} }
} }

View File

@@ -41,6 +41,11 @@ impl Resource for PrincipalResource {
Resourcetype(&[ Resourcetype(&[
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"), ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"), ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"),
// https://github.com/apple/ccs-calendarserver/blob/13c706b985fb728b9aab42dc0fef85aae21921c3/doc/Extensions/caldav-proxy.txt
// ResourcetypeInner(
// Some(rustical_dav::namespace::NS_CALENDARSERVER),
// "calendar-proxy-write",
// ),
]) ])
} }
@@ -121,7 +126,7 @@ impl Resource for PrincipalResource {
} }
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_only( Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.principal.id), user.is_principal(&self.principal.id),
)) ))
} }

View File

@@ -16,13 +16,13 @@ pub enum PrincipalProp {
CalendarUserAddressSet(HrefElement), CalendarUserAddressSet(HrefElement),
// WebDAV Access Control (RFC 3744) // WebDAV Access Control (RFC 3744)
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = "principal-URL")] #[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"principal-URL")]
PrincipalUrl(HrefElement), PrincipalUrl(HrefElement),
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
GroupMembership(GroupMembership), GroupMembership(GroupMembership),
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
GroupMemberSet(GroupMemberSet), GroupMemberSet(GroupMemberSet),
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = "alternate-URI-set")] #[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"alternate-URI-set")]
AlternateUriSet, AlternateUriSet,
// #[xml(ns = "rustical_dav::namespace::NS_DAV")] // #[xml(ns = "rustical_dav::namespace::NS_DAV")]
// PrincipalCollectionSet(HrefElement), // PrincipalCollectionSet(HrefElement),

View File

@@ -46,7 +46,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
type Principal = Principal; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy";
async fn get_resource( async fn get_resource(
&self, &self,

View File

@@ -1,19 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::principal::PrincipalResourceService;
CalDavPrincipalUri,
principal::{PrincipalResource, PrincipalResourceService},
};
use rstest::rstest; use rstest::rstest;
use rustical_dav::resource::{Resource, ResourceService}; use rustical_dav::resource::ResourceService;
use rustical_store::auth::{Principal, PrincipalType::Individual};
use rustical_store_sqlite::{ use rustical_store_sqlite::{
SqliteStore, SqliteStore,
calendar_store::SqliteCalendarStore, calendar_store::SqliteCalendarStore,
principal_store::SqlitePrincipalStore, principal_store::SqlitePrincipalStore,
tests::{get_test_calendar_store, get_test_principal_store, get_test_subscription_store}, tests::{get_test_calendar_store, get_test_principal_store, get_test_subscription_store},
}; };
use rustical_xml::XmlSerializeRoot;
#[rstest] #[rstest]
#[tokio::test] #[tokio::test]
@@ -35,15 +30,6 @@ async fn test_principal_resource(
simplified_home_set: false, simplified_home_set: false,
}; };
// We don't have any calendars here
assert!(
service
.get_members(&("user".to_owned(),))
.await
.unwrap()
.is_empty()
);
assert!(matches!( assert!(matches!(
service service
.get_resource(&("invalid-user".to_owned(),), true) .get_resource(&("invalid-user".to_owned(),), true)
@@ -58,35 +44,4 @@ async fn test_principal_resource(
} }
#[tokio::test] #[tokio::test]
async fn test_propfind() { async fn test_propfind() {}
let propfind = PrincipalResource::parse_propfind(
r#"<?xml version="1.0" encoding="UTF-8"?><propfind xmlns="DAV:"><allprop/></propfind>"#,
)
.unwrap();
let principal = Principal {
id: "user".to_string(),
displayname: None,
principal_type: Individual,
password: None,
memberships: vec!["group".to_string()],
};
let resource = PrincipalResource {
principal: principal.clone(),
members: vec![],
simplified_home_set: false,
};
let response = resource
.propfind(
&format!("/caldav/principal/{}", principal.id),
&propfind.prop,
propfind.include.as_ref(),
&CalDavPrincipalUri("/caldav"),
&principal,
)
.unwrap();
let _output = response.serialize_to_string().unwrap();
}

View File

@@ -1,67 +0,0 @@
use std::io::BufReader;
use crate::Error;
use crate::addressbook::AddressbookResourceService;
use axum::{
extract::{Path, State},
response::{IntoResponse, Response},
};
use http::StatusCode;
use ical::{
parser::{Component, ComponentMut, vcard},
property::Property,
};
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::Principal};
use tracing::instrument;
#[instrument(skip(resource_service))]
pub async fn route_import<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>,
user: Principal,
State(resource_service): State<AddressbookResourceService<AS, S>>,
body: String,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(Error::Unauthorized);
}
let parser = vcard::VcardParser::new(BufReader::new(body.as_bytes()));
let mut objects = vec![];
for res in parser {
let mut card = res.unwrap();
let uid = card.get_uid();
if uid.is_none() {
let mut card_mut = card.mutable();
card_mut.set_property(Property {
name: "UID".to_owned(),
value: Some(uuid::Uuid::new_v4().to_string()),
params: None,
});
card = card_mut.verify().unwrap();
}
objects.push(card.try_into().unwrap());
}
if objects.is_empty() {
return Ok((StatusCode::BAD_REQUEST, "empty addressbook data").into_response());
}
let addressbook = Addressbook {
principal,
id: addressbook_id,
displayname: None,
description: None,
deleted_at: None,
synctoken: 0,
push_topic: uuid::Uuid::new_v4().to_string(),
};
let addr_store = resource_service.addr_store;
addr_store
.import_addressbook(addressbook, objects, false)
.await?;
Ok(StatusCode::OK.into_response())
}

View File

@@ -22,7 +22,7 @@ pub struct MkcolAddressbookProp {
resourcetype: Option<Resourcetype>, resourcetype: Option<Resourcetype>,
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
displayname: Option<String>, displayname: Option<String>,
#[xml(rename = "addressbook-description")] #[xml(rename = b"addressbook-description")]
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")] #[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
description: Option<String>, description: Option<String>,
} }
@@ -34,7 +34,7 @@ pub struct PropElement<T: XmlDeserialize> {
} }
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
#[xml(root = "mkcol")] #[xml(root = b"mkcol")]
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
struct MkcolRequest { struct MkcolRequest {
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]

View File

@@ -1,5 +1,5 @@
pub mod get; pub mod get;
pub mod import;
pub mod mkcol; pub mod mkcol;
pub mod post; pub mod post;
pub mod put;
pub mod report; pub mod report;

View File

@@ -0,0 +1,47 @@
use crate::Error;
use crate::addressbook::AddressbookResourceService;
use axum::response::IntoResponse;
use axum::{
extract::{Path, State},
response::Response,
};
use http::StatusCode;
use ical::VcardParser;
use rustical_ical::AddressObject;
use rustical_store::Addressbook;
use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal};
use tracing::instrument;
#[instrument(skip(addr_store))]
pub async fn route_put<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
user: Principal,
body: String,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(Error::Unauthorized);
}
let mut objects = vec![];
for object in VcardParser::new(body.as_bytes()) {
let object = object.map_err(rustical_ical::Error::from)?;
objects.push(AddressObject::try_from(object)?);
}
let addressbook = Addressbook {
id: addressbook_id.clone(),
principal: principal.clone(),
displayname: None,
description: None,
deleted_at: None,
synctoken: Default::default(),
push_topic: uuid::Uuid::new_v4().to_string(),
};
addr_store
.import_addressbook(principal.clone(), addressbook, objects)
.await?;
Ok(StatusCode::CREATED.into_response())
}

View File

@@ -3,8 +3,8 @@ use super::methods::report::route_report_addressbook;
use crate::address_object::AddressObjectResourceService; use crate::address_object::AddressObjectResourceService;
use crate::address_object::resource::AddressObjectResource; use crate::address_object::resource::AddressObjectResource;
use crate::addressbook::methods::get::route_get; use crate::addressbook::methods::get::route_get;
use crate::addressbook::methods::import::route_import;
use crate::addressbook::methods::post::route_post; use crate::addressbook::methods::post::route_post;
use crate::addressbook::methods::put::route_put;
use crate::addressbook::resource::AddressbookResource; use crate::addressbook::resource::AddressbookResource;
use crate::{CardDavPrincipalUri, Error}; use crate::{CardDavPrincipalUri, Error};
use async_trait::async_trait; use async_trait::async_trait;
@@ -139,9 +139,9 @@ impl<AS: AddressbookStore, S: SubscriptionStore> AxumMethods for AddressbookReso
}) })
} }
fn import() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> { fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| { Some(|state, req| {
let mut service = Handler::with_state(route_import::<AS, S>, state); let mut service = Handler::with_state(route_put::<AS, S>, state);
Box::pin(Service::call(&mut service, req)) Box::pin(Service::call(&mut service, req))
}) })
} }

View File

@@ -8,14 +8,14 @@ use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
#[xml(unit_variants_ident = "PrincipalPropName")] #[xml(unit_variants_ident = "PrincipalPropName")]
pub enum PrincipalProp { pub enum PrincipalProp {
// WebDAV Access Control (RFC 3744) // WebDAV Access Control (RFC 3744)
#[xml(rename = "principal-URL")] #[xml(rename = b"principal-URL")]
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
PrincipalUrl(HrefElement), PrincipalUrl(HrefElement),
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
GroupMembership(GroupMembership), GroupMembership(GroupMembership),
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
GroupMemberSet(GroupMemberSet), GroupMemberSet(GroupMemberSet),
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = "alternate-URI-set")] #[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"alternate-URI-set")]
AlternateUriSet, AlternateUriSet,
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
PrincipalCollectionSet(HrefElement), PrincipalCollectionSet(HrefElement),

View File

@@ -1,4 +1,3 @@
use axum::body::Body;
use http::StatusCode; use http::StatusCode;
use rustical_xml::XmlError; use rustical_xml::XmlError;
use thiserror::Error; use thiserror::Error;
@@ -60,12 +59,7 @@ impl Error {
impl axum::response::IntoResponse for Error { impl axum::response::IntoResponse for Error {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
if matches!( use axum::body::Body;
self.status_code(),
StatusCode::INTERNAL_SERVER_ERROR | StatusCode::PRECONDITION_FAILED
) {
error!("{self}");
}
let mut resp = axum::response::Response::builder().status(self.status_code()); let mut resp = axum::response::Response::builder().status(self.status_code());
if matches!(&self, &Error::Unauthorized) { if matches!(&self, &Error::Unauthorized) {

View File

@@ -14,12 +14,16 @@ impl IntoResponse for InvalidOverwriteHeader {
} }
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Default)]
pub struct Overwrite(pub bool); pub enum Overwrite {
#[default]
T,
F,
}
impl Default for Overwrite { impl Overwrite {
fn default() -> Self { pub fn is_true(&self) -> bool {
Self(true) matches!(self, Self::T)
} }
} }
@@ -43,48 +47,9 @@ impl TryFrom<&[u8]> for Overwrite {
fn try_from(value: &[u8]) -> Result<Self, Self::Error> { fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
match value { match value {
b"T" => Ok(Self(true)), b"T" => Ok(Overwrite::T),
b"F" => Ok(Self(false)), b"F" => Ok(Overwrite::F),
_ => Err(InvalidOverwriteHeader), _ => Err(InvalidOverwriteHeader),
} }
} }
} }
#[cfg(test)]
mod tests {
use axum::{extract::FromRequestParts, response::IntoResponse};
use http::Request;
use crate::header::Overwrite;
#[tokio::test]
async fn test_overwrite_default() {
let request = Request::put("asd").body(()).unwrap();
let (mut parts, _) = request.into_parts();
let overwrite = Overwrite::from_request_parts(&mut parts, &())
.await
.unwrap();
assert_eq!(
Overwrite(true),
overwrite,
"By default we want to overwrite!"
);
}
#[test]
fn test_overwrite() {
assert_eq!(
Overwrite(true),
Overwrite::try_from(b"T".as_slice()).unwrap()
);
assert_eq!(
Overwrite(false),
Overwrite::try_from(b"F".as_slice()).unwrap()
);
if let Err(err) = Overwrite::try_from(b"aslkdjlad".as_slice()) {
let _ = err.into_response();
} else {
unreachable!("should return error")
}
}
}

View File

@@ -1,10 +1,9 @@
use itertools::Itertools;
use quick_xml::name::Namespace; use quick_xml::name::Namespace;
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
// https://datatracker.ietf.org/doc/html/rfc3744 // https://datatracker.ietf.org/doc/html/rfc3744
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq, PartialOrd, Ord)] #[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq)]
pub enum UserPrivilege { pub enum UserPrivilege {
Read, Read,
Write, Write,
@@ -20,18 +19,18 @@ impl XmlSerialize for UserPrivilegeSet {
fn serialize( fn serialize(
&self, &self,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&str>, tag: Option<&[u8]>,
namespaces: &HashMap<Namespace, &str>, namespaces: &HashMap<Namespace, &[u8]>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
#[derive(XmlSerialize)] #[derive(XmlSerialize)]
pub struct FakeUserPrivilegeSet { pub struct FakeUserPrivilegeSet {
#[xml(rename = "privilege", flatten)] #[xml(rename = b"privilege", flatten)]
privileges: Vec<UserPrivilege>, privileges: Vec<UserPrivilege>,
} }
FakeUserPrivilegeSet { FakeUserPrivilegeSet {
privileges: self.privileges.iter().cloned().sorted().collect(), privileges: self.privileges.iter().cloned().collect(),
} }
.serialize(ns, tag, namespaces, writer) .serialize(ns, tag, namespaces, writer)
} }

View File

@@ -38,11 +38,6 @@ pub trait AxumMethods: Sized + Send + Sync + 'static {
None None
} }
#[inline]
fn import() -> Option<MethodFunction<Self>> {
None
}
#[inline] #[inline]
fn allow_header() -> Allow { fn allow_header() -> Allow {
let mut allow = vec![ let mut allow = vec![
@@ -72,9 +67,6 @@ pub trait AxumMethods: Sized + Send + Sync + 'static {
if Self::put().is_some() { if Self::put().is_some() {
allow.push(Method::PUT); allow.push(Method::PUT);
} }
if Self::import().is_some() {
allow.push(Method::from_str("IMPORT").unwrap());
}
allow.into_iter().collect() allow.into_iter().collect()
} }

View File

@@ -97,11 +97,6 @@ where
return svc(self.resource_service.clone(), req); return svc(self.resource_service.clone(), req);
} }
} }
"IMPORT" => {
if let Some(svc) = RS::import() {
return svc(self.resource_service.clone(), req);
}
}
_ => {} _ => {}
}; };
Box::pin(async move { Box::pin(async move {

View File

@@ -17,7 +17,7 @@ pub(crate) async fn axum_route_copy<R: ResourceService>(
State(resource_service): State<R>, State(resource_service): State<R>,
depth: Option<Depth>, depth: Option<Depth>,
principal: R::Principal, principal: R::Principal,
Overwrite(overwrite): Overwrite, overwrite: Overwrite,
matched_path: MatchedPath, matched_path: MatchedPath,
header_map: HeaderMap, header_map: HeaderMap,
) -> Result<Response, R::Error> { ) -> Result<Response, R::Error> {
@@ -39,7 +39,7 @@ pub(crate) async fn axum_route_copy<R: ResourceService>(
.map_err(|_| crate::Error::Forbidden)?; .map_err(|_| crate::Error::Forbidden)?;
if resource_service if resource_service
.copy_resource(&path, &dest_path, &principal, overwrite) .copy_resource(&path, &dest_path, &principal, overwrite.is_true())
.await? .await?
{ {
// Overwritten // Overwritten

View File

@@ -60,11 +60,11 @@ pub async fn route_delete<R: ResourceService>(
return Err(crate::Error::PreconditionFailed.into()); return Err(crate::Error::PreconditionFailed.into());
} }
} }
if let Some(if_none_match) = if_none_match if let Some(if_none_match) = if_none_match {
&& resource.satisfies_if_none_match(&if_none_match) if resource.satisfies_if_none_match(&if_none_match) {
{ // Precondition failed
// Precondition failed return Err(crate::Error::PreconditionFailed.into());
return Err(crate::Error::PreconditionFailed.into()); }
} }
resource_service resource_service
.delete_resource(path_components, !no_trash) .delete_resource(path_components, !no_trash)

View File

@@ -17,7 +17,7 @@ pub(crate) async fn axum_route_move<R: ResourceService>(
State(resource_service): State<R>, State(resource_service): State<R>,
depth: Option<Depth>, depth: Option<Depth>,
principal: R::Principal, principal: R::Principal,
Overwrite(overwrite): Overwrite, overwrite: Overwrite,
matched_path: MatchedPath, matched_path: MatchedPath,
header_map: HeaderMap, header_map: HeaderMap,
) -> Result<Response, R::Error> { ) -> Result<Response, R::Error> {
@@ -39,7 +39,7 @@ pub(crate) async fn axum_route_move<R: ResourceService>(
.map_err(|_| crate::Error::Forbidden)?; .map_err(|_| crate::Error::Forbidden)?;
if resource_service if resource_service
.copy_resource(&path, &dest_path, &principal, overwrite) .copy_resource(&path, &dest_path, &principal, overwrite.is_true())
.await? .await?
{ {
// Overwritten // Overwritten

View File

@@ -6,7 +6,11 @@ use crate::resource::Resource;
use crate::resource::ResourceName; use crate::resource::ResourceName;
use crate::resource::ResourceService; use crate::resource::ResourceService;
use crate::xml::MultistatusElement; use crate::xml::MultistatusElement;
use crate::xml::PropfindElement;
use crate::xml::PropfindType;
use axum::extract::{Extension, OriginalUri, Path, State}; use axum::extract::{Extension, OriginalUri, Path, State};
use rustical_xml::PropName;
use rustical_xml::XmlDocument;
use tracing::instrument; use tracing::instrument;
type RSMultistatus<R> = MultistatusElement< type RSMultistatus<R> = MultistatusElement<
@@ -54,8 +58,24 @@ pub(crate) async fn route_propfind<R: ResourceService>(
} }
// A request body is optional. If empty we MUST return all props // A request body is optional. If empty we MUST return all props
let propfind_self = R::Resource::parse_propfind(body).map_err(Error::XmlError)?; let propfind_self: PropfindElement<<<R::Resource as Resource>::Prop as PropName>::Names> =
let propfind_member = R::MemberType::parse_propfind(body).map_err(Error::XmlError)?; if !body.is_empty() {
PropfindElement::parse_str(body).map_err(Error::XmlError)?
} else {
PropfindElement {
prop: PropfindType::Allprop,
include: None,
}
};
let propfind_member: PropfindElement<<<R::MemberType as Resource>::Prop as PropName>::Names> =
if !body.is_empty() {
PropfindElement::parse_str(body).map_err(Error::XmlError)?
} else {
PropfindElement {
prop: PropfindType::Allprop,
include: None,
}
};
let mut member_responses = Vec::new(); let mut member_responses = Vec::new();
if depth != &Depth::Zero { if depth != &Depth::Zero {

View File

@@ -26,21 +26,21 @@ enum SetPropertyPropWrapper<T: XmlDeserialize> {
// We are <prop> // We are <prop>
#[derive(XmlDeserialize, Clone, Debug)] #[derive(XmlDeserialize, Clone, Debug)]
struct SetPropertyPropWrapperWrapper<T: XmlDeserialize>( struct SetPropertyPropWrapperWrapper<T: XmlDeserialize>(
#[xml(ty = "untagged", flatten)] Vec<SetPropertyPropWrapper<T>>, #[xml(ty = "untagged")] SetPropertyPropWrapper<T>,
); );
// We are <set> // We are <set>
#[derive(XmlDeserialize, Clone, Debug)] #[derive(XmlDeserialize, Clone, Debug)]
struct SetPropertyElement<T: XmlDeserialize> { struct SetPropertyElement<T: XmlDeserialize> {
#[xml(ns = "crate::namespace::NS_DAV")] #[xml(ns = "crate::namespace::NS_DAV")]
prop: SetPropertyPropWrapperWrapper<T>, prop: T,
} }
#[derive(XmlDeserialize, Clone, Debug)] #[derive(XmlDeserialize, Clone, Debug)]
struct TagName(#[xml(ty = "tag_name")] String); struct TagName(#[xml(ty = "tag_name")] String);
#[derive(XmlDeserialize, Clone, Debug)] #[derive(XmlDeserialize, Clone, Debug)]
struct PropertyElement(#[xml(ty = "untagged", flatten)] Vec<TagName>); struct PropertyElement(#[xml(ty = "untagged")] TagName);
#[derive(XmlDeserialize, Clone, Debug)] #[derive(XmlDeserialize, Clone, Debug)]
struct RemovePropertyElement { struct RemovePropertyElement {
@@ -57,7 +57,7 @@ enum Operation<T: XmlDeserialize> {
} }
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
#[xml(root = "propertyupdate")] #[xml(root = b"propertyupdate")]
#[xml(ns = "crate::namespace::NS_DAV")] #[xml(ns = "crate::namespace::NS_DAV")]
struct PropertyupdateElement<T: XmlDeserialize>(#[xml(ty = "untagged", flatten)] Vec<Operation<T>>); struct PropertyupdateElement<T: XmlDeserialize>(#[xml(ty = "untagged", flatten)] Vec<Operation<T>>);
@@ -81,8 +81,9 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
let href = path.to_owned(); let href = path.to_owned();
// Extract operations // Extract operations
let PropertyupdateElement::<<R::Resource as Resource>::Prop>(operations) = let PropertyupdateElement::<SetPropertyPropWrapperWrapper<<R::Resource as Resource>::Prop>>(
XmlDocument::parse_str(body).map_err(Error::XmlError)?; operations,
) = XmlDocument::parse_str(body).map_err(Error::XmlError)?;
let mut resource = resource_service let mut resource = resource_service
.get_resource(path_components, false) .get_resource(path_components, false)
@@ -99,63 +100,59 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
for operation in operations.into_iter() { for operation in operations.into_iter() {
match operation { match operation {
Operation::Set(SetPropertyElement { Operation::Set(SetPropertyElement {
prop: SetPropertyPropWrapperWrapper(properties), prop: SetPropertyPropWrapperWrapper(property),
}) => { }) => {
for property in properties { match property {
match property { SetPropertyPropWrapper::Valid(prop) => {
SetPropertyPropWrapper::Valid(prop) => { let propname: <<R::Resource as Resource>::Prop as PropName>::Names =
let propname: <<R::Resource as Resource>::Prop as PropName>::Names = prop.clone().into();
prop.clone().into(); let (ns, propname): (Option<Namespace>, &str) = propname.into();
let (ns, propname): (Option<Namespace>, &str) = propname.into(); match resource.set_prop(prop) {
match resource.set_prop(prop) { Ok(()) => {
Ok(()) => props_ok props_ok.push((ns.map(NamespaceOwned::from), propname.to_owned()))
.push((ns.map(NamespaceOwned::from), propname.to_owned())),
Err(Error::PropReadOnly) => props_conflict
.push((ns.map(NamespaceOwned::from), propname.to_owned())),
Err(err) => return Err(err.into()),
};
}
SetPropertyPropWrapper::Invalid(invalid) => {
let propname = invalid.tag_name();
if let Some(full_propname) = <R::Resource as Resource>::list_props()
.into_iter()
.find_map(|(ns, tag)| {
if tag == propname.as_str() {
Some((ns.map(NamespaceOwned::from), tag.to_owned()))
} else {
None
}
})
{
// This happens in following cases:
// - read-only properties with #[serde(skip_deserializing)]
// - internal properties
props_conflict.push(full_propname)
} else {
props_not_found.push((None, propname));
} }
Err(Error::PropReadOnly) => props_conflict
.push((ns.map(NamespaceOwned::from), propname.to_owned())),
Err(err) => return Err(err.into()),
};
}
SetPropertyPropWrapper::Invalid(invalid) => {
let propname = invalid.tag_name();
if let Some(full_propname) = <R::Resource as Resource>::list_props()
.into_iter()
.find_map(|(ns, tag)| {
if tag == propname.as_str() {
Some((ns.map(NamespaceOwned::from), tag.to_owned()))
} else {
None
}
})
{
// This happens in following cases:
// - read-only properties with #[serde(skip_deserializing)]
// - internal properties
props_conflict.push(full_propname)
} else {
props_not_found.push((None, propname));
} }
} }
} }
} }
Operation::Remove(remove_el) => { Operation::Remove(remove_el) => {
for tagname in remove_el.prop.0 { let propname = remove_el.prop.0.0;
let propname = tagname.0; match <<R::Resource as Resource>::Prop as PropName>::Names::from_str(&propname) {
match <<R::Resource as Resource>::Prop as PropName>::Names::from_str(&propname) Ok(prop) => match resource.remove_prop(&prop) {
{ Ok(()) => props_ok.push((None, propname)),
Ok(prop) => match resource.remove_prop(&prop) { Err(Error::PropReadOnly) => props_conflict.push({
Ok(()) => props_ok.push((None, propname)), let (ns, tag) = prop.into();
Err(Error::PropReadOnly) => props_conflict.push({ (ns.map(NamespaceOwned::from), tag.to_owned())
let (ns, tag) = prop.into(); }),
(ns.map(NamespaceOwned::from), tag.to_owned()) Err(err) => return Err(err.into()),
}), },
Err(err) => return Err(err.into()), // I guess removing a nonexisting property should be successful :)
}, Err(_) => props_ok.push((None, propname)),
// I guess removing a nonexisting property should be successful :) };
Err(_) => props_ok.push((None, propname)),
};
}
} }
} }
} }

View File

@@ -1,16 +1,15 @@
use crate::Principal; use crate::Principal;
use crate::privileges::UserPrivilegeSet; use crate::privileges::UserPrivilegeSet;
use crate::xml::multistatus::{PropTagWrapper, PropstatElement, PropstatWrapper}; use crate::xml::multistatus::{PropTagWrapper, PropstatElement, PropstatWrapper};
use crate::xml::{PropElement, PropfindElement, PropfindType, Resourcetype}; use crate::xml::{PropElement, PropfindType, Resourcetype};
use crate::xml::{TagList, multistatus::ResponseElement}; use crate::xml::{TagList, multistatus::ResponseElement};
use headers::{ETag, IfMatch, IfNoneMatch}; use headers::{ETag, IfMatch, IfNoneMatch};
use http::StatusCode; use http::StatusCode;
use itertools::Itertools; use itertools::Itertools;
use quick_xml::name::Namespace; use quick_xml::name::Namespace;
pub use resource_service::ResourceService; pub use resource_service::ResourceService;
use rustical_xml::{ use rustical_xml::{EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlSerialize};
EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlDocument, XmlSerialize, use std::collections::HashSet;
};
use std::str::FromStr; use std::str::FromStr;
mod axum_methods; mod axum_methods;
@@ -103,19 +102,6 @@ pub trait Resource: Clone + Send + 'static {
principal: &Self::Principal, principal: &Self::Principal,
) -> Result<UserPrivilegeSet, Self::Error>; ) -> Result<UserPrivilegeSet, Self::Error>;
fn parse_propfind(
body: &str,
) -> Result<PropfindElement<<Self::Prop as PropName>::Names>, rustical_xml::XmlError> {
if !body.is_empty() {
PropfindElement::parse_str(body)
} else {
Ok(PropfindElement {
prop: PropfindType::Allprop,
include: None,
})
}
}
fn propfind( fn propfind(
&self, &self,
path: &str, path: &str,
@@ -130,7 +116,7 @@ pub trait Resource: Clone + Send + 'static {
path.push('/'); path.push('/');
} }
let (mut props, mut invalid_props): (Vec<<Self::Prop as PropName>::Names>, Vec<_>) = let (mut props, mut 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()
@@ -155,7 +141,7 @@ pub trait Resource: Clone + Send + 'static {
vec![], vec![],
), ),
PropfindType::Prop(PropElement(valid_tags, invalid_tags)) => ( PropfindType::Prop(PropElement(valid_tags, invalid_tags)) => (
valid_tags.iter().unique().cloned().collect(), valid_tags.iter().cloned().collect(),
invalid_tags.to_owned(), invalid_tags.to_owned(),
), ),
}; };

View File

@@ -1,72 +1,3 @@
pub mod root; pub mod root;
pub use root::{RootResource, RootResourceService}; pub use root::{RootResource, RootResourceService};
#[cfg(test)]
pub mod test {
use crate::{
Error, Principal,
extensions::{CommonPropertiesExtension, CommonPropertiesProp},
namespace::NS_DAV,
privileges::UserPrivilegeSet,
resource::{PrincipalUri, Resource},
xml::{Resourcetype, ResourcetypeInner},
};
#[derive(Debug, Clone)]
pub struct TestPrincipal(pub String);
impl Principal for TestPrincipal {
fn get_id(&self) -> &str {
&self.0
}
}
impl Resource for TestPrincipal {
type Prop = CommonPropertiesProp;
type Error = Error;
type Principal = Self;
fn is_collection(&self) -> bool {
true
}
fn get_resourcetype(&self) -> crate::xml::Resourcetype {
Resourcetype(&[ResourcetypeInner(Some(NS_DAV), "collection")])
}
fn get_prop(
&self,
principal_uri: &impl crate::resource::PrincipalUri,
principal: &Self::Principal,
prop: &<Self::Prop as rustical_xml::PropName>::Names,
) -> Result<Self::Prop, Self::Error> {
<Self as CommonPropertiesExtension>::get_prop(self, principal_uri, principal, prop)
}
fn get_displayname(&self) -> Option<&str> {
Some(&self.0)
}
fn get_user_privileges(
&self,
principal: &Self::Principal,
) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_only(
principal.get_id() == self.get_id(),
))
}
}
#[derive(Debug, Clone)]
pub struct TestPrincipalUri;
impl PrincipalUri for TestPrincipalUri {
fn principal_collection(&self) -> String {
"/".to_owned()
}
fn principal_uri(&self, principal: &str) -> String {
format!("/{principal}/")
}
}
}

View File

@@ -105,33 +105,3 @@ impl<PRS: ResourceService<Principal = P> + Clone, P: Principal, PURI: PrincipalU
for RootResourceService<PRS, P, PURI> for RootResourceService<PRS, P, PURI>
{ {
} }
#[cfg(test)]
mod test {
use crate::{
resource::Resource,
resources::{
RootResource,
test::{TestPrincipal, TestPrincipalUri},
},
};
#[test]
fn test_root_resource() {
let resource = RootResource::<TestPrincipal, TestPrincipal>::default();
let propfind = RootResource::<TestPrincipal, TestPrincipal>::parse_propfind(
r#"<?xml version="1.0" encoding="UTF-8"?><propfind xmlns="DAV:"><allprop/></propfind>"#,
)
.unwrap();
let _response = resource
.propfind(
"/",
&propfind.prop,
propfind.include.as_ref(),
&TestPrincipalUri,
&TestPrincipal("user".to_owned()),
)
.unwrap();
}
}

View File

@@ -1,12 +1,12 @@
use rustical_xml::{XmlRootTag, XmlSerialize}; use rustical_xml::{XmlRootTag, XmlSerialize};
#[derive(XmlSerialize, XmlRootTag)] #[derive(XmlSerialize, XmlRootTag)]
#[xml(ns = "crate::namespace::NS_DAV", root = "error")] #[xml(ns = "crate::namespace::NS_DAV", root = b"error")]
#[xml(ns_prefix( #[xml(ns_prefix(
crate::namespace::NS_DAV = "", crate::namespace::NS_DAV = b"",
crate::namespace::NS_CARDDAV = "CARD", crate::namespace::NS_CARDDAV = b"CARD",
crate::namespace::NS_CALDAV = "CAL", crate::namespace::NS_CALDAV = b"CAL",
crate::namespace::NS_CALENDARSERVER = "CS", crate::namespace::NS_CALENDARSERVER = b"CS",
crate::namespace::NS_DAVPUSH = "PUSH" crate::namespace::NS_DAVPUSH = b"PUSH"
))] ))]
pub struct ErrorElement<'t, T: XmlSerialize>(#[xml(ty = "untagged")] pub &'t T); pub struct ErrorElement<'t, T: XmlSerialize>(#[xml(ty = "untagged")] pub &'t T);

View File

@@ -22,8 +22,8 @@ pub struct PropstatElement<PropType: XmlSerialize> {
fn xml_serialize_status( fn xml_serialize_status(
status: &StatusCode, status: &StatusCode,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&str>, tag: Option<&[u8]>,
namespaces: &HashMap<Namespace, &str>, namespaces: &HashMap<Namespace, &[u8]>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
XmlSerialize::serialize(&format!("HTTP/1.1 {}", status), ns, tag, namespaces, writer) XmlSerialize::serialize(&format!("HTTP/1.1 {}", status), ns, tag, namespaces, writer)
@@ -39,15 +39,8 @@ pub enum PropstatWrapper<T: XmlSerialize> {
// RFC 2518 // RFC 2518
// <!ELEMENT response (href, ((href*, status)|(propstat+)), // <!ELEMENT response (href, ((href*, status)|(propstat+)),
// responsedescription?) > // responsedescription?) >
#[derive(XmlSerialize, XmlRootTag)] #[derive(XmlSerialize)]
#[xml(ns = "crate::namespace::NS_DAV", root = "response")] #[xml(ns = "crate::namespace::NS_DAV")]
#[xml(ns_prefix(
crate::namespace::NS_DAV = "",
crate::namespace::NS_CARDDAV = "CARD",
crate::namespace::NS_CALDAV = "CAL",
crate::namespace::NS_CALENDARSERVER = "CS",
crate::namespace::NS_DAVPUSH = "PUSH"
))]
pub struct ResponseElement<PropstatType: XmlSerialize> { pub struct ResponseElement<PropstatType: XmlSerialize> {
pub href: String, pub href: String,
#[xml(serialize_with = "xml_serialize_optional_status")] #[xml(serialize_with = "xml_serialize_optional_status")]
@@ -59,8 +52,8 @@ pub struct ResponseElement<PropstatType: XmlSerialize> {
fn xml_serialize_optional_status( fn xml_serialize_optional_status(
val: &Option<StatusCode>, val: &Option<StatusCode>,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&str>, tag: Option<&[u8]>,
namespaces: &HashMap<Namespace, &str>, namespaces: &HashMap<Namespace, &[u8]>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
XmlSerialize::serialize( XmlSerialize::serialize(
@@ -86,18 +79,18 @@ impl<PT: XmlSerialize> Default for ResponseElement<PT> {
// <!ELEMENT multistatus (response+, responsedescription?) > // <!ELEMENT multistatus (response+, responsedescription?) >
// Extended by sync-token as specified in RFC 6578 // Extended by sync-token as specified in RFC 6578
#[derive(XmlSerialize, XmlRootTag)] #[derive(XmlSerialize, XmlRootTag)]
#[xml(root = "multistatus", ns = "crate::namespace::NS_DAV")] #[xml(root = b"multistatus", ns = "crate::namespace::NS_DAV")]
#[xml(ns_prefix( #[xml(ns_prefix(
crate::namespace::NS_DAV = "", crate::namespace::NS_DAV = b"",
crate::namespace::NS_CARDDAV = "CARD", crate::namespace::NS_CARDDAV = b"CARD",
crate::namespace::NS_CALDAV = "CAL", crate::namespace::NS_CALDAV = b"CAL",
crate::namespace::NS_CALENDARSERVER = "CS", crate::namespace::NS_CALENDARSERVER = b"CS",
crate::namespace::NS_DAVPUSH = "PUSH" crate::namespace::NS_DAVPUSH = b"PUSH"
))] ))]
pub struct MultistatusElement<PropType: XmlSerialize, MemberPropType: XmlSerialize> { pub struct MultistatusElement<PropType: XmlSerialize, MemberPropType: XmlSerialize> {
#[xml(rename = "response", flatten)] #[xml(rename = b"response", flatten)]
pub responses: Vec<ResponseElement<PropType>>, pub responses: Vec<ResponseElement<PropType>>,
#[xml(rename = "response", flatten)] #[xml(rename = b"response", flatten)]
pub member_responses: Vec<ResponseElement<MemberPropType>>, pub member_responses: Vec<ResponseElement<MemberPropType>>,
pub sync_token: Option<String>, pub sync_token: Option<String>,
} }

View File

@@ -7,7 +7,7 @@ use rustical_xml::XmlError;
use rustical_xml::XmlRootTag; use rustical_xml::XmlRootTag;
#[derive(Debug, Clone, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, Clone, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = "propfind", ns = "crate::namespace::NS_DAV")] #[xml(root = b"propfind", ns = "crate::namespace::NS_DAV")]
pub struct PropfindElement<PN: XmlDeserialize> { pub struct PropfindElement<PN: XmlDeserialize> {
#[xml(ty = "untagged")] #[xml(ty = "untagged")]
pub prop: PropfindType<PN>, pub prop: PropfindType<PN>,
@@ -66,9 +66,6 @@ impl<PN: XmlDeserialize> XmlDeserialize for PropElement<PN> {
Event::Text(_) | Event::CData(_) => { Event::Text(_) | Event::CData(_) => {
return Err(XmlError::UnsupportedEvent("Not expecting text here")); return Err(XmlError::UnsupportedEvent("Not expecting text here"));
} }
Event::GeneralRef(_) => {
return Err(::rustical_xml::XmlError::UnsupportedEvent("GeneralRef"));
}
Event::Decl(_) | Event::Comment(_) | Event::DocType(_) | Event::PI(_) => { /* ignore */ Event::Decl(_) | Event::Comment(_) | Event::DocType(_) | Event::PI(_) => { /* ignore */
} }
Event::End(_end) => { Event::End(_end) => {

View File

@@ -16,7 +16,7 @@ mod tests {
use super::{Resourcetype, ResourcetypeInner}; use super::{Resourcetype, ResourcetypeInner};
#[derive(XmlSerialize, XmlRootTag)] #[derive(XmlSerialize, XmlRootTag)]
#[xml(root = "document")] #[xml(root = b"document")]
struct Document { struct Document {
resourcetype: Resourcetype, resourcetype: Resourcetype,
} }

View File

@@ -60,7 +60,7 @@ pub struct NresultsElement(#[xml(ty = "text")] u64);
// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)> // <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
// <!-- DAV:limit defined in RFC 5323, Section 5.17 --> // <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
// <!-- DAV:prop defined in RFC 4918, Section 14.18 --> // <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
#[xml(ns = "crate::namespace::NS_DAV", root = "sync-collection")] #[xml(ns = "crate::namespace::NS_DAV", root = b"sync-collection")]
pub struct SyncCollectionRequest<PN: XmlDeserialize> { pub struct SyncCollectionRequest<PN: XmlDeserialize> {
#[xml(ns = "crate::namespace::NS_DAV")] #[xml(ns = "crate::namespace::NS_DAV")]
pub sync_token: String, pub sync_token: String,

View File

@@ -13,8 +13,8 @@ impl XmlSerialize for TagList {
fn serialize( fn serialize(
&self, &self,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&str>, tag: Option<&[u8]>,
namespaces: &HashMap<Namespace, &str>, namespaces: &HashMap<Namespace, &[u8]>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let prefix = ns let prefix = ns
@@ -22,18 +22,23 @@ impl XmlSerialize for TagList {
.unwrap_or(None) .unwrap_or(None)
.map(|prefix| { .map(|prefix| {
if !prefix.is_empty() { if !prefix.is_empty() {
format!("{prefix}:") [*prefix, b":"].concat()
} else { } else {
String::new() Vec::new()
} }
}); });
let has_prefix = prefix.is_some(); let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat()); let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname
.as_ref()
.map(|tagname| ::quick_xml::name::QName(tagname));
if let Some(tagname) = tagname.as_ref() { if let Some(qname) = &qname {
let mut bytes_start = BytesStart::new(tagname); let mut bytes_start = BytesStart::from(qname.to_owned());
if !has_prefix && let Some(ns) = &ns { if !has_prefix {
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref())); if let Some(ns) = &ns {
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
}
} }
writer.write_event(Event::Start(bytes_start))?; writer.write_event(Event::Start(bytes_start))?;
} }
@@ -46,8 +51,8 @@ impl XmlSerialize for TagList {
el.write_empty()?; el.write_empty()?;
} }
if let Some(tagname) = tagname.as_ref() { if let Some(qname) = &qname {
writer.write_event(Event::End(BytesEnd::new(tagname)))?; writer.write_event(Event::End(BytesEnd::from(qname.to_owned())))?;
} }
Ok(()) Ok(())
} }

View File

@@ -24,6 +24,7 @@ rustical_dav.workspace = true
rustical_store.workspace = true rustical_store.workspace = true
http.workspace = true http.workspace = true
base64.workspace = true base64.workspace = true
rand.workspace = true
ece.workspace = true ece.workspace = true
axum.workspace = true axum.workspace = true
openssl.workspace = true openssl.workspace = true

View File

@@ -25,10 +25,10 @@ pub struct ContentUpdate {
} }
#[derive(XmlSerialize, XmlRootTag, Debug)] #[derive(XmlSerialize, XmlRootTag, Debug)]
#[xml(root = "push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(root = b"push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")]
#[xml(ns_prefix( #[xml(ns_prefix(
rustical_dav::namespace::NS_DAVPUSH = "", rustical_dav::namespace::NS_DAVPUSH = b"",
rustical_dav::namespace::NS_DAV = "D", rustical_dav::namespace::NS_DAV = b"D",
))] ))]
struct PushMessage { struct PushMessage {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
@@ -183,7 +183,6 @@ impl<S: SubscriptionStore> DavPushController<S> {
header::CONTENT_TYPE, header::CONTENT_TYPE,
HeaderValue::from_static("application/octet-stream"), HeaderValue::from_static("application/octet-stream"),
); );
hdrs.insert("TTL", HeaderValue::from(60));
client.execute(request).await?; client.execute(request).await?;
Ok(()) Ok(())

View File

@@ -35,12 +35,12 @@ pub enum Trigger {
#[derive(XmlSerialize, XmlDeserialize, PartialEq, Clone, Debug)] #[derive(XmlSerialize, XmlDeserialize, PartialEq, Clone, Debug)]
pub struct ContentUpdate( pub struct ContentUpdate(
#[xml(rename = "depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth, #[xml(rename = b"depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth,
); );
#[derive(XmlSerialize, PartialEq, Clone, Debug)] #[derive(XmlSerialize, PartialEq, Clone, Debug)]
pub struct PropertyUpdate( pub struct PropertyUpdate(
#[xml(rename = "depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth, #[xml(rename = b"depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth,
); );
impl XmlDeserialize for PropertyUpdate { impl XmlDeserialize for PropertyUpdate {
@@ -51,8 +51,8 @@ impl XmlDeserialize for PropertyUpdate {
) -> Result<Self, rustical_xml::XmlError> { ) -> Result<Self, rustical_xml::XmlError> {
#[derive(XmlDeserialize, PartialEq, Clone, Debug)] #[derive(XmlDeserialize, PartialEq, Clone, Debug)]
struct FakePropertyUpdate( struct FakePropertyUpdate(
#[xml(rename = "depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth, #[xml(rename = b"depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth,
#[xml(rename = "prop", ns = "rustical_dav::namespace::NS_DAV")] pub Unparsed, #[xml(rename = b"prop", ns = "rustical_dav::namespace::NS_DAV")] pub Unparsed,
); );
let FakePropertyUpdate(depth, _) = FakePropertyUpdate::deserialize(reader, start, empty)?; let FakePropertyUpdate(depth, _) = FakePropertyUpdate::deserialize(reader, start, empty)?;
Ok(Self(depth)) Ok(Self(depth))

View File

@@ -17,7 +17,7 @@ pub struct WebPushSubscription {
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
pub struct SubscriptionPublicKey { pub struct SubscriptionPublicKey {
#[xml(ty = "attr", rename = "type")] #[xml(ty = "attr", rename = b"type")]
pub ty: String, pub ty: String,
#[xml(ty = "text")] #[xml(ty = "text")]
pub key: String, pub key: String,
@@ -33,7 +33,7 @@ pub struct SubscriptionElement {
pub struct TriggerElement(#[xml(ty = "untagged", flatten)] Vec<Trigger>); pub struct TriggerElement(#[xml(ty = "untagged", flatten)] Vec<Trigger>);
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
#[xml(root = "push-register")] #[xml(root = b"push-register")]
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
pub struct PushRegister { pub struct PushRegister {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]

View File

@@ -11,8 +11,9 @@
] ]
}, },
"imports": { "imports": {
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.5", "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.4",
"lit": "npm:lit@^3.3.1", "lit": "npm:lit@^3.2.1",
"vite": "npm:vite@^7.1.7" "vite": "npm:vite@^6.1.1",
"webdav": "npm:webdav@^5.8.0"
} }
} }

View File

@@ -1,276 +1,205 @@
{ {
"version": "5", "version": "4",
"specifiers": { "specifiers": {
"npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.7__picomatch@4.0.3", "npm:@deno/vite-plugin@^1.0.4": "1.0.4_vite@6.3.5__picomatch@4.0.2",
"npm:lit@^3.3.1": "3.3.1", "npm:lit@^3.2.1": "3.3.0",
"npm:vite@*": "7.1.7_picomatch@4.0.3", "npm:vite@*": "6.3.5_picomatch@4.0.2",
"npm:vite@^7.1.7": "7.1.7_picomatch@4.0.3" "npm:vite@^6.1.1": "6.3.5_picomatch@4.0.2",
"npm:webdav@^5.8.0": "5.8.0"
}, },
"npm": { "npm": {
"@deno/vite-plugin@1.0.5_vite@7.1.7__picomatch@4.0.3": { "@buttercup/fetch@0.2.1": {
"integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==", "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==",
"dependencies": [
"node-fetch"
]
},
"@deno/vite-plugin@1.0.4_vite@6.3.5__picomatch@4.0.2": {
"integrity": "sha512-xg8YT8Wn2sGXSnJgiGTpBGX1Dov0c6fd1rAp8VsfrCUtyBRRWzwVMAnd3fQ4yq8h7LSVvJUxEFN4U421k/DQLA==",
"dependencies": [ "dependencies": [
"vite" "vite"
] ]
}, },
"@esbuild/aix-ppc64@0.25.10": { "@esbuild/aix-ppc64@0.25.5": {
"integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="
"os": ["aix"],
"cpu": ["ppc64"]
}, },
"@esbuild/android-arm64@0.25.10": { "@esbuild/android-arm64@0.25.5": {
"integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="
"os": ["android"],
"cpu": ["arm64"]
}, },
"@esbuild/android-arm@0.25.10": { "@esbuild/android-arm@0.25.5": {
"integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="
"os": ["android"],
"cpu": ["arm"]
}, },
"@esbuild/android-x64@0.25.10": { "@esbuild/android-x64@0.25.5": {
"integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="
"os": ["android"],
"cpu": ["x64"]
}, },
"@esbuild/darwin-arm64@0.25.10": { "@esbuild/darwin-arm64@0.25.5": {
"integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="
"os": ["darwin"],
"cpu": ["arm64"]
}, },
"@esbuild/darwin-x64@0.25.10": { "@esbuild/darwin-x64@0.25.5": {
"integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="
"os": ["darwin"],
"cpu": ["x64"]
}, },
"@esbuild/freebsd-arm64@0.25.10": { "@esbuild/freebsd-arm64@0.25.5": {
"integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="
"os": ["freebsd"],
"cpu": ["arm64"]
}, },
"@esbuild/freebsd-x64@0.25.10": { "@esbuild/freebsd-x64@0.25.5": {
"integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="
"os": ["freebsd"],
"cpu": ["x64"]
}, },
"@esbuild/linux-arm64@0.25.10": { "@esbuild/linux-arm64@0.25.5": {
"integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="
"os": ["linux"],
"cpu": ["arm64"]
}, },
"@esbuild/linux-arm@0.25.10": { "@esbuild/linux-arm@0.25.5": {
"integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="
"os": ["linux"],
"cpu": ["arm"]
}, },
"@esbuild/linux-ia32@0.25.10": { "@esbuild/linux-ia32@0.25.5": {
"integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="
"os": ["linux"],
"cpu": ["ia32"]
}, },
"@esbuild/linux-loong64@0.25.10": { "@esbuild/linux-loong64@0.25.5": {
"integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="
"os": ["linux"],
"cpu": ["loong64"]
}, },
"@esbuild/linux-mips64el@0.25.10": { "@esbuild/linux-mips64el@0.25.5": {
"integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="
"os": ["linux"],
"cpu": ["mips64el"]
}, },
"@esbuild/linux-ppc64@0.25.10": { "@esbuild/linux-ppc64@0.25.5": {
"integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="
"os": ["linux"],
"cpu": ["ppc64"]
}, },
"@esbuild/linux-riscv64@0.25.10": { "@esbuild/linux-riscv64@0.25.5": {
"integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="
"os": ["linux"],
"cpu": ["riscv64"]
}, },
"@esbuild/linux-s390x@0.25.10": { "@esbuild/linux-s390x@0.25.5": {
"integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="
"os": ["linux"],
"cpu": ["s390x"]
}, },
"@esbuild/linux-x64@0.25.10": { "@esbuild/linux-x64@0.25.5": {
"integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="
"os": ["linux"],
"cpu": ["x64"]
}, },
"@esbuild/netbsd-arm64@0.25.10": { "@esbuild/netbsd-arm64@0.25.5": {
"integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="
"os": ["netbsd"],
"cpu": ["arm64"]
}, },
"@esbuild/netbsd-x64@0.25.10": { "@esbuild/netbsd-x64@0.25.5": {
"integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="
"os": ["netbsd"],
"cpu": ["x64"]
}, },
"@esbuild/openbsd-arm64@0.25.10": { "@esbuild/openbsd-arm64@0.25.5": {
"integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="
"os": ["openbsd"],
"cpu": ["arm64"]
}, },
"@esbuild/openbsd-x64@0.25.10": { "@esbuild/openbsd-x64@0.25.5": {
"integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="
"os": ["openbsd"],
"cpu": ["x64"]
}, },
"@esbuild/openharmony-arm64@0.25.10": { "@esbuild/sunos-x64@0.25.5": {
"integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="
"os": ["openharmony"],
"cpu": ["arm64"]
}, },
"@esbuild/sunos-x64@0.25.10": { "@esbuild/win32-arm64@0.25.5": {
"integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="
"os": ["sunos"],
"cpu": ["x64"]
}, },
"@esbuild/win32-arm64@0.25.10": { "@esbuild/win32-ia32@0.25.5": {
"integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="
"os": ["win32"],
"cpu": ["arm64"]
}, },
"@esbuild/win32-ia32@0.25.10": { "@esbuild/win32-x64@0.25.5": {
"integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="
"os": ["win32"],
"cpu": ["ia32"]
}, },
"@esbuild/win32-x64@0.25.10": { "@lit-labs/ssr-dom-shim@1.3.0": {
"integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ=="
"os": ["win32"],
"cpu": ["x64"]
}, },
"@lit-labs/ssr-dom-shim@1.4.0": { "@lit/reactive-element@2.1.0": {
"integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==" "integrity": "sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==",
},
"@lit/reactive-element@2.1.1": {
"integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==",
"dependencies": [ "dependencies": [
"@lit-labs/ssr-dom-shim" "@lit-labs/ssr-dom-shim"
] ]
}, },
"@rollup/rollup-android-arm-eabi@4.52.2": { "@rollup/rollup-android-arm-eabi@4.43.0": {
"integrity": "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==", "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw=="
"os": ["android"],
"cpu": ["arm"]
}, },
"@rollup/rollup-android-arm64@4.52.2": { "@rollup/rollup-android-arm64@4.43.0": {
"integrity": "sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==", "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA=="
"os": ["android"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-darwin-arm64@4.52.2": { "@rollup/rollup-darwin-arm64@4.43.0": {
"integrity": "sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==", "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A=="
"os": ["darwin"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-darwin-x64@4.52.2": { "@rollup/rollup-darwin-x64@4.43.0": {
"integrity": "sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==", "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg=="
"os": ["darwin"],
"cpu": ["x64"]
}, },
"@rollup/rollup-freebsd-arm64@4.52.2": { "@rollup/rollup-freebsd-arm64@4.43.0": {
"integrity": "sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==", "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ=="
"os": ["freebsd"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-freebsd-x64@4.52.2": { "@rollup/rollup-freebsd-x64@4.43.0": {
"integrity": "sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==", "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg=="
"os": ["freebsd"],
"cpu": ["x64"]
}, },
"@rollup/rollup-linux-arm-gnueabihf@4.52.2": { "@rollup/rollup-linux-arm-gnueabihf@4.43.0": {
"integrity": "sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==", "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw=="
"os": ["linux"],
"cpu": ["arm"]
}, },
"@rollup/rollup-linux-arm-musleabihf@4.52.2": { "@rollup/rollup-linux-arm-musleabihf@4.43.0": {
"integrity": "sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==", "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw=="
"os": ["linux"],
"cpu": ["arm"]
}, },
"@rollup/rollup-linux-arm64-gnu@4.52.2": { "@rollup/rollup-linux-arm64-gnu@4.43.0": {
"integrity": "sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==", "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA=="
"os": ["linux"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-linux-arm64-musl@4.52.2": { "@rollup/rollup-linux-arm64-musl@4.43.0": {
"integrity": "sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==", "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA=="
"os": ["linux"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-linux-loong64-gnu@4.52.2": { "@rollup/rollup-linux-loongarch64-gnu@4.43.0": {
"integrity": "sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==", "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg=="
"os": ["linux"],
"cpu": ["loong64"]
}, },
"@rollup/rollup-linux-ppc64-gnu@4.52.2": { "@rollup/rollup-linux-powerpc64le-gnu@4.43.0": {
"integrity": "sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==", "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw=="
"os": ["linux"],
"cpu": ["ppc64"]
}, },
"@rollup/rollup-linux-riscv64-gnu@4.52.2": { "@rollup/rollup-linux-riscv64-gnu@4.43.0": {
"integrity": "sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==", "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g=="
"os": ["linux"],
"cpu": ["riscv64"]
}, },
"@rollup/rollup-linux-riscv64-musl@4.52.2": { "@rollup/rollup-linux-riscv64-musl@4.43.0": {
"integrity": "sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==", "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q=="
"os": ["linux"],
"cpu": ["riscv64"]
}, },
"@rollup/rollup-linux-s390x-gnu@4.52.2": { "@rollup/rollup-linux-s390x-gnu@4.43.0": {
"integrity": "sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==", "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw=="
"os": ["linux"],
"cpu": ["s390x"]
}, },
"@rollup/rollup-linux-x64-gnu@4.52.2": { "@rollup/rollup-linux-x64-gnu@4.43.0": {
"integrity": "sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==", "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ=="
"os": ["linux"],
"cpu": ["x64"]
}, },
"@rollup/rollup-linux-x64-musl@4.52.2": { "@rollup/rollup-linux-x64-musl@4.43.0": {
"integrity": "sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==", "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ=="
"os": ["linux"],
"cpu": ["x64"]
}, },
"@rollup/rollup-openharmony-arm64@4.52.2": { "@rollup/rollup-win32-arm64-msvc@4.43.0": {
"integrity": "sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==", "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw=="
"os": ["openharmony"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-win32-arm64-msvc@4.52.2": { "@rollup/rollup-win32-ia32-msvc@4.43.0": {
"integrity": "sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==", "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw=="
"os": ["win32"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-win32-ia32-msvc@4.52.2": { "@rollup/rollup-win32-x64-msvc@4.43.0": {
"integrity": "sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==", "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw=="
"os": ["win32"],
"cpu": ["ia32"]
}, },
"@rollup/rollup-win32-x64-gnu@4.52.2": { "@types/estree@1.0.7": {
"integrity": "sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
"os": ["win32"],
"cpu": ["x64"]
},
"@rollup/rollup-win32-x64-msvc@4.52.2": {
"integrity": "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==",
"os": ["win32"],
"cpu": ["x64"]
},
"@types/estree@1.0.8": {
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
}, },
"@types/trusted-types@2.0.7": { "@types/trusted-types@2.0.7": {
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
}, },
"esbuild@0.25.10": { "balanced-match@1.0.2": {
"integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
"optionalDependencies": [ },
"base-64@1.0.0": {
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
},
"brace-expansion@2.0.2": {
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dependencies": [
"balanced-match"
]
},
"byte-length@1.0.2": {
"integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q=="
},
"charenc@0.0.2": {
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="
},
"crypt@0.0.2": {
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="
},
"data-uri-to-buffer@4.0.1": {
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
},
"entities@6.0.1": {
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="
},
"esbuild@0.25.5": {
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"dependencies": [
"@esbuild/aix-ppc64", "@esbuild/aix-ppc64",
"@esbuild/android-arm", "@esbuild/android-arm",
"@esbuild/android-arm64", "@esbuild/android-arm64",
@@ -292,75 +221,128 @@
"@esbuild/netbsd-x64", "@esbuild/netbsd-x64",
"@esbuild/openbsd-arm64", "@esbuild/openbsd-arm64",
"@esbuild/openbsd-x64", "@esbuild/openbsd-x64",
"@esbuild/openharmony-arm64",
"@esbuild/sunos-x64", "@esbuild/sunos-x64",
"@esbuild/win32-arm64", "@esbuild/win32-arm64",
"@esbuild/win32-ia32", "@esbuild/win32-ia32",
"@esbuild/win32-x64" "@esbuild/win32-x64"
], ]
"scripts": true,
"bin": true
}, },
"fdir@6.5.0_picomatch@4.0.3": { "fast-xml-parser@4.5.3": {
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
"dependencies": [
"strnum"
]
},
"fdir@6.4.6_picomatch@4.0.2": {
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
"dependencies": [ "dependencies": [
"picomatch"
],
"optionalPeers": [
"picomatch" "picomatch"
] ]
}, },
"fsevents@2.3.3": { "fetch-blob@3.2.0": {
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"os": ["darwin"], "dependencies": [
"scripts": true "node-domexception",
"web-streams-polyfill"
]
}, },
"lit-element@4.2.1": { "formdata-polyfill@4.0.10": {
"integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dependencies": [
"fetch-blob"
]
},
"fsevents@2.3.3": {
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="
},
"hot-patcher@2.0.1": {
"integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q=="
},
"is-buffer@1.1.6": {
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"layerr@3.0.0": {
"integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA=="
},
"lit-element@4.2.0": {
"integrity": "sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==",
"dependencies": [ "dependencies": [
"@lit-labs/ssr-dom-shim", "@lit-labs/ssr-dom-shim",
"@lit/reactive-element", "@lit/reactive-element",
"lit-html" "lit-html"
] ]
}, },
"lit-html@3.3.1": { "lit-html@3.3.0": {
"integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", "integrity": "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==",
"dependencies": [ "dependencies": [
"@types/trusted-types" "@types/trusted-types"
] ]
}, },
"lit@3.3.1": { "lit@3.3.0": {
"integrity": "sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==", "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==",
"dependencies": [ "dependencies": [
"@lit/reactive-element", "@lit/reactive-element",
"lit-element", "lit-element",
"lit-html" "lit-html"
] ]
}, },
"md5@2.3.0": {
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"dependencies": [
"charenc",
"crypt",
"is-buffer"
]
},
"minimatch@9.0.5": {
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dependencies": [
"brace-expansion"
]
},
"nanoid@3.3.11": { "nanoid@3.3.11": {
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
"bin": true },
"nested-property@4.0.0": {
"integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA=="
},
"node-domexception@1.0.0": {
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
},
"node-fetch@3.3.2": {
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dependencies": [
"data-uri-to-buffer",
"fetch-blob",
"formdata-polyfill"
]
},
"path-posix@1.0.0": {
"integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA=="
}, },
"picocolors@1.1.1": { "picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
}, },
"picomatch@4.0.3": { "picomatch@4.0.2": {
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="
}, },
"postcss@8.5.6": { "postcss@8.5.5": {
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==",
"dependencies": [ "dependencies": [
"nanoid", "nanoid",
"picocolors", "picocolors",
"source-map-js" "source-map-js"
] ]
}, },
"rollup@4.52.2": { "querystringify@2.2.0": {
"integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"requires-port@1.0.0": {
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"rollup@4.43.0": {
"integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==",
"dependencies": [ "dependencies": [
"@types/estree"
],
"optionalDependencies": [
"@rollup/rollup-android-arm-eabi", "@rollup/rollup-android-arm-eabi",
"@rollup/rollup-android-arm64", "@rollup/rollup-android-arm64",
"@rollup/rollup-darwin-arm64", "@rollup/rollup-darwin-arm64",
@@ -371,53 +353,84 @@
"@rollup/rollup-linux-arm-musleabihf", "@rollup/rollup-linux-arm-musleabihf",
"@rollup/rollup-linux-arm64-gnu", "@rollup/rollup-linux-arm64-gnu",
"@rollup/rollup-linux-arm64-musl", "@rollup/rollup-linux-arm64-musl",
"@rollup/rollup-linux-loong64-gnu", "@rollup/rollup-linux-loongarch64-gnu",
"@rollup/rollup-linux-ppc64-gnu", "@rollup/rollup-linux-powerpc64le-gnu",
"@rollup/rollup-linux-riscv64-gnu", "@rollup/rollup-linux-riscv64-gnu",
"@rollup/rollup-linux-riscv64-musl", "@rollup/rollup-linux-riscv64-musl",
"@rollup/rollup-linux-s390x-gnu", "@rollup/rollup-linux-s390x-gnu",
"@rollup/rollup-linux-x64-gnu", "@rollup/rollup-linux-x64-gnu",
"@rollup/rollup-linux-x64-musl", "@rollup/rollup-linux-x64-musl",
"@rollup/rollup-openharmony-arm64",
"@rollup/rollup-win32-arm64-msvc", "@rollup/rollup-win32-arm64-msvc",
"@rollup/rollup-win32-ia32-msvc", "@rollup/rollup-win32-ia32-msvc",
"@rollup/rollup-win32-x64-gnu",
"@rollup/rollup-win32-x64-msvc", "@rollup/rollup-win32-x64-msvc",
"@types/estree",
"fsevents" "fsevents"
], ]
"bin": true
}, },
"source-map-js@1.2.1": { "source-map-js@1.2.1": {
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
}, },
"tinyglobby@0.2.15_picomatch@4.0.3": { "strnum@1.1.2": {
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="
},
"tinyglobby@0.2.14_picomatch@4.0.2": {
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"dependencies": [ "dependencies": [
"fdir", "fdir",
"picomatch" "picomatch"
] ]
}, },
"vite@7.1.7_picomatch@4.0.3": { "url-join@5.0.0": {
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="
},
"url-parse@1.5.10": {
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dependencies": [
"querystringify",
"requires-port"
]
},
"vite@6.3.5_picomatch@4.0.2": {
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dependencies": [ "dependencies": [
"esbuild", "esbuild",
"fdir", "fdir",
"fsevents",
"picomatch", "picomatch",
"postcss", "postcss",
"rollup", "rollup",
"tinyglobby" "tinyglobby"
], ]
"optionalDependencies": [ },
"fsevents" "web-streams-polyfill@3.3.3": {
], "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="
"bin": true },
"webdav@5.8.0": {
"integrity": "sha512-iuFG7NamJ41Oshg4930iQgfIpRrUiatPWIekeznYgEf2EOraTRcDPTjy7gIOMtkdpKTaqPk1E68NO5PAGtJahA==",
"dependencies": [
"@buttercup/fetch",
"base-64",
"byte-length",
"entities",
"fast-xml-parser",
"hot-patcher",
"layerr",
"md5",
"minimatch",
"nested-property",
"node-fetch",
"path-posix",
"url-join",
"url-parse"
]
} }
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"npm:@deno/vite-plugin@^1.0.5", "npm:@deno/vite-plugin@^1.0.4",
"npm:lit@^3.3.1", "npm:lit@^3.2.1",
"npm:vite@^7.1.7" "npm:vite@^6.1.1",
"npm:webdav@^5.8.0"
] ]
} }
} }

View File

@@ -1,6 +1,7 @@
import { html, LitElement } from "lit"; 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 { escapeXml } from "."; import { escapeXml } from ".";
@customElement("create-addressbook-form") @customElement("create-addressbook-form")
@@ -14,12 +15,14 @@ export class CreateAddressbookForm extends LitElement {
return this return this
} }
client = createClient("/carddav")
@property() @property()
user: string = '' user: string = ''
@property() @property()
principal: string = '' principal: string = ''
@property() @property()
addr_id: string = self.crypto.randomUUID() addr_id: string = ''
@property() @property()
displayname: string = '' displayname: string = ''
@property() @property()
@@ -46,7 +49,7 @@ export class CreateAddressbookForm extends LitElement {
<br> <br>
<label> <label>
id id
<input type="text" name="id" value=${this.addr_id} @change=${e => this.addr_id = e.target.value} /> <input type="text" name="id" @change=${e => this.addr_id = e.target.value} />
</label> </label>
<br> <br>
<label> <label>
@@ -77,12 +80,8 @@ export class CreateAddressbookForm extends LitElement {
alert("Empty displayname") alert("Empty displayname")
return return
} }
let response = await fetch(`/carddav/principal/${this.principal || this.user}/${this.addr_id}`, { await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, {
method: 'MKCOL', data: `
headers: {
'Content-Type': 'application/xml'
},
body: `
<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>
@@ -92,14 +91,7 @@ export class CreateAddressbookForm extends LitElement {
</set> </set>
</mkcol> </mkcol>
` `
}) })
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload() window.location.reload()
return null return null
} }

View File

@@ -1,6 +1,7 @@
import { html, LitElement } from "lit"; 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 { escapeXml } from "."; import { escapeXml } from ".";
@customElement("create-calendar-form") @customElement("create-calendar-form")
@@ -13,19 +14,19 @@ export class CreateCalendarForm extends LitElement {
return this return this
} }
client = createClient("/caldav")
@property() @property()
user: string = '' user: string = ''
@property() @property()
principal: string = '' principal: string = ''
@property() @property()
cal_id: string = self.crypto.randomUUID() cal_id: string = ''
@property() @property()
displayname: string = '' displayname: string = ''
@property() @property()
description: string = '' description: string = ''
@property() @property()
timezone_id: string = ''
@property()
color: string = '' color: string = ''
@property() @property()
isSubscription: boolean = false isSubscription: boolean = false
@@ -37,6 +38,7 @@ export class CreateCalendarForm extends LitElement {
dialog: Ref<HTMLDialogElement> = createRef() dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef() form: Ref<HTMLFormElement> = createRef()
override render() { override render() {
return html` return html`
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button> <button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
@@ -55,7 +57,7 @@ export class CreateCalendarForm extends LitElement {
<br> <br>
<label> <label>
id id
<input type="text" name="id" value=${this.cal_id} @change=${e => this.cal_id = e.target.value} /> <input type="text" name="id" @change=${e => this.cal_id = e.target.value} />
</label> </label>
<br> <br>
<label> <label>
@@ -63,11 +65,6 @@ export class CreateCalendarForm extends LitElement {
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} /> <input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label> </label>
<br> <br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
</label>
<br>
<label> <label>
Description Description
<input type="text" name="description" @change=${e => this.description = e.target.value} /> <input type="text" name="description" @change=${e => this.description = e.target.value} />
@@ -122,18 +119,12 @@ export class CreateCalendarForm extends LitElement {
alert("No calendar components selected") alert("No calendar components selected")
return return
} }
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.cal_id}`, {
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, { data: `
method: 'MKCOL',
headers: {
'Content-Type': 'application/xml'
},
body: `
<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>${escapeXml(this.displayname)}</displayname> <displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ''}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''} ${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''} ${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
${(this.isSubscription && this.subscriptionUrl) ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ''} ${(this.isSubscription && this.subscriptionUrl) ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ''}
@@ -145,11 +136,6 @@ export class CreateCalendarForm extends LitElement {
</mkcol> </mkcol>
` `
}) })
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload() window.location.reload()
return null return null
} }

View File

@@ -17,7 +17,7 @@ export class DeleteButton extends LitElement {
} }
protected render() { protected render() {
let text = this.trash ? 'Trash' : 'Delete' let text = this.trash ? 'Move to trash' : 'Delete'
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>` return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
} }

View File

@@ -1,103 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { escapeXml } from ".";
@customElement("edit-addressbook-form")
export class EditAddressbookForm extends LitElement {
constructor() {
super()
}
protected override createRenderRoot() {
return this
}
@property()
principal: string = ''
@property()
addr_id: string = ''
@property()
displayname: string = ''
@property()
description: string = ''
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${ref(this.dialog)}>
<h3>Edit addressbook</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label>
Displayname
<input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} />
</label>
<br>
<button type="submit">Submit</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
</form>
</dialog>
`
}
async submit(e: SubmitEvent) {
e.preventDefault()
if (!this.principal) {
alert("Empty principal")
return
}
if (!this.addr_id) {
alert("Empty id")
return
}
if (!this.displayname) {
alert("Empty displayname")
return
}
let response = await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, {
method: 'PROPPATCH',
headers: {
'Content-Type': 'application/xml'
},
body: `
<propertyupdate xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CARD:addressbook-description>${escapeXml(this.description)}</CARD:addressbook-description>` : ''}
</prop>
</set>
<remove>
<prop>
${!this.description ? '<CARD:calendar-description />' : ''}
</prop>
</remove>
</propertyupdate>
`
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}
}
declare global {
interface HTMLElementTagNameMap {
'edit-addressbook-form': EditAddressbookForm
}
}

View File

@@ -1,143 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { escapeXml } from ".";
@customElement("edit-calendar-form")
export class EditCalendarForm extends LitElement {
constructor() {
super()
}
protected override createRenderRoot() {
return this
}
@property()
principal: string
@property()
cal_id: string
@property()
displayname: string = ''
@property()
description: string = ''
@property()
timezone_id: string = ''
@property()
color: string = ''
@property({
converter: {
fromAttribute: (value, _type) => new Set(value ? JSON.parse(value) : []),
toAttribute: (value, _type) => JSON.stringify(value)
}
})
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${ref(this.dialog)}>
<h3>Edit calendar</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label>
Displayname
<input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" .value=${this.color} @change=${e => this.color = e.target.value} />
</label>
<br>
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
<label>
Support ${comp}
<input type="checkbox" value=${comp} ?checked=${this.components.has(comp)} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
</label>
<br>
`)}
<br>
<button type="submit">Submit</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
</form>
</dialog>
`
}
async submit(e: SubmitEvent) {
e.preventDefault()
if (!this.principal) {
alert("Empty principal")
return
}
if (!this.cal_id) {
alert("Empty id")
return
}
if (!this.displayname) {
alert("Empty displayname")
return
}
if (!this.components.size) {
alert("No calendar components selected")
return
}
let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
method: 'PROPPATCH',
headers: {
'Content-Type': 'application/xml'
},
body: `
<propertyupdate xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ''}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
<CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${escapeXml(comp)}" />`).join('\n')}
</CAL:supported-calendar-component-set>
</prop>
</set>
<remove>
<prop>
${!this.timezone_id ? `<CAL:calendar-timezone-id />` : ''}
${!this.description ? '<CAL:calendar-description />' : ''}
${!this.color ? '<ICAL:calendar-color />' : ''}
</prop>
</remove>
</propertyupdate>
`
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}
}
declare global {
interface HTMLElementTagNameMap {
'edit-calendar-form': EditCalendarForm
}
}

View File

@@ -1,92 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
@customElement("import-addressbook-form")
export class ImportAddressbookForm extends LitElement {
constructor() {
super()
}
protected override createRenderRoot() {
return this
}
@property()
user: string = ''
@property()
principal: string
@property()
addressbook_id: string = self.crypto.randomUUID()
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
file: File;
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Import addressbook</button>
<dialog ${ref(this.dialog)}>
<h3>Import addressbook</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label>
principal (for group addressbook)
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map(membership => html`
<option value=${membership}>${membership}</option>
`)}
</select>
</label>
<br>
<label>
id
<input type="text" name="id" value=${this.addressbook_id} @change=${e => this.addressbook_id = e.target.value} />
</label>
<br>
<label>
file
<input type="file" accept="text/vcard" name="file" @change=${e => this.file = e.target.files[0]} />
</label>
<button type="submit">Import</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
</form>
</dialog>
`
}
async submit(e: SubmitEvent) {
e.preventDefault()
this.principal ||= this.user
if (!this.principal) {
alert("Empty principal")
return
}
if (!this.addressbook_id) {
alert("Empty id")
return
}
let response = await fetch(`/carddav/principal/${this.principal}/${this.addressbook_id}`, {
method: 'IMPORT',
headers: {
'Content-Type': 'text/vcard'
},
body: this.file,
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}
}
declare global {
interface HTMLElementTagNameMap {
'import-addressbook-form': ImportAddressbookForm
}
}

View File

@@ -1,92 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
@customElement("import-calendar-form")
export class ImportCalendarForm extends LitElement {
constructor() {
super()
}
protected override createRenderRoot() {
return this
}
@property()
user: string = ''
@property()
principal: string
@property()
cal_id: string = self.crypto.randomUUID()
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
file: File;
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Import calendar</button>
<dialog ${ref(this.dialog)}>
<h3>Import calendar</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label>
principal (for group calendars)
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map(membership => html`
<option value=${membership}>${membership}</option>
`)}
</select>
</label>
<br>
<label>
id
<input type="text" name="id" value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
</label>
<br>
<label>
file
<input type="file" accept="text/calendar" name="file" @change=${e => this.file = e.target.files[0]} />
</label>
<button type="submit">Import</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
</form>
</dialog>
`
}
async submit(e: SubmitEvent) {
e.preventDefault()
this.principal ||= this.user
if (!this.principal) {
alert("Empty principal")
return
}
if (!this.cal_id) {
alert("Empty id")
return
}
let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
method: 'IMPORT',
headers: {
'Content-Type': 'text/calendar'
},
body: this.file,
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}
}
declare global {
interface HTMLElementTagNameMap {
'import-calendar-form': ImportCalendarForm
}
}

View File

@@ -15,11 +15,7 @@ export default defineConfig({
rollupOptions: { rollupOptions: {
input: [ input: [
"lib/create-calendar-form.ts", "lib/create-calendar-form.ts",
"lib/edit-calendar-form.ts",
"lib/import-calendar-form.ts",
"lib/create-addressbook-form.ts", "lib/create-addressbook-form.ts",
"lib/edit-addressbook-form.ts",
"lib/import-addressbook-form.ts",
"lib/delete-button.ts", "lib/delete-button.ts",
], ],
output: { output: {
@@ -27,6 +23,7 @@ export default defineConfig({
format: "es", format: "es",
manualChunks: { manualChunks: {
lit: ["lit"], lit: ["lit"],
webdav: ["webdav"],
} }
} }
}, },

View File

@@ -1,7 +1,7 @@
import { i, x } from "./lit-DkXrt_Iv.mjs"; import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs"; import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs"; import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.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;
var __decorateClass = (decorators, target, key, kind) => { var __decorateClass = (decorators, target, key, kind) => {
@@ -15,9 +15,10 @@ var __decorateClass = (decorators, target, key, kind) => {
let CreateAddressbookForm = class extends i { let CreateAddressbookForm = class extends i {
constructor() { constructor() {
super(); super();
this.client = an("/carddav");
this.user = ""; this.user = "";
this.principal = ""; this.principal = "";
this.addr_id = self.crypto.randomUUID(); this.addr_id = "";
this.displayname = ""; this.displayname = "";
this.description = ""; this.description = "";
this.dialog = e(); this.dialog = e();
@@ -44,7 +45,7 @@ let CreateAddressbookForm = class extends i {
<br> <br>
<label> <label>
id id
<input type="text" name="id" value=${this.addr_id} @change=${(e2) => this.addr_id = e2.target.value} /> <input type="text" name="id" @change=${(e2) => this.addr_id = e2.target.value} />
</label> </label>
<br> <br>
<label> <label>
@@ -78,12 +79,8 @@ let CreateAddressbookForm = class extends i {
alert("Empty displayname"); alert("Empty displayname");
return; return;
} }
let response = await fetch(`/carddav/principal/${this.principal || this.user}/${this.addr_id}`, { await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, {
method: "MKCOL", data: `
headers: {
"Content-Type": "application/xml"
},
body: `
<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>
@@ -94,10 +91,6 @@ let CreateAddressbookForm = class extends i {
</mkcol> </mkcol>
` `
}); });
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload(); window.location.reload();
return null; return null;
} }

View File

@@ -1,7 +1,7 @@
import { i, x } from "./lit-DkXrt_Iv.mjs"; import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs"; import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs"; import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.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;
var __decorateClass = (decorators, target, key, kind) => { var __decorateClass = (decorators, target, key, kind) => {
@@ -15,12 +15,12 @@ var __decorateClass = (decorators, target, key, kind) => {
let CreateCalendarForm = class extends i { let CreateCalendarForm = class extends i {
constructor() { constructor() {
super(); super();
this.client = an("/caldav");
this.user = ""; this.user = "";
this.principal = ""; this.principal = "";
this.cal_id = self.crypto.randomUUID(); this.cal_id = "";
this.displayname = ""; this.displayname = "";
this.description = ""; this.description = "";
this.timezone_id = "";
this.color = ""; this.color = "";
this.isSubscription = false; this.isSubscription = false;
this.subscriptionUrl = ""; this.subscriptionUrl = "";
@@ -49,7 +49,7 @@ let CreateCalendarForm = class extends i {
<br> <br>
<label> <label>
id id
<input type="text" name="id" value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} /> <input type="text" name="id" @change=${(e2) => this.cal_id = e2.target.value} />
</label> </label>
<br> <br>
<label> <label>
@@ -57,11 +57,6 @@ let CreateCalendarForm = class extends i {
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} /> <input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label> </label>
<br> <br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
</label>
<br>
<label> <label>
Description Description
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} /> <input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
@@ -119,17 +114,12 @@ let CreateCalendarForm = class extends i {
alert("No calendar components selected"); alert("No calendar components selected");
return; return;
} }
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, { await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.cal_id}`, {
method: "MKCOL", data: `
headers: {
"Content-Type": "application/xml"
},
body: `
<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>${escapeXml(this.displayname)}</displayname> <displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ""}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""} ${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""} ${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
${this.isSubscription && this.subscriptionUrl ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ""} ${this.isSubscription && this.subscriptionUrl ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ""}
@@ -141,10 +131,6 @@ let CreateCalendarForm = class extends i {
</mkcol> </mkcol>
` `
}); });
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload(); window.location.reload();
return null; return null;
} }
@@ -164,9 +150,6 @@ __decorateClass([
__decorateClass([ __decorateClass([
n$1() n$1()
], CreateCalendarForm.prototype, "description", 2); ], CreateCalendarForm.prototype, "description", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "timezone_id", 2);
__decorateClass([ __decorateClass([
n$1() n$1()
], CreateCalendarForm.prototype, "color", 2); ], CreateCalendarForm.prototype, "color", 2);

View File

@@ -1,5 +1,5 @@
import { i, x } from "./lit-DkXrt_Iv.mjs"; import { i, x } from "./lit-z6_uA4GX.mjs";
import { n, t } from "./property-B8WoKf1Y.mjs"; import { n, t } from "./property-D0NJdseG.mjs";
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => { var __decorateClass = (decorators, target, key, kind) => {
@@ -19,7 +19,7 @@ let DeleteButton = class extends i {
return this; return this;
} }
render() { render() {
let text = this.trash ? "Trash" : "Delete"; let text = this.trash ? "Move to trash" : "Delete";
return x`<button class="delete" @click=${(e) => this._onClick(e)}>${text}</button>`; return x`<button class="delete" @click=${(e) => this._onClick(e)}>${text}</button>`;
} }
async _onClick(event) { async _onClick(event) {

View File

@@ -1,114 +0,0 @@
import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
if (decorator = decorators[i2])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let EditAddressbookForm = class extends i {
constructor() {
super();
this.principal = "";
this.addr_id = "";
this.displayname = "";
this.description = "";
this.dialog = e();
this.form = e();
}
createRenderRoot() {
return this;
}
render() {
return x`
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${n(this.dialog)}>
<h3>Edit addressbook</h3>
<form @submit=${this.submit} ${n(this.form)}>
<label>
Displayname
<input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
</label>
<br>
<button type="submit">Submit</button>
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}} class="cancel">Cancel</button>
</form>
</dialog>
`;
}
async submit(e2) {
e2.preventDefault();
if (!this.principal) {
alert("Empty principal");
return;
}
if (!this.addr_id) {
alert("Empty id");
return;
}
if (!this.displayname) {
alert("Empty displayname");
return;
}
let response = await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, {
method: "PROPPATCH",
headers: {
"Content-Type": "application/xml"
},
body: `
<propertyupdate xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CARD:addressbook-description>${escapeXml(this.description)}</CARD:addressbook-description>` : ""}
</prop>
</set>
<remove>
<prop>
${!this.description ? "<CARD:calendar-description />" : ""}
</prop>
</remove>
</propertyupdate>
`
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}
};
__decorateClass([
n$1()
], EditAddressbookForm.prototype, "principal", 2);
__decorateClass([
n$1()
], EditAddressbookForm.prototype, "addr_id", 2);
__decorateClass([
n$1()
], EditAddressbookForm.prototype, "displayname", 2);
__decorateClass([
n$1()
], EditAddressbookForm.prototype, "description", 2);
EditAddressbookForm = __decorateClass([
t("edit-addressbook-form")
], EditAddressbookForm);
export {
EditAddressbookForm
};

View File

@@ -1,158 +0,0 @@
import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
if (decorator = decorators[i2])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let EditCalendarForm = class extends i {
constructor() {
super();
this.displayname = "";
this.description = "";
this.timezone_id = "";
this.color = "";
this.components = /* @__PURE__ */ new Set();
this.dialog = e();
this.form = e();
}
createRenderRoot() {
return this;
}
render() {
return x`
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${n(this.dialog)}>
<h3>Edit calendar</h3>
<form @submit=${this.submit} ${n(this.form)}>
<label>
Displayname
<input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label>
<br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" .value=${this.color} @change=${(e2) => this.color = e2.target.value} />
</label>
<br>
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
<label>
Support ${comp}
<input type="checkbox" value=${comp} ?checked=${this.components.has(comp)} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
</label>
<br>
`)}
<br>
<button type="submit">Submit</button>
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}} class="cancel">Cancel</button>
</form>
</dialog>
`;
}
async submit(e2) {
e2.preventDefault();
if (!this.principal) {
alert("Empty principal");
return;
}
if (!this.cal_id) {
alert("Empty id");
return;
}
if (!this.displayname) {
alert("Empty displayname");
return;
}
if (!this.components.size) {
alert("No calendar components selected");
return;
}
let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
method: "PROPPATCH",
headers: {
"Content-Type": "application/xml"
},
body: `
<propertyupdate xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ""}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
<CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map((comp) => `<CAL:comp name="${escapeXml(comp)}" />`).join("\n")}
</CAL:supported-calendar-component-set>
</prop>
</set>
<remove>
<prop>
${!this.timezone_id ? `<CAL:calendar-timezone-id />` : ""}
${!this.description ? "<CAL:calendar-description />" : ""}
${!this.color ? "<ICAL:calendar-color />" : ""}
</prop>
</remove>
</propertyupdate>
`
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}
};
__decorateClass([
n$1()
], EditCalendarForm.prototype, "principal", 2);
__decorateClass([
n$1()
], EditCalendarForm.prototype, "cal_id", 2);
__decorateClass([
n$1()
], EditCalendarForm.prototype, "displayname", 2);
__decorateClass([
n$1()
], EditCalendarForm.prototype, "description", 2);
__decorateClass([
n$1()
], EditCalendarForm.prototype, "timezone_id", 2);
__decorateClass([
n$1()
], EditCalendarForm.prototype, "color", 2);
__decorateClass([
n$1({
converter: {
fromAttribute: (value, _type) => new Set(value ? JSON.parse(value) : []),
toAttribute: (value, _type) => JSON.stringify(value)
}
})
], EditCalendarForm.prototype, "components", 2);
EditCalendarForm = __decorateClass([
t("edit-calendar-form")
], EditCalendarForm);
export {
EditCalendarForm
};

View File

@@ -1,100 +0,0 @@
import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
if (decorator = decorators[i2])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let ImportAddressbookForm = class extends i {
constructor() {
super();
this.user = "";
this.addressbook_id = self.crypto.randomUUID();
this.dialog = e();
this.form = e();
}
createRenderRoot() {
return this;
}
render() {
return x`
<button @click=${() => this.dialog.value.showModal()}>Import addressbook</button>
<dialog ${n(this.dialog)}>
<h3>Import addressbook</h3>
<form @submit=${this.submit} ${n(this.form)}>
<label>
principal (for group addressbook)
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map((membership) => x`
<option value=${membership}>${membership}</option>
`)}
</select>
</label>
<br>
<label>
id
<input type="text" name="id" value=${this.addressbook_id} @change=${(e2) => this.addressbook_id = e2.target.value} />
</label>
<br>
<label>
file
<input type="file" accept="text/vcard" name="file" @change=${(e2) => this.file = e2.target.files[0]} />
</label>
<button type="submit">Import</button>
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}} class="cancel">Cancel</button>
</form>
</dialog>
`;
}
async submit(e2) {
e2.preventDefault();
this.principal ||= this.user;
if (!this.principal) {
alert("Empty principal");
return;
}
if (!this.addressbook_id) {
alert("Empty id");
return;
}
let response = await fetch(`/carddav/principal/${this.principal}/${this.addressbook_id}`, {
method: "IMPORT",
headers: {
"Content-Type": "text/vcard"
},
body: this.file
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}
};
__decorateClass([
n$1()
], ImportAddressbookForm.prototype, "user", 2);
__decorateClass([
n$1()
], ImportAddressbookForm.prototype, "principal", 2);
__decorateClass([
n$1()
], ImportAddressbookForm.prototype, "addressbook_id", 2);
ImportAddressbookForm = __decorateClass([
t("import-addressbook-form")
], ImportAddressbookForm);
export {
ImportAddressbookForm
};

View File

@@ -1,100 +0,0 @@
import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
if (decorator = decorators[i2])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let ImportCalendarForm = class extends i {
constructor() {
super();
this.user = "";
this.cal_id = self.crypto.randomUUID();
this.dialog = e();
this.form = e();
}
createRenderRoot() {
return this;
}
render() {
return x`
<button @click=${() => this.dialog.value.showModal()}>Import calendar</button>
<dialog ${n(this.dialog)}>
<h3>Import calendar</h3>
<form @submit=${this.submit} ${n(this.form)}>
<label>
principal (for group calendars)
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map((membership) => x`
<option value=${membership}>${membership}</option>
`)}
</select>
</label>
<br>
<label>
id
<input type="text" name="id" value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
</label>
<br>
<label>
file
<input type="file" accept="text/calendar" name="file" @change=${(e2) => this.file = e2.target.files[0]} />
</label>
<button type="submit">Import</button>
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}} class="cancel">Cancel</button>
</form>
</dialog>
`;
}
async submit(e2) {
e2.preventDefault();
this.principal ||= this.user;
if (!this.principal) {
alert("Empty principal");
return;
}
if (!this.cal_id) {
alert("Empty id");
return;
}
let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
method: "IMPORT",
headers: {
"Content-Type": "text/calendar"
},
body: this.file
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}
};
__decorateClass([
n$1()
], ImportCalendarForm.prototype, "user", 2);
__decorateClass([
n$1()
], ImportCalendarForm.prototype, "principal", 2);
__decorateClass([
n$1()
], ImportCalendarForm.prototype, "cal_id", 2);
ImportCalendarForm = __decorateClass([
t("import-calendar-form")
], ImportCalendarForm);
export {
ImportCalendarForm
};

View File

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

View File

@@ -1,4 +1,4 @@
import { E } from "./lit-DkXrt_Iv.mjs"; import { E } from "./lit-z6_uA4GX.mjs";
/** /**
* @license * @license
* Copyright 2020 Google LLC * Copyright 2020 Google LLC
@@ -33,16 +33,17 @@ class i {
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */
const s = (i2, t2) => { const s = (i2, t2) => {
var _a;
const e2 = i2._$AN; const e2 = i2._$AN;
if (void 0 === e2) return false; if (void 0 === e2) return false;
for (const i3 of e2) i3._$AO?.(t2, false), s(i3, t2); for (const i3 of e2) (_a = i3._$AO) == null ? void 0 : _a.call(i3, t2, false), s(i3, t2);
return true; return true;
}, o$1 = (i2) => { }, o$1 = (i2) => {
let t2, e2; let t2, e2;
do { do {
if (void 0 === (t2 = i2._$AM)) break; if (void 0 === (t2 = i2._$AM)) break;
e2 = t2._$AN, e2.delete(i2), i2 = t2; e2 = t2._$AN, e2.delete(i2), i2 = t2;
} while (0 === e2?.size); } while (0 === (e2 == null ? void 0 : e2.size));
}, r = (i2) => { }, r = (i2) => {
for (let t2; t2 = i2._$AM; i2 = t2) { for (let t2; t2 = i2._$AM; i2 = t2) {
let e2 = t2._$AN; let e2 = t2._$AN;
@@ -61,7 +62,7 @@ function n$1(i2, t2 = false, e2 = 0) {
else s(this, i2); else s(this, i2);
} }
const c = (i2) => { const c = (i2) => {
i2.type == t.CHILD && (i2._$AP ??= n$1, i2._$AQ ??= h$1); i2.type == t.CHILD && (i2._$AP ?? (i2._$AP = n$1), i2._$AQ ?? (i2._$AQ = h$1));
}; };
class f extends i { class f extends i {
constructor() { constructor() {
@@ -71,7 +72,8 @@ class f extends i {
super._$AT(i2, t2, e2), r(this), this.isConnected = i2._$AU; super._$AT(i2, t2, e2), r(this), this.isConnected = i2._$AU;
} }
_$AO(i2, t2 = true) { _$AO(i2, t2 = true) {
i2 !== this.isConnected && (this.isConnected = i2, i2 ? this.reconnected?.() : this.disconnected?.()), t2 && (s(this, i2), o$1(this)); var _a, _b;
i2 !== this.isConnected && (this.isConnected = i2, i2 ? (_a = this.reconnected) == null ? void 0 : _a.call(this) : (_b = this.disconnected) == null ? void 0 : _b.call(this)), t2 && (s(this, i2), o$1(this));
} }
setValue(t2) { setValue(t2) {
if (f$1(this._$Ct)) this._$Ct._$AI(t2, this); if (f$1(this._$Ct)) this._$Ct._$AI(t2, this);
@@ -98,8 +100,9 @@ const o = /* @__PURE__ */ new WeakMap(), n = e$1(class extends f {
return E; return E;
} }
update(i2, [s2]) { update(i2, [s2]) {
var _a;
const e2 = s2 !== this.G; const e2 = s2 !== this.G;
return e2 && void 0 !== this.G && this.rt(void 0), (e2 || this.lt !== this.ct) && (this.G = s2, this.ht = i2.options?.host, this.rt(this.ct = i2.element)), E; return e2 && void 0 !== this.G && this.rt(void 0), (e2 || this.lt !== this.ct) && (this.G = s2, this.ht = (_a = i2.options) == null ? void 0 : _a.host, this.rt(this.ct = i2.element)), E;
} }
rt(t2) { rt(t2) {
if (this.isConnected || (t2 = void 0), "function" == typeof this.G) { if (this.isConnected || (t2 = void 0), "function" == typeof this.G) {
@@ -109,7 +112,8 @@ const o = /* @__PURE__ */ new WeakMap(), n = e$1(class extends f {
} else this.G.value = t2; } else this.G.value = t2;
} }
get lt() { get lt() {
return "function" == typeof this.G ? o.get(this.ht ?? globalThis)?.get(this.G) : this.G?.value; var _a, _b;
return "function" == typeof this.G ? (_a = o.get(this.ht ?? globalThis)) == null ? void 0 : _a.get(this.G) : (_b = this.G) == null ? void 0 : _b.value;
} }
disconnected() { disconnected() {
this.lt === this.ct && this.rt(void 0); this.lt === this.ct && this.rt(void 0);
@@ -118,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

@@ -3,6 +3,7 @@
* Copyright 2019 Google LLC * Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */
var _a;
const t$1 = globalThis, e$2 = t$1.ShadowRoot && (void 0 === t$1.ShadyCSS || t$1.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$2 = Symbol(), o$3 = /* @__PURE__ */ new WeakMap(); const t$1 = globalThis, e$2 = t$1.ShadowRoot && (void 0 === t$1.ShadyCSS || t$1.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$2 = Symbol(), o$3 = /* @__PURE__ */ new WeakMap();
let n$2 = class n { let n$2 = class n {
constructor(t2, e2, o2) { constructor(t2, e2, o2) {
@@ -23,7 +24,7 @@ let n$2 = class n {
} }
}; };
const r$2 = (t2) => new n$2("string" == typeof t2 ? t2 : t2 + "", void 0, s$2), S$1 = (s2, o2) => { const r$2 = (t2) => new n$2("string" == typeof t2 ? t2 : t2 + "", void 0, s$2), S$1 = (s2, o2) => {
if (e$2) s2.adoptedStyleSheets = o2.map(((t2) => t2 instanceof CSSStyleSheet ? t2 : t2.styleSheet)); if (e$2) s2.adoptedStyleSheets = o2.map((t2) => t2 instanceof CSSStyleSheet ? t2 : t2.styleSheet);
else for (const e2 of o2) { else for (const e2 of o2) {
const o3 = document.createElement("style"), n3 = t$1.litNonce; const o3 = document.createElement("style"), n3 = t$1.litNonce;
void 0 !== n3 && o3.setAttribute("nonce", n3), o3.textContent = e2.cssText, s2.appendChild(o3); void 0 !== n3 && o3.setAttribute("nonce", n3), o3.textContent = e2.cssText, s2.appendChild(o3);
@@ -67,10 +68,10 @@ const { is: i$2, defineProperty: e$1, getOwnPropertyDescriptor: h$1, getOwnPrope
} }
return i2; return i2;
} }, f$1 = (t2, s2) => !i$2(t2, s2), b = { attribute: true, type: String, converter: u$1, reflect: false, useDefault: false, hasChanged: f$1 }; } }, f$1 = (t2, s2) => !i$2(t2, s2), b = { attribute: true, type: String, converter: u$1, reflect: false, useDefault: false, hasChanged: f$1 };
Symbol.metadata ??= Symbol("metadata"), a$1.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), a$1.litPropertyMetadata ?? (a$1.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
let y$1 = class y extends HTMLElement { let y$1 = class y extends HTMLElement {
static addInitializer(t2) { static addInitializer(t2) {
this._$Ei(), (this.l ??= []).push(t2); this._$Ei(), (this.l ?? (this.l = [])).push(t2);
} }
static get observedAttributes() { static get observedAttributes() {
return this.finalize(), this._$Eh && [...this._$Eh.keys()]; return this.finalize(), this._$Eh && [...this._$Eh.keys()];
@@ -88,8 +89,8 @@ let y$1 = class y extends HTMLElement {
this[s2] = t3; this[s2] = t3;
} }; } };
return { get: e2, set(s3) { return { get: e2, set(s3) {
const h2 = e2?.call(this); const h2 = e2 == null ? void 0 : e2.call(this);
r2?.call(this, s3), this.requestUpdate(t2, h2, i2); r2 == null ? void 0 : r2.call(this, s3), this.requestUpdate(t2, h2, i2);
}, configurable: true, enumerable: true }; }, configurable: true, enumerable: true };
} }
static getPropertyOptions(t2) { static getPropertyOptions(t2) {
@@ -134,13 +135,16 @@ let y$1 = class y extends HTMLElement {
super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev(); super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();
} }
_$Ev() { _$Ev() {
this._$ES = new Promise(((t2) => this.enableUpdating = t2)), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach(((t2) => t2(this))); var _a2;
this._$ES = new Promise((t2) => this.enableUpdating = t2), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), (_a2 = this.constructor.l) == null ? void 0 : _a2.forEach((t2) => t2(this));
} }
addController(t2) { addController(t2) {
(this._$EO ??= /* @__PURE__ */ new Set()).add(t2), void 0 !== this.renderRoot && this.isConnected && t2.hostConnected?.(); var _a2;
(this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(t2), void 0 !== this.renderRoot && this.isConnected && ((_a2 = t2.hostConnected) == null ? void 0 : _a2.call(t2));
} }
removeController(t2) { removeController(t2) {
this._$EO?.delete(t2); var _a2;
(_a2 = this._$EO) == null ? void 0 : _a2.delete(t2);
} }
_$E_() { _$E_() {
const t2 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties; const t2 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;
@@ -152,42 +156,51 @@ let y$1 = class y extends HTMLElement {
return S$1(t2, this.constructor.elementStyles), t2; return S$1(t2, this.constructor.elementStyles), t2;
} }
connectedCallback() { connectedCallback() {
this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach(((t2) => t2.hostConnected?.())); var _a2;
this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(true), (_a2 = this._$EO) == null ? void 0 : _a2.forEach((t2) => {
var _a3;
return (_a3 = t2.hostConnected) == null ? void 0 : _a3.call(t2);
});
} }
enableUpdating(t2) { enableUpdating(t2) {
} }
disconnectedCallback() { disconnectedCallback() {
this._$EO?.forEach(((t2) => t2.hostDisconnected?.())); var _a2;
(_a2 = this._$EO) == null ? void 0 : _a2.forEach((t2) => {
var _a3;
return (_a3 = t2.hostDisconnected) == null ? void 0 : _a3.call(t2);
});
} }
attributeChangedCallback(t2, s2, i2) { attributeChangedCallback(t2, s2, i2) {
this._$AK(t2, i2); this._$AK(t2, i2);
} }
_$ET(t2, s2) { _$ET(t2, s2) {
var _a2;
const i2 = this.constructor.elementProperties.get(t2), e2 = this.constructor._$Eu(t2, i2); const i2 = this.constructor.elementProperties.get(t2), e2 = this.constructor._$Eu(t2, i2);
if (void 0 !== e2 && true === i2.reflect) { if (void 0 !== e2 && true === i2.reflect) {
const h2 = (void 0 !== i2.converter?.toAttribute ? i2.converter : u$1).toAttribute(s2, i2.type); const h2 = (void 0 !== ((_a2 = i2.converter) == null ? void 0 : _a2.toAttribute) ? i2.converter : u$1).toAttribute(s2, i2.type);
this._$Em = t2, null == h2 ? this.removeAttribute(e2) : this.setAttribute(e2, h2), this._$Em = null; this._$Em = t2, null == h2 ? this.removeAttribute(e2) : this.setAttribute(e2, h2), this._$Em = null;
} }
} }
_$AK(t2, s2) { _$AK(t2, s2) {
var _a2, _b;
const i2 = this.constructor, e2 = i2._$Eh.get(t2); const i2 = this.constructor, e2 = i2._$Eh.get(t2);
if (void 0 !== e2 && this._$Em !== e2) { if (void 0 !== e2 && this._$Em !== e2) {
const t3 = i2.getPropertyOptions(e2), h2 = "function" == typeof t3.converter ? { fromAttribute: t3.converter } : void 0 !== t3.converter?.fromAttribute ? t3.converter : u$1; const t3 = i2.getPropertyOptions(e2), h2 = "function" == typeof t3.converter ? { fromAttribute: t3.converter } : void 0 !== ((_a2 = t3.converter) == null ? void 0 : _a2.fromAttribute) ? t3.converter : u$1;
this._$Em = e2; this._$Em = e2, this[e2] = h2.fromAttribute(s2, t3.type) ?? ((_b = this._$Ej) == null ? void 0 : _b.get(e2)) ?? null, this._$Em = null;
const r2 = h2.fromAttribute(s2, t3.type);
this[e2] = r2 ?? this._$Ej?.get(e2) ?? r2, this._$Em = null;
} }
} }
requestUpdate(t2, s2, i2) { requestUpdate(t2, s2, i2) {
var _a2;
if (void 0 !== t2) { if (void 0 !== t2) {
const e2 = this.constructor, h2 = this[t2]; const e2 = this.constructor, h2 = this[t2];
if (i2 ??= e2.getPropertyOptions(t2), !((i2.hasChanged ?? f$1)(h2, s2) || i2.useDefault && i2.reflect && h2 === this._$Ej?.get(t2) && !this.hasAttribute(e2._$Eu(t2, i2)))) return; if (i2 ?? (i2 = e2.getPropertyOptions(t2)), !((i2.hasChanged ?? f$1)(h2, s2) || i2.useDefault && i2.reflect && h2 === ((_a2 = this._$Ej) == null ? void 0 : _a2.get(t2)) && !this.hasAttribute(e2._$Eu(t2, i2)))) return;
this.C(t2, s2, i2); this.C(t2, s2, i2);
} }
false === this.isUpdatePending && (this._$ES = this._$EP()); false === this.isUpdatePending && (this._$ES = this._$EP());
} }
C(t2, s2, { useDefault: i2, reflect: e2, wrapped: h2 }, r2) { C(t2, s2, { useDefault: i2, reflect: e2, wrapped: h2 }, r2) {
i2 && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(t2) && (this._$Ej.set(t2, r2 ?? s2 ?? this[t2]), true !== h2 || void 0 !== r2) || (this._$AL.has(t2) || (this.hasUpdated || i2 || (s2 = void 0), this._$AL.set(t2, s2)), true === e2 && this._$Em !== t2 && (this._$Eq ??= /* @__PURE__ */ new Set()).add(t2)); i2 && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(t2) && (this._$Ej.set(t2, r2 ?? s2 ?? this[t2]), true !== h2 || void 0 !== r2) || (this._$AL.has(t2) || (this.hasUpdated || i2 || (s2 = void 0), this._$AL.set(t2, s2)), true === e2 && this._$Em !== t2 && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(t2));
} }
async _$EP() { async _$EP() {
this.isUpdatePending = true; this.isUpdatePending = true;
@@ -203,9 +216,10 @@ let y$1 = class y extends HTMLElement {
return this.performUpdate(); return this.performUpdate();
} }
performUpdate() { performUpdate() {
var _a2;
if (!this.isUpdatePending) return; if (!this.isUpdatePending) return;
if (!this.hasUpdated) { if (!this.hasUpdated) {
if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
for (const [t4, s3] of this._$Ep) this[t4] = s3; for (const [t4, s3] of this._$Ep) this[t4] = s3;
this._$Ep = void 0; this._$Ep = void 0;
} }
@@ -218,7 +232,10 @@ let y$1 = class y extends HTMLElement {
let t2 = false; let t2 = false;
const s2 = this._$AL; const s2 = this._$AL;
try { try {
t2 = this.shouldUpdate(s2), t2 ? (this.willUpdate(s2), this._$EO?.forEach(((t3) => t3.hostUpdate?.())), this.update(s2)) : this._$EM(); t2 = this.shouldUpdate(s2), t2 ? (this.willUpdate(s2), (_a2 = this._$EO) == null ? void 0 : _a2.forEach((t3) => {
var _a3;
return (_a3 = t3.hostUpdate) == null ? void 0 : _a3.call(t3);
}), this.update(s2)) : this._$EM();
} catch (s3) { } catch (s3) {
throw t2 = false, this._$EM(), s3; throw t2 = false, this._$EM(), s3;
} }
@@ -227,7 +244,11 @@ let y$1 = class y extends HTMLElement {
willUpdate(t2) { willUpdate(t2) {
} }
_$AE(t2) { _$AE(t2) {
this._$EO?.forEach(((t3) => t3.hostUpdated?.())), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t2)), this.updated(t2); var _a2;
(_a2 = this._$EO) == null ? void 0 : _a2.forEach((t3) => {
var _a3;
return (_a3 = t3.hostUpdated) == null ? void 0 : _a3.call(t3);
}), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t2)), this.updated(t2);
} }
_$EM() { _$EM() {
this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false; this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;
@@ -242,20 +263,20 @@ let y$1 = class y extends HTMLElement {
return true; return true;
} }
update(t2) { update(t2) {
this._$Eq &&= this._$Eq.forEach(((t3) => this._$ET(t3, this[t3]))), this._$EM(); this._$Eq && (this._$Eq = this._$Eq.forEach((t3) => this._$ET(t3, this[t3]))), this._$EM();
} }
updated(t2) { updated(t2) {
} }
firstUpdated(t2) { firstUpdated(t2) {
} }
}; };
y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$1("elementProperties")] = /* @__PURE__ */ new Map(), y$1[d$1("finalized")] = /* @__PURE__ */ new Map(), p$1?.({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ??= []).push("2.1.1"); y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$1("elementProperties")] = /* @__PURE__ */ new Map(), y$1[d$1("finalized")] = /* @__PURE__ */ new Map(), p$1 == null ? void 0 : p$1({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ?? (a$1.reactiveElementVersions = [])).push("2.1.0");
/** /**
* @license * @license
* Copyright 2017 Google LLC * Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */
const t = globalThis, i$1 = t.trustedTypes, s$1 = i$1 ? i$1.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, e = "$lit$", h = `lit$${Math.random().toFixed(9).slice(2)}$`, o$1 = "?" + h, n2 = `<${o$1}>`, r = document, l = () => r.createComment(""), c = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, a = Array.isArray, u = (t2) => a(t2) || "function" == typeof t2?.[Symbol.iterator], d = "[ \n\f\r]", f = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, v = /-->/g, _ = />/g, m = RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^ const t = globalThis, i$1 = t.trustedTypes, s$1 = i$1 ? i$1.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, e = "$lit$", h = `lit$${Math.random().toFixed(9).slice(2)}$`, o$1 = "?" + h, n2 = `<${o$1}>`, r = document, l = () => r.createComment(""), c = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, a = Array.isArray, u = (t2) => a(t2) || "function" == typeof (t2 == null ? void 0 : t2[Symbol.iterator]), d = "[ \n\f\r]", f = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, v = /-->/g, _ = />/g, m = RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^
\f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y2 = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), x = y2(1), T = Symbol.for("lit-noChange"), E = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), C = r.createTreeWalker(r, 129); \f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y2 = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), x = y2(1), T = Symbol.for("lit-noChange"), E = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), C = r.createTreeWalker(r, 129);
function P(t2, i2) { function P(t2, i2) {
if (!a(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array"); if (!a(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array");
@@ -311,10 +332,11 @@ class N {
} }
} }
function S(t2, i2, s2 = t2, e2) { function S(t2, i2, s2 = t2, e2) {
var _a2, _b;
if (i2 === T) return i2; if (i2 === T) return i2;
let h2 = void 0 !== e2 ? s2._$Co?.[e2] : s2._$Cl; let h2 = void 0 !== e2 ? (_a2 = s2._$Co) == null ? void 0 : _a2[e2] : s2._$Cl;
const o2 = c(i2) ? void 0 : i2._$litDirective$; const o2 = c(i2) ? void 0 : i2._$litDirective$;
return h2?.constructor !== o2 && (h2?._$AO?.(false), void 0 === o2 ? h2 = void 0 : (h2 = new o2(t2), h2._$AT(t2, s2, e2)), void 0 !== e2 ? (s2._$Co ??= [])[e2] = h2 : s2._$Cl = h2), void 0 !== h2 && (i2 = S(t2, h2._$AS(t2, i2.values), h2, e2)), i2; return (h2 == null ? void 0 : h2.constructor) !== o2 && ((_b = h2 == null ? void 0 : h2._$AO) == null ? void 0 : _b.call(h2, false), void 0 === o2 ? h2 = void 0 : (h2 = new o2(t2), h2._$AT(t2, s2, e2)), void 0 !== e2 ? (s2._$Co ?? (s2._$Co = []))[e2] = h2 : s2._$Cl = h2), void 0 !== h2 && (i2 = S(t2, h2._$AS(t2, i2.values), h2, e2)), i2;
} }
class M { class M {
constructor(t2, i2) { constructor(t2, i2) {
@@ -327,7 +349,7 @@ class M {
return this._$AM._$AU; return this._$AM._$AU;
} }
u(t2) { u(t2) {
const { el: { content: i2 }, parts: s2 } = this._$AD, e2 = (t2?.creationScope ?? r).importNode(i2, true); const { el: { content: i2 }, parts: s2 } = this._$AD, e2 = ((t2 == null ? void 0 : t2.creationScope) ?? r).importNode(i2, true);
C.currentNode = e2; C.currentNode = e2;
let h2 = C.nextNode(), o2 = 0, n3 = 0, l2 = s2[0]; let h2 = C.nextNode(), o2 = 0, n3 = 0, l2 = s2[0];
for (; void 0 !== l2; ) { for (; void 0 !== l2; ) {
@@ -335,7 +357,7 @@ class M {
let i3; let i3;
2 === l2.type ? i3 = new R(h2, h2.nextSibling, this, t2) : 1 === l2.type ? i3 = new l2.ctor(h2, l2.name, l2.strings, this, t2) : 6 === l2.type && (i3 = new z(h2, this, t2)), this._$AV.push(i3), l2 = s2[++n3]; 2 === l2.type ? i3 = new R(h2, h2.nextSibling, this, t2) : 1 === l2.type ? i3 = new l2.ctor(h2, l2.name, l2.strings, this, t2) : 6 === l2.type && (i3 = new z(h2, this, t2)), this._$AV.push(i3), l2 = s2[++n3];
} }
o2 !== l2?.index && (h2 = C.nextNode(), o2++); o2 !== (l2 == null ? void 0 : l2.index) && (h2 = C.nextNode(), o2++);
} }
return C.currentNode = r, e2; return C.currentNode = r, e2;
} }
@@ -346,15 +368,16 @@ class M {
} }
class R { class R {
get _$AU() { get _$AU() {
return this._$AM?._$AU ?? this._$Cv; var _a2;
return ((_a2 = this._$AM) == null ? void 0 : _a2._$AU) ?? this._$Cv;
} }
constructor(t2, i2, s2, e2) { constructor(t2, i2, s2, e2) {
this.type = 2, this._$AH = E, this._$AN = void 0, this._$AA = t2, this._$AB = i2, this._$AM = s2, this.options = e2, this._$Cv = e2?.isConnected ?? true; this.type = 2, this._$AH = E, this._$AN = void 0, this._$AA = t2, this._$AB = i2, this._$AM = s2, this.options = e2, this._$Cv = (e2 == null ? void 0 : e2.isConnected) ?? true;
} }
get parentNode() { get parentNode() {
let t2 = this._$AA.parentNode; let t2 = this._$AA.parentNode;
const i2 = this._$AM; const i2 = this._$AM;
return void 0 !== i2 && 11 === t2?.nodeType && (t2 = i2.parentNode), t2; return void 0 !== i2 && 11 === (t2 == null ? void 0 : t2.nodeType) && (t2 = i2.parentNode), t2;
} }
get startNode() { get startNode() {
return this._$AA; return this._$AA;
@@ -375,8 +398,9 @@ class R {
this._$AH !== E && c(this._$AH) ? this._$AA.nextSibling.data = t2 : this.T(r.createTextNode(t2)), this._$AH = t2; this._$AH !== E && c(this._$AH) ? this._$AA.nextSibling.data = t2 : this.T(r.createTextNode(t2)), this._$AH = t2;
} }
$(t2) { $(t2) {
var _a2;
const { values: i2, _$litType$: s2 } = t2, e2 = "number" == typeof s2 ? this._$AC(t2) : (void 0 === s2.el && (s2.el = N.createElement(P(s2.h, s2.h[0]), this.options)), s2); const { values: i2, _$litType$: s2 } = t2, e2 = "number" == typeof s2 ? this._$AC(t2) : (void 0 === s2.el && (s2.el = N.createElement(P(s2.h, s2.h[0]), this.options)), s2);
if (this._$AH?._$AD === e2) this._$AH.p(i2); if (((_a2 = this._$AH) == null ? void 0 : _a2._$AD) === e2) this._$AH.p(i2);
else { else {
const t3 = new M(e2, this), s3 = t3.u(this.options); const t3 = new M(e2, this), s3 = t3.u(this.options);
t3.p(i2), this.T(s3), this._$AH = t3; t3.p(i2), this.T(s3), this._$AH = t3;
@@ -394,13 +418,15 @@ class R {
e2 < i2.length && (this._$AR(s2 && s2._$AB.nextSibling, e2), i2.length = e2); e2 < i2.length && (this._$AR(s2 && s2._$AB.nextSibling, e2), i2.length = e2);
} }
_$AR(t2 = this._$AA.nextSibling, i2) { _$AR(t2 = this._$AA.nextSibling, i2) {
for (this._$AP?.(false, true, i2); t2 !== this._$AB; ) { var _a2;
for ((_a2 = this._$AP) == null ? void 0 : _a2.call(this, false, true, i2); t2 && t2 !== this._$AB; ) {
const i3 = t2.nextSibling; const i3 = t2.nextSibling;
t2.remove(), t2 = i3; t2.remove(), t2 = i3;
} }
} }
setConnected(t2) { setConnected(t2) {
void 0 === this._$AM && (this._$Cv = t2, this._$AP?.(t2)); var _a2;
void 0 === this._$AM && (this._$Cv = t2, (_a2 = this._$AP) == null ? void 0 : _a2.call(this, t2));
} }
} }
class k { class k {
@@ -420,7 +446,7 @@ class k {
else { else {
const e3 = t2; const e3 = t2;
let n3, r2; let n3, r2;
for (t2 = h2[0], n3 = 0; n3 < h2.length - 1; n3++) r2 = S(this, e3[s2 + n3], i2, n3), r2 === T && (r2 = this._$AH[n3]), o2 ||= !c(r2) || r2 !== this._$AH[n3], r2 === E ? t2 = E : t2 !== E && (t2 += (r2 ?? "") + h2[n3 + 1]), this._$AH[n3] = r2; for (t2 = h2[0], n3 = 0; n3 < h2.length - 1; n3++) r2 = S(this, e3[s2 + n3], i2, n3), r2 === T && (r2 = this._$AH[n3]), o2 || (o2 = !c(r2) || r2 !== this._$AH[n3]), r2 === E ? t2 = E : t2 !== E && (t2 += (r2 ?? "") + h2[n3 + 1]), this._$AH[n3] = r2;
} }
o2 && !e2 && this.j(t2); o2 && !e2 && this.j(t2);
} }
@@ -454,7 +480,8 @@ class L extends k {
e2 && this.element.removeEventListener(this.name, this, s2), h2 && this.element.addEventListener(this.name, this, t2), this._$AH = t2; e2 && this.element.removeEventListener(this.name, this, s2), h2 && this.element.addEventListener(this.name, this, t2), this._$AH = t2;
} }
handleEvent(t2) { handleEvent(t2) {
"function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t2) : this._$AH.handleEvent(t2); var _a2;
"function" == typeof this._$AH ? this._$AH.call(((_a2 = this.options) == null ? void 0 : _a2.host) ?? this.element, t2) : this._$AH.handleEvent(t2);
} }
} }
class z { class z {
@@ -469,12 +496,12 @@ class z {
} }
} }
const j = t.litHtmlPolyfillSupport; const j = t.litHtmlPolyfillSupport;
j?.(N, R), (t.litHtmlVersions ??= []).push("3.3.1"); j == null ? void 0 : j(N, R), (t.litHtmlVersions ?? (t.litHtmlVersions = [])).push("3.3.0");
const B = (t2, i2, s2) => { const B = (t2, i2, s2) => {
const e2 = s2?.renderBefore ?? i2; const e2 = (s2 == null ? void 0 : s2.renderBefore) ?? i2;
let h2 = e2._$litPart$; let h2 = e2._$litPart$;
if (void 0 === h2) { if (void 0 === h2) {
const t3 = s2?.renderBefore ?? null; const t3 = (s2 == null ? void 0 : s2.renderBefore) ?? null;
e2._$litPart$ = h2 = new R(i2.insertBefore(l(), t3), t3, void 0, s2 ?? {}); e2._$litPart$ = h2 = new R(i2.insertBefore(l(), t3), t3, void 0, s2 ?? {});
} }
return h2._$AI(t2), h2; return h2._$AI(t2), h2;
@@ -490,27 +517,30 @@ class i extends y$1 {
super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
} }
createRenderRoot() { createRenderRoot() {
var _a2;
const t2 = super.createRenderRoot(); const t2 = super.createRenderRoot();
return this.renderOptions.renderBefore ??= t2.firstChild, t2; return (_a2 = this.renderOptions).renderBefore ?? (_a2.renderBefore = t2.firstChild), t2;
} }
update(t2) { update(t2) {
const r2 = this.render(); const r2 = this.render();
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t2), this._$Do = B(r2, this.renderRoot, this.renderOptions); this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t2), this._$Do = B(r2, this.renderRoot, this.renderOptions);
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(), this._$Do?.setConnected(true); var _a2;
super.connectedCallback(), (_a2 = this._$Do) == null ? void 0 : _a2.setConnected(true);
} }
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(), this._$Do?.setConnected(false); var _a2;
super.disconnectedCallback(), (_a2 = this._$Do) == null ? void 0 : _a2.setConnected(false);
} }
render() { render() {
return T; return T;
} }
} }
i._$litElement$ = true, i["finalized"] = true, s.litElementHydrateSupport?.({ LitElement: i }); i._$litElement$ = true, i["finalized"] = true, (_a = s.litElementHydrateSupport) == null ? void 0 : _a.call(s, { LitElement: i });
const o = s.litElementPolyfillSupport; const o = s.litElementPolyfillSupport;
o?.({ LitElement: i }); o == null ? void 0 : o({ LitElement: i });
(s.litElementVersions ??= []).push("4.2.1"); (s.litElementVersions ?? (s.litElementVersions = [])).push("4.2.0");
export { export {
E, E,
f$1 as f, f$1 as f,

View File

@@ -1,13 +1,13 @@
import { f, u } from "./lit-DkXrt_Iv.mjs"; import { f, u } from "./lit-z6_uA4GX.mjs";
/** /**
* @license * @license
* Copyright 2017 Google LLC * Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */
const t = (t2) => (e, o2) => { const t = (t2) => (e, o2) => {
void 0 !== o2 ? o2.addInitializer((() => { void 0 !== o2 ? o2.addInitializer(() => {
customElements.define(t2, e); customElements.define(t2, e);
})) : customElements.define(t2, e); }) : customElements.define(t2, e);
}; };
/** /**
* @license * @license

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -224,15 +224,15 @@ ul.collection-list {
min-height: 80px; min-height: 80px;
height: fit-content; height: fit-content;
grid-template-areas: grid-template-areas:
". color-chip" ". . color-chip"
"title color-chip" "title comps color-chip"
"description color-chip" "description description color-chip"
"subscription-url color-chip" "subscription-url subscription-url color-chip"
"metadata color-chip" "metadata metadata color-chip"
"actions color-chip" "actions actions color-chip"
". color-chip"; ". . color-chip";
grid-template-rows: 12px auto auto auto auto auto 12px; grid-template-rows: 12px auto auto auto auto auto 12px;
grid-template-columns: auto 80px; grid-template-columns: min-content auto 80px;
row-gap: 4px; row-gap: 4px;
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
@@ -260,7 +260,7 @@ ul.collection-list {
} }
.comps { .comps {
display: inline; grid-area: comps;
span { span {
margin: 0 2px; margin: 0 2px;
@@ -290,11 +290,9 @@ ul.collection-list {
.color-chip { .color-chip {
background: var(--color); background: var(--color);
grid-area: color-chip; grid-area: color-chip;
margin-left: 8px;
} }
.actions { .actions {
pointer-events: all;
grid-area: actions; grid-area: actions;
width: fit-content; width: fit-content;
display: flex; display: flex;
@@ -318,10 +316,6 @@ dialog {
padding: 32px; padding: 32px;
} }
dialog::backdrop {
background: color-mix(in srgb, var(--background-color), transparent 50%);
}
footer { footer {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -347,17 +341,6 @@ select {
} }
} }
form {
input[type="text"],
input[type="password"],
input[type="color"],
textarea,
select {
width: 100%;
}
}
svg.icon { svg.icon {
stroke-width: 2px; stroke-width: 2px;
color: var(--text-on-background-color); color: var(--text-on-background-color);

View File

@@ -16,12 +16,6 @@
method="GET"> method="GET">
<button type="submit">Download</button> <button type="submit">Download</button>
</form> </form>
<edit-addressbook-form
principal="{{ addressbook.principal }}"
addr_id="{{ addressbook.id }}"
displayname="{{ addressbook.displayname.as_deref().unwrap_or_default() }}"
description="{{ addressbook.description.as_deref().unwrap_or_default() }}"
></edit-addressbook-form>
<delete-button trash <delete-button trash
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button> href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
</div> </div>
@@ -65,5 +59,4 @@
{% endif %} {% endif %}
<create-addressbook-form user="{{ user.id }}"></create-addressbook-form> <create-addressbook-form user="{{ user.id }}"></create-addressbook-form>
<import-addressbook-form user="{{ user.id }}"></import-addressbook-form>

View File

@@ -1,21 +1,21 @@
<h2>{{ user.id }}'s Calendars</h2> <h2>{{ user.id }}'s Calendars</h2>
<ul class="collection-list"> <ul class="collection-list">
{% for (meta, calendar) in calendars %} {% for (meta, calendar) in calendars %}
{% let color = calendar.meta.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> <a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"></a>
<div class="inner"> <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.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }} {{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
{% endfor %}
</div>
</span> </span>
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
{% endfor %}
</div>
<span class="description"> <span class="description">
{% if let Some(description) = calendar.meta.description %}{{ description }}{% endif %} {% if let Some(description) = calendar.description %}{{ description }}{% endif %}
</span> </span>
{% if let Some(subscription_url) = calendar.subscription_url %} {% if let Some(subscription_url) = calendar.subscription_url %}
<span class="subscription-url">{{ subscription_url }}</span> <span class="subscription-url">{{ subscription_url }}</span>
@@ -25,15 +25,6 @@
<button type="submit">Download</button> <button type="submit">Download</button>
</form> </form>
{% if !calendar.id.starts_with("_birthdays_") %} {% if !calendar.id.starts_with("_birthdays_") %}
<edit-calendar-form
principal="{{ calendar.principal }}"
cal_id="{{ calendar.id }}"
timezone_id="{{ calendar.timezone_id.as_deref().unwrap_or_default() }}"
displayname="{{ calendar.meta.displayname.as_deref().unwrap_or_default() }}"
description="{{ calendar.meta.description.as_deref().unwrap_or_default() }}"
color="{{ calendar.meta.color.as_deref().unwrap_or_default() }}"
components="{{ calendar.components | json }}"
></edit-calendar-form>
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button> <delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
{% endif %} {% endif %}
</div> </div>
@@ -51,21 +42,21 @@
<h3>Deleted Calendars</h3> <h3>Deleted Calendars</h3>
<ul class="collection-list"> <ul class="collection-list">
{% for (meta, calendar) in deleted_calendars %} {% for (meta, calendar) in deleted_calendars %}
{% let color = calendar.meta.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> <a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"></a>
<div class="inner"> <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.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }} {{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
{% endfor %}
</div>
</span> </span>
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
{% endfor %}
</div>
<span class="description"> <span class="description">
{% if let Some(description) = calendar.meta.description %}{{ description }}{% endif %} {% if let Some(description) = calendar.description %}{{ description }}{% endif %}
</span> </span>
<div class="actions"> <div class="actions">
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" <form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"
@@ -84,5 +75,4 @@
</ul> </ul>
{% endif %} {% endif %}
<create-calendar-form user="{{ user.id }}"></create-calendar-form> <create-calendar-form user="{{ user.id }}"></create-calendar-form>
<import-calendar-form user="{{ user.id }}"></import-calendar-form>

View File

@@ -1,4 +1,5 @@
<!-- Adapted from https://iconoir.com/ --> <!-- Adapted from https://iconoir.com/ -->
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon"> <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
<path d="M15 4V2M15 4V6M15 4H10.5M3 10V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V10H3Z" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M15 4V2M15 4V6M15 4H10.5M3 10V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V10H3Z" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M3 10V6C3 4.89543 3.89543 4 5 4H7" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M3 10V6C3 4.89543 3.89543 4 5 4H7" stroke-linecap="round" stroke-linejoin="round"></path>

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 647 B

View File

@@ -1,4 +1,5 @@
<!-- Adapted from https://iconoir.com/ --> <!-- Adapted from https://iconoir.com/ -->
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon"> <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
<path d="M1 20V19C1 15.134 4.13401 12 8 12V12C11.866 12 15 15.134 15 19V20" stroke-linecap="round"></path> <path d="M1 20V19C1 15.134 4.13401 12 8 12V12C11.866 12 15 15.134 15 19V20" stroke-linecap="round"></path>
<path d="M13 14V14C13 11.2386 15.2386 9 18 9V9C20.7614 9 23 11.2386 23 14V14.5" stroke-linecap="round"></path> <path d="M13 14V14C13 11.2386 15.2386 9 18 9V9C20.7614 9 23 11.2386 23 14V14.5" stroke-linecap="round"></path>

Before

Width:  |  Height:  |  Size: 739 B

After

Width:  |  Height:  |  Size: 778 B

View File

@@ -1,4 +1,5 @@
<!-- Adapted from https://iconoir.com/ --> <!-- Adapted from https://iconoir.com/ -->
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon"> <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
<path d="M5 20V19C5 15.134 8.13401 12 12 12V12C15.866 12 19 15.134 19 19V20" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M5 20V19C5 15.134 8.13401 12 12 12V12C15.866 12 19 15.134 19 19V20" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M12 12C14.2091 12 16 10.2091 16 8C16 5.79086 14.2091 4 12 4C9.79086 4 8 5.79086 8 8C8 10.2091 9.79086 12 12 12Z" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M12 12C14.2091 12 16 10.2091 16 8C16 5.79086 14.2091 4 12 4C9.79086 4 8 5.79086 8 8C8 10.2091 9.79086 12 12 12Z" stroke-linecap="round" stroke-linejoin="round"></path>

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -22,9 +22,9 @@
<div id="app"> <div id="app">
{% block content %}<p>Placeholder</p>{% endblock %} {% block content %}<p>Placeholder</p>{% endblock %}
</div> </div>
<footer>
<a href="{{ env!("CARGO_PKG_REPOSITORY") }}" target="_blank">RustiCal {{ env!("CARGO_PKG_VERSION") }}</a>
<a href="/frontend/assets/licenses.html" target="_blank">Open Source Licenses</a>
</footer>
</body> </body>
<footer>
<a href="{{ env!("CARGO_PKG_REPOSITORY") }}" target="_blank">RustiCal {{ env!("CARGO_PKG_VERSION") }}</a>
<a href="/frontend/assets/licenses.html" target="_blank">Open Source Licenses</a>
</footer>
</html> </html>

View File

@@ -4,9 +4,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% let name = calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %} {% let name = calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %}
<h1>{{ calendar.principal }}/{{ name }}</h1> <h1>{{ calendar.principal }}/{{ name }}</h1>
{% if let Some(description) = calendar.meta.description %}<p>{{ description }}</p>{% endif%} {% if let Some(description) = calendar.description %}<p>{{ description }}</p>{% endif%}
{% if let Some(subscription_url) = calendar.subscription_url %} {% if let Some(subscription_url) = calendar.subscription_url %}
<h2>Subscription URL</h2> <h2>Subscription URL</h2>
@@ -25,6 +25,9 @@
{% if let Some(timezone_id) = calendar.timezone_id %} {% if let Some(timezone_id) = calendar.timezone_id %}
<p>{{ timezone_id }}</p> <p>{{ timezone_id }}</p>
{% endif %} {% endif %}
{% if let Some(timezone) = calendar.timezone %}
<textarea rows="16" readonly>{{ timezone }}</textarea>
{% endif %}
<pre>{{ calendar|json }}</pre> <pre>{{ calendar|json }}</pre>

View File

@@ -6,11 +6,7 @@
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML) window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
</script> </script>
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script> <script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/edit-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/import-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script> <script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/edit-addressbook-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/import-addressbook-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/delete-button.mjs" async></script> <script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>
{% endblock %} {% endblock %}
{% block header_center %} {% block header_center %}

View File

@@ -13,7 +13,6 @@ use tower::Service;
#[derive(Clone, RustEmbed, Default)] #[derive(Clone, RustEmbed, Default)]
#[folder = "public/assets"] #[folder = "public/assets"]
#[allow(dead_code)] // Since this is not used with the frontend-dev feature
pub struct Assets; pub struct Assets;
#[derive(Clone, Default)] #[derive(Clone, Default)]

View File

@@ -45,38 +45,38 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
frontend_config: FrontendConfig, frontend_config: FrontendConfig,
oidc_config: Option<OidcConfig>, oidc_config: Option<OidcConfig>,
) -> Router { ) -> Router {
let user_router = Router::new() let mut router = Router::new();
.route("/", get(route_get_home)) router = router
.route("/{user}", get(route_user_named::<CS, AS, AP>)) .route("/", get(route_root))
.route("/user", get(route_get_home))
.route("/user/{user}", get(route_user_named::<CS, AS, AP>))
// App token management // App token management
.route("/{user}/app_token", post(route_post_app_token::<AP>)) .route("/user/{user}/app_token", post(route_post_app_token::<AP>))
.route( .route(
// POST because HTML5 forms don't support DELETE method // POST because HTML5 forms don't support DELETE method
"/{user}/app_token/{id}/delete", "/user/{user}/app_token/{id}/delete",
post(route_delete_app_token::<AP>), post(route_delete_app_token::<AP>),
) )
// Calendar // Calendar
.route("/{user}/calendar", get(route_calendars::<CS>)) .route("/user/{user}/calendar", get(route_calendars::<CS>))
.route("/{user}/calendar/{calendar}", get(route_calendar::<CS>))
.route( .route(
"/{user}/calendar/{calendar}/restore", "/user/{user}/calendar/{calendar}",
get(route_calendar::<CS>),
)
.route(
"/user/{user}/calendar/{calendar}/restore",
post(route_calendar_restore::<CS>), post(route_calendar_restore::<CS>),
) )
// Addressbook // Addressbook
.route("/{user}/addressbook", get(route_addressbooks::<AS>)) .route("/user/{user}/addressbook", get(route_addressbooks::<AS>))
.route( .route(
"/{user}/addressbook/{addressbook}", "/user/{user}/addressbook/{addressbook}",
get(route_addressbook::<AS>), get(route_addressbook::<AS>),
) )
.route( .route(
"/{user}/addressbook/{addressbook}/restore", "/user/{user}/addressbook/{addressbook}/restore",
post(route_addressbook_restore::<AS>), post(route_addressbook_restore::<AS>),
) )
.layer(middleware::from_fn(unauthorized_handler));
let router = Router::new()
.route("/", get(route_root))
.nest("/user", user_router)
.route("/login", get(route_get_login).post(route_post_login::<AP>)) .route("/login", get(route_get_login).post(route_post_login::<AP>))
.route("/logout", post(route_post_logout)); .route("/logout", post(route_post_logout));
@@ -109,7 +109,8 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
.layer(Extension(cal_store.clone())) .layer(Extension(cal_store.clone()))
.layer(Extension(addr_store.clone())) .layer(Extension(addr_store.clone()))
.layer(Extension(frontend_config.clone())) .layer(Extension(frontend_config.clone()))
.layer(Extension(oidc_config.clone())); .layer(Extension(oidc_config.clone()))
.layer(middleware::from_fn(unauthorized_handler));
Router::new() Router::new()
.nest(prefix, router) .nest(prefix, router)
@@ -140,14 +141,15 @@ async fn unauthorized_handler(mut request: Request, next: Next) -> Response {
return resp return resp
.body(Body::new(format!( .body(Body::new(format!(
r#"<!Doctype html> r#"<!Doctype html>
<html> <html>
<head> <head>
<meta http-equiv="refresh" content="1; url={login_url}" /> <meta http-equiv="refresh" content="1; url={login_url}" />
</head> </head>
<body> <body>
Unauthorized, redirecting to <a href="{login_url}">login page</a> Unauthorized, redirecting to <a href="{login_url}">login page</a>
</body> </body>
</html>"#, <html>
"#,
))) )))
.unwrap(); .unwrap();
} }

Some files were not shown because too many files have changed in this diff Show More