]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Bookmark/BookmarkPlugin.php
Merge branch '0.9.x' into noactor
[quix0rs-gnu-social.git] / plugins / Bookmark / BookmarkPlugin.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2010, StatusNet, Inc.
5  *
6  * A plugin to enable social-bookmarking functionality
7  *
8  * PHP version 5
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU Affero General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Affero General Public License for more details.
19  *
20  * You should have received a copy of the GNU Affero General Public License
21  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  *
23  * @category  SocialBookmark
24  * @package   StatusNet
25  * @author    Evan Prodromou <evan@status.net>
26  * @copyright 2010 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
28  * @link      http://status.net/
29  */
30
31 if (!defined('STATUSNET')) {
32     exit(1);
33 }
34
35 /**
36  * Bookmark plugin main class
37  *
38  * @category  Bookmark
39  * @package   StatusNet
40  * @author    Brion Vibber <brionv@status.net>
41  * @author    Evan Prodromou <evan@status.net>
42  * @copyright 2010 StatusNet, Inc.
43  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
44  * @link      http://status.net/
45  */
46
47 class BookmarkPlugin extends Plugin
48 {
49     const VERSION         = '0.1';
50     const IMPORTDELICIOUS = 'BookmarkPlugin:IMPORTDELICIOUS';
51
52     /**
53      * Authorization for importing delicious bookmarks
54      *
55      * By default, everyone can import bookmarks except silenced people.
56      *
57      * @param Profile $profile Person whose rights to check
58      * @param string  $right   Right to check; const value
59      * @param boolean &$result Result of the check, writeable
60      *
61      * @return boolean hook value
62      */
63
64     function onUserRightsCheck($profile, $right, &$result)
65     {
66         if ($right == self::IMPORTDELICIOUS) {
67             $result = !$profile->isSilenced();
68             return false;
69         }
70         return true;
71     }
72
73     /**
74      * Database schema setup
75      *
76      * @see Schema
77      * @see ColumnDef
78      *
79      * @return boolean hook value; true means continue processing, false means stop.
80      */
81
82     function onCheckSchema()
83     {
84         $schema = Schema::get();
85
86         // For storing user-submitted flags on profiles
87
88         $schema->ensureTable('bookmark',
89                              array(new ColumnDef('profile_id',
90                                                  'integer',
91                                                  null,
92                                                  false,
93                                                  'PRI'),
94                                    new ColumnDef('url',
95                                                  'varchar',
96                                                  255,
97                                                  false,
98                                                  'PRI'),
99                                    new ColumnDef('title',
100                                                  'varchar',
101                                                  255),
102                                    new ColumnDef('description',
103                                                  'text'),
104                                    new ColumnDef('uri',
105                                                  'varchar',
106                                                  255,
107                                                  false,
108                                                  'UNI'),
109                                    new ColumnDef('url_crc32',
110                                                  'integer unsigned',
111                                                  null,
112                                                  false,
113                                                  'MUL'),
114                                    new ColumnDef('created',
115                                                  'datetime',
116                                                  null,
117                                                  false,
118                                                  'MUL')));
119
120         try {
121             $schema->createIndex('bookmark', 
122                                  array('profile_id', 
123                                        'url_crc32'),
124                                  'bookmark_profile_url_idx');
125         } catch (Exception $e) {
126             common_log(LOG_ERR, $e->getMessage());
127         }
128
129         return true;
130     }
131
132     /**
133      * When a notice is deleted, delete the related Bookmark
134      *
135      * @param Notice $notice Notice being deleted
136      * 
137      * @return boolean hook value
138      */
139
140     function onNoticeDeleteRelated($notice)
141     {
142         $nb = Bookmark::getByNotice($notice);
143
144         if (!empty($nb)) {
145             $nb->delete();
146         }
147
148         return true;
149     }
150
151     /**
152      * Show the CSS necessary for this plugin
153      *
154      * @param Action $action the action being run
155      *
156      * @return boolean hook value
157      */
158
159     function onEndShowStyles($action)
160     {
161         $action->cssLink('plugins/Bookmark/bookmark.css');
162         return true;
163     }
164
165     /**
166      * Load related modules when needed
167      *
168      * @param string $cls Name of the class to be loaded
169      *
170      * @return boolean hook value; true means continue processing, false means stop.
171      */
172
173     function onAutoload($cls)
174     {
175         $dir = dirname(__FILE__);
176
177         switch ($cls)
178         {
179         case 'ShowbookmarkAction':
180         case 'NewbookmarkAction':
181         case 'BookmarkpopupAction':
182         case 'NoticebyurlAction':
183         case 'ImportdeliciousAction':
184             include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
185             return false;
186         case 'Bookmark':
187             include_once $dir.'/'.$cls.'.php';
188             return false;
189         case 'BookmarkForm':
190         case 'DeliciousBackupImporter':
191         case 'DeliciousBookmarkImporter':
192             include_once $dir.'/'.strtolower($cls).'.php';
193             return false;
194         default:
195             return true;
196         }
197     }
198
199     /**
200      * Map URLs to actions
201      *
202      * @param Net_URL_Mapper $m path-to-action mapper
203      *
204      * @return boolean hook value; true means continue processing, false means stop.
205      */
206
207     function onRouterInitialized($m)
208     {
209         $m->connect('main/bookmark/new',
210                     array('action' => 'newbookmark'),
211                     array('id' => '[0-9]+'));
212
213         $m->connect('main/bookmark/popup',
214                     array('action' => 'bookmarkpopup'));
215
216         $m->connect('main/bookmark/import',
217                     array('action' => 'importdelicious'));
218
219         $m->connect('bookmark/:user/:created/:crc32',
220                     array('action' => 'showbookmark'),
221                     array('user' => '[0-9]+',
222                           'created' => '[0-9]{14}',
223                           'crc32' => '[0-9a-f]{8}'));
224
225         $m->connect('notice/by-url/:id',
226                     array('action' => 'noticebyurl'),
227                     array('id' => '[0-9]+'));
228
229         return true;
230     }
231
232     /**
233      * Output the HTML for a bookmark in a list
234      *
235      * @param NoticeListItem $nli The list item being shown.
236      *
237      * @return boolean hook value
238      */
239
240     function onStartShowNoticeItem($nli)
241     {
242         $nb = Bookmark::getByNotice($nli->notice);
243
244         if (!empty($nb)) {
245
246             $out     = $nli->out;
247             $notice  = $nli->notice;
248             $profile = $nli->profile;
249
250             $atts = $notice->attachments();
251
252             if (count($atts) < 1) {
253                 // Something wrong; let default code deal with it.
254                 return true;
255             }
256
257             $att = $atts[0];
258
259             // XXX: only show the bookmark URL for non-single-page stuff
260
261             if ($out instanceof ShowbookmarkAction) {
262             } else {
263                 $out->elementStart('h3');
264                 $out->element('a',
265                               array('href' => $att->url),
266                               $nb->title);
267                 $out->elementEnd('h3');
268
269                 $countUrl = common_local_url('noticebyurl',
270                                              array('id' => $att->id));
271
272                 $out->element('a', array('class' => 'bookmark_notice_count',
273                                          'href' => $countUrl),
274                               $att->noticeCount());
275             }
276
277             $out->elementStart('ul', array('class' => 'bookmark_tags'));
278             
279             // Replies look like "for:" tags
280
281             $replies = $nli->notice->getReplies();
282
283             if (!empty($replies)) {
284                 foreach ($replies as $reply) {
285                     $other = Profile::staticGet('id', $reply);
286                     $out->elementStart('li');
287                     $out->element('a', array('rel' => 'tag',
288                                              'href' => $other->profileurl,
289                                              'title' => $other->getBestName()),
290                                   sprintf('for:%s', $other->nickname));
291                     $out->elementEnd('li');
292                     $out->text(' ');
293                 }
294             }
295
296             $tags = $nli->notice->getTags();
297
298             foreach ($tags as $tag) {
299                 $out->elementStart('li');
300                 $out->element('a', 
301                               array('rel' => 'tag',
302                                     'href' => Notice_tag::url($tag)),
303                               $tag);
304                 $out->elementEnd('li');
305                 $out->text(' ');
306             }
307
308             $out->elementEnd('ul');
309
310             $out->element('p',
311                           array('class' => 'bookmark_description'),
312                           $nb->description);
313
314             if (common_config('attachments', 'show_thumbs')) {
315                 $al = new InlineAttachmentList($notice, $out);
316                 $al->show();
317             }
318
319             $out->elementStart('p', array('style' => 'float: left'));
320
321             $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
322
323             $out->element('img', array('src' => ($avatar) ?
324                                        $avatar->displayUrl() :
325                                        Avatar::defaultImage(AVATAR_MINI_SIZE),
326                                        'class' => 'avatar photo bookmark_avatar',
327                                        'width' => AVATAR_MINI_SIZE,
328                                        'height' => AVATAR_MINI_SIZE,
329                                        'alt' => $profile->getBestName()));
330             $out->raw('&nbsp;');
331             $out->element('a', array('href' => $profile->profileurl,
332                                      'title' => $profile->getBestName()),
333                           $profile->nickname);
334
335             $nli->showNoticeLink();
336             $nli->showNoticeSource();
337             $nli->showNoticeLocation();
338             $nli->showContext();
339             $nli->showRepeat();
340
341             $out->elementEnd('p');
342
343             $nli->showNoticeOptions();
344
345             return false;
346         }
347         return true;
348     }
349
350     /**
351      * Render a notice as a Bookmark object
352      *
353      * @param Notice         $notice  Notice to render
354      * @param ActivityObject &$object Empty object to fill
355      *
356      * @return boolean hook value
357      */
358      
359     function onStartActivityObjectFromNotice($notice, &$object)
360     {
361         common_log(LOG_INFO,
362                    "Checking {$notice->uri} to see if it's a bookmark.");
363
364         $nb = Bookmark::getByNotice($notice);
365                                          
366         if (!empty($nb)) {
367
368             common_log(LOG_INFO,
369                        "Formatting notice {$notice->uri} as a bookmark.");
370
371             $object->id      = $notice->uri;
372             $object->type    = ActivityObject::BOOKMARK;
373             $object->title   = $nb->title;
374             $object->summary = $nb->description;
375             $object->link    = $notice->bestUrl();
376
377             // Attributes of the URL
378
379             $attachments = $notice->attachments();
380
381             if (count($attachments) != 1) {
382                 throw new ServerException(_('Bookmark notice with the '.
383                                             'wrong number of attachments.'));
384             }
385
386             $target = $attachments[0];
387
388             $attrs = array('rel' => 'related',
389                            'href' => $target->url);
390
391             if (!empty($target->title)) {
392                 $attrs['title'] = $target->title;
393             }
394
395             $object->extra[] = array('link', $attrs, null);
396                                                    
397             // Attributes of the thumbnail, if any
398
399             $thumbnail = $target->getThumbnail();
400
401             if (!empty($thumbnail)) {
402                 $tattrs = array('rel' => 'preview',
403                                 'href' => $thumbnail->url);
404
405                 if (!empty($thumbnail->width)) {
406                     $tattrs['media:width'] = $thumbnail->width;
407                 }
408
409                 if (!empty($thumbnail->height)) {
410                     $tattrs['media:height'] = $thumbnail->height;
411                 }
412
413                 $object->extra[] = array('link', $attrs, null);
414             }
415
416             return false;
417         }
418
419         return true;
420     }
421
422     /**
423      * Add our two queue handlers to the queue manager
424      *
425      * @param QueueManager $qm current queue manager
426      * 
427      * @return boolean hook value
428      */
429
430     function onEndInitializeQueueManager($qm)
431     {
432         $qm->connect('dlcsback', 'DeliciousBackupImporter');
433         $qm->connect('dlcsbkmk', 'DeliciousBookmarkImporter');
434         return true;
435     }
436
437     /**
438      * Plugin version data
439      *
440      * @param array &$versions array of version data
441      * 
442      * @return value
443      */
444
445     function onPluginVersion(&$versions)
446     {
447         $versions[] = array('name' => 'Sample',
448                             'version' => self::VERSION,
449                             'author' => 'Evan Prodromou',
450                             'homepage' => 'http://status.net/wiki/Plugin:Bookmark',
451                             'rawdescription' =>
452                             _m('Simple extension for supporting bookmarks.'));
453         return true;
454     }
455
456     /**
457      * Load our document if requested
458      *
459      * @param string &$title  Title to fetch
460      * @param string &$output HTML to output
461      *
462      * @return boolean hook value
463      */
464
465     function onStartLoadDoc(&$title, &$output)
466     {
467         if ($title == 'bookmarklet') {
468             $filename = INSTALLDIR.'/plugins/Bookmark/bookmarklet';
469
470             $c      = file_get_contents($filename);
471             $output = common_markup_to_html($c);
472             return false; // success!
473         }
474
475         return true;
476     }
477
478     /**
479      * Handle a posted bookmark from PuSH
480      *
481      * @param Activity        $activity activity to handle
482      * @param Ostatus_profile $oprofile Profile for the feed
483      *
484      * @return boolean hook value
485      */
486
487     function onStartHandleFeedEntryWithProfile($activity, $oprofile)
488     {
489         common_log(LOG_INFO, "BookmarkPlugin called for new feed entry.");
490
491         if (self::_isPostBookmark($activity)) {
492
493             common_log(LOG_INFO, 
494                        "Importing activity {$activity->id} as a bookmark.");
495
496             $author = $oprofile->checkAuthorship($activity);
497
498             if (empty($author)) {
499                 throw new ClientException(_('Can\'t get author for activity.'));
500             }
501
502             self::_postRemoteBookmark($author,
503                                       $activity);
504
505             return false;
506         }
507
508         return true;
509     }
510
511     /**
512      * Handle a posted bookmark from Salmon
513      *
514      * @param Activity $activity activity to handle
515      * @param mixed    $target   user or group targeted
516      *
517      * @return boolean hook value
518      */
519
520     function onStartHandleSalmonTarget($activity, $target)
521     {
522         if (self::_isPostBookmark($activity)) {
523
524             $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap.");
525
526             if ($target instanceof User_group) {
527                 $uri = $target->getUri();
528                 if (!in_array($uri, $activity->context->attention)) {
529                     throw new ClientException(_("Bookmark not posted ".
530                                                 "to this group."));
531                 }
532             } else if ($target instanceof User) {
533                 $uri      = $target->uri;
534                 $original = null;
535                 if (!empty($activity->context->replyToID)) {
536                     $original = Notice::staticGet('uri', 
537                                                   $activity->context->replyToID); 
538                 }
539                 if (!in_array($uri, $activity->context->attention) &&
540                     (empty($original) ||
541                      $original->profile_id != $target->id)) {
542                     throw new ClientException(_("Bookmark not posted ".
543                                                 "to this user."));
544                 }
545             } else {
546                 throw new ServerException(_("Don't know how to handle ".
547                                             "this kind of target."));
548             }
549
550             $author = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
551
552             self::_postRemoteBookmark($author,
553                                       $activity);
554
555             return false;
556         }
557
558         return true;
559     }
560
561     /**
562      * Handle bookmark posted via AtomPub
563      *
564      * @param Activity &$activity Activity that was posted
565      * @param User     $user      User that posted it
566      * @param Notice   &$notice   Resulting notice
567      *
568      * @return boolean hook value
569      */
570
571     function onStartAtomPubNewActivity(&$activity, $user, &$notice)
572     {
573         if (self::_isPostBookmark($activity)) {
574             $options = array('source' => 'atompub');
575             $notice  = self::_postBookmark($user->getProfile(),
576                                            $activity,
577                                            $options);
578             return false;
579         }
580
581         return true;
582     }
583
584     /**
585      * Handle bookmark imported from a backup file
586      *
587      * @param User           $user     User to import for
588      * @param ActivityObject $author   Original author per import file
589      * @param Activity       $activity Activity to import
590      * @param boolean        $trusted  Is this a trusted user?
591      * @param boolean        &$done    Is this done (success or unrecoverable error)
592      *
593      * @return boolean hook value
594      */
595
596     function onStartImportActivity($user, $author, $activity, $trusted, &$done)
597     {
598         if (self::_isPostBookmark($activity)) {
599
600             $bookmark = $activity->objects[0];
601
602             $this->log(LOG_INFO,
603                        'Importing Bookmark ' . $bookmark->id . 
604                        ' for user ' . $user->nickname);
605
606             $options = array('uri' => $bookmark->id,
607                              'url' => $bookmark->link,
608                              'source' => 'restore');
609
610             $saved = self::_postBookmark($user->getProfile(), $activity, $options);
611
612             if (!empty($saved)) {
613                 $done = true;
614             }
615
616             return false;
617         }
618
619         return true;
620     }
621
622     /**
623      * Show a link to our delicious import page on profile settings form
624      *
625      * @param Action $action Profile settings action being shown
626      *
627      * @return boolean hook value
628      */
629
630     function onEndProfileSettingsActions($action)
631     {
632         $user = common_current_user();
633         
634         if (!empty($user) && $user->hasRight(self::IMPORTDELICIOUS)) {
635             $action->elementStart('li');
636             $action->element('a',
637                              array('href' => common_local_url('importdelicious')),
638                              _('Import del.icio.us bookmarks'));
639             $action->elementEnd('li');
640         }
641
642         return true;
643     }
644
645     /**
646      * Save a remote bookmark (from Salmon or PuSH)
647      *
648      * @param Ostatus_profile $author   Author of the bookmark
649      * @param Activity        $activity Activity to save
650      *
651      * @return Notice resulting notice.
652      */
653
654     static private function _postRemoteBookmark(Ostatus_profile $author,
655                                                 Activity $activity)
656     {
657         $bookmark = $activity->objects[0];
658
659         $options = array('uri' => $bookmark->id,
660                          'url' => $bookmark->link,
661                          'is_local' => Notice::REMOTE_OMB,
662                          'source' => 'ostatus');
663         
664         return self::_postBookmark($author->localProfile(), $activity, $options);
665     }
666
667     /**
668      * Save a bookmark from an activity
669      *
670      * @param Profile  $profile  Profile to use as author
671      * @param Activity $activity Activity to save
672      * @param array    $options  Options to pass to bookmark-saving code
673      *
674      * @return Notice resulting notice
675      */
676
677     static private function _postBookmark(Profile $profile,
678                                           Activity $activity,
679                                           $options=array())
680     {
681         $bookmark = $activity->objects[0];
682
683         $relLinkEls = ActivityUtils::getLinks($bookmark->element, 'related');
684
685         if (count($relLinkEls) < 1) {
686             throw new ClientException(_('Expected exactly 1 link '.
687                                         'rel=related in a Bookmark.'));
688         }
689
690         if (count($relLinkEls) > 1) {
691             common_log(LOG_WARNING,
692                        "Got too many link rel=related in a Bookmark.");
693         }
694
695         $linkEl = $relLinkEls[0];
696
697         $url = $linkEl->getAttribute('href');
698
699         $tags = array();
700
701         foreach ($activity->categories as $category) {
702             $tags[] = common_canonical_tag($category->term);
703         }
704
705         if (!empty($activity->time)) {
706             $options['created'] = common_sql_date($activity->time);
707         }
708
709         // Fill in location if available
710
711         $location = $activity->context->location;
712
713         if ($location) {
714             $options['lat'] = $location->lat;
715             $options['lon'] = $location->lon;
716             if ($location->location_id) {
717                 $options['location_ns'] = $location->location_ns;
718                 $options['location_id'] = $location->location_id;
719             }
720         }
721
722         $replies = $activity->context->attention;
723
724         $options['groups']  = array();
725         $options['replies'] = array();
726
727         foreach ($replies as $replyURI) {
728             $other = Profile::fromURI($replyURI);
729             if (!empty($other)) {
730                 $options['replies'][] = $replyURI;
731             } else {
732                 $group = User_group::staticGet('uri', $replyURI);
733                 if (!empty($group)) {
734                     $options['groups'][] = $replyURI;
735                 }
736             }
737         }
738
739         // Maintain direct reply associations
740         // @fixme what about conversation ID?
741
742         if (!empty($activity->context->replyToID)) {
743             $orig = Notice::staticGet('uri',
744                                       $activity->context->replyToID);
745             if (!empty($orig)) {
746                 $options['reply_to'] = $orig->id;
747             }
748         }
749
750         return Bookmark::saveNew($profile,
751                                  $bookmark->title,
752                                  $url,
753                                  $tags,
754                                  $bookmark->summary,
755                                  $options);
756     }
757
758     /**
759      * Test if an activity represents posting a bookmark
760      *
761      * @param Activity $activity Activity to test
762      *
763      * @return true if it's a Post of a Bookmark, else false
764      */
765
766     static private function _isPostBookmark($activity)
767     {
768         return ($activity->verb == ActivityVerb::POST &&
769                 $activity->objects[0]->type == ActivityObject::BOOKMARK);
770     }
771 }
772