Skip to content

Commit 5aa4c9b

Browse files
byquantonalainm23
andauthored
Implement CalDAV support (#1714)
* Check if key is valid color * Start working on general CalDAV support * Start on reworking caldav backend * Store calendar_url in Objects.Project and database * Implemented CalDAV login * Removed legacy is_logged_in from CalDAV.Core * Remove old provider system * Use libsoup authentication methods * Create seperate CalDAVClient instances for each Source * Fix CalDAVSetup Page * Remove buildconfig from repo * Fix authentication not working on first request * Fix a bunch of small bugs * Fix content_type for some requests * Resolve well-known caldav endpoint also on CalDAVSetup * Remove resource-id debugging * Fix item completion request * Integration Testing Concept * Fix Task move * Add option to ignore TLS Certificates for CalDAV/Nextcloud sources Fixes #1697 * Add label in SourceRow to show if ignore_ssl is active * Block https to http downgrade in resolve_well_known_caldav * fix bugs in get_item_by_ical_url * Replace Label with Image * Add database migration for calendar_url * set is_deck to always false * add missing newline --------- Co-authored-by: Alain <[email protected]>
1 parent 10bf7c5 commit 5aa4c9b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1956
-1508
lines changed

.buildconfig

Lines changed: 0 additions & 10 deletions
This file was deleted.

.github/workflows/caldav-test.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: CalDAV Integration Test
2+
3+
on: [pull_request, push]
4+
5+
jobs:
6+
caldav-integration:
7+
runs-on: ubuntu-latest
8+
container:
9+
image: debian:13
10+
options: --privileged
11+
env:
12+
CALDAV_URL: http://localhost:5232/
13+
CALDAV_USER: testuser
14+
CALDAV_PASS: testpass
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
19+
- name: Install dependencies
20+
run: |
21+
apt-get update
22+
apt-get install -y sudo pipx git apache2-utils valac meson ninja-build gettext desktop-file-utils libgtk-4-dev libadwaita-1-dev libgee-0.8-dev libgranite-7-dev libjson-glib-dev libecal2.0-dev libsoup-3.0-dev libwebkitgtk-6.0-dev libgtksourceview-5-dev libportal-dev libportal-gtk4-dev
23+
24+
- name: Install Radicale with pipx
25+
run: |
26+
pipx install radicale[bcrypt]
27+
28+
- name: Prepare Radicale config and users
29+
run: |
30+
mkdir -p /tmp/radicale/collections/testuser/
31+
htpasswd -nbB testuser testpass > /tmp/radicale/users
32+
cat <<EOF > /tmp/radicale/config
33+
[auth]
34+
type = htpasswd
35+
htpasswd_filename = /tmp/radicale/users
36+
htpasswd_encryption = autodetect
37+
[storage]
38+
filesystem_folder = /tmp/radicale/collections
39+
EOF
40+
- name: Start Radicale server
41+
run: |
42+
nohup /github/home/.local/bin/radicale --config /tmp/radicale/config &
43+
44+
- name: Configure and build
45+
run: |
46+
meson setup build || meson setup build --reconfigure
47+
ninja -C build
48+
49+
- name: Run CalDAV integration test
50+
run: |
51+
meson test -C build --suite=caldav-integration --print-errorlogs
52+
53+
- name: Upload Meson test log
54+
uses: actions/upload-artifact@v4
55+
with:
56+
name: meson-test-log
57+
path: build/meson-logs/testlog.txt

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ subprojects/gxml**/
1010
*.snap
1111
planify*txt
1212
.flatpak
13+
.buildconfig

core/Enum.vala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -390,15 +390,15 @@ public enum NewTaskPosition {
390390

391391
public enum CalDAVType {
392392
NEXTCLOUD = 0,
393-
RADICALE = 1;
393+
GENERIC = 1;
394394

395395
public string to_string () {
396396
switch (this) {
397397
case NEXTCLOUD:
398398
return "nextcloud";
399399

400-
case RADICALE:
401-
return "radicale";
400+
case GENERIC:
401+
return "generic";
402402

403403
default:
404404
assert_not_reached ();
@@ -410,8 +410,8 @@ public enum CalDAVType {
410410
case NEXTCLOUD:
411411
return _("Nextcloud");
412412

413-
case RADICALE:
414-
return _("Radicale");
413+
case GENERIC:
414+
return _("CalDAV"); // TODO: Maybe rename Generic to CalDAV?
415415

416416
default:
417417
assert_not_reached ();
@@ -424,7 +424,7 @@ public enum CalDAVType {
424424
return CalDAVType.NEXTCLOUD;
425425

426426
case 1:
427-
return CalDAVType.RADICALE;
427+
return CalDAVType.GENERIC;
428428

429429
default:
430430
return CalDAVType.NEXTCLOUD;
@@ -436,8 +436,8 @@ public enum CalDAVType {
436436
case "nextcloud":
437437
return CalDAVType.NEXTCLOUD;
438438

439-
case "radicale":
440-
return CalDAVType.RADICALE;
439+
case "generic":
440+
return CalDAVType.GENERIC;
441441

442442
default:
443443
return CalDAVType.NEXTCLOUD;

core/Objects/Item.vala

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,17 @@ public class Objects.Item : Objects.BaseObject {
177177
}
178178
}
179179

180-
string _ics = "";
181-
public string ics {
180+
string _ical_url = "";
181+
public string ical_url {
182182
get {
183-
_ics = Services.Todoist.get_default ().get_string_member_by_object (extra_data, "ics");
184-
return _ics;
183+
var json_object = Services.Todoist.get_default ().get_object_by_string (extra_data);
184+
185+
if (json_object.has_member ("ics")) {
186+
_ical_url = "%s/%s".printf (project.calendar_url, json_object.get_string_member ("ics")); // TODO: Should the stored data be migrated?
187+
}else {
188+
_ical_url = Services.Todoist.get_default ().get_string_member_by_object (extra_data, "ical_url");
189+
}
190+
return _ical_url;
185191
}
186192
}
187193

@@ -394,25 +400,16 @@ public class Objects.Item : Objects.BaseObject {
394400
}
395401
}
396402

397-
public Item.from_caldav_xml (GXml.DomElement element, string _project_id) {
398-
GXml.DomElement propstat = element.get_elements_by_tag_name ("d:propstat").get_element (0);
399-
GXml.DomElement prop = propstat.get_elements_by_tag_name ("d:prop").get_element (0);
400-
string data = prop.get_elements_by_tag_name ("cal:calendar-data").get_element (0).text_content;
401-
402-
project_id = _project_id;
403-
patch_from_vtodo (data, Util.get_task_id_from_url (element));
404-
}
405-
406-
public Item.from_vtodo (string data, string _ics, string _project_id) {
403+
public Item.from_vtodo (string data, string _ical_url, string _project_id) {
407404
project_id = _project_id;
408-
patch_from_vtodo (data, _ics);
405+
patch_from_vtodo (data, _ical_url);
409406
}
410407

411-
public void update_from_vtodo (string data, string _ics) {
412-
patch_from_vtodo (data, _ics, true);
408+
public void update_from_vtodo (string data, string _ical_url) {
409+
patch_from_vtodo (data, _ical_url, true);
413410
}
414411

415-
public void patch_from_vtodo (string data, string _ics, bool is_update = false) {
412+
public void patch_from_vtodo (string data, string _ical_url, bool is_update = false) {
416413
ICal.Component ical = ICal.Parser.parse_string (data);
417414
ICal.Component ? ical_vtodo = ical.get_first_component (ICal.ComponentKind.VTODO_COMPONENT);
418415
ECal.Component ecal = new ECal.Component.from_icalcomponent (ical_vtodo);
@@ -483,7 +480,7 @@ public class Objects.Item : Objects.BaseObject {
483480
pinned = false;
484481
}
485482

486-
extra_data = Util.generate_extra_data (_ics, "", ical.as_ical_string ());
483+
extra_data = Util.generate_extra_data (_ical_url, "", ical.as_ical_string ());
487484

488485
if (is_update) {
489486
check_labels (get_labels_maps_from_caldav (ecal.get_categories_list ()));
@@ -653,8 +650,9 @@ public class Objects.Item : Objects.BaseObject {
653650
Services.Store.instance ().update_item (this, update_id);
654651
});
655652
} else if (project.source_type == SourceType.CALDAV) {
656-
Services.CalDAV.Core.get_default ().add_task.begin (this, true, (obj, res) => {
657-
HttpResponse response = Services.CalDAV.Core.get_default ().add_task.end (res);
653+
var caldav_client = Services.CalDAV.Core.get_default ().get_client (project.source);
654+
caldav_client.add_item.begin (this, true, (obj, res) => {
655+
HttpResponse response = caldav_client.add_item.end (res);
658656

659657
if (response.status) {
660658
Services.Store.instance ().update_item (this, update_id);
@@ -685,8 +683,9 @@ public class Objects.Item : Objects.BaseObject {
685683
loading = false;
686684
});
687685
} else if (project.source_type == SourceType.CALDAV) {
688-
Services.CalDAV.Core.get_default ().add_task.begin (this, true, (obj, res) => {
689-
HttpResponse response = Services.CalDAV.Core.get_default ().add_task.end (res);
686+
var caldav_client = Services.CalDAV.Core.get_default ().get_client (project.source);
687+
caldav_client.add_item.begin (this, true, (obj, res) => {
688+
HttpResponse response = caldav_client.add_item.end (res);
690689

691690
if (response.status) {
692691
Services.Store.instance ().update_item (this, update_id);
@@ -713,8 +712,9 @@ public class Objects.Item : Objects.BaseObject {
713712
loading = false;
714713
});
715714
} else if (project.source_type == SourceType.CALDAV) {
716-
Services.CalDAV.Core.get_default ().add_task.begin (this, true, (obj, res) => {
717-
HttpResponse response = Services.CalDAV.Core.get_default ().add_task.end (res);
715+
var caldav_client = Services.CalDAV.Core.get_default ().get_client (project.source);
716+
caldav_client.add_item.begin (this, true, (obj, res) => {
717+
HttpResponse response = caldav_client.add_item.end (res);
718718

719719
if (response.status) {
720720
Services.Store.instance ().update_item (this, update_id);
@@ -744,8 +744,9 @@ public class Objects.Item : Objects.BaseObject {
744744
private void _update_pin () {
745745
if (project.source_type == SourceType.CALDAV) {
746746
loading = true;
747-
Services.CalDAV.Core.get_default ().add_task.begin (this, true, (obj, res) => {
748-
HttpResponse response = Services.CalDAV.Core.get_default ().add_task.end (res);
747+
var caldav_client = Services.CalDAV.Core.get_default ().get_client (project.source);
748+
caldav_client.add_item.begin (this, true, (obj, res) => {
749+
HttpResponse response = caldav_client.add_item.end (res);
749750

750751
if (response.status) {
751752
Services.Store.instance ().update_item_pin (this);
@@ -1217,7 +1218,9 @@ public class Objects.Item : Objects.BaseObject {
12171218
if (checked) {
12181219
ical.set_status (ICal.PropertyStatus.COMPLETED);
12191220
ical.add_property (new ICal.Property.percentcomplete (100));
1220-
ical.add_property (new ICal.Property.completed (new ICal.Time.today ()));
1221+
// RFC requires Date-Time (https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.1)
1222+
// Nextcloud also accepted .today () which didn't include the Timezone, but Radicale and probably other CalDAV implementations want Date-Time
1223+
ical.add_property (new ICal.Property.completed (new ICal.Time.current_with_zone (null)));
12211224
} else {
12221225
ical.set_status (ICal.PropertyStatus.NEEDSACTION);
12231226
}
@@ -1336,8 +1339,9 @@ public class Objects.Item : Objects.BaseObject {
13361339
});
13371340
} else if (project.source_type == SourceType.CALDAV) {
13381341
loading = true;
1339-
Services.CalDAV.Core.get_default ().delete_task.begin (this, (obj, res) => {
1340-
HttpResponse response = Services.CalDAV.Core.get_default ().delete_task.end (res);
1342+
var caldav_client = Services.CalDAV.Core.get_default ().get_client (project.source);
1343+
caldav_client.delete_item.begin (this, (obj, res) => {
1344+
HttpResponse response = caldav_client.delete_item.end (res);
13411345
loading = false;
13421346

13431347
if (response.status) {
@@ -1446,8 +1450,9 @@ public class Objects.Item : Objects.BaseObject {
14461450
});
14471451
} else if (project.source_type == SourceType.CALDAV) {
14481452
loading = true;
1449-
Services.CalDAV.Core.get_default ().add_task.begin (this, true, (obj, res) => {
1450-
var response = Services.CalDAV.Core.get_default ().add_task.end (res);
1453+
var caldav_client = Services.CalDAV.Core.get_default ().get_client (project.source);
1454+
caldav_client.add_item.begin (this, true, (obj, res) => {
1455+
var response = caldav_client.add_item.end (res);
14511456
loading = false;
14521457

14531458
if (response.status) {
@@ -1484,8 +1489,9 @@ public class Objects.Item : Objects.BaseObject {
14841489
}
14851490
});
14861491
} else if (project.source_type == SourceType.CALDAV) {
1487-
Services.CalDAV.Core.get_default ().move_task.begin (this, project.id, (obj, res) => {
1488-
var response = Services.CalDAV.Core.get_default ().move_task.end (res);
1492+
var caldav_client = Services.CalDAV.Core.get_default ().get_client (project.source);
1493+
caldav_client.move_item.begin (this, project, (obj, res) => {
1494+
var response = caldav_client.move_item.end (res);
14891495
loading = false;
14901496
show_item = true;
14911497

@@ -1633,7 +1639,8 @@ public class Objects.Item : Objects.BaseObject {
16331639
if (project.source_type == SourceType.TODOIST) {
16341640
response = yield Services.Todoist.get_default ().complete_item (this);
16351641
} else {
1636-
response = yield Services.CalDAV.Core.get_default ().complete_item (this);
1642+
var caldav_client = Services.CalDAV.Core.get_default ().get_client (project.source);
1643+
response = yield caldav_client.complete_item (this);
16371644
}
16381645

16391646
loading = false;

core/Objects/Label.vala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,9 @@ public class Objects.Label : Objects.BaseObject {
225225
loading = true;
226226
foreach (Objects.Item item in Services.Store.instance ().get_items_by_label (this, false)) {
227227
item.delete_item_label (id);
228-
Services.CalDAV.Core.get_default ().add_task.begin (item, true, (obj, res) => {
229-
if (Services.CalDAV.Core.get_default ().add_task.end (res).status) {
228+
var caldav_client = Services.CalDAV.Core.get_default ().get_client (item.project.source);
229+
caldav_client.add_item.begin (item, true, (obj, res) => {
230+
if (caldav_client.add_item.end (res).status) {
230231
Services.Store.instance ().update_item (item);
231232
}
232233
});

0 commit comments

Comments
 (0)