mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
Compare commits
166 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c648ed315d | ||
|
|
2cf481d4e6 | ||
|
|
f3a1f27caf | ||
|
|
0829093571 | ||
|
|
bfe17d0b65 | ||
|
|
9050484932 | ||
|
|
1e90ff3d6c | ||
|
|
94ace71745 | ||
|
|
f22d5ca04b | ||
|
|
68a2e7e2a2 | ||
|
|
4e3c3f3a3b | ||
|
|
b7cfd3301b | ||
|
|
9c114dc204 | ||
|
|
9decef093d | ||
|
|
de2a8a2a8e | ||
|
|
51d2293ff9 | ||
|
|
5c77719ce4 | ||
|
|
91996465f9 | ||
|
|
83f4506578 | ||
|
|
a5bbb82712 | ||
|
|
6a26f44dd7 | ||
|
|
f8a660c222 | ||
|
|
a991baaf7d | ||
|
|
61d226dada | ||
|
|
ce0ce43418 | ||
|
|
038942ff16 | ||
|
|
90c38e7703 | ||
|
|
0159a8d9c9 | ||
|
|
aa8db47f57 | ||
|
|
78f7a7e155 | ||
|
|
e1a7a188f5 | ||
|
|
a42004501b | ||
|
|
89ce14ee86 | ||
|
|
7fc64d219c | ||
|
|
03294ec106 | ||
|
|
a22235d976 | ||
|
|
1ba9a97b3f | ||
|
|
51036ec6d5 | ||
|
|
e1a10338e0 | ||
|
|
918f27e8c2 | ||
|
|
dd34dd23d1 | ||
|
|
9910e4ee31 | ||
|
|
c22469dea6 | ||
|
|
f2899aec6b | ||
|
|
f9380ca7e4 | ||
|
|
e7138b5f8c | ||
|
|
84af24a2b7 | ||
|
|
4bd6271e33 | ||
|
|
d817c1384c | ||
|
|
f8abc22e63 | ||
|
|
b7b5ca4f91 | ||
|
|
caca2d28ed | ||
|
|
3db2f13c1b | ||
|
|
db01024682 | ||
|
|
b2f15f2d77 | ||
|
|
89dd94904b | ||
|
|
5d0263abc1 | ||
|
|
0ef3e19bd3 | ||
|
|
44912057fc | ||
|
|
c4f613a803 | ||
|
|
eb8f301e45 | ||
|
|
d59ae25eba | ||
|
|
d4daa35df6 | ||
|
|
ea43876410 | ||
|
|
18af1b9aa2 | ||
|
|
e69c75102c | ||
|
|
09f1bd20ae | ||
|
|
72f970a857 | ||
|
|
08c250657e | ||
|
|
b8ef2f1ba2 | ||
|
|
c8adf60f48 | ||
|
|
507cb77e85 | ||
|
|
8881ea2a05 | ||
|
|
119e17a8e1 | ||
|
|
8b01c5388b | ||
|
|
35f423d4ca | ||
|
|
a827b40b47 | ||
|
|
16f9ce6f38 | ||
|
|
34839aa2ed | ||
|
|
2724154ed3 | ||
|
|
c490c413ec | ||
|
|
994864c6ef | ||
|
|
92fd28cdbb | ||
|
|
d7e871f0e6 | ||
|
|
a0fc073bd2 | ||
|
|
c8dffb4f9e | ||
|
|
b6d1899636 | ||
|
|
81f1767efa | ||
|
|
54eb9ddfcc | ||
|
|
60a0f16557 | ||
|
|
e4f188d299 | ||
|
|
69163404a1 | ||
|
|
0b7cfea79c | ||
|
|
455b4c405f | ||
|
|
2774d092ac | ||
|
|
32b616fd75 | ||
|
|
b02f7c427a | ||
|
|
eae8e7d768 | ||
|
|
105718a4ca | ||
|
|
0e68f1bdce | ||
|
|
aa744fcea2 | ||
|
|
4a51a669cd | ||
|
|
07fca05e50 | ||
|
|
509cc8d7a1 | ||
|
|
4134ab0520 | ||
|
|
d8803a38a2 | ||
|
|
b5bff08b08 | ||
|
|
3ca02d9792 | ||
|
|
ee2cc2174c | ||
|
|
caf10912e5 | ||
|
|
ec89cd6fa5 | ||
|
|
ae20573670 | ||
|
|
71cee2d20c | ||
|
|
83c6bf247e | ||
|
|
6bcc03d659 | ||
|
|
32f5c01716 | ||
|
|
40938cba02 | ||
|
|
a5663bf006 | ||
|
|
26306fd661 | ||
|
|
d8e4bd1cc4 | ||
|
|
a18ff2b400 | ||
|
|
bf13d95b97 | ||
|
|
ee1faa4c20 | ||
|
|
1e999ca0cc | ||
|
|
f27245f996 | ||
|
|
734455b5ab | ||
|
|
8c6a616015 | ||
|
|
828e7399c8 | ||
|
|
891ef6a9f3 | ||
|
|
7b27ac22a4 | ||
|
|
15668bf399 | ||
|
|
d2de87072f | ||
|
|
ff1e38477b | ||
|
|
f4fbb7c964 | ||
|
|
e8e60d4aac | ||
|
|
283be0a26c | ||
|
|
1060625b9d | ||
|
|
86ae31e94c | ||
|
|
e2f5773e3c | ||
|
|
b54fbebe7c | ||
|
|
fe78a82806 | ||
|
|
22544b8c2f | ||
|
|
340b99e491 | ||
|
|
787ea90376 | ||
|
|
973a86f21a | ||
|
|
39fc2fb55d | ||
|
|
ab4d763304 | ||
|
|
9cf74f7198 | ||
|
|
2c2a6006c7 | ||
|
|
4600f03b45 | ||
|
|
41fc1e6ea5 | ||
|
|
b56591c482 | ||
|
|
d639b18005 | ||
|
|
6046439fc7 | ||
|
|
f9de8a4687 | ||
|
|
8dfb47b28f | ||
|
|
eb720ded99 | ||
|
|
89ef7b2ced | ||
|
|
6e0129130e | ||
|
|
c646986c56 | ||
|
|
503cbe3699 | ||
|
|
79c66a0b46 | ||
|
|
e5687c6e43 | ||
|
|
79b67a17c3 | ||
|
|
7d18faff69 | ||
|
|
753f8e90d3 |
@@ -2,3 +2,5 @@
|
|||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
|
[docs/**/*.md]
|
||||||
|
indent_size = 4
|
||||||
|
|||||||
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -41,12 +41,10 @@ jobs:
|
|||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
# As long as we don't have releases everything on the main branch shall be tagged as latest
|
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT *\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL",
|
"query": "SELECT principal, id, displayname, \"order\", description, color, timezone_id, deleted_at, synctoken, subscription_url, push_topic, comp_event, comp_todo, comp_journal\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -14,68 +14,63 @@
|
|||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "synctoken",
|
"name": "displayname",
|
||||||
"ordinal": 2,
|
"ordinal": 2,
|
||||||
"type_info": "Integer"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "displayname",
|
"name": "order",
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"type_info": "Text"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "description",
|
"name": "description",
|
||||||
"ordinal": 4,
|
"ordinal": 4,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "order",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "color",
|
"name": "color",
|
||||||
"ordinal": 6,
|
"ordinal": 5,
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "timezone",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "timezone_id",
|
"name": "timezone_id",
|
||||||
"ordinal": 8,
|
"ordinal": 6,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "deleted_at",
|
"name": "deleted_at",
|
||||||
"ordinal": 9,
|
"ordinal": 7,
|
||||||
"type_info": "Datetime"
|
"type_info": "Datetime"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "synctoken",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "subscription_url",
|
"name": "subscription_url",
|
||||||
"ordinal": 10,
|
"ordinal": 9,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "push_topic",
|
"name": "push_topic",
|
||||||
"ordinal": 11,
|
"ordinal": 10,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "comp_event",
|
"name": "comp_event",
|
||||||
"ordinal": 12,
|
"ordinal": 11,
|
||||||
"type_info": "Bool"
|
"type_info": "Bool"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "comp_todo",
|
"name": "comp_todo",
|
||||||
"ordinal": 13,
|
"ordinal": 12,
|
||||||
"type_info": "Bool"
|
"type_info": "Bool"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "comp_journal",
|
"name": "comp_journal",
|
||||||
"ordinal": 14,
|
"ordinal": 13,
|
||||||
"type_info": "Bool"
|
"type_info": "Bool"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -85,14 +80,13 @@
|
|||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -100,5 +94,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "cce62f7829bd688cd8c7928b587bc31f0e50865c214b1df113350bea2c254237"
|
"hash": "27ac68a4eea40c1cac663cad034028cf6c373354b29e3a5290c18f58101913cd"
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n REPLACE INTO principals\n (id, displayname, principal_type, password_hash)\n VALUES (?, ?, ?, ?)\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 4
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "2f043f62a7c0eae1023e319f0bc8f35dfdcf6a8247e03b1de3e2cabb2d3ab8ae"
|
|
||||||
}
|
|
||||||
12
.sqlx/query-46ae176a06e314492f661c28436d6370883052c854da43475d7ced60cf8326e3.json
generated
Normal file
12
.sqlx/query-46ae176a06e314492f661c28436d6370883052c854da43475d7ced60cf8326e3.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ?\n WHERE (principal, id) = (?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 13
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "46ae176a06e314492f661c28436d6370883052c854da43475d7ced60cf8326e3"
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT INTO calendars (principal, id, displayname, description, \"order\", color, subscription_url, timezone, timezone_id, push_topic, comp_event, comp_todo, comp_journal)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 13
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "5132ee8198f155242aa332a10019c48ec334884bcf7841c8aa03fd5eb11351d9"
|
|
||||||
}
|
|
||||||
12
.sqlx/query-5c09c2a3c052188435409d4ff076575394e625dd19f00dea2d4c71a9f34a5952.json
generated
Normal file
12
.sqlx/query-5c09c2a3c052188435409d4ff076575394e625dd19f00dea2d4c71a9f34a5952.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO principals\n (id, displayname, principal_type, password_hash) VALUES (?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n (displayname, principal_type, password_hash)\n = (excluded.displayname, excluded.principal_type, excluded.password_hash)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 4
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "5c09c2a3c052188435409d4ff076575394e625dd19f00dea2d4c71a9f34a5952"
|
||||||
|
}
|
||||||
12
.sqlx/query-60b940ff493e7c0fcb2ffe8ae97172c6444525ffeec21b194bd7443d11d06113.json
generated
Normal file
12
.sqlx/query-60b940ff493e7c0fcb2ffe8ae97172c6444525ffeec21b194bd7443d11d06113.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO calendars (principal, id, displayname, description, \"order\", color, subscription_url, timezone_id, push_topic, comp_event, comp_todo, comp_journal)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 12
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "60b940ff493e7c0fcb2ffe8ae97172c6444525ffeec21b194bd7443d11d06113"
|
||||||
|
}
|
||||||
26
.sqlx/query-660833e0505d3bbcd6dd736cce06b1bf14263d0e0e87b27d89d376d422e4e474.json
generated
Normal file
26
.sqlx/query-660833e0505d3bbcd6dd736cce06b1bf14263d0e0e87b27d89d376d422e4e474.json
generated
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT length(vcf) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM addressobjects WHERE principal = ? AND addressbook_id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "length!: u64",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted!: bool",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Datetime"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "660833e0505d3bbcd6dd736cce06b1bf14263d0e0e87b27d89d376d422e4e474"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT *\n FROM calendars\n WHERE (principal, id) = (?, ?)",
|
"query": "SELECT *\n FROM calendars\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?) ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -39,48 +39,43 @@
|
|||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "timezone",
|
"name": "timezone_id",
|
||||||
"ordinal": 7,
|
"ordinal": 7,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "timezone_id",
|
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "deleted_at",
|
"name": "deleted_at",
|
||||||
"ordinal": 9,
|
"ordinal": 8,
|
||||||
"type_info": "Datetime"
|
"type_info": "Datetime"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "subscription_url",
|
"name": "subscription_url",
|
||||||
"ordinal": 10,
|
"ordinal": 9,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "push_topic",
|
"name": "push_topic",
|
||||||
"ordinal": 11,
|
"ordinal": 10,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "comp_event",
|
"name": "comp_event",
|
||||||
"ordinal": 12,
|
"ordinal": 11,
|
||||||
"type_info": "Bool"
|
"type_info": "Bool"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "comp_todo",
|
"name": "comp_todo",
|
||||||
"ordinal": 13,
|
"ordinal": 12,
|
||||||
"type_info": "Bool"
|
"type_info": "Bool"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "comp_journal",
|
"name": "comp_journal",
|
||||||
"ordinal": 14,
|
"ordinal": 13,
|
||||||
"type_info": "Bool"
|
"type_info": "Bool"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 2
|
"Right": 3
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
false,
|
||||||
@@ -93,12 +88,11 @@
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "9f930775043a6d4571a8ffd5a981cadf7c51f3f11a189f8461505abec31076e6"
|
"hash": "bb2fa030f2e7c7afdb38c5c54cb31de5293be332d86cf643977d479999542553"
|
||||||
}
|
}
|
||||||
@@ -39,43 +39,38 @@
|
|||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "timezone",
|
"name": "timezone_id",
|
||||||
"ordinal": 7,
|
"ordinal": 7,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "timezone_id",
|
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "deleted_at",
|
"name": "deleted_at",
|
||||||
"ordinal": 9,
|
"ordinal": 8,
|
||||||
"type_info": "Datetime"
|
"type_info": "Datetime"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "subscription_url",
|
"name": "subscription_url",
|
||||||
"ordinal": 10,
|
"ordinal": 9,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "push_topic",
|
"name": "push_topic",
|
||||||
"ordinal": 11,
|
"ordinal": 10,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "comp_event",
|
"name": "comp_event",
|
||||||
"ordinal": 12,
|
"ordinal": 11,
|
||||||
"type_info": "Bool"
|
"type_info": "Bool"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "comp_todo",
|
"name": "comp_todo",
|
||||||
"ordinal": 13,
|
"ordinal": 12,
|
||||||
"type_info": "Bool"
|
"type_info": "Bool"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "comp_journal",
|
"name": "comp_journal",
|
||||||
"ordinal": 14,
|
"ordinal": 13,
|
||||||
"type_info": "Bool"
|
"type_info": "Bool"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -93,7 +88,6 @@
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ?\n WHERE (principal, id) = (?, ?)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 14
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "d65c9c40606e59dd816a51b9b9ac60fd2ff81aaa358fcc038134e9a68ba45ad7"
|
|
||||||
}
|
|
||||||
26
.sqlx/query-d9f14260a46a7ccd137d462c35d350a7fe338a074131776596c5d803fcda1f48.json
generated
Normal file
26
.sqlx/query-d9f14260a46a7ccd137d462c35d350a7fe338a074131776596c5d803fcda1f48.json
generated
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT length(ics) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM calendarobjects WHERE principal = ? AND cal_id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "length!: u64",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted!: bool",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Datetime"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "d9f14260a46a7ccd137d462c35d350a7fe338a074131776596c5d803fcda1f48"
|
||||||
|
}
|
||||||
1147
Cargo.lock
generated
1147
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -2,7 +2,7 @@
|
|||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.5"
|
version = "0.9.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A CalDAV server"
|
description = "A CalDAV server"
|
||||||
repository = "https://github.com/lennart-k/rustical"
|
repository = "https://github.com/lennart-k/rustical"
|
||||||
@@ -95,8 +95,12 @@ strum = "0.27"
|
|||||||
strum_macros = "0.27"
|
strum_macros = "0.27"
|
||||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||||
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
|
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
|
||||||
ical = { version = "0.11", features = ["generator", "serde"] }
|
ical = { git = "https://github.com/lennart-k/ical-rs", features = [
|
||||||
toml = "0.8"
|
"generator",
|
||||||
|
"serde",
|
||||||
|
"chrono-tz",
|
||||||
|
] }
|
||||||
|
toml = "0.9"
|
||||||
tower = "0.5"
|
tower = "0.5"
|
||||||
tower-http = { version = "0.6", features = [
|
tower-http = { version = "0.6", features = [
|
||||||
"trace",
|
"trace",
|
||||||
@@ -126,7 +130,7 @@ syn = { version = "2.0", features = ["full"] }
|
|||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
heck = "0.5"
|
heck = "0.5"
|
||||||
darling = "0.20"
|
darling = "0.21"
|
||||||
reqwest = { version = "0.12", features = [
|
reqwest = { version = "0.12", features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"charset",
|
"charset",
|
||||||
@@ -135,10 +139,12 @@ reqwest = { version = "0.12", features = [
|
|||||||
openidconnect = "4.0"
|
openidconnect = "4.0"
|
||||||
clap = { version = "4.5", features = ["derive", "env"] }
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "f0591d13" }
|
matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "f0591d13" }
|
||||||
|
vtimezones-rs = "0.2"
|
||||||
ece = { version = "2.3", default-features = false, features = [
|
ece = { version = "2.3", default-features = false, features = [
|
||||||
"backend-openssl",
|
"backend-openssl",
|
||||||
] }
|
] }
|
||||||
openssl = { version = "0.10", features = ["vendored"] }
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
|
async-std = { version = "1.13", features = ["attributes"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustical_store = { workspace = true }
|
rustical_store = { workspace = true }
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
FROM --platform=$BUILDPLATFORM rust:1.86-alpine AS chef
|
FROM --platform=$BUILDPLATFORM rust:1.88-alpine AS chef
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
|
|
||||||
# the compiler will otherwise ask for aarch64-linux-musl-gcc
|
# the compiler will otherwise ask for aarch64-linux-musl-gcc
|
||||||
ENV CC_aarch64_unknown_linux_musl="clang"
|
ENV CC_aarch64_unknown_linux_musl="clang"
|
||||||
ENV AR_aarch64_unknown_linux_musl="llvm-ar"
|
ENV AR_aarch64_unknown_linux_musl="llvm20-ar"
|
||||||
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld"
|
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld"
|
||||||
|
|
||||||
# Stupid workaound with tempfiles since environment variables
|
# Stupid workaound with tempfiles since environment variables
|
||||||
@@ -16,7 +16,7 @@ RUN case $TARGETPLATFORM in \
|
|||||||
*) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \
|
*) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \
|
||||||
esac
|
esac
|
||||||
|
|
||||||
RUN apk add --no-cache musl-dev llvm19 clang perl pkgconf make \
|
RUN apk add --no-cache musl-dev llvm20 clang perl pkgconf make \
|
||||||
&& rustup target add "$(cat /tmp/rust_target)" \
|
&& rustup target add "$(cat /tmp/rust_target)" \
|
||||||
&& cargo install cargo-chef --locked \
|
&& cargo install cargo-chef --locked \
|
||||||
&& rm -rf "$CARGO_HOME/registry"
|
&& rm -rf "$CARGO_HOME/registry"
|
||||||
|
|||||||
12
Justfile
12
Justfile
@@ -1,2 +1,14 @@
|
|||||||
licenses:
|
licenses:
|
||||||
cargo about generate about.hbs > crates/frontend/public/assets/licenses.html
|
cargo about generate about.hbs > crates/frontend/public/assets/licenses.html
|
||||||
|
|
||||||
|
frontend-dev:
|
||||||
|
cd crates/frontend/js-components && deno task dev
|
||||||
|
|
||||||
|
frontend-build:
|
||||||
|
cd crates/frontend/js-components && deno task build
|
||||||
|
|
||||||
|
docs:
|
||||||
|
mkdocs build
|
||||||
|
|
||||||
|
docs-dev:
|
||||||
|
mkdocs serve
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -3,22 +3,23 @@
|
|||||||
a CalDAV/CardDAV server
|
a CalDAV/CardDAV server
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
RustiCal is **not production-ready!**
|
RustiCal is under **active development**!
|
||||||
I've been using RustiCal for the last few weeks and I'm slowly becoming more confident,
|
While I've been successfully using RustiCal productively for a few weeks now,
|
||||||
however you'd be one of the first testers so expect bugs and rough edges.
|
you'd still be one of the first testers so expect bugs and rough edges.
|
||||||
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
|
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- easy to backup, everything saved in one SQLite database
|
- easy to backup, everything saved in one SQLite database
|
||||||
- also export feature in the frontend
|
- also export feature in the frontend
|
||||||
- [WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5
|
- **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5
|
||||||
- lightweight (the container image contains only one binary)
|
- lightweight (the container image contains only one binary)
|
||||||
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
||||||
- deleted calendars are recoverable
|
- deleted calendars are recoverable
|
||||||
- Nextcloud login flow (In DAVx5 you can login through the Nextcloud flow and automatically generate an app token)
|
- Nextcloud login flow (In DAVx5 you can login through the Nextcloud flow and automatically generate an app token)
|
||||||
- Apple configuration profiles (skip copy-pasting passwords and instead generate the configuration in the frontend)
|
- Apple configuration profiles (skip copy-pasting passwords and instead generate the configuration in the frontend)
|
||||||
- OpenID Connect support (with option to disable password login)
|
- **OpenID Connect** support (with option to disable password login)
|
||||||
|
- Group-based **sharing**
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -31,3 +32,4 @@ a CalDAV/CardDAV server
|
|||||||
- Evolution
|
- Evolution
|
||||||
- Apple Calendar
|
- Apple Calendar
|
||||||
- Home Assistant integration
|
- Home Assistant integration
|
||||||
|
- Thunderbird
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ accepted = [
|
|||||||
"CDLA-Permissive-2.0",
|
"CDLA-Permissive-2.0",
|
||||||
"Zlib",
|
"Zlib",
|
||||||
"AGPL-3.0",
|
"AGPL-3.0",
|
||||||
|
"GPL-3.0",
|
||||||
"MPL-2.0",
|
"MPL-2.0",
|
||||||
]
|
]
|
||||||
workarounds = ["ring", "chrono", "rustls"]
|
workarounds = ["ring", "chrono", "rustls"]
|
||||||
|
|||||||
22
compose.oidc.yml
Normal file
22
compose.oidc.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
rustical:
|
||||||
|
image: ghcr.io/lennart-k/rustical:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
RUSTICAL_FRONTEND__ALLOW_PASSWORD_LOGIN: "false"
|
||||||
|
RUSTICAL_OIDC__NAME: "Authelia"
|
||||||
|
RUSTICAL_OIDC__ISSUER: "https://auth.example.com"
|
||||||
|
RUSTICAL_OIDC__CLIENT_ID: "{{ rustical_oidc_client_id }}"
|
||||||
|
RUSTICAL_OIDC__CLIENT_SECRET: "{{ rustical_oidc_client_secret }}"
|
||||||
|
RUSTICAL_OIDC__CLAIM_USERID: "preferred_username"
|
||||||
|
RUSTICAL_OIDC__SCOPES: '["openid", "profile", "groups"]'
|
||||||
|
RUSTICAL_OIDC__REQUIRE_GROUP: "app:rustical" # optional
|
||||||
|
RUSTICAL_OIDC__ALLOW_SIGN_UP: "true"
|
||||||
|
volumes:
|
||||||
|
- data:/var/lib/rustical
|
||||||
|
# Here you probably want to you expose instead
|
||||||
|
ports:
|
||||||
|
- 4000:4000
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
@@ -7,6 +7,12 @@ repository.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rustical_store_sqlite = { workspace = true, features = ["test"] }
|
||||||
|
rstest.workspace = true
|
||||||
|
async-std.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
axum-extra.workspace = true
|
axum-extra.workspace = true
|
||||||
@@ -37,3 +43,4 @@ headers.workspace = true
|
|||||||
tower-http.workspace = true
|
tower-http.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
strum_macros.workspace = true
|
strum_macros.workspace = true
|
||||||
|
vtimezones-rs.workspace = true
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use axum::body::Body;
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::{extract::Path, response::Response};
|
use axum::{extract::Path, response::Response};
|
||||||
use headers::{ContentType, HeaderMapExt};
|
use headers::{ContentType, HeaderMapExt};
|
||||||
use http::{HeaderValue, StatusCode, header};
|
use http::{HeaderValue, Method, StatusCode, header};
|
||||||
use ical::generator::{Emitter, IcalCalendarBuilder};
|
use ical::generator::{Emitter, IcalCalendarBuilder};
|
||||||
use ical::property::Property;
|
use ical::property::Property;
|
||||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||||
@@ -19,19 +19,25 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
Path((principal, calendar_id)): Path<(String, String)>,
|
Path((principal, calendar_id)): Path<(String, String)>,
|
||||||
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||||
user: Principal,
|
user: Principal,
|
||||||
|
method: Method,
|
||||||
) -> Result<Response, Error> {
|
) -> Result<Response, Error> {
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(crate::Error::Unauthorized);
|
return Err(crate::Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
let calendar = cal_store
|
||||||
|
.get_calendar(&principal, &calendar_id, true)
|
||||||
|
.await?;
|
||||||
if !user.is_principal(&calendar.principal) {
|
if !user.is_principal(&calendar.principal) {
|
||||||
return Err(crate::Error::Unauthorized);
|
return Err(crate::Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
let calendar = cal_store
|
||||||
|
.get_calendar(&principal, &calendar_id, true)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let mut timezones = HashMap::new();
|
let mut timezones = HashMap::new();
|
||||||
|
let mut vtimezones = HashMap::new();
|
||||||
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
|
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
|
||||||
|
|
||||||
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
|
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
|
||||||
@@ -58,9 +64,9 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
params: None,
|
params: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let mut ical_calendar = ical_calendar_builder.build();
|
|
||||||
|
|
||||||
for object in &objects {
|
for object in &objects {
|
||||||
|
vtimezones.extend(object.get_vtimezones());
|
||||||
match object.get_data() {
|
match object.get_data() {
|
||||||
CalendarObjectComponent::Event(EventObject {
|
CalendarObjectComponent::Event(EventObject {
|
||||||
event,
|
event,
|
||||||
@@ -68,17 +74,25 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
timezones.extend(object_timezones);
|
timezones.extend(object_timezones);
|
||||||
ical_calendar.events.push(event.clone());
|
ical_calendar_builder = ical_calendar_builder.add_event(event.clone());
|
||||||
}
|
}
|
||||||
CalendarObjectComponent::Todo(TodoObject { todo, .. }) => {
|
CalendarObjectComponent::Todo(TodoObject(todo)) => {
|
||||||
ical_calendar.todos.push(todo.clone());
|
ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone());
|
||||||
}
|
}
|
||||||
CalendarObjectComponent::Journal(JournalObject { journal, .. }) => {
|
CalendarObjectComponent::Journal(JournalObject(journal)) => {
|
||||||
ical_calendar.journals.push(journal.clone());
|
ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for vtimezone in vtimezones.into_values() {
|
||||||
|
ical_calendar_builder = ical_calendar_builder.add_tz(vtimezone.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ical_calendar = ical_calendar_builder
|
||||||
|
.build()
|
||||||
|
.map_err(|parser_error| Error::IcalError(parser_error.into()))?;
|
||||||
|
|
||||||
let mut resp = Response::builder().status(StatusCode::OK);
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
let hdrs = resp.headers_mut().unwrap();
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
|
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
|
||||||
@@ -92,5 +106,9 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
))
|
))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
|
if matches!(method, Method::HEAD) {
|
||||||
|
Ok(resp.body(Body::empty()).unwrap())
|
||||||
|
} else {
|
||||||
|
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
crates/caldav/src/calendar/methods/import.rs
Normal file
102
crates/caldav/src/calendar/methods/import.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
use crate::Error;
|
||||||
|
use crate::calendar::CalendarResourceService;
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use http::StatusCode;
|
||||||
|
use ical::{
|
||||||
|
generator::Emitter,
|
||||||
|
parser::{Component, ComponentMut},
|
||||||
|
};
|
||||||
|
use rustical_ical::{CalendarObject, CalendarObjectType};
|
||||||
|
use rustical_store::{Calendar, CalendarStore, SubscriptionStore, auth::Principal};
|
||||||
|
use std::io::BufReader;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
#[instrument(skip(resource_service))]
|
||||||
|
pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
|
||||||
|
Path((principal, cal_id)): Path<(String, String)>,
|
||||||
|
user: Principal,
|
||||||
|
State(resource_service): State<CalendarResourceService<C, S>>,
|
||||||
|
body: String,
|
||||||
|
) -> Result<Response, Error> {
|
||||||
|
if !user.is_principal(&principal) {
|
||||||
|
return Err(Error::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parser = ical::IcalParser::new(BufReader::new(body.as_bytes()));
|
||||||
|
let mut cal = parser
|
||||||
|
.next()
|
||||||
|
.expect("input must contain calendar")
|
||||||
|
.unwrap()
|
||||||
|
.mutable();
|
||||||
|
if parser.next().is_some() {
|
||||||
|
return Err(rustical_ical::Error::InvalidData(
|
||||||
|
"multiple calendars, only one allowed".to_owned(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract calendar metadata
|
||||||
|
let displayname = cal
|
||||||
|
.get_property("X-WR-CALNAME")
|
||||||
|
.and_then(|prop| prop.value.to_owned());
|
||||||
|
let description = cal
|
||||||
|
.get_property("X-WR-CALDESC")
|
||||||
|
.and_then(|prop| prop.value.to_owned());
|
||||||
|
let timezone_id = cal
|
||||||
|
.get_property("X-WR-TIMEZONE")
|
||||||
|
.and_then(|prop| prop.value.to_owned());
|
||||||
|
// These properties should not appear in the expanded calendar objects
|
||||||
|
cal.remove_property("X-WR-CALNAME");
|
||||||
|
cal.remove_property("X-WR-CALDESC");
|
||||||
|
cal.remove_property("X-WR-TIMEZONE");
|
||||||
|
let cal = cal.verify().unwrap();
|
||||||
|
// Make sure timezone is valid
|
||||||
|
if let Some(timezone_id) = timezone_id.as_ref() {
|
||||||
|
assert!(
|
||||||
|
vtimezones_rs::VTIMEZONES.contains_key(timezone_id),
|
||||||
|
"Invalid calendar timezone id"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract necessary component types
|
||||||
|
let mut cal_components = vec![];
|
||||||
|
if !cal.events.is_empty() {
|
||||||
|
cal_components.push(CalendarObjectType::Event);
|
||||||
|
}
|
||||||
|
if !cal.journals.is_empty() {
|
||||||
|
cal_components.push(CalendarObjectType::Journal);
|
||||||
|
}
|
||||||
|
if !cal.todos.is_empty() {
|
||||||
|
cal_components.push(CalendarObjectType::Todo);
|
||||||
|
}
|
||||||
|
|
||||||
|
let expanded_cals = cal.expand_calendar();
|
||||||
|
// Janky way to convert between IcalCalendar and CalendarObject
|
||||||
|
let objects = expanded_cals
|
||||||
|
.into_iter()
|
||||||
|
.map(|cal| cal.generate())
|
||||||
|
.map(CalendarObject::from_ics)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let new_cal = Calendar {
|
||||||
|
principal,
|
||||||
|
id: cal_id,
|
||||||
|
displayname,
|
||||||
|
order: 0,
|
||||||
|
description,
|
||||||
|
color: None,
|
||||||
|
timezone_id,
|
||||||
|
deleted_at: None,
|
||||||
|
synctoken: 0,
|
||||||
|
subscription_url: None,
|
||||||
|
push_topic: uuid::Uuid::new_v4().to_string(),
|
||||||
|
components: cal_components,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cal_store = resource_service.cal_store;
|
||||||
|
cal_store.import_calendar(new_cal, objects, false).await?;
|
||||||
|
|
||||||
|
Ok(StatusCode::OK.into_response())
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ use crate::calendar::prop::SupportedCalendarComponentSet;
|
|||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use http::{Method, StatusCode};
|
use http::{Method, StatusCode};
|
||||||
|
use ical::IcalParser;
|
||||||
use rustical_dav::xml::HrefElement;
|
use rustical_dav::xml::HrefElement;
|
||||||
use rustical_ical::CalendarObjectType;
|
use rustical_ical::CalendarObjectType;
|
||||||
use rustical_store::auth::Principal;
|
use rustical_store::auth::Principal;
|
||||||
@@ -82,13 +83,38 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
request.displayname = None
|
request.displayname = None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let timezone_id = if let Some(tzid) = request.calendar_timezone_id {
|
||||||
|
Some(tzid)
|
||||||
|
} else if let Some(tz) = request.calendar_timezone {
|
||||||
|
// TODO: Proper error (calendar-timezone precondition)
|
||||||
|
let calendar = IcalParser::new(tz.as_bytes())
|
||||||
|
.next()
|
||||||
|
.ok_or(rustical_dav::Error::BadRequest(
|
||||||
|
"No timezone data provided".to_owned(),
|
||||||
|
))?
|
||||||
|
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
|
||||||
|
|
||||||
|
let timezone = calendar
|
||||||
|
.timezones
|
||||||
|
.first()
|
||||||
|
.ok_or(rustical_dav::Error::BadRequest(
|
||||||
|
"No timezone data provided".to_owned(),
|
||||||
|
))?;
|
||||||
|
let timezone: chrono_tz::Tz = timezone
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
|
||||||
|
|
||||||
|
Some(timezone.name().to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let calendar = Calendar {
|
let calendar = Calendar {
|
||||||
id: cal_id.to_owned(),
|
id: cal_id.to_owned(),
|
||||||
principal: principal.to_owned(),
|
principal: principal.to_owned(),
|
||||||
order: request.calendar_order.unwrap_or(0),
|
order: request.calendar_order.unwrap_or(0),
|
||||||
displayname: request.displayname,
|
displayname: request.displayname,
|
||||||
timezone: request.calendar_timezone,
|
timezone_id,
|
||||||
timezone_id: request.calendar_timezone_id,
|
|
||||||
color: request.calendar_color,
|
color: request.calendar_color,
|
||||||
description: request.calendar_description,
|
description: request.calendar_description,
|
||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod get;
|
pub mod get;
|
||||||
|
pub mod import;
|
||||||
pub mod mkcalendar;
|
pub mod mkcalendar;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
|
|
||||||
let calendar = resource_service
|
let calendar = resource_service
|
||||||
.cal_store
|
.cal_store
|
||||||
.get_calendar(&principal, &cal_id)
|
.get_calendar(&principal, &cal_id, false)
|
||||||
.await?;
|
.await?;
|
||||||
let calendar_resource = CalendarResource {
|
let calendar_resource = CalendarResource {
|
||||||
cal: calendar,
|
cal: calendar,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub(crate) struct TimeRangeElement {
|
|||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.3
|
||||||
struct ParamFilterElement {
|
struct ParamFilterElement {
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
is_not_defined: Option<()>,
|
is_not_defined: Option<()>,
|
||||||
@@ -32,11 +33,13 @@ struct TextMatchElement {
|
|||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
collation: String,
|
collation: String,
|
||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
negate_collation: String,
|
// "yes" or "no", default: "no"
|
||||||
|
negate_condition: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
|
||||||
pub(crate) struct PropFilterElement {
|
pub(crate) struct PropFilterElement {
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
is_not_defined: Option<()>,
|
is_not_defined: Option<()>,
|
||||||
@@ -46,6 +49,9 @@ pub(crate) struct PropFilterElement {
|
|||||||
text_match: Option<TextMatchElement>,
|
text_match: Option<TextMatchElement>,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||||
param_filter: Vec<ParamFilterElement>,
|
param_filter: Vec<ParamFilterElement>,
|
||||||
|
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
@@ -61,7 +67,7 @@ pub(crate) struct CompFilterElement {
|
|||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||||
pub(crate) comp_filter: Vec<CompFilterElement>,
|
pub(crate) comp_filter: Vec<CompFilterElement>,
|
||||||
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,3 +209,102 @@ pub async fn get_objects_calendar_query<C: CalendarStore>(
|
|||||||
}
|
}
|
||||||
Ok(objects)
|
Ok(objects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rustical_dav::xml::PropElement;
|
||||||
|
use rustical_xml::XmlDocument;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
calendar::methods::report::{
|
||||||
|
ReportRequest,
|
||||||
|
calendar_query::{
|
||||||
|
CalendarQueryRequest, CompFilterElement, FilterElement, ParamFilterElement,
|
||||||
|
PropFilterElement, TextMatchElement,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
calendar_object::{CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn calendar_query_7_8_7() {
|
||||||
|
const INPUT: &str = r#"
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<D:prop xmlns:D="DAV:">
|
||||||
|
<D:getetag/>
|
||||||
|
<C:calendar-data/>
|
||||||
|
</D:prop>
|
||||||
|
<C:filter>
|
||||||
|
<C:comp-filter name="VCALENDAR">
|
||||||
|
<C:comp-filter name="VEVENT">
|
||||||
|
<C:prop-filter name="ATTENDEE">
|
||||||
|
<C:text-match collation="i;ascii-casemap">mailto:lisa@example.com</C:text-match>
|
||||||
|
<C:param-filter name="PARTSTAT">
|
||||||
|
<C:text-match collation="i;ascii-casemap">NEEDS-ACTION</C:text-match>
|
||||||
|
</C:param-filter>
|
||||||
|
</C:prop-filter>
|
||||||
|
</C:comp-filter>
|
||||||
|
</C:comp-filter>
|
||||||
|
</C:filter>
|
||||||
|
</C:calendar-query>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let report = ReportRequest::parse_str(INPUT).unwrap();
|
||||||
|
let calendar_query: CalendarQueryRequest =
|
||||||
|
if let ReportRequest::CalendarQuery(query) = report {
|
||||||
|
query
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
calendar_query,
|
||||||
|
CalendarQueryRequest {
|
||||||
|
prop: rustical_dav::xml::PropfindType::Prop(PropElement(
|
||||||
|
vec![
|
||||||
|
CalendarObjectPropWrapperName::CalendarObject(
|
||||||
|
CalendarObjectPropName::Getetag,
|
||||||
|
),
|
||||||
|
CalendarObjectPropWrapperName::CalendarObject(
|
||||||
|
CalendarObjectPropName::CalendarData(Default::default())
|
||||||
|
),
|
||||||
|
],
|
||||||
|
vec![]
|
||||||
|
)),
|
||||||
|
filter: Some(FilterElement {
|
||||||
|
comp_filter: CompFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![CompFilterElement {
|
||||||
|
prop_filter: vec![PropFilterElement {
|
||||||
|
name: "ATTENDEE".to_owned(),
|
||||||
|
text_match: Some(TextMatchElement {
|
||||||
|
collation: "i;ascii-casemap".to_owned(),
|
||||||
|
negate_condition: None
|
||||||
|
}),
|
||||||
|
is_not_defined: None,
|
||||||
|
param_filter: vec![ParamFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "PARTSTAT".to_owned(),
|
||||||
|
text_match: Some(TextMatchElement {
|
||||||
|
collation: "i;ascii-casemap".to_owned(),
|
||||||
|
negate_condition: None
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
time_range: None
|
||||||
|
}],
|
||||||
|
comp_filter: vec![],
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "VEVENT".to_owned(),
|
||||||
|
time_range: None
|
||||||
|
}],
|
||||||
|
name: "VCALENDAR".to_owned()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
timezone: None,
|
||||||
|
timezone_id: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ fn objects_response(
|
|||||||
object,
|
object,
|
||||||
principal: principal.to_owned(),
|
principal: principal.to_owned(),
|
||||||
}
|
}
|
||||||
.propfind(&path, prop, puri, user)?,
|
.propfind(&path, prop, None, puri, user)?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ pub async fn handle_sync_collection<C: CalendarStore>(
|
|||||||
object,
|
object,
|
||||||
principal: principal.to_owned(),
|
principal: principal.to_owned(),
|
||||||
}
|
}
|
||||||
.propfind(&path, &sync_collection.prop, puri, user)?,
|
.propfind(&path, &sync_collection.prop, None, puri, user)?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,6 @@ pub mod resource;
|
|||||||
mod service;
|
mod service;
|
||||||
|
|
||||||
pub use service::CalendarResourceService;
|
pub use service::CalendarResourceService;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::Error;
|
|||||||
use crate::calendar::prop::ReportMethod;
|
use crate::calendar::prop::ReportMethod;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
|
use ical::IcalParser;
|
||||||
use rustical_dav::extensions::{
|
use rustical_dav::extensions::{
|
||||||
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
||||||
};
|
};
|
||||||
@@ -15,7 +16,7 @@ use rustical_store::Calendar;
|
|||||||
use rustical_store::auth::Principal;
|
use rustical_store::auth::Principal;
|
||||||
use rustical_xml::{EnumVariants, PropName};
|
use rustical_xml::{EnumVariants, PropName};
|
||||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||||
use std::str::FromStr;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||||
#[xml(unit_variants_ident = "CalendarPropName")]
|
#[xml(unit_variants_ident = "CalendarPropName")]
|
||||||
@@ -34,7 +35,7 @@ pub enum CalendarProp {
|
|||||||
CalendarTimezoneId(Option<String>),
|
CalendarTimezoneId(Option<String>),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_ICAL")]
|
#[xml(ns = "rustical_dav::namespace::NS_ICAL")]
|
||||||
CalendarOrder(Option<i64>),
|
CalendarOrder(Option<i64>),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
SupportedCalendarComponentSet(SupportedCalendarComponentSet),
|
SupportedCalendarComponentSet(SupportedCalendarComponentSet),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
||||||
SupportedCalendarData(SupportedCalendarData),
|
SupportedCalendarData(SupportedCalendarData),
|
||||||
@@ -62,7 +63,7 @@ pub enum CalendarPropWrapper {
|
|||||||
Common(CommonPropertiesProp),
|
Common(CommonPropertiesProp),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, From, Into)]
|
#[derive(Clone, Debug, From, Into, Deserialize)]
|
||||||
pub struct CalendarResource {
|
pub struct CalendarResource {
|
||||||
pub cal: Calendar,
|
pub cal: Calendar,
|
||||||
pub read_only: bool,
|
pub read_only: bool,
|
||||||
@@ -133,7 +134,9 @@ impl Resource for CalendarResource {
|
|||||||
CalendarProp::CalendarDescription(self.cal.description.clone())
|
CalendarProp::CalendarDescription(self.cal.description.clone())
|
||||||
}
|
}
|
||||||
CalendarPropName::CalendarTimezone => {
|
CalendarPropName::CalendarTimezone => {
|
||||||
CalendarProp::CalendarTimezone(self.cal.timezone.clone())
|
CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
|
||||||
|
vtimezones_rs::VTIMEZONES.get(tzid).map(|tz| tz.to_string())
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
// chrono_tz uses the IANA database
|
// chrono_tz uses the IANA database
|
||||||
CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet(
|
CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet(
|
||||||
@@ -192,21 +195,42 @@ impl Resource for CalendarResource {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarProp::CalendarTimezone(timezone) => {
|
CalendarProp::CalendarTimezone(timezone) => {
|
||||||
// TODO: Ensure that timezone-id is also updated
|
if let Some(tz) = timezone {
|
||||||
self.cal.timezone = timezone;
|
// TODO: Proper error (calendar-timezone precondition)
|
||||||
|
let calendar = IcalParser::new(tz.as_bytes())
|
||||||
|
.next()
|
||||||
|
.ok_or(rustical_dav::Error::BadRequest(
|
||||||
|
"No timezone data provided".to_owned(),
|
||||||
|
))?
|
||||||
|
.map_err(|_| {
|
||||||
|
rustical_dav::Error::BadRequest(
|
||||||
|
"No timezone data provided".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let timezone =
|
||||||
|
calendar
|
||||||
|
.timezones
|
||||||
|
.first()
|
||||||
|
.ok_or(rustical_dav::Error::BadRequest(
|
||||||
|
"No timezone data provided".to_owned(),
|
||||||
|
))?;
|
||||||
|
let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| {
|
||||||
|
rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.cal.timezone_id = Some(timezone.name().to_owned());
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly),
|
CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||||
CalendarProp::CalendarTimezoneId(timezone_id) => {
|
CalendarProp::CalendarTimezoneId(timezone_id) => {
|
||||||
if let Some(tzid) = &timezone_id {
|
if let Some(tzid) = &timezone_id {
|
||||||
// Validate timezone id
|
if !vtimezones_rs::VTIMEZONES.contains_key(tzid) {
|
||||||
chrono_tz::Tz::from_str(tzid).map_err(|_| {
|
return Err(rustical_dav::Error::BadRequest(format!(
|
||||||
rustical_dav::Error::BadRequest(format!(
|
"Invalid timezone-id: {tzid}"
|
||||||
"Invalid timezone-id: {}",
|
)));
|
||||||
tzid
|
}
|
||||||
))
|
|
||||||
})?;
|
|
||||||
// TODO: Ensure that timezone is also updated (For now hope that clients play nice)
|
|
||||||
}
|
}
|
||||||
self.cal.timezone_id = timezone_id;
|
self.cal.timezone_id = timezone_id;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -247,15 +271,11 @@ impl Resource for CalendarResource {
|
|||||||
self.cal.description = None;
|
self.cal.description = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarPropName::CalendarTimezone => {
|
CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
|
||||||
self.cal.timezone = None;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
|
|
||||||
CalendarPropName::CalendarTimezoneId => {
|
|
||||||
self.cal.timezone_id = None;
|
self.cal.timezone_id = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
|
||||||
CalendarPropName::CalendarOrder => {
|
CalendarPropName::CalendarOrder => {
|
||||||
self.cal.order = 0;
|
self.cal.order = 0;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -292,7 +312,12 @@ impl Resource for CalendarResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
|
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
|
||||||
if self.cal.subscription_url.is_some() || self.read_only {
|
if self.cal.subscription_url.is_some() {
|
||||||
|
return Ok(UserPrivilegeSet::owner_write_properties(
|
||||||
|
user.is_principal(&self.cal.principal),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.read_only {
|
||||||
return Ok(UserPrivilegeSet::owner_read(
|
return Ok(UserPrivilegeSet::owner_read(
|
||||||
user.is_principal(&self.cal.principal),
|
user.is_principal(&self.cal.principal),
|
||||||
));
|
));
|
||||||
@@ -303,3 +328,15 @@ impl Resource for CalendarResource {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn test_tzdb_version() {
|
||||||
|
// Ensure that both chrono_tz and vzic_rs use the same tzdb version
|
||||||
|
assert_eq!(
|
||||||
|
chrono_tz::IANA_TZDB_VERSION,
|
||||||
|
vtimezones_rs::IANA_TZDB_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::calendar::methods::get::route_get;
|
use crate::calendar::methods::get::route_get;
|
||||||
|
use crate::calendar::methods::import::route_import;
|
||||||
use crate::calendar::methods::mkcalendar::route_mkcalendar;
|
use crate::calendar::methods::mkcalendar::route_mkcalendar;
|
||||||
use crate::calendar::methods::post::route_post;
|
use crate::calendar::methods::post::route_post;
|
||||||
use crate::calendar::methods::report::route_report_calendar;
|
use crate::calendar::methods::report::route_report_calendar;
|
||||||
@@ -51,13 +52,17 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
|
|||||||
type Principal = Principal;
|
type Principal = Principal;
|
||||||
type PrincipalUri = CalDavPrincipalUri;
|
type PrincipalUri = CalDavPrincipalUri;
|
||||||
|
|
||||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy, webdav-push";
|
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, webdav-push";
|
||||||
|
|
||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
(principal, cal_id): &Self::PathComponents,
|
(principal, cal_id): &Self::PathComponents,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Error> {
|
) -> Result<Self::Resource, Error> {
|
||||||
let calendar = self.cal_store.get_calendar(principal, cal_id).await?;
|
let calendar = self
|
||||||
|
.cal_store
|
||||||
|
.get_calendar(principal, cal_id, show_deleted)
|
||||||
|
.await?;
|
||||||
Ok(CalendarResource {
|
Ok(CalendarResource {
|
||||||
cal: calendar,
|
cal: calendar,
|
||||||
read_only: self.cal_store.is_read_only(cal_id),
|
read_only: self.cal_store.is_read_only(cal_id),
|
||||||
@@ -134,6 +139,13 @@ impl<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarResourceSer
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn import() -> Option<rustical_dav::resource::MethodFunction<Self>> {
|
||||||
|
Some(|state, req| {
|
||||||
|
let mut service = Handler::with_state(route_import::<C, S>, state);
|
||||||
|
Box::pin(Service::call(&mut service, req))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
|
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
|
||||||
{
|
{
|
||||||
Some(|state, req| {
|
Some(|state, req| {
|
||||||
|
|||||||
222
crates/caldav/src/calendar/test_files/propfind.outputs
Normal file
222
crates/caldav/src/calendar/test_files/propfind.outputs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<response xmlns:CS="http://calendarserver.org/ns/" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns="DAV:" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<href>/caldav/principal/user/calendar/</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<calendar-color xmlns="http://apple.com/ns/ical/"/>
|
||||||
|
<calendar-description xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<calendar-timezone xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<timezone-service-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<calendar-timezone-id xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<calendar-order xmlns="http://apple.com/ns/ical/"/>
|
||||||
|
<supported-calendar-component-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<supported-calendar-data xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<max-resource-size xmlns="DAV:"/>
|
||||||
|
<supported-report-set xmlns="DAV:"/>
|
||||||
|
<source xmlns="http://calendarserver.org/ns/"/>
|
||||||
|
<min-date-time xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<max-date-time xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<sync-token xmlns="DAV:"/>
|
||||||
|
<getctag xmlns="http://calendarserver.org/ns/"/>
|
||||||
|
<transports xmlns="https://bitfire.at/webdav-push"/>
|
||||||
|
<topic xmlns="https://bitfire.at/webdav-push"/>
|
||||||
|
<supported-triggers xmlns="https://bitfire.at/webdav-push"/>
|
||||||
|
<resourcetype xmlns="DAV:"/>
|
||||||
|
<displayname xmlns="DAV:"/>
|
||||||
|
<current-user-principal xmlns="DAV:"/>
|
||||||
|
<current-user-privilege-set xmlns="DAV:"/>
|
||||||
|
<owner xmlns="DAV:"/>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<response xmlns:CS="http://calendarserver.org/ns/" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns="DAV:" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<href>/caldav/principal/user/calendar/</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<CAL:calendar-timezone>BEGIN:VCALENDAR
|
||||||
|
PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Europe/Berlin
|
||||||
|
LAST-MODIFIED:20250723T190331Z
|
||||||
|
X-LIC-LOCATION:Europe/Berlin
|
||||||
|
X-PROLEPTIC-TZNAME:LMT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+005328
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:18930401T000000
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19160430T230000
|
||||||
|
RDATE:19400401T020000
|
||||||
|
RDATE:19430329T020000
|
||||||
|
RDATE:19460414T020000
|
||||||
|
RDATE:19470406T030000
|
||||||
|
RDATE:19480418T020000
|
||||||
|
RDATE:19490410T020000
|
||||||
|
RDATE:19800406T020000
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19161001T010000
|
||||||
|
RDATE:19421102T030000
|
||||||
|
RDATE:19431004T030000
|
||||||
|
RDATE:19441002T030000
|
||||||
|
RDATE:19451118T030000
|
||||||
|
RDATE:19461007T030000
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19170416T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T010000Z
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19170917T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T010000Z
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19440403T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T010000Z
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEMT
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0300
|
||||||
|
DTSTART:19450524T020000
|
||||||
|
RDATE:19470511T030000
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0300
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19450924T030000
|
||||||
|
RDATE:19470629T030000
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19460101T000000
|
||||||
|
RDATE:19800101T000000
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19471005T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19491002T010000Z
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19800928T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T010000Z
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19810329T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19961027T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
END:VCALENDAR
|
||||||
|
</CAL:calendar-timezone>
|
||||||
|
<CAL:timezone-service-set>
|
||||||
|
<href>https://www.iana.org/time-zones</href>
|
||||||
|
</CAL:timezone-service-set>
|
||||||
|
<CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
|
||||||
|
<calendar-order xmlns="http://apple.com/ns/ical/">0</calendar-order>
|
||||||
|
<CAL:supported-calendar-component-set>
|
||||||
|
<CAL:comp name="VEVENT"/>
|
||||||
|
<CAL:comp name="VTODO"/>
|
||||||
|
</CAL:supported-calendar-component-set>
|
||||||
|
<CAL:supported-calendar-data>
|
||||||
|
<CAL:calendar-data content-type="text/calendar" version="2.0"/>
|
||||||
|
</CAL:supported-calendar-data>
|
||||||
|
<max-resource-size>10000000</max-resource-size>
|
||||||
|
<supported-report-set>
|
||||||
|
<supported-report>
|
||||||
|
<report>
|
||||||
|
<CAL:calendar-query/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
<supported-report>
|
||||||
|
<report>
|
||||||
|
<CAL:calendar-multiget/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
<supported-report>
|
||||||
|
<report>
|
||||||
|
<sync-collection/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
</supported-report-set>
|
||||||
|
<CAL:min-date-time>-2621430101T000000Z</CAL:min-date-time>
|
||||||
|
<CAL:max-date-time>+2621421231T235959Z</CAL:max-date-time>
|
||||||
|
<sync-token>github.com/lennart-k/rustical/ns/12</sync-token>
|
||||||
|
<CS:getctag>github.com/lennart-k/rustical/ns/12</CS:getctag>
|
||||||
|
<PUSH:transports>
|
||||||
|
<PUSH:web-push/>
|
||||||
|
</PUSH:transports>
|
||||||
|
<PUSH:topic>b28b41e9-8801-4fc5-ae29-8efb5fadeb36</PUSH:topic>
|
||||||
|
<PUSH:supported-triggers>
|
||||||
|
<PUSH:content-update>
|
||||||
|
<depth>1</depth>
|
||||||
|
</PUSH:content-update>
|
||||||
|
<PUSH:property-update>
|
||||||
|
<depth>1</depth>
|
||||||
|
</PUSH:property-update>
|
||||||
|
</PUSH:supported-triggers>
|
||||||
|
<resourcetype>
|
||||||
|
<collection/>
|
||||||
|
<CAL:calendar/>
|
||||||
|
</resourcetype>
|
||||||
|
<displayname>Calendar</displayname>
|
||||||
|
<current-user-principal>
|
||||||
|
<href>/caldav/principal/user/</href>
|
||||||
|
</current-user-principal>
|
||||||
|
<current-user-privilege-set>
|
||||||
|
<privilege>
|
||||||
|
<read/>
|
||||||
|
</privilege>
|
||||||
|
<privilege>
|
||||||
|
<read-acl/>
|
||||||
|
</privilege>
|
||||||
|
<privilege>
|
||||||
|
<read-current-user-privilege-set/>
|
||||||
|
</privilege>
|
||||||
|
</current-user-privilege-set>
|
||||||
|
<owner>
|
||||||
|
<href>/caldav/principal/user/</href>
|
||||||
|
</owner>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "user",
|
||||||
|
"displayname": null,
|
||||||
|
"principal_type": "individual",
|
||||||
|
"password": null,
|
||||||
|
"memberships": [
|
||||||
|
"group"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
6
crates/caldav/src/calendar/test_files/propfind.requests
Normal file
6
crates/caldav/src/calendar/test_files/propfind.requests
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:"><propname/></propfind>
|
||||||
|
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:"><allprop/></propfind>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"cal": {
|
||||||
|
"principal": "user",
|
||||||
|
"id": "calendar",
|
||||||
|
"displayname": "Calendar",
|
||||||
|
"order": 0,
|
||||||
|
"description": null,
|
||||||
|
"color": null,
|
||||||
|
"timezone_id": "Europe/Berlin",
|
||||||
|
"deleted_at": null,
|
||||||
|
"synctoken": 12,
|
||||||
|
"subscription_url": null,
|
||||||
|
"push_topic": "b28b41e9-8801-4fc5-ae29-8efb5fadeb36",
|
||||||
|
"components": [
|
||||||
|
"VEVENT",
|
||||||
|
"VTODO"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"read_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cal": {
|
||||||
|
"principal": "user",
|
||||||
|
"id": "calendar",
|
||||||
|
"displayname": "Calendar",
|
||||||
|
"order": 0,
|
||||||
|
"description": null,
|
||||||
|
"color": null,
|
||||||
|
"timezone_id": "Europe/Berlin",
|
||||||
|
"deleted_at": null,
|
||||||
|
"synctoken": 12,
|
||||||
|
"subscription_url": null,
|
||||||
|
"push_topic": "b28b41e9-8801-4fc5-ae29-8efb5fadeb36",
|
||||||
|
"components": [
|
||||||
|
"VEVENT",
|
||||||
|
"VTODO"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"read_only": true
|
||||||
|
}
|
||||||
|
]
|
||||||
47
crates/caldav/src/calendar/tests.rs
Normal file
47
crates/caldav/src/calendar/tests.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use crate::{CalDavPrincipalUri, calendar::resource::CalendarResource};
|
||||||
|
use rustical_dav::resource::Resource;
|
||||||
|
use rustical_store::auth::Principal;
|
||||||
|
use rustical_xml::XmlSerializeRoot;
|
||||||
|
use serde_json::from_str;
|
||||||
|
|
||||||
|
// #[tokio::test]
|
||||||
|
async fn test_propfind() {
|
||||||
|
let requests: Vec<_> = include_str!("./test_files/propfind.requests")
|
||||||
|
.trim()
|
||||||
|
.split("\n\n")
|
||||||
|
.collect();
|
||||||
|
let principals: Vec<Principal> =
|
||||||
|
from_str(include_str!("./test_files/propfind.principals.json")).unwrap();
|
||||||
|
let resources: Vec<CalendarResource> =
|
||||||
|
from_str(include_str!("./test_files/propfind.resources.json")).unwrap();
|
||||||
|
let outputs: Vec<_> = include_str!("./test_files/propfind.outputs")
|
||||||
|
.trim()
|
||||||
|
.split("\n\n")
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for principal in principals {
|
||||||
|
for ((request, resource), &expected_output) in requests.iter().zip(&resources).zip(&outputs)
|
||||||
|
{
|
||||||
|
let propfind = CalendarResource::parse_propfind(request).unwrap();
|
||||||
|
|
||||||
|
let response = resource
|
||||||
|
.propfind(
|
||||||
|
&format!("/caldav/principal/{}/{}", principal.id, resource.cal.id),
|
||||||
|
&propfind.prop,
|
||||||
|
propfind.include.as_ref(),
|
||||||
|
&CalDavPrincipalUri("/caldav"),
|
||||||
|
&principal,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let expected_output = expected_output.trim();
|
||||||
|
let output = response
|
||||||
|
.serialize_to_string()
|
||||||
|
.unwrap()
|
||||||
|
.trim()
|
||||||
|
.replace("\r\n", "\n");
|
||||||
|
println!("{output}");
|
||||||
|
println!("{}, {} \n\n\n", output.len(), expected_output.len());
|
||||||
|
assert_eq!(output, expected_output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ use axum::extract::{Path, State};
|
|||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||||
use http::{HeaderMap, StatusCode};
|
use http::{HeaderMap, Method, StatusCode};
|
||||||
use rustical_ical::CalendarObject;
|
use rustical_ical::CalendarObject;
|
||||||
use rustical_store::CalendarStore;
|
use rustical_store::CalendarStore;
|
||||||
use rustical_store::auth::Principal;
|
use rustical_store::auth::Principal;
|
||||||
@@ -22,12 +22,15 @@ pub async fn get_event<C: CalendarStore>(
|
|||||||
}): Path<CalendarObjectPathComponents>,
|
}): Path<CalendarObjectPathComponents>,
|
||||||
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
|
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
|
||||||
user: Principal,
|
user: Principal,
|
||||||
|
method: Method,
|
||||||
) -> Result<Response, Error> {
|
) -> Result<Response, Error> {
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(crate::Error::Unauthorized);
|
return Err(crate::Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
let calendar = cal_store
|
||||||
|
.get_calendar(&principal, &calendar_id, false)
|
||||||
|
.await?;
|
||||||
if !user.is_principal(&calendar.principal) {
|
if !user.is_principal(&calendar.principal) {
|
||||||
return Err(crate::Error::Unauthorized);
|
return Err(crate::Error::Unauthorized);
|
||||||
}
|
}
|
||||||
@@ -40,7 +43,11 @@ pub async fn get_event<C: CalendarStore>(
|
|||||||
let hdrs = resp.headers_mut().unwrap();
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
|
hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
|
||||||
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
|
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
|
||||||
Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap())
|
if matches!(method, Method::HEAD) {
|
||||||
|
Ok(resp.body(Body::empty()).unwrap())
|
||||||
|
} else {
|
||||||
|
Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(cal_store))]
|
#[instrument(skip(cal_store))]
|
||||||
@@ -71,12 +78,13 @@ pub async fn put_event<C: CalendarStore>(
|
|||||||
true
|
true
|
||||||
};
|
};
|
||||||
|
|
||||||
let object = match CalendarObject::from_ics(object_id, body) {
|
let object = match CalendarObject::from_ics(body) {
|
||||||
Ok(obj) => obj,
|
Ok(obj) => obj,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
assert_eq!(object.get_id(), object_id);
|
||||||
cal_store
|
cal_store
|
||||||
.put_object(principal, calendar_id, object, overwrite)
|
.put_object(principal, calendar_id, object, overwrite)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ impl Resource for CalendarObjectResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_displayname(&self) -> Option<&str> {
|
fn get_displayname(&self) -> Option<&str> {
|
||||||
// TODO: Extract summary from object
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,10 +58,11 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
|
|||||||
calendar_id,
|
calendar_id,
|
||||||
object_id,
|
object_id,
|
||||||
}: &Self::PathComponents,
|
}: &Self::PathComponents,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Self::Error> {
|
) -> Result<Self::Resource, Self::Error> {
|
||||||
let object = self
|
let object = self
|
||||||
.cal_store
|
.cal_store
|
||||||
.get_object(principal, calendar_id, object_id, false)
|
.get_object(principal, calendar_id, object_id, show_deleted)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(CalendarObjectResource {
|
Ok(CalendarObjectResource {
|
||||||
object,
|
object,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use axum::response::Redirect;
|
|
||||||
use axum::routing::any;
|
|
||||||
use axum::{Extension, Router};
|
use axum::{Extension, Router};
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
use principal::PrincipalResourceService;
|
use principal::PrincipalResourceService;
|
||||||
@@ -14,7 +12,6 @@ pub mod calendar;
|
|||||||
pub mod calendar_object;
|
pub mod calendar_object;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod principal;
|
pub mod principal;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Constructor)]
|
#[derive(Debug, Clone, Constructor)]
|
||||||
@@ -34,23 +31,18 @@ pub fn caldav_router<AP: AuthenticationProvider, C: CalendarStore, S: Subscripti
|
|||||||
auth_provider: Arc<AP>,
|
auth_provider: Arc<AP>,
|
||||||
store: Arc<C>,
|
store: Arc<C>,
|
||||||
subscription_store: Arc<S>,
|
subscription_store: Arc<S>,
|
||||||
|
simplified_home_set: bool,
|
||||||
) -> Router {
|
) -> Router {
|
||||||
let principal_service = PrincipalResourceService {
|
Router::new().nest(
|
||||||
auth_provider: auth_provider.clone(),
|
prefix,
|
||||||
sub_store: subscription_store.clone(),
|
RootResourceService::<_, Principal, CalDavPrincipalUri>::new(PrincipalResourceService {
|
||||||
cal_store: store.clone(),
|
auth_provider: auth_provider.clone(),
|
||||||
};
|
sub_store: subscription_store.clone(),
|
||||||
|
cal_store: store.clone(),
|
||||||
Router::new()
|
simplified_home_set,
|
||||||
.nest(
|
})
|
||||||
prefix,
|
.axum_router()
|
||||||
RootResourceService::<_, Principal, CalDavPrincipalUri>::new(principal_service.clone())
|
.layer(AuthenticationLayer::new(auth_provider))
|
||||||
.axum_router()
|
.layer(Extension(CalDavPrincipalUri(prefix))),
|
||||||
.layer(AuthenticationLayer::new(auth_provider))
|
)
|
||||||
.layer(Extension(CalDavPrincipalUri(prefix))),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/.well-known/caldav",
|
|
||||||
any(async || Redirect::permanent(prefix)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ mod service;
|
|||||||
pub use service::*;
|
pub use service::*;
|
||||||
mod prop;
|
mod prop;
|
||||||
pub use prop::*;
|
pub use prop::*;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PrincipalResource {
|
pub struct PrincipalResource {
|
||||||
principal: Principal,
|
principal: Principal,
|
||||||
members: Vec<String>,
|
members: Vec<String>,
|
||||||
|
// If true only return the principal as the calendar home set, otherwise also groups
|
||||||
|
simplified_home_set: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceName for PrincipalResource {
|
impl ResourceName for PrincipalResource {
|
||||||
@@ -37,11 +41,6 @@ impl Resource for PrincipalResource {
|
|||||||
Resourcetype(&[
|
Resourcetype(&[
|
||||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
|
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
|
||||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"),
|
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"),
|
||||||
// https://github.com/apple/ccs-calendarserver/blob/13c706b985fb728b9aab42dc0fef85aae21921c3/doc/Extensions/caldav-proxy.txt
|
|
||||||
// ResourcetypeInner(
|
|
||||||
// Some(rustical_dav::namespace::NS_CALENDARSERVER),
|
|
||||||
// "calendar-proxy-write",
|
|
||||||
// ),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +61,17 @@ impl Resource for PrincipalResource {
|
|||||||
PrincipalPropName::PrincipalUrl => {
|
PrincipalPropName::PrincipalUrl => {
|
||||||
PrincipalProp::PrincipalUrl(principal_url.into())
|
PrincipalProp::PrincipalUrl(principal_url.into())
|
||||||
}
|
}
|
||||||
PrincipalPropName::CalendarHomeSet => {
|
PrincipalPropName::CalendarHomeSet => PrincipalProp::CalendarHomeSet(
|
||||||
PrincipalProp::CalendarHomeSet(principal_url.into())
|
CalendarHomeSet(if self.simplified_home_set {
|
||||||
}
|
vec![principal_url.into()]
|
||||||
|
} else {
|
||||||
|
self.principal
|
||||||
|
.memberships()
|
||||||
|
.iter()
|
||||||
|
.map(|principal| puri.principal_uri(principal).into())
|
||||||
|
.collect()
|
||||||
|
}),
|
||||||
|
),
|
||||||
PrincipalPropName::CalendarUserAddressSet => {
|
PrincipalPropName::CalendarUserAddressSet => {
|
||||||
PrincipalProp::CalendarUserAddressSet(principal_url.into())
|
PrincipalProp::CalendarUserAddressSet(principal_url.into())
|
||||||
}
|
}
|
||||||
@@ -114,7 +121,7 @@ impl Resource for PrincipalResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
|
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
|
||||||
Ok(UserPrivilegeSet::owner_read(
|
Ok(UserPrivilegeSet::owner_only(
|
||||||
user.is_principal(&self.principal.id),
|
user.is_principal(&self.principal.id),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,12 @@ pub enum PrincipalProp {
|
|||||||
|
|
||||||
// CalDAV (RFC 4791)
|
// CalDAV (RFC 4791)
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
CalendarHomeSet(HrefElement),
|
CalendarHomeSet(CalendarHomeSet),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
||||||
|
pub struct CalendarHomeSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||||
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
||||||
pub enum PrincipalPropWrapper {
|
pub enum PrincipalPropWrapper {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ pub struct PrincipalResourceService<
|
|||||||
pub(crate) auth_provider: Arc<AP>,
|
pub(crate) auth_provider: Arc<AP>,
|
||||||
pub(crate) sub_store: Arc<S>,
|
pub(crate) sub_store: Arc<S>,
|
||||||
pub(crate) cal_store: Arc<CS>,
|
pub(crate) cal_store: Arc<CS>,
|
||||||
|
// If true only return the principal as the calendar home set, otherwise also groups
|
||||||
|
pub(crate) simplified_home_set: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Clone
|
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Clone
|
||||||
@@ -28,6 +30,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Clone
|
|||||||
auth_provider: self.auth_provider.clone(),
|
auth_provider: self.auth_provider.clone(),
|
||||||
sub_store: self.sub_store.clone(),
|
sub_store: self.sub_store.clone(),
|
||||||
cal_store: self.cal_store.clone(),
|
cal_store: self.cal_store.clone(),
|
||||||
|
simplified_home_set: self.simplified_home_set,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,11 +46,12 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
|
|||||||
type Principal = Principal;
|
type Principal = Principal;
|
||||||
type PrincipalUri = CalDavPrincipalUri;
|
type PrincipalUri = CalDavPrincipalUri;
|
||||||
|
|
||||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy";
|
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
|
||||||
|
|
||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
(principal,): &Self::PathComponents,
|
(principal,): &Self::PathComponents,
|
||||||
|
_show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Self::Error> {
|
) -> Result<Self::Resource, Self::Error> {
|
||||||
let user = self
|
let user = self
|
||||||
.auth_provider
|
.auth_provider
|
||||||
@@ -57,6 +61,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
|
|||||||
Ok(PrincipalResource {
|
Ok(PrincipalResource {
|
||||||
members: self.auth_provider.list_members(&user.id).await?,
|
members: self.auth_provider.list_members(&user.id).await?,
|
||||||
principal: user,
|
principal: user,
|
||||||
|
simplified_home_set: self.simplified_home_set,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
83
crates/caldav/src/principal/tests.rs
Normal file
83
crates/caldav/src/principal/tests.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
CalDavPrincipalUri,
|
||||||
|
principal::{PrincipalResource, PrincipalResourceService},
|
||||||
|
};
|
||||||
|
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_xml::XmlSerializeRoot;
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
) {
|
||||||
|
let service = PrincipalResourceService {
|
||||||
|
cal_store: Arc::new(cal_store.await),
|
||||||
|
sub_store: Arc::new(sub_store.await),
|
||||||
|
auth_provider: Arc::new(auth_provider.await),
|
||||||
|
simplified_home_set: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
service
|
||||||
|
.get_resource(&("invalid-user".to_owned(),), true)
|
||||||
|
.await,
|
||||||
|
Err(crate::Error::NotFound)
|
||||||
|
));
|
||||||
|
|
||||||
|
let _principal_resource = service
|
||||||
|
.get_resource(&("user".to_owned(),), true)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_propfind() {
|
||||||
|
let propfind = PrincipalResource::parse_propfind(
|
||||||
|
r#"<?xml version="1.0" encoding="UTF-8"?><propfind xmlns="DAV:"><allprop/></propfind>"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let principal = Principal {
|
||||||
|
id: "user".to_string(),
|
||||||
|
displayname: None,
|
||||||
|
principal_type: Individual,
|
||||||
|
password: None,
|
||||||
|
memberships: vec!["group".to_string()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let resource = PrincipalResource {
|
||||||
|
principal: principal.clone(),
|
||||||
|
members: vec![],
|
||||||
|
simplified_home_set: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = resource
|
||||||
|
.propfind(
|
||||||
|
&format!("/caldav/principal/{}", principal.id),
|
||||||
|
&propfind.prop,
|
||||||
|
propfind.include.as_ref(),
|
||||||
|
&CalDavPrincipalUri("/caldav"),
|
||||||
|
&principal,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let output = response.serialize_to_string().unwrap();
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use axum::extract::{Path, State};
|
|||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||||
|
use http::Method;
|
||||||
use http::{HeaderMap, StatusCode};
|
use http::{HeaderMap, StatusCode};
|
||||||
use rustical_dav::privileges::UserPrivilege;
|
use rustical_dav::privileges::UserPrivilege;
|
||||||
use rustical_dav::resource::Resource;
|
use rustical_dav::resource::Resource;
|
||||||
@@ -25,6 +26,7 @@ pub async fn get_object<AS: AddressbookStore>(
|
|||||||
}): Path<AddressObjectPathComponents>,
|
}): Path<AddressObjectPathComponents>,
|
||||||
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
|
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
|
||||||
user: Principal,
|
user: Principal,
|
||||||
|
method: Method,
|
||||||
) -> Result<Response, Error> {
|
) -> Result<Response, Error> {
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
@@ -49,7 +51,11 @@ pub async fn get_object<AS: AddressbookStore>(
|
|||||||
let hdrs = resp.headers_mut().unwrap();
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
hdrs.typed_insert(ETag::from_str(&object.get_etag()).unwrap());
|
hdrs.typed_insert(ETag::from_str(&object.get_etag()).unwrap());
|
||||||
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
|
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
|
||||||
Ok(resp.body(Body::new(object.get_vcf().to_owned())).unwrap())
|
if matches!(method, Method::HEAD) {
|
||||||
|
Ok(resp.body(Body::empty()).unwrap())
|
||||||
|
} else {
|
||||||
|
Ok(resp.body(Body::new(object.get_vcf().to_owned())).unwrap())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(addr_store, body))]
|
#[instrument(skip(addr_store, body))]
|
||||||
|
|||||||
@@ -49,10 +49,11 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
|
|||||||
addressbook_id,
|
addressbook_id,
|
||||||
object_id,
|
object_id,
|
||||||
}: &Self::PathComponents,
|
}: &Self::PathComponents,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Self::Error> {
|
) -> Result<Self::Resource, Self::Error> {
|
||||||
let object = self
|
let object = self
|
||||||
.addr_store
|
.addr_store
|
||||||
.get_object(principal, addressbook_id, object_id, false)
|
.get_object(principal, addressbook_id, object_id, show_deleted)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(AddressObjectResource {
|
Ok(AddressObjectResource {
|
||||||
object,
|
object,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use axum::body::Body;
|
|||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use axum_extra::headers::{ContentType, HeaderMapExt};
|
use axum_extra::headers::{ContentType, HeaderMapExt};
|
||||||
use http::{HeaderValue, StatusCode, header};
|
use http::{HeaderValue, Method, StatusCode, header};
|
||||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||||
use rustical_dav::privileges::UserPrivilege;
|
use rustical_dav::privileges::UserPrivilege;
|
||||||
use rustical_dav::resource::Resource;
|
use rustical_dav::resource::Resource;
|
||||||
@@ -20,6 +20,7 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
|
|||||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||||
user: Principal,
|
user: Principal,
|
||||||
|
method: Method,
|
||||||
) -> Result<Response, Error> {
|
) -> Result<Response, Error> {
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
@@ -46,7 +47,7 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
|
|||||||
let mut resp = Response::builder().status(StatusCode::OK);
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
let hdrs = resp.headers_mut().unwrap();
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
|
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
|
||||||
let filename = format!("{}_{}.vcf", principal, addressbook_id);
|
let filename = format!("{principal}_{addressbook_id}.vcf");
|
||||||
let filename = utf8_percent_encode(&filename, CONTROLS);
|
let filename = utf8_percent_encode(&filename, CONTROLS);
|
||||||
hdrs.insert(
|
hdrs.insert(
|
||||||
header::CONTENT_DISPOSITION,
|
header::CONTENT_DISPOSITION,
|
||||||
@@ -55,5 +56,9 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
|
|||||||
))
|
))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
Ok(resp.body(Body::new(vcf)).unwrap())
|
if matches!(method, Method::HEAD) {
|
||||||
|
Ok(resp.body(Body::empty()).unwrap())
|
||||||
|
} else {
|
||||||
|
Ok(resp.body(Body::new(vcf)).unwrap())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
crates/carddav/src/addressbook/methods/import.rs
Normal file
67
crates/carddav/src/addressbook/methods/import.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use std::io::BufReader;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
use crate::addressbook::AddressbookResourceService;
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use http::StatusCode;
|
||||||
|
use ical::{
|
||||||
|
parser::{Component, ComponentMut, vcard},
|
||||||
|
property::Property,
|
||||||
|
};
|
||||||
|
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::Principal};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
#[instrument(skip(resource_service))]
|
||||||
|
pub async fn route_import<AS: AddressbookStore, S: SubscriptionStore>(
|
||||||
|
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||||
|
user: Principal,
|
||||||
|
State(resource_service): State<AddressbookResourceService<AS, S>>,
|
||||||
|
body: String,
|
||||||
|
) -> Result<Response, Error> {
|
||||||
|
if !user.is_principal(&principal) {
|
||||||
|
return Err(Error::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parser = vcard::VcardParser::new(BufReader::new(body.as_bytes()));
|
||||||
|
|
||||||
|
let mut objects = vec![];
|
||||||
|
for res in parser {
|
||||||
|
let mut card = res.unwrap();
|
||||||
|
let uid = card.get_uid();
|
||||||
|
if uid.is_none() {
|
||||||
|
let mut card_mut = card.mutable();
|
||||||
|
card_mut.set_property(Property {
|
||||||
|
name: "UID".to_owned(),
|
||||||
|
value: Some(uuid::Uuid::new_v4().to_string()),
|
||||||
|
params: None,
|
||||||
|
});
|
||||||
|
card = card_mut.verify().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
objects.push(card.try_into().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
if objects.is_empty() {
|
||||||
|
return Ok((StatusCode::BAD_REQUEST, "empty addressbook data").into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let addressbook = Addressbook {
|
||||||
|
principal,
|
||||||
|
id: addressbook_id,
|
||||||
|
displayname: None,
|
||||||
|
description: None,
|
||||||
|
deleted_at: None,
|
||||||
|
synctoken: 0,
|
||||||
|
push_topic: uuid::Uuid::new_v4().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let addr_store = resource_service.addr_store;
|
||||||
|
addr_store
|
||||||
|
.import_addressbook(addressbook, objects, false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(StatusCode::OK.into_response())
|
||||||
|
}
|
||||||
@@ -88,15 +88,8 @@ pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match addr_store.insert_addressbook(addressbook).await {
|
addr_store.insert_addressbook(addressbook).await?;
|
||||||
// TODO: The spec says we should return a mkcol-response.
|
Ok(StatusCode::CREATED.into_response())
|
||||||
// However, it works without one but breaks on iPadOS when using an empty one :)
|
|
||||||
Ok(()) => Ok(StatusCode::CREATED.into_response()),
|
|
||||||
Err(err) => {
|
|
||||||
dbg!(err.to_string());
|
|
||||||
Err(err.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pub mod get;
|
pub mod get;
|
||||||
|
pub mod import;
|
||||||
pub mod mkcol;
|
pub mod mkcol;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod put;
|
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
use crate::Error;
|
|
||||||
use crate::addressbook::AddressbookResourceService;
|
|
||||||
use axum::response::IntoResponse;
|
|
||||||
use axum::{
|
|
||||||
extract::{Path, State},
|
|
||||||
response::Response,
|
|
||||||
};
|
|
||||||
use http::StatusCode;
|
|
||||||
use ical::VcardParser;
|
|
||||||
use rustical_ical::AddressObject;
|
|
||||||
use rustical_store::Addressbook;
|
|
||||||
use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal};
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
#[instrument(skip(addr_store))]
|
|
||||||
pub async fn route_put<AS: AddressbookStore, S: SubscriptionStore>(
|
|
||||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
|
||||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
|
||||||
user: Principal,
|
|
||||||
body: String,
|
|
||||||
) -> Result<Response, Error> {
|
|
||||||
if !user.is_principal(&principal) {
|
|
||||||
return Err(Error::Unauthorized);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut objects = vec![];
|
|
||||||
for object in VcardParser::new(body.as_bytes()) {
|
|
||||||
let object = object.map_err(rustical_ical::Error::from)?;
|
|
||||||
objects.push(AddressObject::try_from(object)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let addressbook = Addressbook {
|
|
||||||
id: addressbook_id.clone(),
|
|
||||||
principal: principal.clone(),
|
|
||||||
displayname: None,
|
|
||||||
description: None,
|
|
||||||
deleted_at: None,
|
|
||||||
synctoken: Default::default(),
|
|
||||||
push_topic: uuid::Uuid::new_v4().to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
addr_store
|
|
||||||
.import_addressbook(principal.clone(), addressbook, objects)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(StatusCode::CREATED.into_response())
|
|
||||||
}
|
|
||||||
@@ -58,6 +58,7 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
|||||||
Ok((result, not_found))
|
Ok((result, not_found))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
|
pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
|
||||||
addr_multiget: &AddressbookMultigetRequest,
|
addr_multiget: &AddressbookMultigetRequest,
|
||||||
prop: &PropfindType<AddressObjectPropWrapperName>,
|
prop: &PropfindType<AddressObjectPropWrapperName>,
|
||||||
@@ -80,7 +81,7 @@ pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
|
|||||||
object,
|
object,
|
||||||
principal: principal.to_owned(),
|
principal: principal.to_owned(),
|
||||||
}
|
}
|
||||||
.propfind(&path, prop, puri, user)?,
|
.propfind(&path, prop, None, puri, user)?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ pub async fn handle_sync_collection<AS: AddressbookStore>(
|
|||||||
object,
|
object,
|
||||||
principal: principal.to_owned(),
|
principal: principal.to_owned(),
|
||||||
}
|
}
|
||||||
.propfind(&path, &sync_collection.prop, puri, user)?,
|
.propfind(&path, &sync_collection.prop, None, puri, user)?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use super::methods::report::route_report_addressbook;
|
|||||||
use crate::address_object::AddressObjectResourceService;
|
use crate::address_object::AddressObjectResourceService;
|
||||||
use crate::address_object::resource::AddressObjectResource;
|
use crate::address_object::resource::AddressObjectResource;
|
||||||
use crate::addressbook::methods::get::route_get;
|
use crate::addressbook::methods::get::route_get;
|
||||||
|
use crate::addressbook::methods::import::route_import;
|
||||||
use crate::addressbook::methods::post::route_post;
|
use crate::addressbook::methods::post::route_post;
|
||||||
use crate::addressbook::methods::put::route_put;
|
|
||||||
use crate::addressbook::resource::AddressbookResource;
|
use crate::addressbook::resource::AddressbookResource;
|
||||||
use crate::{CardDavPrincipalUri, Error};
|
use crate::{CardDavPrincipalUri, Error};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -59,10 +59,11 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
|
|||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
(principal, addressbook_id): &Self::PathComponents,
|
(principal, addressbook_id): &Self::PathComponents,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Error> {
|
) -> Result<Self::Resource, Error> {
|
||||||
let addressbook = self
|
let addressbook = self
|
||||||
.addr_store
|
.addr_store
|
||||||
.get_addressbook(principal, addressbook_id, false)
|
.get_addressbook(principal, addressbook_id, show_deleted)
|
||||||
.await
|
.await
|
||||||
.map_err(|_e| Error::NotFound)?;
|
.map_err(|_e| Error::NotFound)?;
|
||||||
Ok(addressbook.into())
|
Ok(addressbook.into())
|
||||||
@@ -138,9 +139,9 @@ impl<AS: AddressbookStore, S: SubscriptionStore> AxumMethods for AddressbookReso
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
fn import() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
Some(|state, req| {
|
Some(|state, req| {
|
||||||
let mut service = Handler::with_state(route_put::<AS, S>, state);
|
let mut service = Handler::with_state(route_import::<AS, S>, state);
|
||||||
Box::pin(Service::call(&mut service, req))
|
Box::pin(Service::call(&mut service, req))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,13 @@ impl Resource for PrincipalResource {
|
|||||||
PrincipalPropWrapper::Principal(match prop {
|
PrincipalPropWrapper::Principal(match prop {
|
||||||
PrincipalPropName::PrincipalUrl => PrincipalProp::PrincipalUrl(principal_href),
|
PrincipalPropName::PrincipalUrl => PrincipalProp::PrincipalUrl(principal_href),
|
||||||
PrincipalPropName::AddressbookHomeSet => {
|
PrincipalPropName::AddressbookHomeSet => {
|
||||||
PrincipalProp::AddressbookHomeSet(principal_href)
|
PrincipalProp::AddressbookHomeSet(AddressbookHomeSet(
|
||||||
|
self.principal
|
||||||
|
.memberships()
|
||||||
|
.iter()
|
||||||
|
.map(|principal| puri.principal_uri(principal).into())
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
PrincipalPropName::PrincipalAddress => PrincipalProp::PrincipalAddress(None),
|
PrincipalPropName::PrincipalAddress => PrincipalProp::PrincipalAddress(None),
|
||||||
PrincipalPropName::GroupMembership => {
|
PrincipalPropName::GroupMembership => {
|
||||||
|
|||||||
@@ -22,11 +22,14 @@ pub enum PrincipalProp {
|
|||||||
|
|
||||||
// CardDAV (RFC 6352)
|
// CardDAV (RFC 6352)
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
AddressbookHomeSet(HrefElement),
|
AddressbookHomeSet(AddressbookHomeSet),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
PrincipalAddress(Option<HrefElement>),
|
PrincipalAddress(Option<HrefElement>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
||||||
|
pub struct AddressbookHomeSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||||
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
||||||
pub enum PrincipalPropWrapper {
|
pub enum PrincipalPropWrapper {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
|
|||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
(principal,): &Self::PathComponents,
|
(principal,): &Self::PathComponents,
|
||||||
|
_show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Self::Error> {
|
) -> Result<Self::Resource, Self::Error> {
|
||||||
let user = self
|
let user = self
|
||||||
.auth_provider
|
.auth_provider
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
use quick_xml::name::Namespace;
|
use quick_xml::name::Namespace;
|
||||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq)]
|
// https://datatracker.ietf.org/doc/html/rfc3744
|
||||||
|
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||||
pub enum UserPrivilege {
|
pub enum UserPrivilege {
|
||||||
Read,
|
Read,
|
||||||
Write,
|
Write,
|
||||||
@@ -15,12 +17,12 @@ pub enum UserPrivilege {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl XmlSerialize for UserPrivilegeSet {
|
impl XmlSerialize for UserPrivilegeSet {
|
||||||
fn serialize<W: std::io::Write>(
|
fn serialize(
|
||||||
&self,
|
&self,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
#[derive(XmlSerialize)]
|
#[derive(XmlSerialize)]
|
||||||
pub struct FakeUserPrivilegeSet {
|
pub struct FakeUserPrivilegeSet {
|
||||||
@@ -29,12 +31,11 @@ impl XmlSerialize for UserPrivilegeSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FakeUserPrivilegeSet {
|
FakeUserPrivilegeSet {
|
||||||
privileges: self.privileges.iter().cloned().collect(),
|
privileges: self.privileges.iter().cloned().sorted().collect(),
|
||||||
}
|
}
|
||||||
.serialize(ns, tag, namespaces, writer)
|
.serialize(ns, tag, namespaces, writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(refining_impl_trait)]
|
|
||||||
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -47,6 +48,12 @@ pub struct UserPrivilegeSet {
|
|||||||
|
|
||||||
impl UserPrivilegeSet {
|
impl UserPrivilegeSet {
|
||||||
pub fn has(&self, privilege: &UserPrivilege) -> bool {
|
pub fn has(&self, privilege: &UserPrivilege) -> bool {
|
||||||
|
if (privilege == &UserPrivilege::WriteProperties
|
||||||
|
|| privilege == &UserPrivilege::WriteContent)
|
||||||
|
&& self.privileges.contains(&UserPrivilege::Write)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All)
|
self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +79,15 @@ impl UserPrivilegeSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn owner_write_properties(is_owner: bool) -> Self {
|
||||||
|
// Content is read-only but we can write properties
|
||||||
|
if is_owner {
|
||||||
|
Self::write_properties()
|
||||||
|
} else {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_only() -> Self {
|
pub fn read_only() -> Self {
|
||||||
Self {
|
Self {
|
||||||
privileges: HashSet::from([
|
privileges: HashSet::from([
|
||||||
@@ -81,6 +97,17 @@ impl UserPrivilegeSet {
|
|||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn write_properties() -> Self {
|
||||||
|
Self {
|
||||||
|
privileges: HashSet::from([
|
||||||
|
UserPrivilege::Read,
|
||||||
|
UserPrivilege::WriteProperties,
|
||||||
|
UserPrivilege::ReadAcl,
|
||||||
|
UserPrivilege::ReadCurrentUserPrivilegeSet,
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> From<[UserPrivilege; N]> for UserPrivilegeSet {
|
impl<const N: usize> From<[UserPrivilege; N]> for UserPrivilegeSet {
|
||||||
|
|||||||
@@ -18,11 +18,6 @@ pub trait AxumMethods: Sized + Send + Sync + 'static {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn head() -> Option<MethodFunction<Self>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn post() -> Option<MethodFunction<Self>> {
|
fn post() -> Option<MethodFunction<Self>> {
|
||||||
None
|
None
|
||||||
@@ -43,6 +38,11 @@ pub trait AxumMethods: Sized + Send + Sync + 'static {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn import() -> Option<MethodFunction<Self>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn allow_header() -> Allow {
|
fn allow_header() -> Allow {
|
||||||
let mut allow = vec![
|
let mut allow = vec![
|
||||||
@@ -58,8 +58,6 @@ pub trait AxumMethods: Sized + Send + Sync + 'static {
|
|||||||
}
|
}
|
||||||
if Self::get().is_some() {
|
if Self::get().is_some() {
|
||||||
allow.push(Method::GET);
|
allow.push(Method::GET);
|
||||||
}
|
|
||||||
if Self::head().is_some() {
|
|
||||||
allow.push(Method::HEAD);
|
allow.push(Method::HEAD);
|
||||||
}
|
}
|
||||||
if Self::post().is_some() {
|
if Self::post().is_some() {
|
||||||
@@ -74,6 +72,9 @@ pub trait AxumMethods: Sized + Send + Sync + 'static {
|
|||||||
if Self::put().is_some() {
|
if Self::put().is_some() {
|
||||||
allow.push(Method::PUT);
|
allow.push(Method::PUT);
|
||||||
}
|
}
|
||||||
|
if Self::import().is_some() {
|
||||||
|
allow.push(Method::from_str("IMPORT").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
allow.into_iter().collect()
|
allow.into_iter().collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,16 +72,11 @@ where
|
|||||||
return svc(self.resource_service.clone(), req);
|
return svc(self.resource_service.clone(), req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"GET" => {
|
"GET" | "HEAD" => {
|
||||||
if let Some(svc) = RS::get() {
|
if let Some(svc) = RS::get() {
|
||||||
return svc(self.resource_service.clone(), req);
|
return svc(self.resource_service.clone(), req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"HEAD" => {
|
|
||||||
if let Some(svc) = RS::head() {
|
|
||||||
return svc(self.resource_service.clone(), req);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"POST" => {
|
"POST" => {
|
||||||
if let Some(svc) = RS::post() {
|
if let Some(svc) = RS::post() {
|
||||||
return svc(self.resource_service.clone(), req);
|
return svc(self.resource_service.clone(), req);
|
||||||
@@ -102,6 +97,11 @@ where
|
|||||||
return svc(self.resource_service.clone(), req);
|
return svc(self.resource_service.clone(), req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"IMPORT" => {
|
||||||
|
if let Some(svc) = RS::import() {
|
||||||
|
return svc(self.resource_service.clone(), req);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
|||||||
@@ -45,10 +45,11 @@ pub async fn route_delete<R: ResourceService>(
|
|||||||
if_match: Option<IfMatch>,
|
if_match: Option<IfMatch>,
|
||||||
if_none_match: Option<IfNoneMatch>,
|
if_none_match: Option<IfNoneMatch>,
|
||||||
) -> Result<(), R::Error> {
|
) -> Result<(), R::Error> {
|
||||||
let resource = resource_service.get_resource(path_components).await?;
|
let resource = resource_service.get_resource(path_components, true).await?;
|
||||||
|
|
||||||
|
// Kind of a bodge since we don't get unbind from the parent
|
||||||
let privileges = resource.get_user_privileges(principal)?;
|
let privileges = resource.get_user_privileges(principal)?;
|
||||||
if !privileges.has(&UserPrivilege::Write) {
|
if !privileges.has(&UserPrivilege::WriteProperties) {
|
||||||
return Err(Error::Unauthorized.into());
|
return Err(Error::Unauthorized.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ use crate::resource::Resource;
|
|||||||
use crate::resource::ResourceName;
|
use crate::resource::ResourceName;
|
||||||
use crate::resource::ResourceService;
|
use crate::resource::ResourceService;
|
||||||
use crate::xml::MultistatusElement;
|
use crate::xml::MultistatusElement;
|
||||||
use crate::xml::PropfindElement;
|
|
||||||
use crate::xml::PropfindType;
|
|
||||||
use axum::extract::{Extension, OriginalUri, Path, State};
|
use axum::extract::{Extension, OriginalUri, Path, State};
|
||||||
use rustical_xml::PropName;
|
|
||||||
use rustical_xml::XmlDocument;
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
type RSMultistatus<R> = MultistatusElement<
|
type RSMultistatus<R> = MultistatusElement<
|
||||||
@@ -49,43 +45,39 @@ pub(crate) async fn route_propfind<R: ResourceService>(
|
|||||||
resource_service: &R,
|
resource_service: &R,
|
||||||
puri: &impl PrincipalUri,
|
puri: &impl PrincipalUri,
|
||||||
) -> Result<RSMultistatus<R>, R::Error> {
|
) -> Result<RSMultistatus<R>, R::Error> {
|
||||||
let resource = resource_service.get_resource(path_components).await?;
|
let resource = resource_service
|
||||||
|
.get_resource(path_components, false)
|
||||||
|
.await?;
|
||||||
let privileges = resource.get_user_privileges(principal)?;
|
let privileges = resource.get_user_privileges(principal)?;
|
||||||
if !privileges.has(&UserPrivilege::Read) {
|
if !privileges.has(&UserPrivilege::Read) {
|
||||||
return Err(Error::Unauthorized.into());
|
return Err(Error::Unauthorized.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// A request body is optional. If empty we MUST return all props
|
// A request body is optional. If empty we MUST return all props
|
||||||
let propfind_self: PropfindElement<<<R::Resource as Resource>::Prop as PropName>::Names> =
|
let propfind_self = R::Resource::parse_propfind(body).map_err(Error::XmlError)?;
|
||||||
if !body.is_empty() {
|
let propfind_member = R::MemberType::parse_propfind(body).map_err(Error::XmlError)?;
|
||||||
PropfindElement::parse_str(body).map_err(Error::XmlError)?
|
|
||||||
} else {
|
|
||||||
PropfindElement {
|
|
||||||
prop: PropfindType::Allprop,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let propfind_member: PropfindElement<<<R::MemberType as Resource>::Prop as PropName>::Names> =
|
|
||||||
if !body.is_empty() {
|
|
||||||
PropfindElement::parse_str(body).map_err(Error::XmlError)?
|
|
||||||
} else {
|
|
||||||
PropfindElement {
|
|
||||||
prop: PropfindType::Allprop,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut member_responses = Vec::new();
|
let mut member_responses = Vec::new();
|
||||||
if depth != &Depth::Zero {
|
if depth != &Depth::Zero {
|
||||||
|
// TODO: authorization check for member resources
|
||||||
for member in resource_service.get_members(path_components).await? {
|
for member in resource_service.get_members(path_components).await? {
|
||||||
member_responses.push(member.propfind(
|
member_responses.push(member.propfind(
|
||||||
&format!("{}/{}", path.trim_end_matches('/'), member.get_name()),
|
&format!("{}/{}", path.trim_end_matches('/'), member.get_name()),
|
||||||
&propfind_member.prop,
|
&propfind_member.prop,
|
||||||
|
propfind_member.include.as_ref(),
|
||||||
puri,
|
puri,
|
||||||
principal,
|
principal,
|
||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = resource.propfind(path, &propfind_self.prop, puri, principal)?;
|
let response = resource.propfind(
|
||||||
|
path,
|
||||||
|
&propfind_self.prop,
|
||||||
|
propfind_self.include.as_ref(),
|
||||||
|
puri,
|
||||||
|
principal,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(MultistatusElement {
|
Ok(MultistatusElement {
|
||||||
responses: vec![response],
|
responses: vec![response],
|
||||||
|
|||||||
@@ -26,21 +26,21 @@ enum SetPropertyPropWrapper<T: XmlDeserialize> {
|
|||||||
// We are <prop>
|
// We are <prop>
|
||||||
#[derive(XmlDeserialize, Clone, Debug)]
|
#[derive(XmlDeserialize, Clone, Debug)]
|
||||||
struct SetPropertyPropWrapperWrapper<T: XmlDeserialize>(
|
struct SetPropertyPropWrapperWrapper<T: XmlDeserialize>(
|
||||||
#[xml(ty = "untagged")] SetPropertyPropWrapper<T>,
|
#[xml(ty = "untagged", flatten)] Vec<SetPropertyPropWrapper<T>>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// We are <set>
|
// We are <set>
|
||||||
#[derive(XmlDeserialize, Clone, Debug)]
|
#[derive(XmlDeserialize, Clone, Debug)]
|
||||||
struct SetPropertyElement<T: XmlDeserialize> {
|
struct SetPropertyElement<T: XmlDeserialize> {
|
||||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||||
prop: T,
|
prop: SetPropertyPropWrapperWrapper<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug)]
|
#[derive(XmlDeserialize, Clone, Debug)]
|
||||||
struct TagName(#[xml(ty = "tag_name")] String);
|
struct TagName(#[xml(ty = "tag_name")] String);
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug)]
|
#[derive(XmlDeserialize, Clone, Debug)]
|
||||||
struct PropertyElement(#[xml(ty = "untagged")] TagName);
|
struct PropertyElement(#[xml(ty = "untagged", flatten)] Vec<TagName>);
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug)]
|
#[derive(XmlDeserialize, Clone, Debug)]
|
||||||
struct RemovePropertyElement {
|
struct RemovePropertyElement {
|
||||||
@@ -81,11 +81,12 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
|
|||||||
let href = path.to_owned();
|
let href = path.to_owned();
|
||||||
|
|
||||||
// Extract operations
|
// Extract operations
|
||||||
let PropertyupdateElement::<SetPropertyPropWrapperWrapper<<R::Resource as Resource>::Prop>>(
|
let PropertyupdateElement::<<R::Resource as Resource>::Prop>(operations) =
|
||||||
operations,
|
XmlDocument::parse_str(body).map_err(Error::XmlError)?;
|
||||||
) = XmlDocument::parse_str(body).map_err(Error::XmlError)?;
|
|
||||||
|
|
||||||
let mut resource = resource_service.get_resource(path_components).await?;
|
let mut resource = resource_service
|
||||||
|
.get_resource(path_components, false)
|
||||||
|
.await?;
|
||||||
let privileges = resource.get_user_privileges(principal)?;
|
let privileges = resource.get_user_privileges(principal)?;
|
||||||
if !privileges.has(&UserPrivilege::Write) {
|
if !privileges.has(&UserPrivilege::Write) {
|
||||||
return Err(Error::Unauthorized.into());
|
return Err(Error::Unauthorized.into());
|
||||||
@@ -98,59 +99,63 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
|
|||||||
for operation in operations.into_iter() {
|
for operation in operations.into_iter() {
|
||||||
match operation {
|
match operation {
|
||||||
Operation::Set(SetPropertyElement {
|
Operation::Set(SetPropertyElement {
|
||||||
prop: SetPropertyPropWrapperWrapper(property),
|
prop: SetPropertyPropWrapperWrapper(properties),
|
||||||
}) => {
|
}) => {
|
||||||
match property {
|
for property in properties {
|
||||||
SetPropertyPropWrapper::Valid(prop) => {
|
match property {
|
||||||
let propname: <<R::Resource as Resource>::Prop as PropName>::Names =
|
SetPropertyPropWrapper::Valid(prop) => {
|
||||||
prop.clone().into();
|
let propname: <<R::Resource as Resource>::Prop as PropName>::Names =
|
||||||
let (ns, propname): (Option<Namespace>, &str) = propname.into();
|
prop.clone().into();
|
||||||
match resource.set_prop(prop) {
|
let (ns, propname): (Option<Namespace>, &str) = propname.into();
|
||||||
Ok(()) => {
|
match resource.set_prop(prop) {
|
||||||
props_ok.push((ns.map(NamespaceOwned::from), propname.to_owned()))
|
Ok(()) => props_ok
|
||||||
}
|
.push((ns.map(NamespaceOwned::from), propname.to_owned())),
|
||||||
Err(Error::PropReadOnly) => props_conflict
|
Err(Error::PropReadOnly) => props_conflict
|
||||||
.push((ns.map(NamespaceOwned::from), propname.to_owned())),
|
.push((ns.map(NamespaceOwned::from), propname.to_owned())),
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
SetPropertyPropWrapper::Invalid(invalid) => {
|
SetPropertyPropWrapper::Invalid(invalid) => {
|
||||||
let propname = invalid.tag_name();
|
let propname = invalid.tag_name();
|
||||||
|
|
||||||
if let Some(full_propname) = <R::Resource as Resource>::list_props()
|
if let Some(full_propname) = <R::Resource as Resource>::list_props()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|(ns, tag)| {
|
.find_map(|(ns, tag)| {
|
||||||
if tag == propname.as_str() {
|
if tag == propname.as_str() {
|
||||||
Some((ns.map(NamespaceOwned::from), tag.to_owned()))
|
Some((ns.map(NamespaceOwned::from), tag.to_owned()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
// This happens in following cases:
|
// This happens in following cases:
|
||||||
// - read-only properties with #[serde(skip_deserializing)]
|
// - read-only properties with #[serde(skip_deserializing)]
|
||||||
// - internal properties
|
// - internal properties
|
||||||
props_conflict.push(full_propname)
|
props_conflict.push(full_propname)
|
||||||
} else {
|
} else {
|
||||||
props_not_found.push((None, propname));
|
props_not_found.push((None, propname));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Operation::Remove(remove_el) => {
|
Operation::Remove(remove_el) => {
|
||||||
let propname = remove_el.prop.0.0;
|
for tagname in remove_el.prop.0 {
|
||||||
match <<R::Resource as Resource>::Prop as PropName>::Names::from_str(&propname) {
|
let propname = tagname.0;
|
||||||
Ok(prop) => match resource.remove_prop(&prop) {
|
match <<R::Resource as Resource>::Prop as PropName>::Names::from_str(&propname)
|
||||||
Ok(()) => props_ok.push((None, propname)),
|
{
|
||||||
Err(Error::PropReadOnly) => props_conflict.push({
|
Ok(prop) => match resource.remove_prop(&prop) {
|
||||||
let (ns, tag) = prop.into();
|
Ok(()) => props_ok.push((None, propname)),
|
||||||
(ns.map(NamespaceOwned::from), tag.to_owned())
|
Err(Error::PropReadOnly) => props_conflict.push({
|
||||||
}),
|
let (ns, tag) = prop.into();
|
||||||
Err(err) => return Err(err.into()),
|
(ns.map(NamespaceOwned::from), tag.to_owned())
|
||||||
},
|
}),
|
||||||
// I guess removing a nonexisting property should be successful :)
|
Err(err) => return Err(err.into()),
|
||||||
Err(_) => props_ok.push((None, propname)),
|
},
|
||||||
};
|
// I guess removing a nonexisting property should be successful :)
|
||||||
|
Err(_) => props_ok.push((None, propname)),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
use crate::Principal;
|
use crate::Principal;
|
||||||
use crate::privileges::UserPrivilegeSet;
|
use crate::privileges::UserPrivilegeSet;
|
||||||
use crate::xml::multistatus::{PropTagWrapper, PropstatElement, PropstatWrapper};
|
use crate::xml::multistatus::{PropTagWrapper, PropstatElement, PropstatWrapper};
|
||||||
use crate::xml::{PropElement, PropfindType, Resourcetype};
|
use crate::xml::{PropElement, PropfindElement, PropfindType, Resourcetype};
|
||||||
use crate::xml::{TagList, multistatus::ResponseElement};
|
use crate::xml::{TagList, multistatus::ResponseElement};
|
||||||
use headers::{ETag, IfMatch, IfNoneMatch};
|
use headers::{ETag, IfMatch, IfNoneMatch};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use quick_xml::name::Namespace;
|
use quick_xml::name::Namespace;
|
||||||
pub use resource_service::ResourceService;
|
pub use resource_service::ResourceService;
|
||||||
use rustical_xml::{EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{
|
||||||
use std::collections::HashSet;
|
EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlDocument, XmlSerialize,
|
||||||
|
};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
mod axum_methods;
|
mod axum_methods;
|
||||||
@@ -102,10 +103,24 @@ pub trait Resource: Clone + Send + 'static {
|
|||||||
principal: &Self::Principal,
|
principal: &Self::Principal,
|
||||||
) -> Result<UserPrivilegeSet, Self::Error>;
|
) -> Result<UserPrivilegeSet, Self::Error>;
|
||||||
|
|
||||||
|
fn parse_propfind(
|
||||||
|
body: &str,
|
||||||
|
) -> Result<PropfindElement<<Self::Prop as PropName>::Names>, rustical_xml::XmlError> {
|
||||||
|
if !body.is_empty() {
|
||||||
|
PropfindElement::parse_str(body)
|
||||||
|
} else {
|
||||||
|
Ok(PropfindElement {
|
||||||
|
prop: PropfindType::Allprop,
|
||||||
|
include: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn propfind(
|
fn propfind(
|
||||||
&self,
|
&self,
|
||||||
path: &str,
|
path: &str,
|
||||||
prop: &PropfindType<<Self::Prop as PropName>::Names>,
|
prop: &PropfindType<<Self::Prop as PropName>::Names>,
|
||||||
|
include: Option<&PropElement<<Self::Prop as PropName>::Names>>,
|
||||||
principal_uri: &impl PrincipalUri,
|
principal_uri: &impl PrincipalUri,
|
||||||
principal: &Self::Principal,
|
principal: &Self::Principal,
|
||||||
) -> Result<ResponseElement<Self::Prop>, Self::Error> {
|
) -> Result<ResponseElement<Self::Prop>, Self::Error> {
|
||||||
@@ -115,36 +130,40 @@ pub trait Resource: Clone + Send + 'static {
|
|||||||
path.push('/');
|
path.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Support include element
|
let (mut props, mut invalid_props): (Vec<<Self::Prop as PropName>::Names>, Vec<_>) =
|
||||||
let (props, invalid_props): (HashSet<<Self::Prop as PropName>::Names>, Vec<_>) = match prop
|
match prop {
|
||||||
{
|
PropfindType::Propname => {
|
||||||
PropfindType::Propname => {
|
let props = Self::list_props()
|
||||||
let props = Self::list_props()
|
.into_iter()
|
||||||
.into_iter()
|
.map(|(ns, tag)| (ns.map(NamespaceOwned::from), tag.to_string()))
|
||||||
.map(|(ns, tag)| (ns.map(NamespaceOwned::from), tag.to_string()))
|
.collect_vec();
|
||||||
.collect_vec();
|
|
||||||
|
|
||||||
return Ok(ResponseElement {
|
return Ok(ResponseElement {
|
||||||
href: path.to_owned(),
|
href: path.to_owned(),
|
||||||
propstat: vec![PropstatWrapper::TagList(PropstatElement {
|
propstat: vec![PropstatWrapper::TagList(PropstatElement {
|
||||||
prop: TagList::from(props),
|
prop: TagList::from(props),
|
||||||
status: StatusCode::OK,
|
status: StatusCode::OK,
|
||||||
})],
|
})],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
PropfindType::Allprop => (
|
PropfindType::Allprop => (
|
||||||
Self::list_props()
|
Self::list_props()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_ns, name)| <Self::Prop as PropName>::Names::from_str(name).unwrap())
|
.map(|(_ns, name)| <Self::Prop as PropName>::Names::from_str(name).unwrap())
|
||||||
.collect(),
|
.collect(),
|
||||||
vec![],
|
vec![],
|
||||||
),
|
),
|
||||||
PropfindType::Prop(PropElement(valid_tags, invalid_tags)) => (
|
PropfindType::Prop(PropElement(valid_tags, invalid_tags)) => (
|
||||||
valid_tags.iter().cloned().collect(),
|
valid_tags.iter().unique().cloned().collect(),
|
||||||
invalid_tags.to_owned(),
|
invalid_tags.to_owned(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(PropElement(valid_tags, invalid_tags)) = include {
|
||||||
|
props.extend(valid_tags.clone());
|
||||||
|
invalid_props.extend(invalid_tags.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
let prop_responses = props
|
let prop_responses = props
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
|
|||||||
|
|
||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
_path: &Self::PathComponents,
|
path: &Self::PathComponents,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Self::Error>;
|
) -> Result<Self::Resource, Self::Error>;
|
||||||
|
|
||||||
async fn save_resource(
|
async fn save_resource(
|
||||||
|
|||||||
@@ -86,7 +86,11 @@ where
|
|||||||
|
|
||||||
const DAV_HEADER: &str = "1, 3, access-control";
|
const DAV_HEADER: &str = "1, 3, access-control";
|
||||||
|
|
||||||
async fn get_resource(&self, _: &()) -> Result<Self::Resource, Self::Error> {
|
async fn get_resource(
|
||||||
|
&self,
|
||||||
|
_: &(),
|
||||||
|
_show_deleted: bool,
|
||||||
|
) -> Result<Self::Resource, Self::Error> {
|
||||||
Ok(RootResource::<PRS::Resource, P>::default())
|
Ok(RootResource::<PRS::Resource, P>::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ pub struct PropstatElement<PropType: XmlSerialize> {
|
|||||||
pub status: StatusCode,
|
pub status: StatusCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xml_serialize_status<W: ::std::io::Write>(
|
fn xml_serialize_status(
|
||||||
status: &StatusCode,
|
status: &StatusCode,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
XmlSerialize::serialize(&format!("HTTP/1.1 {}", status), ns, tag, namespaces, writer)
|
XmlSerialize::serialize(&format!("HTTP/1.1 {}", status), ns, tag, namespaces, writer)
|
||||||
}
|
}
|
||||||
@@ -39,8 +39,15 @@ pub enum PropstatWrapper<T: XmlSerialize> {
|
|||||||
// RFC 2518
|
// RFC 2518
|
||||||
// <!ELEMENT response (href, ((href*, status)|(propstat+)),
|
// <!ELEMENT response (href, ((href*, status)|(propstat+)),
|
||||||
// responsedescription?) >
|
// responsedescription?) >
|
||||||
#[derive(XmlSerialize)]
|
#[derive(XmlSerialize, XmlRootTag)]
|
||||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
#[xml(ns = "crate::namespace::NS_DAV", root = b"response")]
|
||||||
|
#[xml(ns_prefix(
|
||||||
|
crate::namespace::NS_DAV = b"",
|
||||||
|
crate::namespace::NS_CARDDAV = b"CARD",
|
||||||
|
crate::namespace::NS_CALDAV = b"CAL",
|
||||||
|
crate::namespace::NS_CALENDARSERVER = b"CS",
|
||||||
|
crate::namespace::NS_DAVPUSH = b"PUSH"
|
||||||
|
))]
|
||||||
pub struct ResponseElement<PropstatType: XmlSerialize> {
|
pub struct ResponseElement<PropstatType: XmlSerialize> {
|
||||||
pub href: String,
|
pub href: String,
|
||||||
#[xml(serialize_with = "xml_serialize_optional_status")]
|
#[xml(serialize_with = "xml_serialize_optional_status")]
|
||||||
@@ -49,12 +56,12 @@ pub struct ResponseElement<PropstatType: XmlSerialize> {
|
|||||||
pub propstat: Vec<PropstatWrapper<PropstatType>>,
|
pub propstat: Vec<PropstatWrapper<PropstatType>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xml_serialize_optional_status<W: ::std::io::Write>(
|
fn xml_serialize_optional_status(
|
||||||
val: &Option<StatusCode>,
|
val: &Option<StatusCode>,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
XmlSerialize::serialize(
|
XmlSerialize::serialize(
|
||||||
&val.map(|status| format!("HTTP/1.1 {}", status)),
|
&val.map(|status| format!("HTTP/1.1 {}", status)),
|
||||||
@@ -111,11 +118,10 @@ impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
|
|||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
|
|
||||||
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
let output = match self.serialize_to_string() {
|
||||||
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
|
Ok(out) => out,
|
||||||
if let Err(err) = self.serialize_root(&mut writer) {
|
Err(err) => return crate::Error::from(err).into_response(),
|
||||||
return crate::Error::from(err).into_response();
|
};
|
||||||
}
|
|
||||||
|
|
||||||
let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS);
|
let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS);
|
||||||
let hdrs = resp.headers_mut().unwrap();
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ use rustical_xml::XmlRootTag;
|
|||||||
pub struct PropfindElement<PN: XmlDeserialize> {
|
pub struct PropfindElement<PN: XmlDeserialize> {
|
||||||
#[xml(ty = "untagged")]
|
#[xml(ty = "untagged")]
|
||||||
pub prop: PropfindType<PN>,
|
pub prop: PropfindType<PN>,
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||||
|
pub include: Option<PropElement<PN>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
// pub struct PropElement<PN: XmlDeserialize = Propname>(#[xml(ty = "untagged", flatten)] pub Vec<PN>);
|
|
||||||
pub struct PropElement<PN: XmlDeserialize>(
|
pub struct PropElement<PN: XmlDeserialize>(
|
||||||
// valid
|
// valid
|
||||||
pub Vec<PN>,
|
pub Vec<PN>,
|
||||||
|
|||||||
@@ -23,20 +23,23 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serialize_resourcetype() {
|
fn test_serialize_resourcetype() {
|
||||||
let mut buf = Vec::new();
|
let out = Document {
|
||||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
|
||||||
Document {
|
|
||||||
resourcetype: Resourcetype(&[
|
resourcetype: Resourcetype(&[
|
||||||
ResourcetypeInner(Some(crate::namespace::NS_DAV), "displayname"),
|
ResourcetypeInner(Some(crate::namespace::NS_DAV), "displayname"),
|
||||||
ResourcetypeInner(Some(crate::namespace::NS_CALENDARSERVER), "calendar-color"),
|
ResourcetypeInner(Some(crate::namespace::NS_CALENDARSERVER), "calendar-color"),
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
.serialize_root(&mut writer)
|
.serialize_to_string()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let out = String::from_utf8(buf).unwrap();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
out,
|
out,
|
||||||
"<document><resourcetype><displayname xmlns=\"DAV:\"/><calendar-color xmlns=\"http://calendarserver.org/ns/\"/></resourcetype></document>"
|
r#"<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<document>
|
||||||
|
<resourcetype>
|
||||||
|
<displayname xmlns="DAV:"/>
|
||||||
|
<calendar-color xmlns="http://calendarserver.org/ns/"/>
|
||||||
|
</resourcetype>
|
||||||
|
</document>"#
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlDeserialize};
|
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlDeserialize, XmlRootTag};
|
||||||
|
|
||||||
use super::PropfindType;
|
use super::PropfindType;
|
||||||
|
|
||||||
@@ -32,11 +32,35 @@ impl ValueSerialize for SyncLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc5323#section-5.17
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct LimitElement {
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||||
|
pub nresults: NresultsElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u64> for LimitElement {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
nresults: NresultsElement(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LimitElement> for u64 {
|
||||||
|
fn from(value: LimitElement) -> Self {
|
||||||
|
value.nresults.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct NresultsElement(#[xml(ty = "text")] u64);
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, XmlRootTag)]
|
||||||
// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
|
// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
|
||||||
// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
|
// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
|
||||||
// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
|
// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
|
||||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
#[xml(ns = "crate::namespace::NS_DAV", root = b"sync-collection")]
|
||||||
pub struct SyncCollectionRequest<PN: XmlDeserialize> {
|
pub struct SyncCollectionRequest<PN: XmlDeserialize> {
|
||||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||||
pub sync_token: String,
|
pub sync_token: String,
|
||||||
@@ -45,5 +69,48 @@ pub struct SyncCollectionRequest<PN: XmlDeserialize> {
|
|||||||
#[xml(ns = "crate::namespace::NS_DAV", ty = "untagged")]
|
#[xml(ns = "crate::namespace::NS_DAV", ty = "untagged")]
|
||||||
pub prop: PropfindType<PN>,
|
pub prop: PropfindType<PN>,
|
||||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||||
pub limit: Option<u64>,
|
pub limit: Option<LimitElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::xml::{
|
||||||
|
PropElement, PropfindType,
|
||||||
|
sync_collection::{SyncCollectionRequest, SyncLevel},
|
||||||
|
};
|
||||||
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlDocument};
|
||||||
|
|
||||||
|
const SYNC_COLLECTION_REQUEST: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<sync-collection xmlns="DAV:">
|
||||||
|
<sync-token />
|
||||||
|
<sync-level>1</sync-level>
|
||||||
|
<limit>
|
||||||
|
<nresults>100</nresults>
|
||||||
|
</limit>
|
||||||
|
<prop>
|
||||||
|
<getetag />
|
||||||
|
</prop>
|
||||||
|
</sync-collection>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, PropName, EnumVariants, PartialEq)]
|
||||||
|
#[xml(unit_variants_ident = "TestPropName")]
|
||||||
|
enum TestProp {
|
||||||
|
Getetag(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_sync_collection_request() {
|
||||||
|
let request =
|
||||||
|
SyncCollectionRequest::<TestPropName>::parse_str(SYNC_COLLECTION_REQUEST).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
request,
|
||||||
|
SyncCollectionRequest {
|
||||||
|
sync_token: "".to_owned(),
|
||||||
|
sync_level: SyncLevel::One,
|
||||||
|
prop: PropfindType::Prop(PropElement(vec![TestPropName::Getetag], vec![])),
|
||||||
|
limit: Some(100.into())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ use std::collections::HashMap;
|
|||||||
pub struct TagList(Vec<(Option<NamespaceOwned>, String)>);
|
pub struct TagList(Vec<(Option<NamespaceOwned>, String)>);
|
||||||
|
|
||||||
impl XmlSerialize for TagList {
|
impl XmlSerialize for TagList {
|
||||||
fn serialize<W: std::io::Write>(
|
fn serialize(
|
||||||
&self,
|
&self,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
let prefix = ns
|
let prefix = ns
|
||||||
.map(|ns| namespaces.get(&ns))
|
.map(|ns| namespaces.get(&ns))
|
||||||
@@ -57,7 +57,6 @@ impl XmlSerialize for TagList {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(refining_impl_trait)]
|
|
||||||
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ rustical_dav.workspace = true
|
|||||||
rustical_store.workspace = true
|
rustical_store.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
rand.workspace = true
|
|
||||||
ece.workspace = true
|
ece.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
openssl.workspace = true
|
openssl.workspace = true
|
||||||
|
|||||||
@@ -99,13 +99,13 @@ impl<S: SubscriptionStore> DavPushController<S> {
|
|||||||
content_update,
|
content_update,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
let payload = match push_message.serialize_to_string() {
|
||||||
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
|
Ok(payload) => payload,
|
||||||
if let Err(err) = push_message.serialize_root(&mut writer) {
|
Err(err) => {
|
||||||
error!("Could not serialize push message: {}", err);
|
error!("Could not serialize push message: {}", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let payload = String::from_utf8(output).unwrap();
|
};
|
||||||
|
|
||||||
for subsciption in subscriptions {
|
for subsciption in subscriptions {
|
||||||
if let Some(allowed_push_servers) = &self.allowed_push_servers {
|
if let Some(allowed_push_servers) = &self.allowed_push_servers {
|
||||||
@@ -183,6 +183,7 @@ impl<S: SubscriptionStore> DavPushController<S> {
|
|||||||
header::CONTENT_TYPE,
|
header::CONTENT_TYPE,
|
||||||
HeaderValue::from_static("application/octet-stream"),
|
HeaderValue::from_static("application/octet-stream"),
|
||||||
);
|
);
|
||||||
|
hdrs.insert("TTL", HeaderValue::from(60));
|
||||||
client.execute(request).await?;
|
client.execute(request).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
import { createClient } from "webdav";
|
import { escapeXml } from ".";
|
||||||
|
|
||||||
@customElement("create-addressbook-form")
|
@customElement("create-addressbook-form")
|
||||||
export class CreateAddressbookForm extends LitElement {
|
export class CreateAddressbookForm extends LitElement {
|
||||||
@@ -14,16 +14,16 @@ export class CreateAddressbookForm extends LitElement {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
client = createClient("/carddav")
|
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
user: String = ''
|
user: string = ''
|
||||||
@property()
|
@property()
|
||||||
id: String = ''
|
principal: string = ''
|
||||||
@property()
|
@property()
|
||||||
displayname: String = ''
|
addr_id: string = self.crypto.randomUUID()
|
||||||
@property()
|
@property()
|
||||||
description: String = ''
|
displayname: string = ''
|
||||||
|
@property()
|
||||||
|
description: string = ''
|
||||||
|
|
||||||
dialog: Ref<HTMLDialogElement> = createRef()
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
form: Ref<HTMLFormElement> = createRef()
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
@@ -34,9 +34,19 @@ export class CreateAddressbookForm extends LitElement {
|
|||||||
<dialog ${ref(this.dialog)}>
|
<dialog ${ref(this.dialog)}>
|
||||||
<h3>Create addressbook</h3>
|
<h3>Create addressbook</h3>
|
||||||
<form @submit=${this.submit} ${ref(this.form)}>
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
|
<label>
|
||||||
|
principal (for group addressbooks)
|
||||||
|
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
|
||||||
|
<option value=${this.user}>${this.user}</option>
|
||||||
|
${window.rusticalUser.memberships.map(membership => html`
|
||||||
|
<option value=${membership}>${membership}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" @change=${e => this.id = e.target.value} />
|
<input type="text" name="id" value=${this.addr_id} @change=${e => this.addr_id = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
@@ -50,7 +60,7 @@ export class CreateAddressbookForm extends LitElement {
|
|||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
`
|
`
|
||||||
@@ -59,7 +69,7 @@ export class CreateAddressbookForm extends LitElement {
|
|||||||
async submit(e: SubmitEvent) {
|
async submit(e: SubmitEvent) {
|
||||||
console.log(this.displayname)
|
console.log(this.displayname)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!this.id) {
|
if (!this.addr_id) {
|
||||||
alert("Empty id")
|
alert("Empty id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -67,19 +77,29 @@ export class CreateAddressbookForm extends LitElement {
|
|||||||
alert("Empty displayname")
|
alert("Empty displayname")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO: Escape user input: There's not really a security risk here but would be nicer
|
let response = await fetch(`/carddav/principal/${this.principal || this.user}/${this.addr_id}`, {
|
||||||
await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
|
method: 'MKCOL',
|
||||||
data: `
|
headers: {
|
||||||
|
'Content-Type': 'application/xml'
|
||||||
|
},
|
||||||
|
body: `
|
||||||
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||||
<set>
|
<set>
|
||||||
<prop>
|
<prop>
|
||||||
<displayname>${this.displayname}</displayname>
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
${this.description ? `<CARD:addressbook-description>${this.description}</CARD:addressbook-description>` : ''}
|
${this.description ? `<CARD:addressbook-description>${escapeXml(this.description)}</CARD:addressbook-description>` : ''}
|
||||||
</prop>
|
</prop>
|
||||||
</set>
|
</set>
|
||||||
</mkcol>
|
</mkcol>
|
||||||
`
|
`
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,61 @@
|
|||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
import { createClient } from "webdav";
|
import { escapeXml } from ".";
|
||||||
|
|
||||||
@customElement("create-calendar-form")
|
@customElement("create-calendar-form")
|
||||||
export class CreateCalendarForm extends LitElement {
|
export class CreateCalendarForm extends LitElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override createRenderRoot() {
|
protected override createRenderRoot() {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
client = createClient("/caldav")
|
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
user: String = ''
|
user: string = ''
|
||||||
@property()
|
@property()
|
||||||
id: String = ''
|
principal: string = ''
|
||||||
@property()
|
@property()
|
||||||
displayname: String = ''
|
cal_id: string = self.crypto.randomUUID()
|
||||||
@property()
|
@property()
|
||||||
description: String = ''
|
displayname: string = ''
|
||||||
@property()
|
@property()
|
||||||
color: String = ''
|
description: string = ''
|
||||||
@property()
|
@property()
|
||||||
subscriptionUrl: String = ''
|
timezone_id: string = ''
|
||||||
|
@property()
|
||||||
|
color: string = ''
|
||||||
|
@property()
|
||||||
|
isSubscription: boolean = false
|
||||||
|
@property()
|
||||||
|
subscriptionUrl: string = ''
|
||||||
@property()
|
@property()
|
||||||
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
|
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
|
||||||
|
|
||||||
dialog: Ref<HTMLDialogElement> = createRef()
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
form: Ref<HTMLFormElement> = createRef()
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
|
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
||||||
<dialog ${ref(this.dialog)}>
|
<dialog ${ref(this.dialog)}>
|
||||||
<h3>Create calendar</h3>
|
<h3>Create calendar</h3>
|
||||||
<form @submit=${this.submit} ${ref(this.form)}>
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
|
<label>
|
||||||
|
principal (for group calendars)
|
||||||
|
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
|
||||||
|
<option value=${this.user}>${this.user}</option>
|
||||||
|
${window.rusticalUser.memberships.map(membership => html`
|
||||||
|
<option value=${membership}>${membership}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" @change=${e => this.id = e.target.value} />
|
<input type="text" name="id" value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
@@ -51,6 +63,11 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
|
<label>
|
||||||
|
Timezone (optional)
|
||||||
|
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" @change=${e => this.description = e.target.value} />
|
<input type="text" name="description" @change=${e => this.description = e.target.value} />
|
||||||
@@ -61,20 +78,30 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
<input type="color" name="color" @change=${e => this.color = e.target.value} />
|
<input type="color" name="color" @change=${e => this.color = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Subscription URL
|
Calendar is subscription to external calendar
|
||||||
<input type="text" name="subscription_url" @change=${e => this.subscriptionUrl = e.target.value} />
|
<input type="checkbox" name="is_subscription" @change=${e => this.isSubscription = e.target.checked} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
|
${this.isSubscription ? html`
|
||||||
|
<label>
|
||||||
|
Subscription URL
|
||||||
|
<input type="text" name="subscription_url" @change=${e => this.subscriptionUrl = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
`: html``}
|
||||||
|
<br>
|
||||||
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
|
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
|
||||||
<label>
|
<label>
|
||||||
Support ${comp}
|
Support ${comp}
|
||||||
<input type="checkbox" value=${comp} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
<input type="checkbox" value=${comp} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
|
<br>
|
||||||
`)}
|
`)}
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
`
|
`
|
||||||
@@ -83,7 +110,7 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
async submit(e: SubmitEvent) {
|
async submit(e: SubmitEvent) {
|
||||||
console.log(this.displayname)
|
console.log(this.displayname)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!this.id) {
|
if (!this.cal_id) {
|
||||||
alert("Empty id")
|
alert("Empty id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -95,23 +122,34 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
alert("No calendar components selected")
|
alert("No calendar components selected")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
|
|
||||||
data: `
|
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
|
||||||
|
method: 'MKCOL',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/xml'
|
||||||
|
},
|
||||||
|
body: `
|
||||||
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
||||||
<set>
|
<set>
|
||||||
<prop>
|
<prop>
|
||||||
<displayname>${this.displayname}</displayname>
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''}
|
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ''}
|
||||||
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''}
|
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
|
||||||
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ''}
|
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
|
||||||
|
${(this.isSubscription && this.subscriptionUrl) ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ''}
|
||||||
<CAL:supported-calendar-component-set>
|
<CAL:supported-calendar-component-set>
|
||||||
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')}
|
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${escapeXml(comp)}" />`).join('\n')}
|
||||||
</CAL:supported-calendar-component-set>
|
</CAL:supported-calendar-component-set>
|
||||||
</prop>
|
</prop>
|
||||||
</set>
|
</set>
|
||||||
</mkcol>
|
</mkcol>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
43
crates/frontend/js-components/lib/delete-button.ts
Normal file
43
crates/frontend/js-components/lib/delete-button.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
@customElement("delete-button")
|
||||||
|
export class DeleteButton extends LitElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
trash: boolean = false
|
||||||
|
@property()
|
||||||
|
href: string
|
||||||
|
|
||||||
|
protected createRenderRoot() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
let text = this.trash ? 'Move to trash' : 'Delete'
|
||||||
|
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onClick(event: Event) {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!this.trash && !confirm('Do you want to delete this collection permanently?')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await fetch(this.href, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'X-No-Trashbin': this.trash ? '0' : '1'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (response.status < 200 || response.status >= 300) {
|
||||||
|
alert('An error occured, look into the console')
|
||||||
|
console.error(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
103
crates/frontend/js-components/lib/edit-addressbook-form.ts
Normal file
103
crates/frontend/js-components/lib/edit-addressbook-form.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
|
import { escapeXml } from ".";
|
||||||
|
|
||||||
|
@customElement("edit-addressbook-form")
|
||||||
|
export class EditAddressbookForm extends LitElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override createRenderRoot() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@property()
|
||||||
|
principal: string = ''
|
||||||
|
@property()
|
||||||
|
addr_id: string = ''
|
||||||
|
@property()
|
||||||
|
displayname: string = ''
|
||||||
|
@property()
|
||||||
|
description: string = ''
|
||||||
|
|
||||||
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Edit addressbook</button>
|
||||||
|
<dialog ${ref(this.dialog)}>
|
||||||
|
<h3>Create addressbook</h3>
|
||||||
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
|
<label>
|
||||||
|
Displayname
|
||||||
|
<input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Description
|
||||||
|
<input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!this.principal) {
|
||||||
|
alert("Empty principal")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.addr_id) {
|
||||||
|
alert("Empty id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.displayname) {
|
||||||
|
alert("Empty displayname")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let response = await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, {
|
||||||
|
method: 'PROPPATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/xml'
|
||||||
|
},
|
||||||
|
body: `
|
||||||
|
<propertyupdate xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<set>
|
||||||
|
<prop>
|
||||||
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
|
${this.description ? `<CARD:addressbook-description>${escapeXml(this.description)}</CARD:addressbook-description>` : ''}
|
||||||
|
</prop>
|
||||||
|
</set>
|
||||||
|
<remove>
|
||||||
|
<prop>
|
||||||
|
${!this.description ? '<CARD:calendar-description />' : ''}
|
||||||
|
</prop>
|
||||||
|
</remove>
|
||||||
|
</propertyupdate>
|
||||||
|
`
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'edit-addressbook-form': EditAddressbookForm
|
||||||
|
}
|
||||||
|
}
|
||||||
143
crates/frontend/js-components/lib/edit-calendar-form.ts
Normal file
143
crates/frontend/js-components/lib/edit-calendar-form.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
|
import { escapeXml } from ".";
|
||||||
|
|
||||||
|
@customElement("edit-calendar-form")
|
||||||
|
export class EditCalendarForm extends LitElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override createRenderRoot() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@property()
|
||||||
|
principal: string
|
||||||
|
@property()
|
||||||
|
cal_id: string
|
||||||
|
|
||||||
|
@property()
|
||||||
|
displayname: string = ''
|
||||||
|
@property()
|
||||||
|
description: string = ''
|
||||||
|
@property()
|
||||||
|
timezone_id: string = ''
|
||||||
|
@property()
|
||||||
|
color: string = ''
|
||||||
|
@property({
|
||||||
|
converter: {
|
||||||
|
fromAttribute: (value, _type) => new Set(value ? JSON.parse(value) : []),
|
||||||
|
toAttribute: (value, _type) => JSON.stringify(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
|
||||||
|
|
||||||
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
|
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Edit calendar</button>
|
||||||
|
<dialog ${ref(this.dialog)}>
|
||||||
|
<h3>Create calendar</h3>
|
||||||
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
|
<label>
|
||||||
|
Displayname
|
||||||
|
<input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Timezone (optional)
|
||||||
|
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Description
|
||||||
|
<input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Color
|
||||||
|
<input type="color" name="color" .value=${this.color} @change=${e => this.color = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
|
||||||
|
<label>
|
||||||
|
Support ${comp}
|
||||||
|
<input type="checkbox" value=${comp} ?checked=${this.components.has(comp)} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
`)}
|
||||||
|
<br>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!this.principal) {
|
||||||
|
alert("Empty principal")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.cal_id) {
|
||||||
|
alert("Empty id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.displayname) {
|
||||||
|
alert("Empty displayname")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.components.size) {
|
||||||
|
alert("No calendar components selected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
|
||||||
|
method: 'PROPPATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/xml'
|
||||||
|
},
|
||||||
|
body: `
|
||||||
|
<propertyupdate xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
||||||
|
<set>
|
||||||
|
<prop>
|
||||||
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
|
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ''}
|
||||||
|
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
|
||||||
|
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
|
||||||
|
<CAL:supported-calendar-component-set>
|
||||||
|
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${escapeXml(comp)}" />`).join('\n')}
|
||||||
|
</CAL:supported-calendar-component-set>
|
||||||
|
</prop>
|
||||||
|
</set>
|
||||||
|
<remove>
|
||||||
|
<prop>
|
||||||
|
${!this.timezone_id ? `<CAL:calendar-timezone-id />` : ''}
|
||||||
|
${!this.description ? '<CAL:calendar-description />' : ''}
|
||||||
|
${!this.color ? '<ICAL:calendar-color />' : ''}
|
||||||
|
</prop>
|
||||||
|
</remove>
|
||||||
|
</propertyupdate>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'edit-calendar-form': EditCalendarForm
|
||||||
|
}
|
||||||
|
}
|
||||||
9
crates/frontend/js-components/lib/global.d.ts
vendored
Normal file
9
crates/frontend/js-components/lib/global.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
interface Window {
|
||||||
|
rusticalUser: {
|
||||||
|
id: String,
|
||||||
|
displayname: String | null,
|
||||||
|
memberships: Array<String>,
|
||||||
|
principal_type: "individual" | "group" | "room" | String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
92
crates/frontend/js-components/lib/import-addressbook-form.ts
Normal file
92
crates/frontend/js-components/lib/import-addressbook-form.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
|
|
||||||
|
@customElement("import-addressbook-form")
|
||||||
|
export class ImportAddressbookForm extends LitElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override createRenderRoot() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@property()
|
||||||
|
user: string = ''
|
||||||
|
@property()
|
||||||
|
principal: string
|
||||||
|
@property()
|
||||||
|
addressbook_id: string = self.crypto.randomUUID()
|
||||||
|
|
||||||
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
|
file: File;
|
||||||
|
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Import addressbook</button>
|
||||||
|
<dialog ${ref(this.dialog)}>
|
||||||
|
<h3>Import addressbook</h3>
|
||||||
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
|
<label>
|
||||||
|
principal (for group addressbook)
|
||||||
|
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
|
||||||
|
<option value=${this.user}>${this.user}</option>
|
||||||
|
${window.rusticalUser.memberships.map(membership => html`
|
||||||
|
<option value=${membership}>${membership}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
id
|
||||||
|
<input type="text" name="id" value=${this.addressbook_id} @change=${e => this.addressbook_id = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
file
|
||||||
|
<input type="file" accept="text/vcard" name="file" @change=${e => this.file = e.target.files[0]} />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Import</button>
|
||||||
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.principal ||= this.user
|
||||||
|
if (!this.principal) {
|
||||||
|
alert("Empty principal")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.addressbook_id) {
|
||||||
|
alert("Empty id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let response = await fetch(`/carddav/principal/${this.principal}/${this.addressbook_id}`, {
|
||||||
|
method: 'IMPORT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/vcard'
|
||||||
|
},
|
||||||
|
body: this.file,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'import-addressbook-form': ImportAddressbookForm
|
||||||
|
}
|
||||||
|
}
|
||||||
92
crates/frontend/js-components/lib/import-calendar-form.ts
Normal file
92
crates/frontend/js-components/lib/import-calendar-form.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
|
|
||||||
|
@customElement("import-calendar-form")
|
||||||
|
export class ImportCalendarForm extends LitElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override createRenderRoot() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@property()
|
||||||
|
user: string = ''
|
||||||
|
@property()
|
||||||
|
principal: string
|
||||||
|
@property()
|
||||||
|
cal_id: string = self.crypto.randomUUID()
|
||||||
|
|
||||||
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
|
file: File;
|
||||||
|
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Import calendar</button>
|
||||||
|
<dialog ${ref(this.dialog)}>
|
||||||
|
<h3>Import calendar</h3>
|
||||||
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
|
<label>
|
||||||
|
principal (for group calendars)
|
||||||
|
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
|
||||||
|
<option value=${this.user}>${this.user}</option>
|
||||||
|
${window.rusticalUser.memberships.map(membership => html`
|
||||||
|
<option value=${membership}>${membership}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
id
|
||||||
|
<input type="text" name="id" value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
file
|
||||||
|
<input type="file" accept="text/calendar" name="file" @change=${e => this.file = e.target.files[0]} />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Import</button>
|
||||||
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.principal ||= this.user
|
||||||
|
if (!this.principal) {
|
||||||
|
alert("Empty principal")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.cal_id) {
|
||||||
|
alert("Empty id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
|
||||||
|
method: 'IMPORT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/calendar'
|
||||||
|
},
|
||||||
|
body: this.file,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'import-calendar-form': ImportCalendarForm
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function escapeXml(unsafe: string): string {
|
||||||
|
return unsafe.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"module": "nodenext",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2024",
|
"target": "es2024",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
"lib": [
|
"lib": [
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
optimizeDeps: {
|
|
||||||
// include: ["lit"]
|
|
||||||
},
|
|
||||||
build: {
|
build: {
|
||||||
|
minify: false,
|
||||||
|
modulePreload: {
|
||||||
|
polyfill: false
|
||||||
|
},
|
||||||
copyPublicDir: false,
|
copyPublicDir: false,
|
||||||
lib: {
|
lib: {
|
||||||
entry: 'lib/index.ts',
|
entry: 'lib/index.ts',
|
||||||
@@ -14,14 +15,19 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: [
|
input: [
|
||||||
"lib/create-calendar-form.ts",
|
"lib/create-calendar-form.ts",
|
||||||
|
"lib/edit-calendar-form.ts",
|
||||||
|
"lib/import-calendar-form.ts",
|
||||||
"lib/create-addressbook-form.ts",
|
"lib/create-addressbook-form.ts",
|
||||||
|
"lib/edit-addressbook-form.ts",
|
||||||
|
"lib/import-addressbook-form.ts",
|
||||||
|
"lib/delete-button.ts",
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
dir: "../public/assets/js/",
|
dir: "../public/assets/js/",
|
||||||
format: "es",
|
format: "es",
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
lit: ["lit"],
|
lit: ["lit"],
|
||||||
webdav: ["webdav"],
|
// webdav: ["webdav"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,49 +1,76 @@
|
|||||||
import { i as c, x as u } from "./lit-CWlWuEHk.mjs";
|
import { i, x } from "./lit-z6_uA4GX.mjs";
|
||||||
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
|
import { n as n$1, t } from "./property-D0NJdseG.mjs";
|
||||||
import { a as b } from "./webdav-Bz4I5vNH.mjs";
|
import { e, n } from "./ref-CPp9J0V5.mjs";
|
||||||
var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, a = (t, s, l, r) => {
|
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
||||||
for (var e = r > 1 ? void 0 : r ? f(s, l) : s, n = t.length - 1, p; n >= 0; n--)
|
var __defProp = Object.defineProperty;
|
||||||
(p = t[n]) && (e = (r ? p(s, l, e) : p(e)) || e);
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
return r && e && y(s, l, e), e;
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||||||
|
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
|
||||||
|
if (decorator = decorators[i2])
|
||||||
|
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||||||
|
if (kind && result) __defProp(target, key, result);
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
let i = class extends c {
|
let CreateAddressbookForm = class extends i {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(), this.client = b("/carddav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.dialog = d(), this.form = d();
|
super();
|
||||||
|
this.user = "";
|
||||||
|
this.principal = "";
|
||||||
|
this.addr_id = self.crypto.randomUUID();
|
||||||
|
this.displayname = "";
|
||||||
|
this.description = "";
|
||||||
|
this.dialog = e();
|
||||||
|
this.form = e();
|
||||||
}
|
}
|
||||||
createRenderRoot() {
|
createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
return u`
|
return x`
|
||||||
<button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
|
<button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
|
||||||
<dialog ${m(this.dialog)}>
|
<dialog ${n(this.dialog)}>
|
||||||
<h3>Create addressbook</h3>
|
<h3>Create addressbook</h3>
|
||||||
<form @submit=${this.submit} ${m(this.form)}>
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
|
<label>
|
||||||
|
principal (for group addressbooks)
|
||||||
|
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||||
|
<option value=${this.user}>${this.user}</option>
|
||||||
|
${window.rusticalUser.memberships.map((membership) => x`
|
||||||
|
<option value=${membership}>${membership}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" @change=${(t) => this.id = t.target.value} />
|
<input type="text" name="id" value=${this.addr_id} @change=${(e2) => this.addr_id = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Displayname
|
Displayname
|
||||||
<input type="text" name="displayname" value=${this.displayname} @change=${(t) => this.displayname = t.target.value} />
|
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" @change=${(t) => this.description = t.target.value} />
|
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
<button type="submit" @click=${(t) => {
|
<button type="submit" @click=${(event) => {
|
||||||
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
|
event.preventDefault();
|
||||||
}}> Cancel </button>
|
this.dialog.value.close();
|
||||||
|
this.form.value.reset();
|
||||||
|
}} class="cancel">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
async submit(t) {
|
async submit(e2) {
|
||||||
if (console.log(this.displayname), t.preventDefault(), !this.id) {
|
console.log(this.displayname);
|
||||||
|
e2.preventDefault();
|
||||||
|
if (!this.addr_id) {
|
||||||
alert("Empty id");
|
alert("Empty id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -51,35 +78,48 @@ let i = class extends c {
|
|||||||
alert("Empty displayname");
|
alert("Empty displayname");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
|
let response = await fetch(`/carddav/principal/${this.principal || this.user}/${this.addr_id}`, {
|
||||||
data: `
|
method: "MKCOL",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml"
|
||||||
|
},
|
||||||
|
body: `
|
||||||
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||||
<set>
|
<set>
|
||||||
<prop>
|
<prop>
|
||||||
<displayname>${this.displayname}</displayname>
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
${this.description ? `<CARD:addressbook-description>${this.description}</CARD:addressbook-description>` : ""}
|
${this.description ? `<CARD:addressbook-description>${escapeXml(this.description)}</CARD:addressbook-description>` : ""}
|
||||||
</prop>
|
</prop>
|
||||||
</set>
|
</set>
|
||||||
</mkcol>
|
</mkcol>
|
||||||
`
|
`
|
||||||
}), window.location.reload(), null;
|
});
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], i.prototype, "user", 2);
|
], CreateAddressbookForm.prototype, "user", 2);
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], i.prototype, "id", 2);
|
], CreateAddressbookForm.prototype, "principal", 2);
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], i.prototype, "displayname", 2);
|
], CreateAddressbookForm.prototype, "addr_id", 2);
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], i.prototype, "description", 2);
|
], CreateAddressbookForm.prototype, "displayname", 2);
|
||||||
i = a([
|
__decorateClass([
|
||||||
h("create-addressbook-form")
|
n$1()
|
||||||
], i);
|
], CreateAddressbookForm.prototype, "description", 2);
|
||||||
|
CreateAddressbookForm = __decorateClass([
|
||||||
|
t("create-addressbook-form")
|
||||||
|
], CreateAddressbookForm);
|
||||||
export {
|
export {
|
||||||
i as CreateAddressbookForm
|
CreateAddressbookForm
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,66 +1,113 @@
|
|||||||
import { i as u, x as c } from "./lit-CWlWuEHk.mjs";
|
import { i, x } from "./lit-z6_uA4GX.mjs";
|
||||||
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
|
import { n as n$1, t } from "./property-D0NJdseG.mjs";
|
||||||
import { a as b } from "./webdav-Bz4I5vNH.mjs";
|
import { e, n } from "./ref-CPp9J0V5.mjs";
|
||||||
var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, l, s) => {
|
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
||||||
for (var i = s > 1 ? void 0 : s ? $(e, l) : e, n = t.length - 1, p; n >= 0; n--)
|
var __defProp = Object.defineProperty;
|
||||||
(p = t[n]) && (i = (s ? p(e, l, i) : p(i)) || i);
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
return s && i && y(e, l, i), i;
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||||||
|
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
|
||||||
|
if (decorator = decorators[i2])
|
||||||
|
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||||||
|
if (kind && result) __defProp(target, key, result);
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
let r = class extends u {
|
let CreateCalendarForm = class extends i {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(), this.client = b("/caldav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.color = "", this.subscriptionUrl = "", this.components = /* @__PURE__ */ new Set(), this.dialog = d(), this.form = d();
|
super();
|
||||||
|
this.user = "";
|
||||||
|
this.principal = "";
|
||||||
|
this.cal_id = self.crypto.randomUUID();
|
||||||
|
this.displayname = "";
|
||||||
|
this.description = "";
|
||||||
|
this.timezone_id = "";
|
||||||
|
this.color = "";
|
||||||
|
this.isSubscription = false;
|
||||||
|
this.subscriptionUrl = "";
|
||||||
|
this.components = /* @__PURE__ */ new Set();
|
||||||
|
this.dialog = e();
|
||||||
|
this.form = e();
|
||||||
}
|
}
|
||||||
createRenderRoot() {
|
createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
return c`
|
return x`
|
||||||
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
||||||
<dialog ${m(this.dialog)}>
|
<dialog ${n(this.dialog)}>
|
||||||
<h3>Create calendar</h3>
|
<h3>Create calendar</h3>
|
||||||
<form @submit=${this.submit} ${m(this.form)}>
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
|
<label>
|
||||||
|
principal (for group calendars)
|
||||||
|
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||||
|
<option value=${this.user}>${this.user}</option>
|
||||||
|
${window.rusticalUser.memberships.map((membership) => x`
|
||||||
|
<option value=${membership}>${membership}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" @change=${(t) => this.id = t.target.value} />
|
<input type="text" name="id" value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Displayname
|
Displayname
|
||||||
<input type="text" name="displayname" value=${this.displayname} @change=${(t) => this.displayname = t.target.value} />
|
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Timezone (optional)
|
||||||
|
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" @change=${(t) => this.description = t.target.value} />
|
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Color
|
Color
|
||||||
<input type="color" name="color" @change=${(t) => this.color = t.target.value} />
|
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Subscription URL
|
Calendar is subscription to external calendar
|
||||||
<input type="text" name="subscription_url" @change=${(t) => this.subscriptionUrl = t.target.value} />
|
<input type="checkbox" name="is_subscription" @change=${(e2) => this.isSubscription = e2.target.checked} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
${["VEVENT", "VTODO", "VJOURNAL"].map((t) => c`
|
${this.isSubscription ? x`
|
||||||
<label>
|
<label>
|
||||||
Support ${t}
|
Subscription URL
|
||||||
<input type="checkbox" value=${t} @change=${(e) => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
<input type="text" name="subscription_url" @change=${(e2) => this.subscriptionUrl = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
|
<br>
|
||||||
|
` : x``}
|
||||||
|
<br>
|
||||||
|
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
|
||||||
|
<label>
|
||||||
|
Support ${comp}
|
||||||
|
<input type="checkbox" value=${comp} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
`)}
|
`)}
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
<button type="submit" @click=${(t) => {
|
<button type="submit" @click=${(event) => {
|
||||||
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
|
event.preventDefault();
|
||||||
}}> Cancel </button>
|
this.dialog.value.close();
|
||||||
|
this.form.value.reset();
|
||||||
|
}} class="cancel">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
async submit(t) {
|
async submit(e2) {
|
||||||
if (console.log(this.displayname), t.preventDefault(), !this.id) {
|
console.log(this.displayname);
|
||||||
|
e2.preventDefault();
|
||||||
|
if (!this.cal_id) {
|
||||||
alert("Empty id");
|
alert("Empty id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -72,50 +119,69 @@ let r = class extends u {
|
|||||||
alert("No calendar components selected");
|
alert("No calendar components selected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
|
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
|
||||||
data: `
|
method: "MKCOL",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml"
|
||||||
|
},
|
||||||
|
body: `
|
||||||
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
||||||
<set>
|
<set>
|
||||||
<prop>
|
<prop>
|
||||||
<displayname>${this.displayname}</displayname>
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ""}
|
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ""}
|
||||||
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""}
|
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
|
||||||
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ""}
|
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
|
||||||
|
${this.isSubscription && this.subscriptionUrl ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ""}
|
||||||
<CAL:supported-calendar-component-set>
|
<CAL:supported-calendar-component-set>
|
||||||
${Array.from(this.components.keys()).map((e) => `<CAL:comp name="${e}" />`).join(`
|
${Array.from(this.components.keys()).map((comp) => `<CAL:comp name="${escapeXml(comp)}" />`).join("\n")}
|
||||||
`)}
|
|
||||||
</CAL:supported-calendar-component-set>
|
</CAL:supported-calendar-component-set>
|
||||||
</prop>
|
</prop>
|
||||||
</set>
|
</set>
|
||||||
</mkcol>
|
</mkcol>
|
||||||
`
|
`
|
||||||
}), window.location.reload(), null;
|
});
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], r.prototype, "user", 2);
|
], CreateCalendarForm.prototype, "user", 2);
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], r.prototype, "id", 2);
|
], CreateCalendarForm.prototype, "principal", 2);
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], r.prototype, "displayname", 2);
|
], CreateCalendarForm.prototype, "cal_id", 2);
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], r.prototype, "description", 2);
|
], CreateCalendarForm.prototype, "displayname", 2);
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], r.prototype, "color", 2);
|
], CreateCalendarForm.prototype, "description", 2);
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], r.prototype, "subscriptionUrl", 2);
|
], CreateCalendarForm.prototype, "timezone_id", 2);
|
||||||
a([
|
__decorateClass([
|
||||||
o()
|
n$1()
|
||||||
], r.prototype, "components", 2);
|
], CreateCalendarForm.prototype, "color", 2);
|
||||||
r = a([
|
__decorateClass([
|
||||||
h("create-calendar-form")
|
n$1()
|
||||||
], r);
|
], CreateCalendarForm.prototype, "isSubscription", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "subscriptionUrl", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "components", 2);
|
||||||
|
CreateCalendarForm = __decorateClass([
|
||||||
|
t("create-calendar-form")
|
||||||
|
], CreateCalendarForm);
|
||||||
export {
|
export {
|
||||||
r as CreateCalendarForm
|
CreateCalendarForm
|
||||||
};
|
};
|
||||||
|
|||||||
55
crates/frontend/public/assets/js/delete-button.mjs
Normal file
55
crates/frontend/public/assets/js/delete-button.mjs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { i, x } from "./lit-z6_uA4GX.mjs";
|
||||||
|
import { n, t } from "./property-D0NJdseG.mjs";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||||||
|
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
|
||||||
|
if (decorator = decorators[i2])
|
||||||
|
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||||||
|
if (kind && result) __defProp(target, key, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
let DeleteButton = class extends i {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.trash = false;
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
let text = this.trash ? "Move to trash" : "Delete";
|
||||||
|
return x`<button class="delete" @click=${(e) => this._onClick(e)}>${text}</button>`;
|
||||||
|
}
|
||||||
|
async _onClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!this.trash && !confirm("Do you want to delete this collection permanently?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let response = await fetch(this.href, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"X-No-Trashbin": this.trash ? "0" : "1"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.status < 200 || response.status >= 300) {
|
||||||
|
alert("An error occured, look into the console");
|
||||||
|
console.error(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
__decorateClass([
|
||||||
|
n({ type: Boolean })
|
||||||
|
], DeleteButton.prototype, "trash", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n()
|
||||||
|
], DeleteButton.prototype, "href", 2);
|
||||||
|
DeleteButton = __decorateClass([
|
||||||
|
t("delete-button")
|
||||||
|
], DeleteButton);
|
||||||
|
export {
|
||||||
|
DeleteButton
|
||||||
|
};
|
||||||
114
crates/frontend/public/assets/js/edit-addressbook-form.mjs
Normal file
114
crates/frontend/public/assets/js/edit-addressbook-form.mjs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { i, x } from "./lit-z6_uA4GX.mjs";
|
||||||
|
import { n as n$1, t } from "./property-D0NJdseG.mjs";
|
||||||
|
import { e, n } from "./ref-CPp9J0V5.mjs";
|
||||||
|
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||||||
|
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
|
||||||
|
if (decorator = decorators[i2])
|
||||||
|
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||||||
|
if (kind && result) __defProp(target, key, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
let EditAddressbookForm = class extends i {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.principal = "";
|
||||||
|
this.addr_id = "";
|
||||||
|
this.displayname = "";
|
||||||
|
this.description = "";
|
||||||
|
this.dialog = e();
|
||||||
|
this.form = e();
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return x`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Edit addressbook</button>
|
||||||
|
<dialog ${n(this.dialog)}>
|
||||||
|
<h3>Create addressbook</h3>
|
||||||
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
|
<label>
|
||||||
|
Displayname
|
||||||
|
<input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Description
|
||||||
|
<input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
<button type="submit" @click=${(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.dialog.value.close();
|
||||||
|
this.form.value.reset();
|
||||||
|
}} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
async submit(e2) {
|
||||||
|
e2.preventDefault();
|
||||||
|
if (!this.principal) {
|
||||||
|
alert("Empty principal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.addr_id) {
|
||||||
|
alert("Empty id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.displayname) {
|
||||||
|
alert("Empty displayname");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let response = await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, {
|
||||||
|
method: "PROPPATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml"
|
||||||
|
},
|
||||||
|
body: `
|
||||||
|
<propertyupdate xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<set>
|
||||||
|
<prop>
|
||||||
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
|
${this.description ? `<CARD:addressbook-description>${escapeXml(this.description)}</CARD:addressbook-description>` : ""}
|
||||||
|
</prop>
|
||||||
|
</set>
|
||||||
|
<remove>
|
||||||
|
<prop>
|
||||||
|
${!this.description ? "<CARD:calendar-description />" : ""}
|
||||||
|
</prop>
|
||||||
|
</remove>
|
||||||
|
</propertyupdate>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditAddressbookForm.prototype, "principal", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditAddressbookForm.prototype, "addr_id", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditAddressbookForm.prototype, "displayname", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditAddressbookForm.prototype, "description", 2);
|
||||||
|
EditAddressbookForm = __decorateClass([
|
||||||
|
t("edit-addressbook-form")
|
||||||
|
], EditAddressbookForm);
|
||||||
|
export {
|
||||||
|
EditAddressbookForm
|
||||||
|
};
|
||||||
158
crates/frontend/public/assets/js/edit-calendar-form.mjs
Normal file
158
crates/frontend/public/assets/js/edit-calendar-form.mjs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { i, x } from "./lit-z6_uA4GX.mjs";
|
||||||
|
import { n as n$1, t } from "./property-D0NJdseG.mjs";
|
||||||
|
import { e, n } from "./ref-CPp9J0V5.mjs";
|
||||||
|
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||||||
|
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
|
||||||
|
if (decorator = decorators[i2])
|
||||||
|
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||||||
|
if (kind && result) __defProp(target, key, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
let EditCalendarForm = class extends i {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.displayname = "";
|
||||||
|
this.description = "";
|
||||||
|
this.timezone_id = "";
|
||||||
|
this.color = "";
|
||||||
|
this.components = /* @__PURE__ */ new Set();
|
||||||
|
this.dialog = e();
|
||||||
|
this.form = e();
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return x`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Edit calendar</button>
|
||||||
|
<dialog ${n(this.dialog)}>
|
||||||
|
<h3>Create calendar</h3>
|
||||||
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
|
<label>
|
||||||
|
Displayname
|
||||||
|
<input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Timezone (optional)
|
||||||
|
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Description
|
||||||
|
<input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Color
|
||||||
|
<input type="color" name="color" .value=${this.color} @change=${(e2) => this.color = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
|
||||||
|
<label>
|
||||||
|
Support ${comp}
|
||||||
|
<input type="checkbox" value=${comp} ?checked=${this.components.has(comp)} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
`)}
|
||||||
|
<br>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
<button type="submit" @click=${(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.dialog.value.close();
|
||||||
|
this.form.value.reset();
|
||||||
|
}} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
async submit(e2) {
|
||||||
|
e2.preventDefault();
|
||||||
|
if (!this.principal) {
|
||||||
|
alert("Empty principal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.cal_id) {
|
||||||
|
alert("Empty id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.displayname) {
|
||||||
|
alert("Empty displayname");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.components.size) {
|
||||||
|
alert("No calendar components selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
|
||||||
|
method: "PROPPATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml"
|
||||||
|
},
|
||||||
|
body: `
|
||||||
|
<propertyupdate xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
||||||
|
<set>
|
||||||
|
<prop>
|
||||||
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
|
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ""}
|
||||||
|
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
|
||||||
|
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
|
||||||
|
<CAL:supported-calendar-component-set>
|
||||||
|
${Array.from(this.components.keys()).map((comp) => `<CAL:comp name="${escapeXml(comp)}" />`).join("\n")}
|
||||||
|
</CAL:supported-calendar-component-set>
|
||||||
|
</prop>
|
||||||
|
</set>
|
||||||
|
<remove>
|
||||||
|
<prop>
|
||||||
|
${!this.timezone_id ? `<CAL:calendar-timezone-id />` : ""}
|
||||||
|
${!this.description ? "<CAL:calendar-description />" : ""}
|
||||||
|
${!this.color ? "<ICAL:calendar-color />" : ""}
|
||||||
|
</prop>
|
||||||
|
</remove>
|
||||||
|
</propertyupdate>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditCalendarForm.prototype, "principal", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditCalendarForm.prototype, "cal_id", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditCalendarForm.prototype, "displayname", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditCalendarForm.prototype, "description", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditCalendarForm.prototype, "timezone_id", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditCalendarForm.prototype, "color", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1({
|
||||||
|
converter: {
|
||||||
|
fromAttribute: (value, _type) => new Set(value ? JSON.parse(value) : []),
|
||||||
|
toAttribute: (value, _type) => JSON.stringify(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
], EditCalendarForm.prototype, "components", 2);
|
||||||
|
EditCalendarForm = __decorateClass([
|
||||||
|
t("edit-calendar-form")
|
||||||
|
], EditCalendarForm);
|
||||||
|
export {
|
||||||
|
EditCalendarForm
|
||||||
|
};
|
||||||
100
crates/frontend/public/assets/js/import-addressbook-form.mjs
Normal file
100
crates/frontend/public/assets/js/import-addressbook-form.mjs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { i, x } from "./lit-z6_uA4GX.mjs";
|
||||||
|
import { n as n$1, t } from "./property-D0NJdseG.mjs";
|
||||||
|
import { e, n } from "./ref-CPp9J0V5.mjs";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||||||
|
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
|
||||||
|
if (decorator = decorators[i2])
|
||||||
|
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||||||
|
if (kind && result) __defProp(target, key, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
let ImportAddressbookForm = class extends i {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.user = "";
|
||||||
|
this.addressbook_id = self.crypto.randomUUID();
|
||||||
|
this.dialog = e();
|
||||||
|
this.form = e();
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return x`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Import addressbook</button>
|
||||||
|
<dialog ${n(this.dialog)}>
|
||||||
|
<h3>Import addressbook</h3>
|
||||||
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
|
<label>
|
||||||
|
principal (for group addressbook)
|
||||||
|
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||||
|
<option value=${this.user}>${this.user}</option>
|
||||||
|
${window.rusticalUser.memberships.map((membership) => x`
|
||||||
|
<option value=${membership}>${membership}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
id
|
||||||
|
<input type="text" name="id" value=${this.addressbook_id} @change=${(e2) => this.addressbook_id = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
file
|
||||||
|
<input type="file" accept="text/vcard" name="file" @change=${(e2) => this.file = e2.target.files[0]} />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Import</button>
|
||||||
|
<button type="submit" @click=${(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.dialog.value.close();
|
||||||
|
this.form.value.reset();
|
||||||
|
}} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
async submit(e2) {
|
||||||
|
e2.preventDefault();
|
||||||
|
this.principal || (this.principal = this.user);
|
||||||
|
if (!this.principal) {
|
||||||
|
alert("Empty principal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.addressbook_id) {
|
||||||
|
alert("Empty id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let response = await fetch(`/carddav/principal/${this.principal}/${this.addressbook_id}`, {
|
||||||
|
method: "IMPORT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/vcard"
|
||||||
|
},
|
||||||
|
body: this.file
|
||||||
|
});
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], ImportAddressbookForm.prototype, "user", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], ImportAddressbookForm.prototype, "principal", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], ImportAddressbookForm.prototype, "addressbook_id", 2);
|
||||||
|
ImportAddressbookForm = __decorateClass([
|
||||||
|
t("import-addressbook-form")
|
||||||
|
], ImportAddressbookForm);
|
||||||
|
export {
|
||||||
|
ImportAddressbookForm
|
||||||
|
};
|
||||||
100
crates/frontend/public/assets/js/import-calendar-form.mjs
Normal file
100
crates/frontend/public/assets/js/import-calendar-form.mjs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { i, x } from "./lit-z6_uA4GX.mjs";
|
||||||
|
import { n as n$1, t } from "./property-D0NJdseG.mjs";
|
||||||
|
import { e, n } from "./ref-CPp9J0V5.mjs";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||||||
|
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
|
||||||
|
if (decorator = decorators[i2])
|
||||||
|
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||||||
|
if (kind && result) __defProp(target, key, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
let ImportCalendarForm = class extends i {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.user = "";
|
||||||
|
this.cal_id = self.crypto.randomUUID();
|
||||||
|
this.dialog = e();
|
||||||
|
this.form = e();
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return x`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Import calendar</button>
|
||||||
|
<dialog ${n(this.dialog)}>
|
||||||
|
<h3>Import calendar</h3>
|
||||||
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
|
<label>
|
||||||
|
principal (for group calendars)
|
||||||
|
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||||
|
<option value=${this.user}>${this.user}</option>
|
||||||
|
${window.rusticalUser.memberships.map((membership) => x`
|
||||||
|
<option value=${membership}>${membership}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
id
|
||||||
|
<input type="text" name="id" value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
file
|
||||||
|
<input type="file" accept="text/calendar" name="file" @change=${(e2) => this.file = e2.target.files[0]} />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Import</button>
|
||||||
|
<button type="submit" @click=${(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.dialog.value.close();
|
||||||
|
this.form.value.reset();
|
||||||
|
}} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
async submit(e2) {
|
||||||
|
e2.preventDefault();
|
||||||
|
this.principal || (this.principal = this.user);
|
||||||
|
if (!this.principal) {
|
||||||
|
alert("Empty principal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.cal_id) {
|
||||||
|
alert("Empty id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
|
||||||
|
method: "IMPORT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/calendar"
|
||||||
|
},
|
||||||
|
body: this.file
|
||||||
|
});
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], ImportCalendarForm.prototype, "user", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], ImportCalendarForm.prototype, "principal", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], ImportCalendarForm.prototype, "cal_id", 2);
|
||||||
|
ImportCalendarForm = __decorateClass([
|
||||||
|
t("import-calendar-form")
|
||||||
|
], ImportCalendarForm);
|
||||||
|
export {
|
||||||
|
ImportCalendarForm
|
||||||
|
};
|
||||||
6
crates/frontend/public/assets/js/index-_IB1wMbZ.mjs
Normal file
6
crates/frontend/public/assets/js/index-_IB1wMbZ.mjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
function escapeXml(unsafe) {
|
||||||
|
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
escapeXml as e
|
||||||
|
};
|
||||||
@@ -1,551 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2019 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const M = globalThis, B = M.ShadowRoot && (M.ShadyCSS === void 0 || M.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, tt = Symbol(), W = /* @__PURE__ */ new WeakMap();
|
|
||||||
let ot = class {
|
|
||||||
constructor(t, e, s) {
|
|
||||||
if (this._$cssResult$ = !0, s !== tt) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
|
|
||||||
this.cssText = t, this.t = e;
|
|
||||||
}
|
|
||||||
get styleSheet() {
|
|
||||||
let t = this.o;
|
|
||||||
const e = this.t;
|
|
||||||
if (B && t === void 0) {
|
|
||||||
const s = e !== void 0 && e.length === 1;
|
|
||||||
s && (t = W.get(e)), t === void 0 && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), s && W.set(e, t));
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
toString() {
|
|
||||||
return this.cssText;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const ht = (r) => new ot(typeof r == "string" ? r : r + "", void 0, tt), at = (r, t) => {
|
|
||||||
if (B) r.adoptedStyleSheets = t.map((e) => e instanceof CSSStyleSheet ? e : e.styleSheet);
|
|
||||||
else for (const e of t) {
|
|
||||||
const s = document.createElement("style"), i = M.litNonce;
|
|
||||||
i !== void 0 && s.setAttribute("nonce", i), s.textContent = e.cssText, r.appendChild(s);
|
|
||||||
}
|
|
||||||
}, V = B ? (r) => r : (r) => r instanceof CSSStyleSheet ? ((t) => {
|
|
||||||
let e = "";
|
|
||||||
for (const s of t.cssRules) e += s.cssText;
|
|
||||||
return ht(e);
|
|
||||||
})(r) : r;
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const { is: lt, defineProperty: ct, getOwnPropertyDescriptor: dt, getOwnPropertyNames: pt, getOwnPropertySymbols: ut, getPrototypeOf: $t } = Object, f = globalThis, q = f.trustedTypes, _t = q ? q.emptyScript : "", k = f.reactiveElementPolyfillSupport, w = (r, t) => r, j = { toAttribute(r, t) {
|
|
||||||
switch (t) {
|
|
||||||
case Boolean:
|
|
||||||
r = r ? _t : null;
|
|
||||||
break;
|
|
||||||
case Object:
|
|
||||||
case Array:
|
|
||||||
r = r == null ? r : JSON.stringify(r);
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}, fromAttribute(r, t) {
|
|
||||||
let e = r;
|
|
||||||
switch (t) {
|
|
||||||
case Boolean:
|
|
||||||
e = r !== null;
|
|
||||||
break;
|
|
||||||
case Number:
|
|
||||||
e = r === null ? null : Number(r);
|
|
||||||
break;
|
|
||||||
case Object:
|
|
||||||
case Array:
|
|
||||||
try {
|
|
||||||
e = JSON.parse(r);
|
|
||||||
} catch {
|
|
||||||
e = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return e;
|
|
||||||
} }, et = (r, t) => !lt(r, t), J = { attribute: !0, type: String, converter: j, reflect: !1, useDefault: !1, hasChanged: et };
|
|
||||||
Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), f.litPropertyMetadata ?? (f.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
|
|
||||||
let v = class extends HTMLElement {
|
|
||||||
static addInitializer(t) {
|
|
||||||
this._$Ei(), (this.l ?? (this.l = [])).push(t);
|
|
||||||
}
|
|
||||||
static get observedAttributes() {
|
|
||||||
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
|
|
||||||
}
|
|
||||||
static createProperty(t, e = J) {
|
|
||||||
if (e.state && (e.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t) && ((e = Object.create(e)).wrapped = !0), this.elementProperties.set(t, e), !e.noAccessor) {
|
|
||||||
const s = Symbol(), i = this.getPropertyDescriptor(t, s, e);
|
|
||||||
i !== void 0 && ct(this.prototype, t, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static getPropertyDescriptor(t, e, s) {
|
|
||||||
const { get: i, set: n } = dt(this.prototype, t) ?? { get() {
|
|
||||||
return this[e];
|
|
||||||
}, set(o) {
|
|
||||||
this[e] = o;
|
|
||||||
} };
|
|
||||||
return { get: i, set(o) {
|
|
||||||
const a = i == null ? void 0 : i.call(this);
|
|
||||||
n == null || n.call(this, o), this.requestUpdate(t, a, s);
|
|
||||||
}, configurable: !0, enumerable: !0 };
|
|
||||||
}
|
|
||||||
static getPropertyOptions(t) {
|
|
||||||
return this.elementProperties.get(t) ?? J;
|
|
||||||
}
|
|
||||||
static _$Ei() {
|
|
||||||
if (this.hasOwnProperty(w("elementProperties"))) return;
|
|
||||||
const t = $t(this);
|
|
||||||
t.finalize(), t.l !== void 0 && (this.l = [...t.l]), this.elementProperties = new Map(t.elementProperties);
|
|
||||||
}
|
|
||||||
static finalize() {
|
|
||||||
if (this.hasOwnProperty(w("finalized"))) return;
|
|
||||||
if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(w("properties"))) {
|
|
||||||
const e = this.properties, s = [...pt(e), ...ut(e)];
|
|
||||||
for (const i of s) this.createProperty(i, e[i]);
|
|
||||||
}
|
|
||||||
const t = this[Symbol.metadata];
|
|
||||||
if (t !== null) {
|
|
||||||
const e = litPropertyMetadata.get(t);
|
|
||||||
if (e !== void 0) for (const [s, i] of e) this.elementProperties.set(s, i);
|
|
||||||
}
|
|
||||||
this._$Eh = /* @__PURE__ */ new Map();
|
|
||||||
for (const [e, s] of this.elementProperties) {
|
|
||||||
const i = this._$Eu(e, s);
|
|
||||||
i !== void 0 && this._$Eh.set(i, e);
|
|
||||||
}
|
|
||||||
this.elementStyles = this.finalizeStyles(this.styles);
|
|
||||||
}
|
|
||||||
static finalizeStyles(t) {
|
|
||||||
const e = [];
|
|
||||||
if (Array.isArray(t)) {
|
|
||||||
const s = new Set(t.flat(1 / 0).reverse());
|
|
||||||
for (const i of s) e.unshift(V(i));
|
|
||||||
} else t !== void 0 && e.push(V(t));
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
static _$Eu(t, e) {
|
|
||||||
const s = e.attribute;
|
|
||||||
return s === !1 ? void 0 : typeof s == "string" ? s : typeof t == "string" ? t.toLowerCase() : void 0;
|
|
||||||
}
|
|
||||||
constructor() {
|
|
||||||
super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev();
|
|
||||||
}
|
|
||||||
_$Ev() {
|
|
||||||
var t;
|
|
||||||
this._$ES = new Promise((e) => this.enableUpdating = e), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), (t = this.constructor.l) == null || t.forEach((e) => e(this));
|
|
||||||
}
|
|
||||||
addController(t) {
|
|
||||||
var e;
|
|
||||||
(this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(t), this.renderRoot !== void 0 && this.isConnected && ((e = t.hostConnected) == null || e.call(t));
|
|
||||||
}
|
|
||||||
removeController(t) {
|
|
||||||
var e;
|
|
||||||
(e = this._$EO) == null || e.delete(t);
|
|
||||||
}
|
|
||||||
_$E_() {
|
|
||||||
const t = /* @__PURE__ */ new Map(), e = this.constructor.elementProperties;
|
|
||||||
for (const s of e.keys()) this.hasOwnProperty(s) && (t.set(s, this[s]), delete this[s]);
|
|
||||||
t.size > 0 && (this._$Ep = t);
|
|
||||||
}
|
|
||||||
createRenderRoot() {
|
|
||||||
const t = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
|
|
||||||
return at(t, this.constructor.elementStyles), t;
|
|
||||||
}
|
|
||||||
connectedCallback() {
|
|
||||||
var t;
|
|
||||||
this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(!0), (t = this._$EO) == null || t.forEach((e) => {
|
|
||||||
var s;
|
|
||||||
return (s = e.hostConnected) == null ? void 0 : s.call(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
enableUpdating(t) {
|
|
||||||
}
|
|
||||||
disconnectedCallback() {
|
|
||||||
var t;
|
|
||||||
(t = this._$EO) == null || t.forEach((e) => {
|
|
||||||
var s;
|
|
||||||
return (s = e.hostDisconnected) == null ? void 0 : s.call(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
attributeChangedCallback(t, e, s) {
|
|
||||||
this._$AK(t, s);
|
|
||||||
}
|
|
||||||
_$ET(t, e) {
|
|
||||||
var n;
|
|
||||||
const s = this.constructor.elementProperties.get(t), i = this.constructor._$Eu(t, s);
|
|
||||||
if (i !== void 0 && s.reflect === !0) {
|
|
||||||
const o = (((n = s.converter) == null ? void 0 : n.toAttribute) !== void 0 ? s.converter : j).toAttribute(e, s.type);
|
|
||||||
this._$Em = t, o == null ? this.removeAttribute(i) : this.setAttribute(i, o), this._$Em = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_$AK(t, e) {
|
|
||||||
var n, o;
|
|
||||||
const s = this.constructor, i = s._$Eh.get(t);
|
|
||||||
if (i !== void 0 && this._$Em !== i) {
|
|
||||||
const a = s.getPropertyOptions(i), h = typeof a.converter == "function" ? { fromAttribute: a.converter } : ((n = a.converter) == null ? void 0 : n.fromAttribute) !== void 0 ? a.converter : j;
|
|
||||||
this._$Em = i, this[i] = h.fromAttribute(e, a.type) ?? ((o = this._$Ej) == null ? void 0 : o.get(i)) ?? null, this._$Em = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestUpdate(t, e, s) {
|
|
||||||
var i;
|
|
||||||
if (t !== void 0) {
|
|
||||||
const n = this.constructor, o = this[t];
|
|
||||||
if (s ?? (s = n.getPropertyOptions(t)), !((s.hasChanged ?? et)(o, e) || s.useDefault && s.reflect && o === ((i = this._$Ej) == null ? void 0 : i.get(t)) && !this.hasAttribute(n._$Eu(t, s)))) return;
|
|
||||||
this.C(t, e, s);
|
|
||||||
}
|
|
||||||
this.isUpdatePending === !1 && (this._$ES = this._$EP());
|
|
||||||
}
|
|
||||||
C(t, e, { useDefault: s, reflect: i, wrapped: n }, o) {
|
|
||||||
s && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(t) && (this._$Ej.set(t, o ?? e ?? this[t]), n !== !0 || o !== void 0) || (this._$AL.has(t) || (this.hasUpdated || s || (e = void 0), this._$AL.set(t, e)), i === !0 && this._$Em !== t && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(t));
|
|
||||||
}
|
|
||||||
async _$EP() {
|
|
||||||
this.isUpdatePending = !0;
|
|
||||||
try {
|
|
||||||
await this._$ES;
|
|
||||||
} catch (e) {
|
|
||||||
Promise.reject(e);
|
|
||||||
}
|
|
||||||
const t = this.scheduleUpdate();
|
|
||||||
return t != null && await t, !this.isUpdatePending;
|
|
||||||
}
|
|
||||||
scheduleUpdate() {
|
|
||||||
return this.performUpdate();
|
|
||||||
}
|
|
||||||
performUpdate() {
|
|
||||||
var s;
|
|
||||||
if (!this.isUpdatePending) return;
|
|
||||||
if (!this.hasUpdated) {
|
|
||||||
if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
|
|
||||||
for (const [n, o] of this._$Ep) this[n] = o;
|
|
||||||
this._$Ep = void 0;
|
|
||||||
}
|
|
||||||
const i = this.constructor.elementProperties;
|
|
||||||
if (i.size > 0) for (const [n, o] of i) {
|
|
||||||
const { wrapped: a } = o, h = this[n];
|
|
||||||
a !== !0 || this._$AL.has(n) || h === void 0 || this.C(n, void 0, o, h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let t = !1;
|
|
||||||
const e = this._$AL;
|
|
||||||
try {
|
|
||||||
t = this.shouldUpdate(e), t ? (this.willUpdate(e), (s = this._$EO) == null || s.forEach((i) => {
|
|
||||||
var n;
|
|
||||||
return (n = i.hostUpdate) == null ? void 0 : n.call(i);
|
|
||||||
}), this.update(e)) : this._$EM();
|
|
||||||
} catch (i) {
|
|
||||||
throw t = !1, this._$EM(), i;
|
|
||||||
}
|
|
||||||
t && this._$AE(e);
|
|
||||||
}
|
|
||||||
willUpdate(t) {
|
|
||||||
}
|
|
||||||
_$AE(t) {
|
|
||||||
var e;
|
|
||||||
(e = this._$EO) == null || e.forEach((s) => {
|
|
||||||
var i;
|
|
||||||
return (i = s.hostUpdated) == null ? void 0 : i.call(s);
|
|
||||||
}), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(t)), this.updated(t);
|
|
||||||
}
|
|
||||||
_$EM() {
|
|
||||||
this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = !1;
|
|
||||||
}
|
|
||||||
get updateComplete() {
|
|
||||||
return this.getUpdateComplete();
|
|
||||||
}
|
|
||||||
getUpdateComplete() {
|
|
||||||
return this._$ES;
|
|
||||||
}
|
|
||||||
shouldUpdate(t) {
|
|
||||||
return !0;
|
|
||||||
}
|
|
||||||
update(t) {
|
|
||||||
this._$Eq && (this._$Eq = this._$Eq.forEach((e) => this._$ET(e, this[e]))), this._$EM();
|
|
||||||
}
|
|
||||||
updated(t) {
|
|
||||||
}
|
|
||||||
firstUpdated(t) {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
v.elementStyles = [], v.shadowRootOptions = { mode: "open" }, v[w("elementProperties")] = /* @__PURE__ */ new Map(), v[w("finalized")] = /* @__PURE__ */ new Map(), k == null || k({ ReactiveElement: v }), (f.reactiveElementVersions ?? (f.reactiveElementVersions = [])).push("2.1.0");
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const C = globalThis, N = C.trustedTypes, K = N ? N.createPolicy("lit-html", { createHTML: (r) => r }) : void 0, st = "$lit$", _ = `lit$${Math.random().toFixed(9).slice(2)}$`, it = "?" + _, ft = `<${it}>`, g = document, P = () => g.createComment(""), x = (r) => r === null || typeof r != "object" && typeof r != "function", I = Array.isArray, At = (r) => I(r) || typeof (r == null ? void 0 : r[Symbol.iterator]) == "function", D = `[
|
|
||||||
\f\r]`, b = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, Z = /-->/g, F = />/g, A = RegExp(`>|${D}(?:([^\\s"'>=/]+)(${D}*=${D}*(?:[^
|
|
||||||
\f\r"'\`<>=]|("|')|))|$)`, "g"), G = /'/g, Q = /"/g, rt = /^(?:script|style|textarea|title)$/i, mt = (r) => (t, ...e) => ({ _$litType$: r, strings: t, values: e }), xt = mt(1), E = Symbol.for("lit-noChange"), d = Symbol.for("lit-nothing"), X = /* @__PURE__ */ new WeakMap(), m = g.createTreeWalker(g, 129);
|
|
||||||
function nt(r, t) {
|
|
||||||
if (!I(r) || !r.hasOwnProperty("raw")) throw Error("invalid template strings array");
|
|
||||||
return K !== void 0 ? K.createHTML(t) : t;
|
|
||||||
}
|
|
||||||
const yt = (r, t) => {
|
|
||||||
const e = r.length - 1, s = [];
|
|
||||||
let i, n = t === 2 ? "<svg>" : t === 3 ? "<math>" : "", o = b;
|
|
||||||
for (let a = 0; a < e; a++) {
|
|
||||||
const h = r[a];
|
|
||||||
let c, p, l = -1, u = 0;
|
|
||||||
for (; u < h.length && (o.lastIndex = u, p = o.exec(h), p !== null); ) u = o.lastIndex, o === b ? p[1] === "!--" ? o = Z : p[1] !== void 0 ? o = F : p[2] !== void 0 ? (rt.test(p[2]) && (i = RegExp("</" + p[2], "g")), o = A) : p[3] !== void 0 && (o = A) : o === A ? p[0] === ">" ? (o = i ?? b, l = -1) : p[1] === void 0 ? l = -2 : (l = o.lastIndex - p[2].length, c = p[1], o = p[3] === void 0 ? A : p[3] === '"' ? Q : G) : o === Q || o === G ? o = A : o === Z || o === F ? o = b : (o = A, i = void 0);
|
|
||||||
const $ = o === A && r[a + 1].startsWith("/>") ? " " : "";
|
|
||||||
n += o === b ? h + ft : l >= 0 ? (s.push(c), h.slice(0, l) + st + h.slice(l) + _ + $) : h + _ + (l === -2 ? a : $);
|
|
||||||
}
|
|
||||||
return [nt(r, n + (r[e] || "<?>") + (t === 2 ? "</svg>" : t === 3 ? "</math>" : "")), s];
|
|
||||||
};
|
|
||||||
class U {
|
|
||||||
constructor({ strings: t, _$litType$: e }, s) {
|
|
||||||
let i;
|
|
||||||
this.parts = [];
|
|
||||||
let n = 0, o = 0;
|
|
||||||
const a = t.length - 1, h = this.parts, [c, p] = yt(t, e);
|
|
||||||
if (this.el = U.createElement(c, s), m.currentNode = this.el.content, e === 2 || e === 3) {
|
|
||||||
const l = this.el.content.firstChild;
|
|
||||||
l.replaceWith(...l.childNodes);
|
|
||||||
}
|
|
||||||
for (; (i = m.nextNode()) !== null && h.length < a; ) {
|
|
||||||
if (i.nodeType === 1) {
|
|
||||||
if (i.hasAttributes()) for (const l of i.getAttributeNames()) if (l.endsWith(st)) {
|
|
||||||
const u = p[o++], $ = i.getAttribute(l).split(_), H = /([.?@])?(.*)/.exec(u);
|
|
||||||
h.push({ type: 1, index: n, name: H[2], strings: $, ctor: H[1] === "." ? vt : H[1] === "?" ? Et : H[1] === "@" ? St : R }), i.removeAttribute(l);
|
|
||||||
} else l.startsWith(_) && (h.push({ type: 6, index: n }), i.removeAttribute(l));
|
|
||||||
if (rt.test(i.tagName)) {
|
|
||||||
const l = i.textContent.split(_), u = l.length - 1;
|
|
||||||
if (u > 0) {
|
|
||||||
i.textContent = N ? N.emptyScript : "";
|
|
||||||
for (let $ = 0; $ < u; $++) i.append(l[$], P()), m.nextNode(), h.push({ type: 2, index: ++n });
|
|
||||||
i.append(l[u], P());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (i.nodeType === 8) if (i.data === it) h.push({ type: 2, index: n });
|
|
||||||
else {
|
|
||||||
let l = -1;
|
|
||||||
for (; (l = i.data.indexOf(_, l + 1)) !== -1; ) h.push({ type: 7, index: n }), l += _.length - 1;
|
|
||||||
}
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static createElement(t, e) {
|
|
||||||
const s = g.createElement("template");
|
|
||||||
return s.innerHTML = t, s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function S(r, t, e = r, s) {
|
|
||||||
var o, a;
|
|
||||||
if (t === E) return t;
|
|
||||||
let i = s !== void 0 ? (o = e._$Co) == null ? void 0 : o[s] : e._$Cl;
|
|
||||||
const n = x(t) ? void 0 : t._$litDirective$;
|
|
||||||
return (i == null ? void 0 : i.constructor) !== n && ((a = i == null ? void 0 : i._$AO) == null || a.call(i, !1), n === void 0 ? i = void 0 : (i = new n(r), i._$AT(r, e, s)), s !== void 0 ? (e._$Co ?? (e._$Co = []))[s] = i : e._$Cl = i), i !== void 0 && (t = S(r, i._$AS(r, t.values), i, s)), t;
|
|
||||||
}
|
|
||||||
class gt {
|
|
||||||
constructor(t, e) {
|
|
||||||
this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = e;
|
|
||||||
}
|
|
||||||
get parentNode() {
|
|
||||||
return this._$AM.parentNode;
|
|
||||||
}
|
|
||||||
get _$AU() {
|
|
||||||
return this._$AM._$AU;
|
|
||||||
}
|
|
||||||
u(t) {
|
|
||||||
const { el: { content: e }, parts: s } = this._$AD, i = ((t == null ? void 0 : t.creationScope) ?? g).importNode(e, !0);
|
|
||||||
m.currentNode = i;
|
|
||||||
let n = m.nextNode(), o = 0, a = 0, h = s[0];
|
|
||||||
for (; h !== void 0; ) {
|
|
||||||
if (o === h.index) {
|
|
||||||
let c;
|
|
||||||
h.type === 2 ? c = new O(n, n.nextSibling, this, t) : h.type === 1 ? c = new h.ctor(n, h.name, h.strings, this, t) : h.type === 6 && (c = new bt(n, this, t)), this._$AV.push(c), h = s[++a];
|
|
||||||
}
|
|
||||||
o !== (h == null ? void 0 : h.index) && (n = m.nextNode(), o++);
|
|
||||||
}
|
|
||||||
return m.currentNode = g, i;
|
|
||||||
}
|
|
||||||
p(t) {
|
|
||||||
let e = 0;
|
|
||||||
for (const s of this._$AV) s !== void 0 && (s.strings !== void 0 ? (s._$AI(t, s, e), e += s.strings.length - 2) : s._$AI(t[e])), e++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class O {
|
|
||||||
get _$AU() {
|
|
||||||
var t;
|
|
||||||
return ((t = this._$AM) == null ? void 0 : t._$AU) ?? this._$Cv;
|
|
||||||
}
|
|
||||||
constructor(t, e, s, i) {
|
|
||||||
this.type = 2, this._$AH = d, this._$AN = void 0, this._$AA = t, this._$AB = e, this._$AM = s, this.options = i, this._$Cv = (i == null ? void 0 : i.isConnected) ?? !0;
|
|
||||||
}
|
|
||||||
get parentNode() {
|
|
||||||
let t = this._$AA.parentNode;
|
|
||||||
const e = this._$AM;
|
|
||||||
return e !== void 0 && (t == null ? void 0 : t.nodeType) === 11 && (t = e.parentNode), t;
|
|
||||||
}
|
|
||||||
get startNode() {
|
|
||||||
return this._$AA;
|
|
||||||
}
|
|
||||||
get endNode() {
|
|
||||||
return this._$AB;
|
|
||||||
}
|
|
||||||
_$AI(t, e = this) {
|
|
||||||
t = S(this, t, e), x(t) ? t === d || t == null || t === "" ? (this._$AH !== d && this._$AR(), this._$AH = d) : t !== this._$AH && t !== E && this._(t) : t._$litType$ !== void 0 ? this.$(t) : t.nodeType !== void 0 ? this.T(t) : At(t) ? this.k(t) : this._(t);
|
|
||||||
}
|
|
||||||
O(t) {
|
|
||||||
return this._$AA.parentNode.insertBefore(t, this._$AB);
|
|
||||||
}
|
|
||||||
T(t) {
|
|
||||||
this._$AH !== t && (this._$AR(), this._$AH = this.O(t));
|
|
||||||
}
|
|
||||||
_(t) {
|
|
||||||
this._$AH !== d && x(this._$AH) ? this._$AA.nextSibling.data = t : this.T(g.createTextNode(t)), this._$AH = t;
|
|
||||||
}
|
|
||||||
$(t) {
|
|
||||||
var n;
|
|
||||||
const { values: e, _$litType$: s } = t, i = typeof s == "number" ? this._$AC(t) : (s.el === void 0 && (s.el = U.createElement(nt(s.h, s.h[0]), this.options)), s);
|
|
||||||
if (((n = this._$AH) == null ? void 0 : n._$AD) === i) this._$AH.p(e);
|
|
||||||
else {
|
|
||||||
const o = new gt(i, this), a = o.u(this.options);
|
|
||||||
o.p(e), this.T(a), this._$AH = o;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_$AC(t) {
|
|
||||||
let e = X.get(t.strings);
|
|
||||||
return e === void 0 && X.set(t.strings, e = new U(t)), e;
|
|
||||||
}
|
|
||||||
k(t) {
|
|
||||||
I(this._$AH) || (this._$AH = [], this._$AR());
|
|
||||||
const e = this._$AH;
|
|
||||||
let s, i = 0;
|
|
||||||
for (const n of t) i === e.length ? e.push(s = new O(this.O(P()), this.O(P()), this, this.options)) : s = e[i], s._$AI(n), i++;
|
|
||||||
i < e.length && (this._$AR(s && s._$AB.nextSibling, i), e.length = i);
|
|
||||||
}
|
|
||||||
_$AR(t = this._$AA.nextSibling, e) {
|
|
||||||
var s;
|
|
||||||
for ((s = this._$AP) == null ? void 0 : s.call(this, !1, !0, e); t && t !== this._$AB; ) {
|
|
||||||
const i = t.nextSibling;
|
|
||||||
t.remove(), t = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setConnected(t) {
|
|
||||||
var e;
|
|
||||||
this._$AM === void 0 && (this._$Cv = t, (e = this._$AP) == null || e.call(this, t));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class R {
|
|
||||||
get tagName() {
|
|
||||||
return this.element.tagName;
|
|
||||||
}
|
|
||||||
get _$AU() {
|
|
||||||
return this._$AM._$AU;
|
|
||||||
}
|
|
||||||
constructor(t, e, s, i, n) {
|
|
||||||
this.type = 1, this._$AH = d, this._$AN = void 0, this.element = t, this.name = e, this._$AM = i, this.options = n, s.length > 2 || s[0] !== "" || s[1] !== "" ? (this._$AH = Array(s.length - 1).fill(new String()), this.strings = s) : this._$AH = d;
|
|
||||||
}
|
|
||||||
_$AI(t, e = this, s, i) {
|
|
||||||
const n = this.strings;
|
|
||||||
let o = !1;
|
|
||||||
if (n === void 0) t = S(this, t, e, 0), o = !x(t) || t !== this._$AH && t !== E, o && (this._$AH = t);
|
|
||||||
else {
|
|
||||||
const a = t;
|
|
||||||
let h, c;
|
|
||||||
for (t = n[0], h = 0; h < n.length - 1; h++) c = S(this, a[s + h], e, h), c === E && (c = this._$AH[h]), o || (o = !x(c) || c !== this._$AH[h]), c === d ? t = d : t !== d && (t += (c ?? "") + n[h + 1]), this._$AH[h] = c;
|
|
||||||
}
|
|
||||||
o && !i && this.j(t);
|
|
||||||
}
|
|
||||||
j(t) {
|
|
||||||
t === d ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class vt extends R {
|
|
||||||
constructor() {
|
|
||||||
super(...arguments), this.type = 3;
|
|
||||||
}
|
|
||||||
j(t) {
|
|
||||||
this.element[this.name] = t === d ? void 0 : t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class Et extends R {
|
|
||||||
constructor() {
|
|
||||||
super(...arguments), this.type = 4;
|
|
||||||
}
|
|
||||||
j(t) {
|
|
||||||
this.element.toggleAttribute(this.name, !!t && t !== d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class St extends R {
|
|
||||||
constructor(t, e, s, i, n) {
|
|
||||||
super(t, e, s, i, n), this.type = 5;
|
|
||||||
}
|
|
||||||
_$AI(t, e = this) {
|
|
||||||
if ((t = S(this, t, e, 0) ?? d) === E) return;
|
|
||||||
const s = this._$AH, i = t === d && s !== d || t.capture !== s.capture || t.once !== s.once || t.passive !== s.passive, n = t !== d && (s === d || i);
|
|
||||||
i && this.element.removeEventListener(this.name, this, s), n && this.element.addEventListener(this.name, this, t), this._$AH = t;
|
|
||||||
}
|
|
||||||
handleEvent(t) {
|
|
||||||
var e;
|
|
||||||
typeof this._$AH == "function" ? this._$AH.call(((e = this.options) == null ? void 0 : e.host) ?? this.element, t) : this._$AH.handleEvent(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class bt {
|
|
||||||
constructor(t, e, s) {
|
|
||||||
this.element = t, this.type = 6, this._$AN = void 0, this._$AM = e, this.options = s;
|
|
||||||
}
|
|
||||||
get _$AU() {
|
|
||||||
return this._$AM._$AU;
|
|
||||||
}
|
|
||||||
_$AI(t) {
|
|
||||||
S(this, t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const L = C.litHtmlPolyfillSupport;
|
|
||||||
L == null || L(U, O), (C.litHtmlVersions ?? (C.litHtmlVersions = [])).push("3.3.0");
|
|
||||||
const wt = (r, t, e) => {
|
|
||||||
const s = (e == null ? void 0 : e.renderBefore) ?? t;
|
|
||||||
let i = s._$litPart$;
|
|
||||||
if (i === void 0) {
|
|
||||||
const n = (e == null ? void 0 : e.renderBefore) ?? null;
|
|
||||||
s._$litPart$ = i = new O(t.insertBefore(P(), n), n, void 0, e ?? {});
|
|
||||||
}
|
|
||||||
return i._$AI(r), i;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const y = globalThis;
|
|
||||||
class T extends v {
|
|
||||||
constructor() {
|
|
||||||
super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
|
|
||||||
}
|
|
||||||
createRenderRoot() {
|
|
||||||
var e;
|
|
||||||
const t = super.createRenderRoot();
|
|
||||||
return (e = this.renderOptions).renderBefore ?? (e.renderBefore = t.firstChild), t;
|
|
||||||
}
|
|
||||||
update(t) {
|
|
||||||
const e = this.render();
|
|
||||||
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t), this._$Do = wt(e, this.renderRoot, this.renderOptions);
|
|
||||||
}
|
|
||||||
connectedCallback() {
|
|
||||||
var t;
|
|
||||||
super.connectedCallback(), (t = this._$Do) == null || t.setConnected(!0);
|
|
||||||
}
|
|
||||||
disconnectedCallback() {
|
|
||||||
var t;
|
|
||||||
super.disconnectedCallback(), (t = this._$Do) == null || t.setConnected(!1);
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return E;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var Y;
|
|
||||||
T._$litElement$ = !0, T.finalized = !0, (Y = y.litElementHydrateSupport) == null || Y.call(y, { LitElement: T });
|
|
||||||
const z = y.litElementPolyfillSupport;
|
|
||||||
z == null || z({ LitElement: T });
|
|
||||||
(y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.0");
|
|
||||||
export {
|
|
||||||
d as E,
|
|
||||||
et as f,
|
|
||||||
T as i,
|
|
||||||
j as u,
|
|
||||||
xt as x
|
|
||||||
};
|
|
||||||
550
crates/frontend/public/assets/js/lit-z6_uA4GX.mjs
Normal file
550
crates/frontend/public/assets/js/lit-z6_uA4GX.mjs
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2019 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
var _a;
|
||||||
|
const t$1 = globalThis, e$2 = t$1.ShadowRoot && (void 0 === t$1.ShadyCSS || t$1.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$2 = Symbol(), o$3 = /* @__PURE__ */ new WeakMap();
|
||||||
|
let n$2 = class n {
|
||||||
|
constructor(t2, e2, o2) {
|
||||||
|
if (this._$cssResult$ = true, o2 !== s$2) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
|
||||||
|
this.cssText = t2, this.t = e2;
|
||||||
|
}
|
||||||
|
get styleSheet() {
|
||||||
|
let t2 = this.o;
|
||||||
|
const s2 = this.t;
|
||||||
|
if (e$2 && void 0 === t2) {
|
||||||
|
const e2 = void 0 !== s2 && 1 === s2.length;
|
||||||
|
e2 && (t2 = o$3.get(s2)), void 0 === t2 && ((this.o = t2 = new CSSStyleSheet()).replaceSync(this.cssText), e2 && o$3.set(s2, t2));
|
||||||
|
}
|
||||||
|
return t2;
|
||||||
|
}
|
||||||
|
toString() {
|
||||||
|
return this.cssText;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const r$2 = (t2) => new n$2("string" == typeof t2 ? t2 : t2 + "", void 0, s$2), S$1 = (s2, o2) => {
|
||||||
|
if (e$2) s2.adoptedStyleSheets = o2.map((t2) => t2 instanceof CSSStyleSheet ? t2 : t2.styleSheet);
|
||||||
|
else for (const e2 of o2) {
|
||||||
|
const o3 = document.createElement("style"), n3 = t$1.litNonce;
|
||||||
|
void 0 !== n3 && o3.setAttribute("nonce", n3), o3.textContent = e2.cssText, s2.appendChild(o3);
|
||||||
|
}
|
||||||
|
}, c$2 = e$2 ? (t2) => t2 : (t2) => t2 instanceof CSSStyleSheet ? ((t3) => {
|
||||||
|
let e2 = "";
|
||||||
|
for (const s2 of t3.cssRules) e2 += s2.cssText;
|
||||||
|
return r$2(e2);
|
||||||
|
})(t2) : t2;
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const { is: i$2, defineProperty: e$1, getOwnPropertyDescriptor: h$1, getOwnPropertyNames: r$1, getOwnPropertySymbols: o$2, getPrototypeOf: n$1 } = Object, a$1 = globalThis, c$1 = a$1.trustedTypes, l$1 = c$1 ? c$1.emptyScript : "", p$1 = a$1.reactiveElementPolyfillSupport, d$1 = (t2, s2) => t2, u$1 = { toAttribute(t2, s2) {
|
||||||
|
switch (s2) {
|
||||||
|
case Boolean:
|
||||||
|
t2 = t2 ? l$1 : null;
|
||||||
|
break;
|
||||||
|
case Object:
|
||||||
|
case Array:
|
||||||
|
t2 = null == t2 ? t2 : JSON.stringify(t2);
|
||||||
|
}
|
||||||
|
return t2;
|
||||||
|
}, fromAttribute(t2, s2) {
|
||||||
|
let i2 = t2;
|
||||||
|
switch (s2) {
|
||||||
|
case Boolean:
|
||||||
|
i2 = null !== t2;
|
||||||
|
break;
|
||||||
|
case Number:
|
||||||
|
i2 = null === t2 ? null : Number(t2);
|
||||||
|
break;
|
||||||
|
case Object:
|
||||||
|
case Array:
|
||||||
|
try {
|
||||||
|
i2 = JSON.parse(t2);
|
||||||
|
} catch (t3) {
|
||||||
|
i2 = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i2;
|
||||||
|
} }, f$1 = (t2, s2) => !i$2(t2, s2), b = { attribute: true, type: String, converter: u$1, reflect: false, useDefault: false, hasChanged: f$1 };
|
||||||
|
Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), a$1.litPropertyMetadata ?? (a$1.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
|
||||||
|
let y$1 = class y extends HTMLElement {
|
||||||
|
static addInitializer(t2) {
|
||||||
|
this._$Ei(), (this.l ?? (this.l = [])).push(t2);
|
||||||
|
}
|
||||||
|
static get observedAttributes() {
|
||||||
|
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
|
||||||
|
}
|
||||||
|
static createProperty(t2, s2 = b) {
|
||||||
|
if (s2.state && (s2.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t2) && ((s2 = Object.create(s2)).wrapped = true), this.elementProperties.set(t2, s2), !s2.noAccessor) {
|
||||||
|
const i2 = Symbol(), h2 = this.getPropertyDescriptor(t2, i2, s2);
|
||||||
|
void 0 !== h2 && e$1(this.prototype, t2, h2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static getPropertyDescriptor(t2, s2, i2) {
|
||||||
|
const { get: e2, set: r2 } = h$1(this.prototype, t2) ?? { get() {
|
||||||
|
return this[s2];
|
||||||
|
}, set(t3) {
|
||||||
|
this[s2] = t3;
|
||||||
|
} };
|
||||||
|
return { get: e2, set(s3) {
|
||||||
|
const h2 = e2 == null ? void 0 : e2.call(this);
|
||||||
|
r2 == null ? void 0 : r2.call(this, s3), this.requestUpdate(t2, h2, i2);
|
||||||
|
}, configurable: true, enumerable: true };
|
||||||
|
}
|
||||||
|
static getPropertyOptions(t2) {
|
||||||
|
return this.elementProperties.get(t2) ?? b;
|
||||||
|
}
|
||||||
|
static _$Ei() {
|
||||||
|
if (this.hasOwnProperty(d$1("elementProperties"))) return;
|
||||||
|
const t2 = n$1(this);
|
||||||
|
t2.finalize(), void 0 !== t2.l && (this.l = [...t2.l]), this.elementProperties = new Map(t2.elementProperties);
|
||||||
|
}
|
||||||
|
static finalize() {
|
||||||
|
if (this.hasOwnProperty(d$1("finalized"))) return;
|
||||||
|
if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d$1("properties"))) {
|
||||||
|
const t3 = this.properties, s2 = [...r$1(t3), ...o$2(t3)];
|
||||||
|
for (const i2 of s2) this.createProperty(i2, t3[i2]);
|
||||||
|
}
|
||||||
|
const t2 = this[Symbol.metadata];
|
||||||
|
if (null !== t2) {
|
||||||
|
const s2 = litPropertyMetadata.get(t2);
|
||||||
|
if (void 0 !== s2) for (const [t3, i2] of s2) this.elementProperties.set(t3, i2);
|
||||||
|
}
|
||||||
|
this._$Eh = /* @__PURE__ */ new Map();
|
||||||
|
for (const [t3, s2] of this.elementProperties) {
|
||||||
|
const i2 = this._$Eu(t3, s2);
|
||||||
|
void 0 !== i2 && this._$Eh.set(i2, t3);
|
||||||
|
}
|
||||||
|
this.elementStyles = this.finalizeStyles(this.styles);
|
||||||
|
}
|
||||||
|
static finalizeStyles(s2) {
|
||||||
|
const i2 = [];
|
||||||
|
if (Array.isArray(s2)) {
|
||||||
|
const e2 = new Set(s2.flat(1 / 0).reverse());
|
||||||
|
for (const s3 of e2) i2.unshift(c$2(s3));
|
||||||
|
} else void 0 !== s2 && i2.push(c$2(s2));
|
||||||
|
return i2;
|
||||||
|
}
|
||||||
|
static _$Eu(t2, s2) {
|
||||||
|
const i2 = s2.attribute;
|
||||||
|
return false === i2 ? void 0 : "string" == typeof i2 ? i2 : "string" == typeof t2 ? t2.toLowerCase() : void 0;
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();
|
||||||
|
}
|
||||||
|
_$Ev() {
|
||||||
|
var _a2;
|
||||||
|
this._$ES = new Promise((t2) => this.enableUpdating = t2), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), (_a2 = this.constructor.l) == null ? void 0 : _a2.forEach((t2) => t2(this));
|
||||||
|
}
|
||||||
|
addController(t2) {
|
||||||
|
var _a2;
|
||||||
|
(this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(t2), void 0 !== this.renderRoot && this.isConnected && ((_a2 = t2.hostConnected) == null ? void 0 : _a2.call(t2));
|
||||||
|
}
|
||||||
|
removeController(t2) {
|
||||||
|
var _a2;
|
||||||
|
(_a2 = this._$EO) == null ? void 0 : _a2.delete(t2);
|
||||||
|
}
|
||||||
|
_$E_() {
|
||||||
|
const t2 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;
|
||||||
|
for (const i2 of s2.keys()) this.hasOwnProperty(i2) && (t2.set(i2, this[i2]), delete this[i2]);
|
||||||
|
t2.size > 0 && (this._$Ep = t2);
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
const t2 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
|
||||||
|
return S$1(t2, this.constructor.elementStyles), t2;
|
||||||
|
}
|
||||||
|
connectedCallback() {
|
||||||
|
var _a2;
|
||||||
|
this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(true), (_a2 = this._$EO) == null ? void 0 : _a2.forEach((t2) => {
|
||||||
|
var _a3;
|
||||||
|
return (_a3 = t2.hostConnected) == null ? void 0 : _a3.call(t2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
enableUpdating(t2) {
|
||||||
|
}
|
||||||
|
disconnectedCallback() {
|
||||||
|
var _a2;
|
||||||
|
(_a2 = this._$EO) == null ? void 0 : _a2.forEach((t2) => {
|
||||||
|
var _a3;
|
||||||
|
return (_a3 = t2.hostDisconnected) == null ? void 0 : _a3.call(t2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
attributeChangedCallback(t2, s2, i2) {
|
||||||
|
this._$AK(t2, i2);
|
||||||
|
}
|
||||||
|
_$ET(t2, s2) {
|
||||||
|
var _a2;
|
||||||
|
const i2 = this.constructor.elementProperties.get(t2), e2 = this.constructor._$Eu(t2, i2);
|
||||||
|
if (void 0 !== e2 && true === i2.reflect) {
|
||||||
|
const h2 = (void 0 !== ((_a2 = i2.converter) == null ? void 0 : _a2.toAttribute) ? i2.converter : u$1).toAttribute(s2, i2.type);
|
||||||
|
this._$Em = t2, null == h2 ? this.removeAttribute(e2) : this.setAttribute(e2, h2), this._$Em = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_$AK(t2, s2) {
|
||||||
|
var _a2, _b;
|
||||||
|
const i2 = this.constructor, e2 = i2._$Eh.get(t2);
|
||||||
|
if (void 0 !== e2 && this._$Em !== e2) {
|
||||||
|
const t3 = i2.getPropertyOptions(e2), h2 = "function" == typeof t3.converter ? { fromAttribute: t3.converter } : void 0 !== ((_a2 = t3.converter) == null ? void 0 : _a2.fromAttribute) ? t3.converter : u$1;
|
||||||
|
this._$Em = e2, this[e2] = h2.fromAttribute(s2, t3.type) ?? ((_b = this._$Ej) == null ? void 0 : _b.get(e2)) ?? null, this._$Em = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestUpdate(t2, s2, i2) {
|
||||||
|
var _a2;
|
||||||
|
if (void 0 !== t2) {
|
||||||
|
const e2 = this.constructor, h2 = this[t2];
|
||||||
|
if (i2 ?? (i2 = e2.getPropertyOptions(t2)), !((i2.hasChanged ?? f$1)(h2, s2) || i2.useDefault && i2.reflect && h2 === ((_a2 = this._$Ej) == null ? void 0 : _a2.get(t2)) && !this.hasAttribute(e2._$Eu(t2, i2)))) return;
|
||||||
|
this.C(t2, s2, i2);
|
||||||
|
}
|
||||||
|
false === this.isUpdatePending && (this._$ES = this._$EP());
|
||||||
|
}
|
||||||
|
C(t2, s2, { useDefault: i2, reflect: e2, wrapped: h2 }, r2) {
|
||||||
|
i2 && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(t2) && (this._$Ej.set(t2, r2 ?? s2 ?? this[t2]), true !== h2 || void 0 !== r2) || (this._$AL.has(t2) || (this.hasUpdated || i2 || (s2 = void 0), this._$AL.set(t2, s2)), true === e2 && this._$Em !== t2 && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(t2));
|
||||||
|
}
|
||||||
|
async _$EP() {
|
||||||
|
this.isUpdatePending = true;
|
||||||
|
try {
|
||||||
|
await this._$ES;
|
||||||
|
} catch (t3) {
|
||||||
|
Promise.reject(t3);
|
||||||
|
}
|
||||||
|
const t2 = this.scheduleUpdate();
|
||||||
|
return null != t2 && await t2, !this.isUpdatePending;
|
||||||
|
}
|
||||||
|
scheduleUpdate() {
|
||||||
|
return this.performUpdate();
|
||||||
|
}
|
||||||
|
performUpdate() {
|
||||||
|
var _a2;
|
||||||
|
if (!this.isUpdatePending) return;
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
|
||||||
|
for (const [t4, s3] of this._$Ep) this[t4] = s3;
|
||||||
|
this._$Ep = void 0;
|
||||||
|
}
|
||||||
|
const t3 = this.constructor.elementProperties;
|
||||||
|
if (t3.size > 0) for (const [s3, i2] of t3) {
|
||||||
|
const { wrapped: t4 } = i2, e2 = this[s3];
|
||||||
|
true !== t4 || this._$AL.has(s3) || void 0 === e2 || this.C(s3, void 0, i2, e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let t2 = false;
|
||||||
|
const s2 = this._$AL;
|
||||||
|
try {
|
||||||
|
t2 = this.shouldUpdate(s2), t2 ? (this.willUpdate(s2), (_a2 = this._$EO) == null ? void 0 : _a2.forEach((t3) => {
|
||||||
|
var _a3;
|
||||||
|
return (_a3 = t3.hostUpdate) == null ? void 0 : _a3.call(t3);
|
||||||
|
}), this.update(s2)) : this._$EM();
|
||||||
|
} catch (s3) {
|
||||||
|
throw t2 = false, this._$EM(), s3;
|
||||||
|
}
|
||||||
|
t2 && this._$AE(s2);
|
||||||
|
}
|
||||||
|
willUpdate(t2) {
|
||||||
|
}
|
||||||
|
_$AE(t2) {
|
||||||
|
var _a2;
|
||||||
|
(_a2 = this._$EO) == null ? void 0 : _a2.forEach((t3) => {
|
||||||
|
var _a3;
|
||||||
|
return (_a3 = t3.hostUpdated) == null ? void 0 : _a3.call(t3);
|
||||||
|
}), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t2)), this.updated(t2);
|
||||||
|
}
|
||||||
|
_$EM() {
|
||||||
|
this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;
|
||||||
|
}
|
||||||
|
get updateComplete() {
|
||||||
|
return this.getUpdateComplete();
|
||||||
|
}
|
||||||
|
getUpdateComplete() {
|
||||||
|
return this._$ES;
|
||||||
|
}
|
||||||
|
shouldUpdate(t2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
update(t2) {
|
||||||
|
this._$Eq && (this._$Eq = this._$Eq.forEach((t3) => this._$ET(t3, this[t3]))), this._$EM();
|
||||||
|
}
|
||||||
|
updated(t2) {
|
||||||
|
}
|
||||||
|
firstUpdated(t2) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$1("elementProperties")] = /* @__PURE__ */ new Map(), y$1[d$1("finalized")] = /* @__PURE__ */ new Map(), p$1 == null ? void 0 : p$1({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ?? (a$1.reactiveElementVersions = [])).push("2.1.0");
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const t = globalThis, i$1 = t.trustedTypes, s$1 = i$1 ? i$1.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, e = "$lit$", h = `lit$${Math.random().toFixed(9).slice(2)}$`, o$1 = "?" + h, n2 = `<${o$1}>`, r = document, l = () => r.createComment(""), c = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, a = Array.isArray, u = (t2) => a(t2) || "function" == typeof (t2 == null ? void 0 : t2[Symbol.iterator]), d = "[ \n\f\r]", f = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, v = /-->/g, _ = />/g, m = RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^
|
||||||
|
\f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y2 = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), x = y2(1), T = Symbol.for("lit-noChange"), E = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), C = r.createTreeWalker(r, 129);
|
||||||
|
function P(t2, i2) {
|
||||||
|
if (!a(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array");
|
||||||
|
return void 0 !== s$1 ? s$1.createHTML(i2) : i2;
|
||||||
|
}
|
||||||
|
const V = (t2, i2) => {
|
||||||
|
const s2 = t2.length - 1, o2 = [];
|
||||||
|
let r2, l2 = 2 === i2 ? "<svg>" : 3 === i2 ? "<math>" : "", c2 = f;
|
||||||
|
for (let i3 = 0; i3 < s2; i3++) {
|
||||||
|
const s3 = t2[i3];
|
||||||
|
let a2, u2, d2 = -1, y3 = 0;
|
||||||
|
for (; y3 < s3.length && (c2.lastIndex = y3, u2 = c2.exec(s3), null !== u2); ) y3 = c2.lastIndex, c2 === f ? "!--" === u2[1] ? c2 = v : void 0 !== u2[1] ? c2 = _ : void 0 !== u2[2] ? ($.test(u2[2]) && (r2 = RegExp("</" + u2[2], "g")), c2 = m) : void 0 !== u2[3] && (c2 = m) : c2 === m ? ">" === u2[0] ? (c2 = r2 ?? f, d2 = -1) : void 0 === u2[1] ? d2 = -2 : (d2 = c2.lastIndex - u2[2].length, a2 = u2[1], c2 = void 0 === u2[3] ? m : '"' === u2[3] ? g : p) : c2 === g || c2 === p ? c2 = m : c2 === v || c2 === _ ? c2 = f : (c2 = m, r2 = void 0);
|
||||||
|
const x2 = c2 === m && t2[i3 + 1].startsWith("/>") ? " " : "";
|
||||||
|
l2 += c2 === f ? s3 + n2 : d2 >= 0 ? (o2.push(a2), s3.slice(0, d2) + e + s3.slice(d2) + h + x2) : s3 + h + (-2 === d2 ? i3 : x2);
|
||||||
|
}
|
||||||
|
return [P(t2, l2 + (t2[s2] || "<?>") + (2 === i2 ? "</svg>" : 3 === i2 ? "</math>" : "")), o2];
|
||||||
|
};
|
||||||
|
class N {
|
||||||
|
constructor({ strings: t2, _$litType$: s2 }, n3) {
|
||||||
|
let r2;
|
||||||
|
this.parts = [];
|
||||||
|
let c2 = 0, a2 = 0;
|
||||||
|
const u2 = t2.length - 1, d2 = this.parts, [f2, v2] = V(t2, s2);
|
||||||
|
if (this.el = N.createElement(f2, n3), C.currentNode = this.el.content, 2 === s2 || 3 === s2) {
|
||||||
|
const t3 = this.el.content.firstChild;
|
||||||
|
t3.replaceWith(...t3.childNodes);
|
||||||
|
}
|
||||||
|
for (; null !== (r2 = C.nextNode()) && d2.length < u2; ) {
|
||||||
|
if (1 === r2.nodeType) {
|
||||||
|
if (r2.hasAttributes()) for (const t3 of r2.getAttributeNames()) if (t3.endsWith(e)) {
|
||||||
|
const i2 = v2[a2++], s3 = r2.getAttribute(t3).split(h), e2 = /([.?@])?(.*)/.exec(i2);
|
||||||
|
d2.push({ type: 1, index: c2, name: e2[2], strings: s3, ctor: "." === e2[1] ? H : "?" === e2[1] ? I : "@" === e2[1] ? L : k }), r2.removeAttribute(t3);
|
||||||
|
} else t3.startsWith(h) && (d2.push({ type: 6, index: c2 }), r2.removeAttribute(t3));
|
||||||
|
if ($.test(r2.tagName)) {
|
||||||
|
const t3 = r2.textContent.split(h), s3 = t3.length - 1;
|
||||||
|
if (s3 > 0) {
|
||||||
|
r2.textContent = i$1 ? i$1.emptyScript : "";
|
||||||
|
for (let i2 = 0; i2 < s3; i2++) r2.append(t3[i2], l()), C.nextNode(), d2.push({ type: 2, index: ++c2 });
|
||||||
|
r2.append(t3[s3], l());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (8 === r2.nodeType) if (r2.data === o$1) d2.push({ type: 2, index: c2 });
|
||||||
|
else {
|
||||||
|
let t3 = -1;
|
||||||
|
for (; -1 !== (t3 = r2.data.indexOf(h, t3 + 1)); ) d2.push({ type: 7, index: c2 }), t3 += h.length - 1;
|
||||||
|
}
|
||||||
|
c2++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static createElement(t2, i2) {
|
||||||
|
const s2 = r.createElement("template");
|
||||||
|
return s2.innerHTML = t2, s2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function S(t2, i2, s2 = t2, e2) {
|
||||||
|
var _a2, _b;
|
||||||
|
if (i2 === T) return i2;
|
||||||
|
let h2 = void 0 !== e2 ? (_a2 = s2._$Co) == null ? void 0 : _a2[e2] : s2._$Cl;
|
||||||
|
const o2 = c(i2) ? void 0 : i2._$litDirective$;
|
||||||
|
return (h2 == null ? void 0 : h2.constructor) !== o2 && ((_b = h2 == null ? void 0 : h2._$AO) == null ? void 0 : _b.call(h2, false), void 0 === o2 ? h2 = void 0 : (h2 = new o2(t2), h2._$AT(t2, s2, e2)), void 0 !== e2 ? (s2._$Co ?? (s2._$Co = []))[e2] = h2 : s2._$Cl = h2), void 0 !== h2 && (i2 = S(t2, h2._$AS(t2, i2.values), h2, e2)), i2;
|
||||||
|
}
|
||||||
|
class M {
|
||||||
|
constructor(t2, i2) {
|
||||||
|
this._$AV = [], this._$AN = void 0, this._$AD = t2, this._$AM = i2;
|
||||||
|
}
|
||||||
|
get parentNode() {
|
||||||
|
return this._$AM.parentNode;
|
||||||
|
}
|
||||||
|
get _$AU() {
|
||||||
|
return this._$AM._$AU;
|
||||||
|
}
|
||||||
|
u(t2) {
|
||||||
|
const { el: { content: i2 }, parts: s2 } = this._$AD, e2 = ((t2 == null ? void 0 : t2.creationScope) ?? r).importNode(i2, true);
|
||||||
|
C.currentNode = e2;
|
||||||
|
let h2 = C.nextNode(), o2 = 0, n3 = 0, l2 = s2[0];
|
||||||
|
for (; void 0 !== l2; ) {
|
||||||
|
if (o2 === l2.index) {
|
||||||
|
let i3;
|
||||||
|
2 === l2.type ? i3 = new R(h2, h2.nextSibling, this, t2) : 1 === l2.type ? i3 = new l2.ctor(h2, l2.name, l2.strings, this, t2) : 6 === l2.type && (i3 = new z(h2, this, t2)), this._$AV.push(i3), l2 = s2[++n3];
|
||||||
|
}
|
||||||
|
o2 !== (l2 == null ? void 0 : l2.index) && (h2 = C.nextNode(), o2++);
|
||||||
|
}
|
||||||
|
return C.currentNode = r, e2;
|
||||||
|
}
|
||||||
|
p(t2) {
|
||||||
|
let i2 = 0;
|
||||||
|
for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t2, s2, i2), i2 += s2.strings.length - 2) : s2._$AI(t2[i2])), i2++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class R {
|
||||||
|
get _$AU() {
|
||||||
|
var _a2;
|
||||||
|
return ((_a2 = this._$AM) == null ? void 0 : _a2._$AU) ?? this._$Cv;
|
||||||
|
}
|
||||||
|
constructor(t2, i2, s2, e2) {
|
||||||
|
this.type = 2, this._$AH = E, this._$AN = void 0, this._$AA = t2, this._$AB = i2, this._$AM = s2, this.options = e2, this._$Cv = (e2 == null ? void 0 : e2.isConnected) ?? true;
|
||||||
|
}
|
||||||
|
get parentNode() {
|
||||||
|
let t2 = this._$AA.parentNode;
|
||||||
|
const i2 = this._$AM;
|
||||||
|
return void 0 !== i2 && 11 === (t2 == null ? void 0 : t2.nodeType) && (t2 = i2.parentNode), t2;
|
||||||
|
}
|
||||||
|
get startNode() {
|
||||||
|
return this._$AA;
|
||||||
|
}
|
||||||
|
get endNode() {
|
||||||
|
return this._$AB;
|
||||||
|
}
|
||||||
|
_$AI(t2, i2 = this) {
|
||||||
|
t2 = S(this, t2, i2), c(t2) ? t2 === E || null == t2 || "" === t2 ? (this._$AH !== E && this._$AR(), this._$AH = E) : t2 !== this._$AH && t2 !== T && this._(t2) : void 0 !== t2._$litType$ ? this.$(t2) : void 0 !== t2.nodeType ? this.T(t2) : u(t2) ? this.k(t2) : this._(t2);
|
||||||
|
}
|
||||||
|
O(t2) {
|
||||||
|
return this._$AA.parentNode.insertBefore(t2, this._$AB);
|
||||||
|
}
|
||||||
|
T(t2) {
|
||||||
|
this._$AH !== t2 && (this._$AR(), this._$AH = this.O(t2));
|
||||||
|
}
|
||||||
|
_(t2) {
|
||||||
|
this._$AH !== E && c(this._$AH) ? this._$AA.nextSibling.data = t2 : this.T(r.createTextNode(t2)), this._$AH = t2;
|
||||||
|
}
|
||||||
|
$(t2) {
|
||||||
|
var _a2;
|
||||||
|
const { values: i2, _$litType$: s2 } = t2, e2 = "number" == typeof s2 ? this._$AC(t2) : (void 0 === s2.el && (s2.el = N.createElement(P(s2.h, s2.h[0]), this.options)), s2);
|
||||||
|
if (((_a2 = this._$AH) == null ? void 0 : _a2._$AD) === e2) this._$AH.p(i2);
|
||||||
|
else {
|
||||||
|
const t3 = new M(e2, this), s3 = t3.u(this.options);
|
||||||
|
t3.p(i2), this.T(s3), this._$AH = t3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_$AC(t2) {
|
||||||
|
let i2 = A.get(t2.strings);
|
||||||
|
return void 0 === i2 && A.set(t2.strings, i2 = new N(t2)), i2;
|
||||||
|
}
|
||||||
|
k(t2) {
|
||||||
|
a(this._$AH) || (this._$AH = [], this._$AR());
|
||||||
|
const i2 = this._$AH;
|
||||||
|
let s2, e2 = 0;
|
||||||
|
for (const h2 of t2) e2 === i2.length ? i2.push(s2 = new R(this.O(l()), this.O(l()), this, this.options)) : s2 = i2[e2], s2._$AI(h2), e2++;
|
||||||
|
e2 < i2.length && (this._$AR(s2 && s2._$AB.nextSibling, e2), i2.length = e2);
|
||||||
|
}
|
||||||
|
_$AR(t2 = this._$AA.nextSibling, i2) {
|
||||||
|
var _a2;
|
||||||
|
for ((_a2 = this._$AP) == null ? void 0 : _a2.call(this, false, true, i2); t2 && t2 !== this._$AB; ) {
|
||||||
|
const i3 = t2.nextSibling;
|
||||||
|
t2.remove(), t2 = i3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setConnected(t2) {
|
||||||
|
var _a2;
|
||||||
|
void 0 === this._$AM && (this._$Cv = t2, (_a2 = this._$AP) == null ? void 0 : _a2.call(this, t2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class k {
|
||||||
|
get tagName() {
|
||||||
|
return this.element.tagName;
|
||||||
|
}
|
||||||
|
get _$AU() {
|
||||||
|
return this._$AM._$AU;
|
||||||
|
}
|
||||||
|
constructor(t2, i2, s2, e2, h2) {
|
||||||
|
this.type = 1, this._$AH = E, this._$AN = void 0, this.element = t2, this.name = i2, this._$AM = e2, this.options = h2, s2.length > 2 || "" !== s2[0] || "" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = E;
|
||||||
|
}
|
||||||
|
_$AI(t2, i2 = this, s2, e2) {
|
||||||
|
const h2 = this.strings;
|
||||||
|
let o2 = false;
|
||||||
|
if (void 0 === h2) t2 = S(this, t2, i2, 0), o2 = !c(t2) || t2 !== this._$AH && t2 !== T, o2 && (this._$AH = t2);
|
||||||
|
else {
|
||||||
|
const e3 = t2;
|
||||||
|
let n3, r2;
|
||||||
|
for (t2 = h2[0], n3 = 0; n3 < h2.length - 1; n3++) r2 = S(this, e3[s2 + n3], i2, n3), r2 === T && (r2 = this._$AH[n3]), o2 || (o2 = !c(r2) || r2 !== this._$AH[n3]), r2 === E ? t2 = E : t2 !== E && (t2 += (r2 ?? "") + h2[n3 + 1]), this._$AH[n3] = r2;
|
||||||
|
}
|
||||||
|
o2 && !e2 && this.j(t2);
|
||||||
|
}
|
||||||
|
j(t2) {
|
||||||
|
t2 === E ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t2 ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class H extends k {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments), this.type = 3;
|
||||||
|
}
|
||||||
|
j(t2) {
|
||||||
|
this.element[this.name] = t2 === E ? void 0 : t2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class I extends k {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments), this.type = 4;
|
||||||
|
}
|
||||||
|
j(t2) {
|
||||||
|
this.element.toggleAttribute(this.name, !!t2 && t2 !== E);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class L extends k {
|
||||||
|
constructor(t2, i2, s2, e2, h2) {
|
||||||
|
super(t2, i2, s2, e2, h2), this.type = 5;
|
||||||
|
}
|
||||||
|
_$AI(t2, i2 = this) {
|
||||||
|
if ((t2 = S(this, t2, i2, 0) ?? E) === T) return;
|
||||||
|
const s2 = this._$AH, e2 = t2 === E && s2 !== E || t2.capture !== s2.capture || t2.once !== s2.once || t2.passive !== s2.passive, h2 = t2 !== E && (s2 === E || e2);
|
||||||
|
e2 && this.element.removeEventListener(this.name, this, s2), h2 && this.element.addEventListener(this.name, this, t2), this._$AH = t2;
|
||||||
|
}
|
||||||
|
handleEvent(t2) {
|
||||||
|
var _a2;
|
||||||
|
"function" == typeof this._$AH ? this._$AH.call(((_a2 = this.options) == null ? void 0 : _a2.host) ?? this.element, t2) : this._$AH.handleEvent(t2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class z {
|
||||||
|
constructor(t2, i2, s2) {
|
||||||
|
this.element = t2, this.type = 6, this._$AN = void 0, this._$AM = i2, this.options = s2;
|
||||||
|
}
|
||||||
|
get _$AU() {
|
||||||
|
return this._$AM._$AU;
|
||||||
|
}
|
||||||
|
_$AI(t2) {
|
||||||
|
S(this, t2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const j = t.litHtmlPolyfillSupport;
|
||||||
|
j == null ? void 0 : j(N, R), (t.litHtmlVersions ?? (t.litHtmlVersions = [])).push("3.3.0");
|
||||||
|
const B = (t2, i2, s2) => {
|
||||||
|
const e2 = (s2 == null ? void 0 : s2.renderBefore) ?? i2;
|
||||||
|
let h2 = e2._$litPart$;
|
||||||
|
if (void 0 === h2) {
|
||||||
|
const t3 = (s2 == null ? void 0 : s2.renderBefore) ?? null;
|
||||||
|
e2._$litPart$ = h2 = new R(i2.insertBefore(l(), t3), t3, void 0, s2 ?? {});
|
||||||
|
}
|
||||||
|
return h2._$AI(t2), h2;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const s = globalThis;
|
||||||
|
class i extends y$1 {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
var _a2;
|
||||||
|
const t2 = super.createRenderRoot();
|
||||||
|
return (_a2 = this.renderOptions).renderBefore ?? (_a2.renderBefore = t2.firstChild), t2;
|
||||||
|
}
|
||||||
|
update(t2) {
|
||||||
|
const r2 = this.render();
|
||||||
|
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t2), this._$Do = B(r2, this.renderRoot, this.renderOptions);
|
||||||
|
}
|
||||||
|
connectedCallback() {
|
||||||
|
var _a2;
|
||||||
|
super.connectedCallback(), (_a2 = this._$Do) == null ? void 0 : _a2.setConnected(true);
|
||||||
|
}
|
||||||
|
disconnectedCallback() {
|
||||||
|
var _a2;
|
||||||
|
super.disconnectedCallback(), (_a2 = this._$Do) == null ? void 0 : _a2.setConnected(false);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i._$litElement$ = true, i["finalized"] = true, (_a = s.litElementHydrateSupport) == null ? void 0 : _a.call(s, { LitElement: i });
|
||||||
|
const o = s.litElementPolyfillSupport;
|
||||||
|
o == null ? void 0 : o({ LitElement: i });
|
||||||
|
(s.litElementVersions ?? (s.litElementVersions = [])).push("4.2.0");
|
||||||
|
export {
|
||||||
|
E,
|
||||||
|
f$1 as f,
|
||||||
|
i,
|
||||||
|
u$1 as u,
|
||||||
|
x
|
||||||
|
};
|
||||||
47
crates/frontend/public/assets/js/property-D0NJdseG.mjs
Normal file
47
crates/frontend/public/assets/js/property-D0NJdseG.mjs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { f, u } from "./lit-z6_uA4GX.mjs";
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const t = (t2) => (e, o2) => {
|
||||||
|
void 0 !== o2 ? o2.addInitializer(() => {
|
||||||
|
customElements.define(t2, e);
|
||||||
|
}) : customElements.define(t2, e);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const o = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f }, r = (t2 = o, e, r2) => {
|
||||||
|
const { kind: n2, metadata: i } = r2;
|
||||||
|
let s = globalThis.litPropertyMetadata.get(i);
|
||||||
|
if (void 0 === s && globalThis.litPropertyMetadata.set(i, s = /* @__PURE__ */ new Map()), "setter" === n2 && ((t2 = Object.create(t2)).wrapped = true), s.set(r2.name, t2), "accessor" === n2) {
|
||||||
|
const { name: o2 } = r2;
|
||||||
|
return { set(r3) {
|
||||||
|
const n3 = e.get.call(this);
|
||||||
|
e.set.call(this, r3), this.requestUpdate(o2, n3, t2);
|
||||||
|
}, init(e2) {
|
||||||
|
return void 0 !== e2 && this.C(o2, void 0, t2, e2), e2;
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
if ("setter" === n2) {
|
||||||
|
const { name: o2 } = r2;
|
||||||
|
return function(r3) {
|
||||||
|
const n3 = this[o2];
|
||||||
|
e.call(this, r3), this.requestUpdate(o2, n3, t2);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw Error("Unsupported decorator location: " + n2);
|
||||||
|
};
|
||||||
|
function n(t2) {
|
||||||
|
return (e, o2) => "object" == typeof o2 ? r(t2, e, o2) : ((t3, e2, o3) => {
|
||||||
|
const r2 = e2.hasOwnProperty(o3);
|
||||||
|
return e2.constructor.createProperty(o3, t3), r2 ? Object.getOwnPropertyDescriptor(e2, o3) : void 0;
|
||||||
|
})(t2, e, o2);
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
n,
|
||||||
|
t
|
||||||
|
};
|
||||||
128
crates/frontend/public/assets/js/ref-CPp9J0V5.mjs
Normal file
128
crates/frontend/public/assets/js/ref-CPp9J0V5.mjs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { E } from "./lit-z6_uA4GX.mjs";
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2020 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const f$1 = (o2) => void 0 === o2.strings;
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const t = { CHILD: 2 }, e$1 = (t2) => (...e2) => ({ _$litDirective$: t2, values: e2 });
|
||||||
|
class i {
|
||||||
|
constructor(t2) {
|
||||||
|
}
|
||||||
|
get _$AU() {
|
||||||
|
return this._$AM._$AU;
|
||||||
|
}
|
||||||
|
_$AT(t2, e2, i2) {
|
||||||
|
this._$Ct = t2, this._$AM = e2, this._$Ci = i2;
|
||||||
|
}
|
||||||
|
_$AS(t2, e2) {
|
||||||
|
return this.update(t2, e2);
|
||||||
|
}
|
||||||
|
update(t2, e2) {
|
||||||
|
return this.render(...e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const s = (i2, t2) => {
|
||||||
|
var _a;
|
||||||
|
const e2 = i2._$AN;
|
||||||
|
if (void 0 === e2) return false;
|
||||||
|
for (const i3 of e2) (_a = i3._$AO) == null ? void 0 : _a.call(i3, t2, false), s(i3, t2);
|
||||||
|
return true;
|
||||||
|
}, o$1 = (i2) => {
|
||||||
|
let t2, e2;
|
||||||
|
do {
|
||||||
|
if (void 0 === (t2 = i2._$AM)) break;
|
||||||
|
e2 = t2._$AN, e2.delete(i2), i2 = t2;
|
||||||
|
} while (0 === (e2 == null ? void 0 : e2.size));
|
||||||
|
}, r = (i2) => {
|
||||||
|
for (let t2; t2 = i2._$AM; i2 = t2) {
|
||||||
|
let e2 = t2._$AN;
|
||||||
|
if (void 0 === e2) t2._$AN = e2 = /* @__PURE__ */ new Set();
|
||||||
|
else if (e2.has(i2)) break;
|
||||||
|
e2.add(i2), c(t2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function h$1(i2) {
|
||||||
|
void 0 !== this._$AN ? (o$1(this), this._$AM = i2, r(this)) : this._$AM = i2;
|
||||||
|
}
|
||||||
|
function n$1(i2, t2 = false, e2 = 0) {
|
||||||
|
const r2 = this._$AH, h2 = this._$AN;
|
||||||
|
if (void 0 !== h2 && 0 !== h2.size) if (t2) if (Array.isArray(r2)) for (let i3 = e2; i3 < r2.length; i3++) s(r2[i3], false), o$1(r2[i3]);
|
||||||
|
else null != r2 && (s(r2, false), o$1(r2));
|
||||||
|
else s(this, i2);
|
||||||
|
}
|
||||||
|
const c = (i2) => {
|
||||||
|
i2.type == t.CHILD && (i2._$AP ?? (i2._$AP = n$1), i2._$AQ ?? (i2._$AQ = h$1));
|
||||||
|
};
|
||||||
|
class f extends i {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments), this._$AN = void 0;
|
||||||
|
}
|
||||||
|
_$AT(i2, t2, e2) {
|
||||||
|
super._$AT(i2, t2, e2), r(this), this.isConnected = i2._$AU;
|
||||||
|
}
|
||||||
|
_$AO(i2, t2 = true) {
|
||||||
|
var _a, _b;
|
||||||
|
i2 !== this.isConnected && (this.isConnected = i2, i2 ? (_a = this.reconnected) == null ? void 0 : _a.call(this) : (_b = this.disconnected) == null ? void 0 : _b.call(this)), t2 && (s(this, i2), o$1(this));
|
||||||
|
}
|
||||||
|
setValue(t2) {
|
||||||
|
if (f$1(this._$Ct)) this._$Ct._$AI(t2, this);
|
||||||
|
else {
|
||||||
|
const i2 = [...this._$Ct._$AH];
|
||||||
|
i2[this._$Ci] = t2, this._$Ct._$AI(i2, this, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disconnected() {
|
||||||
|
}
|
||||||
|
reconnected() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2020 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const e = () => new h();
|
||||||
|
class h {
|
||||||
|
}
|
||||||
|
const o = /* @__PURE__ */ new WeakMap(), n = e$1(class extends f {
|
||||||
|
render(i2) {
|
||||||
|
return E;
|
||||||
|
}
|
||||||
|
update(i2, [s2]) {
|
||||||
|
var _a;
|
||||||
|
const e2 = s2 !== this.G;
|
||||||
|
return e2 && void 0 !== this.G && this.rt(void 0), (e2 || this.lt !== this.ct) && (this.G = s2, this.ht = (_a = i2.options) == null ? void 0 : _a.host, this.rt(this.ct = i2.element)), E;
|
||||||
|
}
|
||||||
|
rt(t2) {
|
||||||
|
if (this.isConnected || (t2 = void 0), "function" == typeof this.G) {
|
||||||
|
const i2 = this.ht ?? globalThis;
|
||||||
|
let s2 = o.get(i2);
|
||||||
|
void 0 === s2 && (s2 = /* @__PURE__ */ new WeakMap(), o.set(i2, s2)), void 0 !== s2.get(this.G) && this.G.call(this.ht, void 0), s2.set(this.G, t2), void 0 !== t2 && this.G.call(this.ht, t2);
|
||||||
|
} else this.G.value = t2;
|
||||||
|
}
|
||||||
|
get lt() {
|
||||||
|
var _a, _b;
|
||||||
|
return "function" == typeof this.G ? (_a = o.get(this.ht ?? globalThis)) == null ? void 0 : _a.get(this.G) : (_b = this.G) == null ? void 0 : _b.value;
|
||||||
|
}
|
||||||
|
disconnected() {
|
||||||
|
this.lt === this.ct && this.rt(void 0);
|
||||||
|
}
|
||||||
|
reconnected() {
|
||||||
|
this.rt(this.ct);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export {
|
||||||
|
e,
|
||||||
|
n
|
||||||
|
};
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import { f, u as _, E as $ } from "./lit-CWlWuEHk.mjs";
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const T = (t) => (e, s) => {
|
|
||||||
s !== void 0 ? s.addInitializer(() => {
|
|
||||||
customElements.define(t, e);
|
|
||||||
}) : customElements.define(t, e);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const A = { attribute: !0, type: String, converter: _, reflect: !1, hasChanged: f }, p = (t = A, e, s) => {
|
|
||||||
const { kind: i, metadata: n } = s;
|
|
||||||
let r = globalThis.litPropertyMetadata.get(n);
|
|
||||||
if (r === void 0 && globalThis.litPropertyMetadata.set(n, r = /* @__PURE__ */ new Map()), i === "setter" && ((t = Object.create(t)).wrapped = !0), r.set(s.name, t), i === "accessor") {
|
|
||||||
const { name: o } = s;
|
|
||||||
return { set(h) {
|
|
||||||
const l = e.get.call(this);
|
|
||||||
e.set.call(this, h), this.requestUpdate(o, l, t);
|
|
||||||
}, init(h) {
|
|
||||||
return h !== void 0 && this.C(o, void 0, t, h), h;
|
|
||||||
} };
|
|
||||||
}
|
|
||||||
if (i === "setter") {
|
|
||||||
const { name: o } = s;
|
|
||||||
return function(h) {
|
|
||||||
const l = this[o];
|
|
||||||
e.call(this, h), this.requestUpdate(o, l, t);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
throw Error("Unsupported decorator location: " + i);
|
|
||||||
};
|
|
||||||
function O(t) {
|
|
||||||
return (e, s) => typeof s == "object" ? p(t, e, s) : ((i, n, r) => {
|
|
||||||
const o = n.hasOwnProperty(r);
|
|
||||||
return n.constructor.createProperty(r, i), o ? Object.getOwnPropertyDescriptor(n, r) : void 0;
|
|
||||||
})(t, e, s);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2020 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const v = (t) => t.strings === void 0;
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const g = { CHILD: 2 }, C = (t) => (...e) => ({ _$litDirective$: t, values: e });
|
|
||||||
class m {
|
|
||||||
constructor(e) {
|
|
||||||
}
|
|
||||||
get _$AU() {
|
|
||||||
return this._$AM._$AU;
|
|
||||||
}
|
|
||||||
_$AT(e, s, i) {
|
|
||||||
this._$Ct = e, this._$AM = s, this._$Ci = i;
|
|
||||||
}
|
|
||||||
_$AS(e, s) {
|
|
||||||
return this.update(e, s);
|
|
||||||
}
|
|
||||||
update(e, s) {
|
|
||||||
return this.render(...s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const c = (t, e) => {
|
|
||||||
var i;
|
|
||||||
const s = t._$AN;
|
|
||||||
if (s === void 0) return !1;
|
|
||||||
for (const n of s) (i = n._$AO) == null || i.call(n, e, !1), c(n, e);
|
|
||||||
return !0;
|
|
||||||
}, a = (t) => {
|
|
||||||
let e, s;
|
|
||||||
do {
|
|
||||||
if ((e = t._$AM) === void 0) break;
|
|
||||||
s = e._$AN, s.delete(t), t = e;
|
|
||||||
} while ((s == null ? void 0 : s.size) === 0);
|
|
||||||
}, u = (t) => {
|
|
||||||
for (let e; e = t._$AM; t = e) {
|
|
||||||
let s = e._$AN;
|
|
||||||
if (s === void 0) e._$AN = s = /* @__PURE__ */ new Set();
|
|
||||||
else if (s.has(t)) break;
|
|
||||||
s.add(t), M(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
function y(t) {
|
|
||||||
this._$AN !== void 0 ? (a(this), this._$AM = t, u(this)) : this._$AM = t;
|
|
||||||
}
|
|
||||||
function G(t, e = !1, s = 0) {
|
|
||||||
const i = this._$AH, n = this._$AN;
|
|
||||||
if (n !== void 0 && n.size !== 0) if (e) if (Array.isArray(i)) for (let r = s; r < i.length; r++) c(i[r], !1), a(i[r]);
|
|
||||||
else i != null && (c(i, !1), a(i));
|
|
||||||
else c(this, t);
|
|
||||||
}
|
|
||||||
const M = (t) => {
|
|
||||||
t.type == g.CHILD && (t._$AP ?? (t._$AP = G), t._$AQ ?? (t._$AQ = y));
|
|
||||||
};
|
|
||||||
class b extends m {
|
|
||||||
constructor() {
|
|
||||||
super(...arguments), this._$AN = void 0;
|
|
||||||
}
|
|
||||||
_$AT(e, s, i) {
|
|
||||||
super._$AT(e, s, i), u(this), this.isConnected = e._$AU;
|
|
||||||
}
|
|
||||||
_$AO(e, s = !0) {
|
|
||||||
var i, n;
|
|
||||||
e !== this.isConnected && (this.isConnected = e, e ? (i = this.reconnected) == null || i.call(this) : (n = this.disconnected) == null || n.call(this)), s && (c(this, e), a(this));
|
|
||||||
}
|
|
||||||
setValue(e) {
|
|
||||||
if (v(this._$Ct)) this._$Ct._$AI(e, this);
|
|
||||||
else {
|
|
||||||
const s = [...this._$Ct._$AH];
|
|
||||||
s[this._$Ci] = e, this._$Ct._$AI(s, this, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
disconnected() {
|
|
||||||
}
|
|
||||||
reconnected() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2020 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const P = () => new w();
|
|
||||||
class w {
|
|
||||||
}
|
|
||||||
const d = /* @__PURE__ */ new WeakMap(), U = C(class extends b {
|
|
||||||
render(t) {
|
|
||||||
return $;
|
|
||||||
}
|
|
||||||
update(t, [e]) {
|
|
||||||
var i;
|
|
||||||
const s = e !== this.G;
|
|
||||||
return s && this.G !== void 0 && this.rt(void 0), (s || this.lt !== this.ct) && (this.G = e, this.ht = (i = t.options) == null ? void 0 : i.host, this.rt(this.ct = t.element)), $;
|
|
||||||
}
|
|
||||||
rt(t) {
|
|
||||||
if (this.isConnected || (t = void 0), typeof this.G == "function") {
|
|
||||||
const e = this.ht ?? globalThis;
|
|
||||||
let s = d.get(e);
|
|
||||||
s === void 0 && (s = /* @__PURE__ */ new WeakMap(), d.set(e, s)), s.get(this.G) !== void 0 && this.G.call(this.ht, void 0), s.set(this.G, t), t !== void 0 && this.G.call(this.ht, t);
|
|
||||||
} else this.G.value = t;
|
|
||||||
}
|
|
||||||
get lt() {
|
|
||||||
var t, e;
|
|
||||||
return typeof this.G == "function" ? (t = d.get(this.ht ?? globalThis)) == null ? void 0 : t.get(this.G) : (e = this.G) == null ? void 0 : e.value;
|
|
||||||
}
|
|
||||||
disconnected() {
|
|
||||||
this.lt === this.ct && this.rt(void 0);
|
|
||||||
}
|
|
||||||
reconnected() {
|
|
||||||
this.rt(this.ct);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
export {
|
|
||||||
O as a,
|
|
||||||
P as e,
|
|
||||||
U as n,
|
|
||||||
T as t
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user