diary

Text-based journaling program
git clone https://git.in0rdr.ch/diary.git
Log | Files | Refs | README | LICENSE

commit ba4099ce31feb15195d6fe6f5a9dc58865275fde
parent bcb5d288053191612654daf3a3e88b2d72e78697
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Sat, 22 Jun 2024 16:42:44 +0200

Merge branch 'feat/caldav'

Diffstat:
Mconfig/diary.cfg | 11+++++++++--
Mman1/diary.1 | 49++++++++++++++++++++++++++++++++++++++++---------
Mman1/diary.1.html | 39++++++++++++++++++++++++++++++---------
Mman1/diary.1.scd | 39++++++++++++++++++++++++++++++---------
Msrc/caldav.c | 301+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/caldav.h | 1-
Msrc/diary-tp.h | 14++++++++++++++
Msrc/diary.c | 25++++++++++++++++++++-----
Msrc/import.c | 10+++++++---
Msrc/utils.c | 12++++++++----
Msrc/utils.h | 10++++++++--
11 files changed, 383 insertions(+), 128 deletions(-)

diff --git a/config/diary.cfg b/config/diary.cfg @@ -14,10 +14,17 @@ fmt_cmd = #no_mouse = false # Editor to open journal files with editor = -# Google calendar name for CalDAV sync -#google_calendar = +# CalDAV server URI +# For example, Google calendar URI: "https://apidata.googleusercontent.com/caldav/v2" +#caldav_server = +# Calendar name for CalDAV sync +#caldav_calendar = # Google OAuth2 clientid and secretid #google_clientid = #google_secretid = # Google OAuth2 tokenfile #google_tokenfile = ~/.diary-token +# CalDAV server username +#caldav_username = +# CalDAV server password +#caldav_password = diff --git a/man1/diary.1 b/man1/diary.1 @@ -294,13 +294,20 @@ fmt_cmd = #no_mouse = false # Editor to open journal files with editor = -# Google calendar name for CalDAV sync -#google_calendar = +# CalDAV server URI +# For example, Google calendar URI: "https://apidata\&.googleusercontent\&.com/caldav/v2" +#caldav_server = +# Calendar name for CalDAV sync +#caldav_calendar = # Google OAuth2 clientid and secretid #google_clientid = #google_secretid = # Google OAuth2 tokenfile #google_tokenfile = ~/\&.diary-token +# CalDAV server username +#caldav_username = +# CalDAV server password +#caldav_password = EOF .fi .RE @@ -332,6 +339,8 @@ l lx lx lx lx l lx lx lx lx l lx lx lx lx l lx lx lx lx +l lx lx lx lx +l lx lx lx lx l lx lx lx lx. T{ \fBCommand Line Option\fR @@ -435,20 +444,20 @@ T} T{ n/a T} T{ -google_calendar +caldav_calendar T} T{ diary T} T{ (empty) T} T{ -Displayname of Google Calendar for \fBCALDAV SYNC\fR +Calendar name for \fBCALDAV SYNC\fR T} T{ n/a T} T{ google_clientid T} T{ -123.\&com + T} T{ built-in at build time (see \fBGOOGLE CALENDAR OAUTH2\fR) or empty T} T{ @@ -459,7 +468,7 @@ n/a T} T{ google_secretid T} T{ -321 + T} T{ built-in at build time (see \fBGOOGLE CALENDAR OAUTH2\fR) or empty T} T{ @@ -476,6 +485,28 @@ T} T{ T} T{ \fBGOOGLE CALENDAR OAUTH2\fR API token file location T} +T{ +n/a +T} T{ +caldav_username +T} T{ + +T} T{ +(empty) +T} T{ +CalDAV server username for \fBCALDAV SYNC\fR +T} +T{ +n/a +T} T{ +caldav_password +T} T{ + +T} T{ +(empty) +T} T{ +CalDAV server password for \fBCALDAV SYNC\fR +T} .TE .sp 1 .SH CUSTOM FORMATTING @@ -522,7 +553,7 @@ takes quite some time for large batches.\& Currently, only the Google Calendar is supported as remote provider.\& .PP The calender for synchronization can be defined with the configuration key -"google_calendar", see \fBCONFIGURATION FILE\fR.\& This key is empty by default.\& +"caldav_calendar", see \fBCONFIGURATION FILE\fR.\& This key is empty by default.\& .PP Read GOOGLE CALENDAR OAUTH2 to setup authorization with the Google server.\& .PP @@ -549,11 +580,11 @@ The application requires two OAuth2 scopes .IP \(bu 4 "https://www.\&googleapis.\&com/auth/calendar": read/share access to Calendars, required to discover the unique hyperlink/URI for the calendar specified by -config key "google_calendar" +config key "caldav_calendar" .IP \(bu 4 "https://www.\&googleapis.\&com/auth/calendar.\&events": read/write access to Events owned by the user, allows diary to create/read/update/delete events in -"google_calendar" +"caldav_calendar" .PD .PP The user is asked for consent during the first CALDAV SYNC.\& diff --git a/man1/diary.1.html b/man1/diary.1.html @@ -236,13 +236,20 @@ fmt_cmd = #no_mouse = false # Editor to open journal files with editor = -# Google calendar name for CalDAV sync -#google_calendar = +# CalDAV server URI +# For example, Google calendar URI: &quot;https://apidata.googleusercontent.com/caldav/v2&quot; +#caldav_server = +# Calendar name for CalDAV sync +#caldav_calendar = # Google OAuth2 clientid and secretid #google_clientid = #google_secretid = # Google OAuth2 tokenfile #google_tokenfile = ~/.diary-token +# CalDAV server username +#caldav_username = +# CalDAV server password +#caldav_password = EOF</pre> </div> <p class="Pp">To copy the sample file from the source repository:</p> @@ -348,22 +355,22 @@ EOF</pre> </tr> <tr> <td>n/a</td> - <td>google_calendar </td> + <td>caldav_calendar</td> <td>diary </td> <td>(empty)</td> - <td>Displayname of Google Calendar for <b>CALDAV SYNC</b></td> + <td>Calendar name for <b>CALDAV SYNC</b></td> </tr> <tr> <td>n/a</td> <td>google_clientid</td> - <td>123.com</td> + <td></td> <td>built-in at build time (see <b>GOOGLE CALENDAR OAUTH2</b>) or empty</td> <td>Google Calendar for <b>GOOGLE CALENDAR OAUTH2</b> clientid</td> </tr> <tr> <td>n/a</td> <td>google_secretid</td> - <td>321</td> + <td></td> <td>built-in at build time (see <b>GOOGLE CALENDAR OAUTH2</b>) or empty</td> <td>Google Calendar for <b>GOOGLE CALENDAR OAUTH2</b> secretid</td> </tr> @@ -374,6 +381,20 @@ EOF</pre> <td>~/.diary-token</td> <td><b>GOOGLE CALENDAR OAUTH2</b> API token file location</td> </tr> + <tr> + <td>n/a</td> + <td>caldav_username</td> + <td></td> + <td>(empty)</td> + <td>CalDAV server username for <b>CALDAV SYNC</b></td> + </tr> + <tr> + <td>n/a</td> + <td>caldav_password</td> + <td></td> + <td>(empty)</td> + <td>CalDAV server password for <b>CALDAV SYNC</b></td> + </tr> </table> <p class="Pp"></p> </section> @@ -429,7 +450,7 @@ EOF</pre> <p class="Pp">Currently, only the Google Calendar is supported as remote provider.</p> <p class="Pp">The calender for synchronization can be defined with the - configuration key &quot;google_calendar&quot;, see <b>CONFIGURATION + configuration key &quot;caldav_calendar&quot;, see <b>CONFIGURATION FILE</b>. This key is empty by default.</p> <p class="Pp">Read GOOGLE CALENDAR OAUTH2 to setup authorization with the Google server.</p> @@ -457,10 +478,10 @@ EOF</pre> <ul class="Bl-bullet"> <li>&quot;https://www.googleapis.com/auth/calendar&quot;: read/share access to Calendars, required to discover the unique hyperlink/URI for the calendar - specified by config key &quot;google_calendar&quot;</li> + specified by config key &quot;caldav_calendar&quot;</li> <li>&quot;https://www.googleapis.com/auth/calendar.events&quot;: read/write access to Events owned by the user, allows diary to - create/read/update/delete events in &quot;google_calendar&quot;</li> + create/read/update/delete events in &quot;caldav_calendar&quot;</li> </ul> <p class="Pp">The user is asked for consent during the first CALDAV SYNC.</p> </section> diff --git a/man1/diary.1.scd b/man1/diary.1.scd @@ -176,13 +176,20 @@ fmt_cmd = #no_mouse = false # Editor to open journal files with editor = -# Google calendar name for CalDAV sync -#google_calendar = +# CalDAV server URI +# For example, Google calendar URI: "https://apidata.googleusercontent.com/caldav/v2" +#caldav_server = +# Calendar name for CalDAV sync +#caldav_calendar = # Google OAuth2 clientid and secretid #google_clientid = #google_secretid = # Google OAuth2 tokenfile #google_tokenfile = ~/.diary-token +# CalDAV server username +#caldav_username = +# CalDAV server password +#caldav_password = EOF ``` @@ -265,18 +272,18 @@ The following configuration keys are currently supported: the first weekday defaults to 1 (Monday), unless specified otherwise with this option. | n/a -: google_calendar +: caldav_calendar : diary : (empty) -: Displayname of Google Calendar for *CALDAV SYNC* +: Calendar name for *CALDAV SYNC* | n/a : google_clientid -: 123.com +: : built-in at build time (see *GOOGLE CALENDAR OAUTH2*) or empty : Google Calendar for *GOOGLE CALENDAR OAUTH2* clientid | n/a : google_secretid -: 321 +: : built-in at build time (see *GOOGLE CALENDAR OAUTH2*) or empty : Google Calendar for *GOOGLE CALENDAR OAUTH2* secretid | n/a @@ -284,6 +291,16 @@ The following configuration keys are currently supported: : ~/.diary-token : ~/.diary-token : *GOOGLE CALENDAR OAUTH2* API token file location +| n/a +: caldav_username +: +: (empty) +: CalDAV server username for *CALDAV SYNC* +| n/a +: caldav_password +: +: (empty) +: CalDAV server password for *CALDAV SYNC* # CUSTOM FORMATTING The preview of the journal entries can be processed with a custom *--fmt-cmd*. @@ -322,6 +339,10 @@ No sync is performed, when both files (local/remote) have the same modification timestamp or no files exists on the server and on disk at all (i.e., no entry for that date). +The diary creates a full-day calendar entry. It's advised to use a separate +calendar for syncing purposes and not to have multiple events for a day on the +remote calendar. The diary only considers the first event of a given day. + Use the EXPORT/IMPORT functionality for more batch oriented processing. Making a network request and deciding the most recently modified entry (local/remote) takes quite some time for large batches. @@ -329,7 +350,7 @@ takes quite some time for large batches. Currently, only the Google Calendar is supported as remote provider. The calender for synchronization can be defined with the configuration key -"google_calendar", see *CONFIGURATION FILE*. This key is empty by default. +"caldav_calendar", see *CONFIGURATION FILE*. This key is empty by default. Read GOOGLE CALENDAR OAUTH2 to setup authorization with the Google server. @@ -354,10 +375,10 @@ The application requires two OAuth2 scopes - "https://www.googleapis.com/auth/calendar": read/share access to Calendars, required to discover the unique hyperlink/URI for the calendar specified by - config key "google_calendar" + config key "caldav_calendar" - "https://www.googleapis.com/auth/calendar.events": read/write access to Events owned by the user, allows diary to create/read/update/delete events in - "google_calendar" + "caldav_calendar" The user is asked for consent during the first CALDAV SYNC. diff --git a/src/caldav.c b/src/caldav.c @@ -93,12 +93,18 @@ char* read_tokenfile() { token_buf = malloc(token_bytes + 1); if (token_buf != NULL) { - fread(token_buf, sizeof(char), token_bytes, token_file); + size_t items_read = fread(token_buf, sizeof(char), token_bytes, token_file); + if (items_read != token_bytes) { + perror("Error while reading in read_tokenfile()"); + fclose(token_file); + return NULL; + } token_buf[token_bytes] = '\0'; update_global_token_vars(token_buf); } else { perror("malloc failed"); + fclose(token_file); return NULL; } fclose(token_file); @@ -407,7 +413,7 @@ char* get_oauth_code(const char* verifier, WINDOW* header) { } /* Make a CalDAV request */ -char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields, int depth) { +char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields, int depth, bool basicauth, char* content_type, long num_redirects) { // only support depths 0 or 1 if (depth < 0 || depth > 1) { return NULL; @@ -429,29 +435,54 @@ char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields if (curl) { // fail if not authenticated, !CURLE_OK curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); - // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); + //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method); + tracepoint(diary, debug_string, "curl_easy_perform() request url", url); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_mem_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&caldav_resp); // construct header fields struct curl_slist *header = NULL; - char bearer_token[strlen("Authorization: Bearer ") + strlen(access_token) + 1]; - sprintf(bearer_token, "Authorization: Bearer %s", access_token); + + // default to basic auth when Google credentials are not set + if (basicauth) { + char basicauth [strlen(CONFIG.caldav_username) + strlen(CONFIG.caldav_password) + 2]; + sprintf(basicauth, "%s:%s", CONFIG.caldav_username, CONFIG.caldav_password); + curl_easy_setopt(curl, CURLOPT_USERPWD, basicauth); + tracepoint(diary, debug_string, "curl_easy_perform() basicauth", basicauth); + } else { + char bearer_token[strlen("Authorization: Bearer ") + strlen(access_token) + 1]; + sprintf(bearer_token, "Authorization: Bearer %s", access_token); + header = curl_slist_append(header, bearer_token); + } + char depth_header[strlen("Depth: 0") + 1]; sprintf(depth_header, "Depth: %i", depth); header = curl_slist_append(header, depth_header); - header = curl_slist_append(header, bearer_token); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header); + if (strcmp(content_type, "") != 0 && content_type != NULL) { + char contentType[strlen("Content-Type: ") + strlen(content_type) + 1]; + sprintf(contentType, "Content-Type: %s", content_type); + header = curl_slist_append(header, contentType); + } + // set postfields, if any if (postfields != NULL) { curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields); } + // enable redirect following, some providers have certain caldav + // endpoinds on sub-paths + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, num_redirects); + res = curl_easy_perform(curl); + long response_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + // free the memory used for the curl slist curl_slist_free_all(header); @@ -459,6 +490,7 @@ char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields if (res != CURLE_OK) { tracepoint(diary, error_string, "curl_easy_perform() failed in caldav_req()", curl_easy_strerror(res)); + tracepoint(diary, debug_long, "curl_easy_perform() response code", response_code); free(caldav_resp.memory); return NULL; } @@ -469,7 +501,7 @@ char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields /* Return current user principal from CalDAV XML response */ char* parse_caldav_current_user_principal(char* xml) { - char* xml_key_pos = strstr(xml, "<D:current-user-principal>"); + char* xml_key_pos = strstr(xml, "current-user-principal"); // this XML does not contain a user principal at all if (xml_key_pos == NULL) { return NULL; @@ -482,39 +514,60 @@ char* parse_caldav_current_user_principal(char* xml) { tok = strtok(NULL, "<"); // D:href>/caldav/v2/test%40gmail.com/user tok = strstr(tok, ">"); // >/caldav/v2/test%40gmail.com/user tok++; // cut > - char* tok_end = strrchr(tok, '/'); - *tok_end = '\0'; // cut /user + //char* tok_end = strrchr(tok, '/'); + //*tok_end = '\0'; // cut /user } return tok; } +/* Return home set uri from CalDAV home set XML response */ +char* parse_home_set(char* xml) { + char* homeset_needle = "calendar-home-set"; + + char* homeset_pos = strstr(xml, homeset_needle); + if (homeset_pos == NULL) { + tracepoint(diary, debug, "parse_home_set() homeset_pos was null"); + return NULL; + } + tracepoint(diary, debug_string, "parse_home_set() homeset_pos", homeset_pos); + + char* href = strrstr(homeset_pos, "href>/"); + if (href != NULL) { + tracepoint(diary, debug_string, "parse_home_set() href", href); + href = strtok(href, "<"); + tracepoint(diary, debug_string, "parse_home_set() href strtok", href); + if (href != NULL) { + href = strchr(href, '>'); + href++; // cut > + } + return href; + } + return NULL; +} + /* Return calendar uri from CalDAV home set XML response */ char* parse_caldav_calendar(char* xml, char* calendar) { - char displayname_needle[strlen(calendar) + strlen("<D:displayname></D:displayname>")]; - sprintf(displayname_needle, "<D:displayname>%s</D:displayname>", calendar); + char displayname_needle[strlen(calendar) + strlen("displayname></") + 1]; + sprintf(displayname_needle, "displayname>%s</", calendar); char* displayname_pos = strstr(xml, displayname_needle); // this XML multistatus response does not contain the users calendar if (displayname_pos == NULL) { + tracepoint(diary, debug, "parse_caldav_calenar() displayname_pos was null"); return NULL; } - - // <D:response> - // <D:href>/caldav/v2/2fcv7j5mf38o5u2kg5tar4baao%40group.calendar.google.com/events/</D:href> - // <D:propstat> - // <D:status></D:status> - // <D:prop> - // <D:displayname>diary</D:displayname> - // </D:prop> - // </D:propstat> - // </D:response> + tracepoint(diary, debug_string, "parse_caldav_calenar() displayname_pos", displayname_pos); // shorten multistatus response and find last hyperlink *displayname_pos= '\0'; - char* href = strrstr(xml, "<D:href>"); + + tracepoint(diary, debug_string, "parse_caldav_calenar() shortened multistatus response", xml); + char* href = strrstr(xml, "href>/"); if (href != NULL) { + tracepoint(diary, debug_string, "parse_caldav_calenar() last href", href); href = strtok(href, "<"); // :href>/caldav/v2/aaa%40group.calendar.google.com/events/ + tracepoint(diary, debug_string, "parse_caldav_calenar() last href strtok", href); if (href != NULL) { href = strchr(href, '>'); href++; // cut > @@ -525,7 +578,7 @@ char* parse_caldav_calendar(char* xml, char* calendar) { } /* Upload event to CalDAV server */ -void put_event(struct tm* date, const char* dir, size_t dir_size, char* calendar_uri) { +void put_event(struct tm* date, const char* dir, size_t dir_size, char* calendar_uri, bool basicauth) { // get entry path char path[100]; char* ppath = path; @@ -550,14 +603,16 @@ void put_event(struct tm* date, const char* dir, size_t dir_size, char* calendar descr = calloc(descr_size + 1, sizeof(char)); if (descr == NULL) { perror("malloc failed"); + fclose(fp); return; } strcat(descr, "DESCRIPTION:"); - int items_read = fread(descr + descr_label_size, sizeof(char), descr_bytes, fp); + size_t items_read = fread(descr + descr_label_size, sizeof(char), descr_bytes, fp); if (items_read != descr_bytes) { - tracepoint(diary, error_int_long, "Read n items but expected m items, aborting.", items_read, descr_bytes); + perror("Error while reading in read_tokenfile()"); + fclose(fp); return; } @@ -568,6 +623,9 @@ void put_event(struct tm* date, const char* dir, size_t dir_size, char* calendar strftime(uid, sizeof uid, "%Y%m%d", date); char* ics = "BEGIN:VCALENDAR\n" + "PRODID:-//diary\n" // todo: add version + "VERSION:2.0\n" + "CALSCALE:GREGORIAN\n" "BEGIN:VEVENT\n" "UID:%s\n" "DTSTART;VALUE=DATE:%s\n" @@ -582,9 +640,11 @@ void put_event(struct tm* date, const char* dir, size_t dir_size, char* calendar uid, // todo: display first few chars of DESCRIPTION as SUMMARY folded_descr); + tracepoint(diary, debug_string, "PUT text/calendar", postfields); + strcat(calendar_uri, uid); strcat(calendar_uri, ".ics"); - char* response = caldav_req(date, calendar_uri, "PUT", postfields, 0); + char* response = caldav_req(date, calendar_uri, "PUT", postfields, 0, basicauth, "text/calendar", 0); fclose(fp); free(folded_descr); free(descr); @@ -614,10 +674,17 @@ int caldav_sync(struct tm* date, char* info_txt; - if (strcmp(CONFIG.google_clientid, "") == 0 || strcmp(CONFIG.google_secretid, "") == 0 || strcmp(CONFIG.google_calendar, "") == 0 ) { + bool google_oauth_enabled = !(strcmp(CONFIG.google_clientid, "") == 0 || strcmp(CONFIG.google_clientid, "") == 0); + bool basicauth_enabled = !(strcmp(CONFIG.caldav_username, "") == 0 || strcmp(CONFIG.caldav_password, "") == 0); + + if (strcmp(CONFIG.caldav_server, "") == 0 || strcmp(CONFIG.caldav_calendar, "") == 0 || !(google_oauth_enabled || basicauth_enabled)) { + tracepoint(diary, debug_string, "CONFIG.caldav_server", CONFIG.caldav_server); + tracepoint(diary, debug_string, "CONFIG.caldav_calendar", CONFIG.caldav_calendar); + tracepoint(diary, debug_string, "CONFIG.caldav_username", CONFIG.caldav_username); + tracepoint(diary, debug_string, "CONFIG.caldav_password", CONFIG.caldav_password); wclear(header); wresize(header, LINES, getmaxx(header)); - info_txt = "Missing sync parameters. Set Google Client ID, secret and remote calendar name.\n" + info_txt = "Missing sync parameters. Configure CalDAV server and credentials.\n" "Press any key to continue."; mvwaddstr(header, 0, 0, info_txt); wrefresh(header); @@ -629,27 +696,29 @@ int caldav_sync(struct tm* date, wclear(header); } - // fetch existing API tokens from file - char* tokfile = read_tokenfile(); - free(tokfile); + if (google_oauth_enabled) { + // fetch existing API tokens from file + char* tokfile = read_tokenfile(); + free(tokfile); + + if (strcmp(access_token, "") == 0) { + // no access token exists yet, create new verifier + char challenge[GOOGLE_OAUTH_CODE_VERIFIER_LENGTH + 1]; + random_code_challenge(GOOGLE_OAUTH_CODE_VERIFIER_LENGTH, challenge); + + // fetch new code with verifier + char* code = get_oauth_code(challenge, header); + if (code == NULL) { + tracepoint(diary, error, "Error retrieving oauth code in caldav_sync()"); + return -1; + } - if (strcmp(access_token, "") == 0) { - // no access token exists yet, create new verifier - char challenge[GOOGLE_OAUTH_CODE_VERIFIER_LENGTH + 1]; - random_code_challenge(GOOGLE_OAUTH_CODE_VERIFIER_LENGTH, challenge); + // get acess token using code and verifier + tracepoint(diary, debug, "Fetching access token with code challenge"); + get_access_token(code, challenge, false); - // fetch new code with verifier - char* code = get_oauth_code(challenge, header); - if (code == NULL) { - tracepoint(diary, error, "Error retrieving oauth code in caldav_sync()"); - return -1; + free(code); } - - // get acess token using code and verifier - tracepoint(diary, debug, "Fetching access token with code challenge"); - get_access_token(code, challenge, false); - - free(code); } pthread_create(&progress_tid, NULL, show_progress, (void*)header); @@ -661,22 +730,26 @@ int caldav_sync(struct tm* date, // check if we can use the token from the tokenfile - char* user_principal = caldav_req(date, GOOGLE_CALDAV_URI, "PROPFIND", principal_postfields, 0); + char* user_principal = caldav_req(date, CONFIG.caldav_server, "PROPFIND", principal_postfields, 0, basicauth_enabled, "application/xml", 1); + tracepoint(diary, debug_string, "User principal", user_principal); if (user_principal == NULL) { - // The principal could not be fetched, - // get new acess token with refresh token - tracepoint(diary, debug, "Unable to fetch principal, refreshing API token"); - tracepoint(diary, debug, "Fetching access token with refresh token"); - get_access_token(NULL, NULL, true); - // Retry request for event with new token - user_principal = caldav_req(date, GOOGLE_CALDAV_URI, "PROPFIND", principal_postfields, 0); + // The principal could not be fetched + tracepoint(diary, debug, "Unable to fetch principal"); + if (google_oauth_enabled) { + // get new acess token with refresh token + tracepoint(diary, debug, "Fetching access token with refresh token"); + get_access_token(NULL, NULL, true); + // Retry request for event with new token + user_principal = caldav_req(date, CONFIG.caldav_server, "PROPFIND", principal_postfields, 0, basicauth_enabled, "application/xml", 1); + } } if (user_principal == NULL) { pthread_cancel(progress_tid); wclear(header); wresize(header, LINES, getmaxx(header)); + // TODO: this could also be wrong basicauth credentials info_txt = "Offline, corrupted or otherwise invalid OAuth2 credential tokenfile.\n" "Go online or delete tokenfile '%s' and restart diary to retry.\n" "Press any key to continue."; @@ -693,56 +766,107 @@ int caldav_sync(struct tm* date, } char* current_user_principal = parse_caldav_current_user_principal(user_principal); + tracepoint(diary, debug_string, "Current user principal", current_user_principal); + + // extract host without path as basis for further api paths + char *caldav_host; char* caldav_host_scheme; + CURLU *h = curl_url(); + CURLUcode uc = curl_url_set(h, CURLUPART_URL, CONFIG.caldav_server, 0); + uc = curl_url_get(h, CURLUPART_HOST, &caldav_host, 0); + uc = curl_url_get(h, CURLUPART_SCHEME, &caldav_host_scheme, 0); + tracepoint(diary, debug_string, "Caldav server host name", caldav_host); + tracepoint(diary, debug_string, "Caldav server scheme/protocol", caldav_host_scheme); + tracepoint(diary, debug_string, "Caldav server host name", caldav_host); + if (!uc) { + tracepoint(diary, error, "cur_url_get() failed in caldav_sync()"); + } // get the home-set of the user char uri[300]; - sprintf(uri, "%s%s", GOOGLE_API_URI, current_user_principal); + sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, current_user_principal); // free memory allocated by curl request free(user_principal); - char* home_set = caldav_req(date, uri, "PROPFIND", "", 1); + + char* homeset_request = "<d:propfind xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>" + " <d:prop>" + " <c:calendar-home-set />" + " </d:prop>" + "</d:propfind>"; + + char* home_set = caldav_req(date, uri, "PROPFIND", homeset_request, 0, basicauth_enabled, "application/xml", 0); + tracepoint(diary, debug_string, "Home set xml", home_set); + if (home_set == NULL) { + pthread_cancel(progress_tid); + wclear(header); + wresize(header, LINES, getmaxx(header)); + info_txt = "Error while fetching home set in caldav_sync()\n" + "Press any key to continue."; + mvwaddstr(header, 0, 0, info_txt); + wrefresh(header); + + // accept any input to proceed + noecho(); + wgetch(header); + echo(); + + wclear(header); + return -1; + } + + // parse home set from xml response + char* home_set_parsed = parse_home_set(home_set); + sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, home_set_parsed); + + char* calendar_request = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/' xmlns:c='urn:ietf:params:xml:ns:caldav'>" + " <d:prop>" + " <d:resourcetype />" + " <d:displayname />" + " <cs:getctag />" + " <c:supported-calendar-component-set />" + " </d:prop>" + "</d:propfind>"; + + // get calendar from home-set + char* calendar_xml = caldav_req(date, uri, "PROPFIND", calendar_request, 1, basicauth_enabled, "application/xml", 0); + tracepoint(diary, debug_string, "Calendar set xml", calendar_xml); // get calendar URI from the home-set - char* calendar_href = parse_caldav_calendar(home_set, CONFIG.google_calendar); + char* calendar_href = parse_caldav_calendar(calendar_xml, CONFIG.caldav_calendar); char* xml_filter = "<c:calendar-query xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>" - "<d:prop><c:calendar-data/></d:prop>" + "<d:prop><d:getetag/><c:calendar-data/></d:prop>" "<c:filter><c:comp-filter name='VCALENDAR'>" "<c:comp-filter name='VEVENT'>" "<c:time-range start='%s' end='%s'/></c:comp-filter>" "</c:comp-filter></c:filter></c:calendar-query>"; - // construct next day from date+1 - time_t date_time = mktime(date); - struct tm* next_day = localtime(&date_time); - next_day->tm_mday++; - mktime(next_day); - - char dstr_cursor[30]; - char dstr_next_day[30]; - - char* format = "%Y%m%dT000000Z"; - strftime(dstr_cursor, sizeof dstr_cursor, format, date); - strftime(dstr_next_day, sizeof dstr_next_day, format, next_day); + char dstr_start[30]; + char dstr_end[30]; + char* format_start = "%Y%m%dT000000Z"; // start of day + char* format_end = "%Y%m%dT235900Z"; // end of day + strftime(dstr_start, sizeof dstr_start, format_start, date); + strftime(dstr_end, sizeof dstr_end, format_end, date); - char caldata_postfields[strlen(xml_filter)+50]; + char caldata_postfields[strlen(xml_filter) + strlen(dstr_start) + strlen(dstr_end)]; sprintf(caldata_postfields, xml_filter, - dstr_cursor, - dstr_next_day); + dstr_start, + dstr_end); // fetch event for the cursor date - sprintf(uri, "%s%s", GOOGLE_API_URI, calendar_href); + sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, calendar_href); - char* event = caldav_req(date, uri, "REPORT", caldata_postfields, 0); + char* event = caldav_req(date, uri, "REPORT", caldata_postfields, 1, basicauth_enabled, "application/xml", 0); // todo: warn if multiple events, // multistatus has more than just one caldav:calendar-data elements // currently, the code below will just extract the first occurance of - // LAST-MODIFIED, UID and DESCRIPTION (from the first event) + // DTSTAMP, UID and DESCRIPTION (from the first event) if (event == NULL) { // Event not found. The curl request probably failed due to one of the missing input parameters: // - Google Client ID // - Google Secret ID // - Google Calendar Name + tracepoint(diary, debug, "Event not found"); pthread_cancel(progress_tid); wclear(header); free(home_set); @@ -772,17 +896,19 @@ int caldav_sync(struct tm* date, time_t remote_date = 0; long search_pos = 0; - // check remote LAST-MODIFIED:20210521T212441Z of remote event - char* remote_last_mod = extract_ical_field(event, "LAST-MODIFIED", &search_pos, false); + // check remote DTSTAMP of remote event + char* remote_dtstamp = extract_ical_field(event, "DTSTAMP", &search_pos, false); char* remote_uid = extract_ical_field(event, "UID", &search_pos, false); + tracepoint(diary, debug_string, "event", event); + tracepoint(diary, debug_string, "DTSTAMP field", remote_dtstamp); // init remote date to 1970, assume remote file does not exist remote_datetime = gmtime(&epoch_zero); - if (remote_last_mod != NULL) { + if (remote_dtstamp != NULL) { // if remote date does exist, set date to the correct timestamp - strptime(remote_last_mod, "%Y%m%dT%H%M%SZ", remote_datetime); - free(remote_last_mod); + strptime(remote_dtstamp, "%Y%m%dT%H%M%SZ", remote_datetime); + free(remote_dtstamp); } remote_date = mktime(remote_datetime); @@ -801,6 +927,8 @@ int caldav_sync(struct tm* date, pthread_cancel(progress_tid); wclear(header); // free memory allocated to store curl response + curl_free(caldav_host_scheme); + curl_free(caldav_host); free(event); free(remote_uid); free(home_set); @@ -810,13 +938,14 @@ int caldav_sync(struct tm* date, if (remote_uid) { // purge any existing daily calendar entries on the remote side char event_uri[300]; - sprintf(event_uri, "%s%s%s.ics", GOOGLE_API_URI, calendar_href, remote_uid); + sprintf(event_uri, "%s://%s%s%s.ics", caldav_host_scheme, caldav_host, calendar_href, remote_uid); + //sprintf(event_uri, "%s%s%s.ics", CONFIG.caldav_server, calendar_href, remote_uid); tracepoint(diary, debug_string, "Event URI for DELETE request", event_uri); - char* response = caldav_req(date, event_uri, "DELETE", NULL, 0); + char* response = caldav_req(date, event_uri, "DELETE", NULL, 0, basicauth_enabled, "", 0); free(response); } - put_event(date, dir, dir_size, uri); + put_event(date, dir, dir_size, uri, basicauth_enabled); pthread_cancel(progress_tid); wclear(header); @@ -828,6 +957,8 @@ int caldav_sync(struct tm* date, tracepoint(diary, error, "Could not fetch description of remote event. Aborting sync."); pthread_cancel(progress_tid); wclear(header); + curl_free(caldav_host_scheme); + curl_free(caldav_host); free(event); free(remote_uid); free(home_set); @@ -887,6 +1018,8 @@ int caldav_sync(struct tm* date, } // free memory allocated to store curl response + curl_free(caldav_host_scheme); + curl_free(caldav_host); free(event); free(remote_uid); free(home_set); // home_set was required for sprintfing calendar_href to event_uri diff --git a/src/caldav.h b/src/caldav.h @@ -32,7 +32,6 @@ #define GOOGLE_OAUTH_REDIRECT_PORT 9004 #define GOOGLE_OAUTH_REDIRECT_SOCKET_BACKLOG 10 #define GOOGLE_API_URI "https://apidata.googleusercontent.com" -#define GOOGLE_CALDAV_URI GOOGLE_API_URI "/caldav/v2" int caldav_sync(struct tm* date, WINDOW* header, diff --git a/src/diary-tp.h b/src/diary-tp.h @@ -121,6 +121,19 @@ TRACEPOINT_EVENT( TRACEPOINT_EVENT( diary, + debug_long, + TP_ARGS( + char*, msg_arg, + long, n_arg + ), + TP_FIELDS( + ctf_string(msg, msg_arg) + ctf_integer(long, n, n_arg) + ) +) + +TRACEPOINT_EVENT( + diary, debug_sizet, TP_ARGS( char*, msg_arg, @@ -179,6 +192,7 @@ TRACEPOINT_LOGLEVEL(diary, debug, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, debug_string, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, debug_int, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, debug_double, TRACE_DEBUG) +TRACEPOINT_LOGLEVEL(diary, debug_long, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, debug_sizet, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, debug_tm, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, warning_string, TRACE_WARNING) diff --git a/src/diary.c b/src/diary.c @@ -363,9 +363,18 @@ bool read_config(const char* file_path) { } else if (strcmp("google_secretid", key_buf) == 0) { CONFIG.google_secretid = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); strcpy(CONFIG.google_secretid, value_buf); - } else if (strcmp("google_calendar", key_buf) == 0) { - CONFIG.google_calendar = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); - strcpy(CONFIG.google_calendar, value_buf); + } else if (strcmp("caldav_server", key_buf) == 0) { + CONFIG.caldav_server = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); + strcpy(CONFIG.caldav_server, value_buf); + } else if (strcmp("caldav_calendar", key_buf) == 0) { + CONFIG.caldav_calendar = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); + strcpy(CONFIG.caldav_calendar, value_buf); + } else if (strcmp("caldav_username", key_buf) == 0) { + CONFIG.caldav_username = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); + strcpy(CONFIG.caldav_username, value_buf); + } else if (strcmp("caldav_password", key_buf) == 0) { + CONFIG.caldav_password = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); + strcpy(CONFIG.caldav_password, value_buf); } } } @@ -816,7 +825,10 @@ int main(int argc, char** argv) { mousemask(oldmask, NULL); } - system(ecmd); + int ret = system(ecmd); + if (ret == -1) { + perror("Failure while running edit command in main()"); + } if (!CONFIG.no_mouse) { mousemask(ALL_MOUSE_EVENTS, &oldmask); @@ -930,6 +942,9 @@ int main(int argc, char** argv) { free(config_file_path); free(CONFIG.dir); endwin(); - system("clear"); + int ret = system("clear"); + if (ret == -1) { + perror("Failure while running clear command in main()"); + } return 0; } diff --git a/src/import.c b/src/import.c @@ -15,7 +15,12 @@ void ics_import(const char* ics_input, WINDOW* header, WINDOW* cal, WINDOW* asid rewind(pfile); char* ics = malloc(ics_bytes + 1); - fread(ics, 1, ics_bytes, pfile); + size_t read = fread(ics, 1, ics_bytes, pfile); + if (read != ics_bytes) { + perror("Error while reading in ics_import()"); + fclose(pfile); + return; + } fclose(pfile); ics[ics_bytes] = '\0'; @@ -118,4 +123,4 @@ void ics_import(const char* ics_input, WINDOW* header, WINDOW* cal, WINDOW* asid free(vevent_desc); } free(ics); -} -\ No newline at end of file +} diff --git a/src/utils.c b/src/utils.c @@ -106,7 +106,8 @@ char* fold(const char* str) { // break lines after 75 chars // split between any two characters by inserting a CRLF // immediately followed by a white space character - buf[bufl-2] = '\n'; + buf[bufl-2] = '\r'; + buf[bufl-1] = '\n'; escch = ' '; esc = true; continue; @@ -165,7 +166,7 @@ char* unfold(const char* str) { } strcpy(strcp, str); - char* res = strtok(strcp, "\n"); + char* res = strtok(strcp, "\r\n"); if (res == NULL) { tracepoint(diary, debug, "No more lines in multiline string, stop unfolding"); @@ -224,7 +225,7 @@ char* unfold(const char* str) { char* extract_ical_field(const char* ics, char* key, long* start_pos, bool multiline) { regex_t re; regmatch_t pm[1]; - char key_regex[strlen(key) + 1]; + char key_regex[strlen(key) + 2]; sprintf(key_regex, "^%s", key); if (regcomp(&re, key_regex, 0) != 0) { @@ -405,5 +406,8 @@ config CONFIG = { .google_tokenfile = GOOGLE_OAUTH_TOKEN_FILE, .google_clientid = GOOGLE_OAUTH_CLIENT_ID, .google_secretid = GOOGLE_OAUTH_CLIENT_SECRET, - .google_calendar = "" + .caldav_calendar = "", + .caldav_server = "", + .caldav_username = "", + .caldav_password = "" }; diff --git a/src/utils.h b/src/utils.h @@ -62,8 +62,14 @@ typedef struct char* google_clientid; // Google secret id char* google_secretid; - // Google calendar to synchronize - char* google_calendar; + // CalDAV calendar to synchronize + char* caldav_calendar; + // CalDAV server URI + char* caldav_server; + // CalDAV username + char* caldav_username; + // CalDAV password + char* caldav_password; } config; extern config CONFIG;