<commit_before>/*
 Copyright (C) 2009-2014 <NAME> <<EMAIL>>
 Copyright (C) 2009-2012 <NAME> <<EMAIL>>

 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.
*/

namespace Adblock {
    public enum Directive {
        ALLOW,
        BLOCK
    }

    public class Extension : Midori.Extension {
        Config config;
        Subscription custom;
        HashTable<string, Directive?> cache;
        Gtk.TreeView treeview;
        Gtk.ListStore liststore;
        bool disable_toggled;
        List<IconButton> toggle_buttons;

        public class IconButton : Gtk.Button {
            Gtk.Image icon;

            public IconButton () {
                icon = new Gtk.Image ();
                add (icon);
                icon.show ();
            }

            public void set_status (string status) {
                string filename = Midori.Paths.get_res_filename ("adblock/%s.svg".printf (status));
                icon.set_from_file (filename);
            }
        }

#if HAVE_WEBKIT2
        public Extension (WebKit.WebExtension web_extension) {
            init ();
            web_extension.page_created.connect (page_created);
        }

        void page_created (WebKit.WebPage web_page) {
            web_page.send_request.connect (send_request);
        }

        bool send_request (WebKit.WebPage web_page, WebKit.URIRequest request, WebKit.URIResponse? redirected_response) {
            return request_handled (web_page.uri, request.uri);
        }
#else
        public Extension () {
            GLib.Object (name: _("Advertisement blocker"),
                         description: _("Block advertisements according to a filter list"),
                         version: "2.0",
                         authors: "<NAME> <<EMAIL>>");
            activate.connect (extension_activated);
            open_preferences.connect (extension_preferences);
        }

        void extension_preferences () {
            open_dialog (null);
        }

        void open_dialog (string? uri) {
            var dialog = new Gtk.Dialog.with_buttons (_("Configure Advertisement filters"),
                null,
#if !HAVE_GTK3
                Gtk.DialogFlags.NO_SEPARATOR |
#endif
                Gtk.DialogFlags.DESTROY_WITH_PARENT,
                Gtk.STOCK_HELP, Gtk.ResponseType.HELP,
                Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE);
#if HAVE_GTK3
            dialog.get_widget_for_response (Gtk.ResponseType.HELP).get_style_context ().add_class ("help_button");
#endif
            dialog.set_icon_name (Gtk.STOCK_PROPERTIES);
            dialog.set_response_sensitive (Gtk.ResponseType.HELP, false);

            var hbox = new Gtk.HBox (false, 0);
            (dialog.get_content_area () as Gtk.Box).pack_start (hbox, true, true, 12);
            var vbox = new Gtk.VBox (false, 0);
            hbox.pack_start (vbox, true, true, 4);
            var button = new Gtk.Label (null);
            string description = """
                Type the address of a preconfigured filter list in the text entry
                and click "Add" to add it to the list.
                You can find more lists at %s %s.
                """.printf (
                "<a href=\"http://adblockplus.org/en/subscriptions\">adblockplus.org/en/subscriptions</a>",
                "<a href=\"http://easylist.adblockplus.org/\">easylist.adblockplus.org</a>");
            button.activate_link.connect ((uri)=>{
                var browser = get_app ().browser;
                var view = browser.add_uri (uri);
                browser.tab = view;
                return true;
            });
            button.set_markup (description);
            button.set_line_wrap (true);
            vbox.pack_start (button, false, false, 4);

            var entry = new Gtk.Entry ();
            if (uri != null)
                entry.set_text (uri);
            vbox.pack_start (entry, false, false, 4);

            liststore = new Gtk.ListStore (1, typeof (Subscription));
            treeview = new Gtk.TreeView.with_model (liststore);
            treeview.set_headers_visible (false);
            var column = new Gtk.TreeViewColumn ();
            var renderer_toggle = new Gtk.CellRendererToggle ();
            column.pack_start (renderer_toggle, false);
            column.set_cell_data_func (renderer_toggle, (column, renderer, model, iter) => {
                Subscription sub;
                liststore.get (iter, 0, out sub);
                renderer.set ("active", sub.active,
                              "sensitive", sub.mutable);
            });
            renderer_toggle.toggled.connect ((path) => {
                Gtk.TreeIter iter;
                if (liststore.get_iter_from_string (out iter, path)) {
                    Subscription sub;
                    liststore.get (iter, 0, out sub);
                    sub.active = !sub.active;
                }
            });
            treeview.append_column (column);

            column = new Gtk.TreeViewColumn ();
            var renderer_text = new Gtk.CellRendererText ();
            column.pack_start (renderer_text, false);
            renderer_text.set ("editable", true);
            // TODO: renderer_text.edited.connect
            column.set_cell_data_func (renderer_text, (column, renderer, model, iter) => {
                Subscription sub;
                liststore.get (iter, 0, out sub);
                renderer.set ("text", sub.title ?? sub.uri);
            });
            treeview.append_column (column);

            column = new Gtk.TreeViewColumn ();
            Gtk.CellRendererPixbuf renderer_button = new Gtk.CellRendererPixbuf ();
            column.pack_start (renderer_button, false);
            column.set_cell_data_func (renderer_button, on_render_button);
            treeview.append_column (column);

            var scrolled = new Gtk.ScrolledWindow (null, null);
            scrolled.set_policy (Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
            scrolled.add (treeview);
            vbox.pack_start (scrolled);

            foreach (Subscription sub in config)
                liststore.insert_with_values (null, 0, 0, sub);
            treeview.button_release_event.connect (button_released);

            entry.activate.connect (() => {
                var sub = new Subscription (entry.text);
                if (config.add (sub))
                    liststore.insert_with_values (null, 0, 0, sub);
                entry.text = "";
            });

            dialog.get_content_area ().show_all ();

            dialog.response.connect ((response)=>{ dialog.destroy (); });
            dialog.show ();
        }

        void on_render_button (Gtk.CellLayout column, Gtk.CellRenderer renderer,
            Gtk.TreeModel model, Gtk.TreeIter iter) {

            Subscription sub;
            liststore.get (iter, 0, out sub);

            renderer.set ("stock-id", sub.mutable ? Gtk.STOCK_DELETE : null,
                          "stock-size", Gtk.IconSize.MENU);
        }

        bool button_released (Gdk.EventButton event) {
            Gtk.TreePath? path;
            Gtk.TreeViewColumn column;
            if (treeview.get_path_at_pos ((int)event.x, (int)event.y, out path, out column, null, null)) {
                if (path != null) {
                    if (column == treeview.get_column (2)) {
                        Gtk.TreeIter iter;
                        if (liststore.get_iter (out iter, path)) {
                            Subscription sub;
                            liststore.get (iter, 0, out sub);
                            if (sub.mutable) {
                                config.remove (sub);
                                liststore.remove (iter);
                                return true;
                            }
                        }
                    }
                }
            }
            return false;
        }


        void extension_activated (Midori.App app) {
            init ();
            foreach (var browser in app.get_browsers ())
                browser_added (browser);
            app.add_browser.connect (browser_added);
        }

        void browser_added (Midori.Browser browser) {
            foreach (var tab in browser.get_tabs ())
                tab_added (tab);
            browser.add_tab.connect (tab_added);

            var toggle_button = new IconButton ();
            toggle_button.set_status (disable_toggled ? "disabled" : "enabled");
            browser.statusbar.pack_start (toggle_button, false, false, 3);
            toggle_button.show ();
            toggle_button.clicked.connect (icon_clicked);
            toggle_buttons.append (toggle_button);
        }

        void update_buttons (string page_uri="") {
            foreach (var toggle_button in toggle_buttons) {
                if (cache.lookup (page_uri) == Directive.BLOCK)
                    toggle_button.set_status ("blocked");
                else
                    toggle_button.set_status (disable_toggled ? "disabled" : "enabled");
            }
        }

        void icon_clicked (Gtk.Button toggle_button) {
            var menu = new Gtk.Menu ();
            var checkitem = new Gtk.CheckMenuItem.with_label (_("Disabled"));
            checkitem.set_active (disable_toggled);
            checkitem.toggled.connect (() => {
                disable_toggled = checkitem.active;
                update_buttons ();

                config.keyfile.set_boolean ("settings", "disabled", disable_toggled);
                config.save ();
            });
            menu.append (checkitem);

            var menuitem = new Gtk.ImageMenuItem.with_label (_("Preferences"));
            var image = new Gtk.Image.from_stock (Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU);
            menuitem.always_show_image = true;
            menuitem.set_image (image);
            menuitem.activate.connect (() => {
                open_preferences ();
            });
            menu.append (menuitem);

            menu.show_all ();
            Katze.widget_popup (toggle_button, menu, null, Katze.MenuPos.CURSOR);
        }

        void tab_added (Midori.View view) {
            view.web_view.resource_request_starting.connect (resource_requested);
            view.web_view.navigation_policy_decision_requested.connect (navigation_requested);
            view.notify["load-status"].connect ((pspec) => {
                if (!disable_toggled)
                {
                    if (view.load_status == Midori.LoadStatus.FINISHED)
                        inject_css (view, view.uri);
                }
            });
            view.context_menu.connect (context_menu);
        }

        void context_menu (WebKit.HitTestResult hit_test_result, Midori.ContextAction menu) {
            string label, uri;
            if ((hit_test_result.context & WebKit.HitTestResultContext.IMAGE) != 0) {
                label = _("Bl_ock image");
                uri = hit_test_result.image_uri;
            } else if ((hit_test_result.context & WebKit.HitTestResultContext.LINK) != 0)  {
                label = _("Bl_ock link");
                uri = hit_test_result.link_uri;
            } else
                return;
            var action = new Gtk.Action ("BlockElement", label, null, null);
            action.activate.connect ((action) => {
                edit_rule_dialog (uri);
            });
            menu.add (action);
        }

        void edit_rule_dialog (string uri) {
            var dialog = new Gtk.Dialog.with_buttons (_("Edit rule"),
                null,
#if !HAVE_GTK3
                Gtk.DialogFlags.NO_SEPARATOR |
#endif
                Gtk.DialogFlags.DESTROY_WITH_PARENT,
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_ADD, Gtk.ResponseType.ACCEPT);
            dialog.set_icon_name (Gtk.STOCK_ADD);
            dialog.resizable = false;

            var hbox = new Gtk.HBox (false, 8);
            var sizegroup = new Gtk.SizeGroup (Gtk.SizeGroupMode.HORIZONTAL);
            hbox.border_width = 5;
            var label = new Gtk.Label.with_mnemonic (_("_Rule:"));
            sizegroup.add_widget (label);
            hbox.pack_start (label, false, false, 0);
            (dialog.get_content_area () as Gtk.Box).pack_start (hbox, false, true, 0);

            var entry = new Gtk.Entry ();
            sizegroup.add_widget (entry);
            entry.activates_default = true;
            entry.set_text (uri);
            hbox.pack_start (entry, true, true, 0);

            dialog.get_content_area ().show_all ();

            dialog.set_default_response (Gtk.ResponseType.ACCEPT);
            if (dialog.run () != Gtk.ResponseType.ACCEPT)
                return;

            string new_rule = entry.get_text ();
            dialog.destroy ();
            custom.add_rule (new_rule);
        }


        void resource_requested (WebKit.WebView web_view, WebKit.WebFrame frame,
            WebKit.WebResource resource, WebKit.NetworkRequest request, WebKit.NetworkResponse? response) {

            if (request_handled (web_view.uri, request.uri)) {
                request.set_uri ("about:blank");
                update_buttons (web_view.uri);
            }
        }

        bool navigation_requested (WebKit.WebFrame frame, WebKit.NetworkRequest request,
            WebKit.WebNavigationAction action, WebKit.WebPolicyDecision decision) {

            string uri = request.uri;
            if (uri.has_prefix ("abp:")) {
                uri = uri.replace ("abp://", "abp:");
                if (uri.has_prefix ("abp:subscribe?location=")) {
                    /* abp://subscripe?location=http://example.com&title=foo */
                    string[] parts = uri.substring (23, -1).split ("&", 2);
                    decision.ignore ();
                    open_dialog (parts[0]);
                    return true;
                }
            }
            update_buttons (request.uri);
            return false;
        }

        void inject_css (Midori.View view, string page_uri) {
            /* Don't block ads on internal pages */
            if (!Midori.URI.is_http (page_uri))
                return;
            string domain = Midori.URI.parse_hostname (page_uri, null);
            string[] subdomains = domain.split (".");
            if (subdomains == null)
                return;
            int cnt = subdomains.length - 1;
            var subdomain = new StringBuilder (subdomains[cnt]);
            subdomain.prepend_c ('.');
            cnt--;
            var code = new StringBuilder ();
            bool debug_element = "adblock:element" in (Environment.get_variable ("MIDORI_DEBUG") ?? "");
            string hider_css;

            /* Hide elements that were blocked, otherwise we will get "broken image" icon */
            cache.foreach ((key, val) => {
                if (val == Adblock.Directive.BLOCK)
                    code.append ("img[src*=\"%s\"] , iframe[src*=\"%s\"] , ".printf (key, key));
            });
            if (debug_element)
                hider_css = " { background-color: red; border: 4px solid green; }";
            else
                hider_css = " { visiblility: hidden; width: 0; height: 0; }";

            code.truncate (code.len -3);
            code.append (hider_css);
            if (debug_element)
                stdout.printf ("hider css: %s\n", code.str);
            view.inject_stylesheet (code.str);

            code.erase ();
            int blockscnt = 0;
            while (cnt >= 0) {
                subdomain.prepend (subdomains[cnt]);
                string? style = null;
                foreach (Subscription sub in config) {
                    foreach (var feature in sub) {
                        if (feature is Adblock.Element) {
                            style = (feature as Adblock.Element).lookup (subdomain.str);
                            break;
                        }
                    }
                }
                if (style != null) {
                    code.append (style);
                    code.append_c (',');
                    blockscnt++;
                }
                subdomain.prepend_c ('.');
                cnt--;
            }
            if (blockscnt == 0)
                return;
            code.truncate (code.len - 1);

            if (debug_element)
                hider_css = " { background-color: red !important; border: 4px solid green !important; }";
            else
                hider_css = " { display: none !important }";

            code.append (hider_css);
            view.inject_stylesheet (code.str);
            if (debug_element)
                stdout.printf ("css: %s\n", code.str);
        }
#endif

        internal void init () {
            debug ("Adblock2");

            string config_dir = Midori.Paths.get_extension_config_dir ("adblock");
            config = new Config (config_dir);
            string custom_list = GLib.Path.build_filename (config_dir, "custom.list");
            try {
                custom = new Subscription (Filename.to_uri (custom_list, null));
                custom.mutable = false;
                custom.title = _("Custom");
                config.add (custom);
            } catch (Error error) {
                custom = null;
                warning ("Failed to add custom list %s: %s", custom_list, error.message);
            }

            string presets = Midori.Paths.get_extension_preset_filename ("adblock", "config");
            var default_config = new Config (Path.get_dirname (presets));
            foreach (var sub in default_config)
                config.add (sub);
            reload_rules ();

            try {
                disable_toggled = config.keyfile.get_boolean ("settings", "disabled");
            } catch (GLib.Error settings_error) {
                warning ("Error reading settings: %s\n", settings_error.message);
            }
        }<commit_msg>Don't show scarry warning if 'disabled' setting is not found<commit_after>/*
 Copyright (C) 2009-2014 <NAME> <<EMAIL>>
 Copyright (C) 2009-2012 <NAME> <<EMAIL>>

 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.
*/

namespace Adblock {
    public enum Directive {
        ALLOW,
        BLOCK
    }

    public class Extension : Midori.Extension {
        Config config;
        Subscription custom;
        HashTable<string, Directive?> cache;
        Gtk.TreeView treeview;
        Gtk.ListStore liststore;
        bool disable_toggled;
        List<IconButton> toggle_buttons;

        public class IconButton : Gtk.Button {
            Gtk.Image icon;

            public IconButton () {
                icon = new Gtk.Image ();
                add (icon);
                icon.show ();
            }

            public void set_status (string status) {
                string filename = Midori.Paths.get_res_filename ("adblock/%s.svg".printf (status));
                icon.set_from_file (filename);
            }
        }

#if HAVE_WEBKIT2
        public Extension (WebKit.WebExtension web_extension) {
            init ();
            web_extension.page_created.connect (page_created);
        }

        void page_created (WebKit.WebPage web_page) {
            web_page.send_request.connect (send_request);
        }

        bool send_request (WebKit.WebPage web_page, WebKit.URIRequest request, WebKit.URIResponse? redirected_response) {
            return request_handled (web_page.uri, request.uri);
        }
#else
        public Extension () {
            GLib.Object (name: _("Advertisement blocker"),
                         description: _("Block advertisements according to a filter list"),
                         version: "2.0",
                         authors: "<NAME> <<EMAIL>>");
            activate.connect (extension_activated);
            open_preferences.connect (extension_preferences);
        }

        void extension_preferences () {
            open_dialog (null);
        }

        void open_dialog (string? uri) {
            var dialog = new Gtk.Dialog.with_buttons (_("Configure Advertisement filters"),
                null,
#if !HAVE_GTK3
                Gtk.DialogFlags.NO_SEPARATOR |
#endif
                Gtk.DialogFlags.DESTROY_WITH_PARENT,
                Gtk.STOCK_HELP, Gtk.ResponseType.HELP,
                Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE);
#if HAVE_GTK3
            dialog.get_widget_for_response (Gtk.ResponseType.HELP).get_style_context ().add_class ("help_button");
#endif
            dialog.set_icon_name (Gtk.STOCK_PROPERTIES);
            dialog.set_response_sensitive (Gtk.ResponseType.HELP, false);

            var hbox = new Gtk.HBox (false, 0);
            (dialog.get_content_area () as Gtk.Box).pack_start (hbox, true, true, 12);
            var vbox = new Gtk.VBox (false, 0);
            hbox.pack_start (vbox, true, true, 4);
            var button = new Gtk.Label (null);
            string description = """
                Type the address of a preconfigured filter list in the text entry
                and click "Add" to add it to the list.
                You can find more lists at %s %s.
                """.printf (
                "<a href=\"http://adblockplus.org/en/subscriptions\">adblockplus.org/en/subscriptions</a>",
                "<a href=\"http://easylist.adblockplus.org/\">easylist.adblockplus.org</a>");
            button.activate_link.connect ((uri)=>{
                var browser = get_app ().browser;
                var view = browser.add_uri (uri);
                browser.tab = view;
                return true;
            });
            button.set_markup (description);
            button.set_line_wrap (true);
            vbox.pack_start (button, false, false, 4);

            var entry = new Gtk.Entry ();
            if (uri != null)
                entry.set_text (uri);
            vbox.pack_start (entry, false, false, 4);

            liststore = new Gtk.ListStore (1, typeof (Subscription));
            treeview = new Gtk.TreeView.with_model (liststore);
            treeview.set_headers_visible (false);
            var column = new Gtk.TreeViewColumn ();
            var renderer_toggle = new Gtk.CellRendererToggle ();
            column.pack_start (renderer_toggle, false);
            column.set_cell_data_func (renderer_toggle, (column, renderer, model, iter) => {
                Subscription sub;
                liststore.get (iter, 0, out sub);
                renderer.set ("active", sub.active,
                              "sensitive", sub.mutable);
            });
            renderer_toggle.toggled.connect ((path) => {
                Gtk.TreeIter iter;
                if (liststore.get_iter_from_string (out iter, path)) {
                    Subscription sub;
                    liststore.get (iter, 0, out sub);
                    sub.active = !sub.active;
                }
            });
            treeview.append_column (column);

            column = new Gtk.TreeViewColumn ();
            var renderer_text = new Gtk.CellRendererText ();
            column.pack_start (renderer_text, false);
            renderer_text.set ("editable", true);
            // TODO: renderer_text.edited.connect
            column.set_cell_data_func (renderer_text, (column, renderer, model, iter) => {
                Subscription sub;
                liststore.get (iter, 0, out sub);
                renderer.set ("text", sub.title ?? sub.uri);
            });
            treeview.append_column (column);

            column = new Gtk.TreeViewColumn ();
            Gtk.CellRendererPixbuf renderer_button = new Gtk.CellRendererPixbuf ();
            column.pack_start (renderer_button, false);
            column.set_cell_data_func (renderer_button, on_render_button);
            treeview.append_column (column);

            var scrolled = new Gtk.ScrolledWindow (null, null);
            scrolled.set_policy (Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
            scrolled.add (treeview);
            vbox.pack_start (scrolled);

            foreach (Subscription sub in config)
                liststore.insert_with_values (null, 0, 0, sub);
            treeview.button_release_event.connect (button_released);

            entry.activate.connect (() => {
                var sub = new Subscription (entry.text);
                if (config.add (sub))
                    liststore.insert_with_values (null, 0, 0, sub);
                entry.text = "";
            });

            dialog.get_content_area ().show_all ();

            dialog.response.connect ((response)=>{ dialog.destroy (); });
            dialog.show ();
        }

        void on_render_button (Gtk.CellLayout column, Gtk.CellRenderer renderer,
            Gtk.TreeModel model, Gtk.TreeIter iter) {

            Subscription sub;
            liststore.get (iter, 0, out sub);

            renderer.set ("stock-id", sub.mutable ? Gtk.STOCK_DELETE : null,
                          "stock-size", Gtk.IconSize.MENU);
        }

        bool button_released (Gdk.EventButton event) {
            Gtk.TreePath? path;
            Gtk.TreeViewColumn column;
            if (treeview.get_path_at_pos ((int)event.x, (int)event.y, out path, out column, null, null)) {
                if (path != null) {
                    if (column == treeview.get_column (2)) {
                        Gtk.TreeIter iter;
                        if (liststore.get_iter (out iter, path)) {
                            Subscription sub;
                            liststore.get (iter, 0, out sub);
                            if (sub.mutable) {
                                config.remove (sub);
                                liststore.remove (iter);
                                return true;
                            }
                        }
                    }
                }
            }
            return false;
        }


        void extension_activated (Midori.App app) {
            init ();
            foreach (var browser in app.get_browsers ())
                browser_added (browser);
            app.add_browser.connect (browser_added);
        }

        void browser_added (Midori.Browser browser) {
            foreach (var tab in browser.get_tabs ())
                tab_added (tab);
            browser.add_tab.connect (tab_added);

            var toggle_button = new IconButton ();
            toggle_button.set_status (disable_toggled ? "disabled" : "enabled");
            browser.statusbar.pack_start (toggle_button, false, false, 3);
            toggle_button.show ();
            toggle_button.clicked.connect (icon_clicked);
            toggle_buttons.append (toggle_button);
        }

        void update_buttons (string page_uri="") {
            foreach (var toggle_button in toggle_buttons) {
                if (cache.lookup (page_uri) == Directive.BLOCK)
                    toggle_button.set_status ("blocked");
                else
                    toggle_button.set_status (disable_toggled ? "disabled" : "enabled");
            }
        }

        void icon_clicked (Gtk.Button toggle_button) {
            var menu = new Gtk.Menu ();
            var checkitem = new Gtk.CheckMenuItem.with_label (_("Disabled"));
            checkitem.set_active (disable_toggled);
            checkitem.toggled.connect (() => {
                disable_toggled = checkitem.active;
                update_buttons ();

                config.keyfile.set_boolean ("settings", "disabled", disable_toggled);
                config.save ();
            });
            menu.append (checkitem);

            var menuitem = new Gtk.ImageMenuItem.with_label (_("Preferences"));
            var image = new Gtk.Image.from_stock (Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU);
            menuitem.always_show_image = true;
            menuitem.set_image (image);
            menuitem.activate.connect (() => {
                open_preferences ();
            });
            menu.append (menuitem);

            menu.show_all ();
            Katze.widget_popup (toggle_button, menu, null, Katze.MenuPos.CURSOR);
        }

        void tab_added (Midori.View view) {
            view.web_view.resource_request_starting.connect (resource_requested);
            view.web_view.navigation_policy_decision_requested.connect (navigation_requested);
            view.notify["load-status"].connect ((pspec) => {
                if (!disable_toggled)
                {
                    if (view.load_status == Midori.LoadStatus.FINISHED)
                        inject_css (view, view.uri);
                }
            });
            view.context_menu.connect (context_menu);
        }

        void context_menu (WebKit.HitTestResult hit_test_result, Midori.ContextAction menu) {
            string label, uri;
            if ((hit_test_result.context & WebKit.HitTestResultContext.IMAGE) != 0) {
                label = _("Bl_ock image");
                uri = hit_test_result.image_uri;
            } else if ((hit_test_result.context & WebKit.HitTestResultContext.LINK) != 0)  {
                label = _("Bl_ock link");
                uri = hit_test_result.link_uri;
            } else
                return;
            var action = new Gtk.Action ("BlockElement", label, null, null);
            action.activate.connect ((action) => {
                edit_rule_dialog (uri);
            });
            menu.add (action);
        }

        void edit_rule_dialog (string uri) {
            var dialog = new Gtk.Dialog.with_buttons (_("Edit rule"),
                null,
#if !HAVE_GTK3
                Gtk.DialogFlags.NO_SEPARATOR |
#endif
                Gtk.DialogFlags.DESTROY_WITH_PARENT,
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_ADD, Gtk.ResponseType.ACCEPT);
            dialog.set_icon_name (Gtk.STOCK_ADD);
            dialog.resizable = false;

            var hbox = new Gtk.HBox (false, 8);
            var sizegroup = new Gtk.SizeGroup (Gtk.SizeGroupMode.HORIZONTAL);
            hbox.border_width = 5;
            var label = new Gtk.Label.with_mnemonic (_("_Rule:"));
            sizegroup.add_widget (label);
            hbox.pack_start (label, false, false, 0);
            (dialog.get_content_area () as Gtk.Box).pack_start (hbox, false, true, 0);

            var entry = new Gtk.Entry ();
            sizegroup.add_widget (entry);
            entry.activates_default = true;
            entry.set_text (uri);
            hbox.pack_start (entry, true, true, 0);

            dialog.get_content_area ().show_all ();

            dialog.set_default_response (Gtk.ResponseType.ACCEPT);
            if (dialog.run () != Gtk.ResponseType.ACCEPT)
                return;

            string new_rule = entry.get_text ();
            dialog.destroy ();
            custom.add_rule (new_rule);
        }


        void resource_requested (WebKit.WebView web_view, WebKit.WebFrame frame,
            WebKit.WebResource resource, WebKit.NetworkRequest request, WebKit.NetworkResponse? response) {

            if (request_handled (web_view.uri, request.uri)) {
                request.set_uri ("about:blank");
                update_buttons (web_view.uri);
            }
        }

        bool navigation_requested (WebKit.WebFrame frame, WebKit.NetworkRequest request,
            WebKit.WebNavigationAction action, WebKit.WebPolicyDecision decision) {

            string uri = request.uri;
            if (uri.has_prefix ("abp:")) {
                uri = uri.replace ("abp://", "abp:");
                if (uri.has_prefix ("abp:subscribe?location=")) {
                    /* abp://subscripe?location=http://example.com&title=foo */
                    string[] parts = uri.substring (23, -1).split ("&", 2);
                    decision.ignore ();
                    open_dialog (parts[0]);
                    return true;
                }
            }
            update_buttons (request.uri);
            return false;
        }

        void inject_css (Midori.View view, string page_uri) {
            /* Don't block ads on internal pages */
            if (!Midori.URI.is_http (page_uri))
                return;
            string domain = Midori.URI.parse_hostname (page_uri, null);
            string[] subdomains = domain.split (".");
            if (subdomains == null)
                return;
            int cnt = subdomains.length - 1;
            var subdomain = new StringBuilder (subdomains[cnt]);
            subdomain.prepend_c ('.');
            cnt--;
            var code = new StringBuilder ();
            bool debug_element = "adblock:element" in (Environment.get_variable ("MIDORI_DEBUG") ?? "");
            string hider_css;

            /* Hide elements that were blocked, otherwise we will get "broken image" icon */
            cache.foreach ((key, val) => {
                if (val == Adblock.Directive.BLOCK)
                    code.append ("img[src*=\"%s\"] , iframe[src*=\"%s\"] , ".printf (key, key));
            });
            if (debug_element)
                hider_css = " { background-color: red; border: 4px solid green; }";
            else
                hider_css = " { visiblility: hidden; width: 0; height: 0; }";

            code.truncate (code.len -3);
            code.append (hider_css);
            if (debug_element)
                stdout.printf ("hider css: %s\n", code.str);
            view.inject_stylesheet (code.str);

            code.erase ();
            int blockscnt = 0;
            while (cnt >= 0) {
                subdomain.prepend (subdomains[cnt]);
                string? style = null;
                foreach (Subscription sub in config) {
                    foreach (var feature in sub) {
                        if (feature is Adblock.Element) {
                            style = (feature as Adblock.Element).lookup (subdomain.str);
                            break;
                        }
                    }
                }
                if (style != null) {
                    code.append (style);
                    code.append_c (',');
                    blockscnt++;
                }
                subdomain.prepend_c ('.');
                cnt--;
            }
            if (blockscnt == 0)
                return;
            code.truncate (code.len - 1);

            if (debug_element)
                hider_css = " { background-color: red !important; border: 4px solid green !important; }";
            else
                hider_css = " { display: none !important }";

            code.append (hider_css);
            view.inject_stylesheet (code.str);
            if (debug_element)
                stdout.printf ("css: %s\n", code.str);
        }
#endif

        internal void init () {
            debug ("Adblock2");

            string config_dir = Midori.Paths.get_extension_config_dir ("adblock");
            config = new Config (config_dir);
            string custom_list = GLib.Path.build_filename (config_dir, "custom.list");
            try {
                custom = new Subscription (Filename.to_uri (custom_list, null));
                custom.mutable = false;
                custom.title = _("Custom");
                config.add (custom);
            } catch (Error error) {
                custom = null;
                warning ("Failed to add custom list %s: %s", custom_list, error.message);
            }

            string presets = Midori.Paths.get_extension_preset_filename ("adblock", "config");
            var default_config = new Config (Path.get_dirname (presets));
            foreach (var sub in default_config)
                config.add (sub);
            reload_rules ();

            try {
                if (config.keyfile.has_key ("settings", "disabled"))
                    disable_toggled = config.keyfile.get_boolean ("settings", "disabled");
                else
                    disable_toggled = false;
            } catch (GLib.Error settings_error) {
                warning ("Error reading settings: %s\n", settings_error.message);
            }
        }
{'fraction_non_alphanumeric': 0.10434782608695652, 'fraction_numerical': 0.003609515996718622, 'mean_word_length': 1.5944239500567536, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 57, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2210624', 'n_tokens_mistral': 10179, 'n_tokens_neox': 9635, 'n_words': 2250}