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