/* Copyright 2016 Software Freedom Conservancy Inc. * * This software is licensed under the GNU Lesser General Public License * (version 2.1 or later). See the COPYING file in this distribution. */ [DBus(name = "org.gnome.Shotwell.Authenticate")] public interface AuthenticationReceiver : Object { public abstract void callback(string url) throws DBusError, IOError; } [DBus(name = "org.gnome.Shotwell.Authenticate")] internal class AuthenticatorReceiverApp : Gtk.Application, AuthenticationReceiver { private Gee.HashMap pending_auth_requests = new Gee.HashMap(); public AuthenticatorReceiverApp() { Object(application_id: "org.gnome.Shotwell", flags: GLib.ApplicationFlags.HANDLES_OPEN | GLib.ApplicationFlags.HANDLES_COMMAND_LINE); } public override bool dbus_register(DBusConnection connection, string object_path) throws Error { try { connection.register_object(object_path, this); } catch (IOError e) { warning("Failed to register authentication helper on session connection: %s", e.message); } return true; } internal void register_auth_callback(string cookie, Spit.Publishing.AuthenticatedCallback cb) { pending_auth_requests[cookie] = cb; } internal void unregister_auth_callback(string cookie) { pending_auth_requests.unset(cookie); } public void callback(string callback_url) throws DBusError, IOError { try { var uri = Uri.parse(callback_url, UriFlags.NONE); debug("Got authentication callback uri: %s", callback_url); // See if something is waiting for a pending authentication var query = uri.get_query(); if (query == null || query == "") { debug("Callback does not have parameters. Not accepting"); return; } var uri_params = Uri.parse_params(uri.get_query()); if ("sw_auth_cookie" in uri_params) { var cookie = uri_params["sw_auth_cookie"]; if (pending_auth_requests.has_key(cookie)) { pending_auth_requests[cookie].authenticated(uri_params); LibraryWindow.get_app().present(); } else { debug("No call-back registered for cookie %s, probably user cancelled", cookie); } } else if (uri.get_scheme().has_prefix("com.googleusercontent.apps")) { if (pending_auth_requests.has_key(uri.get_scheme())) { pending_auth_requests[uri.get_scheme()].authenticated(uri_params); } else { debug("No call-back registered for cookie %s, probably user cancelled", uri.get_scheme()); } } } catch (Error error) { warning("Got invalid authentication call-back: %s", callback_url); } } } public class Application { public interface AuthCallback : Object { public abstract void authenticated(HashTable params); } private static Application instance = null; public static TimeZone timezone = null; private Gtk.Application system_app = null; private int system_app_run_retval = 0; private bool direct; public virtual signal void starting() { } public virtual signal void exiting(bool panicked) { } public virtual signal void init_done() { } private bool fixup_raw_thumbs = false; public void set_raw_thumbs_fix_required(bool should_fixup) { fixup_raw_thumbs = should_fixup; } public bool get_raw_thumbs_fix_required() { return fixup_raw_thumbs; } public Gtk.Application get_system_app () { return system_app; } private bool running = false; private bool exiting_fired = false; Gee.HashMap pending_auth_requests = new Gee.HashMap(); private Application(bool is_direct) { if (is_direct) { // we allow multiple instances of ourself in direct mode, so DON'T // attempt to be unique. We don't request any command-line handling // here because this is processed elsewhere, and we don't need to handle // command lines from remote instances, since we don't care about them. system_app = new Gtk.Application("org.gnome.Shotwell-Viewer", GLib.ApplicationFlags.HANDLES_OPEN | GLib.ApplicationFlags.NON_UNIQUE); } else { // we've been invoked in library mode; set up for uniqueness and handling // of incoming command lines from remote instances (needed for getting // storage device and camera mounts). system_app = new AuthenticatorReceiverApp(); } // GLib will assert if we don't do this... try { system_app.register(); } catch (Error e) { panic(); } direct = is_direct; if (!direct) { system_app.command_line.connect(on_command_line); var action = new SimpleAction("authenticated", VariantType.STRING); system_app.add_action(action); action.activate.connect((a, p) => { try { var uri = Uri.parse(p.get_string(), UriFlags.NONE); debug("Got authentication callback uri: %s", p.get_string()); // See if something is waiting for a pending authentication var uri_params = Uri.parse_params(uri.get_query()); if ("sw_auth_cookie" in uri_params) { var cookie = uri_params["sw_auth_cookie"]; if (pending_auth_requests.has_key(cookie)) { pending_auth_requests[cookie].authenticated(uri_params); } else { debug("No call-back registered for cookie %s, probably user cancelled", cookie); } } } catch (Error error) { warning("Got invalid authentication call-back: %s", p.get_string()); } }); } system_app.activate.connect(on_activated); system_app.startup.connect(on_activated); } public static void register_auth_callback(string cookie, Spit.Publishing.AuthenticatedCallback cb) { var instance = get_instance(); if (!instance.direct) { ((AuthenticatorReceiverApp)instance.system_app).register_auth_callback(cookie, cb); } } public static void unregister_auth_callback(string cookie) { var instance = get_instance(); if (!instance.direct) { ((AuthenticatorReceiverApp)instance.system_app).unregister_auth_callback(cookie); } } public static double get_scale() { var instance = get_instance().system_app; unowned GLib.List windows = instance.get_windows(); if (windows == null) return 1.0; return windows.data.get_scale_factor(); } /** * @brief This is a helper for library mode that should only be * called if we've gotten a camera mount and are _not_ the primary * instance. */ public static void send_to_primary_instance(string[]? argv) { get_instance().system_app.run(argv); } /** * @brief A helper for library mode that tells the primary * instance to bring its window to the foreground. This * should only be called if we are _not_ the primary instance. */ public static void present_primary_instance() { get_instance().system_app.activate(); } public static bool get_is_remote() { return get_instance().system_app.get_is_remote(); } public static bool get_is_direct() { return get_instance().direct; } public static void set_accels_for_action (string action, string[] accel) { get_instance().system_app.set_accels_for_action (action, accel); } public static void set_menubar (GLib.MenuModel? model) { get_instance().system_app.set_menubar (model); } /** * @brief Signal handler for GApplication's 'activate' signal. * * The most likely scenario for this to be fired is if the user * either tried to run us twice in library mode, or we've just gotten * a camera/removable-storage mount; in either case, the remote instance * will trigger this and exit, and we'll need to bring the window back up... */ public static void on_activated() { get_instance(); LibraryWindow lw = AppWindow.get_instance() as LibraryWindow; if ((lw != null) && (!get_is_direct())) { LibraryWindow.get_app().present(); } } /** * @brief Signal handler for GApplication's 'command-line' signal. * * Gets fired whenever a remote instance tries to run, usually * with an incoming camera connection. * * @note This does _not_ get called in direct-edit mode. */ public static int on_command_line(ApplicationCommandLine acl) { string[]? argv = acl.get_arguments(); if (argv != null) { foreach (string s in argv) { LibraryWindow lw = AppWindow.get_instance() as LibraryWindow; if (lw != null) { lw.mounted_camera_shell_notification(s, false); } } } on_activated(); return 0; } /** * @brief Initializes the Shotwell application object and prepares * it for use. * * @param is_direct Whether the application was invoked in direct * or in library mode; defaults to FALSE, that is, library mode. * * @note This MUST be called prior to calling get_instance(), as the * application needs to know what mode it was brought up in; failure to * call this first will lead to an assertion. */ public static void init(bool is_direct = false) { if (instance == null) instance = new Application(is_direct); } public static void terminate() { get_instance().exit(); } public static Application get_instance() { assert (instance != null); return instance; } public void start(string[]? argv = null) { if (running) return; running = true; starting(); assert(AppWindow.get_instance() != null); system_app.add_window(AppWindow.get_instance()); system_app_run_retval = system_app.run(argv); if (!direct) { system_app.command_line.disconnect(on_command_line); } system_app.activate.disconnect(on_activated); system_app.startup.disconnect(on_activated); running = false; } public void exit() { // only fire this once, but thanks to terminate(), it will be fired at least once (even // if start() is not called and "starting" is not fired) if (exiting_fired || !running) return; exiting_fired = true; exiting(false); system_app.release(); } // This will fire the exiting signal with panicked set to true, but only if exit() hasn't // already been called. This call will immediately halt the application. public void panic() { if (!exiting_fired) { exiting_fired = true; exiting(true); } Posix.exit(1); } /** * @brief Allows the caller to ask for some part of the desktop session's functionality to * be prevented from running; wrapper for Gtk.Application.inhibit(). * * @note The return value is a 'cookie' that needs to be passed to 'uninhibit' to turn * off a requested inhibition and should be saved by the caller. */ public uint inhibit(Gtk.ApplicationInhibitFlags what, string? reason="none given") { return system_app.inhibit(AppWindow.get_instance(), what, reason); } /** * @brief Turns off a previously-requested inhibition. Wrapper for * Gtk.Application.uninhibit(). */ public void uninhibit(uint cookie) { system_app.uninhibit(cookie); } public int get_run_return_value() { return system_app_run_retval; } }