surf/surf.c
Quentin Rameau 934705c420 Fix title handling
Don't forget to always set the title to the current uri, this way it's
up to date when there is no title on the page (ie for local file
exploration).
Thanks to pickfire for reporting the issue.
2015-12-01 12:31:42 +01:00

1553 lines
36 KiB
C

/* See LICENSE file for copyright and license details.
*
* To understand surf, start reading main().
*/
#include <sys/file.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <libgen.h>
#include <limits.h>
#include <pwd.h>
#include <regex.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <gdk/gdk.h>
#include <gdk/gdkkeysyms.h>
#include <gdk/gdkx.h>
#include <glib/gstdio.h>
#include <gtk/gtk.h>
#include <gtk/gtkx.h>
#include <JavaScriptCore/JavaScript.h>
#include <webkit2/webkit2.h>
#include <X11/X.h>
#include <X11/Xatom.h>
#include "arg.h"
#define LENGTH(x) (sizeof(x) / sizeof(x[0]))
#define CLEANMASK(mask) (mask & (MODKEY|GDK_SHIFT_MASK))
enum { AtomFind, AtomGo, AtomUri, AtomLast };
enum {
CaretBrowsing,
FrameFlattening,
Geolocation,
JavaScript,
LoadImages,
Plugins,
ScrollBars,
};
enum {
OnDoc = WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT,
OnLink = WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK,
OnImg = WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE,
OnMedia = WEBKIT_HIT_TEST_RESULT_CONTEXT_MEDIA,
OnEdit = WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE,
OnBar = WEBKIT_HIT_TEST_RESULT_CONTEXT_SCROLLBAR,
OnSel = WEBKIT_HIT_TEST_RESULT_CONTEXT_SELECTION,
OnAny = OnDoc | OnLink | OnImg | OnMedia | OnEdit | OnBar | OnSel,
};
typedef union {
int b;
int i;
const void *v;
} Arg;
typedef struct Client {
GtkWidget *win;
WebKitWebView *view;
WebKitWebInspector *inspector;
WebKitFindController *finder;
WebKitHitTestResult *mousepos;
GTlsCertificateFlags tlsflags;
Window xid;
int progress, fullscreen;
const char *title, *overtitle, *targeturi;
const char *needle;
struct Client *next;
} Client;
typedef struct {
guint mod;
guint keyval;
void (*func)(Client *c, const Arg *a);
const Arg arg;
} Key;
typedef struct {
unsigned int target;
unsigned int mask;
guint button;
void (*func)(Client *c, const Arg *a, WebKitHitTestResult *h);
const Arg arg;
unsigned int stopevent;
} Button;
typedef struct {
char *regex;
char *style;
regex_t re;
} SiteStyle;
/* Surf */
static void usage(void);
static void die(const char *errstr, ...);
static void setup(void);
static void sigchld(int unused);
static char *buildfile(const char *path);
static char *buildpath(const char *path);
static Client *newclient(Client *c);
static void loaduri(Client *c, const Arg *a);
static const char *geturi(Client *c);
static void setatom(Client *c, int a, const char *v);
static const char *getatom(Client *c, int a);
static void updatetitle(Client *c);
static void gettogglestats(Client *c);
static void getpagestats(Client *c);
static WebKitCookieAcceptPolicy cookiepolicy_get(void);
static char cookiepolicy_set(const WebKitCookieAcceptPolicy p);
static const char *getstyle(const char *uri);
static void setstyle(Client *c, const char *stylefile);
static void runscript(Client *c);
static void evalscript(Client *c, const char *jsstr, ...);
static void updatewinid(Client *c);
static void handleplumb(Client *c, const char *uri);
static void newwindow(Client *c, const Arg *a, int noembed);
static void spawn(Client *c, const Arg *a);
static void destroyclient(Client *c);
static void cleanup(void);
/* GTK/WebKit */
static WebKitWebView *newview(Client *c, WebKitWebView *rv);
static GtkWidget *createview(WebKitWebView *v, WebKitNavigationAction *a,
Client *c);
static gboolean buttonreleased(GtkWidget *w, GdkEvent *e, Client *c);
static GdkFilterReturn processx(GdkXEvent *xevent, GdkEvent *event,
gpointer d);
static gboolean winevent(GtkWidget *w, GdkEvent *e, Client *c);
static void showview(WebKitWebView *v, Client *c);
static GtkWidget *createwindow(Client *c);
static void loadchanged(WebKitWebView *v, WebKitLoadEvent e, Client *c);
static void progresschanged(WebKitWebView *v, GParamSpec *ps, Client *c);
static void titlechanged(WebKitWebView *view, GParamSpec *ps, Client *c);
static void mousetargetchanged(WebKitWebView *v, WebKitHitTestResult *h,
guint modifiers, Client *c);
static gboolean permissionrequested(WebKitWebView *v,
WebKitPermissionRequest *r, Client *c);
static gboolean decidepolicy(WebKitWebView *v, WebKitPolicyDecision *d,
WebKitPolicyDecisionType dt, Client *c);
static void decidenavigation(WebKitPolicyDecision *d, Client *c);
static void decidenewwindow(WebKitPolicyDecision *d, Client *c);
static void decideresource(WebKitPolicyDecision *d, Client *c);
static void downloadstarted(WebKitWebContext *wc, WebKitDownload *d,
Client *c);
static void responsereceived(WebKitDownload *d, GParamSpec *ps, Client *c);
static void download(Client *c, WebKitURIResponse *r);
static void closeview(WebKitWebView *v, Client *c);
static void destroywin(GtkWidget* w, Client *c);
/* Hotkeys */
static void pasteuri(GtkClipboard *clipboard, const char *text, gpointer d);
static void reload(Client *c, const Arg *a);
static void print(Client *c, const Arg *a);
static void clipboard(Client *c, const Arg *a);
static void zoom(Client *c, const Arg *a);
static void scroll_v(Client *c, const Arg *a);
static void scroll_h(Client *c, const Arg *a);
static void navigate(Client *c, const Arg *a);
static void stop(Client *c, const Arg *a);
static void toggle(Client *c, const Arg *a);
static void togglefullscreen(Client *c, const Arg *a);
static void togglecookiepolicy(Client *c, const Arg *a);
static void togglestyle(Client *c, const Arg *a);
static void toggleinspector(Client *c, const Arg *a);
static void find(Client *c, const Arg *a);
/* Buttons */
static void clicknavigate(Client *c, const Arg *a, WebKitHitTestResult *h);
static void clicknewwindow(Client *c, const Arg *a, WebKitHitTestResult *h);
static void clickexternplayer(Client *c, const Arg *a, WebKitHitTestResult *h);
static char winid[64];
static char togglestats[10];
static char pagestats[2];
static Atom atoms[AtomLast];
static Window embed;
static int showxid;
static int cookiepolicy;
static Display *dpy;
static Client *clients;
static char *stylefile;
static const char *useragent;
char *argv0;
/* configuration, allows nested code to access above variables */
#include "config.h"
void
usage(void)
{
die("usage: %s [-bBdDfFgGiIkKmMnNpPsSvx] [-a cookiepolicies ] "
"[-c cookiefile] [-e xid] [-r scriptfile] [-t stylefile] "
"[-u useragent] [-z zoomlevel] [uri]\n", basename(argv0));
}
void
die(const char *errstr, ...)
{
va_list ap;
va_start(ap, errstr);
vfprintf(stderr, errstr, ap);
va_end(ap);
exit(1);
}
void
setup(void)
{
int i;
/* clean up any zombies immediately */
sigchld(0);
gtk_init(NULL, NULL);
dpy = GDK_DISPLAY_XDISPLAY(gdk_display_get_default());
/* atoms */
atoms[AtomFind] = XInternAtom(dpy, "_SURF_FIND", False);
atoms[AtomGo] = XInternAtom(dpy, "_SURF_GO", False);
atoms[AtomUri] = XInternAtom(dpy, "_SURF_URI", False);
/* dirs and files */
cookiefile = buildfile(cookiefile);
scriptfile = buildfile(scriptfile);
cachedir = buildpath(cachedir);
if (!stylefile) {
styledir = buildpath(styledir);
for (i = 0; i < LENGTH(styles); ++i) {
if (regcomp(&(styles[i].re), styles[i].regex,
REG_EXTENDED)) {
fprintf(stderr,
"Could not compile regex: %s\n",
styles[i].regex);
styles[i].regex = NULL;
}
styles[i].style = g_strconcat(styledir, "/",
styles[i].style, NULL);
}
g_free(styledir);
} else {
stylefile = buildfile(stylefile);
}
}
void
sigchld(int unused)
{
if (signal(SIGCHLD, sigchld) == SIG_ERR)
die("Can't install SIGCHLD handler");
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
char *
buildfile(const char *path)
{
char *dname, *bname, *bpath, *fpath;
FILE *f;
dname = g_path_get_dirname(path);
bname = g_path_get_basename(path);
bpath = buildpath(dname);
g_free(dname);
fpath = g_build_filename(bpath, bname, NULL);
g_free(bpath);
g_free(bname);
if (!(f = fopen(fpath, "a")))
die("Could not open file: %s\n", fpath);
g_chmod(fpath, 0600); /* always */
fclose(f);
return fpath;
}
char *
buildpath(const char *path)
{
struct passwd *pw;
char *apath, *name, *p, *fpath;
if (path[0] == '~') {
if (path[1] == '/' || path[1] == '\0') {
p = (char *)&path[1];
pw = getpwuid(getuid());
} else {
if ((p = strchr(path, '/')))
name = g_strndup(&path[1], --p - path);
else
name = g_strdup(&path[1]);
if (!(pw = getpwnam(name))) {
die("Can't get user %s home directory: %s.\n",
name, path);
}
g_free(name);
}
apath = g_build_filename(pw->pw_dir, p, NULL);
} else {
apath = g_strdup(path);
}
/* creating directory */
if (g_mkdir_with_parents(apath, 0700) < 0)
die("Could not access directory: %s\n", apath);
fpath = realpath(apath, NULL);
g_free(apath);
return fpath;
}
Client *
newclient(Client *rc)
{
Client *c;
if (!(c = calloc(1, sizeof(Client))))
die("Cannot malloc!\n");
c->next = clients;
clients = c;
c->progress = 100;
c->tlsflags = G_TLS_CERTIFICATE_VALIDATE_ALL + 1;
c->view = newview(c, rc ? rc->view : NULL);
return c;
}
void
loaduri(Client *c, const Arg *a)
{
struct stat st;
char *url, *path;
const char *uri = a->v;
if (g_strcmp0(uri, "") == 0)
return;
if (g_strrstr(uri, "://") || g_str_has_prefix(uri, "about:")) {
url = g_strdup(uri);
} else if (!stat(uri, &st) && (path = realpath(uri, NULL))) {
url = g_strdup_printf("file://%s", path);
free(path);
} else {
url = g_strdup_printf("http://%s", uri);
}
setatom(c, AtomUri, url);
if (strcmp(url, geturi(c)) == 0) {
reload(c, a);
} else {
webkit_web_view_load_uri(c->view, url);
updatetitle(c);
}
g_free(url);
}
const char *
geturi(Client *c)
{
const char *uri;
if (!(uri = webkit_web_view_get_uri(c->view)))
uri = "about:blank";
return uri;
}
void
setatom(Client *c, int a, const char *v)
{
XSync(dpy, False);
XChangeProperty(dpy, c->xid,
atoms[a], XA_STRING, 8, PropModeReplace,
(unsigned char *)v, strlen(v) + 1);
}
const char *
getatom(Client *c, int a)
{
static char buf[BUFSIZ];
Atom adummy;
int idummy;
unsigned long ldummy;
unsigned char *p = NULL;
XGetWindowProperty(dpy, c->xid, atoms[a], 0L, BUFSIZ, False, XA_STRING,
&adummy, &idummy, &ldummy, &ldummy, &p);
if (p)
strncpy(buf, (char *)p, LENGTH(buf) - 1);
else
buf[0] = '\0';
XFree(p);
return buf;
}
void
updatetitle(Client *c)
{
char *title;
const char *name = c->overtitle ? c->overtitle :
c->title ? c->title : "";
if (showindicators) {
gettogglestats(c);
getpagestats(c);
if (c->progress != 100)
title = g_strdup_printf("[%i%%] %s:%s | %s",
c->progress, togglestats, pagestats, name);
else
title = g_strdup_printf("%s:%s | %s",
togglestats, pagestats, name);
gtk_window_set_title(GTK_WINDOW(c->win), title);
g_free(title);
} else {
gtk_window_set_title(GTK_WINDOW(c->win), name);
}
}
void
gettogglestats(Client *c)
{
togglestats[0] = cookiepolicy_set(cookiepolicy_get());
togglestats[1] = enablecaretbrowsing ? 'C' : 'c';
togglestats[2] = allowgeolocation ? 'G' : 'g';
togglestats[3] = enablecache ? 'D' : 'd';
togglestats[4] = loadimages ? 'I' : 'i';
togglestats[5] = enablescripts ? 'S' : 's';
togglestats[6] = enableplugins ? 'V' : 'v';
togglestats[7] = enablestyle ? 'M' : 'm';
togglestats[8] = enableframeflattening ? 'F' : 'f';
togglestats[9] = '\0';
}
void
getpagestats(Client *c)
{
pagestats[0] = c->tlsflags > G_TLS_CERTIFICATE_VALIDATE_ALL ? '-' :
c->tlsflags > 0 ? 'U' : 'T';
pagestats[1] = '\0';
}
WebKitCookieAcceptPolicy
cookiepolicy_get(void)
{
switch (cookiepolicies[cookiepolicy]) {
case 'a':
return WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
case '@':
return WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
default: /* fallthrough */
case 'A':
return WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
}
}
char
cookiepolicy_set(const WebKitCookieAcceptPolicy p)
{
switch (p) {
case WEBKIT_COOKIE_POLICY_ACCEPT_NEVER:
return 'a';
case WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY:
return '@';
default: /* fallthrough */
case WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS:
return 'A';
}
}
const char *
getstyle(const char *uri)
{
int i;
if (stylefile)
return stylefile;
for (i = 0; i < LENGTH(styles); ++i) {
if (styles[i].regex &&
!regexec(&(styles[i].re), uri, 0, NULL, 0))
return styles[i].style;
}
return "";
}
void
setstyle(Client *c, const char *stylefile)
{
gchar *style;
if (!g_file_get_contents(stylefile, &style, NULL, NULL)) {
fprintf(stderr, "Could not read style file: %s\n", stylefile);
return;
}
webkit_user_content_manager_add_style_sheet(
webkit_web_view_get_user_content_manager(c->view),
webkit_user_style_sheet_new(style,
WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
WEBKIT_USER_STYLE_LEVEL_USER,
NULL, NULL));
g_free(style);
}
void
runscript(Client *c)
{
gchar *script;
gsize l;
if (g_file_get_contents(scriptfile, &script, &l, NULL) && l)
evalscript(c, script);
g_free(script);
}
void
evalscript(Client *c, const char *jsstr, ...)
{
va_list ap;
gchar *script;
va_start(ap, jsstr);
script = g_strdup_vprintf(jsstr, ap);
va_end(ap);
webkit_web_view_run_javascript(c->view, script, NULL, NULL, NULL);
g_free(script);
}
void
updatewinid(Client *c)
{
snprintf(winid, LENGTH(winid), "%lu", c->xid);
}
void
handleplumb(Client *c, const char *uri)
{
Arg a = (Arg)PLUMB(uri);
spawn(c, &a);
}
void
newwindow(Client *c, const Arg *a, int noembed)
{
int i = 0;
char tmp[64];
const char *cmd[26], *uri;
const Arg arg = { .v = cmd };
cmd[i++] = argv0;
cmd[i++] = "-a";
cmd[i++] = cookiepolicies;
cmd[i++] = enablescrollbars ? "-B" : "-b";
if (cookiefile && g_strcmp0(cookiefile, "")) {
cmd[i++] = "-c";
cmd[i++] = cookiefile;
}
cmd[i++] = enablecache ? "-D" : "-d";
if (embed && !noembed) {
cmd[i++] = "-e";
snprintf(tmp, LENGTH(tmp), "%lu", embed);
cmd[i++] = tmp;
}
cmd[i++] = runinfullscreen ? "-F" : "-f";
cmd[i++] = allowgeolocation ? "-G" : "-g";
cmd[i++] = loadimages ? "-I" : "-i";
cmd[i++] = kioskmode ? "-K" : "-k";
cmd[i++] = enablestyle ? "-M" : "-m";
cmd[i++] = enableinspector ? "-N" : "-n";
cmd[i++] = enableplugins ? "-P" : "-p";
if (scriptfile && g_strcmp0(scriptfile, "")) {
cmd[i++] = "-r";
cmd[i++] = scriptfile;
}
cmd[i++] = enablescripts ? "-S" : "-s";
if (stylefile && g_strcmp0(stylefile, "")) {
cmd[i++] = "-t";
cmd[i++] = stylefile;
}
if (fulluseragent && g_strcmp0(fulluseragent, "")) {
cmd[i++] = "-u";
cmd[i++] = fulluseragent;
}
if (showxid)
cmd[i++] = "-x";
/* do not keep zoom level */
cmd[i++] = "--";
if ((uri = a->v))
cmd[i++] = uri;
cmd[i] = NULL;
spawn(c, &arg);
}
void
spawn(Client *c, const Arg *a)
{
if (fork() == 0) {
if (dpy)
close(ConnectionNumber(dpy));
setsid();
execvp(((char **)a->v)[0], (char **)a->v);
fprintf(stderr, "%s: execvp %s", argv0, ((char **)a->v)[0]);
perror(" failed");
exit(1);
}
}
void
destroyclient(Client *c)
{
Client *p;
webkit_web_view_stop_loading(c->view);
/* Not needed, has already been called
gtk_widget_destroy(c->win);
*/
for (p = clients; p && p->next != c; p = p->next)
;
if (p)
p->next = c->next;
else
clients = c->next;
free(c);
}
void
cleanup(void)
{
while (clients)
destroyclient(clients);
g_free(cookiefile);
g_free(scriptfile);
g_free(stylefile);
g_free(cachedir);
}
WebKitWebView *
newview(Client *c, WebKitWebView *rv)
{
WebKitWebView *v;
WebKitSettings *settings;
WebKitUserContentManager *contentmanager;
WebKitWebContext *context;
/* Webview */
if (rv) {
v = WEBKIT_WEB_VIEW(
webkit_web_view_new_with_related_view(rv));
} else {
settings = webkit_settings_new_with_settings(
"auto-load-images", loadimages,
"default-font-size", defaultfontsize,
"enable-caret-browsing", enablecaretbrowsing,
"enable-developer-extras", enableinspector,
"enable-dns-prefetching", enablednsprefetching,
"enable-frame-flattening", enableframeflattening,
"enable-html5-database", enablecache,
"enable-html5-local-storage", enablecache,
"enable-javascript", enablescripts,
"enable-plugins", enableplugins,
NULL);
/* For mor interesting settings, have a look at
* http://webkitgtk.org/reference/webkit2gtk/stable/WebKitSettings.html */
if (strcmp(fulluseragent, "")) {
webkit_settings_set_user_agent(settings, fulluseragent);
} else if (surfuseragent) {
webkit_settings_set_user_agent_with_application_details(
settings, "Surf", VERSION);
}
useragent = webkit_settings_get_user_agent(settings);
contentmanager = webkit_user_content_manager_new();
context = webkit_web_context_new_with_website_data_manager(
webkit_website_data_manager_new(
"base-cache-directory", cachedir,
"base-data-directory", cachedir,
NULL));
/* rendering process model, can be a shared unique one
* or one for each view */
webkit_web_context_set_process_model(context,
WEBKIT_PROCESS_MODEL_MULTIPLE_SECONDARY_PROCESSES);
/* ssl */
webkit_web_context_set_tls_errors_policy(context, strictssl ?
WEBKIT_TLS_ERRORS_POLICY_FAIL :
WEBKIT_TLS_ERRORS_POLICY_IGNORE);
/* disk cache */
webkit_web_context_set_cache_model(context, enablecache ?
WEBKIT_CACHE_MODEL_WEB_BROWSER :
WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER);
/* Currently only works with text file to be compatible with curl */
webkit_cookie_manager_set_persistent_storage(
webkit_web_context_get_cookie_manager(context), cookiefile,
WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT);
/* cookie policy */
webkit_cookie_manager_set_accept_policy(
webkit_web_context_get_cookie_manager(context),
cookiepolicy_get());
/* languages */
webkit_web_context_set_preferred_languages(context,
preferedlanguages);
webkit_web_context_set_spell_checking_languages(context,
spellinglanguages);
webkit_web_context_set_spell_checking_enabled(context,
enablespellchecking);
g_signal_connect(G_OBJECT(context), "download-started",
G_CALLBACK(downloadstarted), c);
v = g_object_new(WEBKIT_TYPE_WEB_VIEW,
"settings", settings,
"user-content-manager", contentmanager,
"web-context", context,
NULL);
}
g_signal_connect(G_OBJECT(v), "notify::estimated-load-progress",
G_CALLBACK(progresschanged), c);
g_signal_connect(G_OBJECT(v), "notify::title",
G_CALLBACK(titlechanged), c);
g_signal_connect(G_OBJECT(v), "button-release-event",
G_CALLBACK(buttonreleased), c);
g_signal_connect(G_OBJECT(v), "close",
G_CALLBACK(closeview), c);
g_signal_connect(G_OBJECT(v), "create",
G_CALLBACK(createview), c);
g_signal_connect(G_OBJECT(v), "decide-policy",
G_CALLBACK(decidepolicy), c);
g_signal_connect(G_OBJECT(v), "load-changed",
G_CALLBACK(loadchanged), c);
g_signal_connect(G_OBJECT(v), "mouse-target-changed",
G_CALLBACK(mousetargetchanged), c);
g_signal_connect(G_OBJECT(v), "permission-request",
G_CALLBACK(permissionrequested), c);
g_signal_connect(G_OBJECT(v), "ready-to-show",
G_CALLBACK(showview), c);
return v;
}
GtkWidget *
createview(WebKitWebView *v, WebKitNavigationAction *a, Client *c)
{
Client *n;
switch (webkit_navigation_action_get_navigation_type(a)) {
case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */
/*
* popup windows of type “other” are almost always triggered
* by user gesture, so inverse the logic here
*/
/* instead of this, compare destination uri to mouse-over uri for validating window */
if (webkit_navigation_action_is_user_gesture(a))
return NULL;
case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_RELOAD: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED:
n = newclient(c);
break;
default:
return NULL;
}
return GTK_WIDGET(n->view);
}
gboolean
buttonreleased(GtkWidget *w, GdkEvent *e, Client *c)
{
WebKitHitTestResultContext element;
int i;
element = webkit_hit_test_result_get_context(c->mousepos);
for (i = 0; i < LENGTH(buttons); ++i) {
if (element & buttons[i].target &&
e->button.button == buttons[i].button &&
CLEANMASK(e->button.state) == CLEANMASK(buttons[i].mask) &&
buttons[i].func) {
buttons[i].func(c, &buttons[i].arg, c->mousepos);
return buttons[i].stopevent;
}
}
return FALSE;
}
GdkFilterReturn
processx(GdkXEvent *e, GdkEvent *event, gpointer d)
{
Client *c = (Client *)d;
XPropertyEvent *ev;
Arg a;
if (((XEvent *)e)->type == PropertyNotify) {
ev = &((XEvent *)e)->xproperty;
if (ev->state == PropertyNewValue) {
if (ev->atom == atoms[AtomFind]) {
find(c, NULL);
return GDK_FILTER_REMOVE;
} else if (ev->atom == atoms[AtomGo]) {
a.v = getatom(c, AtomGo);
loaduri(c, &a);
return GDK_FILTER_REMOVE;
}
}
}
return GDK_FILTER_CONTINUE;
}
gboolean
winevent(GtkWidget *w, GdkEvent *e, Client *c)
{
int i;
switch (e->type) {
case GDK_ENTER_NOTIFY:
c->overtitle = c->targeturi;
updatetitle(c);
break;
case GDK_KEY_PRESS:
if (!kioskmode) {
for (i = 0; i < LENGTH(keys); ++i) {
if (gdk_keyval_to_lower(e->key.keyval) ==
keys[i].keyval &&
CLEANMASK(e->key.state) == keys[i].mod &&
keys[i].func) {
updatewinid(c);
keys[i].func(c, &(keys[i].arg));
return TRUE;
}
}
}
case GDK_LEAVE_NOTIFY:
c->overtitle = NULL;
updatetitle(c);
break;
case GDK_WINDOW_STATE:
if (e->window_state.changed_mask ==
GDK_WINDOW_STATE_FULLSCREEN)
c->fullscreen = e->window_state.new_window_state &
GDK_WINDOW_STATE_FULLSCREEN;
break;
default:
break;
}
return FALSE;
}
void
showview(WebKitWebView *v, Client *c)
{
GdkRGBA bgcolor = { 0 };
GdkWindow *gwin;
c->finder = webkit_web_view_get_find_controller(c->view);
if (enableinspector)
c->inspector = webkit_web_view_get_inspector(c->view);
c->win = createwindow(c);
gtk_container_add(GTK_CONTAINER(c->win), GTK_WIDGET(c->view));
gtk_widget_show_all(c->win);
gtk_widget_grab_focus(GTK_WIDGET(c->view));
gwin = gtk_widget_get_window(GTK_WIDGET(c->win));
c->xid = gdk_x11_window_get_xid(gwin);
updatewinid(c);
if (showxid) {
gdk_display_sync(gtk_widget_get_display(c->win));
puts(winid);
}
if (hidebackground)
webkit_web_view_set_background_color(c->view, &bgcolor);
if (!kioskmode) {
gdk_window_set_events(gwin, GDK_ALL_EVENTS_MASK);
gdk_window_add_filter(gwin, processx, c);
}
if (runinfullscreen)
togglefullscreen(c, NULL);
if (zoomlevel != 1.0)
webkit_web_view_set_zoom_level(c->view, zoomlevel);
setatom(c, AtomFind, "");
setatom(c, AtomUri, "about:blank");
}
GtkWidget *
createwindow(Client *c)
{
char *wmstr;
GtkWidget *w;
if (embed) {
w = gtk_plug_new(embed);
} else {
w = gtk_window_new(GTK_WINDOW_TOPLEVEL);
wmstr = g_path_get_basename(argv0);
gtk_window_set_wmclass(GTK_WINDOW(w), wmstr, "Surf");
g_free(wmstr);
wmstr = g_strdup_printf("%s[%lu]", "Surf",
webkit_web_view_get_page_id(c->view));
gtk_window_set_role(GTK_WINDOW(w), wmstr);
g_free(wmstr);
gtk_window_set_default_size(GTK_WINDOW(w), 800, 600);
}
g_signal_connect(G_OBJECT(w), "destroy",
G_CALLBACK(destroywin), c);
g_signal_connect(G_OBJECT(w), "enter-notify-event",
G_CALLBACK(winevent), c);
g_signal_connect(G_OBJECT(w), "key-press-event",
G_CALLBACK(winevent), c);
g_signal_connect(G_OBJECT(w), "leave-notify-event",
G_CALLBACK(winevent), c);
g_signal_connect(G_OBJECT(w), "window-state-event",
G_CALLBACK(winevent), c);
return w;
}
void
loadchanged(WebKitWebView *v, WebKitLoadEvent e, Client *c)
{
const char *title = geturi(c);
switch (e) {
case WEBKIT_LOAD_STARTED:
setatom(c, AtomUri, title);
c->title = title;
c->tlsflags = G_TLS_CERTIFICATE_VALIDATE_ALL + 1;
break;
case WEBKIT_LOAD_REDIRECTED:
setatom(c, AtomUri, title);
c->title = title;
break;
case WEBKIT_LOAD_COMMITTED:
setatom(c, AtomUri, title);
c->title = title;
if (!webkit_web_view_get_tls_info(c->view, NULL,
&(c->tlsflags)))
c->tlsflags = G_TLS_CERTIFICATE_VALIDATE_ALL + 1;
if (enablestyle)
setstyle(c, getstyle(geturi(c)));
break;
case WEBKIT_LOAD_FINISHED:
/* Disabled until we write some WebKitWebExtension for
* manipulating the DOM directly.
evalscript(c, "document.documentElement.style.overflow = '%s'",
enablescrollbars ? "auto" : "hidden");
*/
runscript(c);
break;
}
updatetitle(c);
}
void
progresschanged(WebKitWebView *v, GParamSpec *ps, Client *c)
{
c->progress = webkit_web_view_get_estimated_load_progress(c->view) *
100;
updatetitle(c);
}
void
titlechanged(WebKitWebView *view, GParamSpec *ps, Client *c)
{
c->title = webkit_web_view_get_title(c->view);
updatetitle(c);
}
void
mousetargetchanged(WebKitWebView *v, WebKitHitTestResult *h, guint modifiers,
Client *c)
{
WebKitHitTestResultContext hc = webkit_hit_test_result_get_context(h);
/* Keep the hit test to know where is the pointer on the next click */
c->mousepos = h;
if (hc & OnLink)
c->targeturi = webkit_hit_test_result_get_link_uri(h);
else if (hc & OnImg)
c->targeturi = webkit_hit_test_result_get_image_uri(h);
else if (hc & OnMedia)
c->targeturi = webkit_hit_test_result_get_media_uri(h);
else
c->targeturi = NULL;
c->overtitle = c->targeturi;
updatetitle(c);
}
gboolean
permissionrequested(WebKitWebView *v, WebKitPermissionRequest *r, Client *c)
{
if (WEBKIT_IS_GEOLOCATION_PERMISSION_REQUEST(r)) {
if (allowgeolocation)
webkit_permission_request_allow(r);
else
webkit_permission_request_deny(r);
return TRUE;
}
return FALSE;
}
gboolean
decidepolicy(WebKitWebView *v, WebKitPolicyDecision *d,
WebKitPolicyDecisionType dt, Client *c)
{
switch (dt) {
case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION:
decidenavigation(d, c);
break;
case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION:
decidenewwindow(d, c);
break;
case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
decideresource(d, c);
break;
default:
webkit_policy_decision_ignore(d);
break;
}
return TRUE;
}
void
decidenavigation(WebKitPolicyDecision *d, Client *c)
{
WebKitNavigationAction *a =
webkit_navigation_policy_decision_get_navigation_action(
WEBKIT_NAVIGATION_POLICY_DECISION(d));
switch (webkit_navigation_action_get_navigation_type(a)) {
case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_RELOAD: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */
default:
/* Do not navigate to links with a "_blank" target (popup) */
if (webkit_navigation_policy_decision_get_frame_name(
WEBKIT_NAVIGATION_POLICY_DECISION(d))) {
webkit_policy_decision_ignore(d);
} else {
/* Filter out navigation to different domain ? */
/* get action→urirequest, copy and load in new window+view
* on Ctrl+Click ? */
webkit_policy_decision_use(d);
}
break;
}
}
void
decidenewwindow(WebKitPolicyDecision *d, Client *c)
{
Arg arg;
WebKitNavigationAction *a =
webkit_navigation_policy_decision_get_navigation_action(
WEBKIT_NAVIGATION_POLICY_DECISION(d));
switch (webkit_navigation_action_get_navigation_type(a)) {
case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_RELOAD: /* fallthrough */
case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED:
/* Filter domains here */
/* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event.
* test for link clicked but no button ? */
arg.v = webkit_uri_request_get_uri(
webkit_navigation_action_get_request(a));
newwindow(c, &arg, 0);
break;
case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */
default:
break;
}
webkit_policy_decision_ignore(d);
}
void
decideresource(WebKitPolicyDecision *d, Client *c)
{
int i, isascii = 1;
WebKitResponsePolicyDecision *r = WEBKIT_RESPONSE_POLICY_DECISION(d);
WebKitURIResponse *res =
webkit_response_policy_decision_get_response(r);
const gchar *uri = webkit_uri_response_get_uri(res);
if (g_str_has_suffix(uri, "/favicon.ico")) {
webkit_policy_decision_ignore(d);
return;
}
if (!g_str_has_prefix(uri, "http://")
&& !g_str_has_prefix(uri, "https://")
&& !g_str_has_prefix(uri, "about:")
&& !g_str_has_prefix(uri, "file://")
&& !g_str_has_prefix(uri, "data:")
&& !g_str_has_prefix(uri, "blob:")
&& strlen(uri) > 0) {
for (i = 0; i < strlen(uri); i++) {
if (!g_ascii_isprint(uri[i])) {
isascii = 0;
break;
}
}
if (isascii) {
handleplumb(c, uri);
webkit_policy_decision_ignore(d);
return;
}
}
if (webkit_response_policy_decision_is_mime_type_supported(r)) {
webkit_policy_decision_use(d);
} else {
webkit_policy_decision_ignore(d);
download(c, res);
}
}
void
downloadstarted(WebKitWebContext *wc, WebKitDownload *d, Client *c)
{
g_signal_connect(G_OBJECT(d), "notify::response",
G_CALLBACK(responsereceived), c);
}
void
responsereceived(WebKitDownload *d, GParamSpec *ps, Client *c)
{
download(c, webkit_download_get_response(d));
webkit_download_cancel(d);
}
void
download(Client *c, WebKitURIResponse *r)
{
Arg a = (Arg)DOWNLOAD(webkit_uri_response_get_uri(r), geturi(c));
spawn(c, &a);
}
void
closeview(WebKitWebView *v, Client *c)
{
gtk_widget_destroy(c->win);
}
void
destroywin(GtkWidget* w, Client *c)
{
destroyclient(c);
if (!clients)
gtk_main_quit();
}
void
pasteuri(GtkClipboard *clipboard, const char *text, gpointer d)
{
Arg a = {.v = text };
if (text)
loaduri((Client *) d, &a);
}
void
reload(Client *c, const Arg *a)
{
if (a->b)
webkit_web_view_reload_bypass_cache(c->view);
else
webkit_web_view_reload(c->view);
}
void
print(Client *c, const Arg *a)
{
webkit_print_operation_run_dialog(webkit_print_operation_new(c->view),
GTK_WINDOW(c->win));
}
void
clipboard(Client *c, const Arg *a)
{
if (a->b) { /* load clipboard uri */
gtk_clipboard_request_text(gtk_clipboard_get(
GDK_SELECTION_PRIMARY),
pasteuri, c);
} else { /* copy uri */
gtk_clipboard_set_text(gtk_clipboard_get(
GDK_SELECTION_PRIMARY), c->targeturi
? c->targeturi : geturi(c), -1);
}
}
void
zoom(Client *c, const Arg *a)
{
if (a->i > 0)
webkit_web_view_set_zoom_level(c->view, zoomlevel + 0.1);
else if (a->i < 0)
webkit_web_view_set_zoom_level(c->view, zoomlevel - 0.1);
else
webkit_web_view_set_zoom_level(c->view, 1.0);
zoomlevel = webkit_web_view_get_zoom_level(c->view);
}
void
scroll_v(Client *c, const Arg *a)
{
evalscript(c, "window.scrollBy(0, %d * (window.innerHeight / 100))",
a->i);
}
void
scroll_h(Client *c, const Arg *a)
{
evalscript(c, "window.scrollBy(%d * (window.innerWidth / 100), 0)",
a->i);
}
void
navigate(Client *c, const Arg *a)
{
if (a->i < 0)
webkit_web_view_go_back(c->view);
else if (a->i > 0)
webkit_web_view_go_forward(c->view);
}
void
stop(Client *c, const Arg *a)
{
webkit_web_view_stop_loading(c->view);
}
void
toggle(Client *c, const Arg *a)
{
WebKitSettings *s = webkit_web_view_get_settings(c->view);
switch ((unsigned int)a->i) {
case CaretBrowsing:
enablecaretbrowsing = !enablecaretbrowsing;
webkit_settings_set_enable_caret_browsing(s,
enablecaretbrowsing);
updatetitle(c);
return; /* do not reload */
break;
case FrameFlattening:
enableframeflattening = !enableframeflattening;
webkit_settings_set_enable_frame_flattening(s,
enableframeflattening);
break;
case Geolocation:
allowgeolocation = !allowgeolocation;
break;
case JavaScript:
enablescripts = !enablescripts;
webkit_settings_set_enable_javascript(s, enablescripts);
break;
case LoadImages:
loadimages = !loadimages;
webkit_settings_set_auto_load_images(s, loadimages);
break;
case Plugins:
enableplugins = !enableplugins;
webkit_settings_set_enable_plugins(s, enableplugins);
break;
case ScrollBars:
/* Disabled until we write some WebKitWebExtension for
* manipulating the DOM directly.
enablescrollbars = !enablescrollbars;
evalscript(c, "document.documentElement.style.overflow = '%s'",
enablescrollbars ? "auto" : "hidden");
*/
return; /* do not reload */
break;
default:
break;
}
reload(c, a);
}
void
togglefullscreen(Client *c, const Arg *a)
{
/* toggling value is handled in winevent() */
if (c->fullscreen)
gtk_window_unfullscreen(GTK_WINDOW(c->win));
else
gtk_window_fullscreen(GTK_WINDOW(c->win));
}
void
togglecookiepolicy(Client *c, const Arg *a)
{
++cookiepolicy;
cookiepolicy %= strlen(cookiepolicies);
webkit_cookie_manager_set_accept_policy(
webkit_web_context_get_cookie_manager(
webkit_web_view_get_context(c->view)),
cookiepolicy_get());
updatetitle(c);
/* Do not reload. */
}
void
togglestyle(Client *c, const Arg *a)
{
enablestyle = !enablestyle;
if (enablestyle)
setstyle(c, getstyle(geturi(c)));
else
webkit_user_content_manager_remove_all_style_sheets(
webkit_web_view_get_user_content_manager(c->view));
updatetitle(c);
}
void
toggleinspector(Client *c, const Arg *a)
{
if (enableinspector) {
if (webkit_web_inspector_is_attached(c->inspector))
webkit_web_inspector_close(c->inspector);
else
webkit_web_inspector_show(c->inspector);
}
}
void
find(Client *c, const Arg *a)
{
const char *s, *f;
if (a && a->i) {
if (a->i > 0)
webkit_find_controller_search_next(c->finder);
else
webkit_find_controller_search_previous(c->finder);
} else {
s = getatom(c, AtomFind);
f = webkit_find_controller_get_search_text(c->finder);
if (g_strcmp0(f, s) == 0) /* reset search */
webkit_find_controller_search(c->finder, "", findopts,
G_MAXUINT);
webkit_find_controller_search(c->finder, s, findopts,
G_MAXUINT);
if (strcmp(s, "") == 0)
webkit_find_controller_search_finish(c->finder);
}
}
void
clicknavigate(Client *c, const Arg *a, WebKitHitTestResult *h)
{
navigate(c, a);
}
void
clicknewwindow(Client *c, const Arg *a, WebKitHitTestResult *h)
{
Arg arg;
arg.v = webkit_hit_test_result_get_link_uri(h);
newwindow(c, &arg, a->b);
}
void
clickexternplayer(Client *c, const Arg *a, WebKitHitTestResult *h)
{
Arg arg;
arg = (Arg)VIDEOPLAY(webkit_hit_test_result_get_media_uri(h));
spawn(c, &arg);
}
int
main(int argc, char *argv[])
{
Arg arg;
Client *c;
memset(&arg, 0, sizeof(arg));
/* command line args */
ARGBEGIN {
case 'a':
cookiepolicies = EARGF(usage());
break;
case 'b':
enablescrollbars = 0;
break;
case 'B':
enablescrollbars = 1;
break;
case 'c':
cookiefile = EARGF(usage());
break;
case 'd':
enablecache = 0;
break;
case 'D':
enablecache = 1;
break;
case 'e':
embed = strtol(EARGF(usage()), NULL, 0);
break;
case 'f':
runinfullscreen = 0;
break;
case 'F':
runinfullscreen = 1;
break;
case 'g':
allowgeolocation = 0;
break;
case 'G':
allowgeolocation = 1;
break;
case 'i':
loadimages = 0;
break;
case 'I':
loadimages = 1;
break;
case 'k':
kioskmode = 0;
break;
case 'K':
kioskmode = 1;
break;
case 'm':
enablestyle = 0;
break;
case 'M':
enablestyle = 1;
break;
case 'n':
enableinspector = 0;
break;
case 'N':
enableinspector = 1;
break;
case 'p':
enableplugins = 0;
break;
case 'P':
enableplugins = 1;
break;
case 'r':
scriptfile = EARGF(usage());
break;
case 's':
enablescripts = 0;
break;
case 'S':
enablescripts = 1;
break;
case 't':
stylefile = EARGF(usage());
break;
case 'u':
fulluseragent = EARGF(usage());
break;
case 'v':
die("surf-"VERSION", ©2009-2015 surf engineers, "
"see LICENSE for details\n");
case 'x':
showxid = 1;
break;
case 'z':
zoomlevel = strtof(EARGF(usage()), NULL);
break;
default:
usage();
} ARGEND;
if (argc > 0)
arg.v = argv[0];
else
arg.v = "about:blank";
setup();
c = newclient(NULL);
showview(NULL, c);
loaduri(c, &arg);
updatetitle(c);
gtk_main();
cleanup();
return 0;
}