Catch OAuthException since it signals unsubscription.
[libomb:libomb.git] / service_provider.php
1 <?php
2
3 require_once 'constants.php';
4 require_once 'remoteserviceexception.php';
5
6 /**
7  * OMB service realization
8  *
9  * This class realizes a complete, simple OMB service.
10  *
11  * PHP version 5
12  *
13  * LICENSE: This program is free software: you can redistribute it and/or modify
14  * it under the terms of the GNU Affero General Public License as published by
15  * the Free Software Foundation, either version 3 of the License, or
16  * (at your option) any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU Affero General Public License for more details.
22  *
23  * You should have received a copy of the GNU Affero General Public License
24  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
25  *
26  * @package   OMB
27  * @author    Adrian Lang <mail@adrianlang.de>
28  * @copyright 2009 Adrian Lang
29  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
30  **/
31
32 class OMB_Service_Provider {
33   protected $user; /* An OMB_Profile representing the user */
34   protected $datastore; /* AN OMB_Datastore */
35
36   protected $remote_user; /* An OMB_Profile representing the remote user during
37                             the authorization process */
38
39   protected $oauth_server; /* An OAuthServer; should only be accessed via
40                               getOAuthServer. */
41
42   /**
43    * Initialize an OMB_Service_Provider object
44    *
45    * Constructs an OMB_Service_Provider instance that provides OMB services
46    * referring to a particular user.
47    *
48    * @param OMB_Profile   $user         An OMB_Profile; mandatory for XRDS
49    *                                    output, user auth handling and OMB
50    *                                    action performing
51    * @param OMB_Datastore $datastore    An OMB_Datastore; mandatory for
52    *                                    everything but XRDS output
53    * @param OAuthServer   $oauth_server An OAuthServer; used for token writing
54    *                                    and OMB action handling; will use
55    *                                    default value if not set
56    *
57    * @access public
58    **/
59   public function __construct ($user = null, $datastore = null, $oauth_server = null) {
60     $this->user = $user;
61     $this->datastore = $datastore;
62     $this->oauth_server = $oauth_server;
63   }
64
65   /**
66    * Write a XRDS document
67    *
68    * Writes a XRDS document specifying the OMB service. Optionally uses a
69    * given object of a class implementing OMB_XRDS_Writer for output. Else
70    * OMB_Plain_XRDS_Writer is used.
71    *
72    * @param string          $oauth_base_url The base URL for all OAuth services
73    * @param string          $omb_base_url   The base URL for all OMB services
74    * @param OMB_XRDS_Writer $xrds_writer    Optional; The OMB_XRDS_Writer used
75    *                                        to write the XRDS document
76    *
77    * @access public
78    *
79    * @return mixed Depends on the used OMB_XRDS_Writer; OMB_Plain_XRDS_Writer
80    *               returns nothing.
81    **/
82   public function writeXRDS($oauth_base_url, $omb_base_url, $xrds_writer = null) {
83     if ($xrds_writer == null) {
84         require_once 'plain_xrds_writer.php';
85         $xrds_writer = new OMB_Plain_XRDS_Writer();
86     }
87     return $xrds_writer->writeXRDS($this->user, $oauth_base_url, $omb_base_url);
88   }
89
90   /**
91    * Echo a request token
92    *
93    * Outputs an unauthorized request token for the query found in $_GET or
94    * $_POST.
95    *
96    * @access public
97    **/
98   public function writeRequestToken() {
99     OMB_Helper::removeMagicQuotesFromRequest();
100     echo $this->getOAuthServer()->fetch_request_token(OAuthRequest::from_request());
101   }
102
103   /**
104    * Handle an user authorization request.
105    *
106    * Parses an authorization request. This includes OAuth and OMB verification.
107    * Throws exceptions on failures. Returns an OMB_Profile object representing
108    * the remote user.
109    *
110    * @access public
111    *
112    * @return OMB_Profile The profile of the soon-to-be subscribed, i. e. remote
113    *                     user
114    **/
115   public function handleUserAuth() {
116     OMB_Helper::removeMagicQuotesFromRequest();
117
118     /* Verify the request token. */
119
120     $this->token = $this->datastore->lookup_token(null, "request", $_GET['oauth_token']);
121     if (is_null($this->token)) {
122       throw new OAuthException('The given request token has not been issued ' .
123                                'by this service.');
124     }
125
126     /* Verify the OMB part. */
127
128     if ($_GET['omb_version'] !== OMB_VERSION) {
129       throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
130                                    'Wrong OMB version ' . $_GET['omb_version']);
131     }
132
133     if ($_GET['omb_listener'] !== $this->user->getIdentifierURI()) {
134       throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
135                                  'Wrong OMB listener ' . $_GET['omb_listener']);
136     }
137
138     /* Store given callback for later use. */
139     if (isset($_GET['oauth_callback']) && $_GET['oauth_callback'] !== '') {
140       $this->callback = $_GET['oauth_callback'];
141     }
142     $this->remote_user = OMB_Profile::fromParameters($_GET, 'omb_listenee');
143
144     return $this->remote_user;
145   }
146
147   /**
148    * Continue the OAuth dance after user authorization
149    *
150    * Performs the appropriate actions after user answered the authorization
151    * request.
152    *
153    * @param bool $accepted Whether the user granted authorization
154    *
155    * @access public
156    *
157    * @return array A two-component array with the values:
158    *                - callback The callback URL or null if none given
159    *                - token    The authorized request token or null if not
160    *                           authorized.
161    **/
162   public function continueUserAuth($accepted) {
163     $callback = $this->callback;
164     if (!$accepted) {
165       $this->datastore->revoke_token($this->token);
166       $this->token = null;
167       /* TODO: The handling is probably wrong in terms of OAuth 1.0 but the way
168                laconica works. Moreover I don’t know the right way either. */
169
170     } else {
171       $this->datastore->authorize_token($this->token->key);
172       $this->datastore->saveProfile($this->remote_user);
173       $this->datastore->saveSubscription($this->user->getIdentifierURI(),
174                           $this->remote_user->getIdentifierURI(), $this->token);
175
176       if (!is_null($this->callback)) {
177         /* Callback wants to get some informations as well. */
178         $params = $this->user->asParameters('omb_listener', false);
179
180         $params['oauth_token'] = $this->token->key;
181         $params['omb_version'] = OMB_VERSION;
182
183         $callback .= (parse_url($this->callback, PHP_URL_QUERY) ? '&' : '?');
184         foreach ($params as $k => $v) {
185           $callback .= OAuthUtil::urlencode_rfc3986($k) . '=' .
186                        OAuthUtil::urlencode_rfc3986($v) . '&';
187         }
188       }
189     }
190     return array($callback, $this->token);
191   }
192
193   /**
194    * Echo an access token
195    *
196    * Outputs an access token for the query found in $_GET or $_POST.
197    *
198    * @access public
199    **/
200   public function writeAccessToken() {
201     OMB_Helper::removeMagicQuotesFromRequest();
202     echo $this->getOAuthServer()->fetch_access_token(OAuthRequest::from_request());
203   }
204
205   /**
206    * Handle an updateprofile request
207    *
208    * Handles an updateprofile request posted to this service. Updates the
209    * profile through the OMB_Datastore.
210    *
211    * @access public
212    *
213    * @return OMB_Profile The updated profile
214    **/
215   public function handleUpdateProfile() {
216     list($req, $profile) = $this->handleOMBRequest(OMB_ENDPOINT_UPDATEPROFILE);
217     $profile->updateFromParameters($req->get_parameters(), 'omb_listenee');
218     $this->datastore->saveProfile($profile);
219     $this->finishOMBRequest();
220     return $profile;
221   }
222
223   /**
224    * Handle a postnotice request
225    *
226    * Handles a postnotice request posted to this service.
227    *
228    * @access public
229    *
230    * @return OMB_Notice The received notice
231    **/
232   public function handlePostNotice() {
233     list($req, $profile) = $this->handleOMBRequest(OMB_ENDPOINT_POSTNOTICE);
234     require_once 'notice.php';
235     $notice = OMB_Notice::fromParameters($profile, $req->get_parameters());
236     $this->datastore->saveNotice($notice);
237     $this->finishOMBRequest();
238     return $notice;
239   }
240
241   /**
242    * Handle an OMB request
243    *
244    * Performs common OMB request handling.
245    *
246    * @param string $uri The URI defining the OMB endpoint being served
247    *
248    * @access protected
249    *
250    * @return array(OAuthRequest, OMB_Profile)
251    **/
252   protected function handleOMBRequest($uri) {
253
254     OMB_Helper::removeMagicQuotesFromRequest();
255     $req = OAuthRequest::from_request();
256     $listenee =  $req->get_parameter('omb_listenee');
257
258     try {
259         list($consumer, $token) = $this->getOAuthServer()->verify_request($req);
260     } catch (OAuthException $e) {
261       header('HTTP/1.1 403 Forbidden');
262       throw OMB_RemoteServiceException::forRequest($uri,
263                                    'Revoked accesstoken for ' . $listenee);
264     }
265
266     $version = $req->get_parameter('omb_version');
267     if ($version !== OMB_VERSION) {
268       header('HTTP/1.1 400 Bad Request');
269       throw OMB_RemoteServiceException::forRequest($uri,
270                                    'Wrong OMB version ' . $version);
271     }
272
273     $profile = $this->datastore->getProfile($listenee);
274     if (is_null($profile)) {
275       header('HTTP/1.1 400 Bad Request');
276       throw OMB_RemoteServiceException::forRequest($uri,
277                                    'Unknown remote profile ' . $listenee);
278     }
279
280     $subscribers = $this->datastore->getSubscriptions($listenee);
281     if (count($subscribers) === 0) {
282       header('HTTP/1.1 403 Forbidden');
283       throw OMB_RemoteServiceException::forRequest($uri,
284                                    'No subscriber for ' . $listenee);
285     }
286
287     return array($req, $profile);
288   }
289
290   /**
291    * Finishes an OMB request handling
292    *
293    * Performs common OMB request handling finishing.
294    *
295    * @access protected
296    **/
297   protected function finishOMBRequest() {
298     header('HTTP/1.1 200 OK');
299     header('Content-type: text/plain');
300     /* There should be no clutter but the version. */
301     echo "omb_version=" . OMB_VERSION;
302   }
303
304   /**
305    * Return an OAuthServer
306    *
307    * Checks whether the OAuthServer is null. If so, initializes it with a
308    * default value. Returns the OAuth server.
309    *
310    * @access protected
311    **/
312   protected function getOAuthServer() {
313     if (is_null($this->oauth_server)) {
314       $this->oauth_server = new OAuthServer($this->datastore);
315       $this->oauth_server->add_signature_method(
316                                           new OAuthSignatureMethod_HMAC_SHA1());
317     }
318     return $this->oauth_server;
319   }
320
321   /**
322    * Publish a notice
323    *
324    * Posts an OMB notice. This includes storing the notice and posting it to
325    * subscribed users.
326    *
327    * @param OMB_Notice $notice The new notice
328    *
329    * @access public
330    *
331    * @return array An array mapping subscriber URIs to the exception posting to
332    *               them has raised; Empty array if no exception occured
333    **/
334   public function postNotice($notice) {
335     $uri = $this->user->getIdentifierURI();
336
337     /* $notice is passed by reference and may change. */
338     $this->datastore->saveNotice($notice);
339     $subscribers = $this->datastore->getSubscriptions($uri);
340
341     /* No one to post to. */
342     if (is_null($subscribers)) {
343         return array();
344     }
345
346     require_once 'service_consumer.php';
347
348     $err = array();
349     foreach($subscribers as $subscriber) {
350       try {
351         $service = new OMB_Service_Consumer($subscriber['uri'], $uri, $this->datastore);
352         $service->setToken($subscriber['token'], $subscriber['secret']);
353         $service->postNotice($notice);
354       } catch (Exception $e) {
355         $err[$subscriber['uri']] = $e;
356         continue;
357       }
358     }
359     return $err;
360   }
361
362   /**
363    * Publish a profile update
364    *
365    * Posts the current profile as an OMB profile update. This includes updating
366    * the stored profile and posting it to subscribed users.
367    *
368    * @access public
369    *
370    * @return array An array mapping subscriber URIs to the exception posting to
371    *               them has raised; Empty array if no exception occured
372    **/
373   public function updateProfile() {
374     $uri = $this->user->getIdentifierURI();
375
376     $this->datastore->saveProfile($this->user);
377     $subscribers = $this->datastore->getSubscriptions($uri);
378
379     /* No one to post to. */
380     if (is_null($subscribers)) {
381         return array();
382     }
383
384     require_once 'service_consumer.php';
385
386     $err = array();
387     foreach($subscribers as $subscriber) {
388       try {
389         $service = new OMB_Service_Consumer($subscriber['uri'], $uri, $this->datastore);
390         $service->setToken($subscriber['token'], $subscriber['secret']);
391         $service->updateProfile($this->user);
392       } catch (Exception $e) {
393         $err[$subscriber['uri']] = $e;
394         continue;
395       }
396     }
397     return $err;
398   }
399 }