]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Bookmark/BookmarkPlugin.php
Make AtomPub work for bookmarks
[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             include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
161             return false;
162         case 'Bookmark':
163             include_once $dir.'/'.$cls.'.php';
164             return false;
165         case 'BookmarkForm':
166         case 'DeliciousBackupImporter':
167         case 'DeliciousBookmarkImporter':
168             include_once $dir.'/'.strtolower($cls).'.php';
169             return false;
170         default:
171             return true;
172         }
173     }
174
175     /**
176      * Map URLs to actions
177      *
178      * @param Net_URL_Mapper $m path-to-action mapper
179      *
180      * @return boolean hook value; true means continue processing, false means stop.
181      */
182
183     function onRouterInitialized($m)
184     {
185         $m->connect('main/bookmark/new',
186                     array('action' => 'newbookmark'),
187                     array('id' => '[0-9]+'));
188
189         $m->connect('main/bookmark/popup', array('action' => 'bookmarkpopup'));
190
191         $m->connect('bookmark/:user/:created/:crc32',
192                     array('action' => 'showbookmark'),
193                     array('user' => '[0-9]+',
194                           'created' => '[0-9]{14}',
195                           'crc32' => '[0-9a-f]{8}'));
196
197         return true;
198     }
199
200     /**
201      * Output the HTML for a bookmark in a list
202      *
203      * @param NoticeListItem $nli The list item being shown.
204      *
205      * @return boolean hook value
206      */
207
208     function onStartShowNoticeItem($nli)
209     {
210         $nb = Bookmark::getByNotice($nli->notice);
211
212         if (!empty($nb)) {
213
214             $out     = $nli->out;
215             $notice  = $nli->notice;
216             $profile = $nli->profile;
217
218             $atts = $notice->attachments();
219
220             if (count($atts) < 1) {
221                 // Something wrong; let default code deal with it.
222                 return true;
223             }
224
225             $att = $atts[0];
226
227             // XXX: only show the bookmark URL for non-single-page stuff
228
229             if ($out instanceof ShowbookmarkAction) {
230             } else {
231                 $out->elementStart('h3');
232                 $out->element('a',
233                               array('href' => $att->url),
234                               $nb->title);
235                 $out->elementEnd('h3');
236             }
237
238             $out->elementStart('ul', array('class' => 'bookmark_tags'));
239             
240             // Replies look like "for:" tags
241
242             $replies = $nli->notice->getReplies();
243
244             if (!empty($replies)) {
245                 foreach ($replies as $reply) {
246                     $other = Profile::staticGet('id', $reply);
247                     $out->elementStart('li');
248                     $out->element('a', array('rel' => 'tag',
249                                              'href' => $other->profileurl,
250                                              'title' => $other->getBestName()),
251                                   sprintf('for:%s', $other->nickname));
252                     $out->elementEnd('li');
253                     $out->text(' ');
254                 }
255             }
256
257             $tags = $nli->notice->getTags();
258
259             foreach ($tags as $tag) {
260                 $out->elementStart('li');
261                 $out->element('a', 
262                               array('rel' => 'tag',
263                                     'href' => Notice_tag::url($tag)),
264                               $tag);
265                 $out->elementEnd('li');
266                 $out->text(' ');
267             }
268
269             $out->elementEnd('ul');
270
271             $out->element('p',
272                           array('class' => 'bookmark_description'),
273                           $nb->description);
274
275             if (common_config('attachments', 'show_thumbs')) {
276                 $al = new InlineAttachmentList($notice, $out);
277                 $al->show();
278             }
279
280             $out->elementStart('p', array('style' => 'float: left'));
281
282             $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
283
284             $out->element('img', array('src' => ($avatar) ?
285                                        $avatar->displayUrl() :
286                                        Avatar::defaultImage(AVATAR_MINI_SIZE),
287                                        'class' => 'avatar photo bookmark_avatar',
288                                        'width' => AVATAR_MINI_SIZE,
289                                        'height' => AVATAR_MINI_SIZE,
290                                        'alt' => $profile->getBestName()));
291             $out->raw('&nbsp;');
292             $out->element('a', array('href' => $profile->profileurl,
293                                      'title' => $profile->getBestName()),
294                           $profile->nickname);
295
296             $nli->showNoticeLink();
297             $nli->showNoticeSource();
298             $nli->showNoticeLocation();
299             $nli->showContext();
300             $nli->showRepeat();
301
302             $out->elementEnd('p');
303
304             $nli->showNoticeOptions();
305
306             return false;
307         }
308         return true;
309     }
310
311     /**
312      * Render a notice as a Bookmark object
313      *
314      * @param Notice         $notice  Notice to render
315      * @param ActivityObject &$object Empty object to fill
316      *
317      * @return boolean hook value
318      */
319      
320     function onStartActivityObjectFromNotice($notice, &$object)
321     {
322         common_log(LOG_INFO,
323                    "Checking {$notice->uri} to see if it's a bookmark.");
324
325         $nb = Bookmark::getByNotice($notice);
326                                          
327         if (!empty($nb)) {
328
329             common_log(LOG_INFO,
330                        "Formatting notice {$notice->uri} as a bookmark.");
331
332             $object->id      = $notice->uri;
333             $object->type    = ActivityObject::BOOKMARK;
334             $object->title   = $nb->title;
335             $object->summary = $nb->description;
336             $object->link    = $notice->bestUrl();
337
338             // Attributes of the URL
339
340             $attachments = $notice->attachments();
341
342             if (count($attachments) != 1) {
343                 throw new ServerException(_('Bookmark notice with the '.
344                                             'wrong number of attachments.'));
345             }
346
347             $target = $attachments[0];
348
349             $attrs = array('rel' => 'related',
350                            'href' => $target->url);
351
352             if (!empty($target->title)) {
353                 $attrs['title'] = $target->title;
354             }
355
356             $object->extra[] = array('link', $attrs, null);
357                                                    
358             // Attributes of the thumbnail, if any
359
360             $thumbnail = $target->getThumbnail();
361
362             if (!empty($thumbnail)) {
363                 $tattrs = array('rel' => 'preview',
364                                 'href' => $thumbnail->url);
365
366                 if (!empty($thumbnail->width)) {
367                     $tattrs['media:width'] = $thumbnail->width;
368                 }
369
370                 if (!empty($thumbnail->height)) {
371                     $tattrs['media:height'] = $thumbnail->height;
372                 }
373
374                 $object->extra[] = array('link', $attrs, null);
375             }
376
377             return false;
378         }
379
380         return true;
381     }
382
383     /**
384      * Add our two queue handlers to the queue manager
385      *
386      * @param QueueManager $qm current queue manager
387      * 
388      * @return boolean hook value
389      */
390
391     function onEndInitializeQueueManager($qm)
392     {
393         $qm->connect('dlcsback', 'DeliciousBackupImporter');
394         $qm->connect('dlcsbkmk', 'DeliciousBookmarkImporter');
395         return true;
396     }
397
398     /**
399      * Plugin version data
400      *
401      * @param array &$versions array of version data
402      * 
403      * @return value
404      */
405
406     function onPluginVersion(&$versions)
407     {
408         $versions[] = array('name' => 'Sample',
409                             'version' => self::VERSION,
410                             'author' => 'Evan Prodromou',
411                             'homepage' => 'http://status.net/wiki/Plugin:Bookmark',
412                             'rawdescription' =>
413                             _m('Simple extension for supporting bookmarks.'));
414         return true;
415     }
416
417     /**
418      * Load our document if requested
419      *
420      * @param string &$title  Title to fetch
421      * @param string &$output HTML to output
422      *
423      * @return boolean hook value
424      */
425
426     function onStartLoadDoc(&$title, &$output)
427     {
428         if ($title == 'bookmarklet') {
429             $filename = INSTALLDIR.'/plugins/Bookmark/bookmarklet';
430
431             $c      = file_get_contents($filename);
432             $output = common_markup_to_html($c);
433             return false; // success!
434         }
435
436         return true;
437     }
438
439     /**
440      * Handle a posted bookmark from PuSH
441      *
442      * @param Activity        $activity activity to handle
443      * @param Ostatus_profile $oprofile Profile for the feed
444      *
445      * @return boolean hook value
446      */
447
448     function onStartHandleFeedEntryWithProfile($activity, $oprofile) {
449
450         common_log(LOG_INFO, "BookmarkPlugin called for new feed entry.");
451
452         if (self::_isPostBookmark($activity)) {
453
454             common_log(LOG_INFO, "Importing activity {$activity->id} as a bookmark.");
455
456             $author = $oprofile->checkAuthorship($activity);
457
458             if (empty($author)) {
459                 throw new ClientException(_('Can\'t get author for activity.'));
460             }
461
462             self::_postRemoteBookmark($author,
463                                       $activity);
464
465             return false;
466         }
467
468         return true;
469     }
470
471     /**
472      * Handle a posted bookmark from Salmon
473      *
474      * @param Activity $activity activity to handle
475      * @param mixed    $target   user or group targeted
476      *
477      * @return boolean hook value
478      */
479
480     function onStartHandleSalmonTarget($activity, $target) {
481
482         if (self::_isPostBookmark($activity)) {
483
484             $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap.");
485
486             if ($target instanceof User_group) {
487                 $uri = $target->getUri();
488                 if (!in_array($uri, $activity->context->attention)) {
489                     throw new ClientException(_("Bookmark not posted to this group."));
490                 }
491             } else if ($target instanceof User) {
492                 $uri = $target->uri;
493                 $original = null;
494                 if (!empty($activity->context->replyToID)) {
495                     $original = Notice::staticGet('uri', $activity->context->replyToID); 
496                 }
497                 if (!in_array($uri, $activity->context->attention) &&
498                     (empty($original) || $original->profile_id != $target->id)) {
499                     throw new ClientException(_("Bookmark not posted to this user."));
500                 }
501             } else {
502                 throw new ServerException(_("Don't know how to handle this kind of target."));
503             }
504
505             $author = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
506
507             self::_postRemoteBookmark($author,
508                                       $activity);
509
510             return false;
511         }
512
513         return true;
514     }
515
516     function onStartAtomPubNewActivity(&$activity, $user)
517     {
518         if (self::_isPostBookmark($activity)) {
519             $options = array('source' => 'atompub');
520             self::_postBookmark($user->getProfile(), $activity, $options);
521             return false;
522         }
523
524         return true;
525     }
526
527     static private function _postRemoteBookmark(Ostatus_profile $author, Activity $activity)
528     {
529         $bookmark = $activity->objects[0];
530
531         $options = array('uri' => $bookmark->id,
532                          'url' => $bookmark->link,
533                          'is_local' => Notice::REMOTE_OMB,
534                          'source' => 'ostatus');
535         
536         return self::_postBookmark($author->localProfile(), $activity, $options);
537     }
538
539     static private function _postBookmark(Profile $profile, Activity $activity, $options=array())
540     {
541         $bookmark = $activity->objects[0];
542
543         $relLinkEls = ActivityUtils::getLinks($bookmark->element, 'related');
544
545         if (count($relLinkEls) < 1) {
546             throw new ClientException(_('Expected exactly 1 link rel=related in a Bookmark.'));
547         }
548
549         if (count($relLinkEls) > 1) {
550             common_log(LOG_WARNING, "Got too many link rel=related in a Bookmark.");
551         }
552
553         $linkEl = $relLinkEls[0];
554
555         $url = $linkEl->getAttribute('href');
556
557         $tags = array();
558
559         foreach ($activity->categories as $category) {
560             $tags[] = common_canonical_tag($category->term);
561         }
562
563         if (!empty($activity->time)) {
564             $options['created'] = common_sql_date($activity->time);
565         }
566
567         // Fill in location if available
568
569         $location = $activity->context->location;
570
571         if ($location) {
572             $options['lat'] = $location->lat;
573             $options['lon'] = $location->lon;
574             if ($location->location_id) {
575                 $options['location_ns'] = $location->location_ns;
576                 $options['location_id'] = $location->location_id;
577             }
578         }
579
580         $replies = $activity->context->attention;
581
582         $options['groups'] = array();
583         $options['replies'] = array();
584
585         foreach ($replies as $replyURI) {
586             $other = Profile::fromURI($replyURI);
587             if (!empty($other)) {
588                 $options['replies'][] = $replyURI;
589             } else {
590                 $group = User_group::staticGet('uri', $replyURI);
591                 if (!empty($group)) {
592                     $options['groups'][] = $replyURI;
593                 }
594             }
595         }
596
597         // Maintain direct reply associations
598         // @fixme what about conversation ID?
599
600         if (!empty($activity->context->replyToID)) {
601             $orig = Notice::staticGet('uri',
602                                       $activity->context->replyToID);
603             if (!empty($orig)) {
604                 $options['reply_to'] = $orig->id;
605             }
606         }
607
608         return Bookmark::saveNew($profile,
609                                  $bookmark->title,
610                                  $url,
611                                  $tags,
612                                  $bookmark->summary,
613                                  $options);
614     }
615
616     static private function _isPostBookmark($activity)
617     {
618         return ($activity->verb == ActivityVerb::POST &&
619                 $activity->objects[0]->type == ActivityObject::BOOKMARK);
620     }
621 }
622