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',