1 from oauth import oauth
6 import org.maemo.hermes.engine.provider
13 class OAuthProvider(org.maemo.hermes.engine.provider.Provider):
14 """Basis for OAuth services for Hermes. Sub-classes of this should
15 install keys to '<id>_...', and implement the following
19 * get_urls (tuple of request token URL, access token URL & authorize URL)
20 * verify_verifier (returns name of authenticated user)
21 * additional_prefs [optional]
22 * handle_prefs_button [optional]
24 Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
25 Released under the Artistic Licence."""
27 GCONF_API_KEY = '/apps/maemo/hermes/%s_key'
28 GCONF_API_SECRET = '/apps/maemo/hermes/%s_secret'
29 GCONF_ACCESS_TOKEN = '/apps/maemo/hermes/%s_access_token'
30 GCONF_USER = '/apps/maemo/hermes/%s_user'
32 # -----------------------------------------------------------------------
34 """Initialise the provider, and ensure the environment is going to work."""
36 self._gc = gnome.gconf.client_get_default()
38 api_key = self._gc.get_string(self.GCONF_API_KEY % (self.get_id()))
39 secret_key = self._gc.get_string(self.GCONF_API_SECRET % (self.get_id()))
40 if api_key is None or secret_key is None:
41 raise Exception('No application keys found for %s. Installation error.' % (self.get_id()))
43 self.access_token = self._get_access_token_from_gconf()
44 self.consumer = oauth.OAuthConsumer(api_key, secret_key)
45 self.sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
48 # -----------------------------------------------------------------------
50 """Return a tuple containing request token, access token & authorize URLs."""
52 return (None, None, None)
55 # -----------------------------------------------------------------------
56 def get_account_detail(self):
57 """Return the name of the linked LinkedIn account."""
59 return self._gc.get_string(self.GCONF_USER % (self.get_id()))
62 # -----------------------------------------------------------------------
63 def has_preferences(self):
64 """Whether or not this provider has any preferences. If it does not,
65 open_preferences must NOT be called; as the behaviour is undetermined."""
70 # -----------------------------------------------------------------------
71 def open_preferences(self, parent):
72 """Open the preferences for this provider as a child of the 'parent' widget."""
74 self.main_window = parent
75 dialog = gtk.Dialog(self.get_name(), parent)
76 dialog.add_button(_('Disable'), gtk.RESPONSE_NO)
77 enable = dialog.add_button(_('Enable'), gtk.RESPONSE_YES)
79 button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
80 hildon.BUTTON_ARRANGEMENT_VERTICAL)
81 self._handle_button(None, button, enable)
82 button.connect('clicked', self._handle_button, button, enable)
84 dialog.vbox.add(button)
85 self.additional_prefs(dialog)
86 dialog.vbox.add(gtk.Label(""))
92 self.handle_prefs_response(result)
93 if result == gtk.RESPONSE_CANCEL or result == gtk.RESPONSE_DELETE_EVENT:
96 return result == gtk.RESPONSE_YES
99 # -----------------------------------------------------------------------
100 def additional_prefs(self, dialog):
101 """Override to add additional controls to the authentication dialogue."""
103 dialog.vbox.add(gtk.Label(""))
104 dialog.vbox.add(gtk.Label(""))
107 # -----------------------------------------------------------------------
108 def handle_prefs_response(self, result):
109 """Override to handle the response from a dialogue button."""
114 # -----------------------------------------------------------------------
115 def _handle_button(self, e, button, enable):
116 """Ensure the button state is correct."""
118 authenticated = self._get_access_token_from_gconf() is not None
121 self._remove_access_token_from_gconf()
123 self.authenticate(lambda: None, self.block_for_auth)
125 authenticated = self._get_access_token_from_gconf() is not None
127 button.set_title(authenticated and _("Clear authorisation") or _("Authorise"))
128 enable.set_sensitive(authenticated)
131 # -----------------------------------------------------------------------
132 def block_for_auth(self, url):
133 """Part of the GUI callback API."""
137 note = gtk.Dialog(_('Service authorisation'), self.main_window)
138 note.add_button(_("Validate"), gtk.RESPONSE_OK)
139 input = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
140 input.set_property('is-focus', False)
141 input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_NUMERIC)
142 note.set_title(_("Verification code from web browser"))
148 if result == gtk.RESPONSE_OK:
149 return input.get_text()
154 # -----------------------------------------------------------------------
155 def authenticate(self, need_auth, block_for_auth):
157 token = self._get_request_token()
158 url = self._get_authorize_url(token)
159 verifier = block_for_auth(url)
160 self.access_token = self._get_access_token(token, verifier)
161 name = self.verify_verifier(self.access_token)
162 self._gc.set_string(self.GCONF_USER % (self.get_id()), name)
165 # -----------------------------------------------------------------------
166 def _store_access_token_in_gconf(self, token_str):
167 if "oauth_problem" in token_str:
168 raise Exception("Authorization failure - access token reported OAuth problem")
170 self._gc.set_string(self.GCONF_ACCESS_TOKEN % (self.get_id()), token_str)
173 # -----------------------------------------------------------------------
174 def _get_access_token_from_gconf(self):
175 """Returns an oauth.OAuthToken, or None if the gconf value is empty"""
177 token_str = self._gc.get_string(self.GCONF_ACCESS_TOKEN % (self.get_id()))
178 if not token_str or len(token_str) < 8:
181 return oauth.OAuthToken.from_string(token_str)
185 traceback.print_exc()
189 # -----------------------------------------------------------------------
190 def _remove_access_token_from_gconf(self):
191 """Remove the oauth.OAuthToken, if any."""
193 self._gc.unset(self.GCONF_ACCESS_TOKEN % (self.get_id()))
194 self._gc.unset(self.GCONF_USER % (self.get_id()))
197 # -----------------------------------------------------------------------
198 def _get_request_token(self):
199 """Get a request token from OAuth provider."""
201 url = self.get_urls()[0]
202 hostname = self._get_hostname_from_url(url)
204 oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback="oob", http_url=url)
205 oauth_request.sign_request(self.sig_method, self.consumer, None)
207 connection = httplib.HTTPSConnection(hostname)
208 connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
209 response = connection.getresponse().read()
212 token = oauth.OAuthToken.from_string(response)
215 traceback.print_exc()
217 raise Exception("Authorization failure - failed to get request token")
221 # -----------------------------------------------------------------------
222 def _get_authorize_url(self, token):
223 """The URL that the user should browse to, in order to authorize the
224 application's request to access data"""
226 oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.get_urls()[2])
227 return oauth_request.to_url()
230 # -----------------------------------------------------------------------
231 def _get_access_token(self, token, verifier):
232 """If the verifier (which was displayed in the browser window) is
233 valid, then an access token is returned which should be used to
234 access data on the service."""
236 url = self.get_urls()[1]
237 hostname = self._get_hostname_from_url(url)
239 oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=url)
240 oauth_request.sign_request(self.sig_method, self.consumer, token)
242 connection = httplib.HTTPSConnection(hostname)
243 connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
244 response = connection.getresponse()
245 token_str = response.read()
246 if 'oauth_problem' in token_str:
247 raise Exception("Authorization failure - failed to get access token (" + token_str + ")")
248 self._store_access_token_in_gconf(token_str)
249 return oauth.OAuthToken.from_string(token_str)
252 # -----------------------------------------------------------------------
253 def make_api_request(self, url):
254 hostname = self._get_hostname_from_url(url)
256 oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
257 oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
258 connection = httplib.HTTPSConnection(hostname)
260 connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
261 xml = connection.getresponse().read()
264 raise Exception("Failed to contact LinkedIn at " + url)
267 # -----------------------------------------------------------------------
268 def _get_hostname_from_url(self, url):
269 """Extract the hostname from a URL."""
270 return re.sub(r'^\w+://(.+?)[/:].*$', r'\1', url)