From 662c0bb889fea51088fdd59482311b7b81a8aae2 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 2 Jan 2011 10:21:52 -0800 Subject: [PATCH 01/11] Move discovery library from OStatus plugin to core --- {plugins/OStatus/lib => lib}/discovery.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {plugins/OStatus/lib => lib}/discovery.php (100%) diff --git a/plugins/OStatus/lib/discovery.php b/lib/discovery.php similarity index 100% rename from plugins/OStatus/lib/discovery.php rename to lib/discovery.php From 5dfc751d1429b8ec6fccfff0f53a4652abcf7daa Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 2 Jan 2011 10:49:44 -0800 Subject: [PATCH 02/11] move linkheader.php to core --- lib/linkheader.php | 66 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 lib/linkheader.php diff --git a/lib/linkheader.php b/lib/linkheader.php new file mode 100644 index 0000000000..efa3f65ff3 --- /dev/null +++ b/lib/linkheader.php @@ -0,0 +1,66 @@ +]+>/', $str, $uri_reference); + //if (empty($uri_reference)) return; + + $this->href = trim($uri_reference[0], '<>'); + $this->rel = array(); + $this->type = null; + + // remove uri-reference from header + $str = substr($str, strlen($uri_reference[0])); + + // parse link-params + $params = explode(';', $str); + + foreach ($params as $param) { + if (empty($param)) continue; + list($param_name, $param_value) = explode('=', $param, 2); + $param_name = trim($param_name); + $param_value = preg_replace('(^"|"$)', '', trim($param_value)); + + // for now we only care about 'rel' and 'type' link params + // TODO do something with the other links-params + switch ($param_name) { + case 'rel': + $this->rel = trim($param_value); + break; + + case 'type': + $this->type = trim($param_value); + } + } + } + + static function getLink($response, $rel=null, $type=null) + { + $headers = $response->getHeader('Link'); + if ($headers) { + // Can get an array or string, so try to simplify the path + if (!is_array($headers)) { + $headers = array($headers); + } + + foreach ($headers as $header) { + $lh = new LinkHeader($header); + + if ((is_null($rel) || $lh->rel == $rel) && + (is_null($type) || $lh->type == $type)) { + return $lh->href; + } + } + } + return null; + } +} From fda79a38ac1db57c652b2d0f8f9d0cebc8126cd1 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 2 Jan 2011 11:01:01 -0800 Subject: [PATCH 03/11] move linkheader.php to core --- plugins/OStatus/lib/linkheader.php | 66 ------------------------------ 1 file changed, 66 deletions(-) delete mode 100644 plugins/OStatus/lib/linkheader.php diff --git a/plugins/OStatus/lib/linkheader.php b/plugins/OStatus/lib/linkheader.php deleted file mode 100644 index efa3f65ff3..0000000000 --- a/plugins/OStatus/lib/linkheader.php +++ /dev/null @@ -1,66 +0,0 @@ -]+>/', $str, $uri_reference); - //if (empty($uri_reference)) return; - - $this->href = trim($uri_reference[0], '<>'); - $this->rel = array(); - $this->type = null; - - // remove uri-reference from header - $str = substr($str, strlen($uri_reference[0])); - - // parse link-params - $params = explode(';', $str); - - foreach ($params as $param) { - if (empty($param)) continue; - list($param_name, $param_value) = explode('=', $param, 2); - $param_name = trim($param_name); - $param_value = preg_replace('(^"|"$)', '', trim($param_value)); - - // for now we only care about 'rel' and 'type' link params - // TODO do something with the other links-params - switch ($param_name) { - case 'rel': - $this->rel = trim($param_value); - break; - - case 'type': - $this->type = trim($param_value); - } - } - } - - static function getLink($response, $rel=null, $type=null) - { - $headers = $response->getHeader('Link'); - if ($headers) { - // Can get an array or string, so try to simplify the path - if (!is_array($headers)) { - $headers = array($headers); - } - - foreach ($headers as $header) { - $lh = new LinkHeader($header); - - if ((is_null($rel) || $lh->rel == $rel) && - (is_null($type) || $lh->type == $type)) { - return $lh->href; - } - } - } - return null; - } -} From a6867422a38409665fb9d31539469798f298fb00 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 2 Jan 2011 11:01:28 -0800 Subject: [PATCH 04/11] PHPCS discovery.php --- lib/discovery.php | 219 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 205 insertions(+), 14 deletions(-) diff --git a/lib/discovery.php b/lib/discovery.php index 905ece2ca5..c66790c1d9 100644 --- a/lib/discovery.php +++ b/lib/discovery.php @@ -3,7 +3,7 @@ * StatusNet - the distributed open-source microblogging tool * Copyright (C) 2010, StatusNet, Inc. * - * A sample module to show best practices for StatusNet plugins + * Use Hammer discovery stack to find out interesting things about an URI * * PHP version 5 * @@ -20,6 +20,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * + * @category Discovery * @package StatusNet * @author James Walker * @copyright 2010 StatusNet, Inc. @@ -31,18 +32,33 @@ * This class implements LRDD-based service discovery based on the "Hammer Draft" * (including webfinger) * - * @see http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf + * @category Discovery + * @package StatusNet + * @author James Walker + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + * + * @see http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf */ + class Discovery { - - const LRDD_REL = 'lrdd'; + const LRDD_REL = 'lrdd'; const PROFILEPAGE = 'http://webfinger.net/rel/profile-page'; const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from'; - const HCARD = 'http://microformats.org/profile/hcard'; + const HCARD = 'http://microformats.org/profile/hcard'; public $methods = array(); + /** + * Constructor for a discovery object + * + * Registers different discovery methods. + * + * @return Discovery this + */ + public function __construct() { $this->registerMethod('Discovery_LRDD_Host_Meta'); @@ -50,6 +66,14 @@ class Discovery $this->registerMethod('Discovery_LRDD_Link_HTML'); } + /** + * Register a discovery class + * + * @param string $class Class name + * + * @return void + */ + public function registerMethod($class) { $this->methods[] = $class; @@ -58,7 +82,12 @@ class Discovery /** * Given a "user id" make sure it's normalized to either a webfinger * acct: uri or a profile HTTP URL. + * + * @param string $user_id User ID to normalize + * + * @return string normalized acct: or http(s)?: URI */ + public static function normalize($user_id) { if (substr($user_id, 0, 5) == 'http:' || @@ -67,13 +96,23 @@ class Discovery return $user_id; } - if (strpos($user_id, '@') !== FALSE) { + if (strpos($user_id, '@') !== false) { return 'acct:' . $user_id; } return 'http://' . $user_id; } + /** + * Determine if a string is a Webfinger ID + * + * Webfinger IDs look like foo@example.com or acct:foo@example.com + * + * @param string $user_id ID to check + * + * @return boolean true if $user_id is a Webfinger, else false + */ + public static function isWebfinger($user_id) { $uri = Discovery::normalize($user_id); @@ -82,8 +121,13 @@ class Discovery } /** - * This implements the actual lookup procedure + * Given a user ID, return the first available XRD + * + * @param string $id User ID URI + * + * @return XRD XRD object for the user */ + public function lookup($id) { // Normalize the incoming $id to make sure we have a uri @@ -107,10 +151,20 @@ class Discovery } // TRANS: Exception. - throw new Exception(sprintf(_m('Unable to find services for %s.'),$id)); + throw new Exception(sprintf(_('Unable to find services for %s.'), $id)); } - public static function getService($links, $service) { + /** + * Given an array of links, returns the matching service + * + * @param array $links Links to check + * @param string $service Service to find + * + * @return array $link assoc array representing the link + */ + + public static function getService($links, $service) + { if (!is_array($links)) { return false; } @@ -122,6 +176,17 @@ class Discovery } } + /** + * Apply a template using an ID + * + * Replaces {uri} in template string with the ID given. + * + * @param string $template Template to match + * @param string $id User ID to replace with + * + * @return string replaced values + */ + public static function applyTemplate($template, $id) { $template = str_replace('{uri}', urlencode($id), $template); @@ -129,10 +194,18 @@ class Discovery return $template; } + /** + * Fetch an XRD file and parse + * + * @param string $url URL of the XRD + * + * @return XRD object representing the XRD file + */ + public static function fetchXrd($url) { try { - $client = new HTTPClient(); + $client = new HTTPClient(); $response = $client->get($url); } catch (HTTP_Request2_Exception $e) { return false; @@ -146,13 +219,60 @@ class Discovery } } +/** + * Abstract interface for discovery + * + * Objects that implement this interface can retrieve an array of + * XRD links for the URI. + * + * @category Discovery + * @package StatusNet + * @author James Walker + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + interface Discovery_LRDD { + /** + * Discover interesting info about the URI + * + * @param string $uri URI to inquire about + * + * @return array Links in the XRD file + */ + public function discover($uri); } +/** + * Implementation of discovery using host-meta file + * + * Discovers XRD file for a user by going to the organization's + * host-meta file and trying to find a template for LRDD. + * + * @category Discovery + * @package StatusNet + * @author James Walker + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class Discovery_LRDD_Host_Meta implements Discovery_LRDD { + /** + * Discovery core method + * + * For Webfinger and HTTP URIs, fetch the host-meta file + * and look for LRDD templates + * + * @param string $uri URI to inquire about + * + * @return array Links in the XRD file + */ + public function discover($uri) { if (Discovery::isWebfinger($uri)) { @@ -176,12 +296,38 @@ class Discovery_LRDD_Host_Meta implements Discovery_LRDD } } +/** + * Implementation of discovery using HTTP Link header + * + * Discovers XRD file for a user by fetching the URL and reading any + * Link: headers in the HTTP response. + * + * @category Discovery + * @package StatusNet + * @author James Walker + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class Discovery_LRDD_Link_Header implements Discovery_LRDD { + /** + * Discovery core method + * + * For HTTP IDs fetch the URL and look for Link headers. + * + * @param string $uri URI to inquire about + * + * @return array Links in the XRD file + * + * @todo fail out of Webfinger URIs faster + */ + public function discover($uri) { try { - $client = new HTTPClient(); + $client = new HTTPClient(); $response = $client->get($uri); } catch (HTTP_Request2_Exception $e) { return false; @@ -199,6 +345,14 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD return array(Discovery_LRDD_Link_Header::parseHeader($link_header)); } + /** + * Given a string or array of headers, returns XRD-like assoc array + * + * @param string|array $header string or array of strings for headers + * + * @return array Link header in XRD-like format + */ + protected static function parseHeader($header) { $lh = new LinkHeader($header); @@ -209,12 +363,39 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD } } +/** + * Implementation of discovery using HTML element + * + * Discovers XRD file for a user by fetching the URL and reading any + * elements in the HTML response. + * + * @category Discovery + * @package StatusNet + * @author James Walker + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class Discovery_LRDD_Link_HTML implements Discovery_LRDD { + /** + * Discovery core method + * + * For HTTP IDs, fetch the URL and look for elements + * in the HTML response. + * + * @param string $uri URI to inquire about + * + * @return array Links in XRD-ish assoc array + * + * @todo fail out of Webfinger URIs faster + */ + public function discover($uri) { try { - $client = new HTTPClient(); + $client = new HTTPClient(); $response = $client->get($uri); } catch (HTTP_Request2_Exception $e) { return false; @@ -227,6 +408,16 @@ class Discovery_LRDD_Link_HTML implements Discovery_LRDD return Discovery_LRDD_Link_HTML::parse($response->getBody()); } + /** + * Parse HTML and return elements + * + * Given an HTML string, scans the string for elements + * + * @param string $html HTML to scan + * + * @return array array of associative arrays in XRD-ish format + */ + public function parse($html) { $links = array(); @@ -237,8 +428,8 @@ class Discovery_LRDD_Link_HTML implements Discovery_LRDD preg_match_all('/]*>/i', $head_html, $link_matches); foreach ($link_matches[0] as $link_html) { - $link_url = null; - $link_rel = null; + $link_url = null; + $link_rel = null; $link_type = null; preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches); From 1ea8ca813bf277b05c5736799b3be43ebb91ab30 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 2 Jan 2011 11:08:32 -0800 Subject: [PATCH 05/11] PHPCS linkheader.php --- lib/linkheader.php | 74 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/lib/linkheader.php b/lib/linkheader.php index efa3f65ff3..a08fb67116 100644 --- a/lib/linkheader.php +++ b/lib/linkheader.php @@ -1,6 +1,51 @@ . + * + * @category Discovery + * @package StatusNet + * @author James Walker + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Class to represent Link: headers in an HTTP response + * + * Since these are a fairly important part of Hammer-stack discovery, they're + * reified and implemented here. + * + * @category Discovery + * @package StatusNet + * @author James Walker + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + * + * @see Discovery */ class LinkHeader @@ -9,13 +54,21 @@ class LinkHeader var $rel; var $type; + /** + * Initialize from a string + * + * @param string $str Link: header value + * + * @return LinkHeader self + */ + function __construct($str) { preg_match('/^<[^>]+>/', $str, $uri_reference); //if (empty($uri_reference)) return; $this->href = trim($uri_reference[0], '<>'); - $this->rel = array(); + $this->rel = array(); $this->type = null; // remove uri-reference from header @@ -25,9 +78,12 @@ class LinkHeader $params = explode(';', $str); foreach ($params as $param) { - if (empty($param)) continue; + if (empty($param)) { + continue; + } list($param_name, $param_value) = explode('=', $param, 2); - $param_name = trim($param_name); + + $param_name = trim($param_name); $param_value = preg_replace('(^"|"$)', '', trim($param_value)); // for now we only care about 'rel' and 'type' link params @@ -43,6 +99,16 @@ class LinkHeader } } + /** + * Given an HTTP response, return the requested Link: header + * + * @param HTTP_Request2_Response $response response to check + * @param string $rel relationship to look for + * @param string $type media type to look for + * + * @return LinkHeader discovered header, or null on failure + */ + static function getLink($response, $rel=null, $type=null) { $headers = $response->getHeader('Link'); From 199704458943246108f6aded720337b6ecdde0ab Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 2 Jan 2011 11:10:46 -0800 Subject: [PATCH 06/11] execution protection on discovery.php --- lib/discovery.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/discovery.php b/lib/discovery.php index c66790c1d9..d67ec94f00 100644 --- a/lib/discovery.php +++ b/lib/discovery.php @@ -28,6 +28,10 @@ * @link http://status.net/ */ +if (!defined('STATUSNET')) { + exit(1); +} + /** * This class implements LRDD-based service discovery based on the "Hammer Draft" * (including webfinger) From 810304159e2ba5a7b68aef45480ba822ec3fce2b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 2 Jan 2011 15:21:56 -0800 Subject: [PATCH 07/11] let callers pass in an XMLOutputter to output to --- lib/activity.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/activity.php b/lib/activity.php index 8d7ae1540b..b77d53427c 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -322,6 +322,7 @@ class Activity * * @return DOMElement Atom entry */ + function toAtomEntry() { return null; @@ -330,7 +331,12 @@ class Activity function asString($namespace=false, $author=true, $source=false) { $xs = new XMLStringer(true); + $this->outputTo($xs, $namespace, $author, $source); + return $xs->getString(); + } + function outputTo($xs, $namespace=false, $author=true, $source=false) + { if ($namespace) { $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', @@ -518,9 +524,7 @@ class Activity $xs->elementEnd('entry'); - $str = $xs->getString(); - - return $str; + return; } private function _child($element, $tag, $namespace=self::SPEC) From db899a07a5f10801bc0c7c1cbb4f4574df1e15f1 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 2 Jan 2011 15:22:12 -0800 Subject: [PATCH 08/11] preserve activities in object --- lib/useractivitystream.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/useractivitystream.php b/lib/useractivitystream.php index 0fc315e26e..7d9b02ded8 100644 --- a/lib/useractivitystream.php +++ b/lib/useractivitystream.php @@ -28,6 +28,8 @@ class UserActivityStream extends AtomUserNoticeFeed { + public $activities = array(); + function __construct($user, $indent = true) { parent::__construct($user, null, $indent); @@ -45,10 +47,15 @@ class UserActivityStream extends AtomUserNoticeFeed usort($objs, 'UserActivityStream::compareObject'); foreach ($objs as $obj) { - $act = $obj->asActivity(); + $this->activities[] = $obj->asActivity(); + } + } + + function renderEntries() + { + foreach ($this->activities as $act) { // Only show the author sub-element if it's different from default user - $str = $act->asString(false, ($act->actor->id != $this->user->uri)); - $this->addEntryRaw($str); + $act->outputTo($this, false, ($act->actor->id != $this->user->uri)); } } From f51db8eb0d0eda837e0b309729266201fffc6b44 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 3 Jan 2011 07:40:57 -0800 Subject: [PATCH 09/11] Add the Atom username to the XRD output --- lib/xrdaction.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/xrdaction.php b/lib/xrdaction.php index 855ed1ea89..b59e0f78a4 100644 --- a/lib/xrdaction.php +++ b/lib/xrdaction.php @@ -99,7 +99,9 @@ class XrdAction extends Action $xrd->links[] = array('rel' => 'http://apinamespace.org/atom', 'type' => 'application/atomsvc+xml', - 'href' => common_local_url('ApiAtomService', array('id' => $nick))); + 'href' => common_local_url('ApiAtomService', array('id' => $nick)), + 'property' => array(array('type' => 'http://apinamespace.org/atom/username', + 'value' => $nick))); if (common_config('site', 'fancy')) { $apiRoot = common_path('api/', true); From ef1fdd595f0bee0847be187733eea9d7b705b328 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 3 Jan 2011 07:41:13 -0800 Subject: [PATCH 10/11] Parse properties of links in XRD files --- lib/xrd.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/xrd.php b/lib/xrd.php index 9c6d9f3ab7..40372b9d7a 100644 --- a/lib/xrd.php +++ b/lib/xrd.php @@ -173,6 +173,13 @@ class XRD switch($node->tagName) { case 'Title': $link['title'][] = $node->nodeValue; + break; + case 'Property': + $link['property'][] = array('type' => $node->getAttribute('type'), + 'value' => $node->nodeValue); + break; + default: + common_log(LOG_NOTICE, "Unexpected tag name {$node->tagName} found in XRD file."); } } } From c937d3a8437e23cb5c1c2de3bc501bec8b725802 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 3 Jan 2011 14:59:28 -0800 Subject: [PATCH 11/11] first example of moving a user --- scripts/moveuser.php | 296 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 scripts/moveuser.php diff --git a/scripts/moveuser.php b/scripts/moveuser.php new file mode 100644 index 0000000000..feaca15103 --- /dev/null +++ b/scripts/moveuser.php @@ -0,0 +1,296 @@ +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); + +$shortoptions = 'i:n:r:w:'; +$longoptions = array('id=', 'nickname=', 'remote=', 'password='); + +$helptext = <<svcDocUrl = $svcDocUrl; + $this->username = $username; + $this->password = $password; + + $this->_parseSvcDoc(); + } + + private function _parseSvcDoc() + { + $client = new HTTPClient(); + $response = $client->get($this->svcDocUrl); + + if ($response->getStatus() != 200) { + throw new Exception("Can't get {$this->svcDocUrl}; response status " . $response->getStatus()); + } + + $xml = $response->getBody(); + + $dom = new DOMDocument(); + + // We don't want to bother with white spaces + $dom->preserveWhiteSpace = false; + + // Don't spew XML warnings to output + $old = error_reporting(); + error_reporting($old & ~E_WARNING); + $ok = $dom->loadXML($xml); + error_reporting($old); + + $path = new DOMXPath($dom); + + $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); + $path->registerNamespace('app', 'http://www.w3.org/2007/app'); + $path->registerNamespace('activity', 'http://activitystrea.ms/spec/1.0/'); + + $collections = $path->query('//app:collection'); + + for ($i = 0; $i < $collections->length; $i++) { + $collection = $collections->item($i); + $url = $collection->getAttribute('href'); + $takesEntries = false; + $accepts = $path->query('app:accept', $collection); + for ($j = 0; $j < $accepts->length; $j++) { + $accept = $accepts->item($j); + $acceptValue = $accept->nodeValue; + if (preg_match('#application/atom\+xml(;\s*type=entry)?#', $acceptValue)) { + $takesEntries = true; + break; + } + } + if (!$takesEntries) { + continue; + } + $verbs = $path->query('activity:verb', $collection); + if ($verbs->length == 0) { + $this->_addCollection(ActivityVerb::POST, $url); + } else { + for ($k = 0; $k < $verbs->length; $k++) { + $verb = $verbs->item($k); + $this->_addCollection($verb->nodeValue, $url); + } + } + } + } + + private function _addCollection($verb, $url) + { + if (array_key_exists($verb, $this->collections)) { + $this->collections[$verb][] = $url; + } else { + $this->collections[$verb] = array($url); + } + return; + } + + function postActivity($activity) + { + if (!array_key_exists($activity->verb, $this->collections)) { + throw new Exception("No collection for verb {$activity->verb}"); + } else { + if (count($this->collections[$activity->verb]) > 1) { + common_log(LOG_NOTICE, "More than one collection for verb {$activity->verb}"); + } + $this->postToCollection($this->collections[$activity->verb][0], $activity); + } + } + + function postToCollection($url, $activity) + { + $client = new HTTPClient($url); + + $client->setMethod('POST'); + $client->setAuth($this->username, $this->password); + $client->setHeader('Content-Type', 'application/atom+xml;type=entry'); + $client->setBody($activity->asString(true, true, true)); + + $response = $client->send(); + } +} + +function getServiceDocument($remote) +{ + $discovery = new Discovery(); + + $xrd = $discovery->lookup($remote); + + if (empty($xrd)) { + throw new Exception("Can't find XRD for $remote"); + } + + $svcDocUrl = null; + $username = null; + + foreach ($xrd->links as $link) { + if ($link['rel'] == 'http://apinamespace.org/atom' && + $link['type'] == 'application/atomsvc+xml') { + $svcDocUrl = $link['href']; + if (!empty($link['property'])) { + foreach ($link['property'] as $property) { + if ($property['type'] == 'http://apinamespace.org/atom/username') { + $username = $property['value']; + break; + } + } + } + break; + } + } + + if (empty($svcDocUrl)) { + throw new Exception("No AtomPub API service for $remote."); + } + + return array($svcDocUrl, $username); +} + +class AccountMover +{ + private $_user = null; + private $_profile = null; + private $_remote = null; + private $_sink = null; + + function __construct($user, $remote, $password) + { + $this->_user = $user; + $this->_profile = $user->getProfile(); + + $oprofile = Ostatus_profile::ensureProfileURI($remote); + + if (empty($oprofile)) { + throw new Exception("Can't locate account {$remote}"); + } + + $this->_remote = $oprofile->localProfile(); + + list($svcDocUrl, $username) = getServiceDocument($remote); + + $this->_sink = new ActivitySink($svcDocUrl, $username, $password); + } + + function move() + { + $stream = new UserActivityStream($this->_user); + + $acts = array_reverse($stream->activities); + + // Reverse activities to run in correct chron order + + foreach ($acts as $act) { + $this->_moveActivity($act); + } + } + + private function _moveActivity($act) + { + switch ($act->verb) { + case ActivityVerb::FAVORITE: + // push it, then delete local + $this->_sink->postActivity($act); + $notice = Notice::staticGet('uri', $act->objects[0]->id); + if (!empty($notice)) { + $fave = Fave::pkeyGet(array('user_id' => $this->_user->id, + 'notice_id' => $notice->id)); + $fave->delete(); + } + break; + case ActivityVerb::POST: + // XXX: send a reshare, not a post + common_log(LOG_INFO, "Pushing notice {$act->objects[0]->id} to {$this->_remote->getURI()}"); + $this->_sink->postActivity($act); + $notice = Notice::staticGet('uri', $act->objects[0]->id); + if (!empty($notice)) { + $notice->delete(); + } + break; + case ActivityVerb::JOIN: + $this->_sink->postActivity($act); + $group = User_group::staticGet('uri', $act->objects[0]->id); + if (!empty($group)) { + Group_member::leave($group->id, $this->_user->id); + } + break; + case ActivityVerb::FOLLOW: + if ($act->actor->id == $this->_user->uri) { + $this->_sink->postActivity($act); + $other = Profile::fromURI($act->objects[0]->id); + if (!empty($other)) { + Subscription::cancel($this->_profile, $other); + } + } else { + $otherUser = User::staticGet('uri', $act->actor->id); + if (!empty($otherUser)) { + $otherProfile = $otherUser->getProfile(); + Subscription::start($otherProfile, $this->_remote); + Subscription::cancel($otherProfile, $this->_user->getProfile()); + } else { + // It's a remote subscription. Do something here! + } + } + break; + } + } +} + +try { + + $user = getUser(); + + $remote = get_option_value('r', 'remote'); + + if (empty($remote)) { + show_help(); + exit(1); + } + + $password = get_option_value('w', 'password'); + + $mover = new AccountMover($user, $remote, $password); + + $mover->move(); + +} catch (Exception $e) { + print $e->getMessage()."\n"; + exit(1); +}