summaryrefslogtreecommitdiff
path: root/extlib/libomb/service_provider.php
blob: a1c69e86ff04a0c7e079c24186d408f2ee62e380 (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
<?php

require_once 'constants.php';
require_once 'remoteserviceexception.php';
require_once 'helper.php';

/**
 * OMB service realization
 *
 * This class realizes a complete, simple OMB service.
 *
 * 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_Provider {
  protected $user; /* An OMB_Profile representing the user */
  protected $datastore; /* AN OMB_Datastore */

  protected $remote_user; /* An OMB_Profile representing the remote user during
                            the authorization process */

  protected $oauth_server; /* An OAuthServer; should only be accessed via
                              getOAuthServer. */

  /**
   * Initialize an OMB_Service_Provider object
   *
   * Constructs an OMB_Service_Provider instance that provides OMB services
   * referring to a particular user.
   *
   * @param OMB_Profile   $user         An OMB_Profile; mandatory for XRDS
   *                                    output, user auth handling and OMB
   *                                    action performing
   * @param OMB_Datastore $datastore    An OMB_Datastore; mandatory for
   *                                    everything but XRDS output
   * @param OAuthServer   $oauth_server An OAuthServer; used for token writing
   *                                    and OMB action handling; will use
   *                                    default value if not set
   *
   * @access public
   **/
  public function __construct ($user = null, $datastore = null, $oauth_server = null) {
    $this->user = $user;
    $this->datastore = $datastore;
    $this->oauth_server = $oauth_server;
  }

  public function getRemoteUser() {
    return $this->remote_user;
  }

  /**
   * Write a XRDS document
   *
   * Writes a XRDS document specifying the OMB service. Optionally uses a
   * given object of a class implementing OMB_XRDS_Writer for output. Else
   * OMB_Plain_XRDS_Writer is used.
   *
   * @param OMB_XRDS_Mapper $xrds_mapper An object mapping actions to URLs
   * @param OMB_XRDS_Writer $xrds_writer Optional; The OMB_XRDS_Writer used to
   *                                     write the XRDS document
   *
   * @access public
   *
   * @return mixed Depends on the used OMB_XRDS_Writer; OMB_Plain_XRDS_Writer
   *               returns nothing.
   **/
  public function writeXRDS($xrds_mapper, $xrds_writer = null) {
    if ($xrds_writer == null) {
        require_once 'plain_xrds_writer.php';
        $xrds_writer = new OMB_Plain_XRDS_Writer();
    }
    return $xrds_writer->writeXRDS($this->user, $xrds_mapper);
  }

  /**
   * Echo a request token
   *
   * Outputs an unauthorized request token for the query found in $_GET or
   * $_POST.
   *
   * @access public
   **/
  public function writeRequestToken() {
    OMB_Helper::removeMagicQuotesFromRequest();
    echo $this->getOAuthServer()->fetch_request_token(OAuthRequest::from_request());
  }

  /**
   * Handle an user authorization request.
   *
   * Parses an authorization request. This includes OAuth and OMB verification.
   * Throws exceptions on failures. Returns an OMB_Profile object representing
   * the remote user.
   *
   * The OMB_Profile passed to the constructor of OMB_Service_Provider should
   * not represent the user specified in the authorization request, but the one
   * currently logged in to the service. This condition being satisfied,
   * handleUserAuth will check whether the listener specified in the request is
   * identical to the logged in user.
   *
   * @access public
   *
   * @return OMB_Profile The profile of the soon-to-be subscribed, i. e. remote
   *                     user
   **/
  public function handleUserAuth() {
    OMB_Helper::removeMagicQuotesFromRequest();

    /* Verify the request token. */

    $this->token = $this->datastore->lookup_token(null, "request", $_GET['oauth_token']);
    if (is_null($this->token)) {
      throw new OAuthException('The given request token has not been issued ' .
                               'by this service.');
    }

    /* Verify the OMB part. */

    if ($_GET['omb_version'] !== OMB_VERSION) {
      throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
                                   'Wrong OMB version ' . $_GET['omb_version']);
    }

    if ($_GET['omb_listener'] !== $this->user->getIdentifierURI()) {
      throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
                                 'Wrong OMB listener ' . $_GET['omb_listener']);
    }

    foreach (array('omb_listenee', 'omb_listenee_profile',
                   'omb_listenee_nickname', 'omb_listenee_license') as $param) {
      if (!isset($_GET[$param]) || is_null($_GET[$param])) {
        throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
                                       "Required parameter '$param' not found");
      }
    }

    /* Store given callback for later use. */
    if (isset($_GET['oauth_callback']) && $_GET['oauth_callback'] !== '') {
      $this->callback = $_GET['oauth_callback'];
      if (!OMB_Helper::validateURL($this->callback)) {
        throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
                                              'Invalid callback URL specified');
      }
    }
    $this->remote_user = OMB_Profile::fromParameters($_GET, 'omb_listenee');

    return $this->remote_user;
  }

  /**
   * Continue the OAuth dance after user authorization
   *
   * Performs the appropriate actions after user answered the authorization
   * request.
   *
   * @param bool $accepted Whether the user granted authorization
   *
   * @access public
   *
   * @return array A two-component array with the values:
   *                - callback The callback URL or null if none given
   *                - token    The authorized request token or null if not
   *                           authorized.
   **/
  public function continueUserAuth($accepted) {
    $callback = $this->callback;
    if (!$accepted) {
      $this->datastore->revoke_token($this->token->key);
      $this->token = null;
      /* TODO: The handling is probably wrong in terms of OAuth 1.0 but the way
               laconica works. Moreover I don’t know the right way either. */

    } else {
      $this->datastore->authorize_token($this->token->key);
      $this->datastore->saveProfile($this->remote_user);
      $this->datastore->saveSubscription($this->user->getIdentifierURI(),
                          $this->remote_user->getIdentifierURI(), $this->token);

      if (!is_null($this->callback)) {
        /* Callback wants to get some informations as well. */
        $params = $this->user->asParameters('omb_listener', false);

        $params['oauth_token'] = $this->token->key;
        $params['omb_version'] = OMB_VERSION;

        $callback .= (parse_url($this->callback, PHP_URL_QUERY) ? '&' : '?');
        foreach ($params as $k => $v) {
          $callback .= OAuthUtil::urlencode_rfc3986($k) . '=' .
                       OAuthUtil::urlencode_rfc3986($v) . '&';
        }
      }
    }
    return array($callback, $this->token);
  }

  /**
   * Echo an access token
   *
   * Outputs an access token for the query found in $_POST. OMB 0.1 specifies
   * that the access token request has to be a POST even if OAuth allows GET as
   * well.
   *
   * @access public
   **/
  public function writeAccessToken() {
    OMB_Helper::removeMagicQuotesFromRequest();
    echo $this->getOAuthServer()->fetch_access_token(
                                            OAuthRequest::from_request('POST'));
  }

  /**
   * Handle an updateprofile request
   *
   * Handles an updateprofile request posted to this service. Updates the
   * profile through the OMB_Datastore.
   *
   * @access public
   *
   * @return OMB_Profile The updated profile
   **/
  public function handleUpdateProfile() {
    list($req, $profile) = $this->handleOMBRequest(OMB_ENDPOINT_UPDATEPROFILE);
    $profile->updateFromParameters($req->get_parameters(), 'omb_listenee');
    $this->datastore->saveProfile($profile);
    $this->finishOMBRequest();
    return $profile;
  }

  /**
   * Handle a postnotice request
   *
   * Handles a postnotice request posted to this service. Saves the notice
   * through the OMB_Datastore.
   *
   * @access public
   *
   * @return OMB_Notice The received notice
   **/
  public function handlePostNotice() {
    list($req, $profile) = $this->handleOMBRequest(OMB_ENDPOINT_POSTNOTICE);
    require_once 'notice.php';
    $notice = OMB_Notice::fromParameters($profile, $req->get_parameters());
    $this->datastore->saveNotice($notice);
    $this->finishOMBRequest();
    return $notice;
  }

  /**
   * Handle an OMB request
   *
   * Performs common OMB request handling.
   *
   * @param string $uri The URI defining the OMB endpoint being served
   *
   * @access protected
   *
   * @return array(OAuthRequest, OMB_Profile)
   **/
  protected function handleOMBRequest($uri) {

    OMB_Helper::removeMagicQuotesFromRequest();
    $req = OAuthRequest::from_request('POST');
    $listenee =  $req->get_parameter('omb_listenee');

    try {
        list($consumer, $token) = $this->getOAuthServer()->verify_request($req);
    } catch (OAuthException $e) {
      header('HTTP/1.1 403 Forbidden');
      // @debug hack
      throw OMB_RemoteServiceException::forRequest($uri,
                                   'Revoked accesstoken for ' . $listenee . ': ' . $e->getMessage());
      // @end debug
      throw OMB_RemoteServiceException::forRequest($uri,
                                   'Revoked accesstoken for ' . $listenee);
    }

    $version = $req->get_parameter('omb_version');
    if ($version !== OMB_VERSION) {
      header('HTTP/1.1 400 Bad Request');
      throw OMB_RemoteServiceException::forRequest($uri,
                                   'Wrong OMB version ' . $version);
    }

    $profile = $this->datastore->getProfile($listenee);
    if (is_null($profile)) {
      header('HTTP/1.1 400 Bad Request');
      throw OMB_RemoteServiceException::forRequest($uri,
                                   'Unknown remote profile ' . $listenee);
    }

    $subscribers = $this->datastore->getSubscriptions($listenee);
    if (count($subscribers) === 0) {
      header('HTTP/1.1 403 Forbidden');
      throw OMB_RemoteServiceException::forRequest($uri,
                                   'No subscriber for ' . $listenee);
    }

    return array($req, $profile);
  }

  /**
   * Finishes an OMB request handling
   *
   * Performs common OMB request handling finishing.
   *
   * @access protected
   **/
  protected function finishOMBRequest() {
    header('HTTP/1.1 200 OK');
    header('Content-type: text/plain');
    /* There should be no clutter but the version. */
    echo "omb_version=" . OMB_VERSION;
  }

  /**
   * Return an OAuthServer
   *
   * Checks whether the OAuthServer is null. If so, initializes it with a
   * default value. Returns the OAuth server.
   *
   * @access protected
   **/
  protected function getOAuthServer() {
    if (is_null($this->oauth_server)) {
      $this->oauth_server = new OAuthServer($this->datastore);
      $this->oauth_server->add_signature_method(
                                          new OAuthSignatureMethod_HMAC_SHA1());
    }
    return $this->oauth_server;
  }

  /**
   * Publish a notice
   *
   * Posts an OMB notice. This includes storing the notice and posting it to
   * subscribed users.
   *
   * @param OMB_Notice $notice The new notice
   *
   * @access public
   *
   * @return array An array mapping subscriber URIs to the exception posting to
   *               them has raised; Empty array if no exception occured
   **/
  public function postNotice($notice) {
    $uri = $this->user->getIdentifierURI();

    /* $notice is passed by reference and may change. */
    $this->datastore->saveNotice($notice);
    $subscribers = $this->datastore->getSubscriptions($uri);

    /* No one to post to. */
    if (is_null($subscribers)) {
        return array();
    }

    require_once 'service_consumer.php';

    $err = array();
    foreach($subscribers as $subscriber) {
      try {
        $service = new OMB_Service_Consumer($subscriber['uri'], $uri, $this->datastore);
        $service->setToken($subscriber['token'], $subscriber['secret']);
        $service->postNotice($notice);
      } catch (Exception $e) {
        $err[$subscriber['uri']] = $e;
        continue;
      }
    }
    return $err;
  }

  /**
   * Publish a profile update
   *
   * Posts the current profile as an OMB profile update. This includes updating
   * the stored profile and posting it to subscribed users.
   *
   * @access public
   *
   * @return array An array mapping subscriber URIs to the exception posting to
   *               them has raised; Empty array if no exception occured
   **/
  public function updateProfile() {
    $uri = $this->user->getIdentifierURI();

    $this->datastore->saveProfile($this->user);
    $subscribers = $this->datastore->getSubscriptions($uri);

    /* No one to post to. */
    if (is_null($subscribers)) {
        return array();
    }

    require_once 'service_consumer.php';

    $err = array();
    foreach($subscribers as $subscriber) {
      try {
        $service = new OMB_Service_Consumer($subscriber['uri'], $uri, $this->datastore);
        $service->setToken($subscriber['token'], $subscriber['secret']);
        $service->updateProfile($this->user);
      } catch (Exception $e) {
        $err[$subscriber['uri']] = $e;
        continue;
      }
    }
    return $err;
  }
}