/* Copyright (C) 2018 Christian Dywan 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 Midori { public interface AppActivatable : Peas.ExtensionBase { public abstract App app { owned get; set; } public abstract void activate (); } public class App : Gtk.Application { public File? exec_path { get; protected set; default = null; } static string? app = null; [CCode (array_length = false, array_null_terminated = true)] static string[]? execute = null; static bool help_execute = false; static int inactivity_reset = 0; static bool incognito = false; static bool version = false; const OptionEntry[] options = { { "app", 'a', 0, OptionArg.STRING, ref app, N_("Run ADDRESS as a web application"), N_("ADDRESS") }, { "execute", 'e', 0, OptionArg.STRING_ARRAY, ref execute, N_("Execute the specified command"), null }, { "help-execute", 0, 0, OptionArg.NONE, ref help_execute, N_("List available commands to execute with -e/ --execute"), null }, { "inactivity-reset", 'i', 0, OptionArg.INT, ref inactivity_reset, N_("Reset Midori after SECONDS seconds of inactivity"), N_("SECONDS") }, { "private", 'p', 0, OptionArg.NONE, ref incognito, N_("Private browsing, no changes are saved"), null }, { "version", 'V', 0, OptionArg.NONE, ref version, N_("Display version number"), null }, { null } }; const ActionEntry[] actions = { { "win-incognito-new", win_incognito_new_activated }, { "quit", quit_activated }, }; public App () { Object (application_id: Config.PROJECT_DOMAIN, flags: ApplicationFlags.HANDLES_OPEN | ApplicationFlags.HANDLES_COMMAND_LINE); add_main_option_entries (options); } public override bool local_command_line (ref weak string[] args, out int exit_status) { exit_status = -1; // Get the executable path string executable = args[0]; try { if (!Path.is_absolute (executable)) { executable = Environment.find_program_in_path (executable); if (FileUtils.test (executable, FileTest.IS_SYMLINK)) executable = FileUtils.read_link (executable); } } catch (FileError error) { debug ("Failed to look up exec path: %s", error.message); } exec_path = File.new_for_path (executable); return base.local_command_line (ref args, out exit_status); } public override void startup () { Intl.bindtextdomain (Config.PROJECT_NAME, null); Intl.bind_textdomain_codeset (Config.PROJECT_NAME, "UTF-8"); Intl.textdomain (Config.PROJECT_NAME); base.startup (); Gtk.Window.set_default_icon_name (Config.PROJECT_DOMAIN); var context = WebKit.WebContext.get_default (); context.register_uri_scheme ("internal", (request) => { request.ref (); internal_scheme.begin (request); }); context.register_uri_scheme ("favicon", (request) => { request.ref (); favicon_scheme.begin (request); }); context.register_uri_scheme ("stock", (request) => { request.ref (); stock_scheme.begin (request); }); context.register_uri_scheme ("res", (request) => { try { var stream = resources_open_stream (request.get_path (), ResourceLookupFlags.NONE); request.finish (stream, -1, null); } catch (Error error) { request.finish_error (error); critical ("Failed to load resource %s: %s", request.get_uri (), error.message); } }); string config = Path.build_path (Path.DIR_SEPARATOR_S, Environment.get_user_config_dir (), Environment.get_prgname ()); DirUtils.create_with_parents (config, 0700); string cookies = Path.build_filename (config, "cookies"); context.get_cookie_manager ().set_persistent_storage (cookies, WebKit.CookiePersistentStorage.SQLITE); string cache = Path.build_path (Path.DIR_SEPARATOR_S, Environment.get_user_cache_dir (), Environment.get_prgname ()); string icons = Path.build_path (Path.DIR_SEPARATOR_S, cache, "icondatabase"); context.set_favicon_database_directory (icons); context.set_process_model (WebKit.ProcessModel.MULTIPLE_SECONDARY_PROCESSES); // Try and load web extensions from build folder var web_path = exec_path.get_parent ().get_child ("web"); if (!web_path.query_exists (null)) { // Alternatively look for an installed path web_path = File.new_for_path (Config.PLUGINDIR); } context.set_web_extensions_directory (web_path.get_path ()); context.initialize_web_extensions.connect (() => { // Prefer plugins from the build folder var builtin_path = exec_path.get_parent ().get_child ("extensions"); if (!builtin_path.query_exists (null)) { // System-wide plugins builtin_path = File.new_for_path (Config.PLUGINDIR); } context.set_web_extensions_initialization_user_data (builtin_path.get_path ()); }); var settings = CoreSettings.get_default (); context.set_spell_checking_enabled (settings.enable_spell_checking); settings.notify["enable-spell-checking"].connect ((pspec) => { context.set_spell_checking_enabled (settings.enable_spell_checking); }); context.get_cookie_manager ().set_accept_policy ( settings.first_party_cookies_only ? WebKit.CookieAcceptPolicy.NO_THIRD_PARTY : WebKit.CookieAcceptPolicy.ALWAYS); settings.notify["first-party-cookies-only"].connect ((pspec) => { context.get_cookie_manager ().set_accept_policy ( settings.first_party_cookies_only ? WebKit.CookieAcceptPolicy.NO_THIRD_PARTY : WebKit.CookieAcceptPolicy.ALWAYS); }); apply_proxy_settings (settings, context); settings.notify["proxy-type"].connect ((pspec) => { apply_proxy_settings (settings, context); }); settings.notify["http-proxy"].connect ((pspec) => { apply_proxy_settings (settings, context); }); settings.notify["proxy-port"].connect ((pspec) => { apply_proxy_settings (settings, context); }); add_action_entries (actions, this); var action = new SimpleAction ("win-new", VariantType.STRING); action.activate.connect (win_new_activated); add_action (action); set_accels_for_action ("app.win-new", { "n" }); set_accels_for_action ("app.win-incognito-new", { "p", "n" }); // Unset app menu if not handled by the shell if (!Gtk.Settings.get_default ().gtk_shell_shows_app_menu){ app_menu = null; } // Try and load plugins from build folder var builtin_path = exec_path.get_parent ().get_child ("extensions"); if (!builtin_path.query_exists (null)) { // System-wide plugins builtin_path = File.new_for_path (Config.PLUGINDIR); } var plugins = Plugins.get_default (builtin_path.get_path ()); // Save/ load state of plugins plugins.load_plugin.connect ((info) => { settings.set_plugin_enabled ("lib%s.so".printf (info.get_module_name ()), true); }); plugins.unload_plugin.connect ((info) => { settings.set_plugin_enabled ("lib%s.so".printf (info.get_module_name ()), false); }); var extensions = Plugins.get_default ().plug ("app", this); extensions.extension_added.connect ((info, extension) => ((AppActivatable)extension).activate ()); extensions.foreach ((extensions, info, extension) => { extensions.extension_added (info, extension); }); } async void internal_scheme (WebKit.URISchemeRequest request) { try { var database = HistoryDatabase.get_default (); var shortcuts = yield database.query (null, 9); string content = ""; uint index = 0; foreach (var shortcut in shortcuts) { var statement = database.prepare ("SELECT image FROM %s WHERE uri = :uri LIMIT 1".printf (database.table), ":uri", typeof (string), shortcut.uri); statement.step (); var image_uri = statement.get_string ("image") ?? "favicon:///" + shortcut.uri; index++; content += """ """.printf (image_uri, shortcut.uri, index, shortcut.title); } string stylesheet = (string)resources_lookup_data ("/data/about.css", ResourceLookupFlags.NONE).get_data (); string html = ((string)resources_lookup_data ("/data/speed-dial.html", ResourceLookupFlags.NONE).get_data ()) .replace ("{title}", _("Speed Dial")) .replace ("{icon}", "view-grid") .replace ("{content}", content) .replace ("{stylesheet}", stylesheet); var stream = new MemoryInputStream.from_data (html.data, free); request.finish (stream, html.length, "text/html"); } catch (Error error) { request.finish_error (error); critical ("Failed to render %s: %s", request.get_uri (), error.message); } request.unref (); } void request_finish_pixbuf (WebKit.URISchemeRequest request, Gdk.Pixbuf pixbuf) throws Error { var output = new MemoryOutputStream (null, realloc, free); pixbuf.save_to_stream (output, "png"); output.close (); uint8[] data = output.steal_data (); data.length = (int)output.get_data_size (); var stream = new MemoryInputStream.from_data (data, free); request.finish (stream, -1, null); } async void favicon_scheme (WebKit.URISchemeRequest request) { string page_uri = request.get_path ().substring (1, -1); try { var database = request.get_web_view ().web_context.get_favicon_database (); var surface = yield database.get_favicon (page_uri, null); if (surface != null) { var image = (Cairo.ImageSurface)surface; var icon = Gdk.pixbuf_get_from_surface (image, 0, 0, image.get_width (), image.get_height ()); request_finish_pixbuf (request, icon); } } catch (Error error) { request.finish_error (error); debug ("Failed to render favicon for %s: %s", page_uri, error.message); } request.unref (); } async void stock_scheme (WebKit.URISchemeRequest request) { string icon_name = request.get_path ().substring (1, -1); int icon_size = 48; Gtk.icon_size_lookup ((Gtk.IconSize)Gtk.IconSize.DIALOG, out icon_size, null); try { var icon = Gtk.IconTheme.get_default ().load_icon (icon_name, icon_size, Gtk.IconLookupFlags.FORCE_SYMBOLIC); request_finish_pixbuf (request, icon); } catch (Error error) { request.finish_error (error); critical ("Failed to load icon %s: %s", icon_name, error.message); } request.unref (); } void apply_proxy_settings (CoreSettings settings, WebKit.WebContext context) { switch (settings.proxy_type) { case ProxyType.AUTOMATIC: context.set_network_proxy_settings (WebKit.NetworkProxyMode.DEFAULT, null); break; case ProxyType.HTTP: string proxy_uri = "%s:%d".printf (settings.http_proxy, settings.http_proxy_port); context.set_network_proxy_settings ( WebKit.NetworkProxyMode.CUSTOM, new WebKit.NetworkProxySettings (proxy_uri, null)); break; case ProxyType.NONE: context.set_network_proxy_settings (WebKit.NetworkProxyMode.NO_PROXY, null); break; } } internal WebKit.WebContext ephemeral_context () { var context = new WebKit.WebContext.ephemeral (); context.register_uri_scheme ("internal", (request) => { request.ref (); private_scheme.begin (request); }); context.register_uri_scheme ("stock", (request) => { request.ref (); stock_scheme.begin (request); }); context.register_uri_scheme ("res", (request) => { try { var stream = resources_open_stream (request.get_path (), ResourceLookupFlags.NONE); request.finish (stream, -1, null); } catch (Error error) { request.finish_error (error); critical ("Failed to load resource %s: %s", request.get_uri (), error.message); } }); var settings = CoreSettings.get_default (); context.set_spell_checking_enabled (settings.enable_spell_checking); settings.notify["enable-spell-checking"].connect ((pspec) => { context.set_spell_checking_enabled (settings.enable_spell_checking); }); // Enable the database by resetting the directory to the default context.set_favicon_database_directory (null); context.get_cookie_manager ().set_accept_policy ( settings.first_party_cookies_only ? WebKit.CookieAcceptPolicy.NO_THIRD_PARTY : WebKit.CookieAcceptPolicy.ALWAYS); settings.notify["first-party-cookies-only"].connect ((pspec) => { context.get_cookie_manager ().set_accept_policy ( settings.first_party_cookies_only ? WebKit.CookieAcceptPolicy.NO_THIRD_PARTY : WebKit.CookieAcceptPolicy.ALWAYS); }); apply_proxy_settings (settings, context); settings.notify["proxy-type"].connect ((pspec) => { apply_proxy_settings (settings, context); }); settings.notify["http-proxy"].connect ((pspec) => { apply_proxy_settings (settings, context); }); settings.notify["proxy-port"].connect ((pspec) => { apply_proxy_settings (settings, context); }); return context; } async void private_scheme (WebKit.URISchemeRequest request) { string[] suggestions = { _("No history or web cookies are being saved."), _("HTML5 storage, local database and application caches are disabled."), }; string[] notes = { _("DNS prefetching is disabled."), }; try { string description = "
    "; foreach (var suggestion in suggestions) { description += "
  • %s
  • ".printf (suggestion); } description += "
"; description += "%s
".printf (_("Midori prevents websites from tracking the user:")); description += "
    "; foreach (var note in notes) { description += "
  • %s
  • ".printf (note); } description += "
"; string stylesheet = (string)resources_lookup_data ("/data/about.css", ResourceLookupFlags.NONE).get_data (); string html = ((string)resources_lookup_data ("/data/error.html", ResourceLookupFlags.NONE).get_data ()) .replace ("{title}", _("Private Browsing")) .replace ("{icon}", "user-not-tracked") .replace ("{message}", _("Midori doesn't store any personal data:")) .replace ("{description}", description) .replace ("{tryagain}", "") .replace ("{stylesheet}", stylesheet); var stream = new MemoryInputStream.from_data (html.data, free); request.finish (stream, html.length, "text/html"); } catch (Error error) { request.finish_error (error); critical ("Failed to render %s: %s", request.get_uri (), error.message); } request.unref (); } void win_new_activated (Action action, Variant? parameter) { var browser = new Browser (this); if (!browser.default_tab ()) { browser.add (new Tab (null, browser.web_context)); } string? uri = parameter.get_string () != "" ? parameter.get_string () : null; if (uri != null) { browser.add (new Tab (null, browser.web_context, uri)); } browser.show (); } void win_incognito_new_activated () { var browser = new Browser.incognito (this); if (!browser.default_tab ()) { browser.add (new Tab (null, browser.web_context)); } browser.show (); } void quit_activated () { quit (); } protected override void activate () { if (incognito) { activate_action ("win-incognito-new", null); return; } activate_action ("win-new", ""); } protected override void open (File[] files, string hint) { var browser = incognito ? new Browser.incognito (this) : (active_window as Browser ?? new Browser (this)); foreach (File file in files) { browser.add (new Tab (browser.tab, browser.web_context, file.get_uri ())); } browser.show (); } protected override int handle_local_options (VariantDict options) { if (version) { stdout.printf ("%s %s\n" + "Copyright 2007-2018 Christian Dywan\n" + "Please report comments, suggestions and bugs to:\n" + " %s\n" + "Check for new versions at:\n" + " %s\n ", Config.PROJECT_NAME, Config.CORE_VERSION, Config.PROJECT_BUGS, Config.PROJECT_WEBSITE); return 0; } // Propagate options processed in the primary instance options.insert_value ("app", app ?? ""); options.insert_value ("execute", execute); options.insert_value ("help-execute", help_execute); options.insert_value ("inactivity-reset", inactivity_reset); options.insert_value ("private", incognito); return -1; } protected override int command_line (ApplicationCommandLine command_line) { hold (); // Retrieve values for options passed from another process var options = command_line.get_options_dict (); app = options.lookup_value ("app", VariantType.STRING).get_string (); execute = options.lookup_value ("execute", VariantType.STRING_ARRAY).dup_strv (); help_execute = options.lookup_value ("help-execute", VariantType.BOOLEAN).get_boolean (); inactivity_reset = options.lookup_value ("inactivity-reset", VariantType.INT32).get_int32 (); incognito = options.lookup_value ("private", VariantType.BOOLEAN).get_boolean (); debug ("Processing remote command line %s/ %s\n", string.joinv (", ", command_line.get_arguments ()), options.end ().print (true)); if (help_execute) { foreach (string action in list_actions ()) { command_line.print ("%s\n", action); } var browser = incognito ? new Browser.incognito (this) : new Browser (this); foreach (string action in browser.list_actions ()) { command_line.print ("%s\n", action); } } if (app != "") { var browser = new Browser (this, true); var tab = new Tab (null, browser.web_context, app); tab.pinned = true; browser.add (tab); browser.show (); if (inactivity_reset > 0) { Timeout.add_seconds (inactivity_reset, () => { if (browser.idle) { tab.load_uri (app); } else { browser.idle = true; } return Source.CONTINUE; }, Priority.LOW); } } uint argc = command_line.get_arguments ().length; if (argc <= 1) { if (active_window == null) { activate (); } } else { var files = new File[argc - 1]; uint i = 0; foreach (string argument in command_line.get_arguments ()) { // Skip program name if (i > 0) { files[i - 1] = File.new_for_commandline_arg (argument); } i++; } open (files, ""); } var action_group = active_window as ActionGroup; foreach (string action_ in execute) { // Accept action names regardless of case string action = action_.down (); debug ("Executing %s\n", action); if (action_group.has_action (action)) { action_group.activate_action (action, null); } else { warning (_("Unexpected action '%s'.").printf (action)); } } release (); return 0; } } }