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