/* Copyright (C) 2013 Stephan Haller 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 "cookie-permission-manager.h" #include /* Remove next line if we found a way to show details in infobar */ #define NO_INFOBAR_DETAILS /* Define this class in GObject system */ G_DEFINE_TYPE(CookiePermissionManager, cookie_permission_manager, G_TYPE_OBJECT) /* Properties */ enum { PROP_0, PROP_EXTENSION, PROP_APPLICATION, PROP_DATABASE, PROP_DATABASE_FILENAME, PROP_ASK_FOR_UNKNOWN_POLICY, PROP_LAST }; static GParamSpec* CookiePermissionManagerProperties[PROP_LAST]={ 0, }; /* Private structure - access only by public API if needed */ #define COOKIE_PERMISSION_MANAGER_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE((obj), TYPE_COOKIE_PERMISSION_MANAGER, CookiePermissionManagerPrivate)) struct _CookiePermissionManagerPrivate { /* Extension related */ MidoriExtension *extension; MidoriApp *application; sqlite3 *database; gchar *databaseFilename; gboolean askForUnknownPolicy; /* Cookie jar related */ SoupSession *session; SoupCookieJar *cookieJar; SoupSessionFeatureInterface *featureIface; gint cookieJarChangedID; }; enum { DOMAIN_COLUMN, PATH_COLUMN, NAME_COLUMN, VALUE_COLUMN, EXPIRE_DATE_COLUMN, N_COLUMN }; struct _CookiePermissionManagerModalInfobar { GMainLoop *mainLoop; gint response; }; typedef struct _CookiePermissionManagerModalInfobar CookiePermissionManagerModalInfobar; /* IMPLEMENTATION: Private variables and methods */ /* Show common error dialog */ static void _cookie_permission_manager_error(CookiePermissionManager *self, const gchar *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 cookie permission manager extension " "to continue. You should disable it.")); gtk_window_set_title(GTK_WINDOW(dialog), _("Error in cookie permission manager 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 cookie domains. * Create database and setup table structure if it does not exist yet. */ static void _cookie_permission_manager_open_database(CookiePermissionManager *self) { CookiePermissionManagerPrivate *priv=self->priv; const gchar *configDir; gchar *error=NULL; gint success; sqlite3_stmt *statement=NULL; /* Close any open database */ if(priv->database) { g_free(priv->databaseFilename); priv->databaseFilename=NULL; sqlite3_close(priv->database); priv->database=NULL; g_object_notify_by_pspec(G_OBJECT(self), CookiePermissionManagerProperties[PROP_DATABASE]); g_object_notify_by_pspec(G_OBJECT(self), CookiePermissionManagerProperties[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)); _cookie_permission_manager_error(self, _("Could not create configuration folder for extension.")); return; } /* Open database */ priv->databaseFilename=g_build_filename(configDir, COOKIE_PERMISSION_DATABASE, NULL); success=sqlite3_open(priv->databaseFilename, &priv->database); if(success!=SQLITE_OK) { g_warning(_("Could not open database of extenstion: %s"), sqlite3_errmsg(priv->database)); g_free(priv->databaseFilename); priv->databaseFilename=NULL; if(priv->database) sqlite3_close(priv->database); priv->database=NULL; _cookie_permission_manager_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(domain text, value integer);", NULL, NULL, &error); if(success==SQLITE_OK) { success=sqlite3_exec(priv->database, "CREATE UNIQUE INDEX IF NOT EXISTS " "domain ON policies (domain);", NULL, NULL, &error); } if(success==SQLITE_OK) { success=sqlite3_exec(priv->database, "PRAGMA journal_mode=TRUNCATE;", NULL, NULL, &error); } if(success!=SQLITE_OK || error) { _cookie_permission_manager_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 cookies allowed only in one session success=sqlite3_prepare_v2(priv->database, "SELECT domain FROM policies WHERE value=? ORDER BY domain DESC;", -1, &statement, NULL); if(statement && success==SQLITE_OK) success=sqlite3_bind_int(statement, 1, COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT_FOR_SESSION); if(statement && success==SQLITE_OK) { while(sqlite3_step(statement)==SQLITE_ROW) { gchar *domain=(gchar*)sqlite3_column_text(statement, 0); GSList *cookies, *cookie; #ifdef HAVE_LIBSOUP_2_40_0 SoupURI *uri; uri=soup_uri_new(NULL); soup_uri_set_host(uri, domain); cookies=soup_cookie_jar_get_cookie_list(priv->cookieJar, uri, TRUE); for(cookie=cookies; cookie; cookie->next) { soup_cookie_jar_delete_cookie(priv->cookieJar, (SoupCookie*)cookie->data); } soup_cookies_free(cookies); soup_uri_free(uri); #else cookies=soup_cookie_jar_all_cookies(priv->cookieJar); for(cookie=cookies; cookie; cookie=cookie->next) { if(soup_cookie_domain_matches((SoupCookie*)cookie->data, domain)) { soup_cookie_jar_delete_cookie(priv->cookieJar, (SoupCookie*)cookie->data); } } soup_cookies_free(cookies); #endif } } else g_warning(_("SQL fails: %s"), sqlite3_errmsg(priv->database)); sqlite3_finalize(statement); g_object_notify_by_pspec(G_OBJECT(self), CookiePermissionManagerProperties[PROP_DATABASE]); g_object_notify_by_pspec(G_OBJECT(self), CookiePermissionManagerProperties[PROP_DATABASE_FILENAME]); } /* Get policy for cookies from domain */ static gint _cookie_permission_manager_get_policy(CookiePermissionManager *self, SoupCookie *inCookie) { CookiePermissionManagerPrivate *priv=self->priv; sqlite3_stmt *statement=NULL; gchar *domain; gint error; gint policy=COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED; gboolean foundPolicy=FALSE; /* Check for open database */ g_return_val_if_fail(priv->database, COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED); /* Lookup policy for cookie domain in database */ domain=g_strdup(soup_cookie_get_domain(inCookie)); if(*domain=='.') *domain='%'; error=sqlite3_prepare_v2(priv->database, "SELECT domain, value FROM policies WHERE domain LIKE ? ORDER BY domain DESC;", -1, &statement, NULL); if(statement && error==SQLITE_OK) error=sqlite3_bind_text(statement, 1, domain, -1, NULL); if(statement && error==SQLITE_OK) { while(policy==COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED && sqlite3_step(statement)==SQLITE_ROW) { gchar *policyDomain=(gchar*)sqlite3_column_text(statement, 0); if(soup_cookie_domain_matches(inCookie, policyDomain)) { policy=sqlite3_column_int(statement, 1); foundPolicy=TRUE; } } } else g_warning(_("SQL fails: %s"), sqlite3_errmsg(priv->database)); sqlite3_finalize(statement); /* Check if policy is undetermined. If it is then check if this policy was set by user. * If it was not set by user check if we should ask user for his decision */ if(!priv->askForUnknownPolicy && !foundPolicy) { switch(soup_cookie_jar_get_accept_policy(priv->cookieJar)) { case SOUP_COOKIE_JAR_ACCEPT_ALWAYS: case SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY: policy=COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT; break; case SOUP_COOKIE_JAR_ACCEPT_NEVER: policy=COOKIE_PERMISSION_MANAGER_POLICY_BLOCK; break; default: g_critical(_("Could not determine global cookie policy to set for domain: %s"), domain); break; } } /* Release allocated resources */ g_free(domain); return(policy); } /* Ask user what to do with cookies from domain(s) which were neither marked accepted nor blocked */ static gint _cookie_permission_manager_sort_cookies_by_domain(SoupCookie *inLeft, SoupCookie *inRight) { const gchar *domainLeft=soup_cookie_get_domain(inLeft); const gchar *domainRight=soup_cookie_get_domain(inRight); if(*domainLeft=='.') domainLeft++; if(*domainRight=='.') domainRight++; return(g_ascii_strcasecmp(domainLeft, domainRight)); } static GSList* _cookie_permission_manager_get_number_domains_and_cookies(CookiePermissionManager *self, GSList *inCookies, gint *ioNumberDomains, gint *ioNumberCookies) { GSList *sortedList, *iter; gint domains, cookies; const gchar *lastDomain=NULL; const gchar *cookieDomain; /* Make copy and sort cookies in new list */ sortedList=g_slist_copy(inCookies); /* Sort cookies by domain to prevent a doman counted multiple times */ sortedList=g_slist_sort(sortedList, (GCompareFunc)_cookie_permission_manager_sort_cookies_by_domain); /* Iterate through list and count domains and cookies */ domains=cookies=0; for(iter=sortedList; iter; iter=iter->next) { cookieDomain=soup_cookie_get_domain((SoupCookie*)iter->data); if(!lastDomain || g_ascii_strcasecmp(lastDomain, cookieDomain)!=0) { domains++; lastDomain=cookieDomain; } cookies++; } /* Store counted numbers to final variables */ if(ioNumberDomains) *ioNumberDomains=domains; if(ioNumberCookies) *ioNumberCookies=cookies; /* Return the copied but sorted cookie list. Caller is responsible to free * this list with g_slist_free */ return(sortedList); } /* FIXME: Find a way to add "details" widget */ #ifndef NO_INFOBAR_DETAILS static void _cookie_permission_manager_when_ask_expander_changed(CookiePermissionManager *self, GParamSpec *inSpec, gpointer inUserData) { GtkExpander *expander=GTK_EXPANDER(inUserData); midori_extension_set_boolean(self->priv->extension, "show-details-when-ask", gtk_expander_get_expanded(expander)); } #endif static gboolean _cookie_permission_manager_on_infobar_webview_navigate(WebKitWebView *inView, WebKitWebFrame *inFrame, WebKitNetworkRequest *inRequest, WebKitWebNavigationAction *inAction, WebKitWebPolicyDecision *inDecision, gpointer inUserData) { /* Destroy info bar - that calls another callback which quits main loop */ GtkWidget *infobar=GTK_WIDGET(inUserData); gtk_widget_destroy(infobar); /* Let the default handler decide */ return(FALSE); } static void _cookie_permission_manager_on_infobar_destroy(GtkWidget* inInfobar, gpointer inUserData) { CookiePermissionManagerModalInfobar *modalInfo=(CookiePermissionManagerModalInfobar*)inUserData; /* Quit main loop */ if(g_main_loop_is_running(modalInfo->mainLoop)) g_main_loop_quit(modalInfo->mainLoop); } static void _cookie_permission_manager_on_infobar_policy_decision(GtkWidget* inInfobar, gint inResponse, gpointer inUserData) { CookiePermissionManagerModalInfobar *modalInfo; /* Get modal info struct */ modalInfo=(CookiePermissionManagerModalInfobar*)g_object_get_data(G_OBJECT(inInfobar), "cookie-permission-manager-infobar-data"); /* Store response */ modalInfo->response=inResponse; /* Quit main loop */ if(g_main_loop_is_running(modalInfo->mainLoop)) g_main_loop_quit(modalInfo->mainLoop); } static gint _cookie_permission_manager_ask_for_policy(CookiePermissionManager *self, MidoriView *inView, SoupMessage *inMessage, GSList *inUnknownCookies) { /* Ask user for policy of unkndown domains in an undistracting way. * The idea is to put the message not in a modal window but into midori's info bar. * Then we'll set up our own GMainLoop to simulate a modal info bar. We need to * connect to all possible signals of info bar, web view and so on to handle user's * decision and to get out of our own GMainLoop. After that webkit resumes processing * data. */ CookiePermissionManagerPrivate *priv=self->priv; GtkWidget *infobar; /* FIXME: Find a way to add "details" widget */ #ifndef NO_INFOBAR_DETAILS GtkWidget *widget; GtkWidget *contentArea; GtkWidget *vbox, *hbox; GtkWidget *expander; GtkListStore *listStore; GtkTreeIter listIter; GtkWidget *scrolled; GtkWidget *list; GtkCellRenderer *renderer; GtkTreeViewColumn *column; #endif gchar *text; gint numberDomains, numberCookies; GSList *sortedCookies, *cookies; WebKitWebView *webkitView; CookiePermissionManagerModalInfobar modalInfo; /* Get webkit view of midori view */ webkitView=WEBKIT_WEB_VIEW(midori_view_get_web_view(inView)); /* Create a copy of cookies and sort them */ sortedCookies=_cookie_permission_manager_get_number_domains_and_cookies(self, inUnknownCookies, &numberDomains, &numberCookies); /* FIXME: Find a way to add "details" widget */ #ifndef NO_INFOBAR_DETAILS /* Create list model and fill in data */ listStore=gtk_list_store_new(N_COLUMN, G_TYPE_STRING, /* DOMAIN_COLUMN */ G_TYPE_STRING, /* PATH_COLUMN */ G_TYPE_STRING, /* NAME_COLUMN */ G_TYPE_STRING, /* VALUE_COLUMN */ G_TYPE_STRING /* EXPIRE_DATE_COLUMN */); for(cookies=sortedCookies; cookies; cookies=cookies->next) { SoupCookie *cookie=(SoupCookie*)cookies->data; SoupDate *cookieDate=soup_cookie_get_expires(cookie); if(cookieDate) text=soup_date_to_string(cookieDate, SOUP_DATE_HTTP); else text=g_strdup(_("Till session end")); gtk_list_store_append(listStore, &listIter); gtk_list_store_set(listStore, &listIter, DOMAIN_COLUMN, soup_cookie_get_domain(cookie), PATH_COLUMN, soup_cookie_get_path(cookie), NAME_COLUMN, soup_cookie_get_name(cookie), VALUE_COLUMN, soup_cookie_get_value(cookie), EXPIRE_DATE_COLUMN, text, -1); g_free(text); } #endif /* Create description text */ if(numberDomains==1) { const gchar *cookieDomain=soup_cookie_get_domain((SoupCookie*)sortedCookies->data); if(*cookieDomain=='.') cookieDomain++; if(numberCookies>1) text=g_strdup_printf(_("The website %s wants to store %d cookies."), cookieDomain, numberCookies); else text=g_strdup_printf(_("The website %s wants to store a cookie."), cookieDomain); } else { text=g_strdup_printf(_("Multiple websites want to store %d cookies in total."), numberCookies); } /* Create info bar message and buttons */ infobar=midori_view_add_info_bar(inView, GTK_MESSAGE_QUESTION, text, G_CALLBACK(_cookie_permission_manager_on_infobar_policy_decision), NULL, _("_Accept"), COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT, _("Accept for this _session"), COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT_FOR_SESSION, _("De_ny"), COOKIE_PERMISSION_MANAGER_POLICY_BLOCK, _("Deny _this time"), COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED, NULL); g_free(text); /* midori_view_add_info_bar() in version 0.4.8 expects a GObject as user data * but I don't want to create an GObject just for a simple struct. So set object * data by our own */ g_object_set_data(G_OBJECT(infobar), "cookie-permission-manager-infobar-data", &modalInfo); /* FIXME: Find a way to add "details" widget */ #ifndef NO_INFOBAR_DETAILS /* Get content area of infobar */ #if HAVE_GTK_INFO_BAR contentArea=gtk_info_bar_get_content_area(GTK_INFO_BAR(infobar)); #else contentArea=infobar; #endif /* Create list and set up columns of list */ list=gtk_tree_view_new_with_model(GTK_TREE_MODEL(listStore)); #ifndef HAVE_GTK3 gtk_widget_set_size_request(list, -1, 100); #endif renderer=gtk_cell_renderer_text_new(); column=gtk_tree_view_column_new_with_attributes(_("Domain"), renderer, "text", DOMAIN_COLUMN, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(list), column); renderer=gtk_cell_renderer_text_new(); column=gtk_tree_view_column_new_with_attributes(_("Path"), renderer, "text", PATH_COLUMN, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(list), column); renderer=gtk_cell_renderer_text_new(); column=gtk_tree_view_column_new_with_attributes(_("Name"), renderer, "text", NAME_COLUMN, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(list), column); renderer=gtk_cell_renderer_text_new(); column=gtk_tree_view_column_new_with_attributes(_("Value"), renderer, "text", VALUE_COLUMN, NULL); g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, "width-chars", 30, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(list), column); renderer=gtk_cell_renderer_text_new(); column=gtk_tree_view_column_new_with_attributes(_("Expire date"), renderer, "text", EXPIRE_DATE_COLUMN, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(list), column); scrolled=gtk_scrolled_window_new(NULL, NULL); #ifdef HAVE_GTK3 gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scrolled), 100); #endif gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_container_add(GTK_CONTAINER(scrolled), list); gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN); gtk_container_add(GTK_CONTAINER(expander), scrolled); gtk_widget_show_all(vbox); gtk_container_add(GTK_CONTAINER(contentArea), vbox); /* Set state of expander based on config 'show-details-when-ask' */ gtk_expander_set_expanded(GTK_EXPANDER(expander), midori_extension_get_boolean(priv->extension, "show-details-when-ask")); g_signal_connect_swapped(expander, "notify::expanded", G_CALLBACK(_cookie_permission_manager_when_ask_expander_changed), self); #endif /* Show all widgets of info bar */ gtk_widget_show_all(infobar); /* Connect signals to quit main loop */ g_signal_connect(webkitView, "navigation-policy-decision-requested", G_CALLBACK(_cookie_permission_manager_on_infobar_webview_navigate), infobar); g_signal_connect(infobar, "destroy", G_CALLBACK(_cookie_permission_manager_on_infobar_destroy), &modalInfo); /* Let info bar be modal and set response to default */ modalInfo.response=COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED; modalInfo.mainLoop=g_main_loop_new(NULL, FALSE); GDK_THREADS_LEAVE(); g_main_loop_run(modalInfo.mainLoop); GDK_THREADS_ENTER(); g_main_loop_unref(modalInfo.mainLoop); modalInfo.mainLoop=NULL; /* Disconnect signal handler to webkit's web view */ g_signal_handlers_disconnect_by_func(webkitView, G_CALLBACK(_cookie_permission_manager_on_infobar_webview_navigate), infobar); /* Store user's decision in database if it is not a temporary block. * We use the already sorted list of cookies to prevent multiple * updates of database for the same domain. This sorted list is a copy * to avoid a reorder of cookies */ if(modalInfo.response!=COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED) { const gchar *lastDomain=NULL; /* Iterate through cookies and store decision for each domain once */ for(cookies=sortedCookies; cookies; cookies=cookies->next) { SoupCookie *cookie=(SoupCookie*)cookies->data; const gchar *cookieDomain=soup_cookie_get_domain(cookie); if(*cookieDomain=='.') cookieDomain++; /* Store decision if new domain found while iterating through cookies */ if(!lastDomain || g_ascii_strcasecmp(lastDomain, cookieDomain)!=0) { gchar *sql; gchar *error=NULL; gint success; sql=sqlite3_mprintf("INSERT OR REPLACE INTO policies (domain, value) VALUES ('%q', %d);", cookieDomain, modalInfo.response); 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); lastDomain=cookieDomain; } } } /* Free up allocated resources */ g_slist_free(sortedCookies); /* Return response */ return(modalInfo.response==COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED ? COOKIE_PERMISSION_MANAGER_POLICY_BLOCK : modalInfo.response); } /* A cookie was changed outside a request (e.g. Javascript) */ static void _cookie_permission_manager_on_cookie_changed(CookiePermissionManager *self, SoupCookie *inOldCookie, SoupCookie *inNewCookie, SoupCookieJar *inCookieJar) { /* Do not check changed cookies because they must have been allowed before. * Also do not check removed cookies because they are removed ;) */ if(inNewCookie==NULL || inOldCookie) return; /* New cookie is a new cookie so check */ switch(_cookie_permission_manager_get_policy(self, inNewCookie)) { case COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT: case COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT_FOR_SESSION: break; case COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED: /* Fallthrough! * The problem here is that we don't know the view to ask user * for policy to follow for this cookie domain. Therefore we * delete the cookie from jar and assume that we will be asked * again in _cookie_permission_manager_on_response_received(). */ default: soup_cookie_jar_delete_cookie(inCookieJar, inNewCookie); break; } } /* We received the HTTP headers of the request and it contains cookie-managing headers */ static void _cookie_permission_manager_on_response_received(WebKitWebView *inView, WebKitWebFrame *inFrame, WebKitWebResource *inResource, WebKitNetworkResponse *inResponse, gpointer inUserData) { g_return_if_fail(IS_COOKIE_PERMISSION_MANAGER(inUserData)); CookiePermissionManager *self=COOKIE_PERMISSION_MANAGER(inUserData); CookiePermissionManagerPrivate *priv=self->priv; GSList *newCookies, *cookie; GSList *unknownCookies=NULL, *acceptedCookies=NULL; SoupURI *firstParty; SoupCookieJarAcceptPolicy cookiePolicy; gint unknownCookiesPolicy; SoupMessage *message; /* If policy is to deny all cookies return immediately */ cookiePolicy=soup_cookie_jar_get_accept_policy(priv->cookieJar); if(cookiePolicy==SOUP_COOKIE_JAR_ACCEPT_NEVER) return; /* Get SoupMessage */ message=webkit_network_response_get_message(inResponse); if(!message || !SOUP_IS_MESSAGE(message)) return; /* Iterate through cookies in response and check if they should be * blocked (remove from cookies list) or accepted (added to cookie jar). * If we could not determine what to do collect these cookies and * ask user */ newCookies=soup_cookies_from_response(message); firstParty=soup_message_get_first_party(message); for(cookie=newCookies; cookie; cookie=cookie->next) { switch(_cookie_permission_manager_get_policy(self, cookie->data)) { case COOKIE_PERMISSION_MANAGER_POLICY_BLOCK: soup_cookie_free(cookie->data); break; case COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT: case COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT_FOR_SESSION: if((cookiePolicy==SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY && firstParty!=NULL && firstParty->host && soup_cookie_domain_matches(cookie->data, firstParty->host)) || cookiePolicy==SOUP_COOKIE_JAR_ACCEPT_ALWAYS) { acceptedCookies=g_slist_prepend(acceptedCookies, cookie->data); } else soup_cookie_free(cookie->data); break; case COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED: default: if((cookiePolicy==SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY && firstParty!=NULL && firstParty->host && soup_cookie_domain_matches(cookie->data, firstParty->host)) || cookiePolicy==SOUP_COOKIE_JAR_ACCEPT_ALWAYS) { unknownCookies=g_slist_prepend(unknownCookies, cookie->data); } else soup_cookie_free(cookie->data); break; } } /* Prepending an item to list is the fastest method but the order of cookies * is reversed now and may be added to cookie jar in the wrong order. So we * need to reverse list now of both - undetermined and accepted cookies */ unknownCookies=g_slist_reverse(unknownCookies); acceptedCookies=g_slist_reverse(acceptedCookies); /* Ask user for his decision what to do with cookies whose policy is undetermined * But only ask if there is any undetermined one */ if(g_slist_length(unknownCookies)>0) { /* Get view */ MidoriView *view; view=MIDORI_VIEW(g_object_get_data(G_OBJECT(inView), "midori-view")); /* Ask for user's decision */ unknownCookiesPolicy=_cookie_permission_manager_ask_for_policy(self, view, message, unknownCookies); if(unknownCookiesPolicy==COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT || unknownCookiesPolicy==COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT_FOR_SESSION) { /* Add accepted undetermined cookies to cookie jar */ for(cookie=unknownCookies; cookie; cookie=cookie->next) { soup_cookie_jar_add_cookie(priv->cookieJar, (SoupCookie*)cookie->data); } } else { /* Free cookies because they should be blocked */ for(cookie=unknownCookies; cookie; cookie=cookie->next) { soup_cookie_free((SoupCookie*)cookie->data); } } } /* Add accepted cookies to cookie jar */ for(cookie=acceptedCookies; cookie; cookie=cookie->next) { soup_cookie_jar_add_cookie(priv->cookieJar, (SoupCookie*)cookie->data); } /* Free list of cookies */ g_slist_free(unknownCookies); g_slist_free(acceptedCookies); g_slist_free(newCookies); } /* A tab to a browser was added */ static void _cookie_permission_manager_on_add_tab(CookiePermissionManager *self, MidoriView *inView, gpointer inUserData) { /* Listen to starting network requests */ WebKitWebView *webkitView=WEBKIT_WEB_VIEW(midori_view_get_web_view(inView)); g_object_set_data(G_OBJECT(webkitView), "midori-view", inView); g_signal_connect(webkitView, "resource-response-received", G_CALLBACK(_cookie_permission_manager_on_response_received), self); } /* A browser window was added */ static void _cookie_permission_manager_on_add_browser(CookiePermissionManager *self, MidoriBrowser *inBrowser, gpointer inUserData) { GList *tabs, *iter; /* Set up all current available tabs in browser */ tabs=midori_browser_get_tabs(inBrowser); for(iter=tabs; iter; iter=g_list_next(iter)) { _cookie_permission_manager_on_add_tab(self, iter->data, inBrowser); } g_list_free(tabs); /* Listen to new tabs opened in browser and existing ones closed */ g_signal_connect_swapped(inBrowser, "add-tab", G_CALLBACK(_cookie_permission_manager_on_add_tab), self); } /* Application property has changed */ static void _cookie_permission_manager_on_application_changed(CookiePermissionManager *self) { CookiePermissionManagerPrivate *priv=COOKIE_PERMISSION_MANAGER(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)) { _cookie_permission_manager_on_add_browser(self, MIDORI_BROWSER(iter->data), priv->application); } g_list_free(browsers); /* Listen to new browser windows opened and existing ones closed */ g_signal_connect_swapped(priv->application, "add-browser", G_CALLBACK(_cookie_permission_manager_on_add_browser), self); } /* IMPLEMENTATION: GObject */ /* Finalize this object */ static void cookie_permission_manager_finalize(GObject *inObject) { CookiePermissionManager *self=COOKIE_PERMISSION_MANAGER(inObject); CookiePermissionManagerPrivate *priv=self->priv; GList *browsers, *browser; GList *tabs, *tab; WebKitWebView *webkitView; /* Dispose allocated resources */ if(priv->databaseFilename) { g_free(priv->databaseFilename); priv->databaseFilename=NULL; g_object_notify_by_pspec(inObject, CookiePermissionManagerProperties[PROP_DATABASE_FILENAME]); } if(priv->database) { sqlite3_close(priv->database); priv->database=NULL; g_object_notify_by_pspec(inObject, CookiePermissionManagerProperties[PROP_DATABASE]); } g_signal_handler_disconnect(priv->cookieJar, priv->cookieJarChangedID); g_object_steal_data(G_OBJECT(priv->cookieJar), "cookie-permission-manager"); 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); tabs=midori_browser_get_tabs(MIDORI_BROWSER(browser->data)); for(tab=tabs; tab; tab=g_list_next(tab)) { 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); /* Call parent's class finalize method */ G_OBJECT_CLASS(cookie_permission_manager_parent_class)->finalize(inObject); } /* Set/get properties */ static void cookie_permission_manager_set_property(GObject *inObject, guint inPropID, const GValue *inValue, GParamSpec *inSpec) { CookiePermissionManager *self=COOKIE_PERMISSION_MANAGER(inObject); switch(inPropID) { /* Construct-only properties */ case PROP_EXTENSION: self->priv->extension=g_value_get_object(inValue); _cookie_permission_manager_open_database(self); break; case PROP_APPLICATION: self->priv->application=g_value_get_object(inValue); _cookie_permission_manager_on_application_changed(self); break; case PROP_ASK_FOR_UNKNOWN_POLICY: cookie_permission_manager_set_ask_for_unknown_policy(self, g_value_get_boolean(inValue)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(inObject, inPropID, inSpec); break; } } static void cookie_permission_manager_get_property(GObject *inObject, guint inPropID, GValue *outValue, GParamSpec *inSpec) { CookiePermissionManager *self=COOKIE_PERMISSION_MANAGER(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_ASK_FOR_UNKNOWN_POLICY: g_value_set_boolean(outValue, self->priv->askForUnknownPolicy); 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 cookie_permission_manager_class_init(CookiePermissionManagerClass *klass) { GObjectClass *gobjectClass=G_OBJECT_CLASS(klass); /* Override functions */ gobjectClass->finalize=cookie_permission_manager_finalize; gobjectClass->set_property=cookie_permission_manager_set_property; gobjectClass->get_property=cookie_permission_manager_get_property; /* Set up private structure */ g_type_class_add_private(klass, sizeof(CookiePermissionManagerPrivate)); /* Define properties */ CookiePermissionManagerProperties[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); CookiePermissionManagerProperties[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); CookiePermissionManagerProperties[PROP_DATABASE]= g_param_spec_pointer("database", _("Database instance"), _("Pointer to sqlite database instance used by this extension"), G_PARAM_READABLE); CookiePermissionManagerProperties[PROP_DATABASE_FILENAME]= g_param_spec_string("database-filename", _("Database path"), _("Path to sqlite database instance used by this extension"), NULL, G_PARAM_READABLE); CookiePermissionManagerProperties[PROP_ASK_FOR_UNKNOWN_POLICY]= g_param_spec_boolean("ask-for-unknown-policy", _("Ask for unknown policy"), _("If true this extension ask for policy for every unknown domain." "If false this extension uses the global cookie policy set in Midori settings."), TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT); g_object_class_install_properties(gobjectClass, PROP_LAST, CookiePermissionManagerProperties); } /* Object initialization * Create private structure and set up default values */ static void cookie_permission_manager_init(CookiePermissionManager *self) { CookiePermissionManagerPrivate *priv; priv=self->priv=COOKIE_PERMISSION_MANAGER_GET_PRIVATE(self); /* Set up default values */ priv->database=NULL; priv->databaseFilename=NULL; priv->askForUnknownPolicy=TRUE; /* Hijack session's cookie jar to handle cookies requests on our own in HTTP streams * but remember old handlers to restore them on deactivation */ priv->session=webkit_get_default_session(); priv->cookieJar=SOUP_COOKIE_JAR(soup_session_get_feature(priv->session, SOUP_TYPE_COOKIE_JAR)); priv->featureIface=SOUP_SESSION_FEATURE_GET_CLASS(priv->cookieJar); g_object_set_data(G_OBJECT(priv->cookieJar), "cookie-permission-manager", self); /* Listen to changed cookies set or changed by other sources like javascript */ priv->cookieJarChangedID=g_signal_connect_swapped(priv->cookieJar, "changed", G_CALLBACK(_cookie_permission_manager_on_cookie_changed), self); } /* Implementation: Public API */ /* Create new object */ CookiePermissionManager* cookie_permission_manager_new(MidoriExtension *inExtension, MidoriApp *inApp) { return(g_object_new(TYPE_COOKIE_PERMISSION_MANAGER, "extension", inExtension, "application", inApp, NULL)); } /* Get/set policy to ask for policy if unknown for a domain */ gboolean cookie_permission_manager_get_ask_for_unknown_policy(CookiePermissionManager *self) { g_return_val_if_fail(IS_COOKIE_PERMISSION_MANAGER(self), FALSE); return(self->priv->askForUnknownPolicy); } void cookie_permission_manager_set_ask_for_unknown_policy(CookiePermissionManager *self, gboolean inDoAsk) { g_return_if_fail(IS_COOKIE_PERMISSION_MANAGER(self)); if(inDoAsk!=self->priv->askForUnknownPolicy) { self->priv->askForUnknownPolicy=inDoAsk; midori_extension_set_boolean(self->priv->extension, "ask-for-unknown-policy", inDoAsk); g_object_notify_by_pspec(G_OBJECT(self), CookiePermissionManagerProperties[PROP_ASK_FOR_UNKNOWN_POLICY]); } } /************************************************************************************/ /* Implementation: Enumeration */ GType cookie_permission_manager_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[]= { { COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED, "COOKIE_PERMISSION_MANAGER_POLICY_UNDETERMINED", N_("Undetermined") }, { COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT, "COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT", N_("Accept") }, { COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT_FOR_SESSION, "COOKIE_PERMISSION_MANAGER_POLICY_ACCEPT_FOR_SESSION", N_("Accept for session") }, { COOKIE_PERMISSION_MANAGER_POLICY_BLOCK, "COOKIE_PERMISSION_MANAGER_POLICY_BLOCK", N_("Block") }, { 0, NULL, NULL } }; GType g_define_type_id=g_enum_register_static(g_intern_static_string("CookiePermissionManagerPolicy"), values); g_once_init_leave(&g_define_type_id__volatile, g_define_type_id); } return(g_define_type_id__volatile); }