690dcef5a6d22fc92fbc4f2f8ff33a922150d5b8
[hermes] / package / src / org / maemo / hermes / engine / provider_oauth.py
1 from oauth import oauth
2 import gnome.gconf
3 import gobject
4 import gtk
5 import hildon
6 import org.maemo.hermes.engine.provider
7 import time
8 import thread
9 import httplib
10 import re
11 import webbrowser
12
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
16        methods:
17        
18            * get_name
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]
23
24        Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
25        Released under the Artistic Licence."""
26
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'
31        
32     # -----------------------------------------------------------------------
33     def __init__(self):
34         """Initialise the provider, and ensure the environment is going to work."""
35
36         self._gc = gnome.gconf.client_get_default()
37         
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()))
42
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()
46         
47         
48     # -----------------------------------------------------------------------
49     def get_urls(self):
50         """Return a tuple containing request token, access token & authorize URLs."""
51         
52         return (None, None, None)
53
54     
55     # -----------------------------------------------------------------------
56     def get_account_detail(self):
57         """Return the name of the linked LinkedIn account."""
58         
59         return self._gc.get_string(self.GCONF_USER % (self.get_id()))
60     
61     
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."""
66            
67         return True
68     
69     
70     # -----------------------------------------------------------------------
71     def open_preferences(self, parent):
72         """Open the preferences for this provider as a child of the 'parent' widget."""
73
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)
78     
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)
83             
84         dialog.vbox.add(button)
85         self.additional_prefs(dialog)
86         dialog.vbox.add(gtk.Label(""))
87         
88         dialog.show_all()
89         result = dialog.run()
90         dialog.hide()
91         
92         self.handle_prefs_response(result)
93         if result == gtk.RESPONSE_CANCEL or result == gtk.RESPONSE_DELETE_EVENT:
94             return None
95     
96         return result == gtk.RESPONSE_YES
97
98
99     # -----------------------------------------------------------------------
100     def additional_prefs(self, dialog):
101         """Override to add additional controls to the authentication dialogue."""
102         
103         dialog.vbox.add(gtk.Label(""))
104         dialog.vbox.add(gtk.Label(""))
105
106
107     # -----------------------------------------------------------------------
108     def handle_prefs_response(self, result):
109         """Override to handle the response from a dialogue button."""
110         
111         None
112
113
114     # -----------------------------------------------------------------------
115     def _handle_button(self, e, button, enable):
116         """Ensure the button state is correct."""
117         
118         authenticated = self._get_access_token_from_gconf() is not None
119         if e is not None:
120             if authenticated:
121                 self._remove_access_token_from_gconf()
122             else:
123                 self.authenticate(lambda: None, self.block_for_auth)
124         
125             authenticated = self._get_access_token_from_gconf() is not None
126         
127         button.set_title(authenticated and _("Clear authorisation") or _("Authorise"))
128         enable.set_sensitive(authenticated)
129
130         
131     # -----------------------------------------------------------------------
132     def block_for_auth(self, url):
133         """Part of the GUI callback API."""
134
135         webbrowser.open(url)
136         time.sleep(3)
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"))
143         note.vbox.add(input)
144
145         note.show_all()
146         result = note.run()
147         note.hide()
148         if result == gtk.RESPONSE_OK:
149             return input.get_text()
150         else:
151             return None
152
153
154     # -----------------------------------------------------------------------
155     def authenticate(self, need_auth, block_for_auth):
156         need_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)
163
164
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")
169         
170         self._gc.set_string(self.GCONF_ACCESS_TOKEN % (self.get_id()), token_str)
171
172         
173     # -----------------------------------------------------------------------
174     def _get_access_token_from_gconf(self):
175         """Returns an oauth.OAuthToken, or None if the gconf value is empty"""
176         
177         token_str = self._gc.get_string(self.GCONF_ACCESS_TOKEN % (self.get_id()))
178         if not token_str or len(token_str) < 8:
179             return None
180         try:
181             return oauth.OAuthToken.from_string(token_str)
182         except KeyError, e:
183             print token_str
184             import traceback
185             traceback.print_exc()
186             return None
187
188
189     # -----------------------------------------------------------------------
190     def _remove_access_token_from_gconf(self):
191         """Remove the oauth.OAuthToken, if any."""
192         
193         self._gc.unset(self.GCONF_ACCESS_TOKEN % (self.get_id()))
194         self._gc.unset(self.GCONF_USER % (self.get_id()))
195
196
197     # -----------------------------------------------------------------------
198     def _get_request_token(self):
199         """Get a request token from OAuth provider."""
200         
201         url = self.get_urls()[0]
202         hostname = self._get_hostname_from_url(url)
203
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)
206
207         connection = httplib.HTTPSConnection(hostname)
208         connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
209         response = connection.getresponse().read()
210         
211         try:
212             token = oauth.OAuthToken.from_string(response)
213         except Exception, e:
214             import traceback
215             traceback.print_exc()
216             print response
217             raise Exception("Authorization failure - failed to get request token")
218         return token
219
220     
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"""
225         
226         oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.get_urls()[2])
227         return oauth_request.to_url()
228
229
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."""
235         
236         url = self.get_urls()[1]
237         hostname = self._get_hostname_from_url(url)
238         
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)
241
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)
250     
251
252     # -----------------------------------------------------------------------
253     def make_api_request(self, url):
254         hostname = self._get_hostname_from_url(url)
255
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)
259         try:
260             connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
261             xml = connection.getresponse().read()
262             return xml
263         except:
264             raise Exception("Failed to contact LinkedIn at " + url)
265
266
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)
271