From ec88d2650ea4371cf53229171851747b31587e4b Mon Sep 17 00:00:00 2001 From: Adrian Lang Date: Mon, 10 Aug 2009 14:48:50 +0200 Subject: [PATCH] Replace own OMB stack with libomb. --- actions/accesstoken.php | 28 +- actions/finishremotesubscribe.php | 322 ++++--------- actions/postnotice.php | 91 ++-- actions/remotesubscribe.php | 325 +++---------- actions/requesttoken.php | 21 +- actions/updateprofile.php | 185 ++------ actions/userauthorization.php | 410 +++++------------ actions/xrds.php | 112 ++--- extlib/libomb/base_url_xrds_mapper.php | 51 +++ extlib/libomb/constants.php | 58 +++ extlib/libomb/datastore.php | 198 ++++++++ extlib/libomb/helper.php | 99 ++++ extlib/libomb/invalidparameterexception.php | 32 ++ extlib/libomb/invalidyadisexception.php | 31 ++ extlib/libomb/notice.php | 272 +++++++++++ extlib/libomb/omb_yadis_xrds.php | 196 ++++++++ extlib/libomb/plain_xrds_writer.php | 124 +++++ extlib/libomb/profile.php | 317 +++++++++++++ extlib/libomb/remoteserviceexception.php | 42 ++ extlib/libomb/service_consumer.php | 430 ++++++++++++++++++ extlib/libomb/service_provider.php | 411 +++++++++++++++++ extlib/libomb/unsupportedserviceexception.php | 31 ++ extlib/libomb/xrds_mapper.php | 33 ++ extlib/libomb/xrds_writer.php | 33 ++ lib/oauthstore.php | 357 ++++++++++++++- lib/omb.php | 347 ++++++-------- lib/unqueuemanager.php | 2 +- scripts/ombqueuehandler.php | 2 +- 28 files changed, 3217 insertions(+), 1343 deletions(-) create mode 100755 extlib/libomb/base_url_xrds_mapper.php create mode 100644 extlib/libomb/constants.php create mode 100755 extlib/libomb/datastore.php create mode 100644 extlib/libomb/helper.php create mode 100755 extlib/libomb/invalidparameterexception.php create mode 100755 extlib/libomb/invalidyadisexception.php create mode 100755 extlib/libomb/notice.php create mode 100755 extlib/libomb/omb_yadis_xrds.php create mode 100755 extlib/libomb/plain_xrds_writer.php create mode 100755 extlib/libomb/profile.php create mode 100755 extlib/libomb/remoteserviceexception.php create mode 100755 extlib/libomb/service_consumer.php create mode 100755 extlib/libomb/service_provider.php create mode 100755 extlib/libomb/unsupportedserviceexception.php create mode 100755 extlib/libomb/xrds_mapper.php create mode 100755 extlib/libomb/xrds_writer.php diff --git a/actions/accesstoken.php b/actions/accesstoken.php index 2a8cd17134..dcd04a1b40 100644 --- a/actions/accesstoken.php +++ b/actions/accesstoken.php @@ -1,6 +1,6 @@ fetch_access_token($req); - common_debug('got this token: "'.print_r($token, true).'"', __FILE__); - common_debug('printing the access token', __FILE__); - print $token; - } catch (OAuthException $e) { + $srv = new OMB_Service_Provider(null, omb_oauth_datastore(), + omb_oauth_server()); + $srv->writeAccessToken(); + } catch (Exception $e) { $this->serverError($e->getMessage()); } } } +?> diff --git a/actions/finishremotesubscribe.php b/actions/finishremotesubscribe.php index 5c764aeb0d..13f3678235 100644 --- a/actions/finishremotesubscribe.php +++ b/actions/finishremotesubscribe.php @@ -1,5 +1,16 @@ + * @author Robin Millette + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + * * Laconica - a distributed open-source microblogging tool * Copyright (C) 2008, 2009, Control Yourself, Inc. * @@ -15,285 +26,116 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . + **/ + +if (!defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/extlib/libomb/service_consumer.php'; +require_once INSTALLDIR.'/lib/omb.php'; + +/** + * Handler for remote subscription finish callback + * + * When a remote user subscribes a local user, a redirect to this action is + * issued after the remote user authorized his service to subscribe. + * + * @category Action + * @package Laconica + * @author Evan Prodromou + * @author Robin Millette + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ */ - -if (!defined('LACONICA')) { exit(1); } - -require_once(INSTALLDIR.'/lib/omb.php'); - class FinishremotesubscribeAction extends Action { + /** + * Class handler. + * + * @param array $args query arguments + * + * @return nothing + * + **/ function handle($args) { - parent::handle($args); - if (common_logged_in()) { - $this->clientError(_('You can use the local subscription!')); - return; - } + /* Restore session data. RemotesubscribeAction should have stored + this entry. */ + $service = unserialize($_SESSION['oauth_authorization_request']); - $omb = $_SESSION['oauth_authorization_request']; - - if (!$omb) { + if (!$service) { $this->clientError(_('Not expecting this response!')); return; } - common_debug('stored request: '.print_r($omb,true), __FILE__); + common_debug('stored request: '. print_r($service, true), __FILE__); - common_remove_magic_from_request(); - $req = OAuthRequest::from_request('POST', common_local_url('finishuserauthorization')); - - $token = $req->get_parameter('oauth_token'); - - # I think this is the success metric - - if ($token != $omb['token']) { - $this->clientError(_('Not authorized.')); - return; - } - - $version = $req->get_parameter('omb_version'); - - if ($version != OMB_VERSION_01) { - $this->clientError(_('Unknown version of OMB protocol.')); - return; - } - - $nickname = $req->get_parameter('omb_listener_nickname'); - - if (!$nickname) { - $this->clientError(_('No nickname provided by remote server.')); - return; - } - - $profile_url = $req->get_parameter('omb_listener_profile'); - - if (!$profile_url) { - $this->clientError(_('No profile URL returned by server.')); - return; - } - - if (!Validate::uri($profile_url, array('allowed_schemes' => array('http', 'https')))) { - $this->clientError(_('Invalid profile URL returned by server.')); - return; - } - - if ($profile_url == common_local_url('showstream', array('nickname' => $nickname))) { - $this->clientError(_('You can use the local subscription!')); - return; - } - - common_debug('listenee: "'.$omb['listenee'].'"', __FILE__); - - $user = User::staticGet('nickname', $omb['listenee']); + /* Create user objects for both users. Do it early for request + validation. */ + $listenee = $service->getListeneeURI(); + $user = User::staticGet('uri', $listenee); if (!$user) { $this->clientError(_('User being listened to doesn\'t exist.')); return; } - $other = User::staticGet('uri', $omb['listener']); + $other = User::staticGet('uri', $service->getListenerURI()); if ($other) { $this->clientError(_('You can use the local subscription!')); return; } - $fullname = $req->get_parameter('omb_listener_fullname'); - $homepage = $req->get_parameter('omb_listener_homepage'); - $bio = $req->get_parameter('omb_listener_bio'); - $location = $req->get_parameter('omb_listener_location'); - $avatar_url = $req->get_parameter('omb_listener_avatar'); - - list($newtok, $newsecret) = $this->access_token($omb); - - if (!$newtok || !$newsecret) { - $this->clientError(_('Couldn\'t convert request tokens to access tokens.')); + /* Perform the handling itself via libomb. */ + try { + $service->finishAuthorization($listenee); + } catch (OAuthException $e) { + if ($e->getMessage() == 'The authorized token does not equal the ' . + 'submitted token.') { + $this->clientError(_('Not authorized.')); + return; + } else { + $this->clientError(_('Couldn\'t convert request token to ' . + 'access token.')); + return; + } + } catch (OMB_RemoteServiceException $e) { + $this->clientError(_('Unknown version of OMB protocol.')); + return; + } catch (Exception $e) { + common_debug('Got exception ' . print_r($e, true), __FILE__); + $this->clientError($e->getMessage()); return; } - # XXX: possible attack point; subscribe and return someone else's profile URI + /* The service URLs are not accessible from datastore, so setting them + after insertion of the profile. */ + $remote = Remote_profile::staticGet('uri', $service->getListenerURI()); - $remote = Remote_profile::staticGet('uri', $omb['listener']); + $orig_remote = clone($remote); - if ($remote) { - $exists = true; - $profile = Profile::staticGet($remote->id); - $orig_remote = clone($remote); - $orig_profile = clone($profile); - # XXX: compare current postNotice and updateProfile URLs to the ones - # stored in the DB to avoid (possibly...) above attack - } else { - $exists = false; - $remote = new Remote_profile(); - $remote->uri = $omb['listener']; - $profile = new Profile(); - } + $remote->postnoticeurl = + $service->getServiceURI(OMB_ENDPOINT_POSTNOTICE); + $remote->updateprofileurl = + $service->getServiceURI(OMB_ENDPOINT_UPDATEPROFILE); - $profile->nickname = $nickname; - $profile->profileurl = $profile_url; - - if (!is_null($fullname)) { - $profile->fullname = $fullname; - } - if (!is_null($homepage)) { - $profile->homepage = $homepage; - } - if (!is_null($bio)) { - $profile->bio = $bio; - } - if (!is_null($location)) { - $profile->location = $location; - } - - if ($exists) { - $profile->update($orig_profile); - } else { - $profile->created = DB_DataObject_Cast::dateTime(); # current time - $id = $profile->insert(); - if (!$id) { - $this->serverError(_('Error inserting new profile')); - return; - } - $remote->id = $id; - } - - if ($avatar_url) { - if (!$this->add_avatar($profile, $avatar_url)) { - $this->serverError(_('Error inserting avatar')); - return; - } - } - - $remote->postnoticeurl = $omb['post_notice_url']; - $remote->updateprofileurl = $omb['update_profile_url']; - - if ($exists) { - if (!$remote->update($orig_remote)) { + if (!$remote->update($orig_remote)) { $this->serverError(_('Error updating remote profile')); return; - } - } else { - $remote->created = DB_DataObject_Cast::dateTime(); # current time - if (!$remote->insert()) { - $this->serverError(_('Error inserting remote profile')); - return; - } } - if ($user->hasBlocked($profile)) { - $this->clientError(_('That user has blocked you from subscribing.')); - return; - } - - $sub = new Subscription(); - - $sub->subscriber = $remote->id; - $sub->subscribed = $user->id; - - $sub_exists = false; - - if ($sub->find(true)) { - $sub_exists = true; - $orig_sub = clone($sub); - } else { - $sub_exists = false; - $sub->created = DB_DataObject_Cast::dateTime(); # current time - } - - $sub->token = $newtok; - $sub->secret = $newsecret; - - if ($sub_exists) { - $result = $sub->update($orig_sub); - } else { - $result = $sub->insert(); - } - - if (!$result) { - common_log_db_error($sub, ($sub_exists) ? 'UPDATE' : 'INSERT', __FILE__); - $this->clientError(_('Couldn\'t insert new subscription.')); - return; - } - - # Notify user, if necessary - - mail_subscribe_notify_profile($user, $profile); - - # Clear the data + /* Clear the session data. */ unset($_SESSION['oauth_authorization_request']); - # If we show subscriptions in reverse chron order, this should - # show up close to the top of the page - + /* If we show subscriptions in reverse chronological order, the new one + should show up close to the top of the page. */ common_redirect(common_local_url('subscribers', array('nickname' => $user->nickname)), 303); } - - function add_avatar($profile, $url) - { - $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); - copy($url, $temp_filename); - $imagefile = new ImageFile($profile->id, $temp_filename); - $filename = Avatar::filename($profile->id, - image_type_to_extension($imagefile->type), - null, - common_timestamp()); - rename($temp_filename, Avatar::path($filename)); - return $profile->setOriginal($filename); - } - - function access_token($omb) - { - - common_debug('starting request for access token', __FILE__); - - $con = omb_oauth_consumer(); - $tok = new OAuthToken($omb['token'], $omb['secret']); - - common_debug('using request token "'.$tok.'"', __FILE__); - - $url = $omb['access_token_url']; - - common_debug('using access token url "'.$url.'"', __FILE__); - - # XXX: Is this the right thing to do? Strip off GET params and make them - # POST params? Seems wrong to me. - - $parsed = parse_url($url); - $params = array(); - parse_str($parsed['query'], $params); - - $req = OAuthRequest::from_consumer_and_token($con, $tok, "POST", $url, $params); - - $req->set_parameter('omb_version', OMB_VERSION_01); - - # XXX: test to see if endpoint accepts this signature method - - $req->sign_request(omb_hmac_sha1(), $con, $tok); - - # We re-use this tool's fetcher, since it's pretty good - - common_debug('posting to access token url "'.$req->get_normalized_http_url().'"', __FILE__); - common_debug('posting request data "'.$req->to_postdata().'"', __FILE__); - - $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); - $result = $fetcher->post($req->get_normalized_http_url(), - $req->to_postdata(), - array('User-Agent: Laconica/' . LACONICA_VERSION)); - - common_debug('got result: "'.print_r($result,true).'"', __FILE__); - - if ($result->status != 200) { - return null; - } - - parse_str($result->body, $return); - - return array($return['oauth_token'], $return['oauth_token_secret']); - } } diff --git a/actions/postnotice.php b/actions/postnotice.php index eb2d63b61c..74be47119a 100644 --- a/actions/postnotice.php +++ b/actions/postnotice.php @@ -1,5 +1,16 @@ + * @author Robin Millette + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + * * Laconica - a distributed open-source microblogging tool * Copyright (C) 2008, 2009, Control Yourself, Inc. * @@ -17,75 +28,49 @@ * along with this program. If not, see . */ -if (!defined('LACONICA')) { exit(1); } +if (!defined('LACONICA')) { + exit(1); +} -require_once(INSTALLDIR.'/lib/omb.php'); +require_once INSTALLDIR.'/lib/omb.php'; +require_once INSTALLDIR.'/extlib/libomb/service_provider.php'; +/** + * Handler for postnotice action + * + * @category Action + * @package Laconica + * @author Evan Prodromou + * @author Robin Millette + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + */ class PostnoticeAction extends Action { function handle($args) { parent::handle($args); + if (!$this->checkNotice()) { + return; + } try { - common_remove_magic_from_request(); - $req = OAuthRequest::from_request('POST', common_local_url('postnotice')); - # Note: server-to-server function! - $server = omb_oauth_server(); - list($consumer, $token) = $server->verify_request($req); - if ($this->save_notice($req, $consumer, $token)) { - print "omb_version=".OMB_VERSION_01; - } - } catch (OAuthException $e) { + $srv = new OMB_Service_Provider(null, omb_oauth_datastore(), + omb_oauth_server()); + $srv->handlePostNotice(); + } catch (Exception $e) { $this->serverError($e->getMessage()); return; } } - function save_notice(&$req, &$consumer, &$token) + function checkNotice() { - $version = $req->get_parameter('omb_version'); - if ($version != OMB_VERSION_01) { - $this->clientError(_('Unsupported OMB version'), 400); - return false; - } - # First, check to see - $listenee = $req->get_parameter('omb_listenee'); - $remote_profile = Remote_profile::staticGet('uri', $listenee); - if (!$remote_profile) { - $this->clientError(_('Profile unknown'), 403); - return false; - } - $sub = Subscription::staticGet('token', $token->key); - if (!$sub) { - $this->clientError(_('No such subscription'), 403); - return false; - } - $content = $req->get_parameter('omb_notice_content'); - $content_shortened = common_shorten_links($content); - if (mb_strlen($content_shortened) > 140) { + $content = common_shorten_links($_POST['omb_notice_content']); + if (mb_strlen($content) > 140) { $this->clientError(_('Invalid notice content'), 400); return false; } - $notice_uri = $req->get_parameter('omb_notice'); - if (!Validate::uri($notice_uri) && - !common_valid_tag($notice_uri)) { - $this->clientError(_('Invalid notice uri'), 400); - return false; - } - $notice_url = $req->get_parameter('omb_notice_url'); - if ($notice_url && !common_valid_http_url($notice_url)) { - $this->clientError(_('Invalid notice url'), 400); - return false; - } - $notice = Notice::staticGet('uri', $notice_uri); - if (!$notice) { - $notice = Notice::saveNew($remote_profile->id, $content, 'omb', false, null, $notice_uri); - if (is_string($notice)) { - common_server_serror($notice, 500); - return false; - } - common_broadcast_notice($notice, true); - } return true; } } +?> diff --git a/actions/remotesubscribe.php b/actions/remotesubscribe.php index e658f8d374..5122c11728 100644 --- a/actions/remotesubscribe.php +++ b/actions/remotesubscribe.php @@ -1,5 +1,16 @@ + * @author Robin Millette + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + * * Laconica - a distributed open-source microblogging tool * Copyright (C) 2008, 2009, Control Yourself, Inc. * @@ -15,12 +26,27 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . + **/ + +if (!defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/omb.php'; +require_once INSTALLDIR.'/extlib/libomb/service_consumer.php'; +require_once INSTALLDIR.'/extlib/libomb/profile.php'; + +/** + * Handler for remote subscription + * + * @category Action + * @package Laconica + * @author Evan Prodromou + * @author Robin Millette + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ */ -if (!defined('LACONICA')) { exit(1); } - -require_once(INSTALLDIR.'/lib/omb.php'); - class RemotesubscribeAction extends Action { var $nickname; @@ -36,7 +62,7 @@ class RemotesubscribeAction extends Action return false; } - $this->nickname = $this->trimmed('nickname'); + $this->nickname = $this->trimmed('nickname'); $this->profile_url = $this->trimmed('profile_url'); return true; @@ -47,7 +73,7 @@ class RemotesubscribeAction extends Action parent::handle($args); if ($_SERVER['REQUEST_METHOD'] == 'POST') { - # CSRF protection + /* Use a session token for CSRF protection. */ $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { $this->showForm(_('There was a problem with your session token. '. @@ -90,8 +116,8 @@ class RemotesubscribeAction extends Action function showContent() { - # id = remotesubscribe conflicts with the - # button on profile page + /* The id 'remotesubscribe' conflicts with the + button on profile page. */ $this->elementStart('form', array('id' => 'form_remote_subscribe', 'method' => 'post', 'class' => 'form_settings', @@ -117,13 +143,13 @@ class RemotesubscribeAction extends Action function remoteSubscription() { - $user = $this->getUser(); - - if (!$user) { + if (!$this->nickname) { $this->showForm(_('No such user.')); return; } + $user = User::staticGet('nickname', $this->nickname); + $this->profile_url = $this->trimmed('profile_url'); if (!$this->profile_url) { @@ -131,233 +157,37 @@ class RemotesubscribeAction extends Action return; } - if (!Validate::uri($this->profile_url, array('allowed_schemes' => array('http', 'https')))) { + if (!Validate::uri($this->profile_url, + array('allowed_schemes' => array('http', 'https')))) { $this->showForm(_('Invalid profile URL (bad format)')); return; } - $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); - $yadis = Auth_Yadis_Yadis::discover($this->profile_url, $fetcher); - - if (!$yadis || $yadis->failed) { - $this->showForm(_('Not a valid profile URL (no YADIS document).')); + try { + $service = new OMB_Service_Consumer($this->profile_url, + common_root_url(), + omb_oauth_datastore()); + } catch (OMB_InvalidYadisException $e) { + $this->showForm(_('Not a valid profile URL (no YADIS document or ' . + 'no or invalid XRDS defined).')); return; } - # XXX: a little liberal for sites that accidentally put whitespace before the xml declaration - - $xrds =& Auth_Yadis_XRDS::parseXRDS(trim($yadis->response_text)); - - if (!$xrds) { - $this->showForm(_('Not a valid profile URL (no XRDS defined).')); - return; - } - - $omb = $this->getOmb($xrds); - - if (!$omb) { - $this->showForm(_('Not a valid profile URL (incorrect services).')); - return; - } - - if (omb_service_uri($omb[OAUTH_ENDPOINT_REQUEST]) == - common_local_url('requesttoken')) - { + if ($service->getServiceURI(OAUTH_ENDPOINT_REQUEST) == + common_local_url('requesttoken') || + User::staticGet('uri', $service->getRemoteUserURI())) { $this->showForm(_('That\'s a local profile! Login to subscribe.')); return; } - if (User::staticGet('uri', omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]))) { - $this->showForm(_('That\'s a local profile! Login to subscribe.')); - return; - } - - list($token, $secret) = $this->requestToken($omb); - - if (!$token || !$secret) { + try { + $service->requestToken(); + } catch (OMB_RemoteServiceException $e) { $this->showForm(_('Couldn\'t get a request token.')); return; } - $this->requestAuthorization($user, $omb, $token, $secret); - } - - function getUser() - { - $user = null; - if ($this->nickname) { - $user = User::staticGet('nickname', $this->nickname); - } - return $user; - } - - function getOmb($xrds) - { - static $omb_endpoints = array(OMB_ENDPOINT_UPDATEPROFILE, OMB_ENDPOINT_POSTNOTICE); - static $oauth_endpoints = array(OAUTH_ENDPOINT_REQUEST, OAUTH_ENDPOINT_AUTHORIZE, - OAUTH_ENDPOINT_ACCESS); - $omb = array(); - - # XXX: the following code could probably be refactored to eliminate dupes - - $oauth_services = omb_get_services($xrds, OAUTH_DISCOVERY); - - if (!$oauth_services) { - return null; - } - - $oauth_service = $oauth_services[0]; - - $oauth_xrd = $this->getXRD($oauth_service, $xrds); - - if (!$oauth_xrd) { - return null; - } - - if (!$this->addServices($oauth_xrd, $oauth_endpoints, $omb)) { - return null; - } - - $omb_services = omb_get_services($xrds, OMB_NAMESPACE); - - if (!$omb_services) { - return null; - } - - $omb_service = $omb_services[0]; - - $omb_xrd = $this->getXRD($omb_service, $xrds); - - if (!$omb_xrd) { - return null; - } - - if (!$this->addServices($omb_xrd, $omb_endpoints, $omb)) { - return null; - } - - # XXX: check that we got all the services we needed - - foreach (array_merge($omb_endpoints, $oauth_endpoints) as $type) { - if (!array_key_exists($type, $omb) || !$omb[$type]) { - return null; - } - } - - if (!omb_local_id($omb[OAUTH_ENDPOINT_REQUEST])) { - return null; - } - - return $omb; - } - - function getXRD($main_service, $main_xrds) - { - $uri = omb_service_uri($main_service); - if (strpos($uri, "#") !== 0) { - # FIXME: more rigorous handling of external service definitions - return null; - } - $id = substr($uri, 1); - $nodes = $main_xrds->allXrdNodes; - $parser = $main_xrds->parser; - foreach ($nodes as $node) { - $attrs = $parser->attributes($node); - if (array_key_exists('xml:id', $attrs) && - $attrs['xml:id'] == $id) { - # XXX: trick the constructor into thinking this is the only node - $bogus_nodes = array($node); - return new Auth_Yadis_XRDS($parser, $bogus_nodes); - } - } - return null; - } - - function addServices($xrd, $types, &$omb) - { - foreach ($types as $type) { - $matches = omb_get_services($xrd, $type); - if ($matches) { - $omb[$type] = $matches[0]; - } else { - # no match for type - return false; - } - } - return true; - } - - function requestToken($omb) - { - $con = omb_oauth_consumer(); - - $url = omb_service_uri($omb[OAUTH_ENDPOINT_REQUEST]); - - # XXX: Is this the right thing to do? Strip off GET params and make them - # POST params? Seems wrong to me. - - $parsed = parse_url($url); - $params = array(); - parse_str($parsed['query'], $params); - - $req = OAuthRequest::from_consumer_and_token($con, null, "POST", $url, $params); - - $listener = omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]); - - if (!$listener) { - return null; - } - - $req->set_parameter('omb_listener', $listener); - $req->set_parameter('omb_version', OMB_VERSION_01); - - # XXX: test to see if endpoint accepts this signature method - - $req->sign_request(omb_hmac_sha1(), $con, null); - - # We re-use this tool's fetcher, since it's pretty good - - $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); - - $result = $fetcher->post($req->get_normalized_http_url(), - $req->to_postdata(), - array('User-Agent: Laconica/' . LACONICA_VERSION)); - if ($result->status != 200) { - return null; - } - - parse_str($result->body, $return); - - return array($return['oauth_token'], $return['oauth_token_secret']); - } - - function requestAuthorization($user, $omb, $token, $secret) - { - $con = omb_oauth_consumer(); - $tok = new OAuthToken($token, $secret); - - $url = omb_service_uri($omb[OAUTH_ENDPOINT_AUTHORIZE]); - - # XXX: Is this the right thing to do? Strip off GET params and make them - # POST params? Seems wrong to me. - - $parsed = parse_url($url); - $params = array(); - parse_str($parsed['query'], $params); - - $req = OAuthRequest::from_consumer_and_token($con, $tok, 'GET', $url, $params); - - # We send over a ton of information. This lets the other - # server store info about our user, and it lets the current - # user decide if they really want to authorize the subscription. - - $req->set_parameter('omb_version', OMB_VERSION_01); - $req->set_parameter('omb_listener', omb_local_id($omb[OAUTH_ENDPOINT_REQUEST])); - $req->set_parameter('omb_listenee', $user->uri); - $req->set_parameter('omb_listenee_profile', common_profile_url($user->nickname)); - $req->set_parameter('omb_listenee_nickname', $user->nickname); - $req->set_parameter('omb_listenee_license', common_config('license', 'url')); - + /* Create an OMB_Profile from $user. */ $profile = $user->getProfile(); if (!$profile) { common_log_db_error($user, 'SELECT', __FILE__); @@ -365,49 +195,16 @@ class RemotesubscribeAction extends Action return; } - if (!is_null($profile->fullname)) { - $req->set_parameter('omb_listenee_fullname', $profile->fullname); - } - if (!is_null($profile->homepage)) { - $req->set_parameter('omb_listenee_homepage', $profile->homepage); - } - if (!is_null($profile->bio)) { - $req->set_parameter('omb_listenee_bio', $profile->bio); - } - if (!is_null($profile->location)) { - $req->set_parameter('omb_listenee_location', $profile->location); - } - $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); - if ($avatar) { - $req->set_parameter('omb_listenee_avatar', $avatar->url); - } - - # XXX: add a nonce to prevent replay attacks - - $req->set_parameter('oauth_callback', common_local_url('finishremotesubscribe')); - - # XXX: test to see if endpoint accepts this signature method - - $req->sign_request(omb_hmac_sha1(), $con, $tok); - - # store all our info here - - $omb['listenee'] = $user->nickname; - $omb['listener'] = omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]); - $omb['token'] = $token; - $omb['secret'] = $secret; - # call doesn't work after bounce back so we cache; maybe serialization issue...? - $omb['access_token_url'] = omb_service_uri($omb[OAUTH_ENDPOINT_ACCESS]); - $omb['post_notice_url'] = omb_service_uri($omb[OMB_ENDPOINT_POSTNOTICE]); - $omb['update_profile_url'] = omb_service_uri($omb[OMB_ENDPOINT_UPDATEPROFILE]); + $target_url = $service->requestAuthorization( + profile_to_omb_profile($user->uri, $profile), + common_local_url('finishremotesubscribe')); common_ensure_session(); - $_SESSION['oauth_authorization_request'] = $omb; + $_SESSION['oauth_authorization_request'] = serialize($service); - # Redirect to authorization service - - common_redirect($req->to_url(), 303); - return; + /* Redirect to the remote service for authorization. */ + common_redirect($target_url, 303); } } +?> diff --git a/actions/requesttoken.php b/actions/requesttoken.php index 8d1e3f0043..8328962f2d 100644 --- a/actions/requesttoken.php +++ b/actions/requesttoken.php @@ -34,6 +34,7 @@ if (!defined('LACONICA')) { } require_once INSTALLDIR.'/lib/omb.php'; +require_once INSTALLDIR.'/extlib/libomb/service_provider.php'; /** * Request token action class. @@ -49,17 +50,17 @@ class RequesttokenAction extends Action { /** * Is read only? - * + * * @return boolean false */ - function isReadOnly($args) + function isReadOnly() { return false; } - + /** * Class handler. - * + * * @param array $args array of arguments * * @return void @@ -68,14 +69,12 @@ class RequesttokenAction extends Action { parent::handle($args); try { - common_remove_magic_from_request(); - $req = OAuthRequest::from_request('POST', common_local_url('requesttoken')); - $server = omb_oauth_server(); - $token = $server->fetch_request_token($req); - print $token; - } catch (OAuthException $e) { + $srv = new OMB_Service_Provider(null, omb_oauth_datastore(), + omb_oauth_server()); + $srv->writeRequestToken(); + } catch (Exception $e) { $this->serverError($e->getMessage()); } } } - +?> diff --git a/actions/updateprofile.php b/actions/updateprofile.php index d8b62fb090..345c28b8d7 100644 --- a/actions/updateprofile.php +++ b/actions/updateprofile.php @@ -1,5 +1,16 @@ + * @author Robin Millette + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + * * Laconica - a distributed open-source microblogging tool * Copyright (C) 2008, 2009, Control Yourself, Inc. * @@ -17,167 +28,37 @@ * along with this program. If not, see . */ -if (!defined('LACONICA')) { exit(1); } +if (!defined('LACONICA')) { + exit(1); +} -require_once(INSTALLDIR.'/lib/omb.php'); +require_once INSTALLDIR.'/lib/omb.php'; +require_once INSTALLDIR.'/extlib/libomb/service_provider.php'; +/** + * Handle an updateprofile action + * + * @category Action + * @package Laconica + * @author Evan Prodromou + * @author Robin Millette + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + */ class UpdateprofileAction extends Action { - + function handle($args) { parent::handle($args); try { - common_remove_magic_from_request(); - $req = OAuthRequest::from_request('POST', common_local_url('updateprofile')); - # Note: server-to-server function! - $server = omb_oauth_server(); - list($consumer, $token) = $server->verify_request($req); - if ($this->update_profile($req, $consumer, $token)) { - header('HTTP/1.1 200 OK'); - header('Content-type: text/plain'); - print "omb_version=".OMB_VERSION_01; - } - } catch (OAuthException $e) { + $srv = new OMB_Service_Provider(null, omb_oauth_datastore(), + omb_oauth_server()); + $srv->handleUpdateProfile(); + } catch (Exception $e) { $this->serverError($e->getMessage()); return; } } - - function update_profile($req, $consumer, $token) - { - $version = $req->get_parameter('omb_version'); - if ($version != OMB_VERSION_01) { - $this->clientError(_('Unsupported OMB version'), 400); - return false; - } - # First, check to see if listenee exists - $listenee = $req->get_parameter('omb_listenee'); - $remote = Remote_profile::staticGet('uri', $listenee); - if (!$remote) { - $this->clientError(_('Profile unknown'), 404); - return false; - } - # Second, check to see if they should be able to post updates! - # We see if there are any subscriptions to that remote user with - # the given token. - - $sub = new Subscription(); - $sub->subscribed = $remote->id; - $sub->token = $token->key; - if (!$sub->find(true)) { - $this->clientError(_('You did not send us that profile'), 403); - return false; - } - - $profile = Profile::staticGet('id', $remote->id); - if (!$profile) { - # This one is our fault - $this->serverError(_('Remote profile with no matching profile'), 500); - return false; - } - $nickname = $req->get_parameter('omb_listenee_nickname'); - if ($nickname && !Validate::string($nickname, array('min_length' => 1, - 'max_length' => 64, - 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { - $this->clientError(_('Nickname must have only lowercase letters and numbers and no spaces.')); - return false; - } - $license = $req->get_parameter('omb_listenee_license'); - if ($license && !common_valid_http_url($license)) { - $this->clientError(sprintf(_("Invalid license URL '%s'"), $license)); - return false; - } - $profile_url = $req->get_parameter('omb_listenee_profile'); - if ($profile_url && !common_valid_http_url($profile_url)) { - $this->clientError(sprintf(_("Invalid profile URL '%s'."), $profile_url)); - return false; - } - # optional stuff - $fullname = $req->get_parameter('omb_listenee_fullname'); - if ($fullname && mb_strlen($fullname) > 255) { - $this->clientError(_("Full name is too long (max 255 chars).")); - return false; - } - $homepage = $req->get_parameter('omb_listenee_homepage'); - if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) { - $this->clientError(sprintf(_("Invalid homepage '%s'"), $homepage)); - return false; - } - $bio = $req->get_parameter('omb_listenee_bio'); - if ($bio && mb_strlen($bio) > 140) { - $this->clientError(_("Bio is too long (max 140 chars).")); - return false; - } - $location = $req->get_parameter('omb_listenee_location'); - if ($location && mb_strlen($location) > 255) { - $this->clientError(_("Location is too long (max 255 chars).")); - return false; - } - $avatar = $req->get_parameter('omb_listenee_avatar'); - if ($avatar) { - if (!common_valid_http_url($avatar) || strlen($avatar) > 255) { - $this->clientError(sprintf(_("Invalid avatar URL '%s'"), $avatar)); - return false; - } - $size = @getimagesize($avatar); - if (!$size) { - $this->clientError(sprintf(_("Can't read avatar URL '%s'"), $avatar)); - return false; - } - if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) { - $this->clientError(sprintf(_("Wrong size image at '%s'"), $avatar)); - return false; - } - if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG, - IMAGETYPE_PNG))) { - $this->clientError(sprintf(_("Wrong image type for '%s'"), $avatar)); - return false; - } - } - - $orig_profile = clone($profile); - - /* Use values even if they are an empty string. Parsing an empty string in - updateProfile is the specified way of clearing a parameter in OMB. */ - if (!is_null($nickname)) { - $profile->nickname = $nickname; - } - if (!is_null($profile_url)) { - $profile->profileurl = $profile_url; - } - if (!is_null($fullname)) { - $profile->fullname = $fullname; - } - if (!is_null($homepage)) { - $profile->homepage = $homepage; - } - if (!is_null($bio)) { - $profile->bio = $bio; - } - if (!is_null($location)) { - $profile->location = $location; - } - - if (!$profile->update($orig_profile)) { - $this->serverError(_('Could not save new profile info'), 500); - return false; - } else { - if ($avatar) { - $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar'); - copy($avatar, $temp_filename); - $imagefile = new ImageFile($profile->id, $temp_filename); - $filename = Avatar::filename($profile->id, - image_type_to_extension($imagefile->type), - null, - common_timestamp()); - rename($temp_filename, Avatar::path($filename)); - if (!$profile->setOriginal($filename)) { - $this->serverError(_('Could not save avatar info'), 500); - return false; - } - } - return true; - } - } } +?> diff --git a/actions/userauthorization.php b/actions/userauthorization.php index 8dc2c808d6..d5b6a69986 100644 --- a/actions/userauthorization.php +++ b/actions/userauthorization.php @@ -1,5 +1,16 @@ + * @author Robin Millette + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + * * Laconica - a distributed open-source microblogging tool * Copyright (C) 2008, 2009, Control Yourself, Inc. * @@ -17,9 +28,13 @@ * along with this program. If not, see . */ -if (!defined('LACONICA')) { exit(1); } +if (!defined('LACONICA')) { + exit(1); +} -require_once(INSTALLDIR.'/lib/omb.php'); +require_once INSTALLDIR.'/lib/omb.php'; +require_once INSTALLDIR.'/extlib/libomb/service_provider.php'; +require_once INSTALLDIR.'/extlib/libomb/profile.php'; define('TIMESTAMP_THRESHOLD', 300); class UserauthorizationAction extends Action @@ -32,42 +47,58 @@ class UserauthorizationAction extends Action parent::handle($args); if ($_SERVER['REQUEST_METHOD'] == 'POST') { - # CSRF protection + /* Use a session token for CSRF protection. */ $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { - $params = $this->getStoredParams(); - $this->showForm($params, _('There was a problem with your session token. '. - 'Try again, please.')); + $srv = $this->getStoredParams(); + $this->showForm($srv->getRemoteUser(), _('There was a problem ' . + 'with your session token. Try again, ' . + 'please.')); return; } - # We've shown the form, now post user's choice + /* We've shown the form, now post user's choice. */ $this->sendAuthorization(); } else { if (!common_logged_in()) { - # Go log in, and then come back + /* Go log in, and then come back. */ common_set_returnto($_SERVER['REQUEST_URI']); common_redirect(common_local_url('login')); return; } + $user = common_current_user(); + $profile = $user->getProfile(); + if (!$profile) { + common_log_db_error($user, 'SELECT', __FILE__); + $this->serverError(_('User without matching profile')); + return; + } + + /* TODO: If no token is passed the user should get a prompt to enter + it according to OAuth Core 1.0. */ try { - $this->validateRequest(); - $this->storeParams($_GET); - $this->showForm($_GET); - } catch (OAuthException $e) { + $this->validateOmb(); + $srv = new OMB_Service_Provider( + profile_to_omb_profile($_GET['omb_listener'], $profile), + omb_oauth_datastore()); + + $remote_user = $srv->handleUserAuth(); + } catch (Exception $e) { $this->clearParams(); $this->clientError($e->getMessage()); return; } + $this->storeParams($srv); + $this->showForm($remote_user); } } function showForm($params, $error=null) { $this->params = $params; - $this->error = $error; + $this->error = $error; $this->showPage(); } @@ -79,23 +110,24 @@ class UserauthorizationAction extends Action function showPageNotice() { $this->element('p', null, _('Please check these details to make sure '. - 'that you want to subscribe to this user\'s notices. '. - 'If you didn\'t just ask to subscribe to someone\'s notices, '. - 'click "Reject".')); + 'that you want to subscribe to this ' . + 'user\'s notices. If you didn\'t just ask ' . + 'to subscribe to someone\'s notices, '. + 'click “Reject”.')); } function showContent() { $params = $this->params; - $nickname = $params['omb_listenee_nickname']; - $profile = $params['omb_listenee_profile']; - $license = $params['omb_listenee_license']; - $fullname = $params['omb_listenee_fullname']; - $homepage = $params['omb_listenee_homepage']; - $bio = $params['omb_listenee_bio']; - $location = $params['omb_listenee_location']; - $avatar = $params['omb_listenee_avatar']; + $nickname = $params->getNickname(); + $profile = $params->getProfileURL(); + $license = $params->getLicenseURL(); + $fullname = $params->getFullname(); + $homepage = $params->getHomepage(); + $bio = $params->getBio(); + $location = $params->getLocation(); + $avatar = $params->getAvatarURL(); $this->elementStart('div', array('class' => 'profile')); $this->elementStart('div', 'entity_profile vcard'); @@ -172,11 +204,14 @@ class UserauthorizationAction extends Action 'id' => 'userauthorization', 'class' => 'form_user_authorization', 'name' => 'userauthorization', - 'action' => common_local_url('userauthorization'))); + 'action' => common_local_url( + 'userauthorization'))); $this->hidden('token', common_session_token()); - $this->submit('accept', _('Accept'), 'submit accept', null, _('Subscribe to this user')); - $this->submit('reject', _('Reject'), 'submit reject', null, _('Reject this subscription')); + $this->submit('accept', _('Accept'), 'submit accept', null, + _('Subscribe to this user')); + $this->submit('reject', _('Reject'), 'submit reject', null, + _('Reject this subscription')); $this->elementEnd('form'); $this->elementEnd('li'); $this->elementEnd('ul'); @@ -186,218 +221,56 @@ class UserauthorizationAction extends Action function sendAuthorization() { - $params = $this->getStoredParams(); + $srv = $this->getStoredParams(); - if (!$params) { + if (is_null($srv)) { $this->clientError(_('No authorization request!')); return; } - $callback = $params['oauth_callback']; - - if ($this->arg('accept')) { - if (!$this->authorizeToken($params)) { - $this->clientError(_('Error authorizing token')); - } - if (!$this->saveRemoteProfile($params)) { - $this->clientError(_('Error saving remote profile')); - } - if (!$callback) { - $this->showAcceptMessage($params['oauth_token']); - } else { - $newparams = array(); - $newparams['oauth_token'] = $params['oauth_token']; - $newparams['omb_version'] = OMB_VERSION_01; - $user = User::staticGet('uri', $params['omb_listener']); - $profile = $user->getProfile(); - if (!$profile) { - common_log_db_error($user, 'SELECT', __FILE__); - $this->serverError(_('User without matching profile')); - return; - } - $newparams['omb_listener_nickname'] = $user->nickname; - $newparams['omb_listener_profile'] = common_local_url('showstream', - array('nickname' => $user->nickname)); - if (!is_null($profile->fullname)) { - $newparams['omb_listener_fullname'] = $profile->fullname; - } - if (!is_null($profile->homepage)) { - $newparams['omb_listener_homepage'] = $profile->homepage; - } - if (!is_null($profile->bio)) { - $newparams['omb_listener_bio'] = $profile->bio; - } - if (!is_null($profile->location)) { - $newparams['omb_listener_location'] = $profile->location; - } - $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); - if ($avatar) { - $newparams['omb_listener_avatar'] = $avatar->url; - } - $parts = array(); - foreach ($newparams as $k => $v) { - $parts[] = $k . '=' . OAuthUtil::urlencode_rfc3986($v); - } - $query_string = implode('&', $parts); - $parsed = parse_url($callback); - $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string; - common_redirect($url, 303); - } + $accepted = $this->arg('accept'); + try { + list($val, $token) = $srv->continueUserAuth($accepted); + } catch (Exception $e) { + $this->clientError($e->getMessage()); + return; + } + if ($val !== false) { + common_redirect($val, 303); + } elseif ($accepted) { + $this->showAcceptMessage($token); } else { - if (!$callback) { - $this->showRejectMessage(); - } else { - # XXX: not 100% sure how to signal failure... just redirect without token? - common_redirect($callback, 303); - } + $this->showRejectMessage(); } } - function authorizeToken(&$params) - { - $token_field = $params['oauth_token']; - $rt = new Token(); - $rt->tok = $token_field; - $rt->type = 0; - $rt->state = 0; - if ($rt->find(true)) { - $orig_rt = clone($rt); - $rt->state = 1; # Authorized but not used - if ($rt->update($orig_rt)) { - return true; - } - } - return false; - } - - # XXX: refactor with similar code in finishremotesubscribe.php - - function saveRemoteProfile(&$params) - { - # FIXME: we should really do this when the consumer comes - # back for an access token. If they never do, we've got stuff in a - # weird state. - - $nickname = $params['omb_listenee_nickname']; - $fullname = $params['omb_listenee_fullname']; - $profile_url = $params['omb_listenee_profile']; - $homepage = $params['omb_listenee_homepage']; - $bio = $params['omb_listenee_bio']; - $location = $params['omb_listenee_location']; - $avatar_url = $params['omb_listenee_avatar']; - - $listenee = $params['omb_listenee']; - $remote = Remote_profile::staticGet('uri', $listenee); - - if ($remote) { - $exists = true; - $profile = Profile::staticGet($remote->id); - $orig_remote = clone($remote); - $orig_profile = clone($profile); - } else { - $exists = false; - $remote = new Remote_profile(); - $remote->uri = $listenee; - $profile = new Profile(); - } - - $profile->nickname = $nickname; - $profile->profileurl = $profile_url; - - if (!is_null($fullname)) { - $profile->fullname = $fullname; - } - if (!is_null($homepage)) { - $profile->homepage = $homepage; - } - if (!is_null($bio)) { - $profile->bio = $bio; - } - if (!is_null($location)) { - $profile->location = $location; - } - - if ($exists) { - $profile->update($orig_profile); - } else { - $profile->created = DB_DataObject_Cast::dateTime(); # current time - $id = $profile->insert(); - if (!$id) { - return false; - } - $remote->id = $id; - } - - if ($exists) { - if (!$remote->update($orig_remote)) { - return false; - } - } else { - $remote->created = DB_DataObject_Cast::dateTime(); # current time - if (!$remote->insert()) { - return false; - } - } - - if ($avatar_url) { - if (!$this->addAvatar($profile, $avatar_url)) { - return false; - } - } - - $user = common_current_user(); - - $sub = new Subscription(); - $sub->subscriber = $user->id; - $sub->subscribed = $remote->id; - $sub->token = $params['oauth_token']; # NOTE: request token, not valid for use! - $sub->created = DB_DataObject_Cast::dateTime(); # current time - - if (!$sub->insert()) { - return false; - } - - return true; - } - - function addAvatar($profile, $url) - { - $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar'); - copy($url, $temp_filename); - $imagefile = new ImageFile($profile->id, $temp_filename); - $filename = Avatar::filename($profile->id, - image_type_to_extension($imagefile->type), - null, - common_timestamp()); - rename($temp_filename, Avatar::path($filename)); - return $profile->setOriginal($filename); - } - function showAcceptMessage($tok) { common_show_header(_('Subscription authorized')); $this->element('p', null, _('The subscription has been authorized, but no '. - 'callback URL was passed. Check with the site\'s instructions for '. - 'details on how to authorize the subscription. Your subscription token is:')); + 'callback URL was passed. Check with the site\'s ' . + 'instructions for details on how to authorize the ' . + 'subscription. Your subscription token is:')); $this->element('blockquote', 'token', $tok); common_show_footer(); } - function showRejectMessage($tok) + function showRejectMessage() { common_show_header(_('Subscription rejected')); $this->element('p', null, _('The subscription has been rejected, but no '. - 'callback URL was passed. Check with the site\'s instructions for '. - 'details on how to fully reject the subscription.')); + 'callback URL was passed. Check with the site\'s ' . + 'instructions for details on how to fully reject ' . + 'the subscription.')); common_show_footer(); } function storeParams($params) { common_ensure_session(); - $_SESSION['userauthorizationparams'] = $params; + $_SESSION['userauthorizationparams'] = serialize($params); } function clearParams() @@ -409,138 +282,65 @@ class UserauthorizationAction extends Action function getStoredParams() { common_ensure_session(); - $params = $_SESSION['userauthorizationparams']; + $params = unserialize($_SESSION['userauthorizationparams']); return $params; } - # Throws an OAuthException if anything goes wrong - - function validateRequest() - { - /* Find token. - TODO: If no token is passed the user should get a prompt to enter it - according to OAuth Core 1.0 */ - $t = new Token(); - $t->tok = $_GET['oauth_token']; - $t->type = 0; - if (!$t->find(true)) { - throw new OAuthException("Invalid request token: " . $_GET['oauth_token']); - } - - $this->validateOmb(); - return true; - } - function validateOmb() { - foreach (array('omb_version', 'omb_listener', 'omb_listenee', - 'omb_listenee_profile', 'omb_listenee_nickname', - 'omb_listenee_license') as $param) - { - if (!isset($_GET[$param]) || is_null($_GET[$param])) { - throw new OAuthException("Required parameter '$param' not found"); - } - } - # Now, OMB stuff - $version = $_GET['omb_version']; - if ($version != OMB_VERSION_01) { - throw new OAuthException("OpenMicroBlogging version '$version' not supported"); - } $listener = $_GET['omb_listener']; + $listenee = $_GET['omb_listenee']; + $nickname = $_GET['omb_listenee_nickname']; + $profile = $_GET['omb_listenee_profile']; + $user = User::staticGet('uri', $listener); if (!$user) { - throw new OAuthException("Listener URI '$listener' not found here"); + throw new Exception("Listener URI '$listener' not found here"); } $cur = common_current_user(); if ($cur->id != $user->id) { - throw new OAuthException("Can't add for another user!"); - } - $listenee = $_GET['omb_listenee']; - if (!Validate::uri($listenee) && - !common_valid_tag($listenee)) { - throw new OAuthException("Listenee URI '$listenee' not a recognizable URI"); - } - if (strlen($listenee) > 255) { - throw new OAuthException("Listenee URI '$listenee' too long"); + throw new Exception('Can\'t subscribe for another user!'); } $other = User::staticGet('uri', $listenee); if ($other) { - throw new OAuthException("Listenee URI '$listenee' is local user"); + throw new Exception("Listenee URI '$listenee' is local user"); } $remote = Remote_profile::staticGet('uri', $listenee); if ($remote) { - $sub = new Subscription(); + $sub = new Subscription(); $sub->subscriber = $user->id; $sub->subscribed = $remote->id; if ($sub->find(true)) { - throw new OAuthException("Already subscribed to user!"); + throw new Exception('You are already subscribed to this user.'); } } - $nickname = $_GET['omb_listenee_nickname']; - if (!Validate::string($nickname, array('min_length' => 1, - 'max_length' => 64, - 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { - throw new OAuthException('Nickname must have only letters and numbers and no spaces.'); - } - $profile = $_GET['omb_listenee_profile']; - if (!common_valid_http_url($profile)) { - throw new OAuthException("Invalid profile URL '$profile'."); + + if ($profile == common_profile_url($nickname)) { + throw new Exception("Profile URL '$profile' is for a local user."); } - if ($profile == common_local_url('showstream', array('nickname' => $nickname))) { - throw new OAuthException("Profile URL '$profile' is for a local user."); - } - - $license = $_GET['omb_listenee_license']; - if (!common_valid_http_url($license)) { - throw new OAuthException("Invalid license URL '$license'."); - } + $license = $_GET['omb_listenee_license']; $site_license = common_config('license', 'url'); if (!common_compatible_license($license, $site_license)) { - throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'."); - } - # optional stuff - $fullname = $_GET['omb_listenee_fullname']; - if ($fullname && mb_strlen($fullname) > 255) { - throw new OAuthException("Full name '$fullname' too long."); - } - $homepage = $_GET['omb_listenee_homepage']; - if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) { - throw new OAuthException("Invalid homepage '$homepage'"); - } - $bio = $_GET['omb_listenee_bio']; - if ($bio && mb_strlen($bio) > 140) { - throw new OAuthException("Bio too long '$bio'"); - } - $location = $_GET['omb_listenee_location']; - if ($location && mb_strlen($location) > 255) { - throw new OAuthException("Location too long '$location'"); + throw new Exception("Listenee stream license '$license' is not " . + "compatible with site license '$site_license'."); } $avatar = $_GET['omb_listenee_avatar']; if ($avatar) { if (!common_valid_http_url($avatar) || strlen($avatar) > 255) { - throw new OAuthException("Invalid avatar URL '$avatar'"); + throw new Exception("Invalid avatar URL '$avatar'"); } $size = @getimagesize($avatar); if (!$size) { - throw new OAuthException("Can't read avatar URL '$avatar'"); - } - if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) { - throw new OAuthException("Wrong size image at '$avatar'"); + throw new Exception("Can't read avatar URL '$avatar'."); } if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG))) { - throw new OAuthException("Wrong image type for '$avatar'"); + throw new Exception("Wrong image type for '$avatar'"); } } - $callback = $_GET['oauth_callback']; - if ($callback && !common_valid_http_url($callback)) { - throw new OAuthException("Invalid callback URL '$callback'"); - } - if ($callback && $callback == common_local_url('finishremotesubscribe')) { - throw new OAuthException("Callback URL '$callback' is for local site."); - } } } +?> diff --git a/actions/xrds.php b/actions/xrds.php index 9327a3c831..7518a5f709 100644 --- a/actions/xrds.php +++ b/actions/xrds.php @@ -34,6 +34,8 @@ if (!defined('LACONICA')) { } require_once INSTALLDIR.'/lib/omb.php'; +require_once INSTALLDIR.'/extlib/libomb/service_provider.php'; +require_once INSTALLDIR.'/extlib/libomb/xrds_mapper.php'; /** * XRDS for OpenID @@ -52,7 +54,7 @@ class XrdsAction extends Action * * @return boolean true */ - function isReadOnly($args) + function isReadOnly() { return true; } @@ -85,89 +87,31 @@ class XrdsAction extends Action */ function showXrds($user) { - header('Content-Type: application/xrds+xml'); - $this->startXML(); - $this->elementStart('XRDS', array('xmlns' => 'xri://$xrds')); - - $this->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', - 'xml:id' => 'oauth', - 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', - 'version' => '2.0')); - $this->element('Type', null, 'xri://$xrds*simple'); - $this->showService(OAUTH_ENDPOINT_REQUEST, - common_local_url('requesttoken'), - array(OAUTH_AUTH_HEADER, OAUTH_POST_BODY), - array(OAUTH_HMAC_SHA1), - $user->uri); - $this->showService(OAUTH_ENDPOINT_AUTHORIZE, - common_local_url('userauthorization'), - array(OAUTH_AUTH_HEADER, OAUTH_POST_BODY), - array(OAUTH_HMAC_SHA1)); - $this->showService(OAUTH_ENDPOINT_ACCESS, - common_local_url('accesstoken'), - array(OAUTH_AUTH_HEADER, OAUTH_POST_BODY), - array(OAUTH_HMAC_SHA1)); - $this->showService(OAUTH_ENDPOINT_RESOURCE, - null, - array(OAUTH_AUTH_HEADER, OAUTH_POST_BODY), - array(OAUTH_HMAC_SHA1)); - $this->elementEnd('XRD'); - - // XXX: decide whether to include user's ID/nickname in postNotice URL - $this->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', - 'xml:id' => 'omb', - 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', - 'version' => '2.0')); - $this->element('Type', null, 'xri://$xrds*simple'); - $this->showService(OMB_ENDPOINT_POSTNOTICE, - common_local_url('postnotice')); - $this->showService(OMB_ENDPOINT_UPDATEPROFILE, - common_local_url('updateprofile')); - $this->elementEnd('XRD'); - $this->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', - 'version' => '2.0')); - $this->element('Type', null, 'xri://$xrds*simple'); - $this->showService(OAUTH_DISCOVERY, - '#oauth'); - $this->showService(OMB_NAMESPACE, - '#omb'); - $this->elementEnd('XRD'); - $this->elementEnd('XRDS'); - $this->endXML(); - } - - /** - * Show service. - * - * @param string $type XRDS type - * @param string $uri URI - * @param array $params type parameters, null by default - * @param array $sigs type signatures, null by default - * @param string $localId local ID, null by default - * - * @return void - */ - function showService($type, $uri, $params=null, $sigs=null, $localId=null) - { - $this->elementStart('Service'); - if ($uri) { - $this->element('URI', null, $uri); - } - $this->element('Type', null, $type); - if ($params) { - foreach ($params as $param) { - $this->element('Type', null, $param); - } - } - if ($sigs) { - foreach ($sigs as $sig) { - $this->element('Type', null, $sig); - } - } - if ($localId) { - $this->element('LocalID', null, $localId); - } - $this->elementEnd('Service'); + $srv = new OMB_Service_Provider(profile_to_omb_profile($user->uri, + $user->getProfile())); + /* Use libomb’s default XRDS Writer. */ + $xrds_writer = null; + $srv->writeXRDS(new Laconica_XRDS_Mapper(), $xrds_writer); } } +class Laconica_XRDS_Mapper implements OMB_XRDS_Mapper +{ + protected $urls; + + public function __construct() + { + $this->urls = array( + OAUTH_ENDPOINT_REQUEST => 'requesttoken', + OAUTH_ENDPOINT_AUTHORIZE => 'userauthorization', + OAUTH_ENDPOINT_ACCESS => 'accesstoken', + OMB_ENDPOINT_POSTNOTICE => 'postnotice', + OMB_ENDPOINT_UPDATEPROFILE => 'updateprofile'); + } + + public function getURL($action) + { + return common_local_url($this->urls[$action]); + } +} +?> diff --git a/extlib/libomb/base_url_xrds_mapper.php b/extlib/libomb/base_url_xrds_mapper.php new file mode 100755 index 0000000000..6454595835 --- /dev/null +++ b/extlib/libomb/base_url_xrds_mapper.php @@ -0,0 +1,51 @@ +writeXRDS. + * + * 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 . + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ + +class OMB_Base_URL_XRDS_Mapper implements OMB_XRDS_Mapper { + + protected $urls; + + public function __construct($oauth_base, $omb_base) { + $this->urls = array( + OAUTH_ENDPOINT_REQUEST => $oauth_base . 'requesttoken', + OAUTH_ENDPOINT_AUTHORIZE => $oauth_base . 'userauthorization', + OAUTH_ENDPOINT_ACCESS => $oauth_base . 'accesstoken', + OMB_ENDPOINT_POSTNOTICE => $omb_base . 'postnotice', + OMB_ENDPOINT_UPDATEPROFILE => $omb_base . 'updateprofile'); + } + + public function getURL($action) { + return $this->urls[$action]; + } +} +?> diff --git a/extlib/libomb/constants.php b/extlib/libomb/constants.php new file mode 100644 index 0000000000..a097443acc --- /dev/null +++ b/extlib/libomb/constants.php @@ -0,0 +1,58 @@ +. + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ + +/** + * The OMB constants. + **/ + +define('OMB_VERSION_01', 'http://openmicroblogging.org/protocol/0.1'); + +/* The OMB version supported by this libomb version. */ +define('OMB_VERSION', OMB_VERSION_01); + +define('OMB_ENDPOINT_UPDATEPROFILE', OMB_VERSION . '/updateProfile'); +define('OMB_ENDPOINT_POSTNOTICE', OMB_VERSION . '/postNotice'); + +/** + * The OAuth constants. + **/ + +define('OAUTH_NAMESPACE', 'http://oauth.net/core/1.0/'); + +define('OAUTH_ENDPOINT_REQUEST', OAUTH_NAMESPACE.'endpoint/request'); +define('OAUTH_ENDPOINT_AUTHORIZE', OAUTH_NAMESPACE.'endpoint/authorize'); +define('OAUTH_ENDPOINT_ACCESS', OAUTH_NAMESPACE.'endpoint/access'); +define('OAUTH_ENDPOINT_RESOURCE', OAUTH_NAMESPACE.'endpoint/resource'); + +define('OAUTH_AUTH_HEADER', OAUTH_NAMESPACE.'parameters/auth-header'); +define('OAUTH_POST_BODY', OAUTH_NAMESPACE.'parameters/post-body'); + +define('OAUTH_HMAC_SHA1', OAUTH_NAMESPACE.'signature/HMAC-SHA1'); + +define('OAUTH_DISCOVERY', 'http://oauth.net/discovery/1.0'); +?> diff --git a/extlib/libomb/datastore.php b/extlib/libomb/datastore.php new file mode 100755 index 0000000000..ac51a4ab88 --- /dev/null +++ b/extlib/libomb/datastore.php @@ -0,0 +1,198 @@ +. + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ + +class OMB_Datastore extends OAuthDataStore { + + /********* + * OAUTH * + *********/ + + /** + * Revoke specified OAuth token + * + * Revokes the authorization token specified by $token_key. + * Throws exceptions in case of error. + * + * @param string $token_key The token to be revoked + * + * @access public + **/ + public function revoke_token($token_key) { + throw new Exception(); + } + + /** + * Authorize specified OAuth token + * + * Authorizes the authorization token specified by $token_key. + * Throws exceptions in case of error. + * + * @param string $token_key The token to be authorized + * + * @access public + **/ + public function authorize_token($token_key) { + throw new Exception(); + } + + /********* + * OMB * + *********/ + + /** + * Get profile by identifying URI + * + * Returns an OMB_Profile object representing the OMB profile identified by + * $identifier_uri. + * Returns null if there is no such OMB profile. + * Throws exceptions in case of other error. + * + * @param string $identifier_uri The OMB identifier URI specifying the + * requested profile + * + * @access public + * + * @return OMB_Profile The corresponding profile + **/ + public function getProfile($identifier_uri) { + throw new Exception(); + } + + /** + * Save passed profile + * + * Stores the OMB profile $profile. Overwrites an existing entry. + * Throws exceptions in case of error. + * + * @param OMB_Profile $profile The OMB profile which should be saved + * + * @access public + **/ + public function saveProfile($profile) { + throw new Exception(); + } + + /** + * Save passed notice + * + * Stores the OMB notice $notice. The datastore may change the passed notice. + * This might by neccessary for URIs depending on a database key. Note that + * it is the user’s duty to present a mechanism for his OMB_Datastore to + * appropriately change his OMB_Notice. TODO: Ugly. + * Throws exceptions in case of error. + * + * @param OMB_Notice $notice The OMB notice which should be saved + * + * @access public + **/ + public function saveNotice(&$notice) { + throw new Exception(); + } + + /** + * Get subscriptions of a given profile + * + * Returns an array containing subscription informations for the specified + * profile. Every array entry should in turn be an array with keys + * 'uri´: The identifier URI of the subscriber + * 'token´: The subscribe token + * 'secret´: The secret token + * Throws exceptions in case of error. + * + * @param string $subscribed_user_uri The OMB identifier URI specifying the + * subscribed profile + * + * @access public + * + * @return mixed An array containing the subscriptions or 0 if no + * subscription has been found. + **/ + public function getSubscriptions($subscribed_user_uri) { + throw new Exception(); + } + + /** + * Delete a subscription + * + * Deletes the subscription from $subscriber_uri to $subscribed_user_uri. + * Throws exceptions in case of error. + * + * @param string $subscriber_uri The OMB identifier URI specifying the + * subscribing profile + * + * @param string $subscribed_user_uri The OMB identifier URI specifying the + * subscribed profile + * + * @access public + **/ + public function deleteSubscription($subscriber_uri, $subscribed_user_uri) { + throw new Exception(); + } + + /** + * Save a subscription + * + * Saves the subscription from $subscriber_uri to $subscribed_user_uri. + * Throws exceptions in case of error. + * + * @param string $subscriber_uri The OMB identifier URI specifying + * the subscribing profile + * + * @param string $subscribed_user_uri The OMB identifier URI specifying + * the subscribed profile + * @param OAuthToken $token The access token + * + * @access public + **/ + public function saveSubscription($subscriber_uri, $subscribed_user_uri, + $token) { + throw new Exception(); + } +} +?> diff --git a/extlib/libomb/helper.php b/extlib/libomb/helper.php new file mode 100644 index 0000000000..a1f21f2680 --- /dev/null +++ b/extlib/libomb/helper.php @@ -0,0 +1,99 @@ +. + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ + +class OMB_Helper { + + /** + * Non-scalar constants + * + * The set of OMB and OAuth Services an OMB Server has to implement. + */ + + public static $OMB_SERVICES = + array(OMB_ENDPOINT_UPDATEPROFILE, OMB_ENDPOINT_POSTNOTICE); + public static $OAUTH_SERVICES = + array(OAUTH_ENDPOINT_REQUEST, OAUTH_ENDPOINT_AUTHORIZE, OAUTH_ENDPOINT_ACCESS); + + /** + * Validate URL + * + * Basic URL validation. Currently http, https, ftp and gopher are supported + * schemes. + * + * @param string $url The URL which is to be validated. + * + * @return bool Whether URL is valid. + * + * @access public + */ + public static function validateURL($url) { + return Validate::uri($url, array('allowed_schemes' => array('http', 'https', + 'gopher', 'ftp'))); + } + + /** + * Validate Media type + * + * Basic Media type validation. Checks for valid maintype and correct format. + * + * @param string $mediatype The Media type which is to be validated. + * + * @return bool Whether media type is valid. + * + * @access public + */ + public static function validateMediaType($mediatype) { + if (0 === preg_match('/^(\w+)\/([\w\d-+.]+)$/', $mediatype, $subtypes)) { + return false; + } + if (!in_array(strtolower($subtypes[1]), array('application', 'audio', 'image', + 'message', 'model', 'multipart', 'text', 'video'))) { + return false; + } + return true; + } + + /** + * Remove escaping from request parameters + * + * Neutralise the evil effects of magic_quotes_gpc in the current request. + * This is used before handing a request off to OAuthRequest::from_request. + * Many thanks to Ciaran Gultnieks for this fix. + * + * @access public + */ + public static function removeMagicQuotesFromRequest() { + if(get_magic_quotes_gpc() == 1) { + $_POST = array_map('stripslashes', $_POST); + $_GET = array_map('stripslashes', $_GET); + } + } +} +?> diff --git a/extlib/libomb/invalidparameterexception.php b/extlib/libomb/invalidparameterexception.php new file mode 100755 index 0000000000..163e1dd4cc --- /dev/null +++ b/extlib/libomb/invalidparameterexception.php @@ -0,0 +1,32 @@ +. + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ +class OMB_InvalidParameterException extends Exception { + public function __construct($value, $type, $parameter) { + parent::__construct("Invalid value $value for parameter $parameter in $type"); + } +} +?> diff --git a/extlib/libomb/invalidyadisexception.php b/extlib/libomb/invalidyadisexception.php new file mode 100755 index 0000000000..797b7b95bd --- /dev/null +++ b/extlib/libomb/invalidyadisexception.php @@ -0,0 +1,31 @@ +. + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ +class OMB_InvalidYadisException extends Exception { + +} +?> diff --git a/extlib/libomb/notice.php b/extlib/libomb/notice.php new file mode 100755 index 0000000000..9ac36640a2 --- /dev/null +++ b/extlib/libomb/notice.php @@ -0,0 +1,272 @@ +. + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ + +class OMB_Notice { + protected $author; + protected $uri; + protected $content; + protected $url; + protected $license_url; /* url is an own addition for clarification. */ + protected $seealso_url; /* url is an own addition for clarification. */ + protected $seealso_disposition; + protected $seealso_mediatype; + protected $seealso_license_url; /* url is an addition for clarification. */ + + /* The notice as OMB param array. Cached and rebuild on usage. + false while outdated. */ + protected $param_array; + + /** + * Constructor for OMB_Notice + * + * Initializes the OMB_Notice object with author, uri and content. + * These parameters are mandatory for postNotice. + * + * @param object $author An OMB_Profile object representing the author of the + * notice. + * @param string $uri The notice URI as defined by the OMB. A unique and + * unchanging identifier for a notice. + * @param string $content The content of the notice. 140 chars recommended, + * but there is no limit. + * + * @access public + */ + public function __construct($author, $uri, $content) { + $this->content = $content; + if (is_null($author)) { + throw new OMB_InvalidParameterException('', 'notice', 'omb_listenee'); + } + $this->author = $author; + + if (!Validate::uri($uri)) { + throw new OMB_InvalidParameterException($uri, 'notice', 'omb_notice'); + } + $this->uri = $uri; + + $this->param_array = false; + } + + /** + * Returns the notice as array + * + * The method returns an array which contains the whole notice as array. The + * array is cached and only rebuilt on changes of the notice. + * Empty optional values are not passed. + * + * @access public + * @returns array The notice as parameter array + */ + public function asParameters() { + if ($this->param_array !== false) { + return $this->param_array; + } + + $this->param_array = array( + 'omb_notice' => $this->uri, + 'omb_notice_content' => $this->content); + + if (!is_null($this->url)) + $this->param_array['omb_notice_url'] = $this->url; + + if (!is_null($this->license_url)) + $this->param_array['omb_notice_license'] = $this->license_url; + + if (!is_null($this->seealso_url)) { + $this->param_array['omb_seealso'] = $this->seealso_url; + + /* This is actually a free interpretation of the OMB standard. We assume + that additional seealso parameters are not of any use if seealso itself + is not set. */ + if (!is_null($this->seealso_disposition)) + $this->param_array['omb_seealso_disposition'] = + $this->seealso_disposition; + + if (!is_null($this->seealso_mediatype)) + $this->param_array['omb_seealso_mediatype'] = $this->seealso_mediatype; + + if (!is_null($this->seealso_license_url)) + $this->param_array['omb_seealso_license'] = $this->seealso_license_url; + } + return $this->param_array; + } + + /** + * Builds an OMB_Notice object from array + * + * The method builds an OMB_Notice object from the passed parameters array. + * The array MUST provide a notice URI and content. The array fields HAVE TO + * be named according to the OMB standard, i. e. omb_notice_* and + * omb_seealso_*. Values are handled as not passed if the corresponding array + * fields are not set or the empty string. + * + * @param object $author An OMB_Profile object representing the author of + * the notice. + * @param string $parameters An array containing the notice parameters. + * + * @access public + * + * @returns OMB_Notice The built OMB_Notice. + */ + public static function fromParameters($author, $parameters) { + $notice = new OMB_Notice($author, $parameters['omb_notice'], + $parameters['omb_notice_content']); + + if (isset($parameters['omb_notice_url'])) { + $notice->setURL($parameters['omb_notice_url']); + } + + if (isset($parameters['omb_notice_license'])) { + $notice->setLicenseURL($parameters['omb_notice_license']); + } + + if (isset($parameters['omb_seealso'])) { + $notice->setSeealsoURL($parameters['omb_seealso']); + } + + if (isset($parameters['omb_seealso_disposition'])) { + $notice->setSeealsoDisposition($parameters['omb_seealso_disposition']); + } + + if (isset($parameters['omb_seealso_mediatype'])) { + $notice->setSeealsoMediatype($parameters['omb_seealso_mediatype']); + } + + if (isset($parameters['omb_seealso_license'])) { + $notice->setSeealsoLicenseURL($parameters['omb_seealso_license']); + } + return $notice; + } + + public function getAuthor() { + return $this->author; + } + + public function getIdentifierURI() { + return $this->uri; + } + + public function getContent() { + return $this->content; + } + + public function getURL() { + return $this->url; + } + + public function getLicenseURL() { + return $this->license_url; + } + + public function getSeealsoURL() { + return $this->seealso_url; + } + + public function getSeealsoDisposition() { + return $this->seealso_disposition; + } + + public function getSeealsoMediatype() { + return $this->seealso_mediatype; + } + + public function getSeealsoLicenseURL() { + return $this->seealso_license_url; + } + + public function setURL($url) { + if ($url === '') { + $url = null; + } elseif (!OMB_Helper::validateURL($url)) { + throw new OMB_InvalidParameterException($url, 'notice', 'omb_notice_url'); + } + $this->url = $url; + $this->param_array = false; + } + + public function setLicenseURL($license_url) { + if ($license_url === '') { + $license_url = null; + } elseif (!OMB_Helper::validateURL($license_url)) { + throw new OMB_InvalidParameterException($license_url, 'notice', + 'omb_notice_license'); + } + $this->license_url = $license_url; + $this->param_array = false; + } + + public function setSeealsoURL($seealso_url) { + if ($seealso_url === '') { + $seealso_url = null; + } elseif (!OMB_Helper::validateURL($seealso_url)) { + throw new OMB_InvalidParameterException($seealso_url, 'notice', + 'omb_seealso'); + } + $this->seealso_url = $seealso_url; + $this->param_array = false; + } + + public function setSeealsoDisposition($seealso_disposition) { + if ($seealso_disposition === '') { + $seealso_disposition = null; + } elseif ($seealso_disposition !== 'link' && $seealso_disposition !== 'inline') { + throw new OMB_InvalidParameterException($seealso_disposition, 'notice', + 'omb_seealso_disposition'); + } + $this->seealso_disposition = $seealso_disposition; + $this->param_array = false; + } + + public function setSeealsoMediatype($seealso_mediatype) { + if ($seealso_mediatype === '') { + $seealso_mediatype = null; + } elseif (!OMB_Helper::validateMediaType($seealso_mediatype)) { + throw new OMB_InvalidParameterException($seealso_mediatype, 'notice', + 'omb_seealso_mediatype'); + } + $this->seealso_mediatype = $seealso_mediatype; + $this->param_array = false; + } + + public function setSeealsoLicenseURL($seealso_license_url) { + if ($seealso_license_url === '') { + $seealso_license_url = null; + } elseif (!OMB_Helper::validateURL($seealso_license_url)) { + throw new OMB_InvalidParameterException($seealso_license_url, 'notice', + 'omb_seealso_license'); + } + $this->seealso_license_url = $seealso_license_url; + $this->param_array = false; + } +} +?> diff --git a/extlib/libomb/omb_yadis_xrds.php b/extlib/libomb/omb_yadis_xrds.php new file mode 100755 index 0000000000..89921203b0 --- /dev/null +++ b/extlib/libomb/omb_yadis_xrds.php @@ -0,0 +1,196 @@ +. + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ + +class OMB_Yadis_XRDS extends Auth_Yadis_XRDS { + + protected $fetcher; + + /** + * Create an instance from URL + * + * Constructs an OMB_Yadis_XRDS object from a given URL. A full Yadis + * discovery is performed on the URL and the XRDS is parsed. + * Throws an OMB_InvalidYadisException when no Yadis is discovered or the + * detected XRDS file is broken. + * + * @param string $url The URL on which Yadis discovery + * should be performed on + * @param Auth_Yadis_HTTPFetcher $fetcher A fetcher used to get HTTP + * resources + * + * @access public + * + * @return OMB_Yadis_XRDS The initialized object representing the given + * resource + **/ + public static function fromYadisURL($url, $fetcher) { + /* Perform a Yadis discovery. */ + $yadis = Auth_Yadis_Yadis::discover($url, $fetcher); + if ($yadis->failed) { + throw new OMB_InvalidYadisException($url); + } + + /* Parse the XRDS file. */ + $xrds = OMB_Yadis_XRDS::parseXRDS($yadis->response_text); + if ($xrds === null) { + throw new OMB_InvalidYadisException($url); + } + $xrds->fetcher = $fetcher; + return $xrds; + } + + /** + * Get a specific service + * + * Returns the Auth_Yadis_Service object corresponding to the given service + * URI. + * Throws an OMB_UnsupportedServiceException if the service is not available. + * + * @param string $service URI specifier of the requested service + * + * @access public + * + * @return Auth_Yadis_Service The object representing the requested service + **/ + public function getService($service) { + $match = $this->services(array( create_function('$s', + "return in_array('$service', \$s->getTypes());"))); + if ($match === array()) { + throw new OMB_UnsupportedServiceException($service); + } + return $match[0]; + } + + /** + * Get a specific XRD + * + * Returns the OMB_Yadis_XRDS object corresponding to the given URI. + * Throws an OMB_UnsupportedServiceException if the XRD is not available. + * Note that getXRD tries to resolve external XRD parts as well. + * + * @param string $uri URI specifier of the requested XRD + * + * @access public + * + * @return OMB_Yadis_XRDS The object representing the requested XRD + **/ + public function getXRD($uri) { + $nexthash = strpos($uri, '#'); + if ($nexthash !== 0) { + if ($nexthash !== false) { + $cururi = substr($uri, 0, $nexthash); + $nexturi = substr($uri, $nexthash); + } + return + OMB_Yadis_XRDS::fromYadisURL($cururi, $this->fetcher)->getXRD($nexturi); + } + + $id = substr($uri, 1); + foreach ($this->allXrdNodes as $node) { + $attrs = $this->parser->attributes($node); + if (array_key_exists('xml:id', $attrs) && $attrs['xml:id'] == $id) { + /* Trick the constructor into thinking this is the only node. */ + $bogus_nodes = array($node); + return new OMB_Yadis_XRDS($this->parser, $bogus_nodes); + } + } + throw new OMB_UnsupportedServiceException($uri); + } + + /** + * Parse an XML string containing a XRDS document + * + * Parse an XML string (XRDS document) and return either a + * Auth_Yadis_XRDS object or null, depending on whether the + * XRDS XML is valid. + * Copy and paste from parent to select correct constructor. + * + * @param string $xml_string An XRDS XML string. + * + * @access public + * + * @return mixed An instance of OMB_Yadis_XRDS or null, + * depending on the validity of $xml_string + **/ + + public function &parseXRDS($xml_string, $extra_ns_map = null) { + $_null = null; + + if (!$xml_string) { + return $_null; + } + + $parser = Auth_Yadis_getXMLParser(); + + $ns_map = Auth_Yadis_getNSMap(); + + if ($extra_ns_map && is_array($extra_ns_map)) { + $ns_map = array_merge($ns_map, $extra_ns_map); + } + + if (!($parser && $parser->init($xml_string, $ns_map))) { + return $_null; + } + + // Try to get root element. + $root = $parser->evalXPath('/xrds:XRDS[1]'); + if (!$root) { + return $_null; + } + + if (is_array($root)) { + $root = $root[0]; + } + + $attrs = $parser->attributes($root); + + if (array_key_exists('xmlns:xrd', $attrs) && + $attrs['xmlns:xrd'] != Auth_Yadis_XMLNS_XRDS) { + return $_null; + } else if (array_key_exists('xmlns', $attrs) && + preg_match('/xri/', $attrs['xmlns']) && + $attrs['xmlns'] != Auth_Yadis_XMLNS_XRD_2_0) { + return $_null; + } + + // Get the last XRD node. + $xrd_nodes = $parser->evalXPath('/xrds:XRDS[1]/xrd:XRD'); + + if (!$xrd_nodes) { + return $_null; + } + + $xrds = new OMB_Yadis_XRDS($parser, $xrd_nodes); + return $xrds; + } +} diff --git a/extlib/libomb/plain_xrds_writer.php b/extlib/libomb/plain_xrds_writer.php new file mode 100755 index 0000000000..b4a6e990bc --- /dev/null +++ b/extlib/libomb/plain_xrds_writer.php @@ -0,0 +1,124 @@ +writeXRDS. + * + * 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 . + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ + +class OMB_Plain_XRDS_Writer implements OMB_XRDS_Writer { + public function writeXRDS($user, $mapper) { + header('Content-Type: application/xrds+xml'); + $xw = new XMLWriter(); + $xw->openURI('php://output'); + $xw->setIndent(true); + + $xw->startDocument('1.0', 'UTF-8'); + $this->writeFullElement($xw, 'XRDS', array('xmlns' => 'xri://$xrds'), array( + array('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', + 'xml:id' => 'oauth', + 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', + 'version' => '2.0'), array( + array('Type', null, 'xri://$xrds*simple'), + array('Service', null, array( + array('Type', null, OAUTH_ENDPOINT_REQUEST), + array('URI', null, $mapper->getURL(OAUTH_ENDPOINT_REQUEST)), + array('Type', null, OAUTH_AUTH_HEADER), + array('Type', null, OAUTH_POST_BODY), + array('Type', null, OAUTH_HMAC_SHA1), + array('LocalID', null, $user->getIdentifierURI()) + )), + array('Service', null, array( + array('Type', null, OAUTH_ENDPOINT_AUTHORIZE), + array('URI', null, $mapper->getURL(OAUTH_ENDPOINT_AUTHORIZE)), + array('Type', null, OAUTH_AUTH_HEADER), + array('Type', null, OAUTH_POST_BODY), + array('Type', null, OAUTH_HMAC_SHA1) + )), + array('Service', null, array( + array('Type', null, OAUTH_ENDPOINT_ACCESS), + array('URI', null, $mapper->getURL(OAUTH_ENDPOINT_ACCESS)), + array('Type', null, OAUTH_AUTH_HEADER), + array('Type', null, OAUTH_POST_BODY), + array('Type', null, OAUTH_HMAC_SHA1) + )), + array('Service', null, array( + array('Type', null, OAUTH_ENDPOINT_RESOURCE), + array('Type', null, OAUTH_AUTH_HEADER), + array('Type', null, OAUTH_POST_BODY), + array('Type', null, OAUTH_HMAC_SHA1) + )) + )), + array('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', + 'xml:id' => 'omb', + 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', + 'version' => '2.0'), array( + array('Type', null, 'xri://$xrds*simple'), + array('Service', null, array( + array('Type', null, OMB_ENDPOINT_POSTNOTICE), + array('URI', null, $mapper->getURL(OMB_ENDPOINT_POSTNOTICE)) + )), + array('Service', null, array( + array('Type', null, OMB_ENDPOINT_UPDATEPROFILE), + array('URI', null, $mapper->getURL(OMB_ENDPOINT_UPDATEPROFILE)) + )) + )), + array('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', + 'version' => '2.0'), array( + array('Type', null, 'xri://$xrds*simple'), + array('Service', null, array( + array('Type', null, OAUTH_DISCOVERY), + array('URI', null, '#oauth') + )), + array('Service', null, array( + array('Type', null, OMB_VERSION), + array('URI', null, '#omb') + )) + )) + )); + $xw->endDocument(); + $xw->flush(); + } + + public static function writeFullElement($xw, $tag, $attributes, $content) { + $xw->startElement($tag); + if (!is_null($attributes)) { + foreach ($attributes as $name => $value) { + $xw->writeAttribute($name, $value); + } + } + if (is_array($content)) { + foreach ($content as $values) { + OMB_Plain_XRDS_Writer::writeFullElement($xw, $values[0], $values[1], $values[2]); + } + } else { + $xw->text($content); + } + $xw->fullEndElement(); + } +} +?> diff --git a/extlib/libomb/profile.php b/extlib/libomb/profile.php new file mode 100755 index 0000000000..13314d3e80 --- /dev/null +++ b/extlib/libomb/profile.php @@ -0,0 +1,317 @@ +. + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ + +class OMB_Profile { + protected $identifier_uri; + protected $profile_url; + protected $nickname; + protected $license_url; + protected $fullname; + protected $homepage; + protected $bio; + protected $location; + protected $avatar_url; + + /* The profile as OMB param array. Cached and rebuild on usage. + false while outdated. */ + protected $param_array; + + /** + * Constructor for OMB_Profile + * + * Initializes the OMB_Profile object with an identifier uri. + * + * @param string $identifier_uri The profile URI as defined by the OMB. A unique + * and unchanging identifier for a profile. + * + * @access public + */ + public function __construct($identifier_uri) { + if (!Validate::uri($identifier_uri)) { + throw new OMB_InvalidParameterException($identifier_uri, 'profile', + 'omb_listenee or omb_listener'); + } + $this->identifier_uri = $identifier_uri; + $this->param_array = false; + } + + /** + * Returns the profile as array + * + * The method returns an array which contains the whole profile as array. The + * array is cached and only rebuilt on changes of the profile. + * + * @param bool $force_all Specifies whether empty fields should be added to + * the array as well. This is neccessary to clear + * fields via updateProfile. + * + * @param string $prefix The common prefix to the key for all parameters. + * + * @access public + * + * @return array The profile as parameter array + */ + public function asParameters($prefix, $force_all = false) { + if ($this->param_array === false) { + $this->param_array = array('' => $this->identifier_uri); + + if ($force_all || !is_null($this->profile_url)) { + $this->param_array['_profile'] = $this->profile_url; + } + + if ($force_all || !is_null($this->homepage)) { + $this->param_array['_homepage'] = $this->homepage; + } + + if ($force_all || !is_null($this->nickname)) { + $this->param_array['_nickname'] = $this->nickname; + } + + if ($force_all || !is_null($this->license_url)) { + $this->param_array['_license'] = $this->license_url; + } + + if ($force_all || !is_null($this->fullname)) { + $this->param_array['_fullname'] = $this->fullname; + } + + if ($force_all || !is_null($this->bio)) { + $this->param_array['_bio'] = $this->bio; + } + + if ($force_all || !is_null($this->location)) { + $this->param_array['_location'] = $this->location; + } + + if ($force_all || !is_null($this->avatar_url)) { + $this->param_array['_avatar'] = $this->avatar_url; + } + + } + $ret = array(); + foreach ($this->param_array as $k => $v) { + $ret[$prefix . $k] = $v; + } + return $ret; + } + + /** + * Builds an OMB_Profile object from array + * + * The method builds an OMB_Profile object from the passed parameters array. The + * array MUST provide a profile URI. The array fields HAVE TO be named according + * to the OMB standard. The prefix (omb_listener or omb_listenee) is passed as a + * parameter. + * + * @param string $parameters An array containing the profile parameters. + * @param string $prefix The common prefix of the profile parameter keys. + * + * @access public + * + * @returns OMB_Profile The built OMB_Profile. + */ + public static function fromParameters($parameters, $prefix) { + if (!isset($parameters[$prefix])) { + throw new OMB_InvalidParameterException('', 'profile', $prefix); + } + + $profile = new OMB_Profile($parameters[$prefix]); + $profile->updateFromParameters($parameters, $prefix); + return $profile; + } + + /** + * Update from array + * + * Updates from the passed parameters array. The array does not have to + * provide a profile URI. The array fields HAVE TO be named according to the + * OMB standard. The prefix (omb_listener or omb_listenee) is passed as a + * parameter. + * + * @param string $parameters An array containing the profile parameters. + * @param string $prefix The common prefix of the profile parameter keys. + * + * @access public + */ + public function updateFromParameters($parameters, $prefix) { + if (isset($parameters[$prefix.'_profile'])) { + $this->setProfileURL($parameters[$prefix.'_profile']); + } + + if (isset($parameters[$prefix.'_license'])) { + $this->setLicenseURL($parameters[$prefix.'_license']); + } + + if (isset($parameters[$prefix.'_nickname'])) { + $this->setNickname($parameters[$prefix.'_nickname']); + } + + if (isset($parameters[$prefix.'_fullname'])) { + $this->setFullname($parameters[$prefix.'_fullname']); + } + + if (isset($parameters[$prefix.'_homepage'])) { + $this->setHomepage($parameters[$prefix.'_homepage']); + } + + if (isset($parameters[$prefix.'_bio'])) { + $this->setBio($parameters[$prefix.'_bio']); + } + + if (isset($parameters[$prefix.'_location'])) { + $this->setLocation($parameters[$prefix.'_location']); + } + + if (isset($parameters[$prefix.'_avatar'])) { + $this->setAvatarURL($parameters[$prefix.'_avatar']); + } + } + + public function getIdentifierURI() { + return $this->identifier_uri; + } + + public function getProfileURL() { + return $this->profile_url; + } + + public function getHomepage() { + return $this->homepage; + } + + public function getNickname() { + return $this->nickname; + } + + public function getLicenseURL() { + return $this->license_url; + } + + public function getFullname() { + return $this->fullname; + } + + public function getBio() { + return $this->bio; + } + + public function getLocation() { + return $this->location; + } + + public function getAvatarURL() { + return $this->avatar_url; + } + + public function setProfileURL($profile_url) { + if (!OMB_Helper::validateURL($profile_url)) { + throw new OMB_InvalidParameterException($profile_url, 'profile', + 'omb_listenee_profile or omb_listener_profile'); + } + $this->profile_url = $profile_url; + $this->param_array = false; + } + + public function setNickname($nickname) { + if (!Validate::string($nickname, + array('min_length' => 1, + 'max_length' => 64, + 'format' => VALIDATE_NUM . VALIDATE_ALPHA))) { + throw new OMB_InvalidParameterException($nickname, 'profile', 'nickname'); + } + + $this->nickname = $nickname; + $this->param_array = false; + } + + public function setLicenseURL($license_url) { + if (!OMB_Helper::validateURL($license_url)) { + throw new OMB_InvalidParameterException($license_url, 'profile', + 'omb_listenee_license or omb_listener_license'); + } + $this->license_url = $license_url; + $this->param_array = false; + } + + public function setFullname($fullname) { + if ($fullname === '') { + $fullname = null; + } elseif (!Validate::string($fullname, array('max_length' => 255))) { + throw new OMB_InvalidParameterException($fullname, 'profile', 'fullname'); + } + $this->fullname = $fullname; + $this->param_array = false; + } + + public function setHomepage($homepage) { + if ($homepage === '') { + $homepage = null; + } + $this->homepage = $homepage; + $this->param_array = false; + } + + public function setBio($bio) { + if ($bio === '') { + $bio = null; + } elseif (!Validate::string($bio, array('max_length' => 140))) { + throw new OMB_InvalidParameterException($bio, 'profile', 'fullname'); + } + $this->bio = $bio; + $this->param_array = false; + } + + public function setLocation($location) { + if ($location === '') { + $location = null; + } elseif (!Validate::string($location, array('max_length' => 255))) { + throw new OMB_InvalidParameterException($location, 'profile', 'fullname'); + } + $this->location = $location; + $this->param_array = false; + } + + public function setAvatarURL($avatar_url) { + if ($avatar_url === '') { + $avatar_url = null; + } elseif (!OMB_Helper::validateURL($avatar_url)) { + throw new OMB_InvalidParameterException($avatar_url, 'profile', + 'omb_listenee_avatar or omb_listener_avatar'); + } + $this->avatar_url = $avatar_url; + $this->param_array = false; + } + +} +?> diff --git a/extlib/libomb/remoteserviceexception.php b/extlib/libomb/remoteserviceexception.php new file mode 100755 index 0000000000..374d15973b --- /dev/null +++ b/extlib/libomb/remoteserviceexception.php @@ -0,0 +1,42 @@ +. + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ +class OMB_RemoteServiceException extends Exception { + public static function fromYadis($request_uri, $result) { + if ($result->status == 200) { + $err = 'Got wrong response ' . $result->body; + } else { + $err = 'Got error code ' . $result->status . ' with response ' . $result->body; + } + return new OMB_RemoteServiceException($request_uri . ': ' . $err); + } + + public static function forRequest($action_uri, $failure) { + return new OMB_RemoteServiceException("Handler for $action_uri: " . $failure); + } +} +?> diff --git a/extlib/libomb/service_consumer.php b/extlib/libomb/service_consumer.php new file mode 100755 index 0000000000..273fd052ec --- /dev/null +++ b/extlib/libomb/service_consumer.php @@ -0,0 +1,430 @@ +. + * + * @package OMB + * @author Adrian Lang + * @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); + } + } +} diff --git a/extlib/libomb/service_provider.php b/extlib/libomb/service_provider.php new file mode 100755 index 0000000000..b3ad537531 --- /dev/null +++ b/extlib/libomb/service_provider.php @@ -0,0 +1,411 @@ +. + * + * @package OMB + * @author Adrian Lang + * @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. + * + * @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']; + } + $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 $_GET or $_POST. + * + * @access public + **/ + public function writeAccessToken() { + OMB_Helper::removeMagicQuotesFromRequest(); + echo $this->getOAuthServer()->fetch_access_token(OAuthRequest::from_request()); + } + + /** + * 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. + * + * @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(); + $listenee = $req->get_parameter('omb_listenee'); + + try { + list($consumer, $token) = $this->getOAuthServer()->verify_request($req); + } catch (OAuthException $e) { + header('HTTP/1.1 403 Forbidden'); + 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; + } +} diff --git a/extlib/libomb/unsupportedserviceexception.php b/extlib/libomb/unsupportedserviceexception.php new file mode 100755 index 0000000000..4dab63ebee --- /dev/null +++ b/extlib/libomb/unsupportedserviceexception.php @@ -0,0 +1,31 @@ +. + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ +class OMB_UnsupportedServiceException extends Exception { + +} +?> diff --git a/extlib/libomb/xrds_mapper.php b/extlib/libomb/xrds_mapper.php new file mode 100755 index 0000000000..7552154e55 --- /dev/null +++ b/extlib/libomb/xrds_mapper.php @@ -0,0 +1,33 @@ +writeXRDS. + * + * 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 . + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ + +interface OMB_XRDS_Mapper { + public function getURL($action); +} +?> diff --git a/extlib/libomb/xrds_writer.php b/extlib/libomb/xrds_writer.php new file mode 100755 index 0000000000..31b451b9ce --- /dev/null +++ b/extlib/libomb/xrds_writer.php @@ -0,0 +1,33 @@ +writeXRDS. + * + * 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 . + * + * @package OMB + * @author Adrian Lang + * @copyright 2009 Adrian Lang + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0 + **/ + +interface OMB_XRDS_Writer { + public function writeXRDS($user, $mapper); +} +?> diff --git a/lib/oauthstore.php b/lib/oauthstore.php index f224c6c221..87d8cf2137 100644 --- a/lib/oauthstore.php +++ b/lib/oauthstore.php @@ -17,15 +17,16 @@ * along with this program. If not, see . */ -if (!defined('LACONICA')) { exit(1); } +if (!defined('LACONICA')) { + exit(1); +} -require_once(INSTALLDIR.'/lib/omb.php'); +require_once 'libomb/datastore.php'; -class LaconicaOAuthDataStore extends OAuthDataStore +class LaconicaDataStore extends OMB_Datastore { // We keep a record of who's contacted us - function lookup_consumer($consumer_key) { $con = Consumer::staticGet('consumer_key', $consumer_key); @@ -44,7 +45,9 @@ class LaconicaOAuthDataStore extends OAuthDataStore function lookup_token($consumer, $token_type, $token_key) { $t = new Token(); - $t->consumer_key = $consumer->key; + if (!is_null($consumer)) { + $t->consumer_key = $consumer->key; + } $t->tok = $token_key; $t->type = ($token_type == 'access') ? 1 : 0; if ($t->find(true)) { @@ -154,4 +157,348 @@ class LaconicaOAuthDataStore extends OAuthDataStore { return $this->new_access_token($consumer); } + + + /** + * Revoke specified OAuth token + * + * Revokes the authorization token specified by $token_key. + * Throws exceptions in case of error. + * + * @param string $token_key The token to be revoked + * + * @access public + **/ + public function revoke_token($token_key) { + $rt = new Token(); + $rt->tok = $token_key; + $rt->type = 0; + $rt->state = 0; + if (!$rt->find(true)) { + throw new Exception('Tried to revoke unknown token'); + } + if (!$rt->delete()) { + throw new Exception('Failed to delete revoked token'); + } + } + + /** + * Authorize specified OAuth token + * + * Authorizes the authorization token specified by $token_key. + * Throws exceptions in case of error. + * + * @param string $token_key The token to be authorized + * + * @access public + **/ + public function authorize_token($token_key) { + $rt = new Token(); + $rt->tok = $token_key; + $rt->type = 0; + $rt->state = 0; + if (!$rt->find(true)) { + throw new Exception('Tried to authorize unknown token'); + } + $orig_rt = clone($rt); + $rt->state = 1; # Authorized but not used + if (!$rt->update($orig_rt)) { + throw new Exception('Failed to authorize token'); + } + } + + /** + * Get profile by identifying URI + * + * Returns an OMB_Profile object representing the OMB profile identified by + * $identifier_uri. + * Returns null if there is no such OMB profile. + * Throws exceptions in case of other error. + * + * @param string $identifier_uri The OMB identifier URI specifying the + * requested profile + * + * @access public + * + * @return OMB_Profile The corresponding profile + **/ + public function getProfile($identifier_uri) { + /* getProfile is only used for remote profiles by libomb. + TODO: Make it work with local ones anyway. */ + $remote = Remote_profile::staticGet('uri', $identifier_uri); + if (!$remote) throw new Exception('No such remote profile'); + $profile = Profile::staticGet('id', $remote->id); + if (!$profile) throw new Exception('No profile for remote user'); + + require_once INSTALLDIR.'/lib/omb.php'; + return profile_to_omb_profile($identifier_uri, $profile); + } + + /** + * Save passed profile + * + * Stores the OMB profile $profile. Overwrites an existing entry. + * Throws exceptions in case of error. + * + * @param OMB_Profile $profile The OMB profile which should be saved + * + * @access public + **/ + public function saveProfile($omb_profile) { + if (common_profile_url($omb_profile->getNickname()) == + $omb_profile->getProfileURL()) { + throw new Exception('Not implemented'); + } else { + $remote = Remote_profile::staticGet('uri', $omb_profile->getIdentifierURI()); + + if ($remote) { + $exists = true; + $profile = Profile::staticGet($remote->id); + $orig_remote = clone($remote); + $orig_profile = clone($profile); + # XXX: compare current postNotice and updateProfile URLs to the ones + # stored in the DB to avoid (possibly...) above attack + } else { + $exists = false; + $remote = new Remote_profile(); + $remote->uri = $omb_profile->getIdentifierURI(); + $profile = new Profile(); + } + + $profile->nickname = $omb_profile->getNickname(); + $profile->profileurl = $omb_profile->getProfileURL(); + + $fullname = $omb_profile->getFullname(); + $profile->fullname = is_null($fullname) ? '' : $fullname; + $homepage = $omb_profile->getHomepage(); + $profile->homepage = is_null($homepage) ? '' : $homepage; + $bio = $omb_profile->getBio(); + $profile->bio = is_null($bio) ? '' : $bio; + $location = $omb_profile->getLocation(); + $profile->location = is_null($location) ? '' : $location; + + if ($exists) { + $profile->update($orig_profile); + } else { + $profile->created = DB_DataObject_Cast::dateTime(); # current time + $id = $profile->insert(); + if (!$id) { + throw new Exception(_('Error inserting new profile')); + } + $remote->id = $id; + } + + $avatar_url = $omb_profile->getAvatarURL(); + if ($avatar_url) { + if (!$this->add_avatar($profile, $avatar_url)) { + throw new Exception(_('Error inserting avatar')); + } + } else { + $avatar = $profile->getOriginalAvatar(); + if($avatar) $avatar->delete(); + $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + if($avatar) $avatar->delete(); + $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE); + if($avatar) $avatar->delete(); + $avatar = $profile->getAvatar(AVATAR_MINI_SIZE); + if($avatar) $avatar->delete(); + } + + if ($exists) { + if (!$remote->update($orig_remote)) { + throw new Exception(_('Error updating remote profile')); + } + } else { + $remote->created = DB_DataObject_Cast::dateTime(); # current time + if (!$remote->insert()) { + throw new Exception(_('Error inserting remote profile')); + } + } + } + } + + function add_avatar($profile, $url) + { + $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); + copy($url, $temp_filename); + $imagefile = new ImageFile($profile->id, $temp_filename); + $filename = Avatar::filename($profile->id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + return $profile->setOriginal($filename); + } + + /** + * Save passed notice + * + * Stores the OMB notice $notice. The datastore may change the passed notice. + * This might by neccessary for URIs depending on a database key. Note that + * it is the user’s duty to present a mechanism for his OMB_Datastore to + * appropriately change his OMB_Notice. + * Throws exceptions in case of error. + * + * @param OMB_Notice $notice The OMB notice which should be saved + * + * @access public + **/ + public function saveNotice(&$omb_notice) { + if (Notice::staticGet('uri', $omb_notice->getIdentifierURI())) { + throw new Exception(_('Duplicate notice')); + } + $author_uri = $omb_notice->getAuthor()->getIdentifierURI(); + common_log(LOG_DEBUG, $author_uri, __FILE__); + $author = Remote_profile::staticGet('uri', $author_uri); + if (!$author) { + $author = User::staticGet('uri', $author_uri); + } + if (!$author) { + throw new Exception('No such user'); + } + + common_log(LOG_DEBUG, print_r($author, true), __FILE__); + + $notice = Notice::saveNew($author->id, + $omb_notice->getContent(), + 'omb', + false, + null, + $omb_notice->getIdentifierURI()); + if (is_string($notice)) { + throw new Exception($notice); + } + common_broadcast_notice($notice, true); + } + + /** + * Get subscriptions of a given profile + * + * Returns an array containing subscription informations for the specified + * profile. Every array entry should in turn be an array with keys + * 'uri´: The identifier URI of the subscriber + * 'token´: The subscribe token + * 'secret´: The secret token + * Throws exceptions in case of error. + * + * @param string $subscribed_user_uri The OMB identifier URI specifying the + * subscribed profile + * + * @access public + * + * @return mixed An array containing the subscriptions or 0 if no + * subscription has been found. + **/ + public function getSubscriptions($subscribed_user_uri) { + $sub = new Subscription(); + + $user = $this->_getAnyProfile($subscribed_user_uri); + + $sub->subscribed = $user->id; + + if (!$sub->find(true)) { + return 0; + } + + /* Since we do not use OMB_Service_Provider’s action methods, there + is no need to actually return the subscriptions. */ + return 1; + } + + private function _getAnyProfile($uri) + { + $user = Remote_profile::staticGet('uri', $uri); + if (!$user) { + $user = User::staticGet('uri', $uri); + } + if (!$user) { + throw new Exception('No such user'); + } + return $user; + } + + /** + * Delete a subscription + * + * Deletes the subscription from $subscriber_uri to $subscribed_user_uri. + * Throws exceptions in case of error. + * + * @param string $subscriber_uri The OMB identifier URI specifying the + * subscribing profile + * + * @param string $subscribed_user_uri The OMB identifier URI specifying the + * subscribed profile + * + * @access public + **/ + public function deleteSubscription($subscriber_uri, $subscribed_user_uri) + { + $sub = new Subscription(); + + $subscribed = $this->_getAnyProfile($subscribed_user_uri); + $subscriber = $this->_getAnyProfile($subscriber_uri); + + $sub->subscribed = $subscribed->id; + $sub->subscriber = $subscriber->id; + + $sub->delete(); + } + + /** + * Save a subscription + * + * Saves the subscription from $subscriber_uri to $subscribed_user_uri. + * Throws exceptions in case of error. + * + * @param string $subscriber_uri The OMB identifier URI specifying + * the subscribing profile + * + * @param string $subscribed_user_uri The OMB identifier URI specifying + * the subscribed profile + * @param OAuthToken $token The access token + * + * @access public + **/ + public function saveSubscription($subscriber_uri, $subscribed_user_uri, + $token) + { + $sub = new Subscription(); + + $subscribed = $this->_getAnyProfile($subscribed_user_uri); + $subscriber = $this->_getAnyProfile($subscriber_uri); + + $sub->subscribed = $subscribed->id; + $sub->subscriber = $subscriber->id; + + $sub_exists = $sub->find(true); + + if ($sub_exists) { + $orig_sub = clone($sub); + } else { + $sub->created = DB_DataObject_Cast::dateTime(); + } + + $sub->token = $token->key; + $sub->secret = $token->secret; + + if ($sub_exists) { + $result = $sub->update($orig_sub); + } else { + $result = $sub->insert(); + } + + if (!$result) { + common_log_db_error($sub, ($sub_exists) ? 'UPDATE' : 'INSERT', __FILE__); + throw new Exception(_('Couldn\'t insert new subscription.')); + return; + } + + /* Notify user, if necessary. */ + + if ($subscribed instanceof User) { + mail_subscribe_notify_profile($subscribed, + Profile::staticGet($subscriber->id)); + } + } } +?> diff --git a/lib/omb.php b/lib/omb.php index 4f6a960954..b9d0eef64e 100644 --- a/lib/omb.php +++ b/lib/omb.php @@ -17,36 +17,22 @@ * along with this program. If not, see . */ -if (!defined('LACONICA')) { exit(1); } +if (!defined('LACONICA')) { + exit(1); +} -require_once('OAuth.php'); -require_once(INSTALLDIR.'/lib/oauthstore.php'); - -require_once(INSTALLDIR.'/classes/Consumer.php'); -require_once(INSTALLDIR.'/classes/Nonce.php'); -require_once(INSTALLDIR.'/classes/Token.php'); - -require_once('Auth/Yadis/Yadis.php'); - -define('OAUTH_NAMESPACE', 'http://oauth.net/core/1.0/'); -define('OMB_NAMESPACE', 'http://openmicroblogging.org/protocol/0.1'); -define('OMB_VERSION_01', 'http://openmicroblogging.org/protocol/0.1'); -define('OAUTH_DISCOVERY', 'http://oauth.net/discovery/1.0'); - -define('OMB_ENDPOINT_UPDATEPROFILE', OMB_NAMESPACE.'/updateProfile'); -define('OMB_ENDPOINT_POSTNOTICE', OMB_NAMESPACE.'/postNotice'); -define('OAUTH_ENDPOINT_REQUEST', OAUTH_NAMESPACE.'endpoint/request'); -define('OAUTH_ENDPOINT_AUTHORIZE', OAUTH_NAMESPACE.'endpoint/authorize'); -define('OAUTH_ENDPOINT_ACCESS', OAUTH_NAMESPACE.'endpoint/access'); -define('OAUTH_ENDPOINT_RESOURCE', OAUTH_NAMESPACE.'endpoint/resource'); -define('OAUTH_AUTH_HEADER', OAUTH_NAMESPACE.'parameters/auth-header'); -define('OAUTH_POST_BODY', OAUTH_NAMESPACE.'parameters/post-body'); -define('OAUTH_HMAC_SHA1', OAUTH_NAMESPACE.'signature/HMAC-SHA1'); +require_once INSTALLDIR.'/lib/oauthstore.php'; +require_once 'OAuth.php'; +require_once 'libomb/constants.php'; +require_once 'libomb/service_consumer.php'; +require_once 'libomb/notice.php'; +require_once 'libomb/profile.php'; +require_once 'Auth/Yadis/Yadis.php'; function omb_oauth_consumer() { static $con = null; - if (!$con) { + if (is_null($con)) { $con = new OAuthConsumer(common_root_url(), ''); } return $con; @@ -55,7 +41,7 @@ function omb_oauth_consumer() function omb_oauth_server() { static $server = null; - if (!$server) { + if (is_null($server)) { $server = new OAuthServer(omb_oauth_datastore()); $server->add_signature_method(omb_hmac_sha1()); } @@ -65,8 +51,8 @@ function omb_oauth_server() function omb_oauth_datastore() { static $store = null; - if (!$store) { - $store = new LaconicaOAuthDataStore(); + if (is_null($store)) { + $store = new LaconicaDataStore(); } return $store; } @@ -74,57 +60,18 @@ function omb_oauth_datastore() function omb_hmac_sha1() { static $hmac_method = null; - if (!$hmac_method) { + if (is_null($hmac_method)) { $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); } return $hmac_method; } -function omb_get_services($xrd, $type) -{ - return $xrd->services(array(omb_service_filter($type))); -} - -function omb_service_filter($type) -{ - return create_function('$s', - 'return omb_match_service($s, \''.$type.'\');'); -} - -function omb_match_service($service, $type) -{ - return in_array($type, $service->getTypes()); -} - -function omb_service_uri($service) -{ - if (!$service) { - return null; - } - $uris = $service->getURIs(); - if (!$uris) { - return null; - } - return $uris[0]; -} - -function omb_local_id($service) -{ - if (!$service) { - return null; - } - $els = $service->getElements('xrd:LocalID'); - if (!$els) { - return null; - } - $el = $els[0]; - return $service->parser->content($el); -} - -function omb_broadcast_remote_subscribers($notice) +function omb_broadcast_notice($notice) { - # First, get remote users subscribed to this profile + $omb_notice = notice_to_omb_notice($notice); + + /* Get remote users subscribed to this profile. */ $rp = new Remote_profile(); $rp->query('SELECT postnoticeurl, token, secret ' . @@ -135,170 +82,148 @@ function omb_broadcast_remote_subscribers($notice) $posted = array(); while ($rp->fetch()) { - if (!$posted[$rp->postnoticeurl]) { - common_log(LOG_DEBUG, 'Posting to ' . $rp->postnoticeurl); - if (omb_post_notice_keys($notice, $rp->postnoticeurl, $rp->token, $rp->secret)) { - common_log(LOG_DEBUG, 'Finished to ' . $rp->postnoticeurl); - $posted[$rp->postnoticeurl] = true; - } else { - common_log(LOG_DEBUG, 'Failed posting to ' . $rp->postnoticeurl); - } + if (isset($posted[$rp->postnoticeurl])) { + /* We already posted to this url. */ + continue; } + common_debug('Posting to ' . $rp->postnoticeurl, __FILE__); + + /* Post notice. */ + $service = new Laconica_OMB_Service_Consumer( + array(OMB_ENDPOINT_POSTNOTICE => $rp->postnoticeurl)); + try { + $service->setToken($rp->token, $rp->secret); + $service->postNotice($omb_notice); + } catch (Exception $e) { + common_log(LOG_ERR, 'Failed posting to ' . $rp->postnoticeurl); + common_log(LOG_ERR, 'Error status '.$e); + continue; + } + $posted[$rp->postnoticeurl] = true; + + common_debug('Finished to ' . $rp->postnoticeurl, __FILE__); } - $rp->free(); - unset($rp); - - return true; + return; } -function omb_post_notice($notice, $remote_profile, $subscription) +function omb_broadcast_profile($profile) { - return omb_post_notice_keys($notice, $remote_profile->postnoticeurl, $subscription->token, $subscription->secret); -} - -function omb_post_notice_keys($notice, $postnoticeurl, $tk, $secret) -{ - $user = User::staticGet('id', $notice->profile_id); + $user = User::staticGet('id', $profile->id); if (!$user) { return false; } - $con = omb_oauth_consumer(); + $profile = $user->getProfile(); - $token = new OAuthToken($tk, $secret); + $omb_profile = profile_to_omb_profile($user->uri, $profile, true); - $url = $postnoticeurl; - $parsed = parse_url($url); - $params = array(); - parse_str($parsed['query'], $params); + /* Get remote users subscribed to this profile. */ + $rp = new Remote_profile(); - $req = OAuthRequest::from_consumer_and_token($con, $token, - 'POST', $url, $params); + $rp->query('SELECT updateprofileurl, token, secret ' . + 'FROM subscription JOIN remote_profile ' . + 'ON subscription.subscriber = remote_profile.id ' . + 'WHERE subscription.subscribed = ' . $profile->id . ' '); - $req->set_parameter('omb_version', OMB_VERSION_01); - $req->set_parameter('omb_listenee', $user->uri); - $req->set_parameter('omb_notice', $notice->uri); - $req->set_parameter('omb_notice_content', $notice->content); - $req->set_parameter('omb_notice_url', common_local_url('shownotice', - array('notice' => - $notice->id))); - $req->set_parameter('omb_notice_license', common_config('license', 'url')); + $posted = array(); - $user->free(); - unset($user); - - $req->sign_request(omb_hmac_sha1(), $con, $token); - - # We re-use this tool's fetcher, since it's pretty good - - $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); - - if (!$fetcher) { - common_log(LOG_WARNING, 'Failed to initialize Yadis fetcher.', __FILE__); - return false; - } - - $result = $fetcher->post($req->get_normalized_http_url(), - $req->to_postdata(), - array('User-Agent: Laconica/' . LACONICA_VERSION)); - - if ($result->status == 403) { # not authorized, don't send again - common_debug('403 result, deleting subscription', __FILE__); - # FIXME: figure out how to delete this - # $subscription->delete(); - return false; - } else if ($result->status != 200) { - common_debug('Error status '.$result->status, __FILE__); - return false; - } else { # success! - parse_str($result->body, $return); - if ($return['omb_version'] == OMB_VERSION_01) { - return true; - } else { - return false; + while ($rp->fetch()) { + if (isset($posted[$rp->updateprofileurl])) { + /* We already posted to this url. */ + continue; } + common_debug('Posting to ' . $rp->updateprofileurl, __FILE__); + + /* Update profile. */ + $service = new Laconica_OMB_Service_Consumer( + array(OMB_ENDPOINT_UPDATEPROFILE => $rp->updateprofileurl)); + try { + $service->setToken($rp->token, $rp->secret); + $service->updateProfile($omb_profile); + } catch (Exception $e) { + common_log(LOG_ERR, 'Failed posting to ' . $rp->updateprofileurl); + common_log(LOG_ERR, 'Error status '.$e); + continue; + } + $posted[$rp->updateprofileurl] = true; + + common_debug('Finished to ' . $rp->updateprofileurl, __FILE__); } + + return; } -function omb_broadcast_profile($profile) -{ - # First, get remote users subscribed to this profile - # XXX: use a join here rather than looping through results - $sub = new Subscription(); - $sub->subscribed = $profile->id; - if ($sub->find()) { - $updated = array(); - while ($sub->fetch()) { - $rp = Remote_profile::staticGet('id', $sub->subscriber); - if ($rp) { - if (!array_key_exists($rp->updateprofileurl, $updated)) { - if (omb_update_profile($profile, $rp, $sub)) { - $updated[$rp->updateprofileurl] = true; - } - } - } - } +class Laconica_OMB_Service_Consumer extends OMB_Service_Consumer { + public function __construct($urls) + { + $this->services = $urls; + $this->datastore = omb_oauth_datastore(); + $this->oauth_consumer = omb_oauth_consumer(); + $this->fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); } + } -function omb_update_profile($profile, $remote_profile, $subscription) +function profile_to_omb_profile($uri, $profile, $force = false) { - $user = User::staticGet($profile->id); - $con = omb_oauth_consumer(); - $token = new OAuthToken($subscription->token, $subscription->secret); - $url = $remote_profile->updateprofileurl; - $parsed = parse_url($url); - $params = array(); - parse_str($parsed['query'], $params); - $req = OAuthRequest::from_consumer_and_token($con, $token, - "POST", $url, $params); - $req->set_parameter('omb_version', OMB_VERSION_01); - $req->set_parameter('omb_listenee', $user->uri); - $req->set_parameter('omb_listenee_profile', common_profile_url($profile->nickname)); - $req->set_parameter('omb_listenee_nickname', $profile->nickname); - - # We use blanks to force emptying any existing values in these optional fields - - $req->set_parameter('omb_listenee_fullname', - ($profile->fullname) ? $profile->fullname : ''); - $req->set_parameter('omb_listenee_homepage', - ($profile->homepage) ? $profile->homepage : ''); - $req->set_parameter('omb_listenee_bio', - ($profile->bio) ? $profile->bio : ''); - $req->set_parameter('omb_listenee_location', - ($profile->location) ? $profile->location : ''); + $omb_profile = new OMB_Profile($uri); + $omb_profile->setNickname($profile->nickname); + $omb_profile->setLicenseURL(common_config('license', 'url')); + if (!is_null($profile->fullname)) { + $omb_profile->setFullname($profile->fullname); + } elseif ($force) { + $omb_profile->setFullname(''); + } + if (!is_null($profile->homepage)) { + $omb_profile->setHomepage($profile->homepage); + } elseif ($force) { + $omb_profile->setHomepage(''); + } + if (!is_null($profile->bio)) { + $omb_profile->setBio($profile->bio); + } elseif ($force) { + $omb_profile->setBio(''); + } + if (!is_null($profile->location)) { + $omb_profile->setLocation($profile->location); + } elseif ($force) { + $omb_profile->setLocation(''); + } + if (!is_null($profile->profileurl)) { + $omb_profile->setProfileURL($profile->profileurl); + } elseif ($force) { + $omb_profile->setProfileURL(''); + } $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); - $req->set_parameter('omb_listenee_avatar', - ($avatar) ? $avatar->url : ''); - - $req->sign_request(omb_hmac_sha1(), $con, $token); - - # We re-use this tool's fetcher, since it's pretty good - - $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); - - $result = $fetcher->post($req->get_normalized_http_url(), - $req->to_postdata(), - array('User-Agent: Laconica/' . LACONICA_VERSION)); - - if (empty($result) || !$result) { - common_debug("Unable to contact " . $req->get_normalized_http_url()); - } else if ($result->status == 403) { # not authorized, don't send again - common_debug('403 result, deleting subscription', __FILE__); - $subscription->delete(); - return false; - } else if ($result->status != 200) { - common_debug('Error status '.$result->status, __FILE__); - return false; - } else { # success! - parse_str($result->body, $return); - if (isset($return['omb_version']) && $return['omb_version'] === OMB_VERSION_01) { - return true; - } else { - return false; - } + if ($avatar) { + $omb_profile->setAvatarURL($avatar->url); + } elseif ($force) { + $omb_profile->setAvatarURL(''); } + return $omb_profile; } + +function notice_to_omb_notice($notice) +{ + /* Create an OMB_Notice for $notice. */ + $user = User::staticGet('id', $notice->profile_id); + + if (!$user) { + return null; + } + + $profile = $user->getProfile(); + + $omb_notice = new OMB_Notice(profile_to_omb_profile($user->uri, $profile), + $notice->uri, + $notice->content); + $omb_notice->setURL(common_local_url('shownotice', array('notice' => + $notice->id))); + $omb_notice->setLicenseURL(common_config('license', 'url')); + + return $omb_notice; +} +?> diff --git a/lib/unqueuemanager.php b/lib/unqueuemanager.php index 5154610725..34c7188b2d 100644 --- a/lib/unqueuemanager.php +++ b/lib/unqueuemanager.php @@ -39,7 +39,7 @@ class UnQueueManager case 'omb': if ($this->_isLocal($notice)) { require_once(INSTALLDIR.'/lib/omb.php'); - omb_broadcast_remote_subscribers($notice); + omb_broadcast_notice($notice); } break; case 'public': diff --git a/scripts/ombqueuehandler.php b/scripts/ombqueuehandler.php index 1587192b6f..cc5263ae23 100755 --- a/scripts/ombqueuehandler.php +++ b/scripts/ombqueuehandler.php @@ -57,7 +57,7 @@ class OmbQueueHandler extends QueueHandler $this->log(LOG_DEBUG, 'Ignoring remote notice ' . $notice->id); return true; } else { - return omb_broadcast_remote_subscribers($notice); + return omb_broadcast_notice($notice); } }