Skip to content

Commit 9570760

Browse files
committed
fix files
1 parent 167b000 commit 9570760

File tree

2 files changed

+332
-333
lines changed

2 files changed

+332
-333
lines changed

core/Services/CalDAV/Core.vala

Lines changed: 185 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,61 @@
1919
* Authored by: Alain M. <[email protected]>
2020
*/
2121

22-
public class Services.CalDAV.WebDAVClient : GLib.Object {
22+
public class Services.CalDAV.Core : GLib.Object {
2323

24-
protected Soup.Session session;
24+
private Soup.Session session;
25+
private Gee.HashMap<string, Services.CalDAV.CalDAVClient> clients;
2526

26-
protected string username;
27-
protected string password;
28-
protected string base_url;
29-
protected bool ignore_ssl;
27+
private static Core ? _instance;
28+
public static Core get_default () {
29+
if (_instance == null) {
30+
_instance = new Core ();
31+
}
32+
33+
return _instance;
34+
}
35+
36+
public signal void first_sync_started ();
37+
public signal void first_sync_finished ();
38+
39+
40+
public Core () {
41+
session = new Soup.Session ();
42+
clients = new Gee.HashMap<string, Services.CalDAV.CalDAVClient> ();
43+
}
44+
45+
46+
public Services.CalDAV.CalDAVClient get_client (Objects.Source source) {
47+
if (!clients.has_key (source.id)) {
48+
var client = new Services.CalDAV.CalDAVClient (
49+
new Soup.Session (),
50+
source.caldav_data.server_url,
51+
source.caldav_data.username,
52+
source.caldav_data.password,
53+
source.caldav_data.ignore_ssl
54+
);
55+
clients[source.id] = client;
56+
}
57+
return clients[source.id];
58+
}
59+
60+
public Services.CalDAV.CalDAVClient? get_client_by_id (string source_id) {
61+
if (clients.has_key (source_id)) {
62+
return clients[source_id];
63+
}
64+
return null;
65+
}
3066

67+
public void remove_client (string source_id) {
68+
clients.unset (source_id);
69+
}
3170

32-
public WebDAVClient (Soup.Session session, string base_url, string username, string password, bool ignore_ssl = false) {
33-
this.session = session;
34-
this.base_url = base_url;
35-
this.username = username;
36-
this.password = password;
37-
this.ignore_ssl = ignore_ssl;
71+
public void clear () {
72+
clients.clear ();
3873
}
3974

40-
public string get_absolute_url (string href) {
75+
76+
private string make_absolute_url (string base_url, string href) {
4177
string abs_url = null;
4278
try {
4379
abs_url = GLib.Uri.resolve_relative (base_url, href, GLib.UriFlags.NONE).to_string ();
@@ -47,186 +83,187 @@ public class Services.CalDAV.WebDAVClient : GLib.Object {
4783
return abs_url;
4884
}
4985

50-
public async WebDAVMultiStatus propfind (string url, string xml, string depth, GLib.Cancellable cancellable) throws GLib.Error {
51-
return new WebDAVMultiStatus.from_string (yield send_request ("PROPFIND", url, "application/xml", xml, depth, cancellable, { Soup.Status.MULTI_STATUS }));
52-
}
53-
54-
public async WebDAVMultiStatus report (string url, string xml, string depth, GLib.Cancellable cancellable) throws GLib.Error {
55-
return new WebDAVMultiStatus.from_string (yield send_request ("REPORT", url, "application/xml", xml, depth, cancellable, { Soup.Status.MULTI_STATUS }));
56-
}
57-
58-
protected async string send_request (string method, string url, string content_type, string? body, string? depth, GLib.Cancellable? cancellable, Soup.Status[] expected_statuses, HashTable<string,string>? extra_headers = null) throws GLib.Error {
59-
var abs_url = get_absolute_url (url);
60-
if (abs_url == null)
61-
throw new GLib.IOError.FAILED ("Invalid URL: %s".printf (url));
62-
63-
var msg = new Soup.Message (method, abs_url);
86+
public async string resolve_well_known_caldav (Soup.Session session, string base_url, bool ignore_ssl = false) {
87+
var well_known_url = make_absolute_url (base_url, "/.well-known/caldav");
88+
var msg = new Soup.Message ("GET", well_known_url);
6489
msg.request_headers.append ("User-Agent", Constants.SOUP_USER_AGENT);
6590

66-
msg.authenticate.connect ((auth, retrying) => {
67-
if (retrying) {
68-
warning ("Authentication failed\n");
69-
return false;
70-
}
71-
72-
if (auth.scheme_name == "Digest" || auth.scheme_name == "Basic") {
73-
auth.authenticate (this.username, this.password);
74-
return true;
75-
}
76-
warning ("Unsupported auth schema: %s", auth.scheme_name);
77-
return false;
78-
});
79-
80-
// After authentication, the body of the message needs to be set again when the message is resent.
81-
// https://gitlab.gnome.org/GNOME/libsoup/-/issues/358
82-
msg.restarted.connect (() => {
83-
if (body != null) {
84-
msg.set_request_body_from_bytes (content_type, new GLib.Bytes (body.data));
85-
}
86-
});
91+
msg.set_flags (Soup.MessageFlags.NO_REDIRECT);
8792

8893
if (ignore_ssl) {
8994
msg.accept_certificate.connect (() => {
9095
return true;
9196
});
9297
}
9398

94-
if (depth != null) {
95-
msg.request_headers.replace ("Depth", depth);
99+
try {
100+
yield session.send_and_read_async (msg, Priority.DEFAULT, null);
101+
// These are all the redirect status codes.
102+
// https://www.rfc-editor.org/rfc/rfc6764#section-5
103+
// https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
104+
if (msg.status_code == 301 || msg.status_code == 302 || msg.status_code == 307 || msg.status_code == 308 ) {
105+
string? location = msg.response_headers.get_one ("Location");
106+
if (location != null) {
107+
if (location.has_prefix ("/")) {
108+
location = make_absolute_url (base_url, location);
109+
}
110+
111+
// Prevent https → http downgrade
112+
// See https://github.com/alainm23/planify/issues/1149#issuecomment-3236718109
113+
var base_scheme = GLib.Uri.parse_scheme (base_url);
114+
var location_scheme = GLib.Uri.parse_scheme (location);
115+
116+
if (base_scheme == "https" && location_scheme == "http") {
117+
if (location.has_prefix ("http://")) {
118+
warning ("Resolving .well-known/caldav caused a redirect from https to http. Preventing downgrade.");
119+
location = "https" + location.substring (4); // removes http and puts https infront
120+
} else {
121+
warning ("Redirect location has http scheme but unexpected format: %s", location);
122+
return base_url;
123+
}
124+
}
125+
126+
return location;
127+
}
128+
}
129+
return base_url;
130+
} catch (Error e) {
131+
warning ("Failed to check .well-known/caldav: %s", e.message);
132+
return base_url;
96133
}
134+
}
97135

98-
if (extra_headers != null) {
99-
foreach (var key in extra_headers.get_keys ())
100-
msg.request_headers.replace (key, extra_headers.lookup (key));
101-
}
102136

103-
if (body != null) {
104-
msg.set_request_body_from_bytes (content_type, new GLib.Bytes (body.data));
105-
}
137+
public async string? resolve_calendar_home (CalDAVType caldav_type, string dav_url, string username, string password, GLib.Cancellable cancellable, bool ignore_ssl = false) {
138+
var caldav_client = new Services.CalDAV.CalDAVClient (session, dav_url, username, password, ignore_ssl);
106139

107-
GLib.Bytes response = yield session.send_and_read_async (msg, Priority.DEFAULT, cancellable);
140+
try {
141+
string? principal_url = yield caldav_client.get_principal_url (cancellable);
108142

109-
bool ok = false;
110-
foreach (var code in expected_statuses) {
111-
if (msg.status_code == code) {
112-
ok = true;
113-
break;
143+
if (principal_url == null) {
144+
critical ("No principal url received");
145+
return null;
114146
}
147+
148+
var calendar_home = yield caldav_client.get_calendar_home (principal_url, cancellable);
149+
150+
return calendar_home;
151+
} catch (Error e) {
152+
print ("login error: %s".printf (e.message));
153+
return null;
115154
}
155+
}
116156

117-
if (!ok) {
118-
var response_text = (string) response.get_data ();
119-
throw new GLib.IOError.FAILED (
120-
"%s %s failed: HTTP %u %s\n%s".printf (
121-
method, abs_url, msg.status_code, msg.reason_phrase ?? "", response_text ?? "")
122-
);
157+
public async HttpResponse login (CalDAVType caldav_type, string dav_url, string username, string password, string calendar_home, GLib.Cancellable cancellable, bool ignore_ssl = false) {
158+
HttpResponse response = new HttpResponse ();
159+
160+
if (Services.Store.instance ().source_caldav_exists (dav_url, username)) {
161+
response.error_code = 409;
162+
response.error = _("Source already exists");
163+
return response;
123164
}
124165

125-
return (string) response.get_data ();
126-
}
166+
var caldav_client = new Services.CalDAV.CalDAVClient (new Soup.Session (), dav_url, username, password, ignore_ssl);
127167

128-
}
168+
try {
169+
string? principal_url = yield caldav_client.get_principal_url (cancellable);
129170

171+
if (principal_url == null) {
172+
response.error_code = 409;
173+
response.error = _("Failed to resolve principal url");
174+
return response;
175+
}
130176

131-
public class Services.CalDAV.WebDAVMultiStatus : Object {
132-
private GXml.DomElement root;
177+
var source = new Objects.Source ();
178+
source.id = Util.get_default ().generate_id ();
179+
source.source_type = SourceType.CALDAV;
180+
source.last_sync = new GLib.DateTime.now_local ().to_string ();
133181

134-
public WebDAVMultiStatus.from_string (string xml) throws GLib.Error {
135-
print (xml + "\n");
136-
var doc = new GXml.XDocument.from_string (xml);
137-
this.root = doc.document_element;
138-
}
182+
Objects.SourceCalDAVData caldav_data = new Objects.SourceCalDAVData ();
183+
caldav_data.server_url = dav_url;
184+
caldav_data.calendar_home_url = calendar_home;
185+
caldav_data.username = username;
186+
caldav_data.password = password;
187+
caldav_data.caldav_type = caldav_type;
188+
caldav_data.ignore_ssl = ignore_ssl;
139189

140-
public Gee.ArrayList<WebDAVResponse> responses () {
141-
var list = new Gee.ArrayList<WebDAVResponse> ();
142-
foreach (var resp in root.get_elements_by_tag_name ("response")) {
143-
list.add (new WebDAVResponse (resp));
144-
}
145-
return list;
146-
}
190+
source.data = caldav_data;
147191

148-
public string ? get_first_text_content_by_tag_name (string tag_name) {
149-
foreach (var h in root.get_elements_by_tag_name (tag_name)) {
150-
var text = h.text_content.strip ();
151-
if (text != null && text.length > 0) {
152-
return text;
153-
}
192+
GLib.Value _data_object = Value (typeof (Objects.Source));
193+
_data_object.set_object (source);
194+
195+
response.data_object = _data_object;
196+
response.status = true;
197+
198+
clients[source.id] = caldav_client;
199+
} catch (Error e) {
200+
print ("login error: %s".printf (e.message));
201+
response.error_code = e.code;
202+
response.error = e.message;
154203
}
155204

156-
return null;
205+
return response;
157206
}
158-
}
159207

208+
// TODO: why is this a seperate method, can this be merged with login?
209+
public async HttpResponse add_caldav_account (Objects.Source source, GLib.Cancellable cancellable) {
210+
HttpResponse response = new HttpResponse ();
211+
var caldav_client = get_client (source);
160212

161-
public class Services.CalDAV.WebDAVResponse : Object {
162-
public string? href { get; private set; }
163-
private GXml.DomElement element;
213+
first_sync_started ();
164214

165-
public WebDAVResponse (GXml.DomElement element) {
166-
this.element = element;
167-
parse_href ();
168-
}
215+
try {
216+
string? principal_url = yield caldav_client.get_principal_url (cancellable);
169217

170-
private void parse_href () {
171-
foreach (var h in element.get_elements_by_tag_name ("href")) {
172-
var text = h.text_content.strip ();
173-
if (text != null && text.length > 0) {
174-
href = text;
175-
break;
218+
if (principal_url == null) {
219+
response.error_code = 409;
220+
response.error = _("Failed to resolve principal url");
221+
return response;
176222
}
177-
}
178-
}
179223

180-
public Gee.ArrayList<WebDAVPropStat> propstats () {
181-
var results = new Gee.ArrayList<WebDAVPropStat> ();
182-
foreach (var ps in element.get_elements_by_tag_name ("propstat")) {
183-
results.add (new WebDAVPropStat (ps));
184-
}
185-
return results;
186-
}
187-
}
188224

225+
yield caldav_client.update_userdata (principal_url, source, cancellable);
189226

190-
public class Services.CalDAV.WebDAVPropStat : Object {
191-
public Soup.Status status { get; private set; }
192-
public GXml.DomElement prop { get; private set; }
227+
Services.Store.instance ().insert_source (source);
193228

194-
public WebDAVPropStat (GXml.DomElement element) {
195-
var status_list = element.get_elements_by_tag_name ("status");
196-
if (status_list.length == 1) {
197-
var text = status_list[0].text_content.strip ();
198-
if (text != null && text.length > 0)
199-
status = parse_status (text);
200-
}
229+
Gee.ArrayList<Objects.Project> projects = yield caldav_client.fetch_project_list (source, cancellable);
201230

202-
var prop_list = element.get_elements_by_tag_name ("prop");
203-
if (prop_list.length == 1) {
204-
prop = prop_list[0];
205-
}
206-
}
231+
foreach (Objects.Project project in projects) {
232+
Services.Store.instance ().insert_project (project);
233+
yield caldav_client.fetch_items_for_project (project, cancellable);
234+
}
207235

208-
private Soup.Status parse_status (string status_line) {
209-
Soup.HTTPVersion ver;
210-
uint code;
211-
string reason;
236+
first_sync_finished ();
212237

213-
if (Soup.headers_parse_status_line (status_line, out ver, out code, out reason)) {
214-
return (Soup.Status) code;
238+
response.status = true;
239+
} catch (Error e) {
240+
response.error_code = e.code;
241+
response.error = e.message;
242+
debug (e.message);
215243
}
216244

217-
return Soup.Status.NONE;
245+
return response;
218246
}
219247

220-
public GXml.DomElement? get_first_prop_with_tagname (string tagname) {
221-
if (prop == null) {
222-
return null;
223-
}
224248

225-
foreach (var e in prop.get_elements_by_tag_name (tagname)) {
226-
return e;
227-
}
249+
public async void sync (Objects.Source source) {
250+
var caldav_client = get_client (source);
228251

229-
return null;
230-
}
252+
source.sync_started ();
231253

254+
try {
255+
var cancellable = new GLib.Cancellable ();
256+
yield caldav_client.sync (source, cancellable);
257+
258+
foreach (Objects.Project project in Services.Store.instance ().get_projects_by_source (source.id)) {
259+
yield caldav_client.sync_tasklist (project, cancellable);
260+
}
261+
262+
source.sync_finished ();
263+
source.last_sync = new GLib.DateTime.now_local ().to_string ();
264+
} catch (Error e) {
265+
warning ("Failed to sync: %s", e.message);
266+
source.sync_failed ();
267+
}
268+
}
232269
}

0 commit comments

Comments
 (0)