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