diff options
Diffstat (limited to 'plugins')
| -rw-r--r-- | plugins/authenticator/shotwell/GoogleAuthenticator.vala | 35 | ||||
| -rw-r--r-- | plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala | 4 | ||||
| -rw-r--r-- | plugins/common/RESTSupport.vala | 12 | ||||
| -rw-r--r-- | plugins/common/Resources.vala | 24 | ||||
| -rw-r--r-- | plugins/shotwell-publishing/PhotosPublisher.vala | 22 | ||||
| -rw-r--r-- | plugins/shotwell-publishing/PiwigoPublishing.vala | 34 | ||||
| -rw-r--r-- | plugins/shotwell-publishing/YouTubePublishing.vala | 3 |
7 files changed, 70 insertions, 64 deletions
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<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, @@ -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 <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/common/RESTSupport.vala b/plugins/common/RESTSupport.vala index cc810fe..13286de 100644 --- a/plugins/common/RESTSupport.vala +++ b/plugins/common/RESTSupport.vala @@ -4,6 +4,8 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +extern const string _VERSION; + namespace Publishing.RESTSupport { // Ported from librest @@ -34,9 +36,11 @@ public abstract class Session { public signal void authenticated(); public signal void authentication_failed(Spit.Publishing.PublishingError err); - protected Session(string? endpoint_url = null) { + protected Session(string? endpoint_url = null, Soup.SessionFeature[] features = {}) { this.endpoint_url = endpoint_url; soup_session = new Soup.Session (); + // The trailing space is intentional to make libsoup append its version info + soup_session.set_user_agent("Shotwell/%s ".printf(_VERSION)); if (Environment.get_variable("SHOTWELL_SOUP_LOG") != null) { var logger = new Soup.Logger(Soup.LoggerLogLevel.BODY); logger.set_request_filter((logger, msg) => { @@ -49,6 +53,10 @@ public abstract class Session { }); soup_session.add_feature (logger); } + + foreach (var feature in features) { + soup_session.add_feature(feature); + } } protected void notify_wire_message_unqueued(Soup.Message message) { @@ -361,7 +369,7 @@ public class Transaction { protected virtual void add_header(string key, string value) { message.request_headers.append(key, value); } - + // set custom_payload to null to have this transaction send the default payload of // key-value pairs appended through add_argument(...) (this is how most REST requests work). // To send a payload other than traditional key-value pairs (such as an XML document or a JPEG diff --git a/plugins/common/Resources.vala b/plugins/common/Resources.vala index 16306f2..d8c3fd3 100644 --- a/plugins/common/Resources.vala +++ b/plugins/common/Resources.vala @@ -30,30 +30,6 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc., public const string TRANSLATORS = _("translator-credits"); -// TODO: modify to load multiple icons -// -// provided all the icons in the set follow a known naming convention (such as iconName_nn.png, -// where 'nn' is a size value in pixels, for example plugins_16.png -- this convention seems -// pretty common in the GNOME world), then this function can be modified to load an entire icon -// set without its interface needing to change, since given one icon filename, we can -// determine the others. -public Gdk.Pixbuf[]? load_icon_set(GLib.File? icon_file) { - Gdk.Pixbuf? icon = null; - try { - icon = new Gdk.Pixbuf.from_file(icon_file.get_path()); - } catch (Error err) { - warning("couldn't load icon set from %s: %s", icon_file.get_path(), err.message); - } - - if (icon != null) { - Gdk.Pixbuf[] icon_pixbuf_set = new Gdk.Pixbuf[0]; - icon_pixbuf_set += icon; - return icon_pixbuf_set; - } - - return null; -} - public Gdk.Pixbuf[]? load_from_resource (string resource_path) { Gdk.Pixbuf? icon = null; try { 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/PiwigoPublishing.vala b/plugins/shotwell-publishing/PiwigoPublishing.vala index 9bf0013..07af9b6 100644 --- a/plugins/shotwell-publishing/PiwigoPublishing.vala +++ b/plugins/shotwell-publishing/PiwigoPublishing.vala @@ -1555,9 +1555,14 @@ internal class Session : Publishing.RESTSupport.Session { private string? pwg_url = null; private string? pwg_id = null; private string? username = null; + private Soup.CookieJar? jar = null; public Session() { - base(""); + var cookies = new Soup.CookieJar(); + base("", {cookies}); + + // Member only exists after this point, so set it after the base constructor + this.jar = cookies; } public override bool is_authenticated() { @@ -1574,6 +1579,10 @@ internal class Session : Publishing.RESTSupport.Session { pwg_url = null; pwg_id = null; username = null; + var cookies = jar.all_cookies(); + foreach (var cookie in cookies) { + jar.delete_cookie(cookie); + } } public string get_username() { @@ -1597,21 +1606,17 @@ internal class Session : Publishing.RESTSupport.Session { * Generic REST transaction class. * * This class implements the generic logic for all REST transactions used - * by the Piwigo publishing plugin. In particular, it ensures that if the - * session has been authenticated, the pwg_id token is included in the - * transaction header. + * by the Piwigo publishing plugin. */ internal class Transaction : Publishing.RESTSupport.Transaction { public Transaction(Session session) { base(session); - if (session.is_authenticated()) { - add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); - } + add_header("Referer", session.get_pwg_url()); } public Transaction.authenticated(Session session) { base.with_endpoint_url(session, session.get_pwg_url()); - add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); + add_header("Referer", session.get_pwg_url()); } public static string? validate_xml(Publishing.RESTSupport.XmlDocument doc) { @@ -1674,7 +1679,6 @@ internal class SessionLoginTransaction : Transaction { internal class SessionGetStatusTransaction : Transaction { public SessionGetStatusTransaction.unauthenticated(Session session, string url, string pwg_id) { base.with_endpoint_url(session, url); - add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); add_argument("method", "pwg.session.getStatus"); } @@ -1729,9 +1733,6 @@ private class ImagesAddTransaction : Publishing.RESTSupport.UploadTransaction { public ImagesAddTransaction(Session session, PublishingParameters parameters, Spit.Publishing.Publishable publishable) { base.with_endpoint_url(session, publishable, session.get_pwg_url()); - if (session.is_authenticated()) { - add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); - } this.session = session; this.parameters = parameters; @@ -1806,7 +1807,7 @@ private class ImagesAddTransaction : Publishing.RESTSupport.UploadTransaction { Xml.Node* image_node = resp_doc.get_named_child(resp_doc.get_root_node(), "image_id"); string image_id = image_node->get_content(); - if (!parameters.no_upload_ratings) + if (!parameters.no_upload_ratings && publishable.get_rating() > 0) new ImagesAddRating(session, publishable, image_id); } catch(Spit.Publishing.PublishingError err) { debug("Response parse error"); @@ -1814,12 +1815,9 @@ private class ImagesAddTransaction : Publishing.RESTSupport.UploadTransaction { } } -private class ImagesAddRating : Publishing.RESTSupport.UploadTransaction { +private class ImagesAddRating : Transaction { public ImagesAddRating(Session session, Spit.Publishing.Publishable publishable, string image_id) { - base.with_endpoint_url(session, publishable, session.get_pwg_url()); - if (session.is_authenticated()) { - add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); - } + base.with_endpoint_url(session, session.get_pwg_url()); add_argument("method", "pwg.images.rate"); add_argument("image_id", image_id); add_argument("rate", publishable.get_rating().to_string()); 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, |
