1/* 2 Copyright (C) 2018 Christian Dywan <christian@twotoats.de> 3 4 This library is free software; you can redistribute it and/or 5 modify it under the terms of the GNU Lesser General Public 6 License as published by the Free Software Foundation; either 7 version 2.1 of the License, or (at your option) any later version. 8 9 See the file COPYING for the full license text. 10*/ 11 12namespace Midori { 13 public interface AppActivatable : Peas.ExtensionBase { 14 public abstract App app { owned get; set; } 15 public abstract void activate (); 16 } 17 18 public class App : Gtk.Application { 19 public File? exec_path { get; protected set; default = null; } 20 21 static string? app = null; 22 [CCode (array_length = false, array_null_terminated = true)] 23 static string[]? execute = null; 24 static bool help_execute = false; 25 static int inactivity_reset = 0; 26 static bool incognito = false; 27 static bool version = false; 28 const OptionEntry[] options = { 29 { "app", 'a', 0, OptionArg.STRING, ref app, N_("Run ADDRESS as a web application"), N_("ADDRESS") }, 30 { "execute", 'e', 0, OptionArg.STRING_ARRAY, ref execute, N_("Execute the specified command"), null }, 31 { "help-execute", 0, 0, OptionArg.NONE, ref help_execute, N_("List available commands to execute with -e/ --execute"), null }, 32 { "inactivity-reset", 'i', 0, OptionArg.INT, ref inactivity_reset, N_("Reset Midori after SECONDS seconds of inactivity"), N_("SECONDS") }, 33 { "private", 'p', 0, OptionArg.NONE, ref incognito, N_("Private browsing, no changes are saved"), null }, 34 { "version", 'V', 0, OptionArg.NONE, ref version, N_("Display version number"), null }, 35 { null } 36 }; 37 const ActionEntry[] actions = { 38 { "win-incognito-new", win_incognito_new_activated }, 39 { "quit", quit_activated }, 40 }; 41 42 public App () { 43 Object (application_id: Config.PROJECT_DOMAIN, 44 flags: ApplicationFlags.HANDLES_OPEN 45 | ApplicationFlags.HANDLES_COMMAND_LINE); 46 47 add_main_option_entries (options); 48 } 49 50 public override bool local_command_line (ref weak string[] args, out int exit_status) { 51 exit_status = -1; 52 // Get the executable path 53 string executable = args[0]; 54 try { 55 if (!Path.is_absolute (executable)) { 56 executable = Environment.find_program_in_path (executable); 57 if (FileUtils.test (executable, FileTest.IS_SYMLINK)) 58 executable = FileUtils.read_link (executable); 59 } 60 } catch (FileError error) { 61 debug ("Failed to look up exec path: %s", error.message); 62 } 63 exec_path = File.new_for_path (executable); 64 65 return base.local_command_line (ref args, out exit_status); 66 } 67 68 public override void startup () { 69 Intl.bindtextdomain (Config.PROJECT_NAME, null); 70 Intl.bind_textdomain_codeset (Config.PROJECT_NAME, "UTF-8"); 71 Intl.textdomain (Config.PROJECT_NAME); 72 73 base.startup (); 74 75 Gtk.Window.set_default_icon_name (Config.PROJECT_DOMAIN); 76 77 var context = WebKit.WebContext.get_default (); 78 context.register_uri_scheme ("internal", (request) => { 79 request.ref (); 80 internal_scheme.begin (request); 81 }); 82 context.register_uri_scheme ("favicon", (request) => { 83 request.ref (); 84 favicon_scheme.begin (request); 85 }); 86 context.register_uri_scheme ("stock", (request) => { 87 request.ref (); 88 stock_scheme.begin (request); 89 }); 90 context.register_uri_scheme ("res", (request) => { 91 try { 92 var stream = resources_open_stream (request.get_path (), 93 ResourceLookupFlags.NONE); 94 request.finish (stream, -1, null); 95 } catch (Error error) { 96 request.finish_error (error); 97 critical ("Failed to load resource %s: %s", request.get_uri (), error.message); 98 } 99 }); 100 string config = Path.build_path (Path.DIR_SEPARATOR_S, 101 Environment.get_user_config_dir (), Environment.get_prgname ()); 102 DirUtils.create_with_parents (config, 0700); 103 string cookies = Path.build_filename (config, "cookies"); 104 context.get_cookie_manager ().set_persistent_storage (cookies, WebKit.CookiePersistentStorage.SQLITE); 105 string cache = Path.build_path (Path.DIR_SEPARATOR_S, 106 Environment.get_user_cache_dir (), Environment.get_prgname ()); 107 string icons = Path.build_path (Path.DIR_SEPARATOR_S, cache, "icondatabase"); 108 context.set_favicon_database_directory (icons); 109 context.set_process_model (WebKit.ProcessModel.MULTIPLE_SECONDARY_PROCESSES); 110 111 // Try and load web extensions from build folder 112 var web_path = exec_path.get_parent ().get_child ("web"); 113 if (!web_path.query_exists (null)) { 114 // Alternatively look for an installed path 115 web_path = File.new_for_path (Config.PLUGINDIR); 116 } 117 context.set_web_extensions_directory (web_path.get_path ()); 118 context.initialize_web_extensions.connect (() => { 119 // Prefer plugins from the build folder 120 var builtin_path = exec_path.get_parent ().get_child ("extensions"); 121 if (!builtin_path.query_exists (null)) { 122 // System-wide plugins 123 builtin_path = File.new_for_path (Config.PLUGINDIR); 124 } 125 context.set_web_extensions_initialization_user_data (builtin_path.get_path ()); 126 }); 127 var settings = CoreSettings.get_default (); 128 context.set_spell_checking_enabled (settings.enable_spell_checking); 129 settings.notify["enable-spell-checking"].connect ((pspec) => { 130 context.set_spell_checking_enabled (settings.enable_spell_checking); 131 }); 132 context.get_cookie_manager ().set_accept_policy ( 133 settings.first_party_cookies_only ? WebKit.CookieAcceptPolicy.NO_THIRD_PARTY : WebKit.CookieAcceptPolicy.ALWAYS); 134 settings.notify["first-party-cookies-only"].connect ((pspec) => { 135 context.get_cookie_manager ().set_accept_policy ( 136 settings.first_party_cookies_only ? WebKit.CookieAcceptPolicy.NO_THIRD_PARTY : WebKit.CookieAcceptPolicy.ALWAYS); 137 }); 138 apply_proxy_settings (settings, context); 139 settings.notify["proxy-type"].connect ((pspec) => { 140 apply_proxy_settings (settings, context); 141 }); 142 settings.notify["http-proxy"].connect ((pspec) => { 143 apply_proxy_settings (settings, context); 144 }); 145 settings.notify["proxy-port"].connect ((pspec) => { 146 apply_proxy_settings (settings, context); 147 }); 148 149 add_action_entries (actions, this); 150 151 var action = new SimpleAction ("win-new", VariantType.STRING); 152 action.activate.connect (win_new_activated); 153 add_action (action); 154 set_accels_for_action ("app.win-new", { "<Primary>n" }); 155 set_accels_for_action ("app.win-incognito-new", { "<Primary><Shift>p", "<Primary><Shift>n" }); 156 157 // Unset app menu if not handled by the shell 158 if (!Gtk.Settings.get_default ().gtk_shell_shows_app_menu){ 159 app_menu = null; 160 } 161 162 // Try and load plugins from build folder 163 var builtin_path = exec_path.get_parent ().get_child ("extensions"); 164 if (!builtin_path.query_exists (null)) { 165 // System-wide plugins 166 builtin_path = File.new_for_path (Config.PLUGINDIR); 167 } 168 var plugins = Plugins.get_default (builtin_path.get_path ()); 169 // Save/ load state of plugins 170 plugins.load_plugin.connect ((info) => { 171 settings.set_plugin_enabled ("lib%s.so".printf (info.get_module_name ()), true); 172 }); 173 plugins.unload_plugin.connect ((info) => { 174 settings.set_plugin_enabled ("lib%s.so".printf (info.get_module_name ()), false); 175 }); 176 177 var extensions = Plugins.get_default ().plug<AppActivatable> ("app", this); 178 extensions.extension_added.connect ((info, extension) => ((AppActivatable)extension).activate ()); 179 extensions.foreach ((extensions, info, extension) => { extensions.extension_added (info, extension); }); 180 } 181 182 async void internal_scheme (WebKit.URISchemeRequest request) { 183 try { 184 var database = HistoryDatabase.get_default (); 185 var shortcuts = yield database.query (null, 9); 186 string content = ""; 187 uint index = 0; 188 foreach (var shortcut in shortcuts) { 189 var statement = database.prepare ("SELECT image FROM %s WHERE uri = :uri LIMIT 1".printf (database.table), 190 ":uri", typeof (string), shortcut.uri); 191 statement.step (); 192 var image_uri = statement.get_string ("image") ?? "favicon:///" + shortcut.uri; 193 index++; 194 content += """ 195 <div class="shortcut" style="background-image: url('%s')"> 196 <a href="%s" accesskey="%u"> 197 <span class="title">%s</span> 198 </a> 199 </div>""".printf (image_uri, shortcut.uri, index, shortcut.title); 200 } 201 string stylesheet = (string)resources_lookup_data ("/data/about.css", 202 ResourceLookupFlags.NONE).get_data (); 203 string html = ((string)resources_lookup_data ("/data/speed-dial.html", 204 ResourceLookupFlags.NONE).get_data ()) 205 .replace ("{title}", _("Speed Dial")) 206 .replace ("{icon}", "view-grid") 207 .replace ("{content}", content) 208 .replace ("{stylesheet}", stylesheet); 209 var stream = new MemoryInputStream.from_data (html.data, free); 210 request.finish (stream, html.length, "text/html"); 211 } catch (Error error) { 212 request.finish_error (error); 213 critical ("Failed to render %s: %s", request.get_uri (), error.message); 214 } 215 request.unref (); 216 } 217 218 void request_finish_pixbuf (WebKit.URISchemeRequest request, Gdk.Pixbuf pixbuf) throws Error { 219 var output = new MemoryOutputStream (null, realloc, free); 220 pixbuf.save_to_stream (output, "png"); 221 output.close (); 222 uint8[] data = output.steal_data (); 223 data.length = (int)output.get_data_size (); 224 var stream = new MemoryInputStream.from_data (data, free); 225 request.finish (stream, -1, null); 226 } 227 228 async void favicon_scheme (WebKit.URISchemeRequest request) { 229 string page_uri = request.get_path ().substring (1, -1); 230 try { 231 var database = request.get_web_view ().web_context.get_favicon_database (); 232 var surface = yield database.get_favicon (page_uri, null); 233 if (surface != null) { 234 var image = (Cairo.ImageSurface)surface; 235 var icon = Gdk.pixbuf_get_from_surface (image, 0, 0, image.get_width (), image.get_height ()); 236 request_finish_pixbuf (request, icon); 237 } 238 } catch (Error error) { 239 request.finish_error (error); 240 debug ("Failed to render favicon for %s: %s", page_uri, error.message); 241 } 242 request.unref (); 243 } 244 245 async void stock_scheme (WebKit.URISchemeRequest request) { 246 string icon_name = request.get_path ().substring (1, -1); 247 int icon_size = 48; 248 Gtk.icon_size_lookup ((Gtk.IconSize)Gtk.IconSize.DIALOG, out icon_size, null); 249 try { 250 var icon = Gtk.IconTheme.get_default ().load_icon (icon_name, icon_size, Gtk.IconLookupFlags.FORCE_SYMBOLIC); 251 request_finish_pixbuf (request, icon); 252 } catch (Error error) { 253 request.finish_error (error); 254 critical ("Failed to load icon %s: %s", icon_name, error.message); 255 } 256 request.unref (); 257 } 258 259 void apply_proxy_settings (CoreSettings settings, WebKit.WebContext context) { 260 switch (settings.proxy_type) { 261 case ProxyType.AUTOMATIC: 262 context.set_network_proxy_settings (WebKit.NetworkProxyMode.DEFAULT, null); 263 break; 264 case ProxyType.HTTP: 265 string proxy_uri = "%s:%d".printf (settings.http_proxy, settings.http_proxy_port); 266 context.set_network_proxy_settings ( 267 WebKit.NetworkProxyMode.CUSTOM, 268 new WebKit.NetworkProxySettings (proxy_uri, null)); 269 break; 270 case ProxyType.NONE: 271 context.set_network_proxy_settings (WebKit.NetworkProxyMode.NO_PROXY, null); 272 break; 273 } 274 } 275 276 internal WebKit.WebContext ephemeral_context () { 277 var context = new WebKit.WebContext.ephemeral (); 278 context.register_uri_scheme ("internal", (request) => { 279 request.ref (); 280 private_scheme.begin (request); 281 }); 282 context.register_uri_scheme ("stock", (request) => { 283 request.ref (); 284 stock_scheme.begin (request); 285 }); 286 context.register_uri_scheme ("res", (request) => { 287 try { 288 var stream = resources_open_stream (request.get_path (), 289 ResourceLookupFlags.NONE); 290 request.finish (stream, -1, null); 291 } catch (Error error) { 292 request.finish_error (error); 293 critical ("Failed to load resource %s: %s", request.get_uri (), error.message); 294 } 295 }); 296 var settings = CoreSettings.get_default (); 297 context.set_spell_checking_enabled (settings.enable_spell_checking); 298 settings.notify["enable-spell-checking"].connect ((pspec) => { 299 context.set_spell_checking_enabled (settings.enable_spell_checking); 300 }); 301 // Enable the database by resetting the directory to the default 302 context.set_favicon_database_directory (null); 303 context.get_cookie_manager ().set_accept_policy ( 304 settings.first_party_cookies_only ? WebKit.CookieAcceptPolicy.NO_THIRD_PARTY : WebKit.CookieAcceptPolicy.ALWAYS); 305 settings.notify["first-party-cookies-only"].connect ((pspec) => { 306 context.get_cookie_manager ().set_accept_policy ( 307 settings.first_party_cookies_only ? WebKit.CookieAcceptPolicy.NO_THIRD_PARTY : WebKit.CookieAcceptPolicy.ALWAYS); 308 }); 309 apply_proxy_settings (settings, context); 310 settings.notify["proxy-type"].connect ((pspec) => { 311 apply_proxy_settings (settings, context); 312 }); 313 settings.notify["http-proxy"].connect ((pspec) => { 314 apply_proxy_settings (settings, context); 315 }); 316 settings.notify["proxy-port"].connect ((pspec) => { 317 apply_proxy_settings (settings, context); 318 }); 319 return context; 320 } 321 322 async void private_scheme (WebKit.URISchemeRequest request) { 323 string[] suggestions = { 324 _("No history or web cookies are being saved."), 325 _("HTML5 storage, local database and application caches are disabled."), 326 }; 327 string[] notes = { 328 _("DNS prefetching is disabled."), 329 }; 330 331 try { 332 string description = "<ul>"; 333 foreach (var suggestion in suggestions) { 334 description += "<li>%s</li>".printf (suggestion); 335 } 336 description += "</ul>"; 337 description += "<b>%s</b><br>".printf (_("Midori prevents websites from tracking the user:")); 338 description += "<ul>"; 339 foreach (var note in notes) { 340 description += "<li>%s</li>".printf (note); 341 } 342 description += "</ul>"; 343 string stylesheet = (string)resources_lookup_data ("/data/about.css", 344 ResourceLookupFlags.NONE).get_data (); 345 string html = ((string)resources_lookup_data ("/data/error.html", 346 ResourceLookupFlags.NONE).get_data ()) 347 .replace ("{title}", _("Private Browsing")) 348 .replace ("{icon}", "user-not-tracked") 349 .replace ("{message}", _("Midori doesn't store any personal data:")) 350 .replace ("{description}", description) 351 .replace ("{tryagain}", "") 352 .replace ("{stylesheet}", stylesheet); 353 var stream = new MemoryInputStream.from_data (html.data, free); 354 request.finish (stream, html.length, "text/html"); 355 } catch (Error error) { 356 request.finish_error (error); 357 critical ("Failed to render %s: %s", request.get_uri (), error.message); 358 } 359 request.unref (); 360 } 361 362 void win_new_activated (Action action, Variant? parameter) { 363 var browser = new Browser (this); 364 if (!browser.default_tab ()) { 365 browser.add (new Tab (null, browser.web_context)); 366 } 367 string? uri = parameter.get_string () != "" ? parameter.get_string () : null; 368 if (uri != null) { 369 browser.add (new Tab (null, browser.web_context, uri)); 370 } 371 browser.show (); 372 } 373 374 void win_incognito_new_activated () { 375 var browser = new Browser.incognito (this); 376 if (!browser.default_tab ()) { 377 browser.add (new Tab (null, browser.web_context)); 378 } 379 browser.show (); 380 } 381 382 void quit_activated () { 383 quit (); 384 } 385 386 protected override void activate () { 387 if (incognito) { 388 activate_action ("win-incognito-new", null); 389 return; 390 } 391 activate_action ("win-new", ""); 392 } 393 394 protected override void open (File[] files, string hint) { 395 var browser = incognito 396 ? new Browser.incognito (this) 397 : (active_window as Browser ?? new Browser (this)); 398 foreach (File file in files) { 399 browser.add (new Tab (browser.tab, browser.web_context, file.get_uri ())); 400 } 401 browser.show (); 402 } 403 404 protected override int handle_local_options (VariantDict options) { 405 if (version) { 406 stdout.printf ("%s %s\n" + 407 "Copyright 2007-2018 Christian Dywan\n" + 408 "Please report comments, suggestions and bugs to:\n" + 409 " %s\n" + 410 "Check for new versions at:\n" + 411 " %s\n ", 412 Config.PROJECT_NAME, Config.CORE_VERSION, 413 Config.PROJECT_BUGS, Config.PROJECT_WEBSITE); 414 return 0; 415 } 416 417 // Propagate options processed in the primary instance 418 options.insert_value ("app", app ?? ""); 419 options.insert_value ("execute", execute); 420 options.insert_value ("help-execute", help_execute); 421 options.insert_value ("inactivity-reset", inactivity_reset); 422 options.insert_value ("private", incognito); 423 return -1; 424 } 425 426 protected override int command_line (ApplicationCommandLine command_line) { 427 hold (); 428 429 // Retrieve values for options passed from another process 430 var options = command_line.get_options_dict (); 431 app = options.lookup_value ("app", VariantType.STRING).get_string (); 432 execute = options.lookup_value ("execute", VariantType.STRING_ARRAY).dup_strv (); 433 help_execute = options.lookup_value ("help-execute", VariantType.BOOLEAN).get_boolean (); 434 inactivity_reset = options.lookup_value ("inactivity-reset", VariantType.INT32).get_int32 (); 435 incognito = options.lookup_value ("private", VariantType.BOOLEAN).get_boolean (); 436 debug ("Processing remote command line %s/ %s\n", 437 string.joinv (", ", command_line.get_arguments ()), options.end ().print (true)); 438 439 if (help_execute) { 440 foreach (string action in list_actions ()) { 441 command_line.print ("%s\n", action); 442 } 443 var browser = incognito ? new Browser.incognito (this) : new Browser (this); 444 foreach (string action in browser.list_actions ()) { 445 command_line.print ("%s\n", action); 446 } 447 } 448 449 if (app != "") { 450 var browser = new Browser (this, true); 451 var tab = new Tab (null, browser.web_context, app); 452 tab.pinned = true; 453 browser.add (tab); 454 browser.show (); 455 if (inactivity_reset > 0) { 456 Timeout.add_seconds (inactivity_reset, () => { 457 if (browser.idle) { 458 tab.load_uri (app); 459 } else { 460 browser.idle = true; 461 } 462 return Source.CONTINUE; 463 }, Priority.LOW); 464 } 465 } 466 467 uint argc = command_line.get_arguments ().length; 468 if (argc <= 1) { 469 if (active_window == null) { 470 activate (); 471 } 472 } else { 473 var files = new File[argc - 1]; 474 uint i = 0; 475 foreach (string argument in command_line.get_arguments ()) { 476 // Skip program name 477 if (i > 0) { 478 files[i - 1] = File.new_for_commandline_arg (argument); 479 } 480 i++; 481 } 482 open (files, ""); 483 } 484 485 var action_group = active_window as ActionGroup; 486 foreach (string action_ in execute) { 487 // Accept action names regardless of case 488 string action = action_.down (); 489 debug ("Executing %s\n", action); 490 if (action_group.has_action (action)) { 491 action_group.activate_action (action, null); 492 } else { 493 warning (_("Unexpected action '%s'.").printf (action)); 494 } 495 } 496 497 release (); 498 return 0; 499 } 500 } 501} 502