Hi y'all,

The attached patch adds support for user styles and scripts, two WebKit
APIs for URL-based style/script injection. Mostly just parsing and
injection of those here, but also a new structure (UserContent) and a
bug:

When applying styles (old SiteSpecific ones, from styledir), current
implementation does webkit_user_content_manager_remove_all_style_sheets
and thus disables the ones I've added. How do we approach this? I'm
thinking either

- Storing the applied script inside the SiteSpecific object and only
  removing it, instead of all styles. Lightweight, but changes the
  struct layout.

- Re-injecting all the styles on every page load, along with the old
  site-specific styles. That's a lot of allocations.

- Moving to the user styles infra exclusively. Not a good decision.
  Deprecating stuff is not fun. And the user content allowlist patterns
  are not as flexible as regex either.

>From e0f9c36dca1a1ca90d9b1777d374af142b89ef92 Mon Sep 17 00:00:00 2001
From: Artyom Bologov <g...@aartaka.me>
Date: Sun, 20 Apr 2025 23:11:10 +0400
Subject: [PATCH] Support user scripts and user styles.

---
 config.def.h | 11 ++++++++++
 surf.c       | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 72 insertions(+)

diff --git a/config.def.h b/config.def.h
index 9b0f6f6..8ee9808 100644
--- a/config.def.h
+++ b/config.def.h
@@ -3,6 +3,7 @@ static int surfuseragent    = 1;  /* Append Surf version to default WebKit user
 static char *fulluseragent  = ""; /* Or override the whole user agent string */
 static char *scriptfile     = "~/.surf/script.js";
 static char *styledir       = "~/.surf/styles/";
+static char *usercontentdir = "~/.surf/usercontent/";
 static char *certdir        = "~/.surf/certificates/";
 static char *cachedir       = "~/.surf/cache/";
 static char *cookiefile     = "~/.surf/cookies.txt";
@@ -121,6 +122,16 @@ static SiteSpecific certs[] = {
 	{ "://suckless\\.org/", "suckless.org.crt" },
 };
 
+static UserContent userstyles[] = {
+	/* patterns           file in $usercontentdir   priority*/
+	{ "*://*/*",          "default.css",            1}
+};
+
+static UserContent userscripts[] = {
+	/* patterns           file in $usercontentdir   priority*/
+	{ "*://*/*",          "default.js",             1}
+};
+
 #define MODKEY GDK_CONTROL_MASK
 
 /* hotkeys */
diff --git a/surf.c b/surf.c
index 7a0f624..188a3eb 100644
--- a/surf.c
+++ b/surf.c
@@ -144,6 +144,13 @@ typedef struct {
 	regex_t re;
 } SiteSpecific;
 
+typedef struct {
+	const char *allow_pattern;
+	char *file;
+	int prio;
+	char *slurped;
+} UserContent;
+
 /* Surf */
 static void die(const char *errstr, ...);
 static void usage(void);
@@ -329,7 +336,9 @@ setup(void)
 {
 	GIOChannel *gchanin;
 	GdkDisplay *gdpy;
+	char *contentpath, *slurped;
 	int i, j;
+	size_t l;
 
 	/* clean up any zombies immediately */
 	sigchld(0);
@@ -354,6 +363,7 @@ setup(void)
 	/* dirs and files */
 	cookiefile = buildfile(cookiefile);
 	scriptfile = buildfile(scriptfile);
+	usercontentdir = buildpath(usercontentdir);
 	certdir    = buildpath(certdir);
 	if (curconfig[Ephemeral].val.i)
 		cachedir = NULL;
@@ -403,6 +413,20 @@ setup(void)
 		stylefile = buildfile(stylefile);
 	}
 
+	usercontentdir = buildpath(usercontentdir);
+
+	for (i = 0; i < LENGTH(userstyles); ++i) {
+		contentpath = g_strconcat(usercontentdir, "/", userstyles[i].file, NULL);
+		g_file_get_contents(contentpath, &slurped, &l, NULL);
+		userstyles[i].slurped = slurped;
+	}
+
+	for (i = 0; i < LENGTH(userscripts); ++i) {
+		contentpath = g_strconcat(usercontentdir, "/", userscripts[i].file, NULL);
+		g_file_get_contents(contentpath, &slurped, &l, NULL);
+		userscripts[i].slurped = slurped;
+	}
+
 	for (i = 0; i < LENGTH(uriparams); ++i) {
 		if (regcomp(&(uriparams[i].re), uriparams[i].uri,
 		    REG_EXTENDED)) {
@@ -1093,6 +1117,7 @@ cleanup(void)
 	g_free(cookiefile);
 	g_free(scriptfile);
 	g_free(stylefile);
+	g_free(usercontentdir);
 	g_free(cachedir);
 	XCloseDisplay(dpy);
 }
@@ -1100,11 +1125,14 @@ cleanup(void)
 WebKitWebView *
 newview(Client *c, WebKitWebView *rv)
 {
+	int i;
 	WebKitWebView *v;
 	WebKitSettings *settings;
 	WebKitWebContext *context;
 	WebKitCookieManager *cookiemanager;
 	WebKitUserContentManager *contentmanager;
+	WebKitUserStyleSheet *style;
+	WebKitUserScript *script;
 
 	/* Webview */
 	if (rv) {
@@ -1142,6 +1170,39 @@ newview(Client *c, WebKitWebView *rv)
 
 		contentmanager = webkit_user_content_manager_new();
 
+		for (i = 0; i < LENGTH(userstyles); ++i) {
+			if (userstyles[i].slurped) {
+				const char *allowlist[2] = {userstyles[i].allow_pattern, NULL};
+				style = webkit_user_style_sheet_new(
+					userstyles[i].slurped,
+					WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
+					(userstyles[i].prio
+					 ? WEBKIT_USER_STYLE_LEVEL_USER
+					 : WEBKIT_USER_STYLE_LEVEL_AUTHOR),
+					allowlist,
+					NULL);
+				webkit_user_content_manager_add_style_sheet(contentmanager, style);
+				webkit_user_style_sheet_unref(style);
+			}
+		}
+
+		for (i = 0; i < LENGTH(userscripts); ++i) {
+			if (userscripts[i].slurped) {
+				printf("Script for %s is \n%s\n", userscripts[i].allow_pattern, userscripts[i].slurped);
+				const char *allowlist[2] = {userscripts[i].allow_pattern, NULL};
+				script = webkit_user_script_new(
+					userscripts[i].slurped,
+					WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
+					(userscripts[i].prio
+					 ? WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END
+					 : WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START),
+					allowlist,
+					NULL);
+				webkit_user_content_manager_add_script(contentmanager, script);
+				webkit_user_script_unref(script);
+			}
+		}
+
 		if (curconfig[Ephemeral].val.i) {
 			context = webkit_web_context_new_ephemeral();
 		} else {
-- 
2.48.1

Regardless of what we decide, it can belong to a different patch. The
user script/style injection is ready and works properly... once the
issue with style erasure is resolved.

Thanks,
-- 
Artyom Bologov
https://aartaka.me

Reply via email to