]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Bookmark/BookmarkPlugin.php
Merge branch '0.9.x' into socialbookmark
[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
244             $out->elementStart('ul', array('class' => 'bookmark_tags'));
245             
246             // Replies look like "for:" tags
247
248             $replies = $nli->notice->getReplies();
249
250             if (!empty($replies)) {
251                 foreach ($replies as $reply) {
252                     $other = Profile::staticGet('id', $reply);
253                     $out->elementStart('li');
254                     $out->element('a', array('rel' => 'tag',
255                                              'href' => $other->profileurl,
256                                              'title' => $other->getBestName()),
257                                   sprintf('for:%s', $other->nickname));
258                     $out->elementEnd('li');
259                     $out->text(' ');
260                 }
261             }
262
263             $tags = $nli->notice->getTags();
264
265             foreach ($tags as $tag) {
266                 $out->elementStart('li');
267                 $out->element('a', 
268                               array('rel' => 'tag',
269                                     'href' => Notice_tag::url($tag)),
270                               $tag);
271                 $out->elementEnd('li');
272                 $out->text(' ');
273             }
274
275             $out->elementEnd('ul');
276
277             $out->element('p',
278                           array('class' => 'bookmark_description'),
279                           $nb->description);
280
281             if (common_config('attachments', 'show_thumbs')) {
282                 $al = new InlineAttachmentList($notice, $out);
283                 $al->show();
284             }
285
286             $out->elementStart('p', array('style' => 'float: left'));
287
288             $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
289
290             $out->element('img', array('src' => ($avatar) ?
291                                        $avatar->displayUrl() :
292                                        Avatar::defaultImage(AVATAR_MINI_SIZE),
293                                        'class' => 'avatar photo bookmark_avatar',
294                                        'width' => AVATAR_MINI_SIZE,
295                                        'height' => AVATAR_MINI_SIZE,
296                                        'alt' => $profile->getBestName()));
297             $out->raw('&nbsp;');
298             $out->element('a', array('href' => $profile->profileurl,
299                                      'title' => $profile->getBestName()),
300                           $profile->nickname);
301
302             $nli->showNoticeLink();
303             $nli->showNoticeSource();
304             $nli->showNoticeLocation();
305             $nli->showContext();
306             $nli->showRepeat();
307
308             $out->elementEnd('p');
309
310             $nli->showNoticeOptions();
311
312             return false;
313         }
314         return true;
315     }
316
317     /**
318      * Render a notice as a Bookmark object
319      *
320      * @param Notice         $notice  Notice to render
321      * @param ActivityObject &$object Empty object to fill
322      *
323      * @return boolean hook value
324      */
325      
326     function onStartActivityObjectFromNotice($notice, &$object)
327     {
328         common_log(LOG_INFO,
329                    "Checking {$notice->uri} to see if it's a bookmark.");
330
331         $nb = Bookmark::getByNotice($notice);
332                                          
333         if (!empty($nb)) {
334
335             common_log(LOG_INFO,
336                        "Formatting notice {$notice->uri} as a bookmark.");
337
338             $object->id      = $notice->uri;
339             $object->type    = ActivityObject::BOOKMARK;
340             $object->title   = $nb->title;
341             $object->summary = $nb->description;
342             $object->link    = $notice->bestUrl();
343
344             // Attributes of the URL
345
346             $attachments = $notice->attachments();
347
348             if (count($attachments) != 1) {
349                 throw new ServerException(_('Bookmark notice with the '.
350                                             'wrong number of attachments.'));
351             }
352
353             $target = $attachments[0];
354
355             $attrs = array('rel' => 'related',
356                            'href' => $target->url);
357
358             if (!empty($target->title)) {
359                 $attrs['title'] = $target->title;
360             }
361
362             $object->extra[] = array('link', $attrs, null);
363                                                    
364             // Attributes of the thumbnail, if any
365
366             $thumbnail = $target->getThumbnail();
367
368             if (!empty($thumbnail)) {
369                 $tattrs = array('rel' => 'preview',
370                                 'href' => $thumbnail->url);
371
372                 if (!empty($thumbnail->width)) {
373                     $tattrs['media:width'] = $thumbnail->width;
374                 }
375
376                 if (!empty($thumbnail->height)) {
377                     $tattrs['media:height'] = $thumbnail->height;
378                 }
379
380                 $object->extra[] = array('link', $attrs, null);
381             }
382
383             return false;
384         }
385
386         return true;
387     }
388
389     /**
390      * Add our two queue handlers to the queue manager
391      *
392      * @param QueueManager $qm current queue manager
393      * 
394      * @return boolean hook value
395      */
396
397     function onEndInitializeQueueManager($qm)
398     {
399         $qm->connect('dlcsback', 'DeliciousBackupImporter');
400         $qm->connect('dlcsbkmk', 'DeliciousBookmarkImporter');
401         return true;
402     }
403
404     /**
405      * Plugin version data
406      *
407      * @param array &$versions array of version data
408      * 
409      * @return value
410      */
411
412     function onPluginVersion(&$versions)
413     {
414         $versions[] = array('name' => 'Sample',
415                             'version' => self::VERSION,
416                             'author' => 'Evan Prodromou',
417                             'homepage' => 'http://status.net/wiki/Plugin:Bookmark',
418                             'rawdescription' =>
419                             _m('Simple extension for supporting bookmarks.'));
420         return true;
421     }
422
423     /**
424      * Load our document if requested
425      *
426      * @param string &$title  Title to fetch
427      * @param string &$output HTML to output
428      *
429      * @return boolean hook value
430      */
431
432     function onStartLoadDoc(&$title, &$output)
433     {
434         if ($title == 'bookmarklet') {
435             $filename = INSTALLDIR.'/plugins/Bookmark/bookmarklet';
436
437             $c      = file_get_contents($filename);
438             $output = common_markup_to_html($c);
439             return false; // success!
440         }
441
442         return true;
443     }
444
445     /**
446      * Handle a posted bookmark from PuSH
447      *
448      * @param Activity        $activity activity to handle
449      * @param Ostatus_profile $oprofile Profile for the feed
450      *
451      * @return boolean hook value
452      */
453
454     function onStartHandleFeedEntryWithProfile($activity, $oprofile) {
455
456         common_log(LOG_INFO, "BookmarkPlugin called for new feed entry.");
457
458         if (self::_isPostBookmark($activity)) {
459
460             common_log(LOG_INFO, "Importing activity {$activity->id} as a bookmark.");
461
462             $author = $oprofile->checkAuthorship($activity);
463
464             if (empty($author)) {
465                 throw new ClientException(_('Can\'t get author for activity.'));
466             }
467
468             self::_postRemoteBookmark($author,
469                                       $activity);
470
471             return false;
472         }
473
474         return true;
475     }
476
477     /**
478      * Handle a posted bookmark from Salmon
479      *
480      * @param Activity $activity activity to handle
481      * @param mixed    $target   user or group targeted
482      *
483      * @return boolean hook value
484      */
485
486     function onStartHandleSalmonTarget($activity, $target) {
487
488         if (self::_isPostBookmark($activity)) {
489
490             $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap.");
491
492             if ($target instanceof User_group) {
493                 $uri = $target->getUri();
494                 if (!in_array($uri, $activity->context->attention)) {
495                     throw new ClientException(_("Bookmark not posted to this group."));
496                 }
497             } else if ($target instanceof User) {
498                 $uri = $target->uri;
499                 $original = null;
500                 if (!empty($activity->context->replyToID)) {
501                     $original = Notice::staticGet('uri', $activity->context->replyToID); 
502                 }
503                 if (!in_array($uri, $activity->context->attention) &&
504                     (empty($original) || $original->profile_id != $target->id)) {
505                     throw new ClientException(_("Bookmark not posted to this user."));
506                 }
507             } else {
508                 throw new ServerException(_("Don't know how to handle this kind of target."));
509             }
510
511             $author = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
512
513             self::_postRemoteBookmark($author,
514                                       $activity);
515
516             return false;
517         }
518
519         return true;
520     }
521
522     function onStartAtomPubNewActivity(&$activity, $user, &$notice)
523     {
524         if (self::_isPostBookmark($activity)) {
525             $options = array('source' => 'atompub');
526             $notice = self::_postBookmark($user->getProfile(), $activity, $options);
527             return false;
528         }
529
530         return true;
531     }
532
533     function onStartImportActivity($user, $author, $activity, $trusted, &$done) {
534
535         if (self::_isPostBookmark($activity)) {
536
537             $bookmark = $activity->objects[0];
538
539             $this->log(LOG_INFO, 'Importing Bookmark ' . $bookmark->id . ' for user ' . $user->nickname);
540
541             $options = array('uri' => $bookmark->id,
542                              'url' => $bookmark->link,
543                              'source' => 'restore');
544
545             $saved = self::_postBookmark($user->getProfile(), $activity, $options);
546
547             if (!empty($saved)) {
548                 $done = true;
549             }
550
551             return false;
552         }
553
554         return true;
555     }
556
557     static private function _postRemoteBookmark(Ostatus_profile $author, Activity $activity)
558     {
559         $bookmark = $activity->objects[0];
560
561         $options = array('uri' => $bookmark->id,
562                          'url' => $bookmark->link,
563                          'is_local' => Notice::REMOTE_OMB,
564                          'source' => 'ostatus');
565         
566         return self::_postBookmark($author->localProfile(), $activity, $options);
567     }
568
569     static private function _postBookmark(Profile $profile, Activity $activity, $options=array())
570     {
571         $bookmark = $activity->objects[0];
572
573         $relLinkEls = ActivityUtils::getLinks($bookmark->element, 'related');
574
575         if (count($relLinkEls) < 1) {
576             throw new ClientException(_('Expected exactly 1 link rel=related in a Bookmark.'));
577         }
578
579         if (count($relLinkEls) > 1) {
580             common_log(LOG_WARNING, "Got too many link rel=related in a Bookmark.");
581         }
582
583         $linkEl = $relLinkEls[0];
584
585         $url = $linkEl->getAttribute('href');
586
587         $tags = array();
588
589         foreach ($activity->categories as $category) {
590             $tags[] = common_canonical_tag($category->term);
591         }
592
593         if (!empty($activity->time)) {
594             $options['created'] = common_sql_date($activity->time);
595         }
596
597         // Fill in location if available
598
599         $location = $activity->context->location;
600
601         if ($location) {
602             $options['lat'] = $location->lat;
603             $options['lon'] = $location->lon;
604             if ($location->location_id) {
605                 $options['location_ns'] = $location->location_ns;
606                 $options['location_id'] = $location->location_id;
607             }
608         }
609
610         $replies = $activity->context->attention;
611
612         $options['groups'] = array();
613         $options['replies'] = array();
614
615         foreach ($replies as $replyURI) {
616             $other = Profile::fromURI($replyURI);
617             if (!empty($other)) {
618                 $options['replies'][] = $replyURI;
619             } else {
620                 $group = User_group::staticGet('uri', $replyURI);
621                 if (!empty($group)) {
622                     $options['groups'][] = $replyURI;
623                 }
624             }
625         }
626
627         // Maintain direct reply associations
628         // @fixme what about conversation ID?
629
630         if (!empty($activity->context->replyToID)) {
631             $orig = Notice::staticGet('uri',
632                                       $activity->context->replyToID);
633             if (!empty($orig)) {
634                 $options['reply_to'] = $orig->id;
635             }
636         }
637
638         return Bookmark::saveNew($profile,
639                                  $bookmark->title,
640                                  $url,
641                                  $tags,
642                                  $bookmark->summary,
643                                  $options);
644     }
645
646     static private function _isPostBookmark($activity)
647     {
648         return ($activity->verb == ActivityVerb::POST &&
649                 $activity->objects[0]->type == ActivityObject::BOOKMARK);
650     }
651 }
652