summaryrefslogtreecommitdiff
path: root/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala
blob: e381ae99f794707ce3d658ef2ecdcead9412774a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
/* 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.Plugins;

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 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 const string SERVICE_DISCLAIMER = "<b>This product uses the Flickr API but is not endorsed or certified by SmugMug, Inc.</b>";

    internal class AuthenticationRequestTransaction : Publishing.RESTSupport.OAuth1.Transaction {
        public AuthenticationRequestTransaction(Publishing.RESTSupport.OAuth1.Session session, string cookie) {
            base.with_uri(session, "https://www.flickr.com/services/oauth/request_token",
                    Publishing.RESTSupport.HttpMethod.GET);
            add_argument("oauth_callback", "shotwell-oauth2://localhost?sw_auth_cookie=%s".printf(cookie));
        }
    }

    internal class AccessTokenFetchTransaction : Publishing.RESTSupport.OAuth1.Transaction {
        public AccessTokenFetchTransaction(Publishing.RESTSupport.OAuth1.Session session, string user_verifier, string cookie) {
            base.with_uri(session, "https://www.flickr.com/services/oauth/access_token",
                    Publishing.RESTSupport.HttpMethod.GET);
            add_argument("oauth_verifier", user_verifier);
            add_argument("oauth_token", session.get_request_phase_token());
            add_argument("oauth_callback", "shotwell-oauth2://localhost?sw_auth_cookie=%s".printf(cookie));
        }
    }

    internal class Flickr : Publishing.Authenticator.Shotwell.OAuth1.Authenticator {
        private Common.ExternalWebPane pane;
        private string auth_cookie = Uuid.string_random();

        public Flickr(Spit.Publishing.PluginHost host) {
            base("Flickr", API_KEY, API_SECRET, host);
        }

        public override 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 override bool can_logout() {
            return true;
        }

        public override void logout () {
            session.deauthenticate();
            invalidate_persistent_session();
        }

        public override 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("%s\n\n%s".printf(SERVICE_WELCOME_MESSAGE, SERVICE_DISCLAIMER), 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.begin();
        }

        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, auth_cookie);
            try {
                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);

                this.authentication_failed();
            }
        }

        private void do_parse_token_info_from_auth_request(string response) {
            debug("ACTION: parsing authorization request response '%s' into token and secret", response);

            string? oauth_token = null;
            string? oauth_token_secret = null;

            var data = Soup.Form.decode(response);
            data.lookup_extended("oauth_token", null, out oauth_token);
            data.lookup_extended("oauth_token_secret", null, out oauth_token_secret);

            if (oauth_token == null || oauth_token_secret == null)
                host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(
                            "'%s' isn't a valid response to an OAuth authentication request", response));


            on_authentication_token_available(oauth_token, oauth_token_secret);
        }

        private void on_authentication_token_available(string token, string token_secret) {
            debug("EVENT: OAuth authentication token (%s) and token secret (%s) available",
                    token, token_secret);

            session.set_request_phase_credentials(token, token_secret);

            do_web_authentication.begin(token);
        }

        private class AuthCallback : Spit.Publishing.AuthenticatedCallback, Object {
            public signal void auth(GLib.HashTable<string, string> params);

            public void authenticated(GLib.HashTable<string, string> params) {
                auth(params);
            }
        }

        private async void do_web_authentication(string token) {
            var uri = "https://www.flickr.com/services/oauth/authorize?oauth_token=%s&perms=write".printf(token);
            pane = new Common.ExternalWebPane(uri);
            host.install_dialog_pane(pane);
            var auth_callback = new AuthCallback();
            string? web_auth_code = null;
            auth_callback.auth.connect((prm) => {
                if ("oauth_verifier" in prm) {
                    web_auth_code = prm["oauth_verifier"];
                }
                do_web_authentication.callback();
            });
            host.register_auth_callback(auth_cookie, auth_callback);
            yield;
            host.unregister_auth_callback(auth_cookie);
            yield do_verify_pin(web_auth_code);
        }

        private async void do_verify_pin(string pin) {
            debug("ACTION: validating authorization PIN %s", pin);

            host.set_service_locked(true);
            host.install_static_message_pane(_("Verifying authorization…"));

            AccessTokenFetchTransaction txn = new AccessTokenFetchTransaction(session, pin, auth_cookie);

            try {
                yield txn.execute_async();
                debug("EVENT: fetching OAuth access token over the network succeeded");

                do_extract_access_phase_credentials_from_response(txn.get_response());
            } catch (Error err) {
                debug("EVENT: fetching OAuth access token over the network caused an error.");

                host.post_error(err);
                this.authentication_failed();
            }
        }

        private void do_extract_access_phase_credentials_from_response(string response) {
            debug("ACTION: extracting access phase credentials from '%s'", response);

            string? token = null;
            string? token_secret = null;
            string? username = 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);
            }
        }

    }
}