]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/OStatusPlugin.php
Fix URL mention regular expression in OStatusPlugin
[quix0rs-gnu-social.git] / plugins / OStatus / OStatusPlugin.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2009-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  * OStatusPlugin implementation for GNU Social
22  *
23  * Depends on: WebFinger plugin
24  *
25  * @package OStatusPlugin
26  * @maintainer Brion Vibber <brion@status.net>
27  */
28
29 if (!defined('GNUSOCIAL')) { exit(1); }
30
31 set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/phpseclib');
32
33 class OStatusPlugin extends Plugin
34 {
35     /**
36      * Hook for RouterInitialized event.
37      *
38      * @param URLMapper $m path-to-action mapper
39      * @return boolean hook return
40      */
41     public function onRouterInitialized(URLMapper $m)
42     {
43         // Discovery actions
44         $m->connect('main/ostatustag',
45                     array('action' => 'ostatustag'));
46         $m->connect('main/ostatustag?nickname=:nickname',
47                     array('action' => 'ostatustag'), array('nickname' => '[A-Za-z0-9_-]+'));
48         $m->connect('main/ostatus/nickname/:nickname',
49                   array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
50         $m->connect('main/ostatus/group/:group',
51                   array('action' => 'ostatusinit'), array('group' => '[A-Za-z0-9_-]+'));
52         $m->connect('main/ostatus/peopletag/:peopletag/tagger/:tagger',
53                   array('action' => 'ostatusinit'), array('tagger' => '[A-Za-z0-9_-]+',
54                                                           'peopletag' => '[A-Za-z0-9_-]+'));
55         $m->connect('main/ostatus',
56                     array('action' => 'ostatusinit'));
57
58         // Remote subscription actions
59         $m->connect('main/ostatussub',
60                     array('action' => 'ostatussub'));
61         $m->connect('main/ostatusgroup',
62                     array('action' => 'ostatusgroup'));
63         $m->connect('main/ostatuspeopletag',
64                     array('action' => 'ostatuspeopletag'));
65
66         // PuSH actions
67         $m->connect('main/push/hub', array('action' => 'pushhub'));
68
69         $m->connect('main/push/callback/:feed',
70                     array('action' => 'pushcallback'),
71                     array('feed' => '[0-9]+'));
72
73         // Salmon endpoint
74         $m->connect('main/salmon/user/:id',
75                     array('action' => 'usersalmon'),
76                     array('id' => '[0-9]+'));
77         $m->connect('main/salmon/group/:id',
78                     array('action' => 'groupsalmon'),
79                     array('id' => '[0-9]+'));
80         $m->connect('main/salmon/peopletag/:id',
81                     array('action' => 'peopletagsalmon'),
82                     array('id' => '[0-9]+'));
83         return true;
84     }
85
86     public function onAutoload($cls)
87     {
88         switch ($cls) {
89         case 'Crypt_AES':
90         case 'Crypt_RSA':
91             // Crypt_AES becomes Crypt/AES.php which is found in extlib/phpseclib/
92             // which has been added to our include_path before
93             require_once str_replace('_', '/', $cls) . '.php';
94             return false;
95         }
96
97         return parent::onAutoload($cls);
98     }
99
100     /**
101      * Set up queue handlers for outgoing hub pushes
102      * @param QueueManager $qm
103      * @return boolean hook return
104      */
105     function onEndInitializeQueueManager(QueueManager $qm)
106     {
107         // Prepare outgoing distributions after notice save.
108         $qm->connect('ostatus', 'OStatusQueueHandler');
109
110         // Outgoing from our internal PuSH hub
111         $qm->connect('hubconf', 'HubConfQueueHandler');
112         $qm->connect('hubprep', 'HubPrepQueueHandler');
113
114         $qm->connect('hubout', 'HubOutQueueHandler');
115
116         // Outgoing Salmon replies (when we don't need a return value)
117         $qm->connect('salmon', 'SalmonQueueHandler');
118
119         // Incoming from a foreign PuSH hub
120         $qm->connect('pushin', 'PushInQueueHandler');
121
122         // Re-subscribe feeds that need renewal
123         $qm->connect('pushrenew', 'PushRenewQueueHandler');
124         return true;
125     }
126
127     /**
128      * Put saved notices into the queue for pubsub distribution.
129      */
130     function onStartEnqueueNotice($notice, &$transports)
131     {
132         if ($notice->inScope(null)) {
133             // put our transport first, in case there's any conflict (like OMB)
134             array_unshift($transports, 'ostatus');
135             $this->log(LOG_INFO, "Notice {$notice->id} queued for OStatus processing");
136         } else {
137             // FIXME: we don't do privacy-controlled OStatus updates yet.
138             // once that happens, finer grain of control here.
139             $this->log(LOG_NOTICE, "Not queueing notice {$notice->id} for OStatus because of privacy; scope = {$notice->scope}");
140         }
141         return true;
142     }
143
144     /**
145      * Set up a PuSH hub link to our internal link for canonical timeline
146      * Atom feeds for users and groups.
147      */
148     function onStartApiAtom($feed)
149     {
150         $id = null;
151
152         if ($feed instanceof AtomUserNoticeFeed) {
153             $salmonAction = 'usersalmon';
154             $user = $feed->getUser();
155             $id   = $user->id;
156             $profile = $user->getProfile();
157         } else if ($feed instanceof AtomGroupNoticeFeed) {
158             $salmonAction = 'groupsalmon';
159             $group = $feed->getGroup();
160             $id = $group->id;
161         } else if ($feed instanceof AtomListNoticeFeed) {
162             $salmonAction = 'peopletagsalmon';
163             $peopletag = $feed->getList();
164             $id = $peopletag->id;
165         } else {
166             return true;
167         }
168
169         if (!empty($id)) {
170             $hub = common_config('ostatus', 'hub');
171             if (empty($hub)) {
172                 // Updates will be handled through our internal PuSH hub.
173                 $hub = common_local_url('pushhub');
174             }
175             $feed->addLink($hub, array('rel' => 'hub'));
176
177             // Also, we'll add in the salmon link
178             $salmon = common_local_url($salmonAction, array('id' => $id));
179             $feed->addLink($salmon, array('rel' => Salmon::REL_SALMON));
180
181             // XXX: these are deprecated, but StatusNet only looks for NS_REPLIES
182             $feed->addLink($salmon, array('rel' => Salmon::NS_REPLIES));
183             $feed->addLink($salmon, array('rel' => Salmon::NS_MENTIONS));
184         }
185
186         return true;
187     }
188
189     /**
190      * Add in an OStatus subscribe button
191      */
192     function onStartProfileRemoteSubscribe($output, $profile)
193     {
194         $this->onStartProfileListItemActionElements($output, $profile);
195         return false;
196     }
197
198     function onStartGroupSubscribe($widget, $group)
199     {
200         $cur = common_current_user();
201
202         if (empty($cur)) {
203             $widget->out->elementStart('li', 'entity_subscribe');
204
205             $url = common_local_url('ostatusinit',
206                                     array('group' => $group->nickname));
207             $widget->out->element('a', array('href' => $url,
208                                              'class' => 'entity_remote_subscribe'),
209                                 // TRANS: Link to subscribe to a remote entity.
210                                 _m('Subscribe'));
211
212             $widget->out->elementEnd('li');
213             return false;
214         }
215
216         return true;
217     }
218
219     function onStartSubscribePeopletagForm($output, $peopletag)
220     {
221         $cur = common_current_user();
222
223         if (empty($cur)) {
224             $output->elementStart('li', 'entity_subscribe');
225             $profile = $peopletag->getTagger();
226             $url = common_local_url('ostatusinit',
227                                     array('tagger' => $profile->nickname, 'peopletag' => $peopletag->tag));
228             $output->element('a', array('href' => $url,
229                                         'class' => 'entity_remote_subscribe'),
230                                 // TRANS: Link to subscribe to a remote entity.
231                                 _m('Subscribe'));
232
233             $output->elementEnd('li');
234             return false;
235         }
236
237         return true;
238     }
239
240     /*
241      * If the field being looked for is URI look for the profile
242      */
243     function onStartProfileCompletionSearch($action, $profile, $search_engine) {
244         if ($action->field == 'uri') {
245             $profile->joinAdd(array('id', 'user:id'));
246             $profile->whereAdd('uri LIKE "%' . $profile->escape($q) . '%"');
247             $profile->query();
248
249             $validate = new Validate();
250
251             if ($profile->N == 0) {
252                 try {
253                     if ($validate->email($q)) {
254                         $oprofile = Ostatus_profile::ensureWebfinger($q);
255                     } else if ($validate->uri($q)) {
256                         $oprofile = Ostatus_profile::ensureProfileURL($q);
257                     } else {
258                         // TRANS: Exception in OStatus when invalid URI was entered.
259                         throw new Exception(_m('Invalid URI.'));
260                     }
261                     return $this->filter(array($oprofile->localProfile()));
262
263                 } catch (Exception $e) {
264                 // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
265                 // TRANS: and example.net, as these are official standard domain names for use in examples.
266                     $this->msg = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.");
267                     return array();
268                 }
269             }
270             return false;
271         }
272         return true;
273     }
274
275     /**
276      * Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz
277      *
278      * @return  array   The matching IDs (without @ or acct:) and each respective position in the given string.
279      */
280     static function extractWebfingerIds($text)
281     {
282         $wmatches = array();
283         $result = preg_match_all('/(?:^|\s+)@((?:\w+[\w\-\_\.]?)*(?:[\w\-\_\.]*\w+)@'.URL_REGEX_DOMAIN_NAME.')/',
284                        $text,
285                        $wmatches,
286                        PREG_OFFSET_CAPTURE);
287         if ($result === false) {
288             common_log(LOG_ERR, __METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error=='.preg_last_error().').');
289         } else {
290             common_debug(sprintf('Found %i matches for WebFinger IDs: %s', count($wmatches), _ve($wmatches)));
291         }
292         return $wmatches[1];
293     }
294
295     /**
296      * Profile URL matches: @example.com/mublog/user
297      *
298      * @return  array   The matching URLs (without @ or acct:) and each respective position in the given string.
299      */
300     static function extractUrlMentions($text)
301     {
302         $wmatches = array();
303         $result = preg_match_all('/(?:^|\s+)@('.URL_REGEX_DOMAIN_NAME.'(?:\/\w+)*)/',
304                        $text,
305                        $wmatches,
306                        PREG_OFFSET_CAPTURE);
307         if ($result === false) {
308             common_log(LOG_ERR, __METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error=='.preg_last_error().').');
309         } else {
310             common_debug(sprintf('Found %i matches for profile URL mentions: %s', count($wmatches), _ve($wmatches)));
311         }
312         return $wmatches[1];
313     }
314
315     /**
316      * Find any explicit remote mentions. Accepted forms:
317      *   Webfinger: @user@example.com
318      *   Profile link: @example.com/mublog/user
319      * @param Profile $sender
320      * @param string $text input markup text
321      * @param array &$mention in/out param: set of found mentions
322      * @return boolean hook return value
323      */
324     function onEndFindMentions(Profile $sender, $text, &$mentions)
325     {
326         $matches = array();
327
328         foreach (self::extractWebfingerIds($text) as $wmatch) {
329             list($target, $pos) = $wmatch;
330             $this->log(LOG_INFO, "Checking webfinger '$target'");
331             $profile = null;
332             try {
333                 $oprofile = Ostatus_profile::ensureWebfinger($target);
334                 if (!$oprofile instanceof Ostatus_profile || !$oprofile->isPerson()) {
335                     continue;
336                 }
337                 $profile = $oprofile->localProfile();
338             } catch (OStatusShadowException $e) {
339                 // This means we got a local user in the webfinger lookup
340                 $profile = $e->profile;
341             } catch (Exception $e) {
342                 $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
343                 continue;
344             }
345
346             assert($profile instanceof Profile);
347
348             $text = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target)
349                     ? $profile->getNickname()   // TODO: we could do getBestName() or getFullname() here
350                     : $target;
351             $url = $profile->getUri();
352             if (!common_valid_http_url($url)) {
353                 $url = $profile->getUrl();
354             }
355             $matches[$pos] = array('mentioned' => array($profile),
356                                    'type' => 'mention',
357                                    'text' => $text,
358                                    'position' => $pos,
359                                    'length' => mb_strlen($target),
360                                    'url' => $url);
361         }
362
363         foreach (self::extractUrlMentions($text) as $wmatch) {
364             list($target, $pos) = $wmatch;
365             $schemes = array('http', 'https');
366             foreach ($schemes as $scheme) {
367                 $url = "$scheme://$target";
368                 $this->log(LOG_INFO, "Checking profile address '$url'");
369                 try {
370                     $oprofile = Ostatus_profile::ensureProfileURL($url);
371                     if ($oprofile instanceof Ostatus_profile && !$oprofile->isGroup()) {
372                         $profile = $oprofile->localProfile();
373                         $text = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target) ?
374                                 $profile->nickname : $target;
375                         $matches[$pos] = array('mentioned' => array($profile),
376                                                'type' => 'mention',
377                                                'text' => $text,
378                                                'position' => $pos,
379                                                'length' => mb_strlen($target),
380                                                'url' => $profile->getUrl());
381                         break;
382                     }
383                 } catch (Exception $e) {
384                     $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
385                 }
386             }
387         }
388
389         foreach ($mentions as $i => $other) {
390             // If we share a common prefix with a local user, override it!
391             $pos = $other['position'];
392             if (isset($matches[$pos])) {
393                 $mentions[$i] = $matches[$pos];
394                 unset($matches[$pos]);
395             }
396         }
397         foreach ($matches as $mention) {
398             $mentions[] = $mention;
399         }
400
401         return true;
402     }
403
404     /**
405      * Allow remote profile references to be used in commands:
406      *   sub update@status.net
407      *   whois evan@identi.ca
408      *   reply http://identi.ca/evan hey what's up
409      *
410      * @param Command $command
411      * @param string $arg
412      * @param Profile &$profile
413      * @return hook return code
414      */
415     function onStartCommandGetProfile($command, $arg, &$profile)
416     {
417         $oprofile = $this->pullRemoteProfile($arg);
418         if ($oprofile instanceof Ostatus_profile && !$oprofile->isGroup()) {
419             try {
420                 $profile = $oprofile->localProfile();
421             } catch (NoProfileException $e) {
422                 // No locally stored profile found for remote profile
423                 return true;
424             }
425             return false;
426         } else {
427             return true;
428         }
429     }
430
431     /**
432      * Allow remote group references to be used in commands:
433      *   join group+statusnet@identi.ca
434      *   join http://identi.ca/group/statusnet
435      *   drop identi.ca/group/statusnet
436      *
437      * @param Command $command
438      * @param string $arg
439      * @param User_group &$group
440      * @return hook return code
441      */
442     function onStartCommandGetGroup($command, $arg, &$group)
443     {
444         $oprofile = $this->pullRemoteProfile($arg);
445         if ($oprofile instanceof Ostatus_profile && $oprofile->isGroup()) {
446             $group = $oprofile->localGroup();
447             return false;
448         } else {
449             return true;
450         }
451     }
452
453     protected function pullRemoteProfile($arg)
454     {
455         $oprofile = null;
456         if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
457             // webfinger lookup
458             try {
459                 return Ostatus_profile::ensureWebfinger($arg);
460             } catch (Exception $e) {
461                 common_log(LOG_ERR, 'Webfinger lookup failed for ' .
462                                     $arg . ': ' . $e->getMessage());
463             }
464         }
465
466         // Look for profile URLs, with or without scheme:
467         $urls = array();
468         if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
469             $urls[] = $arg;
470         }
471         if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
472             $schemes = array('http', 'https');
473             foreach ($schemes as $scheme) {
474                 $urls[] = "$scheme://$arg";
475             }
476         }
477
478         foreach ($urls as $url) {
479             try {
480                 return Ostatus_profile::ensureProfileURL($url);
481             } catch (Exception $e) {
482                 common_log(LOG_ERR, 'Profile lookup failed for ' .
483                                     $arg . ': ' . $e->getMessage());
484             }
485         }
486         return null;
487     }
488
489     function onEndProfileSettingsActions($out) {
490         $siteName = common_config('site', 'name');
491         $js = 'navigator.registerContentHandler("application/vnd.mozilla.maybe.feed", "'.addslashes(common_local_url('ostatussub', null, array('profile' => '%s'))).'", "'.addslashes($siteName).'")';
492         $out->elementStart('li');
493         $out->element('a',
494                       array('href' => 'javascript:'.$js),
495                       // TRANS: Option in profile settings to add this instance to Firefox as a feedreader
496                       _('Add to Firefox as feedreader'));
497         $out->elementEnd('li');
498     }
499
500     /**
501      * Make sure necessary tables are filled out.
502      */
503     function onCheckSchema() {
504         $schema = Schema::get();
505         $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
506         $schema->ensureTable('ostatus_source', Ostatus_source::schemaDef());
507         $schema->ensureTable('feedsub', FeedSub::schemaDef());
508         $schema->ensureTable('hubsub', HubSub::schemaDef());
509         $schema->ensureTable('magicsig', Magicsig::schemaDef());
510         return true;
511     }
512
513     public function onEndShowStylesheets(Action $action) {
514         $action->cssLink($this->path('theme/base/css/ostatus.css'));
515         return true;
516     }
517
518     function onEndShowStatusNetScripts($action) {
519         $action->script($this->path('js/ostatus.js'));
520         return true;
521     }
522
523     /**
524      * Override the "from ostatus" bit in notice lists to link to the
525      * original post and show the domain it came from.
526      *
527      * @param Notice in $notice
528      * @param string out &$name
529      * @param string out &$url
530      * @param string out &$title
531      * @return mixed hook return code
532      */
533     function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
534     {
535         // If we don't handle this, keep the event handler going
536         if (!in_array($notice->source, array('ostatus', 'share'))) {
537             return true;
538         }
539
540         try {
541             $url = $notice->getUrl();
542             // If getUrl() throws exception, $url is never set
543             
544             $bits = parse_url($url);
545             $domain = $bits['host'];
546             if (substr($domain, 0, 4) == 'www.') {
547                 $name = substr($domain, 4);
548             } else {
549                 $name = $domain;
550             }
551
552             // TRANS: Title. %s is a domain name.
553             $title = sprintf(_m('Sent from %s via OStatus'), $domain);
554
555             // Abort event handler, we have a name and URL!
556             return false;
557         } catch (InvalidUrlException $e) {
558             // This just means we don't have the notice source data
559             return true;
560         }
561     }
562
563     /**
564      * Send incoming PuSH feeds for OStatus endpoints in for processing.
565      *
566      * @param FeedSub $feedsub
567      * @param DOMDocument $feed
568      * @return mixed hook return code
569      */
570     function onStartFeedSubReceive($feedsub, $feed)
571     {
572         $oprofile = Ostatus_profile::getKV('feeduri', $feedsub->uri);
573         if ($oprofile instanceof Ostatus_profile) {
574             $oprofile->processFeed($feed, 'push');
575         } else {
576             common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri");
577         }
578     }
579
580     /**
581      * Tell the FeedSub infrastructure whether we have any active OStatus
582      * usage for the feed; if not it'll be able to garbage-collect the
583      * feed subscription.
584      *
585      * @param FeedSub $feedsub
586      * @param integer $count in/out
587      * @return mixed hook return code
588      */
589     function onFeedSubSubscriberCount($feedsub, &$count)
590     {
591         $oprofile = Ostatus_profile::getKV('feeduri', $feedsub->uri);
592         if ($oprofile instanceof Ostatus_profile) {
593             $count += $oprofile->subscriberCount();
594         }
595         return true;
596     }
597
598     /**
599      * When about to subscribe to a remote user, start a server-to-server
600      * PuSH subscription if needed. If we can't establish that, abort.
601      *
602      * @fixme If something else aborts later, we could end up with a stray
603      *        PuSH subscription. This is relatively harmless, though.
604      *
605      * @param Profile $profile  subscriber
606      * @param Profile $other    subscribee
607      *
608      * @return hook return code
609      *
610      * @throws Exception
611      */
612     function onStartSubscribe(Profile $profile, Profile $other)
613     {
614         if (!$profile->isLocal()) {
615             return true;
616         }
617
618         $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
619         if (!$oprofile instanceof Ostatus_profile) {
620             return true;
621         }
622
623         $oprofile->subscribe();
624     }
625
626     /**
627      * Having established a remote subscription, send a notification to the
628      * remote OStatus profile's endpoint.
629      *
630      * @param Profile $profile  subscriber
631      * @param Profile $other    subscribee
632      *
633      * @return hook return code
634      *
635      * @throws Exception
636      */
637     function onEndSubscribe(Profile $profile, Profile $other)
638     {
639         if (!$profile->isLocal()) {
640             return true;
641         }
642
643         $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
644         if (!$oprofile instanceof Ostatus_profile) {
645             return true;
646         }
647
648         $sub = Subscription::pkeyGet(array('subscriber' => $profile->id,
649                                            'subscribed' => $other->id));
650
651         $act = $sub->asActivity();
652
653         $oprofile->notifyActivity($act, $profile);
654
655         return true;
656     }
657
658     /**
659      * Notify remote server and garbage collect unused feeds on unsubscribe.
660      * @todo FIXME: Send these operations to background queues
661      *
662      * @param User $user
663      * @param Profile $other
664      * @return hook return value
665      */
666     function onEndUnsubscribe(Profile $profile, Profile $other)
667     {
668         if (!$profile->isLocal()) {
669             return true;
670         }
671
672         $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
673         if (!$oprofile instanceof Ostatus_profile) {
674             return true;
675         }
676
677         // Drop the PuSH subscription if there are no other subscribers.
678         $oprofile->garbageCollect();
679
680         $act = new Activity();
681
682         $act->verb = ActivityVerb::UNFOLLOW;
683
684         $act->id   = TagURI::mint('unfollow:%d:%d:%s',
685                                   $profile->id,
686                                   $other->id,
687                                   common_date_iso8601(time()));
688
689         $act->time    = time();
690         // TRANS: Title for unfollowing a remote profile.
691         $act->title   = _m('TITLE','Unfollow');
692         // TRANS: Success message for unsubscribe from user attempt through OStatus.
693         // TRANS: %1$s is the unsubscriber's name, %2$s is the unsubscribed user's name.
694         $act->content = sprintf(_m('%1$s stopped following %2$s.'),
695                                $profile->getBestName(),
696                                $other->getBestName());
697
698         $act->actor   = $profile->asActivityObject();
699         $act->objects[] = $other->asActivityObject();
700
701         $oprofile->notifyActivity($act, $profile);
702
703         return true;
704     }
705
706     /**
707      * When one of our local users tries to join a remote group,
708      * notify the remote server. If the notification is rejected,
709      * deny the join.
710      *
711      * @param User_group $group
712      * @param Profile    $profile
713      *
714      * @return mixed hook return value
715      * @throws Exception of various kinds, some from $oprofile->subscribe();
716      */
717     function onStartJoinGroup($group, $profile)
718     {
719         $oprofile = Ostatus_profile::getKV('group_id', $group->id);
720         if (!$oprofile instanceof Ostatus_profile) {
721             return true;
722         }
723
724         $oprofile->subscribe();
725
726         // NOTE: we don't use Group_member::asActivity() since that record
727         // has not yet been created.
728
729         $act = new Activity();
730         $act->id = TagURI::mint('join:%d:%d:%s',
731                                 $profile->id,
732                                 $group->id,
733                                 common_date_iso8601(time()));
734
735         $act->actor = $profile->asActivityObject();
736         $act->verb = ActivityVerb::JOIN;
737         $act->objects[] = $oprofile->asActivityObject();
738
739         $act->time = time();
740         // TRANS: Title for joining a remote groep.
741         $act->title = _m('TITLE','Join');
742         // TRANS: Success message for subscribe to group attempt through OStatus.
743         // TRANS: %1$s is the member name, %2$s is the subscribed group's name.
744         $act->content = sprintf(_m('%1$s has joined group %2$s.'),
745                                 $profile->getBestName(),
746                                 $oprofile->getBestName());
747
748         if ($oprofile->notifyActivity($act, $profile)) {
749             return true;
750         } else {
751             $oprofile->garbageCollect();
752             // TRANS: Exception thrown when joining a remote group fails.
753             throw new Exception(_m('Failed joining remote group.'));
754         }
755     }
756
757     /**
758      * When one of our local users leaves a remote group, notify the remote
759      * server.
760      *
761      * @fixme Might be good to schedule a resend of the leave notification
762      * if it failed due to a transitory error. We've canceled the local
763      * membership already anyway, but if the remote server comes back up
764      * it'll be left with a stray membership record.
765      *
766      * @param User_group $group
767      * @param Profile $profile
768      *
769      * @return mixed hook return value
770      */
771     function onEndLeaveGroup($group, $profile)
772     {
773         $oprofile = Ostatus_profile::getKV('group_id', $group->id);
774         if (!$oprofile instanceof Ostatus_profile) {
775             return true;
776         }
777
778         // Drop the PuSH subscription if there are no other subscribers.
779         $oprofile->garbageCollect();
780
781         $member = $profile;
782
783         $act = new Activity();
784         $act->id = TagURI::mint('leave:%d:%d:%s',
785                                 $member->id,
786                                 $group->id,
787                                 common_date_iso8601(time()));
788
789         $act->actor = $member->asActivityObject();
790         $act->verb = ActivityVerb::LEAVE;
791         $act->objects[] = $oprofile->asActivityObject();
792
793         $act->time = time();
794         // TRANS: Title for leaving a remote group.
795         $act->title = _m('TITLE','Leave');
796         // TRANS: Success message for unsubscribe from group attempt through OStatus.
797         // TRANS: %1$s is the member name, %2$s is the unsubscribed group's name.
798         $act->content = sprintf(_m('%1$s has left group %2$s.'),
799                                 $member->getBestName(),
800                                 $oprofile->getBestName());
801
802         $oprofile->notifyActivity($act, $member);
803     }
804
805     /**
806      * When one of our local users tries to subscribe to a remote peopletag,
807      * notify the remote server. If the notification is rejected,
808      * deny the subscription.
809      *
810      * @param Profile_list $peopletag
811      * @param User         $user
812      *
813      * @return mixed hook return value
814      * @throws Exception of various kinds, some from $oprofile->subscribe();
815      */
816
817     function onStartSubscribePeopletag($peopletag, $user)
818     {
819         $oprofile = Ostatus_profile::getKV('peopletag_id', $peopletag->id);
820         if (!$oprofile instanceof Ostatus_profile) {
821             return true;
822         }
823
824         $oprofile->subscribe();
825
826         $sub = $user->getProfile();
827         $tagger = Profile::getKV($peopletag->tagger);
828
829         $act = new Activity();
830         $act->id = TagURI::mint('subscribe_peopletag:%d:%d:%s',
831                                 $sub->id,
832                                 $peopletag->id,
833                                 common_date_iso8601(time()));
834
835         $act->actor = $sub->asActivityObject();
836         $act->verb = ActivityVerb::FOLLOW;
837         $act->objects[] = $oprofile->asActivityObject();
838
839         $act->time = time();
840         // TRANS: Title for following a remote list.
841         $act->title = _m('TITLE','Follow list');
842         // TRANS: Success message for remote list follow through OStatus.
843         // TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
844         $act->content = sprintf(_m('%1$s is now following people listed in %2$s by %3$s.'),
845                                 $sub->getBestName(),
846                                 $oprofile->getBestName(),
847                                 $tagger->getBestName());
848
849         if ($oprofile->notifyActivity($act, $sub)) {
850             return true;
851         } else {
852             $oprofile->garbageCollect();
853             // TRANS: Exception thrown when subscription to remote list fails.
854             throw new Exception(_m('Failed subscribing to remote list.'));
855         }
856     }
857
858     /**
859      * When one of our local users unsubscribes to a remote peopletag, notify the remote
860      * server.
861      *
862      * @param Profile_list $peopletag
863      * @param User         $user
864      *
865      * @return mixed hook return value
866      */
867
868     function onEndUnsubscribePeopletag($peopletag, $user)
869     {
870         $oprofile = Ostatus_profile::getKV('peopletag_id', $peopletag->id);
871         if (!$oprofile instanceof Ostatus_profile) {
872             return true;
873         }
874
875         // Drop the PuSH subscription if there are no other subscribers.
876         $oprofile->garbageCollect();
877
878         $sub = Profile::getKV($user->id);
879         $tagger = Profile::getKV($peopletag->tagger);
880
881         $act = new Activity();
882         $act->id = TagURI::mint('unsubscribe_peopletag:%d:%d:%s',
883                                 $sub->id,
884                                 $peopletag->id,
885                                 common_date_iso8601(time()));
886
887         $act->actor = $member->asActivityObject();
888         $act->verb = ActivityVerb::UNFOLLOW;
889         $act->objects[] = $oprofile->asActivityObject();
890
891         $act->time = time();
892         // TRANS: Title for unfollowing a remote list.
893         $act->title = _m('Unfollow list');
894         // TRANS: Success message for remote list unfollow through OStatus.
895         // TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
896         $act->content = sprintf(_m('%1$s stopped following the list %2$s by %3$s.'),
897                                 $sub->getBestName(),
898                                 $oprofile->getBestName(),
899                                 $tagger->getBestName());
900
901         $oprofile->notifyActivity($act, $user);
902     }
903
904     /**
905      * Notify remote users when their notices get favorited.
906      *
907      * @param Profile or User $profile of local user doing the faving
908      * @param Notice $notice being favored
909      * @return hook return value
910      */
911     function onEndFavorNotice(Profile $profile, Notice $notice)
912     {
913         // Only distribute local users' favor actions, remote users
914         // will have already distributed theirs.
915         if (!$profile->isLocal()) {
916             return true;
917         }
918
919         $oprofile = Ostatus_profile::getKV('profile_id', $notice->profile_id);
920         if (!$oprofile instanceof Ostatus_profile) {
921             return true;
922         }
923
924         $fav = Fave::pkeyGet(array('user_id' => $profile->id,
925                                    'notice_id' => $notice->id));
926
927         if (!$fav instanceof Fave) {
928             // That's weird.
929             // TODO: Make pkeyGet throw exception, since this is a critical failure.
930             return true;
931         }
932
933         $act = $fav->asActivity();
934
935         $oprofile->notifyActivity($act, $profile);
936
937         return true;
938     }
939
940     /**
941      * Notify remote user it has got a new people tag
942      *   - tag verb is queued
943      *   - the subscription is done immediately if not present
944      *
945      * @param Profile_tag $ptag the people tag that was created
946      * @return hook return value
947      * @throws Exception of various kinds, some from $oprofile->subscribe();
948      */
949     function onEndTagProfile($ptag)
950     {
951         $oprofile = Ostatus_profile::getKV('profile_id', $ptag->tagged);
952         if (!$oprofile instanceof Ostatus_profile) {
953             return true;
954         }
955
956         $plist = $ptag->getMeta();
957         if ($plist->private) {
958             return true;
959         }
960
961         $act = new Activity();
962
963         $tagger = $plist->getTagger();
964         $tagged = Profile::getKV('id', $ptag->tagged);
965
966         $act->verb = ActivityVerb::TAG;
967         $act->id   = TagURI::mint('tag_profile:%d:%d:%s',
968                                   $plist->tagger, $plist->id,
969                                   common_date_iso8601(time()));
970         $act->time = time();
971         // TRANS: Title for listing a remote profile.
972         $act->title = _m('TITLE','List');
973         // TRANS: Success message for remote list addition through OStatus.
974         // TRANS: %1$s is the list creator's name, %2$s is the added list member, %3$s is the list name.
975         $act->content = sprintf(_m('%1$s listed %2$s in the list %3$s.'),
976                                 $tagger->getBestName(),
977                                 $tagged->getBestName(),
978                                 $plist->getBestName());
979
980         $act->actor  = $tagger->asActivityObject();
981         $act->objects = array($tagged->asActivityObject());
982         $act->target = ActivityObject::fromPeopletag($plist);
983
984         $oprofile->notifyDeferred($act, $tagger);
985
986         // initiate a PuSH subscription for the person being tagged
987         $oprofile->subscribe();
988         return true;
989     }
990
991     /**
992      * Notify remote user that a people tag has been removed
993      *   - untag verb is queued
994      *   - the subscription is undone immediately if not required
995      *     i.e garbageCollect()'d
996      *
997      * @param Profile_tag $ptag the people tag that was deleted
998      * @return hook return value
999      */
1000     function onEndUntagProfile($ptag)
1001     {
1002         $oprofile = Ostatus_profile::getKV('profile_id', $ptag->tagged);
1003         if (!$oprofile instanceof Ostatus_profile) {
1004             return true;
1005         }
1006
1007         $plist = $ptag->getMeta();
1008         if ($plist->private) {
1009             return true;
1010         }
1011
1012         $act = new Activity();
1013
1014         $tagger = $plist->getTagger();
1015         $tagged = Profile::getKV('id', $ptag->tagged);
1016
1017         $act->verb = ActivityVerb::UNTAG;
1018         $act->id   = TagURI::mint('untag_profile:%d:%d:%s',
1019                                   $plist->tagger, $plist->id,
1020                                   common_date_iso8601(time()));
1021         $act->time = time();
1022         // TRANS: Title for unlisting a remote profile.
1023         $act->title = _m('TITLE','Unlist');
1024         // TRANS: Success message for remote list removal through OStatus.
1025         // TRANS: %1$s is the list creator's name, %2$s is the removed list member, %3$s is the list name.
1026         $act->content = sprintf(_m('%1$s removed %2$s from the list %3$s.'),
1027                                 $tagger->getBestName(),
1028                                 $tagged->getBestName(),
1029                                 $plist->getBestName());
1030
1031         $act->actor  = $tagger->asActivityObject();
1032         $act->objects = array($tagged->asActivityObject());
1033         $act->target = ActivityObject::fromPeopletag($plist);
1034
1035         $oprofile->notifyDeferred($act, $tagger);
1036
1037         // unsubscribe to PuSH feed if no more required
1038         $oprofile->garbageCollect();
1039
1040         return true;
1041     }
1042
1043     /**
1044      * Notify remote users when their notices get de-favorited.
1045      *
1046      * @param Profile $profile Profile person doing the de-faving
1047      * @param Notice  $notice  Notice being favored
1048      *
1049      * @return hook return value
1050      */
1051     function onEndDisfavorNotice(Profile $profile, Notice $notice)
1052     {
1053         // Only distribute local users' disfavor actions, remote users
1054         // will have already distributed theirs.
1055         if (!$profile->isLocal()) {
1056             return true;
1057         }
1058
1059         $oprofile = Ostatus_profile::getKV('profile_id', $notice->profile_id);
1060         if (!$oprofile instanceof Ostatus_profile) {
1061             return true;
1062         }
1063
1064         $act = new Activity();
1065
1066         $act->verb = ActivityVerb::UNFAVORITE;
1067         $act->id   = TagURI::mint('disfavor:%d:%d:%s',
1068                                   $profile->id,
1069                                   $notice->id,
1070                                   common_date_iso8601(time()));
1071         $act->time    = time();
1072         // TRANS: Title for unliking a remote notice.
1073         $act->title   = _m('Unlike');
1074         // TRANS: Success message for remove a favorite notice through OStatus.
1075         // TRANS: %1$s is the unfavoring user's name, %2$s is URI to the no longer favored notice.
1076         $act->content = sprintf(_m('%1$s no longer likes %2$s.'),
1077                                $profile->getBestName(),
1078                                $notice->getUrl());
1079
1080         $act->actor   = $profile->asActivityObject();
1081         $act->objects[]  = $notice->asActivityObject();
1082
1083         $oprofile->notifyActivity($act, $profile);
1084
1085         return true;
1086     }
1087
1088     function onStartGetProfileUri($profile, &$uri)
1089     {
1090         $oprofile = Ostatus_profile::getKV('profile_id', $profile->id);
1091         if ($oprofile instanceof Ostatus_profile) {
1092             $uri = $oprofile->uri;
1093             return false;
1094         }
1095         return true;
1096     }
1097
1098     function onStartUserGroupHomeUrl($group, &$url)
1099     {
1100         return $this->onStartUserGroupPermalink($group, $url);
1101     }
1102
1103     function onStartUserGroupPermalink($group, &$url)
1104     {
1105         $oprofile = Ostatus_profile::getKV('group_id', $group->id);
1106         if ($oprofile instanceof Ostatus_profile) {
1107             // @fixme this should probably be in the user_group table
1108             // @fixme this uri not guaranteed to be a profile page
1109             $url = $oprofile->uri;
1110             return false;
1111         }
1112     }
1113
1114     function onStartShowSubscriptionsContent($action)
1115     {
1116         $this->showEntityRemoteSubscribe($action);
1117
1118         return true;
1119     }
1120
1121     function onStartShowUserGroupsContent($action)
1122     {
1123         $this->showEntityRemoteSubscribe($action, 'ostatusgroup');
1124
1125         return true;
1126     }
1127
1128     function onEndShowSubscriptionsMiniList($action)
1129     {
1130         $this->showEntityRemoteSubscribe($action);
1131
1132         return true;
1133     }
1134
1135     function onEndShowGroupsMiniList($action)
1136     {
1137         $this->showEntityRemoteSubscribe($action, 'ostatusgroup');
1138
1139         return true;
1140     }
1141
1142     function showEntityRemoteSubscribe($action, $target='ostatussub')
1143     {
1144         if (!$action->getScoped() instanceof Profile) {
1145             // early return if we're not logged in
1146             return true;
1147         }
1148
1149         if ($action->getScoped()->sameAs($action->getTarget())) {
1150             $action->elementStart('div', 'entity_actions');
1151             $action->elementStart('p', array('id' => 'entity_remote_subscribe',
1152                                              'class' => 'entity_subscribe'));
1153             $action->element('a', array('href' => common_local_url($target),
1154                                         'class' => 'entity_remote_subscribe'),
1155                                 // TRANS: Link text for link to remote subscribe.
1156                                 _m('Remote'));
1157             $action->elementEnd('p');
1158             $action->elementEnd('div');
1159         }
1160     }
1161
1162     /**
1163      * Ping remote profiles with updates to this profile.
1164      * Salmon pings are queued for background processing.
1165      */
1166     function onEndBroadcastProfile(Profile $profile)
1167     {
1168         $user = User::getKV('id', $profile->id);
1169
1170         // Find foreign accounts I'm subscribed to that support Salmon pings.
1171         //
1172         // @fixme we could run updates through the PuSH feed too,
1173         // in which case we can skip Salmon pings to folks who
1174         // are also subscribed to me.
1175         $sql = "SELECT * FROM ostatus_profile " .
1176                "WHERE profile_id IN " .
1177                "(SELECT subscribed FROM subscription WHERE subscriber=%d) " .
1178                "OR group_id IN " .
1179                "(SELECT group_id FROM group_member WHERE profile_id=%d)";
1180         $oprofile = new Ostatus_profile();
1181         $oprofile->query(sprintf($sql, $profile->id, $profile->id));
1182
1183         if ($oprofile->N == 0) {
1184             common_log(LOG_DEBUG, "No OStatus remote subscribees for $profile->nickname");
1185             return true;
1186         }
1187
1188         $act = new Activity();
1189
1190         $act->verb = ActivityVerb::UPDATE_PROFILE;
1191         $act->id   = TagURI::mint('update-profile:%d:%s',
1192                                   $profile->id,
1193                                   common_date_iso8601(time()));
1194         $act->time    = time();
1195         // TRANS: Title for activity.
1196         $act->title   = _m('Profile update');
1197         // TRANS: Ping text for remote profile update through OStatus.
1198         // TRANS: %s is user that updated their profile.
1199         $act->content = sprintf(_m('%s has updated their profile page.'),
1200                                $profile->getBestName());
1201
1202         $act->actor   = $profile->asActivityObject();
1203         $act->objects[]  = $act->actor;
1204
1205         while ($oprofile->fetch()) {
1206             $oprofile->notifyDeferred($act, $profile);
1207         }
1208
1209         return true;
1210     }
1211
1212     // FIXME: This one can accept both an Action and a Widget. Confusing! Refactor to (HTMLOutputter $out, Profile $target)!
1213     function onStartProfileListItemActionElements($item)
1214     {
1215         if (common_logged_in()) {
1216             // only non-logged in users get to see the "remote subscribe" form
1217             return true;
1218         } elseif (!$item->getTarget()->isLocal()) {
1219             // we can (for now) only provide remote subscribe forms for local users
1220             return true;
1221         }
1222
1223         if ($item instanceof ProfileAction) {
1224             $output = $item;
1225         } elseif ($item instanceof Widget) {
1226             $output = $item->out;
1227         } else {
1228             // Bad $item class, don't know how to use this for outputting!
1229             throw new ServerException('Bad item type for onStartProfileListItemActionElements');
1230         }
1231
1232         // Add an OStatus subscribe
1233         $output->elementStart('li', 'entity_subscribe');
1234         $url = common_local_url('ostatusinit',
1235                                 array('nickname' => $item->getTarget()->getNickname()));
1236         $output->element('a', array('href' => $url,
1237                                     'class' => 'entity_remote_subscribe'),
1238                           // TRANS: Link text for a user to subscribe to an OStatus user.
1239                          _m('Subscribe'));
1240         $output->elementEnd('li');
1241
1242         $output->elementStart('li', 'entity_tag');
1243         $url = common_local_url('ostatustag',
1244                                 array('nickname' => $item->getTarget()->getNickname()));
1245         $output->element('a', array('href' => $url,
1246                                     'class' => 'entity_remote_tag'),
1247                           // TRANS: Link text for a user to list an OStatus user.
1248                          _m('List'));
1249         $output->elementEnd('li');
1250
1251         return true;
1252     }
1253
1254     function onPluginVersion(array &$versions)
1255     {
1256         $versions[] = array('name' => 'OStatus',
1257                             'version' => GNUSOCIAL_VERSION,
1258                             'author' => 'Evan Prodromou, James Walker, Brion Vibber, Zach Copley',
1259                             'homepage' => 'http://status.net/wiki/Plugin:OStatus',
1260                             // TRANS: Plugin description.
1261                             'rawdescription' => _m('Follow people across social networks that implement '.
1262                                '<a href="http://ostatus.org/">OStatus</a>.'));
1263
1264         return true;
1265     }
1266
1267     /**
1268      * Utility function to check if the given URI is a canonical group profile
1269      * page, and if so return the ID number.
1270      *
1271      * @param string $url
1272      * @return mixed int or false
1273      */
1274     public static function localGroupFromUrl($url)
1275     {
1276         $group = User_group::getKV('uri', $url);
1277         if ($group instanceof User_group) {
1278             if ($group->isLocal()) {
1279                 return $group->id;
1280             }
1281         } else {
1282             // To find local groups which haven't had their uri fields filled out...
1283             // If the domain has changed since a subscriber got the URI, it'll
1284             // be broken.
1285             $template = common_local_url('groupbyid', array('id' => '31337'));
1286             $template = preg_quote($template, '/');
1287             $template = str_replace('31337', '(\d+)', $template);
1288             if (preg_match("/$template/", $url, $matches)) {
1289                 return intval($matches[1]);
1290             }
1291         }
1292         return false;
1293     }
1294
1295     public function onStartProfileGetAtomFeed($profile, &$feed)
1296     {
1297         $oprofile = Ostatus_profile::getKV('profile_id', $profile->id);
1298
1299         if (!$oprofile instanceof Ostatus_profile) {
1300             return true;
1301         }
1302
1303         $feed = $oprofile->feeduri;
1304         return false;
1305     }
1306
1307     function onStartGetProfileFromURI($uri, &$profile)
1308     {
1309         // Don't want to do Web-based discovery on our own server,
1310         // so we check locally first. This duplicates the functionality
1311         // in the Profile class, since the plugin always runs before
1312         // that local lookup, but since we return false it won't run double.
1313
1314         $user = User::getKV('uri', $uri);
1315         if ($user instanceof User) {
1316             $profile = $user->getProfile();
1317             return false;
1318         } else {
1319             $group = User_group::getKV('uri', $uri);
1320             if ($group instanceof User_group) {
1321                 $profile = $group->getProfile();
1322                 return false;
1323             }
1324         }
1325
1326         // Now, check remotely
1327         try {
1328             $oprofile = Ostatus_profile::ensureProfileURI($uri);
1329             $profile = $oprofile->localProfile();
1330             return !($profile instanceof Profile);  // localProfile won't throw exception but can return null
1331         } catch (Exception $e) {
1332             return true; // It's not an OStatus profile as far as we know, continue event handling
1333         }
1334     }
1335
1336     function onEndWebFingerNoticeLinks(XML_XRD $xrd, Notice $target)
1337     {
1338         $author = $target->getProfile();
1339         $profiletype = $this->profileTypeString($author);
1340         $salmon_url = common_local_url("{$profiletype}salmon", array('id' => $author->id));
1341         $xrd->links[] = new XML_XRD_Element_Link(Salmon::REL_SALMON, $salmon_url);
1342         return true;
1343     }
1344
1345     function onEndWebFingerProfileLinks(XML_XRD $xrd, Profile $target)
1346     {
1347         if ($target->getObjectType() === ActivityObject::PERSON) {
1348             $this->addWebFingerPersonLinks($xrd, $target);
1349         }
1350
1351         // Salmon
1352         $profiletype = $this->profileTypeString($target);
1353         $salmon_url = common_local_url("{$profiletype}salmon", array('id' => $target->id));
1354
1355         $xrd->links[] = new XML_XRD_Element_Link(Salmon::REL_SALMON, $salmon_url);
1356
1357         // XXX: these are deprecated, but StatusNet only looks for NS_REPLIES
1358         $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_REPLIES, $salmon_url);
1359         $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_MENTIONS, $salmon_url);
1360
1361         // TODO - finalize where the redirect should go on the publisher
1362         $xrd->links[] = new XML_XRD_Element_Link('http://ostatus.org/schema/1.0/subscribe',
1363                               common_local_url('ostatussub') . '?profile={uri}',
1364                               null, // type not set
1365                               true); // isTemplate
1366
1367         return true;
1368     }
1369
1370     protected function profileTypeString(Profile $target)
1371     {
1372         // This is just used to have a definitive string response to "USERsalmon" or "GROUPsalmon"
1373         switch ($target->getObjectType()) {
1374         case ActivityObject::PERSON:
1375             return 'user';
1376         case ActivityObject::GROUP:
1377             return 'group';
1378         default:
1379             throw new ServerException('Unknown profile type for WebFinger profile links');
1380         }
1381     }
1382
1383     protected function addWebFingerPersonLinks(XML_XRD $xrd, Profile $target)
1384     {
1385         $xrd->links[] = new XML_XRD_Element_Link(Discovery::UPDATESFROM,
1386                             common_local_url('ApiTimelineUser',
1387                                 array('id' => $target->id, 'format' => 'atom')),
1388                             'application/atom+xml');
1389
1390         // Get this profile's keypair
1391         $magicsig = Magicsig::getKV('user_id', $target->id);
1392         if (!$magicsig instanceof Magicsig && $target->isLocal()) {
1393             $magicsig = Magicsig::generate($target->getUser());
1394         }
1395
1396         if (!$magicsig instanceof Magicsig) {
1397             return false;   // value doesn't mean anything, just figured I'd indicate this function didn't do anything
1398         }
1399         if (Event::handle('StartAttachPubkeyToUserXRD', array($magicsig, $xrd, $target))) {
1400             $xrd->links[] = new XML_XRD_Element_Link(Magicsig::PUBLICKEYREL,
1401                                 'data:application/magic-public-key,'. $magicsig->toString());
1402             // The following event handles plugins like Diaspora which add their own version of the Magicsig pubkey
1403             Event::handle('EndAttachPubkeyToUserXRD', array($magicsig, $xrd, $target));
1404         }
1405     }
1406
1407     public function onGetLocalAttentions(Profile $actor, array $attention_uris, array &$mentions, array &$groups)
1408     {
1409         list($groups, $mentions) = Ostatus_profile::filterAttention($actor, $attention_uris);
1410     }
1411
1412     // FIXME: Maybe this shouldn't be so authoritative that it breaks other remote profile lookups?
1413     static public function onCheckActivityAuthorship(Activity $activity, Profile &$profile)
1414     {
1415         try {
1416             $oprofile = Ostatus_profile::ensureProfileURL($profile->getUrl());
1417             $profile = $oprofile->checkAuthorship($activity);
1418         } catch (Exception $e) {
1419             common_log(LOG_ERR, 'Could not get a profile or check authorship ('.get_class($e).': "'.$e->getMessage().'") for activity ID: '.$activity->id);
1420             $profile = null;
1421             return false;
1422         }
1423         return true;
1424     }
1425
1426     public function onProfileDeleteRelated($profile, &$related)
1427     {
1428         // Ostatus_profile has a 'profile_id' property, which will be used to find the object
1429         $related[] = 'Ostatus_profile';
1430
1431         // Magicsig has a "user_id" column instead, so we have to delete it more manually:
1432         $magicsig = Magicsig::getKV('user_id', $profile->id);
1433         if ($magicsig instanceof Magicsig) {
1434             $magicsig->delete();
1435         }
1436         return true;
1437     }
1438
1439     public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target=null)
1440     {
1441         $envxml = $magic_env->toXML($target);
1442
1443         $headers = array('Content-Type: application/magic-envelope+xml');
1444
1445         try {
1446             $client = new HTTPClient();
1447             $client->setBody($envxml);
1448             $response = $client->post($endpoint_uri, $headers);
1449         } catch (Exception $e) {
1450             common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage());
1451             return false;
1452         }
1453         if ($response->getStatus() === 422) {
1454             common_debug(sprintf('Salmon (from profile %d) endpoint %s returned status %s. We assume it is a Diaspora seed; will adapt and try again if that plugin is enabled!', $magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus()));
1455             return true;
1456         }
1457
1458         // 200 OK is the best response
1459         // 202 Accepted is what we get from Diaspora for example
1460         if (!in_array($response->getStatus(), array(200, 202))) {
1461             common_log(LOG_ERR, sprintf('Salmon (from profile %d) endpoint %s returned status %s: %s',
1462                                 $magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus(), $response->getBody()));
1463             return true;
1464         }
1465
1466         // Since we completed the salmon slap, we discontinue the event
1467         return false;
1468     }
1469
1470     public function onCronDaily()
1471     {
1472         try {
1473             $sub = FeedSub::renewalCheck();
1474         } catch (NoResultException $e) {
1475             common_log(LOG_INFO, "There were no expiring feeds.");
1476             return;
1477         }
1478
1479         $qm = QueueManager::get();
1480         while ($sub->fetch()) {
1481             $item = array('feedsub_id' => $sub->id);
1482             $qm->enqueue($item, 'pushrenew');
1483         }
1484     }
1485 }