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