All Downloads are FREE. Search and download functionalities are using the official Maven repository.

library.webkitgtk_extension.c Maven / Gradle / Ivy

There is a newer version: 3.128.0
Show newest version
#include "webkitgtk_extension.h"

/*
 * To debug this extension:
 *  -ensure this is built with debug flags (look for '-g*' in make_linux, or 'SWT_LIB_DEBUG' macro)
 *  -connect to the WebKitWebProcess with the PID of this extension. It can be found as follows:
		g_print("Extension PID: %d\n", getpid());
 */

// Client address of the main process GDBusServer -- the extension connects to this
const gchar *swt_main_proc_client_address = NULL;

// Client address of the extension's GDBusServer -- SWT main process connects to this
const gchar *extension_server_address = NULL;

// See: WebKitGTK.java's 'TYPE NOTES'
guchar SWT_DBUS_MAGIC_NUMBER_EMPTY_ARRAY = 101;
guchar SWT_DBUS_MAGIC_NUMBER_NULL = 48;

// This struct represents a BrowserFunction
typedef struct {
    guint64 page_id; // page ID
    gchar *function; // JS function
    gchar *url; // URL the function belongs to
} BrowserFunction;

// The list of BrowserFunctions registered to this extension
GSList *function_list = NULL;

// GDBusConnection to the SWT main process
GDBusConnection *connection_to_main_proc = NULL;

// This extension's GDBusServer and related resources
GDBusServer *server = NULL;
GDBusAuthObserver *auth_observer = NULL;
gchar *guid = NULL;

// GDBusConnection from SWT main process
GDBusConnection *connection_from_main_proc = NULL;

/**
 * Caller should free the returned GVariant
 */
GVariant *call_main_proc_sync(char * method_name, GVariant *parameters) {
	GError *error = NULL; // Some functions return errors through params
	GVariant *result;     // The value result from a call

	// Send a message
	result = g_dbus_connection_call_sync(connection_to_main_proc, WEBKIT_MAIN_PROCESS_DBUS_NAME,
			WEBKIT_MAIN_PROCESS_OBJECT_PATH, WEBKIT_MAIN_PROCESS_INTERFACE_NAME, method_name, parameters,
			NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
	// Error checking.
	if (result == NULL) {
		if (error != NULL) {
			g_error("call_main_proc_sync failed because '%s.'\n", error->message);
		} else {
			g_error("call_main_proc_sync failed for an unknown reason.\n");
		}
		return NULL;
	}

	return result;
}

// +--------------------------------------------------+
// | JavaScriptCore to/from conversion GVariant logic |
// +--------------------------------------------------+

/** Return true if the given JSValueRef is one we can push over gdbus. False otherwise.
 *  We support basic types, nulls and arrays of basic types.*/
gboolean is_js_valid(JSContextRef context, JSValueRef value) {
	JSType type = JSValueGetType(context, value);
	if (type == kJSTypeBoolean
			|| type == kJSTypeNumber
			|| type == kJSTypeString
			|| type == kJSTypeNull
			|| type == kJSTypeUndefined) {
		return true;
	}
	if (type == kJSTypeObject && JSValueIsArray(context, value)) {
		JSStringRef propertyName = JSStringCreateWithUTF8CString("length");
		JSObjectRef object = JSValueToObject(context, value, NULL);
		JSValueRef valuePtr = JSObjectGetProperty(context, object, propertyName, NULL);
		JSStringRelease(propertyName);
		int length = (int) JSValueToNumber(context, valuePtr, NULL);
		int i;
		for (i = 0; i < length; i++) {
			const JSValueRef child = JSObjectGetPropertyAtIndex(context, object, i, NULL);
			if (!is_js_valid(context, child)) {
				return false;
			}
		}
		return true;
	}
	return false;
}

/*
 * Developer note:
 * JavaScriptCore defines a "Number" to be a double in general. It doesn't seem to be using "Int".
 */
static GVariant * convert_js_to_gvariant (JSContextRef context, JSValueRef value){
	g_assert(context != NULL);
	g_assert(value != NULL);
	JSType type = JSValueGetType(context, value);

	if (type == kJSTypeBoolean) {
		gboolean result = JSValueToNumber(context, value, NULL) != 0;
		return g_variant_new_boolean(result);
	}

	if (type == kJSTypeNumber) {
		double result = JSValueToNumber(context, value, NULL);
		return g_variant_new_double(result);
	}

	if (type == kJSTypeString) {
		JSStringRef stringRef = JSValueToStringCopy(context, value, NULL);
		size_t length = JSStringGetMaximumUTF8CStringSize(stringRef);
		char* string = (char*) malloc(length);
		JSStringGetUTF8CString(stringRef, string, length);
		GVariant *variant = g_variant_new_string(string);
		free(string);
		return variant;
	}

	if (type == kJSTypeNull || type == kJSTypeUndefined) {
		return g_variant_new_byte(SWT_DBUS_MAGIC_NUMBER_NULL);
	}

	if (type == kJSTypeObject) {
		JSStringRef propertyName = JSStringCreateWithUTF8CString("length");
		JSObjectRef object = JSValueToObject(context, value, NULL);
		JSValueRef valuePtr = JSObjectGetProperty(context, object, propertyName, NULL);
		JSStringRelease(propertyName);

		if (JSValueGetType(context, valuePtr) == kJSTypeNumber) {
			int length = (int) JSValueToNumber(context, valuePtr, NULL);

			if (length == 0) {
				return g_variant_new_byte(SWT_DBUS_MAGIC_NUMBER_EMPTY_ARRAY);
			}
			GVariant **children = g_new(GVariant *, length);
			int i = 0;
			for (i = 0; i < length; i++) {
				const JSValueRef child = JSObjectGetPropertyAtIndex(context, object, i, NULL);
				children[i] = convert_js_to_gvariant(context, child);
			}
			GVariant* variant = g_variant_new_tuple(children, length);
			g_free(children);
			return variant;
		}
	}

	// Get type value string
	JSStringRef valueIString = JSValueToStringCopy(context, value, NULL);
	size_t valueUTF8Size = JSStringGetMaximumUTF8CStringSize(valueIString);
	char* valueUTF8 = (char*) malloc(valueUTF8Size);
	JSStringGetUTF8CString(valueIString, valueUTF8, valueUTF8Size);

	g_warning("Unhandled type %d value: %s \n", type, valueUTF8);
	free(valueUTF8);
	JSStringRelease(valueIString);

	return NULL;
}


static JSValueRef convert_gvariant_to_js (JSContextRef context, GVariant * value){
	g_assert(context != NULL);
	g_assert(value != NULL);

	if (g_variant_is_of_type(value, G_VARIANT_TYPE_BYTE)) {  // see: WebKitGTK.java 'TYPE NOTES'
		guchar magic_number = g_variant_get_byte(value);
		if (magic_number == SWT_DBUS_MAGIC_NUMBER_NULL) {
			// 'JSValueMakeUndefined' is used as oppose to 'JSValueMakeNull' (from what I gather) for legacy reasons.
			// I.e webkit1 used it, so we shall use it in webkit2 also.
			return JSValueMakeUndefined(context);
		} else if (magic_number == SWT_DBUS_MAGIC_NUMBER_EMPTY_ARRAY) {
				return JSObjectMakeArray(context, 0, NULL, NULL); // The empty array with no children.
		} else {
			g_error("Java sent an unknown magic number: '%d' , this should never happen. \n", magic_number);
		}
	}

	if (g_variant_is_of_type(value, G_VARIANT_TYPE_BOOLEAN)) {
		return JSValueMakeBoolean(context, g_variant_get_boolean(value));
	}

	if (g_variant_is_of_type(value, G_VARIANT_TYPE_DOUBLE)) {
		return JSValueMakeNumber(context, g_variant_get_double(value));
	}

	if (g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)) {
		JSStringRef stringRef = JSStringCreateWithUTF8CString(g_variant_get_string(value, NULL));
		JSValueRef result = JSValueMakeString(context, stringRef);
		JSStringRelease(stringRef);
		return result;
	}

	if (g_variant_is_of_type(value, G_VARIANT_TYPE_TUPLE)) {
		gsize length = g_variant_n_children(value);
		JSValueRef *children = g_new(JSValueRef, length);

		gsize i = 0;
		for (i = 0; i < length; i++) {
			children[i] = convert_gvariant_to_js(context, g_variant_get_child_value(value, i));
		}
		JSValueRef result = JSObjectMakeArray(context, length, children, NULL);
		g_free(children);
		return result;
	}
	g_error("Unhandled type %s \n", g_variant_get_type_string(value));
	return NULL;
}

// +--------------------+---------------------------------------------------------
// | WebExtension Logic |
// +--------------------+

// Reached by calling "webkit2callJava();" in javascript console.
// Some basic c function to be exposed to the javascript environment
static JSValueRef webkit2callJava (JSContextRef context,
                               JSObjectRef function,
                               JSObjectRef thisObject,
                               size_t argumentCount,
                               const JSValueRef arguments[], // [String webview, double index, String Token, Object[] args]
                               JSValueRef *exception) {
	g_assert (argumentCount == 4);
	GVariant *g_var_params;     // The parameters to a function call

	// Need to ensure user arguments won't break gdbus.
	if (!is_js_valid(context, arguments[3])) {
		g_warning("Arguments contain an invalid type (object). Only Number,Boolean,null,String and (mixed) arrays of basic types are supported");
		return 0;
	}

	g_var_params = g_variant_new ("(@s@d@s@*)",   // pointer to String, pointer to double, pointer to string, pointer to any type.
			convert_js_to_gvariant(context, arguments[0]), // String webView
			convert_js_to_gvariant(context, arguments[1]), // int index
			convert_js_to_gvariant(context, arguments[2]), // String Token
			convert_js_to_gvariant(context, arguments[3])  // js args
			);

	GVariant *g_var_result = call_main_proc_sync("webkit2callJava", g_var_params);
	if (g_var_result == NULL) {
		g_error("Java call returned NULL. This should never happpen\n");
		return 0;
	}

	// gdbus dynamic call always returns an array(tuple) with return types.
	// In our case, we return a single type or an array.
	// E.g  java:int -> gdbus:(i)   (array containing one int)
	// E.g java [int,str] -> gdbus:((is))   (array with array of (int+str).
	// So we always extract the first child, convert and pass to js.
	JSValueRef retVal = 0;
	if (g_variant_is_of_type(g_var_result, G_VARIANT_TYPE_TUPLE)) {
		if (g_variant_n_children(g_var_result) != 1) {
			g_error("Should only receive a single item in the tuple, but length is: %ud\n", (unsigned int) g_variant_n_children(g_var_result));
		}
		retVal = convert_gvariant_to_js(context, g_variant_get_child_value(g_var_result, 0));
	} else {
		g_error("Unsupported return type. Should be an array, but received a single type.\n");
	}

	g_variant_unref(g_var_result);
	return retVal;
}

static void web_page_created_callback(WebKitWebExtension *extension, WebKitWebPage *web_page, gpointer user_data) {
	// Observation. This seems to be called only once.
}

/**
 * Returns the main frame of the WebPage with the given ID
 */
static WebKitFrame *webkitgtk_extension_get_main_frame (const guint64 id) {
	WebKitWebPage *web_page = webkit_web_extension_get_page (this_extension, id);
	return webkit_web_page_get_main_frame (web_page);
}

/*
 * Execute the Javascript for the given page and URL.
 */
static gboolean webkitgtk_extension_execute_script (const guint64 page_id, gchar* script, gchar* url) {
	WebKitFrame *main_frame = webkitgtk_extension_get_main_frame (page_id);

	JSStringRef url_string = JSStringCreateWithUTF8CString (url);
	JSStringRef script_string = JSStringCreateWithUTF8CString (script);

	/*
	 * TODO Bug 570285: Replace with webkit_frame_get_js_context()
	 * when minimal WebKitGTK version is 2.22+
	 */
	G_GNUC_BEGIN_IGNORE_DEPRECATIONS
	JSGlobalContextRef context = webkit_frame_get_javascript_global_context (main_frame);
	G_GNUC_END_IGNORE_DEPRECATIONS

	JSValueRef exception;
	JSValueRef result = JSEvaluateScript(context, script_string, NULL, url_string, 0,  &exception);
	if (!result) {
		JSStringRef exceptionIString = JSValueToStringCopy(context, exception, NULL);
		size_t exceptionUTF8Size = JSStringGetMaximumUTF8CStringSize(exceptionIString);
		char* exceptionUTF8 = (char*)malloc(exceptionUTF8Size);
		JSStringGetUTF8CString(exceptionIString, exceptionUTF8, exceptionUTF8Size);
		g_error("Failed to execute script exception: %s\n", exceptionUTF8);
		free(exceptionUTF8);
		JSStringRelease(exceptionIString);
	}

	JSStringRelease (url_string);
	JSStringRelease (script_string);

	return result != NULL;
}

void execute_browser_functions(gconstpointer item, gpointer page) {
	BrowserFunction *function = (BrowserFunction *) item;
	if (function != NULL && function->page_id == GPOINTER_TO_UINT(page)) {
		webkitgtk_extension_execute_script(function->page_id, function->function, function->url);
	}
	return;
}

gint find_browser_function (gconstpointer item, gconstpointer target) {
	BrowserFunction *element = (BrowserFunction *) item;
	BrowserFunction *remove = (BrowserFunction *) target;
	if (element->page_id == remove->page_id && g_strcmp0(element->function, remove->function) == 0 &&
			g_strcmp0(element->url, remove->url) == 0) {
		return 0;
	}
	return 1;
}

void add_browser_function(guint64 page_id, const gchar *function, const gchar *url) {
	BrowserFunction *func = g_slice_new0(BrowserFunction);
	func->page_id = page_id;
	func->function = g_strdup(function);
	func->url = g_strdup(url);
	function_list = g_slist_append(function_list, func);
}

void remove_browser_function(guint64 page_id, const gchar *function, const gchar *url) {
	BrowserFunction *func = g_slice_new0(BrowserFunction);
	func->page_id = page_id;
	func->function = g_strdup(function);
	func->url = g_strdup(url);
	GSList *to_remove = g_slist_find_custom(function_list, func, find_browser_function);
	if (to_remove != NULL) {
		BrowserFunction *delete_func = to_remove->data;
		g_free(delete_func->function);
		g_free(delete_func->url);
		function_list = g_slist_delete_link(function_list, to_remove);
	}
	g_free(func->function);
	g_free(func->url);
	g_slice_free(BrowserFunction, func);
}

void unpack_browser_function_array(GVariant *array) {
	GVariantIter iter;
	GVariant *child;

	g_variant_iter_init (&iter, array);
	while ((child = g_variant_iter_next_value (&iter))) {
	    gsize length = (int)g_variant_n_children (child);
	    if (length > 3) {
	    	// If the length is longer than three, something went wrong and this tuple should be skipped
	    	g_warning("There was an error unpacking the GVariant tuple for a BrowserFunction in the web extension.\n");
	    	continue;
	    }
	    guint64 page = g_variant_get_uint64(g_variant_get_child_value(child, 0));
	    if (page == -1UL) {
	    	// Empty or malformed BrowserFunction, skip this one
	    	continue;
	    } else {
	    	const gchar *function = g_variant_get_string(g_variant_get_child_value(child, 1), NULL);
	    	const gchar *url = g_variant_get_string(g_variant_get_child_value(child, 2), NULL);
	    	if (function != NULL && url != NULL) {
	    		add_browser_function(page, function, url);
	    	} else {
	    		g_warning("There was an error unpacking the function string or URL.\n");
	    	}
	    }
	 	g_variant_unref (child);
	}
}

/*
 * Every time a webpage is loaded, we should re-register the 'webkit2callJava' function.
 * Additionally, we re-register all BrowserFunctions that are stored in the function_list
 * GSList.
 */
static void window_object_cleared_callback (WebKitScriptWorld *world, WebKitWebPage *web_page,
                                WebKitFrame       *frame,
                                gpointer           user_data) {
	// Observation: This is called every time a webpage is loaded.
	JSGlobalContextRef jsContext;
    JSObjectRef        globalObject;
    JSValueRef exception = 0;

    /*
     * TODO Bug 570285: Replace with webkit_frame_get_js_context_for_script_world()
     * when minimal WebKitGTK version is 2.22+
     */
    G_GNUC_BEGIN_IGNORE_DEPRECATIONS
    jsContext = webkit_frame_get_javascript_context_for_script_world (frame, world);
    G_GNUC_END_IGNORE_DEPRECATIONS
    globalObject = JSContextGetGlobalObject (jsContext);

    JSStringRef function_name = JSStringCreateWithUTF8CString("webkit2callJava"); // Func reference by javascript
    JSObjectRef jsFunction = JSObjectMakeFunctionWithCallback(jsContext, function_name, webkit2callJava); // C reference to func
    JSObjectSetProperty(jsContext, globalObject, function_name, jsFunction,
    		kJSPropertyAttributeDontDelete | kJSPropertyAttributeReadOnly, &exception);

    if (exception) {
    	g_print("OJSObjectSetProperty exception occurred");
    }

    /*
     * Iterate over the list of BrowserFunctions and execute each one of them for the current page.
     * This ensures that BrowserFunctions are not lost on page reloads. See bug 536141.
     */
    if (function_list != NULL) {
    	guint64 page_id = webkit_web_page_get_id (web_page);
    	if (page_id != -1UL) {
    		g_slist_foreach(function_list, (GFunc)execute_browser_functions, GUINT_TO_POINTER(page_id));
    	} else {
    		g_warning("There was an error fetching the page ID in the object_cleared callback.\n");
    	}
    }
}

static void
webkitgtk_extension_handle_method_call (GDBusConnection *connection, const gchar *sender,
                    const gchar           *object_path,
                    const gchar           *interface_name,
                    const gchar           *method_name,
                    GVariant              *parameters,
                    GDBusMethodInvocation *invocation,
                    gpointer               user_data) {
	gboolean result = FALSE;
	const gchar *script;
	const gchar *url;
	guint64 page_id;
	// Check method names
	if (g_strcmp0(method_name, "webkitgtk_extension_register_function") == 0) {
		g_variant_get(parameters, "(t&s&s)", &page_id, &script, &url);
		if (page_id != -1UL) {
			result = TRUE;
			// Return before processing the linked list, to prevent DBus from hanging
			g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", result));
			add_browser_function(page_id, script, url);
			return;
		}
		g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", result));
		return;
	}
	if (g_strcmp0(method_name, "webkitgtk_extension_deregister_function") == 0) {
		g_variant_get(parameters, "(t&s&s)", &page_id, &script, &url);
		if (page_id != -1UL) {
			result = TRUE;
			// Return before processing the linked list, to prevent DBus from hanging
			g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", result));
			remove_browser_function(page_id, script, url);
			return;
		}
		g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", result));
		return;
	}
	g_error ("Unknown method %s\n", method_name);
}

static const GDBusInterfaceVTable interface_vtable = {.method_call = webkitgtk_extension_handle_method_call};

static void connection_closed_cb (GDBusConnection *connection, gboolean remote_peer_vanished, GError *error,
		gpointer user_data) {
	/*
	 * If this connection is closed we can shut the server down. NOTE: all connections are freed in the main
	 * SWT process, including connections created here in this extension. This is done for the sake of
	 * consistency to avoid double frees.
	 */
	if (connection == connection_from_main_proc) {
		// Free server and its objects
		g_dbus_server_stop(server);
		g_object_unref(auth_observer);
		g_object_unref(server);
		g_free(guid);
	}
}

static gboolean new_connection_cb (GDBusServer *server, GDBusConnection *connection, gpointer user_data) {
	// Create introspection XML
	dbus_introspection_xml = g_new (gchar, strlen(dbus_introspection_xml_template) +
			strlen(WEBKITGTK_EXTENSION_INTERFACE_NAME) + 1);
	g_sprintf (dbus_introspection_xml, dbus_introspection_xml_template, WEBKITGTK_EXTENSION_INTERFACE_NAME);

	// Create DBus node
	dbus_node = g_dbus_node_info_new_for_xml (dbus_introspection_xml, NULL);
	g_assert (dbus_node != NULL);

	// Register node on the connection that was just created
	dbus_interface = g_dbus_node_info_lookup_interface(dbus_node, WEBKITGTK_EXTENSION_INTERFACE_NAME);
	guint registration_id = g_dbus_connection_register_object(connection, WEBKITGTK_EXTENSION_OBJECT_PATH,
		      dbus_interface,
		      &interface_vtable, NULL, /* user_data */
		      NULL, /* user_data_free_func */
		      NULL); /* GError** */
	g_assert(registration_id > 0);

	// This connection will be the one from the main SWT process
	connection_from_main_proc = g_object_ref(connection);

	// Listen to the "close" signal on this connection so we can free resources in the extension when
	// the time comes.
	g_signal_connect(connection_from_main_proc, "closed", G_CALLBACK (connection_closed_cb), NULL);

	return 1;
}

static gboolean extension_authorize_peer (GDBusAuthObserver *observer, GIOStream *stream, GCredentials *credentials,
		gpointer user_data) {
	g_autoptr (GError) error = NULL;
	gboolean authorized = FALSE;
	if (credentials != NULL) {
		  GCredentials *own_credentials;
		  own_credentials = g_credentials_new ();
		  if (g_credentials_is_same_user (credentials, own_credentials, &error)) {
			  authorized = TRUE;
	      }
		  g_object_unref (own_credentials);
	}
	if (error) {
		g_warning ("Error authenticating client connection: %s", error->message);
	}
	return authorized;
}

// Returns a valid GDBusServer address -- this will be the address used to connect to the extension's
// GDBusServer. On Linux, the address required is used for as an abstract path. If abstract path is not supported
// one must create & manage temporary directories. See Bug562443.
gchar *construct_server_address () {
	gchar *tmp_address = g_strdup("unix:tmpdir=/tmp/SWT-WebExtensionGDBusServer");

	return tmp_address;
}

// Creates the GDBusServer for this web extension
static void create_server () {
	g_autoptr (GError) error = NULL;
	extension_server_address = construct_server_address();
	auth_observer = g_dbus_auth_observer_new();

	guid = g_dbus_generate_guid();
	server = g_dbus_server_new_sync(extension_server_address, G_DBUS_SERVER_FLAGS_NONE, guid,
			auth_observer, NULL, &error);

	if (error) {
		g_error ("Failed to create server: %s", error->message);
	}

	if (server) {
		g_signal_connect(server, "new-connection", G_CALLBACK(new_connection_cb), NULL);
		g_dbus_server_start(server);
		g_signal_connect (auth_observer, "authorize-authenticated-peer", G_CALLBACK(extension_authorize_peer), NULL);
	}
}

// Identifies this web extension to the main SWT process by sending its
// server address over DBus.
static void identify_extension_to_main_proc () {
	const gchar *client_address_for_extension_server = g_dbus_server_get_client_address(server);
	GVariant *result = call_main_proc_sync("webkitWebExtensionIdentifier", g_variant_new ("(s)",
			client_address_for_extension_server));

	// Process and register any pending BrowserFunctions from the main SWT process
	if (g_variant_is_of_type(result, G_VARIANT_TYPE_TUPLE)) {
		  unpack_browser_function_array(g_variant_get_child_value(result, 0));
	} else {
		g_warning("webkitWebExtensionIdentifier return value from SWT was an unexpected type""(not a tuple).\n");
	}
}

// Creates a GDBusConnection to the main SWT process
static void connect_to_main_proc (GVariant *user_data) {
	swt_main_proc_client_address = g_variant_get_string(user_data, NULL);

	g_autoptr (GError) error = NULL;
	connection_to_main_proc = g_dbus_connection_new_for_address_sync (swt_main_proc_client_address,
			G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, NULL, NULL, &error);

	if (error) {
		g_error ("Failed to create connection: %s", error->message);
	}

	if (connection_to_main_proc && g_dbus_connection_is_closed(connection_to_main_proc)) {
			g_error ("Failed to created connection: connection is closed");
	}
}

G_MODULE_EXPORT void webkit_web_extension_initialize_with_user_data(WebKitWebExtension *extension, GVariant *user_data) {
	this_extension = extension;

	// Connect to the main SWT process
	connect_to_main_proc(user_data);

	// Create this extension's GDBusServer
	create_server();

	// Identify this extension's server address to the main process
	identify_extension_to_main_proc();

	// WebKit callbacks
    g_signal_connect(extension, "page-created",  G_CALLBACK(web_page_created_callback), NULL);
    g_signal_connect (webkit_script_world_get_default (), "window-object-cleared", G_CALLBACK (window_object_cleared_callback), NULL);
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy