3 * StatusNet - the distributed open-source microblogging tool
4 * Copyright (C) 2010 StatusNet, Inc.
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
16 * You should have received a copy of the GNU Affero General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
22 $shortoptions = 'i:n:r:w:';
23 $longoptions = array('id=', 'nickname=', 'remote=', 'password=');
25 $helptext = <<<END_OF_MOVEUSER_HELP
26 moveuser.php [options]
27 Move a local user to a remote account.
29 -i --id ID of user to move
30 -n --nickname nickname of the user to move
31 -r --remote Full ID of remote users
32 -w --password Password of remote user
34 Remote user identity must be a Webfinger (nickname@example.com) or
35 an HTTP or HTTPS URL (http://example.com/social/site/user/nickname).
39 require_once INSTALLDIR.'/scripts/commandline.inc';
40 require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
44 protected $svcDocUrl = null;
45 protected $username = null;
46 protected $password = null;
47 protected $collections = array();
49 function __construct($svcDocUrl, $username, $password)
51 $this->svcDocUrl = $svcDocUrl;
52 $this->username = $username;
53 $this->password = $password;
55 $this->_parseSvcDoc();
58 private function _parseSvcDoc()
60 $client = new HTTPClient();
61 $response = $client->get($this->svcDocUrl);
63 if ($response->getStatus() != 200) {
64 throw new Exception("Can't get {$this->svcDocUrl}; response status " . $response->getStatus());
67 $xml = $response->getBody();
69 $dom = new DOMDocument();
71 // We don't want to bother with white spaces
72 $dom->preserveWhiteSpace = false;
74 // Don't spew XML warnings to output
75 $old = error_reporting();
76 error_reporting($old & ~E_WARNING);
77 $ok = $dom->loadXML($xml);
78 error_reporting($old);
80 $path = new DOMXPath($dom);
82 $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
83 $path->registerNamespace('app', 'http://www.w3.org/2007/app');
84 $path->registerNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
86 $collections = $path->query('//app:collection');
88 for ($i = 0; $i < $collections->length; $i++) {
89 $collection = $collections->item($i);
90 $url = $collection->getAttribute('href');
91 $takesEntries = false;
92 $accepts = $path->query('app:accept', $collection);
93 for ($j = 0; $j < $accepts->length; $j++) {
94 $accept = $accepts->item($j);
95 $acceptValue = $accept->nodeValue;
96 if (preg_match('#application/atom\+xml(;\s*type=entry)?#', $acceptValue)) {
101 if (!$takesEntries) {
104 $verbs = $path->query('activity:verb', $collection);
105 if ($verbs->length == 0) {
106 $this->_addCollection(ActivityVerb::POST, $url);
108 for ($k = 0; $k < $verbs->length; $k++) {
109 $verb = $verbs->item($k);
110 $this->_addCollection($verb->nodeValue, $url);
116 private function _addCollection($verb, $url)
118 if (array_key_exists($verb, $this->collections)) {
119 $this->collections[$verb][] = $url;
121 $this->collections[$verb] = array($url);
126 function postActivity($activity)
128 if (!array_key_exists($activity->verb, $this->collections)) {
129 throw new Exception("No collection for verb {$activity->verb}");
131 if (count($this->collections[$activity->verb]) > 1) {
132 common_log(LOG_NOTICE, "More than one collection for verb {$activity->verb}");
134 $this->postToCollection($this->collections[$activity->verb][0], $activity);
138 function postToCollection($url, $activity)
140 $client = new HTTPClient($url);
142 $client->setMethod('POST');
143 $client->setAuth($this->username, $this->password);
144 $client->setHeader('Content-Type', 'application/atom+xml;type=entry');
145 $client->setBody($activity->asString(true, true, true));
147 $response = $client->send();
151 function getServiceDocument($remote)
153 $discovery = new Discovery();
155 $xrd = $discovery->lookup($remote);
158 throw new Exception("Can't find XRD for $remote");
164 foreach ($xrd->links as $link) {
165 if ($link['rel'] == 'http://apinamespace.org/atom' &&
166 $link['type'] == 'application/atomsvc+xml') {
167 $svcDocUrl = $link['href'];
168 if (!empty($link['property'])) {
169 foreach ($link['property'] as $property) {
170 if ($property['type'] == 'http://apinamespace.org/atom/username') {
171 $username = $property['value'];
180 if (empty($svcDocUrl)) {
181 throw new Exception("No AtomPub API service for $remote.");
184 return array($svcDocUrl, $username);
189 private $_user = null;
190 private $_profile = null;
191 private $_remote = null;
192 private $_sink = null;
194 function __construct($user, $remote, $password)
196 $this->_user = $user;
197 $this->_profile = $user->getProfile();
199 $oprofile = Ostatus_profile::ensureProfileURI($remote);
201 if (empty($oprofile)) {
202 throw new Exception("Can't locate account {$remote}");
205 $this->_remote = $oprofile->localProfile();
207 list($svcDocUrl, $username) = getServiceDocument($remote);
209 $this->_sink = new ActivitySink($svcDocUrl, $username, $password);
214 $stream = new UserActivityStream($this->_user);
216 $acts = array_reverse($stream->activities);
218 // Reverse activities to run in correct chron order
220 foreach ($acts as $act) {
221 $this->_moveActivity($act);
225 private function _moveActivity($act)
227 switch ($act->verb) {
228 case ActivityVerb::FAVORITE:
229 // push it, then delete local
230 $this->_sink->postActivity($act);
231 $notice = Notice::staticGet('uri', $act->objects[0]->id);
232 if (!empty($notice)) {
233 $fave = Fave::pkeyGet(array('user_id' => $this->_user->id,
234 'notice_id' => $notice->id));
238 case ActivityVerb::POST:
239 // XXX: send a reshare, not a post
240 common_log(LOG_INFO, "Pushing notice {$act->objects[0]->id} to {$this->_remote->getURI()}");
241 $this->_sink->postActivity($act);
242 $notice = Notice::staticGet('uri', $act->objects[0]->id);
243 if (!empty($notice)) {
247 case ActivityVerb::JOIN:
248 $this->_sink->postActivity($act);
249 $group = User_group::staticGet('uri', $act->objects[0]->id);
250 if (!empty($group)) {
251 Group_member::leave($group->id, $this->_user->id);
254 case ActivityVerb::FOLLOW:
255 if ($act->actor->id == $this->_user->uri) {
256 $this->_sink->postActivity($act);
257 $other = Profile::fromURI($act->objects[0]->id);
258 if (!empty($other)) {
259 Subscription::cancel($this->_profile, $other);
262 $otherUser = User::staticGet('uri', $act->actor->id);
263 if (!empty($otherUser)) {
264 $otherProfile = $otherUser->getProfile();
265 Subscription::start($otherProfile, $this->_remote);
266 Subscription::cancel($otherProfile, $this->_user->getProfile());
268 // It's a remote subscription. Do something here!
280 $remote = get_option_value('r', 'remote');
282 if (empty($remote)) {
287 $password = get_option_value('w', 'password');
289 $mover = new AccountMover($user, $remote, $password);
293 } catch (Exception $e) {
294 print $e->getMessage()."\n";