From 5697e4edb0fc4bb1d4c9365100501e795b2553de Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 22 Mar 2010 10:35:54 -0700 Subject: [PATCH 1/7] Replace the "give up and dump object" attachment view fallback with a client-side redirect to the target URL, which will at least be useful. --- lib/attachmentlist.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php index 51ceca8576..fe38281af9 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -306,7 +306,7 @@ class Attachment extends AttachmentListItem function showRepresentation() { if (empty($this->oembed->type)) { if (empty($this->attachment->mimetype)) { - $this->out->element('pre', null, 'oh well... not sure how to handle the following: ' . print_r($this->attachment, true)); + $this->showFallback(); } else { switch ($this->attachment->mimetype) { case 'image/gif': @@ -332,6 +332,8 @@ class Attachment extends AttachmentListItem $this->out->element('param', array('name' => 'autoStart', 'value' => 1)); $this->out->elementEnd('object'); break; + default: + $this->showFallback(); } } } else { @@ -354,9 +356,23 @@ class Attachment extends AttachmentListItem break; default: - $this->out->element('pre', null, 'oh well... not sure how to handle the following oembed: ' . print_r($this->oembed, true)); + $this->showFallback(); } } } + + function showFallback() + { + // If we don't know how to display an attachment inline, we probably + // shouldn't have gotten to this point. + // + // But, here we are... displaying details on a file or remote URL + // either on the main view or in an ajax-loaded lightbox. As a lesser + // of several evils, we'll try redirecting to the actual target via + // client-side JS. + + common_log(LOG_ERR, "Empty or unknown type for file id {$this->attachment->id}; falling back to client-side redirect."); + $this->out->raw(''); + } } From fcdbf421ab2bbbe4d1b601a8c80d4612c91f62fe Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 23 Mar 2010 11:36:02 -0400 Subject: [PATCH 2/7] reformat OpenIDPlugin for PHPCS --- plugins/OpenID/OpenIDPlugin.php | 268 ++++++++++++++++++++++++-------- 1 file changed, 199 insertions(+), 69 deletions(-) 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', From 80b16c8499d0cfdb4deb442ba18345befed4e29d Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 23 Mar 2010 09:50:01 -0700 Subject: [PATCH 3/7] Don't add PHPSESSID parameter onto notice and conversation URIs if we save a notice during a session override. This was being triggered by welcomebot messages created at account creation time, then propagated through replies. --- classes/Conversation.php | 3 ++- lib/util.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/classes/Conversation.php b/classes/Conversation.php index ea8bd87b56..f540004ef3 100755 --- a/classes/Conversation.php +++ b/classes/Conversation.php @@ -63,7 +63,8 @@ class Conversation extends Memcached_DataObject } $orig = clone($conv); - $orig->uri = common_local_url('conversation', array('id' => $id)); + $orig->uri = common_local_url('conversation', array('id' => $id), + null, null, false); $result = $orig->update($conv); if (empty($result)) { diff --git a/lib/util.php b/lib/util.php index 44ccc0deff..3d4ed087f9 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1521,7 +1521,8 @@ function common_user_uri(&$user) function common_notice_uri(&$notice) { return common_local_url('shownotice', - array('notice' => $notice->id)); + array('notice' => $notice->id), + null, null, false); } // 36 alphanums - lookalikes (0, O, 1, I) = 32 chars = 5 bits From 44caa3a93f452777c795006edb52ef4c5c2c4997 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 23 Mar 2010 11:06:37 -0700 Subject: [PATCH 4/7] Consistently send Profiles into Fave::addNew() --- actions/apifavoritecreate.php | 2 +- classes/Fave.php | 10 +++++++++- lib/command.php | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/actions/apifavoritecreate.php b/actions/apifavoritecreate.php index 3618f94018..00b6349b0a 100644 --- a/actions/apifavoritecreate.php +++ b/actions/apifavoritecreate.php @@ -123,7 +123,7 @@ class ApiFavoriteCreateAction extends ApiAuthAction return; } - $fave = Fave::addNew($this->user, $this->notice); + $fave = Fave::addNew($this->user->getProfile(), $this->notice); if (empty($fave)) { $this->clientError( diff --git a/classes/Fave.php b/classes/Fave.php index a04f15e9c4..7ca9ade7f0 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -21,7 +21,15 @@ class Fave extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - static function addNew($profile, $notice) { + /** + * Save a favorite record. + * @fixme post-author notification should be moved here + * + * @param Profile $profile the local or remote user who likes + * @param Notice $notice the notice that is liked + * @return mixed false on failure, or Fave record on success + */ + static function addNew(Profile $profile, Notice $notice) { $fave = null; diff --git a/lib/command.php b/lib/command.php index 9d550550f7..8080fb8bc0 100644 --- a/lib/command.php +++ b/lib/command.php @@ -273,7 +273,7 @@ class FavCommand extends Command function handle($channel) { $notice = $this->getNotice($this->other); - $fave = Fave::addNew($this->user, $notice); + $fave = Fave::addNew($this->user->getProfile(), $notice); if (!$fave) { $channel->error($this->user, _('Could not create favorite.')); From 7dc24b4ca7dffda85338d35da3618ec50ce0dbf7 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 23 Mar 2010 13:10:23 -0700 Subject: [PATCH 5/7] FOAF was missing OStatus remote subscriptions, now fixed. --- actions/foaf.php | 68 ++++++++++++++++-------------------------------- 1 file changed, 23 insertions(+), 45 deletions(-) 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); } From 5f32cf32cd7d4a5df7ba64d4f1e7d9edee8d418c Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 23 Mar 2010 14:18:45 -0700 Subject: [PATCH 6/7] Don't spew XML parse warnings to output when checking a remote XRD page --- plugins/OStatus/lib/xrd.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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); From df8c9090c0deabe20b804e0fd0766d6b86b7968f Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 23 Mar 2010 14:19:12 -0700 Subject: [PATCH 7/7] Add basic subscribe/unsubscribe test to OStatus test cases --- plugins/OStatus/tests/remote-tests.php | 176 ++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 19 deletions(-) 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; } }