]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Bookmark/BookmarkPlugin.php
Change Bookmark plugin version details
[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('GNUSOCIAL')) { exit(1); }
32
33 /**
34  * Bookmark plugin main class
35  *
36  * @category  Bookmark
37  * @package   StatusNet
38  * @author    Brion Vibber <brionv@status.net>
39  * @author    Evan Prodromou <evan@status.net>
40  * @copyright 2010 StatusNet, Inc.
41  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
42  * @link      http://status.net/
43  */
44 class BookmarkPlugin extends MicroAppPlugin
45 {
46     const VERSION         = '0.1';
47     const IMPORTDELICIOUS = 'BookmarkPlugin:IMPORTDELICIOUS';
48
49     /**
50      * Authorization for importing delicious bookmarks
51      *
52      * By default, everyone can import bookmarks except silenced people.
53      *
54      * @param Profile $profile Person whose rights to check
55      * @param string  $right   Right to check; const value
56      * @param boolean &$result Result of the check, writeable
57      *
58      * @return boolean hook value
59      */
60     function onUserRightsCheck($profile, $right, &$result)
61     {
62         if ($right == self::IMPORTDELICIOUS) {
63             $result = !$profile->isSilenced();
64             return false;
65         }
66         return true;
67     }
68
69     /**
70      * Database schema setup
71      *
72      * @see Schema
73      * @see ColumnDef
74      *
75      * @return boolean hook value; true means continue processing, false means stop.
76      */
77     function onCheckSchema()
78     {
79         $schema = Schema::get();
80
81         $schema->ensureTable('bookmark', Bookmark::schemaDef());
82
83         return true;
84     }
85
86     /**
87      * Show the CSS necessary for this plugin
88      *
89      * @param Action $action the action being run
90      *
91      * @return boolean hook value
92      */
93     function onEndShowStyles($action)
94     {
95         $action->cssLink($this->path('css/bookmark.css'));
96         return true;
97     }
98
99     function onEndShowScripts($action)
100     {
101         $action->script($this->path('js/bookmark.js'));
102         return true;
103     }
104
105     /**
106      * Map URLs to actions
107      *
108      * @param URLMapper $m path-to-action mapper
109      *
110      * @return boolean hook value; true means continue processing, false means stop.
111      */
112     public function onRouterInitialized(URLMapper $m)
113     {
114         if (common_config('singleuser', 'enabled')) {
115             $nickname = User::singleUserNickname();
116             $m->connect('bookmarks',
117                         array('action' => 'bookmarks', 'nickname' => $nickname));
118             $m->connect('bookmarks/rss',
119                         array('action' => 'bookmarksrss', 'nickname' => $nickname));
120         } else {
121             $m->connect(':nickname/bookmarks',
122                         array('action' => 'bookmarks'),
123                         array('nickname' => Nickname::DISPLAY_FMT));
124             $m->connect(':nickname/bookmarks/rss',
125                         array('action' => 'bookmarksrss'),
126                         array('nickname' => Nickname::DISPLAY_FMT));
127         }
128
129         $m->connect('api/bookmarks/:id.:format',
130                     array('action' => 'ApiTimelineBookmarks',
131                           'id' => Nickname::INPUT_FMT,
132                           'format' => '(xml|json|rss|atom|as)'));
133
134         $m->connect('main/bookmark/new',
135                     array('action' => 'newbookmark'),
136                     array('id' => '[0-9]+'));
137
138         $m->connect('main/bookmark/popup',
139                     array('action' => 'bookmarkpopup'));
140
141         $m->connect('main/bookmark/import',
142                     array('action' => 'importdelicious'));
143
144         $m->connect('main/bookmark/forurl',
145                     array('action' => 'bookmarkforurl'));
146
147         $m->connect('bookmark/:id',
148                     array('action' => 'showbookmark'),
149                     array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
150
151         $m->connect('notice/by-url/:id',
152                     array('action' => 'noticebyurl'),
153                     array('id' => '[0-9]+'));
154
155         return true;
156     }
157
158
159     /**
160      * Add our two queue handlers to the queue manager
161      *
162      * @param QueueManager $qm current queue manager
163      *
164      * @return boolean hook value
165      */
166     function onEndInitializeQueueManager($qm)
167     {
168         $qm->connect('dlcsback', 'DeliciousBackupImporter');
169         $qm->connect('dlcsbkmk', 'DeliciousBookmarkImporter');
170         return true;
171     }
172
173     /**
174      * Plugin version data
175      *
176      * @param array &$versions array of version data
177      *
178      * @return value
179      */
180     function onPluginVersion(array &$versions)
181     {
182         $versions[] = array('name' => 'Bookmark',
183                             'version' => GNUSOCIAL_VERSION,
184                             'author' => 'Evan Prodromou, Stephane Berube, Jean Baptiste Favre, Mikael Nordfeldth',
185                             'homepage' => 'https://gnu.io/social',
186                             'description' =>
187                             // TRANS: Plugin description.
188                             _m('Plugin for posting bookmarks. ') .
189                             'BookmarkList feature has been developped by Stephane Berube. ' .
190                             'Integration has been done by Jean Baptiste Favre.');
191         return true;
192     }
193
194     /**
195      * Load our document if requested
196      *
197      * @param string &$title  Title to fetch
198      * @param string &$output HTML to output
199      *
200      * @return boolean hook value
201      */
202     function onStartLoadDoc(&$title, &$output)
203     {
204         if ($title == 'bookmarklet') {
205             $filename = INSTALLDIR.'/plugins/Bookmark/bookmarklet';
206
207             $c      = file_get_contents($filename);
208             $output = common_markup_to_html($c);
209             return false; // success!
210         }
211
212         return true;
213     }
214
215     /**
216      * Show a link to our delicious import page on profile settings form
217      *
218      * @param Action $action Profile settings action being shown
219      *
220      * @return boolean hook value
221      */
222     function onEndProfileSettingsActions($action)
223     {
224         $user = common_current_user();
225
226         if (!empty($user) && $user->hasRight(self::IMPORTDELICIOUS)) {
227             $action->elementStart('li');
228             $action->element('a',
229                              array('href' => common_local_url('importdelicious')),
230                              // TRANS: Link text in proile leading to import form.
231                              _m('Import del.icio.us bookmarks'));
232             $action->elementEnd('li');
233         }
234
235         return true;
236     }
237
238     /**
239      * Modify the default menu to link to our custom action
240      *
241      * Using event handlers, it's possible to modify the default UI for pages
242      * almost without limit. In this method, we add a menu item to the default
243      * primary menu for the interface to link to our action.
244      *
245      * The Action class provides a rich set of events to hook, as well as output
246      * methods.
247      *
248      * @param Action $action The current action handler. Use this to
249      * do any output.
250      *
251      * @return boolean hook value; true means continue processing, false means stop.
252      *
253      * @see Action
254      */
255     function onEndPersonalGroupNav(Menu $menu, Profile $target, Profile $scoped=null)
256     {
257         $menu->menuItem(common_local_url('bookmarks', array('nickname' => $target->getNickname())),
258                           // TRANS: Menu item in sample plugin.
259                           _m('Bookmarks'),
260                           // TRANS: Menu item title in sample plugin.
261                           _m('A list of your bookmarks'), false, 'nav_timeline_bookmarks');
262         return true;
263     }
264
265     function types()
266     {
267         return array(ActivityObject::BOOKMARK);
268     }
269
270     /**
271      * When a notice is deleted, delete the related Bookmark
272      *
273      * @param Notice $notice Notice being deleted
274      *
275      * @return boolean hook value
276      */
277     function deleteRelated(Notice $notice)
278     {
279         try {
280             $nb = Bookmark::fromStored($notice);
281         } catch (NoResultException $e) {
282             throw new AlreadyFulfilledException('Bookmark already gone when deleting: '.$e->getMessage());
283         }
284         $nb->delete();
285         
286         return true;
287     }
288
289     /**
290      * Save a bookmark from an activity
291      *
292      * @param Activity $activity Activity to save
293      * @param Profile  $actor    Profile to use as author
294      * @param array    $options  Options to pass to bookmark-saving code
295      *
296      * @return Notice resulting notice
297      */
298     protected function saveObjectFromActivity(Activity $activity, Notice $stored, array $options=array())
299     {
300         $actobj = $activity->objects[0];
301
302         $relLinkEls = ActivityUtils::getLinks($actobj->element, 'related');
303
304         if (count($relLinkEls) !== 1) {
305             // TRANS: Client exception thrown when a bookmark is formatted incorrectly.
306             throw new ClientException(sprintf(_m('Expected exactly 1 link rel=related in a Bookmark, got %1$d.'), count($relLinkEls)));
307         }
308
309         return Bookmark::addNew($stored,
310                                  $actobj->title,
311                                  $relLinkEls[0]->getAttribute('href'),
312                                  $actobj->summary);
313     }
314
315     function activityObjectFromNotice(Notice $notice)
316     {
317         assert($this->isMyNotice($notice));
318
319         common_log(LOG_INFO,
320                    "Formatting notice {$notice->uri} as a bookmark.");
321
322         $object = new ActivityObject();
323         $nb = Bookmark::fromStored($notice);
324
325         $object->id      = $notice->uri;
326         $object->type    = ActivityObject::BOOKMARK;
327         $object->title   = $nb->getTitle();
328         $object->summary = $nb->getDescription();
329         $object->link    = $notice->getUrl();
330
331         // Attributes of the URL
332
333         $attachments = $notice->attachments();
334
335         if (count($attachments) != 1) {
336             // TRANS: Server exception thrown when a bookmark has multiple attachments.
337             throw new ServerException(_m('Bookmark notice with the '.
338                                         'wrong number of attachments.'));
339         }
340
341         $target = $attachments[0];
342
343         $attrs = array('rel' => 'related',
344                        'href' => $target->getUrl());
345
346         if (!empty($target->title)) {
347             $attrs['title'] = $target->title;
348         }
349
350         $object->extra[] = array('link', $attrs, null);
351
352         // Attributes of the thumbnail, if any
353
354         try {
355             $thumbnail = $target->getThumbnail();
356             $tattrs = array('rel' => 'preview',
357                             'href' => $thumbnail->getUrl());
358
359             if (!empty($thumbnail->width)) {
360                 $tattrs['media:width'] = $thumbnail->width;
361             }
362
363             if (!empty($thumbnail->height)) {
364                 $tattrs['media:height'] = $thumbnail->height;
365             }
366
367             $object->extra[] = array('link', $tattrs, null);
368         } catch (UnsupportedMediaException $e) {
369             // No image thumbnail metadata available
370         }
371
372         return $object;
373     }
374
375     function entryForm($out)
376     {
377         return new InitialBookmarkForm($out);
378     }
379
380     function tag()
381     {
382         return 'bookmark';
383     }
384
385     function appTitle()
386     {
387         // TRANS: Application title.
388         return _m('TITLE','Bookmark');
389     }
390
391     function onEndUpgrade()
392     {
393         // Version 0.9.x of the plugin didn't stamp notices
394         // with verb and object-type (for obvious reasons). Update
395         // those notices here.
396
397         $notice = new Notice();
398         
399         $notice->whereAdd('exists (select uri from bookmark where bookmark.uri = notice.uri)');
400         $notice->whereAdd('((object_type is null) or (object_type = "' .ActivityObject::NOTE.'"))');
401
402         $notice->find();
403
404         while ($notice->fetch()) {
405             $original = clone($notice);
406             $notice->verb        = ActivityVerb::POST;
407             $notice->object_type = ActivityObject::BOOKMARK;
408             $notice->update($original);
409         }
410     }
411
412     public function activityObjectOutputJson(ActivityObject $obj, array &$out)
413     {
414         assert($obj->type == ActivityObject::BOOKMARK);
415
416         $bm = Bookmark::getByPK(array('uri' => $obj->id));
417
418         $out['displayName'] = $bm->getTitle();
419         $out['targetUrl']   = $bm->getUrl();
420
421         return true;
422     }
423
424     protected function showNoticeItemNotice(NoticeListItem $nli)
425     {
426         $nli->out->elementStart('div', 'entry-title');
427         $nli->showAuthor();
428         $nli->showContent();
429         $nli->out->elementEnd('div');
430     }
431
432     protected function showNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped=null)
433     {
434         $nb = Bookmark::fromStored($stored);
435
436         $profile = $stored->getProfile();
437
438         // Whether to nofollow
439         $attrs = array('href' => $nb->getUrl(), 'class' => 'bookmark-title');
440
441         $nf = common_config('nofollow', 'external');
442
443         if ($nf == 'never' || ($nf == 'sometimes' and $out instanceof ShowstreamAction)) {
444             $attrs['rel'] = 'external';
445         } else {
446             $attrs['rel'] = 'nofollow external';
447         }
448
449         $out->elementStart('h3');
450         $out->element('a', $attrs, $nb->title);
451         $out->elementEnd('h3');
452
453         // Replies look like "for:" tags
454         $replies = $stored->getReplies();
455         $tags = $stored->getTags();
456
457         if (!empty($nb->description)) {
458             $out->element('p',
459                           array('class' => 'bookmark-description'),
460                           $nb->description);
461         }
462
463         if (!empty($replies) || !empty($tags)) {
464
465             $out->elementStart('ul', array('class' => 'bookmark-tags'));
466
467             foreach ($replies as $reply) {
468                 $other = Profile::getKV('id', $reply);
469                 if (!empty($other)) {
470                     $out->elementStart('li');
471                     $out->element('a', array('rel' => 'tag',
472                                              'href' => $other->profileurl,
473                                              'title' => $other->getBestName()),
474                                   sprintf('for:%s', $other->nickname));
475                     $out->elementEnd('li');
476                     $out->text(' ');
477                 }
478             }
479
480             foreach ($tags as $tag) {
481                 $tag = trim($tag);
482                 if (!empty($tag)) {
483                     $out->elementStart('li');
484                     $out->element('a',
485                                   array('rel' => 'tag',
486                                         'href' => Notice_tag::url($tag)),
487                                   $tag);
488                     $out->elementEnd('li');
489                     $out->text(' ');
490                 }
491             }
492
493             $out->elementEnd('ul');
494         }
495
496     }
497 }