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/>.
21 * @package OStatusPlugin
22 * @author James Walker <james@status.net>
25 if (!defined('GNUSOCIAL')) { exit(1); }
27 class SalmonAction extends Action
29 protected $needPost = true;
31 protected $oprofile = null; // Ostatus_profile of the actor
32 protected $actor = null; // Profile object of the actor
38 protected function prepare(array $args=array())
40 GNUsocial::setApi(true); // Send smaller error pages
42 parent::prepare($args);
44 if (!isset($_SERVER['CONTENT_TYPE'])) {
45 // TRANS: Client error. Do not translate "Content-type"
46 $this->clientError(_m('Salmon requires a Content-type header.'));
49 switch ($_SERVER['CONTENT_TYPE']) {
50 case 'application/magic-envelope+xml':
51 $envxml = file_get_contents('php://input');
53 case 'application/x-www-form-urlencoded':
54 $envxml = Magicsig::base64_url_decode($this->trimmed('xml'));
57 // TRANS: Client error. Do not translate the quoted "application/[type]" strings.
58 $this->clientError(_m('Salmon requires "application/magic-envelope+xml". For Diaspora we also accept "application/x-www-form-urlencoded" with an "xml" parameter.', 415));
63 throw new ClientException('No magic envelope supplied in POST.');
65 $magic_env = new MagicEnvelope($envxml); // parse incoming XML as a MagicEnvelope
67 $entry = $magic_env->getPayload(); // Not cryptographically verified yet!
68 $this->activity = new Activity($entry->documentElement);
69 if (empty($this->activity->actor->id)) {
70 common_log(LOG_ERR, "broken actor: " . var_export($this->activity->actor->id, true));
71 common_log(LOG_ERR, "activity with no actor: " . var_export($this->activity, true));
73 throw new Exception(_m('Received a salmon slap from unidentified actor.'));
75 // ensureProfiles sets $this->actor and $this->oprofile
76 $this->ensureProfiles();
77 } catch (Exception $e) {
78 common_debug('Salmon envelope parsing failed with: '.$e->getMessage());
79 $this->clientError($e->getMessage());
82 // Cryptographic verification test, throws exception on failure
83 $magic_env->verify($this->actor);
89 * Check the posted activity type and break out to appropriate processing.
92 protected function handle()
96 common_log(LOG_DEBUG, "Got a " . $this->activity->verb);
98 if (Event::handle('StartHandleSalmonTarget', array($this->activity, $this->target)) &&
99 Event::handle('StartHandleSalmon', array($this->activity))) {
100 switch ($this->activity->verb) {
101 case ActivityVerb::POST:
104 case ActivityVerb::SHARE:
105 $this->handleShare();
107 case ActivityVerb::FOLLOW:
108 case ActivityVerb::FRIEND:
109 $this->handleFollow();
111 case ActivityVerb::UNFOLLOW:
112 $this->handleUnfollow();
114 case ActivityVerb::JOIN:
117 case ActivityVerb::LEAVE:
118 $this->handleLeave();
120 case ActivityVerb::TAG:
123 case ActivityVerb::UNTAG:
124 $this->handleUntag();
126 case ActivityVerb::UPDATE_PROFILE:
127 $this->handleUpdateProfile();
130 // TRANS: Client exception.
131 throw new ClientException(_m('Unrecognized activity type.'));
133 Event::handle('EndHandleSalmon', array($this->activity));
134 Event::handle('EndHandleSalmonTarget', array($this->activity, $this->target));
136 } catch (AlreadyFulfilledException $e) {
137 // The action's results are already fulfilled. Maybe it was a
138 // duplicate? Maybe someone's database is out of sync?
139 // Let's just accept it and move on.
140 common_log(LOG_INFO, 'Salmon slap carried an event which had already been fulfilled.');
144 function handlePost()
146 // TRANS: Client exception.
147 throw new ClientException(_m('This target does not understand posts.'));
150 function handleFollow()
152 // TRANS: Client exception.
153 throw new ClientException(_m('This target does not understand follows.'));
156 function handleUnfollow()
158 // TRANS: Client exception.
159 throw new ClientException(_m('This target does not understand unfollows.'));
162 function handleShare()
164 // TRANS: Client exception.
165 throw new ClientException(_m('This target does not understand share events.'));
168 function handleJoin()
170 // TRANS: Client exception.
171 throw new ClientException(_m('This target does not understand joins.'));
174 function handleLeave()
176 // TRANS: Client exception.
177 throw new ClientException(_m('This target does not understand leave events.'));
182 // TRANS: Client exception.
183 throw new ClientException(_m('This target does not understand list events.'));
186 function handleUntag()
188 // TRANS: Client exception.
189 throw new ClientException(_m('This target does not understand unlist events.'));
193 * Remote user sent us an update to their profile.
194 * If we already know them, accept the updates.
196 function handleUpdateProfile()
198 $oprofile = Ostatus_profile::getActorProfile($this->activity);
199 if ($oprofile instanceof Ostatus_profile) {
200 common_log(LOG_INFO, "Got a profile-update ping from $oprofile->uri");
201 $oprofile->updateFromActivityObject($this->activity->actor);
203 common_log(LOG_INFO, "Ignoring profile-update ping from unknown " . $this->activity->actor->id);
207 function ensureProfiles()
210 $this->oprofile = Ostatus_profile::getActorProfile($this->activity);
211 if (!$this->oprofile instanceof Ostatus_profile) {
212 throw new UnknownUriException($this->activity->actor->id);
214 } catch (UnknownUriException $e) {
215 // Apparently we didn't find the Profile object based on our URI,
216 // so OStatus doesn't have it with this URI in ostatus_profile.
217 // Try to look it up again, remote side may have changed from http to https
218 // or maybe publish an acct: URI now instead of an http: URL.
221 // 1. Check the newly received URI. Who does it say it is?
222 // 2. Compare these alleged identities to our local database.
223 // 3. If we found any locally stored identities, ask it about its aliases.
224 // 4. Do any of the aliases from our known identity match the recently introduced one?
226 // Example: We have stored http://example.com/user/1 but this URI says https://example.com/user/1
227 common_debug('No local Profile object found for a magicsigned activity author URI: '.$e->object_uri);
228 $disco = new Discovery();
229 $xrd = $disco->lookup($e->object_uri);
230 // Step 1: We got a bunch of discovery data for https://example.com/user/1 which includes
231 // aliases https://example.com/user and hopefully our original http://example.com/user/1 too
232 $all_ids = array_merge(array($xrd->subject), $xrd->aliases);
234 if (!in_array($e->object_uri, $all_ids)) {
235 common_debug('The activity author URI we got was not listed itself when doing discovery on it.');
239 // Go through each reported alias from lookup to see if we know this already
240 foreach ($all_ids as $aliased_uri) {
241 $oprofile = Ostatus_profile::getKV('uri', $aliased_uri);
242 if (!$oprofile instanceof Ostatus_profile) {
243 continue; // unknown locally, check the next alias
245 // Step 2: We found the alleged http://example.com/user/1 URI in our local database,
246 // but this can't be trusted yet because anyone can publish any alias.
247 common_debug('Found a local Ostatus_profile for "'.$e->object_uri.'" with this URI: '.$aliased_uri);
249 // We found an existing OStatus profile, but is it really the same? Do a callback to the URI's origin
250 // Step 3: lookup our previously known http://example.com/user/1 webfinger etc.
251 $xrd = $disco->lookup($oprofile->getUri()); // getUri returns ->uri, which we filtered on earlier
252 $doublecheck_aliases = array_merge(array($xrd->subject), $xrd->aliases);
253 common_debug('Trying to match known "'.$aliased_uri.'" against its returned aliases: '.implode(' ', $doublecheck_aliases));
254 // if we find our original URI here, it is a legitimate alias
255 // Step 4: Is the newly introduced https://example.com/user/1 URI in the list of aliases
256 // presented by http://example.com/user/1 (i.e. do they both say they are the same identity?)
257 if (in_array($e->object_uri, $doublecheck_aliases)) {
258 $oprofile->updateUriKeys($e->object_uri, DiscoveryHints::fromXRD($xrd));
259 $this->oprofile = $oprofile;
260 break; // don't iterate through aliases anymore
264 // We might end up here after $all_ids is iterated through without a $this->oprofile value,
265 if (!$this->oprofile instanceof Ostatus_profile) {
266 common_debug("We do not have a local profile to connect to this activity's author. Let's create one.");
267 // ensureActivityObjectProfile throws exception on failure
268 $this->oprofile = Ostatus_profile::ensureActivityObjectProfile($this->activity->actor);
272 assert($this->oprofile instanceof Ostatus_profile);
274 $this->actor = $this->oprofile->localProfile();
277 function saveNotice()
279 if (!$this->oprofile instanceof Ostatus_profile) {
280 common_debug('Ostatus_profile missing in ' . get_class(). ' profile: '.var_export($this->profile, true));
282 return $this->oprofile->processPost($this->activity, 'salmon');