class AccesstokenAction extends Action {
function handle($args) {
parent::handle($args);
- common_server_error(_t('Not yet implemented.'));
+ try {
+ $req = OAuthRequest::from_request();
+ $server = common_oauth_server();
+ $token = $server->fetch_access_token($req);
+ print $token;
+ } catch (OAuthException $e) {
+ common_server_error($e->getMessage());
+ }
}
}
--- /dev/null
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once(INSTALLDIR.'/lib/omb.php');
+require_once('Auth/Yadis/Yadis.php');
+
+class FinishremotesubscribeAction extends Action {
+
+ function handle($args) {
+
+ parent::handle($args);
+
+ if (common_logged_in()) {
+ common_user_error(_t('You can use the local subscription!'));
+ return;
+ }
+
+ $nonce = $this->trimmed('nonce');
+
+ if (!$omb) {
+ common_user_error(_t('No nonce returned!'));
+ return;
+ }
+
+ $omb = $_SESSION[$nonce];
+
+ if (!$omb) {
+ common_user_error(_t('Not expecting this response!'));
+ return;
+ }
+
+ $req = OAuthRequest::from_request();
+
+ $token = $req->get_parameter('oauth_token');
+
+ # I think this is the success metric
+
+ if ($token != $omb['token']) {
+ common_user_error(_t('Not authorized.'));
+ return;
+ }
+
+ $version = $req->get_parameter('omb_version');
+
+ if ($version != OMB_VERSION_01) {
+ common_user_error(_t('Unknown version of OMB protocol.'));
+ return;
+ }
+
+ $nickname = $req->get_parameter('omb_listener_nickname');
+
+ if (!$nickname) {
+ common_user_error(_t('No nickname provided by remote server.'));
+ return;
+ }
+
+ $profile_url = $req->get_parameter('omb_listener_profile');
+
+ if (!$profile_url) {
+ common_user_error(_t('No profile URL returned by server.'));
+ return;
+ }
+
+ if (!Validate::uri($profile_url, array('allowed_schemes' => array('http', 'https')))) {
+ common_user_error(_t('Invalid profile URL returned by server.'));
+ return;
+ }
+
+ $user = User::staticGet('uri', $omb['listenee']);
+
+ if (!$user) {
+ common_user_error(_t('User being listened to doesn\'t exist.'));
+ 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) {
+ common_user_error(_t('Couldn\'t convert request tokens to access tokens.'));
+ return;
+ }
+
+ # XXX: possible attack point; subscribe and return someone else's profile URI
+
+ $remote = Remote_profile::staticGet('uri', $omb['listener']);
+
+ 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();
+ }
+
+ $profile->nickname = $nickname;
+ $profile->profileurl = $profile_url;
+
+ if ($fullname) {
+ $profile->fullname = $fullname;
+ }
+ if ($homepage) {
+ $profile->homepage = $homepage;
+ }
+ if ($bio) {
+ $profile->bio = $bio;
+ }
+ if ($location) {
+ $profile->location = $location;
+ }
+
+ if ($exists) {
+ $profile->update($orig_profile);
+ } else {
+ $profile->created = DB_DataObject_Cast::dateTime(); # current time
+ $id = $profile->insert();
+ $remote->id = $id;
+ }
+
+ if ($avatar_url) {
+ $this->add_avatar($avatar_url);
+ }
+
+ $remote->postnoticeurl = $omb[OMB_ENDPOINT_POSTNOTICE];
+ $remote->updateprofileurl = $omb[OMB_ENDPOINT_UPDATEPROFILE];
+
+ if ($exists) {
+ $remote->update($orig_remote);
+ } else {
+ $remote->created = DB_DataObject_Cast::dateTime(); # current time
+ $remote->insert;
+ }
+
+ $sub = new Subscription();
+ $sub->subscriber = $remote->id;
+ $sub->subscribed = $user->id;
+ $sub->token = $newtok;
+ $sub->secret = $newsecret;
+ $sub->created = DB_DataObject_Cast::dateTime(); # current time
+
+ if (!$sub->insert()) {
+ common_user_error(_t('Couldn\'t insert new subscription.'));
+ return;
+ }
+
+ # Clear the data
+ unset($_SESSION[$nonce]);
+
+ # If we show subscriptions in reverse chron order, this should
+ # show up close to the top of the page
+
+ common_redirect(common_local_url('subscribed', array('nickname' =>
+ $user->nickname)));
+ }
+
+ function access_token($omb) {
+
+ $con = omb_oauth_consumer();
+ $tok = new OAuthToken($omb['token'], $omb['secret']);
+
+ $url = $omb[OAUTH_ENDPOINT_ACCESS][0];
+
+ # 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, 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());
+
+ if ($result->status != 200) {
+ return NULL;
+ }
+
+ parse_str($result->body, $return);
+
+ return array($return['oauth_token'], $return['oauth_token_secret']);
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once(INSTALLDIR.'/lib/omb.php');
+require_once('Auth/Yadis/Yadis.php');
+
+class RemotesubscribeAction extends Action {
+
+ function handle($args) {
+
+ parent::handle($args);
+
+ if (common_logged_in()) {
+ common_user_error(_t('You can use the local subscription!'));
+ return;
+ }
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->remote_subscription();
+ } else {
+ $this->show_form();
+ }
+ }
+
+ function show_form($err=NULL) {
+ common_show_header(_t('Remote subscribe'));
+ if ($err) {
+ common_element('div', 'error', $err);
+ }
+ common_element_start('form', array('id' => 'remotesubscribe', 'method' => 'POST',
+ 'action' => common_local_url('remotesubscribe')));
+ common_input('profile', _t('Profile URL'));
+ common_submit('submit', _t('Subscribe'));
+ common_element_end('form');
+ }
+
+ function remote_subscription() {
+ $user = $this->get_user();
+
+ if (!$user) {
+ $this->show_form(_t('No such user!'));
+ return;
+ }
+
+ $profile = $this->trimmed('profile');
+
+ if (!$profile) {
+ $this->show_form(_t('No such user!'));
+ return;
+ }
+
+ if (!Validate::uri($profile, array('allowed_schemes' => array('http', 'https')))) {
+ $this->show_form(_t('Invalid profile URL (bad format)'));
+ return;
+ }
+
+ $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
+ $yadis = Auth_Yadis_Yadis::discover($profile, $fetcher);
+
+ if (!$yadis) {
+ $this->show_form(_t('Not a valid profile URL (no YADIS document).'));
+ return;
+ }
+
+ $omb = $this->getOmb($yadis);
+
+ if (!$omb) {
+ $this->show_form(_t('Not a valid profile URL (incorrect services).'));
+ return;
+ }
+
+ list($token, $secret) = $this->request_token($omb);
+
+ if (!$token || !$secret) {
+ $this->show_form(_t('Couldn\'t get a request token.'));
+ return;
+ }
+
+ $this->request_authorization($user, $omb, $token, $secret);
+ }
+
+ function get_user() {
+ $user = NULL;
+ $nickname = $this->trimmed('nickname');
+ if ($nickname) {
+ $user = User::staticGet('nickname', $nickname);
+ }
+ return $user;
+ }
+
+ function getOmb($yadis) {
+ static $endpoints = array(OMB_ENDPOINT_UPDATEPROFILE, OMB_ENDPOINT_POSTNOTICE,
+ OAUTH_ENDPOINT_REQUEST, OAUTH_ENDPOINT_AUTHORIZE,
+ OAUTH_ENDPOINT_ACCESS);
+ $omb = array();
+ $services = $yadis->services(); # ordered by priority
+ foreach ($services as $service) {
+ $types = $service->matchTypes($endpoints);
+ foreach ($types as $type) {
+ # We take the first one, since it's the highest priority
+ if (!array_key_exists($type, $omb)) {
+ # URIs is an array, priority-ordered
+ $omb[$type] = $service->getURIs();
+ # Special handling for request
+ if ($type == OAUTH_ENDPOINT_REQUEST) {
+ $nodes = $service->getElements('LocalID');
+ if (!$nodes) {
+ # error
+ return NULL;
+ }
+ $omb['listener'] = $service->parser->content($nodes[0]);
+ }
+ }
+ }
+ }
+ foreach ($endpoints as $ep) {
+ if (!array_key_exists($ep, $omb)) {
+ return NULL;
+ }
+ }
+ if (!array_key_exists('listener', $omb)) {
+ return NULL;
+ }
+ return $omb;
+ }
+
+ function request_token($omb) {
+ $con = omb_oauth_consumer();
+
+ $url = $omb[OAUTH_ENDPOINT_REQUEST][0];
+
+ # 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);
+
+ $req->set_parameter('omb_listener', $omb['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());
+
+ if ($result->status != 200) {
+ return NULL;
+ }
+
+ parse_str($result->body, $return);
+
+ return array($return['oauth_token'], $return['oauth_token_secret']);
+ }
+
+ function request_authorization($user, $omb, $token, $secret) {
+ global $config; # for license URL
+
+ $con = omb_oauth_consumer();
+ $tok = new OAuthToken($token, $secret);
+
+ $url = $omb[OAUTH_ENDPOINT_AUTHORIZE][0];
+
+ # 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['listener']);
+ $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', $config['license']['url']);
+ $profile = $user->getProfile();
+ if ($profile->fullname) {
+ $req->set_parameter('omb_listenee_fullname', $profile->fullname);
+ }
+ if ($profile->homepage) {
+ $req->set_parameter('omb_listenee_homepage', $profile->homepage);
+ }
+ if ($profile->bio) {
+ $req->set_parameter('omb_listenee_bio', $profile->bio);
+ }
+ if ($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);
+ }
+
+ $nonce = $this->make_nonce();
+
+ $req->set_parameter('oauth_callback', common_local_url('finishremotesubscribe',
+ array('nonce' => $nonce)));
+
+ # 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['token'] = $token;
+ $omb['secret'] = $secret;
+
+ $_SESSION[$nonce] = $omb;
+
+ # Redirect to authorization service
+
+ common_redirect($req->to_url());
+ return;
+ }
+}
\ No newline at end of file
if (!defined('LACONICA')) { exit(1); }
+require_once(INSTALLDIR.'/lib/omb.php');
+
class RequesttokenAction extends Action {
function handle($args) {
parent::handle($args);
- common_server_error(_t('Not yet implemented.'));
+ try {
+ $req = OAuthRequest::from_request();
+ $server = common_oauth_server();
+ $token = $server->fetch_request_token($req);
+ print $token;
+ } catch (OAuthException $e) {
+ common_server_error($e->getMessage());
+ }
}
}
$user->nickname)),
'type' => 'application/rss+xml',
'title' => _t('Notice feed for ') . $user->nickname));
+ # for remote subscriptions etc.
+ common_element('meta', array('http-equiv' => 'X-XRDS-Location',
+ 'content' => common_local_url('xrds', array('nickname' =>
+ $user->nickname))));
}
function no_such_user() {
class UserauthorizationAction extends Action {
function handle($args) {
parent::handle($args);
- common_server_error(_t('Not yet implemented.'));
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->send_authorization();
+ } else {
+ try {
+ $req = $this->get_request();
+ $server = common_oauth_server();
+ list($consumer, $token) = $server->verify_request($req);
+ } catch (OAuthException $e) {
+ $this->clear_request();
+ common_server_error($e->getMessage());
+ return;
+ }
+
+ if (common_logged_in()) {
+ $this->show_form($req);
+ } else {
+ common_return_to(common_local_url('userauthorization'));
+ common_redirect(common_local_url('login'));
+ }
+ }
+ }
+
+ function store_request($req) {
+ common_ensure_session();
+ $_SESSION['userauthorizationrequest'] = $req;
+ }
+
+ function get_request() {
+ common_ensure_session();
+ $req = $_SESSION['userauthorizationrequest'];
+ if (!$req) {
+ # XXX: may have an uncaught exception
+ $req = OAuthRequest::from_request();
+ $this->store_request($req);
+ }
+ return $req;
+ }
+
+ function show_form($req) {
+ common_show_header(_t('Authorize subscription'));
+
+ common_show_footer();
+ }
+
+ function send_authorization() {
+ $req = $this->get_request();
+ if (!$req) {
+ common_user_error(_t('No authorization request!'));
+ return;
+ }
+
+ if ($this->boolean('authorize')) {
+
+ }
}
}
function show_service($type, $uri, $params=NULL, $sigs=NULL, $localId=NULL) {
common_element_start('Service');
- common_element('URI', NULL, $uri);
+ if ($uri) {
+ common_element('URI', NULL, $uri);
+ }
common_element('Type', NULL, $type);
if ($params) {
foreach ($params as $param) {
subscriber integer not null comment 'profile listening',
subscribed integer not null comment 'profile being listened to',
token varchar(255) comment 'authorization token',
+ secret varchar(255) comment 'token secret',
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified',
use the openidenabled.com libraries for OpenID auth sometime in the
future. Note that this is no longer distributed separately; it's only
in the openidenabled.com OpenID PHP tarball.
-
+- OAuth.php from http://oauth.googlecode.com/svn/code/php/
+
+ public stream link in top menu
+ dump, fix, undump database
+ release 0.2
-- YADIS document link on showstream
-- YADIS document
++ YADIS document link on showstream
++ YADIS document
- subscribe remote
- add subscriber remote
-- send remote notice
+- server side of user authorization
+- server side of request token
+- server side of access token
+- OAuth store
+- log of consumers who ask for access
- receive remote notice
+- send remote notice
+- subscribe form for not-logged-in users on showstream
- pretty URLs
- doc action
- about doc
- add a next page link to public
- add a next page link to all
- AGPL notification
+- Check licenses of all libraries for compatibility
- gettext
- release 0.3
- license per notice
Initiation
==========
-The user submits their profile URL [*] to the remote service somehow --
+The user submits their profile URL [*]_ to the remote service somehow --
for example, with an HTML form on the remote service's Web site.
.. [*] For OAuth Discovery, this is the "protected resource". It may
The remote service must go through the OAuth 1.0 dance to get
authorization to post notices and update profiles.
-In all OAuth, the consumer key should be blank (''), unless the remote
-server and local service have negotiated another key. Such negotiation
-is out-of-scope for this document, and we assume an "open" network of
-microblogging services. But if you want to have that kind of network,
-do it with this key.
+In all OAuth, the consumer key should be the root URL for the
+microblogging service, if available. The secret should be the blank
+string (''), unless the remote server and local service have negotiated
+another key. Such negotiation is out-of-scope for this document, and we
+assume an "open" network of microblogging services. But if you want to
+have that kind of network, do it with this key.
The remote service MUST do OAuth for every new listener, regardless of
whether they've already received authorization for posting to the
to anyone.
The remote service SHOULD NOT send a message with the same notice URL
-to the same postNotice URL more than once. [2]_ If the request returns
+to the same postNotice URL more than once. [*]_ If the request returns
a 403 Unauthorized message, the remote service SHOULD NOT post
messages to the same URL again with the same listenee, until another
-listener has gone through the OAuth dance. [3]_
+listener has gone through the OAuth dance. [*]_
-.. [2] A half-assed optimization. A local service may have a lot of
+.. [*] A half-assed optimization. A local service may have a lot of
listeners listening to the same listenee. It would be pointless to
have the remote service post the same notice 100 times to the same
service. However, if the local service wants fine-grained control,
it can have a different postNotice URL for each listener.
-.. [3] If there's one postNotice URL per listener, the 403 message
+.. [*] If there's one postNotice URL per listener, the 403 message
means the listener has told the local service not to allow posting
any more ("unsubscribed"). If there's one postNotice URL per local
service, it means that the count of listeners has dropped to 0.
if (!defined('LACONICA')) { exit(1); }
+require_once('OAuth.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_POST_BODY', OAUTH_NAMESPACE.'parameters/post-body');
define('OAUTH_HMAC_SHA1', OAUTH_NAMESPACE.'signature/HMAC-SHA1');
+function omb_oauth_consumer() {
+ static $con = null;
+ if (!$con) {
+ $con = new OAuthConsumer(common_root_url(), '');
+ }
+ return $con;
+}
+
+function omb_hmac_sha1() {
+ static $hmac_method = NULL;
+ if (!$hmac_method) {
+ $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
+ }
+ return $hmac_method;
+}
\ No newline at end of file
$config['tag']['date'].':'.$config['tag']['prefix'].$extra;
}
+# Should make up a reasonable root URL
+
+function common_root_url() {
+ global $config;
+ $pathpart = ($config['site']['path']) ? $config['site']['path']."/" : '';
+ return "http://".$config['site']['server'].'/'.$pathpart;
+}
+
// XXX: set up gettext
function _t($str) {