1/*-
2 * SPDX-License-Identifier: BSD-3-Clause
3 *
4 * Copyright (c) 1989, 1993, 1994
5 *	The Regents of the University of California.  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 * 1. Redistributions of source code must retain the above copyright
11 *    notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright
13 *    notice, this list of conditions and the following disclaimer in the
14 *    documentation and/or other materials provided with the distribution.
15 * 3. Neither the name of the University nor the names of its contributors
16 *    may be used to endorse or promote products derived from this software
17 *    without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
23 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29 * SUCH DAMAGE.
30 */
31
32#include <sys/param.h>
33#include <sys/stat.h>
34#include <sys/wait.h>
35#include <ctype.h>
36#include <err.h>
37#include <errno.h>
38#include <fcntl.h>
39#include <libutil.h>
40#include <locale.h>
41#include <pwd.h>
42#include <stdbool.h>
43#include <stdio.h>
44#include <stdlib.h>
45#include <string.h>
46#include <stringlist.h>
47#include <time.h>
48#include <unistd.h>
49
50#include "pathnames.h"
51#include "calendar.h"
52
53enum {
54	T_OK = 0,
55	T_ERR,
56	T_PROCESS,
57};
58
59const char *calendarFile = "calendar";	/* default calendar file */
60static const char *calendarHomes[] = {".calendar", _PATH_INCLUDE_LOCAL, _PATH_INCLUDE}; /* HOME */
61static const char *calendarNoMail = "nomail";/* don't sent mail if file exist */
62
63static char path[MAXPATHLEN];
64static const char *cal_home;
65static const char *cal_dir;
66static const char *cal_file;
67static int cal_line;
68
69struct fixs neaster, npaskha, ncny, nfullmoon, nnewmoon;
70struct fixs nmarequinox, nsepequinox, njunsolstice, ndecsolstice;
71
72static int cal_parse(FILE *in, FILE *out);
73
74static StringList *definitions = NULL;
75static struct event *events[MAXCOUNT];
76static char *extradata[MAXCOUNT];
77
78static char *
79trimlr(char **buf)
80{
81	char *walk = *buf;
82	char *sep;
83	char *last;
84
85	while (isspace(*walk))
86		walk++;
87	*buf = walk;
88
89	sep = walk;
90	while (*sep != '\0' && !isspace(*sep))
91		sep++;
92
93	if (*sep != '\0') {
94		last = sep + strlen(sep) - 1;
95		while (last > walk && isspace(*last))
96			last--;
97		*(last+1) = 0;
98	}
99
100	return (sep);
101}
102
103static FILE *
104cal_fopen(const char *file)
105{
106	static int cwdfd = -1;
107	FILE *fp;
108	char *home = getenv("HOME");
109	unsigned int i;
110	int fd;
111	struct stat sb;
112	static bool warned = false;
113	static char calendarhome[MAXPATHLEN];
114
115	if (home == NULL || *home == '\0') {
116		warnx("Cannot get home directory");
117		return (NULL);
118	}
119
120	/*
121	 * On -a runs, we would have done a chdir() earlier on, but we also
122	 * shouldn't have used the initial cwd anyways lest we bring
123	 * unpredictable behavior upon us.
124	 */
125	if (!doall && cwdfd == -1) {
126		cwdfd = open(".", O_DIRECTORY | O_PATH);
127		if (cwdfd == -1)
128			err(1, "open(cwd)");
129	}
130
131	/*
132	 * Check $PWD first as documented.
133	 */
134	if (cwdfd != -1) {
135		if ((fd = openat(cwdfd, file, O_RDONLY)) != -1) {
136			if ((fp = fdopen(fd, "r")) == NULL)
137				err(1, "fdopen(%s)", file);
138
139			cal_home = NULL;
140			cal_dir = NULL;
141			cal_file = file;
142			return (fp);
143		} else if (errno != ENOENT && errno != ENAMETOOLONG) {
144			err(1, "open(%s)", file);
145		}
146	}
147
148	if (chdir(home) != 0) {
149		warnx("Cannot enter home directory \"%s\"", home);
150		return (NULL);
151	}
152
153	for (i = 0; i < nitems(calendarHomes); i++) {
154		if (snprintf(calendarhome, sizeof (calendarhome), calendarHomes[i],
155			getlocalbase()) >= (int)sizeof (calendarhome))
156			continue;
157
158		if (chdir(calendarhome) != 0)
159			continue;
160
161		if ((fp = fopen(file, "r")) != NULL) {
162			cal_home = home;
163			cal_dir = calendarhome;
164			cal_file = file;
165			return (fp);
166		}
167	}
168
169	warnx("can't open calendar file \"%s\"", file);
170	if (!warned) {
171		snprintf(path, sizeof(path), _PATH_INCLUDE_LOCAL, getlocalbase());
172		if (stat(path, &sb) != 0) {
173			warnx("calendar data files now provided by calendar-data pkg.");
174			warned = true;
175		}
176	}
177
178	return (NULL);
179}
180
181static char*
182cal_path(void)
183{
184	static char buffer[MAXPATHLEN + 10];
185
186	if (cal_dir == NULL)
187		snprintf(buffer, sizeof(buffer), "%s", cal_file);
188	else if (cal_dir[0] == '/')
189		snprintf(buffer, sizeof(buffer), "%s/%s", cal_dir, cal_file);
190	else
191		snprintf(buffer, sizeof(buffer), "%s/%s/%s", cal_home, cal_dir, cal_file);
192	return (buffer);
193}
194
195#define	WARN0(format)		   \
196	warnx(format " in %s line %d", cal_path(), cal_line)
197#define	WARN1(format, arg1)		   \
198	warnx(format " in %s line %d", arg1, cal_path(), cal_line)
199
200static char*
201cmptoken(char *line, const char* token)
202{
203	char len = strlen(token);
204
205	if (strncmp(line, token, len) != 0)
206		return NULL;
207	return (line + len);
208}
209
210static int
211token(char *line, FILE *out, int *skip, int *unskip)
212{
213	char *walk, *sep, a, c;
214	const char *this_cal_home;
215	const char *this_cal_dir;
216	const char *this_cal_file;
217	int this_cal_line;
218
219	while (isspace(*line))
220		line++;
221
222	if (cmptoken(line, "endif")) {
223		if (*skip + *unskip == 0) {
224			WARN0("#endif without prior #ifdef or #ifndef");
225			return (T_ERR);
226		}
227		if (*skip > 0)
228			--*skip;
229		else
230			--*unskip;
231
232		return (T_OK);
233	}
234
235	walk = cmptoken(line, "ifdef");
236	if (walk != NULL) {
237		sep = trimlr(&walk);
238
239		if (*walk == '\0') {
240			WARN0("Expecting arguments after #ifdef");
241			return (T_ERR);
242		}
243		if (*sep != '\0') {
244			WARN1("Expecting a single word after #ifdef "
245			    "but got \"%s\"", walk);
246			return (T_ERR);
247		}
248
249		if (*skip != 0 ||
250		    definitions == NULL || sl_find(definitions, walk) == NULL)
251			++*skip;
252		else
253			++*unskip;
254
255		return (T_OK);
256	}
257
258	walk = cmptoken(line, "ifndef");
259	if (walk != NULL) {
260		sep = trimlr(&walk);
261
262		if (*walk == '\0') {
263			WARN0("Expecting arguments after #ifndef");
264			return (T_ERR);
265		}
266		if (*sep != '\0') {
267			WARN1("Expecting a single word after #ifndef "
268			    "but got \"%s\"", walk);
269			return (T_ERR);
270		}
271
272		if (*skip != 0 ||
273		    (definitions != NULL && sl_find(definitions, walk) != NULL))
274			++*skip;
275		else
276			++*unskip;
277
278		return (T_OK);
279	}
280
281	walk = cmptoken(line, "else");
282	if (walk != NULL) {
283		(void)trimlr(&walk);
284
285		if (*walk != '\0') {
286			WARN0("Expecting no arguments after #else");
287			return (T_ERR);
288		}
289		if (*skip + *unskip == 0) {
290			WARN0("#else without prior #ifdef or #ifndef");
291			return (T_ERR);
292		}
293
294		if (*skip == 0) {
295			++*skip;
296			--*unskip;
297		} else if (*skip == 1) {
298			--*skip;
299			++*unskip;
300		}
301
302		return (T_OK);
303	}
304
305	if (*skip != 0)
306		return (T_OK);
307
308	walk = cmptoken(line, "include");
309	if (walk != NULL) {
310		(void)trimlr(&walk);
311
312		if (*walk == '\0') {
313			WARN0("Expecting arguments after #include");
314			return (T_ERR);
315		}
316
317		if (*walk != '<' && *walk != '\"') {
318			WARN0("Excecting '<' or '\"' after #include");
319			return (T_ERR);
320		}
321
322		a = *walk == '<' ? '>' : '\"';
323		walk++;
324		c = walk[strlen(walk) - 1];
325
326		if (a != c) {
327			WARN1("Unterminated include expecting '%c'", a);
328			return (T_ERR);
329		}
330		walk[strlen(walk) - 1] = '\0';
331
332		this_cal_home = cal_home;
333		this_cal_dir = cal_dir;
334		this_cal_file = cal_file;
335		this_cal_line = cal_line;
336		if (cal_parse(cal_fopen(walk), out))
337			return (T_ERR);
338		cal_home = this_cal_home;
339		cal_dir = this_cal_dir;
340		cal_file = this_cal_file;
341		cal_line = this_cal_line;
342
343		return (T_OK);
344	}
345
346	walk = cmptoken(line, "define");
347	if (walk != NULL) {
348		if (definitions == NULL)
349			definitions = sl_init();
350		sep = trimlr(&walk);
351		*sep = '\0';
352
353		if (*walk == '\0') {
354			WARN0("Expecting arguments after #define");
355			return (T_ERR);
356		}
357
358		if (sl_find(definitions, walk) == NULL)
359			sl_add(definitions, strdup(walk));
360		return (T_OK);
361	}
362
363	walk = cmptoken(line, "undef");
364	if (walk != NULL) {
365		if (definitions != NULL) {
366			sep = trimlr(&walk);
367
368			if (*walk == '\0') {
369				WARN0("Expecting arguments after #undef");
370				return (T_ERR);
371			}
372			if (*sep != '\0') {
373				WARN1("Expecting a single word after #undef "
374				    "but got \"%s\"", walk);
375				return (T_ERR);
376			}
377
378			walk = sl_find(definitions, walk);
379			if (walk != NULL)
380				walk[0] = '\0';
381		}
382		return (T_OK);
383	}
384
385	walk = cmptoken(line, "warning");
386	if (walk != NULL) {
387		(void)trimlr(&walk);
388		WARN1("Warning: %s", walk);
389	}
390
391	walk = cmptoken(line, "error");
392	if (walk != NULL) {
393		(void)trimlr(&walk);
394		WARN1("Error: %s", walk);
395		return (T_ERR);
396	}
397
398	WARN1("Undefined pre-processor command \"#%s\"", line);
399	return (T_ERR);
400}
401
402static void
403setup_locale(const char *locale)
404{
405	(void)setlocale(LC_ALL, locale);
406#ifdef WITH_ICONV
407	if (!doall)
408		set_new_encoding();
409#endif
410	setnnames();
411}
412
413#define	REPLACE(string, slen, struct_) \
414		if (strncasecmp(buf, (string), (slen)) == 0 && buf[(slen)]) { \
415			if (struct_.name != NULL)			      \
416				free(struct_.name);			      \
417			if ((struct_.name = strdup(buf + (slen))) == NULL)    \
418				errx(1, "cannot allocate memory");	      \
419			struct_.len = strlen(buf + (slen));		      \
420			continue;					      \
421		}
422static int
423cal_parse(FILE *in, FILE *out)
424{
425	char *mylocale = NULL;
426	char *line = NULL;
427	char *buf, *bufp;
428	size_t linecap = 0;
429	ssize_t linelen;
430	ssize_t l;
431	static int count = 0;
432	int i;
433	int month[MAXCOUNT];
434	int day[MAXCOUNT];
435	int year[MAXCOUNT];
436	int skip = 0;
437	int unskip = 0;
438	char *pp, p;
439	int flags;
440	char *c, *cc;
441	bool incomment = false;
442
443	if (in == NULL)
444		return (1);
445
446	cal_line = 0;
447	while ((linelen = getline(&line, &linecap, in)) > 0) {
448		cal_line++;
449		buf = line;
450		if (buf[linelen - 1] == '\n')
451			buf[--linelen] = '\0';
452
453		if (incomment) {
454			c = strstr(buf, "*/");
455			if (c) {
456				c += 2;
457				linelen -= c - buf;
458				buf = c;
459				incomment = false;
460			} else {
461				continue;
462			}
463		}
464		if (!incomment) {
465			bufp = buf;
466			do {
467				c = strstr(bufp, "//");
468				cc = strstr(bufp, "/*");
469				if (c != NULL && (cc == NULL || c - cc < 0)) {
470					bufp = c + 2;
471					/* ignore "//" within string to allow it in an URL */
472					if (c == buf || isspace(c[-1])) {
473						/* single line comment */
474						*c = '\0';
475						linelen = c - buf;
476						break;
477					}
478				} else if (cc != NULL) {
479					c = strstr(cc + 2, "*/");
480					if (c != NULL) { // 'a /* b */ c' -- cc=2, c=7+2
481						/* multi-line comment ending on same line */
482						c += 2;
483						memmove(cc, c, buf + linelen + 1 - c);
484						linelen -= c - cc;
485						bufp = cc;
486					} else {
487						/* multi-line comment */
488						*cc = '\0';
489						linelen = cc - buf;
490						incomment = true;
491						break;
492					}
493				}
494			} while (c != NULL || cc != NULL);
495		}
496
497		for (l = linelen;
498		     l > 0 && isspace((unsigned char)buf[l - 1]);
499		     l--)
500			;
501		buf[l] = '\0';
502		if (buf[0] == '\0')
503			continue;
504
505		if (buf == line && *buf == '#') {
506			switch (token(buf+1, out, &skip, &unskip)) {
507			case T_ERR:
508				free(line);
509				return (1);
510			case T_OK:
511				continue;
512			case T_PROCESS:
513				break;
514			default:
515				break;
516			}
517		}
518
519		if (skip != 0)
520			continue;
521
522		/*
523		 * Setting LANG in user's calendar was an old workaround
524		 * for 'calendar -a' being run with C locale to properly
525		 * print user's calendars in their native languages.
526		 * Now that 'calendar -a' does fork with setusercontext(),
527		 * and does not run iconv(), this variable has little use.
528		 */
529		if (strncmp(buf, "LANG=", 5) == 0) {
530			if (mylocale == NULL)
531				mylocale = strdup(setlocale(LC_ALL, NULL));
532			setup_locale(buf + 5);
533			continue;
534		}
535		/* Parse special definitions: Easter, Paskha etc */
536		REPLACE("Easter=", 7, neaster);
537		REPLACE("Paskha=", 7, npaskha);
538		REPLACE("ChineseNewYear=", 15, ncny);
539		REPLACE("NewMoon=", 8, nnewmoon);
540		REPLACE("FullMoon=", 9, nfullmoon);
541		REPLACE("MarEquinox=", 11, nmarequinox);
542		REPLACE("SepEquinox=", 11, nsepequinox);
543		REPLACE("JunSolstice=", 12, njunsolstice);
544		REPLACE("DecSolstice=", 12, ndecsolstice);
545		if (strncmp(buf, "SEQUENCE=", 9) == 0) {
546			setnsequences(buf + 9);
547			continue;
548		}
549
550		/*
551		 * If the line starts with a tab, the data has to be
552		 * added to the previous line
553		 */
554		if (buf[0] == '\t') {
555			for (i = 0; i < count; i++)
556				event_continue(events[i], buf);
557			continue;
558		}
559
560		/* Get rid of leading spaces (non-standard) */
561		while (isspace((unsigned char)buf[0]))
562			memcpy(buf, buf + 1, strlen(buf));
563
564		/* No tab in the line, then not a valid line */
565		if ((pp = strchr(buf, '\t')) == NULL)
566			continue;
567
568		/* Trim spaces in front of the tab */
569		while (isspace((unsigned char)pp[-1]))
570			pp--;
571
572		p = *pp;
573		*pp = '\0';
574		if ((count = parsedaymonth(buf, year, month, day, &flags,
575		    extradata)) == 0)
576			continue;
577		*pp = p;
578		if (count < 0) {
579			/* Show error status based on return value */
580			if (debug)
581				WARN1("Ignored: \"%s\"", buf);
582			if (count == -1)
583				continue;
584			count = -count + 1;
585		}
586
587		/* Find the last tab */
588		while (pp[1] == '\t')
589			pp++;
590
591		for (i = 0; i < count; i++) {
592			if (debug)
593				WARN1("got \"%s\"", pp);
594			events[i] = event_add(year[i], month[i], day[i],
595			    ((flags &= F_VARIABLE) != 0) ? 1 : 0, pp,
596			    extradata[i]);
597		}
598	}
599	while (skip-- > 0 || unskip-- > 0) {
600		cal_line++;
601		WARN0("Missing #endif assumed");
602	}
603
604	free(line);
605	fclose(in);
606	if (mylocale != NULL) {
607		setup_locale(mylocale);
608		free(mylocale);
609	}
610
611	return (0);
612}
613
614void
615cal(void)
616{
617	FILE *fpin;
618	FILE *fpout;
619	int i;
620
621	for (i = 0; i < MAXCOUNT; i++)
622		extradata[i] = (char *)calloc(1, 20);
623
624
625	if ((fpin = opencalin()) == NULL)
626		return;
627
628	if ((fpout = opencalout()) == NULL) {
629		fclose(fpin);
630		return;
631	}
632
633	if (cal_parse(fpin, fpout))
634		return;
635
636	event_print_all(fpout);
637	closecal(fpout);
638}
639
640FILE *
641opencalin(void)
642{
643	struct stat sbuf;
644	FILE *fpin;
645
646	/* open up calendar file */
647	cal_file = calendarFile;
648	if ((fpin = fopen(calendarFile, "r")) == NULL) {
649		if (doall) {
650			if (chdir(calendarHomes[0]) != 0)
651				return (NULL);
652			if (stat(calendarNoMail, &sbuf) == 0)
653				return (NULL);
654			if ((fpin = fopen(calendarFile, "r")) == NULL)
655				return (NULL);
656		} else {
657			fpin = cal_fopen(calendarFile);
658		}
659	}
660	return (fpin);
661}
662
663FILE *
664opencalout(void)
665{
666	int fd;
667
668	/* not reading all calendar files, just set output to stdout */
669	if (!doall)
670		return (stdout);
671
672	/* set output to a temporary file, so if no output don't send mail */
673	snprintf(path, sizeof(path), "%s/_calXXXXXX", _PATH_TMP);
674	if ((fd = mkstemp(path)) < 0)
675		return (NULL);
676	return (fdopen(fd, "w+"));
677}
678
679void
680closecal(FILE *fp)
681{
682	struct stat sbuf;
683	int nread, pdes[2], status;
684	char buf[1024];
685
686	if (!doall)
687		return;
688
689	rewind(fp);
690	if (fstat(fileno(fp), &sbuf) || !sbuf.st_size)
691		goto done;
692	if (pipe(pdes) < 0)
693		goto done;
694	switch (fork()) {
695	case -1:			/* error */
696		(void)close(pdes[0]);
697		(void)close(pdes[1]);
698		goto done;
699	case 0:
700		/* child -- set stdin to pipe output */
701		if (pdes[0] != STDIN_FILENO) {
702			(void)dup2(pdes[0], STDIN_FILENO);
703			(void)close(pdes[0]);
704		}
705		(void)close(pdes[1]);
706		execl(_PATH_SENDMAIL, "sendmail", "-i", "-t", "-F",
707		    "\"Reminder Service\"", (char *)NULL);
708		warn(_PATH_SENDMAIL);
709		_exit(1);
710	}
711	/* parent -- write to pipe input */
712	(void)close(pdes[0]);
713
714	write(pdes[1], "From: \"Reminder Service\" <", 26);
715	write(pdes[1], pw->pw_name, strlen(pw->pw_name));
716	write(pdes[1], ">\nTo: <", 7);
717	write(pdes[1], pw->pw_name, strlen(pw->pw_name));
718	write(pdes[1], ">\nSubject: ", 11);
719	write(pdes[1], dayname, strlen(dayname));
720	write(pdes[1], "'s Calendar\nPrecedence: bulk\n\n", 30);
721
722	while ((nread = read(fileno(fp), buf, sizeof(buf))) > 0)
723		(void)write(pdes[1], buf, nread);
724	(void)close(pdes[1]);
725done:	(void)fclose(fp);
726	(void)unlink(path);
727	while (wait(&status) >= 0);
728}
729