1044 lines
30 KiB
C
1044 lines
30 KiB
C
/*
|
|
Copyright (C) 2013 Stephan Haller <nomad@froevel.de>
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
See the file COPYING for the full license text.
|
|
*/
|
|
|
|
#include "nojs.h"
|
|
#include "nojs-view.h"
|
|
|
|
#include <errno.h>
|
|
|
|
/* Define this class in GObject system */
|
|
G_DEFINE_TYPE(NoJS,
|
|
nojs,
|
|
G_TYPE_OBJECT)
|
|
|
|
/* Properties */
|
|
enum
|
|
{
|
|
PROP_0,
|
|
|
|
PROP_EXTENSION,
|
|
PROP_APPLICATION,
|
|
|
|
PROP_DATABASE,
|
|
PROP_DATABASE_FILENAME,
|
|
PROP_ALLOW_LOCAL_PAGES,
|
|
PROP_ONLY_SECOND_LEVEL,
|
|
PROP_UNKNOWN_DOMAIN_POLICY,
|
|
|
|
PROP_LAST
|
|
};
|
|
|
|
static GParamSpec* NoJSProperties[PROP_LAST]={ 0, };
|
|
|
|
/* Signals */
|
|
enum
|
|
{
|
|
URI_LOAD_POLICY_STATUS,
|
|
POLICY_CHANGED,
|
|
|
|
SIGNAL_LAST
|
|
};
|
|
|
|
static guint NoJSSignals[SIGNAL_LAST]={ 0, };
|
|
|
|
/* Private structure - access only by public API if needed */
|
|
#define NOJS_GET_PRIVATE(obj) \
|
|
(G_TYPE_INSTANCE_GET_PRIVATE((obj), TYPE_NOJS, NoJSPrivate))
|
|
|
|
struct _NoJSPrivate
|
|
{
|
|
/* Extension related */
|
|
MidoriExtension *extension;
|
|
MidoriApp *application;
|
|
sqlite3 *database;
|
|
gchar *databaseFilename;
|
|
gboolean allowLocalPages;
|
|
gboolean checkOnlySecondLevel;
|
|
NoJSPolicy unknownDomainPolicy;
|
|
|
|
guint requestStartedSignalID;
|
|
};
|
|
|
|
/* Taken from http://www.w3.org/html/wg/drafts/html/master/scripting-1.html#scriptingLanguages
|
|
* A list of javascript mime types
|
|
*/
|
|
static const gchar* javascriptTypes[]= {
|
|
"application/ecmascript",
|
|
"application/javascript",
|
|
"application/x-ecmascript",
|
|
"application/x-javascript",
|
|
"text/ecmascript",
|
|
"text/javascript",
|
|
"text/javascript1.0",
|
|
"text/javascript1.1",
|
|
"text/javascript1.2",
|
|
"text/javascript1.3",
|
|
"text/javascript1.4",
|
|
"text/javascript1.5",
|
|
"text/jscript",
|
|
"text/livescript",
|
|
"text/x-ecmascript",
|
|
"text/x-javascript",
|
|
NULL
|
|
};
|
|
|
|
/* IMPLEMENTATION: Private variables and methods */
|
|
|
|
/* Closure for: void (*closure)(NoJS *self, gchar *inURI, NoJSPolicy inPolicy) */
|
|
static void _nojs_closure_VOID__STRING_ENUM(GClosure *inClosure,
|
|
GValue *ioReturnValue G_GNUC_UNUSED,
|
|
guint inNumberValues,
|
|
const GValue *inValues,
|
|
gpointer inInvocationHint G_GNUC_UNUSED,
|
|
gpointer inMarshalData)
|
|
{
|
|
typedef void (*GMarshalFunc_VOID__STRING_ENUM)(gpointer inObject, gpointer inArg1, gint inArg2, gpointer inUserData);
|
|
|
|
register GMarshalFunc_VOID__STRING_ENUM callback;
|
|
register GCClosure *closure=(GCClosure*)inClosure;
|
|
register gpointer object, userData;
|
|
|
|
g_return_if_fail(inNumberValues==3);
|
|
|
|
if(G_CCLOSURE_SWAP_DATA(inClosure))
|
|
{
|
|
object=inClosure->data;
|
|
userData=g_value_peek_pointer(inValues+0);
|
|
}
|
|
else
|
|
{
|
|
object=g_value_peek_pointer(inValues+0);
|
|
userData=inClosure->data;
|
|
}
|
|
|
|
callback=(GMarshalFunc_VOID__STRING_ENUM)(inMarshalData ? inMarshalData : closure->callback);
|
|
|
|
callback(object,
|
|
(gchar*)g_value_get_string(inValues+1),
|
|
g_value_get_enum(inValues+2),
|
|
userData);
|
|
}
|
|
|
|
/* Show common error dialog */
|
|
static void _nojs_error(NoJS *self, const gchar *inReason)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
g_return_if_fail(inReason);
|
|
|
|
GtkWidget *dialog;
|
|
|
|
/* Show confirmation dialog for undetermined cookies */
|
|
dialog=gtk_message_dialog_new(NULL,
|
|
GTK_DIALOG_MODAL,
|
|
GTK_MESSAGE_ERROR,
|
|
GTK_BUTTONS_OK,
|
|
_("A fatal error occurred which prevents "
|
|
"the NoJS extension to continue. "
|
|
"You should disable it."));
|
|
|
|
gtk_window_set_title(GTK_WINDOW(dialog), _("Error in NoJS extension"));
|
|
gtk_window_set_icon_name(GTK_WINDOW (dialog), "midori");
|
|
|
|
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
|
|
"%s:\n%s",
|
|
_("Reason"),
|
|
inReason);
|
|
|
|
gtk_dialog_run(GTK_DIALOG(dialog));
|
|
|
|
/* Free up allocated resources */
|
|
gtk_widget_destroy(dialog);
|
|
}
|
|
|
|
/* Open database containing policies for javascript sites.
|
|
* Create database and setup table structure if it does not exist yet.
|
|
*/
|
|
static void _nojs_open_database(NoJS *self)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
|
|
NoJSPrivate *priv=self->priv;
|
|
const gchar *configDir;
|
|
gchar *sql;
|
|
gchar *error=NULL;
|
|
gint success;
|
|
|
|
/* Close any open database */
|
|
if(priv->database)
|
|
{
|
|
priv->databaseFilename=NULL;
|
|
|
|
sqlite3_close(priv->database);
|
|
priv->database=NULL;
|
|
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_DATABASE]);
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_DATABASE_FILENAME]);
|
|
}
|
|
|
|
/* Build path to database file */
|
|
configDir=midori_extension_get_config_dir(priv->extension);
|
|
if(!configDir)
|
|
return;
|
|
|
|
if(katze_mkdir_with_parents(configDir, 0700))
|
|
{
|
|
g_warning(_("Could not create configuration folder for extension: %s"), g_strerror(errno));
|
|
|
|
_nojs_error(self, _("Could not create configuration folder for extension."));
|
|
return;
|
|
}
|
|
|
|
/* Open database */
|
|
priv->databaseFilename=g_build_filename(configDir, NOJS_DATABASE, NULL);
|
|
success=sqlite3_open(priv->databaseFilename, &priv->database);
|
|
if(success!=SQLITE_OK)
|
|
{
|
|
g_warning(_("Could not open database of extension: %s"), sqlite3_errmsg(priv->database));
|
|
|
|
g_free(priv->databaseFilename);
|
|
priv->databaseFilename=NULL;
|
|
|
|
if(priv->database) sqlite3_close(priv->database);
|
|
priv->database=NULL;
|
|
|
|
_nojs_error(self, _("Could not open database of extension."));
|
|
return;
|
|
}
|
|
|
|
/* Create table structure if it does not exist */
|
|
success=sqlite3_exec(priv->database,
|
|
"CREATE TABLE IF NOT EXISTS "
|
|
"policies(site text, value integer);",
|
|
NULL,
|
|
NULL,
|
|
&error);
|
|
|
|
if(success==SQLITE_OK)
|
|
{
|
|
success=sqlite3_exec(priv->database,
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS "
|
|
"site ON policies (site);",
|
|
NULL,
|
|
NULL,
|
|
&error);
|
|
}
|
|
|
|
if(success==SQLITE_OK)
|
|
{
|
|
success=sqlite3_exec(priv->database,
|
|
"PRAGMA journal_mode=TRUNCATE;",
|
|
NULL,
|
|
NULL,
|
|
&error);
|
|
}
|
|
|
|
if(success!=SQLITE_OK || error)
|
|
{
|
|
_nojs_error(self, _("Could not set up database structure of extension."));
|
|
|
|
if(error)
|
|
{
|
|
g_critical(_("Failed to execute database statement: %s"), error);
|
|
sqlite3_free(error);
|
|
}
|
|
|
|
g_free(priv->databaseFilename);
|
|
priv->databaseFilename=NULL;
|
|
|
|
sqlite3_close(priv->database);
|
|
priv->database=NULL;
|
|
return;
|
|
}
|
|
|
|
/* Delete all temporarily allowed sites */
|
|
sql=sqlite3_mprintf("DELETE FROM policies WHERE value=%d;", NOJS_POLICY_ACCEPT_TEMPORARILY);
|
|
success=sqlite3_exec(priv->database, sql, NULL, NULL, &error);
|
|
if(success!=SQLITE_OK) g_warning(_("SQL fails: %s"), error);
|
|
if(error) sqlite3_free(error);
|
|
sqlite3_free(sql);
|
|
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_DATABASE]);
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_DATABASE_FILENAME]);
|
|
}
|
|
|
|
/* A request through libsoup is going to start and http headers must be
|
|
* checked for content type
|
|
*/
|
|
static void _nojs_on_got_headers(NoJS *self, gpointer inUserData)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
g_return_if_fail(SOUP_IS_MESSAGE(inUserData));
|
|
|
|
NoJSPrivate *priv=self->priv;
|
|
SoupMessage *message=SOUP_MESSAGE(inUserData);
|
|
SoupSession *session=webkit_get_default_session();
|
|
SoupMessageHeaders *headers;
|
|
SoupMessageBody *body;
|
|
const gchar *contentType;
|
|
SoupURI *uri;
|
|
gchar *uriText;
|
|
NoJSPolicy policy;
|
|
gboolean isJS;
|
|
const gchar **iter;
|
|
|
|
/* Get headers from message to retrieve content type */
|
|
g_object_get(message, "response-headers", &headers, NULL);
|
|
if(!headers)
|
|
{
|
|
g_warning("Could not get headers from message to check for javascript.");
|
|
return;
|
|
}
|
|
|
|
/* Get content type of uri and check if it is a javascript resource */
|
|
contentType=soup_message_headers_get_content_type(headers, NULL);
|
|
|
|
isJS=FALSE;
|
|
iter=javascriptTypes;
|
|
while(*iter && !isJS)
|
|
{
|
|
isJS=(g_strcmp0(contentType, *iter)==0);
|
|
iter++;
|
|
}
|
|
|
|
if(!isJS) return;
|
|
|
|
/* The document being loaded is javascript so get URI from message,
|
|
* get policy for domain of URI and emit signal
|
|
*/
|
|
uri=soup_message_get_uri(message);
|
|
policy=nojs_get_policy(self, uri);
|
|
|
|
if(policy==NOJS_POLICY_UNDETERMINED)
|
|
{
|
|
g_warning("Got invalid policy. Using default policy for unknown domains.");
|
|
policy=priv->unknownDomainPolicy;
|
|
}
|
|
|
|
uriText=soup_uri_to_string(uri, FALSE);
|
|
|
|
g_signal_emit(self, NoJSSignals[URI_LOAD_POLICY_STATUS], 0, uriText, policy==NOJS_POLICY_UNDETERMINED ? NOJS_POLICY_BLOCK : policy);
|
|
|
|
g_free(uriText);
|
|
|
|
/* Return here if policy is any type of accept */
|
|
if(policy!=NOJS_POLICY_UNDETERMINED && policy!=NOJS_POLICY_BLOCK) return;
|
|
|
|
/* Cancel this message */
|
|
soup_session_cancel_message(session, message, SOUP_STATUS_CANCELLED);
|
|
|
|
/* Discard any load data */
|
|
g_object_get(message, "response-body", &body, NULL);
|
|
if(body) soup_message_body_truncate(body);
|
|
}
|
|
|
|
static void _nojs_on_request_started(NoJS *self,
|
|
SoupMessage *inMessage,
|
|
SoupSocket *inSocket,
|
|
gpointer inUserData)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
g_return_if_fail(SOUP_IS_MESSAGE(inMessage));
|
|
|
|
/* Connect to "got-headers" to cancel loading javascript documents early */
|
|
g_signal_connect_swapped(inMessage, "got-headers", G_CALLBACK(_nojs_on_got_headers), self);
|
|
}
|
|
|
|
/* The icon in statusbar was clicked */
|
|
static void _nojs_on_statusbar_icon_clicked(MidoriBrowser *inBrowser, gpointer inUserData)
|
|
{
|
|
g_return_if_fail(MIDORI_IS_BROWSER(inBrowser));
|
|
|
|
MidoriView *activeView;
|
|
NoJSView *view;
|
|
GtkMenu *menu;
|
|
|
|
/* Get current active midori view */
|
|
activeView=MIDORI_VIEW(midori_browser_get_current_tab(inBrowser));
|
|
g_return_if_fail(MIDORI_IS_VIEW(activeView));
|
|
|
|
/* Get NoJS view of current active midori view */
|
|
view=NOJS_VIEW(g_object_get_data(G_OBJECT(activeView), "nojs-view-instance"));
|
|
g_return_if_fail(NOJS_IS_VIEW(view));
|
|
|
|
/* Get menu of current view */
|
|
menu=nojs_view_get_menu(view);
|
|
g_return_if_fail(menu);
|
|
|
|
/* Show menu */
|
|
gtk_menu_popup(menu, NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time());
|
|
}
|
|
|
|
gchar* nojs_get_icon_path (const gchar* icon)
|
|
{
|
|
gchar* nojs_dir = midori_paths_get_res_filename("nojs");
|
|
return g_build_filename (nojs_dir, icon, NULL);
|
|
}
|
|
|
|
/* Menu icon of a view has changed */
|
|
static void _nojs_on_menu_icon_changed(MidoriBrowser *inBrowser, GParamSpec *inSpec, gpointer inUserData)
|
|
{
|
|
g_return_if_fail(MIDORI_IS_BROWSER(inBrowser));
|
|
g_return_if_fail(NOJS_IS_VIEW(inUserData));
|
|
|
|
NoJSView *view=NOJS_VIEW(inUserData);
|
|
NoJSMenuIconState menuIconState;
|
|
GtkWidget *statusbarIcon;
|
|
GtkWidget *buttonImage;
|
|
gchar *imageFilename;
|
|
|
|
/* Get icon in status bar of this browser */
|
|
statusbarIcon=GTK_WIDGET(g_object_get_data(G_OBJECT(inBrowser), "nojs-statusicon"));
|
|
g_return_if_fail(GTK_IS_WIDGET(statusbarIcon));
|
|
|
|
/* Get menu icon state of view */
|
|
menuIconState=nojs_view_get_menu_icon_state(view);
|
|
|
|
/* Create image for statusbar button */
|
|
imageFilename=NULL;
|
|
switch(menuIconState)
|
|
{
|
|
case NOJS_MENU_ICON_STATE_ALLOWED:
|
|
imageFilename=nojs_get_icon_path("nojs-statusicon-allowed.png");
|
|
break;
|
|
|
|
case NOJS_MENU_ICON_STATE_MIXED:
|
|
imageFilename=nojs_get_icon_path("nojs-statusicon-mixed.png");
|
|
break;
|
|
|
|
case NOJS_MENU_ICON_STATE_DENIED:
|
|
case NOJS_MENU_ICON_STATE_UNDETERMINED:
|
|
imageFilename=nojs_get_icon_path("nojs-statusicon-denied.png");
|
|
break;
|
|
}
|
|
|
|
buttonImage=gtk_image_new_from_file(imageFilename);
|
|
g_free(imageFilename);
|
|
|
|
/* Set image at statusbar button */
|
|
gtk_button_set_image(GTK_BUTTON(statusbarIcon), buttonImage);
|
|
}
|
|
|
|
/* A tab in browser was activated */
|
|
static void _nojs_on_switch_tab(NoJS *self, MidoriView *inOldView, MidoriView *inNewView, gpointer inUserData)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
g_return_if_fail(MIDORI_IS_BROWSER(inUserData));
|
|
|
|
MidoriBrowser *browser=MIDORI_BROWSER(inUserData);
|
|
NoJSView *view;
|
|
|
|
/* Disconnect signal handlers from old view */
|
|
if(inOldView)
|
|
{
|
|
/* Get NoJS view of old view */
|
|
view=(NoJSView*)g_object_get_data(G_OBJECT(inOldView), "nojs-view-instance");
|
|
g_return_if_fail(NOJS_IS_VIEW(view));
|
|
|
|
/* Disconnect signal handlers */
|
|
g_signal_handlers_disconnect_by_func(view, G_CALLBACK(_nojs_on_menu_icon_changed), browser);
|
|
}
|
|
|
|
/* Get NoJS view of new view */
|
|
view=(NoJSView*)g_object_get_data(G_OBJECT(inNewView), "nojs-view-instance");
|
|
g_return_if_fail(NOJS_IS_VIEW(view));
|
|
|
|
/* Connect signals */
|
|
g_signal_connect_swapped(view, "notify::menu-icon-state", G_CALLBACK(_nojs_on_menu_icon_changed), browser);
|
|
|
|
/* Update menu icon*/
|
|
_nojs_on_menu_icon_changed(browser, NULL, view);
|
|
}
|
|
|
|
/* A tab of a browser was removed */
|
|
static void _nojs_on_remove_tab(NoJS *self, MidoriView *inView, gpointer inUserData)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
|
|
NoJSView *view;
|
|
|
|
/* Get NoJS view of current active midori view */
|
|
view=NOJS_VIEW(g_object_get_data(G_OBJECT(inView), "nojs-view-instance"));
|
|
g_return_if_fail(NOJS_IS_VIEW(view));
|
|
|
|
g_object_unref(view);
|
|
}
|
|
|
|
/* A tab of a browser was added */
|
|
static void _nojs_on_add_tab(NoJS *self, MidoriView *inView, gpointer inUserData)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
g_return_if_fail(MIDORI_IS_BROWSER(inUserData));
|
|
|
|
/* Create nojs view and add to tab */
|
|
MidoriBrowser *browser=MIDORI_BROWSER(inUserData);
|
|
|
|
nojs_view_new(self, browser, inView);
|
|
}
|
|
|
|
/* A browser window was added */
|
|
static void _nojs_on_add_browser(NoJS *self, MidoriBrowser *inBrowser, gpointer inUserData)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
g_return_if_fail(MIDORI_IS_BROWSER(inBrowser));
|
|
|
|
GList *tabs, *iter;
|
|
GtkWidget *statusbar;
|
|
GtkWidget *statusbarIcon;
|
|
MidoriView *view;
|
|
NoJSView *nojsView;
|
|
|
|
/* Set up all current available tabs in browser */
|
|
tabs=midori_browser_get_tabs(inBrowser);
|
|
for(iter=tabs; iter; iter=g_list_next(iter)) _nojs_on_add_tab(self, iter->data, inBrowser);
|
|
g_list_free(tabs);
|
|
|
|
/* Add status bar icon to browser */
|
|
g_object_get(inBrowser, "statusbar", &statusbar, NULL);
|
|
if(statusbar)
|
|
{
|
|
/* Create and set up status icon */
|
|
statusbarIcon=gtk_button_new();
|
|
gtk_button_set_relief(GTK_BUTTON(statusbarIcon), GTK_RELIEF_NONE);
|
|
gtk_widget_show_all(statusbarIcon);
|
|
gtk_box_pack_end(GTK_BOX(statusbar), statusbarIcon, FALSE, FALSE, 0);
|
|
g_object_set_data_full(G_OBJECT(inBrowser), "nojs-statusicon", g_object_ref(statusbarIcon), (GDestroyNotify)gtk_widget_destroy);
|
|
|
|
/* Connect signals */
|
|
g_signal_connect_swapped(statusbarIcon, "clicked", G_CALLBACK(_nojs_on_statusbar_icon_clicked), inBrowser);
|
|
|
|
/* Release our reference to statusbar and status icon */
|
|
g_object_unref(statusbarIcon);
|
|
g_object_unref(statusbar);
|
|
|
|
/* Update menu icon*/
|
|
view=MIDORI_VIEW(midori_browser_get_current_tab(inBrowser));
|
|
if(view)
|
|
{
|
|
nojsView=(NoJSView*)g_object_get_data(G_OBJECT(view), "nojs-view-instance");
|
|
if(nojsView) _nojs_on_menu_icon_changed(inBrowser, NULL, nojsView);
|
|
}
|
|
}
|
|
|
|
/* Listen to new tabs opened in browser */
|
|
g_signal_connect_swapped(inBrowser, "add-tab", G_CALLBACK(_nojs_on_add_tab), self);
|
|
g_signal_connect_swapped(inBrowser, "switch-tab", G_CALLBACK(_nojs_on_switch_tab), self);
|
|
g_signal_connect_swapped(inBrowser, "remove-tab", G_CALLBACK(_nojs_on_remove_tab), self);
|
|
}
|
|
|
|
/* Application property has changed */
|
|
static void _nojs_on_application_changed(NoJS *self)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
|
|
NoJSPrivate *priv=NOJS(self)->priv;
|
|
GList *browsers, *iter;
|
|
|
|
/* Set up all current open browser windows */
|
|
browsers=midori_app_get_browsers(priv->application);
|
|
for(iter=browsers; iter; iter=g_list_next(iter)) _nojs_on_add_browser(self, MIDORI_BROWSER(iter->data), priv->application);
|
|
g_list_free(browsers);
|
|
|
|
/* Listen to new browser windows opened */
|
|
g_signal_connect_swapped(priv->application, "add-browser", G_CALLBACK(_nojs_on_add_browser), self);
|
|
|
|
/* Notify about property change */
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_APPLICATION]);
|
|
}
|
|
|
|
/* IMPLEMENTATION: GObject */
|
|
|
|
/* Finalize this object */
|
|
static void nojs_finalize(GObject *inObject)
|
|
{
|
|
NoJS *self=NOJS(inObject);
|
|
NoJSPrivate *priv=self->priv;
|
|
GList *browsers, *browser;
|
|
GList *tabs, *tab;
|
|
WebKitWebView *webkitView;
|
|
SoupSession *session;
|
|
|
|
/* Dispose allocated resources */
|
|
session=webkit_get_default_session();
|
|
g_signal_handlers_disconnect_by_data(session, self);
|
|
|
|
if(priv->databaseFilename)
|
|
{
|
|
g_free(priv->databaseFilename);
|
|
priv->databaseFilename=NULL;
|
|
}
|
|
|
|
if(priv->database)
|
|
{
|
|
sqlite3_close(priv->database);
|
|
priv->database=NULL;
|
|
}
|
|
|
|
if(priv->application)
|
|
{
|
|
g_signal_handlers_disconnect_by_data(priv->application, self);
|
|
|
|
browsers=midori_app_get_browsers(priv->application);
|
|
for(browser=browsers; browser; browser=g_list_next(browser))
|
|
{
|
|
g_signal_handlers_disconnect_by_data(browser->data, self);
|
|
g_object_set_data(G_OBJECT(browser->data), "nojs-statusicon", NULL);
|
|
|
|
tabs=midori_browser_get_tabs(MIDORI_BROWSER(browser->data));
|
|
for(tab=tabs; tab; tab=g_list_next(tab))
|
|
{
|
|
g_signal_handlers_disconnect_by_data(tab->data, self);
|
|
|
|
webkitView=WEBKIT_WEB_VIEW(midori_view_get_web_view(MIDORI_VIEW(tab->data)));
|
|
g_signal_handlers_disconnect_by_data(webkitView, self);
|
|
}
|
|
g_list_free(tabs);
|
|
}
|
|
g_list_free(browsers);
|
|
|
|
priv->application=NULL;
|
|
}
|
|
|
|
/* Call parent's class finalize method */
|
|
G_OBJECT_CLASS(nojs_parent_class)->finalize(inObject);
|
|
}
|
|
|
|
/* Set/get properties */
|
|
static void nojs_set_property(GObject *inObject,
|
|
guint inPropID,
|
|
const GValue *inValue,
|
|
GParamSpec *inSpec)
|
|
{
|
|
NoJS *self=NOJS(inObject);
|
|
|
|
switch(inPropID)
|
|
{
|
|
/* Construct-only properties */
|
|
case PROP_EXTENSION:
|
|
self->priv->extension=g_value_get_object(inValue);
|
|
_nojs_open_database(self);
|
|
break;
|
|
|
|
case PROP_APPLICATION:
|
|
self->priv->application=g_value_get_object(inValue);
|
|
_nojs_on_application_changed(self);
|
|
break;
|
|
|
|
case PROP_ALLOW_LOCAL_PAGES:
|
|
self->priv->allowLocalPages=g_value_get_boolean(inValue);
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_ALLOW_LOCAL_PAGES]);
|
|
break;
|
|
|
|
case PROP_ONLY_SECOND_LEVEL:
|
|
self->priv->checkOnlySecondLevel=g_value_get_boolean(inValue);
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_ONLY_SECOND_LEVEL]);
|
|
break;
|
|
|
|
case PROP_UNKNOWN_DOMAIN_POLICY:
|
|
self->priv->unknownDomainPolicy=g_value_get_enum(inValue);
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_UNKNOWN_DOMAIN_POLICY]);
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(inObject, inPropID, inSpec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void nojs_get_property(GObject *inObject,
|
|
guint inPropID,
|
|
GValue *outValue,
|
|
GParamSpec *inSpec)
|
|
{
|
|
NoJS *self=NOJS(inObject);
|
|
|
|
switch(inPropID)
|
|
{
|
|
case PROP_EXTENSION:
|
|
g_value_set_object(outValue, self->priv->extension);
|
|
break;
|
|
|
|
case PROP_APPLICATION:
|
|
g_value_set_object(outValue, self->priv->application);
|
|
break;
|
|
|
|
case PROP_DATABASE:
|
|
g_value_set_pointer(outValue, self->priv->database);
|
|
break;
|
|
|
|
case PROP_DATABASE_FILENAME:
|
|
g_value_set_string(outValue, self->priv->databaseFilename);
|
|
break;
|
|
|
|
case PROP_ALLOW_LOCAL_PAGES:
|
|
g_value_set_boolean(outValue, self->priv->allowLocalPages);
|
|
break;
|
|
|
|
case PROP_ONLY_SECOND_LEVEL:
|
|
g_value_set_boolean(outValue, self->priv->checkOnlySecondLevel);
|
|
break;
|
|
|
|
case PROP_UNKNOWN_DOMAIN_POLICY:
|
|
g_value_set_enum(outValue, self->priv->unknownDomainPolicy);
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(inObject, inPropID, inSpec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Class initialization
|
|
* Override functions in parent classes and define properties and signals
|
|
*/
|
|
static void nojs_class_init(NoJSClass *klass)
|
|
{
|
|
GObjectClass *gobjectClass=G_OBJECT_CLASS(klass);
|
|
|
|
/* Override functions */
|
|
gobjectClass->finalize=nojs_finalize;
|
|
gobjectClass->set_property=nojs_set_property;
|
|
gobjectClass->get_property=nojs_get_property;
|
|
|
|
/* Set up private structure */
|
|
g_type_class_add_private(klass, sizeof(NoJSPrivate));
|
|
|
|
/* Define properties */
|
|
NoJSProperties[PROP_EXTENSION]=
|
|
g_param_spec_object("extension",
|
|
_("Extension instance"),
|
|
_("The Midori extension instance for this extension"),
|
|
MIDORI_TYPE_EXTENSION,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
|
|
|
|
NoJSProperties[PROP_APPLICATION]=
|
|
g_param_spec_object("application",
|
|
_("Application instance"),
|
|
_("The Midori application instance this extension belongs to"),
|
|
MIDORI_TYPE_APP,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
|
|
|
|
NoJSProperties[PROP_DATABASE]=
|
|
g_param_spec_pointer("database",
|
|
_("Database instance"),
|
|
_("Pointer to sqlite database instance used by this extension"),
|
|
G_PARAM_READABLE);
|
|
|
|
NoJSProperties[PROP_DATABASE_FILENAME]=
|
|
g_param_spec_string("database-filename",
|
|
_("Database path"),
|
|
_("Path to sqlite database instance used by this extension"),
|
|
NULL,
|
|
G_PARAM_READABLE);
|
|
|
|
NoJSProperties[PROP_ALLOW_LOCAL_PAGES]=
|
|
g_param_spec_boolean("allow-local-pages",
|
|
_("Allow local pages"),
|
|
_("Allow scripts to run on local (file://) pages"),
|
|
TRUE,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
|
|
|
|
NoJSProperties[PROP_ONLY_SECOND_LEVEL]=
|
|
g_param_spec_boolean("only-second-level",
|
|
_("Only second level"),
|
|
_("Reduce each domain to its second-level (e.g. www.example.org to example.org) for comparison"),
|
|
TRUE,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
|
|
|
|
NoJSProperties[PROP_UNKNOWN_DOMAIN_POLICY]=
|
|
g_param_spec_enum("unknown-domain-policy",
|
|
_("Unknown domain policy"),
|
|
_("Policy to use for unknown domains"),
|
|
NOJS_TYPE_POLICY,
|
|
NOJS_POLICY_BLOCK,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
|
|
|
|
g_object_class_install_properties(gobjectClass, PROP_LAST, NoJSProperties);
|
|
|
|
/* Define signals */
|
|
|
|
/* Why does this signal exist?
|
|
*
|
|
* The problem I faced when developing this extension was
|
|
* that I needed to cancel a SoupMessage as soon as possible
|
|
* (when http headers were received).
|
|
* I tried to connect to signal "resource-response-received"
|
|
* of WebKitWebView but the SoupMessage instance was not
|
|
* exactly the same which were sent or received by SoupSession.
|
|
* So I could not cancel the SoupMessage or better: I cancelled
|
|
* a SoupMessage which is not be handled so it had no effect.
|
|
* The body of SoupMessage was still being loaded and javascript
|
|
* was executed. I think the problem is that webkit-gtk creates
|
|
* a copy of the real SoupMessage which is going to be sent and
|
|
* received.
|
|
*
|
|
* So I decided to connect to signal "got-headers" of every
|
|
* SoupMessage sent by the default SoupSession which I notice
|
|
* by connecting to signal "request-started" of SoupSession. Each
|
|
* NoJSView connects to signal "resource-request-starting" of
|
|
* WebKitWebView to remember each URI going to be loaded. When
|
|
* a SoupMessage hits "got-headers" and is a javascript resource
|
|
* I can cancel the message immediately and clear the body which
|
|
* causes webkit-gtk to copy a empty body if it does at all as the
|
|
* SoupMessage was cancelled. Then I emit this signal
|
|
* "uri-load-policy-status" to notify each view but the cancellation.
|
|
* (It also notifies all views if it is going to load to keep the
|
|
* menu in right state.) Each view will check if it _could_ be a
|
|
* resource itself requested and will update its menu accordingly.
|
|
* It might happen that a request will match two views because only
|
|
* the URI will be checked by the view because I cannot determine
|
|
* to which view the SoupMessage belongs to. But it doesn't matter
|
|
* because if a javascript resource was denied or allowed in one view
|
|
* it is likely be denied or allowed in other views too ;)
|
|
*/
|
|
NoJSSignals[URI_LOAD_POLICY_STATUS]=
|
|
g_signal_new("uri-load-policy-status",
|
|
G_TYPE_FROM_CLASS(klass),
|
|
G_SIGNAL_RUN_LAST,
|
|
G_STRUCT_OFFSET(NoJSClass, uri_load_policy_status),
|
|
NULL,
|
|
NULL,
|
|
_nojs_closure_VOID__STRING_ENUM,
|
|
G_TYPE_NONE,
|
|
2,
|
|
G_TYPE_STRING,
|
|
NOJS_TYPE_POLICY);
|
|
|
|
NoJSSignals[POLICY_CHANGED]=
|
|
g_signal_new("policy-changed",
|
|
G_TYPE_FROM_CLASS(klass),
|
|
G_SIGNAL_RUN_LAST,
|
|
G_STRUCT_OFFSET(NoJSClass, policy_changed),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__STRING,
|
|
G_TYPE_NONE,
|
|
1,
|
|
G_TYPE_STRING);
|
|
}
|
|
|
|
/* Object initialization
|
|
* Create private structure and set up default values
|
|
*/
|
|
|
|
static void nojs_init(NoJS *self)
|
|
{
|
|
NoJSPrivate *priv;
|
|
SoupSession *session;
|
|
|
|
priv=self->priv=NOJS_GET_PRIVATE(self);
|
|
|
|
/* Set up default values */
|
|
priv->database=NULL;
|
|
priv->databaseFilename=NULL;
|
|
priv->allowLocalPages=TRUE;
|
|
priv->checkOnlySecondLevel=TRUE;
|
|
priv->unknownDomainPolicy=NOJS_POLICY_BLOCK;
|
|
|
|
/* Connect to signals on session to be able to cancel messages
|
|
* loading javascript documents
|
|
*/
|
|
session=webkit_get_default_session();
|
|
g_signal_connect_swapped(session, "request-started", G_CALLBACK(_nojs_on_request_started), self);
|
|
}
|
|
|
|
/* Implementation: Public API */
|
|
|
|
/* Create new object */
|
|
NoJS* nojs_new(MidoriExtension *inExtension, MidoriApp *inApp)
|
|
{
|
|
return(g_object_new(TYPE_NOJS,
|
|
"extension", inExtension,
|
|
"application", inApp,
|
|
NULL));
|
|
}
|
|
|
|
/* Retrieves domain from uri depending on preferences (e.g. only second level domain) */
|
|
gchar* nojs_get_domain(NoJS *self, SoupURI *inURI)
|
|
{
|
|
g_return_val_if_fail(IS_NOJS(self), NULL);
|
|
g_return_val_if_fail(inURI, NULL);
|
|
|
|
NoJSPrivate *priv=self->priv;
|
|
const gchar *realDomain;
|
|
gchar *finalDomain;
|
|
|
|
/* Get domain of site to lookup */
|
|
realDomain=soup_uri_get_host(inURI);
|
|
|
|
if(priv->checkOnlySecondLevel)
|
|
finalDomain=midori_uri_get_base_domain(realDomain);
|
|
else
|
|
finalDomain=midori_uri_to_ascii(realDomain);
|
|
|
|
/* Return domain */
|
|
return(finalDomain);
|
|
}
|
|
|
|
/* Get/set policy for javascript from site */
|
|
gint nojs_get_policy(NoJS *self, SoupURI *inURI)
|
|
{
|
|
g_return_val_if_fail(IS_NOJS(self), NOJS_POLICY_UNDETERMINED);
|
|
g_return_val_if_fail(inURI, NOJS_POLICY_UNDETERMINED);
|
|
|
|
NoJSPrivate *priv=self->priv;
|
|
sqlite3_stmt *statement=NULL;
|
|
gint error;
|
|
gint policy=NOJS_POLICY_UNDETERMINED;
|
|
gchar *inDomain;
|
|
|
|
/* Check to allow local pages */
|
|
if(soup_uri_get_scheme(inURI) == SOUP_URI_SCHEME_FILE)
|
|
{
|
|
if(priv->allowLocalPages) return(NOJS_POLICY_ACCEPT);
|
|
else return(priv->unknownDomainPolicy);
|
|
}
|
|
|
|
/* Check for open database */
|
|
g_return_val_if_fail(priv->database, policy);
|
|
|
|
/* Get domain from URI */
|
|
inDomain=nojs_get_domain(self, inURI);
|
|
|
|
/* Lookup policy for site in database */
|
|
error=sqlite3_prepare_v2(priv->database,
|
|
"SELECT site, value FROM policies WHERE site LIKE ? LIMIT 1;",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if(statement && error==SQLITE_OK) error=sqlite3_bind_text(statement, 1, inDomain, -1, NULL);
|
|
if(statement && error==SQLITE_OK)
|
|
{
|
|
if(sqlite3_step(statement)==SQLITE_ROW) policy=sqlite3_column_int(statement, 1);
|
|
}
|
|
else g_warning(_("SQL fails: %s"), sqlite3_errmsg(priv->database));
|
|
|
|
sqlite3_finalize(statement);
|
|
|
|
/* If we have not found a policy for the domain then it is an unknown domain.
|
|
* Get default policy for unknown domains.
|
|
*/
|
|
if(policy==NOJS_POLICY_UNDETERMINED) policy=priv->unknownDomainPolicy;
|
|
|
|
return(policy);
|
|
}
|
|
|
|
void nojs_set_policy(NoJS *self, const gchar *inDomain, NoJSPolicy inPolicy)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
g_return_if_fail(inDomain);
|
|
g_return_if_fail(inPolicy>=NOJS_POLICY_ACCEPT && inPolicy<=NOJS_POLICY_BLOCK);
|
|
|
|
NoJSPrivate *priv=self->priv;
|
|
gchar *sql;
|
|
gchar *error=NULL;
|
|
gint success;
|
|
|
|
/* Check for open database */
|
|
g_return_if_fail(priv->database);
|
|
|
|
/* Update policy in database */
|
|
sql=sqlite3_mprintf("INSERT OR REPLACE INTO policies (site, value) VALUES ('%q', %d);",
|
|
inDomain,
|
|
inPolicy);
|
|
success=sqlite3_exec(priv->database, sql, NULL, NULL, &error);
|
|
if(success!=SQLITE_OK) g_warning(_("SQL fails: %s"), error);
|
|
if(error) sqlite3_free(error);
|
|
sqlite3_free(sql);
|
|
|
|
/* Emit signal to notify about policy change */
|
|
if(success==SQLITE_OK) g_signal_emit(self, NoJSSignals[POLICY_CHANGED], 0, inDomain);
|
|
}
|
|
|
|
/* Get/set default policy for unknown domains */
|
|
NoJSPolicy nojs_get_policy_for_unknown_domain(NoJS *self)
|
|
{
|
|
g_return_val_if_fail(IS_NOJS(self), NOJS_POLICY_UNDETERMINED);
|
|
|
|
return(self->priv->unknownDomainPolicy);
|
|
}
|
|
|
|
void nojs_set_policy_for_unknown_domain(NoJS *self, NoJSPolicy inPolicy)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
g_return_if_fail(inPolicy>=NOJS_POLICY_ACCEPT && inPolicy<=NOJS_POLICY_BLOCK);
|
|
|
|
if(self->priv->unknownDomainPolicy!=inPolicy)
|
|
{
|
|
self->priv->unknownDomainPolicy=inPolicy;
|
|
midori_extension_set_integer(self->priv->extension, "unknown-domain-policy", inPolicy);
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_UNKNOWN_DOMAIN_POLICY]);
|
|
}
|
|
}
|
|
|
|
/* Get/set flag to allow javascript on local pages */
|
|
gboolean nojs_get_allow_local_pages(NoJS *self)
|
|
{
|
|
g_return_val_if_fail(IS_NOJS(self), TRUE);
|
|
|
|
return(self->priv->allowLocalPages);
|
|
}
|
|
|
|
void nojs_set_allow_local_pages(NoJS *self, gboolean inAllow)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
|
|
if(self->priv->allowLocalPages!=inAllow)
|
|
{
|
|
self->priv->allowLocalPages=inAllow;
|
|
midori_extension_set_boolean(self->priv->extension, "allow-local-pages", inAllow);
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_ALLOW_LOCAL_PAGES]);
|
|
}
|
|
}
|
|
|
|
/* Get/set flag to check for second-level domains only */
|
|
gboolean nojs_get_only_second_level_domain(NoJS *self)
|
|
{
|
|
g_return_val_if_fail(IS_NOJS(self), TRUE);
|
|
|
|
return(self->priv->checkOnlySecondLevel);
|
|
}
|
|
|
|
void nojs_set_only_second_level_domain(NoJS *self, gboolean inOnlySecondLevel)
|
|
{
|
|
g_return_if_fail(IS_NOJS(self));
|
|
|
|
if(self->priv->checkOnlySecondLevel!=inOnlySecondLevel)
|
|
{
|
|
self->priv->checkOnlySecondLevel=inOnlySecondLevel;
|
|
midori_extension_set_boolean(self->priv->extension, "only-second-level", inOnlySecondLevel);
|
|
g_object_notify_by_pspec(G_OBJECT(self), NoJSProperties[PROP_ONLY_SECOND_LEVEL]);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************/
|
|
|
|
/* Implementation: Enumeration */
|
|
GType nojs_policy_get_type(void)
|
|
{
|
|
static volatile gsize g_define_type_id__volatile=0;
|
|
|
|
if(g_once_init_enter(&g_define_type_id__volatile))
|
|
{
|
|
static const GEnumValue values[]=
|
|
{
|
|
{ NOJS_POLICY_UNDETERMINED, "NOJS_POLICY_UNDETERMINED", N_("Undetermined") },
|
|
{ NOJS_POLICY_ACCEPT, "NOJS_POLICY_ACCEPT", N_("Accept") },
|
|
{ NOJS_POLICY_ACCEPT_TEMPORARILY, "NOJS_POLICY_ACCEPT_TEMPORARILY", N_("Accept temporarily") },
|
|
{ NOJS_POLICY_BLOCK, "NOJS_POLICY_BLOCK", N_("Block") },
|
|
{ 0, NULL, NULL }
|
|
};
|
|
|
|
GType g_define_type_id=g_enum_register_static(g_intern_static_string("NoJSPolicy"), values);
|
|
g_once_init_leave(&g_define_type_id__volatile, g_define_type_id);
|
|
}
|
|
|
|
return(g_define_type_id__volatile);
|
|
}
|