browser.c
1/**
2 * Copyright (C) 2023 Chris Noxz
3 * Author(s): Chris Noxz <chris@noxz.tech>
4 *
5 * This program is free software: you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the Free
7 * Software Foundation, either version 3 of the License, or (at your option)
8 * any later version.
9 *
10 * This program is distributed in the hope that it will be useful, but WITHOUT
11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 * more details.
14 *
15 * You should have received a copy of the GNU General Public License along with
16 * this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#include "browser.h"
20#include "config.h"
21
22
23void
24about_scheme_request(WebKitURISchemeRequest *request,
25 gpointer data)
26{
27 (void) data;
28
29 GError *e; /* error holder */
30 GInputStream *s; /* stream to send */
31 GString *m; /* message of stream */
32 const gchar *p; /* path holder eg. 'config' */
33 gchar **w; /* temporary word holder */
34 gsize sl; /* size of stream */
35 int st[4] = {0}; /* memory stats */
36
37 p = webkit_uri_scheme_request_get_path(request);
38
39 /* about:config */
40 if (!strcmp(p, "config")) {
41 g_string_append_printf((m = g_string_new(NULL)), "<html><body>"
42 "<table style=\"width:100%%;\">"
43 "<tr>"
44 "<th align=\"left\">Configurable Name</th>"
45 "<th align=\"left\">Value</th>"
46 "</tr>"
47 "<tr><td>"__NAME__" version</td><td>%s</td></tr>"
48 "<tr><td>WebKit version</td><td>%u.%u.%u</td></tr>",
49 VERSION,
50 webkit_get_major_version(),
51 webkit_get_minor_version(),
52 webkit_get_micro_version());
53 for (int i = 0; i < LastConfig; i++) {
54 if (cfg[i].e == NULL)
55 continue;
56 g_string_append_printf(m, "<tr><td>%s</td><td>", cfg[i].e);
57 switch (cfg[i].t) {
58 case CFG_STRING:
59 g_string_append_printf(m, "%s", cfg[i].v.s);
60 break;
61 case CFG_INT:
62 g_string_append_printf(m, "%d", cfg[i].v.i);
63 break;
64 case CFG_FLOAT:
65 g_string_append_printf(m, "%f", cfg[i].v.f);
66 break;
67 case CFG_BOOL:
68 g_string_append_printf(m, "%s", cfg[i].v.b ? "TRUE" : "FALSE");
69 break;
70 case CFG_LIST:
71 for (w = cfg[i].v.l; w && *w; w++)
72 g_string_append_printf(m, "%s%s", w == cfg[i].v.l
73 ? "" : "; ", *w);
74 break;
75 }
76 g_string_append_printf(m, "</td></tr>");
77 }
78 g_string_append_printf(m, "</table>");
79 g_string_append_printf(m, "</body></html>");
80 /* about:memory (only exists if memory can be read) */
81 } else if (!strcmp(p, "memory")
82 && get_memory(&(st[0]), &(st[1]), &(st[2]), &(st[3]))) {
83 g_string_append_printf((m = g_string_new(NULL)), "<html><body>"
84 "<table style=\"width:100%%;\">"
85 "<tr><td>current real memory</td><td>%'d kB</tr>"
86 "<tr><td>peak real memory</td><td>%'d kB</tr>"
87 "<tr><td>current virt memory</td><td>%'d kB</tr>"
88 "<tr><td>peak virt memory</td><td>%'d kB</tr>"
89 "</table>"
90 "</body></html>",
91 st[0], st[1], st[2], st[3]
92 );
93 } else {
94 webkit_uri_scheme_request_finish_error(request, (e = g_error_new(
95 BROWSER_ERROR,
96 BROWSER_ERROR_INVALID_ABOUT_PATH,
97 "Invalid 'about:%s' page", p)));
98 g_error_free(e);
99 return;
100 }
101
102 sl = strlen(m->str);
103 s = g_memory_input_stream_new_from_data(m->str, sl, NULL);
104 webkit_uri_scheme_request_finish(request, s, sl, "text/html");
105 g_object_unref(s);
106 g_string_free(m, TRUE);
107}
108
109struct Client*
110client_create(const gchar *uri,
111 WebKitWebView *related_wv,
112 gboolean show,
113 gboolean focus_tab)
114{
115 struct Client *c; /* client to be */
116 gchar *u = NULL; /* translated uri */
117 GtkWidget *e, /* event box */
118 *t; /* tab box */
119 int i;
120
121 /* communicate uri through ipc if constructed */
122 if (uri != NULL && gl.ipc == IPC_CLIENT) {
123 if ((u = resolve_uri(uri)) == NULL || ipc_send(u) <= 0
124 || ipc_send("\n") <= 0)
125 fprintf(stderr, __NAME__": Could not send message '%s'\n", u);
126 g_free(u);
127 return NULL; /* the request was handled elsewhere */
128 }
129
130 /* do not create a new client if existing uri was resolved to NULL, as it
131 * was handled as an XDG-OPEN event. Still process uri == NULL for "create"
132 * callback signals, such as "Open Link in New Window". */
133 if (uri != NULL && (u = resolve_uri(uri)) == NULL)
134 return NULL;
135
136 if (!(c = calloc(1, sizeof(struct Client))))
137 die(__NAME__ ": fatal: calloc failed\n");
138
139 if (related_wv == NULL)
140 c->wv = webkit_web_view_new();
141 else
142 c->wv = webkit_web_view_new_with_related_view(related_wv);
143
144 /* connect signal callbacks */
145 CB(c->wv, "button-release-event", cb_wv_hid, c);
146 CB(c->wv, "close", cb_wv_close, c);
147 CB(c->wv, "context-menu", cb_context_menu, c);
148 CB(c->wv, "create", cb_wv_create, NULL);
149 CB(c->wv, "decide-policy", cb_wv_decide_policy, NULL);
150 CB(c->wv, "key-press-event", cb_wv_hid, c);
151 CB(c->wv, "load-changed", cb_wv_load_changed, c);
152 CB(c->wv, "load-failed-with-tls-errors", cb_wv_tls_load_failed, c);
153 CB(c->wv, "mouse-target-changed", cb_wv_hover, c);
154 CB(c->wv, "notify::estimated-load-progress",cb_wv_load_progress_changed, c);
155 CB(c->wv, "notify::favicon", cb_favicon_changed, c);
156 CB(c->wv, "notify::title", cb_title_changed, c);
157 CB(c->wv, "notify::uri", cb_uri_changed, c);
158 CB(c->wv, "scroll-event", cb_wv_hid, c);
159 CB(c->wv, "web-process-crashed", cb_wv_crashed, c);
160
161 /* load config into webkit settings */
162 c->settings = webkit_settings_new();
163 for (i = 0; i < LastConfig; i++)
164 if (cfg[i].s != NULL)
165 g_object_set(
166 G_OBJECT(c->settings),
167 cfg[i].s,
168 cfg[i].i ? (Arg)!(cfg[i].v.b) : (cfg[i].v),
169 NULL
170 );
171 webkit_web_view_set_settings(WEBKIT_WEB_VIEW(c->wv), c->settings);
172
173 if (CFG_L(AcceptedLanguages) != NULL)
174 webkit_web_context_set_preferred_languages(
175 webkit_web_view_get_context(WEBKIT_WEB_VIEW(c->wv)),
176 CFG_L(AcceptedLanguages)
177 );
178 if (CFG_S(CookieFile) != NULL)
179 webkit_cookie_manager_set_persistent_storage(
180 webkit_web_context_get_cookie_manager(
181 webkit_web_view_get_context(WEBKIT_WEB_VIEW(c->wv))
182 ),
183 CFG_S(CookieFile),
184 WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT
185 );
186 if (CFG_S(ProxyUri) != NULL)
187 webkit_website_data_manager_set_network_proxy_settings(
188 webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(c->wv)),
189 WEBKIT_NETWORK_PROXY_MODE_CUSTOM,
190 webkit_network_proxy_settings_new(
191 CFG_S(ProxyUri), CFG_L(ProxyIgnore)
192 )
193 );
194 webkit_web_view_set_zoom_level(
195 WEBKIT_WEB_VIEW(c->wv), CFG_F(ZoomLevel)
196 );
197
198 /* create entry */
199 c->entry = gtk_entry_new();
200 CB(c->entry, "key-press-event", cb_entry_hid, c);
201 CB(c->entry, "icon-release", cb_entry_icon_hid, c);
202 gtk_entry_set_icon_from_icon_name(
203 GTK_ENTRY(c->entry), GTK_ENTRY_ICON_SECONDARY, CFG_B(DisableJavaScript)
204 ? ICON_JS_OFF : ICON_JS_ON
205 );
206
207 /* create vertical box to store the web view and the entry */
208 c->vbx = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
209 gtk_box_pack_start(GTK_BOX(c->vbx), c->wv, TRUE, TRUE, 0);
210 gtk_box_pack_start(GTK_BOX(c->vbx), c->entry, FALSE, FALSE, 0);
211 gtk_container_set_focus_child(GTK_CONTAINER(c->vbx), c->wv);
212
213 /* create tab containing an icon and a label */
214 c->tab_icon = gtk_image_new_from_icon_name(ICON_GLOBE, GTK_ICON_SIZE_MENU);
215 c->tab_label = gtk_label_new(__NAME__);
216 gtk_label_set_ellipsize(GTK_LABEL(c->tab_label), PANGO_ELLIPSIZE_END);
217 gtk_widget_set_halign(GTK_WIDGET(c->tab_label), GTK_ALIGN_START);
218 gtk_widget_set_hexpand(GTK_WIDGET(c->tab_label), TRUE);
219 gtk_widget_set_has_tooltip(GTK_WIDGET(c->tab_label), TRUE);
220 /* pack icon and label in a horizontal box */
221 t = gtk_box_new(
222 GTK_ORIENTATION_HORIZONTAL, 5 * gtk_widget_get_scale_factor(mw.win)
223 );
224 gtk_box_pack_start(GTK_BOX(t), c->tab_icon, FALSE, FALSE, 0);
225 gtk_box_pack_start(
226 GTK_BOX(t),
227 c->tab_label,
228 TRUE,
229 TRUE,
230 3 * gtk_widget_get_scale_factor(mw.win)
231 );
232 /* back the tab inside an event box to enable scroll and button events */
233 e = gtk_event_box_new();
234 gtk_container_add(GTK_CONTAINER(e), t);
235 gtk_widget_add_events(e, GDK_SCROLL_MASK);
236 CB(e, "button-release-event", cb_tab_hid, c);
237 CB(e, "scroll-event", cb_tab_hid, c);
238 /* store a (non client) reference to the label for later use */
239 g_object_set_data(G_OBJECT(e), __NAME__"-tab-label", c->tab_label);
240 /* show tab */
241 gtk_widget_show_all(e);
242
243 /* append everything reorderable to the notebook after current page */
244 gtk_notebook_insert_page(
245 GTK_NOTEBOOK(mw.nb),
246 c->vbx,
247 e,
248 gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.nb)) + 1
249 );
250 gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(mw.nb), c->vbx, TRUE);
251
252 /* manage which tab should be focused */
253 c->focus_new_tab = focus_tab;
254 if (show)
255 show_web_view(c);
256 else
257 CB(c->wv, "ready-to-show", cb_wv_show, c);
258
259 /* finally load the uri */
260 if (u != NULL)
261 webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->wv), u);
262 g_free(u);
263
264 gl.clients++;
265 return c;
266}
267
268void
269client_destroy(struct Client *c)
270{
271 int i;
272
273 /* disconnect all handlers */
274 g_signal_handlers_disconnect_by_data(G_OBJECT(c->wv), c);
275 g_signal_handlers_disconnect_by_data(G_OBJECT(c->wv), NULL);
276
277 if ((i = gtk_notebook_page_num(GTK_NOTEBOOK(mw.nb), c->vbx)) != -1)
278 gtk_notebook_remove_page(GTK_NOTEBOOK(mw.nb), i);
279 else
280 fprintf(stderr, __NAME__": Page does not exist in notebook\n");
281
282 free(c);
283
284 if (--gl.clients == 0)
285 quit();
286}
287
288gboolean
289command(struct Client *c,
290 const gchar *t)
291{
292 if (t[0] == '/') { /* in-page search */
293 g_free(gl.search_text);
294 gl.search_text = g_strdup(t + 1);
295 search(c, IPS_INIT);
296 return TRUE;
297 } else if (t[0] == 'q') { /* quit (vim-like) */
298 quit();
299 return TRUE;
300 }
301
302 return FALSE;
303}
304
305void
306create_context_menu(struct Client *c,
307 WebKitContextMenu *context_menu,
308 WebKitHitTestResult *hit_test_result)
309{
310 guint x; /* hit test result context */
311
312 x = webkit_hit_test_result_get_context(hit_test_result);
313
314 webkit_context_menu_prepend(
315 context_menu, webkit_context_menu_item_new_separator()
316 );
317
318 /* if document is the only context */
319 if (x == WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT) {
320 create_context_menu_item(
321 c,
322 context_menu,
323 "open-external",
324 "Open Page Externally",
325 cb_open_external
326 );
327 return; /* no need to check further */
328 }
329
330 if (x & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK)
331 create_context_menu_item(
332 c,
333 context_menu,
334 "open-external",
335 "Open Link Externally",
336 cb_open_external
337 );
338
339 /* requires javascript for DOM access from here on */
340 if (!webkit_settings_get_enable_javascript(c->settings))
341 return;
342
343 if (x & WEBKIT_HIT_TEST_RESULT_CONTEXT_SELECTION)
344 create_context_menu_item(
345 c,
346 context_menu,
347 "selection-search",
348 "Search Selection",
349 cb_selection_search
350 );
351}
352
353void
354create_context_menu_item(struct Client *c,
355 WebKitContextMenu *context_menu,
356 const gchar *name,
357 const gchar *label,
358 void *action)
359{
360 GAction *a = (GAction*)g_simple_action_new(name, NULL);
361
362 CB(a, "activate", action, c);
363 webkit_context_menu_prepend(
364 context_menu,
365 webkit_context_menu_item_new_from_gaction(a, label, NULL)
366 );
367 g_object_unref(a);
368}
369
370void
371die(const char *msg)
372{
373 fprintf(stderr, msg);
374 exit(EXIT_FAILURE);
375}
376
377int
378get_memory(int *cr, /* current real memory */
379 int *pr, /* peak real memory */
380 int *cv, /* current virtual memory */
381 int *pv) /* peak virtual memory */
382{
383 char b[256] = {0};
384 FILE *f;
385
386 *cr = *pr = *cv = *pv = -1;
387
388 if (!(f = fopen("/proc/self/status", "r")))
389 return 0;
390
391 while (fscanf(f, " %255s", b) == 1)
392 if (!strcmp(b, "VmRSS:"))
393 fscanf(f, " %d", cr);
394 else if (!strcmp(b, "VmHWM:"))
395 fscanf(f, " %d", pr);
396 else if (!strcmp(b, "VmSize:"))
397 fscanf(f, " %d", cv);
398 else if (!strcmp(b, "VmPeak:"))
399 fscanf(f, " %d", pv);
400 fclose(f);
401
402 return (*cr != -1 && *pr != -1 && *cv != -1 && *pv != -1);
403}
404
405gchar*
406get_uri(GtkWidget* wv)
407{
408 const gchar *u;
409
410 if (!(u = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(wv))))
411 return NULL;
412
413 if (!strncmp(u, ABOUT_SCHEME":", strlen(ABOUT_SCHEME":")))
414 return g_strconcat("about:", u + strlen(ABOUT_SCHEME":"), NULL);
415
416 return g_strdup(u);
417}
418
419gboolean
420ipc_request(GIOChannel *src,
421 GIOCondition condition,
422 gpointer data)
423{
424 gchar *u = NULL; /* uri received */
425
426 (void)condition; /* not used */
427 (void)data; /* not used */
428
429 g_io_channel_read_line(src, &u, NULL, NULL, NULL);
430 if (u) {
431 g_strstrip(u);
432 client_create(u, NULL, TRUE, TRUE);
433 g_free(u);
434 }
435 return TRUE;
436}
437
438ssize_t
439ipc_send(char *data)
440{
441 size_t s = 0, /* data amount sent */
442 l = strlen(data); /* data length */
443 ssize_t w; /* data written */
444
445 while (s < l) { /* write until everything is sent */
446 if ((w = write(gl.ipc_pipe_fd, data + s, l - s)) == -1) {
447 if (errno == EINTR)
448 continue; /* EINTR: so retry */
449 perror(__NAME__": Error writing to fifo");
450 return w;
451 } else if (w == 0) {
452 return 0;
453 }
454 s += w;
455 }
456
457 return s;
458}
459
460void
461ipc_setup(void)
462{
463 gchar *f = NULL, /* fifo file name */
464 *p = NULL; /* fifo file path */
465
466 /* only setup for ipc clients */
467 if (gl.ipc != IPC_CLIENT)
468 return;
469
470 /* create fifo file path and allow for several ipc setups by naming them */
471 f = g_strdup_printf("%s_%s.fifo", __NAME__, CFG_S(FifoName));
472 p = g_build_filename(g_get_user_runtime_dir(), f, NULL);
473
474 /* fail hard if fifo cannot be created */
475 if (!g_file_test(p, G_FILE_TEST_EXISTS) && mkfifo(p, 0600) == -1)
476 die(__NAME__ ": fatal: mkfifo failed\n");
477
478 /* if no one is listening create the ipc host instance */
479 if ((gl.ipc_pipe_fd = open(p, O_WRONLY | O_NONBLOCK)) == -1) {
480 g_io_add_watch(
481 g_io_channel_new_file(p, "r+", NULL),
482 G_IO_IN,
483 (GIOFunc)ipc_request,
484 NULL
485 );
486 gl.ipc = IPC_HOST; /* convert client to host */
487 }
488
489 g_free(f);
490 g_free(p);
491}
492
493gboolean
494key_common(struct Client *c,
495 GdkEventKey *event)
496{
497 int i,
498 m;
499 gchar *u = NULL;
500
501 /* only handle key presses using the alt key */
502 if (!(event->state & GDK_MOD1_MASK))
503 return FALSE;
504
505 /* key presses using ctrl+alt keys */
506 if (event->state & GDK_CONTROL_MASK) {
507 switch (event->keyval) {
508 case GDK_KEY_J:
509 case GDK_KEY_K: /* move page/tab backwards or forwards in stack */
510 i = gtk_notebook_page_num(GTK_NOTEBOOK(mw.nb), c->vbx);
511 m = gtk_notebook_get_n_pages(GTK_NOTEBOOK(mw.nb)) - 1;
512 i += event->keyval == GDK_KEY_J
513 ? i == 0 ? 0 : -1
514 : i == m ? m : 1;
515 gtk_notebook_reorder_child(GTK_NOTEBOOK(mw.nb), c->vbx, i);
516 return TRUE;
517 case GDK_KEY_j: /* go to previous page/tab */
518 gtk_notebook_prev_page(GTK_NOTEBOOK(mw.nb));
519 return TRUE;
520 case GDK_KEY_k: /* go to next page/tab */
521 gtk_notebook_next_page(GTK_NOTEBOOK(mw.nb));
522 return TRUE;
523 }
524 }
525
526 switch (event->keyval) {
527 case GDK_KEY_q: /* destroy client and close tab */
528 client_destroy(c);
529 return TRUE;
530 case GDK_KEY_w: /* go to home page */
531 if ((u = resolve_uri(CFG_S(HomeUri))) != NULL)
532 webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->wv), u);
533 g_free(u);
534 return TRUE;
535 case GDK_KEY_t: /* create new client/tab */
536 if ((u = resolve_uri(CFG_S(HomeUri))) != NULL) {
537 c = client_create(u, NULL, TRUE, TRUE);
538 g_free(u);
539 gtk_widget_grab_focus(c->entry);
540 }
541 return TRUE;
542 case GDK_KEY_r: /* reload, while bypassing cache */
543 webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(c->wv));
544 return TRUE;
545 case GDK_KEY_i:
546 toggle_inspector(c);
547 return TRUE;
548 case GDK_KEY_o: /* focus entry */
549 gtk_widget_grab_focus(c->entry);
550 return TRUE;
551 case GDK_KEY_n: /* search forward */
552 search(c, IPS_FORWARD);
553 return TRUE;
554 case GDK_KEY_N: /* search backward */
555 search(c, IPS_BACKWARD);
556 return TRUE;
557 case GDK_KEY_slash: /* initiate in-page search */
558 gtk_widget_grab_focus(c->entry);
559 gtk_entry_set_text(GTK_ENTRY(c->entry), ":/");
560 gtk_editable_set_position(GTK_EDITABLE(c->entry), -1);
561 return TRUE;
562 case GDK_KEY_b: /* toggle tab visibility */
563 gtk_notebook_set_show_tabs(
564 GTK_NOTEBOOK(mw.nb),
565 !gtk_notebook_get_show_tabs(GTK_NOTEBOOK(mw.nb))
566 );
567 return TRUE;
568 case GDK_KEY_1:
569 case GDK_KEY_2:
570 case GDK_KEY_3:
571 case GDK_KEY_4:
572 case GDK_KEY_5:
573 case GDK_KEY_6:
574 case GDK_KEY_7:
575 case GDK_KEY_8:
576 case GDK_KEY_9: /* go to nth tab */
577 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.nb), event->keyval-0x31);
578 return TRUE;
579 case GDK_KEY_0:
580 case GDK_KEY_minus:
581 case GDK_KEY_equal: /* set zoom level */
582 webkit_web_view_set_zoom_level(
583 WEBKIT_WEB_VIEW(c->wv),
584 event->keyval == GDK_KEY_0
585 ? CFG_F(ZoomLevel)
586 : webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(c->wv))
587 + ((event->keyval == GDK_KEY_minus) ? -0.1 : 0.1)
588 );
589 return TRUE;
590 case GDK_KEY_H: /* go back in history */
591 webkit_web_view_go_back(WEBKIT_WEB_VIEW(c->wv));
592 return TRUE;
593 case GDK_KEY_L: /* go forward in history */
594 webkit_web_view_go_forward(WEBKIT_WEB_VIEW(c->wv));
595 return TRUE;
596 case GDK_KEY_s: /* toggle javascript (on or off) */
597 set_javascript_policy(c, JSP_TOGGLE);
598 return TRUE;
599 case GDK_KEY_c: /* toggle tls error policy (ignore or fail) */
600 toggle_tls_error_policy(c);
601 return TRUE;
602 case GDK_KEY_h:
603 case GDK_KEY_j:
604 case GDK_KEY_k:
605 case GDK_KEY_l: /* in-page movement by emulating arrow keys... */
606 case GDK_KEY_g:
607 case GDK_KEY_G: /* ... as well as home and end */
608 event->keyval /* translate key press accordingly */
609 = event->keyval == GDK_KEY_h ? GDK_KEY_Left
610 : event->keyval == GDK_KEY_j ? GDK_KEY_Down
611 : event->keyval == GDK_KEY_k ? GDK_KEY_Up
612 : event->keyval == GDK_KEY_l ? GDK_KEY_Right
613 : event->keyval == GDK_KEY_g ? GDK_KEY_Home
614 : /* */ GDK_KEY_End;
615 event->state = 0;
616 gtk_propagate_event(GTK_WIDGET(c->wv), (GdkEvent*)event);
617 return TRUE;
618 case GDK_KEY_p: /* open print dialog */
619 webkit_print_operation_run_dialog(
620 webkit_print_operation_new(WEBKIT_WEB_VIEW(c->wv)),
621 GTK_WINDOW(mw.win)
622 );
623 return TRUE;
624 }
625
626 /* check external handler keys */
627 if (cfg[ExternalHandlerKeys].v.b &&
628 event->keyval < 256 &&
629 g_strv_contains(
630 CFG_L(ExternalHandlerKeys), (gchar[]){(gchar)event->keyval, 0}
631 )) {
632 open_external(c, (gchar)event->keyval);
633 return TRUE;
634 }
635
636 return FALSE;
637}
638
639gboolean
640key_entry(struct Client *c,
641 GdkEventKey *event)
642{
643 const gchar *t;
644 gchar *u = NULL,
645 *l = NULL;
646 int p,
647 m;
648
649 /* handle key presses using the alt key (takes precedence over common) */
650 if (event->state & GDK_MOD1_MASK) {
651 switch (event->keyval) { /* movements inside entry */
652 case GDK_KEY_l:
653 case GDK_KEY_h:
654 p = gtk_editable_get_position(GTK_EDITABLE(c->entry));
655 m = gtk_entry_get_text_length(GTK_ENTRY(c->entry));
656 p += event->keyval == GDK_KEY_l
657 ? m > p ? 1 : 0 /* move right */
658 : 0 < p ? -1 : 0; /* move left */
659 gtk_editable_set_position(GTK_EDITABLE(c->entry), p);
660 return TRUE;
661 }
662 }
663
664 /* handle common key presses */
665 if (key_common(c, event))
666 return TRUE;
667
668 /* handle any key press (not just using the alt key) */
669 switch (event->keyval) {
670 case GDK_KEY_KP_Enter:
671 case GDK_KEY_Return:
672 gtk_widget_grab_focus(c->wv);
673 if ((t = gtk_entry_get_text(GTK_ENTRY(c->entry))) == NULL)
674 return TRUE;
675 if (t[0] != ':') { /* if not a command */
676 /* store current uri before loading new uri */
677 l = get_uri(c->wv);
678 if ((u = resolve_uri(t)) != NULL)
679 webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->wv), u);
680 g_free(u);
681 /* fix: notify::uri won't be raised if current uri is unchanged */
682 if (!strcmp(l, (u = get_uri(c->wv))))
683 uri_changed(c);
684 g_free(u);
685 g_free(l);
686 return TRUE;
687 } else if (command(c, t + 1)) { /* if command */
688 return TRUE;
689 } /* FALL THROUGH */
690 case GDK_KEY_Escape:
691 gtk_widget_grab_focus(c->wv);
692 u = get_uri(c->wv);
693 gtk_entry_set_text(GTK_ENTRY(c->entry), (u == NULL ? __NAME__ : u));
694 gtk_editable_set_position(GTK_EDITABLE(c->entry), -1);
695 g_free(u);
696 return TRUE;
697 }
698
699 return FALSE;
700}
701
702gboolean
703key_tab(struct Client *c,
704 GdkEvent *event)
705{
706 GdkScrollDirection d;
707
708 if (event->type == GDK_BUTTON_RELEASE) {
709 if (((GdkEventButton *)event)->button == 2) { /* scroll wheel click */
710 client_destroy(c);
711 return TRUE;
712 }
713 } else if (event->type == GDK_SCROLL) { /* scroll wheel selection */
714 gdk_event_get_scroll_direction(event, &d);
715 if (d == GDK_SCROLL_UP)
716 gtk_notebook_next_page(GTK_NOTEBOOK(mw.nb));
717 else if (d == GDK_SCROLL_DOWN)
718 gtk_notebook_prev_page(GTK_NOTEBOOK(mw.nb));
719 return TRUE;
720 }
721
722 /* regain lost focus while clicking a tab (not a perfect solution)
723 * note: this won't work if the cursor is dragged while a button is
724 * pressed. One pseudo solution for this would be to subscribe to
725 * event-after (specifically the GDK_ENTER_NOTIFY event type) on tab hid
726 * events, however this only works if the selected tab is hovered while a
727 * button is released and would also cause the corresponding web view for
728 * any hovering tab to grab focus (effectively stealing focus from the
729 * current web view or the entry when not desirable). */
730 gtk_widget_grab_focus(c->wv);
731 gtk_editable_set_position(GTK_EDITABLE(c->entry), -1);
732
733 return FALSE;
734}
735
736gboolean
737key_web_view(struct Client *c,
738 GdkEvent *event)
739{
740 gdouble dx, /* scroll x-delta */
741 dy; /* scroll y-delta */
742
743 /* handle common key presses */
744 if (event->type == GDK_KEY_PRESS && key_common(c, (GdkEventKey *)event))
745 return TRUE;
746
747 /* escape key: stop web rendering */
748 if (event->type == GDK_KEY_PRESS
749 && ((GdkEventKey *)event)->keyval == GDK_KEY_Escape) {
750 webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(c->wv));
751 gtk_entry_set_progress_fraction(GTK_ENTRY(c->entry), 0);
752 /* mouse button events */
753 } else if (event->type == GDK_BUTTON_RELEASE) {
754 switch (((GdkEventButton *)event)->button) {
755 case 2: /* mouse middle button: open in new tab */
756 if (c->hover_uri != NULL) {
757 client_create(c->hover_uri, NULL, TRUE, FALSE);
758 return TRUE;
759 }
760 break;
761 case 8: /* mouse button back: go back in history */
762 webkit_web_view_go_back(WEBKIT_WEB_VIEW(c->wv));
763 return TRUE;
764 case 9: /* mouse button forward: go forward in history */
765 webkit_web_view_go_forward(WEBKIT_WEB_VIEW(c->wv));
766 return TRUE;
767 }
768 /* scroll using alt pressed: zoom in or out */
769 } else if (event->type == GDK_SCROLL
770 && (((GdkEventScroll *)event)->state & GDK_MOD1_MASK)) {
771 gdk_event_get_scroll_deltas(event, &dx, &dy);
772 webkit_web_view_set_zoom_level(
773 WEBKIT_WEB_VIEW(c->wv),
774 dx != 0
775 ? CFG_F(ZoomLevel)
776 : webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(c->wv)) + -dy * 0.1
777 );
778 return TRUE;
779 }
780
781 return FALSE;
782}
783
784void
785load_changed(struct Client *c,
786 WebKitLoadEvent event_type)
787{
788 switch (event_type) {
789 /* when page load starts, reset everything */
790 case WEBKIT_LOAD_STARTED:
791 c->https = FALSE;
792 if (c->error_page)
793 c->error_page = FALSE;
794 else
795 g_clear_object(&c->failed_crt);
796 break;
797 /* when page load is redirected, continue as usual */
798 case WEBKIT_LOAD_REDIRECTED:
799 break;
800 /* when page load is committed, get https and tls state */
801 case WEBKIT_LOAD_COMMITTED:
802 c->https = webkit_web_view_get_tls_info(
803 WEBKIT_WEB_VIEW(c->wv), &c->crt, &c->tls_error
804 );
805 break;
806 /* when page load is finished, run all user scripts */
807 case WEBKIT_LOAD_FINISHED:
808 load_user_styles(WEBKIT_WEB_VIEW(c->wv));
809 run_user_scripts(WEBKIT_WEB_VIEW(c->wv));
810 break;
811 }
812
813 update_title(c);
814}
815
816void
817load_configuration(void)
818{
819 const gchar *e; /* environment variable name */
820 int i;
821
822 /* set global defaults */
823 gl.clients = 0; /* client counter */
824 gl.ipc = IPC_CLIENT; /* ipc type */
825 gl.ipc_pipe_fd = 0; /* ipc pipe file descriptor */
826 gl.search_text = NULL; /* in page search phrase */
827 gl.state_lock = TRUE; /* prevents state save */
828
829 /* load default configuration */
830 for (i = 0; i < LastConfig; i++) {
831 if (cfg[i].e && (e = g_getenv(cfg[i].e)) != NULL) {
832 switch (cfg[i].t) {
833 case CFG_INT: cfg[i].v.i = atoi(e); break;
834 case CFG_FLOAT: cfg[i].v.f = atof(e); break;
835 case CFG_BOOL: cfg[i].v.b = TRUE; break;
836 case CFG_STRING: cfg[i].v.s = g_strdup(e); break;
837 case CFG_LIST: cfg[i].v.l = g_strsplit(e, ",", -1); break;
838 }
839 }
840 }
841}
842
843void
844load_state(void)
845{
846 char u[URI_MAX]; /* line/uri holder */
847 FILE *fp = NULL; /* file pointer */
848 int c, /* character holder */
849 x, /* state indicators */
850 idx = 0; /* current page index */
851 unsigned i = 0u;
852
853 /* only load states for ipc host */
854 if (CFG_S(StateFile) == NULL || gl.ipc != IPC_HOST)
855 return;
856
857 if (!(fp = fopen(CFG_S(StateFile), "r"))) {
858 /* fail if file exists as it cannot be accessed, else remove state lock
859 * to see if saving the state will create it (ie. the parent exists) */
860 if (access(CFG_S(StateFile), F_OK) == 0)
861 perror(__NAME__": Error opening state file");
862 else
863 gl.state_lock = FALSE;
864 return;
865 }
866
867 do {
868 c = fgetc(fp);
869
870 if (i == URI_MAX && !(c == '\n' || c == EOF)) { /* uri is too long */
871 i = 0u;
872 while ((c = fgetc(fp)) != '\n' && c != EOF);
873 if (c == EOF)
874 break;
875 }
876
877 if (c != '\n' && c != EOF) {
878 u[i++] = (char)c;
879 continue; /* character is added to string, not more to do */
880 }
881
882 if (i == 0)
883 continue; /* skip empty lines */
884
885 u[i] = '\0';
886 i = 0u;
887
888 if (sscanf(u, "%d:%s", &x, u) != 2)
889 continue; /* incorrect format, so skip it */
890
891 set_javascript_policy(
892 client_create(u, NULL, TRUE, TRUE),
893 (x & STATE_JAVASCRIPT) != 0
894 );
895 idx = x & STATE_CURRENT ? gl.clients - 1 : idx;
896
897 } while (c != EOF);
898
899 fclose(fp);
900
901 /* select current tab if any */
902 if (gl.clients > 0)
903 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.nb), idx);
904
905 /* enable state save */
906 gl.state_lock = FALSE;
907
908}
909
910void
911load_stdin()
912{
913 int c; /* character holder */
914 GString *s = g_string_new(NULL);
915
916 /* read until end of stream */
917 while ((c = getc(stdin)) != EOF)
918 g_string_append_c(s, (gchar)c);
919
920 /* ignore content of stdin if instance is not independent, as there is no
921 * uri to send over IPC anyway */
922 if (gl.ipc != IPC_NONE)
923 fprintf(stderr, __NAME__": stdin detected, use with -I instead\n");
924 else
925 /* load the html content, which won't survive a reload */
926 webkit_web_view_load_html(
927 WEBKIT_WEB_VIEW(client_create(NULL, NULL, TRUE, TRUE)->wv),
928 s->str,
929 NULL
930 );
931 g_string_free(s, TRUE);
932}
933
934void
935load_user_styles(WebKitWebView *web_view)
936{
937 gchar *c = NULL, /* file content */
938 *p = NULL, /* path (file) */
939 *b = NULL; /* base directory (scripts) */
940 const gchar *e = NULL; /* directory file entry */
941 GDir *s = NULL; /* user style directory */
942
943 b = g_build_filename(g_get_user_config_dir(), __NAME__, CFG_S(UcDir), NULL);
944 if ((s = g_dir_open(b, 0, NULL)) == NULL) {
945 g_free(b);
946 return;
947 }
948
949 webkit_user_content_manager_remove_all_style_sheets(
950 webkit_web_view_get_user_content_manager(web_view)
951 );
952
953 while ((e = g_dir_read_name(s)) != NULL) {
954 if (g_str_has_suffix((p = g_build_filename(b, e, NULL)), ".css")
955 && g_file_get_contents(p, &c, NULL, NULL)) {
956 webkit_user_content_manager_add_style_sheet(
957 webkit_web_view_get_user_content_manager(web_view),
958 webkit_user_style_sheet_new(
959 c,
960 WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
961 WEBKIT_USER_STYLE_LEVEL_USER,
962 NULL,
963 NULL
964 )
965 );
966 g_free(c);
967 }
968 g_free(p);
969 }
970
971 g_dir_close(s);
972 g_free(b);
973}
974
975void
976main_window_setup(void)
977{
978 GtkWidget *v; /* vertical box */
979
980 /* define window elements */
981 mw.win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
982 mw.nb = gtk_notebook_new();
983 mw.dbx = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
984 v = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
985
986 /* window settings */
987 gtk_window_set_default_size(GTK_WINDOW(mw.win), 800, 600);
988 gtk_window_set_title(GTK_WINDOW(mw.win), __NAME__);
989
990 /* notebook settings */
991 gtk_notebook_set_scrollable(GTK_NOTEBOOK(mw.nb), TRUE);
992 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(mw.nb), GTK_POS_TOP);
993
994 /* prevent notebook (including tabs) from stealing focus */
995 gtk_widget_set_can_focus(mw.nb, FALSE);
996
997 /* pack everything in a vertical box */
998 gtk_box_pack_start(GTK_BOX(v), mw.nb, TRUE, TRUE, 0);
999 gtk_box_pack_start(GTK_BOX(v), mw.dbx, FALSE, FALSE, 0);
1000 gtk_container_add(GTK_CONTAINER(mw.win), v);
1001
1002 /* connect signal callbacks */
1003 CB(mw.win, "destroy", cb_quit, NULL);
1004 CB(mw.nb, "switch-page", cb_notebook_switch_page, NULL);
1005 CBA(mw.nb, "switch-page", cb_notebook_modified, NULL);
1006 CB(mw.nb, "page-added", cb_notebook_modified, NULL);
1007 CB(mw.nb, "page-removed", cb_notebook_modified, NULL);
1008 CB(mw.nb, "page-reordered", cb_notebook_modified, NULL);
1009}
1010
1011void
1012open_external(struct Client *c, const gchar s)
1013{
1014 char b[URI_MAX]; /* command line buffer */
1015 gchar *p = NULL; /* external handler path */
1016 p = g_build_filename(
1017 g_get_user_config_dir(), __NAME__, CFG_S(ExternalHandlerFile), NULL
1018 );
1019 if (s)
1020 sprintf(b, "%s %c:%s &", p, s, gtk_entry_get_text(GTK_ENTRY(c->entry)));
1021 else
1022 sprintf(b, "%s %s &", p, gtk_entry_get_text(GTK_ENTRY(c->entry)));
1023 g_free(p);
1024
1025 system(b);
1026}
1027
1028void
1029prepare_download(WebKitDownload *d,
1030 gchar *suggested_filename)
1031{
1032 gchar *s = g_strdup(suggested_filename),
1033 *p = NULL, /* proposed path */
1034 *f = NULL, /* final path */
1035 *u = NULL; /* uri */
1036 GtkWidget *b; /* button */
1037 int i;
1038 static gboolean c = FALSE; /* condition, TRUE=finished */
1039
1040 /* make suggested filename safe, by replacing directory separators */
1041 for (i = 0; s[i]; i++)
1042 if (s[i] == G_DIR_SEPARATOR)
1043 s[i] = '_';
1044
1045 p = g_build_filename(CFG_S(DownloadDirectory), s, NULL);
1046 f = g_strdup(p);
1047
1048 /* check for a free suffix */
1049 for (i = 1; g_file_test(f, G_FILE_TEST_EXISTS) && i < SUFFIX_MAX; i++) {
1050 g_free(f);
1051 f = rebuild_filename(p, i);
1052 }
1053
1054 if (i < SUFFIX_MAX) {
1055 u = g_filename_to_uri(f, NULL, NULL);
1056 webkit_download_set_destination(d, u);
1057
1058 b = gtk_button_new_with_label(s);
1059 gtk_button_set_always_show_image(GTK_BUTTON(b), TRUE);
1060 gtk_button_set_image(
1061 GTK_BUTTON(b),
1062 gtk_image_new_from_icon_name(ICON_DOWNLOAD, GTK_ICON_SIZE_BUTTON)
1063 );
1064 gtk_widget_set_margin_end(b, 1);
1065 gtk_box_pack_start(GTK_BOX(mw.dbx), b, FALSE, FALSE, 0);
1066 gtk_box_reorder_child(GTK_BOX(mw.dbx), b, 0);
1067 gtk_widget_show_all(mw.dbx);
1068
1069 g_object_set_data(G_OBJECT(b), __NAME__"-finished", (gpointer)&c);
1070
1071 CB(d, "notify::estimated-progress", cb_download_changed_progress, b);
1072 CB(d, "finished", cb_download_finished, b);
1073 g_object_ref(d);
1074 CB(b, "button-press-event", cb_download_press, d);
1075 } else { /* could not find a free suffix under the SUFFIX_MAX limit */
1076 fprintf(stderr, __NAME__": Limit reached for filename suffix\n");
1077 webkit_download_cancel(d);
1078 }
1079
1080 g_free(p);
1081 g_free(s);
1082 g_free(f);
1083 g_free(u);
1084}
1085
1086void
1087quit(void)
1088{
1089 save_state();
1090 gtk_main_quit();
1091}
1092
1093gchar *
1094rebuild_filename(
1095 gchar *p,
1096 int n)
1097{
1098 int i, l = strlen(p);
1099 gchar *f;
1100
1101 for (i = l; i >= 0; i--)
1102 if (p[i] == '.' || (p[i] == '/' && (i = l) >= 0))
1103 break;
1104
1105 if (i == -1)
1106 die(__NAME__ ": fatal: invalid download path\n");
1107
1108 if (i < l) {
1109 p[i] = '\0';
1110 f = g_strdup_printf("%s.%d.%s", p, n, &p[i + 1]);
1111 p[i] = '.';
1112 } else {
1113 f = g_strdup_printf("%s.%d", p, n);
1114 }
1115
1116 return f;
1117}
1118
1119void
1120render_tls_error(struct Client *c,
1121 gchar *uri,
1122 GTlsCertificate *crt,
1123 GTlsCertificateFlags cert_flags)
1124{
1125 GString *m = NULL; /* message (error) */
1126 gchar *h = NULL, /* html code */
1127 *s = NULL, /* subject name */
1128 *i = NULL, /* issuer name */
1129 *b = NULL, /* not before date */
1130 *a = NULL, /* not after date */
1131 *p = NULL; /* pem block */
1132 GDateTime *bd = NULL, /* not before date */
1133 *ad = NULL; /* not after date */
1134
1135 m = g_string_new(NULL);
1136 c->failed_crt = g_object_ref(crt);
1137 c->tls_error = cert_flags;
1138 c->error_page = TRUE;
1139
1140 /* translate all flags to messages */
1141 if (c->tls_error & G_TLS_CERTIFICATE_UNKNOWN_CA)
1142 g_string_append(m, MSG_TLS_CERTIFICATE_UNKNOWN_CA);
1143 if (c->tls_error & G_TLS_CERTIFICATE_BAD_IDENTITY)
1144 g_string_append(m, MSG_TLS_CERTIFICATE_BAD_IDENTITY);
1145 if (c->tls_error & G_TLS_CERTIFICATE_NOT_ACTIVATED)
1146 g_string_append(m, MSG_TLS_CERTIFICATE_NOT_ACTIVATED);
1147 if (c->tls_error & G_TLS_CERTIFICATE_EXPIRED)
1148 g_string_append(m, MSG_TLS_CERTIFICATE_EXPIRED);
1149 if (c->tls_error & G_TLS_CERTIFICATE_REVOKED)
1150 g_string_append(m, MSG_TLS_CERTIFICATE_REVOKED);
1151 if (c->tls_error & G_TLS_CERTIFICATE_INSECURE)
1152 g_string_append(m, MSG_TLS_CERTIFICATE_INSECURE);
1153 if (c->tls_error & G_TLS_CERTIFICATE_GENERIC_ERROR)
1154 g_string_append(m, MSG_TLS_CERTIFICATE_GENERIC_ERROR);
1155
1156 /* construct html code and load it */
1157 g_object_get(crt, "subject-name", &s, NULL);
1158 g_object_get(crt, "issuer-name", &i, NULL);
1159 g_object_get(crt, "not-valid-before", &bd, NULL);
1160 g_object_get(crt, "not-valid-after", &ad, NULL);
1161 g_object_get(crt, "certificate-pem", &p, NULL);
1162 b = g_date_time_format_iso8601(bd);
1163 a = g_date_time_format_iso8601(ad);
1164 h = g_strdup_printf(TLS_MSG_FORMAT, uri, m->str, s, i, b, a, p);
1165 webkit_web_view_load_alternate_html(WEBKIT_WEB_VIEW(c->wv), h, uri, NULL);
1166
1167 g_string_free(m, TRUE);
1168 g_free(h);
1169 g_free(s);
1170 g_free(i);
1171 g_free(b);
1172 g_free(a);
1173 g_free(p);
1174 g_date_time_unref(ad);
1175 g_date_time_unref(bd);
1176}
1177
1178void
1179run_user_scripts(WebKitWebView *web_view)
1180{
1181 gchar *c = NULL, /* file content */
1182 *p = NULL, /* path (file) */
1183 *b = NULL; /* base directory (scripts) */
1184 const gchar *e = NULL; /* directory file entry */
1185 GDir *s = NULL; /* user script directory */
1186
1187 b = g_build_filename(g_get_user_config_dir(), __NAME__, CFG_S(UsDir), NULL);
1188 if ((s = g_dir_open(b, 0, NULL)) == NULL) {
1189 g_free(b);
1190 return;
1191 }
1192
1193 while ((e = g_dir_read_name(s)) != NULL) {
1194 if (g_str_has_suffix((p = g_build_filename(b, e, NULL)), ".js")
1195 && g_file_get_contents(p, &c, NULL, NULL)) {
1196 webkit_web_view_evaluate_javascript(
1197 web_view, c, -1, NULL, NULL, NULL, NULL, NULL
1198 );
1199 g_free(c);
1200 }
1201 g_free(p);
1202 }
1203
1204 g_dir_close(s);
1205 g_free(b);
1206}
1207
1208void
1209save_history(const gchar *t)
1210{
1211 FILE *fp;
1212
1213 if (CFG_S(HistoryFile) == NULL)
1214 return;
1215
1216 if ((fp = fopen(CFG_S(HistoryFile), "a")) != NULL) {
1217 fprintf(fp, "%s\n", t);
1218 fclose(fp);
1219 } else {
1220 perror(__NAME__": Error opening history file");
1221 }
1222}
1223
1224void
1225save_state(void)
1226{
1227 GList *c; /* child (notebook page) */
1228 int x, /* state indicators */
1229 i,
1230 l; /* length (number of tabs) */
1231 FILE *fp;
1232 WebKitWebView *wv;
1233 WebKitSettings *s;
1234 gchar *u = NULL;
1235
1236 /* only save state for ipc host, when state isn't locked for loading */
1237 if (CFG_S(StateFile) == NULL || gl.ipc != IPC_HOST || gl.state_lock)
1238 return;
1239
1240 if ((fp = fopen(CFG_S(StateFile), "w")) == NULL) {
1241 perror(__NAME__": Error opening state file");
1242 return;
1243 }
1244
1245 /* save uri from each tab into state file */
1246 for (i = 0, l = gtk_notebook_get_n_pages(GTK_NOTEBOOK(mw.nb)); i < l; i++) {
1247 c = gtk_container_get_children(
1248 GTK_CONTAINER(gtk_notebook_get_nth_page(GTK_NOTEBOOK(mw.nb), i))
1249 );
1250 if (strcmp(gtk_widget_get_name((c->data)), "WebKitWebView"))
1251 continue;
1252 wv = WEBKIT_WEB_VIEW(c->data);
1253 s = webkit_web_view_get_settings(wv);
1254 x = 0;
1255 if (gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.nb)) == i)
1256 x |= STATE_CURRENT;
1257 if (webkit_settings_get_enable_javascript_markup(s))
1258 x |= STATE_JAVASCRIPT;
1259 fprintf(fp, "%d:%s\n", x, (u = get_uri((GtkWidget*)wv)));
1260 g_free(u);
1261 }
1262
1263 fclose(fp);
1264}
1265
1266void
1267search(struct Client *c,
1268 enum inpage_search_type type)
1269{
1270 if (gl.search_text == NULL)
1271 return;
1272
1273 switch (type) {
1274 case IPS_INIT:
1275 webkit_find_controller_search(
1276 webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(c->wv)),
1277 gl.search_text,
1278 WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE
1279 | WEBKIT_FIND_OPTIONS_WRAP_AROUND,
1280 G_MAXUINT
1281 );
1282 break;
1283 case IPS_FORWARD:
1284 webkit_find_controller_search_next(
1285 webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(c->wv))
1286 );
1287 break;
1288 case IPS_BACKWARD:
1289 webkit_find_controller_search_previous(
1290 webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(c->wv))
1291 );
1292 break;
1293 }
1294}
1295
1296void
1297selection_search(struct Client *c)
1298{
1299 webkit_web_view_evaluate_javascript(
1300 WEBKIT_WEB_VIEW(c->wv),
1301 "window.getSelection().toString();",
1302 -1,
1303 NULL,
1304 NULL,
1305 NULL,
1306 cb_selection_search_finished,
1307 c
1308 );
1309}
1310
1311void
1312selection_search_finished(struct Client *c,
1313 GAsyncResult *result)
1314{
1315 JSCValue *v = NULL;
1316 gchar *s = NULL,
1317 *u = NULL;
1318
1319 if ((v = webkit_web_view_evaluate_javascript_finish(
1320 WEBKIT_WEB_VIEW(c->wv), result, NULL
1321 )) == NULL)
1322 return;
1323
1324 if (jsc_value_is_string(v)
1325 && strlen(s = jsc_value_to_string(v))
1326 && !jsc_context_get_exception(jsc_value_get_context(v))
1327 && (u = resolve_uri(s)) != NULL)
1328 client_create(u, NULL, TRUE, FALSE);
1329
1330 g_free(u);
1331 g_free(s);
1332}
1333
1334void
1335set_default_web_context(void)
1336{
1337 gchar *p = NULL; /* web extensions path */
1338 WebKitWebContext *c; /* web context */
1339
1340 /* no context needed for clients */
1341 if (gl.ipc == IPC_CLIENT)
1342 return;
1343
1344 c = webkit_web_context_get_default();
1345
1346 p = g_build_filename(g_get_user_config_dir(), __NAME__, CFG_S(WeDir), NULL);
1347#if GTK_CHECK_VERSION(3, 98, 0) /* seems to be fixed in 3.98.0, we'll see */
1348 webkit_web_context_set_sandbox_enabled(c, TRUE);
1349 webkit_web_context_add_path_to_sandbox(c, p, TRUE);
1350#else
1351 webkit_web_context_set_web_extensions_directory(c, p);
1352#endif
1353
1354 CB(c, "download-started", cb_download_start, NULL);
1355 webkit_web_context_set_favicon_database_directory(c, NULL);
1356 webkit_web_context_register_uri_scheme(
1357 c, ABOUT_SCHEME, (WebKitURISchemeRequestCallback)about_scheme_request,
1358 NULL, NULL
1359 );
1360
1361 g_free(p);
1362}
1363
1364void
1365set_hover_uri(struct Client *c,
1366 WebKitHitTestResult *hit_test_result)
1367{
1368 const char *t; /* entry text holder */
1369 gchar *u = NULL; /* uri text holder */
1370
1371 g_free(c->hover_uri);
1372
1373 /* only display hovered links */
1374 if (webkit_hit_test_result_context_is_link(hit_test_result)) {
1375 t = webkit_hit_test_result_get_link_uri(hit_test_result);
1376 c->hover_uri = g_strdup(t);
1377 } else {
1378 u = get_uri(c->wv);
1379 c->hover_uri = NULL;
1380 }
1381
1382 if (!gtk_widget_is_focus(c->entry))
1383 gtk_entry_set_text(GTK_ENTRY(c->entry), u ? u : t);
1384
1385 g_free(u);
1386}
1387
1388void
1389set_javascript_policy(struct Client *c,
1390 enum javascript_policy policy)
1391{
1392 webkit_settings_set_enable_javascript_markup(
1393 c->settings,
1394 policy == JSP_TOGGLE
1395 ? !webkit_settings_get_enable_javascript_markup(c->settings)
1396 : policy
1397 );
1398 gtk_entry_set_icon_from_icon_name(
1399 GTK_ENTRY(c->entry),
1400 GTK_ENTRY_ICON_SECONDARY,
1401 webkit_settings_get_enable_javascript_markup(c->settings)
1402 ? ICON_JS_ON
1403 : ICON_JS_OFF
1404 );
1405 webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(c->wv));
1406 save_state();
1407}
1408
1409void
1410set_window_title(gint i)
1411{
1412 GtkWidget *c; /* notebook child/page */
1413
1414 if ((c = gtk_notebook_get_nth_page(GTK_NOTEBOOK(mw.nb), i)) == NULL)
1415 return;
1416
1417 gtk_window_set_title(GTK_WINDOW(mw.win),
1418 gtk_label_get_text(
1419 GTK_LABEL((GtkWidget *)g_object_get_data(
1420 G_OBJECT(gtk_notebook_get_tab_label(GTK_NOTEBOOK(mw.nb), c)),
1421 __NAME__"-tab-label"
1422 ))
1423 )
1424 );
1425}
1426
1427void
1428show_web_view(struct Client *c)
1429{
1430 gint i;
1431
1432 gtk_widget_show_all(mw.win);
1433
1434 if (c->focus_new_tab) {
1435 if ((i = gtk_notebook_page_num(GTK_NOTEBOOK(mw.nb), c->vbx)) != -1)
1436 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.nb), i);
1437 gtk_widget_grab_focus(c->wv);
1438 }
1439}
1440
1441void
1442toggle_inspector(struct Client *c)
1443{
1444 WebKitWebInspector *i;
1445
1446 i = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(c->wv));
1447
1448 /* assumes that the inspector has not been detached by the user */
1449 if (webkit_web_inspector_is_attached(i))
1450 webkit_web_inspector_close(i);
1451 else
1452 webkit_web_inspector_show(i);
1453}
1454
1455void
1456toggle_tls_error_policy(struct Client *c)
1457{
1458 webkit_website_data_manager_set_tls_errors_policy(
1459 webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(c->wv)),
1460 !webkit_website_data_manager_get_tls_errors_policy(
1461 webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(c->wv))
1462 )
1463 );
1464 webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(c->wv));
1465}
1466
1467gchar *
1468resolve_uri(const gchar *t)
1469{
1470 gchar *u = NULL, /* uri to return */
1471 *l = NULL, /* temporary string */
1472 *e = NULL; /* uri encoded string */
1473 const gchar *s = NULL;
1474 wordexp_t x; /* wordexp struct */
1475 int r = -1; /* result holder for wordexp */
1476
1477 /* cannot expand string in file scheme, so try without scheme */
1478 if (!strncmp("file://", t, 7) && (t[7] == '~' || t[7] == '$'))
1479 return resolve_uri(t + 7);
1480
1481 /* use internal about page so that about: prefix is ignored by WebKit */
1482 if (!strncmp(t, "about:", 6) && strcmp(t, "about:blank"))
1483 u = g_strdup_printf(ABOUT_SCHEME":%s", t + 6);
1484 /* check if valid scheme, and if so just create a copy of the text */
1485 else if ((s = g_uri_peek_scheme(t)) && g_strv_contains(CFG_L(UriSchemes), s))
1486 u = g_strdup(t);
1487 /* if no match, then test xdg schemes (schemes that are redirected) */
1488 else if (s && CFG_L(XdgSchemes) && g_strv_contains(CFG_L(XdgSchemes), s))
1489 xdg_open(s, t);
1490 /* if path is local, use the file scheme, else try to see if the string is
1491 * expandable and is a local path */
1492 else if ((l = realpath(t, NULL)) != NULL || ((r = wordexp(t, &x, 0)) == 0
1493 && (l = resolve_uri_words(x.we_wordc, x.we_wordv)) != NULL))
1494 u = g_strdup_printf("file://%s", l);
1495 /* else, check if the text can be interpreted as a valid https uri; it's
1496 * not enough to check if uri is valid - check for period and no spaces */
1497 else if (strchr(t, '.') && !strchr(t, ' ')
1498 && g_uri_is_valid((l = g_strdup_printf("https://%s", t)), 0, NULL))
1499 u = g_strdup(l);
1500 /* fallback to web search, using a specified search engine */
1501 else
1502 u = g_strdup_printf(
1503 CFG_S(SearchEngineUriFormat),
1504 (e = g_uri_escape_string(t, NULL, FALSE))
1505 );
1506
1507 if (r == 0 || r == WRDE_NOSPACE) /* free on success (r == 0) and NOSPACE */
1508 wordfree(&x);
1509 g_free(e);
1510 g_free(l);
1511 return u; /* return a pointer that the caller is responsible for freeing */
1512}
1513
1514char *
1515resolve_uri_words(int c, char **w)
1516{
1517 gchar *d = NULL, /* uri decoded string */
1518 *p = NULL, /* path to return */
1519 *s = NULL; /* concatenated string */
1520 size_t l, /* arbitrary length holder */
1521 wl[c]; /* word length */
1522 int i;
1523
1524 for (i = 0, l = 0; i < c; l += (wl[i] = strlen(w[i])) + 1, i++);
1525
1526 if (!(s = (char *)malloc(l * sizeof(char))))
1527 die(__NAME__ ": fatal: malloc failed\n");
1528
1529 /* concatenate, or join, words into a space separated string */
1530 for (i = 0, l = 0; i < c; l += wl[i], i++) {
1531 memcpy(s + l, w[i], wl[i]);
1532 memcpy(s + l++ + wl[i], i == c - 1 ? "\0" : " ", 1);
1533 }
1534
1535 p = realpath((d = g_uri_unescape_string(s, NULL)), NULL);
1536
1537 g_free(d);
1538 g_free(s);
1539 return p; /* return a pointer that the caller is responsible for freeing */
1540}
1541
1542void
1543update_download_button(WebKitDownload *d,
1544 GtkButton *btn,
1545 gboolean done)
1546{
1547 gdouble p, /* percent completed */
1548 l; /* response length (bytes) */
1549 gchar *t = NULL, /* text holder*/
1550 *f = NULL, /* file name */
1551 *b = NULL; /* base name */
1552
1553 if ((f = g_filename_from_uri(webkit_download_get_destination(d), NULL, NULL)
1554 ) == NULL)
1555 return; /* should never happen, but just in case... */
1556
1557 p = webkit_download_get_estimated_progress(d);
1558 p = (p > 1 ? 1 : p < 0 ? 0 : p) * 100;
1559 l = webkit_uri_response_get_content_length(webkit_download_get_response(d));
1560 b = g_path_get_basename(f);
1561
1562 gtk_button_set_label(btn, !done /* set label based on progress */
1563 ? (t = g_strdup_printf(" %s (%.0f%% of %.1f MB)", b, p, l / 1e6))
1564 : (t = g_strdup_printf(" %s", b))
1565 );
1566
1567 g_free(t);
1568 g_free(f);
1569 g_free(b);
1570}
1571
1572void
1573update_favicon(struct Client *c)
1574{
1575 cairo_surface_t *f; /* favicon */
1576 GdkPixbuf *b, /* pix buffer */
1577 *s; /* pix buffer (scaled) */
1578
1579 /* set fallback favicon */
1580 gtk_image_set_from_icon_name(
1581 GTK_IMAGE(c->tab_icon), ICON_GLOBE, GTK_ICON_SIZE_SMALL_TOOLBAR
1582 );
1583
1584 if ((f = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(c->wv))) == NULL)
1585 return;
1586
1587 if ((b = gdk_pixbuf_get_from_surface(f, 0, 0,
1588 cairo_image_surface_get_width(f),
1589 cairo_image_surface_get_height(f))) == NULL)
1590 return;
1591
1592 s = gdk_pixbuf_scale_simple(b,
1593 16 * gtk_widget_get_scale_factor(c->tab_icon),
1594 16 * gtk_widget_get_scale_factor(c->tab_icon),
1595 GDK_INTERP_BILINEAR
1596 );
1597 gtk_image_set_from_pixbuf(GTK_IMAGE(c->tab_icon), s);
1598
1599 g_object_unref(s);
1600 g_object_unref(b);
1601}
1602
1603void
1604update_load_progress(struct Client *c)
1605{
1606 gdouble p;
1607
1608 p = webkit_web_view_get_estimated_load_progress(WEBKIT_WEB_VIEW(c->wv));
1609 gtk_entry_set_progress_fraction(GTK_ENTRY(c->entry), (p == 1 ? 0 : p));
1610}
1611
1612void
1613update_title(struct Client *c)
1614{
1615 const gchar *t;
1616 gchar *m = NULL,
1617 *u;
1618
1619 u = get_uri(c->wv);
1620 t = webkit_web_view_get_title(WEBKIT_WEB_VIEW(c->wv));
1621
1622 /* title priority: title, url, __NAME__ */
1623 t = t == NULL || t[0] == 0 ? (u == NULL || u[0] == 0 ? __NAME__ : u) : t;
1624
1625 /* set label markup and certificate icon based on tls status */
1626 if (c->failed_crt
1627 || (c->https && c->tls_error != G_TLS_CERTIFICATE_NO_FLAGS)) {
1628 m = g_markup_printf_escaped(CFG_S(BadTlsTabFormat), t);
1629 gtk_entry_set_icon_from_icon_name(
1630 GTK_ENTRY(c->entry), GTK_ENTRY_ICON_PRIMARY, ICON_BAD_TLS
1631 );
1632 } else {
1633 m = g_markup_printf_escaped(CFG_S(NormalTabFormat), t);
1634 if (c->https)
1635 gtk_entry_set_icon_from_icon_name(
1636 GTK_ENTRY(c->entry), GTK_ENTRY_ICON_PRIMARY, ICON_TLS
1637 );
1638 else
1639 gtk_entry_set_icon_from_icon_name(
1640 GTK_ENTRY(c->entry), GTK_ENTRY_ICON_PRIMARY, NULL
1641 );
1642 }
1643
1644 gtk_label_set_markup(GTK_LABEL(c->tab_label), m);
1645 gtk_widget_set_tooltip_text(c->tab_label, t);
1646 set_window_title(gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.nb)));
1647 g_free(m);
1648 g_free(u);
1649}
1650
1651void
1652uri_changed(struct Client *c)
1653{
1654 gchar *t = NULL;
1655
1656 /* make sure to not overwrite the "WEB PROCESS CRASHED" message */
1657 if ((t = get_uri(c->wv)) != NULL && strlen(t) > 0) {
1658 gtk_entry_set_text(GTK_ENTRY(c->entry), t);
1659 save_history(t);
1660 save_state();
1661 }
1662
1663 g_free(t);
1664}
1665
1666void
1667web_view_crashed(struct Client *c)
1668{
1669 gchar *t = NULL,
1670 *u = NULL;
1671
1672 gtk_entry_set_text(
1673 GTK_ENTRY(c->entry),
1674 (t = g_strdup_printf("WEB PROCESS CRASHED: %s", (u = get_uri(c->wv))))
1675 );
1676 g_free(t);
1677 g_free(u);
1678}
1679
1680void
1681xdg_open(const gchar *s,
1682 const gchar *t)
1683{
1684 char b[URI_MAX]; /* command line buffer */
1685
1686 /* make sure to send the scheme the way it was matched */
1687 sprintf(b, "%s %s%s &", XDG_OPEN, s, t+strlen(s));
1688 system(b);
1689}
1690
1691int
1692main(int argc, char **argv)
1693{
1694 int opt,
1695 i;
1696
1697 gtk_init(&argc, &argv);
1698
1699 /* load default configuration before reading command-line arguments */
1700 load_configuration();
1701
1702 while ((opt = getopt(argc, argv, "I")) != -1)
1703 switch (opt) {
1704 case 'I':
1705 gl.ipc = IPC_NONE;
1706 break;
1707 default:
1708 die("Usage: " __NAME__ " [-I [-]] [URI ...] [FILE ...]\n");
1709 }
1710
1711 ipc_setup();
1712 set_default_web_context();
1713 main_window_setup();
1714 load_state();
1715
1716 /* load a default home uri if no clients and no arguments exist */
1717 if (optind >= argc && gl.clients == 0)
1718 client_create(CFG_S(HomeUri), NULL, TRUE, TRUE);
1719 /* load stdin if first argument is '-' */
1720 if (optind < argc && !strcmp(argv[optind], "-"))
1721 load_stdin();
1722 /* load remaining command line arguments as uris into new clients */
1723 else if (optind < argc)
1724 for (i = optind; i < argc; i++)
1725 client_create(argv[i], NULL, TRUE, TRUE);
1726
1727 /* don't load gtk for ipc clients, they have done their bit, and make sure
1728 * there are at least one client to view */
1729 if (gl.ipc != IPC_CLIENT && gl.clients > 0)
1730 gtk_main();
1731
1732 exit(EXIT_SUCCESS);
1733}