summaryrefslogtreecommitdiff
path: root/plugins/authenticator/shotwell
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/authenticator/shotwell')
-rw-r--r--plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala111
-rw-r--r--plugins/authenticator/shotwell/GoogleAuthenticator.vala151
-rw-r--r--plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala4
-rw-r--r--plugins/authenticator/shotwell/TumblrAuthenticator.vala250
-rw-r--r--plugins/authenticator/shotwell/meson.build2
5 files changed, 192 insertions, 326 deletions
diff --git a/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala b/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala
index 23de183..e381ae9 100644
--- a/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala
+++ b/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala
@@ -18,86 +18,26 @@ namespace Publishing.Authenticator.Shotwell.Flickr {
internal const string SERVICE_DISCLAIMER = "<b>This product uses the Flickr API but is not endorsed or certified by SmugMug, Inc.</b>";
internal class AuthenticationRequestTransaction : Publishing.RESTSupport.OAuth1.Transaction {
- public AuthenticationRequestTransaction(Publishing.RESTSupport.OAuth1.Session session) {
+ public AuthenticationRequestTransaction(Publishing.RESTSupport.OAuth1.Session session, string cookie) {
base.with_uri(session, "https://www.flickr.com/services/oauth/request_token",
Publishing.RESTSupport.HttpMethod.GET);
- add_argument("oauth_callback", "shotwell-auth://local-callback");
+ add_argument("oauth_callback", "shotwell-oauth2://localhost?sw_auth_cookie=%s".printf(cookie));
}
}
internal class AccessTokenFetchTransaction : Publishing.RESTSupport.OAuth1.Transaction {
- public AccessTokenFetchTransaction(Publishing.RESTSupport.OAuth1.Session session, string user_verifier) {
+ public AccessTokenFetchTransaction(Publishing.RESTSupport.OAuth1.Session session, string user_verifier, string cookie) {
base.with_uri(session, "https://www.flickr.com/services/oauth/access_token",
Publishing.RESTSupport.HttpMethod.GET);
add_argument("oauth_verifier", user_verifier);
add_argument("oauth_token", session.get_request_phase_token());
- add_argument("oauth_callback", "shotwell-auth://local-callback");
- }
- }
-
- internal class WebAuthenticationPane : Common.WebAuthenticationPane {
- private string? auth_code = null;
- private const string LOGIN_URI = "https://www.flickr.com/services/oauth/authorize?oauth_token=%s&perms=write";
-
- public signal void authorized(string auth_code);
- public signal void error();
-
- public WebAuthenticationPane(string token) {
- Object(login_uri : LOGIN_URI.printf(token));
- }
-
- public override void constructed() {
- base.constructed();
-
- var ctx = WebKit.WebContext.get_default();
- ctx.register_uri_scheme("shotwell-auth", this.on_shotwell_auth_request_cb);
-
- var mgr = ctx.get_security_manager();
- mgr.register_uri_scheme_as_secure("shotwell-auth");
- mgr.register_uri_scheme_as_cors_enabled("shotwell-auth");
- }
-
- public override void on_page_load() {
- if (this.load_error != null) {
- this.error();
-
- return;
- }
-
- try {
- var uri = GLib.Uri.parse(get_view().get_uri(), GLib.UriFlags.NONE);
- if (uri.get_scheme() == "shotwell-auth" && this.auth_code == null) {
- var form_data = Soup.Form.decode (uri.get_query());
- this.auth_code = form_data.lookup("oauth_verifier");
- }
- } catch (Error err) {
- this.error();
-
- return;
- }
-
- if (this.auth_code != null) {
- this.authorized(this.auth_code);
- }
- }
-
- private void on_shotwell_auth_request_cb(WebKit.URISchemeRequest request) {
- try {
- var uri = GLib.Uri.parse(request.get_uri(), GLib.UriFlags.NONE);
- var form_data = Soup.Form.decode (uri.get_query());
- this.auth_code = form_data.lookup("oauth_verifier");
- } catch (Error err) {
- debug ("Failed to parse URI %s: %s", request.get_uri(), err.message);
- }
-
- var response = "";
- var mins = new MemoryInputStream.from_data(response.data);
- request.finish(mins, -1, "text/plain");
+ add_argument("oauth_callback", "shotwell-oauth2://localhost?sw_auth_cookie=%s".printf(cookie));
}
}
internal class Flickr : Publishing.Authenticator.Shotwell.OAuth1.Authenticator {
- private WebAuthenticationPane pane;
+ private Common.ExternalWebPane pane;
+ private string auth_cookie = Uuid.string_random();
public Flickr(Spit.Publishing.PluginHost host) {
base("Flickr", API_KEY, API_SECRET, host);
@@ -147,7 +87,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr {
host.set_service_locked(true);
host.install_static_message_pane(_("Preparing for login…"));
- AuthenticationRequestTransaction txn = new AuthenticationRequestTransaction(session);
+ AuthenticationRequestTransaction txn = new AuthenticationRequestTransaction(session, auth_cookie);
try {
yield txn.execute_async();
debug("EVENT: OAuth authentication request transaction completed; response = '%s'",
@@ -185,22 +125,33 @@ namespace Publishing.Authenticator.Shotwell.Flickr {
session.set_request_phase_credentials(token, token_secret);
- do_web_authentication(token);
+ do_web_authentication.begin(token);
}
- private void do_web_authentication(string token) {
- pane = new WebAuthenticationPane(token);
- host.install_dialog_pane(pane);
- pane.authorized.connect((pin) => { this.do_verify_pin.begin(pin); });
- pane.error.connect(this.on_web_login_error);
- }
+ private class AuthCallback : Spit.Publishing.AuthenticatedCallback, Object {
+ public signal void auth(GLib.HashTable<string, string> params);
- private void on_web_login_error() {
- if (pane.load_error != null) {
- host.post_error(pane.load_error);
- return;
+ public void authenticated(GLib.HashTable<string, string> params) {
+ auth(params);
}
- host.post_error(new Spit.Publishing.PublishingError.PROTOCOL_ERROR(_("Flickr authorization failed")));
+ }
+
+ private async void do_web_authentication(string token) {
+ var uri = "https://www.flickr.com/services/oauth/authorize?oauth_token=%s&perms=write".printf(token);
+ pane = new Common.ExternalWebPane(uri);
+ host.install_dialog_pane(pane);
+ var auth_callback = new AuthCallback();
+ string? web_auth_code = null;
+ auth_callback.auth.connect((prm) => {
+ if ("oauth_verifier" in prm) {
+ web_auth_code = prm["oauth_verifier"];
+ }
+ do_web_authentication.callback();
+ });
+ host.register_auth_callback(auth_cookie, auth_callback);
+ yield;
+ host.unregister_auth_callback(auth_cookie);
+ yield do_verify_pin(web_auth_code);
}
private async void do_verify_pin(string pin) {
@@ -209,7 +160,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr {
host.set_service_locked(true);
host.install_static_message_pane(_("Verifying authorization…"));
- AccessTokenFetchTransaction txn = new AccessTokenFetchTransaction(session, pin);
+ AccessTokenFetchTransaction txn = new AccessTokenFetchTransaction(session, pin, auth_cookie);
try {
yield txn.execute_async();
diff --git a/plugins/authenticator/shotwell/GoogleAuthenticator.vala b/plugins/authenticator/shotwell/GoogleAuthenticator.vala
index 9fc5b27..1fe2448 100644
--- a/plugins/authenticator/shotwell/GoogleAuthenticator.vala
+++ b/plugins/authenticator/shotwell/GoogleAuthenticator.vala
@@ -5,73 +5,11 @@ namespace Publishing.Authenticator.Shotwell.Google {
private const string OAUTH_CLIENT_ID = "534227538559-hvj2e8bj0vfv2f49r7gvjoq6jibfav67.apps.googleusercontent.com";
private const string REVERSE_CLIENT_ID = "com.googleusercontent.apps.534227538559-hvj2e8bj0vfv2f49r7gvjoq6jibfav67";
private const string OAUTH_CLIENT_SECRET = "pwpzZ7W1TCcD5uIfYCu8sM7x";
- private const string OAUTH_CALLBACK_URI = REVERSE_CLIENT_ID + ":/auth-callback";
+ private const string OAUTH_CALLBACK_URI = REVERSE_CLIENT_ID + ":/localhost";
private const string SCHEMA_KEY_PROFILE_ID = "shotwell-profile-id";
private const string SCHEMA_KEY_ACCOUNTNAME = "accountname";
- private class WebAuthenticationPane : Common.WebAuthenticationPane {
- public static bool cache_dirty = false;
- private string? auth_code = null;
-
- public signal void error();
-
- public override void constructed() {
- base.constructed();
-
- var ctx = WebKit.WebContext.get_default();
- ctx.register_uri_scheme(REVERSE_CLIENT_ID, this.on_shotwell_auth_request_cb);
- }
-
- public override void on_page_load() {
- if (this.load_error != null) {
- this.error ();
-
- return;
- }
-
- try {
- var uri = GLib.Uri.parse(get_view().get_uri(), UriFlags.NONE);
- if (uri.get_scheme() == REVERSE_CLIENT_ID && this.auth_code == null) {
- var form_data = Soup.Form.decode (uri.get_query());
- this.auth_code = form_data.lookup("code");
- }
- } catch (Error err) {
- debug ("Failed to parse auth code from URI %s: %s", get_view().get_uri(),
- err.message);
- }
-
- if (this.auth_code != null) {
- this.authorized(this.auth_code);
- }
- }
-
- private void on_shotwell_auth_request_cb(WebKit.URISchemeRequest request) {
- try {
- var uri = GLib.Uri.parse(request.get_uri(), GLib.UriFlags.NONE);
- debug("URI: %s", request.get_uri());
- var form_data = Soup.Form.decode (uri.get_query());
- this.auth_code = form_data.lookup("code");
- } catch (Error err) {
- debug("Failed to parse request URI: %s", err.message);
- }
-
- var response = "";
- var mins = new MemoryInputStream.from_data(response.data);
- request.finish(mins, -1, "text/plain");
- }
-
- public signal void authorized(string auth_code);
-
- public WebAuthenticationPane(string auth_sequence_start_url) {
- Object (login_uri : auth_sequence_start_url);
- }
-
- public static bool is_cache_dirty() {
- return cache_dirty;
- }
- }
-
private class Session : Publishing.RESTSupport.Session {
public string access_token = null;
public string refresh_token = null;
@@ -126,23 +64,22 @@ namespace Publishing.Authenticator.Shotwell.Google {
internal class Google : Spit.Publishing.Authenticator, Object {
private const string PASSWORD_SCHEME = "org.gnome.Shotwell.Google";
- private string scope = null;
+ private string[] scopes = null;
// Prepare for multiple user accounts
private string accountname = "default";
private Spit.Publishing.PluginHost host = null;
private GLib.HashTable<string, Variant> params = null;
- private WebAuthenticationPane web_auth_pane = null;
private Session session = null;
private string welcome_message = null;
private Secret.Schema? schema = null;
- public Google(string scope,
+ public Google(string[] scopes,
string welcome_message,
Spit.Publishing.PluginHost host) {
this.host = host;
this.params = new GLib.HashTable<string, Variant>(str_hash, str_equal);
- this.scope = scope;
+ this.scopes = scopes;
this.session = new Session();
this.welcome_message = welcome_message;
this.schema = new Secret.Schema(PASSWORD_SCHEME, Secret.SchemaFlags.NONE,
@@ -156,7 +93,7 @@ namespace Publishing.Authenticator.Shotwell.Google {
try {
refresh_token = Secret.password_lookup_sync(this.schema, null,
SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(),
- SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope);
+ SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", get_scopes());
} catch (Error err) {
critical("Failed to lookup refresh_token from password store: %s", err.message);
}
@@ -166,14 +103,11 @@ namespace Publishing.Authenticator.Shotwell.Google {
return;
}
- // FIXME: Find a way for a proper logout
- if (WebAuthenticationPane.is_cache_dirty()) {
- host.set_service_locked(false);
+ this.do_show_service_welcome_pane();
+ }
- host.install_static_message_pane(_("You have already logged in and out of a Google service during this Shotwell session.\n\nTo continue publishing to Google services, quit and restart Shotwell, then try publishing again."));
- } else {
- this.do_show_service_welcome_pane();
- }
+ public string get_scopes(string separator=",") {
+ return string.joinv(separator, this.scopes);
}
public bool can_logout() {
@@ -189,9 +123,9 @@ namespace Publishing.Authenticator.Shotwell.Google {
try {
Secret.password_clear_sync(this.schema, null,
SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(),
- SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope);
+ SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", get_scopes());
} catch (Error err) {
- critical("Failed to remove password for scope %s: %s", this.scope, err.message);
+ critical("Failed to remove password for scope %s: %s", get_scopes(), err.message);
}
}
@@ -202,37 +136,54 @@ namespace Publishing.Authenticator.Shotwell.Google {
public void set_accountname(string accountname) {
this.accountname = accountname;
}
+ private class AuthCallback : Spit.Publishing.AuthenticatedCallback, Object {
+ public signal void auth(GLib.HashTable<string, string> params);
+
+ public void authenticated(GLib.HashTable<string, string> params) {
+ auth(params);
+ }
+ }
- private void do_hosted_web_authentication() {
+ private async void do_hosted_web_authentication() {
debug("ACTION: running OAuth authentication flow in hosted web pane.");
string user_authorization_url = "https://accounts.google.com/o/oauth2/auth?" +
"response_type=code&" +
"client_id=" + OAUTH_CLIENT_ID + "&" +
"redirect_uri=" + GLib.Uri.escape_string(OAUTH_CALLBACK_URI, null) + "&" +
- "scope=" + GLib.Uri.escape_string(this.scope, null) + "+" +
+ "scope=" + GLib.Uri.escape_string(get_scopes(" "), null) + "+" +
GLib.Uri.escape_string("https://www.googleapis.com/auth/userinfo.profile", null) + "&" +
"state=connect&" +
"access_type=offline&" +
"approval_prompt=force";
- web_auth_pane = new WebAuthenticationPane(user_authorization_url);
- web_auth_pane.authorized.connect(on_web_auth_pane_authorized);
- web_auth_pane.error.connect(on_web_auth_pane_error);
-
- host.install_dialog_pane(web_auth_pane);
- }
-
- private void on_web_auth_pane_authorized(string auth_code) {
- web_auth_pane.authorized.disconnect(on_web_auth_pane_authorized);
+ var auth_callback = new AuthCallback();
+ string? web_auth_code = null;
- debug("EVENT: user authorized scope %s with auth_code %s", scope, auth_code);
+ auth_callback.auth.connect((prm) => {
+ if ("code" in prm) {
+ web_auth_code = prm["code"];
+ }
+ if ("scope" in prm) {
+ debug("Effective scopes as returned from login: %s", prm["scope"]);
+ }
+ do_hosted_web_authentication.callback();
+ });
+ host.register_auth_callback(REVERSE_CLIENT_ID, auth_callback);
+ try {
+ debug("Launching external authentication on URI %s", user_authorization_url);
+ AppInfo.launch_default_for_uri(user_authorization_url, null);
+ host.install_login_wait_pane();
+ yield;
- do_get_access_tokens.begin(auth_code);
- }
+ // FIXME throw error missing scopes
- private void on_web_auth_pane_error() {
- host.post_error(web_auth_pane.load_error);
+ yield do_get_access_tokens(web_auth_code);
+ } catch (Error err) {
+ host.post_error(err);
+ } finally {
+ host.unregister_auth_callback(REVERSE_CLIENT_ID);
+ }
}
private async void do_get_access_tokens(string auth_code) {
@@ -375,16 +326,15 @@ namespace Publishing.Authenticator.Shotwell.Google {
assert(session.is_authenticated());
try {
Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT,
- "Shotwell publishing (Google account scope %s@%s)".printf(this.accountname, this.scope),
+ "Shotwell publishing (Google account scope %s@%s)".printf(this.accountname, get_scopes()),
session.refresh_token, null,
SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(),
- SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope);
+ SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", get_scopes());
} catch (Error err) {
- critical("Failed to look up password for scope %s: %s", this.scope, err.message);
+ critical("Failed to look up password for scope %s: %s", get_scopes(), err.message);
}
this.authenticated();
- web_auth_pane.clear();
}
private async void do_exchange_refresh_token_for_access_token() {
@@ -413,15 +363,14 @@ namespace Publishing.Authenticator.Shotwell.Google {
try {
Secret.password_clear_sync(this.schema, null,
SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(),
- SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope);
+ SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", get_scopes());
} catch (Error err) {
- critical("Failed to remove password for accountname@scope %s@%s: %s", this.accountname, this.scope, err.message);
+ critical("Failed to remove password for accountname@scope %s@%s: %s", this.accountname, get_scopes(), err.message);
}
Idle.add (() => { this.authenticate(); return false; });
}
- web_auth_pane.clear();
host.post_error(err);
}
}
@@ -435,7 +384,7 @@ namespace Publishing.Authenticator.Shotwell.Google {
private void on_service_welcome_login() {
debug("EVENT: user clicked 'Login' in welcome pane.");
- this.do_hosted_web_authentication();
+ this.do_hosted_web_authentication.begin();
}
}
}
diff --git a/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala b/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala
index 01fa3c3..c289006 100644
--- a/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala
+++ b/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala
@@ -27,11 +27,11 @@ namespace Publishing.Authenticator {
case "flickr":
return new Shotwell.Flickr.Flickr(host);
case "youtube":
- return new Shotwell.Google.Google("https://www.googleapis.com/auth/youtube", _("You are not currently logged into YouTube.\n\nYou must have already signed up for a Google account and set it up for use with YouTube to continue. You can set up most accounts by using your browser to log into the YouTube site at least once.\n\nShotwell uses the YouTube API services <a href=\"https://developers.google.com/youtube\">https://developers.google.com/youtube</a> for accessing your YouTube channel and upload the videos. By using Shotwell to access YouTube, you agree to be bound to the YouTube Terms of Service as available at <a href=\"https://www.youtube.com/t/terms\">https://www.youtube.com/t/terms</a>\n\nShotwell's privacy policy regarding the use of data related to your Google account in general and YouTube in particular can be found in our <a href=\"help:shotwell/privacy-policy\">online services privacy policy</a>\n\nFor Google's own privacy policy, please refer to <a href=\"https://policies.google.com/privacy\">https://policies.google.com/privacy</a>"), host);
+ return new Shotwell.Google.Google({"https://www.googleapis.com/auth/youtube"}, _("You are not currently logged into YouTube.\n\nYou must have already signed up for a Google account and set it up for use with YouTube to continue. You can set up most accounts by using your browser to log into the YouTube site at least once.\n\nShotwell uses the YouTube API services <a href=\"https://developers.google.com/youtube\">https://developers.google.com/youtube</a> for accessing your YouTube channel and upload the videos. By using Shotwell to access YouTube, you agree to be bound to the YouTube Terms of Service as available at <a href=\"https://www.youtube.com/t/terms\">https://www.youtube.com/t/terms</a>\n\nShotwell's privacy policy regarding the use of data related to your Google account in general and YouTube in particular can be found in our <a href=\"help:shotwell/privacy-policy\">online services privacy policy</a>\n\nFor Google's own privacy policy, please refer to <a href=\"https://policies.google.com/privacy\">https://policies.google.com/privacy</a>"), host);
case "tumblr":
return new Shotwell.Tumblr.Tumblr(host);
case "google-photos":
- return new Shotwell.Google.Google("https://www.googleapis.com/auth/photoslibrary", _("You are not currently logged into Google Photos.\n\nYou must have already signed up for a Google account and set it up for use with Google Photos. Shotwell uses the Google Photos API services <a href=\"https://developers.google.com/photos/\">https://developers.google.com/photos/</a> for all interaction with your Google Photos data. You will have to grant access Shotwell to your Google Photos library.\n\nShotwell's privacy policy regarding the use of data related to your Google account in general and Google Photos in particular can be found in our <a href=\"help:shotwell/privacy-policy\">online services privacy policy</a>. For Google's own privacy policy, please refer to <a href=\"https://policies.google.com/privacy\">https://policies.google.com/privacy</a>"), host);
+ return new Shotwell.Google.Google({"https://www.googleapis.com/auth/photoslibrary.appendonly", "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata"}, _("You are not currently logged into Google Photos.\n\nYou must have already signed up for a Google account and set it up for use with Google Photos. Shotwell uses the Google Photos API services <a href=\"https://developers.google.com/photos/\">https://developers.google.com/photos/</a> for all interaction with your Google Photos data. You will have to grant access Shotwell to your Google Photos library.\n\nShotwell's privacy policy regarding the use of data related to your Google account in general and Google Photos in particular can be found in our <a href=\"help:shotwell/privacy-policy\">online services privacy policy</a>. For Google's own privacy policy, please refer to <a href=\"https://policies.google.com/privacy\">https://policies.google.com/privacy</a>"), host);
default:
return null;
}
diff --git a/plugins/authenticator/shotwell/TumblrAuthenticator.vala b/plugins/authenticator/shotwell/TumblrAuthenticator.vala
index e77814b..3ad6b9f 100644
--- a/plugins/authenticator/shotwell/TumblrAuthenticator.vala
+++ b/plugins/authenticator/shotwell/TumblrAuthenticator.vala
@@ -7,125 +7,35 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
+using Shotwell.Plugins;
+
namespace Publishing.Authenticator.Shotwell.Tumblr {
internal const string ENDPOINT_URL = "https://www.tumblr.com/";
internal const string API_KEY = "NdXvXQuKVccOsCOj0H4k9HUJcbcjDBYSo2AkaHzXFECHGNuP9k";
internal const string API_SECRET = "BN0Uoig0MwbeD27OgA0IwYlp3Uvonyfsrl9pf1cnnMj1QoEUvi";
- internal const string ENCODE_RFC_3986_EXTRA = "!*'();:@&=+$,/?%#[] \\";
-
- /**
- * The authentication pane used when asking service URL, user name and password
- * from the user.
- */
- internal class AuthenticationPane : Spit.Publishing.DialogPane, Object {
- public enum Mode {
- INTRO,
- FAILED_RETRY_USER
- }
- private static string INTRO_MESSAGE = _("Enter the username and password associated with your Tumblr account.");
- private static string FAILED_RETRY_USER_MESSAGE = _("Username and/or password invalid. Please try again");
-
- private Gtk.Box pane_widget = null;
- private Gtk.Builder builder;
- private Gtk.Entry username_entry;
- private Gtk.Entry password_entry;
- private Gtk.Button login_button;
-
- public signal void login(string user, string password);
-
- public AuthenticationPane(Mode mode = Mode.INTRO) {
- this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
-
- try {
- builder = new Gtk.Builder();
- builder.add_from_resource (Resources.RESOURCE_PATH + "/tumblr_authentication_pane.ui");
- builder.connect_signals(null);
- var content = builder.get_object ("content") as Gtk.Widget;
-
- Gtk.Label message_label = builder.get_object("message_label") as Gtk.Label;
- switch (mode) {
- case Mode.INTRO:
- message_label.set_text(INTRO_MESSAGE);
- break;
-
- case Mode.FAILED_RETRY_USER:
- message_label.set_markup("<b>%s</b>\n\n%s".printf(_(
- "Invalid User Name or Password"), FAILED_RETRY_USER_MESSAGE));
- break;
- }
-
- username_entry = builder.get_object ("username_entry") as Gtk.Entry;
-
- password_entry = builder.get_object ("password_entry") as Gtk.Entry;
-
-
-
- login_button = builder.get_object("login_button") as Gtk.Button;
-
- username_entry.changed.connect(on_user_changed);
- password_entry.changed.connect(on_password_changed);
- login_button.clicked.connect(on_login_button_clicked);
-
- content.parent.remove (content);
- pane_widget.add (content);
- } catch (Error e) {
- warning(_("Could not load UI: %s"), e.message);
- }
- }
-
- public Gtk.Widget get_default_widget() {
- return login_button;
- }
-
- private void on_login_button_clicked() {
- login(username_entry.get_text(),
- password_entry.get_text());
- }
-
-
- private void on_user_changed() {
- update_login_button_sensitivity();
- }
-
- private void on_password_changed() {
- update_login_button_sensitivity();
- }
+ internal const string SERVICE_WELCOME_MESSAGE = _("You are not currently logged into Tumblr.\n\nClick Log in to log into Tumblr in your Web browser. You will have to authorize Shotwell Connect to link to your Tumblr account.");
- private void update_login_button_sensitivity() {
- login_button.set_sensitive(username_entry.text_length > 0 &&
- password_entry.text_length > 0);
- }
-
- public Gtk.Widget get_widget() {
- return pane_widget;
- }
-
- public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() {
- return Spit.Publishing.DialogPane.GeometryOptions.NONE;
- }
-
- public void on_pane_installed() {
- username_entry.grab_focus();
- password_entry.set_activates_default(true);
- login_button.can_default = true;
- update_login_button_sensitivity();
- }
-
- public void on_pane_uninstalled() {
+ internal class AuthenticationRequestTransaction : Publishing.RESTSupport.OAuth1.Transaction {
+ public AuthenticationRequestTransaction(Publishing.RESTSupport.OAuth1.Session session, string cookie) {
+ base.with_uri(session, "https://www.tumblr.com/oauth/request_token",
+ Publishing.RESTSupport.HttpMethod.POST);
+ add_argument("oauth_callback", "shotwell-oauth2://localhost?sw_auth_cookie=%s".printf(cookie));
}
}
internal class AccessTokenFetchTransaction : Publishing.RESTSupport.OAuth1.Transaction {
- public AccessTokenFetchTransaction(Publishing.RESTSupport.OAuth1.Session session, string username, string password) {
+ public AccessTokenFetchTransaction(Publishing.RESTSupport.OAuth1.Session session, string user_verifier, string cookie) {
base.with_uri(session, "https://www.tumblr.com/oauth/access_token",
Publishing.RESTSupport.HttpMethod.POST);
- add_argument("x_auth_username", username);
- add_argument("x_auth_password", password);
- add_argument("x_auth_mode", "client_auth");
+ add_argument("oauth_verifier", user_verifier);
+ add_argument("oauth_token", session.get_request_phase_token());
+ add_argument("oauth_callback", "shotwell-oauth2://localhost?sw_auth_cookie=%s".printf(cookie));
}
}
internal class Tumblr : Publishing.Authenticator.Shotwell.OAuth1.Authenticator {
+ private string auth_cookie = Uuid.string_random();
+
public Tumblr(Spit.Publishing.PluginHost host) {
base("Tumblr", API_KEY, API_SECRET, host);
}
@@ -167,79 +77,135 @@ namespace Publishing.Authenticator.Shotwell.Tumblr {
*
* @param mode the mode for the authentication pane
*/
- private void do_show_authentication_pane(AuthenticationPane.Mode mode = AuthenticationPane.Mode.INTRO) {
+ private void do_show_authentication_pane() {
debug("ACTION: installing authentication pane");
host.set_service_locked(false);
- AuthenticationPane authentication_pane = new AuthenticationPane(mode);
- authentication_pane.login.connect(on_authentication_pane_login_clicked);
- host.install_dialog_pane(authentication_pane, Spit.Publishing.PluginHost.ButtonMode.CLOSE);
- host.set_dialog_default_widget(authentication_pane.get_default_widget());
+ host.install_welcome_pane("%s".printf(SERVICE_WELCOME_MESSAGE), on_welcome_pane_login_clicked);
}
- /**
- * Event triggered when the login button in the authentication panel is
- * clicked.
- *
- * This event is triggered when the login button in the authentication
- * panel is clicked. It then triggers a network login interaction.
- *
- * @param username the name of the Tumblr user as entered in the dialog
- * @param password the password of the Tumblr as entered in the dialog
- */
- private void on_authentication_pane_login_clicked( string username, string password ) {
- debug("EVENT: on_authentication_pane_login_clicked");
+ private void on_welcome_pane_login_clicked() {
+ debug("EVENT: user clicked 'Login' button in the welcome pane");
- do_network_login.begin(username, password);
+ do_run_authentication_request_transaction.begin();
}
- /**
- * Action to perform a network login to a Tumblr blog.
- *
- * This action performs a network login a Tumblr blog specified the given user name and password as credentials.
- *
- * @param username the name of the Tumblr user used to login
- * @param password the password of the Tumblr user used to login
- */
- private async void do_network_login(string username, string password) {
- debug("ACTION: logging in");
+ private async void do_run_authentication_request_transaction() {
+ debug("ACTION: running authentication request transaction");
+
host.set_service_locked(true);
- host.install_login_wait_pane();
+ host.install_static_message_pane(_("Preparing for login…"));
- AccessTokenFetchTransaction txn = new AccessTokenFetchTransaction(session,username,password);
+ AuthenticationRequestTransaction txn = new AuthenticationRequestTransaction(session, auth_cookie);
try {
yield txn.execute_async();
debug("EVENT: OAuth authentication request transaction completed; response = '%s'",
- txn.get_response());
-
+ txn.get_response());
do_parse_token_info_from_auth_request(txn.get_response());
} catch (Error err) {
debug("EVENT: OAuth authentication request transaction caused a network error");
host.post_error(err);
+
+ this.authentication_failed();
}
}
private void do_parse_token_info_from_auth_request(string response) {
+ debug("ACTION: parsing authorization request response '%s' into token and secret", response);
+
+ string? oauth_token = null;
+ string? oauth_token_secret = null;
+
+ var data = Soup.Form.decode(response);
+ data.lookup_extended("oauth_token", null, out oauth_token);
+ data.lookup_extended("oauth_token_secret", null, out oauth_token_secret);
+
+ if (oauth_token == null || oauth_token_secret == null)
+ host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(
+ "'%s' isn't a valid response to an OAuth authentication request", response));
+
+
+ on_authentication_token_available(oauth_token, oauth_token_secret);
+ }
+
+ private void on_authentication_token_available(string token, string token_secret) {
+ debug("EVENT: OAuth authentication token (%s) and token secret (%s) available",
+ token, token_secret);
+
+ session.set_request_phase_credentials(token, token_secret);
+
+ do_web_authentication.begin(token);
+ }
+ private class AuthCallback : Spit.Publishing.AuthenticatedCallback, Object {
+ public signal void auth(GLib.HashTable<string, string> params);
+
+ public void authenticated(GLib.HashTable<string, string> params) {
+ auth(params);
+ }
+ }
+
+ private async void do_web_authentication(string token) {
+ var uri = "https://www.tumblr.com/oauth/authorize?oauth_token=%s&perms=write".printf(token);
+ var pane = new Common.ExternalWebPane(uri);
+ host.install_dialog_pane(pane);
+ var auth_callback = new AuthCallback();
+ string? web_auth_code = null;
+ auth_callback.auth.connect((prm) => {
+ if ("oauth_verifier" in prm) {
+ web_auth_code = prm["oauth_verifier"];
+ }
+ do_web_authentication.callback();
+ });
+ host.register_auth_callback(auth_cookie, auth_callback);
+ yield;
+ host.unregister_auth_callback(auth_cookie);
+ yield do_verify_pin(web_auth_code);
+ }
+
+ private async void do_verify_pin(string pin) {
+ debug("ACTION: validating authorization PIN %s", pin);
+
+ host.set_service_locked(true);
+ host.install_static_message_pane(_("Verifying authorization…"));
+
+ AccessTokenFetchTransaction txn = new AccessTokenFetchTransaction(session, pin, auth_cookie);
+
+ try {
+ yield txn.execute_async();
+ debug("EVENT: fetching OAuth access token over the network succeeded");
+
+ do_extract_access_phase_credentials_from_response(txn.get_response());
+ } catch (Error err) {
+ debug("EVENT: fetching OAuth access token over the network caused an error.");
+
+ host.post_error(err);
+ this.authentication_failed();
+ }
+ }
+
+ private void do_extract_access_phase_credentials_from_response(string response) {
debug("ACTION: extracting access phase credentials from '%s'", response);
string? token = null;
string? token_secret = null;
+ string? username = "unused";
var data = Soup.Form.decode(response);
data.lookup_extended("oauth_token", null, out token);
data.lookup_extended("oauth_token_secret", null, out token_secret);
- debug("access phase credentials: { token = '%s'; token_secret = '%s' }",
- token, token_secret);
+ debug("access phase credentials: { token = '%s'; token_secret = '%s'; username = '%s' }",
+ token, token_secret, username);
- if (token == null || token_secret == null) {
- host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE("Expected " +
- "access phase credentials to contain token and token secret but at " +
+ if (token == null || token_secret == null || username == null) {
+ host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE("expected " +
+ "access phase credentials to contain token, token secret, and username but at " +
"least one of these is absent"));
this.authentication_failed();
} else {
- session.set_access_phase_credentials(token, token_secret, "unused");
+ session.set_access_phase_credentials(token, token_secret, username);
}
}
+
}
}
diff --git a/plugins/authenticator/shotwell/meson.build b/plugins/authenticator/shotwell/meson.build
index 037ec3b..a6475e0 100644
--- a/plugins/authenticator/shotwell/meson.build
+++ b/plugins/authenticator/shotwell/meson.build
@@ -11,7 +11,7 @@ authenticator_shotwell_resources = gnome.compile_resources('authenticator-resour
source_dir : meson.project_source_root())
authenticator_shotwell_deps = [gee, gtk, gio, soup, json_glib, sw_plugin,
- sw_plugin_common_dep, json_glib, xml, webkit, secret]
+ sw_plugin_common_dep, json_glib, xml, secret]
authenticator = library('shotwell-authenticator',
authenticator_shotwell_sources + authenticator_shotwell_resources,