activityimporter.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. <?php
  2. /**
  3. * StatusNet - the distributed open-source microblogging tool
  4. * Copyright (C) 2010, StatusNet, Inc.
  5. *
  6. * class to import activities as part of a user's timeline
  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 Cache
  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('GNUSOCIAL')) { exit(1); }
  31. /**
  32. * Class comment
  33. *
  34. * @category General
  35. * @package StatusNet
  36. * @author Evan Prodromou <evan@status.net>
  37. * @copyright 2010 StatusNet, Inc.
  38. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
  39. * @link http://status.net/
  40. */
  41. class ActivityImporter extends QueueHandler
  42. {
  43. private $trusted = false;
  44. /**
  45. * Function comment
  46. *
  47. * @param
  48. *
  49. * @return
  50. */
  51. function handle($data)
  52. {
  53. list($user, $author, $activity, $trusted) = $data;
  54. $this->trusted = $trusted;
  55. $done = null;
  56. try {
  57. if (Event::handle('StartImportActivity',
  58. array($user, $author, $activity, $trusted, &$done))) {
  59. switch ($activity->verb) {
  60. case ActivityVerb::FOLLOW:
  61. $this->subscribeProfile($user, $author, $activity);
  62. break;
  63. case ActivityVerb::JOIN:
  64. $this->joinGroup($user, $activity);
  65. break;
  66. case ActivityVerb::POST:
  67. $this->postNote($user, $author, $activity);
  68. break;
  69. default:
  70. // TRANS: Client exception thrown when using an unknown verb for the activity importer.
  71. throw new ClientException(sprintf(_("Unknown verb: \"%s\"."),$activity->verb));
  72. }
  73. Event::handle('EndImportActivity',
  74. array($user, $author, $activity, $trusted));
  75. $done = true;
  76. }
  77. } catch (Exception $e) {
  78. common_log(LOG_ERR, $e->getMessage());
  79. $done = true;
  80. }
  81. return $done;
  82. }
  83. function subscribeProfile($user, $author, $activity)
  84. {
  85. $profile = $user->getProfile();
  86. if ($activity->objects[0]->id == $author->id) {
  87. if (!$this->trusted) {
  88. // TRANS: Client exception thrown when trying to force a subscription for an untrusted user.
  89. throw new ClientException(_('Cannot force subscription for untrusted user.'));
  90. }
  91. $other = $activity->actor;
  92. $otherUser = User::getKV('uri', $other->id);
  93. if (!$otherUser instanceof User) {
  94. // TRANS: Client exception thrown when trying to force a remote user to subscribe.
  95. throw new Exception(_('Cannot force remote user to subscribe.'));
  96. }
  97. $otherProfile = $otherUser->getProfile();
  98. // XXX: don't do this for untrusted input!
  99. Subscription::start($otherProfile, $profile);
  100. } else if (empty($activity->actor)
  101. || $activity->actor->id == $author->id) {
  102. $other = $activity->objects[0];
  103. try {
  104. $otherProfile = Profile::fromUri($other->id);
  105. // TRANS: Client exception thrown when trying to subscribe to an unknown profile.
  106. } catch (UnknownUriException $e) {
  107. // Let's convert it to a client exception instead of server.
  108. throw new ClientException(_('Unknown profile.'));
  109. }
  110. Subscription::start($profile, $otherProfile);
  111. } else {
  112. // TRANS: Client exception thrown when trying to import an event not related to the importing user.
  113. throw new Exception(_('This activity seems unrelated to our user.'));
  114. }
  115. }
  116. function joinGroup($user, $activity)
  117. {
  118. // XXX: check that actor == subject
  119. $uri = $activity->objects[0]->id;
  120. $group = User_group::getKV('uri', $uri);
  121. if (!$group instanceof User_group) {
  122. $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
  123. if (!$oprofile->isGroup()) {
  124. // TRANS: Client exception thrown when trying to join a remote group that is not a group.
  125. throw new ClientException(_('Remote profile is not a group!'));
  126. }
  127. $group = $oprofile->localGroup();
  128. }
  129. assert(!empty($group));
  130. if ($user->isMember($group)) {
  131. // TRANS: Client exception thrown when trying to join a group the importing user is already a member of.
  132. throw new ClientException(_("User is already a member of this group."));
  133. }
  134. $user->joinGroup($group);
  135. }
  136. // XXX: largely cadged from Ostatus_profile::processNote()
  137. function postNote($user, $author, $activity)
  138. {
  139. $note = $activity->objects[0];
  140. $sourceUri = $note->id;
  141. $notice = Notice::getKV('uri', $sourceUri);
  142. if ($notice instanceof Notice) {
  143. common_log(LOG_INFO, "Notice {$sourceUri} already exists.");
  144. if ($this->trusted) {
  145. $profile = $notice->getProfile();
  146. $uri = $profile->getUri();
  147. if ($uri === $author->id) {
  148. common_log(LOG_INFO, sprintf('Updating notice author from %s to %s', $author->id, $user->getUri()));
  149. $orig = clone($notice);
  150. $notice->profile_id = $user->id;
  151. $notice->update($orig);
  152. return;
  153. } else {
  154. // TRANS: Client exception thrown when trying to import a notice by another user.
  155. // TRANS: %1$s is the source URI of the notice, %2$s is the URI of the author.
  156. throw new ClientException(sprintf(_('Already know about notice %1$s and '.
  157. ' it has a different author %2$s.'),
  158. $sourceUri, $uri));
  159. }
  160. } else {
  161. // TRANS: Client exception thrown when trying to overwrite the author information for a non-trusted user during import.
  162. throw new ClientException(_('Not overwriting author info for non-trusted user.'));
  163. }
  164. }
  165. // Use summary as fallback for content
  166. if (!empty($note->content)) {
  167. $sourceContent = $note->content;
  168. } else if (!empty($note->summary)) {
  169. $sourceContent = $note->summary;
  170. } else if (!empty($note->title)) {
  171. $sourceContent = $note->title;
  172. } else {
  173. // @fixme fetch from $sourceUrl?
  174. // TRANS: Client exception thrown when trying to import a notice without content.
  175. // TRANS: %s is the notice URI.
  176. throw new ClientException(sprintf(_('No content for notice %s.'),$sourceUri));
  177. }
  178. // Get (safe!) HTML and text versions of the content
  179. $rendered = $this->purify($sourceContent);
  180. $content = common_strip_html($rendered);
  181. $shortened = $user->shortenLinks($content);
  182. $options = array('is_local' => Notice::LOCAL_PUBLIC,
  183. 'uri' => $sourceUri,
  184. 'rendered' => $rendered,
  185. 'replies' => array(),
  186. 'groups' => array(),
  187. 'tags' => array(),
  188. 'urls' => array(),
  189. 'distribute' => false);
  190. // Check for optional attributes...
  191. if (!empty($activity->time)) {
  192. $options['created'] = common_sql_date($activity->time);
  193. }
  194. if ($activity->context) {
  195. // Any individual or group attn: targets?
  196. list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention);
  197. // Maintain direct reply associations
  198. // @fixme what about conversation ID?
  199. if (!empty($activity->context->replyToID)) {
  200. $orig = Notice::getKV('uri', $activity->context->replyToID);
  201. if ($orig instanceof Notice) {
  202. $options['reply_to'] = $orig->id;
  203. }
  204. }
  205. $location = $activity->context->location;
  206. if ($location) {
  207. $options['lat'] = $location->lat;
  208. $options['lon'] = $location->lon;
  209. if ($location->location_id) {
  210. $options['location_ns'] = $location->location_ns;
  211. $options['location_id'] = $location->location_id;
  212. }
  213. }
  214. }
  215. // Atom categories <-> hashtags
  216. foreach ($activity->categories as $cat) {
  217. if ($cat->term) {
  218. $term = common_canonical_tag($cat->term);
  219. if ($term) {
  220. $options['tags'][] = $term;
  221. }
  222. }
  223. }
  224. // Atom enclosures -> attachment URLs
  225. foreach ($activity->enclosures as $href) {
  226. // @fixme save these locally or....?
  227. $options['urls'][] = $href;
  228. }
  229. common_log(LOG_INFO, "Saving notice {$options['uri']}");
  230. $saved = Notice::saveNew($user->id,
  231. $content,
  232. 'restore', // TODO: restore the actual source
  233. $options);
  234. return $saved;
  235. }
  236. protected function filterAttention(array $attn)
  237. {
  238. $groups = array(); // TODO: context->attention
  239. $replies = array(); // TODO: context->attention
  240. foreach ($attn as $recipient=>$type) {
  241. // Is the recipient a local user?
  242. $user = User::getKV('uri', $recipient);
  243. if ($user instanceof User) {
  244. // TODO: @fixme sender verification, spam etc?
  245. $replies[] = $recipient;
  246. continue;
  247. }
  248. // Is the recipient a remote group?
  249. $oprofile = Ostatus_profile::ensureProfileURI($recipient);
  250. if ($oprofile) {
  251. if (!$oprofile->isGroup()) {
  252. // may be canonicalized or something
  253. $replies[] = $oprofile->uri;
  254. }
  255. continue;
  256. }
  257. // Is the recipient a local group?
  258. // TODO: @fixme uri on user_group isn't reliable yet
  259. // $group = User_group::getKV('uri', $recipient);
  260. $id = OStatusPlugin::localGroupFromUrl($recipient);
  261. if ($id) {
  262. $group = User_group::getKV('id', $id);
  263. if ($group) {
  264. // Deliver to all members of this local group if allowed.
  265. $profile = $sender->localProfile();
  266. if ($profile->isMember($group)) {
  267. $groups[] = $group->id;
  268. } else {
  269. common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
  270. }
  271. continue;
  272. } else {
  273. common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
  274. }
  275. }
  276. }
  277. return array($groups, $replies);
  278. }
  279. function purify($content)
  280. {
  281. require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
  282. $config = array('safe' => 1,
  283. 'deny_attribute' => 'id,style,on*');
  284. return htmLawed($content, $config);
  285. }
  286. }