From 7868ff68cff97b21fe6d8681f8bc0334849c4d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= Date: Mon, 9 Jun 2025 10:49:17 +0200 Subject: New upstream version 0.32.13 --- .../shotwell/GoogleAuthenticator.vala | 35 ++++++++++++++-------- .../shotwell/ShotwellAuthenticatorFactory.vala | 4 +-- plugins/shotwell-publishing/PhotosPublisher.vala | 22 ++++++++++++-- plugins/shotwell-publishing/YouTubePublishing.vala | 3 -- 4 files changed, 44 insertions(+), 20 deletions(-) (limited to 'plugins') diff --git a/plugins/authenticator/shotwell/GoogleAuthenticator.vala b/plugins/authenticator/shotwell/GoogleAuthenticator.vala index 5a0d934..1fe2448 100644 --- a/plugins/authenticator/shotwell/GoogleAuthenticator.vala +++ b/plugins/authenticator/shotwell/GoogleAuthenticator.vala @@ -64,7 +64,7 @@ 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"; @@ -74,12 +74,12 @@ namespace Publishing.Authenticator.Shotwell.Google { 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(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, @@ -93,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); } @@ -106,6 +106,10 @@ namespace Publishing.Authenticator.Shotwell.Google { this.do_show_service_welcome_pane(); } + public string get_scopes(string separator=",") { + return string.joinv(separator, this.scopes); + } + public bool can_logout() { return true; } @@ -119,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); } } @@ -147,7 +151,7 @@ namespace Publishing.Authenticator.Shotwell.Google { "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&" + @@ -155,18 +159,25 @@ namespace Publishing.Authenticator.Shotwell.Google { var auth_callback = new AuthCallback(); string? web_auth_code = null; + 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; + // FIXME throw error missing scopes + yield do_get_access_tokens(web_auth_code); } catch (Error err) { host.post_error(err); @@ -315,12 +326,12 @@ 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(); @@ -352,9 +363,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 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; }); 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 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); + 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 policy. For 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.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 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/shotwell-publishing/PhotosPublisher.vala b/plugins/shotwell-publishing/PhotosPublisher.vala index b592317..67c3ecb 100644 --- a/plugins/shotwell-publishing/PhotosPublisher.vala +++ b/plugins/shotwell-publishing/PhotosPublisher.vala @@ -111,6 +111,7 @@ internal class PublishingParameters { } private class MediaCreationTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + // SCOPE: photoslibrary.appendonly private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate"; private string[] upload_tokens; private string[] titles; @@ -154,6 +155,7 @@ private class MediaCreationTransaction : Publishing.RESTSupport.GooglePublisher. } private class AlbumCreationTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + // SCOPE: photoslibrary.appendonly private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/albums"; private string title; @@ -179,6 +181,7 @@ private class AlbumCreationTransaction : Publishing.RESTSupport.GooglePublisher. } private class AlbumDirectoryTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + // SCOPE: photoslibrary.readonly.appcreateddata private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/albums"; public AlbumDirectoryTransaction(Publishing.RESTSupport.GoogleSession session, string? token) { @@ -244,8 +247,15 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { if (!is_running()) return; + var json = Json.from_string (txn.get_response()); var object = json.get_object (); + // Work-around for Google sometimes sending an empty JSON object '{}' instead of + // not setting the nextPageToken on the previous page + if (object.get_size() == 0) { + break; + } + if (!object.has_member ("albums")) { throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE("Album fetch did not contain expected data"); } @@ -279,7 +289,10 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { debug("EVENT: fetching album information failed; response = '%s'.", txn.get_response()); - if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { + if (txn.get_status_code() == 403) { + debug("Lacking permission to download album list, showing publishing options anyway"); + show_publishing_options_pane(); + } else if (txn.get_status_code() == 404) { do_logout(); } else { // If we get any other kind of error, we can't recover, so just post it to the user @@ -345,10 +358,13 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { yield do_upload(); } catch (Error err) { - debug("EVENT: creating album failed; response = '%s'.", + debug("EVENT: creating album failed; status = '%u', response = '%s'.", txn.get_status_code(), txn.get_response()); - if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { + if (txn.get_status_code() == 403) { + get_host().install_static_message_pane(_("Could not create album, Shotwell is lacking permission to do so. Please re-authenticate and grant Shotwell the required permission to create new media and albums"), + Spit.Publishing.PluginHost.ButtonMode.CLOSE); + } else if (txn.get_status_code() == 404) { do_logout(); } else { // If we get any other kind of error, we can't recover, so just post it to the user diff --git a/plugins/shotwell-publishing/YouTubePublishing.vala b/plugins/shotwell-publishing/YouTubePublishing.vala index 88218cc..13e4afd 100644 --- a/plugins/shotwell-publishing/YouTubePublishing.vala +++ b/plugins/shotwell-publishing/YouTubePublishing.vala @@ -44,9 +44,6 @@ public class YouTubeService : Object, Spit.Pluggable, Spit.Publishing.Service { namespace Publishing.YouTube { -private const string DEVELOPER_KEY = - "AIzaSyB6hLnm0n5j8Y6Bkvh9bz3i8ADM2bJdYeY"; - private enum PrivacySetting { PUBLIC, UNLISTED, -- cgit v1.2.3