BookmarkPlugin.php 18 KB

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