/* Copyright (C) 2008 Christian Dywan Copyright (C) 2008-2010 Arno Renevier 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. */ /* This extensions add support for user addons: userscripts and userstyles */ #include #include #include #include #include "config.h" #if HAVE_UNISTD_H #include #endif #ifndef X_OK #define X_OK 1 #endif typedef enum { ADDON_NONE, ADDONS_USER_SCRIPTS, ADDONS_USER_STYLES } AddonsKind; #define ADDONS_TYPE \ (addons_get_type ()) #define ADDONS(obj) \ (G_TYPE_CHECK_INSTANCE_CAST ((obj), ADDONS_TYPE, Addons)) #define ADDONS_CLASS(klass) \ (G_TYPE_CHECK_CLASS_CAST ((klass), ADDONS_TYPE, AddonsClass)) #define IS_ADDONS(obj) \ (G_TYPE_CHECK_INSTANCE_TYPE ((obj), ADDONS_TYPE)) #define IS_ADDONS_CLASS(klass) \ (G_TYPE_CHECK_CLASS_TYPE ((klass), ADDONS_TYPE)) #define ADDONS_GET_CLASS(obj) \ (G_TYPE_INSTANCE_GET_CLASS ((obj), ADDONS_TYPE, AddonsClass)) typedef struct _Addons Addons; typedef struct _AddonsClass AddonsClass; struct _AddonsClass { GtkVBoxClass parent_class; }; struct _Addons { GtkVBox parent_instance; GtkWidget* toolbar; GtkWidget* treeview; AddonsKind kind; }; static void addons_iface_init (MidoriViewableIface* iface); G_DEFINE_TYPE_WITH_CODE (Addons, addons, GTK_TYPE_VBOX, G_IMPLEMENT_INTERFACE (MIDORI_TYPE_VIEWABLE, addons_iface_init)); struct AddonElement { gchar* fullpath; gchar* displayname; gchar* description; gchar* script_content; gboolean enabled; gboolean broken; GSList* includes; GSList* excludes; }; struct AddonsList { GtkListStore* liststore; GSList* elements; }; static void midori_addons_button_add_clicked_cb (GtkToolItem* toolitem, Addons* addons) { gchar* addons_type; gchar* path; GtkWidget* dialog; if (addons->kind == ADDONS_USER_SCRIPTS) { addons_type = g_strdup ("userscripts"); path = g_build_path (G_DIR_SEPARATOR_S, g_get_user_data_dir (), PACKAGE_NAME, "scripts", NULL); } else if (addons->kind == ADDONS_USER_STYLES) { addons_type = g_strdup ("userstyles"); path = g_build_path (G_DIR_SEPARATOR_S, g_get_user_data_dir (), PACKAGE_NAME, "styles", NULL); } else g_assert_not_reached (); dialog = gtk_message_dialog_new ( GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (addons))), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, _("Copy %s to the folder %s."), addons_type, path); g_free (addons_type); g_free (path); gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); } GtkWidget* addons_get_toolbar (MidoriViewable* viewable) { GtkWidget* toolbar; GtkToolItem* toolitem; g_return_val_if_fail (IS_ADDONS (viewable), NULL); if (!ADDONS (viewable)->toolbar) { toolbar = gtk_toolbar_new (); gtk_toolbar_set_style (GTK_TOOLBAR (toolbar), GTK_TOOLBAR_BOTH_HORIZ); gtk_toolbar_set_icon_size (GTK_TOOLBAR (toolbar), GTK_ICON_SIZE_BUTTON); toolitem = gtk_tool_item_new (); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), toolitem, -1); gtk_widget_show (GTK_WIDGET (toolitem)); /* separator */ toolitem = gtk_separator_tool_item_new (); gtk_separator_tool_item_set_draw (GTK_SEPARATOR_TOOL_ITEM (toolitem), FALSE); gtk_tool_item_set_expand (toolitem, TRUE); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), toolitem, -1); gtk_widget_show (GTK_WIDGET (toolitem)); /* add button */ toolitem = gtk_tool_button_new_from_stock (GTK_STOCK_ADD); gtk_tool_item_set_is_important (toolitem, TRUE); g_signal_connect (toolitem, "clicked", G_CALLBACK (midori_addons_button_add_clicked_cb), viewable); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), toolitem, -1); gtk_widget_show (GTK_WIDGET (toolitem)); ADDONS (viewable)->toolbar = toolbar; g_signal_connect (toolbar, "destroy", G_CALLBACK (gtk_widget_destroyed), &ADDONS (viewable)->toolbar); } return ADDONS (viewable)->toolbar; } static const gchar* addons_get_label (MidoriViewable* viewable) { Addons* addons = ADDONS (viewable); if (addons->kind == ADDONS_USER_SCRIPTS) return _("Userscripts"); else if (addons->kind == ADDONS_USER_STYLES) return _("Userstyles"); return NULL; } static const gchar* addons_get_stock_id (MidoriViewable* viewable) { Addons* addons = ADDONS (viewable); if (addons->kind == ADDONS_USER_SCRIPTS) return STOCK_SCRIPT; else if (addons->kind == ADDONS_USER_STYLES) return STOCK_STYLE; return NULL; } static void addons_iface_init (MidoriViewableIface* iface) { iface->get_stock_id = addons_get_stock_id; iface->get_label = addons_get_label; iface->get_toolbar = addons_get_toolbar; } static void addons_free_elements (GSList* elements) { struct AddonElement* element; GSList* start = elements; while (elements) { element = elements->data; g_free (element->fullpath); g_free (element->displayname); g_free (element->description); g_free (element->script_content); g_slist_free (element->includes); g_slist_free (element->excludes); elements = g_slist_next (elements); } g_slist_free (start); } static void addons_class_init (AddonsClass* class) { } static void addons_treeview_render_tick_cb (GtkTreeViewColumn* column, GtkCellRenderer* renderer, GtkTreeModel* model, GtkTreeIter* iter, GtkWidget* treeview) { struct AddonElement *element; gtk_tree_model_get (model, iter, 0, &element, -1); g_object_set (renderer, "active", element->enabled, "sensitive", !element->broken, NULL); } static void addons_cell_renderer_toggled_cb (GtkCellRendererToggle* renderer, const gchar* path, Addons* addons) { GtkTreeModel* model; GtkTreeIter iter; model = gtk_tree_view_get_model (GTK_TREE_VIEW (addons->treeview)); if (gtk_tree_model_get_iter_from_string (model, &iter, path)) { struct AddonElement *element; GtkTreePath* tree_path; gtk_tree_model_get (model, &iter, 0, &element, -1); element->enabled = !element->enabled; /* After enabling or disabling an element, the tree view is not updated automatically; we need to notify tree model in order to take the modification into account */ tree_path = gtk_tree_path_new_from_string (path); gtk_tree_model_row_changed (model, tree_path, &iter); gtk_tree_path_free (tree_path); } } static void addons_treeview_render_text_cb (GtkTreeViewColumn* column, GtkCellRenderer* renderer, GtkTreeModel* model, GtkTreeIter* iter, GtkWidget* treeview) { struct AddonElement *element; gtk_tree_model_get (model, iter, 0, &element, -1); g_object_set (renderer, "text", element->displayname, NULL); if (!element->enabled) g_object_set (renderer, "sensitive", false, NULL); else g_object_set (renderer, "sensitive", true, NULL); } static void addons_treeview_row_activated_cb (GtkTreeView* treeview, GtkTreePath* path, GtkTreeViewColumn* column, Addons* addons) { GtkTreeModel* model = gtk_tree_view_get_model (treeview); GtkTreeIter iter; if (gtk_tree_model_get_iter (model, &iter, path)) { struct AddonElement *element; gtk_tree_model_get (model, &iter, 0, &element, -1); element->enabled = !element->enabled; /* After enabling or disabling an element, the tree view is not updated automatically; we need to notify tree model in order to take the modification into account */ gtk_tree_model_row_changed (model, path, &iter); } } static GSList* addons_get_directories (AddonsKind kind) { gchar* folder_name; GSList* directories; const char* const* datadirs; gchar* path; g_assert (kind == ADDONS_USER_SCRIPTS || kind == ADDONS_USER_STYLES); directories = NULL; if (kind == ADDONS_USER_SCRIPTS) folder_name = g_strdup ("scripts"); else if (kind == ADDONS_USER_STYLES) folder_name = g_strdup ("styles"); else g_assert_not_reached (); path = g_build_path (G_DIR_SEPARATOR_S, g_get_user_data_dir (), PACKAGE_NAME, folder_name, NULL); if (g_access (path, X_OK) == 0) directories = g_slist_prepend (directories, path); else g_free (path); datadirs = g_get_system_data_dirs (); while (*datadirs) { path = g_build_path (G_DIR_SEPARATOR_S, *datadirs, PACKAGE_NAME, folder_name, NULL); if (g_access (path, X_OK) == 0) directories = g_slist_prepend (directories, path); else g_free (path); datadirs++; } g_free (folder_name); return directories; } static GSList* addons_get_files (AddonsKind kind) { GSList* files; GDir* addon_dir; GSList* directories; const gchar* filename; gchar* dirname; gchar* fullname; gchar* file_extension; g_assert (kind == ADDONS_USER_SCRIPTS || kind == ADDONS_USER_STYLES); if (kind == ADDONS_USER_SCRIPTS) file_extension = g_strdup (".js"); else if (kind == ADDONS_USER_STYLES) file_extension = g_strdup (".css"); files = NULL; directories = addons_get_directories (kind); while (directories) { dirname = directories->data; if ((addon_dir = g_dir_open (dirname, 0, NULL))) { while ((filename = g_dir_read_name (addon_dir))) { if (g_str_has_suffix (filename, file_extension)) { fullname = g_build_filename (dirname, filename, NULL); files = g_slist_prepend (files, fullname); } } g_dir_close (addon_dir); } g_free (dirname); directories = g_slist_next (directories); } g_free (file_extension); return files; } static gboolean js_metadata_from_file (const gchar* filename, GSList** includes, GSList** excludes, gchar** name, gchar** description) { GIOChannel* channel; gboolean found_meta; gchar* line; gchar* rest_of_line; if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR | G_FILE_TEST_IS_SYMLINK)) return FALSE; channel = g_io_channel_new_file (filename, "r", 0); if (!channel) return FALSE; found_meta = FALSE; while (g_io_channel_read_line (channel, &line, NULL, NULL, NULL) == G_IO_STATUS_NORMAL) { if (g_str_has_prefix (line, "// ==UserScript==")) found_meta = TRUE; else if (found_meta) { if (g_str_has_prefix (line, "// ==/UserScript==")) found_meta = FALSE; else if (g_str_has_prefix (line, "// @require ") || g_str_has_prefix (line, "// @resource ")) { /* We don't support these, so abort here */ g_free (line); g_io_channel_shutdown (channel, false, 0); g_slist_free (*includes); g_slist_free (*excludes); *includes = NULL; *excludes = NULL; return FALSE; } else if (includes && g_str_has_prefix (line, "// @include ")) { rest_of_line = g_strdup (line + strlen ("// @include ")); rest_of_line = g_strstrip (rest_of_line); *includes = g_slist_prepend (*includes, rest_of_line); } else if (excludes && g_str_has_prefix (line, "// @exclude ")) { rest_of_line = g_strdup (line + strlen ("// @exclude ")); rest_of_line = g_strstrip (rest_of_line); *excludes = g_slist_prepend (*excludes, rest_of_line); } else if (name && g_str_has_prefix (line, "// @name ")) { rest_of_line = g_strdup (line + strlen ("// @name ")); rest_of_line = g_strstrip (rest_of_line); *name = rest_of_line; } else if (description && g_str_has_prefix (line, "// @description ")) { rest_of_line = g_strdup (line + strlen ("// @description ")); rest_of_line = g_strstrip (rest_of_line); *description = rest_of_line; } } g_free (line); } g_io_channel_shutdown (channel, false, 0); g_io_channel_unref (channel); return TRUE; } static gboolean css_metadata_from_file (const gchar* filename, GSList** includes, GSList** excludes) { GIOChannel* channel; gchar* line; gchar* rest_of_line; if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR | G_FILE_TEST_IS_SYMLINK)) return FALSE; channel = g_io_channel_new_file (filename, "r", 0); if (!channel) return FALSE; while (g_io_channel_read_line (channel, &line, NULL, NULL, NULL) == G_IO_STATUS_NORMAL) { if (g_str_has_prefix (line, "@-moz-document")) { /* FIXME: We merely look for includes. We should honor blocks. */ if (includes) { gchar** parts; guint i; rest_of_line = g_strdup (line + strlen ("@-moz-document")); rest_of_line = g_strstrip (rest_of_line); parts = g_strsplit (rest_of_line, " ", 0); i = 0; while (parts[i]) { gchar* value = NULL; if (g_str_has_prefix (parts[i], "url-prefix(")) value = g_strdup (parts[i] + strlen ("url-prefix(")); else if (g_str_has_prefix (parts[i], "url(")) value = g_strdup (parts[i] + strlen ("url(")); if (value) { guint j; if (value[0] != '\'' && value[0] != '"') { /* Wrong syntax, abort */ g_free (value); g_strfreev (parts); g_free (line); g_io_channel_shutdown (channel, false, 0); g_slist_free (*includes); g_slist_free (*excludes); *includes = NULL; *excludes = NULL; return FALSE; } j = 1; while (value[j] != '\0') { if (value[j] == value[0]) break; j++; } *includes = g_slist_prepend (*includes, g_strndup (value + 1, j - 1)); g_free (value); } /* FIXME: Recognize "domain" */ i++; } g_strfreev (parts); } } g_free (line); } g_io_channel_shutdown (channel, false, 0); g_io_channel_unref (channel); return TRUE; } static gboolean addons_get_element_content (gchar* file_path, AddonsKind kind, gchar** content) { gchar* file_content; guint meta; guint i, n; g_assert (kind == ADDONS_USER_SCRIPTS || kind == ADDONS_USER_STYLES); if (g_file_get_contents (file_path, &file_content, NULL, NULL)) { if (kind == ADDONS_USER_SCRIPTS) { *content = g_strdup_printf ( "window.addEventListener ('DOMContentLoaded'," "function () { %s }, true);", file_content); } else if (kind == ADDONS_USER_STYLES) { meta = 0; n = strlen (file_content); for (i = 0; i < n; i++) { /* Replace line breaks with spaces */ if (file_content[i] == '\n' || file_content[i] == '\r') file_content[i] = ' '; /* Change all single quotes to double quotes */ if (file_content[i] == '\'') file_content[i] = '\"'; /* Turn metadata we inspected earlier into comments */ if (!meta && file_content[i] == '@') { file_content[i] = '/'; meta++; } else if (meta == 1 && (file_content[i] == '-' || file_content[i] == 'n')) { file_content[i] = '*'; meta++; } else if (meta == 2 && file_content[i] == '{') { file_content[i - 1] = '*'; file_content[i] = '/'; meta++; } else if (meta == 3 && file_content[i] == '{') meta++; else if (meta == 4 && file_content[i] == '}') meta--; else if (meta == 3 && file_content[i] == '}') { file_content[i] = ' '; meta = 0; } } *content = g_strdup_printf ( "window.addEventListener ('DOMContentLoaded'," "function () {" "var mystyle = document.createElement(\"style\");" "mystyle.setAttribute(\"type\", \"text/css\");" "mystyle.appendChild(document.createTextNode('%s'));" "var head = document.getElementsByTagName(\"head\")[0];" "if (head) head.appendChild(mystyle);" "else document.documentElement.insertBefore" "(mystyle, document.documentElement.firstChild);" "}, true);", file_content); } g_free (file_content); if (*content) return TRUE; } return FALSE; } static void addons_update_elements (MidoriExtension* extension, AddonsKind kind) { GSList* addon_files; GSList* files_list; gchar* name; gchar* fullpath; struct AddonElement* element; struct AddonsList* addons_list; GSList* elements = NULL; GtkListStore* liststore = NULL; GtkTreeIter iter; gchar* config_file; GKeyFile* keyfile; if (kind == ADDONS_USER_SCRIPTS) addons_list = g_object_get_data (G_OBJECT (extension), "scripts-list"); else if (kind == ADDONS_USER_STYLES) addons_list = g_object_get_data (G_OBJECT (extension), "styles-list"); else g_assert_not_reached (); if (addons_list) { liststore = addons_list->liststore; elements = addons_list->elements; } if (elements) addons_free_elements (elements); if (liststore) gtk_list_store_clear (liststore); else liststore = gtk_list_store_new (3, G_TYPE_POINTER, G_TYPE_INT, G_TYPE_STRING); keyfile = g_key_file_new (); config_file = g_build_filename (midori_extension_get_config_dir (extension), "addons", NULL); g_key_file_load_from_file (keyfile, config_file, G_KEY_FILE_NONE, NULL); addon_files = addons_get_files (kind); files_list = addon_files; elements = NULL; while (addon_files) { fullpath = addon_files->data; element = g_new (struct AddonElement, 1); element->displayname = g_filename_display_basename (fullpath); element->fullpath = fullpath; element->enabled = TRUE; element->broken = FALSE; element->includes = NULL; element->excludes = NULL; element->description = NULL; element->script_content = NULL; if (kind == ADDONS_USER_SCRIPTS) { name = NULL; if (!js_metadata_from_file (fullpath, &element->includes, &element->excludes, &name, &element->description)) element->broken = TRUE; if (name) katze_assign (element->displayname, name); if (!element->broken) if (!addons_get_element_content (fullpath, kind, &(element->script_content))) element->broken = TRUE; if (g_key_file_get_integer (keyfile, "scripts", fullpath, NULL) & 1) element->enabled = FALSE; } else if (kind == ADDONS_USER_STYLES) { if (!css_metadata_from_file (fullpath, &element->includes, &element->excludes)) element->broken = TRUE; if (!element->broken) if (!addons_get_element_content (fullpath, kind, &(element->script_content))) element->broken = TRUE; if (g_key_file_get_integer (keyfile, "styles", fullpath, NULL) & 1) element->enabled = FALSE; } gtk_list_store_append (liststore, &iter); gtk_list_store_set (liststore, &iter, 0, element, 1, 0, 2, element->fullpath, -1); addon_files = g_slist_next (addon_files); elements = g_slist_prepend (elements, element); } g_slist_free (files_list); g_free (config_file); g_key_file_free (keyfile); if (addons_list) g_free (addons_list); addons_list = g_new (struct AddonsList, 1); addons_list->elements = elements; addons_list->liststore = liststore; if (kind == ADDONS_USER_SCRIPTS) g_object_set_data (G_OBJECT (extension), "scripts-list", addons_list); else if (kind == ADDONS_USER_STYLES) g_object_set_data (G_OBJECT (extension), "styles-list", addons_list); } static void addons_init (Addons* addons) { GtkTreeViewColumn* column; GtkCellRenderer* renderer_text; GtkCellRenderer* renderer_toggle; addons->treeview = gtk_tree_view_new (); gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (addons->treeview), FALSE); column = gtk_tree_view_column_new (); renderer_toggle = gtk_cell_renderer_toggle_new (); gtk_tree_view_column_pack_start (column, renderer_toggle, FALSE); gtk_tree_view_column_set_cell_data_func (column, renderer_toggle, (GtkTreeCellDataFunc)addons_treeview_render_tick_cb, addons->treeview, NULL); g_signal_connect (renderer_toggle, "toggled", G_CALLBACK (addons_cell_renderer_toggled_cb), addons); gtk_tree_view_append_column (GTK_TREE_VIEW (addons->treeview), column); column = gtk_tree_view_column_new (); renderer_text = gtk_cell_renderer_text_new (); gtk_tree_view_column_pack_start (column, renderer_text, FALSE); gtk_tree_view_column_set_cell_data_func (column, renderer_text, (GtkTreeCellDataFunc)addons_treeview_render_text_cb, addons->treeview, NULL); gtk_tree_view_append_column (GTK_TREE_VIEW (addons->treeview), column); gtk_tree_view_set_tooltip_column (GTK_TREE_VIEW (addons->treeview), 2); g_signal_connect (addons->treeview, "row-activated", G_CALLBACK (addons_treeview_row_activated_cb), addons); gtk_widget_show (addons->treeview); gtk_box_pack_start (GTK_BOX (addons), addons->treeview, TRUE, TRUE, 0); } static gchar* addons_convert_to_simple_regexp (const gchar* pattern) { guint len; gchar* dest; guint pos; guint i; gchar c; len = strlen (pattern); dest = g_malloc0 (len * 2 + 1); dest[0] = '^'; pos = 1; for (i = 0; i < len; i++) { c = pattern[i]; switch (c) { case '*': dest[pos] = '.'; dest[pos + 1] = c; pos++; pos++; break; case '.' : case '?' : case '^' : case '$' : case '+' : case '{' : case '[' : case '|' : case '(' : case ')' : case ']' : case '\\' : dest[pos] = '\\'; dest[pos + 1] = c; pos++; pos++; break; case ' ' : break; default: dest[pos] = pattern[i]; pos ++; } } return dest; } static gboolean addons_may_run (const gchar* uri, GSList** includes, GSList** excludes) { gboolean match; GSList* list; if (*includes) match = FALSE; else match = TRUE; list = *includes; while (list) { gchar* re = addons_convert_to_simple_regexp (list->data); gboolean matched = g_regex_match_simple (re, uri, 0, 0); g_free (re); if (matched) { match = TRUE; break; } list = g_slist_next (list); } if (!match) return FALSE; list = *excludes; while (list) { gchar* re = addons_convert_to_simple_regexp (list->data); gboolean matched = g_regex_match_simple (re, uri, 0, 0); g_free (re); if (matched) { match = FALSE; break; } list = g_slist_next (list); } return match; } static gboolean addons_skip_element (struct AddonElement* element, gchar* uri) { if (!element->enabled || element->broken) return TRUE; if (element->includes || element->excludes) if (!addons_may_run (uri, &element->includes, &element->excludes)) return TRUE; return FALSE; } static void addons_context_ready_cb (WebKitWebView* web_view, WebKitWebFrame* web_frame, JSContextRef js_context, JSObjectRef js_window, MidoriExtension* extension) { gchar* uri; GSList* scripts, *styles; struct AddonElement* script, *style; struct AddonsList* scripts_list, *styles_list; uri = katze_object_get_string (web_view, "uri"); /* Don't run scripts or styles on blank or special pages */ if (!(uri && *uri && strncmp (uri, "about:", 6))) { g_free (uri); return; } scripts_list = g_object_get_data (G_OBJECT (extension), "scripts-list"); scripts = scripts_list->elements; while (scripts) { script = scripts->data; if (addons_skip_element (script, uri)) { scripts = g_slist_next (scripts); continue; } if (script->script_content) webkit_web_view_execute_script (web_view, script->script_content); scripts = g_slist_next (scripts); } styles_list = g_object_get_data (G_OBJECT (extension), "styles-list"); styles = styles_list->elements; while (styles) { style = styles->data; if (addons_skip_element (style, uri)) { styles = g_slist_next (styles); continue; } if (style->script_content) webkit_web_view_execute_script (web_view, style->script_content); styles = g_slist_next (styles); } } static void addons_add_tab_cb (MidoriBrowser* browser, MidoriView* view, MidoriExtension* extension) { GtkWidget* web_view = midori_view_get_web_view (view); g_signal_connect (web_view, "window-object-cleared", G_CALLBACK (addons_context_ready_cb), extension); } static void addons_add_tab_foreach_cb (MidoriView* view, MidoriBrowser* browser, MidoriExtension* extension) { addons_add_tab_cb (browser, view, extension); } static void addons_deactivate_tabs (MidoriView* view, MidoriExtension* extension) { GtkWidget* web_view = midori_view_get_web_view (view); g_signal_handlers_disconnect_by_func ( web_view, addons_context_ready_cb, extension); } static void addons_browser_destroy (MidoriBrowser* browser, MidoriExtension* extension) { GtkWidget* scripts, *styles; midori_browser_foreach (browser, (GtkCallback)addons_deactivate_tabs, extension); g_signal_handlers_disconnect_by_func (browser, addons_add_tab_cb, extension); scripts = (GtkWidget*)g_object_get_data (G_OBJECT (browser), "scripts-addons"); gtk_widget_destroy (scripts); styles = (GtkWidget*)g_object_get_data (G_OBJECT (browser), "styles-addons"); gtk_widget_destroy (styles); } GtkWidget* addons_new (AddonsKind kind, MidoriExtension* extension) { GtkWidget* addons; GtkListStore* liststore; struct AddonsList* list; addons = g_object_new (ADDONS_TYPE, NULL); ADDONS (addons)->kind = kind; if (kind == ADDONS_USER_SCRIPTS) list = g_object_get_data (G_OBJECT (extension), "scripts-list"); else if (kind == ADDONS_USER_STYLES) list = g_object_get_data (G_OBJECT (extension), "styles-list"); else g_assert_not_reached (); liststore = list->liststore; gtk_tree_view_set_model (GTK_TREE_VIEW (ADDONS(addons)->treeview), GTK_TREE_MODEL (liststore)); gtk_widget_queue_draw (GTK_WIDGET (ADDONS(addons)->treeview)); return addons; } static void addons_app_add_browser_cb (MidoriApp* app, MidoriBrowser* browser, MidoriExtension* extension) { GtkWidget* panel; GtkWidget* scripts, *styles; midori_browser_foreach (browser, (GtkCallback)addons_add_tab_foreach_cb, extension); g_signal_connect (browser, "add-tab", G_CALLBACK (addons_add_tab_cb), extension); panel = katze_object_get_object (browser, "panel"); scripts = addons_new (ADDONS_USER_SCRIPTS, extension); gtk_widget_show (scripts); midori_panel_append_page (MIDORI_PANEL (panel), MIDORI_VIEWABLE (scripts)); g_object_set_data (G_OBJECT (browser), "scripts-addons", scripts); styles = addons_new (ADDONS_USER_STYLES, extension); gtk_widget_show (styles); midori_panel_append_page (MIDORI_PANEL (panel), MIDORI_VIEWABLE (styles)); g_object_set_data (G_OBJECT (browser), "styles-addons", styles); g_object_unref (panel); } static void addons_save_settings (MidoriApp* app, MidoriExtension* extension) { struct AddonsList* scripts_list, *styles_list; struct AddonElement* script, *style; GSList* scripts, *styles; GKeyFile* keyfile; const gchar* config_dir; gchar* config_file; GError* error = NULL; keyfile = g_key_file_new (); /* scripts */ scripts_list = g_object_get_data (G_OBJECT (extension), "scripts-list"); scripts = scripts_list->elements; while (scripts) { script = scripts->data; if (!script->enabled) g_key_file_set_integer (keyfile, "scripts", script->fullpath, 1); scripts = g_slist_next (scripts); } /* styles */ styles_list = g_object_get_data (G_OBJECT (extension), "styles-list"); styles = styles_list->elements; while (styles) { style = styles->data; if (!style->enabled) g_key_file_set_integer (keyfile, "styles", style->fullpath, 1); styles = g_slist_next (styles); } config_dir = midori_extension_get_config_dir (extension); config_file = g_build_filename (config_dir, "addons", NULL); katze_mkdir_with_parents (config_dir, 0700); sokoke_key_file_save_to_file (keyfile, config_file, &error); if (error) { g_warning (_("The configuration of the extension '%s' couldn't be saved: %s\n"), _("User addons"), error->message); g_error_free (error); } g_free (config_file); g_key_file_free (keyfile); } static void addons_disable_monitors (MidoriExtension* extension) { GSList* monitors; monitors = g_object_get_data (G_OBJECT (extension), "monitors"); if (!monitors) return; g_slist_foreach (monitors, (GFunc)g_file_monitor_cancel, NULL); g_slist_free (monitors); g_object_set_data (G_OBJECT (extension), "monitors", NULL); } static void addons_deactivate_cb (MidoriExtension* extension, MidoriApp* app) { KatzeArray* browsers; MidoriBrowser* browser; GSource* source; guint i = 0; addons_disable_monitors (extension); addons_save_settings (NULL, extension); browsers = katze_object_get_object (app, "browsers"); while ((browser = katze_array_get_nth_item (browsers, i++))) addons_browser_destroy (browser, extension); source = g_object_get_data (G_OBJECT (extension), "monitor-timer"); if (source && !g_source_is_destroyed (source)) g_source_destroy (source); g_signal_handlers_disconnect_by_func ( app, addons_app_add_browser_cb, extension); g_signal_handlers_disconnect_by_func ( app, addons_save_settings, extension); g_signal_handlers_disconnect_by_func ( extension, addons_deactivate_cb, app); g_object_unref (browsers); } static gboolean addons_reset_all_elements_cb (MidoriExtension* extension) { addons_save_settings (NULL, extension); addons_update_elements (extension, ADDONS_USER_STYLES); addons_update_elements (extension, ADDONS_USER_SCRIPTS); g_object_set_data (G_OBJECT (extension), "monitor-timer", NULL); return FALSE; } static void addons_directory_monitor_changed (GFileMonitor* monitor, GFile* child, GFile* other_file, GFileMonitorEvent flags, MidoriExtension* extension) { char* basename; GSource* source; basename = g_file_get_basename (child); if (g_str_has_prefix (basename, ".") || g_str_has_suffix (basename, "~")) /* Hidden or temporary files */ return; /* We receive a lot of change events, so we use a timeout to trigger elements update only once */ source = g_object_get_data (G_OBJECT (extension), "monitor-timer"); if (source && !g_source_is_destroyed (source)) g_source_destroy (source); source = g_timeout_source_new_seconds (1); g_source_set_callback (source, (GSourceFunc)addons_reset_all_elements_cb, extension, NULL); g_source_attach (source, NULL); g_object_set_data (G_OBJECT (extension), "monitor-timer", source); g_source_unref (source); } static void addons_monitor_directories (MidoriExtension* extension, AddonsKind kind) { GSList* directories; GError* error; GSList* monitors; GFileMonitor* monitor; GFile* directory; g_assert (kind == ADDONS_USER_SCRIPTS || kind == ADDONS_USER_STYLES); monitors = g_object_get_data (G_OBJECT (extension), "monitors"); directories = addons_get_directories (kind); while (directories) { directory = g_file_new_for_path (directories->data); directories = g_slist_next (directories); error = NULL; monitor = g_file_monitor_directory (directory, G_FILE_MONITOR_NONE, NULL, &error); if (monitor) { g_signal_connect (monitor, "changed", G_CALLBACK (addons_directory_monitor_changed), extension); monitors = g_slist_prepend (monitors, monitor); } else { g_warning (_("Can't monitor folder '%s': %s"), g_file_get_parse_name (directory), error->message); g_error_free (error); } g_object_unref (directory); } g_object_set_data (G_OBJECT (extension), "monitors", monitors); g_slist_free (directories); } static void addons_activate_cb (MidoriExtension* extension, MidoriApp* app) { KatzeArray* browsers; MidoriBrowser* browser; guint i; browsers = katze_object_get_object (app, "browsers"); addons_update_elements (extension, ADDONS_USER_STYLES); addons_monitor_directories (extension, ADDONS_USER_STYLES); addons_update_elements (extension, ADDONS_USER_SCRIPTS); addons_monitor_directories (extension, ADDONS_USER_SCRIPTS); i = 0; while ((browser = katze_array_get_nth_item (browsers, i++))) addons_app_add_browser_cb (app, browser, extension); g_object_unref (browsers); g_signal_connect (app, "add-browser", G_CALLBACK (addons_app_add_browser_cb), extension); g_signal_connect (app, "quit", G_CALLBACK (addons_save_settings), extension); g_signal_connect (extension, "deactivate", G_CALLBACK (addons_deactivate_cb), app); } MidoriExtension* extension_init (void) { MidoriExtension* extension = g_object_new (MIDORI_TYPE_EXTENSION, "name", _("User addons"), "description", _("Support for userscripts and userstyles"), "version", "0.1", "authors", "Arno Renevier ", NULL); g_signal_connect (extension, "activate", G_CALLBACK (addons_activate_cb), NULL); return extension; }