1/* Copyright 2016 Software Freedom Conservancy Inc. 2 * 3 * This software is licensed under the GNU Lesser General Public License 4 * (version 2.1 or later). See the COPYING file in this distribution. 5 */ 6 7using Shotwell; 8using Shotwell.Plugins; 9 10namespace Publishing.Authenticator.Shotwell.Facebook { 11 private const string APPLICATION_ID = "1612018629063184"; 12 13 private class WebAuthenticationPane : Common.WebAuthenticationPane { 14 private static bool cache_dirty = false; 15 16 public signal void login_succeeded(string success_url); 17 public signal void login_failed(); 18 19 public WebAuthenticationPane() { 20 Object (login_uri : get_login_url ()); 21 } 22 23 private class LocaleLookup { 24 public string prefix; 25 public string translation; 26 public string? exception_code; 27 public string? exception_translation; 28 public string? exception_code_2; 29 public string? exception_translation_2; 30 31 public LocaleLookup(string prefix, string translation, string? exception_code = null, 32 string? exception_translation = null, string? exception_code_2 = null, 33 string? exception_translation_2 = null) { 34 this.prefix = prefix; 35 this.translation = translation; 36 this.exception_code = exception_code; 37 this.exception_translation = exception_translation; 38 this.exception_code_2 = exception_code_2; 39 this.exception_translation_2 = exception_translation_2; 40 } 41 42 } 43 44 private static LocaleLookup[] locale_lookup_table = { 45 new LocaleLookup( "es", "es-la", "ES", "es-es" ), 46 new LocaleLookup( "en", "en-gb", "US", "en-us" ), 47 new LocaleLookup( "fr", "fr-fr", "CA", "fr-ca" ), 48 new LocaleLookup( "pt", "pt-br", "PT", "pt-pt" ), 49 new LocaleLookup( "zh", "zh-cn", "HK", "zh-hk", "TW", "zh-tw" ), 50 new LocaleLookup( "af", "af-za" ), 51 new LocaleLookup( "ar", "ar-ar" ), 52 new LocaleLookup( "nb", "nb-no" ), 53 new LocaleLookup( "no", "nb-no" ), 54 new LocaleLookup( "id", "id-id" ), 55 new LocaleLookup( "ms", "ms-my" ), 56 new LocaleLookup( "ca", "ca-es" ), 57 new LocaleLookup( "cs", "cs-cz" ), 58 new LocaleLookup( "cy", "cy-gb" ), 59 new LocaleLookup( "da", "da-dk" ), 60 new LocaleLookup( "de", "de-de" ), 61 new LocaleLookup( "tl", "tl-ph" ), 62 new LocaleLookup( "ko", "ko-kr" ), 63 new LocaleLookup( "hr", "hr-hr" ), 64 new LocaleLookup( "it", "it-it" ), 65 new LocaleLookup( "lt", "lt-lt" ), 66 new LocaleLookup( "hu", "hu-hu" ), 67 new LocaleLookup( "nl", "nl-nl" ), 68 new LocaleLookup( "ja", "ja-jp" ), 69 new LocaleLookup( "nb", "nb-no" ), 70 new LocaleLookup( "no", "nb-no" ), 71 new LocaleLookup( "pl", "pl-pl" ), 72 new LocaleLookup( "ro", "ro-ro" ), 73 new LocaleLookup( "ru", "ru-ru" ), 74 new LocaleLookup( "sk", "sk-sk" ), 75 new LocaleLookup( "sl", "sl-si" ), 76 new LocaleLookup( "sv", "sv-se" ), 77 new LocaleLookup( "th", "th-th" ), 78 new LocaleLookup( "vi", "vi-vn" ), 79 new LocaleLookup( "tr", "tr-tr" ), 80 new LocaleLookup( "el", "el-gr" ), 81 new LocaleLookup( "bg", "bg-bg" ), 82 new LocaleLookup( "sr", "sr-rs" ), 83 new LocaleLookup( "he", "he-il" ), 84 new LocaleLookup( "hi", "hi-in" ), 85 new LocaleLookup( "bn", "bn-in" ), 86 new LocaleLookup( "pa", "pa-in" ), 87 new LocaleLookup( "ta", "ta-in" ), 88 new LocaleLookup( "te", "te-in" ), 89 new LocaleLookup( "ml", "ml-in" ) 90 }; 91 92 private static string get_system_locale_as_facebook_locale() { 93 unowned string? raw_system_locale = Intl.setlocale(LocaleCategory.ALL, ""); 94 if (raw_system_locale == null || raw_system_locale == "") 95 return "www"; 96 97 string system_locale = raw_system_locale.split(".")[0]; 98 99 foreach (LocaleLookup locale_lookup in locale_lookup_table) { 100 if (!system_locale.has_prefix(locale_lookup.prefix)) 101 continue; 102 103 if (locale_lookup.exception_code != null) { 104 assert(locale_lookup.exception_translation != null); 105 106 if (system_locale.contains(locale_lookup.exception_code)) 107 return locale_lookup.exception_translation; 108 } 109 110 if (locale_lookup.exception_code_2 != null) { 111 assert(locale_lookup.exception_translation_2 != null); 112 113 if (system_locale.contains(locale_lookup.exception_code_2)) 114 return locale_lookup.exception_translation_2; 115 } 116 117 return locale_lookup.translation; 118 } 119 120 // default 121 return "www"; 122 } 123 124 private static string get_login_url() { 125 var facebook_locale = get_system_locale_as_facebook_locale(); 126 127 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); 128 } 129 130 public override void on_page_load() { 131 string loaded_url = get_view ().uri.dup(); 132 debug("loaded url: " + loaded_url); 133 134 // strip parameters from the loaded url 135 if (loaded_url.contains("?")) { 136 int index = loaded_url.index_of_char('?'); 137 string params = loaded_url[index:loaded_url.length]; 138 loaded_url = loaded_url.replace(params, ""); 139 } 140 141 // were we redirected to the facebook login success page? 142 if (loaded_url.contains("login_success")) { 143 cache_dirty = true; 144 login_succeeded(get_view ().uri); 145 return; 146 } 147 148 // were we redirected to the login total failure page? 149 if (loaded_url.contains("login_failure")) { 150 login_failed(); 151 return; 152 } 153 } 154 155 public static bool is_cache_dirty() { 156 return cache_dirty; 157 } 158 } 159 160 internal class Facebook : Spit.Publishing.Authenticator, GLib.Object { 161 private Spit.Publishing.PluginHost host; 162 private Publishing.Authenticator.Shotwell.Facebook.WebAuthenticationPane web_auth_pane = null; 163 private GLib.HashTable<string, Variant> params; 164 165 private const string SERVICE_WELCOME_MESSAGE = 166 _("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."); 167 private const string RESTART_ERROR_MESSAGE = 168 _("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."); 169 170 /* Interface functions */ 171 public Facebook(Spit.Publishing.PluginHost host) { 172 this.host = host; 173 this.params = new GLib.HashTable<string, Variant>(str_hash, str_equal); 174 } 175 176 public void authenticate() { 177 // Do we have saved user credentials? If so, go ahead and authenticate the session 178 // with the saved credentials and proceed with the publishing interaction. Otherwise, show 179 // the Welcome pane 180 if (is_persistent_session_valid()) { 181 var access_token = get_persistent_access_token(); 182 this.params.insert("AccessToken", new Variant.string(access_token)); 183 this.authenticated(); 184 return; 185 } 186 187 // FIXME: Find a way for a proper logout 188 if (WebAuthenticationPane.is_cache_dirty()) { 189 host.set_service_locked(false); 190 host.install_static_message_pane(RESTART_ERROR_MESSAGE, 191 Spit.Publishing.PluginHost.ButtonMode.CANCEL); 192 } else { 193 this.do_show_service_welcome_pane(); 194 } 195 } 196 197 public bool can_logout() { 198 return true; 199 } 200 201 public GLib.HashTable<string, Variant> get_authentication_parameter() { 202 return this.params; 203 } 204 205 public void invalidate_persistent_session() { 206 debug("invalidating saved Facebook session."); 207 set_persistent_access_token(""); 208 } 209 210 public void logout() { 211 invalidate_persistent_session(); 212 } 213 214 public void refresh() { 215 // No-Op with Flickr 216 } 217 218 /* Private functions */ 219 private bool is_persistent_session_valid() { 220 string? token = get_persistent_access_token(); 221 222 if (token != null) 223 debug("existing Facebook session found in configuration database (access_token = %s).", 224 token); 225 else 226 debug("no existing Facebook session available."); 227 228 return token != null; 229 } 230 231 private string? get_persistent_access_token() { 232 return host.get_config_string("access_token", null); 233 } 234 235 private void set_persistent_access_token(string access_token) { 236 host.set_config_string("access_token", access_token); 237 } 238 239 private void do_show_service_welcome_pane() { 240 debug("ACTION: showing service welcome pane."); 241 242 host.install_welcome_pane(SERVICE_WELCOME_MESSAGE, on_login_clicked); 243 host.set_service_locked(false); 244 } 245 246 private void on_login_clicked() { 247 debug("EVENT: user clicked 'Login' on welcome pane."); 248 249 do_hosted_web_authentication(); 250 } 251 252 private void do_hosted_web_authentication() { 253 debug("ACTION: doing hosted web authentication."); 254 255 this.host.set_service_locked(false); 256 257 this.web_auth_pane = new WebAuthenticationPane(); 258 this.web_auth_pane.login_succeeded.connect(on_web_auth_pane_login_succeeded); 259 this.web_auth_pane.login_failed.connect(on_web_auth_pane_login_failed); 260 261 this.host.install_dialog_pane(this.web_auth_pane, 262 Spit.Publishing.PluginHost.ButtonMode.CANCEL); 263 264 } 265 266 private void on_web_auth_pane_login_succeeded(string success_url) { 267 debug("EVENT: hosted web login succeeded."); 268 269 do_authenticate_session(success_url); 270 } 271 272 private void on_web_auth_pane_login_failed() { 273 debug("EVENT: hosted web login failed."); 274 275 // In this case, "failed" doesn't mean that the user didn't enter the right username and 276 // password -- Facebook handles that case inside the Facebook Connect web control. Instead, 277 // it means that no session was initiated in response to our login request. The only 278 // way this happens is if the user clicks the "Cancel" button that appears inside 279 // the web control. In this case, the correct behavior is to return the user to the 280 // service welcome pane so that they can start the web interaction again. 281 do_show_service_welcome_pane(); 282 } 283 284 private void do_authenticate_session(string good_login_uri) { 285 debug("ACTION: preparing to extract session information encoded in uri = '%s'", 286 good_login_uri); 287 288 // the raw uri is percent-encoded, so decode it 289 string decoded_uri = Soup.URI.decode(good_login_uri); 290 291 // locate the access token within the URI 292 string? access_token = null; 293 int index = decoded_uri.index_of("#access_token="); 294 if (index >= 0) 295 access_token = decoded_uri[index:decoded_uri.length]; 296 if (access_token == null) { 297 host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( 298 "Server redirect URL contained no access token")); 299 return; 300 } 301 302 // remove any trailing parameters from the session description string 303 string? trailing_params = null; 304 index = access_token.index_of_char('&'); 305 if (index >= 0) 306 trailing_params = access_token[index:access_token.length]; 307 if (trailing_params != null) 308 access_token = access_token.replace(trailing_params, ""); 309 310 // remove the key from the session description string 311 access_token = access_token.replace("#access_token=", ""); 312 this.params.insert("AccessToken", new Variant.string(access_token)); 313 set_persistent_access_token(access_token); 314 315 this.authenticated(); 316 } 317 } 318} // namespace Publishing.Facebook; 319