summaryrefslogtreecommitdiff
path: root/extlib/libomb/service_consumer.php
blob: 273fd052ecd44dd749b1b8ffad40d0ed9f80e9f2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
<?php

require_once 'constants.php';
require_once 'Validate.php';
require_once 'Auth/Yadis/Yadis.php';
require_once 'OAuth.php';
require_once 'unsupportedserviceexception.php';
require_once 'remoteserviceexception.php';
require_once 'omb_yadis_xrds.php';
require_once 'helper.php';

/**
 * OMB service representation
 *
 * This class represents a complete remote OMB service. It provides discovery
 * and execution of the service’s methods.
 *
 * PHP version 5
 *
 * LICENSE: This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @package   OMB
 * @author    Adrian Lang <mail@adrianlang.de>
 * @copyright 2009 Adrian Lang
 * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
 **/

class OMB_Service_Consumer {
  protected $url; /* The service URL */
  protected $services; /* An array of strings mapping service URI to
                          service URL */

  protected $token; /* An OAuthToken */

  protected $listener_uri; /* The URI identifying the listener, i. e. the
                              remote user. */

  protected $listenee_uri; /* The URI identifying the listenee, i. e. the
                              local user during an auth request. */

  /**
   * According to OAuth Core 1.0, an user authorization request is no full-blown
   * OAuth request. nonce, timestamp, consumer_key and signature are not needed
   * in this step. See http://laconi.ca/trac/ticket/827 for more informations.
   *
   * Since Laconica up to version 0.7.2 performs a full OAuth request check, a
   * correct request would fail.
   **/
  public $performLegacyAuthRequest = true;

  /* Helper stuff we are going to need. */
  protected $fetcher;
  protected $oauth_consumer;
  protected $datastore;

  /**
   * Constructor for OMB_Service_Consumer
   *
   * Initializes an OMB_Service_Consumer object representing the OMB service
   * specified by $service_url. Performs a complete service discovery using
   * Yadis.
   * Throws OMB_UnsupportedServiceException if XRDS file does not specify a
   * complete OMB service.
   *
   * @param string        $service_url  The URL of the service
   * @param string        $consumer_url An URL representing the consumer
   * @param OMB_Datastore $datastore    An instance of a class implementing
   *                                    OMB_Datastore
   *
   * @access public
   **/
  public function __construct ($service_url, $consumer_url, $datastore) {
    $this->url = $service_url;
    $this->fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
    $this->datastore = $datastore;
    $this->oauth_consumer = new OAuthConsumer($consumer_url, '');

    $xrds = OMB_Yadis_XRDS::fromYadisURL($service_url, $this->fetcher);

    /* Detect our services. This performs a validation as well, since
       getService und getXRD throw exceptions on failure. */
    $this->services = array();

    foreach (array(OAUTH_DISCOVERY => OMB_Helper::$OAUTH_SERVICES,
                   OMB_VERSION     => OMB_Helper::$OMB_SERVICES)
             as $service_root => $targetservices) {
      $uris = $xrds->getService($service_root)->getURIs();
      $xrd = $xrds->getXRD($uris[0]);
      foreach ($targetservices as $targetservice) {
        $yadis_service = $xrd->getService($targetservice);
        if ($targetservice == OAUTH_ENDPOINT_REQUEST) {
            $localid = $yadis_service->getElements('xrd:LocalID');
            $this->listener_uri = $yadis_service->parser->content($localid[0]);
        }
        $uris = $yadis_service->getURIs();
        $this->services[$targetservice] = $uris[0];
      }
    }
  }

  /**
   * Get the handler URI for a service
   *
   * Returns the URI the remote web service has specified for the given
   * service.
   *
   * @param string $service The URI identifying the service
   *
   * @access public
   *
   * @return string The service handler URI
   **/
  public function getServiceURI($service) {
    return $this->services[$service];
  }

  /**
   * Get the remote user’s URI
   *
   * Returns the URI of the remote user, i. e. the listener.
   *
   * @access public
   *
   * @return string The remote user’s URI
   **/
  public function getRemoteUserURI() {
    return $this->listener_uri;
  }

  /**
   * Get the listenee’s URI
   *
   * Returns the URI of the user being subscribed to, i. e. the local user.
   *
   * @access public
   *
   * @return string The local user’s URI
   **/
  public function getListeneeURI() {
    return $this->listenee_uri;
  }

  /**
   * Request a request token
   *
   * Performs a token request on the service. Returns an OAuthToken on success.
   * Throws an exception if the request fails.
   *
   * @access public
   *
   * @return OAuthToken An unauthorized request token
   **/
  public function requestToken() {
    /* Set the token to null just in case the user called setToken. */
    $this->token = null;

    $result = $this->performAction(OAUTH_ENDPOINT_REQUEST,
                                  array('omb_listener' => $this->listener_uri));
    if ($result->status != 200) {
      throw OMB_RemoteServiceException::fromYadis(OAUTH_ENDPOINT_REQUEST,
                                                  $result);
    }
    parse_str($result->body, $return);
    if (!isset($return['oauth_token']) || !isset($return['oauth_token_secret'])) {
      throw OMB_RemoteServiceException::fromYadis(OAUTH_ENDPOINT_REQUEST,
                                                  $result);
    }
    $this->setToken($return['oauth_token'], $return['oauth_token_secret']);
    return $this->token;
  }

  /**
   *
   * Request authorization
   *
   * Returns an URL which equals to an authorization request. The end user
   * should be redirected to this location to perform authorization.
   * The $finish_url should be a local resource which invokes
   * OMB_Consumer::finishAuthorization on request.
   *
   * @param OMB_Profile $profile    An OMB_Profile object representing the
   *                                soon-to-be subscribed (i. e. local) user
   * @param string      $finish_url Target location after successful
   *                                authorization
   *
   * @access public
   *
   * @return string An URL representing an authorization request
   **/
  public function requestAuthorization($profile, $finish_url) {
    if ($this->performLegacyAuthRequest) {
      $params = $profile->asParameters('omb_listenee', false);
      $params['omb_listener'] = $this->listener_uri;
      $params['oauth_callback'] = $finish_url;

      $url = $this->prepareAction(OAUTH_ENDPOINT_AUTHORIZE, $params, 'GET')->to_url();
    } else {

      $params = array(
                'oauth_callback' => $finish_url,
                'oauth_token'    => $this->token->key,
                'omb_version'    => OMB_VERSION,
                'omb_listener'   => $this->listener_uri);

      $params = array_merge($profile->asParameters('omb_listenee', false). $params);

      /* Build result URL. */
      $url = $this->services[OAUTH_ENDPOINT_AUTHORIZE];
      $url .= (strrpos($url, '?') === false ? '?' : '&');
      foreach ($params as $k => $v) {
        $url .= OAuthUtil::urlencode_rfc3986($k) . '=' . OAuthUtil::urlencode_rfc3986($v) . '&';
      }
    }

    $this->listenee_uri = $profile->getIdentifierURI();

    return $url;
  }

  /**
   * Finish authorization
   *
   * Finish the subscription process by converting the received and authorized
   * request token into an access token. After that, the subscriber’s profile
   * and the subscription are stored in the database.
   * Expects an OAuthRequest in query parameters.
   * Throws exceptions on failure.
   *
   * @access public
   **/
  public function finishAuthorization() {
    OMB_Helper::removeMagicQuotesFromRequest();
    $req = OAuthRequest::from_request();
    if ($req->get_parameter('oauth_token') !=
          $this->token->key) {
      /* That’s not the token I wanted to get authorized. */
      throw new OAuthException('The authorized token does not equal the ' .
                               'submitted token.');
    }

    if ($req->get_parameter('omb_version') != OMB_VERSION) {
      throw new OMB_RemoteServiceException('The remote service uses an ' .
                                           'unsupported OMB version');
    }

    /* Construct the profile to validate it. */

    /* Fix OMB bug. Listener URI is not passed. */
    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
      $params = $_POST;
    } else {
      $params = $_GET;
    }
    $params['omb_listener'] = $this->listener_uri;

    require_once 'profile.php';
    $listener = OMB_Profile::fromParameters($params, 'omb_listener');

    /* Ask the remote service to convert the authorized request token into an
       access token. */

    $result = $this->performAction(OAUTH_ENDPOINT_ACCESS, array());
    if ($result->status != 200) {
      throw new OAuthException('Could not get access token');
    }

    parse_str($result->body, $return);
    if (!isset($return['oauth_token']) || !isset($return['oauth_token_secret'])) {
      throw new OAuthException('Could not get access token');
    }
    $this->setToken($return['oauth_token'], $return['oauth_token_secret']);

    /* Subscription is finished and valid. Now store the new subscriber and the
       subscription in the database. */

    $this->datastore->saveProfile($listener);
    $this->datastore->saveSubscription($this->listener_uri,
                                       $this->listenee_uri,
                                       $this->token);
  }

  /**
   * Return the URI identifying the listener
   *
   * Returns the URI for the OMB user who tries to subscribe or already has
   * subscribed our user. This method is a workaround for a serious OMB flaw:
   * The Listener URI is not passed in the finishauthorization call.
   *
   * @access public
   *
   * @return string the listener’s URI
   **/
  public function getListenerURI() {
    return $this->listener_uri;
  }

  /**
   * Inform the service about a profile update
   *
   * Sends an updated profile to the service.
   *
   * @param OMB_Profile $profile The profile that has changed
   *
   * @access public
   **/
  public function updateProfile($profile) {
    $params = $profile->asParameters('omb_listenee', true);
    $this->performOMBAction(OMB_ENDPOINT_UPDATEPROFILE, $params, $profile->getIdentifierURI());
  }

  /**
   * Inform the service about a new notice
   *
   * Sends a notice to the service.
   *
   * @param OMB_Notice $notice The notice
   *
   * @access public
   **/
  public function postNotice($notice) {
    $params = $notice->asParameters();
    $params['omb_listenee'] = $notice->getAuthor()->getIdentifierURI();
    $this->performOMBAction(OMB_ENDPOINT_POSTNOTICE, $params, $params['omb_listenee']);
  }

  /**
   * Set the token member variable
   *
   * Initializes the token based on given token and secret token.
   *
   * @param string $token  The token
   * @param string $secret The secret token
   *
   * @access public
   **/
  public function setToken($token, $secret) {
    $this->token = new OAuthToken($token, $secret);
  }

  /**
   * Prepare an OAuthRequest object
   *
   * Creates an OAuthRequest object mapping the request specified by the
   * parameters.
   *
   * @param string $action_uri The URI specifying the target service
   * @param array  $params     Additional parameters for the service call
   * @param string $method     The HTTP method used to call the service
   *                           ('POST' or 'GET', usually)
   *
   * @access protected
   *
   * @return OAuthRequest the prepared request
   **/
  protected function prepareAction($action_uri, $params, $method) {
    $url = $this->services[$action_uri];

    $url_params = array();
    parse_str(parse_url($url, PHP_URL_QUERY), $url_params);

    /* Add OMB version. */
    $url_params['omb_version'] = OMB_VERSION;

    /* Add user-defined parameters. */
    $url_params = array_merge($url_params, $params);

    $req = OAuthRequest::from_consumer_and_token($this->oauth_consumer,
                                      $this->token, $method, $url, $url_params);

    /* Sign the request. */
    $req->sign_request(new OAuthSignatureMethod_HMAC_SHA1(),
                                           $this->oauth_consumer, $this->token);

    return $req;
  }

  /**
   * Perform a service call
   *
   * Creates an OAuthRequest object and execute the mapped call as POST request.
   *
   * @param string $action_uri The URI specifying the target service
   * @param array  $params     Additional parameters for the service call
   *
   * @access protected
   *
   * @return Auth_Yadis_HTTPResponse The POST request response
   **/
  protected function performAction($action_uri, $params) {
    $req = $this->prepareAction($action_uri, $params, 'POST');

    /* Return result page. */
    return $this->fetcher->post($req->get_normalized_http_url(), $req->to_postdata(), array());
  }

  /**
   * Perform an OMB action
   *
   * Executes an OMB action – to date, it’s one of updateProfile or postNotice.
   *
   * @param string $action_uri   The URI specifying the target service
   * @param array  $params       Additional parameters for the service call
   * @param string $listenee_uri The URI identifying the local user for whom
   *                             the action is performed
   *
   * @access protected
   **/
  protected function performOMBAction($action_uri, $params, $listenee_uri) {
    $result = $this->performAction($action_uri, $params);
    if ($result->status == 403) {
      /* The remote user unsubscribed us. */
      $this->datastore->deleteSubscription($this->listener_uri, $listenee_uri);
    } else if ($result->status != 200 ||
               strpos($result->body, 'omb_version=' . OMB_VERSION) === false) {
      /* The server signaled an error or sent an incorrect response. */
      throw OMB_RemoteServiceException::fromYadis($action_uri, $result);
    }
  }
}