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