]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Bookmark/BookmarkPlugin.php
first (non-working) move to microapp structure for bookmarks
[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 Profile  $profile  Profile to use as author
369      * @param Activity $activity Activity to save
370      * @param array    $options  Options to pass to bookmark-saving code
371      *
372      * @return Notice resulting notice
373      */
374
375     function saveNoticeFromActivity($activity, $profile)
376     {
377         $options = array();
378
379         $bookmark = $activity->objects[0];
380
381         $relLinkEls = ActivityUtils::getLinks($bookmark->element, 'related');
382
383         if (count($relLinkEls) < 1) {
384             throw new ClientException(_('Expected exactly 1 link '.
385                                         'rel=related in a Bookmark.'));
386         }
387
388         if (count($relLinkEls) > 1) {
389             common_log(LOG_WARNING,
390                        "Got too many link rel=related in a Bookmark.");
391         }
392
393         $linkEl = $relLinkEls[0];
394
395         $url = $linkEl->getAttribute('href');
396
397         $tags = array();
398
399         foreach ($activity->categories as $category) {
400             $tags[] = common_canonical_tag($category->term);
401         }
402
403         if (!empty($activity->time)) {
404             $options['created'] = common_sql_date($activity->time);
405         }
406
407         // Fill in location if available
408
409         $location = $activity->context->location;
410
411         if ($location) {
412             $options['lat'] = $location->lat;
413             $options['lon'] = $location->lon;
414             if ($location->location_id) {
415                 $options['location_ns'] = $location->location_ns;
416                 $options['location_id'] = $location->location_id;
417             }
418         }
419
420         $replies = $activity->context->attention;
421
422         $options['groups']  = array();
423         $options['replies'] = array();
424
425         foreach ($replies as $replyURI) {
426             $other = Profile::fromURI($replyURI);
427             if (!empty($other)) {
428                 $options['replies'][] = $replyURI;
429             } else {
430                 $group = User_group::staticGet('uri', $replyURI);
431                 if (!empty($group)) {
432                     $options['groups'][] = $replyURI;
433                 }
434             }
435         }
436
437         // Maintain direct reply associations
438         // @fixme what about conversation ID?
439
440         if (!empty($activity->context->replyToID)) {
441             $orig = Notice::staticGet('uri',
442                                       $activity->context->replyToID);
443             if (!empty($orig)) {
444                 $options['reply_to'] = $orig->id;
445             }
446         }
447
448         return Bookmark::saveNew($profile,
449                                  $bookmark->title,
450                                  $url,
451                                  $tags,
452                                  $bookmark->summary,
453                                  $options);
454     }
455
456     function activityObjectFromNotice($notice)
457     {
458         assert($this->isMyNotice($notice));
459
460         common_log(LOG_INFO,
461                    "Formatting notice {$notice->uri} as a bookmark.");
462
463         $object = new ActivityObject();
464
465         $object->id      = $notice->uri;
466         $object->type    = ActivityObject::BOOKMARK;
467         $object->title   = $nb->title;
468         $object->summary = $nb->description;
469         $object->link    = $notice->bestUrl();
470
471         // Attributes of the URL
472
473         $attachments = $notice->attachments();
474
475         if (count($attachments) != 1) {
476             throw new ServerException(_('Bookmark notice with the '.
477                                         'wrong number of attachments.'));
478         }
479
480         $target = $attachments[0];
481
482         $attrs = array('rel' => 'related',
483                        'href' => $target->url);
484
485         if (!empty($target->title)) {
486             $attrs['title'] = $target->title;
487         }
488
489         $object->extra[] = array('link', $attrs, null);
490                                                    
491         // Attributes of the thumbnail, if any
492
493         $thumbnail = $target->getThumbnail();
494
495         if (!empty($thumbnail)) {
496             $tattrs = array('rel' => 'preview',
497                             'href' => $thumbnail->url);
498
499             if (!empty($thumbnail->width)) {
500                 $tattrs['media:width'] = $thumbnail->width;
501             }
502
503             if (!empty($thumbnail->height)) {
504                 $tattrs['media:height'] = $thumbnail->height;
505             }
506
507             $object->extra[] = array('link', $attrs, null);
508         }
509
510         return $object;
511     }
512
513     function showNotice($notice, $out)
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             throw new Exception("That can't be right.");
522         }
523
524         $att = $atts[0];
525
526         // XXX: only show the bookmark URL for non-single-page stuff
527
528         if ($out instanceof ShowbookmarkAction) {
529         } else {
530             $out->elementStart('h3');
531             $out->element('a',
532                           array('href' => $att->url,
533                                 'class' => 'bookmark-title entry-title'),
534                           $nb->title);
535             $out->elementEnd('h3');
536
537             $countUrl = common_local_url('noticebyurl',
538                                          array('id' => $att->id));
539
540             $out->element('a', array('class' => 'bookmark-notice-count',
541                                      'href' => $countUrl),
542                           $att->noticeCount());
543         }
544
545         // Replies look like "for:" tags
546
547         $replies = $nli->notice->getReplies();
548         $tags = $nli->notice->getTags();
549
550         if (!empty($replies) || !empty($tags)) {
551
552             $out->elementStart('ul', array('class' => 'bookmark-tags'));
553             
554             foreach ($replies as $reply) {
555                 $other = Profile::staticGet('id', $reply);
556                 $out->elementStart('li');
557                 $out->element('a', array('rel' => 'tag',
558                                          'href' => $other->profileurl,
559                                          'title' => $other->getBestName()),
560                               sprintf('for:%s', $other->nickname));
561                 $out->elementEnd('li');
562                 $out->text(' ');
563             }
564
565             foreach ($tags as $tag) {
566                 $out->elementStart('li');
567                 $out->element('a', 
568                               array('rel' => 'tag',
569                                     'href' => Notice_tag::url($tag)),
570                               $tag);
571                 $out->elementEnd('li');
572                 $out->text(' ');
573             }
574
575             $out->elementEnd('ul');
576         }
577
578         if (!empty($nb->description)) {
579             $out->element('p',
580                           array('class' => 'bookmark-description'),
581                           $nb->description);
582         }
583
584         if (common_config('attachments', 'show_thumbs')) {
585             $haveThumbs = false;
586             foreach ($atts as $check) {
587                 $thumbnail = File_thumbnail::staticGet('file_id', $check->id);
588                 if (!empty($thumbnail)) {
589                     $haveThumbs = true;
590                     break;
591                 }
592             }
593             if ($haveThumbs) {
594                 $al = new InlineAttachmentList($notice, $out);
595                 $al->show();
596             }
597         }
598
599         $out->elementStart('div', array('class' => 'bookmark-info entry-content'));
600
601         $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
602
603         $out->element('img', 
604                       array('src' => ($avatar) ?
605                             $avatar->displayUrl() :
606                             Avatar::defaultImage(AVATAR_MINI_SIZE),
607                             'class' => 'avatar photo bookmark-avatar',
608                             'width' => AVATAR_MINI_SIZE,
609                             'height' => AVATAR_MINI_SIZE,
610                             'alt' => $profile->getBestName()));
611
612         $out->raw('&nbsp;');
613
614         $out->element('a', 
615                       array('href' => $profile->profileurl,
616                             'title' => $profile->getBestName()),
617                       $profile->nickname);
618     }
619
620     function entryForm($out)
621     {
622         return new BookmarkForm($out);
623     }
624
625 }