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