]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/lib/salmonaction.php
f7e9dde067d53ccb08b525b80480787ff11cbfef
[quix0rs-gnu-social.git] / plugins / OStatus / lib / salmonaction.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2010, StatusNet, Inc.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 /**
21  * @package OStatusPlugin
22  * @author James Walker <james@status.net>
23  */
24
25 if (!defined('GNUSOCIAL')) { exit(1); }
26
27 class SalmonAction extends Action
28 {
29     protected $needPost = true;
30
31     protected $oprofile = null; // Ostatus_profile of the actor
32     protected $actor    = null; // Profile object of the actor
33
34     var $xml      = null;
35     var $activity = null;
36     var $target   = null;
37
38     protected function prepare(array $args=array())
39     {
40         GNUsocial::setApi(true); // Send smaller error pages
41
42         parent::prepare($args);
43
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.'));
47         }
48         $envxml = null;
49         switch ($_SERVER['CONTENT_TYPE']) {
50         case 'application/magic-envelope+xml':
51             $envxml = file_get_contents('php://input');
52             break;
53         case 'application/x-www-form-urlencoded':
54             $envxml = Magicsig::base64_url_decode($this->trimmed('xml'));
55             break;
56         default:
57             // TRANS: Client error. Do not translate the quoted "application/[type]" strings.
58             throw new ClientException(_m('Salmon requires "application/magic-envelope+xml". For Diaspora we also accept "application/x-www-form-urlencoded" with an "xml" parameter.', 415));
59         }
60
61         if (empty($envxml)) {
62             throw new ClientException('No magic envelope supplied in POST.');
63         }
64         try {
65             $magic_env = new MagicEnvelope($envxml);   // parse incoming XML as a MagicEnvelope
66
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));
72                 // TRANS: Exception.
73                 throw new ClientException(_m('Activity in salmon slap has no actor id.'));
74             }
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             // convert exception to ClientException
80             throw new ClientException($e->getMessage());
81         }
82
83         // Cryptographic verification test, throws exception on failure
84         $magic_env->verify($this->actor);
85
86         return true;
87     }
88
89     /**
90      * Check the posted activity type and break out to appropriate processing.
91      */
92
93     protected function handle()
94     {
95         parent::handle();
96
97         assert($this->activity instanceof Activity);
98         assert($this->target instanceof Profile);
99
100         common_log(LOG_DEBUG, "Got a " . $this->activity->verb);
101
102         // Notice must either be a) in reply to a notice by this user
103         // or b) in reply to a notice to the attention of this user
104         // or c) to the attention of this user
105         // or d) reference the user as an activity:object
106
107         $notice = null;
108
109         if (!empty($this->activity->context->replyToID)) {
110             try {
111                 $notice = Notice::getKV('uri', $this->activity->context->replyToID);
112             } catch (NoResultException $e) {
113                 $notice = false;
114             }
115         }
116
117         if ($notice instanceof Notice &&
118                 ($this->target->sameAs($notice->getProfile())
119                     || array_key_exists($this->target->getID(), $notice->getAttentionProfileIDs())
120                 )) {
121             // In reply to a notice either from or mentioning this user.
122             common_debug('User is the owner or was in the attention list of thr:in-reply-to activity.');
123         } elseif (!empty($this->activity->context->attention) &&
124                    array_key_exists($this->target->getUri(), $this->activity->context->attention)) {
125             // To the attention of this user.
126             common_debug('User was in attention list of salmon slap.');
127         } elseif (!empty($this->activity->objects) && $this->activity->objects[0]->id === $this->target->getUri()) {
128             // The user is the object of this slap (unfollow for example)
129             common_debug('User URI was the id of the salmon slap object.');
130         } else {
131             common_debug('User was NOT found in salmon slap context.');
132             // TRANS: Client exception.
133             throw new ClientException(_m('The owner of this salmon endpoint was not in the context of the carried slap.'));
134         }
135
136         try {
137             $options = [ 'source' => 'ostatus' ];
138             common_debug('Save salmon slap directly with Notice::saveActivity for actor=='.$this->actor->getID());
139             $stored = Notice::saveActivity($this->activity, $this->actor, $options);
140             common_debug('Save salmon slap finished, notice id=='.$stored->getID());
141             return true;
142         } catch (AlreadyFulfilledException $e) {
143             // The action's results are already fulfilled. Maybe it was a
144             // duplicate? Maybe someone's database is out of sync?
145             // Let's just accept it and move on.
146             common_log(LOG_INFO, 'Salmon slap carried an event which had already been fulfilled.');
147             return true;
148         } catch (NoticeSaveException $e) {
149             common_debug('Notice::saveActivity did not save our '._ve($this->activity->verb).' activity, trying old-fashioned salmon saving.');
150         }
151
152         try {
153             if (Event::handle('StartHandleSalmonTarget', array($this->activity, $this->target)) &&
154                     Event::handle('StartHandleSalmon', array($this->activity))) {
155                 switch ($this->activity->verb) {
156                 case ActivityVerb::POST:
157                     $this->handlePost();
158                     break;
159                 case ActivityVerb::SHARE:
160                     $this->handleShare();
161                     break;
162                 case ActivityVerb::FOLLOW:
163                 case ActivityVerb::FRIEND:
164                     $this->handleFollow();
165                     break;
166                 case ActivityVerb::UNFOLLOW:
167                     $this->handleUnfollow();
168                     break;
169                 case ActivityVerb::JOIN:
170                     $this->handleJoin();
171                     break;
172                 case ActivityVerb::LEAVE:
173                     $this->handleLeave();
174                     break;
175                 case ActivityVerb::TAG:
176                     $this->handleTag();
177                     break;
178                 case ActivityVerb::UNTAG:
179                     $this->handleUntag();
180                     break;
181                 case ActivityVerb::UPDATE_PROFILE:
182                     $this->handleUpdateProfile();
183                     break;
184                 default:
185                     // TRANS: Client exception.
186                     throw new ClientException(_m('Unrecognized activity type.'));
187                 }
188                 Event::handle('EndHandleSalmon', array($this->activity));
189                 Event::handle('EndHandleSalmonTarget', array($this->activity, $this->target));
190             }
191         } catch (AlreadyFulfilledException $e) {
192             // The action's results are already fulfilled. Maybe it was a
193             // duplicate? Maybe someone's database is out of sync?
194             // Let's just accept it and move on.
195             common_log(LOG_INFO, 'Salmon slap carried an event which had already been fulfilled.');
196         }
197     }
198
199     function handlePost()
200     {
201         // TRANS: Client exception.
202         throw new ClientException(_m('This target does not understand posts.'));
203     }
204
205     function handleFollow()
206     {
207         // TRANS: Client exception.
208         throw new ClientException(_m('This target does not understand follows.'));
209     }
210
211     function handleUnfollow()
212     {
213         // TRANS: Client exception.
214         throw new ClientException(_m('This target does not understand unfollows.'));
215     }
216
217     function handleShare()
218     {
219         // TRANS: Client exception.
220         throw new ClientException(_m('This target does not understand share events.'));
221     }
222
223     function handleJoin()
224     {
225         // TRANS: Client exception.
226         throw new ClientException(_m('This target does not understand joins.'));
227     }
228
229     function handleLeave()
230     {
231         // TRANS: Client exception.
232         throw new ClientException(_m('This target does not understand leave events.'));
233     }
234
235     function handleTag()
236     {
237         // TRANS: Client exception.
238         throw new ClientException(_m('This target does not understand list events.'));
239     }
240
241     function handleUntag()
242     {
243         // TRANS: Client exception.
244         throw new ClientException(_m('This target does not understand unlist events.'));
245     }
246
247     /**
248      * Remote user sent us an update to their profile.
249      * If we already know them, accept the updates.
250      */
251     function handleUpdateProfile()
252     {
253         $oprofile = Ostatus_profile::getActorProfile($this->activity);
254         if ($oprofile instanceof Ostatus_profile) {
255             common_log(LOG_INFO, "Got a profile-update ping from $oprofile->uri");
256             $oprofile->updateFromActivityObject($this->activity->actor);
257         } else {
258             common_log(LOG_INFO, "Ignoring profile-update ping from unknown " . $this->activity->actor->id);
259         }
260     }
261
262     function ensureProfiles()
263     {
264         try {
265             $this->oprofile = Ostatus_profile::getActorProfile($this->activity);
266             if (!$this->oprofile instanceof Ostatus_profile) {
267                 throw new UnknownUriException($this->activity->actor->id);
268             }
269         } catch (UnknownUriException $e) {
270             // Apparently we didn't find the Profile object based on our URI,
271             // so OStatus doesn't have it with this URI in ostatus_profile.
272             // Try to look it up again, remote side may have changed from http to https
273             // or maybe publish an acct: URI now instead of an http: URL.
274             //
275             // Steps:
276             // 1. Check the newly received URI. Who does it say it is?
277             // 2. Compare these alleged identities to our local database.
278             // 3. If we found any locally stored identities, ask it about its aliases.
279             // 4. Do any of the aliases from our known identity match the recently introduced one?
280             //
281             // Example: We have stored http://example.com/user/1 but this URI says https://example.com/user/1
282             common_debug('No local Profile object found for a magicsigned activity author URI: '.$e->object_uri);
283             $disco = new Discovery();
284             $xrd = $disco->lookup($e->object_uri);
285             // Step 1: We got a bunch of discovery data for https://example.com/user/1 which includes
286             //         aliases https://example.com/user and hopefully our original http://example.com/user/1 too
287             $all_ids = array_merge(array($xrd->subject), $xrd->aliases);
288
289             if (!in_array($e->object_uri, $all_ids)) {
290                 common_debug('The activity author URI we got was not listed itself when doing discovery on it.');
291                 throw $e;
292             }
293
294             // Go through each reported alias from lookup to see if we know this already
295             foreach ($all_ids as $aliased_uri) {
296                 $oprofile = Ostatus_profile::getKV('uri', $aliased_uri);
297                 if (!$oprofile instanceof Ostatus_profile) {
298                     continue;   // unknown locally, check the next alias
299                 }
300                 // Step 2: We found the alleged http://example.com/user/1 URI in our local database,
301                 //         but this can't be trusted yet because anyone can publish any alias.
302                 common_debug('Found a local Ostatus_profile for "'.$e->object_uri.'" with this URI: '.$aliased_uri);
303
304                 // We found an existing OStatus profile, but is it really the same? Do a callback to the URI's origin
305                 // Step 3: lookup our previously known http://example.com/user/1 webfinger etc.
306                 $xrd = $disco->lookup($oprofile->getUri()); // getUri returns ->uri, which we filtered on earlier
307                 $doublecheck_aliases = array_merge(array($xrd->subject), $xrd->aliases);
308                 common_debug('Trying to match known "'.$aliased_uri.'" against its returned aliases: '.implode(' ', $doublecheck_aliases));
309                 // if we find our original URI here, it is a legitimate alias
310                 // Step 4: Is the newly introduced https://example.com/user/1 URI in the list of aliases
311                 //         presented by http://example.com/user/1 (i.e. do they both say they are the same identity?)
312                 if (in_array($e->object_uri, $doublecheck_aliases)) {
313                     $oprofile->updateUriKeys($e->object_uri, DiscoveryHints::fromXRD($xrd));
314                     $this->oprofile = $oprofile;
315                     break;  // don't iterate through aliases anymore
316                 }
317             }
318
319             // We might end up here after $all_ids is iterated through without a $this->oprofile value,
320             if (!$this->oprofile instanceof Ostatus_profile) {
321                 common_debug("We do not have a local profile to connect to this activity's author. Let's create one.");
322                 // ensureActivityObjectProfile throws exception on failure
323                 $this->oprofile = Ostatus_profile::ensureActivityObjectProfile($this->activity->actor);
324             }
325         }
326
327         assert($this->oprofile instanceof Ostatus_profile);
328
329         $this->actor = $this->oprofile->localProfile();
330     }
331
332     function saveNotice()
333     {
334         if (!$this->oprofile instanceof Ostatus_profile) {
335             common_debug('Ostatus_profile missing in ' . get_class(). ' profile: '.var_export($this->profile, true));
336         }
337         return $this->oprofile->processPost($this->activity, 'salmon');
338     }
339 }