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