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