3 require_once 'constants.php';
4 require_once 'remoteserviceexception.php';
5 require_once 'helper.php';
8 * OMB service realization
10 * This class realizes a complete, simple OMB service.
14 * LICENSE: This program is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU Affero General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU Affero General Public License for more details.
24 * You should have received a copy of the GNU Affero General Public License
25 * along with this program. If not, see <http://www.gnu.org/licenses/>.
28 * @author Adrian Lang <mail@adrianlang.de>
29 * @copyright 2009 Adrian Lang
30 * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
33 class OMB_Service_Provider {
34 protected $user; /* An OMB_Profile representing the user */
35 protected $datastore; /* AN OMB_Datastore */
37 protected $remote_user; /* An OMB_Profile representing the remote user during
38 the authorization process */
40 protected $oauth_server; /* An OAuthServer; should only be accessed via
44 * Initialize an OMB_Service_Provider object
46 * Constructs an OMB_Service_Provider instance that provides OMB services
47 * referring to a particular user.
49 * @param OMB_Profile $user An OMB_Profile; mandatory for XRDS
50 * output, user auth handling and OMB
52 * @param OMB_Datastore $datastore An OMB_Datastore; mandatory for
53 * everything but XRDS output
54 * @param OAuthServer $oauth_server An OAuthServer; used for token writing
55 * and OMB action handling; will use
56 * default value if not set
60 public function __construct ($user = null, $datastore = null, $oauth_server = null) {
62 $this->datastore = $datastore;
63 $this->oauth_server = $oauth_server;
66 public function getRemoteUser() {
67 return $this->remote_user;
71 * Write a XRDS document
73 * Writes a XRDS document specifying the OMB service. Optionally uses a
74 * given object of a class implementing OMB_XRDS_Writer for output. Else
75 * OMB_Plain_XRDS_Writer is used.
77 * @param OMB_XRDS_Mapper $xrds_mapper An object mapping actions to URLs
78 * @param OMB_XRDS_Writer $xrds_writer Optional; The OMB_XRDS_Writer used to
79 * write the XRDS document
83 * @return mixed Depends on the used OMB_XRDS_Writer; OMB_Plain_XRDS_Writer
86 public function writeXRDS($xrds_mapper, $xrds_writer = null) {
87 if ($xrds_writer == null) {
88 require_once 'plain_xrds_writer.php';
89 $xrds_writer = new OMB_Plain_XRDS_Writer();
91 return $xrds_writer->writeXRDS($this->user, $xrds_mapper);
95 * Echo a request token
97 * Outputs an unauthorized request token for the query found in $_GET or
102 public function writeRequestToken() {
103 OMB_Helper::removeMagicQuotesFromRequest();
104 echo $this->getOAuthServer()->fetch_request_token(OAuthRequest::from_request());
108 * Handle an user authorization request.
110 * Parses an authorization request. This includes OAuth and OMB verification.
111 * Throws exceptions on failures. Returns an OMB_Profile object representing
114 * The OMB_Profile passed to the constructor of OMB_Service_Provider should
115 * not represent the user specified in the authorization request, but the one
116 * currently logged in to the service. This condition being satisfied,
117 * handleUserAuth will check whether the listener specified in the request is
118 * identical to the logged in user.
122 * @return OMB_Profile The profile of the soon-to-be subscribed, i. e. remote
125 public function handleUserAuth() {
126 OMB_Helper::removeMagicQuotesFromRequest();
128 /* Verify the request token. */
130 $this->token = $this->datastore->lookup_token(null, "request", $_GET['oauth_token']);
131 if (is_null($this->token)) {
132 throw new OAuthException('The given request token has not been issued ' .
136 /* Verify the OMB part. */
138 if ($_GET['omb_version'] !== OMB_VERSION) {
139 throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
140 'Wrong OMB version ' . $_GET['omb_version']);
143 if ($_GET['omb_listener'] !== $this->user->getIdentifierURI()) {
144 throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
145 'Wrong OMB listener ' . $_GET['omb_listener']);
148 foreach (array('omb_listenee', 'omb_listenee_profile',
149 'omb_listenee_nickname', 'omb_listenee_license') as $param) {
150 if (!isset($_GET[$param]) || is_null($_GET[$param])) {
151 throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
152 "Required parameter '$param' not found");
156 /* Store given callback for later use. */
157 if (isset($_GET['oauth_callback']) && $_GET['oauth_callback'] !== '') {
158 $this->callback = $_GET['oauth_callback'];
159 if (!OMB_Helper::validateURL($this->callback)) {
160 throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
161 'Invalid callback URL specified');
164 $this->remote_user = OMB_Profile::fromParameters($_GET, 'omb_listenee');
166 return $this->remote_user;
170 * Continue the OAuth dance after user authorization
172 * Performs the appropriate actions after user answered the authorization
175 * @param bool $accepted Whether the user granted authorization
179 * @return array A two-component array with the values:
180 * - callback The callback URL or null if none given
181 * - token The authorized request token or null if not
184 public function continueUserAuth($accepted) {
185 $callback = $this->callback;
187 $this->datastore->revoke_token($this->token->key);
189 /* TODO: The handling is probably wrong in terms of OAuth 1.0 but the way
190 laconica works. Moreover I don’t know the right way either. */
193 $this->datastore->authorize_token($this->token->key);
194 $this->datastore->saveProfile($this->remote_user);
195 $this->datastore->saveSubscription($this->user->getIdentifierURI(),
196 $this->remote_user->getIdentifierURI(), $this->token);
198 if (!is_null($this->callback)) {
199 /* Callback wants to get some informations as well. */
200 $params = $this->user->asParameters('omb_listener', false);
202 $params['oauth_token'] = $this->token->key;
203 $params['omb_version'] = OMB_VERSION;
205 $callback .= (parse_url($this->callback, PHP_URL_QUERY) ? '&' : '?');
206 foreach ($params as $k => $v) {
207 $callback .= OAuthUtil::urlencode_rfc3986($k) . '=' .
208 OAuthUtil::urlencode_rfc3986($v) . '&';
212 return array($callback, $this->token);
216 * Echo an access token
218 * Outputs an access token for the query found in $_POST. OMB 0.1 specifies
219 * that the access token request has to be a POST even if OAuth allows GET as
224 public function writeAccessToken() {
225 OMB_Helper::removeMagicQuotesFromRequest();
226 echo $this->getOAuthServer()->fetch_access_token(
227 OAuthRequest::from_request('POST'));
231 * Handle an updateprofile request
233 * Handles an updateprofile request posted to this service. Updates the
234 * profile through the OMB_Datastore.
238 * @return OMB_Profile The updated profile
240 public function handleUpdateProfile() {
241 list($req, $profile) = $this->handleOMBRequest(OMB_ENDPOINT_UPDATEPROFILE);
242 $profile->updateFromParameters($req->get_parameters(), 'omb_listenee');
243 $this->datastore->saveProfile($profile);
244 $this->finishOMBRequest();
249 * Handle a postnotice request
251 * Handles a postnotice request posted to this service. Saves the notice
252 * through the OMB_Datastore.
256 * @return OMB_Notice The received notice
258 public function handlePostNotice() {
259 list($req, $profile) = $this->handleOMBRequest(OMB_ENDPOINT_POSTNOTICE);
260 require_once 'notice.php';
261 $notice = OMB_Notice::fromParameters($profile, $req->get_parameters());
262 $this->datastore->saveNotice($notice);
263 $this->finishOMBRequest();
268 * Handle an OMB request
270 * Performs common OMB request handling.
272 * @param string $uri The URI defining the OMB endpoint being served
276 * @return array(OAuthRequest, OMB_Profile)
278 protected function handleOMBRequest($uri) {
280 OMB_Helper::removeMagicQuotesFromRequest();
281 $req = OAuthRequest::from_request('POST');
282 $listenee = $req->get_parameter('omb_listenee');
285 list($consumer, $token) = $this->getOAuthServer()->verify_request($req);
286 } catch (OAuthException $e) {
287 header('HTTP/1.1 403 Forbidden');
289 throw OMB_RemoteServiceException::forRequest($uri,
290 'Revoked accesstoken for ' . $listenee . ': ' . $e->getMessage());
292 throw OMB_RemoteServiceException::forRequest($uri,
293 'Revoked accesstoken for ' . $listenee);
296 $version = $req->get_parameter('omb_version');
297 if ($version !== OMB_VERSION) {
298 header('HTTP/1.1 400 Bad Request');
299 throw OMB_RemoteServiceException::forRequest($uri,
300 'Wrong OMB version ' . $version);
303 $profile = $this->datastore->getProfile($listenee);
304 if (is_null($profile)) {
305 header('HTTP/1.1 400 Bad Request');
306 throw OMB_RemoteServiceException::forRequest($uri,
307 'Unknown remote profile ' . $listenee);
310 $subscribers = $this->datastore->getSubscriptions($listenee);
311 if (count($subscribers) === 0) {
312 header('HTTP/1.1 403 Forbidden');
313 throw OMB_RemoteServiceException::forRequest($uri,
314 'No subscriber for ' . $listenee);
317 return array($req, $profile);
321 * Finishes an OMB request handling
323 * Performs common OMB request handling finishing.
327 protected function finishOMBRequest() {
328 header('HTTP/1.1 200 OK');
329 header('Content-type: text/plain');
330 /* There should be no clutter but the version. */
331 echo "omb_version=" . OMB_VERSION;
335 * Return an OAuthServer
337 * Checks whether the OAuthServer is null. If so, initializes it with a
338 * default value. Returns the OAuth server.
342 protected function getOAuthServer() {
343 if (is_null($this->oauth_server)) {
344 $this->oauth_server = new OAuthServer($this->datastore);
345 $this->oauth_server->add_signature_method(
346 new OAuthSignatureMethod_HMAC_SHA1());
348 return $this->oauth_server;
354 * Posts an OMB notice. This includes storing the notice and posting it to
357 * @param OMB_Notice $notice The new notice
361 * @return array An array mapping subscriber URIs to the exception posting to
362 * them has raised; Empty array if no exception occured
364 public function postNotice($notice) {
365 $uri = $this->user->getIdentifierURI();
367 /* $notice is passed by reference and may change. */
368 $this->datastore->saveNotice($notice);
369 $subscribers = $this->datastore->getSubscriptions($uri);
371 /* No one to post to. */
372 if (is_null($subscribers)) {
376 require_once 'service_consumer.php';
379 foreach($subscribers as $subscriber) {
381 $service = new OMB_Service_Consumer($subscriber['uri'], $uri, $this->datastore);
382 $service->setToken($subscriber['token'], $subscriber['secret']);
383 $service->postNotice($notice);
384 } catch (Exception $e) {
385 $err[$subscriber['uri']] = $e;
393 * Publish a profile update
395 * Posts the current profile as an OMB profile update. This includes updating
396 * the stored profile and posting it to subscribed users.
400 * @return array An array mapping subscriber URIs to the exception posting to
401 * them has raised; Empty array if no exception occured
403 public function updateProfile() {
404 $uri = $this->user->getIdentifierURI();
406 $this->datastore->saveProfile($this->user);
407 $subscribers = $this->datastore->getSubscriptions($uri);
409 /* No one to post to. */
410 if (is_null($subscribers)) {
414 require_once 'service_consumer.php';
417 foreach($subscribers as $subscriber) {
419 $service = new OMB_Service_Consumer($subscriber['uri'], $uri, $this->datastore);
420 $service->setToken($subscriber['token'], $subscriber['secret']);
421 $service->updateProfile($this->user);
422 } catch (Exception $e) {
423 $err[$subscriber['uri']] = $e;