surf/surf.c
Quentin Rameau 42829cdad0 Scrolling is now handled through WebKit
We don't have access anymore to the scrolling process and have to rely
entirely on WebKit.
We could be able to manage scrolling and bars directly by manipulating
the DOM via a WebKitExtension. To be continued…
2015-11-20 00:23:59 +01:00

1606 lines
37 KiB
C

/* See LICENSE file for copyright and license details.
*
* To understand surf, start reading main().
*/
#include <signal.h>
#include <X11/X.h>
#include <X11/Xatom.h>
#include <gtk/gtkx.h>
#include <gtk/gtk.h>
#include <gdk/gdkx.h>
#include <gdk/gdk.h>
#include <gdk/gdkkeysyms.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
#include <webkit2/webkit2.h>
#include <glib/gstdio.h>
#include <JavaScriptCore/JavaScript.h>
#include <sys/file.h>
#include <libgen.h>
#include <stdarg.h>
#include <regex.h>
#include <pwd.h>
#include <string.h>
#include "arg.h"
char *argv0;
#define LENGTH(x) (sizeof(x) / sizeof(x[0]))
#define CLEANMASK(mask) (mask & (MODKEY|GDK_SHIFT_MASK))
enum { AtomFind, AtomGo, AtomUri, AtomLast };
enum {
ClkDoc = WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT,
ClkLink = WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK,
ClkImg = WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE,
ClkMedia = WEBKIT_HIT_TEST_RESULT_CONTEXT_MEDIA,
ClkSel = WEBKIT_HIT_TEST_RESULT_CONTEXT_SELECTION,
ClkEdit = WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE,
ClkAny = ClkDoc | ClkLink | ClkImg | ClkMedia | ClkSel | ClkEdit,
};
typedef union Arg Arg;
union Arg {
gboolean b;
gint i;
const void *v;
};
typedef struct Client {
GtkWidget *win;
Window xid;
WebKitWebView *view;
WebKitWebInspector *inspector;
char *title, *linkhover;
const char *needle;
gint progress;
struct Client *next;
gboolean zoomed, fullscreen, isinspecting, sslfailed;
} Client;
typedef struct {
guint mod;
guint keyval;
void (*func)(Client *c, const Arg *arg);
const Arg arg;
} Key;
typedef struct {
unsigned int click;
unsigned int mask;
guint button;
void (*func)(Client *c, const Arg *arg);
const Arg arg;
} Button;
typedef struct {
char *regex;
char *style;
regex_t re;
} SiteStyle;
static Display *dpy;
static Atom atoms[AtomLast];
static Client *clients = NULL;
static Window embed = 0;
static gboolean showxid = FALSE;
static char winid[64];
static char togglestat[9];
static char pagestat[3];
static GTlsDatabase *tlsdb;
static int cookiepolicy;
static char *stylefile = NULL;
static void addaccelgroup(Client *c);
static void beforerequest(WebKitWebView *w, WebKitWebFrame *f,
WebKitWebResource *r, WebKitNetworkRequest *req,
WebKitNetworkResponse *resp, Client *c);
static char *buildfile(const char *path);
static char *buildpath(const char *path);
static gboolean buttonrelease(WebKitWebView *web, GdkEventButton *e, Client *c);
static void cleanup(void);
static void clipboard(Client *c, const Arg *arg);
static WebKitCookieAcceptPolicy cookiepolicy_get(void);
static char cookiepolicy_set(const WebKitCookieAcceptPolicy p);
static char *copystr(char **str, const char *src);
static WebKitWebView *createwindow(WebKitWebView *v, WebKitWebFrame *f,
Client *c);
static gboolean decidedownload(WebKitWebView *v, WebKitWebFrame *f,
WebKitNetworkRequest *r, gchar *m,
WebKitWebPolicyDecision *p, Client *c);
static gboolean decidewindow(WebKitWebView *v, WebKitWebFrame *f,
WebKitNetworkRequest *r, WebKitWebNavigationAction
*n, WebKitWebPolicyDecision *p, Client *c);
static gboolean deletion_interface(WebKitWebView *view,
WebKitDOMHTMLElement *arg1, Client *c);
static void destroyclient(Client *c);
static void destroywin(GtkWidget* w, Client *c);
static void die(const char *errstr, ...);
static void eval(Client *c, const Arg *arg);
static void find(Client *c, const Arg *arg);
static void fullscreen(Client *c, const Arg *arg);
static void geopolicyrequested(WebKitWebView *v, WebKitWebFrame *f,
WebKitGeolocationPolicyDecision *d, Client *c);
static const char *getatom(Client *c, int a);
static void gettogglestat(Client *c);
static void getpagestat(Client *c);
static char *geturi(Client *c);
static const gchar *getstyle(const char *uri);
static void setstyle(Client *c, const char *style);
static void handleplumb(Client *c, WebKitWebView *w, const gchar *uri);
static gboolean initdownload(WebKitWebView *v, WebKitDownload *o, Client *c);
static void inspector(Client *c, const Arg *arg);
static WebKitWebView *inspector_new(WebKitWebInspector *i, WebKitWebView *v,
Client *c);
static gboolean inspector_show(WebKitWebInspector *i, Client *c);
static gboolean inspector_close(WebKitWebInspector *i, Client *c);
static void inspector_finished(WebKitWebInspector *i, Client *c);
static gboolean keypress(GtkAccelGroup *group, GObject *obj, guint key,
GdkModifierType mods, Client *c);
static void linkhover(WebKitWebView *v, const char* t, const char* l,
Client *c);
static void loadstatuschange(WebKitWebView *view, GParamSpec *pspec,
Client *c);
static void loaduri(Client *c, const Arg *arg);
static void navigate(Client *c, const Arg *arg);
static Client *newclient(void);
static void newwindow(Client *c, const Arg *arg, gboolean noembed);
static void pasteuri(GtkClipboard *clipboard, const char *text, gpointer d);
static gboolean contextmenu(WebKitWebView *view, GtkWidget *menu,
WebKitHitTestResult *target, gboolean keyboard,
Client *c);
static void menuactivate(GtkMenuItem *item, Client *c);
static void print(Client *c, const Arg *arg);
static GdkFilterReturn processx(GdkXEvent *xevent, GdkEvent *event,
gpointer d);
static void progresschange(WebKitWebView *view, GParamSpec *pspec, Client *c);
static void linkopen(Client *c, const Arg *arg);
static void linkopenembed(Client *c, const Arg *arg);
static void reload(Client *c, const Arg *arg);
static void scroll_h(Client *c, const Arg *arg);
static void scroll_v(Client *c, const Arg *arg);
static void scroll(GtkAdjustment *a, const Arg *arg);
static void setatom(Client *c, int a, const char *v);
static void setup(void);
static void sigchld(int unused);
static void spawn(Client *c, const Arg *arg);
static void stop(Client *c, const Arg *arg);
static void titlechange(WebKitWebView *view, GParamSpec *pspec, Client *c);
static void titlechangeleave(void *a, void *b, Client *c);
static void toggle(Client *c, const Arg *arg);
static void togglecookiepolicy(Client *c, const Arg *arg);
static void togglegeolocation(Client *c, const Arg *arg);
static void togglescrollbars(Client *c, const Arg *arg);
static void togglestyle(Client *c, const Arg *arg);
static void updatetitle(Client *c);
static void updatewinid(Client *c);
static void usage(void);
static void windowobjectcleared(GtkWidget *w, WebKitWebFrame *frame,
JSContextRef js, JSObjectRef win, Client *c);
static void zoom(Client *c, const Arg *arg);
/* configuration, allows nested code to access above variables */
#include "config.h"
void
addaccelgroup(Client *c)
{
int i;
GtkAccelGroup *group = gtk_accel_group_new();
GClosure *closure;
for (i = 0; i < LENGTH(keys); i++) {
closure = g_cclosure_new(G_CALLBACK(keypress), c, NULL);
gtk_accel_group_connect(group, keys[i].keyval, keys[i].mod, 0,
closure);
}
gtk_window_add_accel_group(GTK_WINDOW(c->win), group);
}
void
beforerequest(WebKitWebView *w, WebKitWebFrame *f, WebKitWebResource *r,
WebKitNetworkRequest *req, WebKitNetworkResponse *resp,
Client *c)
{
const gchar *uri = webkit_network_request_get_uri(req);
int i, isascii = 1;
if (g_str_has_suffix(uri, "/favicon.ico"))
webkit_network_request_set_uri(req, "about:blank");
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, w, uri);
}
}
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;
}
gboolean
buttonrelease(WebKitWebView *web, GdkEventButton *e, Client *c)
{
WebKitHitTestResultContext context;
WebKitHitTestResult *result;
Arg arg;
unsigned int i;
result = webkit_web_view_get_hit_test_result(web, e);
g_object_get(result, "context", &context, NULL);
g_object_get(result, "link-uri", &arg.v, NULL);
for (i = 0; i < LENGTH(buttons); i++) {
if (context & buttons[i].click
&& e->button == buttons[i].button
&& CLEANMASK(e->state) == CLEANMASK(buttons[i].mask)
&& buttons[i].func) {
buttons[i].func(c, buttons[i].click == ClkLink
&& buttons[i].arg.i == 0 ? &arg : &buttons[i].arg);
return true;
}
}
return false;
}
void
cleanup(void)
{
while (clients)
destroyclient(clients);
g_free(cookiefile);
g_free(scriptfile);
g_free(stylefile);
}
WebKitCookieAcceptPolicy
cookiepolicy_get(void)
{
switch (cookiepolicies[cookiepolicy]) {
case 'a':
return WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
case '@':
return WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
case 'A':
default:
break;
}
return WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
}
char
cookiepolicy_set(const WebKitCookieAcceptPolicy ep)
{
switch (ep) {
case WEBKIT_COOKIE_POLICY_ACCEPT_NEVER:
return 'a';
case WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY:
return '@';
case WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS:
default:
break;
}
return 'A';
}
void
evalscript(JSContextRef js, char *script, char* scriptname)
{
JSStringRef jsscript, jsscriptname;
JSValueRef exception = NULL;
jsscript = JSStringCreateWithUTF8CString(script);
jsscriptname = JSStringCreateWithUTF8CString(scriptname);
JSEvaluateScript(js, jsscript, JSContextGetGlobalObject(js),
jsscriptname, 0, &exception);
JSStringRelease(jsscript);
JSStringRelease(jsscriptname);
}
void
runscript(WebKitWebFrame *frame)
{
char *script;
GError *error;
if (g_file_get_contents(scriptfile, &script, NULL, &error)) {
evalscript(webkit_web_frame_get_global_context(frame), script,
scriptfile);
}
}
void
clipboard(Client *c, const Arg *arg)
{
gboolean paste = *(gboolean *)arg;
if (paste) {
gtk_clipboard_request_text(gtk_clipboard_get(
GDK_SELECTION_PRIMARY),
pasteuri, c);
} else {
gtk_clipboard_set_text(gtk_clipboard_get(
GDK_SELECTION_PRIMARY), c->linkhover
? c->linkhover : geturi(c), -1);
}
}
char *
copystr(char **str, const char *src)
{
char *tmp;
tmp = g_strdup(src);
if (str && *str) {
g_free(*str);
*str = tmp;
}
return tmp;
}
WebKitWebView *
createwindow(WebKitWebView *v, WebKitWebFrame *f, Client *c)
{
Client *n = newclient();
return n->view;
}
gboolean
decidedownload(WebKitWebView *v, WebKitWebFrame *f, WebKitNetworkRequest *r,
gchar *m, WebKitWebPolicyDecision *p, Client *c)
{
if (!webkit_web_view_can_show_mime_type(v, m)) {
webkit_web_policy_decision_download(p);
return TRUE;
}
return FALSE;
}
gboolean
decidewindow(WebKitWebView *view, WebKitWebFrame *f, WebKitNetworkRequest *r,
WebKitWebNavigationAction *n, WebKitWebPolicyDecision *p,
Client *c)
{
Arg arg;
if (webkit_web_navigation_action_get_reason(n)
== WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
webkit_web_policy_decision_ignore(p);
arg.v = (void *)webkit_network_request_get_uri(r);
newwindow(NULL, &arg, 0);
return TRUE;
}
return FALSE;
}
gboolean
deletion_interface(WebKitWebView *view, WebKitDOMHTMLElement *arg1, Client *c)
{
return FALSE;
}
void
destroyclient(Client *c)
{
Client *p;
webkit_web_view_stop_loading(c->view);
gtk_widget_destroy(GTK_WIDGET(c->view));
gtk_widget_destroy(c->scroll);
gtk_widget_destroy(c->vbox);
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);
if (clients == NULL)
gtk_main_quit();
}
void
destroywin(GtkWidget* w, Client *c)
{
destroyclient(c);
}
void
die(const char *errstr, ...)
{
va_list ap;
va_start(ap, errstr);
vfprintf(stderr, errstr, ap);
va_end(ap);
exit(EXIT_FAILURE);
}
void
find(Client *c, const Arg *arg)
{
const char *s;
s = getatom(c, AtomFind);
gboolean forward = *(gboolean *)arg;
webkit_web_view_search_text(c->view, s, FALSE, forward, TRUE);
}
void
fullscreen(Client *c, const Arg *arg)
{
if (c->fullscreen)
gtk_window_unfullscreen(GTK_WINDOW(c->win));
else
gtk_window_fullscreen(GTK_WINDOW(c->win));
c->fullscreen = !c->fullscreen;
}
void
geopolicyrequested(WebKitWebView *v, WebKitWebFrame *f,
WebKitGeolocationPolicyDecision *d, Client *c)
{
if (allowgeolocation)
webkit_geolocation_policy_allow(d);
else
webkit_geolocation_policy_deny(d);
}
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;
}
char *
geturi(Client *c)
{
char *uri;
if (!(uri = (char *)webkit_web_view_get_uri(c->view)))
uri = "about:blank";
return uri;
}
const gchar *
getstyle(const char *uri)
{
int i;
if (stylefile != NULL)
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 *style)
{
WebKitWebSettings *settings = webkit_web_view_get_settings(c->view);
g_object_set(G_OBJECT(settings), "user-stylesheet-uri", style, NULL);
}
void
handleplumb(Client *c, WebKitWebView *w, const gchar *uri)
{
Arg arg;
webkit_web_view_stop_loading(w);
arg = (Arg)PLUMB((char *)uri);
spawn(c, &arg);
}
gboolean
initdownload(WebKitWebView *view, WebKitDownload *o, Client *c)
{
Arg arg;
updatewinid(c);
arg = (Arg)DOWNLOAD((char *)webkit_download_get_uri(o), geturi(c));
spawn(c, &arg);
return FALSE;
}
void
inspector(Client *c, const Arg *arg)
{
if (enableinspector) {
if (c->isinspecting)
webkit_web_inspector_close(c->inspector);
else
webkit_web_inspector_show(c->inspector);
}
}
WebKitWebView *
inspector_new(WebKitWebInspector *i, WebKitWebView *v, Client *c)
{
return WEBKIT_WEB_VIEW(webkit_web_view_new());
}
gboolean
inspector_show(WebKitWebInspector *i, Client *c)
{
WebKitWebView *w;
if (c->isinspecting)
return false;
w = webkit_web_inspector_get_web_view(i);
gtk_paned_pack2(GTK_PANED(c->pane), GTK_WIDGET(w), TRUE, TRUE);
gtk_widget_show(GTK_WIDGET(w));
c->isinspecting = true;
return true;
}
gboolean
inspector_close(WebKitWebInspector *i, Client *c)
{
GtkWidget *w;
if (!c->isinspecting)
return false;
w = GTK_WIDGET(webkit_web_inspector_get_web_view(i));
gtk_widget_hide(w);
gtk_widget_destroy(w);
c->isinspecting = false;
return true;
}
void
inspector_finished(WebKitWebInspector *i, Client *c)
{
g_free(c->inspector);
}
gboolean
keypress(GtkAccelGroup *group, GObject *obj, guint key, GdkModifierType mods,
Client *c)
{
guint i;
gboolean processed = FALSE;
mods = CLEANMASK(mods);
key = gdk_keyval_to_lower(key);
updatewinid(c);
for (i = 0; i < LENGTH(keys); i++) {
if (key == keys[i].keyval
&& mods == keys[i].mod
&& keys[i].func) {
keys[i].func(c, &(keys[i].arg));
processed = TRUE;
}
}
return processed;
}
void
linkhover(WebKitWebView *v, const char* t, const char* l, Client *c)
{
if (l) {
c->linkhover = copystr(&c->linkhover, l);
} else if (c->linkhover) {
free(c->linkhover);
c->linkhover = NULL;
}
updatetitle(c);
}
void
loadstatuschange(WebKitWebView *view, GParamSpec *pspec, Client *c)
{
WebKitWebFrame *frame;
WebKitWebDataSource *src;
WebKitNetworkRequest *request;
SoupMessage *msg;
char *uri;
switch (webkit_web_view_get_load_status (c->view)) {
case WEBKIT_LOAD_COMMITTED:
uri = geturi(c);
if (strstr(uri, "https://") == uri) {
frame = webkit_web_view_get_main_frame(c->view);
src = webkit_web_frame_get_data_source(frame);
request = webkit_web_data_source_get_request(src);
msg = webkit_network_request_get_message(request);
c->sslfailed = !(soup_message_get_flags(msg)
& SOUP_MESSAGE_CERTIFICATE_TRUSTED);
}
setatom(c, AtomUri, uri);
if (enablestyle)
setstyle(c, getstyle(uri));
break;
case WEBKIT_LOAD_FINISHED:
c->progress = 100;
updatetitle(c);
break;
default:
break;
}
}
void
loaduri(Client *c, const Arg *arg)
{
char *u = NULL, *rp;
const char *uri = (char *)arg->v;
Arg a = { .b = FALSE };
struct stat st;
if (strcmp(uri, "") == 0)
return;
/* In case it's a file path. */
if (stat(uri, &st) == 0) {
rp = realpath(uri, NULL);
u = g_strdup_printf("file://%s", rp);
free(rp);
} else {
u = g_strrstr(uri, "://") ? g_strdup(uri)
: g_strdup_printf("http://%s", uri);
}
setatom(c, AtomUri, uri);
/* prevents endless loop */
if (strcmp(u, geturi(c)) == 0) {
reload(c, &a);
} else {
webkit_web_view_load_uri(c->view, u);
c->progress = 0;
c->title = copystr(&c->title, u);
updatetitle(c);
}
g_free(u);
}
void
navigate(Client *c, const Arg *arg)
{
int steps = *(int *)arg;
webkit_web_view_go_back_or_forward(c->view, steps);
}
Client *
newclient(void)
{
Client *c;
WebKitWebSettings *settings;
GdkGeometry hints = { 1, 1 };
GdkScreen *screen;
GdkWindow *gwin;
gdouble dpi;
char *ua;
if (!(c = calloc(1, sizeof(Client))))
die("Cannot malloc!\n");
c->title = NULL;
c->progress = 100;
/* Window */
if (embed) {
c->win = gtk_plug_new(embed);
} else {
c->win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
/* TA: 20091214: Despite what the GNOME docs say, the ICCCM
* is always correct, so we should still call this function.
* But when doing so, we *must* differentiate between a
* WM_CLASS and a resource on the window. By convention, the
* window class (WM_CLASS) is capped, while the resource is in
* lowercase. Both these values come as a pair.
*/
gtk_window_set_wmclass(GTK_WINDOW(c->win), "surf", "Surf");
/* TA: 20091214: And set the role here as well -- so that
* sessions can pick this up.
*/
gtk_window_set_role(GTK_WINDOW(c->win), "Surf");
}
gtk_window_set_default_size(GTK_WINDOW(c->win), 800, 600);
g_signal_connect(G_OBJECT(c->win),
"destroy",
G_CALLBACK(destroywin), c);
g_signal_connect(G_OBJECT(c->win),
"leave_notify_event",
G_CALLBACK(titlechangeleave), c);
if (!kioskmode)
addaccelgroup(c);
/* Webview */
c->view = WEBKIT_WEB_VIEW(webkit_web_view_new());
g_signal_connect(G_OBJECT(c->view),
"notify::title",
G_CALLBACK(titlechange), c);
g_signal_connect(G_OBJECT(c->view),
"hovering-over-link",
G_CALLBACK(linkhover), c);
g_signal_connect(G_OBJECT(c->view),
"geolocation-policy-decision-requested",
G_CALLBACK(geopolicyrequested), c);
g_signal_connect(G_OBJECT(c->view),
"create-web-view",
G_CALLBACK(createwindow), c);
g_signal_connect(G_OBJECT(c->view),
"new-window-policy-decision-requested",
G_CALLBACK(decidewindow), c);
g_signal_connect(G_OBJECT(c->view),
"mime-type-policy-decision-requested",
G_CALLBACK(decidedownload), c);
g_signal_connect(G_OBJECT(c->view),
"window-object-cleared",
G_CALLBACK(windowobjectcleared), c);
g_signal_connect(G_OBJECT(c->view),
"notify::load-status",
G_CALLBACK(loadstatuschange), c);
g_signal_connect(G_OBJECT(c->view),
"notify::progress",
G_CALLBACK(progresschange), c);
g_signal_connect(G_OBJECT(c->view),
"download-requested",
G_CALLBACK(initdownload), c);
g_signal_connect(G_OBJECT(c->view),
"button-release-event",
G_CALLBACK(buttonrelease), c);
g_signal_connect(G_OBJECT(c->view),
"context-menu",
G_CALLBACK(contextmenu), c);
g_signal_connect(G_OBJECT(c->view),
"resource-request-starting",
G_CALLBACK(beforerequest), c);
g_signal_connect(G_OBJECT(c->view),
"should-show-delete-interface-for-element",
G_CALLBACK(deletion_interface), c);
/* Arranging */
gtk_container_add(GTK_CONTAINER(c->win), GTK_WIDGET(c->view));
/* Setup */
gtk_widget_grab_focus(GTK_WIDGET(c->view));
gtk_widget_show(GTK_WIDGET(c->view));
gtk_widget_show(c->win);
gwin = gtk_widget_get_window(GTK_WIDGET(c->win));
c->xid = gdk_x11_window_get_xid(gwin);
gtk_window_set_geometry_hints(GTK_WINDOW(c->win), NULL, &hints,
GDK_HINT_MIN_SIZE);
gdk_window_set_events(gwin, GDK_ALL_EVENTS_MASK);
gdk_window_add_filter(gwin, processx, c);
webkit_web_view_set_full_content_zoom(c->view, TRUE);
runscript(frame);
settings = webkit_web_view_get_settings(c->view);
if (!(ua = getenv("SURF_USERAGENT")))
ua = useragent;
g_object_set(G_OBJECT(settings), "user-agent", ua, NULL);
g_object_set(G_OBJECT(settings),
"auto-load-images", loadimages, NULL);
g_object_set(G_OBJECT(settings),
"enable-plugins", enableplugins, NULL);
g_object_set(G_OBJECT(settings),
"enable-scripts", enablescripts, NULL);
g_object_set(G_OBJECT(settings),
"enable-spatial-navigation", enablespatialbrowsing, NULL);
g_object_set(G_OBJECT(settings),
"enable-developer-extras", enableinspector, NULL);
g_object_set(G_OBJECT(settings),
"enable-default-context-menu", kioskmode ^ 1, NULL);
g_object_set(G_OBJECT(settings),
"default-font-size", defaultfontsize, NULL);
g_object_set(G_OBJECT(settings),
"resizable-text-areas", 1, NULL);
if (enablestyle)
setstyle(c, getstyle("about:blank"));
/*
* While stupid, CSS specifies that a pixel represents 1/96 of an inch.
* This ensures websites are not unusably small with a high DPI screen.
* It is equivalent to firefox's "layout.css.devPixelsPerPx" setting.
*/
if (zoomto96dpi) {
screen = gdk_window_get_screen(gwin);
dpi = gdk_screen_get_resolution(screen);
if (dpi != -1) {
g_object_set(G_OBJECT(settings),
"enforce-96-dpi", true, NULL);
webkit_web_view_set_zoom_level(c->view, dpi/96);
}
}
/* This might conflict with _zoomto96dpi_. */
if (zoomlevel != 1.0)
webkit_web_view_set_zoom_level(c->view, zoomlevel);
if (enableinspector) {
c->inspector = webkit_web_view_get_inspector(c->view);
g_signal_connect(G_OBJECT(c->inspector), "inspect-web-view",
G_CALLBACK(inspector_new), c);
g_signal_connect(G_OBJECT(c->inspector), "show-window",
G_CALLBACK(inspector_show), c);
g_signal_connect(G_OBJECT(c->inspector), "close-window",
G_CALLBACK(inspector_close), c);
g_signal_connect(G_OBJECT(c->inspector), "finished",
G_CALLBACK(inspector_finished), c);
c->isinspecting = false;
}
if (runinfullscreen)
fullscreen(c, NULL);
setatom(c, AtomFind, "");
setatom(c, AtomUri, "about:blank");
if (hidebackground)
webkit_web_view_set_transparent(c->view, TRUE);
c->next = clients;
clients = c;
if (showxid) {
gdk_display_sync(gtk_widget_get_display(c->win));
printf("%lu\n", c->xid);
fflush(NULL);
if (fclose(stdout) != 0) {
die("Error closing stdout");
}
}
return c;
}
void
newwindow(Client *c, const Arg *arg, gboolean noembed)
{
guint i = 0;
const char *cmd[18], *uri;
const Arg a = { .v = (void *)cmd };
char tmp[64];
cmd[i++] = argv0;
cmd[i++] = "-a";
cmd[i++] = cookiepolicies;
if (!enablescrollbars)
cmd[i++] = "-b";
if (embed && !noembed) {
cmd[i++] = "-e";
snprintf(tmp, LENGTH(tmp), "%u", (int)embed);
cmd[i++] = tmp;
}
if (!allowgeolocation)
cmd[i++] = "-g";
if (!loadimages)
cmd[i++] = "-i";
if (kioskmode)
cmd[i++] = "-k";
if (!enableplugins)
cmd[i++] = "-p";
if (!enablescripts)
cmd[i++] = "-s";
if (showxid)
cmd[i++] = "-x";
if (enablediskcache)
cmd[i++] = "-D";
cmd[i++] = "-c";
cmd[i++] = cookiefile;
cmd[i++] = "--";
uri = arg->v ? (char *)arg->v : c->linkhover;
if (uri)
cmd[i++] = uri;
cmd[i++] = NULL;
spawn(NULL, &a);
}
gboolean
contextmenu(WebKitWebView *view, GtkWidget *menu, WebKitHitTestResult *target,
gboolean keyboard, Client *c)
{
GList *items = gtk_container_get_children(GTK_CONTAINER(GTK_MENU(menu)));
for (GList *l = items; l; l = l->next)
g_signal_connect(l->data, "activate", G_CALLBACK(menuactivate), c);
g_list_free(items);
return FALSE;
}
void
menuactivate(GtkMenuItem *item, Client *c)
{
/*
* context-menu-action-2000 open link
* context-menu-action-1 open link in window
* context-menu-action-2 download linked file
* context-menu-action-3 copy link location
* context-menu-action-7 copy image address
* context-menu-action-13 reload
* context-menu-action-10 back
* context-menu-action-11 forward
* context-menu-action-12 stop
*/
const gchar *name, *uri;
GtkClipboard *prisel, *clpbrd;
name = gtk_actionable_get_action_name(GTK_ACTIONABLE(item));
if (name == NULL)
return;
if (!g_strcmp0(name, "context-menu-action-3")) {
prisel = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
gtk_clipboard_set_text(prisel, c->linkhover, -1);
} else if (!g_strcmp0(name, "context-menu-action-7")) {
prisel = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
clpbrd = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
uri = gtk_clipboard_wait_for_text(clpbrd);
if (uri)
gtk_clipboard_set_text(prisel, uri, -1);
}
}
void
pasteuri(GtkClipboard *clipboard, const char *text, gpointer d)
{
Arg arg = {.v = text };
if (text != NULL)
loaduri((Client *) d, &arg);
}
void
print(Client *c, const Arg *arg)
{
webkit_web_frame_print(webkit_web_view_get_main_frame(c->view));
}
GdkFilterReturn
processx(GdkXEvent *e, GdkEvent *event, gpointer d)
{
Client *c = (Client *)d;
XPropertyEvent *ev;
Arg arg;
if (((XEvent *)e)->type == PropertyNotify) {
ev = &((XEvent *)e)->xproperty;
if (ev->state == PropertyNewValue) {
if (ev->atom == atoms[AtomFind]) {
arg.b = TRUE;
find(c, &arg);
return GDK_FILTER_REMOVE;
} else if (ev->atom == atoms[AtomGo]) {
arg.v = getatom(c, AtomGo);
loaduri(c, &arg);
return GDK_FILTER_REMOVE;
}
}
}
return GDK_FILTER_CONTINUE;
}
void
progresschange(WebKitWebView *view, GParamSpec *pspec, Client *c)
{
c->progress = webkit_web_view_get_progress(c->view) * 100;
updatetitle(c);
}
void
linkopen(Client *c, const Arg *arg)
{
newwindow(NULL, arg, 1);
}
void
linkopenembed(Client *c, const Arg *arg)
{
newwindow(NULL, arg, 0);
}
void
reload(Client *c, const Arg *arg)
{
gboolean nocache = *(gboolean *)arg;
if (nocache)
webkit_web_view_reload_bypass_cache(c->view);
else
webkit_web_view_reload(c->view);
}
void
scroll_h(Client *c, const Arg *arg)
{
scroll(gtk_scrolled_window_get_hadjustment(
GTK_SCROLLED_WINDOW(c->scroll)), arg);
}
void
scroll_v(Client *c, const Arg *arg)
{
scroll(gtk_scrolled_window_get_vadjustment(
GTK_SCROLLED_WINDOW(c->scroll)), arg);
}
void
scroll(GtkAdjustment *a, const Arg *arg)
{
gdouble v;
v = gtk_adjustment_get_value(a);
switch (arg->i) {
case +10000:
case -10000:
v += gtk_adjustment_get_page_increment(a) * (arg->i / 10000);
break;
case +20000:
case -20000:
default:
v += gtk_adjustment_get_step_increment(a) * arg->i;
}
v = MAX(v, 0.0);
v = MIN(v, gtk_adjustment_get_upper(a) -
gtk_adjustment_get_page_size(a));
gtk_adjustment_set_value(a, v);
}
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);
}
void
setup(void)
{
int i;
char *styledirfile, *stylepath;
WebKitWebContext *context;
GError *error = NULL;
/* 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);
cachefolder = buildpath(cachefolder);
if (stylefile == NULL) {
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;
}
styledirfile = g_strconcat(styledir, "/",
styles[i].style, NULL);
stylepath = buildfile(styledirfile);
styles[i].style = g_strconcat("file://", stylepath,
NULL);
g_free(styledirfile);
g_free(stylepath);
}
g_free(styledir);
} else {
stylepath = buildfile(stylefile);
stylefile = g_strconcat("file://", stylepath, NULL);
g_free(stylepath);
}
/* cookie policy */
webkit_cookie_manager_set_persistent_storage(
webkit_web_context_get_cookie_manager(context), cookiefile,
WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT);
webkit_cookie_manager_set_accept_policy(
webkit_web_context_get_cookie_manager(context),
cookiepolicy_get());
/* disk cache */
webkit_web_context_set_cache_model(context, enablecache ?
WEBKIT_CACHE_MODEL_WEB_BROWSER :
WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER);
/* ssl */
webkit_web_context_set_tls_errors_policy(context, strictssl ?
WEBKIT_TLS_ERRORS_POLICY_FAIL :
WEBKIT_TLS_ERRORS_POLICY_IGNORE);
}
void
sigchld(int unused)
{
if (signal(SIGCHLD, sigchld) == SIG_ERR)
die("Can't install SIGCHLD handler");
while (0 < waitpid(-1, NULL, WNOHANG));
}
void
spawn(Client *c, const Arg *arg)
{
if (fork() == 0) {
if (dpy)
close(ConnectionNumber(dpy));
setsid();
execvp(((char **)arg->v)[0], (char **)arg->v);
fprintf(stderr, "surf: execvp %s", ((char **)arg->v)[0]);
perror(" failed");
exit(0);
}
}
void
eval(Client *c, const Arg *arg)
{
WebKitWebFrame *frame = webkit_web_view_get_main_frame(c->view);
evalscript(webkit_web_frame_get_global_context(frame),
((char **)arg->v)[0], "");
}
void
stop(Client *c, const Arg *arg)
{
webkit_web_view_stop_loading(c->view);
}
void
titlechange(WebKitWebView *view, GParamSpec *pspec, Client *c)
{
const gchar *t = webkit_web_view_get_title(view);
if (t) {
c->title = copystr(&c->title, t);
updatetitle(c);
}
}
void
titlechangeleave(void *a, void *b, Client *c)
{
c->linkhover = NULL;
updatetitle(c);
}
void
toggle(Client *c, const Arg *arg)
{
WebKitWebSettings *settings;
char *name = (char *)arg->v;
gboolean value;
Arg a = { .b = FALSE };
settings = webkit_web_view_get_settings(c->view);
g_object_get(G_OBJECT(settings), name, &value, NULL);
g_object_set(G_OBJECT(settings), name, !value, NULL);
reload(c, &a);
}
void
togglecookiepolicy(Client *c, const Arg *arg)
{
++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
togglegeolocation(Client *c, const Arg *arg)
{
Arg a = { .b = FALSE };
allowgeolocation ^= 1;
reload(c, &a);
}
void
twitch(Client *c, const Arg *arg)
{
GtkAdjustment *a;
gdouble v;
a = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(
c->scroll));
v = gtk_adjustment_get_value(a);
v += arg->i;
v = MAX(v, 0.0);
v = MIN(v, gtk_adjustment_get_upper(a) -
gtk_adjustment_get_page_size(a));
gtk_adjustment_set_value(a, v);
}
void
togglescrollbars(Client *c, const Arg *arg)
{
GtkPolicyType vspolicy;
Arg a;
gtk_scrolled_window_get_policy(GTK_SCROLLED_WINDOW(c->scroll), NULL,
&vspolicy);
if (vspolicy == GTK_POLICY_AUTOMATIC) {
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(c->scroll),
GTK_POLICY_NEVER,
GTK_POLICY_NEVER);
} else {
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(c->scroll),
GTK_POLICY_AUTOMATIC,
GTK_POLICY_AUTOMATIC);
a.i = +1;
twitch(c, &a);
a.i = -1;
twitch(c, &a);
}
}
void
togglestyle(Client *c, const Arg *arg)
{
enablestyle = !enablestyle;
setstyle(c, enablestyle ? getstyle(geturi(c)) : "");
updatetitle(c);
}
void
gettogglestat(Client *c)
{
gboolean value;
int p = 0;
WebKitWebSettings *settings = webkit_web_view_get_settings(c->view);
togglestat[p++] = cookiepolicy_set(cookiepolicy_get());
g_object_get(G_OBJECT(settings), "enable-caret-browsing", &value,
NULL);
togglestat[p++] = value? 'C': 'c';
togglestat[p++] = allowgeolocation? 'G': 'g';
togglestat[p++] = enablediskcache? 'D': 'd';
g_object_get(G_OBJECT(settings), "auto-load-images", &value, NULL);
togglestat[p++] = value? 'I': 'i';
g_object_get(G_OBJECT(settings), "enable-scripts", &value, NULL);
togglestat[p++] = value? 'S': 's';
g_object_get(G_OBJECT(settings), "enable-plugins", &value, NULL);
togglestat[p++] = value? 'V': 'v';
togglestat[p++] = enablestyle ? 'M': 'm';
togglestat[p] = '\0';
}
void
getpagestat(Client *c)
{
const char *uri = geturi(c);
if (strstr(uri, "https://") == uri)
pagestat[0] = c->sslfailed ? 'U' : 'T';
else
pagestat[0] = '-';
pagestat[1] = '\0';
}
void
updatetitle(Client *c)
{
char *t;
if (showindicators) {
gettogglestat(c);
getpagestat(c);
if (c->linkhover) {
t = g_strdup_printf("%s:%s | %s", togglestat, pagestat,
c->linkhover);
} else if (c->progress != 100) {
t = g_strdup_printf("[%i%%] %s:%s | %s", c->progress,
togglestat, pagestat,
c->title == NULL ? "" : c->title);
} else {
t = g_strdup_printf("%s:%s | %s", togglestat, pagestat,
c->title == NULL ? "" : c->title);
}
gtk_window_set_title(GTK_WINDOW(c->win), t);
g_free(t);
} else {
gtk_window_set_title(GTK_WINDOW(c->win), (c->title == NULL) ?
"" : c->title);
}
}
void
updatewinid(Client *c)
{
snprintf(winid, LENGTH(winid), "%lu", c->xid);
}
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
windowobjectcleared(GtkWidget *w, WebKitWebFrame *frame, JSContextRef js,
JSObjectRef win, Client *c)
{
runscript(frame);
}
void
zoom(Client *c, const Arg *arg)
{
c->zoomed = TRUE;
if (arg->i < 0) {
/* zoom out */
webkit_web_view_zoom_out(c->view);
} else if (arg->i > 0) {
/* zoom in */
webkit_web_view_zoom_in(c->view);
} else {
/* reset */
c->zoomed = FALSE;
webkit_web_view_set_zoom_level(c->view, 1.0);
}
}
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':
enablediskcache = 0;
break;
case 'D':
enablediskcache = 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':
useragent = EARGF(usage());
break;
case 'v':
die("surf-"VERSION", ©2009-2015 surf engineers, "
"see LICENSE for details\n");
case 'x':
showxid = TRUE;
break;
case 'z':
zoomlevel = strtof(EARGF(usage()), NULL);
break;
default:
usage();
} ARGEND;
if (argc > 0)
arg.v = argv[0];
setup();
c = newclient();
if (arg.v)
loaduri(clients, &arg);
else
updatetitle(c);
gtk_main();
cleanup();
return EXIT_SUCCESS;
}