summaryrefslogtreecommitdiff
path: root/plugins/authenticator/shotwell/FacebookPublishingAuthenticator.vala
blob: 26a2363e94a8fffc2f85e49b498107199e051774 (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
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/* 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;
using Shotwell.Plugins;

namespace Publishing.Authenticator.Shotwell.Facebook {
    private const string APPLICATION_ID = "1612018629063184";

    private class WebAuthenticationPane : Common.WebAuthenticationPane {
        private static bool cache_dirty = false;

        public signal void login_succeeded(string success_url);
        public signal void login_failed();

        public WebAuthenticationPane() {
            Object (login_uri : get_login_url ());
        }

        private class LocaleLookup {
            public string prefix;
            public string translation;
            public string? exception_code;
            public string? exception_translation;
            public string? exception_code_2;
            public string? exception_translation_2;

            public LocaleLookup(string prefix, string translation, string? exception_code = null,
                string? exception_translation  = null, string? exception_code_2  = null,
                string? exception_translation_2 = null) {
                this.prefix = prefix;
                this.translation = translation;
                this.exception_code = exception_code;
                this.exception_translation = exception_translation;
                this.exception_code_2 = exception_code_2;
                this.exception_translation_2 = exception_translation_2;
            }

        }

        private static LocaleLookup[] locale_lookup_table = {
            new LocaleLookup( "es", "es-la", "ES", "es-es" ),
            new LocaleLookup( "en", "en-gb", "US", "en-us" ),
            new LocaleLookup( "fr", "fr-fr", "CA", "fr-ca" ),
            new LocaleLookup( "pt", "pt-br", "PT", "pt-pt" ),
            new LocaleLookup( "zh", "zh-cn", "HK", "zh-hk", "TW", "zh-tw" ),
            new LocaleLookup( "af", "af-za" ),
            new LocaleLookup( "ar", "ar-ar" ),
            new LocaleLookup( "nb", "nb-no" ),
            new LocaleLookup( "no", "nb-no" ),
            new LocaleLookup( "id", "id-id" ),
            new LocaleLookup( "ms", "ms-my" ),
            new LocaleLookup( "ca", "ca-es" ),
            new LocaleLookup( "cs", "cs-cz" ),
            new LocaleLookup( "cy", "cy-gb" ),
            new LocaleLookup( "da", "da-dk" ),
            new LocaleLookup( "de", "de-de" ),
            new LocaleLookup( "tl", "tl-ph" ),
            new LocaleLookup( "ko", "ko-kr" ),
            new LocaleLookup( "hr", "hr-hr" ),
            new LocaleLookup( "it", "it-it" ),
            new LocaleLookup( "lt", "lt-lt" ),
            new LocaleLookup( "hu", "hu-hu" ),
            new LocaleLookup( "nl", "nl-nl" ),
            new LocaleLookup( "ja", "ja-jp" ),
            new LocaleLookup( "nb", "nb-no" ),
            new LocaleLookup( "no", "nb-no" ),
            new LocaleLookup( "pl", "pl-pl" ),
            new LocaleLookup( "ro", "ro-ro" ),
            new LocaleLookup( "ru", "ru-ru" ),
            new LocaleLookup( "sk", "sk-sk" ),
            new LocaleLookup( "sl", "sl-si" ),
            new LocaleLookup( "sv", "sv-se" ),
            new LocaleLookup( "th", "th-th" ),
            new LocaleLookup( "vi", "vi-vn" ),
            new LocaleLookup( "tr", "tr-tr" ),
            new LocaleLookup( "el", "el-gr" ),
            new LocaleLookup( "bg", "bg-bg" ),
            new LocaleLookup( "sr", "sr-rs" ),
            new LocaleLookup( "he", "he-il" ),
            new LocaleLookup( "hi", "hi-in" ),
            new LocaleLookup( "bn", "bn-in" ),
            new LocaleLookup( "pa", "pa-in" ),
            new LocaleLookup( "ta", "ta-in" ),
            new LocaleLookup( "te", "te-in" ),
            new LocaleLookup( "ml", "ml-in" )
        };

        private static string get_system_locale_as_facebook_locale() {
            unowned string? raw_system_locale = Intl.setlocale(LocaleCategory.ALL, "");
            if (raw_system_locale == null || raw_system_locale == "")
                return "www";

            string system_locale = raw_system_locale.split(".")[0];

            foreach (LocaleLookup locale_lookup in locale_lookup_table) {
                if (!system_locale.has_prefix(locale_lookup.prefix))
                    continue;

                if (locale_lookup.exception_code != null) {
                    assert(locale_lookup.exception_translation != null);

                    if (system_locale.contains(locale_lookup.exception_code))
                        return locale_lookup.exception_translation;
                }

                if (locale_lookup.exception_code_2 != null) {
                    assert(locale_lookup.exception_translation_2 != null);

                    if (system_locale.contains(locale_lookup.exception_code_2))
                        return locale_lookup.exception_translation_2;
                }

                return locale_lookup.translation;
            }

            // default
            return "www";
        }

        private static string get_login_url() {
            var facebook_locale = get_system_locale_as_facebook_locale();

            return "https://%s.facebook.com/dialog/oauth?client_id=%s&redirect_uri=https://www.facebook.com/connect/login_success.html&display=popup&scope=publish_actions,user_photos,user_videos&response_type=token".printf(facebook_locale, APPLICATION_ID);
        }

        public override void on_page_load() {
            string loaded_url = get_view ().uri.dup();
            debug("loaded url: " + loaded_url);

            // strip parameters from the loaded url
            if (loaded_url.contains("?")) {
                int index = loaded_url.index_of_char('?');
                string params = loaded_url[index:loaded_url.length];
                loaded_url = loaded_url.replace(params, "");
            }

            // were we redirected to the facebook login success page?
            if (loaded_url.contains("login_success")) {
                cache_dirty = true;
                login_succeeded(get_view ().uri);
                return;
            }

            // were we redirected to the login total failure page?
            if (loaded_url.contains("login_failure")) {
                login_failed();
                return;
            }
        }

        public static bool is_cache_dirty() {
            return cache_dirty;
        }
    }

    internal class Facebook : Spit.Publishing.Authenticator, GLib.Object {
        private Spit.Publishing.PluginHost host;
        private Publishing.Authenticator.Shotwell.Facebook.WebAuthenticationPane web_auth_pane = null;
        private GLib.HashTable<string, Variant> params;

        private const string SERVICE_WELCOME_MESSAGE =
    _("You are not currently logged into Facebook.\n\nIf you don’t yet have a Facebook account, you can create one during the login process. During login, Shotwell Connect may ask you for permission to upload photos and publish to your feed. These permissions are required for Shotwell Connect to function.");
        private const string RESTART_ERROR_MESSAGE =
    _("You have already logged in and out of Facebook during this Shotwell session.\nTo continue publishing to Facebook, quit and restart Shotwell, then try publishing again.");

        /* Interface functions */
        public Facebook(Spit.Publishing.PluginHost host) {
            this.host = host;
            this.params = new GLib.HashTable<string, Variant>(str_hash, str_equal);
        }

        public void authenticate() {
            // Do we have saved user credentials? If so, go ahead and authenticate the session
            // with the saved credentials and proceed with the publishing interaction. Otherwise, show
            // the Welcome pane
            if (is_persistent_session_valid()) {
                var access_token = get_persistent_access_token();
                this.params.insert("AccessToken", new Variant.string(access_token));
                this.authenticated();
                return;
            }

            // FIXME: Find a way for a proper logout
            if (WebAuthenticationPane.is_cache_dirty()) {
                host.set_service_locked(false);
                host.install_static_message_pane(RESTART_ERROR_MESSAGE,
                                                 Spit.Publishing.PluginHost.ButtonMode.CANCEL);
            } else {
                this.do_show_service_welcome_pane();
            }
        }

        public bool can_logout() {
            return true;
        }

        public GLib.HashTable<string, Variant> get_authentication_parameter() {
            return this.params;
        }

        public void invalidate_persistent_session() {
            debug("invalidating saved Facebook session.");
            set_persistent_access_token("");
        }

        public void logout() {
            invalidate_persistent_session();
        }

        public void refresh() {
            // No-Op with Flickr
        }

        /* Private functions */
        private bool is_persistent_session_valid() {
            string? token = get_persistent_access_token();

            if (token != null)
                debug("existing Facebook session found in configuration database (access_token = %s).",
                        token);
            else
                debug("no existing Facebook session available.");

            return token != null;
        }

        private string? get_persistent_access_token() {
            return host.get_config_string("access_token", null);
        }

        private void set_persistent_access_token(string access_token) {
            host.set_config_string("access_token", access_token);
        }

        private void do_show_service_welcome_pane() {
            debug("ACTION: showing service welcome pane.");

            host.install_welcome_pane(SERVICE_WELCOME_MESSAGE, on_login_clicked);
            host.set_service_locked(false);
        }

        private void on_login_clicked() {
            debug("EVENT: user clicked 'Login' on welcome pane.");

            do_hosted_web_authentication();
        }

        private void do_hosted_web_authentication() {
            debug("ACTION: doing hosted web authentication.");

            this.host.set_service_locked(false);

            this.web_auth_pane = new WebAuthenticationPane();
            this.web_auth_pane.login_succeeded.connect(on_web_auth_pane_login_succeeded);
            this.web_auth_pane.login_failed.connect(on_web_auth_pane_login_failed);

            this.host.install_dialog_pane(this.web_auth_pane,
                                          Spit.Publishing.PluginHost.ButtonMode.CANCEL);

        }

        private void on_web_auth_pane_login_succeeded(string success_url) {
            debug("EVENT: hosted web login succeeded.");

            do_authenticate_session(success_url);
        }

        private void on_web_auth_pane_login_failed() {
            debug("EVENT: hosted web login failed.");

            // In this case, "failed" doesn't mean that the user didn't enter the right username and
            // password -- Facebook handles that case inside the Facebook Connect web control. Instead,
            // it means that no session was initiated in response to our login request. The only
            // way this happens is if the user clicks the "Cancel" button that appears inside
            // the web control. In this case, the correct behavior is to return the user to the
            // service welcome pane so that they can start the web interaction again.
            do_show_service_welcome_pane();
        }

        private void do_authenticate_session(string good_login_uri) {
            debug("ACTION: preparing to extract session information encoded in uri = '%s'",
                 good_login_uri);

            // the raw uri is percent-encoded, so decode it
            string decoded_uri = Soup.URI.decode(good_login_uri);

            // locate the access token within the URI
            string? access_token = null;
            int index = decoded_uri.index_of("#access_token=");
            if (index >= 0)
                access_token = decoded_uri[index:decoded_uri.length];
            if (access_token == null) {
                host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(
                    "Server redirect URL contained no access token"));
                return;
            }

            // remove any trailing parameters from the session description string
            string? trailing_params = null;
            index = access_token.index_of_char('&');
            if (index >= 0)
                trailing_params = access_token[index:access_token.length];
            if (trailing_params != null)
                access_token = access_token.replace(trailing_params, "");

            // remove the key from the session description string
            access_token = access_token.replace("#access_token=", "");
            this.params.insert("AccessToken", new Variant.string(access_token));
            set_persistent_access_token(access_token);

            this.authenticated();
        }
    }
} // namespace Publishing.Facebook;