]> 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
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                              _('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             $nli->out->elementStart('li', array('class' => 'hentry notice bookmark',
298                                                  'id' => 'notice-' . $id));
299             Event::handle('EndOpenNoticeListItemElement', array($nli));
300             return false;
301         }
302         return true;
303     }
304
305     /**
306      * Save a remote bookmark (from Salmon or PuSH)
307      *
308      * @param Ostatus_profile $author   Author of the bookmark
309      * @param Activity        $activity Activity to save
310      *
311      * @return Notice resulting notice.
312      */
313
314     static private function _postRemoteBookmark(Ostatus_profile $author,
315                                                 Activity $activity)
316     {
317         $bookmark = $activity->objects[0];
318
319         $options = array('uri' => $bookmark->id,
320                          'url' => $bookmark->link,
321                          'is_local' => Notice::REMOTE_OMB,
322                          'source' => 'ostatus');
323         
324         return self::_postBookmark($author->localProfile(), $activity, $options);
325     }
326
327     /**
328      * Test if an activity represents posting a bookmark
329      *
330      * @param Activity $activity Activity to test
331      *
332      * @return true if it's a Post of a Bookmark, else false
333      */
334
335     static private function _isPostBookmark($activity)
336     {
337         return ($activity->verb == ActivityVerb::POST &&
338                 $activity->objects[0]->type == ActivityObject::BOOKMARK);
339     }
340
341     function types()
342     {
343         return array(ActivityObject::BOOKMARK);
344     }
345
346     /**
347      * When a notice is deleted, delete the related Bookmark
348      *
349      * @param Notice $notice Notice being deleted
350      * 
351      * @return boolean hook value
352      */
353
354     function deleteRelated($notice)
355     {
356         $nb = Bookmark::getByNotice($notice);
357
358         if (!empty($nb)) {
359             $nb->delete();
360         }
361
362         return true;
363     }
364
365     /**
366      * Save a bookmark from an activity
367      *
368      * @param Activity $activity Activity to save
369      * @param Profile  $profile  Profile to use as author
370      * @param array    $options  Options to pass to bookmark-saving code
371      *
372      * @return Notice resulting notice
373      */
374
375     function saveNoticeFromActivity($activity, $profile, $options=array())
376     {
377         $bookmark = $activity->objects[0];
378
379         $relLinkEls = ActivityUtils::getLinks($bookmark->element, 'related');
380
381         if (count($relLinkEls) < 1) {
382             throw new ClientException(_('Expected exactly 1 link '.
383                                         'rel=related in a Bookmark.'));
384         }
385
386         if (count($relLinkEls) > 1) {
387             common_log(LOG_WARNING,
388                        "Got too many link rel=related in a Bookmark.");
389         }
390
391         $linkEl = $relLinkEls[0];
392
393         $url = $linkEl->getAttribute('href');
394
395         $tags = array();
396
397         foreach ($activity->categories as $category) {
398             $tags[] = common_canonical_tag($category->term);
399         }
400
401         if (!empty($activity->time)) {
402             $options['created'] = common_sql_date($activity->time);
403         }
404
405         // Fill in location if available
406
407         $location = $activity->context->location;
408
409         if ($location) {
410             $options['lat'] = $location->lat;
411             $options['lon'] = $location->lon;
412             if ($location->location_id) {
413                 $options['location_ns'] = $location->location_ns;
414                 $options['location_id'] = $location->location_id;
415             }
416         }
417
418         $replies = $activity->context->attention;
419
420         $options['groups']  = array();
421         $options['replies'] = array();
422
423         foreach ($replies as $replyURI) {
424             $other = Profile::fromURI($replyURI);
425             if (!empty($other)) {
426                 $options['replies'][] = $replyURI;
427             } else {
428                 $group = User_group::staticGet('uri', $replyURI);
429                 if (!empty($group)) {
430                     $options['groups'][] = $replyURI;
431                 }
432             }
433         }
434
435         // Maintain direct reply associations
436         // @fixme what about conversation ID?
437
438         if (!empty($activity->context->replyToID)) {
439             $orig = Notice::staticGet('uri',
440                                       $activity->context->replyToID);
441             if (!empty($orig)) {
442                 $options['reply_to'] = $orig->id;
443             }
444         }
445
446         return Bookmark::saveNew($profile,
447                                  $bookmark->title,
448                                  $url,
449                                  $tags,
450                                  $bookmark->summary,
451                                  $options);
452     }
453
454     function activityObjectFromNotice($notice)
455     {
456         assert($this->isMyNotice($notice));
457
458         common_log(LOG_INFO,
459                    "Formatting notice {$notice->uri} as a bookmark.");
460
461         $object = new ActivityObject();
462
463         $object->id      = $notice->uri;
464         $object->type    = ActivityObject::BOOKMARK;
465         $object->title   = $nb->title;
466         $object->summary = $nb->description;
467         $object->link    = $notice->bestUrl();
468
469         // Attributes of the URL
470
471         $attachments = $notice->attachments();
472
473         if (count($attachments) != 1) {
474             throw new ServerException(_('Bookmark notice with the '.
475                                         'wrong number of attachments.'));
476         }
477
478         $target = $attachments[0];
479
480         $attrs = array('rel' => 'related',
481                        'href' => $target->url);
482
483         if (!empty($target->title)) {
484             $attrs['title'] = $target->title;
485         }
486
487         $object->extra[] = array('link', $attrs, null);
488                                                    
489         // Attributes of the thumbnail, if any
490
491         $thumbnail = $target->getThumbnail();
492
493         if (!empty($thumbnail)) {
494             $tattrs = array('rel' => 'preview',
495                             'href' => $thumbnail->url);
496
497             if (!empty($thumbnail->width)) {
498                 $tattrs['media:width'] = $thumbnail->width;
499             }
500
501             if (!empty($thumbnail->height)) {
502                 $tattrs['media:height'] = $thumbnail->height;
503             }
504
505             $object->extra[] = array('link', $attrs, null);
506         }
507
508         return $object;
509     }
510
511     /**
512      * @fixme WARNING WARNING WARNING this opens a 'div' that is apparently closed by MicroAppPlugin
513      * @fixme that's probably wrong?
514      *
515      * @param Notice $notice
516      * @param HTMLOutputter $out
517      */
518     function showNotice($notice, $out)
519     {
520         $nb = Bookmark::getByNotice($notice);
521
522         $profile = $notice->getProfile();
523
524         $atts = $notice->attachments();
525
526         if (count($atts) < 1) {
527             // Something wrong; let default code deal with it.
528             throw new Exception("That can't be right.");
529         }
530
531         $att = $atts[0];
532
533         // XXX: only show the bookmark URL for non-single-page stuff
534
535         if ($out instanceof ShowbookmarkAction) {
536         } else {
537             $out->elementStart('h3');
538             $out->element('a',
539                           array('href' => $att->url,
540                                 'class' => 'bookmark-title entry-title'),
541                           $nb->title);
542             $out->elementEnd('h3');
543
544             $countUrl = common_local_url('noticebyurl',
545                                          array('id' => $att->id));
546
547             $out->element('a', array('class' => 'bookmark-notice-count',
548                                      'href' => $countUrl),
549                           $att->noticeCount());
550         }
551
552         // Replies look like "for:" tags
553
554         $replies = $notice->getReplies();
555         $tags = $notice->getTags();
556
557         if (!empty($replies) || !empty($tags)) {
558
559             $out->elementStart('ul', array('class' => 'bookmark-tags'));
560             
561             foreach ($replies as $reply) {
562                 $other = Profile::staticGet('id', $reply);
563                 $out->elementStart('li');
564                 $out->element('a', array('rel' => 'tag',
565                                          'href' => $other->profileurl,
566                                          'title' => $other->getBestName()),
567                               sprintf('for:%s', $other->nickname));
568                 $out->elementEnd('li');
569                 $out->text(' ');
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         return _m('Bookmark');
643     }
644 }