]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Bookmark/BookmarkPlugin.php
XSS vulnerability when remote-subscribing
[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         return Bookmark::saveActivityObject($activity->objects[0], $stored);
301     }
302
303     public function onEndNoticeAsActivity(Notice $stored, Activity $act, Profile $scoped=null)
304     {
305         if (!$this->isMyNotice($stored)) {
306             return true;
307         }
308
309         common_debug('Extending activity '.$stored->id.' with '.get_called_class());
310         $this->extendActivity($stored, $act, $scoped);
311         return false;
312     }
313
314     public function extendActivity(Notice $stored, Activity $act, Profile $scoped=null)
315     {
316         /*$hashtags = array();
317         $taglinks = array();
318
319         foreach ($tags as $tag) {
320             $hashtags[] = '#'.$tag;
321             $attrs      = array('href' => Notice_tag::url($tag),
322                                 'rel'  => $tag,
323                                 'class' => 'tag');
324             $taglinks[] = XMLStringer::estring('a', $attrs, $tag);
325         }*/
326     }
327
328     function activityObjectFromNotice(Notice $notice)
329     {
330         return Bookmark::fromStored($notice)->asActivityObject();
331     }
332
333     function entryForm($out)
334     {
335         return new InitialBookmarkForm($out);
336     }
337
338     function tag()
339     {
340         return 'bookmark';
341     }
342
343     function appTitle()
344     {
345         // TRANS: Application title.
346         return _m('TITLE','Bookmark');
347     }
348
349     function onEndUpgrade()
350     {
351         // Version 0.9.x of the plugin didn't stamp notices
352         // with verb and object-type (for obvious reasons). Update
353         // those notices here.
354
355         $notice = new Notice();
356         
357         $notice->whereAdd('exists (select uri from bookmark where bookmark.uri = notice.uri)');
358         $notice->whereAdd('((object_type is null) or (object_type = "' .ActivityObject::NOTE.'"))');
359
360         $notice->find();
361
362         while ($notice->fetch()) {
363             $original = clone($notice);
364             $notice->verb        = ActivityVerb::POST;
365             $notice->object_type = ActivityObject::BOOKMARK;
366             $notice->update($original);
367         }
368     }
369
370     public function activityObjectOutputJson(ActivityObject $obj, array &$out)
371     {
372         assert($obj->type == ActivityObject::BOOKMARK);
373
374         $bm = Bookmark::getByPK(array('uri' => $obj->id));
375
376         $out['displayName'] = $bm->getTitle();
377         $out['targetUrl']   = $bm->getUrl();
378
379         return true;
380     }
381
382     protected function showNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped=null)
383     {
384         $nb = Bookmark::fromStored($stored);
385
386         // Whether to nofollow
387         $attrs = array('href' => $nb->getUrl(), 'class' => 'bookmark-title');
388
389         $nf = common_config('nofollow', 'external');
390
391         if ($nf == 'never' || ($nf == 'sometimes' and $out instanceof ShowstreamAction)) {
392             $attrs['rel'] = 'external';
393         } else {
394             $attrs['rel'] = 'nofollow external';
395         }
396
397         $out->elementStart('h3');
398         $out->element('a', $attrs, $nb->getTitle());
399         $out->elementEnd('h3');
400
401         // Replies look like "for:" tags
402         $replies = $stored->getReplies();
403         $tags = $stored->getTags();
404
405         if (!empty($nb->description)) {
406             $out->element('p',
407                           array('class' => 'bookmark-description'),
408                           $nb->description);
409         }
410
411         if (!empty($replies) || !empty($tags)) {
412
413             $out->elementStart('ul', array('class' => 'bookmark-tags'));
414
415             foreach ($replies as $reply) {
416                 $other = Profile::getByPK($reply);
417                 $out->elementStart('li');
418                 $out->element('a', array('rel' => 'tag',
419                                          'href' => $other->getUrl(),
420                                          'title' => $other->getBestName()),
421                               sprintf('for:%s', $other->getNickname()));
422                 $out->elementEnd('li');
423                 $out->text(' ');
424             }
425
426             foreach ($tags as $tag) {
427                 $tag = trim($tag);
428                 if (!empty($tag)) {
429                     $out->elementStart('li');
430                     $out->element('a',
431                                   array('rel' => 'tag',
432                                         'href' => Notice_tag::url($tag)),
433                                   $tag);
434                     $out->elementEnd('li');
435                     $out->text(' ');
436                 }
437             }
438
439             $out->elementEnd('ul');
440         }
441
442     }
443 }