From d443a3c2509889533ca812c163056bace396b586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= Date: Wed, 14 Jun 2023 20:35:58 +0200 Subject: New upstream version 0.32.1 --- .../shotwell/FacebookPublishingAuthenticator.vala | 318 --------------------- .../shotwell/FlickrPublishingAuthenticator.vala | 106 +++---- .../shotwell/GoogleAuthenticator.vala | 219 +++++++------- .../shotwell/OAuth1Authenticator.vala | 119 ++++++-- .../shotwell/ShotwellAuthenticatorFactory.vala | 4 +- .../shotwell/TumblrAuthenticator.vala | 41 +-- plugins/authenticator/shotwell/meson.build | 5 +- 7 files changed, 267 insertions(+), 545 deletions(-) delete mode 100644 plugins/authenticator/shotwell/FacebookPublishingAuthenticator.vala (limited to 'plugins/authenticator') diff --git a/plugins/authenticator/shotwell/FacebookPublishingAuthenticator.vala b/plugins/authenticator/shotwell/FacebookPublishingAuthenticator.vala deleted file mode 100644 index 26a2363..0000000 --- a/plugins/authenticator/shotwell/FacebookPublishingAuthenticator.vala +++ /dev/null @@ -1,318 +0,0 @@ -/* 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. - */ - -using Shotwell; -using Shotwell.Plugins; - -namespace Publishing.Authenticator.Shotwell.Facebook { - private const string APPLICATION_ID = "1612018629063184"; - - private class WebAuthenticationPane : Common.WebAuthenticationPane { - private static bool cache_dirty = false; - - public signal void login_succeeded(string success_url); - public signal void login_failed(); - - public WebAuthenticationPane() { - Object (login_uri : get_login_url ()); - } - - private class LocaleLookup { - public string prefix; - public string translation; - public string? exception_code; - public string? exception_translation; - public string? exception_code_2; - public string? exception_translation_2; - - public LocaleLookup(string prefix, string translation, string? exception_code = null, - string? exception_translation = null, string? exception_code_2 = null, - string? exception_translation_2 = null) { - this.prefix = prefix; - this.translation = translation; - this.exception_code = exception_code; - this.exception_translation = exception_translation; - this.exception_code_2 = exception_code_2; - this.exception_translation_2 = exception_translation_2; - } - - } - - private static LocaleLookup[] locale_lookup_table = { - new LocaleLookup( "es", "es-la", "ES", "es-es" ), - new LocaleLookup( "en", "en-gb", "US", "en-us" ), - new LocaleLookup( "fr", "fr-fr", "CA", "fr-ca" ), - new LocaleLookup( "pt", "pt-br", "PT", "pt-pt" ), - new LocaleLookup( "zh", "zh-cn", "HK", "zh-hk", "TW", "zh-tw" ), - new LocaleLookup( "af", "af-za" ), - new LocaleLookup( "ar", "ar-ar" ), - new LocaleLookup( "nb", "nb-no" ), - new LocaleLookup( "no", "nb-no" ), - new LocaleLookup( "id", "id-id" ), - new LocaleLookup( "ms", "ms-my" ), - new LocaleLookup( "ca", "ca-es" ), - new LocaleLookup( "cs", "cs-cz" ), - new LocaleLookup( "cy", "cy-gb" ), - new LocaleLookup( "da", "da-dk" ), - new LocaleLookup( "de", "de-de" ), - new LocaleLookup( "tl", "tl-ph" ), - new LocaleLookup( "ko", "ko-kr" ), - new LocaleLookup( "hr", "hr-hr" ), - new LocaleLookup( "it", "it-it" ), - new LocaleLookup( "lt", "lt-lt" ), - new LocaleLookup( "hu", "hu-hu" ), - new LocaleLookup( "nl", "nl-nl" ), - new LocaleLookup( "ja", "ja-jp" ), - new LocaleLookup( "nb", "nb-no" ), - new LocaleLookup( "no", "nb-no" ), - new LocaleLookup( "pl", "pl-pl" ), - new LocaleLookup( "ro", "ro-ro" ), - new LocaleLookup( "ru", "ru-ru" ), - new LocaleLookup( "sk", "sk-sk" ), - new LocaleLookup( "sl", "sl-si" ), - new LocaleLookup( "sv", "sv-se" ), - new LocaleLookup( "th", "th-th" ), - new LocaleLookup( "vi", "vi-vn" ), - new LocaleLookup( "tr", "tr-tr" ), - new LocaleLookup( "el", "el-gr" ), - new LocaleLookup( "bg", "bg-bg" ), - new LocaleLookup( "sr", "sr-rs" ), - new LocaleLookup( "he", "he-il" ), - new LocaleLookup( "hi", "hi-in" ), - new LocaleLookup( "bn", "bn-in" ), - new LocaleLookup( "pa", "pa-in" ), - new LocaleLookup( "ta", "ta-in" ), - new LocaleLookup( "te", "te-in" ), - new LocaleLookup( "ml", "ml-in" ) - }; - - private static string get_system_locale_as_facebook_locale() { - unowned string? raw_system_locale = Intl.setlocale(LocaleCategory.ALL, ""); - if (raw_system_locale == null || raw_system_locale == "") - return "www"; - - string system_locale = raw_system_locale.split(".")[0]; - - foreach (LocaleLookup locale_lookup in locale_lookup_table) { - if (!system_locale.has_prefix(locale_lookup.prefix)) - continue; - - if (locale_lookup.exception_code != null) { - assert(locale_lookup.exception_translation != null); - - if (system_locale.contains(locale_lookup.exception_code)) - return locale_lookup.exception_translation; - } - - if (locale_lookup.exception_code_2 != null) { - assert(locale_lookup.exception_translation_2 != null); - - if (system_locale.contains(locale_lookup.exception_code_2)) - return locale_lookup.exception_translation_2; - } - - return locale_lookup.translation; - } - - // default - return "www"; - } - - private static string get_login_url() { - var facebook_locale = get_system_locale_as_facebook_locale(); - - return "https://%s.facebook.com/dialog/oauth?client_id=%s&redirect_uri=https://www.facebook.com/connect/login_success.html&display=popup&scope=publish_actions,user_photos,user_videos&response_type=token".printf(facebook_locale, APPLICATION_ID); - } - - public override void on_page_load() { - string loaded_url = get_view ().uri.dup(); - debug("loaded url: " + loaded_url); - - // strip parameters from the loaded url - if (loaded_url.contains("?")) { - int index = loaded_url.index_of_char('?'); - string params = loaded_url[index:loaded_url.length]; - loaded_url = loaded_url.replace(params, ""); - } - - // were we redirected to the facebook login success page? - if (loaded_url.contains("login_success")) { - cache_dirty = true; - login_succeeded(get_view ().uri); - return; - } - - // were we redirected to the login total failure page? - if (loaded_url.contains("login_failure")) { - login_failed(); - return; - } - } - - public static bool is_cache_dirty() { - return cache_dirty; - } - } - - internal class Facebook : Spit.Publishing.Authenticator, GLib.Object { - private Spit.Publishing.PluginHost host; - private Publishing.Authenticator.Shotwell.Facebook.WebAuthenticationPane web_auth_pane = null; - private GLib.HashTable params; - - private const string SERVICE_WELCOME_MESSAGE = - _("You are not currently logged into Facebook.\n\nIf you don’t yet have a Facebook account, you can create one during the login process. During login, Shotwell Connect may ask you for permission to upload photos and publish to your feed. These permissions are required for Shotwell Connect to function."); - private const string RESTART_ERROR_MESSAGE = - _("You have already logged in and out of Facebook during this Shotwell session.\nTo continue publishing to Facebook, quit and restart Shotwell, then try publishing again."); - - /* Interface functions */ - public Facebook(Spit.Publishing.PluginHost host) { - this.host = host; - this.params = new GLib.HashTable(str_hash, str_equal); - } - - public void authenticate() { - // Do we have saved user credentials? If so, go ahead and authenticate the session - // with the saved credentials and proceed with the publishing interaction. Otherwise, show - // the Welcome pane - if (is_persistent_session_valid()) { - var access_token = get_persistent_access_token(); - this.params.insert("AccessToken", new Variant.string(access_token)); - this.authenticated(); - return; - } - - // FIXME: Find a way for a proper logout - if (WebAuthenticationPane.is_cache_dirty()) { - host.set_service_locked(false); - host.install_static_message_pane(RESTART_ERROR_MESSAGE, - Spit.Publishing.PluginHost.ButtonMode.CANCEL); - } else { - this.do_show_service_welcome_pane(); - } - } - - public bool can_logout() { - return true; - } - - public GLib.HashTable get_authentication_parameter() { - return this.params; - } - - public void invalidate_persistent_session() { - debug("invalidating saved Facebook session."); - set_persistent_access_token(""); - } - - public void logout() { - invalidate_persistent_session(); - } - - public void refresh() { - // No-Op with Flickr - } - - /* Private functions */ - private bool is_persistent_session_valid() { - string? token = get_persistent_access_token(); - - if (token != null) - debug("existing Facebook session found in configuration database (access_token = %s).", - token); - else - debug("no existing Facebook session available."); - - return token != null; - } - - private string? get_persistent_access_token() { - return host.get_config_string("access_token", null); - } - - private void set_persistent_access_token(string access_token) { - host.set_config_string("access_token", access_token); - } - - private void do_show_service_welcome_pane() { - debug("ACTION: showing service welcome pane."); - - host.install_welcome_pane(SERVICE_WELCOME_MESSAGE, on_login_clicked); - host.set_service_locked(false); - } - - private void on_login_clicked() { - debug("EVENT: user clicked 'Login' on welcome pane."); - - do_hosted_web_authentication(); - } - - private void do_hosted_web_authentication() { - debug("ACTION: doing hosted web authentication."); - - this.host.set_service_locked(false); - - this.web_auth_pane = new WebAuthenticationPane(); - this.web_auth_pane.login_succeeded.connect(on_web_auth_pane_login_succeeded); - this.web_auth_pane.login_failed.connect(on_web_auth_pane_login_failed); - - this.host.install_dialog_pane(this.web_auth_pane, - Spit.Publishing.PluginHost.ButtonMode.CANCEL); - - } - - private void on_web_auth_pane_login_succeeded(string success_url) { - debug("EVENT: hosted web login succeeded."); - - do_authenticate_session(success_url); - } - - private void on_web_auth_pane_login_failed() { - debug("EVENT: hosted web login failed."); - - // In this case, "failed" doesn't mean that the user didn't enter the right username and - // password -- Facebook handles that case inside the Facebook Connect web control. Instead, - // it means that no session was initiated in response to our login request. The only - // way this happens is if the user clicks the "Cancel" button that appears inside - // the web control. In this case, the correct behavior is to return the user to the - // service welcome pane so that they can start the web interaction again. - do_show_service_welcome_pane(); - } - - private void do_authenticate_session(string good_login_uri) { - debug("ACTION: preparing to extract session information encoded in uri = '%s'", - good_login_uri); - - // the raw uri is percent-encoded, so decode it - string decoded_uri = Soup.URI.decode(good_login_uri); - - // locate the access token within the URI - string? access_token = null; - int index = decoded_uri.index_of("#access_token="); - if (index >= 0) - access_token = decoded_uri[index:decoded_uri.length]; - if (access_token == null) { - host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( - "Server redirect URL contained no access token")); - return; - } - - // remove any trailing parameters from the session description string - string? trailing_params = null; - index = access_token.index_of_char('&'); - if (index >= 0) - trailing_params = access_token[index:access_token.length]; - if (trailing_params != null) - access_token = access_token.replace(trailing_params, ""); - - // remove the key from the session description string - access_token = access_token.replace("#access_token=", ""); - this.params.insert("AccessToken", new Variant.string(access_token)); - set_persistent_access_token(access_token); - - this.authenticated(); - } - } -} // namespace Publishing.Facebook; diff --git a/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala b/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala index 82448e2..26af6d1 100644 --- a/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala +++ b/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala @@ -21,7 +21,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr { public AuthenticationRequestTransaction(Publishing.RESTSupport.OAuth1.Session session) { base.with_uri(session, "https://www.flickr.com/services/oauth/request_token", Publishing.RESTSupport.HttpMethod.GET); - add_argument("oauth_callback", "shotwell-auth%3A%2F%2Flocal-callback"); + add_argument("oauth_callback", "shotwell-auth://local-callback"); } } @@ -31,7 +31,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr { 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%3A%2F%2Flocal-callback"); + add_argument("oauth_callback", "shotwell-auth://local-callback"); } } @@ -64,10 +64,16 @@ namespace Publishing.Authenticator.Shotwell.Flickr { return; } - var uri = new Soup.URI(get_view().get_uri()); - if (uri.scheme == "shotwell-auth" && this.auth_code == null) { - var form_data = Soup.Form.decode (uri.query); - this.auth_code = form_data.lookup("oauth_verifier"); + 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) { @@ -76,9 +82,13 @@ namespace Publishing.Authenticator.Shotwell.Flickr { } private void on_shotwell_auth_request_cb(WebKit.URISchemeRequest request) { - var uri = new Soup.URI(request.get_uri()); - var form_data = Soup.Form.decode (uri.query); - this.auth_code = form_data.lookup("oauth_verifier"); + 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, null); @@ -90,7 +100,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr { private WebAuthenticationPane pane; public Flickr(Spit.Publishing.PluginHost host) { - base(API_KEY, API_SECRET, host); + base("Flickr", API_KEY, API_SECRET, host); } public override void authenticate() { @@ -128,45 +138,27 @@ namespace Publishing.Authenticator.Shotwell.Flickr { private void on_welcome_pane_login_clicked() { debug("EVENT: user clicked 'Login' button in the welcome pane"); - do_run_authentication_request_transaction(); + do_run_authentication_request_transaction.begin(); } - private void do_run_authentication_request_transaction() { + private async void do_run_authentication_request_transaction() { debug("ACTION: running authentication request transaction"); host.set_service_locked(true); host.install_static_message_pane(_("Preparing for login…")); AuthenticationRequestTransaction txn = new AuthenticationRequestTransaction(session); - txn.completed.connect(on_auth_request_txn_completed); - txn.network_error.connect(on_auth_request_txn_error); - try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield txn.execute_async(); + debug("EVENT: OAuth authentication request transaction completed; response = '%s'", + 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); - } - } - - private void on_auth_request_txn_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_auth_request_txn_completed); - txn.network_error.disconnect(on_auth_request_txn_error); - - debug("EVENT: OAuth authentication request transaction completed; response = '%s'", - txn.get_response()); - - do_parse_token_info_from_auth_request(txn.get_response()); - } - private void on_auth_request_txn_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_auth_request_txn_completed); - txn.network_error.disconnect(on_auth_request_txn_error); - - debug("EVENT: OAuth authentication request transaction caused a network error"); - host.post_error(err); - - this.authentication_failed(); + this.authentication_failed(); + } } private void do_parse_token_info_from_auth_request(string response) { @@ -199,7 +191,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr { private void do_web_authentication(string token) { pane = new WebAuthenticationPane(token); host.install_dialog_pane(pane); - pane.authorized.connect(this.do_verify_pin); + pane.authorized.connect((pin) => { this.do_verify_pin.begin(pin); }); pane.error.connect(this.on_web_login_error); } @@ -211,44 +203,28 @@ namespace Publishing.Authenticator.Shotwell.Flickr { host.post_error(new Spit.Publishing.PublishingError.PROTOCOL_ERROR(_("Flickr authorization failed"))); } - private void do_verify_pin(string pin) { + 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); - txn.completed.connect(on_access_token_fetch_txn_completed); - txn.network_error.connect(on_access_token_fetch_error); try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - } - } - - private void on_access_token_fetch_txn_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_access_token_fetch_txn_completed); - txn.network_error.disconnect(on_access_token_fetch_error); + yield txn.execute_async(); + debug("EVENT: fetching OAuth access token over the network succeeded"); - 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."); - do_extract_access_phase_credentials_from_reponse(txn.get_response()); - } - - private void on_access_token_fetch_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_access_token_fetch_txn_completed); - txn.network_error.disconnect(on_access_token_fetch_error); - - debug("EVENT: fetching OAuth access token over the network caused an error."); - - host.post_error(err); - this.authentication_failed(); + host.post_error(err); + this.authentication_failed(); + } } - private void do_extract_access_phase_credentials_from_reponse(string response) { + private void do_extract_access_phase_credentials_from_response(string response) { debug("ACTION: extracting access phase credentials from '%s'", response); string? token = null; diff --git a/plugins/authenticator/shotwell/GoogleAuthenticator.vala b/plugins/authenticator/shotwell/GoogleAuthenticator.vala index a607cd0..3276e67 100644 --- a/plugins/authenticator/shotwell/GoogleAuthenticator.vala +++ b/plugins/authenticator/shotwell/GoogleAuthenticator.vala @@ -7,6 +7,9 @@ namespace Publishing.Authenticator.Shotwell.Google { private const string OAUTH_CLIENT_SECRET = "pwpzZ7W1TCcD5uIfYCu8sM7x"; private const string OAUTH_CALLBACK_URI = REVERSE_CLIENT_ID + ":/auth-callback"; + 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; @@ -27,10 +30,15 @@ namespace Publishing.Authenticator.Shotwell.Google { return; } - var uri = new Soup.URI(get_view().get_uri()); - if (uri.scheme == REVERSE_CLIENT_ID && this.auth_code == null) { - var form_data = Soup.Form.decode (uri.query); - this.auth_code = form_data.lookup("code"); + 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) { @@ -39,10 +47,14 @@ namespace Publishing.Authenticator.Shotwell.Google { } private void on_shotwell_auth_request_cb(WebKit.URISchemeRequest request) { - var uri = new Soup.URI(request.get_uri()); - debug("URI: %s", request.get_uri()); - var form_data = Soup.Form.decode (uri.query); - this.auth_code = form_data.lookup("code"); + 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, null); @@ -77,7 +89,7 @@ namespace Publishing.Authenticator.Shotwell.Google { } private class GetAccessTokensTransaction : Publishing.RESTSupport.Transaction { - private const string ENDPOINT_URL = "https://accounts.google.com/o/oauth2/token"; + private const string ENDPOINT_URL = "https://oauth2.googleapis.com/token"; public GetAccessTokensTransaction(Session session, string auth_code) { base.with_endpoint_url(session, ENDPOINT_URL); @@ -91,7 +103,7 @@ namespace Publishing.Authenticator.Shotwell.Google { } private class RefreshAccessTokenTransaction : Publishing.RESTSupport.Transaction { - private const string ENDPOINT_URL = "https://accounts.google.com/o/oauth2/token"; + private const string ENDPOINT_URL = "https://oauth2.googleapis.com/token"; public RefreshAccessTokenTransaction(Session session) { base.with_endpoint_url(session, ENDPOINT_URL); @@ -112,12 +124,18 @@ 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; + + // Prepare for multiple user accounts + private string accountname = "default"; private Spit.Publishing.PluginHost host = null; private GLib.HashTable 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, string welcome_message, @@ -127,13 +145,24 @@ namespace Publishing.Authenticator.Shotwell.Google { this.scope = scope; this.session = new Session(); this.welcome_message = welcome_message; + this.schema = new Secret.Schema(PASSWORD_SCHEME, Secret.SchemaFlags.NONE, + SCHEMA_KEY_PROFILE_ID, Secret.SchemaAttributeType.STRING, + SCHEMA_KEY_ACCOUNTNAME, Secret.SchemaAttributeType.STRING, + "scope", Secret.SchemaAttributeType.STRING); } public void authenticate() { - var refresh_token = host.get_config_string("refresh_token", null); + string? refresh_token = null; + 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); + } catch (Error err) { + critical("Failed to lookup refresh_token from password store: %s", err.message); + } if (refresh_token != null && refresh_token != "") { on_refresh_token_available(refresh_token); - do_exchange_refresh_token_for_access_token(); + do_exchange_refresh_token_for_access_token.begin(); return; } @@ -157,22 +186,32 @@ namespace Publishing.Authenticator.Shotwell.Google { public void logout() { session.deauthenticate(); - host.set_config_string("refresh_token", ""); + 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); + } catch (Error err) { + critical("Failed to remove password for scope %s: %s", this.scope, err.message); + } } public void refresh() { // TODO: Needs to re-auth } + public void set_accountname(string accountname) { + this.accountname = accountname; + } + private 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=" + Soup.URI.encode(OAUTH_CALLBACK_URI, null) + "&" + - "scope=" + Soup.URI.encode(this.scope, null) + "+" + - Soup.URI.encode("https://www.googleapis.com/auth/userinfo.profile", null) + "&" + + "redirect_uri=" + GLib.Uri.escape_string(OAUTH_CALLBACK_URI, null) + "&" + + "scope=" + GLib.Uri.escape_string(this.scope, null) + "+" + + GLib.Uri.escape_string("https://www.googleapis.com/auth/userinfo.profile", null) + "&" + "state=connect&" + "access_type=offline&" + "approval_prompt=force"; @@ -189,48 +228,31 @@ namespace Publishing.Authenticator.Shotwell.Google { debug("EVENT: user authorized scope %s with auth_code %s", scope, auth_code); - do_get_access_tokens(auth_code); + do_get_access_tokens.begin(auth_code); } private void on_web_auth_pane_error() { host.post_error(web_auth_pane.load_error); } - private void do_get_access_tokens(string auth_code) { + private async void do_get_access_tokens(string auth_code) { debug("ACTION: exchanging authorization code for access & refresh tokens"); host.install_login_wait_pane(); GetAccessTokensTransaction tokens_txn = new GetAccessTokensTransaction(session, auth_code); - tokens_txn.completed.connect(on_get_access_tokens_complete); - tokens_txn.network_error.connect(on_get_access_tokens_error); try { - tokens_txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield tokens_txn.execute_async(); + debug("EVENT: network transaction to exchange authorization code for access tokens " + + "completed successfully."); + do_extract_tokens(tokens_txn.get_response()); + } catch (Error err) { + debug("EVENT: network transaction to exchange authorization code for access tokens " + + "failed; response = '%s'", tokens_txn.get_response()); host.post_error(err); } - } - - private void on_get_access_tokens_complete(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_get_access_tokens_complete); - txn.network_error.disconnect(on_get_access_tokens_error); - debug("EVENT: network transaction to exchange authorization code for access tokens " + - "completed successfully."); - - do_extract_tokens(txn.get_response()); - } - - private void on_get_access_tokens_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_get_access_tokens_complete); - txn.network_error.disconnect(on_get_access_tokens_error); - - debug("EVENT: network transaction to exchange authorization code for access tokens " + - "failed; response = '%s'", txn.get_response()); - - host.post_error(err); } private void do_extract_tokens(string response_body) { @@ -297,45 +319,28 @@ namespace Publishing.Authenticator.Shotwell.Google { session.access_token = token; this.params.insert("AccessToken", new Variant.string(token)); - do_fetch_username(); + do_fetch_username.begin(); } - private void do_fetch_username() { + private async void do_fetch_username() { debug("ACTION: running network transaction to fetch username."); host.install_login_wait_pane(); host.set_service_locked(true); UsernameFetchTransaction txn = new UsernameFetchTransaction(session); - txn.completed.connect(on_fetch_username_transaction_completed); - txn.network_error.connect(on_fetch_username_transaction_error); try { - txn.execute(); + yield txn.execute_async(); + debug("EVENT: username fetch transaction completed successfully."); + do_extract_username(txn.get_response()); } catch (Error err) { + debug("EVENT: username fetch transaction caused a network error"); + host.post_error(err); } } - private void on_fetch_username_transaction_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_fetch_username_transaction_completed); - txn.network_error.disconnect(on_fetch_username_transaction_error); - - debug("EVENT: username fetch transaction completed successfully."); - - do_extract_username(txn.get_response()); - } - - private void on_fetch_username_transaction_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_fetch_username_transaction_completed); - txn.network_error.disconnect(on_fetch_username_transaction_error); - - debug("EVENT: username fetch transaction caused a network error"); - - host.post_error(err); - } - private void do_extract_username(string response_body) { debug("ACTION: extracting username from body of server response"); @@ -368,61 +373,57 @@ namespace Publishing.Authenticator.Shotwell.Google { // by the time we get a username, the session should be authenticated, or else something // really tragic has happened assert(session.is_authenticated()); - host.set_config_string("refresh_token", session.refresh_token); + try { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (Google account scope %s@%s)".printf(this.accountname, this.scope), + session.refresh_token, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope); + } catch (Error err) { + critical("Failed to look up password for scope %s: %s", this.scope, err.message); + } this.authenticated(); web_auth_pane.clear(); } - - private void do_exchange_refresh_token_for_access_token() { + private async void do_exchange_refresh_token_for_access_token() { debug("ACTION: exchanging OAuth refresh token for OAuth access token."); host.install_login_wait_pane(); RefreshAccessTokenTransaction txn = new RefreshAccessTokenTransaction(session); - - txn.completed.connect(on_refresh_access_token_transaction_completed); - txn.network_error.connect(on_refresh_access_token_transaction_error); - try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - } - } - - private void on_refresh_access_token_transaction_completed(Publishing.RESTSupport.Transaction - txn) { - txn.completed.disconnect(on_refresh_access_token_transaction_completed); - txn.network_error.disconnect(on_refresh_access_token_transaction_error); + yield txn.execute_async(); + debug("EVENT: refresh access token transaction completed successfully."); - debug("EVENT: refresh access token transaction completed successfully."); - - if (session.is_authenticated()) // ignore these events if the session is already auth'd - return; - - do_extract_tokens(txn.get_response()); - } - - private void on_refresh_access_token_transaction_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_refresh_access_token_transaction_completed); - txn.network_error.disconnect(on_refresh_access_token_transaction_error); - - debug("EVENT: refresh access token transaction caused a network error."); - - if (session.is_authenticated()) // ignore these events if the session is already auth'd - return; - if (txn.get_status_code() == Soup.Status.BAD_REQUEST || - txn.get_status_code() == Soup.Status.UNAUTHORIZED) { - // Refresh token invalid, starting over - host.set_config_string("refresh_token", ""); - Idle.add (() => { this.authenticate(); return false; }); + if (session.is_authenticated()) // ignore these events if the session is already auth'd + return; + + do_extract_tokens(txn.get_response()); + } catch (Error err) { + debug("EVENT: refresh access token transaction caused a network error."); + + if (session.is_authenticated()) // ignore these events if the session is already auth'd + return; + + if (txn.get_status_code() == Soup.Status.BAD_REQUEST || + txn.get_status_code() == Soup.Status.UNAUTHORIZED) { + // Refresh token invalid, starting over + 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); + } catch (Error err) { + critical("Failed to remove password for accountname@scope %s@%s: %s", this.accountname, this.scope, err.message); + } + + Idle.add (() => { this.authenticate(); return false; }); + } + + web_auth_pane.clear(); + host.post_error(err); } - - web_auth_pane.clear(); - host.post_error(err); } private void do_show_service_welcome_pane() { @@ -436,7 +437,5 @@ namespace Publishing.Authenticator.Shotwell.Google { this.do_hosted_web_authentication(); } - - } } diff --git a/plugins/authenticator/shotwell/OAuth1Authenticator.vala b/plugins/authenticator/shotwell/OAuth1Authenticator.vala index 39752ec..e79c6fd 100644 --- a/plugins/authenticator/shotwell/OAuth1Authenticator.vala +++ b/plugins/authenticator/shotwell/OAuth1Authenticator.vala @@ -11,10 +11,23 @@ namespace Publishing.Authenticator.Shotwell.OAuth1 { protected GLib.HashTable params; protected Publishing.RESTSupport.OAuth1.Session session; protected Spit.Publishing.PluginHost host; - - protected Authenticator(string api_key, string api_secret, Spit.Publishing.PluginHost host) { + private Secret.Schema? schema = null; + private const string SECRET_TYPE_USERNAME = "username"; + private const string SECRET_TYPE_AUTH_TOKEN = "auth-token"; + private const string SECRET_TYPE_AUTH_TOKEN_SECRET = "auth-token-secret"; + private const string SCHEMA_KEY_ACCOUNTNAME = "accountname"; + private const string SCHEMA_KEY_PROFILE_ID = "shotwell-profile-id"; + private string service = null; + private string accountname = "default"; + + protected Authenticator(string service, string api_key, string api_secret, Spit.Publishing.PluginHost host) { base(); this.host = host; + this.service = service; + this.schema = new Secret.Schema("org.gnome.Shotwell." + service, Secret.SchemaFlags.NONE, + SCHEMA_KEY_PROFILE_ID, Secret.SchemaAttributeType.STRING, + SCHEMA_KEY_ACCOUNTNAME, Secret.SchemaAttributeType.STRING, + "type", Secret.SchemaAttributeType.STRING); params = new GLib.HashTable(str_hash, str_equal); params.insert("ConsumerKey", api_key); @@ -42,11 +55,16 @@ namespace Publishing.Authenticator.Shotwell.OAuth1 { public abstract void refresh(); + public virtual void set_accountname(string name) { + this.accountname = name; + } + public void invalidate_persistent_session() { - set_persistent_access_phase_token(""); - set_persistent_access_phase_token_secret(""); - set_persistent_access_phase_username(""); + set_persistent_access_phase_token(null); + set_persistent_access_phase_token_secret(null); + set_persistent_access_phase_username(null); } + protected bool is_persistent_session_valid() { return (get_persistent_access_phase_username() != null && get_persistent_access_phase_token() != null && @@ -54,30 +72,99 @@ namespace Publishing.Authenticator.Shotwell.OAuth1 { } protected string? get_persistent_access_phase_username() { - return host.get_config_string("access_phase_username", null); + try { + return Secret.password_lookup_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "type", SECRET_TYPE_USERNAME); + } catch (Error err) { + critical("Failed to lookup username from password store: %s", err.message); + return null; + } } - protected void set_persistent_access_phase_username(string username) { - host.set_config_string("access_phase_username", username); + protected void set_persistent_access_phase_username(string? username) { + try { + if (username == null || username == "") { + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_USERNAME); + } else { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (%s@%s)".printf(this.accountname, this.service), + username, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "type", SECRET_TYPE_USERNAME); + } + } catch (Error err) { + critical("Failed to store username in store: %s", err.message); + } } protected string? get_persistent_access_phase_token() { - return host.get_config_string("access_phase_token", null); + try { + return Secret.password_lookup_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN); + } catch (Error err) { + critical("Failed to lookup auth-token from password store: %s", err.message); + return null; + } } - protected void set_persistent_access_phase_token(string token) { - host.set_config_string("access_phase_token", token); + protected void set_persistent_access_phase_token(string? token) { + try { + if (token == null || token == "") { + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN); + } else { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (%s@%s)".printf(this.accountname, this.service), + token, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN); + } + } catch (Error err) { + critical("Failed to store auth-token store: %s", err.message); + } } protected string? get_persistent_access_phase_token_secret() { - return host.get_config_string("access_phase_token_secret", null); + try { + return Secret.password_lookup_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN_SECRET); + } catch (Error err) { + critical("Failed to lookup auth-token-secret from password store: %s", err.message); + return null; + } } - protected void set_persistent_access_phase_token_secret(string secret) { - host.set_config_string("access_phase_token_secret", secret); + protected void set_persistent_access_phase_token_secret(string? secret) { + try { + if (secret == null || secret == "") { + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN_SECRET); + } else { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (%s@%s)".printf(this.accountname, this.service), + secret, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN_SECRET); + } + } catch (Error err) { + critical("Failed to store auth-token-secret store: %s", err.message); + } } - protected void on_session_authenticated() { params.insert("AuthToken", session.get_access_phase_token()); params.insert("AuthTokenSecret", session.get_access_phase_token_secret()); @@ -90,7 +177,5 @@ namespace Publishing.Authenticator.Shotwell.OAuth1 { this.authenticated(); } - } - } diff --git a/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala b/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala index 759822c..01fa3c3 100644 --- a/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala +++ b/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala @@ -26,14 +26,12 @@ namespace Publishing.Authenticator { switch (provider) { case "flickr": return new Shotwell.Flickr.Flickr(host); - case "facebook": - return new Shotwell.Facebook.Facebook(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 https://developers.google.com/youtube 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 https://www.youtube.com/t/terms\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 online services privacy policy\n\nFor Google's own privacy policy, please refer to https://policies.google.com/privacy"), 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 https://developers.google.com/photos/ 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 online services privacy policyFor Google's own privacy policy, please refer to https://policies.google.com/privacy"), host); + 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 https://developers.google.com/photos/ 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 online services privacy policy. For Google's own privacy policy, please refer to https://policies.google.com/privacy"), host); default: return null; } diff --git a/plugins/authenticator/shotwell/TumblrAuthenticator.vala b/plugins/authenticator/shotwell/TumblrAuthenticator.vala index 8907a80..e77814b 100644 --- a/plugins/authenticator/shotwell/TumblrAuthenticator.vala +++ b/plugins/authenticator/shotwell/TumblrAuthenticator.vala @@ -119,7 +119,7 @@ namespace Publishing.Authenticator.Shotwell.Tumblr { public AccessTokenFetchTransaction(Publishing.RESTSupport.OAuth1.Session session, string username, string password) { base.with_uri(session, "https://www.tumblr.com/oauth/access_token", Publishing.RESTSupport.HttpMethod.POST); - add_argument("x_auth_username", Soup.URI.encode(username, ENCODE_RFC_3986_EXTRA)); + add_argument("x_auth_username", username); add_argument("x_auth_password", password); add_argument("x_auth_mode", "client_auth"); } @@ -127,7 +127,7 @@ namespace Publishing.Authenticator.Shotwell.Tumblr { internal class Tumblr : Publishing.Authenticator.Shotwell.OAuth1.Authenticator { public Tumblr(Spit.Publishing.PluginHost host) { - base(API_KEY, API_SECRET, host); + base("Tumblr", API_KEY, API_SECRET, host); } public override void authenticate() { @@ -190,7 +190,7 @@ namespace Publishing.Authenticator.Shotwell.Tumblr { private void on_authentication_pane_login_clicked( string username, string password ) { debug("EVENT: on_authentication_pane_login_clicked"); - do_network_login(username, password); + do_network_login.begin(username, password); } /** @@ -201,41 +201,24 @@ namespace Publishing.Authenticator.Shotwell.Tumblr { * @param username the name of the Tumblr user used to login * @param password the password of the Tumblr user used to login */ - private void do_network_login(string username, string password) { + private async void do_network_login(string username, string password) { debug("ACTION: logging in"); host.set_service_locked(true); host.install_login_wait_pane(); AccessTokenFetchTransaction txn = new AccessTokenFetchTransaction(session,username,password); - txn.completed.connect(on_auth_request_txn_completed); - txn.network_error.connect(on_auth_request_txn_error); - try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield txn.execute_async(); + debug("EVENT: OAuth authentication request transaction completed; response = '%s'", + 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); } } - private void on_auth_request_txn_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_auth_request_txn_completed); - txn.network_error.disconnect(on_auth_request_txn_error); - - debug("EVENT: OAuth authentication request transaction completed; response = '%s'", - txn.get_response()); - - do_parse_token_info_from_auth_request(txn.get_response()); - } - - private void on_auth_request_txn_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_auth_request_txn_completed); - txn.network_error.disconnect(on_auth_request_txn_error); - - debug("EVENT: OAuth authentication request transaction caused a network error"); - host.post_error(err); - } - private void do_parse_token_info_from_auth_request(string response) { debug("ACTION: extracting access phase credentials from '%s'", response); @@ -255,7 +238,7 @@ namespace Publishing.Authenticator.Shotwell.Tumblr { "least one of these is absent")); this.authentication_failed(); } else { - session.set_access_phase_credentials(token, token_secret, ""); + session.set_access_phase_credentials(token, token_secret, "unused"); } } } diff --git a/plugins/authenticator/shotwell/meson.build b/plugins/authenticator/shotwell/meson.build index 4404242..037ec3b 100644 --- a/plugins/authenticator/shotwell/meson.build +++ b/plugins/authenticator/shotwell/meson.build @@ -1,6 +1,5 @@ authenticator_shotwell_sources = [ 'ShotwellAuthenticatorFactory.vala', - 'FacebookPublishingAuthenticator.vala', 'FlickrPublishingAuthenticator.vala', 'GoogleAuthenticator.vala', 'OAuth1Authenticator.vala', @@ -9,10 +8,10 @@ authenticator_shotwell_sources = [ authenticator_shotwell_resources = gnome.compile_resources('authenticator-resource', 'org.gnome.Shotwell.Authenticator.gresource.xml', - source_dir : meson.source_root()) + 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] + sw_plugin_common_dep, json_glib, xml, webkit, secret] authenticator = library('shotwell-authenticator', authenticator_shotwell_sources + authenticator_shotwell_resources, -- cgit v1.2.3