]> git.armaanb.net Git - chorizo.git/blob - browser.c
5828b40b74e456c9cb02de23a3a0568b3dc61256
[chorizo.git] / browser.c
1 #include <limits.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7 #include <string.h>
8
9 #include <gtk/gtk.h>
10 #include <gtk/gtkx.h>
11 #include <gdk/gdkkeysyms.h>
12 #include <gio/gio.h>
13 #include <webkit2/webkit2.h>
14 #include <JavaScriptCore/JavaScript.h>
15
16
17 static gboolean button_tablabel(GtkWidget *, GdkEvent *, gpointer);
18 static void client_destroy(GtkWidget *, gpointer);
19 static WebKitWebView *client_new(const gchar *, WebKitWebView *, gboolean);
20 static WebKitWebView *client_new_request(WebKitWebView *, WebKitNavigationAction *,
21                                          gpointer);
22 static void cooperation_setup(void);
23 static void changed_download_progress(GObject *, GParamSpec *, gpointer);
24 static void changed_load_progress(GObject *, GParamSpec *, gpointer);
25 static void changed_favicon(GObject *, GParamSpec *, gpointer);
26 static void changed_title(GObject *, GParamSpec *, gpointer);
27 static void changed_uri(GObject *, GParamSpec *, gpointer);
28 static gboolean crashed_web_view(WebKitWebView *, gpointer);
29 static gboolean decide_policy(WebKitWebView *, WebKitPolicyDecision *,
30                               WebKitPolicyDecisionType, gpointer);
31 static gboolean download_handle(WebKitDownload *, gchar *, gpointer);
32 static void download_handle_start(WebKitWebView *, WebKitDownload *, gpointer);
33 static void downloadmanager_cancel(GtkToolButton *, gpointer);
34 static gboolean downloadmanager_delete(GtkWidget *, gpointer);
35 static void downloadmanager_setup(void);
36 static gchar *ensure_uri_scheme(const gchar *);
37 static void external_handler_run(GSimpleAction *, GVariant *, gpointer);
38 static void grab_environment_configuration(void);
39 static void grab_feeds_finished(GObject *, GAsyncResult *, gpointer);
40 static void hover_web_view(WebKitWebView *, WebKitHitTestResult *, guint, gpointer);
41 static void icon_location(GtkEntry *, GtkEntryIconPosition, GdkEvent *, gpointer);
42 static gboolean key_common(GtkWidget *, GdkEvent *, gpointer);
43 static gboolean key_downloadmanager(GtkWidget *, GdkEvent *, gpointer);
44 static gboolean key_location(GtkWidget *, GdkEvent *, gpointer);
45 static gboolean key_web_view(GtkWidget *, GdkEvent *, gpointer);
46 static void keywords_load(void);
47 static gboolean keywords_try_search(WebKitWebView *, const gchar *);
48 static void mainwindow_setup(void);
49 static void mainwindow_title(gint);
50 static void mainwindow_title_before(GtkNotebook *, GtkWidget *, guint, gpointer);
51 static gboolean menu_web_view(WebKitWebView *, WebKitContextMenu *, GdkEvent *,
52                               WebKitHitTestResult *, gpointer);
53 static gboolean quit_if_nothing_active(void);
54 static gboolean remote_msg(GIOChannel *, GIOCondition, gpointer);
55 static void run_user_scripts(WebKitWebView *);
56 static void search(gpointer, gint);
57 static void show_web_view(WebKitWebView *, gpointer);
58 static void trust_user_certs(WebKitWebContext *);
59
60
61 struct Client
62 {
63     gchar *external_handler_uri;
64     gchar *hover_uri;
65     gchar *feed_html;
66     GtkWidget *location;
67     GtkWidget *tabicon;
68     GtkWidget *tablabel;
69     GtkWidget *vbox;
70     GtkWidget *web_view;
71 };
72
73 struct MainWindow
74 {
75     GtkWidget *win;
76     GtkWidget *notebook;
77 } mw;
78
79 struct DownloadManager
80 {
81     GtkWidget *scroll;
82     GtkWidget *toolbar;
83     GtkWidget *win;
84 } dm;
85
86
87 static const gchar *accepted_language[2] = { NULL, NULL };
88 static gint clients = 0, downloads = 0;
89 static gboolean cooperative_alone = TRUE;
90 static gboolean cooperative_instances = TRUE;
91 static int cooperative_pipe_fp = 0;
92 static gchar *download_dir = "/var/tmp";
93 static gboolean enable_console_to_stdout = FALSE;
94 static gchar *fifo_suffix = "main";
95 static gdouble global_zoom = 1.0;
96 static gchar *history_file = NULL;
97 static gchar *home_uri = "about:blank";
98 static gboolean initial_wc_setup_done = FALSE;
99 static GHashTable *keywords = NULL;
100 static gchar *search_text = NULL;
101 static GtkPositionType tab_pos = GTK_POS_TOP;
102 static gint tab_width_chars = 20;
103 static gchar *user_agent = NULL;
104
105
106 gboolean
107 button_tablabel(GtkWidget *widget, GdkEvent *event, gpointer data)
108 {
109     if (event->type == GDK_BUTTON_RELEASE)
110     {
111         switch (((GdkEventButton *)event)->button)
112         {
113             case 2:
114                 client_destroy(NULL, data);
115                 return TRUE;
116         }
117     }
118     return FALSE;
119 }
120
121 void
122 client_destroy(GtkWidget *widget, gpointer data)
123 {
124     struct Client *c = (struct Client *)data;
125     gint idx;
126
127     g_signal_handlers_disconnect_by_func(G_OBJECT(c->web_view),
128                                          changed_load_progress, c);
129
130     idx = gtk_notebook_page_num(GTK_NOTEBOOK(mw.notebook), c->vbox);
131     if (idx == -1)
132         fprintf(stderr, __NAME__": Tab index was -1, bamboozled\n");
133     else
134         gtk_notebook_remove_page(GTK_NOTEBOOK(mw.notebook), idx);
135
136     free(c);
137     clients--;
138
139     quit_if_nothing_active();
140 }
141
142 WebKitWebView *
143 client_new(const gchar *uri, WebKitWebView *related_wv, gboolean show)
144 {
145     struct Client *c;
146     WebKitWebContext *wc;
147     gchar *f;
148     GtkWidget *evbox, *tabbox;
149
150     if (uri != NULL && cooperative_instances && !cooperative_alone)
151     {
152         f = ensure_uri_scheme(uri);
153         write(cooperative_pipe_fp, f, strlen(f));
154         write(cooperative_pipe_fp, "\n", 1);
155         g_free(f);
156         return NULL;
157     }
158
159     c = calloc(1, sizeof(struct Client));
160     if (!c)
161     {
162         fprintf(stderr, __NAME__": fatal: calloc failed\n");
163         exit(EXIT_FAILURE);
164     }
165
166     if (related_wv == NULL)
167         c->web_view = webkit_web_view_new();
168     else
169         c->web_view = webkit_web_view_new_with_related_view(related_wv);
170     wc = webkit_web_view_get_context(WEBKIT_WEB_VIEW(c->web_view));
171
172     webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(c->web_view), global_zoom);
173     g_signal_connect(G_OBJECT(c->web_view), "notify::favicon",
174                      G_CALLBACK(changed_favicon), c);
175     g_signal_connect(G_OBJECT(c->web_view), "notify::title",
176                      G_CALLBACK(changed_title), c);
177     g_signal_connect(G_OBJECT(c->web_view), "notify::uri",
178                      G_CALLBACK(changed_uri), c);
179     g_signal_connect(G_OBJECT(c->web_view), "notify::estimated-load-progress",
180                      G_CALLBACK(changed_load_progress), c);
181     g_signal_connect(G_OBJECT(c->web_view), "create",
182                      G_CALLBACK(client_new_request), NULL);
183     g_signal_connect(G_OBJECT(c->web_view), "context-menu",
184                      G_CALLBACK(menu_web_view), c);
185     g_signal_connect(G_OBJECT(c->web_view), "close",
186                      G_CALLBACK(client_destroy), c);
187     g_signal_connect(G_OBJECT(c->web_view), "decide-policy",
188                      G_CALLBACK(decide_policy), NULL);
189     g_signal_connect(G_OBJECT(c->web_view), "key-press-event",
190                      G_CALLBACK(key_web_view), c);
191     g_signal_connect(G_OBJECT(c->web_view), "button-release-event",
192                      G_CALLBACK(key_web_view), c);
193     g_signal_connect(G_OBJECT(c->web_view), "scroll-event",
194                      G_CALLBACK(key_web_view), c);
195     g_signal_connect(G_OBJECT(c->web_view), "mouse-target-changed",
196                      G_CALLBACK(hover_web_view), c);
197     g_signal_connect(G_OBJECT(c->web_view), "web-process-crashed",
198                      G_CALLBACK(crashed_web_view), c);
199
200     if (!initial_wc_setup_done)
201     {
202         if (accepted_language[0] != NULL)
203             webkit_web_context_set_preferred_languages(wc, accepted_language);
204
205         g_signal_connect(G_OBJECT(wc), "download-started",
206                          G_CALLBACK(download_handle_start), NULL);
207
208         trust_user_certs(wc);
209
210         webkit_web_context_set_favicon_database_directory(wc, NULL);
211
212         initial_wc_setup_done = TRUE;
213     }
214
215     if (user_agent != NULL)
216         g_object_set(G_OBJECT(webkit_web_view_get_settings(WEBKIT_WEB_VIEW(c->web_view))),
217                      "user-agent", user_agent, NULL);
218
219     if (enable_console_to_stdout)
220         webkit_settings_set_enable_write_console_messages_to_stdout(webkit_web_view_get_settings(WEBKIT_WEB_VIEW(c->web_view)), TRUE);
221
222     webkit_settings_set_enable_developer_extras(webkit_web_view_get_settings(WEBKIT_WEB_VIEW(c->web_view)), TRUE);
223
224     c->location = gtk_entry_new();
225     g_signal_connect(G_OBJECT(c->location), "key-press-event",
226                      G_CALLBACK(key_location), c);
227     g_signal_connect(G_OBJECT(c->location), "icon-release",
228                      G_CALLBACK(icon_location), c);
229     /* XXX This is a workaround. Setting this to NULL (which is done in
230      * grab_feeds_finished() if no feed has been detected) adds a little
231      * padding left of the text. Not sure why. The point of this call
232      * right here is to have that padding right from the start. This
233      * avoids a graphical artifact. */
234     gtk_entry_set_icon_from_icon_name(GTK_ENTRY(c->location),
235                                       GTK_ENTRY_ICON_PRIMARY,
236                                       NULL);
237
238     c->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
239     gtk_box_pack_start(GTK_BOX(c->vbox), c->location, FALSE, FALSE, 0);
240     gtk_box_pack_start(GTK_BOX(c->vbox), c->web_view, TRUE, TRUE, 0);
241
242     c->tabicon = gtk_image_new_from_icon_name("text-html", GTK_ICON_SIZE_LARGE_TOOLBAR);
243
244     c->tablabel = gtk_label_new(__NAME__);
245     gtk_label_set_ellipsize(GTK_LABEL(c->tablabel), PANGO_ELLIPSIZE_END);
246     gtk_label_set_width_chars(GTK_LABEL(c->tablabel), tab_width_chars);
247
248     tabbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
249     gtk_box_pack_start(GTK_BOX(tabbox), c->tabicon, FALSE, FALSE, 0);
250     gtk_box_pack_start(GTK_BOX(tabbox), c->tablabel, TRUE, TRUE, 0);
251
252     evbox = gtk_event_box_new();
253     gtk_container_add(GTK_CONTAINER(evbox), tabbox);
254     g_signal_connect(G_OBJECT(evbox), "button-release-event",
255                      G_CALLBACK(button_tablabel), c);
256
257     /* This only shows the event box and the label inside, nothing else.
258      * Needed because the evbox/label is "internal" to the notebook and
259      * not part of the normal "widget tree" (IIUC). */
260     gtk_widget_show_all(evbox);
261
262     gtk_notebook_append_page(GTK_NOTEBOOK(mw.notebook), c->vbox, evbox);
263     gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(mw.notebook), c->vbox, TRUE);
264
265     if (show)
266         show_web_view(NULL, c);
267     else
268         g_signal_connect(G_OBJECT(c->web_view), "ready-to-show",
269                          G_CALLBACK(show_web_view), c);
270
271     if (uri != NULL)
272     {
273         f = ensure_uri_scheme(uri);
274         webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), f);
275         g_free(f);
276     }
277
278     clients++;
279
280     return WEBKIT_WEB_VIEW(c->web_view);
281 }
282
283 WebKitWebView *
284 client_new_request(WebKitWebView *web_view,
285                    WebKitNavigationAction *navigation_action, gpointer data)
286 {
287     return client_new(NULL, web_view, FALSE);
288 }
289
290 void
291 cooperation_setup(void)
292 {
293     GIOChannel *towatch;
294     gchar *fifofilename, *fifopath;
295
296     fifofilename = g_strdup_printf("%s-%s", __NAME__".fifo", fifo_suffix);
297     fifopath = g_build_filename(g_get_user_runtime_dir(), fifofilename, NULL);
298     g_free(fifofilename);
299
300     if (!g_file_test(fifopath, G_FILE_TEST_EXISTS))
301         mkfifo(fifopath, 0600);
302
303     cooperative_pipe_fp = open(fifopath, O_WRONLY | O_NONBLOCK);
304     if (!cooperative_pipe_fp)
305     {
306         fprintf(stderr, __NAME__": Can't open FIFO at all.\n");
307     }
308     else
309     {
310         if (write(cooperative_pipe_fp, "", 0) == -1)
311         {
312             /* Could not do an empty write to the FIFO which means there's
313              * no one listening. */
314             close(cooperative_pipe_fp);
315             towatch = g_io_channel_new_file(fifopath, "r+", NULL);
316             g_io_add_watch(towatch, G_IO_IN, (GIOFunc)remote_msg, NULL);
317         }
318         else
319             cooperative_alone = FALSE;
320     }
321
322     g_free(fifopath);
323 }
324
325 void
326 changed_download_progress(GObject *obj, GParamSpec *pspec, gpointer data)
327 {
328     WebKitDownload *download = WEBKIT_DOWNLOAD(obj);
329     WebKitURIResponse *resp;
330     GtkToolItem *tb = GTK_TOOL_ITEM(data);
331     gdouble p, size_mb;
332     const gchar *uri;
333     gchar *t, *filename, *base;
334
335     p = webkit_download_get_estimated_progress(download);
336     p = p > 1 ? 1 : p;
337     p = p < 0 ? 0 : p;
338     p *= 100;
339     resp = webkit_download_get_response(download);
340     size_mb = webkit_uri_response_get_content_length(resp) / 1e6;
341
342     uri = webkit_download_get_destination(download);
343     filename = g_filename_from_uri(uri, NULL, NULL);
344     if (filename == NULL)
345     {
346         /* This really should not happen because WebKit uses that URI to
347          * write to a file... */
348         fprintf(stderr, __NAME__": Could not construct file name from URI!\n");
349         t = g_strdup_printf("%s (%.0f%% of %.1f MB)",
350                             webkit_uri_response_get_uri(resp), p, size_mb);
351     }
352     else
353     {
354         base = g_path_get_basename(filename);
355         t = g_strdup_printf("%s (%.0f%% of %.1f MB)", base, p, size_mb);
356         g_free(filename);
357         g_free(base);
358     }
359     gtk_tool_button_set_label(GTK_TOOL_BUTTON(tb), t);
360     g_free(t);
361 }
362
363 void
364 changed_load_progress(GObject *obj, GParamSpec *pspec, gpointer data)
365 {
366     struct Client *c = (struct Client *)data;
367     gdouble p;
368     gchar *grab_feeds =
369         "a = document.querySelectorAll('"
370         "    html > head > link[rel=\"alternate\"][href][type=\"application/atom+xml\"],"
371         "    html > head > link[rel=\"alternate\"][href][type=\"application/rss+xml\"]"
372         "');"
373         "if (a.length == 0)"
374         "    null;"
375         "else"
376         "{"
377         "    out = '';"
378         "    for (i = 0; i < a.length; i++)"
379         "    {"
380         "        url = encodeURIComponent(a[i].href);"
381         "        if ('title' in a[i] && a[i].title != '')"
382         "            title = encodeURIComponent(a[i].title);"
383         "        else"
384         "            title = url;"
385         "        out += '<li><a href=\"' + url + '\">' + title + '</a></li>';"
386         "    }"
387         "    out;"
388         "}";
389
390     p = webkit_web_view_get_estimated_load_progress(WEBKIT_WEB_VIEW(c->web_view));
391     if (p == 1)
392     {
393         p = 0;
394
395         /* The page has loaded fully. We now run the short JavaScript
396          * snippet above that operates on the DOM. It tries to grab all
397          * occurences of <link rel="alternate" ...>, i.e. RSS/Atom feed
398          * references. */
399         webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(c->web_view),
400                                        grab_feeds, NULL,
401                                        grab_feeds_finished, c);
402
403         run_user_scripts(WEBKIT_WEB_VIEW(c->web_view));
404     }
405     gtk_entry_set_progress_fraction(GTK_ENTRY(c->location), p);
406 }
407
408 void
409 changed_favicon(GObject *obj, GParamSpec *pspec, gpointer data)
410 {
411     struct Client *c = (struct Client *)data;
412     cairo_surface_t *f;
413     int w, h;
414     GdkPixbuf *pb;
415
416     f = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(c->web_view));
417     if (f != NULL)
418     {
419         w = cairo_image_surface_get_width(f);
420         h = cairo_image_surface_get_height(f);
421         pb = gdk_pixbuf_get_from_surface(f, 0, 0, w, h);
422         if (pb != NULL)
423         {
424             /* TODO: Resize icon */
425             gtk_image_set_from_pixbuf(GTK_IMAGE(c->tabicon), pb);
426             g_object_unref(pb);
427         }
428     }
429 }
430
431 void
432 changed_title(GObject *obj, GParamSpec *pspec, gpointer data)
433 {
434     const gchar *t, *u;
435     struct Client *c = (struct Client *)data;
436
437     u = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
438     t = webkit_web_view_get_title(WEBKIT_WEB_VIEW(c->web_view));
439
440     u = u == NULL ? __NAME__ : u;
441     u = u[0] == 0 ? __NAME__ : u;
442
443     t = t == NULL ? u : t;
444     t = t[0] == 0 ? u : t;
445
446     gtk_label_set_text(GTK_LABEL(c->tablabel), t);
447     mainwindow_title(-1);
448 }
449
450 void
451 changed_uri(GObject *obj, GParamSpec *pspec, gpointer data)
452 {
453     const gchar *t;
454     struct Client *c = (struct Client *)data;
455     FILE *fp;
456
457     t = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
458
459     /* When a web process crashes, we get a "notify::uri" signal, but we
460      * can no longer read a meaningful URI. It's just an empty string
461      * now. Not updating the location bar in this scenario is important,
462      * because we would override the "WEB PROCESS CRASHED" message. */
463     if (t != NULL && strlen(t) > 0)
464     {
465         gtk_entry_set_text(GTK_ENTRY(c->location), t);
466
467         if (history_file != NULL)
468         {
469             fp = fopen(history_file, "a");
470             if (fp != NULL)
471             {
472                 fprintf(fp, "%s\n", t);
473                 fclose(fp);
474             }
475             else
476                 perror(__NAME__": Error opening history file");
477         }
478     }
479 }
480
481 gboolean
482 crashed_web_view(WebKitWebView *web_view, gpointer data)
483 {
484     gchar *t;
485     struct Client *c = (struct Client *)data;
486
487     t = g_strdup_printf("WEB PROCESS CRASHED: %s",
488                         webkit_web_view_get_uri(WEBKIT_WEB_VIEW(web_view)));
489     gtk_entry_set_text(GTK_ENTRY(c->location), t);
490     g_free(t);
491
492     return TRUE;
493 }
494
495 gboolean
496 decide_policy(WebKitWebView *web_view, WebKitPolicyDecision *decision,
497               WebKitPolicyDecisionType type, gpointer data)
498 {
499     WebKitResponsePolicyDecision *r;
500
501     switch (type)
502     {
503         case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
504             r = WEBKIT_RESPONSE_POLICY_DECISION(decision);
505             if (!webkit_response_policy_decision_is_mime_type_supported(r))
506                 webkit_policy_decision_download(decision);
507             else
508                 webkit_policy_decision_use(decision);
509             break;
510         default:
511             /* Use whatever default there is. */
512             return FALSE;
513     }
514     return TRUE;
515 }
516
517 void
518 download_handle_finished(WebKitDownload *download, gpointer data)
519 {
520     downloads--;
521 }
522
523 void
524 download_handle_start(WebKitWebView *web_view, WebKitDownload *download,
525                       gpointer data)
526 {
527     g_signal_connect(G_OBJECT(download), "decide-destination",
528                      G_CALLBACK(download_handle), data);
529 }
530
531 gboolean
532 download_handle(WebKitDownload *download, gchar *suggested_filename, gpointer data)
533 {
534     gchar *sug_clean, *path, *path2 = NULL, *uri;
535     GtkToolItem *tb;
536     int suffix = 1;
537     size_t i;
538
539     sug_clean = g_strdup(suggested_filename);
540     for (i = 0; i < strlen(sug_clean); i++)
541         if (sug_clean[i] == G_DIR_SEPARATOR)
542             sug_clean[i] = '_';
543
544     path = g_build_filename(download_dir, sug_clean, NULL);
545     path2 = g_strdup(path);
546     while (g_file_test(path2, G_FILE_TEST_EXISTS) && suffix < 1000)
547     {
548         g_free(path2);
549
550         path2 = g_strdup_printf("%s.%d", path, suffix);
551         suffix++;
552     }
553
554     if (suffix == 1000)
555     {
556         fprintf(stderr, __NAME__": Suffix reached limit for download.\n");
557         webkit_download_cancel(download);
558     }
559     else
560     {
561         uri = g_filename_to_uri(path2, NULL, NULL);
562         webkit_download_set_destination(download, uri);
563         g_free(uri);
564
565         tb = gtk_tool_button_new(NULL, NULL);
566         gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(tb), "gtk-delete");
567         gtk_tool_button_set_label(GTK_TOOL_BUTTON(tb), sug_clean);
568         gtk_toolbar_insert(GTK_TOOLBAR(dm.toolbar), tb, 0);
569         gtk_widget_show_all(dm.win);
570
571         g_signal_connect(G_OBJECT(download), "notify::estimated-progress",
572                          G_CALLBACK(changed_download_progress), tb);
573
574         downloads++;
575         g_signal_connect(G_OBJECT(download), "finished",
576                          G_CALLBACK(download_handle_finished), NULL);
577
578         g_object_ref(download);
579         g_signal_connect(G_OBJECT(tb), "clicked",
580                          G_CALLBACK(downloadmanager_cancel), download);
581     }
582
583     g_free(sug_clean);
584     g_free(path);
585     g_free(path2);
586
587     /* Propagate -- to whom it may concern. */
588     return FALSE;
589 }
590
591 void
592 downloadmanager_cancel(GtkToolButton *tb, gpointer data)
593 {
594     WebKitDownload *download = WEBKIT_DOWNLOAD(data);
595
596     webkit_download_cancel(download);
597     g_object_unref(download);
598
599     gtk_widget_destroy(GTK_WIDGET(tb));
600 }
601
602 gboolean
603 downloadmanager_delete(GtkWidget *obj, gpointer data)
604 {
605     if (!quit_if_nothing_active())
606         gtk_widget_hide(dm.win);
607
608     return TRUE;
609 }
610
611 void
612 downloadmanager_setup(void)
613 {
614     dm.win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
615     gtk_window_set_type_hint(GTK_WINDOW(dm.win), GDK_WINDOW_TYPE_HINT_DIALOG);
616     gtk_window_set_default_size(GTK_WINDOW(dm.win), 500, 250);
617     gtk_window_set_title(GTK_WINDOW(dm.win), __NAME__" - Download Manager");
618     g_signal_connect(G_OBJECT(dm.win), "delete-event",
619                      G_CALLBACK(downloadmanager_delete), NULL);
620     g_signal_connect(G_OBJECT(dm.win), "key-press-event",
621                      G_CALLBACK(key_downloadmanager), NULL);
622
623     dm.toolbar = gtk_toolbar_new();
624     gtk_orientable_set_orientation(GTK_ORIENTABLE(dm.toolbar),
625                                    GTK_ORIENTATION_VERTICAL);
626     gtk_toolbar_set_style(GTK_TOOLBAR(dm.toolbar), GTK_TOOLBAR_BOTH_HORIZ);
627     gtk_toolbar_set_show_arrow(GTK_TOOLBAR(dm.toolbar), FALSE);
628
629     dm.scroll = gtk_scrolled_window_new(NULL, NULL);
630     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(dm.scroll),
631                                    GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
632     gtk_container_add(GTK_CONTAINER(dm.scroll), dm.toolbar);
633
634     gtk_container_add(GTK_CONTAINER(dm.win), dm.scroll);
635 }
636
637 gchar *
638 ensure_uri_scheme(const gchar *t)
639 {
640     gchar *f, *fabs;
641
642     f = g_ascii_strdown(t, -1);
643     if (!g_str_has_prefix(f, "http:") &&
644         !g_str_has_prefix(f, "https:") &&
645         !g_str_has_prefix(f, "file:") &&
646         !g_str_has_prefix(f, "about:") &&
647         !g_str_has_prefix(f, "data:"))
648     {
649         g_free(f);
650         fabs = realpath(t, NULL);
651         if (fabs != NULL)
652         {
653             f = g_strdup_printf("file://%s", fabs);
654             free(fabs);
655         }
656         else
657             f = g_strdup_printf("http://%s", t);
658         return f;
659     }
660     else
661         return g_strdup(t);
662 }
663
664 void
665 external_handler_run(GSimpleAction *simple, GVariant *param, gpointer data)
666 {
667     struct Client *c = (struct Client *)data;
668     gchar *argv[] = { "lariza-external-handler", "-u", NULL, NULL };
669     GPid pid;
670     GError *err = NULL;
671
672     (void)simple;
673     (void)param;
674
675     argv[2] = c->external_handler_uri;
676     if (!g_spawn_async(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
677                        &pid, &err))
678     {
679         fprintf(stderr, __NAME__": Could not launch key handler: %s\n",
680                 err->message);
681         g_error_free(err);
682     }
683     else
684         g_spawn_close_pid(pid);
685 }
686
687 void
688 grab_environment_configuration(void)
689 {
690     const gchar *e;
691
692     e = g_getenv(__NAME_UPPERCASE__"_ACCEPTED_LANGUAGE");
693     if (e != NULL)
694         accepted_language[0] = g_strdup(e);
695
696     e = g_getenv(__NAME_UPPERCASE__"_DOWNLOAD_DIR");
697     if (e != NULL)
698         download_dir = g_strdup(e);
699
700     e = g_getenv(__NAME_UPPERCASE__"_ENABLE_CONSOLE_TO_STDOUT");
701     if (e != NULL)
702         enable_console_to_stdout = TRUE;
703
704     e = g_getenv(__NAME_UPPERCASE__"_FIFO_SUFFIX");
705     if (e != NULL)
706         fifo_suffix = g_strdup(e);
707
708     e = g_getenv(__NAME_UPPERCASE__"_HISTORY_FILE");
709     if (e != NULL)
710         history_file = g_strdup(e);
711
712     e = g_getenv(__NAME_UPPERCASE__"_HOME_URI");
713     if (e != NULL)
714         home_uri = g_strdup(e);
715
716     e = g_getenv(__NAME_UPPERCASE__"_TAB_POS");
717     if (e != NULL)
718     {
719         if (strcmp(e, "top") == 0)
720             tab_pos = GTK_POS_TOP;
721         if (strcmp(e, "right") == 0)
722             tab_pos = GTK_POS_RIGHT;
723         if (strcmp(e, "bottom") == 0)
724             tab_pos = GTK_POS_BOTTOM;
725         if (strcmp(e, "left") == 0)
726             tab_pos = GTK_POS_LEFT;
727     }
728
729     e = g_getenv(__NAME_UPPERCASE__"_TAB_WIDTH_CHARS");
730     if (e != NULL)
731         tab_width_chars = atoi(e);
732
733     e = g_getenv(__NAME_UPPERCASE__"_USER_AGENT");
734     if (e != NULL)
735         user_agent = g_strdup(e);
736
737     e = g_getenv(__NAME_UPPERCASE__"_ZOOM");
738     if (e != NULL)
739         global_zoom = atof(e);
740 }
741
742 void
743 grab_feeds_finished(GObject *object, GAsyncResult *result, gpointer data)
744 {
745     struct Client *c = (struct Client *)data;
746     WebKitJavascriptResult *js_result;
747     JSCValue *value;
748     JSCException *exception;
749     GError *err = NULL;
750     gchar *str_value;
751
752     g_free(c->feed_html);
753     c->feed_html = NULL;
754
755     /* This was taken almost verbatim from the example in WebKit's
756      * documentation:
757      *
758      * https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html#webkit-web-view-run-javascript-finish */
759
760     js_result = webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(object),
761                                                       result, &err);
762     if (!js_result)
763     {
764         fprintf(stderr, __NAME__": Error running javascript: %s\n", err->message);
765         g_error_free(err);
766         return;
767     }
768
769     value = webkit_javascript_result_get_js_value(js_result);
770     if (jsc_value_is_string(value))
771     {
772         str_value = jsc_value_to_string(value);
773         exception = jsc_context_get_exception(jsc_value_get_context(value));
774         if (exception != NULL)
775         {
776             fprintf(stderr, __NAME__": Error running javascript: %s\n",
777                     jsc_exception_get_message(exception));
778         }
779         else
780             c->feed_html = str_value;
781
782         gtk_entry_set_icon_from_icon_name(GTK_ENTRY(c->location),
783                                           GTK_ENTRY_ICON_PRIMARY,
784                                           "application-rss+xml-symbolic");
785         gtk_entry_set_icon_activatable(GTK_ENTRY(c->location),
786                                        GTK_ENTRY_ICON_PRIMARY,
787                                        TRUE);
788     }
789     else
790     {
791         gtk_entry_set_icon_from_icon_name(GTK_ENTRY(c->location),
792                                           GTK_ENTRY_ICON_PRIMARY,
793                                           NULL);
794     }
795
796     webkit_javascript_result_unref(js_result);
797 }
798
799 void
800 hover_web_view(WebKitWebView *web_view, WebKitHitTestResult *ht, guint modifiers,
801                gpointer data)
802 {
803     struct Client *c = (struct Client *)data;
804
805     if (!gtk_widget_is_focus(c->location))
806     {
807         if (webkit_hit_test_result_context_is_link(ht))
808         {
809             gtk_entry_set_text(GTK_ENTRY(c->location),
810                                webkit_hit_test_result_get_link_uri(ht));
811
812             if (c->hover_uri != NULL)
813                 g_free(c->hover_uri);
814             c->hover_uri = g_strdup(webkit_hit_test_result_get_link_uri(ht));
815         }
816         else
817         {
818             gtk_entry_set_text(GTK_ENTRY(c->location),
819                                webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view)));
820
821             if (c->hover_uri != NULL)
822                 g_free(c->hover_uri);
823             c->hover_uri = NULL;
824         }
825     }
826 }
827
828 void
829 icon_location(GtkEntry *entry, GtkEntryIconPosition icon_pos, GdkEvent *event,
830               gpointer data)
831 {
832     struct Client *c = (struct Client *)data;
833     gchar *d;
834     gchar *data_template =
835         "data:text/html,"
836         "<!DOCTYPE html>"
837         "<html>"
838         "    <head>"
839         "        <meta charset=\"UTF-8\">"
840         "        <title>Feeds</title>"
841         "    </head>"
842         "    <body>"
843         "        <p>Feeds found on this page:</p>"
844         "        <ul>"
845         "        %s"
846         "        </ul>"
847         "    </body>"
848         "</html>";
849
850     if (c->feed_html != NULL)
851     {
852         /* What we're actually trying to do is show a simple HTML page
853          * that lists all the feeds on the current page. The function
854          * webkit_web_view_load_html() looks like the proper way to do
855          * that. Sad thing is, it doesn't create a history entry, but
856          * instead simply replaces the content of the current page. This
857          * is not what we want.
858          *
859          * RFC 2397 [0] defines the data URI scheme [1]. We abuse this
860          * mechanism to show my custom HTML snippet *and* create a
861          * history entry.
862          *
863          * [0]: https://tools.ietf.org/html/rfc2397
864          * [1]: https://en.wikipedia.org/wiki/Data_URI_scheme */
865         d = g_strdup_printf(data_template, c->feed_html);
866         webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), d);
867         g_free(d);
868     }
869 }
870
871 gboolean
872 key_common(GtkWidget *widget, GdkEvent *event, gpointer data)
873 {
874     struct Client *c = (struct Client *)data;
875     WebKitWebContext *wc = webkit_web_view_get_context(WEBKIT_WEB_VIEW(c->web_view));
876     gchar *f;
877
878     if (event->type == GDK_KEY_PRESS)
879     {
880         if (((GdkEventKey *)event)->state & GDK_MOD1_MASK)
881         {
882             switch (((GdkEventKey *)event)->keyval)
883             {
884                 case GDK_KEY_q:  /* close window (left hand) */
885                     client_destroy(NULL, c);
886                     return TRUE;
887                 case GDK_KEY_w:  /* home (left hand) */
888                     f = ensure_uri_scheme(home_uri);
889                     webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), f);
890                     g_free(f);
891                     return TRUE;
892                 case GDK_KEY_e:  /* new tab (left hand) */
893                     f = ensure_uri_scheme(home_uri);
894                     client_new(f, NULL, TRUE);
895                     g_free(f);
896                     return TRUE;
897                 case GDK_KEY_r:  /* reload (left hand) */
898                     webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(
899                                                         c->web_view));
900                     return TRUE;
901                 case GDK_KEY_d:  /* download manager (left hand) */
902                     gtk_widget_show_all(dm.win);
903                     return TRUE;
904                 case GDK_KEY_2:  /* search forward (left hand) */
905                 case GDK_KEY_n:  /* search forward (maybe both hands) */
906                     search(c, 1);
907                     return TRUE;
908                 case GDK_KEY_3:  /* search backward (left hand) */
909                     search(c, -1);
910                     return TRUE;
911                 case GDK_KEY_l:  /* location (BOTH hands) */
912                     gtk_widget_grab_focus(c->location);
913                     return TRUE;
914                 case GDK_KEY_k:  /* initiate search (BOTH hands) */
915                     gtk_widget_grab_focus(c->location);
916                     gtk_entry_set_text(GTK_ENTRY(c->location), ":/");
917                     gtk_editable_set_position(GTK_EDITABLE(c->location), -1);
918                     return TRUE;
919                 case GDK_KEY_c:  /* reload trusted certs (left hand) */
920                     trust_user_certs(wc);
921                     return TRUE;
922                 case GDK_KEY_x:  /* launch external handler (left hand) */
923                     if (c->external_handler_uri != NULL)
924                         g_free(c->external_handler_uri);
925                     c->external_handler_uri = g_strdup(
926                         webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view)));
927                     external_handler_run(NULL, NULL, c);
928                     return TRUE;
929                 case GDK_KEY_a:  /* go one tab to the left (left hand) */
930                     gtk_notebook_prev_page(GTK_NOTEBOOK(mw.notebook));
931                     return TRUE;
932                 case GDK_KEY_s:  /* go one tab to the right (left hand) */
933                     gtk_notebook_next_page(GTK_NOTEBOOK(mw.notebook));
934                     return TRUE;
935             }
936         }
937         /* navigate backward (left hand) */
938         else if (((GdkEventKey *)event)->keyval == GDK_KEY_F2)
939         {
940             webkit_web_view_go_back(WEBKIT_WEB_VIEW(c->web_view));
941             return TRUE;
942         }
943         /* navigate forward (left hand) */
944         else if (((GdkEventKey *)event)->keyval == GDK_KEY_F3)
945         {
946             webkit_web_view_go_forward(WEBKIT_WEB_VIEW(c->web_view));
947             return TRUE;
948         }
949     }
950
951     return FALSE;
952 }
953
954 gboolean
955 key_downloadmanager(GtkWidget *widget, GdkEvent *event, gpointer data)
956 {
957     if (event->type == GDK_KEY_PRESS)
958     {
959         if (((GdkEventKey *)event)->state & GDK_MOD1_MASK)
960         {
961             switch (((GdkEventKey *)event)->keyval)
962             {
963                 case GDK_KEY_d:  /* close window (left hand) */
964                 case GDK_KEY_q:
965                     downloadmanager_delete(dm.win, NULL);
966                     return TRUE;
967             }
968         }
969     }
970
971     return FALSE;
972 }
973
974 gboolean
975 key_location(GtkWidget *widget, GdkEvent *event, gpointer data)
976 {
977     struct Client *c = (struct Client *)data;
978     const gchar *t;
979     gchar *f;
980
981     if (key_common(widget, event, data))
982         return TRUE;
983
984     if (event->type == GDK_KEY_PRESS)
985     {
986         switch (((GdkEventKey *)event)->keyval)
987         {
988             case GDK_KEY_KP_Enter:
989             case GDK_KEY_Return:
990                 gtk_widget_grab_focus(c->web_view);
991                 t = gtk_entry_get_text(GTK_ENTRY(c->location));
992                 if (t != NULL && t[0] == ':' && t[1] == '/')
993                 {
994                     if (search_text != NULL)
995                         g_free(search_text);
996                     search_text = g_strdup(t + 2);
997                     search(c, 0);
998                 }
999                 else if (!keywords_try_search(WEBKIT_WEB_VIEW(c->web_view), t))
1000                 {
1001                     f = ensure_uri_scheme(t);
1002                     webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), f);
1003                     g_free(f);
1004                 }
1005                 return TRUE;
1006             case GDK_KEY_Escape:
1007                 t = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
1008                 gtk_entry_set_text(GTK_ENTRY(c->location),
1009                                    (t == NULL ? __NAME__ : t));
1010                 return TRUE;
1011         }
1012     }
1013
1014     return FALSE;
1015 }
1016
1017 gboolean
1018 key_web_view(GtkWidget *widget, GdkEvent *event, gpointer data)
1019 {
1020     struct Client *c = (struct Client *)data;
1021     gdouble dx, dy;
1022     gfloat z;
1023
1024     if (key_common(widget, event, data))
1025         return TRUE;
1026
1027     if (event->type == GDK_KEY_PRESS)
1028     {
1029         if (((GdkEventKey *)event)->keyval == GDK_KEY_Escape)
1030         {
1031             webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(c->web_view));
1032             gtk_entry_set_progress_fraction(GTK_ENTRY(c->location), 0);
1033         }
1034     }
1035     else if (event->type == GDK_BUTTON_RELEASE)
1036     {
1037         switch (((GdkEventButton *)event)->button)
1038         {
1039             case 2:
1040                 if (c->hover_uri != NULL)
1041                 {
1042                     client_new(c->hover_uri, NULL, TRUE);
1043                     return TRUE;
1044                 }
1045                 break;
1046             case 8:
1047                 webkit_web_view_go_back(WEBKIT_WEB_VIEW(c->web_view));
1048                 return TRUE;
1049             case 9:
1050                 webkit_web_view_go_forward(WEBKIT_WEB_VIEW(c->web_view));
1051                 return TRUE;
1052         }
1053     }
1054     else if (event->type == GDK_SCROLL)
1055     {
1056         if (((GdkEventScroll *)event)->state & GDK_MOD1_MASK ||
1057             ((GdkEventScroll *)event)->state & GDK_CONTROL_MASK)
1058         {
1059             gdk_event_get_scroll_deltas(event, &dx, &dy);
1060             z = webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(c->web_view));
1061             z += -dy * 0.1;
1062             z = dx != 0 ? global_zoom : z;
1063             webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(c->web_view), z);
1064             return TRUE;
1065         }
1066     }
1067
1068     return FALSE;
1069 }
1070
1071 void
1072 keywords_load(void)
1073 {
1074     GError *err = NULL;
1075     GIOChannel *channel = NULL;
1076     gchar *path = NULL, *buf = NULL;
1077     gchar **tokens = NULL;
1078
1079     keywords = g_hash_table_new(g_str_hash, g_str_equal);
1080
1081     path = g_build_filename(g_get_user_config_dir(), __NAME__, "keywordsearch",
1082                             NULL);
1083     channel = g_io_channel_new_file(path, "r", &err);
1084     if (channel != NULL)
1085     {
1086         while (g_io_channel_read_line(channel, &buf, NULL, NULL, NULL)
1087                == G_IO_STATUS_NORMAL)
1088         {
1089             g_strstrip(buf);
1090             if (buf[0] != '#')
1091             {
1092                 tokens = g_strsplit(buf, " ", 2);
1093                 if (tokens[0] != NULL && tokens[1] != NULL)
1094                     g_hash_table_insert(keywords, g_strdup(tokens[0]),
1095                                         g_strdup(tokens[1]));
1096                 g_strfreev(tokens);
1097             }
1098             g_free(buf);
1099         }
1100         g_io_channel_shutdown(channel, FALSE, NULL);
1101     }
1102     g_free(path);
1103 }
1104
1105 gboolean
1106 keywords_try_search(WebKitWebView *web_view, const gchar *t)
1107 {
1108     gboolean ret = FALSE;
1109     gchar **tokens = NULL;
1110     gchar *val = NULL, *escaped = NULL, *uri = NULL;
1111
1112     tokens = g_strsplit(t, " ", 2);
1113     if (tokens[0] != NULL && tokens[1] != NULL)
1114     {
1115         val = g_hash_table_lookup(keywords, tokens[0]);
1116         if (val != NULL)
1117         {
1118             escaped = g_uri_escape_string(tokens[1], NULL, TRUE);
1119             uri = g_strdup_printf((gchar *)val, escaped);
1120             webkit_web_view_load_uri(web_view, uri);
1121             g_free(uri);
1122             g_free(escaped);
1123             ret = TRUE;
1124         }
1125     }
1126     g_strfreev(tokens);
1127
1128     return ret;
1129 }
1130
1131 void
1132 mainwindow_setup(void)
1133 {
1134     mw.win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1135     gtk_window_set_default_size(GTK_WINDOW(mw.win), 800, 600);
1136     g_signal_connect(G_OBJECT(mw.win), "destroy", gtk_main_quit, NULL);
1137     gtk_window_set_title(GTK_WINDOW(mw.win), __NAME__);
1138
1139     mw.notebook = gtk_notebook_new();
1140     gtk_notebook_set_scrollable(GTK_NOTEBOOK(mw.notebook), TRUE);
1141     gtk_notebook_set_tab_pos(GTK_NOTEBOOK(mw.notebook), tab_pos);
1142     gtk_container_add(GTK_CONTAINER(mw.win), mw.notebook);
1143     g_signal_connect(G_OBJECT(mw.notebook), "switch-page",
1144                      G_CALLBACK(mainwindow_title_before), NULL);
1145 }
1146
1147 void
1148 mainwindow_title_before(GtkNotebook *nb, GtkWidget *p, guint idx, gpointer data)
1149 {
1150     mainwindow_title(idx);
1151 }
1152
1153 void
1154 mainwindow_title(gint idx)
1155 {
1156     GtkWidget *child, *tabbox, *evbox, *label;
1157     GList *tabbox_children, *last;
1158     const gchar *text;
1159
1160     if (idx == -1)
1161     {
1162         idx = gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.notebook));
1163         if (idx == -1)
1164             return;
1165     }
1166
1167     child = gtk_notebook_get_nth_page(GTK_NOTEBOOK(mw.notebook), idx);
1168     if (child == NULL)
1169         return;
1170
1171     evbox = gtk_notebook_get_tab_label(GTK_NOTEBOOK(mw.notebook), child);
1172     tabbox = gtk_bin_get_child(GTK_BIN(evbox));
1173     tabbox_children = gtk_container_get_children(GTK_CONTAINER(tabbox));
1174     last = g_list_last(tabbox_children);
1175     label = last->data;
1176     text = gtk_label_get_text(GTK_LABEL(label));
1177     gtk_window_set_title(GTK_WINDOW(mw.win), text);
1178     g_list_free(tabbox_children);
1179 }
1180
1181 gboolean
1182 menu_web_view(WebKitWebView *web_view, WebKitContextMenu *menu, GdkEvent *ev,
1183               WebKitHitTestResult *ht, gpointer data)
1184 {
1185     struct Client *c = (struct Client *)data;
1186     GSimpleAction *action = NULL;
1187     WebKitContextMenuItem *mi = NULL;
1188     const gchar *uri = NULL;
1189
1190     (void)ev;
1191
1192     if (webkit_hit_test_result_context_is_link(ht))
1193         uri = webkit_hit_test_result_get_link_uri(ht);
1194     else if (webkit_hit_test_result_context_is_image(ht))
1195         uri = webkit_hit_test_result_get_image_uri(ht);
1196     else if (webkit_hit_test_result_context_is_media(ht))
1197         uri = webkit_hit_test_result_get_media_uri(ht);
1198
1199     if (uri != NULL)
1200     {
1201         webkit_context_menu_append(menu, webkit_context_menu_item_new_separator());
1202
1203         if (c->external_handler_uri != NULL)
1204             g_free(c->external_handler_uri);
1205         c->external_handler_uri = g_strdup(uri);
1206         action = g_simple_action_new("external_handler", NULL);
1207         g_signal_connect(G_OBJECT(action), "activate",
1208                          G_CALLBACK(external_handler_run), data);
1209         mi = webkit_context_menu_item_new_from_gaction(G_ACTION(action),
1210                                                        "Open with external handler",
1211                                                        NULL);
1212         webkit_context_menu_append(menu, mi);
1213         g_object_unref(action);
1214     }
1215
1216     /* FALSE = Show the menu. (TRUE = Don't ever show it.) */
1217     return FALSE;
1218 }
1219
1220 gboolean
1221 quit_if_nothing_active(void)
1222 {
1223     if (clients == 0)
1224     {
1225         if (downloads == 0)
1226         {
1227             gtk_main_quit();
1228             return TRUE;
1229         }
1230         else
1231             gtk_widget_show_all(dm.win);
1232     }
1233
1234     return FALSE;
1235 }
1236
1237 gboolean
1238 remote_msg(GIOChannel *channel, GIOCondition condition, gpointer data)
1239 {
1240     gchar *uri = NULL;
1241
1242     g_io_channel_read_line(channel, &uri, NULL, NULL, NULL);
1243     if (uri)
1244     {
1245         g_strstrip(uri);
1246         client_new(uri, NULL, TRUE);
1247         g_free(uri);
1248     }
1249     return TRUE;
1250 }
1251
1252 void
1253 run_user_scripts(WebKitWebView *web_view)
1254 {
1255     gchar *base = NULL, *path = NULL, *contents = NULL;
1256     const gchar *entry = NULL;
1257     GDir *scriptdir = NULL;
1258
1259     base = g_build_filename(g_get_user_config_dir(), __NAME__, "user-scripts", NULL);
1260     scriptdir = g_dir_open(base, 0, NULL);
1261     if (scriptdir != NULL)
1262     {
1263         while ((entry = g_dir_read_name(scriptdir)) != NULL)
1264         {
1265             path = g_build_filename(base, entry, NULL);
1266             if (g_str_has_suffix(path, ".js"))
1267             {
1268                 if (g_file_get_contents(path, &contents, NULL, NULL))
1269                 {
1270                     webkit_web_view_run_javascript(web_view, contents, NULL, NULL, NULL);
1271                     g_free(contents);
1272                 }
1273             }
1274             g_free(path);
1275         }
1276         g_dir_close(scriptdir);
1277     }
1278
1279     g_free(base);
1280 }
1281
1282 void
1283 search(gpointer data, gint direction)
1284 {
1285     struct Client *c = (struct Client *)data;
1286     WebKitWebView *web_view = WEBKIT_WEB_VIEW(c->web_view);
1287     WebKitFindController *fc = webkit_web_view_get_find_controller(web_view);
1288
1289     if (search_text == NULL)
1290         return;
1291
1292     switch (direction)
1293     {
1294         case 0:
1295             webkit_find_controller_search(fc, search_text,
1296                                           WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE |
1297                                           WEBKIT_FIND_OPTIONS_WRAP_AROUND,
1298                                           G_MAXUINT);
1299             break;
1300         case 1:
1301             webkit_find_controller_search_next(fc);
1302             break;
1303         case -1:
1304             webkit_find_controller_search_previous(fc);
1305             break;
1306     }
1307 }
1308
1309 void
1310 show_web_view(WebKitWebView *web_view, gpointer data)
1311 {
1312     struct Client *c = (struct Client *)data;
1313     gint idx;
1314
1315     (void)web_view;
1316
1317     gtk_widget_show_all(mw.win);
1318
1319     idx = gtk_notebook_page_num(GTK_NOTEBOOK(mw.notebook), c->vbox);
1320     if (idx != -1)
1321         gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), idx);
1322
1323     gtk_widget_grab_focus(c->web_view);
1324 }
1325
1326 void
1327 trust_user_certs(WebKitWebContext *wc)
1328 {
1329     GTlsCertificate *cert;
1330     const gchar *basedir, *file, *absfile;
1331     GDir *dir;
1332
1333     basedir = g_build_filename(g_get_user_config_dir(), __NAME__, "certs", NULL);
1334     dir = g_dir_open(basedir, 0, NULL);
1335     if (dir != NULL)
1336     {
1337         file = g_dir_read_name(dir);
1338         while (file != NULL)
1339         {
1340             absfile = g_build_filename(g_get_user_config_dir(), __NAME__, "certs",
1341                                        file, NULL);
1342             cert = g_tls_certificate_new_from_file(absfile, NULL);
1343             if (cert == NULL)
1344                 fprintf(stderr, __NAME__": Could not load trusted cert '%s'\n", file);
1345             else
1346                 webkit_web_context_allow_tls_certificate_for_host(wc, cert, file);
1347             file = g_dir_read_name(dir);
1348         }
1349         g_dir_close(dir);
1350     }
1351 }
1352
1353
1354 int
1355 main(int argc, char **argv)
1356 {
1357     gchar *c;
1358     int opt, i;
1359
1360     gtk_init(&argc, &argv);
1361     webkit_web_context_set_process_model(webkit_web_context_get_default(),
1362         WEBKIT_PROCESS_MODEL_MULTIPLE_SECONDARY_PROCESSES);
1363
1364     grab_environment_configuration();
1365
1366     while ((opt = getopt(argc, argv, "C")) != -1)
1367     {
1368         switch (opt)
1369         {
1370             case 'C':
1371                 cooperative_instances = FALSE;
1372                 break;
1373             default:
1374                 fprintf(stderr, "Usage: "__NAME__" [OPTION]... [URI]...\n");
1375                 exit(EXIT_FAILURE);
1376         }
1377     }
1378
1379     keywords_load();
1380     if (cooperative_instances)
1381         cooperation_setup();
1382     downloadmanager_setup();
1383
1384     mainwindow_setup();
1385
1386     if (!cooperative_instances || cooperative_alone)
1387     {
1388         c = g_build_filename(g_get_user_config_dir(), __NAME__, "web_extensions",
1389                              NULL);
1390         webkit_web_context_set_web_extensions_directory(
1391             webkit_web_context_get_default(), c
1392         );
1393     }
1394
1395     if (optind >= argc)
1396         client_new(home_uri, NULL, TRUE);
1397     else
1398     {
1399         for (i = optind; i < argc; i++)
1400             client_new(argv[i], NULL, TRUE);
1401     }
1402
1403     if (!cooperative_instances || cooperative_alone)
1404         gtk_main();
1405     exit(EXIT_SUCCESS);
1406 }