summaryrefslogtreecommitdiff
path: root/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala')
-rw-r--r--plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala552
1 files changed, 552 insertions, 0 deletions
diff --git a/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala b/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala
new file mode 100644
index 0000000..e389908
--- /dev/null
+++ b/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala
@@ -0,0 +1,552 @@
+/* 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.
+ */
+
+namespace Publishing.Authenticator.Shotwell.Flickr {
+ internal const string ENDPOINT_URL = "https://api.flickr.com/services/rest";
+ internal const string EXPIRED_SESSION_ERROR_CODE = "98";
+ internal const string ENCODE_RFC_3986_EXTRA = "!*'();:@&=+$,/?%#[] \\";
+
+ internal class Transaction : Publishing.RESTSupport.Transaction {
+ public Transaction(Session session, Publishing.RESTSupport.HttpMethod method =
+ Publishing.RESTSupport.HttpMethod.POST) {
+ base(session, method);
+ setup_arguments();
+ }
+
+ public Transaction.with_uri(Session session, string uri,
+ Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.POST) {
+ base.with_endpoint_url(session, uri, method);
+ setup_arguments();
+ }
+
+ private void setup_arguments() {
+ var session = (Session) get_parent_session();
+
+ add_argument("oauth_nonce", session.get_oauth_nonce());
+ add_argument("oauth_signature_method", "HMAC-SHA1");
+ add_argument("oauth_version", "1.0");
+ add_argument("oauth_callback", "oob");
+ add_argument("oauth_timestamp", session.get_oauth_timestamp());
+ add_argument("oauth_consumer_key", session.get_consumer_key());
+ }
+
+
+ public override void execute() throws Spit.Publishing.PublishingError {
+ ((Session) get_parent_session()).sign_transaction(this);
+
+ base.execute();
+ }
+ }
+
+ internal class Session : Publishing.RESTSupport.Session {
+ private string? request_phase_token = null;
+ private string? request_phase_token_secret = null;
+ private string? access_phase_token = null;
+ private string? access_phase_token_secret = null;
+ private string? username = null;
+ private string? consumer_key = null;
+ private string? consumer_secret = null;
+
+ public Session() {
+ base();
+ }
+
+ public override bool is_authenticated() {
+ return (access_phase_token != null && access_phase_token_secret != null &&
+ username != null);
+ }
+
+ public void authenticate_from_persistent_credentials(string token, string secret,
+ string username) {
+ this.access_phase_token = token;
+ this.access_phase_token_secret = secret;
+ this.username = username;
+
+ this.authenticated();
+ }
+
+ public void deauthenticate() {
+ access_phase_token = null;
+ access_phase_token_secret = null;
+ username = null;
+ }
+
+ public void set_api_credentials(string consumer_key, string consumer_secret) {
+ this.consumer_key = consumer_key;
+ this.consumer_secret = consumer_secret;
+ }
+
+ public void sign_transaction(Publishing.RESTSupport.Transaction txn) {
+ string http_method = txn.get_method().to_string();
+
+ debug("signing transaction with parameters:");
+ debug("HTTP method = " + http_method);
+
+ Publishing.RESTSupport.Argument[] base_string_arguments = txn.get_arguments();
+
+ Publishing.RESTSupport.Argument[] sorted_args =
+ Publishing.RESTSupport.Argument.sort(base_string_arguments);
+
+ string arguments_string = "";
+ for (int i = 0; i < sorted_args.length; i++) {
+ arguments_string += (sorted_args[i].key + "=" + sorted_args[i].value);
+ if (i < sorted_args.length - 1)
+ arguments_string += "&";
+ }
+
+ string? signing_key = null;
+ if (access_phase_token_secret != null) {
+ debug("access phase token secret available; using it as signing key");
+
+ signing_key = consumer_secret + "&" + access_phase_token_secret;
+ } else if (request_phase_token_secret != null) {
+ debug("request phase token secret available; using it as signing key");
+
+ signing_key = consumer_secret + "&" + request_phase_token_secret;
+ } else {
+ debug("neither access phase nor request phase token secrets available; using API " +
+ "key as signing key");
+
+ signing_key = consumer_secret + "&";
+ }
+
+ string signature_base_string = http_method + "&" + Soup.URI.encode(
+ txn.get_endpoint_url(), ENCODE_RFC_3986_EXTRA) + "&" +
+ Soup.URI.encode(arguments_string, ENCODE_RFC_3986_EXTRA);
+
+ debug("signature base string = '%s'", signature_base_string);
+
+ debug("signing key = '%s'", signing_key);
+
+ // compute the signature
+ string signature = RESTSupport.hmac_sha1(signing_key, signature_base_string);
+ signature = Soup.URI.encode(signature, ENCODE_RFC_3986_EXTRA);
+
+ debug("signature = '%s'", signature);
+
+ txn.add_argument("oauth_signature", signature);
+ }
+
+ public void set_request_phase_credentials(string token, string secret) {
+ this.request_phase_token = token;
+ this.request_phase_token_secret = secret;
+ }
+
+ public void set_access_phase_credentials(string token, string secret, string username) {
+ this.access_phase_token = token;
+ this.access_phase_token_secret = secret;
+ this.username = username;
+
+ authenticated();
+ }
+
+ public string get_oauth_nonce() {
+ TimeVal currtime = TimeVal();
+ currtime.get_current_time();
+
+ return Checksum.compute_for_string(ChecksumType.MD5, currtime.tv_sec.to_string() +
+ currtime.tv_usec.to_string());
+ }
+
+ public string get_oauth_timestamp() {
+ return GLib.get_real_time().to_string().substring(0, 10);
+ }
+
+ public string get_consumer_key() {
+ assert(consumer_key != null);
+ return consumer_key;
+ }
+
+ public string get_request_phase_token() {
+ assert(request_phase_token != null);
+ return request_phase_token;
+ }
+
+ public string get_access_phase_token() {
+ assert(access_phase_token != null);
+ return access_phase_token;
+ }
+
+ public string get_access_phase_token_secret() {
+ assert(access_phase_token_secret != null);
+ return access_phase_token_secret;
+ }
+
+ public string get_username() {
+ assert(is_authenticated());
+ return username;
+ }
+ }
+ internal const string API_KEY = "60dd96d4a2ad04888b09c9e18d82c26f";
+ internal const string API_SECRET = "d0960565e03547c1";
+
+ internal const string SERVICE_WELCOME_MESSAGE =
+ _("You are not currently logged into Flickr.\n\nClick Log in to log into Flickr in your Web browser. You will have to authorize Shotwell Connect to link to your Flickr account.");
+
+ internal class AuthenticationRequestTransaction : Transaction {
+ public AuthenticationRequestTransaction(Session session) {
+ base.with_uri(session, "https://www.flickr.com/services/oauth/request_token",
+ Publishing.RESTSupport.HttpMethod.GET);
+ }
+ }
+
+ internal class AccessTokenFetchTransaction : Transaction {
+ public AccessTokenFetchTransaction(Session session, string user_verifier) {
+ 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());
+ }
+ }
+
+ internal class PinEntryPane : Spit.Publishing.DialogPane, GLib.Object {
+ private Gtk.Box pane_widget = null;
+ private Gtk.Button continue_button = null;
+ private Gtk.Entry pin_entry = null;
+ private Gtk.Label pin_entry_caption = null;
+ private Gtk.Label explanatory_text = null;
+ private Gtk.Builder builder = null;
+
+ public signal void proceed(PinEntryPane sender, string authorization_pin);
+
+ public PinEntryPane(Gtk.Builder builder) {
+ this.builder = builder;
+ assert(builder != null);
+ assert(builder.get_objects().length() > 0);
+
+ explanatory_text = builder.get_object("explanatory_text") as Gtk.Label;
+ pin_entry_caption = builder.get_object("pin_entry_caption") as Gtk.Label;
+ pin_entry = builder.get_object("pin_entry") as Gtk.Entry;
+ continue_button = builder.get_object("continue_button") as Gtk.Button;
+
+ pane_widget = builder.get_object("pane_widget") as Gtk.Box;
+
+ pane_widget.show_all();
+
+ on_pin_entry_contents_changed();
+ }
+
+ private void on_continue_clicked() {
+ proceed(this, pin_entry.get_text());
+ }
+
+ private void on_pin_entry_contents_changed() {
+ continue_button.set_sensitive(pin_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() {
+ continue_button.clicked.connect(on_continue_clicked);
+ pin_entry.changed.connect(on_pin_entry_contents_changed);
+ }
+
+ public void on_pane_uninstalled() {
+ continue_button.clicked.disconnect(on_continue_clicked);
+ pin_entry.changed.disconnect(on_pin_entry_contents_changed);
+ }
+ }
+
+
+ internal class Flickr : GLib.Object, Spit.Publishing.Authenticator {
+ private GLib.HashTable<string, Variant> params;
+ private Session session;
+ private Spit.Publishing.PluginHost host;
+
+ public Flickr(Spit.Publishing.PluginHost host) {
+ base();
+
+ this.host = host;
+ params = new GLib.HashTable<string, Variant>(str_hash, str_equal);
+ params.insert("ConsumerKey", API_KEY);
+ params.insert("ConsumerSecret", API_SECRET);
+
+ session = new Session();
+ session.set_api_credentials(API_KEY, API_SECRET);
+ session.authenticated.connect(on_session_authenticated);
+ }
+
+ ~Flickr() {
+ session.authenticated.disconnect(on_session_authenticated);
+ }
+
+ public void invalidate_persistent_session() {
+ set_persistent_access_phase_token("");
+ set_persistent_access_phase_token_secret("");
+ set_persistent_access_phase_username("");
+ }
+
+ private bool is_persistent_session_valid() {
+ return (get_persistent_access_phase_username() != null &&
+ get_persistent_access_phase_token() != null &&
+ get_persistent_access_phase_token_secret() != null);
+ }
+
+ private string? get_persistent_access_phase_username() {
+ return host.get_config_string("access_phase_username", null);
+ }
+
+ private void set_persistent_access_phase_username(string username) {
+ host.set_config_string("access_phase_username", username);
+ }
+
+ private string? get_persistent_access_phase_token() {
+ return host.get_config_string("access_phase_token", null);
+ }
+
+ private void set_persistent_access_phase_token(string token) {
+ host.set_config_string("access_phase_token", token);
+ }
+
+ private string? get_persistent_access_phase_token_secret() {
+ return host.get_config_string("access_phase_token_secret", null);
+ }
+
+ private void set_persistent_access_phase_token_secret(string secret) {
+ host.set_config_string("access_phase_token_secret", secret);
+ }
+
+ public void authenticate() {
+ if (is_persistent_session_valid()) {
+ debug("attempt start: a persistent session is available; using it");
+
+ session.authenticate_from_persistent_credentials(get_persistent_access_phase_token(),
+ get_persistent_access_phase_token_secret(), get_persistent_access_phase_username());
+ } else {
+ debug("attempt start: no persistent session available; showing login welcome pane");
+ do_show_login_welcome_pane();
+ }
+ }
+
+ public bool can_logout() {
+ return true;
+ }
+
+ public GLib.HashTable<string, Variant> get_authentication_parameter() {
+ return this.params;
+ }
+
+ public void logout () {
+ session.deauthenticate();
+ invalidate_persistent_session();
+ }
+
+ public void refresh() {
+ // No-Op with flickr
+ }
+
+ private void do_show_login_welcome_pane() {
+ debug("ACTION: installing login welcome pane");
+
+ host.set_service_locked(false);
+ host.install_welcome_pane(SERVICE_WELCOME_MESSAGE, on_welcome_pane_login_clicked);
+ }
+
+ private void on_welcome_pane_login_clicked() {
+ debug("EVENT: user clicked 'Login' button in the welcome pane");
+
+ do_run_authentication_request_transaction();
+ }
+
+ private 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) {
+ 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();
+ }
+
+ 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_launch_system_browser(token);
+ }
+
+ private void on_system_browser_launched() {
+ debug("EVENT: system browser launched.");
+
+ do_show_pin_entry_pane();
+ }
+
+ private void on_pin_entry_proceed(PinEntryPane sender, string pin) {
+ sender.proceed.disconnect(on_pin_entry_proceed);
+
+ debug("EVENT: user clicked 'Continue' in PIN entry pane.");
+
+ do_verify_pin(pin);
+ }
+
+ private void do_launch_system_browser(string token) {
+ string login_uri = "https://www.flickr.com/services/oauth/authorize?oauth_token=" + token +
+ "&perms=write";
+
+ debug("ACTION: launching system browser with uri = '%s'", login_uri);
+
+ try {
+ Process.spawn_command_line_async("xdg-open " + login_uri);
+ } catch (SpawnError e) {
+ host.post_error(new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(
+ "couldn't launch system web browser to complete Flickr login"));
+ return;
+ }
+
+ on_system_browser_launched();
+ }
+
+ private void do_show_pin_entry_pane() {
+ debug("ACTION: showing PIN entry pane");
+
+ Gtk.Builder builder = new Gtk.Builder();
+
+ try {
+ builder.add_from_resource (Resources.RESOURCE_PATH + "/" +
+ "flickr_pin_entry_pane.ui");
+ } catch (Error e) {
+ warning("Could not parse UI file! Error: %s.", e.message);
+ host.post_error(
+ new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(
+ _("A file required for publishing is unavailable. Publishing to Flickr can’t continue.")));
+ return;
+ }
+
+ PinEntryPane pin_entry_pane = new PinEntryPane(builder);
+ pin_entry_pane.proceed.connect(on_pin_entry_proceed);
+ host.install_dialog_pane(pin_entry_pane);
+ }
+
+ private 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);
+
+ debug("EVENT: fetching OAuth access token over the network succeeded");
+
+ 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();
+ }
+
+ private void do_extract_access_phase_credentials_from_reponse(string response) {
+ debug("ACTION: extracting access phase credentials from '%s'", response);
+
+ string? token = null;
+ string? token_secret = null;
+ string? username = null;
+
+ var data = Soup.Form.decode(response);
+ data.lookup_extended("oauth_token", null, out token);
+ data.lookup_extended("oauth_token_secret", null, out token_secret);
+ data.lookup_extended("username", null, out username);
+
+ debug("access phase credentials: { token = '%s'; token_secret = '%s'; username = '%s' }",
+ token, token_secret, username);
+
+ 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, username);
+ }
+ }
+
+ private void on_session_authenticated() {
+ params.insert("AuthToken", session.get_access_phase_token());
+ params.insert("AuthTokenSecret", session.get_access_phase_token_secret());
+ params.insert("Username", session.get_username());
+
+ set_persistent_access_phase_token(session.get_access_phase_token());
+ set_persistent_access_phase_token_secret(session.get_access_phase_token_secret());
+ set_persistent_access_phase_username(session.get_username());
+
+
+ this.authenticated();
+ }
+ }
+}