]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Bookmark/BookmarkPlugin.php
Move bookmark rendering to notice list item adapter
[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 'ShowbookmarkAction':
152         case 'NewbookmarkAction':
153         case 'BookmarkpopupAction':
154         case 'NoticebyurlAction':
155         case 'BookmarkforurlAction':
156         case 'ImportdeliciousAction':
157             include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
158             return false;
159         case 'Bookmark':
160             include_once $dir.'/'.$cls.'.php';
161             return false;
162         case 'BookmarkForm':
163         case 'InitialBookmarkForm':
164         case 'DeliciousBackupImporter':
165         case 'DeliciousBookmarkImporter':
166             include_once $dir.'/'.strtolower($cls).'.php';
167             return false;
168         default:
169             return true;
170         }
171     }
172
173     /**
174      * Map URLs to actions
175      *
176      * @param Net_URL_Mapper $m path-to-action mapper
177      *
178      * @return boolean hook value; true means continue processing, false means stop.
179      */
180     function onRouterInitialized($m)
181     {
182         $m->connect('main/bookmark/new',
183                     array('action' => 'newbookmark'),
184                     array('id' => '[0-9]+'));
185
186         $m->connect('main/bookmark/popup',
187                     array('action' => 'bookmarkpopup'));
188
189         $m->connect('main/bookmark/import',
190                     array('action' => 'importdelicious'));
191
192         $m->connect('main/bookmark/forurl',
193                     array('action' => 'bookmarkforurl'));
194
195         $m->connect('bookmark/:id',
196                     array('action' => 'showbookmark'),
197                     array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
198
199         $m->connect('notice/by-url/:id',
200                     array('action' => 'noticebyurl'),
201                     array('id' => '[0-9]+'));
202
203         return true;
204     }
205
206
207     /**
208      * Add our two queue handlers to the queue manager
209      *
210      * @param QueueManager $qm current queue manager
211      *
212      * @return boolean hook value
213      */
214     function onEndInitializeQueueManager($qm)
215     {
216         $qm->connect('dlcsback', 'DeliciousBackupImporter');
217         $qm->connect('dlcsbkmk', 'DeliciousBookmarkImporter');
218         return true;
219     }
220
221     /**
222      * Plugin version data
223      *
224      * @param array &$versions array of version data
225      *
226      * @return value
227      */
228     function onPluginVersion(&$versions)
229     {
230         $versions[] = array('name' => 'Bookmark',
231                             'version' => self::VERSION,
232                             'author' => 'Evan Prodromou',
233                             'homepage' => 'http://status.net/wiki/Plugin:Bookmark',
234                             'description' =>
235                             // TRANS: Plugin description.
236                             _m('Simple extension for supporting bookmarks.'));
237         return true;
238     }
239
240     /**
241      * Load our document if requested
242      *
243      * @param string &$title  Title to fetch
244      * @param string &$output HTML to output
245      *
246      * @return boolean hook value
247      */
248     function onStartLoadDoc(&$title, &$output)
249     {
250         if ($title == 'bookmarklet') {
251             $filename = INSTALLDIR.'/plugins/Bookmark/bookmarklet';
252
253             $c      = file_get_contents($filename);
254             $output = common_markup_to_html($c);
255             return false; // success!
256         }
257
258         return true;
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     function onEndProfileSettingsActions($action)
269     {
270         $user = common_current_user();
271
272         if (!empty($user) && $user->hasRight(self::IMPORTDELICIOUS)) {
273             $action->elementStart('li');
274             $action->element('a',
275                              array('href' => common_local_url('importdelicious')),
276                              // TRANS: Link text in proile leading to import form.
277                              _m('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             $class = 'hentry notice bookmark';
298             if ($nli->notice->scope != 0 && $nli->notice->scope != 1) {
299                 $class .= ' limited-scope';
300             }
301             $nli->out->elementStart('li', array('class' => $class,
302                                                  'id' => 'notice-' . $id));
303             Event::handle('EndOpenNoticeListItemElement', array($nli));
304             return false;
305         }
306         return true;
307     }
308
309     /**
310      * Save a remote bookmark (from Salmon or PuSH)
311      *
312      * @param Ostatus_profile $author   Author of the bookmark
313      * @param Activity        $activity Activity to save
314      *
315      * @return Notice resulting notice.
316      */
317     static private function _postRemoteBookmark(Ostatus_profile $author,
318                                                 Activity $activity)
319     {
320         $bookmark = $activity->objects[0];
321
322         $options = array('uri' => $bookmark->id,
323                          'url' => $bookmark->link,
324                          'is_local' => Notice::REMOTE_OMB,
325                          'source' => 'ostatus');
326
327         return self::_postBookmark($author->localProfile(), $activity, $options);
328     }
329
330     /**
331      * Test if an activity represents posting a bookmark
332      *
333      * @param Activity $activity Activity to test
334      *
335      * @return true if it's a Post of a Bookmark, else false
336      */
337     static private function _isPostBookmark($activity)
338     {
339         return ($activity->verb == ActivityVerb::POST &&
340                 $activity->objects[0]->type == ActivityObject::BOOKMARK);
341     }
342
343     function types()
344     {
345         return array(ActivityObject::BOOKMARK);
346     }
347
348     /**
349      * When a notice is deleted, delete the related Bookmark
350      *
351      * @param Notice $notice Notice being deleted
352      *
353      * @return boolean hook value
354      */
355     function deleteRelated($notice)
356     {
357         $nb = Bookmark::getByNotice($notice);
358
359         if (!empty($nb)) {
360             $nb->delete();
361         }
362
363         return true;
364     }
365
366     /**
367      * Save a bookmark from an activity
368      *
369      * @param Activity $activity Activity to save
370      * @param Profile  $profile  Profile to use as author
371      * @param array    $options  Options to pass to bookmark-saving code
372      *
373      * @return Notice resulting notice
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             // TRANS: Client exception thrown when a bookmark is formatted incorrectly.
383             throw new ClientException(_m('Expected exactly 1 link '.
384                                         'rel=related in a Bookmark.'));
385         }
386
387         if (count($relLinkEls) > 1) {
388             common_log(LOG_WARNING,
389                        "Got too many link rel=related in a Bookmark.");
390         }
391
392         $linkEl = $relLinkEls[0];
393
394         $url = $linkEl->getAttribute('href');
395
396         $tags = array();
397
398         foreach ($activity->categories as $category) {
399             $tags[] = common_canonical_tag($category->term);
400         }
401
402         if (!empty($activity->time)) {
403             $options['created'] = common_sql_date($activity->time);
404         }
405
406         // Fill in location if available
407
408         $location = $activity->context->location;
409
410         if ($location) {
411             $options['lat'] = $location->lat;
412             $options['lon'] = $location->lon;
413             if ($location->location_id) {
414                 $options['location_ns'] = $location->location_ns;
415                 $options['location_id'] = $location->location_id;
416             }
417         }
418
419         $replies = $activity->context->attention;
420
421         $options['groups']  = array();
422         $options['replies'] = array();
423
424         foreach ($replies as $replyURI) {
425             $other = Profile::fromURI($replyURI);
426             if (!empty($other)) {
427                 $options['replies'][] = $replyURI;
428             } else {
429                 $group = User_group::staticGet('uri', $replyURI);
430                 if (!empty($group)) {
431                     $options['groups'][] = $replyURI;
432                 }
433             }
434         }
435
436         // Maintain direct reply associations
437         // @fixme what about conversation ID?
438
439         if (!empty($activity->context->replyToID)) {
440             $orig = Notice::staticGet('uri',
441                                       $activity->context->replyToID);
442             if (!empty($orig)) {
443                 $options['reply_to'] = $orig->id;
444             }
445         }
446
447         return Bookmark::saveNew($profile,
448                                  $bookmark->title,
449                                  $url,
450                                  $tags,
451                                  $bookmark->summary,
452                                  $options);
453     }
454
455     function activityObjectFromNotice($notice)
456     {
457         assert($this->isMyNotice($notice));
458
459         common_log(LOG_INFO,
460                    "Formatting notice {$notice->uri} as a bookmark.");
461
462         $object = new ActivityObject();
463         $nb = Bookmark::getByNotice($notice);
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             // TRANS: Server exception thrown when a bookmark has multiple attachments.
477             throw new ServerException(_m('Bookmark notice with the '.
478                                         'wrong number of attachments.'));
479         }
480
481         $target = $attachments[0];
482
483         $attrs = array('rel' => 'related',
484                        'href' => $target->url);
485
486         if (!empty($target->title)) {
487             $attrs['title'] = $target->title;
488         }
489
490         $object->extra[] = array('link', $attrs, null);
491
492         // Attributes of the thumbnail, if any
493
494         $thumbnail = $target->getThumbnail();
495
496         if (!empty($thumbnail)) {
497             $tattrs = array('rel' => 'preview',
498                             'href' => $thumbnail->url);
499
500             if (!empty($thumbnail->width)) {
501                 $tattrs['media:width'] = $thumbnail->width;
502             }
503
504             if (!empty($thumbnail->height)) {
505                 $tattrs['media:height'] = $thumbnail->height;
506             }
507
508             $object->extra[] = array('link', $attrs, null);
509         }
510
511         return $object;
512     }
513
514     /**
515      * Given a notice list item, returns an adapter specific
516      * to this plugin.
517      *
518      * @param NoticeListItem $nli item to adapt
519      *
520      * @return NoticeListItemAdapter adapter or null
521      */
522     function adaptNoticeListItem($nli)
523     {
524         return new BookmarkNoticeListItemAdapter($nli);
525     }
526
527     function entryForm($out)
528     {
529         return new InitialBookmarkForm($out);
530     }
531
532     function tag()
533     {
534         return 'bookmark';
535     }
536
537     function appTitle()
538     {
539         // TRANS: Application title.
540         return _m('TITLE','Bookmark');
541     }
542 }