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