1 /*
2  * Calcurse - text-based organizer
3  *
4  * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org>
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  *      - Redistributions of source code must retain the above
12  *        copyright notice, this list of conditions and the
13  *        following disclaimer.
14  *
15  *      - Redistributions in binary form must reproduce the above
16  *        copyright notice, this list of conditions and the
17  *        following disclaimer in the documentation and/or other
18  *        materials provided with the distribution.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  *
32  * Send your feedback or comments to : misc@calcurse.org
33  * Calcurse home page : http://calcurse.org
34  *
35  */
36 
37 #include <stdarg.h>
38 #include <stdlib.h>
39 #include <string.h>
40 #include <sys/stat.h>
41 #include <sys/types.h>
42 #include <signal.h>
43 #include <time.h>
44 #include <math.h>
45 #include <unistd.h>
46 #include <errno.h>
47 
48 #include "calcurse.h"
49 #include "sha1.h"
50 
51 struct ht_keybindings_s {
52 	const char *label;
53 	enum key key;
54 	 HTABLE_ENTRY(ht_keybindings_s);
55 };
56 
57 static void load_keys_ht_getkey(struct ht_keybindings_s *, const char **,
58 				int *);
59 static int load_keys_ht_compare(struct ht_keybindings_s *,
60 				struct ht_keybindings_s *);
61 
62 #define HSIZE 256
63 HTABLE_HEAD(ht_keybindings, HSIZE, ht_keybindings_s);
64 HTABLE_PROTOTYPE(ht_keybindings, ht_keybindings_s)
65     HTABLE_GENERATE(ht_keybindings, ht_keybindings_s, load_keys_ht_getkey,
66 		load_keys_ht_compare)
67 
68 static int modified = 0;
69 static char apts_sha1[SHA1_DIGESTLEN * 2 + 1];
70 static char todo_sha1[SHA1_DIGESTLEN * 2 + 1];
71 
72 /* Ask user for a file name to export data to. */
get_export_stream(enum export_type type)73 static FILE *get_export_stream(enum export_type type)
74 {
75 	FILE *stream;
76 	char *home, *stream_name;
77 	const char *question =
78 	    _("Choose the file used to export calcurse data:");
79 	const char *wrong_name =
80 	    _("The file cannot be accessed, please enter another file name.");
81 	const char *press_enter = _("Press [ENTER] to continue.");
82 	const char *file_ext[IO_EXPORT_NBTYPES] = { "ical", "txt" };
83 
84 	stream = NULL;
85 	if ((home = getenv("HOME")) != NULL)
86 		asprintf(&stream_name, "%s/calcurse.%s", home, file_ext[type]);
87 	else
88 		asprintf(&stream_name, "%s/calcurse.%s", get_tempdir(),
89 			 file_ext[type]);
90 
91 	while (stream == NULL) {
92 		status_mesg(question, "");
93 		if (updatestring(win[STA].p, &stream_name, 0, 1)) {
94 			mem_free(stream_name);
95 			return NULL;
96 		}
97 		stream = fopen(stream_name, "w");
98 		if (stream == NULL) {
99 			status_mesg(wrong_name, press_enter);
100 			keys_wait_for_any_key(win[KEY].p);
101 		}
102 	}
103 
104 	mem_free(stream_name);
105 	return stream;
106 }
107 
108 /* Append a line to a file. */
io_fprintln(const char * fname,const char * fmt,...)109 unsigned io_fprintln(const char *fname, const char *fmt, ...)
110 {
111 	FILE *fp;
112 	va_list ap;
113 	char *buf;
114 	int ret;
115 
116 	fp = fopen(fname, "a");
117 	RETVAL_IF(!fp, 0, _("Failed to open \"%s\", - %s\n"), fname,
118 		  strerror(errno));
119 
120 	va_start(ap, fmt);
121 	ret = vasprintf(&buf, fmt, ap);
122 	RETVAL_IF(ret < 0, 0, _("Failed to build message\n"));
123 	va_end(ap);
124 
125 	ret = fprintf(fp, "%s", buf);
126 	RETVAL_IF(ret < 0, 0, _("Failed to print message \"%s\"\n"), buf);
127 
128 	ret = fclose(fp);
129 	RETVAL_IF(ret != 0, 0, _("Failed to close \"%s\" - %s\n"),
130 		  fname, strerror(errno));
131 
132 	mem_free(buf);
133 	return 1;
134 }
135 
136 /*
137  * Initialization of data paths. The cfile argument is the variable
138  * which contains the calendar file. If none is given, then the default
139  * one (~/.local/share/calcurse/apts) is taken. If the one given does not exist,
140  * it is created.
141  * The datadir argument can be used to specify an alternative data root dir.
142  * The confdir argument can be used to specify an alternative configuration dir.
143  * If ~/.calcurse exists, it will be used instead for backward compatibility.
144  */
io_init(const char * cfile,const char * datadir,const char * confdir)145 void io_init(const char *cfile, const char *datadir, const char *confdir)
146 {
147 	char* home_dir = getenv("HOME");
148 	char* legacy_dir = NULL;
149 
150 	if (home_dir) {
151 		asprintf(&legacy_dir, "%s%s", home_dir, "/" DIR_NAME_LEGACY);
152 		if (!io_dir_exists(legacy_dir)) {
153 			mem_free(legacy_dir);
154 			legacy_dir = NULL;
155 		}
156 	}
157 
158 	if (datadir)
159 		asprintf(&path_ddir, "%s%s", datadir, "/");
160 	else if (legacy_dir)
161 		path_ddir = mem_strdup(legacy_dir);
162 	else if ((path_ddir = getenv("XDG_DATA_HOME")))
163 		asprintf(&path_ddir, "%s%s", path_ddir, "/" DIR_NAME);
164 	else if (home_dir)
165 		asprintf(&path_ddir, "%s%s", home_dir, "/.local/share/" DIR_NAME);
166 	else
167 		path_ddir = mem_strdup("./." DIR_NAME);
168 
169 
170 	if (confdir)
171 		asprintf(&path_cdir, "%s%s", confdir, "/");
172 	else if (datadir)
173 		path_cdir = mem_strdup(path_ddir);
174 	else if (legacy_dir)
175 		path_cdir = mem_strdup(legacy_dir);
176 	else if ((path_cdir = getenv("XDG_CONFIG_HOME")))
177 		asprintf(&path_cdir, "%s%s", path_cdir, "/" DIR_NAME);
178 	else if (home_dir)
179 		asprintf(&path_cdir, "%s%s", home_dir, "/.config/" DIR_NAME);
180 	else
181 		path_cdir = mem_strdup("./." DIR_NAME);
182 
183 	if (legacy_dir)
184 		mem_free(legacy_dir);
185 
186 	/* Data files */
187 	if (cfile) {
188 		path_apts = mem_strdup(cfile);
189 		EXIT_IF(!io_file_exists(path_apts), _("%s does not exist"),
190 			path_apts);
191 	} else {
192 		asprintf(&path_apts, "%s%s", path_ddir, APTS_PATH_NAME);
193 	}
194 	asprintf(&path_todo, "%s%s", path_ddir, TODO_PATH_NAME);
195 	asprintf(&path_cpid, "%s%s", path_ddir, CPID_PATH_NAME);
196 	asprintf(&path_dpid, "%s%s", path_ddir, DPID_PATH_NAME);
197 	asprintf(&path_notes, "%s%s", path_ddir, NOTES_DIR_NAME);
198 	asprintf(&path_dmon_log, "%s%s", path_ddir, DLOG_PATH_NAME);
199 
200 	/* Configuration files */
201 	asprintf(&path_conf, "%s%s", path_cdir, CONF_PATH_NAME);
202 	asprintf(&path_keys, "%s%s", path_cdir, KEYS_PATH_NAME);
203 	asprintf(&path_hooks, "%s%s", path_cdir, HOOKS_DIR_NAME);
204 }
205 
io_extract_data(char * dst_data,const char * org,int len)206 void io_extract_data(char *dst_data, const char *org, int len)
207 {
208 	int i;
209 
210 	for (; *org == ' ' || *org == '\t'; org++) ;
211 	for (i = 0; i < len - 1; i++) {
212 		if (*org == '\n' || *org == '\0')
213 			break;
214 		*dst_data++ = *org++;
215 	}
216 	*dst_data = '\0';
217 }
218 
219 static pthread_mutex_t io_mutex = PTHREAD_MUTEX_INITIALIZER;
220 static pthread_mutex_t io_periodic_save_mutex = PTHREAD_MUTEX_INITIALIZER;
221 
io_mutex_lock(void)222 static void io_mutex_lock(void)
223 {
224 	pthread_mutex_lock(&io_mutex);
225 }
226 
io_mutex_unlock(void)227 static void io_mutex_unlock(void)
228 {
229 	pthread_mutex_unlock(&io_mutex);
230 }
231 
232 /* Print all appointments and events to stdout. */
io_dump_apts(const char * fmt_apt,const char * fmt_rapt,const char * fmt_ev,const char * fmt_rev)233 void io_dump_apts(const char *fmt_apt, const char *fmt_rapt,
234 		  const char *fmt_ev, const char *fmt_rev)
235 {
236 	llist_item_t *i;
237 
238 	LLIST_FOREACH(&recur_elist, i) {
239 		struct recur_event *rev = LLIST_GET_DATA(i);
240 		time_t day = DAY(rev->day);
241 		print_recur_event(fmt_rev, day, rev);
242 	}
243 
244 	LLIST_TS_FOREACH(&recur_alist_p, i) {
245 		struct recur_apoint *rapt = LLIST_GET_DATA(i);
246 		time_t day = DAY(rapt->start);
247 		print_recur_apoint(fmt_rapt, day, rapt->start, rapt);
248 	}
249 
250 	LLIST_TS_FOREACH(&alist_p, i) {
251 		struct apoint *apt = LLIST_TS_GET_DATA(i);
252 		time_t day = DAY(apt->start);
253 		print_apoint(fmt_apt, day, apt);
254 	}
255 
256 	LLIST_FOREACH(&eventlist, i) {
257 		struct event *ev = LLIST_TS_GET_DATA(i);
258 		time_t day = DAY(ev->day);
259 		print_event(fmt_ev, day, ev);
260 	}
261 }
262 
263 /*
264  * Save the apts data file, which contains the
265  * appointments first, and then the events.
266  * Recursive items are written first.
267  */
io_save_apts(const char * aptsfile)268 unsigned io_save_apts(const char *aptsfile)
269 {
270 	llist_item_t *i;
271 	FILE *fp;
272 
273 	if (aptsfile) {
274 		if (read_only)
275 			return 1;
276 
277 		if ((fp = fopen(aptsfile, "w")) == NULL)
278 			return 0;
279 	} else {
280 		fp = stdout;
281 	}
282 
283 	recur_save_data(fp);
284 
285 	if (ui_mode == UI_CURSES)
286 		LLIST_TS_LOCK(&alist_p);
287 	LLIST_TS_FOREACH(&alist_p, i) {
288 		struct apoint *apt = LLIST_TS_GET_DATA(i);
289 		apoint_write(apt, fp);
290 	}
291 	if (ui_mode == UI_CURSES)
292 		LLIST_TS_UNLOCK(&alist_p);
293 
294 	LLIST_FOREACH(&eventlist, i) {
295 		struct event *ev = LLIST_TS_GET_DATA(i);
296 		event_write(ev, fp);
297 	}
298 
299 	if (aptsfile)
300 		file_close(fp, __FILE_POS__);
301 
302 	return 1;
303 }
304 
305 /* Print all todo items to stdout. */
io_dump_todo(const char * fmt_todo)306 void io_dump_todo(const char *fmt_todo)
307 {
308 	llist_item_t *i;
309 
310 	LLIST_FOREACH(&todolist, i) {
311 		struct todo *todo = LLIST_TS_GET_DATA(i);
312 		print_todo(fmt_todo, todo);
313 	}
314 }
315 
316 /* Save the todo data file. */
io_save_todo(const char * todofile)317 unsigned io_save_todo(const char *todofile)
318 {
319 	llist_item_t *i;
320 	FILE *fp;
321 
322 	if (todofile) {
323 		if (read_only)
324 			return 1;
325 
326 		if ((fp = fopen(todofile, "w")) == NULL)
327 			return 0;
328 	} else {
329 		fp = stdout;
330 	}
331 
332 	LLIST_FOREACH(&todolist, i) {
333 		struct todo *todo = LLIST_TS_GET_DATA(i);
334 		todo_write(todo, fp);
335 	}
336 
337 	if (todofile)
338 		file_close(fp, __FILE_POS__);
339 
340 	return 1;
341 }
342 
343 /* Save user-defined keys */
io_save_keys(void)344 unsigned io_save_keys(void)
345 {
346 	FILE *fp;
347 
348 	if (read_only)
349 		return 1;
350 
351 	if ((fp = fopen(path_keys, "w")) == NULL)
352 		return 0;
353 
354 	keys_save_bindings(fp);
355 	file_close(fp, __FILE_POS__);
356 
357 	return 1;
358 }
359 
io_compute_hash(const char * path,char * buf)360 static int io_compute_hash(const char *path, char *buf)
361 {
362 	FILE *fp = fopen(path, "r");
363 
364 	if (!fp)
365 		return 0;
366 	sha1_stream(fp, buf);
367 	fclose(fp);
368 
369 	return 1;
370 }
371 
372 /* A merge implies a save operation and must be followed by reload of data. */
io_merge_data(void)373 static void io_merge_data(void)
374 {
375 	char *path_apts_new, *path_todo_new;
376 	const char *new_ext = ".new";
377 
378 	asprintf(&path_apts_new, "%s%s", path_apts, new_ext);
379 	asprintf(&path_todo_new, "%s%s", path_todo, new_ext);
380 
381 	io_save_apts(path_apts_new);
382 	io_save_todo(path_todo_new);
383 
384 	/*
385 	 * We do not directly write to the data files here; however, the
386 	 * external merge tool might incorporate changes from the new file into
387 	 * the main data files.
388 	 */
389 	run_hook("pre-save");
390 
391 	if (!io_files_equal(path_apts, path_apts_new)) {
392 		const char *arg_apts[] = { conf.mergetool, path_apts,
393 					   path_apts_new, NULL };
394 		wins_launch_external(arg_apts);
395 	}
396 
397 	if (!io_files_equal(path_todo, path_todo_new)) {
398 		const char *arg_todo[] = { conf.mergetool, path_todo,
399 					   path_todo_new, NULL };
400 		wins_launch_external(arg_todo);
401 	}
402 
403 	mem_free(path_apts_new);
404 	mem_free(path_todo_new);
405 
406 	/*
407 	 * We do not directly write to the data files here; however, the
408 	 * external merge tool will likely have incorporated changes from the
409 	 * new file into the main data files at this point.
410 	 */
411 	run_hook("post-save");
412 
413 	/*
414 	 * The user has merged, so override the modified flag
415 	 * (and follow up with reload of the data files).
416 	 */
417 	io_unset_modified();
418 }
419 
420 /* For the return values, see io_save_cal() below. */
resolve_save_conflict(void)421 static int resolve_save_conflict(void)
422 {
423 	char *msg_um_asktype = NULL;
424 	const char *msg_um_prefix =
425 			_("Data files have changed and will be overwritten:");
426 	const char *msg_um_overwrite = _("(c)ontinue");
427 	const char *msg_um_merge = _("(m)erge");
428 	const char *msg_um_keep = _("c(a)ncel");
429 	const char *msg_um_choice = _("[cma]");
430 	int ret = IO_SAVE_CANCEL;
431 
432 	asprintf(&msg_um_asktype, "%s %s, %s, %s", msg_um_prefix,
433 		 msg_um_overwrite, msg_um_merge, msg_um_keep);
434 
435 	switch (status_ask_choice(msg_um_asktype, msg_um_choice, 3)) {
436 	case 1:
437 		ret = IO_SAVE_CTINUE;
438 		break;
439 	case 2:
440 		io_merge_data();
441 		io_load_data(NULL, FORCE);
442 		ret = IO_SAVE_RELOAD;
443 		break;
444 	case 3:
445 		/* FALLTHROUGH */
446 	default:
447 		ret = IO_SAVE_CANCEL;
448 	}
449 
450 	mem_free(msg_um_asktype);
451 	return ret;
452 }
453 
454 /*
455  * Return codes for new_data() and io_load_data().
456  * Note that they are file internal.
457  */
458 #define NONEW		0
459 #define APTS		(1 << 0)
460 #define TODO		(1 << 1)
461 #define APTS_TODO	APTS | TODO
462 #define NOKNOW		-1
new_data()463 static int new_data()
464 {
465 	char sha1_new[SHA1_DIGESTLEN * 2 + 1];
466 	int ret = NONEW;
467 
468 	if (io_compute_hash(path_apts, sha1_new)) {
469 		if (strncmp(sha1_new, apts_sha1, SHA1_DIGESTLEN * 2) != 0) {
470 			ret |= APTS;
471 		}
472 	} else {
473 		ret = NOKNOW;
474 		goto exit;
475 	}
476 
477 	if (io_compute_hash(path_todo, sha1_new)) {
478 		if (strncmp(sha1_new, todo_sha1, SHA1_DIGESTLEN * 2) != 0) {
479 			ret |= TODO;
480 		}
481 	} else {
482 		ret = NOKNOW;
483 		goto exit;
484 	}
485    exit:
486 	return ret;
487 }
488 
489 /*
490  * Save the calendar data.
491  * The return value tells how a possible save conflict should be/was resolved:
492  * IO_SAVE_CTINUE: continue save operation and overwrite the data files
493  * IO_SAVE_RELOAD: cancel save operation (data files changed and reloaded)
494  * IO_SAVE_CANCEL: cancel save operation (user's decision, keep data files, no reload)
495  * IO_SAVE_NOOP: cancel save operation (nothing has changed)
496  * IO_SAVE_ERROR: cannot access data
497  */
io_save_cal(enum save_type s_t)498 int io_save_cal(enum save_type s_t)
499 {
500 	int ret, new;
501 
502 	if (read_only)
503 		return IO_SAVE_CANCEL;
504 
505 	io_mutex_lock();
506 	if ((new = new_data()) == NOKNOW) {
507 		ret = IO_SAVE_ERROR;
508 		goto cleanup;
509 	}
510 	if (new) { /* New data */
511 		if (s_t == periodic) {
512 			ret = IO_SAVE_CANCEL;
513 			goto cleanup;
514 		}
515 		/* Interactively decide what to do. */
516 		if ((ret = resolve_save_conflict()))
517 			goto cleanup;
518 	} else /* No new data */
519 		if (!io_get_modified()) {
520 			ret = IO_SAVE_NOOP;
521 			goto cleanup;
522 		}
523 
524 	ret = IO_SAVE_CTINUE;
525 	run_hook("pre-save");
526 	if (io_save_todo(path_todo) &&
527 	    io_save_apts(path_apts)) {
528 		io_compute_hash(path_apts, apts_sha1);
529 		io_compute_hash(path_todo, todo_sha1);
530 		io_unset_modified();
531 	} else
532 		ret = IO_SAVE_ERROR;
533 	run_hook("post-save");
534 
535 cleanup:
536 	io_mutex_unlock();
537 	return ret;
538 }
539 
io_load_error(const char * filename,unsigned line,const char * mesg)540 static void io_load_error(const char *filename, unsigned line,
541 			  const char *mesg)
542 {
543 	EXIT("%s:%u: %s", filename, line, mesg);
544 }
545 
546 /*
547  * Check what type of data is written in the appointment file,
548  * and then load either: a new appointment, a new event, or a new
549  * recursive item (which can also be either an event or an appointment).
550  */
io_load_app(struct item_filter * filter)551 void io_load_app(struct item_filter *filter)
552 {
553 	FILE *data_file;
554 	int c, is_appointment, is_event, is_recursive;
555 	struct tm start, end, until, lt;
556 	struct rpt rpt;
557 	time_t t;
558 	int id = 0;
559 	char type, state = 0L;
560 	char note[MAX_NOTESIZ + 1], *notep;
561 	unsigned line = 0;
562 	char *scan_error;
563 
564 	t = time(NULL);
565 	localtime_r(&t, &lt);
566 	start = end = until = lt;
567 
568 	data_file = fopen(path_apts, "r");
569 	EXIT_IF(data_file == NULL, _("failed to open appointment file"));
570 
571 	sha1_stream(data_file, apts_sha1);
572 	rewind(data_file);
573 
574 	for (;;) {
575 		is_appointment = is_event = is_recursive = 0;
576 		line++;
577 		scan_error = NULL;
578 
579 		c = getc(data_file);
580 		if (c == EOF)
581 			break;
582 		ungetc(c, data_file);
583 
584 		/* Read the date first: it is common to both events
585 		 * and appointments.
586 		 */
587 		if (fscanf(data_file, "%d / %d / %d ",
588 			   &start.tm_mon, &start.tm_mday,
589 			   &start.tm_year) != 3)
590 			io_load_error(path_apts, line,
591 				      _("syntax error in the item date"));
592 
593 		/* Read the next character : if it is an '@' then we have
594 		 * an appointment, else if it is an '[' we have en event.
595 		 */
596 		c = getc(data_file);
597 
598 		if (c == '@')
599 			is_appointment = 1;
600 		else if (c == '[')
601 			is_event = 1;
602 		else
603 			io_load_error(path_apts, line,
604 				      _("no event nor appointment found"));
605 
606 		/* Read the remaining informations. */
607 		if (is_appointment) {
608 			if (fscanf
609 			    (data_file,
610 			     " %d : %d -> %d / %d / %d @ %d : %d ",
611 			     &start.tm_hour, &start.tm_min, &end.tm_mon,
612 			     &end.tm_mday, &end.tm_year, &end.tm_hour,
613 			     &end.tm_min) != 7)
614 				io_load_error(path_apts, line,
615 					      _("syntax error in item time or duration"));
616 		} else if (is_event) {
617 			if (fscanf(data_file, " %d ", &id) != 1
618 			    || getc(data_file) != ']')
619 				io_load_error(path_apts, line,
620 					      _("syntax error in item identifier"));
621 			while ((c = getc(data_file)) == ' ') ;
622 			ungetc(c, data_file);
623 		} else {
624 			io_load_error(path_apts, line,
625 				      _("wrong format in the appointment or event"));
626 			/* NOTREACHED */
627 		}
628 
629 		/* Check if we have a recursive item. */
630 		c = getc(data_file);
631 
632 		if (c == '{') {
633 			is_recursive = 1;
634 			if (fscanf(data_file, " %d%c ", &rpt.freq, &type) != 2)
635 				io_load_error(path_apts, line,
636 					      _("syntax error in item repetition"));
637 			else
638 				rpt.type = recur_char2def(type);
639 			c = getc(data_file);
640 			/* Optional until date */
641 			if (c == '-' && getc(data_file) == '>') {
642 				if (fscanf
643 				    (data_file, " %d / %d / %d ",
644 				     &until.tm_mon, &until.tm_mday,
645 				     &until.tm_year) != 3)
646 					io_load_error(path_apts, line,
647 						      _("syntax error in until date"));
648 				if (!check_date(until.tm_year, until.tm_mon,
649 						until.tm_mday))
650 					io_load_error(path_apts, line,
651 						      _("until date error"));
652 				until.tm_hour = 0;
653 				until.tm_min = 0;
654 				until.tm_sec = 0;
655 				until.tm_isdst = -1;
656 				until.tm_year -= 1900;
657 				until.tm_mon--;
658 				rpt.until = mktime(&until);
659 				c = getc(data_file);
660 			} else
661 				rpt.until = 0;
662 			/* Optional bymonthday list */
663 			if (c == 'd') {
664 				if (rpt.type == RECUR_WEEKLY)
665 					io_load_error(path_apts, line,
666 						      _("BYMONTHDAY illegal with WEEKLY"));
667 				ungetc(c, data_file);
668 				recur_bymonthday(&rpt.bymonthday, data_file);
669 				c = getc(data_file);
670 			} else
671 				LLIST_INIT(&rpt.bymonthday);
672 			/* Optional bywday list */
673 			if (c == 'w') {
674 				ungetc(c, data_file);
675 				recur_bywday(rpt.type, &rpt.bywday, data_file);
676 				c = getc(data_file);
677 			} else
678 				LLIST_INIT(&rpt.bywday);
679 			/* Optional bymonth list */
680 			if (c == 'm') {
681 				ungetc(c, data_file);
682 				recur_bymonth(&rpt.bymonth, data_file);
683 				c = getc(data_file);
684 			} else
685 				LLIST_INIT(&rpt.bymonth);
686 			/* Optional exception dates */
687 			if (c == '!') {
688 				ungetc(c, data_file);
689 				recur_exc_scan(&rpt.exc, data_file);
690 				c = getc(data_file);
691 			} else
692 				LLIST_INIT(&rpt.exc);
693 			/* End of recurrence rule */
694 			if (c != '}')
695 				io_load_error(path_apts, line,
696 					      _("missing end of recurrence"));
697 			while ((c = getc(data_file)) == ' ') ;
698 		}
699 
700 		/* Check if a note is attached to the item. */
701 		if (c == '>') {
702 			note_read(note, data_file);
703 			c = getc(data_file);
704 			notep = note;
705 		} else
706 			notep = NULL;
707 
708 		/*
709 		 * Last: read the item description and load it into its
710 		 * corresponding linked list, depending on the item type.
711 		 */
712 		if (is_appointment) {
713 			if (c == '!')
714 				state |= APOINT_NOTIFY;
715 			else if (c == '|')
716 				state = 0L;
717 			else
718 				io_load_error(path_apts, line,
719 					      _("syntax error in item state"));
720 
721 			if (is_recursive)
722 				scan_error = recur_apoint_scan(data_file, start, end, state,
723 						  notep, filter, &rpt);
724 			else
725 				scan_error = apoint_scan(data_file, start, end, state,
726 					    notep, filter);
727 		} else if (is_event) {
728 			ungetc(c, data_file);
729 			if (is_recursive)
730 				scan_error = recur_event_scan(data_file, start, id, notep,
731 						 filter, &rpt);
732 			else
733 				scan_error = event_scan(data_file, start, id, notep, filter);
734 		} else {
735 			io_load_error(path_apts, line,
736 				      _("wrong format in the appointment or event"));
737 			/* NOTREACHED */
738 		}
739 		if (scan_error)
740 			io_load_error(path_apts, line, scan_error);
741 	}
742 	file_close(data_file, __FILE_POS__);
743 }
744 
745 /* Load the todo data */
io_load_todo(struct item_filter * filter)746 void io_load_todo(struct item_filter *filter)
747 {
748 	FILE *data_file;
749 	char *newline;
750 	int c, id, completed, cond;
751 	char buf[BUFSIZ], e_todo[BUFSIZ], note[MAX_NOTESIZ + 1];
752 	unsigned line = 0;
753 
754 	data_file = fopen(path_todo, "r");
755 	EXIT_IF(data_file == NULL, _("failed to open todo file"));
756 
757 	sha1_stream(data_file, todo_sha1);
758 	rewind(data_file);
759 
760 	for (;;) {
761 		line++;
762 		c = getc(data_file);
763 		if (c == EOF) {
764 			break;
765 		} else if (c == '[') {
766 			/* new style with id */
767 			c = getc(data_file);
768 			if (c == '-') {
769 				completed = 1;
770 			} else {
771 				completed = 0;
772 				ungetc(c, data_file);
773 			}
774 			if (fscanf(data_file, " %d ", &id) != 1
775 			    || getc(data_file) != ']')
776 				io_load_error(path_todo, line,
777 					      _("syntax error in item identifier"));
778 			while ((c = getc(data_file)) == ' ') ;
779 			ungetc(c, data_file);
780 		} else {
781 			id = 9;
782 			completed = 0;
783 			ungetc(c, data_file);
784 		}
785 		/* Now read the attached note, if any. */
786 		c = getc(data_file);
787 		if (c == '>') {
788 			note_read(note, data_file);
789 		} else {
790 			note[0] = '\0';
791 			ungetc(c, data_file);
792 		}
793 		/* Then read todo description. */
794 		if (!fgets(buf, sizeof buf, data_file))
795 			buf[0] = '\0';
796 		newline = strchr(buf, '\n');
797 		if (newline)
798 			*newline = '\0';
799 		io_extract_data(e_todo, buf, sizeof buf);
800 
801 		/* Filter item. */
802 		struct todo *todo = NULL;
803 		if (filter) {
804 			cond = (
805 				!(filter->type_mask & TYPE_MASK_TODO) ||
806 				(filter->regex && regexec(filter->regex, e_todo, 0, 0, 0)) ||
807 				(filter->priority && id != filter->priority) ||
808 				(filter->completed && !completed) ||
809 				(filter->uncompleted && completed)
810 			);
811 			if (filter->hash) {
812 				todo = todo_add(e_todo, id, completed, note);
813 				char *hash = todo_hash(todo);
814 				cond = cond || !hash_matches(filter->hash, hash);
815 				mem_free(hash);
816 			}
817 
818 			if ((!filter->invert && cond) || (filter->invert && !cond)) {
819 				if (filter->hash)
820 					todo_delete(todo);
821 				continue;
822 			}
823 		}
824 
825 		if (!todo)
826 			todo = todo_add(e_todo, id, completed, note);
827 	}
828 	file_close(data_file, __FILE_POS__);
829 }
830 
831 /*
832  * Load appointments and todo items.
833  * Unless told otherwise, the function will only load a file that has changed
834  * since last saved or loaded. The new_data() return code is passed on when
835  * force is false. When force is true (FORCE), the return code is of no use.
836  */
io_load_data(struct item_filter * filter,int force)837 int io_load_data(struct item_filter *filter, int force)
838 {
839 	run_hook("pre-load");
840 	if (force)
841 		force = APTS_TODO;
842 	else
843 		force = new_data();
844 
845 	if (force == NOKNOW)
846 		goto exit;
847 
848 	if (force & APTS) {
849 		apoint_llist_free();
850 		event_llist_free();
851 		recur_apoint_llist_free();
852 		recur_event_llist_free();
853 		apoint_llist_init();
854 		event_llist_init();
855 		recur_apoint_llist_init();
856 		recur_event_llist_init();
857 		io_load_app(filter);
858 	}
859 	if (force & TODO) {
860 		todo_free_list();
861 		todo_init_list();
862 		io_load_todo(filter);
863 	}
864 
865 	io_unset_modified();
866    exit:
867 	run_hook("post-load");
868 	return force;
869 }
870 
871 /*
872  * The return codes reflect the user choice in case of unsaved in-memory changes.
873  */
io_reload_data(void)874 int io_reload_data(void)
875 {
876 	char *msg_um_asktype = NULL;
877 	int load = NOFORCE;
878 	int ret = IO_RELOAD_LOAD;
879 
880 	io_mutex_lock();
881 	if (io_get_modified()) {
882 		const char *msg_um_prefix =
883 				_("Screen data have changed and will be lost:");
884 		const char *msg_um_discard = _("(c)ontinue");
885 		const char *msg_um_merge = _("(m)erge");
886 		const char *msg_um_keep = _("c(a)ncel");
887 		const char *msg_um_choice = _("[cma]");
888 
889 		asprintf(&msg_um_asktype, "%s %s, %s, %s", msg_um_prefix,
890 			 msg_um_discard, msg_um_merge, msg_um_keep);
891 
892 		switch (status_ask_choice(msg_um_asktype, msg_um_choice, 3)) {
893 		case 1:
894 			load = FORCE;
895 			ret = IO_RELOAD_CTINUE;
896 			break;
897 		case 2:
898 			io_merge_data();
899 			load = FORCE;
900 			ret = IO_RELOAD_MERGE;
901 			break;
902 		case 3:
903 			ret = IO_RELOAD_CANCEL;
904 			/* FALLTHROUGH */
905 		default:
906 			goto cleanup;
907 		}
908 	}
909 
910 	load = io_load_data(NULL, load);
911 	if (load == NONEW)
912 		ret = IO_RELOAD_NOOP;
913 	else if (load == NOKNOW)
914 		ret = IO_RELOAD_ERROR;
915 cleanup:
916 	io_mutex_unlock();
917 	mem_free(msg_um_asktype);
918 	return ret;
919 }
920 
921 static void
load_keys_ht_getkey(struct ht_keybindings_s * data,const char ** key,int * len)922 load_keys_ht_getkey(struct ht_keybindings_s *data, const char **key,
923 		    int *len)
924 {
925 	*key = data->label;
926 	*len = strlen(data->label);
927 }
928 
929 static int
load_keys_ht_compare(struct ht_keybindings_s * data1,struct ht_keybindings_s * data2)930 load_keys_ht_compare(struct ht_keybindings_s *data1,
931 		     struct ht_keybindings_s *data2)
932 {
933 	const int KEYLEN = strlen(data1->label);
934 
935 	if (strlen(data2->label) == KEYLEN
936 	    && !memcmp(data1->label, data2->label, KEYLEN))
937 		return 0;
938 	else
939 		return 1;
940 }
941 
942 /*
943  * isblank(3) is protected by the __BSD_VISIBLE macro and this fails to be
944  * visible in some specific cases. Thus replace it by the following is_blank()
945  * function.
946  */
is_blank(int c)947 static int is_blank(int c)
948 {
949 	return c == ' ' || c == '\t';
950 }
951 
952 /*
953  * Load user-definable keys from file.
954  * A hash table is used to speed up loading process in avoiding string
955  * comparisons.
956  * A log file is also built in case some errors were found in the key
957  * configuration file.
958  */
io_load_keys(const char * pager)959 void io_load_keys(const char *pager)
960 {
961 	struct ht_keybindings_s keys[NBKEYS];
962 	FILE *keyfp;
963 	char buf[BUFSIZ];
964 	struct io_file *log;
965 	int i, skipped, loaded, line;
966 	const int MAX_ERRORS = 5;
967 
968 	keys_init();
969 
970 	struct ht_keybindings ht_keys = HTABLE_INITIALIZER(&ht_keys);
971 
972 	for (i = 0; i < NBKEYS; i++) {
973 		keys[i].key = (enum key)i;
974 		keys[i].label = keys_get_label((enum key)i);
975 		HTABLE_INSERT(ht_keybindings, &ht_keys, &keys[i]);
976 	}
977 
978 	keyfp = fopen(path_keys, "r");
979 	EXIT_IF(keyfp == NULL, _("failed to open key file"));
980 
981 	log = io_log_init();
982 	skipped = loaded = line = 0;
983 	while (fgets(buf, BUFSIZ, keyfp) != NULL) {
984 		char key_label[BUFSIZ], *p;
985 		struct ht_keybindings_s *ht_elm, ht_entry;
986 		const int AWAITED = 1;
987 		int assigned;
988 
989 		line++;
990 		if (skipped > MAX_ERRORS) {
991 			const char *too_many =
992 			    _("\nToo many errors while reading configuration file!\n"
993 			     "Please backup your keys file, remove it from directory, "
994 			     "and launch calcurse again.\n");
995 
996 			io_log_print(log, line, too_many);
997 			break;
998 		}
999 		for (p = buf; is_blank((int)*p); p++) ;
1000 		if (p != buf)
1001 			memmove(buf, p, strlen(p));
1002 		if (buf[0] == '#' || buf[0] == '\n')
1003 			continue;
1004 
1005 		if (sscanf(buf, "%s", key_label) != AWAITED) {
1006 			skipped++;
1007 			io_log_print(log, line,
1008 				     _("Could not read key label"));
1009 			continue;
1010 		}
1011 
1012 		/* Skip legacy entries. */
1013 		if (strcmp(key_label, "generic-cut") == 0)
1014 			continue;
1015 
1016 		ht_entry.label = key_label;
1017 		p = buf + strlen(key_label) + 1;
1018 		ht_elm =
1019 		    HTABLE_LOOKUP(ht_keybindings, &ht_keys, &ht_entry);
1020 		if (!ht_elm) {
1021 			skipped++;
1022 			io_log_print(log, line,
1023 				     _("Key label not recognized"));
1024 			continue;
1025 		}
1026 		assigned = 0;
1027 		for (;;) {
1028 			char key_ch[BUFSIZ], tmpbuf[BUFSIZ];
1029 
1030 			while (*p == ' ')
1031 				p++;
1032 			(void)strncpy(tmpbuf, p, BUFSIZ);
1033 			tmpbuf[BUFSIZ - 1] = '\0';
1034 			if (sscanf(tmpbuf, "%s", key_ch) == AWAITED) {
1035 				int ch;
1036 
1037 				if ((ch = keys_str2int(key_ch)) < 0) {
1038 					char *unknown_key;
1039 
1040 					skipped++;
1041 					asprintf(&unknown_key,
1042 						 _("Error reading key: \"%s\""),
1043 						 key_ch);
1044 					io_log_print(log, line, unknown_key);
1045 					mem_free(unknown_key);
1046 				} else {
1047 					int used;
1048 
1049 					used =
1050 					    keys_assign_binding(ch,
1051 								ht_elm->
1052 								key);
1053 					if (used) {
1054 						char *already_assigned;
1055 
1056 						skipped++;
1057 						asprintf(&already_assigned,
1058 							 _("\"%s\" assigned multiple times!"),
1059 							 key_ch);
1060 						io_log_print(log, line,
1061 							     already_assigned);
1062 						mem_free(already_assigned);
1063 					} else {
1064 						assigned++;
1065 					}
1066 				}
1067 				p += strlen(key_ch) + 1;
1068 			} else {
1069 				if (assigned)
1070 					loaded++;
1071 				break;
1072 			}
1073 		}
1074 	}
1075 	file_close(keyfp, __FILE_POS__);
1076 	file_close(log->fd, __FILE_POS__);
1077 	if (skipped > 0) {
1078 		const char *view_log =
1079 			_("There were some errors when loading keys file.");
1080 		io_log_display(log, view_log, pager);
1081 	}
1082 	io_log_free(log);
1083 	EXIT_IF(skipped > MAX_ERRORS,
1084 		_("Too many errors while reading keys file, aborting..."));
1085 	if (loaded < NBKEYS)
1086 		keys_fill_missing();
1087 	if (keys_check_missing_bindings())
1088 		WARN_MSG(_("Some actions do not have any associated key bindings!"));
1089 }
1090 
io_check_dir(const char * dir)1091 int io_check_dir(const char *dir)
1092 {
1093 	if (read_only)
1094 		return -1;
1095 
1096 	char *path = mem_strdup(dir);
1097 	char *index;
1098 
1099 	int existed = 1, failed = 0;
1100 	errno = 0;
1101 	for (index = path + 1; *index; index++) {
1102 		if (*index == '/') {
1103 			*index = '\0';
1104 			if (mkdir(path, 0700) != 0) {
1105 				if (errno != EEXIST) {
1106 					failed = 1;
1107 					break;
1108 				}
1109 			} else {
1110 				existed = 0;
1111 			}
1112 			*index = '/';
1113 		}
1114 	}
1115 
1116 	if (!failed && mkdir(path, 0700) != 0) {
1117 		if (errno != EEXIST)
1118 			failed = 1;
1119 	} else {
1120 		existed = 0;
1121 	}
1122 
1123 	if(failed) {
1124 		fprintf(stderr,
1125 			_("FATAL ERROR: could not create %s: %s\n"),
1126 			path, strerror(errno));
1127 		exit_calcurse(EXIT_FAILURE);
1128 	}
1129 
1130 	mem_free(path);
1131 	return existed;
1132 }
1133 
io_dir_exists(const char * path)1134 unsigned io_dir_exists(const char *path)
1135 {
1136 	struct stat st;
1137 
1138 	return (!stat(path, &st) && S_ISDIR(st.st_mode));
1139 }
1140 
io_file_exists(const char * file)1141 unsigned io_file_exists(const char *file)
1142 {
1143 	FILE *fd;
1144 
1145 	if (file && (fd = fopen(file, "r")) != NULL) {
1146 		fclose(fd);
1147 		return 1;
1148 	} else {
1149 		return 0;
1150 	}
1151 }
1152 
io_check_file(const char * file)1153 int io_check_file(const char *file)
1154 {
1155 	if (read_only)
1156 		return -1;
1157 
1158 	errno = 0;
1159 	if (io_file_exists(file)) {
1160 		return 1;
1161 	} else {
1162 		FILE *fd;
1163 
1164 		if ((fd = fopen(file, "w")) == NULL) {
1165 			fprintf(stderr,
1166 				_("FATAL ERROR: could not create %s: %s\n"),
1167 				file, strerror(errno));
1168 			exit_calcurse(EXIT_FAILURE);
1169 		}
1170 		file_close(fd, __FILE_POS__);
1171 
1172 		return 0;
1173 	}
1174 }
1175 
1176 /*
1177  * Checks if data files exist. If not, create them.
1178  * The following structure has to be created:
1179  *
1180  *   <datadir>   <configdir>
1181  *        |             |
1182  *        |__ apts      |___ conf
1183  *        |__ todo      |___ keys
1184  *        |__ notes/    |___ hooks/
1185  *
1186  * Defaults:
1187  * - datadir: $XDG_DATA_HOME/calcurse (~/.local/share/calcurse)
1188  * - configdir: $XDG_CONFIG_HOME/calcurse (~/.config/calcurse)
1189  */
io_check_data_files(void)1190 int io_check_data_files(void)
1191 {
1192 	int missing = 0;
1193 
1194 	missing += io_check_dir(path_ddir) ? 0 : 1;
1195 	missing += io_check_dir(path_notes) ? 0 : 1;
1196 	missing += io_check_file(path_todo) ? 0 : 1;
1197 	missing += io_check_file(path_apts) ? 0 : 1;
1198 	missing += io_check_dir(path_cdir) ? 0 : 1;
1199 	missing += io_check_file(path_conf) ? 0 : 1;
1200 	missing += io_check_dir(path_hooks) ? 0 : 1;
1201 
1202 	if (!io_check_file(path_keys)) {
1203 		missing++;
1204 		keys_dump_defaults(path_keys);
1205 	}
1206 
1207 	return missing;
1208 }
1209 
1210 /* Export calcurse data. */
io_export_data(enum export_type type,int export_uid)1211 void io_export_data(enum export_type type, int export_uid)
1212 {
1213 	FILE *stream = NULL;
1214 	const char *success = _("The data were successfully exported");
1215 	const char *enter = _("Press [ENTER] to continue");
1216 
1217 	if (type < IO_EXPORT_ICAL || type >= IO_EXPORT_NBTYPES)
1218 		EXIT(_("unknown export type"));
1219 
1220 	switch (ui_mode) {
1221 	case UI_CMDLINE:
1222 		stream = stdout;
1223 		break;
1224 	case UI_CURSES:
1225 		stream = get_export_stream(type);
1226 		break;
1227 	default:
1228 		EXIT(_("wrong export mode"));
1229 		/* NOTREACHED */
1230 	}
1231 
1232 	if (stream == NULL)
1233 		return;
1234 
1235 	if (type == IO_EXPORT_ICAL)
1236 		ical_export_data(stream, export_uid);
1237 	else if (type == IO_EXPORT_PCAL)
1238 		pcal_export_data(stream);
1239 
1240 	if (!quiet && ui_mode == UI_CURSES) {
1241 		fclose(stream);
1242 		status_mesg(success, enter);
1243 		keys_wait_for_any_key(win[KEY].p);
1244 	}
1245 }
1246 
get_import_stream(enum import_type type,char ** stream_name)1247 static FILE *get_import_stream(enum import_type type, char **stream_name)
1248 {
1249 	FILE *stream = NULL;
1250 	const char *ask_fname =
1251 	    _("Enter the file name to import data from:");
1252 	const char *wrong_file =
1253 	    _("The file cannot be accessed, please enter another file name.");
1254 	const char *press_enter = _("Press [ENTER] to continue.");
1255 
1256 	*stream_name = mem_malloc(BUFSIZ);
1257 	memset(*stream_name, 0, BUFSIZ);
1258 	while (stream == NULL) {
1259 		status_mesg(ask_fname, "");
1260 		if (updatestring(win[STA].p, stream_name, 0, 1)) {
1261 			mem_free(*stream_name);
1262 			return NULL;
1263 		}
1264 		stream = fopen(*stream_name, "r");
1265 		if (stream == NULL) {
1266 			status_mesg(wrong_file, press_enter);
1267 			keys_wait_for_any_key(win[KEY].p);
1268 		}
1269 	}
1270 
1271 	return stream;
1272 }
1273 
1274 /*
1275  * Import data from a given stream (either stdin in non-interactive mode, or the
1276  * user given file in interactive mode).
1277  * A temporary log file is created in /tmp to store the import process report,
1278  * and is cleared at the end.
1279  */
io_import_data(enum import_type type,char * stream_name,const char * fmt_ev,const char * fmt_rev,const char * fmt_apt,const char * fmt_rapt,const char * fmt_todo)1280 int  io_import_data(enum import_type type, char *stream_name,
1281 		    const char *fmt_ev, const char *fmt_rev,
1282 		    const char *fmt_apt, const char *fmt_rapt,
1283 		    const char *fmt_todo)
1284 {
1285 	const char *proc_report =
1286 	    _("Import process report: %04d lines read");
1287 	char *stats_str[4];
1288 	FILE *stream = NULL;
1289 	struct io_file *log;
1290 	struct {
1291 		unsigned events, apoints, todos, lines, skipped;
1292 	} stats;
1293 
1294 	EXIT_IF(type < 0
1295 		|| type >= IO_IMPORT_NBTYPES, _("unknown import type"));
1296 	switch (ui_mode) {
1297 	case UI_CMDLINE:
1298 		if (!strcmp(stream_name, "-"))
1299 			stream = stdin;
1300 		else
1301 			stream = fopen(stream_name, "r");
1302 		EXIT_IF(stream == NULL,
1303 			_("FATAL ERROR: the input file cannot be accessed, "
1304 			 "Aborting..."));
1305 		break;
1306 	case UI_CURSES:
1307 		stream = get_import_stream(type, &stream_name);
1308 		break;
1309 	default:
1310 		EXIT(_("FATAL ERROR: wrong import mode"));
1311 		/* NOTREACHED */
1312 	}
1313 
1314 	if (stream == NULL)
1315 		return 0;
1316 
1317 	memset(&stats, 0, sizeof stats);
1318 
1319 	log = io_log_init();
1320 	if (log == NULL) {
1321 		if (stream != stdin)
1322 			file_close(stream, __FILE_POS__);
1323 		return 0;
1324 	}
1325 
1326 	if (type == IO_IMPORT_ICAL)
1327 		ical_import_data(stream_name, stream, log->fd, &stats.events,
1328 				 &stats.apoints, &stats.todos,
1329 				 &stats.lines, &stats.skipped, fmt_ev, fmt_rev,
1330 				 fmt_apt, fmt_rapt, fmt_todo);
1331 
1332 	if (stream != stdin)
1333 		file_close(stream, __FILE_POS__);
1334 
1335 	if (ui_mode == UI_CURSES &&
1336 	    (stats.apoints > 0 || stats.events > 0 || stats.todos > 0))
1337 		io_set_modified();
1338 
1339 	asprintf(&stats_str[0], ngettext("%d app", "%d apps", stats.apoints),
1340 		 stats.apoints);
1341 	asprintf(&stats_str[1],
1342 		 ngettext("%d event", "%d events", stats.events),
1343 		 stats.events);
1344 	asprintf(&stats_str[2], ngettext("%d todo", "%d todos", stats.todos),
1345 		 stats.todos);
1346 	asprintf(&stats_str[3], _("%d skipped"), stats.skipped);
1347 
1348 	if (ui_mode == UI_CURSES && !quiet) {
1349 		char *read, *stat;
1350 
1351 		asprintf(&read, proc_report, stats.lines);
1352 		asprintf(&stat, "%s / %s / %s / %s (%s)",
1353 			 stats_str[0], stats_str[1], stats_str[2],
1354 			 stats_str[3], _("Press [ENTER] to continue"));
1355 		status_mesg(read, stat);
1356 		mem_free(read);
1357 		mem_free(stat);
1358 		keys_wait_for_any_key(win[KEY].p);
1359 	} else if (ui_mode == UI_CMDLINE && !quiet) {
1360 		printf(proc_report, stats.lines);
1361 		printf("\n%s / %s / %s / %s\n", stats_str[0], stats_str[1],
1362 		       stats_str[2], stats_str[3]);
1363 	}
1364 
1365 	/* User has the choice to look at the log file if some items could not be
1366 	   imported.
1367 	 */
1368 	file_close(log->fd, __FILE_POS__);
1369 	if (stats.skipped > 0) {
1370 		const char *view_log = _("Some items could not be imported.");
1371 		io_log_display(log, view_log, conf.pager);
1372 	}
1373 
1374 	mem_free(stats_str[0]);
1375 	mem_free(stats_str[1]);
1376 	mem_free(stats_str[2]);
1377 	mem_free(stats_str[3]);
1378 	if (ui_mode == UI_CURSES)
1379 		mem_free(stream_name);
1380 	if (!stats.skipped) {
1381 		io_log_free(log);
1382 		return 1;
1383 	} else
1384 		return 0;
1385 }
1386 
io_log_init(void)1387 struct io_file *io_log_init(void)
1388 {
1389 	char *logprefix, *logname;
1390 	struct io_file *log = mem_malloc(sizeof(struct io_file));
1391 
1392 	if (!log) {
1393 		ERROR_MSG(_("Warning: could not open temporary log file, Aborting..."));
1394 		return NULL;
1395 	}
1396 	asprintf(&logprefix, "%s/calcurse_log", get_tempdir());
1397 	logname = new_tempfile(logprefix);
1398 	if (!logname) {
1399 		ERROR_MSG(_("Warning: could not create temporary log file, Aborting..."));
1400 		goto error;
1401 	}
1402 	log->name = mem_strdup(logname);
1403 	log->fd = fopen(log->name, "w");
1404 	if (log->fd == NULL) {
1405 		ERROR_MSG(_("Warning: could not open temporary log file, Aborting..."));
1406 		goto error;
1407 	}
1408 
1409 	goto cleanup;
1410 error:
1411 	mem_free(log);
1412 	log = NULL;
1413 cleanup:
1414 	mem_free(logprefix);
1415 	mem_free(logname);
1416 	return log;
1417 }
1418 
io_log_print(struct io_file * log,int line,const char * msg)1419 void io_log_print(struct io_file *log, int line, const char *msg)
1420 {
1421 	if (log && log->fd)
1422 		fprintf(log->fd, "line %d: %s\n", line, msg);
1423 }
1424 
io_log_display(struct io_file * log,const char * msg,const char * pager)1425 void io_log_display(struct io_file *log, const char *msg, const char *pager)
1426 {
1427 	char *msgq;
1428 
1429 	RETURN_IF(log == NULL, _("No log file to display!"));
1430 	if (ui_mode == UI_CMDLINE) {
1431 		fprintf(stderr, "\n%s\n", msg);
1432 		fprintf(stderr, _("See %s for details."), log->name);
1433 		fputc('\n', stderr);
1434 	} else {
1435 		asprintf(&msgq, "%s %s", msg, _("Display log file?"));
1436 		if (status_ask_bool(msgq) == 1) {
1437 			const char *arg[] = { pager, log->name, NULL };
1438 			wins_launch_external(arg);
1439 		}
1440 		mem_free(msgq);
1441 		wins_erase_status_bar();
1442 	}
1443 }
1444 
io_log_free(struct io_file * log)1445 void io_log_free(struct io_file *log)
1446 {
1447 	if (!log)
1448 		return;
1449 	EXIT_IF(unlink(log->name) != 0,
1450 		_("Warning: could not erase temporary log file %s, Aborting..."),
1451 		log->name);
1452 	mem_free(log->name);
1453 	mem_free(log);
1454 }
1455 
1456 /* Thread used to periodically save data. */
io_psave_thread(void * arg)1457 static void *io_psave_thread(void *arg)
1458 {
1459 	int delay = conf.periodic_save;
1460 	EXIT_IF(delay < 0, _("Invalid delay"));
1461 	char *mesg = _("Periodic save cancelled. Data files have changed. "
1462 		     "Save and merge interactively");
1463 
1464 	pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
1465 	for (;;) {
1466 		sleep(delay * MININSEC);
1467 		pthread_mutex_lock(&io_periodic_save_mutex);
1468 		if (io_save_cal(periodic) == IO_SAVE_CANCEL)
1469 			que_ins(mesg, now(), 2);
1470 		pthread_mutex_unlock(&io_periodic_save_mutex);
1471 	}
1472 }
1473 
1474 /* Launch the thread which handles periodic saves. */
io_start_psave_thread(void)1475 void io_start_psave_thread(void)
1476 {
1477 	pthread_create(&io_t_psave, NULL, io_psave_thread, NULL);
1478 }
1479 
1480 /* Stop periodic data saves. */
io_stop_psave_thread(void)1481 void io_stop_psave_thread(void)
1482 {
1483 	/* Is the thread running? */
1484 	if (pthread_equal(io_t_psave, pthread_self()))
1485 		return;
1486 
1487 	/* Lock the mutex to avoid cancelling the thread during saving. */
1488 	pthread_mutex_lock(&io_periodic_save_mutex);
1489 	pthread_cancel(io_t_psave);
1490 	pthread_join(io_t_psave, NULL);
1491 	pthread_mutex_unlock(&io_periodic_save_mutex);
1492 	io_t_psave = pthread_self();
1493 }
1494 
1495 /*
1496  * This sets a lock file to prevent from having two different instances of
1497  * calcurse running.
1498  *
1499  * If the lock cannot be obtained, then warn the user and exit calcurse. Else,
1500  * create a .calcurse.pid file in the user defined directory, which will be
1501  * removed when calcurse exits.
1502  *
1503  * Note: When creating the lock file, the interactive mode is not initialized
1504  * yet.
1505  */
io_set_lock(void)1506 void io_set_lock(void)
1507 {
1508 	FILE *lock = fopen(path_cpid, "r");
1509 	int pid;
1510 
1511 	if (lock != NULL) {
1512 		/* If there is a lock file, check whether the process exists. */
1513 		if (fscanf(lock, "%d", &pid) == 1) {
1514 			fclose(lock);
1515 			if (kill(pid, 0) != 0 && errno == ESRCH)
1516 				lock = NULL;
1517 		} else {
1518 			fclose(lock);
1519 		}
1520 	}
1521 
1522 	if (lock != NULL) {
1523 		fprintf(stderr,
1524 			_("\nWARNING: it seems that another calcurse instance is "
1525 			 "already running.\n"
1526 			 "If this is not the case, please remove the following "
1527 			 "lock file: \n\"%s\"\n"
1528 			 "and restart calcurse.\n"), path_cpid);
1529 		exit(EXIT_FAILURE);
1530 	}
1531 
1532 	if (!io_dump_pid(path_cpid))
1533 		EXIT(_("FATAL ERROR: could not create %s: %s\n"),
1534 		     path_cpid, strerror(errno));
1535 }
1536 
1537 /*
1538  * Create a new file and write the process pid inside (used to create a simple
1539  * lock for example). Overwrite already existing files.
1540  */
io_dump_pid(char * file)1541 unsigned io_dump_pid(char *file)
1542 {
1543 	pid_t pid;
1544 	FILE *fp;
1545 
1546 	if (!file)
1547 		return 0;
1548 
1549 	pid = getpid();
1550 	if (!(fp = fopen(file, "w"))
1551 	    || fprintf(fp, "%ld\n", (long)pid) < 0 || fclose(fp) != 0)
1552 		return 0;
1553 
1554 	return 1;
1555 }
1556 
1557 /*
1558  * Return the pid number contained in a file previously created with
1559  * io_dump_pid ().
1560  * If no file was found, return 0.
1561  */
io_get_pid(char * file)1562 unsigned io_get_pid(char *file)
1563 {
1564 	FILE *fp;
1565 	unsigned pid;
1566 
1567 	if (!file)
1568 		return 0;
1569 
1570 	if ((fp = fopen(file, "r")) == NULL)
1571 		return 0;
1572 
1573 	if (fscanf(fp, "%u", &pid) != 1)
1574 		return 0;
1575 
1576 	fclose(fp);
1577 
1578 	return pid;
1579 }
1580 
1581 /*
1582  * Check whether two files are equal.
1583  */
io_files_equal(const char * file1,const char * file2)1584 int io_files_equal(const char *file1, const char *file2)
1585 {
1586 	FILE *fp1, *fp2;
1587 	int ret = 0;
1588 
1589 	if (!file1 || !file2)
1590 		return 0;
1591 
1592 	fp1 = fopen(file1, "rb");
1593 	fp2 = fopen(file2, "rb");
1594 
1595 	while (!feof(fp1) && !feof(fp2)) {
1596 		if (fgetc(fp1) != fgetc(fp2))
1597 			goto cleanup;
1598 	}
1599 
1600 	ret = 1;
1601 cleanup:
1602 	fclose(fp1);
1603 	fclose(fp2);
1604 	return ret;
1605 }
1606 
1607 /*
1608  * Copy an existing file to a new location.
1609  */
io_file_cp(const char * src,const char * dst)1610 int io_file_cp(const char *src, const char *dst)
1611 {
1612 	FILE *fp_src, *fp_dst;
1613 	char *buffer[BUFSIZ];
1614 	unsigned int bytes_read;
1615 
1616 	if (!(fp_src = fopen(src, "rb")))
1617 		return 0;
1618 	if (!(fp_dst = fopen(dst, "wb")))
1619 		return 0;
1620 
1621 	while (!feof(fp_src)) {
1622 		bytes_read = fread(buffer, 1, BUFSIZ, fp_src);
1623 		if (bytes_read > 0) {
1624 			if (fwrite(buffer, 1, bytes_read, fp_dst) !=
1625 			    bytes_read)
1626 				return 0;
1627 		} else {
1628 			return 0;
1629 		}
1630 	}
1631 
1632 	fclose(fp_dst);
1633 	fclose(fp_src);
1634 
1635 	return 1;
1636 }
1637 
io_unset_modified(void)1638 void io_unset_modified(void)
1639 {
1640 	modified = 0;
1641 }
1642 
io_set_modified(void)1643 void io_set_modified(void)
1644 {
1645 	modified = 1;
1646 }
1647 
io_get_modified(void)1648 int io_get_modified(void)
1649 {
1650 	return modified;
1651 }
1652