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