]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Bookmark/BookmarkPlugin.php
plugins onAutoload now only overloads if necessary (extlibs etc.)
[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     function onEndShowScripts($action)
134     {
135         $action->script($this->path('js/bookmark.js'));
136         return true;
137     }
138
139     /**
140      * Map URLs to actions
141      *
142      * @param Net_URL_Mapper $m path-to-action mapper
143      *
144      * @return boolean hook value; true means continue processing, false means stop.
145      */
146     function onRouterInitialized($m)
147     {
148         if (common_config('singleuser', 'enabled')) {
149             $nickname = User::singleUserNickname();
150             $m->connect('bookmarks',
151                         array('action' => 'bookmarks', 'nickname' => $nickname));
152             $m->connect('bookmarks/rss',
153                         array('action' => 'bookmarksrss', 'nickname' => $nickname));
154         } else {
155             $m->connect(':nickname/bookmarks',
156                         array('action' => 'bookmarks'),
157                         array('nickname' => Nickname::DISPLAY_FMT));
158             $m->connect(':nickname/bookmarks/rss',
159                         array('action' => 'bookmarksrss'),
160                         array('nickname' => Nickname::DISPLAY_FMT));
161         }
162
163         $m->connect('api/bookmarks/:id.:format',
164                     array('action' => 'ApiTimelineBookmarks',
165                           'id' => Nickname::INPUT_FMT,
166                           'format' => '(xml|json|rss|atom|as)'));
167
168         $m->connect('main/bookmark/new',
169                     array('action' => 'newbookmark'),
170                     array('id' => '[0-9]+'));
171
172         $m->connect('main/bookmark/popup',
173                     array('action' => 'bookmarkpopup'));
174
175         $m->connect('main/bookmark/import',
176                     array('action' => 'importdelicious'));
177
178         $m->connect('main/bookmark/forurl',
179                     array('action' => 'bookmarkforurl'));
180
181         $m->connect('bookmark/:id',
182                     array('action' => 'showbookmark'),
183                     array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
184
185         $m->connect('notice/by-url/:id',
186                     array('action' => 'noticebyurl'),
187                     array('id' => '[0-9]+'));
188
189         return true;
190     }
191
192
193     /**
194      * Add our two queue handlers to the queue manager
195      *
196      * @param QueueManager $qm current queue manager
197      *
198      * @return boolean hook value
199      */
200     function onEndInitializeQueueManager($qm)
201     {
202         $qm->connect('dlcsback', 'DeliciousBackupImporter');
203         $qm->connect('dlcsbkmk', 'DeliciousBookmarkImporter');
204         return true;
205     }
206
207     /**
208      * Plugin version data
209      *
210      * @param array &$versions array of version data
211      *
212      * @return value
213      */
214     function onPluginVersion(&$versions)
215     {
216         $versions[] = array('name' => 'Bookmark',
217                             'version' => self::VERSION,
218                             'author' => 'Evan Prodromou, Stephane Berube, Jean Baptiste Favre',
219                             'homepage' => 'http://status.net/wiki/Plugin:Bookmark',
220                             'description' =>
221                             // TRANS: Plugin description.
222                             _m('Simple extension for supporting bookmarks. ') .
223                             'BookmarkList feature has been developped by Stephane Berube. ' .
224                             'Integration has been done by Jean Baptiste Favre.');
225         return true;
226     }
227
228     /**
229      * Load our document if requested
230      *
231      * @param string &$title  Title to fetch
232      * @param string &$output HTML to output
233      *
234      * @return boolean hook value
235      */
236     function onStartLoadDoc(&$title, &$output)
237     {
238         if ($title == 'bookmarklet') {
239             $filename = INSTALLDIR.'/plugins/Bookmark/bookmarklet';
240
241             $c      = file_get_contents($filename);
242             $output = common_markup_to_html($c);
243             return false; // success!
244         }
245
246         return true;
247     }
248
249     /**
250      * Show a link to our delicious import page on profile settings form
251      *
252      * @param Action $action Profile settings action being shown
253      *
254      * @return boolean hook value
255      */
256     function onEndProfileSettingsActions($action)
257     {
258         $user = common_current_user();
259
260         if (!empty($user) && $user->hasRight(self::IMPORTDELICIOUS)) {
261             $action->elementStart('li');
262             $action->element('a',
263                              array('href' => common_local_url('importdelicious')),
264                              // TRANS: Link text in proile leading to import form.
265                              _m('Import del.icio.us bookmarks'));
266             $action->elementEnd('li');
267         }
268
269         return true;
270     }
271
272     /**
273      * Output our CSS class for bookmark notice list elements
274      *
275      * @param NoticeListItem $nli The item being shown
276      *
277      * @return boolean hook value
278      */
279
280     function onStartOpenNoticeListItemElement($nli)
281     {
282         if (!$this->isMyNotice($nli->notice)) {
283                 return true;
284         }
285         
286         $nb = Bookmark::getByNotice($nli->notice);
287         
288         if (empty($nb)) {
289                 $this->log(LOG_INFO, "Notice {$nli->notice->id} has bookmark class but no matching Bookmark record.");
290                 return true;
291         }
292                 
293             $id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id;
294             $class = 'hentry notice bookmark';
295             if ($nli->notice->scope != 0 && $nli->notice->scope != 1) {
296                 $class .= ' limited-scope';
297             }
298             $nli->out->elementStart('li', array('class' => $class,
299                                                 'id' => 'notice-' . $id));
300                                                 
301             Event::handle('EndOpenNoticeListItemElement', array($nli));
302             return false;
303     }
304
305     /**
306      * Modify the default menu to link to our custom action
307      *
308      * Using event handlers, it's possible to modify the default UI for pages
309      * almost without limit. In this method, we add a menu item to the default
310      * primary menu for the interface to link to our action.
311      *
312      * The Action class provides a rich set of events to hook, as well as output
313      * methods.
314      *
315      * @param Action $action The current action handler. Use this to
316      * do any output.
317      *
318      * @return boolean hook value; true means continue processing, false means stop.
319      *
320      * @see Action
321      */
322     function onEndPersonalGroupNav($action)
323     {
324         $this->user = common_current_user();
325
326         if (!$this->user) {
327             // TRANS: Client error displayed when trying to display bookmarks for a non-existing user.
328             $this->clientError(_('No such user.'));
329             return false;
330         }
331
332         $action->menuItem(common_local_url('bookmarks', array('nickname' => $this->user->nickname)),
333                           // TRANS: Menu item in sample plugin.
334                           _m('Bookmarks'),
335                           // TRANS: Menu item title in sample plugin.
336                           _m('A list of your bookmarks'), false, 'nav_timeline_bookmarks');
337         return true;
338     }
339
340     /**
341      * Save a remote bookmark (from Salmon or PuSH)
342      *
343      * @param Ostatus_profile $author   Author of the bookmark
344      * @param Activity        $activity Activity to save
345      *
346      * @return Notice resulting notice.
347      */
348     static private function _postRemoteBookmark(Ostatus_profile $author,
349                                                 Activity $activity)
350     {
351         $bookmark = $activity->objects[0];
352
353         $options = array('uri' => $bookmark->id,
354                          'url' => $bookmark->link,
355                          'is_local' => Notice::REMOTE,
356                          'source' => 'ostatus');
357
358         return self::_postBookmark($author->localProfile(), $activity, $options);
359     }
360
361     /**
362      * Test if an activity represents posting a bookmark
363      *
364      * @param Activity $activity Activity to test
365      *
366      * @return true if it's a Post of a Bookmark, else false
367      */
368     static private function _isPostBookmark($activity)
369     {
370         return ($activity->verb == ActivityVerb::POST &&
371                 $activity->objects[0]->type == ActivityObject::BOOKMARK);
372     }
373
374     function types()
375     {
376         return array(ActivityObject::BOOKMARK);
377     }
378
379     /**
380      * When a notice is deleted, delete the related Bookmark
381      *
382      * @param Notice $notice Notice being deleted
383      *
384      * @return boolean hook value
385      */
386     function deleteRelated($notice)
387     {
388         if ($this->isMyNotice($notice)) {
389                 
390                 $nb = Bookmark::getByNotice($notice);
391
392                 if (!empty($nb)) {
393                 $nb->delete();
394                 }
395         }
396         
397         return true;
398     }
399
400     /**
401      * Save a bookmark from an activity
402      *
403      * @param Activity $activity Activity to save
404      * @param Profile  $profile  Profile to use as author
405      * @param array    $options  Options to pass to bookmark-saving code
406      *
407      * @return Notice resulting notice
408      */
409     function saveNoticeFromActivity($activity, $profile, $options=array())
410     {
411         $bookmark = $activity->objects[0];
412
413         $relLinkEls = ActivityUtils::getLinks($bookmark->element, 'related');
414
415         if (count($relLinkEls) < 1) {
416             // TRANS: Client exception thrown when a bookmark is formatted incorrectly.
417             throw new ClientException(_m('Expected exactly 1 link '.
418                                         'rel=related in a Bookmark.'));
419         }
420
421         if (count($relLinkEls) > 1) {
422             common_log(LOG_WARNING,
423                        "Got too many link rel=related in a Bookmark.");
424         }
425
426         $linkEl = $relLinkEls[0];
427
428         $url = $linkEl->getAttribute('href');
429
430         $tags = array();
431
432         foreach ($activity->categories as $category) {
433             $tags[] = common_canonical_tag($category->term);
434         }
435
436         if (!empty($activity->time)) {
437             $options['created'] = common_sql_date($activity->time);
438         }
439
440         // Fill in location if available
441
442         $location = $activity->context->location;
443
444         if ($location) {
445             $options['lat'] = $location->lat;
446             $options['lon'] = $location->lon;
447             if ($location->location_id) {
448                 $options['location_ns'] = $location->location_ns;
449                 $options['location_id'] = $location->location_id;
450             }
451         }
452
453         $replies = $activity->context->attention;
454
455         $options['groups']  = array();
456         $options['replies'] = array();
457
458         foreach ($replies as $replyURI) {
459             $other = Profile::fromURI($replyURI);
460             if (!empty($other)) {
461                 $options['replies'][] = $replyURI;
462             } else {
463                 $group = User_group::getKV('uri', $replyURI);
464                 if (!empty($group)) {
465                     $options['groups'][] = $replyURI;
466                 }
467             }
468         }
469
470         // Maintain direct reply associations
471         // @fixme what about conversation ID?
472
473         if (!empty($activity->context->replyToID)) {
474             $orig = Notice::getKV('uri',
475                                       $activity->context->replyToID);
476             if (!empty($orig)) {
477                 $options['reply_to'] = $orig->id;
478             }
479         }
480
481         return Bookmark::saveNew($profile,
482                                  $bookmark->title,
483                                  $url,
484                                  $tags,
485                                  $bookmark->summary,
486                                  $options);
487     }
488
489     function activityObjectFromNotice($notice)
490     {
491         assert($this->isMyNotice($notice));
492
493         common_log(LOG_INFO,
494                    "Formatting notice {$notice->uri} as a bookmark.");
495
496         $object = new ActivityObject();
497         $nb = Bookmark::getByNotice($notice);
498
499         $object->id      = $notice->uri;
500         $object->type    = ActivityObject::BOOKMARK;
501         $object->title   = $nb->title;
502         $object->summary = $nb->description;
503         $object->link    = $notice->bestUrl();
504
505         // Attributes of the URL
506
507         $attachments = $notice->attachments();
508
509         if (count($attachments) != 1) {
510             // TRANS: Server exception thrown when a bookmark has multiple attachments.
511             throw new ServerException(_m('Bookmark notice with the '.
512                                         'wrong number of attachments.'));
513         }
514
515         $target = $attachments[0];
516
517         $attrs = array('rel' => 'related',
518                        'href' => $target->url);
519
520         if (!empty($target->title)) {
521             $attrs['title'] = $target->title;
522         }
523
524         $object->extra[] = array('link', $attrs, null);
525
526         // Attributes of the thumbnail, if any
527
528         $thumbnail = $target->getThumbnail();
529
530         if (!empty($thumbnail)) {
531             $tattrs = array('rel' => 'preview',
532                             'href' => $thumbnail->url);
533
534             if (!empty($thumbnail->width)) {
535                 $tattrs['media:width'] = $thumbnail->width;
536             }
537
538             if (!empty($thumbnail->height)) {
539                 $tattrs['media:height'] = $thumbnail->height;
540             }
541
542             $object->extra[] = array('link', $attrs, null);
543         }
544
545         return $object;
546     }
547
548     /**
549      * Given a notice list item, returns an adapter specific
550      * to this plugin.
551      *
552      * @param NoticeListItem $nli item to adapt
553      *
554      * @return NoticeListItemAdapter adapter or null
555      */
556     function adaptNoticeListItem($nli)
557     {
558         return new BookmarkListItem($nli);
559     }
560
561     function entryForm($out)
562     {
563         return new InitialBookmarkForm($out);
564     }
565
566     function tag()
567     {
568         return 'bookmark';
569     }
570
571     function appTitle()
572     {
573         // TRANS: Application title.
574         return _m('TITLE','Bookmark');
575     }
576
577     function onEndUpgrade()
578     {
579         // Version 0.9.x of the plugin didn't stamp notices
580         // with verb and object-type (for obvious reasons). Update
581         // those notices here.
582
583         $notice = new Notice();
584         
585         $notice->whereAdd('exists (select uri from bookmark where bookmark.uri = notice.uri)');
586         $notice->whereAdd('((object_type is null) or (object_type = "' .ActivityObject::NOTE.'"))');
587
588         $notice->find();
589
590         while ($notice->fetch()) {
591             $original = clone($notice);
592             $notice->verb        = ActivityVerb::POST;
593             $notice->object_type = ActivityObject::BOOKMARK;
594             $notice->update($original);
595         }
596     }
597
598     public function activityObjectOutputJson(ActivityObject $obj, array &$out)
599     {
600         assert($obj->type == ActivityObject::BOOKMARK);
601
602         $bm = Bookmark::getKV('uri', $obj->id);
603
604         if (empty($bm)) {
605             throw new ServerException("Unknown bookmark: " . $obj->id);
606         }
607
608         $out['displayName'] = $bm->title;
609         $out['targetUrl']   = $bm->url;
610
611         return true;
612     }
613 }