mirror of
https://github.com/lennart-k/rustical.git
synced 2026-01-30 12:58:28 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
037e6f5c92 | ||
|
|
311ceb6bc5 | ||
|
|
1174af3a4b | ||
|
|
845b3e61e3 | ||
|
|
e5d6541ffb | ||
|
|
b632ff6fe8 | ||
|
|
1ee873ac93 | ||
|
|
bf5bdb96bc | ||
|
|
47c2a55941 | ||
|
|
bfcd94f096 | ||
|
|
8cbb72719d | ||
|
|
ff0246c4fc | ||
|
|
15124a2fd5 | ||
|
|
5c6f63a5f3 | ||
|
|
17ba8faef2 | ||
|
|
578ddde36d | ||
|
|
9c3972e21c | ||
|
|
816c26565a | ||
|
|
4de0f9f665 | ||
|
|
cf31a51965 | ||
|
|
9fc099f6f4 | ||
|
|
498be172c9 | ||
|
|
b8c395e746 | ||
|
|
4b8a8c61f2 | ||
|
|
f778c470d0 | ||
|
|
d44a172261 | ||
|
|
e0ba34baea | ||
|
|
a7893ddbda | ||
|
|
ed7becffc2 | ||
|
|
c29400a799 | ||
|
|
047552a726 | ||
|
|
1cfc8e7c23 | ||
|
|
b0fdca1b64 |
19
.github/workflows/cicd.yml
vendored
19
.github/workflows/cicd.yml
vendored
@@ -5,6 +5,7 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
RUST_VERSION: "1.92"
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
@@ -12,7 +13,9 @@ jobs:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: rustup update
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- run: cargo check
|
||||
@@ -21,7 +24,9 @@ jobs:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: rustup update
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- run: cargo test --all-features --verbose --workspace
|
||||
@@ -30,7 +35,9 @@ jobs:
|
||||
name: Test Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: rustup update
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
- name: Install tarpaulin
|
||||
run: cargo install cargo-tarpaulin
|
||||
|
||||
@@ -44,8 +51,10 @@ jobs:
|
||||
name: Lints
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: rustup update
|
||||
- run: rustup component add rustfmt clippy
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
|
||||
6
.github/workflows/docker-publish.yml
vendored
6
.github/workflows/docker-publish.yml
vendored
@@ -26,12 +26,12 @@ jobs:
|
||||
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64,linux/amd64
|
||||
|
||||
250
Cargo.lock
generated
250
Cargo.lock
generated
@@ -129,11 +129,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.14.0"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
|
||||
checksum = "bb7125972258312e79827b60c9eb93938334100245081cf701a2dee981b17427"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"askama_macros",
|
||||
"itoa",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
@@ -142,9 +142,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.14.0"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
|
||||
checksum = "8ba5e7259a1580c61571e3116ebaaa01e3c001b2132b17c4cc5c70780ca3e994"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"basic-toml",
|
||||
@@ -154,26 +154,36 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_macros"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "236ce20b77cb13506eaf5024899f4af6e12e8825f390bd943c4c37fd8f322e46"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.14.0"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
|
||||
checksum = "f3c63392767bb2df6aa65a6e1e3b80fd89bb7af6d58359b924c0695620f1512e"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"unicode-ident",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_web"
|
||||
version = "0.14.7"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1acadd534892f9ef8c3809b47997e3cd857fad735edceff77a88be1c8236920"
|
||||
checksum = "c0d6576f8e59513752a3e2673ca602fb403be7d0d0aacba5cd8b219838ab58fe"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_web_derive",
|
||||
@@ -184,13 +194,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "askama_web_derive"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34921de3d57974069bad483fdfe0ec65d88c4ff892edd1ab4d8b03be0dda1b9b"
|
||||
checksum = "9767c17d33a63daf6da5872ffaf2ab0c289cd73ce7ed4f41d5ddf9149c004873"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -275,9 +285,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.1"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
|
||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||
dependencies = [
|
||||
"event-listener 5.4.1",
|
||||
"event-listener-strategy",
|
||||
@@ -325,7 +335,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -360,9 +370,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.7"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
|
||||
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"bytes",
|
||||
@@ -393,9 +403,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.5"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
|
||||
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -412,9 +422,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-extra"
|
||||
version = "0.12.2"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbfe9f610fe4e99cf0cfcd03ccf8c63c28c616fe714d80475ef731f3b13dd21b"
|
||||
checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-core",
|
||||
@@ -563,9 +573,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.50"
|
||||
version = "1.2.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c"
|
||||
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -674,7 +684,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -888,7 +898,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -922,7 +932,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -935,7 +945,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -946,7 +956,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||
dependencies = [
|
||||
"darling_core 0.21.3",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -957,7 +967,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -983,24 +993,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618"
|
||||
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b"
|
||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
@@ -1024,7 +1034,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1231,9 +1241,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
||||
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
@@ -1361,7 +1371,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1761,13 +1771,15 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ical"
|
||||
version = "0.11.0"
|
||||
source = "git+https://github.com/lennart-k/ical-rs#d384dd45495722d69c7f76d62a54a8d6481e90ee"
|
||||
source = "git+https://github.com/lennart-k/ical-rs#dcd3b106758a054f46a5172103abb17972ad032d"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"derive_more",
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"rrule",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
@@ -1910,9 +1922,9 @@ checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.45.0"
|
||||
version = "1.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b76866be74d68b1595eb8060cb9191dca9c021db2316558e52ddc5d55d41b66c"
|
||||
checksum = "983e3b24350c84ab8a65151f537d67afbbf7153bb9f1110e03e9fa9b07f67a5c"
|
||||
dependencies = [
|
||||
"console",
|
||||
"once_cell",
|
||||
@@ -1929,9 +1941,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.9"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
|
||||
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -1972,9 +1984,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
@@ -2018,13 +2030,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.11"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"redox_syscall 0.6.0",
|
||||
"redox_syscall 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2092,9 +2104,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ea5f97102eb9e54ab99fb70bb175589073f554bdadfb74d9bd656482ea73e2a"
|
||||
checksum = "b3eede3bdf92f3b4f9dc04072a9ce5ab557d5ec9038773bf9ffcd5588b3cc05b"
|
||||
|
||||
[[package]]
|
||||
name = "matchit-serde"
|
||||
@@ -2102,7 +2114,7 @@ version = "0.1.0"
|
||||
source = "git+https://github.com/lennart-k/matchit-serde?rev=e18e65d7#e18e65d75cb20ab5f6a193c84a87ee2db8e6ae0b"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"matchit 0.9.0",
|
||||
"matchit 0.9.1",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"thiserror 2.0.17",
|
||||
@@ -2322,7 +2334,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2557,7 +2569,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2630,7 +2642,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2769,9 +2781,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2784,7 +2796,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
"version_check",
|
||||
"yansi",
|
||||
]
|
||||
@@ -2809,7 +2821,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2981,9 +2993,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5"
|
||||
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
@@ -3005,7 +3017,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3045,9 +3057,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.26"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -3179,7 +3191,7 @@ dependencies = [
|
||||
"regex",
|
||||
"relative-path",
|
||||
"rustc_version",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -3191,7 +3203,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"rand 0.8.5",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3224,7 +3236,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -3261,7 +3273,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -3306,7 +3318,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_caldav"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -3348,7 +3360,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_carddav"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3362,6 +3374,7 @@ dependencies = [
|
||||
"insta",
|
||||
"percent-encoding",
|
||||
"quick-xml",
|
||||
"rstest",
|
||||
"rustical_dav",
|
||||
"rustical_dav_push",
|
||||
"rustical_ical",
|
||||
@@ -3381,7 +3394,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3393,7 +3406,7 @@ dependencies = [
|
||||
"ical",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"matchit 0.9.0",
|
||||
"matchit 0.9.1",
|
||||
"matchit-serde",
|
||||
"quick-xml",
|
||||
"rustical_xml",
|
||||
@@ -3407,7 +3420,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav_push"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3432,7 +3445,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_frontend"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_web",
|
||||
@@ -3468,7 +3481,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_ical"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -3485,7 +3498,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_oidc"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3501,7 +3514,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3534,7 +3547,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store_sqlite"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3557,7 +3570,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_xml"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"thiserror 2.0.17",
|
||||
@@ -3566,9 +3579,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
@@ -3620,9 +3633,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
@@ -3647,9 +3660,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
|
||||
checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
@@ -3720,20 +3733,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.145"
|
||||
version = "1.0.148"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3798,7 +3811,7 @@ dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.12.1",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.1.0",
|
||||
"schemars 1.2.0",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
@@ -3814,7 +3827,7 @@ dependencies = [
|
||||
"darling 0.21.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3856,10 +3869,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.7"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -4002,7 +4016,7 @@ dependencies = [
|
||||
"quote",
|
||||
"sqlx-core",
|
||||
"sqlx-macros-core",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4025,7 +4039,7 @@ dependencies = [
|
||||
"sqlx-mysql",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
@@ -4177,7 +4191,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4199,9 +4213,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
version = "2.0.112"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4225,14 +4239,14 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.23.0"
|
||||
version = "3.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
@@ -4267,7 +4281,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4278,7 +4292,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4382,7 +4396,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4694,7 +4708,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4983,7 +4997,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -5087,7 +5101,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5098,7 +5112,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5379,14 +5393,14 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xml_derive"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"heck",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5414,7 +5428,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -5435,7 +5449,7 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5455,7 +5469,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -5495,5 +5509,11 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3280a1b827474fcd5dbef4b35a674deb52ba5c312363aef9135317df179d81b"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.11.6"
|
||||
rust-version = "1.91"
|
||||
version = "0.11.8"
|
||||
rust-version = "1.92"
|
||||
edition = "2024"
|
||||
description = "A CalDAV server"
|
||||
documentation = "https://lennart-k.github.io/rustical/"
|
||||
@@ -89,8 +89,8 @@ derive_more = { version = "2.1", features = [
|
||||
"constructor",
|
||||
"display",
|
||||
] }
|
||||
askama = { version = "0.14", features = ["serde_json"] }
|
||||
askama_web = { version = "0.14", features = ["axum-0.8"] }
|
||||
askama = { version = "0.15", features = ["serde_json"] }
|
||||
askama_web = { version = "0.15", features = ["axum-0.8"] }
|
||||
sqlx = { version = "0.8", default-features = false, features = [
|
||||
"sqlx-sqlite",
|
||||
"uuid",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM rust:1.91-alpine AS chef
|
||||
FROM --platform=$BUILDPLATFORM rust:1.92-alpine AS chef
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
[](https://raw.githubusercontent.com/lennart-k/rustical/main/LICENSE)
|
||||
[](https://coveralls.io/github/lennart-k/rustical?branch=main)
|
||||
|
||||
# RustiCal
|
||||
|
||||
a CalDAV/CardDAV server
|
||||
|
||||
@@ -137,7 +137,7 @@ impl CompFilterable for CalendarObjectComponent {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{TimeZone, Utc};
|
||||
use rustical_dav::xml::{NegateCondition, TextCollation, TextMatchElement};
|
||||
use rustical_dav::xml::{MatchType, NegateCondition, TextCollation, TextMatchElement};
|
||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||
|
||||
use crate::calendar::methods::report::calendar_query::{
|
||||
@@ -217,6 +217,7 @@ END:VCALENDAR";
|
||||
name: "VERSION".to_string(),
|
||||
time_range: None,
|
||||
text_match: Some(TextMatchElement {
|
||||
match_type: MatchType::Contains,
|
||||
needle: "2.0".to_string(),
|
||||
collation: TextCollation::default(),
|
||||
negate_condition: NegateCondition::default(),
|
||||
@@ -240,6 +241,7 @@ END:VCALENDAR";
|
||||
name: "SUMMARY".to_string(),
|
||||
time_range: None,
|
||||
text_match: Some(TextMatchElement {
|
||||
match_type: MatchType::Contains,
|
||||
collation: TextCollation::default(),
|
||||
negate_condition: NegateCondition(false),
|
||||
needle: "weekly".to_string(),
|
||||
@@ -327,6 +329,7 @@ END:VCALENDAR";
|
||||
name: "TZID".to_string(),
|
||||
time_range: None,
|
||||
text_match: Some(TextMatchElement {
|
||||
match_type: MatchType::Contains,
|
||||
collation: TextCollation::AsciiCasemap,
|
||||
negate_condition: NegateCondition::default(),
|
||||
needle: "Europe/Berlin".to_string(),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::comp_filter::{CompFilterElement, CompFilterable};
|
||||
use crate::calendar_object::CalendarObjectPropWrapperName;
|
||||
use ical::property::Property;
|
||||
use rustical_dav::xml::{PropfindType, TextMatchElement};
|
||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||
use rustical_store::calendar_store::CalendarQuery;
|
||||
use rustical_xml::XmlDeserialize;
|
||||
use rustical_xml::{XmlDeserialize, XmlRootTag};
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
@@ -27,7 +28,25 @@ pub struct ParamFilterElement {
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
impl ParamFilterElement {
|
||||
#[must_use]
|
||||
pub fn match_property(&self, prop: &Property) -> bool {
|
||||
let Some(param) = prop.get_param(&self.name) else {
|
||||
return self.is_not_defined.is_some();
|
||||
};
|
||||
if self.is_not_defined.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(text_match) = self.text_match.as_ref() else {
|
||||
return true;
|
||||
};
|
||||
text_match.match_text(param)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
|
||||
#[xml(root = "filter", ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
#[allow(dead_code)]
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
|
||||
pub struct FilterElement {
|
||||
|
||||
@@ -5,6 +5,8 @@ use rustical_store::CalendarStore;
|
||||
mod comp_filter;
|
||||
mod elements;
|
||||
mod prop_filter;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[allow(unused_imports)]
|
||||
pub use comp_filter::{CompFilterElement, CompFilterable};
|
||||
pub use elements::*;
|
||||
@@ -27,7 +29,7 @@ pub async fn get_objects_calendar_query<C: CalendarStore>(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod xml_tests {
|
||||
use super::{
|
||||
CalendarQueryRequest, FilterElement, ParamFilterElement, comp_filter::CompFilterElement,
|
||||
prop_filter::PropFilterElement,
|
||||
@@ -36,7 +38,9 @@ mod tests {
|
||||
calendar::methods::report::ReportRequest,
|
||||
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||
};
|
||||
use rustical_dav::xml::{NegateCondition, PropElement, TextCollation, TextMatchElement};
|
||||
use rustical_dav::xml::{
|
||||
MatchType, NegateCondition, PropElement, TextCollation, TextMatchElement,
|
||||
};
|
||||
use rustical_xml::XmlDocument;
|
||||
|
||||
#[test]
|
||||
@@ -49,16 +53,16 @@ mod tests {
|
||||
<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 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:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>
|
||||
"#;
|
||||
@@ -93,6 +97,7 @@ mod tests {
|
||||
prop_filter: vec![PropFilterElement {
|
||||
name: "ATTENDEE".to_owned(),
|
||||
text_match: Some(TextMatchElement {
|
||||
match_type: MatchType::Contains,
|
||||
collation: TextCollation::AsciiCasemap,
|
||||
negate_condition: NegateCondition(false),
|
||||
needle: "mailto:lisa@example.com".to_string()
|
||||
@@ -102,6 +107,7 @@ mod tests {
|
||||
is_not_defined: None,
|
||||
name: "PARTSTAT".to_owned(),
|
||||
text_match: Some(TextMatchElement {
|
||||
match_type: MatchType::Contains,
|
||||
collation: TextCollation::AsciiCasemap,
|
||||
negate_condition: NegateCondition(false),
|
||||
needle: "NEEDS-ACTION".to_string()
|
||||
|
||||
@@ -30,18 +30,8 @@ pub struct PropFilterElement {
|
||||
}
|
||||
|
||||
impl PropFilterElement {
|
||||
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
||||
let property = comp.get_property(&self.name);
|
||||
let property = match (self.is_not_defined.is_some(), property) {
|
||||
// We are the component that's not supposed to be defined
|
||||
(true, Some(_))
|
||||
// We don't match
|
||||
| (false, None) => return false,
|
||||
// We shall not be and indeed we aren't
|
||||
(true, None) => return true,
|
||||
(false, Some(property)) => property
|
||||
};
|
||||
|
||||
#[must_use]
|
||||
pub fn match_property(&self, property: &Property) -> bool {
|
||||
if let Some(TimeRangeElement { start, end }) = &self.time_range {
|
||||
// TODO: Respect timezones
|
||||
let Ok(timestamp) = CalDateTime::parse_prop(property, &HashMap::default()) else {
|
||||
@@ -67,58 +57,75 @@ impl PropFilterElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: param-filter
|
||||
if !self
|
||||
.param_filter
|
||||
.iter()
|
||||
.all(|param_filter| param_filter.match_property(property))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
||||
let properties = comp.get_named_properties(&self.name);
|
||||
if self.is_not_defined.is_some() {
|
||||
return properties.is_empty();
|
||||
}
|
||||
|
||||
// The filter matches when one property instance matches
|
||||
// Example where this matters: We have multiple attendees and want to match one
|
||||
properties.iter().any(|prop| self.match_property(prop))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PropFilterable {
|
||||
fn get_property(&self, name: &str) -> Option<&Property>;
|
||||
fn get_named_properties(&self, name: &str) -> Vec<&Property>;
|
||||
}
|
||||
|
||||
impl PropFilterable for CalendarObject {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Self::get_property(self, name)
|
||||
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||
Self::get_named_properties(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for IcalEvent {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Component::get_property(self, name)
|
||||
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||
Component::get_named_properties(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for IcalTodo {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Component::get_property(self, name)
|
||||
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||
Component::get_named_properties(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for IcalJournal {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Component::get_property(self, name)
|
||||
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||
Component::get_named_properties(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for IcalCalendar {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Component::get_property(self, name)
|
||||
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||
Component::get_named_properties(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for IcalTimeZone {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Component::get_property(self, name)
|
||||
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||
Component::get_named_properties(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for CalendarObjectComponent {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||
match self {
|
||||
Self::Event(event, _) => PropFilterable::get_property(&event.event, name),
|
||||
Self::Todo(todo, _) => PropFilterable::get_property(todo, name),
|
||||
Self::Journal(journal, _) => PropFilterable::get_property(journal, name),
|
||||
Self::Event(event, _) => PropFilterable::get_named_properties(&event.event, name),
|
||||
Self::Todo(todo, _) => PropFilterable::get_named_properties(todo, name),
|
||||
Self::Journal(journal, _) => PropFilterable::get_named_properties(journal, name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
use super::FilterElement;
|
||||
use rstest::rstest;
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_xml::XmlDocument;
|
||||
|
||||
const ICS_1: &str = r"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
BEGIN:VTIMEZONE
|
||||
LAST-MODIFIED:20040110T032845Z
|
||||
TZID:US/Eastern
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20000404T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
DTSTART:20001026T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
|
||||
TZNAME:EST
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
|
||||
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
|
||||
DTSTAMP:20060206T001220Z
|
||||
DTSTART;TZID=US/Eastern:20060104T100000
|
||||
DURATION:PT1H
|
||||
LAST-MODIFIED:20060206T001330Z
|
||||
ORGANIZER:mailto:cyrus@example.com
|
||||
SEQUENCE:1
|
||||
STATUS:TENTATIVE
|
||||
SUMMARY:Event #3
|
||||
UID:DC6C50A017428C5216A2F1CD@example.com
|
||||
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
";
|
||||
|
||||
const FILTER_1: &str = r#"
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:filter xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<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>
|
||||
"#;
|
||||
|
||||
const FILTER_2: &str = r#"
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:filter xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<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">ACCEPTED</C:text-match>
|
||||
</C:param-filter>
|
||||
</C:prop-filter>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
"#;
|
||||
|
||||
#[rstest]
|
||||
#[case(ICS_1, FILTER_1, true)]
|
||||
#[case(ICS_1, FILTER_2, false)]
|
||||
fn yeet(#[case] ics: &str, #[case] filter: &str, #[case] matches: bool) {
|
||||
let obj = CalendarObject::from_ics(ics.to_owned(), None).unwrap();
|
||||
let filter = FilterElement::parse_str(filter).unwrap();
|
||||
assert_eq!(matches, filter.matches(&obj));
|
||||
}
|
||||
@@ -54,6 +54,7 @@ impl Default for SupportedCollationSet {
|
||||
fn default() -> Self {
|
||||
Self(vec![
|
||||
SupportedCollation(TextCollation::AsciiCasemap),
|
||||
SupportedCollation(TextCollation::UnicodeCasemap),
|
||||
SupportedCollation(TextCollation::Octet),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use rustical_store::auth::Principal;
|
||||
use rustical_xml::{EnumVariants, PropName};
|
||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "CalendarPropName")]
|
||||
@@ -72,8 +73,8 @@ pub struct CalendarResource {
|
||||
}
|
||||
|
||||
impl ResourceName for CalendarResource {
|
||||
fn get_name(&self) -> String {
|
||||
self.cal.id.clone()
|
||||
fn get_name(&self) -> Cow<'_, str> {
|
||||
Cow::from(&self.cal.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
source: crates/caldav/src/calendar/tests.rs
|
||||
expression: output
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<response xmlns="DAV:">
|
||||
<href>/caldav/principal/user/calendar/</href>
|
||||
<propstat>
|
||||
<prop xmlns="DAV:">
|
||||
<calendar-timezone xmlns="urn:ietf:params:xml:ns:caldav">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
|
||||
</calendar-timezone>
|
||||
<timezone-service-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||
<href xmlns="DAV:">https://www.iana.org/time-zones</href>
|
||||
</timezone-service-set>
|
||||
<calendar-timezone-id xmlns="urn:ietf:params:xml:ns:caldav">Europe/Berlin</calendar-timezone-id>
|
||||
<calendar-order xmlns="http://apple.com/ns/ical/">0</calendar-order>
|
||||
<supported-calendar-component-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||
<comp xmlns="urn:ietf:params:xml:ns:caldav" name="VEVENT"/>
|
||||
<comp xmlns="urn:ietf:params:xml:ns:caldav" name="VTODO"/>
|
||||
</supported-calendar-component-set>
|
||||
<supported-calendar-data xmlns="urn:ietf:params:xml:ns:caldav">
|
||||
<calendar-data xmlns="urn:ietf:params:xml:ns:caldav" content-type="text/calendar" version="2.0"/>
|
||||
</supported-calendar-data>
|
||||
<supported-collation-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;ascii-casemap</supported-collation>
|
||||
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;unicode-casemap</supported-collation>
|
||||
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;octet</supported-collation>
|
||||
</supported-collation-set>
|
||||
<max-resource-size xmlns="DAV:">10000000</max-resource-size>
|
||||
<supported-report-set xmlns="DAV:">
|
||||
<supported-report xmlns="DAV:">
|
||||
<report xmlns="DAV:">
|
||||
<calendar-query xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||
</report>
|
||||
</supported-report>
|
||||
<supported-report xmlns="DAV:">
|
||||
<report xmlns="DAV:">
|
||||
<calendar-multiget xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||
</report>
|
||||
</supported-report>
|
||||
<supported-report xmlns="DAV:">
|
||||
<report xmlns="DAV:">
|
||||
<sync-collection xmlns="DAV:"/>
|
||||
</report>
|
||||
</supported-report>
|
||||
</supported-report-set>
|
||||
<min-date-time xmlns="urn:ietf:params:xml:ns:caldav">-2621430101T000000Z</min-date-time>
|
||||
<max-date-time xmlns="urn:ietf:params:xml:ns:caldav">+2621421231T235959Z</max-date-time>
|
||||
<sync-token xmlns="DAV:">github.com/lennart-k/rustical/ns/12</sync-token>
|
||||
<getctag xmlns="http://calendarserver.org/ns/">github.com/lennart-k/rustical/ns/12</getctag>
|
||||
<transports xmlns="https://bitfire.at/webdav-push">
|
||||
<web-push xmlns="https://bitfire.at/webdav-push"/>
|
||||
</transports>
|
||||
<topic xmlns="https://bitfire.at/webdav-push">b28b41e9-8801-4fc5-ae29-8efb5fadeb36</topic>
|
||||
<supported-triggers xmlns="https://bitfire.at/webdav-push">
|
||||
<content-update xmlns="https://bitfire.at/webdav-push">
|
||||
<depth xmlns="DAV:">1</depth>
|
||||
</content-update>
|
||||
<property-update xmlns="https://bitfire.at/webdav-push">
|
||||
<depth xmlns="DAV:">1</depth>
|
||||
</property-update>
|
||||
</supported-triggers>
|
||||
<resourcetype xmlns="DAV:">
|
||||
<collection xmlns="DAV:"/>
|
||||
<calendar xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||
</resourcetype>
|
||||
<displayname xmlns="DAV:">Calendar</displayname>
|
||||
<current-user-principal xmlns="DAV:">
|
||||
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||
</current-user-principal>
|
||||
<current-user-privilege-set xmlns="DAV:">
|
||||
<privilege>
|
||||
<read/>
|
||||
</privilege>
|
||||
<privilege>
|
||||
<write-properties/>
|
||||
</privilege>
|
||||
<privilege>
|
||||
<read-acl/>
|
||||
</privilege>
|
||||
<privilege>
|
||||
<read-current-user-privilege-set/>
|
||||
</privilege>
|
||||
</current-user-privilege-set>
|
||||
<owner xmlns="DAV:">
|
||||
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||
</owner>
|
||||
</prop>
|
||||
<status xmlns="DAV:">HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
</response>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
source: crates/caldav/src/calendar/tests.rs
|
||||
expression: output
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<response xmlns="DAV:">
|
||||
<href>/caldav/principal/user/calendar/</href>
|
||||
<propstat>
|
||||
<prop xmlns="DAV:">
|
||||
<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"/>
|
||||
<supported-collation-set 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 xmlns="DAV:">HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
</response>
|
||||
@@ -1,230 +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"/>
|
||||
<supported-collation-set 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>
|
||||
<CAL:supported-collation-set>
|
||||
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
||||
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
||||
</CAL:supported-collation-set>
|
||||
<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>
|
||||
<write-properties/>
|
||||
</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>
|
||||
@@ -14,14 +14,9 @@ async fn test_propfind() {
|
||||
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)
|
||||
{
|
||||
for (request, resource) in requests.iter().zip(&resources) {
|
||||
let propfind = CalendarResource::parse_propfind(request).unwrap();
|
||||
|
||||
let response = resource
|
||||
@@ -33,13 +28,12 @@ async fn test_propfind() {
|
||||
&principal,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_output = expected_output.trim();
|
||||
let output = response
|
||||
.serialize_to_string()
|
||||
.unwrap()
|
||||
.trim()
|
||||
.replace("\r\n", "\n");
|
||||
similar_asserts::assert_eq!(expected_output, output);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::prop::{
|
||||
CalendarData, CalendarObjectProp, CalendarObjectPropName, CalendarObjectPropWrapper,
|
||||
CalendarObjectPropWrapperName,
|
||||
@@ -20,8 +22,8 @@ pub struct CalendarObjectResource {
|
||||
}
|
||||
|
||||
impl ResourceName for CalendarObjectResource {
|
||||
fn get_name(&self) -> String {
|
||||
format!("{}.ics", self.object.get_id())
|
||||
fn get_name(&self) -> Cow<'_, str> {
|
||||
Cow::from(format!("{}.ics", self.object.get_id()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use rustical_dav::xml::{
|
||||
GroupMemberSet, GroupMembership, Resourcetype, ResourcetypeInner, SupportedReportSet,
|
||||
};
|
||||
use rustical_store::auth::Principal;
|
||||
use std::borrow::Cow;
|
||||
|
||||
mod service;
|
||||
pub use service::*;
|
||||
@@ -23,8 +24,8 @@ pub struct PrincipalResource {
|
||||
}
|
||||
|
||||
impl ResourceName for PrincipalResource {
|
||||
fn get_name(&self) -> String {
|
||||
self.principal.id.clone()
|
||||
fn get_name(&self) -> Cow<'_, str> {
|
||||
Cow::from(&self.principal.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,51 +3,51 @@ source: crates/caldav/src/principal/tests.rs
|
||||
expression: response.serialize_to_string().unwrap()
|
||||
---
|
||||
<?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">
|
||||
<response xmlns="DAV:">
|
||||
<href>/caldav/principal/user/</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
<CAL:calendar-user-type>INDIVIDUAL</CAL:calendar-user-type>
|
||||
<CAL:calendar-user-address-set>
|
||||
<href>/caldav/principal/user/</href>
|
||||
</CAL:calendar-user-address-set>
|
||||
<principal-URL>
|
||||
<href>/caldav/principal/user/</href>
|
||||
<prop xmlns="DAV:">
|
||||
<calendar-user-type xmlns="urn:ietf:params:xml:ns:caldav">INDIVIDUAL</calendar-user-type>
|
||||
<calendar-user-address-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||
</calendar-user-address-set>
|
||||
<principal-URL xmlns="DAV:">
|
||||
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||
</principal-URL>
|
||||
<group-membership>
|
||||
<href>/caldav/principal/group/</href>
|
||||
<group-membership xmlns="DAV:">
|
||||
<href xmlns="DAV:">/caldav/principal/group/</href>
|
||||
</group-membership>
|
||||
<group-member-set>
|
||||
<group-member-set xmlns="DAV:">
|
||||
</group-member-set>
|
||||
<alternate-URI-set/>
|
||||
<supported-report-set>
|
||||
<supported-report>
|
||||
<report>
|
||||
<principal-match/>
|
||||
<alternate-URI-set xmlns="DAV:"/>
|
||||
<supported-report-set xmlns="DAV:">
|
||||
<supported-report xmlns="DAV:">
|
||||
<report xmlns="DAV:">
|
||||
<principal-match xmlns="DAV:"/>
|
||||
</report>
|
||||
</supported-report>
|
||||
</supported-report-set>
|
||||
<CAL:calendar-home-set>
|
||||
<href>/caldav/principal/group/</href>
|
||||
<href>/caldav/principal/user/</href>
|
||||
</CAL:calendar-home-set>
|
||||
<resourcetype>
|
||||
<collection/>
|
||||
<principal/>
|
||||
<calendar-home-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||
<href xmlns="DAV:">/caldav/principal/group/</href>
|
||||
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||
</calendar-home-set>
|
||||
<resourcetype xmlns="DAV:">
|
||||
<collection xmlns="DAV:"/>
|
||||
<principal xmlns="DAV:"/>
|
||||
</resourcetype>
|
||||
<displayname>user</displayname>
|
||||
<current-user-principal>
|
||||
<href>/caldav/principal/user/</href>
|
||||
<displayname xmlns="DAV:">user</displayname>
|
||||
<current-user-principal xmlns="DAV:">
|
||||
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||
</current-user-principal>
|
||||
<current-user-privilege-set>
|
||||
<current-user-privilege-set xmlns="DAV:">
|
||||
<privilege>
|
||||
<all/>
|
||||
</privilege>
|
||||
</current-user-privilege-set>
|
||||
<owner>
|
||||
<href>/caldav/principal/user/</href>
|
||||
<owner xmlns="DAV:">
|
||||
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||
</owner>
|
||||
</prop>
|
||||
<status>HTTP/1.1 200 OK</status>
|
||||
<status xmlns="DAV:">HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
</response>
|
||||
|
||||
@@ -5,32 +5,27 @@ use crate::{
|
||||
use rstest::rstest;
|
||||
use rustical_dav::resource::{Resource, ResourceService};
|
||||
use rustical_store::auth::{Principal, PrincipalType::Individual};
|
||||
use rustical_store_sqlite::{
|
||||
SqliteStore,
|
||||
calendar_store::SqliteCalendarStore,
|
||||
principal_store::SqlitePrincipalStore,
|
||||
tests::{get_test_calendar_store, get_test_principal_store, get_test_subscription_store},
|
||||
};
|
||||
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||
use rustical_xml::XmlSerializeRoot;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_principal_resource(
|
||||
#[from(get_test_calendar_store)]
|
||||
#[future]
|
||||
cal_store: SqliteCalendarStore,
|
||||
#[from(get_test_principal_store)]
|
||||
#[future]
|
||||
auth_provider: SqlitePrincipalStore,
|
||||
#[from(get_test_subscription_store)]
|
||||
#[future]
|
||||
sub_store: SqliteStore,
|
||||
#[from(test_store_context)]
|
||||
context: TestStoreContext,
|
||||
) {
|
||||
let TestStoreContext {
|
||||
cal_store,
|
||||
sub_store,
|
||||
principal_store: auth_provider,
|
||||
..
|
||||
} = context.await;
|
||||
let service = PrincipalResourceService {
|
||||
cal_store: Arc::new(cal_store.await),
|
||||
sub_store: Arc::new(sub_store.await),
|
||||
auth_provider: Arc::new(auth_provider.await),
|
||||
cal_store: Arc::new(cal_store),
|
||||
sub_store: Arc::new(sub_store),
|
||||
auth_provider: Arc::new(auth_provider),
|
||||
simplified_home_set: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ percent-encoding.workspace = true
|
||||
ical.workspace = true
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
rstest.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
insta.workspace = true
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
Error,
|
||||
address_object::{
|
||||
@@ -22,8 +24,8 @@ pub struct AddressObjectResource {
|
||||
}
|
||||
|
||||
impl ResourceName for AddressObjectResource {
|
||||
fn get_name(&self) -> String {
|
||||
format!("{}.vcf", self.object.get_id())
|
||||
fn get_name(&self) -> Cow<'_, str> {
|
||||
Cow::from(format!("{}.vcf", self.object.get_id()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ use crate::{
|
||||
address_object::AddressObjectPropWrapperName,
|
||||
addressbook::methods::report::addressbook_query::PropFilterElement,
|
||||
};
|
||||
use derive_more::{From, Into};
|
||||
use ical::property::Property;
|
||||
use rustical_dav::xml::{PropfindType, TextMatchElement};
|
||||
use rustical_ical::{AddressObject, UtcDateTime};
|
||||
use rustical_xml::XmlDeserialize;
|
||||
use rustical_xml::{ValueDeserialize, XmlDeserialize, XmlRootTag};
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
@@ -28,18 +30,51 @@ pub struct ParamFilterElement {
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
impl ParamFilterElement {
|
||||
#[must_use]
|
||||
pub fn match_property(&self, prop: &Property) -> bool {
|
||||
let Some(param) = prop.get_param(&self.name) else {
|
||||
return self.is_not_defined.is_some();
|
||||
};
|
||||
if self.is_not_defined.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(text_match) = self.text_match.as_ref() else {
|
||||
return true;
|
||||
};
|
||||
text_match.match_text(param)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default, From, Into)]
|
||||
pub struct Allof(pub bool);
|
||||
|
||||
impl ValueDeserialize for Allof {
|
||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||
Ok(Self(match val {
|
||||
"allof" => true,
|
||||
"anyof" => false,
|
||||
_ => {
|
||||
return Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||
"Invalid test parameter: {val}"
|
||||
)));
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// <!ELEMENT filter (prop-filter*)>
|
||||
// <!ATTLIST filter test (anyof | allof) "anyof">
|
||||
// <!-- test value:
|
||||
// anyof logical OR for prop-filter matches
|
||||
// allof logical AND for prop-filter matches -->
|
||||
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq, Eq)]
|
||||
#[xml(root = "filter", ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
#[allow(dead_code)]
|
||||
pub struct FilterElement {
|
||||
#[xml(ty = "attr")]
|
||||
pub anyof: Option<String>,
|
||||
#[xml(ty = "attr")]
|
||||
pub allof: Option<String>,
|
||||
#[xml(ty = "attr", default = "Default::default")]
|
||||
pub test: Allof,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
||||
pub(crate) prop_filter: Vec<PropFilterElement>,
|
||||
}
|
||||
@@ -47,11 +82,7 @@ pub struct FilterElement {
|
||||
impl FilterElement {
|
||||
#[must_use]
|
||||
pub fn matches(&self, addr_object: &AddressObject) -> bool {
|
||||
let allof = match (self.allof.is_some(), self.anyof.is_some()) {
|
||||
(true, false) => true,
|
||||
(false, _) => false,
|
||||
(true, true) => panic!("wat"),
|
||||
};
|
||||
let Allof(allof) = self.test;
|
||||
let mut results = self
|
||||
.prop_filter
|
||||
.iter()
|
||||
|
||||
@@ -7,6 +7,9 @@ pub use prop_filter::{PropFilterElement, PropFilterable};
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::AddressbookStore;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub async fn get_objects_addressbook_query<AS: AddressbookStore>(
|
||||
addr_query: &AddressbookQueryRequest,
|
||||
principal: &str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::ParamFilterElement;
|
||||
use super::{Allof, ParamFilterElement};
|
||||
use ical::{parser::Component, property::Property};
|
||||
use rustical_dav::xml::TextMatchElement;
|
||||
use rustical_ical::AddressObject;
|
||||
@@ -22,54 +22,52 @@ pub struct PropFilterElement {
|
||||
pub(crate) text_match: Vec<TextMatchElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
||||
pub(crate) param_filter: Vec<ParamFilterElement>,
|
||||
#[xml(ty = "attr", default = "Default::default")]
|
||||
pub test: Allof,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) name: String,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub anyof: Option<String>,
|
||||
#[xml(ty = "attr")]
|
||||
pub allof: Option<String>,
|
||||
}
|
||||
|
||||
impl PropFilterElement {
|
||||
#[must_use]
|
||||
pub fn match_property(&self, property: &Property) -> bool {
|
||||
let Allof(allof) = self.test;
|
||||
let text_matches = self
|
||||
.text_match
|
||||
.iter()
|
||||
.map(|text_match| text_match.match_property(property));
|
||||
|
||||
let param_matches = self
|
||||
.param_filter
|
||||
.iter()
|
||||
.map(|param_filter| param_filter.match_property(property));
|
||||
let mut matches = text_matches.chain(param_matches);
|
||||
|
||||
if allof {
|
||||
matches.all(|a| a)
|
||||
} else {
|
||||
matches.any(|a| a)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
||||
let property = comp.get_property(&self.name);
|
||||
let _property = match (self.is_not_defined.is_some(), property) {
|
||||
// We are the component that's not supposed to be defined
|
||||
(true, Some(_))
|
||||
// We don't match
|
||||
| (false, None) => return false,
|
||||
// We shall not be and indeed we aren't
|
||||
(true, None) => return true,
|
||||
(false, Some(property)) => property
|
||||
};
|
||||
let properties = comp.get_named_properties(&self.name);
|
||||
if self.is_not_defined.is_some() {
|
||||
return properties.is_empty();
|
||||
}
|
||||
|
||||
let _allof = match (self.allof.is_some(), self.anyof.is_some()) {
|
||||
(true, false) => true,
|
||||
(false, _) => false,
|
||||
(true, true) => panic!("wat"),
|
||||
};
|
||||
|
||||
// TODO: IMPLEMENT
|
||||
// if let Some(text_match) = &self.text_match
|
||||
// && !text_match.match_property(property)
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// TODO: param-filter
|
||||
|
||||
true
|
||||
// The filter matches when one property instance matches
|
||||
properties.iter().any(|prop| self.match_property(prop))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PropFilterable {
|
||||
fn get_property(&self, name: &str) -> Option<&Property>;
|
||||
fn get_named_properties(&self, name: &str) -> Vec<&Property>;
|
||||
}
|
||||
|
||||
impl PropFilterable for AddressObject {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
self.get_vcard().get_property(name)
|
||||
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||
self.get_vcard().get_named_properties(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use super::FilterElement;
|
||||
use rstest::rstest;
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_xml::XmlDocument;
|
||||
|
||||
const VCF_1: &str = r"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:Simon Perreault
|
||||
N:Perreault;Simon;;;ing. jr,M.Sc.
|
||||
BDAY:--0203
|
||||
GENDER:M
|
||||
EMAIL;TYPE=work:simon.perreault@viagenie.ca
|
||||
END:VCARD";
|
||||
|
||||
const VCF_2: &str = r"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
N:Gump;Forrest;;Mr.;
|
||||
FN:Forrest Gump
|
||||
ORG:Bubba Gump Shrimp Co.
|
||||
TITLE:Shrimp Man
|
||||
PHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif
|
||||
TEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212
|
||||
TEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212
|
||||
ADR;TYPE=WORK;PREF=1;LABEL=100 Waters Edge\\nBaytown\\, LA 30314\\nUnited S
|
||||
tates of America:;;100 Waters Edge;Baytown;LA;30314;United States of Ameri
|
||||
ca
|
||||
ADR;TYPE=HOME;LABEL=42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States
|
||||
of America:;;42 Plantation St.;Baytown;LA;30314;United States of America
|
||||
EMAIL:forrestgump@example.com
|
||||
REV:20080424T195243Z
|
||||
x-qq:21588891
|
||||
UID:890a9da4-bb6d-4afb-9f32-b5eff6494a53
|
||||
END:VCARD
|
||||
";
|
||||
|
||||
const FILTER_1: &str = r#"
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:filter xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||
<C:prop-filter name="EMAIL" test="allof">
|
||||
<C:text-match collation="i;ascii-casemap">simon.perreault@viagenie.ca</C:text-match>
|
||||
<C:param-filter name="TYPE">
|
||||
<C:text-match match-type="equals" collation="i;unicode-casemap">WORK</C:text-match>
|
||||
</C:param-filter>
|
||||
</C:prop-filter>
|
||||
</C:filter>
|
||||
"#;
|
||||
|
||||
const FILTER_2: &str = r#"
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:filter xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||
<C:prop-filter name="EMAIL" test="anyof">
|
||||
<C:text-match collation="i;ascii-casemap">forrestgump@example.com</C:text-match>
|
||||
<C:param-filter name="TYPE">
|
||||
<C:text-match match-type="equals" collation="i;ascii-casemap">WORK</C:text-match>
|
||||
</C:param-filter>
|
||||
</C:prop-filter>
|
||||
</C:filter>
|
||||
"#;
|
||||
|
||||
#[rstest]
|
||||
#[case(VCF_1, FILTER_1, true)]
|
||||
#[case(VCF_2, FILTER_1, false)]
|
||||
#[case(VCF_1, FILTER_2, true)]
|
||||
#[case(VCF_2, FILTER_2, true)]
|
||||
fn test_filter(#[case] vcf: &str, #[case] filter: &str, #[case] matches: bool) {
|
||||
dbg!(vcf);
|
||||
let obj = AddressObject::from_vcf(String::new(), vcf.to_owned()).unwrap();
|
||||
let filter = FilterElement::parse_str(filter).unwrap();
|
||||
assert_eq!(matches, filter.matches(&obj));
|
||||
}
|
||||
@@ -158,7 +158,9 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
address_object::AddressObjectPropName,
|
||||
addressbook::methods::report::addressbook_query::{FilterElement, PropFilterElement},
|
||||
addressbook::methods::report::addressbook_query::{
|
||||
Allof, FilterElement, PropFilterElement,
|
||||
},
|
||||
};
|
||||
use rustical_dav::xml::{PropElement, sync_collection::SyncLevel};
|
||||
|
||||
@@ -250,15 +252,13 @@ mod tests {
|
||||
vec![]
|
||||
)),
|
||||
filter: FilterElement {
|
||||
anyof: None,
|
||||
allof: None,
|
||||
test: Allof::default(),
|
||||
prop_filter: vec![PropFilterElement {
|
||||
name: "FN".to_owned(),
|
||||
is_not_defined: None,
|
||||
text_match: vec![],
|
||||
param_filter: vec![],
|
||||
allof: None,
|
||||
anyof: None
|
||||
test: Allof::default()
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use derive_more::{From, Into};
|
||||
use rustical_dav::{
|
||||
extensions::{CommonPropertiesProp, SyncTokenExtensionProp},
|
||||
xml::SupportedReportSet,
|
||||
xml::{SupportedReportSet, TextCollation},
|
||||
};
|
||||
use rustical_dav_push::DavPushExtensionProp;
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
@@ -14,6 +15,8 @@ pub enum AddressbookProp {
|
||||
AddressbookDescription(Option<String>),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
||||
SupportedAddressData(SupportedAddressData),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
||||
SupportedCollationSet(SupportedCollationSet),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
|
||||
SupportedReportSet(SupportedReportSet<ReportMethod>),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
@@ -60,6 +63,29 @@ impl Default for SupportedAddressData {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
|
||||
pub struct SupportedCollation(#[xml(ty = "text")] pub TextCollation);
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq)]
|
||||
pub struct SupportedCollationSet(
|
||||
#[xml(
|
||||
ns = "rustical_dav::namespace::NS_CARDDAV",
|
||||
flatten,
|
||||
rename = "supported-collation"
|
||||
)]
|
||||
pub Vec<SupportedCollation>,
|
||||
);
|
||||
|
||||
impl Default for SupportedCollationSet {
|
||||
fn default() -> Self {
|
||||
Self(vec![
|
||||
SupportedCollation(TextCollation::AsciiCasemap),
|
||||
SupportedCollation(TextCollation::UnicodeCasemap),
|
||||
SupportedCollation(TextCollation::Octet),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, PartialEq, Eq, VariantArray)]
|
||||
pub enum ReportMethod {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::prop::SupportedAddressData;
|
||||
use crate::Error;
|
||||
use crate::addressbook::prop::{
|
||||
AddressbookProp, AddressbookPropName, AddressbookPropWrapper, AddressbookPropWrapperName,
|
||||
SupportedCollationSet,
|
||||
};
|
||||
use derive_more::derive::{From, Into};
|
||||
use rustical_dav::extensions::{CommonPropertiesExtension, SyncTokenExtension};
|
||||
@@ -11,13 +12,14 @@ use rustical_dav::xml::{Resourcetype, ResourcetypeInner, SupportedReportSet};
|
||||
use rustical_dav_push::DavPushExtension;
|
||||
use rustical_store::Addressbook;
|
||||
use rustical_store::auth::Principal;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Clone, Debug, From, Into)]
|
||||
pub struct AddressbookResource(pub(crate) Addressbook);
|
||||
|
||||
impl ResourceName for AddressbookResource {
|
||||
fn get_name(&self) -> String {
|
||||
self.0.id.clone()
|
||||
fn get_name(&self) -> Cow<'_, str> {
|
||||
Cow::from(&self.0.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +63,9 @@ impl Resource for AddressbookResource {
|
||||
AddressbookPropName::MaxResourceSize => {
|
||||
AddressbookProp::MaxResourceSize(10_000_000)
|
||||
}
|
||||
AddressbookPropName::SupportedCollationSet => {
|
||||
AddressbookProp::SupportedCollationSet(SupportedCollationSet::default())
|
||||
}
|
||||
AddressbookPropName::SupportedReportSet => {
|
||||
AddressbookProp::SupportedReportSet(SupportedReportSet::all())
|
||||
}
|
||||
@@ -93,6 +98,7 @@ impl Resource for AddressbookResource {
|
||||
Ok(())
|
||||
}
|
||||
AddressbookProp::MaxResourceSize(_)
|
||||
| AddressbookProp::SupportedCollationSet(_)
|
||||
| AddressbookProp::SupportedReportSet(_)
|
||||
| AddressbookProp::SupportedAddressData(_) => {
|
||||
Err(rustical_dav::Error::PropReadOnly)
|
||||
@@ -115,6 +121,7 @@ impl Resource for AddressbookResource {
|
||||
Ok(())
|
||||
}
|
||||
AddressbookPropName::MaxResourceSize
|
||||
| AddressbookPropName::SupportedCollationSet
|
||||
| AddressbookPropName::SupportedReportSet
|
||||
| AddressbookPropName::SupportedAddressData => {
|
||||
Err(rustical_dav::Error::PropReadOnly)
|
||||
|
||||
@@ -31,6 +31,23 @@ ResponseElement {
|
||||
},
|
||||
),
|
||||
),
|
||||
Addressbook(
|
||||
SupportedCollationSet(
|
||||
SupportedCollationSet(
|
||||
[
|
||||
SupportedCollation(
|
||||
AsciiCasemap,
|
||||
),
|
||||
SupportedCollation(
|
||||
UnicodeCasemap,
|
||||
),
|
||||
SupportedCollation(
|
||||
Octet,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Addressbook(
|
||||
SupportedReportSet(
|
||||
SupportedReportSet {
|
||||
|
||||
@@ -3,57 +3,62 @@ source: crates/carddav/src/addressbook/tests.rs
|
||||
expression: response.serialize_to_string().unwrap()
|
||||
---
|
||||
<?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">
|
||||
<response xmlns="DAV:">
|
||||
<href>/carddav/principal/user/yeet/</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
<CARD:supported-address-data>
|
||||
<CARD:address-data-type content-type="text/vcard" version="3.0"/>
|
||||
<CARD:address-data-type content-type="text/vcard" version="4.0"/>
|
||||
</CARD:supported-address-data>
|
||||
<supported-report-set>
|
||||
<supported-report>
|
||||
<report>
|
||||
<CARD:addressbook-multiget/>
|
||||
<prop xmlns="DAV:">
|
||||
<supported-address-data xmlns="urn:ietf:params:xml:ns:carddav">
|
||||
<address-data-type xmlns="urn:ietf:params:xml:ns:carddav" content-type="text/vcard" version="3.0"/>
|
||||
<address-data-type xmlns="urn:ietf:params:xml:ns:carddav" content-type="text/vcard" version="4.0"/>
|
||||
</supported-address-data>
|
||||
<supported-collation-set xmlns="urn:ietf:params:xml:ns:carddav">
|
||||
<supported-collation xmlns="urn:ietf:params:xml:ns:carddav">i;ascii-casemap</supported-collation>
|
||||
<supported-collation xmlns="urn:ietf:params:xml:ns:carddav">i;unicode-casemap</supported-collation>
|
||||
<supported-collation xmlns="urn:ietf:params:xml:ns:carddav">i;octet</supported-collation>
|
||||
</supported-collation-set>
|
||||
<supported-report-set xmlns="DAV:">
|
||||
<supported-report xmlns="DAV:">
|
||||
<report xmlns="DAV:">
|
||||
<addressbook-multiget xmlns="urn:ietf:params:xml:ns:carddav"/>
|
||||
</report>
|
||||
</supported-report>
|
||||
<supported-report>
|
||||
<report>
|
||||
<sync-collection/>
|
||||
<supported-report xmlns="DAV:">
|
||||
<report xmlns="DAV:">
|
||||
<sync-collection xmlns="DAV:"/>
|
||||
</report>
|
||||
</supported-report>
|
||||
</supported-report-set>
|
||||
<max-resource-size>10000000</max-resource-size>
|
||||
<sync-token>github.com/lennart-k/rustical/ns/0</sync-token>
|
||||
<CS:getctag>github.com/lennart-k/rustical/ns/0</CS:getctag>
|
||||
<PUSH:transports>
|
||||
<PUSH:web-push/>
|
||||
</PUSH:transports>
|
||||
<PUSH:topic>asdasd</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/>
|
||||
<CARD:addressbook/>
|
||||
<max-resource-size xmlns="DAV:">10000000</max-resource-size>
|
||||
<sync-token xmlns="DAV:">github.com/lennart-k/rustical/ns/0</sync-token>
|
||||
<getctag xmlns="http://calendarserver.org/ns/">github.com/lennart-k/rustical/ns/0</getctag>
|
||||
<transports xmlns="https://bitfire.at/webdav-push">
|
||||
<web-push xmlns="https://bitfire.at/webdav-push"/>
|
||||
</transports>
|
||||
<topic xmlns="https://bitfire.at/webdav-push">asdasd</topic>
|
||||
<supported-triggers xmlns="https://bitfire.at/webdav-push">
|
||||
<content-update xmlns="https://bitfire.at/webdav-push">
|
||||
<depth xmlns="DAV:">1</depth>
|
||||
</content-update>
|
||||
<property-update xmlns="https://bitfire.at/webdav-push">
|
||||
<depth xmlns="DAV:">1</depth>
|
||||
</property-update>
|
||||
</supported-triggers>
|
||||
<resourcetype xmlns="DAV:">
|
||||
<collection xmlns="DAV:"/>
|
||||
<addressbook xmlns="urn:ietf:params:xml:ns:carddav"/>
|
||||
</resourcetype>
|
||||
<current-user-principal>
|
||||
<href>/carddav/principal/user/</href>
|
||||
<current-user-principal xmlns="DAV:">
|
||||
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||
</current-user-principal>
|
||||
<current-user-privilege-set>
|
||||
<current-user-privilege-set xmlns="DAV:">
|
||||
<privilege>
|
||||
<all/>
|
||||
</privilege>
|
||||
</current-user-privilege-set>
|
||||
<owner>
|
||||
<href>/carddav/principal/user/</href>
|
||||
<owner xmlns="DAV:">
|
||||
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||
</owner>
|
||||
</prop>
|
||||
<status>HTTP/1.1 200 OK</status>
|
||||
<status xmlns="DAV:">HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
</response>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::Error;
|
||||
use rustical_dav::extensions::CommonPropertiesExtension;
|
||||
use rustical_dav::privileges::UserPrivilegeSet;
|
||||
@@ -21,8 +23,8 @@ pub struct PrincipalResource {
|
||||
}
|
||||
|
||||
impl ResourceName for PrincipalResource {
|
||||
fn get_name(&self) -> String {
|
||||
self.principal.id.clone()
|
||||
fn get_name(&self) -> Cow<'_, str> {
|
||||
Cow::from(&self.principal.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,43 +3,43 @@ source: crates/carddav/src/principal/tests.rs
|
||||
expression: response.serialize_to_string().unwrap()
|
||||
---
|
||||
<?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">
|
||||
<response xmlns="DAV:">
|
||||
<href>/carddav/principal/user/</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
<principal-URL>
|
||||
<href>/carddav/principal/user/</href>
|
||||
<prop xmlns="DAV:">
|
||||
<principal-URL xmlns="DAV:">
|
||||
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||
</principal-URL>
|
||||
<group-membership>
|
||||
<href>/carddav/principal/group/</href>
|
||||
<group-membership xmlns="DAV:">
|
||||
<href xmlns="DAV:">/carddav/principal/group/</href>
|
||||
</group-membership>
|
||||
<group-member-set>
|
||||
<group-member-set xmlns="DAV:">
|
||||
</group-member-set>
|
||||
<alternate-URI-set/>
|
||||
<principal-collection-set>
|
||||
<href>/carddav/principal/</href>
|
||||
<alternate-URI-set xmlns="DAV:"/>
|
||||
<principal-collection-set xmlns="DAV:">
|
||||
<href xmlns="DAV:">/carddav/principal/</href>
|
||||
</principal-collection-set>
|
||||
<CARD:addressbook-home-set>
|
||||
<href>/carddav/principal/group/</href>
|
||||
<href>/carddav/principal/user/</href>
|
||||
</CARD:addressbook-home-set>
|
||||
<resourcetype>
|
||||
<collection/>
|
||||
<principal/>
|
||||
<addressbook-home-set xmlns="urn:ietf:params:xml:ns:carddav">
|
||||
<href xmlns="DAV:">/carddav/principal/group/</href>
|
||||
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||
</addressbook-home-set>
|
||||
<resourcetype xmlns="DAV:">
|
||||
<collection xmlns="DAV:"/>
|
||||
<principal xmlns="DAV:"/>
|
||||
</resourcetype>
|
||||
<displayname>user</displayname>
|
||||
<current-user-principal>
|
||||
<href>/carddav/principal/user/</href>
|
||||
<displayname xmlns="DAV:">user</displayname>
|
||||
<current-user-principal xmlns="DAV:">
|
||||
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||
</current-user-principal>
|
||||
<current-user-privilege-set>
|
||||
<current-user-privilege-set xmlns="DAV:">
|
||||
<privilege>
|
||||
<all/>
|
||||
</privilege>
|
||||
</current-user-privilege-set>
|
||||
<owner>
|
||||
<href>/carddav/principal/user/</href>
|
||||
<owner xmlns="DAV:">
|
||||
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||
</owner>
|
||||
</prop>
|
||||
<status>HTTP/1.1 200 OK</status>
|
||||
<status xmlns="DAV:">HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
</response>
|
||||
|
||||
@@ -11,6 +11,7 @@ pub use resource_service::ResourceService;
|
||||
use rustical_xml::{
|
||||
EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlDocument, XmlSerialize,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod axum_methods;
|
||||
@@ -30,7 +31,7 @@ pub trait ResourcePropName: FromStr {}
|
||||
impl<T: FromStr> ResourcePropName for T {}
|
||||
|
||||
pub trait ResourceName {
|
||||
fn get_name(&self) -> String;
|
||||
fn get_name(&self) -> Cow<'_, str>;
|
||||
}
|
||||
|
||||
pub trait Resource: Clone + Send + 'static {
|
||||
|
||||
@@ -42,13 +42,6 @@ pub enum PropstatWrapper<T: XmlSerialize> {
|
||||
// responsedescription?) >
|
||||
#[derive(XmlSerialize, XmlRootTag, Debug)]
|
||||
#[xml(ns = "crate::namespace::NS_DAV", root = "response")]
|
||||
#[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 href: String,
|
||||
#[xml(serialize_with = "xml_serialize_optional_status")]
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use ical::property::Property;
|
||||
use rustical_xml::{ValueDeserialize, XmlDeserialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum TextCollation {
|
||||
#[default]
|
||||
AsciiCasemap,
|
||||
UnicodeCasemap,
|
||||
Octet,
|
||||
}
|
||||
|
||||
impl TextCollation {
|
||||
// Check whether a haystack contains a needle respecting the collation
|
||||
#[must_use]
|
||||
pub fn match_text(&self, needle: &str, haystack: &str) -> bool {
|
||||
pub fn normalise<'a>(&self, value: &'a str) -> Cow<'a, str> {
|
||||
match self {
|
||||
// https://datatracker.ietf.org/doc/html/rfc4790#section-9.2
|
||||
Self::AsciiCasemap => haystack
|
||||
.to_ascii_uppercase()
|
||||
.contains(&needle.to_ascii_uppercase()),
|
||||
Self::Octet => haystack.contains(needle),
|
||||
Self::AsciiCasemap => Cow::from(value.to_ascii_uppercase()),
|
||||
Self::UnicodeCasemap => Cow::from(value.to_uppercase()),
|
||||
Self::Octet => Cow::from(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ impl AsRef<str> for TextCollation {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::AsciiCasemap => "i;ascii-casemap",
|
||||
Self::UnicodeCasemap => "i;unicode-casemap",
|
||||
Self::Octet => "i;octet",
|
||||
}
|
||||
}
|
||||
@@ -35,6 +36,7 @@ impl ValueDeserialize for TextCollation {
|
||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||
match val {
|
||||
"i;ascii-casemap" => Ok(Self::AsciiCasemap),
|
||||
"i;unicode-casemap" => Ok(Self::UnicodeCasemap),
|
||||
"i;octet" => Ok(Self::Octet),
|
||||
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||
"Invalid collation: {val}"
|
||||
@@ -58,6 +60,46 @@ impl ValueDeserialize for NegateCondition {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum MatchType {
|
||||
Equals,
|
||||
#[default]
|
||||
Contains,
|
||||
StartsWith,
|
||||
EndsWith,
|
||||
}
|
||||
|
||||
impl MatchType {
|
||||
#[must_use]
|
||||
pub fn match_text(&self, collation: &TextCollation, needle: &str, haystack: &str) -> bool {
|
||||
let haystack = collation.normalise(haystack);
|
||||
let needle = collation.normalise(needle);
|
||||
|
||||
match &self {
|
||||
Self::Equals => haystack == needle,
|
||||
Self::Contains => haystack.contains(needle.as_ref()),
|
||||
Self::StartsWith => haystack.starts_with(needle.as_ref()),
|
||||
Self::EndsWith => haystack.ends_with(needle.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueDeserialize for MatchType {
|
||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||
Ok(match val {
|
||||
"equals" => Self::Equals,
|
||||
"contains" => Self::Contains,
|
||||
"starts-with" => Self::StartsWith,
|
||||
"ends-with" => Self::EndsWith,
|
||||
_ => {
|
||||
return Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||
"Invalid match-type parameter: {val}"
|
||||
)));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub struct TextMatchElement {
|
||||
@@ -65,39 +107,48 @@ pub struct TextMatchElement {
|
||||
pub collation: TextCollation,
|
||||
#[xml(ty = "attr", default = "Default::default")]
|
||||
pub negate_condition: NegateCondition,
|
||||
#[xml(ty = "attr", default = "Default::default")]
|
||||
pub match_type: MatchType,
|
||||
#[xml(ty = "text")]
|
||||
pub needle: String,
|
||||
}
|
||||
|
||||
impl TextMatchElement {
|
||||
#[must_use]
|
||||
pub fn match_property(&self, property: &Property) -> bool {
|
||||
pub fn match_text(&self, haystack: &str) -> bool {
|
||||
let Self {
|
||||
collation,
|
||||
negate_condition,
|
||||
needle,
|
||||
match_type,
|
||||
} = self;
|
||||
|
||||
let matches = property
|
||||
.value
|
||||
.as_ref()
|
||||
.is_some_and(|haystack| collation.match_text(needle, haystack));
|
||||
|
||||
let matches = match_type.match_text(collation, needle, haystack);
|
||||
// XOR
|
||||
negate_condition.0 ^ matches
|
||||
}
|
||||
#[must_use]
|
||||
pub fn match_property(&self, property: &Property) -> bool {
|
||||
let text = property.value.as_deref().unwrap_or("");
|
||||
self.match_text(text)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TextCollation;
|
||||
use crate::xml::MatchType;
|
||||
|
||||
#[test]
|
||||
fn test_collation() {
|
||||
assert!(TextCollation::AsciiCasemap.match_text("GrüN", "grün"));
|
||||
assert!(!TextCollation::AsciiCasemap.match_text("GrÜN", "grün"));
|
||||
assert!(!TextCollation::Octet.match_text("GrÜN", "grün"));
|
||||
assert!(TextCollation::Octet.match_text("hallo", "hallo"));
|
||||
assert!(TextCollation::AsciiCasemap.match_text("HaLlo", "hAllo"));
|
||||
assert!(!MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrÜN", "grünsd"));
|
||||
assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
|
||||
assert!(!MatchType::Contains.match_text(&TextCollation::Octet, "GrüN", "grün"));
|
||||
assert!(MatchType::Contains.match_text(&TextCollation::UnicodeCasemap, "GrÜN", "grün"));
|
||||
assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
|
||||
assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
|
||||
assert!(MatchType::StartsWith.match_text(&TextCollation::Octet, "hello", "hello you"));
|
||||
assert!(MatchType::EndsWith.match_text(&TextCollation::Octet, "mama", "joe mama"));
|
||||
assert!(MatchType::Equals.match_text(&TextCollation::UnicodeCasemap, "GrÜN", "grün"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
]
|
||||
},
|
||||
"imports": {
|
||||
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.5",
|
||||
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.6",
|
||||
"lit": "npm:lit@^3.3.1",
|
||||
"vite": "npm:vite@^7.3.0"
|
||||
}
|
||||
|
||||
8
crates/frontend/js-components/deno.lock
generated
8
crates/frontend/js-components/deno.lock
generated
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.3.0__picomatch@4.0.3",
|
||||
"npm:@deno/vite-plugin@^1.0.6": "1.0.6_vite@7.3.0__picomatch@4.0.3",
|
||||
"npm:lit@^3.3.1": "3.3.1",
|
||||
"npm:vite@*": "7.3.0_picomatch@4.0.3",
|
||||
"npm:vite@^7.3.0": "7.3.0_picomatch@4.0.3"
|
||||
},
|
||||
"npm": {
|
||||
"@deno/vite-plugin@1.0.5_vite@7.3.0__picomatch@4.0.3": {
|
||||
"integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==",
|
||||
"@deno/vite-plugin@1.0.6_vite@7.3.0__picomatch@4.0.3": {
|
||||
"integrity": "sha512-Sh5XqvFuKAwjARTesi0n6xRpEXm1V0UeqKh+SxIrexCofxOaieNDMqXZD02RiZCg0mrJ43V8eCMuVrDfq6mLmg==",
|
||||
"dependencies": [
|
||||
"vite"
|
||||
]
|
||||
@@ -415,7 +415,7 @@
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"npm:@deno/vite-plugin@^1.0.5",
|
||||
"npm:@deno/vite-plugin@^1.0.6",
|
||||
"npm:lit@^3.3.1",
|
||||
"npm:vite@^7.3.0"
|
||||
]
|
||||
|
||||
@@ -9,9 +9,9 @@ use axum::{
|
||||
extract::Path,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::{TypedHeader, extract::Host};
|
||||
use axum_extra::TypedHeader;
|
||||
use chrono::{Duration, Utc};
|
||||
use headers::UserAgent;
|
||||
use headers::{Host, UserAgent};
|
||||
use http::StatusCode;
|
||||
use rustical_store::auth::{AuthenticationProvider, Principal};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -21,7 +21,7 @@ use tracing::instrument;
|
||||
pub async fn post_nextcloud_login(
|
||||
Extension(state): Extension<Arc<NextcloudFlows>>,
|
||||
TypedHeader(user_agent): TypedHeader<UserAgent>,
|
||||
Host(host): Host,
|
||||
TypedHeader(host): TypedHeader<Host>,
|
||||
) -> Json<NextcloudLoginResponse> {
|
||||
let flow_id = uuid::Uuid::new_v4().to_string();
|
||||
let token = uuid::Uuid::new_v4().to_string();
|
||||
@@ -150,7 +150,7 @@ pub async fn post_nextcloud_flow(
|
||||
user: Principal,
|
||||
Extension(state): Extension<Arc<NextcloudFlows>>,
|
||||
Path(flow_id): Path<String>,
|
||||
Host(host): Host,
|
||||
TypedHeader(host): TypedHeader<Host>,
|
||||
Form(form): Form<NextcloudAuthorizeForm>,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if let Some(flow) = state.flows.write().await.get_mut(&flow_id) {
|
||||
|
||||
@@ -7,8 +7,8 @@ use axum::{
|
||||
extract::Path,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
use headers::{ContentType, HeaderMapExt};
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::{ContentType, HeaderMapExt, Host};
|
||||
use http::{HeaderValue, StatusCode, header};
|
||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||
use rand::{Rng, distr::Alphanumeric};
|
||||
@@ -50,7 +50,7 @@ pub async fn route_post_app_token<AP: AuthenticationProvider>(
|
||||
user: Principal,
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
Path(user_id): Path<String>,
|
||||
Host(hostname): Host,
|
||||
TypedHeader(host): TypedHeader<Host>,
|
||||
Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
assert!(!name.is_empty());
|
||||
@@ -66,10 +66,10 @@ pub async fn route_post_app_token<AP: AuthenticationProvider>(
|
||||
if apple {
|
||||
let profile = AppleConfig {
|
||||
token_name: name,
|
||||
account_description: format!("{}@{}", &user.id, &hostname),
|
||||
hostname: hostname.clone(),
|
||||
caldav_principal_url: format!("https://{hostname}/caldav-compat/principal/{user_id}"),
|
||||
carddav_principal_url: format!("https://{hostname}/carddav/principal/{user_id}"),
|
||||
account_description: format!("{}@{}", &user.id, &host),
|
||||
hostname: host.to_string(),
|
||||
caldav_principal_url: format!("https://{host}/caldav-compat/principal/{user_id}"),
|
||||
carddav_principal_url: format!("https://{host}/carddav/principal/{user_id}"),
|
||||
user: user.id.clone(),
|
||||
token,
|
||||
caldav_profile_uuid: Uuid::new_v4(),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{FrontendConfig, OidcConfig, pages::DefaultLayoutData};
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
@@ -8,10 +6,12 @@ use axum::{
|
||||
extract::Query,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::Host;
|
||||
use http::StatusCode;
|
||||
use rustical_store::auth::AuthenticationProvider;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tower_sessions::Session;
|
||||
use tracing::{instrument, warn};
|
||||
use url::Url;
|
||||
@@ -73,7 +73,7 @@ pub async fn route_post_login<AP: AuthenticationProvider>(
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
Extension(config): Extension<FrontendConfig>,
|
||||
session: Session,
|
||||
Host(host): Host,
|
||||
TypedHeader(host): TypedHeader<Host>,
|
||||
Form(PostLoginForm {
|
||||
username,
|
||||
password,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::pages::user::{Section, UserPage};
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{
|
||||
@@ -7,13 +8,11 @@ use axum::{
|
||||
extract::Path,
|
||||
response::{IntoResponse, Redirect},
|
||||
};
|
||||
use axum_extra::{TypedHeader, extract::Host};
|
||||
use headers::UserAgent;
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::{Host, UserAgent};
|
||||
use http::StatusCode;
|
||||
use rustical_store::auth::{AppToken, AuthenticationProvider, Principal};
|
||||
|
||||
use crate::pages::user::{Section, UserPage};
|
||||
|
||||
impl Section for ProfileSection {
|
||||
fn name() -> &'static str {
|
||||
"profile"
|
||||
@@ -33,7 +32,7 @@ pub async fn route_user_named<AP: AuthenticationProvider>(
|
||||
Path(user_id): Path<String>,
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
TypedHeader(user_agent): TypedHeader<UserAgent>,
|
||||
Host(host): Host,
|
||||
TypedHeader(host): TypedHeader<Host>,
|
||||
user: Principal,
|
||||
) -> impl IntoResponse {
|
||||
if user_id != user.id {
|
||||
@@ -41,7 +40,10 @@ pub async fn route_user_named<AP: AuthenticationProvider>(
|
||||
}
|
||||
|
||||
let is_apple = user_agent.as_str().contains("Apple") || user_agent.as_str().contains("Mac OS");
|
||||
let davx5_hostname = user_agent.as_str().contains("Android").then_some(host);
|
||||
let davx5_hostname = user_agent
|
||||
.as_str()
|
||||
.contains("Android")
|
||||
.then_some(host.to_string());
|
||||
|
||||
UserPage {
|
||||
section: ProfileSection {
|
||||
|
||||
@@ -181,7 +181,7 @@ END:VCALENDAR",
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_vcard(&self) -> &VcardContact {
|
||||
pub const fn get_vcard(&self) -> &VcardContact {
|
||||
&self.vcard
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,4 +328,12 @@ impl CalendarObject {
|
||||
.iter()
|
||||
.find(|property| property.name == name)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||
self.properties
|
||||
.iter()
|
||||
.filter(|property| property.name == name)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ use axum::{
|
||||
extract::Query,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
use axum_extra::TypedHeader;
|
||||
pub use config::OidcConfig;
|
||||
use config::UserIdClaim;
|
||||
use error::OidcError;
|
||||
use headers::Host;
|
||||
use openidconnect::{
|
||||
AuthenticationFlow, AuthorizationCode, CsrfToken, EndpointMaybeSet, EndpointNotSet,
|
||||
EndpointSet, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier,
|
||||
@@ -100,10 +101,12 @@ pub struct GetOidcForm {
|
||||
pub async fn route_post_oidc(
|
||||
Extension(oidc_config): Extension<OidcConfig>,
|
||||
session: Session,
|
||||
Host(host): Host,
|
||||
TypedHeader(host): TypedHeader<Host>,
|
||||
Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
|
||||
) -> Result<Response, OidcError> {
|
||||
dbg!(&host);
|
||||
let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
|
||||
dbg!(&callback_uri);
|
||||
|
||||
let http_client = get_http_client();
|
||||
let oidc_client = get_oidc_client(
|
||||
@@ -155,7 +158,7 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
|
||||
Extension(service_config): Extension<OidcServiceConfig>,
|
||||
session: Session,
|
||||
Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>,
|
||||
Host(host): Host,
|
||||
TypedHeader(host): TypedHeader<Host>,
|
||||
) -> Result<Response, OidcError> {
|
||||
let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use rustical_ical::{CalendarObject, CalendarObjectType};
|
||||
use rustical_store::{Calendar, CalendarMetadata, CalendarStore};
|
||||
use rustical_store_sqlite::tests::get_test_calendar_store;
|
||||
use rustical_store_sqlite::tests::test_store_context;
|
||||
|
||||
fn benchmark(c: &mut Criterion) {
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
let cal_store = runtime.block_on(async {
|
||||
let cal_store = get_test_calendar_store().await;
|
||||
let cal_store = test_store_context().await.cal_store;
|
||||
|
||||
cal_store
|
||||
.insert_calendar(Calendar {
|
||||
|
||||
@@ -27,7 +27,7 @@ impl TryFrom<AddressObjectRow> for AddressObject {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Constructor)]
|
||||
#[derive(Debug, Clone, Constructor)]
|
||||
pub struct SqliteAddressbookStore {
|
||||
db: SqlitePool,
|
||||
sender: Sender<CollectionOperation>,
|
||||
|
||||
@@ -87,7 +87,7 @@ impl From<CalendarRow> for Calendar {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Constructor)]
|
||||
#[derive(Debug, Clone, Constructor)]
|
||||
pub struct SqliteCalendarStore {
|
||||
db: SqlitePool,
|
||||
sender: Sender<CollectionOperation>,
|
||||
|
||||
@@ -24,7 +24,7 @@ pub(crate) enum ChangeOperation {
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SqliteStore {
|
||||
db: SqlitePool,
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ impl TryFrom<PrincipalRow> for Principal {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Constructor)]
|
||||
#[derive(Debug, Clone, Constructor)]
|
||||
pub struct SqlitePrincipalStore {
|
||||
db: SqlitePool,
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{addressbook_store::SqliteAddressbookStore, tests::get_test_addressbook_store};
|
||||
use crate::tests::{TestStoreContext, test_store_context};
|
||||
use rstest::rstest;
|
||||
use rustical_store::{Addressbook, AddressbookStore};
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_addressbook_store(
|
||||
#[from(get_test_addressbook_store)]
|
||||
#[from(test_store_context)]
|
||||
#[future]
|
||||
addr_store: SqliteAddressbookStore,
|
||||
context: TestStoreContext,
|
||||
) {
|
||||
let addr_store = addr_store.await;
|
||||
let addr_store = context.await.addr_store;
|
||||
|
||||
let cal = Addressbook {
|
||||
id: "addr".to_string(),
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{calendar_store::SqliteCalendarStore, tests::get_test_calendar_store};
|
||||
use crate::tests::{TestStoreContext, test_store_context};
|
||||
use rstest::rstest;
|
||||
use rustical_store::{Calendar, CalendarMetadata, CalendarStore};
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_calendar_store(
|
||||
#[from(get_test_calendar_store)]
|
||||
#[future]
|
||||
cal_store: SqliteCalendarStore,
|
||||
#[from(test_store_context)]
|
||||
context: TestStoreContext,
|
||||
) {
|
||||
let cal_store = cal_store.await;
|
||||
let TestStoreContext { cal_store, .. } = context.await;
|
||||
|
||||
let cal_store = cal_store;
|
||||
|
||||
let cal = Calendar {
|
||||
principal: "fake-user".to_string(),
|
||||
|
||||
@@ -2,64 +2,59 @@ use crate::{
|
||||
SqliteStore, addressbook_store::SqliteAddressbookStore, calendar_store::SqliteCalendarStore,
|
||||
principal_store::SqlitePrincipalStore,
|
||||
};
|
||||
use rustical_store::{
|
||||
Secret,
|
||||
auth::{AuthenticationProvider, Principal, PrincipalType},
|
||||
};
|
||||
use rstest::fixture;
|
||||
use rustical_store::auth::{AuthenticationProvider, Principal, PrincipalType};
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
static DB: OnceCell<SqlitePool> = OnceCell::const_new();
|
||||
|
||||
mod addressbook_store;
|
||||
mod calendar_store;
|
||||
|
||||
async fn get_test_db() -> SqlitePool {
|
||||
DB.get_or_init(async || {
|
||||
let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::migrate!("./migrations").run(&db).await.unwrap();
|
||||
let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::migrate!("./migrations").run(&db).await.unwrap();
|
||||
|
||||
// Populate with test data
|
||||
let principal_store = SqlitePrincipalStore::new(db.clone());
|
||||
principal_store
|
||||
.insert_principal(
|
||||
Principal {
|
||||
id: "user".to_owned(),
|
||||
displayname: None,
|
||||
memberships: vec![],
|
||||
password: None,
|
||||
principal_type: PrincipalType::Individual,
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
principal_store
|
||||
.add_app_token("user", "test".to_string(), "pass".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
// Populate with test data
|
||||
let principal_store = SqlitePrincipalStore::new(db.clone());
|
||||
principal_store
|
||||
.insert_principal(
|
||||
Principal {
|
||||
id: "user".to_owned(),
|
||||
displayname: None,
|
||||
memberships: vec![],
|
||||
password: None,
|
||||
principal_type: PrincipalType::Individual,
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
principal_store
|
||||
.add_app_token("user", "test".to_string(), "pass".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db
|
||||
})
|
||||
.await
|
||||
.clone()
|
||||
db
|
||||
}
|
||||
|
||||
#[rstest::fixture]
|
||||
pub async fn get_test_addressbook_store() -> SqliteAddressbookStore {
|
||||
let (send, _recv) = tokio::sync::mpsc::channel(1000);
|
||||
SqliteAddressbookStore::new(get_test_db().await, send)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestStoreContext {
|
||||
pub db: SqlitePool,
|
||||
pub addr_store: SqliteAddressbookStore,
|
||||
pub cal_store: SqliteCalendarStore,
|
||||
pub principal_store: SqlitePrincipalStore,
|
||||
pub sub_store: SqliteStore,
|
||||
}
|
||||
#[rstest::fixture]
|
||||
pub async fn get_test_calendar_store() -> SqliteCalendarStore {
|
||||
let (send, _recv) = tokio::sync::mpsc::channel(1000);
|
||||
SqliteCalendarStore::new(get_test_db().await, send)
|
||||
}
|
||||
#[rstest::fixture]
|
||||
pub async fn get_test_subscription_store() -> SqliteStore {
|
||||
SqliteStore::new(get_test_db().await)
|
||||
}
|
||||
#[rstest::fixture]
|
||||
pub async fn get_test_principal_store() -> SqlitePrincipalStore {
|
||||
SqlitePrincipalStore::new(get_test_db().await)
|
||||
|
||||
#[fixture]
|
||||
pub async fn test_store_context() -> TestStoreContext {
|
||||
let (send_addr, _recv) = tokio::sync::mpsc::channel(1);
|
||||
let (send_cal, _recv) = tokio::sync::mpsc::channel(1);
|
||||
let db = get_test_db().await;
|
||||
TestStoreContext {
|
||||
db: db.clone(),
|
||||
addr_store: SqliteAddressbookStore::new(db.clone(), send_addr),
|
||||
cal_store: SqliteCalendarStore::new(db.clone(), send_cal),
|
||||
principal_store: SqlitePrincipalStore::new(db.clone()),
|
||||
sub_store: SqliteStore::new(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use headers::{Authorization, HeaderMapExt};
|
||||
use http::{HeaderValue, StatusCode};
|
||||
use rstest::rstest;
|
||||
use rustical_store::{CalendarMetadata, CalendarStore};
|
||||
use rustical_store_sqlite::{calendar_store::SqliteCalendarStore, tests::get_test_calendar_store};
|
||||
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn mkcalendar_template(
|
||||
@@ -48,15 +48,13 @@ fn mkcalendar_template(
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_caldav_calendar(
|
||||
#[from(get_app)]
|
||||
#[from(test_store_context)]
|
||||
#[future]
|
||||
app: axum::Router,
|
||||
#[from(get_test_calendar_store)]
|
||||
#[future]
|
||||
cal_store: SqliteCalendarStore,
|
||||
context: TestStoreContext,
|
||||
) {
|
||||
let app = app.await;
|
||||
let cal_store = cal_store.await;
|
||||
let context = context.await;
|
||||
let app = get_app(context.clone());
|
||||
let cal_store = context.cal_store;
|
||||
|
||||
let mut calendar_meta = CalendarMetadata {
|
||||
displayname: Some("Calendar".to_string()),
|
||||
|
||||
@@ -4,6 +4,7 @@ use axum::extract::Request;
|
||||
use headers::{Authorization, HeaderMapExt};
|
||||
use http::{HeaderValue, StatusCode};
|
||||
use rstest::rstest;
|
||||
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||
use tower::ServiceExt;
|
||||
|
||||
mod calendar;
|
||||
@@ -11,11 +12,11 @@ mod calendar;
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_caldav_root(
|
||||
#[from(get_app)]
|
||||
#[from(test_store_context)]
|
||||
#[future]
|
||||
app: axum::Router,
|
||||
context: TestStoreContext,
|
||||
) {
|
||||
let app = app.await;
|
||||
let app = get_app(context.await);
|
||||
|
||||
let request_template = || {
|
||||
Request::builder()
|
||||
@@ -53,11 +54,11 @@ async fn test_caldav_root(
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_caldav_principal(
|
||||
#[from(get_app)]
|
||||
#[from(test_store_context)]
|
||||
#[future]
|
||||
app: axum::Router,
|
||||
context: TestStoreContext,
|
||||
) {
|
||||
let app = app.await;
|
||||
let app = get_app(context.await);
|
||||
|
||||
let request_template = || {
|
||||
Request::builder()
|
||||
|
||||
@@ -4,7 +4,7 @@ expression: body
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<multistatus 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">
|
||||
<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">
|
||||
<response>
|
||||
<href>/caldav/principal/user/calendar/</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
@@ -136,6 +136,7 @@ END:VCALENDAR
|
||||
</CAL:supported-calendar-data>
|
||||
<CAL:supported-collation-set>
|
||||
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
||||
<CAL:supported-collation>i;unicode-casemap</CAL:supported-collation>
|
||||
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
||||
</CAL:supported-collation-set>
|
||||
<max-resource-size>10000000</max-resource-size>
|
||||
|
||||
@@ -4,7 +4,7 @@ expression: body
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<multistatus 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">
|
||||
<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">
|
||||
<response>
|
||||
<href>/caldav/principal/user/calendar</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
|
||||
@@ -4,7 +4,7 @@ expression: body
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<multistatus 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">
|
||||
<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">
|
||||
<response>
|
||||
<href>/caldav/</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
|
||||
@@ -4,7 +4,7 @@ expression: body
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<multistatus 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">
|
||||
<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">
|
||||
<response>
|
||||
<href>/caldav/principal/user/</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
|
||||
@@ -4,7 +4,7 @@ expression: body
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<multistatus 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">
|
||||
<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">
|
||||
<response>
|
||||
<href>/caldav/principal/user/</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
@@ -50,192 +50,4 @@ expression: body
|
||||
<status>HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
</response>
|
||||
<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/">#00FF00</calendar-color>
|
||||
<CAL:calendar-description>Description</CAL:calendar-description>
|
||||
<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:comp name="VJOURNAL"/>
|
||||
</CAL:supported-calendar-component-set>
|
||||
<CAL:supported-calendar-data>
|
||||
<CAL:calendar-data content-type="text/calendar" version="2.0"/>
|
||||
</CAL:supported-calendar-data>
|
||||
<CAL:supported-collation-set>
|
||||
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
||||
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
||||
</CAL:supported-collation-set>
|
||||
<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/0</sync-token>
|
||||
<CS:getctag>github.com/lennart-k/rustical/ns/0</CS:getctag>
|
||||
<PUSH:transports>
|
||||
<PUSH:web-push/>
|
||||
</PUSH:transports>
|
||||
<PUSH:topic>[PUSH_TOPIC]</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>
|
||||
<all/>
|
||||
</privilege>
|
||||
</current-user-privilege-set>
|
||||
<owner>
|
||||
<href>/caldav/principal/user/</href>
|
||||
</owner>
|
||||
</prop>
|
||||
<status>HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
</response>
|
||||
</multistatus>
|
||||
|
||||
194
src/integration_tests/carddav/addressbook.rs
Normal file
194
src/integration_tests/carddav/addressbook.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use crate::integration_tests::{ResponseExtractString, get_app};
|
||||
use axum::body::Body;
|
||||
use axum::extract::Request;
|
||||
use headers::{Authorization, HeaderMapExt};
|
||||
use http::{HeaderValue, StatusCode};
|
||||
use rstest::rstest;
|
||||
use rustical_store::AddressbookStore;
|
||||
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn mkcol_template(displayname: &str, description: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||
<set>
|
||||
<prop>
|
||||
<resourcetype>
|
||||
<collection />
|
||||
<CARD:addressbook />
|
||||
</resourcetype>
|
||||
<displayname>{displayname}</displayname>
|
||||
<CARD:addressbook-description>{description}</CARD:addressbook-description>
|
||||
</prop>
|
||||
</set>
|
||||
</mkcol>
|
||||
"#,
|
||||
)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_carddav_addressbook(
|
||||
#[from(test_store_context)]
|
||||
#[future]
|
||||
context: TestStoreContext,
|
||||
) {
|
||||
let context = context.await;
|
||||
let app = get_app(context.clone());
|
||||
let addr_store = context.addr_store;
|
||||
|
||||
let (mut displayname, mut description) = (
|
||||
Some("Contacts".to_owned()),
|
||||
Some("Amazing contacts!".to_owned()),
|
||||
);
|
||||
let (principal, addr_id) = ("user", "contacts");
|
||||
let url = format!("/carddav/principal/{principal}/{addr_id}");
|
||||
|
||||
let request_template = || {
|
||||
Request::builder()
|
||||
.method("MKCOL")
|
||||
.uri(&url)
|
||||
.body(Body::from(mkcol_template(
|
||||
displayname.as_ref().unwrap(),
|
||||
description.as_ref().unwrap(),
|
||||
)))
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
// Try OPTIONS without authentication
|
||||
let request = Request::builder()
|
||||
.method("OPTIONS")
|
||||
.uri(&url)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
insta::assert_debug_snapshot!(response, @r#"
|
||||
Response {
|
||||
status: 200,
|
||||
version: HTTP/1.1,
|
||||
headers: {
|
||||
"dav": "1, 3, access-control, addressbook, webdav-push",
|
||||
"allow": "PROPFIND, PROPPATCH, COPY, MOVE, DELETE, OPTIONS, REPORT, GET, HEAD, POST, MKCOL, IMPORT",
|
||||
},
|
||||
body: Body(
|
||||
UnsyncBoxBody,
|
||||
),
|
||||
}
|
||||
"#);
|
||||
|
||||
// Try without authentication
|
||||
let request = request_template();
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
// Try with correct credentials
|
||||
let mut request = request_template();
|
||||
request
|
||||
.headers_mut()
|
||||
.typed_insert(Authorization::basic("user", "pass"));
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::CREATED);
|
||||
let body = response.extract_string().await;
|
||||
insta::assert_snapshot!("mkcol_body", body);
|
||||
|
||||
let mut request = Request::builder()
|
||||
.method("GET")
|
||||
.uri(&url)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.typed_insert(Authorization::basic("user", "pass"));
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.extract_string().await;
|
||||
insta::assert_snapshot!("get_body", body);
|
||||
|
||||
let saved_addressbook = addr_store
|
||||
.get_addressbook(principal, addr_id, false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(saved_addressbook.displayname, saved_addressbook.description),
|
||||
(displayname, description)
|
||||
);
|
||||
|
||||
let mut request = Request::builder()
|
||||
.method("PROPFIND")
|
||||
.uri(&url)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.typed_insert(Authorization::basic("user", "pass"));
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::MULTI_STATUS);
|
||||
let body = response.extract_string().await;
|
||||
insta::with_settings!({
|
||||
filters => vec![
|
||||
(r"<PUSH:topic>[0-9a-f-]+</PUSH:topic>", "<PUSH:topic>[PUSH_TOPIC]</PUSH:topic>")
|
||||
]
|
||||
}, {
|
||||
insta::assert_snapshot!("propfind_body", body);
|
||||
});
|
||||
|
||||
let proppatch_request: &str = r#"
|
||||
<propertyupdate xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||
<set>
|
||||
<prop>
|
||||
<displayname>New Displayname</displayname>
|
||||
<CARD:addressbook-description>Test</CARD:addressbook-description>
|
||||
</prop>
|
||||
</set>
|
||||
<remove>
|
||||
<prop>
|
||||
<CARD:addressbook-description />
|
||||
</prop>
|
||||
</remove>
|
||||
</propertyupdate>
|
||||
"#;
|
||||
let mut request = Request::builder()
|
||||
.method("PROPPATCH")
|
||||
.uri(&url)
|
||||
.body(Body::from(proppatch_request))
|
||||
.unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.typed_insert(Authorization::basic("user", "pass"));
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::MULTI_STATUS);
|
||||
let body = response.extract_string().await;
|
||||
insta::assert_snapshot!("proppatch_body", body);
|
||||
|
||||
displayname = Some("New Displayname".to_string());
|
||||
description = None;
|
||||
let saved_addressbook = addr_store
|
||||
.get_addressbook(principal, addr_id, false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(saved_addressbook.displayname, saved_addressbook.description),
|
||||
(displayname, description)
|
||||
);
|
||||
|
||||
let mut request = Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(&url)
|
||||
.header("X-No-Trashbin", HeaderValue::from_static("1"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.typed_insert(Authorization::basic("user", "pass"));
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.extract_string().await;
|
||||
insta::assert_snapshot!("delete_body", body);
|
||||
|
||||
assert!(matches!(
|
||||
addr_store.get_addressbook(principal, addr_id, false).await,
|
||||
Err(rustical_store::Error::NotFound)
|
||||
));
|
||||
}
|
||||
73
src/integration_tests/carddav/addressbook_import.rs
Normal file
73
src/integration_tests/carddav/addressbook_import.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use crate::integration_tests::{ResponseExtractString, get_app};
|
||||
use axum::body::Body;
|
||||
use axum::extract::Request;
|
||||
use headers::{Authorization, HeaderMapExt};
|
||||
use http::StatusCode;
|
||||
use rstest::rstest;
|
||||
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_import(
|
||||
#[from(test_store_context)]
|
||||
#[future]
|
||||
context: TestStoreContext,
|
||||
) {
|
||||
let context = context.await;
|
||||
let app = get_app(context.clone());
|
||||
|
||||
let (principal, addr_id) = ("user", "contacts");
|
||||
let url = format!("/carddav/principal/{principal}/{addr_id}");
|
||||
|
||||
let request_template = || {
|
||||
Request::builder()
|
||||
.method("IMPORT")
|
||||
.uri(&url)
|
||||
.body(Body::from(
|
||||
r"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:Simon Perreault
|
||||
N:Perreault;Simon;;;ing. jr,M.Sc.
|
||||
BDAY:--0203
|
||||
GENDER:M
|
||||
EMAIL;TYPE=work:simon.perreault@viagenie.ca
|
||||
END:VCARD",
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
// Try without authentication
|
||||
let request = request_template();
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
// Try with correct credentials
|
||||
let mut request = request_template();
|
||||
request
|
||||
.headers_mut()
|
||||
.typed_insert(Authorization::basic("user", "pass"));
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.extract_string().await;
|
||||
insta::assert_snapshot!("import_body", body);
|
||||
|
||||
let mut request = Request::builder()
|
||||
.method("GET")
|
||||
.uri(&url)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.typed_insert(Authorization::basic("user", "pass"));
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.extract_string().await;
|
||||
insta::with_settings!({
|
||||
filters => vec![
|
||||
(r"UID:.+", "UID:[UID]")
|
||||
]
|
||||
}, {
|
||||
insta::assert_snapshot!("get_body", body);
|
||||
});
|
||||
}
|
||||
@@ -4,16 +4,20 @@ use axum::extract::Request;
|
||||
use headers::{Authorization, HeaderMapExt};
|
||||
use http::StatusCode;
|
||||
use rstest::rstest;
|
||||
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||
use tower::ServiceExt;
|
||||
|
||||
mod addressbook;
|
||||
mod addressbook_import;
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_carddav_root(
|
||||
#[from(get_app)]
|
||||
#[from(test_store_context)]
|
||||
#[future]
|
||||
app: axum::Router,
|
||||
context: TestStoreContext,
|
||||
) {
|
||||
let app = app.await;
|
||||
let app = get_app(context.await);
|
||||
|
||||
let request_template = || {
|
||||
Request::builder()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: src/integration_tests/carddav/addressbook.rs
|
||||
expression: body
|
||||
---
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: src/integration_tests/carddav/addressbook.rs
|
||||
expression: body
|
||||
---
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: src/integration_tests/carddav/addressbook.rs
|
||||
expression: body
|
||||
---
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
source: src/integration_tests/carddav/addressbook.rs
|
||||
expression: body
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<multistatus 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">
|
||||
<response>
|
||||
<href>/carddav/principal/user/contacts/</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
<CARD:addressbook-description>Amazing contacts!</CARD:addressbook-description>
|
||||
<CARD:supported-address-data>
|
||||
<CARD:address-data-type content-type="text/vcard" version="3.0"/>
|
||||
<CARD:address-data-type content-type="text/vcard" version="4.0"/>
|
||||
</CARD:supported-address-data>
|
||||
<CARD:supported-collation-set>
|
||||
<CARD:supported-collation>i;ascii-casemap</CARD:supported-collation>
|
||||
<CARD:supported-collation>i;unicode-casemap</CARD:supported-collation>
|
||||
<CARD:supported-collation>i;octet</CARD:supported-collation>
|
||||
</CARD:supported-collation-set>
|
||||
<supported-report-set>
|
||||
<supported-report>
|
||||
<report>
|
||||
<CARD:addressbook-multiget/>
|
||||
</report>
|
||||
</supported-report>
|
||||
<supported-report>
|
||||
<report>
|
||||
<sync-collection/>
|
||||
</report>
|
||||
</supported-report>
|
||||
</supported-report-set>
|
||||
<max-resource-size>10000000</max-resource-size>
|
||||
<sync-token>github.com/lennart-k/rustical/ns/0</sync-token>
|
||||
<CS:getctag>github.com/lennart-k/rustical/ns/0</CS:getctag>
|
||||
<PUSH:transports>
|
||||
<PUSH:web-push/>
|
||||
</PUSH:transports>
|
||||
<PUSH:topic>[PUSH_TOPIC]</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/>
|
||||
<CARD:addressbook/>
|
||||
</resourcetype>
|
||||
<displayname>Contacts</displayname>
|
||||
<current-user-principal>
|
||||
<href>/carddav/principal/user/</href>
|
||||
</current-user-principal>
|
||||
<current-user-privilege-set>
|
||||
<privilege>
|
||||
<all/>
|
||||
</privilege>
|
||||
</current-user-privilege-set>
|
||||
<owner>
|
||||
<href>/carddav/principal/user/</href>
|
||||
</owner>
|
||||
</prop>
|
||||
<status>HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
</response>
|
||||
</multistatus>
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: src/integration_tests/carddav/addressbook.rs
|
||||
expression: body
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<multistatus 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">
|
||||
<response>
|
||||
<href>/carddav/principal/user/contacts</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
<displayname xmlns="DAV:"/>
|
||||
<addressbook-description xmlns="urn:ietf:params:xml:ns:carddav"/>
|
||||
<addressbook-description/>
|
||||
</prop>
|
||||
<status>HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
<propstat>
|
||||
<prop>
|
||||
</prop>
|
||||
<status>HTTP/1.1 404 Not Found</status>
|
||||
</propstat>
|
||||
<propstat>
|
||||
<prop>
|
||||
</prop>
|
||||
<status>HTTP/1.1 409 Conflict</status>
|
||||
</propstat>
|
||||
</response>
|
||||
</multistatus>
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: src/integration_tests/carddav/addressbook_import.rs
|
||||
expression: body
|
||||
---
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:Simon Perreault
|
||||
N:Perreault;Simon;;;ing. jr,M.Sc.
|
||||
BDAY:--0203
|
||||
GENDER:M
|
||||
EMAIL;TYPE=work:simon.perreault@viagenie.ca
|
||||
UID:[UID]
|
||||
END:VCARD
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: src/integration_tests/carddav/addressbook_import.rs
|
||||
expression: body
|
||||
---
|
||||
|
||||
@@ -4,7 +4,7 @@ expression: body
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<multistatus 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">
|
||||
<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">
|
||||
<response>
|
||||
<href>/carddav/</href>
|
||||
<propstat>
|
||||
<prop>
|
||||
|
||||
@@ -3,44 +3,24 @@ use axum::extract::Request;
|
||||
use axum::{body::Body, response::Response};
|
||||
use rstest::rstest;
|
||||
use rustical_frontend::FrontendConfig;
|
||||
use rustical_store_sqlite::{
|
||||
SqliteStore,
|
||||
addressbook_store::SqliteAddressbookStore,
|
||||
calendar_store::SqliteCalendarStore,
|
||||
principal_store::SqlitePrincipalStore,
|
||||
tests::{
|
||||
get_test_addressbook_store, get_test_calendar_store, get_test_principal_store,
|
||||
get_test_subscription_store,
|
||||
},
|
||||
};
|
||||
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||
use std::sync::Arc;
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[rstest::fixture]
|
||||
pub async fn get_app(
|
||||
#[from(get_test_calendar_store)]
|
||||
#[future]
|
||||
cal_store: SqliteCalendarStore,
|
||||
#[from(get_test_addressbook_store)]
|
||||
#[future]
|
||||
addr_store: SqliteAddressbookStore,
|
||||
#[from(get_test_principal_store)]
|
||||
#[future]
|
||||
principal_store: SqlitePrincipalStore,
|
||||
#[from(get_test_subscription_store)]
|
||||
#[future]
|
||||
sub_store: SqliteStore,
|
||||
) -> axum::Router {
|
||||
let addr_store = Arc::new(addr_store.await);
|
||||
let cal_store = Arc::new(cal_store.await);
|
||||
let sub_store = Arc::new(sub_store.await);
|
||||
let principal_store = Arc::new(principal_store.await);
|
||||
|
||||
make_app(
|
||||
pub fn get_app(context: TestStoreContext) -> axum::Router {
|
||||
let TestStoreContext {
|
||||
addr_store,
|
||||
cal_store,
|
||||
sub_store,
|
||||
principal_store,
|
||||
sub_store,
|
||||
..
|
||||
} = context;
|
||||
|
||||
make_app(
|
||||
Arc::new(addr_store),
|
||||
Arc::new(cal_store),
|
||||
Arc::new(sub_store),
|
||||
Arc::new(principal_store),
|
||||
FrontendConfig {
|
||||
enabled: true,
|
||||
allow_password_login: true,
|
||||
@@ -70,11 +50,11 @@ impl ResponseExtractString for Response {
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_ping(
|
||||
#[from(get_app)]
|
||||
#[from(test_store_context)]
|
||||
#[future]
|
||||
app: axum::Router,
|
||||
context: TestStoreContext,
|
||||
) {
|
||||
let app = app.await;
|
||||
let app = get_app(context.await);
|
||||
|
||||
let response = app
|
||||
.oneshot(Request::builder().uri("/ping").body(Body::empty()).unwrap())
|
||||
|
||||
Reference in New Issue
Block a user