Tidy up syncjob references
[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         print token_str
179         if not token_str or len(token_str) < 8:
180             return None
181         return oauth.OAuthToken.from_string(token_str)
182
183
184     # -----------------------------------------------------------------------
185     def _remove_access_token_from_gconf(self):
186         """Remove the oauth.OAuthToken, if any."""
187         
188         self._gc.unset(self.GCONF_ACCESS_TOKEN % (self.get_id()))
189         self._gc.unset(self.GCONF_USER % (self.get_id()))
190
191
192     # -----------------------------------------------------------------------
193     def _get_request_token(self):
194         """Get a request token from OAuth provider."""
195         
196         url = self.get_urls()[0]
197         hostname = self._get_hostname_from_url(url)
198
199         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback="oob", http_url=url)
200         oauth_request.sign_request(self.sig_method, self.consumer, None)
201
202         connection = httplib.HTTPSConnection(hostname)
203         connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
204         response = connection.getresponse().read()
205         
206         try:
207             token = oauth.OAuthToken.from_string(response)
208         except Exception, e:
209             import traceback
210             traceback.print_exc()
211             print response
212             raise Exception("Authorization failure - failed to get request token")
213         return token
214
215     
216     # -----------------------------------------------------------------------
217     def _get_authorize_url(self, token):
218         """The URL that the user should browse to, in order to authorize the 
219            application's request to access data"""
220         
221         oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.get_urls()[2])
222         return oauth_request.to_url()
223
224
225     # -----------------------------------------------------------------------
226     def _get_access_token(self, token, verifier):
227         """If the verifier (which was displayed in the browser window) is 
228            valid, then an access token is returned which should be used to 
229            access data on the service."""
230         
231         url = self.get_urls()[1]
232         hostname = self._get_hostname_from_url(url)
233         
234         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=url)
235         oauth_request.sign_request(self.sig_method, self.consumer, token)
236
237         connection = httplib.HTTPSConnection(hostname)
238         connection.request(oauth_request.http_method, url, headers=oauth_request.to_header()) 
239         response = connection.getresponse()
240         token_str = response.read()
241         if 'oauth_problem' in token_str:
242             raise Exception("Authorization failure - failed to get access token (" + token_str + ")")
243         self._store_access_token_in_gconf(token_str)
244         return oauth.OAuthToken.from_string(token_str)
245     
246
247     # -----------------------------------------------------------------------
248     def make_api_request(self, url):
249         hostname = self._get_hostname_from_url(url)
250
251         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
252         oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
253         connection = httplib.HTTPSConnection(hostname)
254         try:
255             connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
256             xml = connection.getresponse().read()
257             return xml
258         except:
259             raise Exception("Failed to contact LinkedIn at " + url)
260
261
262     # -----------------------------------------------------------------------
263     def _get_hostname_from_url(self, url):
264         """Extract the hostname from a URL."""
265         return re.sub(r'^\w+://(.+?)[/:].*$', r'\1', url)
266