]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Bookmark/BookmarkPlugin.php
Bookmark uses Plugin::path()
[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($this->path('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                                     'class' => 'bookmark-title entry-title'),
256                               $nb->title);
257                 $out->elementEnd('h3');
258
259                 $countUrl = common_local_url('noticebyurl',
260                                              array('id' => $att->id));
261
262                 $out->element('a', array('class' => 'bookmark-notice-count',
263                                          'href' => $countUrl),
264                               $att->noticeCount());
265             }
266
267             // Replies look like "for:" tags
268
269             $replies = $nli->notice->getReplies();
270             $tags = $nli->notice->getTags();
271
272             if (!empty($replies) || !empty($tags)) {
273
274                 $out->elementStart('ul', array('class' => 'bookmark-tags'));
275             
276                 foreach ($replies as $reply) {
277                     $other = Profile::staticGet('id', $reply);
278                     $out->elementStart('li');
279                     $out->element('a', array('rel' => 'tag',
280                                              'href' => $other->profileurl,
281                                              'title' => $other->getBestName()),
282                                   sprintf('for:%s', $other->nickname));
283                     $out->elementEnd('li');
284                     $out->text(' ');
285                 }
286
287                 foreach ($tags as $tag) {
288                     $out->elementStart('li');
289                     $out->element('a', 
290                                   array('rel' => 'tag',
291                                         'href' => Notice_tag::url($tag)),
292                                   $tag);
293                     $out->elementEnd('li');
294                     $out->text(' ');
295                 }
296
297                 $out->elementEnd('ul');
298             }
299
300             if (!empty($nb->description)) {
301                 $out->element('p',
302                               array('class' => 'bookmark-description'),
303                               $nb->description);
304             }
305
306             if (common_config('attachments', 'show_thumbs')) {
307                 $haveThumbs = false;
308                 foreach ($atts as $check) {
309                     $thumbnail = File_thumbnail::staticGet('file_id', $check->id);
310                     if (!empty($thumbnail)) {
311                         $haveThumbs = true;
312                         break;
313                     }
314                 }
315                 if ($haveThumbs) {
316                     $al = new InlineAttachmentList($notice, $out);
317                     $al->show();
318                 }
319             }
320
321             $out->elementStart('div', array('class' => 'bookmark-info entry-content'));
322
323             $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
324
325             $out->element('img', 
326                           array('src' => ($avatar) ?
327                                 $avatar->displayUrl() :
328                                 Avatar::defaultImage(AVATAR_MINI_SIZE),
329                                 'class' => 'avatar photo bookmark-avatar',
330                                 'width' => AVATAR_MINI_SIZE,
331                                 'height' => AVATAR_MINI_SIZE,
332                                 'alt' => $profile->getBestName()));
333
334             $out->raw('&nbsp;');
335
336             $out->element('a', 
337                           array('href' => $profile->profileurl,
338                                 'title' => $profile->getBestName()),
339                           $profile->nickname);
340
341             $nli->showNoticeLink();
342             $nli->showNoticeSource();
343             $nli->showNoticeLocation();
344             $nli->showContext();
345             $nli->showRepeat();
346
347             $out->elementEnd('div');
348
349             $nli->showNoticeOptions();
350
351             return false;
352         }
353         return true;
354     }
355
356     /**
357      * Render a notice as a Bookmark object
358      *
359      * @param Notice         $notice  Notice to render
360      * @param ActivityObject &$object Empty object to fill
361      *
362      * @return boolean hook value
363      */
364      
365     function onStartActivityObjectFromNotice($notice, &$object)
366     {
367         common_log(LOG_INFO,
368                    "Checking {$notice->uri} to see if it's a bookmark.");
369
370         $nb = Bookmark::getByNotice($notice);
371                                          
372         if (!empty($nb)) {
373
374             common_log(LOG_INFO,
375                        "Formatting notice {$notice->uri} as a bookmark.");
376
377             $object->id      = $notice->uri;
378             $object->type    = ActivityObject::BOOKMARK;
379             $object->title   = $nb->title;
380             $object->summary = $nb->description;
381             $object->link    = $notice->bestUrl();
382
383             // Attributes of the URL
384
385             $attachments = $notice->attachments();
386
387             if (count($attachments) != 1) {
388                 throw new ServerException(_('Bookmark notice with the '.
389                                             'wrong number of attachments.'));
390             }
391
392             $target = $attachments[0];
393
394             $attrs = array('rel' => 'related',
395                            'href' => $target->url);
396
397             if (!empty($target->title)) {
398                 $attrs['title'] = $target->title;
399             }
400
401             $object->extra[] = array('link', $attrs, null);
402                                                    
403             // Attributes of the thumbnail, if any
404
405             $thumbnail = $target->getThumbnail();
406
407             if (!empty($thumbnail)) {
408                 $tattrs = array('rel' => 'preview',
409                                 'href' => $thumbnail->url);
410
411                 if (!empty($thumbnail->width)) {
412                     $tattrs['media:width'] = $thumbnail->width;
413                 }
414
415                 if (!empty($thumbnail->height)) {
416                     $tattrs['media:height'] = $thumbnail->height;
417                 }
418
419                 $object->extra[] = array('link', $attrs, null);
420             }
421
422             return false;
423         }
424
425         return true;
426     }
427
428     /**
429      * Add our two queue handlers to the queue manager
430      *
431      * @param QueueManager $qm current queue manager
432      * 
433      * @return boolean hook value
434      */
435
436     function onEndInitializeQueueManager($qm)
437     {
438         $qm->connect('dlcsback', 'DeliciousBackupImporter');
439         $qm->connect('dlcsbkmk', 'DeliciousBookmarkImporter');
440         return true;
441     }
442
443     /**
444      * Plugin version data
445      *
446      * @param array &$versions array of version data
447      * 
448      * @return value
449      */
450
451     function onPluginVersion(&$versions)
452     {
453         $versions[] = array('name' => 'Sample',
454                             'version' => self::VERSION,
455                             'author' => 'Evan Prodromou',
456                             'homepage' => 'http://status.net/wiki/Plugin:Bookmark',
457                             'rawdescription' =>
458                             _m('Simple extension for supporting bookmarks.'));
459         return true;
460     }
461
462     /**
463      * Load our document if requested
464      *
465      * @param string &$title  Title to fetch
466      * @param string &$output HTML to output
467      *
468      * @return boolean hook value
469      */
470
471     function onStartLoadDoc(&$title, &$output)
472     {
473         if ($title == 'bookmarklet') {
474             $filename = INSTALLDIR.'/plugins/Bookmark/bookmarklet';
475
476             $c      = file_get_contents($filename);
477             $output = common_markup_to_html($c);
478             return false; // success!
479         }
480
481         return true;
482     }
483
484     /**
485      * Handle a posted bookmark from PuSH
486      *
487      * @param Activity        $activity activity to handle
488      * @param Ostatus_profile $oprofile Profile for the feed
489      *
490      * @return boolean hook value
491      */
492
493     function onStartHandleFeedEntryWithProfile($activity, $oprofile)
494     {
495         common_log(LOG_INFO, "BookmarkPlugin called for new feed entry.");
496
497         if (self::_isPostBookmark($activity)) {
498
499             common_log(LOG_INFO, 
500                        "Importing activity {$activity->id} as a bookmark.");
501
502             $author = $oprofile->checkAuthorship($activity);
503
504             if (empty($author)) {
505                 throw new ClientException(_('Can\'t get author for activity.'));
506             }
507
508             self::_postRemoteBookmark($author,
509                                       $activity);
510
511             return false;
512         }
513
514         return true;
515     }
516
517     /**
518      * Handle a posted bookmark from Salmon
519      *
520      * @param Activity $activity activity to handle
521      * @param mixed    $target   user or group targeted
522      *
523      * @return boolean hook value
524      */
525
526     function onStartHandleSalmonTarget($activity, $target)
527     {
528         if (self::_isPostBookmark($activity)) {
529
530             $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap.");
531
532             if ($target instanceof User_group) {
533                 $uri = $target->getUri();
534                 if (!in_array($uri, $activity->context->attention)) {
535                     throw new ClientException(_("Bookmark not posted ".
536                                                 "to this group."));
537                 }
538             } else if ($target instanceof User) {
539                 $uri      = $target->uri;
540                 $original = null;
541                 if (!empty($activity->context->replyToID)) {
542                     $original = Notice::staticGet('uri', 
543                                                   $activity->context->replyToID); 
544                 }
545                 if (!in_array($uri, $activity->context->attention) &&
546                     (empty($original) ||
547                      $original->profile_id != $target->id)) {
548                     throw new ClientException(_("Bookmark not posted ".
549                                                 "to this user."));
550                 }
551             } else {
552                 throw new ServerException(_("Don't know how to handle ".
553                                             "this kind of target."));
554             }
555
556             $author = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
557
558             self::_postRemoteBookmark($author,
559                                       $activity);
560
561             return false;
562         }
563
564         return true;
565     }
566
567     /**
568      * Handle bookmark posted via AtomPub
569      *
570      * @param Activity &$activity Activity that was posted
571      * @param User     $user      User that posted it
572      * @param Notice   &$notice   Resulting notice
573      *
574      * @return boolean hook value
575      */
576
577     function onStartAtomPubNewActivity(&$activity, $user, &$notice)
578     {
579         if (self::_isPostBookmark($activity)) {
580             $options = array('source' => 'atompub');
581             $notice  = self::_postBookmark($user->getProfile(),
582                                            $activity,
583                                            $options);
584             return false;
585         }
586
587         return true;
588     }
589
590     /**
591      * Handle bookmark imported from a backup file
592      *
593      * @param User           $user     User to import for
594      * @param ActivityObject $author   Original author per import file
595      * @param Activity       $activity Activity to import
596      * @param boolean        $trusted  Is this a trusted user?
597      * @param boolean        &$done    Is this done (success or unrecoverable error)
598      *
599      * @return boolean hook value
600      */
601
602     function onStartImportActivity($user, $author, $activity, $trusted, &$done)
603     {
604         if (self::_isPostBookmark($activity)) {
605
606             $bookmark = $activity->objects[0];
607
608             $this->log(LOG_INFO,
609                        'Importing Bookmark ' . $bookmark->id . 
610                        ' for user ' . $user->nickname);
611
612             $options = array('uri' => $bookmark->id,
613                              'url' => $bookmark->link,
614                              'source' => 'restore');
615
616             $saved = self::_postBookmark($user->getProfile(), $activity, $options);
617
618             if (!empty($saved)) {
619                 $done = true;
620             }
621
622             return false;
623         }
624
625         return true;
626     }
627
628     /**
629      * Show a link to our delicious import page on profile settings form
630      *
631      * @param Action $action Profile settings action being shown
632      *
633      * @return boolean hook value
634      */
635
636     function onEndProfileSettingsActions($action)
637     {
638         $user = common_current_user();
639         
640         if (!empty($user) && $user->hasRight(self::IMPORTDELICIOUS)) {
641             $action->elementStart('li');
642             $action->element('a',
643                              array('href' => common_local_url('importdelicious')),
644                              _('Import del.icio.us bookmarks'));
645             $action->elementEnd('li');
646         }
647
648         return true;
649     }
650
651     /**
652      * Output our CSS class for bookmark notice list elements
653      *
654      * @param NoticeListItem $nli The item being shown
655      *
656      * @return boolean hook value
657      */
658
659     function onStartOpenNoticeListItemElement($nli)
660     {
661         $nb = Bookmark::getByNotice($nli->notice);
662         if (!empty($nb)) {
663             $id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id;
664             $nli->out->elementStart('li', array('class' => 'hentry notice bookmark',
665                                                  'id' => 'notice-' . $id));
666             Event::handle('EndOpenNoticeListItemElement', array($nli));
667             return false;
668         }
669         return true;
670     }
671
672     /**
673      * Save a remote bookmark (from Salmon or PuSH)
674      *
675      * @param Ostatus_profile $author   Author of the bookmark
676      * @param Activity        $activity Activity to save
677      *
678      * @return Notice resulting notice.
679      */
680
681     static private function _postRemoteBookmark(Ostatus_profile $author,
682                                                 Activity $activity)
683     {
684         $bookmark = $activity->objects[0];
685
686         $options = array('uri' => $bookmark->id,
687                          'url' => $bookmark->link,
688                          'is_local' => Notice::REMOTE_OMB,
689                          'source' => 'ostatus');
690         
691         return self::_postBookmark($author->localProfile(), $activity, $options);
692     }
693
694     /**
695      * Save a bookmark from an activity
696      *
697      * @param Profile  $profile  Profile to use as author
698      * @param Activity $activity Activity to save
699      * @param array    $options  Options to pass to bookmark-saving code
700      *
701      * @return Notice resulting notice
702      */
703
704     static private function _postBookmark(Profile $profile,
705                                           Activity $activity,
706                                           $options=array())
707     {
708         $bookmark = $activity->objects[0];
709
710         $relLinkEls = ActivityUtils::getLinks($bookmark->element, 'related');
711
712         if (count($relLinkEls) < 1) {
713             throw new ClientException(_('Expected exactly 1 link '.
714                                         'rel=related in a Bookmark.'));
715         }
716
717         if (count($relLinkEls) > 1) {
718             common_log(LOG_WARNING,
719                        "Got too many link rel=related in a Bookmark.");
720         }
721
722         $linkEl = $relLinkEls[0];
723
724         $url = $linkEl->getAttribute('href');
725
726         $tags = array();
727
728         foreach ($activity->categories as $category) {
729             $tags[] = common_canonical_tag($category->term);
730         }
731
732         if (!empty($activity->time)) {
733             $options['created'] = common_sql_date($activity->time);
734         }
735
736         // Fill in location if available
737
738         $location = $activity->context->location;
739
740         if ($location) {
741             $options['lat'] = $location->lat;
742             $options['lon'] = $location->lon;
743             if ($location->location_id) {
744                 $options['location_ns'] = $location->location_ns;
745                 $options['location_id'] = $location->location_id;
746             }
747         }
748
749         $replies = $activity->context->attention;
750
751         $options['groups']  = array();
752         $options['replies'] = array();
753
754         foreach ($replies as $replyURI) {
755             $other = Profile::fromURI($replyURI);
756             if (!empty($other)) {
757                 $options['replies'][] = $replyURI;
758             } else {
759                 $group = User_group::staticGet('uri', $replyURI);
760                 if (!empty($group)) {
761                     $options['groups'][] = $replyURI;
762                 }
763             }
764         }
765
766         // Maintain direct reply associations
767         // @fixme what about conversation ID?
768
769         if (!empty($activity->context->replyToID)) {
770             $orig = Notice::staticGet('uri',
771                                       $activity->context->replyToID);
772             if (!empty($orig)) {
773                 $options['reply_to'] = $orig->id;
774             }
775         }
776
777         return Bookmark::saveNew($profile,
778                                  $bookmark->title,
779                                  $url,
780                                  $tags,
781                                  $bookmark->summary,
782                                  $options);
783     }
784
785     /**
786      * Test if an activity represents posting a bookmark
787      *
788      * @param Activity $activity Activity to test
789      *
790      * @return true if it's a Post of a Bookmark, else false
791      */
792
793     static private function _isPostBookmark($activity)
794     {
795         return ($activity->verb == ActivityVerb::POST &&
796                 $activity->objects[0]->type == ActivityObject::BOOKMARK);
797     }
798 }