]> git.armaanb.net Git - chorizo.git/blob - browser.c
draft 8
[chorizo.git] / browser.c
1 #include <errno.h>
2 #include <fcntl.h>
3 #include <libgen.h>
4 #include <sys/stat.h>
5 #include <unistd.h>
6 #include <webkit2/webkit2.h>
7
8 #include "config.h"
9 #include "chorizo.h"
10
11 struct MainWindow mw;
12 struct Configuration cfg;
13 gboolean switch_tab;
14
15 gint clients = 0;
16
17 int cooperative_pipe_fp = 0;
18 gchar *fifopath;
19 char **closed_tabs;
20 size_t num_closed = 0;
21
22 gboolean
23 isearch_counted_matches(GtkWidget *widget, guint matches, gpointer data)
24 {
25         struct Client *c = (struct Client *)data;
26         char *text = malloc(12);
27         sprintf(text, "%d matches", matches);
28         gtk_label_set_text(GTK_LABEL(c->isearch_matches), text);
29         free(text);
30         return TRUE;
31 }
32
33 gboolean
34 quit_if_nothing_active(void)
35 {
36         if (clients == 0) {
37                 if (downloads == 0) {
38                         gtk_main_quit();
39                         return TRUE;
40                 } else {
41                         downloadmanager_show();
42                 }
43         }
44         return FALSE;
45 }
46 gboolean
47 remote_msg(GIOChannel *channel, GIOCondition condition, gpointer data)
48 {
49         gchar *uri = NULL;
50         g_io_channel_read_line(channel, &uri, NULL, NULL, NULL);
51         if (uri) {
52                 g_strstrip(uri);
53                 client_new(uri, NULL);
54                 g_free(uri);
55         }
56         return TRUE;
57 }
58
59 void
60 allocfail(void)
61 {
62         fprintf(stderr, "chorizo: fatal: alloc failed\n");
63         exit(EXIT_FAILURE);
64 }
65
66 void
67 client_destroy(GtkWidget *widget, gpointer data)
68 {
69         struct Client *c = (struct Client *)data;
70         gint idx;
71         g_signal_handlers_disconnect_by_func(G_OBJECT(c->web_view),
72                                              changed_load_progress, c);
73
74         idx = gtk_notebook_page_num(GTK_NOTEBOOK(mw.notebook), c->vbox);
75         if (idx == -1)
76                 fprintf(stderr, "chorizo: warning: tab index was -1\n");
77         else
78                 gtk_notebook_remove_page(GTK_NOTEBOOK(mw.notebook), idx);
79
80         if (!cfg.private && WEBKIT_IS_WEB_VIEW(c->web_view)) {
81                 const char *uri =
82                         webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
83
84                 // TODO: Shift everything left if over certain amount
85                 if (uri != NULL) {
86                         num_closed++;
87                         if (num_closed > cfg_max_tabs_closed) {
88                                 memmove(closed_tabs, closed_tabs,
89                                         cfg_max_tabs_closed - 1);
90                                 num_closed = cfg_max_tabs_closed;
91                         } else {
92                                 closed_tabs = realloc(
93                                         closed_tabs,
94                                         num_closed * sizeof(closed_tabs[0]));
95                                 if (!closed_tabs) allocfail();
96                         }
97                         closed_tabs[num_closed - 1] = strdup(uri);
98                 }
99         }
100
101         clients--;
102         free(c);
103
104         quit_if_nothing_active();
105 }
106
107 void
108 set_uri(const char *uri, struct Client *c)
109 {
110         if (!gtk_widget_is_focus(c->location))
111                 gtk_entry_set_text(GTK_ENTRY(c->location),
112                                    (uri != NULL) ? uri : "");
113 }
114
115 WebKitWebView *
116 client_new(const gchar *uri, WebKitWebView *related_wv)
117 {
118         struct Client *c;
119         gchar *f;
120         GtkWidget *evbox, *tabbox;
121         if (uri != NULL && !cfg.noncooperative_instances &&
122             !cfg.cooperative_alone) {
123                 f = ensure_uri_scheme(uri);
124                 write(cooperative_pipe_fp, f, strlen(f));
125                 write(cooperative_pipe_fp, "\n", 1);
126                 g_free(f);
127                 return NULL;
128         }
129         c = calloc(1, sizeof(struct Client));
130         if (!c) allocfail();
131
132         if (related_wv == NULL) {
133                 WebKitUserContentManager *ucm =
134                         webkit_user_content_manager_new();
135                 WebKitUserScript *wkscript;
136                 WebKitUserStyleSheet *wkstyle;
137                 gchar *path = NULL, *source, *base;
138                 const gchar *entry = NULL;
139                 GDir *dir = NULL;
140                 base = g_build_filename(g_get_user_data_dir(), "chorizo",
141                                         "user-scripts", NULL);
142                 dir = g_dir_open(base, 0, NULL);
143                 if (dir != NULL) {
144                         while ((entry = g_dir_read_name(dir)) != NULL) {
145                                 path = g_build_filename(base, entry, NULL);
146                                 if (g_str_has_suffix(path, ".js")) {
147                                         g_file_get_contents(path, &source, NULL,
148                                                             NULL);
149                                         // clang-format off
150                                         wkscript = webkit_user_script_new(
151                                                 source,
152                                           WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
153                                     WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
154                                                 NULL, NULL);
155                                         // clang-format on
156                                         webkit_user_content_manager_add_script(
157                                                 ucm, wkscript);
158                                         webkit_user_script_unref(wkscript);
159                                 }
160                                 g_free(path);
161                                 if (source) g_free(source);
162                         }
163                         g_dir_close(dir);
164                 }
165                 base = g_build_filename(g_get_user_data_dir(), "chorizo",
166                                         "user-styles", NULL);
167                 dir = g_dir_open(base, 0, NULL);
168                 if (dir != NULL) {
169                         while ((entry = g_dir_read_name(dir)) != NULL) {
170                                 path = g_build_filename(base, entry, NULL);
171                                 if (g_str_has_suffix(path, ".css")) {
172                                         g_file_get_contents(path, &source, NULL,
173                                                             NULL);
174                                         // clang-format off
175                                         wkstyle = webkit_user_style_sheet_new(
176                                                 source,
177                                           WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
178                                                    WEBKIT_USER_STYLE_LEVEL_USER,
179                                                 NULL, NULL);
180                                     webkit_user_content_manager_add_style_sheet(
181                                                 ucm, wkstyle);
182                                         // clang-format on
183                                         webkit_user_style_sheet_unref(wkstyle);
184                                 }
185                                 g_free(path);
186                                 g_free(source);
187                         }
188                         g_dir_close(dir);
189                 }
190                 g_free(base);
191
192                 c->web_view =
193                         webkit_web_view_new_with_user_content_manager(ucm);
194
195                 c->settings = webkit_web_view_get_settings(
196                         WEBKIT_WEB_VIEW(c->web_view));
197                 if (cfg.verbose) {
198                         // clang-format off
199                 webkit_settings_set_enable_write_console_messages_to_stdout(
200                                 c->settings, true);
201                         // clang-format on
202                 }
203                 webkit_settings_set_enable_developer_extras(c->settings, TRUE);
204         } else {
205                 c->web_view = webkit_web_view_new_with_related_view(related_wv);
206         }
207
208         g_signal_connect(G_OBJECT(c->web_view), "notify::favicon",
209                          G_CALLBACK(changed_favicon), c);
210         g_signal_connect(G_OBJECT(c->web_view), "notify::title",
211                          G_CALLBACK(changed_title), c);
212         g_signal_connect(G_OBJECT(c->web_view), "notify::uri",
213                          G_CALLBACK(changed_uri), c);
214         g_signal_connect(G_OBJECT(c->web_view),
215                          "notify::estimated-load-progress",
216                          G_CALLBACK(changed_load_progress), c);
217         g_signal_connect(G_OBJECT(c->web_view), "create",
218                          G_CALLBACK(client_new_request), c);
219         g_signal_connect(G_OBJECT(c->web_view), "close",
220                          G_CALLBACK(client_destroy), c);
221         g_signal_connect(G_OBJECT(c->web_view), "decide-policy",
222                          G_CALLBACK(decide_policy), NULL);
223         g_signal_connect(G_OBJECT(c->web_view), "key-press-event",
224                          G_CALLBACK(key_web_view), c);
225         g_signal_connect(G_OBJECT(c->web_view), "scroll-event",
226                          G_CALLBACK(key_web_view), c);
227         g_signal_connect(G_OBJECT(c->web_view), "mouse-target-changed",
228                          G_CALLBACK(hover_web_view), c);
229         g_signal_connect(G_OBJECT(c->web_view), "web-process-crashed",
230                          G_CALLBACK(crashed_web_view), c);
231
232         GtkWidget *locbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
233
234         c->location = gtk_entry_new();
235         gtk_entry_set_placeholder_text(GTK_ENTRY(c->location), "URL");
236         char *location_symbol;
237         if (cfg.private) {
238                 location_symbol = "security-high-symbolic";
239                 gtk_entry_set_icon_tooltip_markup(
240                         GTK_ENTRY(c->location), GTK_ENTRY_ICON_PRIMARY,
241                         "You are in private mode. No history, caches, or "
242                         "cookies will be saved beyond this session.");
243         } else {
244                 location_symbol = "text-x-generic-symbolic";
245         }
246
247         gtk_entry_set_icon_from_icon_name(GTK_ENTRY(c->location),
248                                           GTK_ENTRY_ICON_PRIMARY,
249                                           location_symbol);
250         gtk_box_pack_start(GTK_BOX(locbox), c->location, TRUE, TRUE, 0);
251
252         c->wsearch = gtk_entry_new();
253         gtk_entry_set_placeholder_text(GTK_ENTRY(c->wsearch), "Search the web");
254         gtk_entry_set_icon_from_icon_name(GTK_ENTRY(c->wsearch),
255                                           GTK_ENTRY_ICON_PRIMARY,
256                                           "system-search-symbolic");
257         gtk_box_pack_start(GTK_BOX(locbox), c->wsearch, FALSE, TRUE, 0);
258
259         g_signal_connect(G_OBJECT(c->location), "key-press-event",
260                          G_CALLBACK(key_location), c);
261         g_signal_connect(G_OBJECT(c->location), "icon-release",
262                          G_CALLBACK(icon_location), c);
263         g_signal_connect(G_OBJECT(c->wsearch), "key-press-event",
264                          G_CALLBACK(key_wsearch), c);
265         /*
266          * XXX This is a workaround. Setting this to NULL (which is done in
267          * grab_feeds_finished() if no feed has been detected) adds a little
268          * padding left of the text. Not sure why. The point of this call
269          * right here is to have that padding right from the start. This
270          * avoids a graphical artifact.
271          */
272
273         gtk_entry_set_icon_from_icon_name(GTK_ENTRY(c->location),
274                                           GTK_ENTRY_ICON_SECONDARY, NULL);
275
276         c->isearch_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
277         c->isearch = gtk_entry_new();
278         gtk_entry_set_placeholder_text(GTK_ENTRY(c->isearch), "Search page");
279         gtk_box_pack_start(GTK_BOX(c->isearch_box), c->isearch, FALSE, TRUE, 0);
280         gtk_entry_set_icon_from_icon_name(GTK_ENTRY(c->isearch),
281                                           GTK_ENTRY_ICON_PRIMARY,
282                                           "system-search-symbolic");
283         g_signal_connect(G_OBJECT(c->isearch), "key-press-event",
284                          G_CALLBACK(key_isearch), c);
285
286         c->isearch_matches = gtk_label_new("0 matches");
287         gtk_box_pack_start(GTK_BOX(c->isearch_box), c->isearch_matches, FALSE,
288                            TRUE, 5);
289
290         c->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
291         gtk_box_pack_start(GTK_BOX(c->vbox), c->web_view, TRUE, TRUE, 0);
292         gtk_box_pack_start(GTK_BOX(c->vbox), c->isearch_box, FALSE, TRUE, 0);
293         gtk_box_pack_start(GTK_BOX(c->vbox), locbox, FALSE, FALSE, 0);
294
295         gtk_container_set_focus_child(GTK_CONTAINER(c->vbox), c->web_view);
296
297         c->tabicon = gtk_image_new_from_icon_name("text-html",
298                                                   GTK_ICON_SIZE_SMALL_TOOLBAR);
299
300         c->tablabel = gtk_label_new("chorizo");
301         gtk_label_set_ellipsize(GTK_LABEL(c->tablabel), PANGO_ELLIPSIZE_END);
302         gtk_label_set_width_chars(GTK_LABEL(c->tablabel), cfg_tab_width);
303         gtk_widget_set_has_tooltip(c->tablabel, TRUE);
304
305         /*
306          * XXX I don't own a HiDPI screen, so I don't know if scale_factor
307          * does the right thing.
308          */
309         tabbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL,
310                              5 * gtk_widget_get_scale_factor(mw.win));
311         gtk_box_pack_start(GTK_BOX(tabbox), c->tabicon, FALSE, FALSE, 0);
312         gtk_box_pack_start(GTK_BOX(tabbox), c->tablabel, TRUE, TRUE, 0);
313
314         evbox = gtk_event_box_new();
315         gtk_container_add(GTK_CONTAINER(evbox), tabbox);
316         g_signal_connect(G_OBJECT(evbox), "button-release-event",
317                          G_CALLBACK(key_tablabel), c);
318
319         gtk_widget_add_events(evbox, GDK_SCROLL_MASK);
320         g_signal_connect(G_OBJECT(evbox), "scroll-event",
321                          G_CALLBACK(key_tablabel), c);
322
323         // For easy access, store a reference to our label.
324         g_object_set_data(G_OBJECT(evbox), "chorizo-tab-label", c->tablabel);
325
326         /*
327          * This only shows the event box and the label inside, nothing else.
328          * Needed because the evbox/label is "internal" to the notebook and
329          * not part of the normal "widget tree" (IIUC).
330          */
331         gtk_widget_show_all(evbox);
332
333         int page = gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.notebook)) + 1;
334         gtk_notebook_insert_page(GTK_NOTEBOOK(mw.notebook), c->vbox, evbox,
335                                  page);
336         gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(mw.notebook), c->vbox,
337                                          TRUE);
338
339         show_web_view(NULL, c);
340
341         if (uri != NULL) {
342                 f = ensure_uri_scheme(uri);
343                 webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), f);
344                 g_free(f);
345         }
346         set_uri(uri, c);
347
348         clients++;
349
350         if (uri == NULL) gtk_widget_grab_focus(c->location);
351
352         return WEBKIT_WEB_VIEW(c->web_view);
353 }
354
355 WebKitWebView *
356 client_new_request(WebKitWebView *web_view,
357                    WebKitNavigationAction *navigation_action, gpointer data)
358 {
359         switch_tab = FALSE;
360         WebKitWebView *new_web_view = client_new(NULL, web_view);
361         gtk_widget_grab_focus(GTK_WIDGET(new_web_view));
362         return new_web_view;
363 }
364
365 void
366 mkdirp(const char *dir, mode_t mode)
367 {
368         char tmp[256];
369         char *p = NULL;
370         size_t len;
371         snprintf(tmp, sizeof(tmp), "%s", dir);
372         len = strlen(tmp);
373         if (tmp[len - 1] == '/') tmp[len - 1] = 0;
374         for (p = tmp + 1; *p; p++)
375                 if (*p == '/') {
376                         *p = 0;
377                         mkdir(tmp, mode);
378                         *p = '/';
379                 }
380         mkdir(tmp, S_IRWXU);
381 }
382
383 void
384 cooperation_setup(void)
385 {
386         GIOChannel *towatch;
387         gchar *fifofilename;
388
389         gchar *priv = (cfg.private) ? "-private" : "";
390         const gchar *fifo_suffix_env = g_getenv("CHORIZO_FIFO_SUFFIX");
391         const gchar *fifo_suffix = (fifo_suffix_env) ? fifo_suffix_env : "";
392         fifofilename = g_strdup_printf("%s%s%s%s", "chorizo", priv, ".fifo",
393                                        fifo_suffix);
394         fifopath = g_build_filename(g_get_user_runtime_dir(), "chorizo",
395                                     fifofilename, NULL);
396         mkdirp(dirname(fifopath), 0600);
397         g_free(fifofilename);
398
399         if (!g_file_test(fifopath, G_FILE_TEST_EXISTS)) mkfifo(fifopath, 0600);
400
401         cooperative_pipe_fp = open(fifopath, O_WRONLY | O_NONBLOCK);
402         if (!cooperative_pipe_fp) {
403                 fprintf(stderr, "chorizo: error: can't open FIFO\n");
404         } else {
405                 if (write(cooperative_pipe_fp, "", 0) == -1) {
406                         /*
407                          * Could not do an empty write to the FIFO which
408                          * means there's no one listening.
409                          */
410                         close(cooperative_pipe_fp);
411                         towatch = g_io_channel_new_file(fifopath, "r+", NULL);
412                         g_io_add_watch(towatch, G_IO_IN, (GIOFunc)remote_msg,
413                                        NULL);
414                 } else {
415                         cfg.cooperative_alone = FALSE;
416                 }
417         }
418 }
419
420 void
421 changed_load_progress(GObject *obj, GParamSpec *pspec, gpointer data)
422 {
423         struct Client *c = (struct Client *)data;
424         gdouble p;
425         gchar *grab_feeds =
426                 "a = document.querySelectorAll('"
427                 "    html > head > "
428                 "link[rel=\"alternate\"][href][type=\"application/atom+xml\"],"
429                 "    html > head > "
430                 "link[rel=\"alternate\"][href][type=\"application/rss+xml\"]"
431                 "');"
432                 "if (a.length == 0)"
433                 "    null;"
434                 "else {"
435                 "    out = '';"
436                 "    for (i = 0; i < a.length; i++) {"
437                 "        url = encodeURIComponent(a[i].href);"
438                 "        if ('title' in a[i] && a[i].title != '')"
439                 "            title = encodeURIComponent(a[i].title);"
440                 "        else"
441                 "            title = url;"
442                 "        out += '<li><a href=\"' + url + '\">' + title + "
443                 "'</a></li>';"
444                 "    }"
445                 "    out;"
446                 "}";
447         p = webkit_web_view_get_estimated_load_progress(
448                 WEBKIT_WEB_VIEW(c->web_view));
449         if (p == 1) {
450                 p = 0;
451
452                 /*
453                  * The page has loaded fully. We now run the short JavaScript
454                  * snippet above that operates on the DOM. It tries to grab
455                  * all occurences of <link rel="alternate" ...>, i.e.
456                  * RSS/Atom feed references.
457                  */
458                 webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(c->web_view),
459                                                grab_feeds, NULL,
460                                                grab_feeds_finished, c);
461         }
462         gtk_entry_set_progress_fraction(GTK_ENTRY(c->location), p);
463 }
464
465 void
466 changed_favicon(GObject *obj, GParamSpec *pspec, gpointer data)
467 {
468         struct Client *c = (struct Client *)data;
469         cairo_surface_t *f;
470         int w, h, w_should, h_should;
471         GdkPixbuf *pb, *pb_scaled;
472         f = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(c->web_view));
473         if (f == NULL) {
474                 gtk_image_set_from_icon_name(GTK_IMAGE(c->tabicon), "text-html",
475                                              GTK_ICON_SIZE_SMALL_TOOLBAR);
476         } else {
477                 w = cairo_image_surface_get_width(f);
478                 h = cairo_image_surface_get_height(f);
479                 pb = gdk_pixbuf_get_from_surface(f, 0, 0, w, h);
480                 if (pb != NULL) {
481                         w_should = 16 * gtk_widget_get_scale_factor(c->tabicon);
482                         h_should = 16 * gtk_widget_get_scale_factor(c->tabicon);
483                         pb_scaled = gdk_pixbuf_scale_simple(
484                                 pb, w_should, h_should, GDK_INTERP_BILINEAR);
485                         gtk_image_set_from_pixbuf(GTK_IMAGE(c->tabicon),
486                                                   pb_scaled);
487
488                         g_object_unref(pb_scaled);
489                         g_object_unref(pb);
490                 }
491         }
492 }
493
494 void
495 changed_title(GObject *obj, GParamSpec *pspec, gpointer data)
496 {
497         const gchar *t, *u;
498         struct Client *c = (struct Client *)data;
499         u = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
500         t = webkit_web_view_get_title(WEBKIT_WEB_VIEW(c->web_view));
501
502         if (t == NULL) t = (u == NULL) ? "chorizo" : u;
503
504         gchar *name = malloc(strlen(t) + 4);
505         if (!name) allocfail();
506         gboolean mute =
507                 webkit_web_view_get_is_muted(WEBKIT_WEB_VIEW(c->web_view));
508         gchar *muted = (mute) ? "[M] " : "";
509         sprintf(name, "%s%s", muted, t);
510         gtk_label_set_text(GTK_LABEL(c->tablabel), name);
511         g_free(name);
512
513         gtk_widget_set_tooltip_text(c->tablabel, t);
514         mainwindow_title(
515                 gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.notebook)));
516 }
517
518 void
519 changed_uri(GObject *obj, GParamSpec *pspec, gpointer data)
520 {
521         const gchar *t;
522         struct Client *c = (struct Client *)data;
523         FILE *fp;
524         t = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
525
526         if (t != NULL && strlen(t) > 0) {
527                 set_uri(t, c);
528
529                 // No g_get_user_state_dir unfortunately
530                 gchar *state_env = getenv("XDG_STATE_DIR");
531                 // clang-format off
532                 gchar *state_dir = (state_env)
533                                         ? state_env
534                                         : g_build_filename(g_get_home_dir(),
535                                                             ".local", "state",
536                                                             "chorizo", NULL);
537                 // clang-format on
538
539                 gchar *history_file =
540                         g_build_filename(state_dir, "history", NULL);
541                 if (!cfg.private) {
542                         mkdirp(state_dir, 0700);
543                         fp = fopen(history_file, "a");
544                         if (fp != NULL) {
545                                 fprintf(fp, "%s\n", t);
546                                 fclose(fp);
547                         } else {
548                                 perror("chorizo: error: could not open history"
549                                        "file");
550                         }
551                 }
552                 g_free(history_file);
553                 g_free(state_dir);
554         }
555 }
556
557 gboolean
558 crashed_web_view(WebKitWebView *web_view, gpointer data)
559 {
560         GtkDialogFlags flags = GTK_DIALOG_DESTROY_WITH_PARENT;
561         GtkWidget *dialog = gtk_message_dialog_new(
562                 GTK_WINDOW(mw.win), flags, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
563                 "ERROR: Web process %s crashed.\n%s",
564                 webkit_web_view_get_uri(WEBKIT_WEB_VIEW(web_view)),
565                 g_strerror(errno));
566         gtk_dialog_run(GTK_DIALOG(dialog));
567         gtk_widget_destroy(dialog);
568
569         return TRUE;
570 }
571
572 gboolean
573 decide_policy(WebKitWebView *web_view, WebKitPolicyDecision *decision,
574               WebKitPolicyDecisionType type, gpointer data)
575 {
576         WebKitResponsePolicyDecision *r;
577         switch (type) {
578         case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
579                 r = WEBKIT_RESPONSE_POLICY_DECISION(decision);
580                 if (!webkit_response_policy_decision_is_mime_type_supported(r))
581                         webkit_policy_decision_download(decision);
582                 else
583                         webkit_policy_decision_use(decision);
584                 break;
585         default:
586                 // Use whatever default there is.
587                 return FALSE;
588         }
589         return TRUE;
590 }
591
592 gchar *
593 ensure_uri_scheme(const gchar *t)
594 {
595         gchar *f, *fabs;
596         f = g_ascii_strdown(t, -1);
597         if (!g_str_has_prefix(f, "http:") && !g_str_has_prefix(f, "https:") &&
598             !g_str_has_prefix(f, "file:") && !g_str_has_prefix(f, "about:") &&
599             !g_str_has_prefix(f, "data:") && !g_str_has_prefix(f, "webkit:")) {
600                 g_free(f);
601                 fabs = realpath(t, NULL);
602                 if (fabs != NULL) {
603                         f = g_strdup_printf("file://%s", fabs);
604                         free(fabs);
605                 } else {
606                         f = g_strdup_printf("http://%s", t);
607                 }
608                 return f;
609         } else
610                 return g_strdup(t);
611 }
612
613 void
614 grab_feeds_finished(GObject *object, GAsyncResult *result, gpointer data)
615 {
616         struct Client *c = (struct Client *)data;
617         WebKitJavascriptResult *js_result;
618         JSCValue *value;
619         JSCException *exception;
620         GError *err = NULL;
621         gchar *str_value;
622         g_free(c->feed_html);
623         c->feed_html = NULL;
624
625         /*
626          * This was taken almost verbatim from the example in WebKit's
627          * documentation:
628          *
629          * https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html
630          */
631
632         js_result = webkit_web_view_run_javascript_finish(
633                 WEBKIT_WEB_VIEW(object), result, &err);
634         if (!js_result) {
635                 fprintf(stderr,
636                         "chorizo: error: error running javascript: %s\n",
637                         err->message);
638                 g_error_free(err);
639                 return;
640         }
641         value = webkit_javascript_result_get_js_value(js_result);
642         if (jsc_value_is_string(value)) {
643                 str_value = jsc_value_to_string(value);
644                 exception =
645                         jsc_context_get_exception(jsc_value_get_context(value));
646                 if (exception != NULL) {
647                         fprintf(stderr,
648                                 "chorizo: warning: error running javascript:"
649                                 "%s\n",
650                                 jsc_exception_get_message(exception));
651                 } else {
652                         c->feed_html = str_value;
653                 }
654
655                 gtk_entry_set_icon_from_icon_name(
656                         GTK_ENTRY(c->location), GTK_ENTRY_ICON_SECONDARY,
657                         "application-rss+xml-symbolic");
658                 gtk_entry_set_icon_activatable(GTK_ENTRY(c->location),
659                                                GTK_ENTRY_ICON_SECONDARY, TRUE);
660         } else {
661                 gtk_entry_set_icon_from_icon_name(
662                         GTK_ENTRY(c->location), GTK_ENTRY_ICON_SECONDARY, NULL);
663         }
664
665         webkit_javascript_result_unref(js_result);
666 }
667
668 void
669 hover_web_view(WebKitWebView *web_view, WebKitHitTestResult *ht,
670                guint modifiers, gpointer data)
671 {
672         struct Client *c = (struct Client *)data;
673         const char *to_show;
674         g_free(c->hover_uri);
675
676         if (webkit_hit_test_result_context_is_link(ht)) {
677                 to_show = webkit_hit_test_result_get_link_uri(ht);
678                 c->hover_uri = g_strdup(to_show);
679         } else {
680                 to_show = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
681                 c->hover_uri = NULL;
682         }
683
684         if (!gtk_widget_is_focus(c->location)) set_uri(to_show, c);
685 }
686
687 void
688 icon_location(GtkEntry *entry, GtkEntryIconPosition icon_pos, GdkEvent *event,
689               gpointer data)
690 {
691         struct Client *c = (struct Client *)data;
692         gchar *d;
693         gchar *data_template = "data:text/html,"
694                                "<!DOCTYPE html>"
695                                "<html>"
696                                "    <head>"
697                                "        <meta charset=\"UTF-8\">"
698                                "        <title>Feeds</title>"
699                                "    </head>"
700                                "    <body>"
701                                "        <p>Feeds found on this page:</p>"
702                                "        <ul>"
703                                "        %s"
704                                "        </ul>"
705                                "    </body>"
706                                "</html>";
707         if (c->feed_html != NULL) {
708                 /*
709                  * What we're actually trying to do is show a simple HTML
710                  * page that lists all the feeds on the current page. The
711                  * function webkit_web_view_load_html() looks like the proper
712                  * way to do that. Sad thing is, it doesn't create a history
713                  * entry, but instead simply replaces the content of the
714                  * current page. This is not what we want.
715                  *
716                  * RFC 2397 [0] defines the data URI scheme [1]. We abuse this
717                  * mechanism to show my custom HTML snippet* and*create a
718                  * history entry.
719                  *
720                  * [0]: https://tools.ietf.org/html/rfc2397 [1]:
721                  * https://en.wikipedia.org/wiki/Data_URI_scheme
722                  */
723
724                 d = g_strdup_printf(data_template, c->feed_html);
725                 webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), d);
726                 g_free(d);
727         }
728 }
729
730 void
731 init_default_web_context(void)
732 {
733         gchar *p;
734         WebKitWebContext *wc;
735         WebKitCookieManager *cm;
736         wc = (cfg.private) ? webkit_web_context_new_ephemeral() :
737                                    webkit_web_context_get_default();
738
739         p = g_build_filename(g_get_user_config_dir(), "chorizo", "adblock",
740                              NULL);
741         webkit_web_context_set_sandbox_enabled(wc, TRUE);
742         webkit_web_context_add_path_to_sandbox(wc, p, TRUE);
743         g_free(p);
744
745         webkit_web_context_set_process_model(
746                 wc, WEBKIT_PROCESS_MODEL_MULTIPLE_SECONDARY_PROCESSES);
747
748         p = g_build_filename(g_get_user_data_dir(), "chorizo", "web-extensions",
749                              NULL);
750         webkit_web_context_set_web_extensions_directory(wc, p);
751         g_free(p);
752
753         char *xdg_down = getenv("XDG_DOWNLOAD_DIR");
754         g_signal_connect(G_OBJECT(wc), "download-started",
755                          G_CALLBACK(download_start),
756                          (xdg_down) ? xdg_down : "/var/tmp");
757
758         trust_user_certs(wc);
759
760         cm = webkit_web_context_get_cookie_manager(wc);
761         webkit_cookie_manager_set_accept_policy(cm, cfg_cookie_policy);
762
763         if (!cfg.private) {
764                 webkit_web_context_set_favicon_database_directory(wc, NULL);
765
766                 gchar *fname = g_build_filename("/", g_get_user_data_dir(),
767                                                 "chorizo", "cookies.db", NULL);
768                 mkdirp(dirname(fname), 0700);
769                 WebKitCookiePersistentStorage type =
770                         WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE;
771                 webkit_cookie_manager_set_persistent_storage(cm, fname, type);
772                 g_free(fname);
773         }
774         webkit_web_context_set_spell_checking_enabled(wc, TRUE);
775 }
776
777 void
778 isearch(gpointer data, gint direction)
779 {
780         struct Client *c = (struct Client *)data;
781         WebKitWebView *web_view = WEBKIT_WEB_VIEW(c->web_view);
782         WebKitFindController *fc =
783                 webkit_web_view_get_find_controller(web_view);
784         const gchar *isearch_text = gtk_entry_get_text(GTK_ENTRY(c->isearch));
785         if (isearch_text == NULL) return;
786
787         switch (direction) {
788         case 0:
789                 g_signal_connect(G_OBJECT(fc), "counted-matches",
790                                  G_CALLBACK(isearch_counted_matches), c);
791                 webkit_find_controller_count_matches(
792                         fc, isearch_text, cfg_isearch_options, G_MAXUINT);
793                 webkit_find_controller_search(fc, isearch_text,
794                                               cfg_isearch_options, G_MAXUINT);
795                 break;
796         case 1:
797                 webkit_find_controller_search_next(fc);
798                 break;
799         case -1:
800                 webkit_find_controller_search_previous(fc);
801                 break;
802         case 2:
803                 webkit_find_controller_search_finish(fc);
804                 break;
805         }
806 }
807
808 void
809 isearch_init(struct Client *c, int direction)
810 {
811         if (webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view))) {
812                 gtk_widget_show(c->isearch_box);
813                 gtk_widget_grab_focus(c->isearch);
814         }
815 }
816
817 void
818 reopen_tab(void)
819 {
820         if (num_closed == 0) return;
821         client_new(closed_tabs[num_closed - 1], NULL);
822         num_closed--;
823         closed_tabs = realloc(closed_tabs, num_closed * sizeof(closed_tabs[0]));
824         if (!closed_tabs) allocfail();
825 }
826
827 void
828 mainwindow_setup(void)
829 {
830         mw.win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
831         gtk_window_set_default_size(GTK_WINDOW(mw.win), 1200, 900);
832         g_signal_connect(G_OBJECT(mw.win), "destroy", gtk_main_quit, NULL);
833
834         gchar *priv = (cfg.private) ? "-private" : "";
835         gchar *title = malloc(strlen(priv) + 7);
836         if (!title) allocfail();
837         sprintf(title, "%s%s", "chorizo", priv);
838         gtk_window_set_title(GTK_WINDOW(mw.win), title);
839         g_free(title);
840
841         mw.notebook = gtk_notebook_new();
842         gtk_notebook_set_scrollable(GTK_NOTEBOOK(mw.notebook), TRUE);
843         gtk_notebook_set_tab_pos(GTK_NOTEBOOK(mw.notebook), GTK_POS_LEFT);
844         gtk_container_add(GTK_CONTAINER(mw.win), mw.notebook);
845         g_signal_connect(G_OBJECT(mw.notebook), "switch-page",
846                          G_CALLBACK(notebook_switch_page), NULL);
847
848         GtkCssProvider *css = gtk_css_provider_new();
849         const char *css_data = "notebook header.left * { \
850                                         margin: 0; \
851                                         padding-top: 0; \
852                                         padding-bottom: 0; \
853                                 }";
854         gtk_css_provider_load_from_data(css, css_data, strlen(css_data), NULL);
855         gtk_style_context_add_provider_for_screen(
856                 gdk_screen_get_default(), GTK_STYLE_PROVIDER(css),
857                 GTK_STYLE_PROVIDER_PRIORITY_USER);
858
859         gtk_widget_show_all(mw.win);
860 }
861
862 void
863 mainwindow_title(gint idx)
864 {
865         GtkWidget *child, *widg, *tablabel;
866         const gchar *text;
867         child = gtk_notebook_get_nth_page(GTK_NOTEBOOK(mw.notebook), idx);
868         if (child == NULL) return;
869
870         widg = gtk_notebook_get_tab_label(GTK_NOTEBOOK(mw.notebook), child);
871         tablabel = (GtkWidget *)g_object_get_data(G_OBJECT(widg),
872                                                   "chorizo-tab-label");
873         text = gtk_label_get_text(GTK_LABEL(tablabel));
874         gtk_window_set_title(GTK_WINDOW(mw.win), text);
875 }
876
877 void
878 notebook_switch_page(GtkNotebook *nb, GtkWidget *p, guint idx, gpointer data)
879 {
880         mainwindow_title(idx);
881 }
882
883 void
884 show_web_view(WebKitWebView *web_view, gpointer data)
885 {
886         struct Client *c = (struct Client *)data;
887         (void)web_view;
888         gint idx = gtk_notebook_page_num(GTK_NOTEBOOK(mw.notebook), c->vbox);
889
890         gtk_widget_show_all(GTK_WIDGET(
891                 gtk_notebook_get_nth_page(GTK_NOTEBOOK(mw.notebook), idx)));
892         gtk_widget_hide(c->isearch_box);
893
894         if (idx != -1 && switch_tab)
895                 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), idx);
896         gtk_widget_grab_focus(c->web_view);
897 }
898
899 void
900 trust_user_certs(WebKitWebContext *wc)
901 {
902         GTlsCertificate *cert;
903         gchar *basedir, *absfile;
904         const gchar *file;
905         GDir *dir = NULL;
906         basedir = g_build_filename(g_get_user_data_dir(), "chorizo", "certs",
907                                    NULL);
908         dir = g_dir_open(basedir, 0, NULL);
909         g_free(basedir);
910         if (dir != NULL) {
911                 file = g_dir_read_name(dir);
912                 while (file != NULL) {
913                         absfile = g_build_filename(g_get_user_data_dir(),
914                                                    "chorizo", "certs", file,
915                                                    NULL);
916                         cert = g_tls_certificate_new_from_file(absfile, NULL);
917                         g_free(absfile);
918                         if (cert == NULL)
919                                 fprintf(stderr,
920                                         "chorizo: warning: could not load"
921                                         "trusted cert: %s\n",
922                                         file);
923                         else
924                                 // clang-format off
925                               webkit_web_context_allow_tls_certificate_for_host(
926                                         wc, cert, file);
927                         // clang-format on
928                         file = g_dir_read_name(dir);
929                 }
930                 g_dir_close(dir);
931         }
932 }
933
934 void
935 version(void)
936 {
937         printf("%s %s\n", "chorizo", VERSION);
938 }
939
940 int
941 main(int argc, char **argv)
942 {
943         int opt, i;
944
945         // TODO:pretty this
946         cfg.noncooperative_instances = FALSE;
947         cfg.cooperative_alone = TRUE;
948         closed_tabs = malloc(0);
949         if (!closed_tabs) allocfail();
950
951         while ((opt = getopt(argc, argv, "cpvV")) != -1) {
952                 switch (opt) {
953                 case 'c':
954                         cfg.noncooperative_instances = TRUE;
955                         break;
956                 case 'p':
957                         cfg.private = TRUE;
958                         break;
959                 case 'v':
960                         cfg.verbose = TRUE;
961                         break;
962                 case 'V':
963                         version();
964                         exit(0);
965                 default:
966                         fprintf(stderr,
967                                 "usage: chorizo [OPTION]... [URI]...\n");
968                         exit(EXIT_FAILURE);
969                 }
970         }
971
972         if (cfg.verbose) version();
973
974         gtk_init(&argc, &argv);
975
976         // Keep clipboard contents after program closes
977         gtk_clipboard_store(gtk_clipboard_get_for_display(
978                 gdk_display_get_default(), GDK_SELECTION_CLIPBOARD));
979
980         if (!cfg.noncooperative_instances) cooperation_setup();
981
982         if (cfg.noncooperative_instances || cfg.cooperative_alone)
983                 init_default_web_context();
984
985         downloadmanager_setup();
986         mainwindow_setup();
987
988         if (optind >= argc) {
989                 client_new(cfg_home_uri, NULL);
990         } else {
991                 for (i = optind; i < argc; i++) client_new(argv[i], NULL);
992         }
993
994         if (cfg.noncooperative_instances || cfg.cooperative_alone) {
995                 gtk_main();
996                 remove(fifopath);
997         }
998
999         exit(EXIT_SUCCESS);
1000 }