Add config option for microphone and webcam access
[surf.git] / surf.c
1 /* See LICENSE file for copyright and license details.
2  *
3  * To understand surf, start reading main().
4  */
5 #include <sys/file.h>
6 #include <sys/types.h>
7 #include <sys/wait.h>
8 #include <glib.h>
9 #include <libgen.h>
10 #include <limits.h>
11 #include <pwd.h>
12 #include <regex.h>
13 #include <signal.h>
14 #include <stdarg.h>
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <unistd.h>
19
20 #include <gdk/gdk.h>
21 #include <gdk/gdkkeysyms.h>
22 #include <gdk/gdkx.h>
23 #include <glib/gstdio.h>
24 #include <gtk/gtk.h>
25 #include <gtk/gtkx.h>
26 #include <gcr/gcr.h>
27 #include <JavaScriptCore/JavaScript.h>
28 #include <webkit2/webkit2.h>
29 #include <X11/X.h>
30 #include <X11/Xatom.h>
31
32 #include "arg.h"
33
34 #define LENGTH(x)               (sizeof(x) / sizeof(x[0]))
35 #define CLEANMASK(mask)         (mask & (MODKEY|GDK_SHIFT_MASK))
36 #define SETB(p, s)              [p] = { { .b = s }, }
37 #define SETI(p, s)              [p] = { { .i = s }, }
38 #define SETV(p, s)              [p] = { { .v = s }, }
39 #define SETF(p, s)              [p] = { { .f = s }, }
40 #define FSETB(p, s)             [p] = { { .b = s }, 1 }
41 #define FSETI(p, s)             [p] = { { .i = s }, 1 }
42 #define FSETV(p, s)             [p] = { { .v = s }, 1 }
43 #define FSETF(p, s)             [p] = { { .f = s }, 1 }
44 #define CSETB(p, s)             [p] = (Parameter){ { .b = s }, 1 }
45 #define CSETI(p, s)             [p] = (Parameter){ { .i = s }, 1 }
46 #define CSETV(p, s)             [p] = (Parameter){ { .v = s }, 1 }
47 #define CSETF(p, s)             [p] = (Parameter){ { .f = s }, 1 }
48
49 enum { AtomFind, AtomGo, AtomUri, AtomLast };
50
51 enum {
52         OnDoc   = WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT,
53         OnLink  = WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK,
54         OnImg   = WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE,
55         OnMedia = WEBKIT_HIT_TEST_RESULT_CONTEXT_MEDIA,
56         OnEdit  = WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE,
57         OnBar   = WEBKIT_HIT_TEST_RESULT_CONTEXT_SCROLLBAR,
58         OnSel   = WEBKIT_HIT_TEST_RESULT_CONTEXT_SELECTION,
59         OnAny   = OnDoc | OnLink | OnImg | OnMedia | OnEdit | OnBar | OnSel,
60 };
61
62 typedef enum {
63         AcceleratedCanvas,
64         AccessMicrophone,
65         AccessWebcam,
66         CaretBrowsing,
67         Certificate,
68         CookiePolicies,
69         DiskCache,
70         DefaultCharset,
71         DNSPrefetch,
72         FontSize,
73         FrameFlattening,
74         Geolocation,
75         HideBackground,
76         Inspector,
77         Java,
78         JavaScript,
79         KioskMode,
80         LoadImages,
81         MediaManualPlay,
82         Plugins,
83         PreferredLanguages,
84         RunInFullscreen,
85         ScrollBars,
86         ShowIndicators,
87         SiteQuirks,
88         SmoothScrolling,
89         SpellChecking,
90         SpellLanguages,
91         StrictTLS,
92         Style,
93         ZoomLevel,
94         ParameterLast,
95 } ParamName;
96
97 typedef union {
98         int b;
99         int i;
100         float f;
101         const void *v;
102 } Arg;
103
104 typedef struct {
105         Arg val;
106         int force;
107 } Parameter;
108
109 typedef struct Client {
110         GtkWidget *win;
111         WebKitWebView *view;
112         WebKitWebInspector *inspector;
113         WebKitFindController *finder;
114         WebKitHitTestResult *mousepos;
115         GTlsCertificate *cert, *failedcert;
116         GTlsCertificateFlags tlserr;
117         Window xid;
118         int progress, fullscreen, https, insecure, errorpage;
119         const char *title, *overtitle, *targeturi;
120         const char *needle;
121         struct Client *next;
122 } Client;
123
124 typedef struct {
125         guint mod;
126         guint keyval;
127         void (*func)(Client *c, const Arg *a);
128         const Arg arg;
129 } Key;
130
131 typedef struct {
132         unsigned int target;
133         unsigned int mask;
134         guint button;
135         void (*func)(Client *c, const Arg *a, WebKitHitTestResult *h);
136         const Arg arg;
137         unsigned int stopevent;
138 } Button;
139
140 typedef struct {
141         const char *uri;
142         Parameter config[ParameterLast];
143         regex_t re;
144 } UriParameters;
145
146 typedef struct {
147         char *regex;
148         char *file;
149         regex_t re;
150 } SiteSpecific;
151
152 /* Surf */
153 static void usage(void);
154 static void die(const char *errstr, ...);
155 static void setup(void);
156 static void sigchld(int unused);
157 static void sighup(int unused);
158 static char *buildfile(const char *path);
159 static char *buildpath(const char *path);
160 static const char *getuserhomedir(const char *user);
161 static const char *getcurrentuserhomedir(void);
162 static Client *newclient(Client *c);
163 static void loaduri(Client *c, const Arg *a);
164 static const char *geturi(Client *c);
165 static void setatom(Client *c, int a, const char *v);
166 static const char *getatom(Client *c, int a);
167 static void updatetitle(Client *c);
168 static void gettogglestats(Client *c);
169 static void getpagestats(Client *c);
170 static WebKitCookieAcceptPolicy cookiepolicy_get(void);
171 static char cookiepolicy_set(const WebKitCookieAcceptPolicy p);
172 static void seturiparameters(Client *c, const char *uri);
173 static void setparameter(Client *c, int refresh, ParamName p, const Arg *a);
174 static const char *getcert(const char *uri);
175 static void setcert(Client *c, const char *file);
176 static const char *getstyle(const char *uri);
177 static void setstyle(Client *c, const char *file);
178 static void runscript(Client *c);
179 static void evalscript(Client *c, const char *jsstr, ...);
180 static void updatewinid(Client *c);
181 static void handleplumb(Client *c, const char *uri);
182 static void newwindow(Client *c, const Arg *a, int noembed);
183 static void spawn(Client *c, const Arg *a);
184 static void destroyclient(Client *c);
185 static void cleanup(void);
186
187 /* GTK/WebKit */
188 static WebKitWebView *newview(Client *c, WebKitWebView *rv);
189 static void initwebextensions(WebKitWebContext *wc, Client *c);
190 static GtkWidget *createview(WebKitWebView *v, WebKitNavigationAction *a,
191                              Client *c);
192 static gboolean buttonreleased(GtkWidget *w, GdkEvent *e, Client *c);
193 static GdkFilterReturn processx(GdkXEvent *xevent, GdkEvent *event,
194                                 gpointer d);
195 static gboolean winevent(GtkWidget *w, GdkEvent *e, Client *c);
196 static void showview(WebKitWebView *v, Client *c);
197 static GtkWidget *createwindow(Client *c);
198 static gboolean loadfailedtls(WebKitWebView *v, gchar *uri,
199                               GTlsCertificate *cert,
200                               GTlsCertificateFlags err, Client *c);
201 static void loadchanged(WebKitWebView *v, WebKitLoadEvent e, Client *c);
202 static void progresschanged(WebKitWebView *v, GParamSpec *ps, Client *c);
203 static void titlechanged(WebKitWebView *view, GParamSpec *ps, Client *c);
204 static void mousetargetchanged(WebKitWebView *v, WebKitHitTestResult *h,
205                                guint modifiers, Client *c);
206 static gboolean permissionrequested(WebKitWebView *v,
207                                     WebKitPermissionRequest *r, Client *c);
208 static gboolean decidepolicy(WebKitWebView *v, WebKitPolicyDecision *d,
209                              WebKitPolicyDecisionType dt, Client *c);
210 static void decidenavigation(WebKitPolicyDecision *d, Client *c);
211 static void decidenewwindow(WebKitPolicyDecision *d, Client *c);
212 static void decideresource(WebKitPolicyDecision *d, Client *c);
213 static void insecurecontent(WebKitWebView *v, WebKitInsecureContentEvent e,
214                             Client *c);
215 static void downloadstarted(WebKitWebContext *wc, WebKitDownload *d,
216                             Client *c);
217 static void responsereceived(WebKitDownload *d, GParamSpec *ps, Client *c);
218 static void download(Client *c, WebKitURIResponse *r);
219 static void closeview(WebKitWebView *v, Client *c);
220 static void destroywin(GtkWidget* w, Client *c);
221
222 /* Hotkeys */
223 static void pasteuri(GtkClipboard *clipboard, const char *text, gpointer d);
224 static void reload(Client *c, const Arg *a);
225 static void print(Client *c, const Arg *a);
226 static void showcert(Client *c, const Arg *a);
227 static void clipboard(Client *c, const Arg *a);
228 static void zoom(Client *c, const Arg *a);
229 static void scroll(Client *c, const Arg *a);
230 static void navigate(Client *c, const Arg *a);
231 static void stop(Client *c, const Arg *a);
232 static void toggle(Client *c, const Arg *a);
233 static void togglefullscreen(Client *c, const Arg *a);
234 static void togglecookiepolicy(Client *c, const Arg *a);
235 static void toggleinspector(Client *c, const Arg *a);
236 static void find(Client *c, const Arg *a);
237
238 /* Buttons */
239 static void clicknavigate(Client *c, const Arg *a, WebKitHitTestResult *h);
240 static void clicknewwindow(Client *c, const Arg *a, WebKitHitTestResult *h);
241 static void clickexternplayer(Client *c, const Arg *a, WebKitHitTestResult *h);
242
243 static char winid[64];
244 static char togglestats[12];
245 static char pagestats[2];
246 static Atom atoms[AtomLast];
247 static Window embed;
248 static int showxid;
249 static int cookiepolicy;
250 static Display *dpy;
251 static Client *clients;
252 static GdkDevice *gdkkb;
253 static char *stylefile;
254 static const char *useragent;
255 static Parameter *curconfig;
256 char *argv0;
257
258 /* configuration, allows nested code to access above variables */
259 #include "config.h"
260
261 void
262 usage(void)
263 {
264         die("usage: surf [-bBdDfFgGiIkKmMnNpPsStTvwxX]\n"
265             "[-a cookiepolicies ] [-c cookiefile] [-C stylefile] [-e xid]\n"
266             "[-r scriptfile] [-u useragent] [-z zoomlevel] [uri]\n");
267 }
268
269 void
270 die(const char *errstr, ...)
271 {
272         va_list ap;
273
274         va_start(ap, errstr);
275         vfprintf(stderr, errstr, ap);
276         va_end(ap);
277         exit(1);
278 }
279
280 void
281 setup(void)
282 {
283         GdkDisplay *gdpy;
284         int i, j;
285
286         /* clean up any zombies immediately */
287         sigchld(0);
288         if (signal(SIGHUP, sighup) == SIG_ERR)
289                 die("Can't install SIGHUP handler");
290
291         if (!(dpy = XOpenDisplay(NULL)))
292                 die("Can't open default display");
293
294         /* atoms */
295         atoms[AtomFind] = XInternAtom(dpy, "_SURF_FIND", False);
296         atoms[AtomGo] = XInternAtom(dpy, "_SURF_GO", False);
297         atoms[AtomUri] = XInternAtom(dpy, "_SURF_URI", False);
298
299         gtk_init(NULL, NULL);
300
301         gdpy = gdk_display_get_default();
302
303         curconfig = defconfig;
304
305         /* dirs and files */
306         cookiefile = buildfile(cookiefile);
307         scriptfile = buildfile(scriptfile);
308         cachedir   = buildpath(cachedir);
309         certdir    = buildpath(certdir);
310
311         gdkkb = gdk_seat_get_keyboard(gdk_display_get_default_seat(gdpy));
312
313         for (i = 0; i < LENGTH(certs); ++i) {
314                 if (!regcomp(&(certs[i].re), certs[i].regex, REG_EXTENDED)) {
315                         certs[i].file = g_strconcat(certdir, "/", certs[i].file,
316                                                     NULL);
317                 } else {
318                         fprintf(stderr, "Could not compile regex: %s\n",
319                                 certs[i].regex);
320                         certs[i].regex = NULL;
321                 }
322         }
323
324         if (!stylefile) {
325                 styledir = buildpath(styledir);
326                 for (i = 0; i < LENGTH(styles); ++i) {
327                         if (!regcomp(&(styles[i].re), styles[i].regex,
328                             REG_EXTENDED)) {
329                                 styles[i].file = g_strconcat(styledir, "/",
330                                                     styles[i].file, NULL);
331                         } else {
332                                 fprintf(stderr, "Could not compile regex: %s\n",
333                                         styles[i].regex);
334                                 styles[i].regex = NULL;
335                         }
336                 }
337                 g_free(styledir);
338         } else {
339                 stylefile = buildfile(stylefile);
340         }
341
342         for (i = 0; i < LENGTH(uriparams); ++i) {
343                 if (!regcomp(&(uriparams[i].re), uriparams[i].uri,
344                     REG_EXTENDED)) {
345                         /* copy default parameters if they are not already set
346                          * or if they are forced */
347                         for (j = 0; j < ParameterLast; ++j) {
348                                 if (!uriparams[i].config[j].force ||
349                                     defconfig[j].force)
350                                         uriparams[i].config[j] = defconfig[j];
351                         }
352                 } else {
353                         fprintf(stderr, "Could not compile regex: %s\n",
354                                 uriparams[i].uri);
355                         uriparams[i].uri = NULL;
356                 }
357         }
358 }
359
360 void
361 sigchld(int unused)
362 {
363         if (signal(SIGCHLD, sigchld) == SIG_ERR)
364                 die("Can't install SIGCHLD handler");
365         while (waitpid(-1, NULL, WNOHANG) > 0)
366                 ;
367 }
368
369 void
370 sighup(int unused)
371 {
372         Arg a = { .b = 0 };
373         Client *c;
374
375         for (c = clients; c; c = c->next)
376                 reload(c, &a);
377 }
378
379 char *
380 buildfile(const char *path)
381 {
382         char *dname, *bname, *bpath, *fpath;
383         FILE *f;
384
385         dname = g_path_get_dirname(path);
386         bname = g_path_get_basename(path);
387
388         bpath = buildpath(dname);
389         g_free(dname);
390
391         fpath = g_build_filename(bpath, bname, NULL);
392         g_free(bpath);
393         g_free(bname);
394
395         if (!(f = fopen(fpath, "a")))
396                 die("Could not open file: %s\n", fpath);
397
398         g_chmod(fpath, 0600); /* always */
399         fclose(f);
400
401         return fpath;
402 }
403
404 static const char*
405 getuserhomedir(const char *user)
406 {
407         struct passwd *pw = getpwnam(user);
408
409         if (!pw)
410                 die("Can't get user %s login information.\n", user);
411
412         return pw->pw_dir;
413 }
414
415 static const char*
416 getcurrentuserhomedir(void)
417 {
418         const char *homedir;
419         const char *user;
420         struct passwd *pw;
421
422         homedir = getenv("HOME");
423         if (homedir)
424                 return homedir;
425
426         user = getenv("USER");
427         if (user)
428                 return getuserhomedir(user);
429
430         pw = getpwuid(getuid());
431         if (!pw)
432                 die("Can't get current user home directory\n");
433
434         return pw->pw_dir;
435 }
436
437 char *
438 buildpath(const char *path)
439 {
440         char *apath, *name, *p, *fpath;
441         const char *homedir;
442
443         if (path[0] == '~') {
444                 if (path[1] == '/' || path[1] == '\0') {
445                         p = (char *)&path[1];
446                         homedir = getcurrentuserhomedir();
447                 } else {
448                         if ((p = strchr(path, '/')))
449                                 name = g_strndup(&path[1], --p - path);
450                         else
451                                 name = g_strdup(&path[1]);
452
453                         homedir = getuserhomedir(name);
454                         g_free(name);
455                 }
456                 apath = g_build_filename(homedir, p, NULL);
457         } else {
458                 apath = g_strdup(path);
459         }
460
461         /* creating directory */
462         if (g_mkdir_with_parents(apath, 0700) < 0)
463                 die("Could not access directory: %s\n", apath);
464
465         fpath = realpath(apath, NULL);
466         g_free(apath);
467
468         return fpath;
469 }
470
471 Client *
472 newclient(Client *rc)
473 {
474         Client *c;
475
476         if (!(c = calloc(1, sizeof(Client))))
477                 die("Cannot malloc!\n");
478
479         c->next = clients;
480         clients = c;
481
482         c->progress = 100;
483         c->view = newview(c, rc ? rc->view : NULL);
484
485         return c;
486 }
487
488 void
489 loaduri(Client *c, const Arg *a)
490 {
491         struct stat st;
492         char *url, *path;
493         const char *uri = a->v;
494
495         if (g_strcmp0(uri, "") == 0)
496                 return;
497
498         if (g_str_has_prefix(uri, "http://")  ||
499             g_str_has_prefix(uri, "https://") ||
500             g_str_has_prefix(uri, "file://")  ||
501             g_str_has_prefix(uri, "about:")) {
502                 url = g_strdup(uri);
503         } else if (!stat(uri, &st) && (path = realpath(uri, NULL))) {
504                 url = g_strdup_printf("file://%s", path);
505                 free(path);
506         } else {
507                 url = g_strdup_printf("http://%s", uri);
508         }
509
510         setatom(c, AtomUri, url);
511
512         if (strcmp(url, geturi(c)) == 0) {
513                 reload(c, a);
514         } else {
515                 webkit_web_view_load_uri(c->view, url);
516                 updatetitle(c);
517         }
518
519         g_free(url);
520 }
521
522 const char *
523 geturi(Client *c)
524 {
525         const char *uri;
526
527         if (!(uri = webkit_web_view_get_uri(c->view)))
528                 uri = "about:blank";
529         return uri;
530 }
531
532 void
533 setatom(Client *c, int a, const char *v)
534 {
535         XChangeProperty(dpy, c->xid,
536                         atoms[a], XA_STRING, 8, PropModeReplace,
537                         (unsigned char *)v, strlen(v) + 1);
538         XSync(dpy, False);
539 }
540
541 const char *
542 getatom(Client *c, int a)
543 {
544         static char buf[BUFSIZ];
545         Atom adummy;
546         int idummy;
547         unsigned long ldummy;
548         unsigned char *p = NULL;
549
550         XSync(dpy, False);
551         XGetWindowProperty(dpy, c->xid, atoms[a], 0L, BUFSIZ, False, XA_STRING,
552                            &adummy, &idummy, &ldummy, &ldummy, &p);
553         if (p)
554                 strncpy(buf, (char *)p, LENGTH(buf) - 1);
555         else
556                 buf[0] = '\0';
557         XFree(p);
558
559         return buf;
560 }
561
562 void
563 updatetitle(Client *c)
564 {
565         char *title;
566         const char *name = c->overtitle ? c->overtitle :
567                            c->title ? c->title : "";
568
569         if (curconfig[ShowIndicators].val.b) {
570                 gettogglestats(c);
571                 getpagestats(c);
572
573                 if (c->progress != 100)
574                         title = g_strdup_printf("[%i%%] %s:%s | %s",
575                                 c->progress, togglestats, pagestats, name);
576                 else
577                         title = g_strdup_printf("%s:%s | %s",
578                                 togglestats, pagestats, name);
579
580                 gtk_window_set_title(GTK_WINDOW(c->win), title);
581                 g_free(title);
582         } else {
583                 gtk_window_set_title(GTK_WINDOW(c->win), name);
584         }
585 }
586
587 void
588 gettogglestats(Client *c)
589 {
590         togglestats[0] = cookiepolicy_set(cookiepolicy_get());
591         togglestats[1] = curconfig[CaretBrowsing].val.b ?   'C' : 'c';
592         togglestats[2] = curconfig[Geolocation].val.b ?     'G' : 'g';
593         togglestats[3] = curconfig[DiskCache].val.b ?       'D' : 'd';
594         togglestats[4] = curconfig[LoadImages].val.b ?      'I' : 'i';
595         togglestats[5] = curconfig[JavaScript].val.b ?      'S' : 's';
596         togglestats[6] = curconfig[Plugins].val.b ?         'V' : 'v';
597         togglestats[7] = curconfig[Style].val.b ?           'M' : 'm';
598         togglestats[8] = curconfig[FrameFlattening].val.b ? 'F' : 'f';
599         togglestats[9] = curconfig[Certificate].val.b ?     'X' : 'x';
600         togglestats[10] = curconfig[StrictTLS].val.b ?      'T' : 't';
601         togglestats[11] = '\0';
602 }
603
604 void
605 getpagestats(Client *c)
606 {
607         if (c->https)
608                 pagestats[0] = (c->tlserr || c->insecure) ?  'U' : 'T';
609         else
610                 pagestats[0] = '-';
611         pagestats[1] = '\0';
612 }
613
614 WebKitCookieAcceptPolicy
615 cookiepolicy_get(void)
616 {
617         switch (((char *)curconfig[CookiePolicies].val.v)[cookiepolicy]) {
618         case 'a':
619                 return WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
620         case '@':
621                 return WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
622         default: /* fallthrough */
623         case 'A':
624                 return WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
625         }
626 }
627
628 char
629 cookiepolicy_set(const WebKitCookieAcceptPolicy p)
630 {
631         switch (p) {
632         case WEBKIT_COOKIE_POLICY_ACCEPT_NEVER:
633                 return 'a';
634         case WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY:
635                 return '@';
636         default: /* fallthrough */
637         case WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS:
638                 return 'A';
639         }
640 }
641
642 void
643 seturiparameters(Client *c, const char *uri)
644 {
645         Parameter *newconfig = NULL;
646         int i;
647
648         for (i = 0; i < LENGTH(uriparams); ++i) {
649                 if (uriparams[i].uri &&
650                     !regexec(&(uriparams[i].re), uri, 0, NULL, 0)) {
651                         newconfig = uriparams[i].config;
652                         break;
653                 }
654         }
655
656         if (!newconfig)
657                 newconfig = defconfig;
658         if (newconfig == curconfig)
659                 return;
660
661         for (i = 0; i < ParameterLast; ++i) {
662                 if (defconfig[i].force)
663                         continue;
664                 if (newconfig[i].force)
665                         setparameter(c, 0, i, &newconfig[i].val);
666                 else if (curconfig[i].force)
667                         setparameter(c, 0, i, &defconfig[i].val);
668         }
669
670         curconfig = newconfig;
671 }
672
673 void
674 setparameter(Client *c, int refresh, ParamName p, const Arg *a)
675 {
676         GdkRGBA bgcolor = { 0 };
677         WebKitSettings *s = webkit_web_view_get_settings(c->view);
678
679         switch (p) {
680         case AcceleratedCanvas:
681                 webkit_settings_set_enable_accelerated_2d_canvas(s, a->b);
682                 break;
683         case AccessMicrophone:
684                 return; /* do nothing */
685         case AccessWebcam:
686                 return; /* do nothing */
687         case CaretBrowsing:
688                 webkit_settings_set_enable_caret_browsing(s, a->b);
689                 refresh = 0;
690                 break;
691         case Certificate:
692                 if (a->b)
693                         setcert(c, geturi(c));
694                 return; /* do not update */
695         case CookiePolicies:
696                 webkit_cookie_manager_set_accept_policy(
697                     webkit_web_context_get_cookie_manager(
698                     webkit_web_view_get_context(c->view)),
699                     cookiepolicy_get());
700                 refresh = 0;
701                 break;
702         case DiskCache:
703                 webkit_web_context_set_cache_model(
704                     webkit_web_view_get_context(c->view), a->b ?
705                     WEBKIT_CACHE_MODEL_WEB_BROWSER :
706                     WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER);
707                 return; /* do not update */
708         case DefaultCharset:
709                 webkit_settings_set_default_charset(s, a->v);
710                 return; /* do not update */
711         case DNSPrefetch:
712                 webkit_settings_set_enable_dns_prefetching(s, a->b);
713                 return; /* do not update */
714         case FontSize:
715                 webkit_settings_set_default_font_size(s, a->i);
716                 return; /* do not update */
717         case FrameFlattening:
718                 webkit_settings_set_enable_frame_flattening(s, a->b);
719                 break;
720         case Geolocation:
721                 refresh = 0;
722                 break;
723         case HideBackground:
724                 if (a->b)
725                         webkit_web_view_set_background_color(c->view, &bgcolor);
726                 return; /* do not update */
727         case Inspector:
728                 webkit_settings_set_enable_developer_extras(s, a->b);
729                 return; /* do not update */
730         case Java:
731                 webkit_settings_set_enable_java(s, a->b);
732                 return; /* do not update */
733         case JavaScript:
734                 webkit_settings_set_enable_javascript(s, a->b);
735                 break;
736         case KioskMode:
737                 return; /* do nothing */
738         case LoadImages:
739                 webkit_settings_set_auto_load_images(s, a->b);
740                 break;
741         case MediaManualPlay:
742                 webkit_settings_set_media_playback_requires_user_gesture(s, a->b);
743                 break;
744         case Plugins:
745                 webkit_settings_set_enable_plugins(s, a->b);
746                 break;
747         case PreferredLanguages:
748                 return; /* do nothing */
749         case RunInFullscreen:
750                 return; /* do nothing */
751         case ScrollBars:
752                 /* Disabled until we write some WebKitWebExtension for
753                  * manipulating the DOM directly.
754                 enablescrollbars = !enablescrollbars;
755                 evalscript(c, "document.documentElement.style.overflow = '%s'",
756                     enablescrollbars ? "auto" : "hidden");
757                 */
758                 return; /* do not update */
759         case ShowIndicators:
760                 break;
761         case SmoothScrolling:
762                 webkit_settings_set_enable_smooth_scrolling(s, a->b);
763                 return; /* do not update */
764         case SiteQuirks:
765                 webkit_settings_set_enable_site_specific_quirks(s, a->b);
766                 break;
767         case SpellChecking:
768                 webkit_web_context_set_spell_checking_enabled(
769                     webkit_web_view_get_context(c->view), a->b);
770                 return; /* do not update */
771         case SpellLanguages:
772                 return; /* do nothing */
773         case StrictTLS:
774                 webkit_web_context_set_tls_errors_policy(
775                     webkit_web_view_get_context(c->view), a->b ?
776                     WEBKIT_TLS_ERRORS_POLICY_FAIL :
777                     WEBKIT_TLS_ERRORS_POLICY_IGNORE);
778                 break;
779         case Style:
780                 if (a->b)
781                         setstyle(c, getstyle(geturi(c)));
782                 else
783                         webkit_user_content_manager_remove_all_style_sheets(
784                             webkit_web_view_get_user_content_manager(c->view));
785                 refresh = 0;
786                 break;
787         case ZoomLevel:
788                 webkit_web_view_set_zoom_level(c->view, a->f);
789                 return; /* do not update */
790         default:
791                 return; /* do nothing */
792         }
793
794         updatetitle(c);
795         if (refresh)
796                 reload(c, a);
797 }
798
799 const char *
800 getcert(const char *uri)
801 {
802         int i;
803
804         for (i = 0; i < LENGTH(certs); ++i) {
805                 if (certs[i].regex &&
806                     !regexec(&(certs[i].re), uri, 0, NULL, 0))
807                         return certs[i].file;
808         }
809
810         return NULL;
811 }
812
813 void
814 setcert(Client *c, const char *uri)
815 {
816         const char *file = getcert(uri);
817         char *host;
818         GTlsCertificate *cert;
819
820         if (!file)
821                 return;
822
823         if (!(cert = g_tls_certificate_new_from_file(file, NULL))) {
824                 fprintf(stderr, "Could not read certificate file: %s\n", file);
825                 return;
826         }
827
828         if ((uri = strstr(uri, "https://"))) {
829                 uri += sizeof("https://") - 1;
830                 host = g_strndup(uri, strchr(uri, '/') - uri);
831                 webkit_web_context_allow_tls_certificate_for_host(
832                     webkit_web_view_get_context(c->view), cert, host);
833                 g_free(host);
834         }
835
836         g_object_unref(cert);
837
838 }
839
840 const char *
841 getstyle(const char *uri)
842 {
843         int i;
844
845         if (stylefile)
846                 return stylefile;
847
848         for (i = 0; i < LENGTH(styles); ++i) {
849                 if (styles[i].regex &&
850                     !regexec(&(styles[i].re), uri, 0, NULL, 0))
851                         return styles[i].file;
852         }
853
854         return "";
855 }
856
857 void
858 setstyle(Client *c, const char *file)
859 {
860         gchar *style;
861
862         if (!g_file_get_contents(file, &style, NULL, NULL)) {
863                 fprintf(stderr, "Could not read style file: %s\n", file);
864                 return;
865         }
866
867         webkit_user_content_manager_add_style_sheet(
868             webkit_web_view_get_user_content_manager(c->view),
869             webkit_user_style_sheet_new(style,
870             WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
871             WEBKIT_USER_STYLE_LEVEL_USER,
872             NULL, NULL));
873
874         g_free(style);
875 }
876
877 void
878 runscript(Client *c)
879 {
880         gchar *script;
881         gsize l;
882
883         if (g_file_get_contents(scriptfile, &script, &l, NULL) && l)
884                 evalscript(c, script);
885         g_free(script);
886 }
887
888 void
889 evalscript(Client *c, const char *jsstr, ...)
890 {
891         va_list ap;
892         gchar *script;
893
894         va_start(ap, jsstr);
895         script = g_strdup_vprintf(jsstr, ap);
896         va_end(ap);
897
898         webkit_web_view_run_javascript(c->view, script, NULL, NULL, NULL);
899         g_free(script);
900 }
901
902 void
903 updatewinid(Client *c)
904 {
905         snprintf(winid, LENGTH(winid), "%lu", c->xid);
906 }
907
908 void
909 handleplumb(Client *c, const char *uri)
910 {
911         Arg a = (Arg)PLUMB(uri);
912         spawn(c, &a);
913 }
914
915 void
916 newwindow(Client *c, const Arg *a, int noembed)
917 {
918         int i = 0;
919         char tmp[64];
920         const char *cmd[29], *uri;
921         const Arg arg = { .v = cmd };
922
923         cmd[i++] = argv0;
924         cmd[i++] = "-a";
925         cmd[i++] = curconfig[CookiePolicies].val.v;
926         cmd[i++] = curconfig[ScrollBars].val.b ? "-B" : "-b";
927         if (cookiefile && g_strcmp0(cookiefile, "")) {
928                 cmd[i++] = "-c";
929                 cmd[i++] = cookiefile;
930         }
931         if (stylefile && g_strcmp0(stylefile, "")) {
932                 cmd[i++] = "-C";
933                 cmd[i++] = stylefile;
934         }
935         cmd[i++] = curconfig[DiskCache].val.b ? "-D" : "-d";
936         if (embed && !noembed) {
937                 cmd[i++] = "-e";
938                 snprintf(tmp, LENGTH(tmp), "%lu", embed);
939                 cmd[i++] = tmp;
940         }
941         cmd[i++] = curconfig[RunInFullscreen].val.b ? "-F" : "-f" ;
942         cmd[i++] = curconfig[Geolocation].val.b ?     "-G" : "-g" ;
943         cmd[i++] = curconfig[LoadImages].val.b ?      "-I" : "-i" ;
944         cmd[i++] = curconfig[KioskMode].val.b ?       "-K" : "-k" ;
945         cmd[i++] = curconfig[Style].val.b ?           "-M" : "-m" ;
946         cmd[i++] = curconfig[Inspector].val.b ?       "-N" : "-n" ;
947         cmd[i++] = curconfig[Plugins].val.b ?         "-P" : "-p" ;
948         if (scriptfile && g_strcmp0(scriptfile, "")) {
949                 cmd[i++] = "-r";
950                 cmd[i++] = scriptfile;
951         }
952         cmd[i++] = curconfig[JavaScript].val.b ? "-S" : "-s";
953         cmd[i++] = curconfig[StrictTLS].val.b ? "-T" : "-t";
954         if (fulluseragent && g_strcmp0(fulluseragent, "")) {
955                 cmd[i++] = "-u";
956                 cmd[i++] = fulluseragent;
957         }
958         if (showxid)
959                 cmd[i++] = "-w";
960         cmd[i++] = curconfig[Certificate].val.b ? "-X" : "-x" ;
961         /* do not keep zoom level */
962         cmd[i++] = "--";
963         if ((uri = a->v))
964                 cmd[i++] = uri;
965         cmd[i] = NULL;
966
967         spawn(c, &arg);
968 }
969
970 void
971 spawn(Client *c, const Arg *a)
972 {
973         if (fork() == 0) {
974                 if (dpy)
975                         close(ConnectionNumber(dpy));
976                 setsid();
977                 execvp(((char **)a->v)[0], (char **)a->v);
978                 fprintf(stderr, "%s: execvp %s", argv0, ((char **)a->v)[0]);
979                 perror(" failed");
980                 exit(1);
981         }
982 }
983
984 void
985 destroyclient(Client *c)
986 {
987         Client *p;
988
989         webkit_web_view_stop_loading(c->view);
990         /* Not needed, has already been called
991         gtk_widget_destroy(c->win);
992          */
993
994         for (p = clients; p && p->next != c; p = p->next)
995                 ;
996         if (p)
997                 p->next = c->next;
998         else
999                 clients = c->next;
1000         free(c);
1001 }
1002
1003 void
1004 cleanup(void)
1005 {
1006         while (clients)
1007                 destroyclient(clients);
1008         g_free(cookiefile);
1009         g_free(scriptfile);
1010         g_free(stylefile);
1011         g_free(cachedir);
1012         XCloseDisplay(dpy);
1013 }
1014
1015 WebKitWebView *
1016 newview(Client *c, WebKitWebView *rv)
1017 {
1018         WebKitWebView *v;
1019         WebKitSettings *settings;
1020         WebKitUserContentManager *contentmanager;
1021         WebKitWebContext *context;
1022
1023         /* Webview */
1024         if (rv) {
1025                 v = WEBKIT_WEB_VIEW(
1026                     webkit_web_view_new_with_related_view(rv));
1027         } else {
1028                 settings = webkit_settings_new_with_settings(
1029                    "auto-load-images", curconfig[LoadImages].val.b,
1030                    "default-charset", curconfig[DefaultCharset].val.v,
1031                    "default-font-size", curconfig[FontSize].val.i,
1032                    "enable-caret-browsing", curconfig[CaretBrowsing].val.b,
1033                    "enable-developer-extras", curconfig[Inspector].val.b,
1034                    "enable-dns-prefetching", curconfig[DNSPrefetch].val.b,
1035                    "enable-frame-flattening", curconfig[FrameFlattening].val.b,
1036                    "enable-html5-database", curconfig[DiskCache].val.b,
1037                    "enable-html5-local-storage", curconfig[DiskCache].val.b,
1038                    "enable-java", curconfig[Java].val.b,
1039                    "enable-javascript", curconfig[JavaScript].val.b,
1040                    "enable-plugins", curconfig[Plugins].val.b,
1041                    "enable-accelerated-2d-canvas", curconfig[AcceleratedCanvas].val.b,
1042                    "enable-site-specific-quirks", curconfig[SiteQuirks].val.b,
1043                    "enable-smooth-scrolling", curconfig[SmoothScrolling].val.b,
1044                    "media-playback-requires-user-gesture", curconfig[MediaManualPlay].val.b,
1045                    NULL);
1046 /* For more interesting settings, have a look at
1047  * http://webkitgtk.org/reference/webkit2gtk/stable/WebKitSettings.html */
1048
1049                 if (strcmp(fulluseragent, "")) {
1050                         webkit_settings_set_user_agent(settings, fulluseragent);
1051                 } else if (surfuseragent) {
1052                         webkit_settings_set_user_agent_with_application_details(
1053                             settings, "Surf", VERSION);
1054                 }
1055                 useragent = webkit_settings_get_user_agent(settings);
1056
1057                 contentmanager = webkit_user_content_manager_new();
1058
1059                 context = webkit_web_context_new_with_website_data_manager(
1060                           webkit_website_data_manager_new(
1061                           "base-cache-directory", cachedir,
1062                           "base-data-directory", cachedir,
1063                           NULL));
1064
1065                 /* rendering process model, can be a shared unique one
1066                  * or one for each view */
1067                 webkit_web_context_set_process_model(context,
1068                     WEBKIT_PROCESS_MODEL_MULTIPLE_SECONDARY_PROCESSES);
1069                 /* TLS */
1070                 webkit_web_context_set_tls_errors_policy(context,
1071                     curconfig[StrictTLS].val.b ? WEBKIT_TLS_ERRORS_POLICY_FAIL :
1072                     WEBKIT_TLS_ERRORS_POLICY_IGNORE);
1073                 /* disk cache */
1074                 webkit_web_context_set_cache_model(context,
1075                     curconfig[DiskCache].val.b ? WEBKIT_CACHE_MODEL_WEB_BROWSER :
1076                     WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER);
1077
1078                 /* Currently only works with text file to be compatible with curl */
1079                 webkit_cookie_manager_set_persistent_storage(
1080                     webkit_web_context_get_cookie_manager(context), cookiefile,
1081                     WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT);
1082                 /* cookie policy */
1083                 webkit_cookie_manager_set_accept_policy(
1084                     webkit_web_context_get_cookie_manager(context),
1085                     cookiepolicy_get());
1086                 /* languages */
1087                 webkit_web_context_set_preferred_languages(context,
1088                     curconfig[PreferredLanguages].val.v);
1089                 webkit_web_context_set_spell_checking_languages(context,
1090                     curconfig[SpellLanguages].val.v);
1091                 webkit_web_context_set_spell_checking_enabled(context,
1092                     curconfig[SpellChecking].val.b);
1093
1094                 g_signal_connect(G_OBJECT(context), "download-started",
1095                                  G_CALLBACK(downloadstarted), c);
1096                 g_signal_connect(G_OBJECT(context), "initialize-web-extensions",
1097                                  G_CALLBACK(initwebextensions), c);
1098
1099                 v = g_object_new(WEBKIT_TYPE_WEB_VIEW,
1100                     "settings", settings,
1101                     "user-content-manager", contentmanager,
1102                     "web-context", context,
1103                     NULL);
1104         }
1105
1106         g_signal_connect(G_OBJECT(v), "notify::estimated-load-progress",
1107                          G_CALLBACK(progresschanged), c);
1108         g_signal_connect(G_OBJECT(v), "notify::title",
1109                          G_CALLBACK(titlechanged), c);
1110         g_signal_connect(G_OBJECT(v), "button-release-event",
1111                          G_CALLBACK(buttonreleased), c);
1112         g_signal_connect(G_OBJECT(v), "close",
1113                         G_CALLBACK(closeview), c);
1114         g_signal_connect(G_OBJECT(v), "create",
1115                          G_CALLBACK(createview), c);
1116         g_signal_connect(G_OBJECT(v), "decide-policy",
1117                          G_CALLBACK(decidepolicy), c);
1118         g_signal_connect(G_OBJECT(v), "insecure-content-detected",
1119                          G_CALLBACK(insecurecontent), c);
1120         g_signal_connect(G_OBJECT(v), "load-failed-with-tls-errors",
1121                          G_CALLBACK(loadfailedtls), c);
1122         g_signal_connect(G_OBJECT(v), "load-changed",
1123                          G_CALLBACK(loadchanged), c);
1124         g_signal_connect(G_OBJECT(v), "mouse-target-changed",
1125                          G_CALLBACK(mousetargetchanged), c);
1126         g_signal_connect(G_OBJECT(v), "permission-request",
1127                          G_CALLBACK(permissionrequested), c);
1128         g_signal_connect(G_OBJECT(v), "ready-to-show",
1129                          G_CALLBACK(showview), c);
1130
1131         return v;
1132 }
1133
1134 void
1135 initwebextensions(WebKitWebContext *wc, Client *c)
1136 {
1137         webkit_web_context_set_web_extensions_directory(wc, WEBEXTDIR);
1138 }
1139
1140 GtkWidget *
1141 createview(WebKitWebView *v, WebKitNavigationAction *a, Client *c)
1142 {
1143         Client *n;
1144
1145         switch (webkit_navigation_action_get_navigation_type(a)) {
1146         case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */
1147                 /*
1148                  * popup windows of type “other” are almost always triggered
1149                  * by user gesture, so inverse the logic here
1150                  */
1151 /* instead of this, compare destination uri to mouse-over uri for validating window */
1152                 if (webkit_navigation_action_is_user_gesture(a))
1153                         return NULL;
1154         case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: /* fallthrough */
1155         case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: /* fallthrough */
1156         case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: /* fallthrough */
1157         case WEBKIT_NAVIGATION_TYPE_RELOAD: /* fallthrough */
1158         case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED:
1159                 n = newclient(c);
1160                 break;
1161         default:
1162                 return NULL;
1163         }
1164
1165         return GTK_WIDGET(n->view);
1166 }
1167
1168 gboolean
1169 buttonreleased(GtkWidget *w, GdkEvent *e, Client *c)
1170 {
1171         WebKitHitTestResultContext element;
1172         int i;
1173
1174         element = webkit_hit_test_result_get_context(c->mousepos);
1175
1176         for (i = 0; i < LENGTH(buttons); ++i) {
1177                 if (element & buttons[i].target &&
1178                     e->button.button == buttons[i].button &&
1179                     CLEANMASK(e->button.state) == CLEANMASK(buttons[i].mask) &&
1180                     buttons[i].func) {
1181                         buttons[i].func(c, &buttons[i].arg, c->mousepos);
1182                         return buttons[i].stopevent;
1183                 }
1184         }
1185
1186         return FALSE;
1187 }
1188
1189 GdkFilterReturn
1190 processx(GdkXEvent *e, GdkEvent *event, gpointer d)
1191 {
1192         Client *c = (Client *)d;
1193         XPropertyEvent *ev;
1194         Arg a;
1195
1196         if (((XEvent *)e)->type == PropertyNotify) {
1197                 ev = &((XEvent *)e)->xproperty;
1198                 if (ev->state == PropertyNewValue) {
1199                         if (ev->atom == atoms[AtomFind]) {
1200                                 find(c, NULL);
1201
1202                                 return GDK_FILTER_REMOVE;
1203                         } else if (ev->atom == atoms[AtomGo]) {
1204                                 a.v = getatom(c, AtomGo);
1205                                 loaduri(c, &a);
1206
1207                                 return GDK_FILTER_REMOVE;
1208                         }
1209                 }
1210         }
1211         return GDK_FILTER_CONTINUE;
1212 }
1213
1214 gboolean
1215 winevent(GtkWidget *w, GdkEvent *e, Client *c)
1216 {
1217         int i;
1218
1219         switch (e->type) {
1220         case GDK_ENTER_NOTIFY:
1221                 c->overtitle = c->targeturi;
1222                 updatetitle(c);
1223                 break;
1224         case GDK_KEY_PRESS:
1225                 if (!curconfig[KioskMode].val.b) {
1226                         for (i = 0; i < LENGTH(keys); ++i) {
1227                                 if (gdk_keyval_to_lower(e->key.keyval) ==
1228                                     keys[i].keyval &&
1229                                     CLEANMASK(e->key.state) == keys[i].mod &&
1230                                     keys[i].func) {
1231                                         updatewinid(c);
1232                                         keys[i].func(c, &(keys[i].arg));
1233                                         return TRUE;
1234                                 }
1235                         }
1236                 }
1237         case GDK_LEAVE_NOTIFY:
1238                 c->overtitle = NULL;
1239                 updatetitle(c);
1240                 break;
1241         case GDK_WINDOW_STATE:
1242                 if (e->window_state.changed_mask ==
1243                     GDK_WINDOW_STATE_FULLSCREEN)
1244                         c->fullscreen = e->window_state.new_window_state &
1245                                         GDK_WINDOW_STATE_FULLSCREEN;
1246                 break;
1247         default:
1248                 break;
1249         }
1250
1251         return FALSE;
1252 }
1253
1254 void
1255 showview(WebKitWebView *v, Client *c)
1256 {
1257         GdkRGBA bgcolor = { 0 };
1258         GdkWindow *gwin;
1259
1260         c->finder = webkit_web_view_get_find_controller(c->view);
1261         c->inspector = webkit_web_view_get_inspector(c->view);
1262
1263         c->win = createwindow(c);
1264
1265         gtk_container_add(GTK_CONTAINER(c->win), GTK_WIDGET(c->view));
1266         gtk_widget_show_all(c->win);
1267         gtk_widget_grab_focus(GTK_WIDGET(c->view));
1268
1269         gwin = gtk_widget_get_window(GTK_WIDGET(c->win));
1270         c->xid = gdk_x11_window_get_xid(gwin);
1271         updatewinid(c);
1272         if (showxid) {
1273                 gdk_display_sync(gtk_widget_get_display(c->win));
1274                 puts(winid);
1275         }
1276
1277         if (curconfig[HideBackground].val.b)
1278                 webkit_web_view_set_background_color(c->view, &bgcolor);
1279
1280         if (!curconfig[KioskMode].val.b) {
1281                 gdk_window_set_events(gwin, GDK_ALL_EVENTS_MASK);
1282                 gdk_window_add_filter(gwin, processx, c);
1283         }
1284
1285         if (curconfig[RunInFullscreen].val.b)
1286                 togglefullscreen(c, NULL);
1287
1288         if (curconfig[ZoomLevel].val.f != 1.0)
1289                 webkit_web_view_set_zoom_level(c->view,
1290                                                curconfig[ZoomLevel].val.f);
1291
1292         setatom(c, AtomFind, "");
1293         setatom(c, AtomUri, "about:blank");
1294 }
1295
1296 GtkWidget *
1297 createwindow(Client *c)
1298 {
1299         char *wmstr;
1300         GtkWidget *w;
1301
1302         if (embed) {
1303                 w = gtk_plug_new(embed);
1304         } else {
1305                 w = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1306
1307                 wmstr = g_path_get_basename(argv0);
1308                 gtk_window_set_wmclass(GTK_WINDOW(w), wmstr, "Surf");
1309                 g_free(wmstr);
1310
1311                 wmstr = g_strdup_printf("%s[%lu]", "Surf",
1312                         webkit_web_view_get_page_id(c->view));
1313                 gtk_window_set_role(GTK_WINDOW(w), wmstr);
1314                 g_free(wmstr);
1315
1316                 gtk_window_set_default_size(GTK_WINDOW(w), winsize[0], winsize[1]);
1317         }
1318
1319         g_signal_connect(G_OBJECT(w), "destroy",
1320                          G_CALLBACK(destroywin), c);
1321         g_signal_connect(G_OBJECT(w), "enter-notify-event",
1322                          G_CALLBACK(winevent), c);
1323         g_signal_connect(G_OBJECT(w), "key-press-event",
1324                          G_CALLBACK(winevent), c);
1325         g_signal_connect(G_OBJECT(w), "leave-notify-event",
1326                          G_CALLBACK(winevent), c);
1327         g_signal_connect(G_OBJECT(w), "window-state-event",
1328                          G_CALLBACK(winevent), c);
1329
1330         return w;
1331 }
1332
1333 gboolean
1334 loadfailedtls(WebKitWebView *v, gchar *uri, GTlsCertificate *cert,
1335               GTlsCertificateFlags err, Client *c)
1336 {
1337         GString *errmsg = g_string_new(NULL);
1338         gchar *html, *pem;
1339
1340         c->failedcert = g_object_ref(cert);
1341         c->tlserr = err;
1342         c->errorpage = 1;
1343
1344         if (err & G_TLS_CERTIFICATE_UNKNOWN_CA)
1345                 g_string_append(errmsg,
1346                     "The signing certificate authority is not known.<br>");
1347         if (err & G_TLS_CERTIFICATE_BAD_IDENTITY)
1348                 g_string_append(errmsg,
1349                     "The certificate does not match the expected identity "
1350                     "of the site that it was retrieved from.<br>");
1351         if (err & G_TLS_CERTIFICATE_NOT_ACTIVATED)
1352                 g_string_append(errmsg,
1353                     "The certificate's activation time "
1354                     "is still in the future.<br>");
1355         if (err & G_TLS_CERTIFICATE_EXPIRED)
1356                 g_string_append(errmsg, "The certificate has expired.<br>");
1357         if (err & G_TLS_CERTIFICATE_REVOKED)
1358                 g_string_append(errmsg,
1359                     "The certificate has been revoked according to "
1360                     "the GTlsConnection's certificate revocation list.<br>");
1361         if (err & G_TLS_CERTIFICATE_INSECURE)
1362                 g_string_append(errmsg,
1363                     "The certificate's algorithm is considered insecure.<br>");
1364         if (err & G_TLS_CERTIFICATE_GENERIC_ERROR)
1365                 g_string_append(errmsg,
1366                     "Some error occurred validating the certificate.<br>");
1367
1368         g_object_get(cert, "certificate-pem", &pem, NULL);
1369         html = g_strdup_printf("<p>Could not validate TLS for “%s”<br>%s</p>"
1370                                "<p>You can inspect the following certificate "
1371                                "with Ctrl-t (default keybinding).</p>"
1372                                "<p><pre>%s</pre></p>", uri, errmsg->str, pem);
1373         g_free(pem);
1374         g_string_free(errmsg, TRUE);
1375
1376         webkit_web_view_load_alternate_html(c->view, html, uri, NULL);
1377         g_free(html);
1378
1379         return TRUE;
1380 }
1381
1382 void
1383 loadchanged(WebKitWebView *v, WebKitLoadEvent e, Client *c)
1384 {
1385         const char *uri = geturi(c);
1386
1387         switch (e) {
1388         case WEBKIT_LOAD_STARTED:
1389                 setatom(c, AtomUri, uri);
1390                 c->title = uri;
1391                 c->https = c->insecure = 0;
1392                 seturiparameters(c, uri);
1393                 if (c->errorpage)
1394                         c->errorpage = 0;
1395                 else
1396                         g_clear_object(&c->failedcert);
1397                 break;
1398         case WEBKIT_LOAD_REDIRECTED:
1399                 setatom(c, AtomUri, uri);
1400                 c->title = uri;
1401                 seturiparameters(c, uri);
1402                 break;
1403         case WEBKIT_LOAD_COMMITTED:
1404                 c->https = webkit_web_view_get_tls_info(c->view, &c->cert,
1405                                                         &c->tlserr);
1406                 break;
1407         case WEBKIT_LOAD_FINISHED:
1408                 /* Disabled until we write some WebKitWebExtension for
1409                  * manipulating the DOM directly.
1410                 evalscript(c, "document.documentElement.style.overflow = '%s'",
1411                     enablescrollbars ? "auto" : "hidden");
1412                 */
1413                 runscript(c);
1414                 break;
1415         }
1416         updatetitle(c);
1417 }
1418
1419 void
1420 progresschanged(WebKitWebView *v, GParamSpec *ps, Client *c)
1421 {
1422         c->progress = webkit_web_view_get_estimated_load_progress(c->view) *
1423                       100;
1424         updatetitle(c);
1425 }
1426
1427 void
1428 titlechanged(WebKitWebView *view, GParamSpec *ps, Client *c)
1429 {
1430         c->title = webkit_web_view_get_title(c->view);
1431         updatetitle(c);
1432 }
1433
1434 void
1435 mousetargetchanged(WebKitWebView *v, WebKitHitTestResult *h, guint modifiers,
1436     Client *c)
1437 {
1438         WebKitHitTestResultContext hc = webkit_hit_test_result_get_context(h);
1439
1440         /* Keep the hit test to know where is the pointer on the next click */
1441         c->mousepos = h;
1442
1443         if (hc & OnLink)
1444                 c->targeturi = webkit_hit_test_result_get_link_uri(h);
1445         else if (hc & OnImg)
1446                 c->targeturi = webkit_hit_test_result_get_image_uri(h);
1447         else if (hc & OnMedia)
1448                 c->targeturi = webkit_hit_test_result_get_media_uri(h);
1449         else
1450                 c->targeturi = NULL;
1451
1452         c->overtitle = c->targeturi;
1453         updatetitle(c);
1454 }
1455
1456 gboolean
1457 permissionrequested(WebKitWebView *v, WebKitPermissionRequest *r, Client *c)
1458 {
1459         ParamName param = ParameterLast;
1460
1461         if (WEBKIT_IS_GEOLOCATION_PERMISSION_REQUEST(r)) {
1462                 param = Geolocation;
1463         } else if (WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST(r)) {
1464                 if (webkit_user_media_permission_is_for_audio_device(
1465                     WEBKIT_USER_MEDIA_PERMISSION_REQUEST(r)))
1466                         param = AccessMicrophone;
1467                 else if (webkit_user_media_permission_is_for_video_device(
1468                          WEBKIT_USER_MEDIA_PERMISSION_REQUEST(r)))
1469                         param = AccessWebcam;
1470         } else {
1471                 return FALSE;
1472         }
1473
1474         if (curconfig[param].val.b)
1475                 webkit_permission_request_allow(r);
1476         else
1477                 webkit_permission_request_deny(r);
1478
1479         return TRUE;
1480 }
1481
1482 gboolean
1483 decidepolicy(WebKitWebView *v, WebKitPolicyDecision *d,
1484     WebKitPolicyDecisionType dt, Client *c)
1485 {
1486         switch (dt) {
1487         case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION:
1488                 decidenavigation(d, c);
1489                 break;
1490         case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION:
1491                 decidenewwindow(d, c);
1492                 break;
1493         case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
1494                 decideresource(d, c);
1495                 break;
1496         default:
1497                 webkit_policy_decision_ignore(d);
1498                 break;
1499         }
1500         return TRUE;
1501 }
1502
1503 void
1504 decidenavigation(WebKitPolicyDecision *d, Client *c)
1505 {
1506         WebKitNavigationAction *a =
1507             webkit_navigation_policy_decision_get_navigation_action(
1508             WEBKIT_NAVIGATION_POLICY_DECISION(d));
1509
1510         switch (webkit_navigation_action_get_navigation_type(a)) {
1511         case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: /* fallthrough */
1512         case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: /* fallthrough */
1513         case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: /* fallthrough */
1514         case WEBKIT_NAVIGATION_TYPE_RELOAD: /* fallthrough */
1515         case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED: /* fallthrough */
1516         case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */
1517         default:
1518                 /* Do not navigate to links with a "_blank" target (popup) */
1519                 if (webkit_navigation_policy_decision_get_frame_name(
1520                     WEBKIT_NAVIGATION_POLICY_DECISION(d))) {
1521                         webkit_policy_decision_ignore(d);
1522                 } else {
1523                         /* Filter out navigation to different domain ? */
1524                         /* get action→urirequest, copy and load in new window+view
1525                          * on Ctrl+Click ? */
1526                         webkit_policy_decision_use(d);
1527                 }
1528                 break;
1529         }
1530 }
1531
1532 void
1533 decidenewwindow(WebKitPolicyDecision *d, Client *c)
1534 {
1535         Arg arg;
1536         WebKitNavigationAction *a =
1537             webkit_navigation_policy_decision_get_navigation_action(
1538             WEBKIT_NAVIGATION_POLICY_DECISION(d));
1539
1540
1541         switch (webkit_navigation_action_get_navigation_type(a)) {
1542         case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: /* fallthrough */
1543         case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: /* fallthrough */
1544         case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: /* fallthrough */
1545         case WEBKIT_NAVIGATION_TYPE_RELOAD: /* fallthrough */
1546         case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED:
1547                 /* Filter domains here */
1548 /* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event.
1549  * test for link clicked but no button ? */
1550                 arg.v = webkit_uri_request_get_uri(
1551                         webkit_navigation_action_get_request(a));
1552                 newwindow(c, &arg, 0);
1553                 break;
1554         case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */
1555         default:
1556                 break;
1557         }
1558
1559         webkit_policy_decision_ignore(d);
1560 }
1561
1562 void
1563 decideresource(WebKitPolicyDecision *d, Client *c)
1564 {
1565         int i, isascii = 1;
1566         WebKitResponsePolicyDecision *r = WEBKIT_RESPONSE_POLICY_DECISION(d);
1567         WebKitURIResponse *res =
1568             webkit_response_policy_decision_get_response(r);
1569         const gchar *uri = webkit_uri_response_get_uri(res);
1570
1571         if (g_str_has_suffix(uri, "/favicon.ico")) {
1572                 webkit_policy_decision_ignore(d);
1573                 return;
1574         }
1575
1576         if (!g_str_has_prefix(uri, "http://")
1577             && !g_str_has_prefix(uri, "https://")
1578             && !g_str_has_prefix(uri, "about:")
1579             && !g_str_has_prefix(uri, "file://")
1580             && !g_str_has_prefix(uri, "data:")
1581             && !g_str_has_prefix(uri, "blob:")
1582             && strlen(uri) > 0) {
1583                 for (i = 0; i < strlen(uri); i++) {
1584                         if (!g_ascii_isprint(uri[i])) {
1585                                 isascii = 0;
1586                                 break;
1587                         }
1588                 }
1589                 if (isascii) {
1590                         handleplumb(c, uri);
1591                         webkit_policy_decision_ignore(d);
1592                         return;
1593                 }
1594         }
1595
1596         if (webkit_response_policy_decision_is_mime_type_supported(r)) {
1597                 webkit_policy_decision_use(d);
1598         } else {
1599                 webkit_policy_decision_ignore(d);
1600                 download(c, res);
1601         }
1602 }
1603
1604 void
1605 insecurecontent(WebKitWebView *v, WebKitInsecureContentEvent e, Client *c)
1606 {
1607         c->insecure = 1;
1608 }
1609
1610 void
1611 downloadstarted(WebKitWebContext *wc, WebKitDownload *d, Client *c)
1612 {
1613         g_signal_connect(G_OBJECT(d), "notify::response",
1614                          G_CALLBACK(responsereceived), c);
1615 }
1616
1617 void
1618 responsereceived(WebKitDownload *d, GParamSpec *ps, Client *c)
1619 {
1620         download(c, webkit_download_get_response(d));
1621         webkit_download_cancel(d);
1622 }
1623
1624 void
1625 download(Client *c, WebKitURIResponse *r)
1626 {
1627         Arg a = (Arg)DOWNLOAD(webkit_uri_response_get_uri(r), geturi(c));
1628         spawn(c, &a);
1629 }
1630
1631 void
1632 closeview(WebKitWebView *v, Client *c)
1633 {
1634         gtk_widget_destroy(c->win);
1635 }
1636
1637 void
1638 destroywin(GtkWidget* w, Client *c)
1639 {
1640         destroyclient(c);
1641         if (!clients)
1642                 gtk_main_quit();
1643 }
1644
1645 void
1646 pasteuri(GtkClipboard *clipboard, const char *text, gpointer d)
1647 {
1648         Arg a = {.v = text };
1649         if (text)
1650                 loaduri((Client *) d, &a);
1651 }
1652
1653 void
1654 reload(Client *c, const Arg *a)
1655 {
1656         if (a->b)
1657                 webkit_web_view_reload_bypass_cache(c->view);
1658         else
1659                 webkit_web_view_reload(c->view);
1660 }
1661
1662 void
1663 print(Client *c, const Arg *a)
1664 {
1665         webkit_print_operation_run_dialog(webkit_print_operation_new(c->view),
1666                                           GTK_WINDOW(c->win));
1667 }
1668
1669 void
1670 showcert(Client *c, const Arg *a)
1671 {
1672         GTlsCertificate *cert = c->failedcert ? c->failedcert : c->cert;
1673         GcrCertificate *gcrt;
1674         GByteArray *crt;
1675         GtkWidget *win;
1676         GcrCertificateWidget *wcert;
1677
1678         if (!cert)
1679                 return;
1680
1681         g_object_get(cert, "certificate", &crt, NULL);
1682         gcrt = gcr_simple_certificate_new(crt->data, crt->len);
1683         g_byte_array_unref(crt);
1684
1685         win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1686         wcert = gcr_certificate_widget_new(gcrt);
1687         g_object_unref(gcrt);
1688
1689         gtk_container_add(GTK_CONTAINER(win), GTK_WIDGET(wcert));
1690         gtk_widget_show_all(win);
1691 }
1692
1693 void
1694 clipboard(Client *c, const Arg *a)
1695 {
1696         if (a->b) { /* load clipboard uri */
1697                 gtk_clipboard_request_text(gtk_clipboard_get(
1698                                            GDK_SELECTION_PRIMARY),
1699                                            pasteuri, c);
1700         } else { /* copy uri */
1701                 gtk_clipboard_set_text(gtk_clipboard_get(
1702                                        GDK_SELECTION_PRIMARY), c->targeturi
1703                                        ? c->targeturi : geturi(c), -1);
1704         }
1705 }
1706
1707 void
1708 zoom(Client *c, const Arg *a)
1709 {
1710         if (a->i > 0)
1711                 webkit_web_view_set_zoom_level(c->view,
1712                                                curconfig[ZoomLevel].val.f + 0.1);
1713         else if (a->i < 0)
1714                 webkit_web_view_set_zoom_level(c->view,
1715                                                curconfig[ZoomLevel].val.f - 0.1);
1716         else
1717                 webkit_web_view_set_zoom_level(c->view, 1.0);
1718
1719         curconfig[ZoomLevel].val.f = webkit_web_view_get_zoom_level(c->view);
1720 }
1721
1722 void
1723 scroll(Client *c, const Arg *a)
1724 {
1725         GdkEvent *ev = gdk_event_new(GDK_KEY_PRESS);
1726
1727         gdk_event_set_device(ev, gdkkb);
1728         ev->key.window = gtk_widget_get_window(GTK_WIDGET(c->win));
1729         ev->key.state = GDK_CONTROL_MASK;
1730         ev->key.time = GDK_CURRENT_TIME;
1731
1732         switch (a->i) {
1733         case 'd':
1734                 ev->key.keyval = GDK_KEY_Down;
1735                 break;
1736         case 'D':
1737                 ev->key.keyval = GDK_KEY_Page_Down;
1738                 break;
1739         case 'l':
1740                 ev->key.keyval = GDK_KEY_Left;
1741                 break;
1742         case 'r':
1743                 ev->key.keyval = GDK_KEY_Right;
1744                 break;
1745         case 'U':
1746                 ev->key.keyval = GDK_KEY_Page_Up;
1747                 break;
1748         case 'u':
1749                 ev->key.keyval = GDK_KEY_Up;
1750                 break;
1751         }
1752
1753         gdk_event_put(ev);
1754 }
1755
1756 void
1757 navigate(Client *c, const Arg *a)
1758 {
1759         if (a->i < 0)
1760                 webkit_web_view_go_back(c->view);
1761         else if (a->i > 0)
1762                 webkit_web_view_go_forward(c->view);
1763 }
1764
1765 void
1766 stop(Client *c, const Arg *a)
1767 {
1768         webkit_web_view_stop_loading(c->view);
1769 }
1770
1771 void
1772 toggle(Client *c, const Arg *a)
1773 {
1774         curconfig[a->i].val.b ^= 1;
1775         setparameter(c, 1, (ParamName)a->i, &curconfig[a->i].val);
1776 }
1777
1778 void
1779 togglefullscreen(Client *c, const Arg *a)
1780 {
1781         /* toggling value is handled in winevent() */
1782         if (c->fullscreen)
1783                 gtk_window_unfullscreen(GTK_WINDOW(c->win));
1784         else
1785                 gtk_window_fullscreen(GTK_WINDOW(c->win));
1786 }
1787
1788 void
1789 togglecookiepolicy(Client *c, const Arg *a)
1790 {
1791         ++cookiepolicy;
1792         cookiepolicy %= strlen(curconfig[CookiePolicies].val.v);
1793
1794         setparameter(c, 0, CookiePolicies, NULL);
1795 }
1796
1797 void
1798 toggleinspector(Client *c, const Arg *a)
1799 {
1800         if (webkit_web_inspector_is_attached(c->inspector))
1801                 webkit_web_inspector_close(c->inspector);
1802         else if (curconfig[Inspector].val.b)
1803                 webkit_web_inspector_show(c->inspector);
1804 }
1805
1806 void
1807 find(Client *c, const Arg *a)
1808 {
1809         const char *s, *f;
1810
1811         if (a && a->i) {
1812                 if (a->i > 0)
1813                         webkit_find_controller_search_next(c->finder);
1814                 else
1815                         webkit_find_controller_search_previous(c->finder);
1816         } else {
1817                 s = getatom(c, AtomFind);
1818                 f = webkit_find_controller_get_search_text(c->finder);
1819
1820                 if (g_strcmp0(f, s) == 0) /* reset search */
1821                         webkit_find_controller_search(c->finder, "", findopts,
1822                                                       G_MAXUINT);
1823
1824                 webkit_find_controller_search(c->finder, s, findopts,
1825                                               G_MAXUINT);
1826
1827                 if (strcmp(s, "") == 0)
1828                         webkit_find_controller_search_finish(c->finder);
1829         }
1830 }
1831
1832 void
1833 clicknavigate(Client *c, const Arg *a, WebKitHitTestResult *h)
1834 {
1835         navigate(c, a);
1836 }
1837
1838 void
1839 clicknewwindow(Client *c, const Arg *a, WebKitHitTestResult *h)
1840 {
1841         Arg arg;
1842
1843         arg.v = webkit_hit_test_result_get_link_uri(h);
1844         newwindow(c, &arg, a->b);
1845 }
1846
1847 void
1848 clickexternplayer(Client *c, const Arg *a, WebKitHitTestResult *h)
1849 {
1850         Arg arg;
1851
1852         arg = (Arg)VIDEOPLAY(webkit_hit_test_result_get_media_uri(h));
1853         spawn(c, &arg);
1854 }
1855
1856 int
1857 main(int argc, char *argv[])
1858 {
1859         Arg arg;
1860         Client *c;
1861
1862         memset(&arg, 0, sizeof(arg));
1863
1864         /* command line args */
1865         ARGBEGIN {
1866         case 'a':
1867                 defconfig CSETV(CookiePolicies, EARGF(usage()));
1868                 break;
1869         case 'b':
1870                 defconfig CSETB(ScrollBars, 0);
1871                 break;
1872         case 'B':
1873                 defconfig CSETB(ScrollBars, 1);
1874                 break;
1875         case 'c':
1876                 cookiefile = EARGF(usage());
1877                 break;
1878         case 'C':
1879                 stylefile = EARGF(usage());
1880                 break;
1881         case 'd':
1882                 defconfig CSETB(DiskCache, 0);
1883                 break;
1884         case 'D':
1885                 defconfig CSETB(DiskCache, 1);
1886                 break;
1887         case 'e':
1888                 embed = strtol(EARGF(usage()), NULL, 0);
1889                 break;
1890         case 'f':
1891                 defconfig CSETB(RunInFullscreen, 0);
1892                 break;
1893         case 'F':
1894                 defconfig CSETB(RunInFullscreen, 1);
1895                 break;
1896         case 'g':
1897                 defconfig CSETB(Geolocation, 0);
1898                 break;
1899         case 'G':
1900                 defconfig CSETB(Geolocation, 1);
1901                 break;
1902         case 'i':
1903                 defconfig CSETB(LoadImages, 0);
1904                 break;
1905         case 'I':
1906                 defconfig CSETB(LoadImages, 1);
1907                 break;
1908         case 'k':
1909                 defconfig CSETB(KioskMode, 0);
1910                 break;
1911         case 'K':
1912                 defconfig CSETB(KioskMode, 1);
1913                 break;
1914         case 'm':
1915                 defconfig CSETB(Style, 0);
1916                 break;
1917         case 'M':
1918                 defconfig CSETB(Style, 1);
1919                 break;
1920         case 'n':
1921                 defconfig CSETB(Inspector, 0);
1922                 break;
1923         case 'N':
1924                 defconfig CSETB(Inspector, 1);
1925                 break;
1926         case 'p':
1927                 defconfig CSETB(Plugins, 0);
1928                 break;
1929         case 'P':
1930                 defconfig CSETB(Plugins, 1);
1931                 break;
1932         case 'r':
1933                 scriptfile = EARGF(usage());
1934                 break;
1935         case 's':
1936                 defconfig CSETB(JavaScript, 0);
1937                 break;
1938         case 'S':
1939                 defconfig CSETB(JavaScript, 1);
1940                 break;
1941         case 't':
1942                 defconfig CSETB(StrictTLS, 0);
1943                 break;
1944         case 'T':
1945                 defconfig CSETB(StrictTLS, 1);
1946                 break;
1947         case 'u':
1948                 fulluseragent = EARGF(usage());
1949                 break;
1950         case 'v':
1951                 die("surf-"VERSION", see LICENSE for © details\n");
1952         case 'w':
1953                 showxid = 1;
1954                 break;
1955         case 'x':
1956                 defconfig CSETB(Certificate, 0);
1957                 break;
1958         case 'X':
1959                 defconfig CSETB(Certificate, 1);
1960                 break;
1961         case 'z':
1962                 defconfig CSETF(ZoomLevel, strtof(EARGF(usage()), NULL));
1963                 break;
1964         default:
1965                 usage();
1966         } ARGEND;
1967         if (argc > 0)
1968                 arg.v = argv[0];
1969         else
1970                 arg.v = "about:blank";
1971
1972         setup();
1973         c = newclient(NULL);
1974         showview(NULL, c);
1975
1976         loaduri(c, &arg);
1977         updatetitle(c);
1978
1979         gtk_main();
1980         cleanup();
1981
1982         return 0;
1983 }