diff --git a/actions/foaf.php b/actions/foaf.php
index fc2ec9b12f..fc56e19b4f 100644
--- a/actions/foaf.php
+++ b/actions/foaf.php
@@ -162,40 +162,29 @@ class FoafAction extends Action
if ($sub->find()) {
while ($sub->fetch()) {
- if ($sub->token) {
- $other = Remote_profile::staticGet('id', $sub->subscriber);
- $profile = Profile::staticGet('id', $sub->subscriber);
- } else {
- $other = User::staticGet('id', $sub->subscriber);
- $profile = Profile::staticGet('id', $sub->subscriber);
- }
- if (!$other) {
+ $profile = Profile::staticGet('id', $sub->subscriber);
+ if (empty($profile)) {
common_debug('Got a bad subscription: '.print_r($sub,true));
continue;
}
- if (array_key_exists($other->uri, $person)) {
- $person[$other->uri][0] = BOTH;
+ $user = $profile->getUser();
+ $other_uri = $profile->getUri();
+ if (array_key_exists($other_uri, $person)) {
+ $person[$other_uri][0] = BOTH;
} else {
- $person[$other->uri] = array(LISTENER,
- $other->id,
- $profile->nickname,
- (empty($sub->token)) ? 'User' : 'Remote_profile');
+ $person[$other_uri] = array(LISTENER,
+ $profile->id,
+ $profile->nickname,
+ $user ? 'local' : 'remote');
}
- $other->free();
- $other = null;
- unset($other);
- $profile->free();
- $profile = null;
unset($profile);
}
}
- $sub->free();
- $sub = null;
unset($sub);
foreach ($person as $uri => $p) {
- list($type, $id, $nickname, $cls) = $p;
+ list($type, $id, $nickname, $local) = $p;
if ($type == BOTH) {
$this->element('knows', array('rdf:resource' => $uri));
}
@@ -206,8 +195,8 @@ class FoafAction extends Action
foreach ($person as $uri => $p) {
$foaf_url = null;
- list($type, $id, $nickname, $cls) = $p;
- if ($cls == 'User') {
+ list($type, $id, $nickname, $local) = $p;
+ if ($local == 'local') {
$foaf_url = common_local_url('foaf', array('nickname' => $nickname));
}
$profile = Profile::staticGet($id);
@@ -216,7 +205,7 @@ class FoafAction extends Action
$this->element('knows', array('rdf:resource' => $this->user->uri));
}
$this->showMicrobloggingAccount($profile,
- ($cls == 'User') ? common_root_url() : null,
+ ($local == 'local') ? common_root_url() : null,
$uri,
true);
if ($foaf_url) {
@@ -275,33 +264,22 @@ class FoafAction extends Action
if ($sub->find()) {
while ($sub->fetch()) {
- if (!empty($sub->token)) {
- $other = Remote_profile::staticGet('id', $sub->subscribed);
- $profile = Profile::staticGet('id', $sub->subscribed);
- } else {
- $other = User::staticGet('id', $sub->subscribed);
- $profile = Profile::staticGet('id', $sub->subscribed);
- }
- if (empty($other)) {
+ $profile = Profile::staticGet('id', $sub->subscribed);
+ if (empty($profile)) {
common_debug('Got a bad subscription: '.print_r($sub,true));
continue;
}
- $this->element('sioc:follows', array('rdf:resource' => $other->uri.'#acct'));
- $person[$other->uri] = array(LISTENEE,
- $other->id,
- $profile->nickname,
- (empty($sub->token)) ? 'User' : 'Remote_profile');
- $other->free();
- $other = null;
- unset($other);
- $profile->free();
- $profile = null;
+ $user = $profile->getUser();
+ $other_uri = $profile->getUri();
+ $this->element('sioc:follows', array('rdf:resource' => $other_uri.'#acct'));
+ $person[$other_uri] = array(LISTENEE,
+ $profile->id,
+ $profile->nickname,
+ $user ? 'local' : 'remote');
unset($profile);
}
}
- $sub->free();
- $sub = null;
unset($sub);
}
diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php
index aa13ef0242..34b28790b7 100644
--- a/plugins/OStatus/lib/xrd.php
+++ b/plugins/OStatus/lib/xrd.php
@@ -53,7 +53,14 @@ class XRD
$xrd = new XRD();
$dom = new DOMDocument();
- if (!$dom->loadXML($xml)) {
+
+ // Don't spew XML warnings to output
+ $old = error_reporting();
+ error_reporting($old & ~E_WARNING);
+ $ok = $dom->loadXML($xml);
+ error_reporting($old);
+
+ if (!$ok) {
throw new Exception("Invalid XML");
}
$xrd_element = $dom->getElementsByTagName('XRD')->item(0);
diff --git a/plugins/OStatus/tests/remote-tests.php b/plugins/OStatus/tests/remote-tests.php
index 103ca066c0..b064114911 100644
--- a/plugins/OStatus/tests/remote-tests.php
+++ b/plugins/OStatus/tests/remote-tests.php
@@ -40,6 +40,20 @@ class TestBase
}
return true;
}
+
+ function assertTrue($a)
+ {
+ if (!$a) {
+ throw new Exception("Failed to assert true: got false");
+ }
+ }
+
+ function assertFalse($a)
+ {
+ if ($a) {
+ throw new Exception("Failed to assert false: got true");
+ }
+ }
}
class OStatusTester extends TestBase
@@ -60,8 +74,12 @@ class OStatusTester extends TestBase
function run()
{
$this->setup();
+
$this->testLocalPost();
$this->testMentionUrl();
+ $this->testSubscribe();
+ $this->testUnsubscribe();
+
$this->log("DONE!");
}
@@ -98,6 +116,25 @@ class OStatusTester extends TestBase
$post = $this->pub->post("@$base/$name should have this in home and replies");
$this->sub->assertReceived($post);
}
+
+ function testSubscribe()
+ {
+ $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
+ $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
+ $this->sub->subscribe($this->pub->getProfileLink());
+ $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
+ $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
+ }
+
+ function testUnsubscribe()
+ {
+ $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
+ $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
+ $this->sub->unsubscribe($this->pub->getProfileLink());
+ $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
+ $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
+ }
+
}
class SNTestClient extends TestBase
@@ -202,6 +239,43 @@ class SNTestClient extends TestBase
return $dom;
}
+ protected function parseXml($path, $body)
+ {
+ $dom = new DOMDocument();
+ if ($dom->loadXML($body)) {
+ return $dom;
+ } else {
+ throw new Exception("Bogus XML data from $path:\n$body");
+ }
+ }
+
+ /**
+ * Make a hit to a REST-y XML page on the site, without authentication.
+ * @param string $path URL fragment for something relative to base
+ * @param array $params POST parameters to send
+ * @return DOMDocument
+ * @throws Exception on low-level error conditions
+ */
+ protected function xml($path, $params=array())
+ {
+ $response = $this->hit($path, $params, true);
+ $body = $response->getBody();
+ return $this->parseXml($path, $body);
+ }
+
+ protected function parseJson($path, $body)
+ {
+ $data = json_decode($body, true);
+ if ($data !== null) {
+ if (!empty($data['error'])) {
+ throw new Exception("JSON API returned error: " . $data['error']);
+ }
+ return $data;
+ } else {
+ throw new Exception("Bogus JSON data from $path:\n$body");
+ }
+ }
+
/**
* Make an API hit to this site, with authentication.
* @param string $path URL fragment for something under 'api' folder
@@ -215,22 +289,9 @@ class SNTestClient extends TestBase
$response = $this->hit("api/$path.$style", $params, true);
$body = $response->getBody();
if ($style == 'json') {
- $data = json_decode($body, true);
- if ($data !== null) {
- if (!empty($data['error'])) {
- throw new Exception("JSON API returned error: " . $data['error']);
- }
- return $data;
- } else {
- throw new Exception("Bogus JSON data from $path:\n$body");
- }
+ return $this->parseJson($path, $body);
} else if ($style == 'xml' || $style == 'atom') {
- $dom = new DOMDocument();
- if ($dom->loadXML($body)) {
- return $dom;
- } else {
- throw new Exception("Bogus XML data from $path:\n$body");
- }
+ return $this->parseXml($path, $body);
} else {
throw new Exception("API needs to be JSON, XML, or Atom");
}
@@ -257,6 +318,24 @@ class SNTestClient extends TestBase
'submit' => 'Register'));
}
+ /**
+ * @return string canonical URI/URL to profile page
+ */
+ function getProfileUri()
+ {
+ $data = $this->api('account/verify_credentials', 'json');
+ $id = $data['id'];
+ return $this->basepath . '/user/' . $id;
+ }
+
+ /**
+ * @return string human-friendly URL to profile page
+ */
+ function getProfileLink()
+ {
+ return $this->basepath . '/' . $this->username;
+ }
+
/**
* Check that the account has been registered and can be used.
* On failure, throws a test failure exception.
@@ -349,22 +428,81 @@ class SNTestClient extends TestBase
return false;
}
+ /**
+ * @param string $profile user page link or webfinger
+ */
+ function subscribe($profile)
+ {
+ // This uses the command interface, since there's not currently
+ // a friendly Twit-API way to do a fresh remote subscription and
+ // the web form's a pain to use.
+ $this->post('follow ' . $profile);
+ }
+
+ /**
+ * @param string $profile user page link or webfinger
+ */
+ function unsubscribe($profile)
+ {
+ // This uses the command interface, since there's not currently
+ // a friendly Twit-API way to do a fresh remote subscription and
+ // the web form's a pain to use.
+ $this->post('leave ' . $profile);
+ }
+
/**
* Check that this account is subscribed to the given profile.
* @param string $profile_uri URI for the profile to check for
+ * @return boolean
*/
- function assertHasSubscription($profile_uri)
+ function hasSubscription($profile_uri)
{
- throw new Exception('tbi');
+ $this->log("Checking if $this->username has a subscription to $profile_uri");
+
+ $me = $this->getProfileUri();
+ return $this->checkSubscription($me, $profile_uri);
}
/**
* Check that this account is subscribed to by the given profile.
* @param string $profile_uri URI for the profile to check for
+ * @return boolean
*/
- function assertHasSubscriber($profile_uri)
+ function hasSubscriber($profile_uri)
{
- throw new Exception('tbi');
+ $this->log("Checking if $this->username is subscribed to by $profile_uri");
+
+ $me = $this->getProfileUri();
+ return $this->checkSubscription($profile_uri, $me);
+ }
+
+ protected function checkSubscription($subscriber, $subscribed)
+ {
+ // Using FOAF as the API methods for checking the social graph
+ // currently are unfriendly to remote profiles
+ $ns_foaf = 'http://xmlns.com/foaf/0.1/';
+ $ns_sioc = 'http://rdfs.org/sioc/ns#';
+ $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
+
+ $dom = $this->xml($this->username . '/foaf');
+ $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent');
+ foreach ($agents as $agent) {
+ $agent_uri = $agent->getAttributeNS($ns_rdf, 'about');
+ if ($agent_uri == $subscriber) {
+ $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows');
+ foreach ($follows as $follow) {
+ $target = $follow->getAttributeNS($ns_rdf, 'resource');
+ if ($target == ($subscribed . '#acct')) {
+ $this->log("Confirmed $subscriber subscribed to $subscribed");
+ return true;
+ }
+ }
+ $this->log("We found $subscriber but they don't follow $subscribed");
+ return false;
+ }
+ }
+ $this->log("Can't find $subscriber in {$this->username}'s social graph.");
+ return false;
}
}
diff --git a/plugins/OpenID/OpenIDPlugin.php b/plugins/OpenID/OpenIDPlugin.php
index 6b35ec3e14..1724b5f7be 100644
--- a/plugins/OpenID/OpenIDPlugin.php
+++ b/plugins/OpenID/OpenIDPlugin.php
@@ -59,6 +59,8 @@ class OpenIDPlugin extends Plugin
*
* Hook for RouterInitialized event.
*
+ * @param Net_URL_Mapper $m URL mapper
+ *
* @return boolean hook return
*/
@@ -67,54 +69,87 @@ class OpenIDPlugin extends Plugin
$m->connect('main/openid', array('action' => 'openidlogin'));
$m->connect('main/openidtrust', array('action' => 'openidtrust'));
$m->connect('settings/openid', array('action' => 'openidsettings'));
- $m->connect('index.php?action=finishopenidlogin', array('action' => 'finishopenidlogin'));
- $m->connect('index.php?action=finishaddopenid', array('action' => 'finishaddopenid'));
+ $m->connect('index.php?action=finishopenidlogin',
+ array('action' => 'finishopenidlogin'));
+ $m->connect('index.php?action=finishaddopenid',
+ array('action' => 'finishaddopenid'));
$m->connect('main/openidserver', array('action' => 'openidserver'));
return true;
}
+ /**
+ * Public XRDS output hook
+ *
+ * Puts the bits of code needed by some OpenID providers to show
+ * we're good citizens.
+ *
+ * @param Action $action Action being executed
+ * @param XMLOutputter &$xrdsOutputter Output channel
+ *
+ * @return boolean hook return
+ */
+
function onEndPublicXRDS($action, &$xrdsOutputter)
{
$xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)',
- 'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
- 'version' => '2.0'));
+ 'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
+ 'version' => '2.0'));
$xrdsOutputter->element('Type', null, 'xri://$xrds*simple');
//consumer
foreach (array('finishopenidlogin', 'finishaddopenid') as $finish) {
$xrdsOutputter->showXrdsService(Auth_OpenID_RP_RETURN_TO_URL_TYPE,
- common_local_url($finish));
+ common_local_url($finish));
}
//provider
$xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/server',
- common_local_url('openidserver'),
- null,
- null,
- 'http://specs.openid.net/auth/2.0/identifier_select');
+ common_local_url('openidserver'),
+ null,
+ null,
+ 'http://specs.openid.net/auth/2.0/identifier_select');
$xrdsOutputter->elementEnd('XRD');
}
+ /**
+ * User XRDS output hook
+ *
+ * Puts the bits of code needed to discover OpenID endpoints.
+ *
+ * @param Action $action Action being executed
+ * @param XMLOutputter &$xrdsOutputter Output channel
+ *
+ * @return boolean hook return
+ */
+
function onEndUserXRDS($action, &$xrdsOutputter)
{
$xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)',
- 'xml:id' => 'openid',
- 'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
- 'version' => '2.0'));
+ 'xml:id' => 'openid',
+ 'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
+ 'version' => '2.0'));
$xrdsOutputter->element('Type', null, 'xri://$xrds*simple');
//consumer
$xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/return_to',
- common_local_url('finishopenidlogin'));
+ common_local_url('finishopenidlogin'));
//provider
$xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/signon',
- common_local_url('openidserver'),
- null,
- null,
- common_profile_url($action->user->nickname));
+ common_local_url('openidserver'),
+ null,
+ null,
+ common_profile_url($action->user->nickname));
$xrdsOutputter->elementEnd('XRD');
}
+ /**
+ * Menu item for login
+ *
+ * @param Action &$action Action being executed
+ *
+ * @return boolean hook return
+ */
+
function onEndLoginGroupNav(&$action)
{
$action_name = $action->trimmed('action');
@@ -127,6 +162,14 @@ class OpenIDPlugin extends Plugin
return true;
}
+ /**
+ * Menu item for OpenID admin
+ *
+ * @param Action &$action Action being executed
+ *
+ * @return boolean hook return
+ */
+
function onEndAccountSettingsNav(&$action)
{
$action_name = $action->trimmed('action');
@@ -139,68 +182,102 @@ class OpenIDPlugin extends Plugin
return true;
}
+ /**
+ * Autoloader
+ *
+ * Loads our classes if they're requested.
+ *
+ * @param string $cls Class requested
+ *
+ * @return boolean hook return
+ */
+
function onAutoload($cls)
{
switch ($cls)
{
- case 'OpenidloginAction':
- case 'FinishopenidloginAction':
- case 'FinishaddopenidAction':
- case 'XrdsAction':
- case 'PublicxrdsAction':
- case 'OpenidsettingsAction':
- case 'OpenidserverAction':
- case 'OpenidtrustAction':
- require_once(INSTALLDIR.'/plugins/OpenID/' . strtolower(mb_substr($cls, 0, -6)) . '.php');
+ case 'OpenidloginAction':
+ case 'FinishopenidloginAction':
+ case 'FinishaddopenidAction':
+ case 'XrdsAction':
+ case 'PublicxrdsAction':
+ case 'OpenidsettingsAction':
+ case 'OpenidserverAction':
+ case 'OpenidtrustAction':
+ require_once INSTALLDIR.'/plugins/OpenID/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
return false;
- case 'User_openid':
- require_once(INSTALLDIR.'/plugins/OpenID/User_openid.php');
+ case 'User_openid':
+ require_once INSTALLDIR.'/plugins/OpenID/User_openid.php';
return false;
- case 'User_openid_trustroot':
- require_once(INSTALLDIR.'/plugins/OpenID/User_openid_trustroot.php');
+ case 'User_openid_trustroot':
+ require_once INSTALLDIR.'/plugins/OpenID/User_openid_trustroot.php';
return false;
- default:
- return true;
- }
- }
-
- function onSensitiveAction($action, &$ssl)
- {
- switch ($action)
- {
- case 'finishopenidlogin':
- case 'finishaddopenid':
- $ssl = true;
- return false;
- default:
- return true;
- }
- }
-
- function onLoginAction($action, &$login)
- {
- switch ($action)
- {
- case 'openidlogin':
- case 'finishopenidlogin':
- case 'openidserver':
- $login = true;
- return false;
- default:
+ default:
return true;
}
}
/**
- * We include a element linking to the publicxrds page, for OpenID
+ * Sensitive actions
+ *
+ * These actions should use https when SSL support is 'sometimes'
+ *
+ * @param Action $action Action to form an URL for
+ * @param boolean &$ssl Whether to mark it for SSL
+ *
+ * @return boolean hook return
+ */
+
+ function onSensitiveAction($action, &$ssl)
+ {
+ switch ($action)
+ {
+ case 'finishopenidlogin':
+ case 'finishaddopenid':
+ $ssl = true;
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Login actions
+ *
+ * These actions should be visible even when the site is marked private
+ *
+ * @param Action $action Action to show
+ * @param boolean &$login Whether it's a login action
+ *
+ * @return boolean hook return
+ */
+
+ function onLoginAction($action, &$login)
+ {
+ switch ($action)
+ {
+ case 'openidlogin':
+ case 'finishopenidlogin':
+ case 'openidserver':
+ $login = true;
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * We include a element linking to the userxrds page, for OpenID
* client-side authentication.
*
+ * @param Action $action Action being shown
+ *
* @return void
*/
function onEndShowHeadElements($action)
{
- if($action instanceof ShowstreamAction){
+ if ($action instanceof ShowstreamAction) {
$action->element('link', array('rel' => 'openid2.provider',
'href' => common_local_url('openidserver')));
$action->element('link', array('rel' => 'openid2.local_id',
@@ -216,6 +293,9 @@ class OpenIDPlugin extends Plugin
/**
* Redirect to OpenID login if they have an OpenID
*
+ * @param Action $action Action being executed
+ * @param User $user User doing the action
+ *
* @return boolean whether to continue
*/
@@ -228,13 +308,21 @@ class OpenIDPlugin extends Plugin
return true;
}
+ /**
+ * Show some extra instructions for using OpenID
+ *
+ * @param Action $action Action being executed
+ *
+ * @return boolean hook value
+ */
+
function onEndShowPageNotice($action)
{
$name = $action->trimmed('action');
switch ($name)
{
- case 'register':
+ case 'register':
if (common_logged_in()) {
$instr = '(Have an [OpenID](http://openid.net/)? ' .
'[Add an OpenID to your account](%%action.openidsettings%%)!';
@@ -244,12 +332,12 @@ class OpenIDPlugin extends Plugin
'(%%action.openidlogin%%)!)';
}
break;
- case 'login':
+ case 'login':
$instr = '(Have an [OpenID](http://openid.net/)? ' .
'Try our [OpenID login]'.
'(%%action.openidlogin%%)!)';
break;
- default:
+ default:
return true;
}
@@ -258,13 +346,21 @@ class OpenIDPlugin extends Plugin
return true;
}
+ /**
+ * Load our document if requested
+ *
+ * @param string &$title Title to fetch
+ * @param string &$output HTML to output
+ *
+ * @return boolean hook value
+ */
+
function onStartLoadDoc(&$title, &$output)
{
- if ($title == 'openid')
- {
+ if ($title == 'openid') {
$filename = INSTALLDIR.'/plugins/OpenID/doc-src/openid';
- $c = file_get_contents($filename);
+ $c = file_get_contents($filename);
$output = common_markup_to_html($c);
return false; // success!
}
@@ -272,10 +368,18 @@ class OpenIDPlugin extends Plugin
return true;
}
+ /**
+ * Add our document to the global menu
+ *
+ * @param string $title Title being fetched
+ * @param string &$output HTML being output
+ *
+ * @return boolean hook value
+ */
+
function onEndLoadDoc($title, &$output)
{
- if ($title == 'help')
- {
+ if ($title == 'help') {
$menuitem = '* [OpenID](%%doc.openid%%) - what OpenID is and how to use it with this service';
$output .= common_markup_to_html($menuitem);
@@ -284,7 +388,16 @@ class OpenIDPlugin extends Plugin
return true;
}
- function onCheckSchema() {
+ /**
+ * Data definitions
+ *
+ * Assure that our data objects are available in the DB
+ *
+ * @return boolean hook value
+ */
+
+ function onCheckSchema()
+ {
$schema = Schema::get();
$schema->ensureTable('user_openid',
array(new ColumnDef('canonical', 'varchar',
@@ -307,6 +420,15 @@ class OpenIDPlugin extends Plugin
return true;
}
+ /**
+ * Add our tables to be deleted when a user is deleted
+ *
+ * @param User $user User being deleted
+ * @param array &$tables Array of table names
+ *
+ * @return boolean hook value
+ */
+
function onUserDeleteRelated($user, &$tables)
{
$tables[] = 'User_openid';
@@ -314,6 +436,14 @@ class OpenIDPlugin extends Plugin
return true;
}
+ /**
+ * Add our version information to output
+ *
+ * @param array &$versions Array of version-data arrays
+ *
+ * @return boolean hook value
+ */
+
function onPluginVersion(&$versions)
{
$versions[] = array('name' => 'OpenID',