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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
|
/* 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) {
base.with_uri(session, "https://www.flickr.com/services/oauth/request_token",
Publishing.RESTSupport.HttpMethod.GET);
add_argument("oauth_callback", "shotwell-auth://local-callback");
}
}
internal class AccessTokenFetchTransaction : Publishing.RESTSupport.OAuth1.Transaction {
public AccessTokenFetchTransaction(Publishing.RESTSupport.OAuth1.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());
add_argument("oauth_callback", "shotwell-auth://local-callback");
}
}
internal class WebAuthenticationPane : Common.WebAuthenticationPane {
private string? auth_code = null;
private const string LOGIN_URI = "https://www.flickr.com/services/oauth/authorize?oauth_token=%s&perms=write";
public signal void authorized(string auth_code);
public signal void error();
public WebAuthenticationPane(string token) {
Object(login_uri : LOGIN_URI.printf(token));
}
public override void constructed() {
base.constructed();
var ctx = WebKit.WebContext.get_default();
ctx.register_uri_scheme("shotwell-auth", this.on_shotwell_auth_request_cb);
var mgr = ctx.get_security_manager();
mgr.register_uri_scheme_as_secure("shotwell-auth");
mgr.register_uri_scheme_as_cors_enabled("shotwell-auth");
}
public override void on_page_load() {
if (this.load_error != null) {
this.error();
return;
}
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) {
this.authorized(this.auth_code);
}
}
private void on_shotwell_auth_request_cb(WebKit.URISchemeRequest request) {
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);
request.finish(mins, -1, "text/plain");
}
}
internal class Flickr : Publishing.Authenticator.Shotwell.OAuth1.Authenticator {
private WebAuthenticationPane pane;
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);
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(token);
}
private void do_web_authentication(string token) {
pane = new WebAuthenticationPane(token);
host.install_dialog_pane(pane);
pane.authorized.connect((pin) => { this.do_verify_pin.begin(pin); });
pane.error.connect(this.on_web_login_error);
}
private void on_web_login_error() {
if (pane.load_error != null) {
host.post_error(pane.load_error);
return;
}
host.post_error(new Spit.Publishing.PublishingError.PROTOCOL_ERROR(_("Flickr authorization failed")));
}
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);
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);
}
}
}
}
|