Blob


1 /*
2 * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
17 #include "compat.h"
19 #include <sys/time.h>
21 #include <errno.h>
22 #include <fcntl.h>
23 #include <limits.h>
24 #include <stdio.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include <time.h>
28 #include <unistd.h>
30 #include "defaults.h"
31 #include "ev.h"
32 #include "fs.h"
33 #include "hist.h"
34 #include "imsgev.h"
35 #include "minibuffer.h"
36 #include "session.h"
37 #include "tofu.h"
38 #include "ui.h"
40 struct history history;
42 static unsigned int autosavetimer;
44 void
45 switch_to_tab(struct tab *tab)
46 {
47 current_tab = tab;
48 tab->flags &= ~TAB_URGENT;
50 if (operating && tab->flags & TAB_LAZY)
51 load_url_in_tab(tab, hist_cur(tab->hist), NULL,
52 LU_MODE_NOHIST);
53 }
55 unsigned int
56 tab_new_id(void)
57 {
58 static uint32_t tab_counter;
60 return tab_counter++;
61 }
63 struct tab *
64 new_tab(const char *url, const char *base, struct tab *after)
65 {
66 struct tab *tab;
68 ui_schedule_redraw();
69 autosave_hook();
71 if ((tab = calloc(1, sizeof(*tab))) == NULL) {
72 ev_break();
73 return NULL;
74 }
76 if ((tab->hist = hist_new(HIST_LINEAR)) == NULL) {
77 free(tab);
78 ev_break();
79 return NULL;
80 }
82 TAILQ_INIT(&tab->buffer.head);
83 TAILQ_INIT(&tab->buffer.page.head);
85 tab->id = tab_new_id();
87 if (after != NULL)
88 TAILQ_INSERT_AFTER(&tabshead, after, tab, tabs);
89 else
90 TAILQ_INSERT_TAIL(&tabshead, tab, tabs);
92 if (!operating)
93 tab->flags |= TAB_LAZY;
94 load_url_in_tab(tab, url, base, 0);
95 switch_to_tab(tab);
96 return tab;
97 }
99 /*
100 * Move a tab from the tablist to the killed tab list and erase its
101 * contents. Append should always be 0 to prepend tabs so unkill_tab
102 * can work correctly; appending is only useful during startup when
103 * receiving the list of killed tabs to keep the correct order.
104 * NB: doesn't update the current_tab.
105 */
106 void
107 kill_tab(struct tab *tab, int append)
109 int count;
111 stop_tab(tab);
112 erase_buffer(&tab->buffer);
113 TAILQ_REMOVE(&tabshead, tab, tabs);
114 ui_schedule_redraw();
115 autosave_hook();
117 ev_timer_cancel(tab->loading_timer);
119 if (append)
120 TAILQ_INSERT_TAIL(&ktabshead, tab, tabs);
121 else
122 TAILQ_INSERT_HEAD(&ktabshead, tab, tabs);
124 /* gc closed tabs */
125 count = 0;
126 TAILQ_FOREACH(tab, &ktabshead, tabs)
127 count++;
128 while (count > max_killed_tabs) {
129 count--;
130 free_tab(TAILQ_LAST(&ktabshead, tabshead));
134 /*
135 * Resurrects the lastest killed tab and returns it. The tab is already
136 * added to the tab list with the TAB_LAZY flag set. NB: this doesn't
137 * update current_tab.
138 */
139 struct tab *
140 unkill_tab(void)
142 struct tab *t;
144 if (TAILQ_EMPTY(&ktabshead))
145 return NULL;
147 ui_schedule_redraw();
148 autosave_hook();
150 t = TAILQ_FIRST(&ktabshead);
151 TAILQ_REMOVE(&ktabshead, t, tabs);
152 TAILQ_INSERT_TAIL(&tabshead, t, tabs);
153 t->flags |= TAB_LAZY;
154 return t;
157 /*
158 * Free every resource linked to the tab, including the tab itself, and
159 * removes it from the *killed* tablist.
160 */
161 void
162 free_tab(struct tab *tab)
164 TAILQ_REMOVE(&ktabshead, tab, tabs);
165 hist_free(tab->hist);
166 free(tab);
169 void
170 stop_tab(struct tab *tab)
172 ui_send_net(IMSG_STOP, tab->id, -1, NULL, 0);
175 static inline void
176 savetab(FILE *fp, struct tab *tab, int killed)
178 size_t i, size, cur;
179 size_t top_line, current_line;
181 get_scroll_position(tab, &top_line, &current_line);
183 fprintf(fp, "%s ", hist_cur(tab->hist));
184 if (tab == current_tab)
185 fprintf(fp, "current,");
186 if (killed)
187 fprintf(fp, "killed,");
189 fprintf(fp, "top=%zu,cur=%zu %s\n", top_line, current_line,
190 tab->buffer.page.title);
192 cur = hist_off(tab->hist);
193 size = hist_size(tab->hist);
194 for (i = 0; i < size; ++i) {
195 if (i == cur)
196 continue;
197 fprintf(fp, "%s %s\n", i > cur ? ">" : "<",
198 hist_nth(tab->hist, i));
202 static void
203 save_tabs(void)
205 FILE *fp;
206 struct tab *tab;
207 int fd, err;
208 char sfn[PATH_MAX];
210 strlcpy(sfn, session_file_tmp, sizeof(sfn));
211 if ((fd = mkstemp(sfn)) == -1 ||
212 (fp = fdopen(fd, "w")) == NULL) {
213 if (fd != -1) {
214 unlink(sfn);
215 close(fd);
217 return;
220 TAILQ_FOREACH(tab, &tabshead, tabs)
221 savetab(fp, tab, 0);
222 TAILQ_FOREACH(tab, &ktabshead, tabs)
223 savetab(fp, tab, 1);
225 err = fflush(fp) == EOF;
226 fclose(fp);
228 if (err || rename(sfn, session_file) == -1)
229 unlink(sfn);
232 static void
233 save_all_history(void)
235 FILE *fp;
236 size_t i;
237 int fd, err;
238 char sfn[PATH_MAX];
240 strlcpy(sfn, history_file_tmp, sizeof(sfn));
241 if ((fd = mkstemp(sfn)) == -1 ||
242 (fp = fdopen(fd, "w")) == NULL) {
243 if (fd != -1) {
244 unlink(sfn);
245 close(fd);
247 return;
250 for (i = 0; i < history.len; ++i) {
251 history.items[i].dirty = 0;
252 fprintf(fp, "%lld %s\n", (long long)history.items[i].ts,
253 history.items[i].uri);
256 err = fflush(fp) == EOF;
257 fclose(fp);
259 if (err || rename(sfn, history_file) == -1) {
260 unlink(sfn);
261 return;
264 history.dirty = 0;
265 history.extra = 0;
268 static void
269 save_dirty_history(void)
271 FILE *fp;
272 size_t i;
274 if ((fp = fopen(history_file, "a")) == NULL)
275 return;
277 for (i = 0; i < history.len && history.dirty > 0; ++i) {
278 if (!history.items[i].dirty)
279 continue;
280 history.dirty--;
281 history.items[i].dirty = 0;
282 fprintf(fp, "%lld %s\n", (long long)history.items[i].ts,
283 history.items[i].uri);
285 history.dirty = 0;
287 fclose(fp);
290 void
291 save_session(void)
293 if (safe_mode)
294 return;
296 save_tabs();
298 if (history.extra > HISTORY_CAP/2)
299 save_all_history();
300 else if (history.dirty)
301 save_dirty_history();
304 void
305 history_push(struct histitem *hi)
307 size_t i, oldest = 0;
308 char *uri;
310 for (i = 0; i < history.len; ++i) {
311 if (history.items[i].ts < history.items[oldest].ts)
312 oldest = i;
314 /* remove duplicates */
315 if (!strcmp(history.items[i].uri, hi->uri))
316 return;
319 if ((uri = strdup(hi->uri)) == NULL)
320 abort();
322 /* don't grow too much; replace the oldest */
323 if (history.len == HISTORY_CAP) {
324 history.items[oldest].ts = hi->ts;
325 free(history.items[oldest].uri);
326 history.items[oldest].uri = uri;
328 /* Growed past the max value, signal to regen the file. */
329 history.extra++;
330 return;
333 history.items[history.len].ts = hi->ts;
334 history.items[history.len].uri = uri;
335 history.len++;
338 static int
339 history_cmp(const void *a, const void *b)
341 const struct history_item *i = a, *j = b;
342 return strcmp(i->uri, j->uri);
345 void
346 history_sort(void)
348 qsort(history.items, history.len, sizeof(history.items[0]),
349 history_cmp);
352 void
353 history_add(const char *uri)
355 size_t i, j, insert = 0, oldest = 0;
356 char *u;
357 int c;
359 for (i = 0; i < history.len; ++i) {
360 if (history.items[i].ts < history.items[oldest].ts)
361 oldest = i;
363 if (insert != 0 && insert < i)
364 continue;
366 c = strcmp(uri, history.items[i].uri);
367 if (c == 0) {
368 history.items[i].ts = time(NULL);
369 history.items[i].dirty = 1;
370 history.dirty++;
371 autosave_hook();
372 return;
375 if (c > 0)
376 insert = i;
379 if ((u = strdup(uri)) == NULL)
380 return;
382 /* if history is full, replace the oldest one */
383 if (history.len == HISTORY_CAP) {
384 free(history.items[oldest].uri);
385 history.items[oldest].uri = u;
386 history.items[oldest].ts = time(NULL);
387 history.items[oldest].dirty = 1;
388 history.dirty++;
389 history_sort();
390 autosave_hook();
391 return;
394 /* otherwise just insert in the right spot */
396 for (j = history.len; j > insert; --j)
397 memcpy(&history.items[j], &history.items[j-1],
398 sizeof(history.items[j]));
400 history.items[insert].ts = time(NULL);
401 history.items[insert].uri = u;
402 history.items[insert].dirty = 1;
403 history.dirty++;
404 history.len++;
405 autosave_hook();
408 void
409 autosave_init(void)
411 return;
414 void
415 autosave_timer(int fd, int event, void *data)
417 save_session();
420 /*
421 * Function to be called in "interesting" places where we may want to
422 * schedule an autosave (like on new tab or before loading an url.)
423 */
424 void
425 autosave_hook(void)
427 struct timeval tv;
429 if (autosave <= 0)
430 return;
432 if (!ev_timer_pending(autosavetimer)) {
433 tv.tv_sec = autosave;
434 tv.tv_usec = 0;
436 autosavetimer = ev_timer(&tv, autosave_timer, NULL);
440 static inline int
441 parse_khost_line(char *line, char *tmp[3])
443 char **ap;
445 for (ap = tmp; ap < &tmp[3] &&
446 (*ap = strsep(&line, " \t\n")) != NULL;) {
447 if (**ap != '\0')
448 ap++;
451 return ap == &tmp[3] && *line == '\0';
454 static void
455 load_certs(struct ohash *certs)
457 char *tmp[3], *line = NULL;
458 const char *errstr;
459 size_t lineno = 0, linesize = 0;
460 ssize_t linelen;
461 FILE *f;
462 struct tofu_entry *e;
464 if ((f = fopen(known_hosts_file, "r")) == NULL)
465 return;
467 if ((e = calloc(1, sizeof(*e))) == NULL) {
468 fclose(f);
469 return;
472 while ((linelen = getline(&line, &linesize, f)) != -1) {
473 lineno++;
475 if (parse_khost_line(line, tmp)) {
476 strlcpy(e->domain, tmp[0], sizeof(e->domain));
477 strlcpy(e->hash, tmp[1], sizeof(e->hash));
479 e->verified = strtonum(tmp[2], 0, 1, &errstr);
480 if (errstr != NULL)
481 errx(1, "%s:%zu verification for %s is %s: %s",
482 known_hosts_file, lineno,
483 e->domain, errstr, tmp[2]);
485 tofu_add(certs, e);
486 } else
487 warnx("%s:%zu invalid entry",
488 known_hosts_file, lineno);
491 free(line);
492 fclose(f);
493 return;
497 static void
498 load_hist(void)
500 FILE *hist;
501 size_t linesize = 0;
502 ssize_t linelen;
503 char *nl, *spc, *line = NULL;
504 const char *errstr;
505 struct histitem hi;
507 if ((hist = fopen(history_file, "r")) == NULL)
508 return;
510 while ((linelen = getline(&line, &linesize, hist)) != -1) {
511 if ((nl = strchr(line, '\n')) != NULL)
512 *nl = '\0';
513 if ((spc = strchr(line, ' ')) == NULL)
514 continue;
515 *spc = '\0';
516 spc++;
518 memset(&hi, 0, sizeof(hi));
519 hi.ts = strtonum(line, INT64_MIN, INT64_MAX, &errstr);
520 if (errstr != NULL)
521 continue;
522 if (strlcpy(hi.uri, spc, sizeof(hi.uri)) >= sizeof(hi.uri))
523 continue;
525 history_push(&hi);
528 fclose(hist);
529 free(line);
531 history_sort();
534 /*
535 * Check if the last time telescope crashed. The check is done by
536 * looking at `crashed_file': if it exists then last time we crashed.
537 * Then, while here, touch the file too, it's removed during the
538 * teardown.
539 */
540 static int
541 last_time_crashed(void)
543 int fd, crashed = 1;
545 if (safe_mode)
546 return 0;
548 if (unlink(crashed_file) == -1 && errno == ENOENT)
549 crashed = 0;
551 if ((fd = open(crashed_file, O_CREAT|O_WRONLY, 0600)) == -1)
552 return crashed;
553 close(fd);
555 return crashed;
558 /*
559 * Parse and restore a tab from the session file. The format is:
561 * URL [flags,...] [title]\n
562 */
563 static inline struct tab *
564 parse_tab_line(char *line, struct tab **ct)
566 struct tab *tab;
567 char *s, *t, *ap;
568 const char *uri, *title = "";
569 int current = 0, killed = 0;
570 size_t tline = 0, cline = 0;
572 uri = line;
573 if ((s = strchr(line, ' ')) == NULL)
574 return NULL;
575 *s++ = '\0';
577 if ((t = strchr(s, ' ')) != NULL) {
578 *t++ = '\0';
579 title = t;
582 while ((ap = strsep(&s, ",")) != NULL) {
583 if (!strcmp(ap, "current"))
584 current = 1;
585 else if (!strcmp(ap, "killed"))
586 killed = 1;
587 else if (!strncmp(ap, "top=", 4))
588 tline = strtonum(ap+4, 0, UINT32_MAX, NULL);
589 else if (!strncmp(ap, "cur=", 4))
590 cline = strtonum(ap + 4, 0, UINT32_MAX, NULL);
593 if (tline > cline) {
594 tline = 0;
595 cline = 0;
598 if ((tab = new_tab(uri, NULL, NULL)) == NULL)
599 err(1, "new_tab");
600 hist_set_offs(tab->hist, tline, cline);
601 strlcpy(tab->buffer.page.title, title, sizeof(tab->buffer.page.title));
603 if (current)
604 *ct = tab;
605 else if (killed)
606 kill_tab(tab, 1);
608 return tab;
611 static void
612 load_tabs(void)
614 struct tab *tab = NULL, *ct = NULL;
615 FILE *session;
616 size_t lineno = 0, linesize = 0;
617 ssize_t linelen;
618 char *uri, *line = NULL;
620 if ((session = fopen(session_file, "r")) == NULL) {
621 new_tab("about:new", NULL, NULL);
622 new_tab("about:help", NULL, NULL);
623 return;
626 while ((linelen = getline(&line, &linesize, session)) != -1) {
627 lineno++;
629 if (linelen > 0 && line[linelen-1] == '\n')
630 line[linelen-1] = '\0';
632 if (*line == '<' || *line == '>') {
633 uri = line + 1;
634 if (*uri != ' ' || tab == NULL) {
635 fprintf(stderr, "%s:%zu invalid line\n",
636 session_file, lineno);
637 continue;
639 uri++;
641 if (*line == '>') /* future hist */
642 hist_append(tab->hist, uri);
643 else
644 hist_prepend(tab->hist, uri);
645 } else
646 tab = parse_tab_line(line, &ct);
649 fclose(session);
650 free(line);
652 if (ct == NULL || TAILQ_EMPTY(&tabshead))
653 ct = new_tab("about:new", NULL, NULL);
655 switch_to_tab(ct);
657 if (last_time_crashed())
658 new_tab("about:crash", NULL, NULL);
661 int
662 load_session(struct ohash *certs)
664 load_certs(certs);
665 load_hist();
666 load_tabs();
667 return 0;
670 int
671 lock_session(void)
673 struct flock lock;
674 int fd;
676 if ((fd = open(lockfile_path, O_WRONLY|O_CREAT, 0600)) == -1)
677 return -1;
679 lock.l_start = 0;
680 lock.l_len = 0;
681 lock.l_type = F_WRLCK;
682 lock.l_whence = SEEK_SET;
684 if (fcntl(fd, F_SETLK, &lock) == -1) {
685 close(fd);
686 return -1;
689 return fd;