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