Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1066 lines
33KB

  1. <?php
  2. /**
  3. * @file mod/item.php
  4. */
  5. /*
  6. * This is the POST destination for most all locally posted
  7. * text stuff. This function handles status, wall-to-wall status,
  8. * local comments, and remote coments that are posted on this site
  9. * (as opposed to being delivered in a feed).
  10. * Also processed here are posts and comments coming through the
  11. * statusnet/twitter API.
  12. *
  13. * All of these become an "item" which is our basic unit of
  14. * information.
  15. */
  16. use Friendica\App;
  17. use Friendica\Content\Pager;
  18. use Friendica\Content\Text\BBCode;
  19. use Friendica\Content\Text\HTML;
  20. use Friendica\Core\Addon;
  21. use Friendica\Core\Config;
  22. use Friendica\Core\L10n;
  23. use Friendica\Core\Logger;
  24. use Friendica\Core\Protocol;
  25. use Friendica\Core\System;
  26. use Friendica\Core\Worker;
  27. use Friendica\Database\DBA;
  28. use Friendica\Model\Contact;
  29. use Friendica\Model\Conversation;
  30. use Friendica\Model\FileTag;
  31. use Friendica\Model\Item;
  32. use Friendica\Protocol\Diaspora;
  33. use Friendica\Protocol\Email;
  34. use Friendica\Util\DateTimeFormat;
  35. use Friendica\Util\Emailer;
  36. use Friendica\Util\Security;
  37. use Friendica\Util\Strings;
  38. function item_post(App $a) {
  39. if (!local_user() && !remote_user()) {
  40. return 0;
  41. }
  42. $uid = local_user();
  43. if (!empty($_REQUEST['dropitems'])) {
  44. $arr_drop = explode(',', $_REQUEST['dropitems']);
  45. drop_items($arr_drop);
  46. $json = ['success' => 1];
  47. echo json_encode($json);
  48. killme();
  49. }
  50. Addon::callHooks('post_local_start', $_REQUEST);
  51. Logger::log('postvars ' . print_r($_REQUEST, true), Logger::DATA);
  52. $api_source = defaults($_REQUEST, 'api_source', false);
  53. $message_id = ((!empty($_REQUEST['message_id']) && $api_source) ? strip_tags($_REQUEST['message_id']) : '');
  54. $return_path = defaults($_REQUEST, 'return', '');
  55. $preview = intval(defaults($_REQUEST, 'preview', 0));
  56. /*
  57. * Check for doubly-submitted posts, and reject duplicates
  58. * Note that we have to ignore previews, otherwise nothing will post
  59. * after it's been previewed
  60. */
  61. if (!$preview && !empty($_REQUEST['post_id_random'])) {
  62. if (!empty($_SESSION['post-random']) && $_SESSION['post-random'] == $_REQUEST['post_id_random']) {
  63. Logger::log("item post: duplicate post", Logger::DEBUG);
  64. item_post_return(System::baseUrl(), $api_source, $return_path);
  65. } else {
  66. $_SESSION['post-random'] = $_REQUEST['post_id_random'];
  67. }
  68. }
  69. // Is this a reply to something?
  70. $thr_parent = intval(defaults($_REQUEST, 'parent', 0));
  71. $thr_parent_uri = trim(defaults($_REQUEST, 'parent_uri', ''));
  72. $thr_parent_contact = null;
  73. $parent = 0;
  74. $parent_item = null;
  75. $parent_user = null;
  76. $parent_contact = null;
  77. $objecttype = null;
  78. $profile_uid = defaults($_REQUEST, 'profile_uid', local_user());
  79. $posttype = defaults($_REQUEST, 'post_type', Item::PT_ARTICLE);
  80. if ($thr_parent || $thr_parent_uri) {
  81. if ($thr_parent) {
  82. $parent_item = Item::selectFirst([], ['id' => $thr_parent]);
  83. } elseif ($thr_parent_uri) {
  84. $parent_item = Item::selectFirst([], ['uri' => $thr_parent_uri, 'uid' => $profile_uid]);
  85. }
  86. // if this isn't the real parent of the conversation, find it
  87. if (DBA::isResult($parent_item)) {
  88. // The URI and the contact is taken from the direct parent which needn't to be the top parent
  89. $thr_parent_uri = $parent_item['uri'];
  90. $thr_parent_contact = Contact::getDetailsByURL($parent_item["author-link"]);
  91. if ($parent_item['id'] != $parent_item['parent']) {
  92. $parent_item = Item::selectFirst(Item::ITEM_FIELDLIST, ['id' => $parent_item['parent']]);
  93. }
  94. }
  95. if (!DBA::isResult($parent_item)) {
  96. notice(L10n::t('Unable to locate original post.') . EOL);
  97. if (!empty($_REQUEST['return'])) {
  98. $a->internalRedirect($return_path);
  99. }
  100. killme();
  101. }
  102. $parent = $parent_item['id'];
  103. $parent_user = $parent_item['uid'];
  104. $parent_contact = Contact::getDetailsByURL($parent_item["author-link"]);
  105. $objecttype = ACTIVITY_OBJ_COMMENT;
  106. }
  107. if ($parent) {
  108. Logger::log('mod_item: item_post parent=' . $parent);
  109. }
  110. $post_id = intval(defaults($_REQUEST, 'post_id', 0));
  111. $app = strip_tags(defaults($_REQUEST, 'source', ''));
  112. $extid = strip_tags(defaults($_REQUEST, 'extid', ''));
  113. $object = defaults($_REQUEST, 'object', '');
  114. // Don't use "defaults" here. It would turn 0 to 1
  115. if (!isset($_REQUEST['wall'])) {
  116. $wall = 1;
  117. } else {
  118. $wall = $_REQUEST['wall'];
  119. }
  120. // Ensure that the user id in a thread always stay the same
  121. if (!is_null($parent_user) && in_array($parent_user, [local_user(), 0])) {
  122. $profile_uid = $parent_user;
  123. }
  124. // Check for multiple posts with the same message id (when the post was created via API)
  125. if (($message_id != '') && ($profile_uid != 0)) {
  126. if (Item::exists(['uri' => $message_id, 'uid' => $profile_uid])) {
  127. Logger::log("Message with URI ".$message_id." already exists for user ".$profile_uid, Logger::DEBUG);
  128. return 0;
  129. }
  130. }
  131. // Allow commenting if it is an answer to a public post
  132. $allow_comment = local_user() && ($profile_uid == 0) && $parent && in_array($parent_item['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
  133. // Now check that valid personal details have been provided
  134. if (!Security::canWriteToUserWall($profile_uid) && !$allow_comment) {
  135. notice(L10n::t('Permission denied.') . EOL);
  136. if (!empty($_REQUEST['return'])) {
  137. $a->internalRedirect($return_path);
  138. }
  139. killme();
  140. }
  141. // Init post instance
  142. $orig_post = null;
  143. // is this an edited post?
  144. if ($post_id > 0) {
  145. $orig_post = Item::selectFirst(Item::ITEM_FIELDLIST, ['id' => $post_id]);
  146. }
  147. $user = DBA::selectFirst('user', [], ['uid' => $profile_uid]);
  148. if (!DBA::isResult($user) && !$parent) {
  149. return 0;
  150. }
  151. $categories = '';
  152. $postopts = '';
  153. $emailcc = '';
  154. if (!empty($orig_post)) {
  155. $str_group_allow = $orig_post['allow_gid'];
  156. $str_contact_allow = $orig_post['allow_cid'];
  157. $str_group_deny = $orig_post['deny_gid'];
  158. $str_contact_deny = $orig_post['deny_cid'];
  159. $location = $orig_post['location'];
  160. $coord = $orig_post['coord'];
  161. $verb = $orig_post['verb'];
  162. $objecttype = $orig_post['object-type'];
  163. $app = $orig_post['app'];
  164. $categories = $orig_post['file'];
  165. $title = Strings::escapeTags(trim($_REQUEST['title']));
  166. $body = Strings::escapeHtml(trim($_REQUEST['body']));
  167. $private = $orig_post['private'];
  168. $pubmail_enabled = $orig_post['pubmail'];
  169. $network = $orig_post['network'];
  170. $guid = $orig_post['guid'];
  171. $extid = $orig_post['extid'];
  172. } else {
  173. /*
  174. * if coming from the API and no privacy settings are set,
  175. * use the user default permissions - as they won't have
  176. * been supplied via a form.
  177. */
  178. if ($api_source
  179. && !array_key_exists('contact_allow', $_REQUEST)
  180. && !array_key_exists('group_allow', $_REQUEST)
  181. && !array_key_exists('contact_deny', $_REQUEST)
  182. && !array_key_exists('group_deny', $_REQUEST)) {
  183. $str_group_allow = $user['allow_gid'];
  184. $str_contact_allow = $user['allow_cid'];
  185. $str_group_deny = $user['deny_gid'];
  186. $str_contact_deny = $user['deny_cid'];
  187. } else {
  188. // use the posted permissions
  189. $str_group_allow = perms2str(defaults($_REQUEST, 'group_allow', ''));
  190. $str_contact_allow = perms2str(defaults($_REQUEST, 'contact_allow', ''));
  191. $str_group_deny = perms2str(defaults($_REQUEST, 'group_deny', ''));
  192. $str_contact_deny = perms2str(defaults($_REQUEST, 'contact_deny', ''));
  193. }
  194. $title = Strings::escapeTags(trim(defaults($_REQUEST, 'title' , '')));
  195. $location = Strings::escapeTags(trim(defaults($_REQUEST, 'location', '')));
  196. $coord = Strings::escapeTags(trim(defaults($_REQUEST, 'coord' , '')));
  197. $verb = Strings::escapeTags(trim(defaults($_REQUEST, 'verb' , '')));
  198. $emailcc = Strings::escapeTags(trim(defaults($_REQUEST, 'emailcc' , '')));
  199. $body = Strings::escapeHtml(trim(defaults($_REQUEST, 'body' , '')));
  200. $network = Strings::escapeTags(trim(defaults($_REQUEST, 'network' , Protocol::DFRN)));
  201. $guid = System::createUUID();
  202. $postopts = defaults($_REQUEST, 'postopts', '');
  203. $private = ((strlen($str_group_allow) || strlen($str_contact_allow) || strlen($str_group_deny) || strlen($str_contact_deny)) ? 1 : 0);
  204. if ($user['hidewall']) {
  205. $private = 2;
  206. }
  207. // If this is a comment, set the permissions from the parent.
  208. if ($parent_item) {
  209. // for non native networks use the network of the original post as network of the item
  210. if (($parent_item['network'] != Protocol::DIASPORA)
  211. && ($parent_item['network'] != Protocol::OSTATUS)
  212. && ($network == "")) {
  213. $network = $parent_item['network'];
  214. }
  215. $str_contact_allow = $parent_item['allow_cid'];
  216. $str_group_allow = $parent_item['allow_gid'];
  217. $str_contact_deny = $parent_item['deny_cid'];
  218. $str_group_deny = $parent_item['deny_gid'];
  219. $private = $parent_item['private'];
  220. $wall = $parent_item['wall'];
  221. }
  222. $pubmail_enabled = defaults($_REQUEST, 'pubmail_enable', false) && !$private;
  223. // if using the API, we won't see pubmail_enable - figure out if it should be set
  224. if ($api_source && $profile_uid && $profile_uid == local_user() && !$private) {
  225. if (function_exists('imap_open') && !Config::get('system', 'imap_disabled')) {
  226. $pubmail_enabled = DBA::exists('mailacct', ["`uid` = ? AND `server` != ? AND `pubmail`", local_user(), '']);
  227. }
  228. }
  229. if (!strlen($body)) {
  230. if ($preview) {
  231. killme();
  232. }
  233. info(L10n::t('Empty post discarded.') . EOL);
  234. if (!empty($_REQUEST['return'])) {
  235. $a->internalRedirect($return_path);
  236. }
  237. killme();
  238. }
  239. }
  240. if (!empty($categories))
  241. {
  242. // get the "fileas" tags for this post
  243. $filedas = FileTag::fileToList($categories, 'file');
  244. }
  245. // save old and new categories, so we can determine what needs to be deleted from pconfig
  246. $categories_old = $categories;
  247. $categories = FileTag::listToFile(trim(defaults($_REQUEST, 'category', '')), 'category');
  248. $categories_new = $categories;
  249. if (!empty($filedas))
  250. {
  251. // append the fileas stuff to the new categories list
  252. $categories .= FileTag::listToFile($filedas, 'file');
  253. }
  254. // get contact info for poster
  255. $author = null;
  256. $self = false;
  257. $contact_id = 0;
  258. if (local_user() && ((local_user() == $profile_uid) || $allow_comment)) {
  259. $self = true;
  260. $author = DBA::selectFirst('contact', [], ['uid' => local_user(), 'self' => true]);
  261. } elseif (remote_user()) {
  262. if (!empty($_SESSION['remote']) && is_array($_SESSION['remote'])) {
  263. foreach ($_SESSION['remote'] as $v) {
  264. if ($v['uid'] == $profile_uid) {
  265. $contact_id = $v['cid'];
  266. break;
  267. }
  268. }
  269. }
  270. if ($contact_id) {
  271. $author = DBA::selectFirst('contact', [], ['id' => $contact_id]);
  272. }
  273. }
  274. if (DBA::isResult($author)) {
  275. $contact_id = $author['id'];
  276. }
  277. // get contact info for owner
  278. if ($profile_uid == local_user() || $allow_comment) {
  279. $contact_record = $author;
  280. } else {
  281. $contact_record = DBA::selectFirst('contact', [], ['uid' => $profile_uid, 'self' => true]);
  282. }
  283. // Look for any tags and linkify them
  284. $str_tags = '';
  285. $inform = '';
  286. $tags = BBCode::getTags($body);
  287. // Add a tag if the parent contact is from ActivityPub or OStatus (This will notify them)
  288. if ($parent && in_array($thr_parent_contact['network'], [Protocol::OSTATUS, Protocol::ACTIVITYPUB])) {
  289. $contact = '@[url=' . $thr_parent_contact['url'] . ']' . $thr_parent_contact['nick'] . '[/url]';
  290. if (!stripos(implode($tags), '[url=' . $thr_parent_contact['url'] . ']')) {
  291. $tags[] = $contact;
  292. }
  293. }
  294. $tagged = [];
  295. $private_forum = false;
  296. $only_to_forum = false;
  297. $forum_contact = [];
  298. if (count($tags)) {
  299. foreach ($tags as $tag) {
  300. $tag_type = substr($tag, 0, 1);
  301. if ($tag_type == '#') {
  302. continue;
  303. }
  304. /*
  305. * If we already tagged 'Robert Johnson', don't try and tag 'Robert'.
  306. * Robert Johnson should be first in the $tags array
  307. */
  308. $fullnametagged = false;
  309. /// @TODO $tagged is initialized above if () block and is not filled, maybe old-lost code?
  310. foreach ($tagged as $nextTag) {
  311. if (stristr($nextTag, $tag . ' ')) {
  312. $fullnametagged = true;
  313. break;
  314. }
  315. }
  316. if ($fullnametagged) {
  317. continue;
  318. }
  319. $success = handle_tag($a, $body, $inform, $str_tags, local_user() ? local_user() : $profile_uid, $tag, $network);
  320. if ($success['replaced']) {
  321. $tagged[] = $tag;
  322. }
  323. // When the forum is private or the forum is addressed with a "!" make the post private
  324. if (is_array($success['contact']) && (!empty($success['contact']['prv']) || ($tag_type == '!'))) {
  325. $private_forum = $success['contact']['prv'];
  326. $only_to_forum = ($tag_type == '!');
  327. $private_id = $success['contact']['id'];
  328. $forum_contact = $success['contact'];
  329. } elseif (is_array($success['contact']) && !empty($success['contact']['forum']) &&
  330. ($str_contact_allow == '<' . $success['contact']['id'] . '>')) {
  331. $private_forum = false;
  332. $only_to_forum = true;
  333. $private_id = $success['contact']['id'];
  334. $forum_contact = $success['contact'];
  335. }
  336. }
  337. }
  338. $original_contact_id = $contact_id;
  339. if (!$parent && count($forum_contact) && ($private_forum || $only_to_forum)) {
  340. // we tagged a forum in a top level post. Now we change the post
  341. $private = $private_forum;
  342. $str_group_allow = '';
  343. $str_contact_deny = '';
  344. $str_group_deny = '';
  345. if ($private_forum) {
  346. $str_contact_allow = '<' . $private_id . '>';
  347. } else {
  348. $str_contact_allow = '';
  349. }
  350. $contact_id = $private_id;
  351. $contact_record = $forum_contact;
  352. $_REQUEST['origin'] = false;
  353. $wall = 0;
  354. }
  355. /*
  356. * When a photo was uploaded into the message using the (profile wall) ajax
  357. * uploader, The permissions are initially set to disallow anybody but the
  358. * owner from seeing it. This is because the permissions may not yet have been
  359. * set for the post. If it's private, the photo permissions should be set
  360. * appropriately. But we didn't know the final permissions on the post until
  361. * now. So now we'll look for links of uploaded messages that are in the
  362. * post and set them to the same permissions as the post itself.
  363. */
  364. $match = null;
  365. /// @todo these lines should be moved to Model/Photo
  366. if (!$preview && preg_match_all("/\[img([\=0-9x]*?)\](.*?)\[\/img\]/",$body,$match)) {
  367. $images = $match[2];
  368. if (count($images)) {
  369. $objecttype = ACTIVITY_OBJ_IMAGE;
  370. foreach ($images as $image) {
  371. if (!stristr($image, System::baseUrl() . '/photo/')) {
  372. continue;
  373. }
  374. $image_uri = substr($image,strrpos($image,'/') + 1);
  375. $image_uri = substr($image_uri,0, strpos($image_uri,'-'));
  376. if (!strlen($image_uri)) {
  377. continue;
  378. }
  379. // Ensure to only modify photos that you own
  380. $srch = '<' . intval($original_contact_id) . '>';
  381. $condition = ['allow_cid' => $srch, 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => '',
  382. 'resource-id' => $image_uri, 'uid' => $profile_uid];
  383. if (!DBA::exists('photo', $condition)) {
  384. continue;
  385. }
  386. $fields = ['allow_cid' => $str_contact_allow, 'allow_gid' => $str_group_allow,
  387. 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_group_deny];
  388. $condition = ['resource-id' => $image_uri, 'uid' => $profile_uid];
  389. DBA::update('photo', $fields, $condition);
  390. }
  391. }
  392. }
  393. /*
  394. * Next link in any attachment references we find in the post.
  395. */
  396. $match = false;
  397. /// @todo these lines should be moved to Model/Attach (Once it exists)
  398. if (!$preview && preg_match_all("/\[attachment\](.*?)\[\/attachment\]/", $body, $match)) {
  399. $attaches = $match[1];
  400. if (count($attaches)) {
  401. foreach ($attaches as $attach) {
  402. // Ensure to only modify attachments that you own
  403. $srch = '<' . intval($original_contact_id) . '>';
  404. $condition = ['allow_cid' => $srch, 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => '',
  405. 'id' => $attach];
  406. if (!DBA::exists('attach', $condition)) {
  407. continue;
  408. }
  409. $fields = ['allow_cid' => $str_contact_allow, 'allow_gid' => $str_group_allow,
  410. 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_group_deny];
  411. $condition = ['id' => $attach];
  412. DBA::update('attach', $fields, $condition);
  413. }
  414. }
  415. }
  416. // embedded bookmark or attachment in post? set bookmark flag
  417. $data = BBCode::getAttachmentData($body);
  418. if ((preg_match_all("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", $body, $match, PREG_SET_ORDER) || isset($data["type"]))
  419. && ($posttype != Item::PT_PERSONAL_NOTE)) {
  420. $posttype = Item::PT_PAGE;
  421. $objecttype = ACTIVITY_OBJ_BOOKMARK;
  422. }
  423. $body = bb_translate_video($body);
  424. // Fold multi-line [code] sequences
  425. $body = preg_replace('/\[\/code\]\s*\[code\]/ism', "\n", $body);
  426. $body = BBCode::scaleExternalImages($body, false);
  427. // Setting the object type if not defined before
  428. if (!$objecttype) {
  429. $objecttype = ACTIVITY_OBJ_NOTE; // Default value
  430. $objectdata = BBCode::getAttachedData($body);
  431. if ($objectdata["type"] == "link") {
  432. $objecttype = ACTIVITY_OBJ_BOOKMARK;
  433. } elseif ($objectdata["type"] == "video") {
  434. $objecttype = ACTIVITY_OBJ_VIDEO;
  435. } elseif ($objectdata["type"] == "photo") {
  436. $objecttype = ACTIVITY_OBJ_IMAGE;
  437. }
  438. }
  439. $attachments = '';
  440. $match = false;
  441. if (preg_match_all('/(\[attachment\]([0-9]+)\[\/attachment\])/',$body,$match)) {
  442. foreach ($match[2] as $mtch) {
  443. $fields = ['id', 'filename', 'filesize', 'filetype'];
  444. $attachment = DBA::selectFirst('attach', $fields, ['id' => $mtch]);
  445. if (DBA::isResult($attachment)) {
  446. if (strlen($attachments)) {
  447. $attachments .= ',';
  448. }
  449. $attachments .= '[attach]href="' . System::baseUrl() . '/attach/' . $attachment['id'] .
  450. '" length="' . $attachment['filesize'] . '" type="' . $attachment['filetype'] .
  451. '" title="' . ($attachment['filename'] ? $attachment['filename'] : '') . '"[/attach]';
  452. }
  453. $body = str_replace($match[1],'',$body);
  454. }
  455. }
  456. if (!strlen($verb)) {
  457. $verb = ACTIVITY_POST;
  458. }
  459. if ($network == "") {
  460. $network = Protocol::DFRN;
  461. }
  462. $gravity = ($parent ? GRAVITY_COMMENT : GRAVITY_PARENT);
  463. // even if the post arrived via API we are considering that it
  464. // originated on this site by default for determining relayability.
  465. // Don't use "defaults" here. It would turn 0 to 1
  466. if (!isset($_REQUEST['origin'])) {
  467. $origin = 1;
  468. } else {
  469. $origin = $_REQUEST['origin'];
  470. }
  471. $notify_type = ($parent ? 'comment-new' : 'wall-new');
  472. $uri = ($message_id ? $message_id : Item::newURI($api_source ? $profile_uid : $uid, $guid));
  473. // Fallback so that we alway have a parent uri
  474. if (!$thr_parent_uri || !$parent) {
  475. $thr_parent_uri = $uri;
  476. }
  477. $datarray = [];
  478. $datarray['uid'] = $profile_uid;
  479. $datarray['wall'] = $wall;
  480. $datarray['gravity'] = $gravity;
  481. $datarray['network'] = $network;
  482. $datarray['contact-id'] = $contact_id;
  483. $datarray['owner-name'] = $contact_record['name'];
  484. $datarray['owner-link'] = $contact_record['url'];
  485. $datarray['owner-avatar'] = $contact_record['thumb'];
  486. $datarray['owner-id'] = Contact::getIdForURL($datarray['owner-link']);
  487. $datarray['author-name'] = $author['name'];
  488. $datarray['author-link'] = $author['url'];
  489. $datarray['author-avatar'] = $author['thumb'];
  490. $datarray['author-id'] = Contact::getIdForURL($datarray['author-link']);
  491. $datarray['created'] = DateTimeFormat::utcNow();
  492. $datarray['edited'] = DateTimeFormat::utcNow();
  493. $datarray['commented'] = DateTimeFormat::utcNow();
  494. $datarray['received'] = DateTimeFormat::utcNow();
  495. $datarray['changed'] = DateTimeFormat::utcNow();
  496. $datarray['extid'] = $extid;
  497. $datarray['guid'] = $guid;
  498. $datarray['uri'] = $uri;
  499. $datarray['title'] = $title;
  500. $datarray['body'] = $body;
  501. $datarray['app'] = $app;
  502. $datarray['location'] = $location;
  503. $datarray['coord'] = $coord;
  504. $datarray['tag'] = $str_tags;
  505. $datarray['file'] = $categories;
  506. $datarray['inform'] = $inform;
  507. $datarray['verb'] = $verb;
  508. $datarray['post-type'] = $posttype;
  509. $datarray['object-type'] = $objecttype;
  510. $datarray['allow_cid'] = $str_contact_allow;
  511. $datarray['allow_gid'] = $str_group_allow;
  512. $datarray['deny_cid'] = $str_contact_deny;
  513. $datarray['deny_gid'] = $str_group_deny;
  514. $datarray['private'] = $private;
  515. $datarray['pubmail'] = $pubmail_enabled;
  516. $datarray['attach'] = $attachments;
  517. // This is not a bug. The item store function changes 'parent-uri' to 'thr-parent' and fetches 'parent-uri' new. (We should change this)
  518. $datarray['parent-uri'] = $thr_parent_uri;
  519. $datarray['postopts'] = $postopts;
  520. $datarray['origin'] = $origin;
  521. $datarray['moderated'] = false;
  522. $datarray['object'] = $object;
  523. /*
  524. * These fields are for the convenience of addons...
  525. * 'self' if true indicates the owner is posting on their own wall
  526. * If parent is 0 it is a top-level post.
  527. */
  528. $datarray['parent'] = $parent;
  529. $datarray['self'] = $self;
  530. // This triggers posts via API and the mirror functions
  531. $datarray['api_source'] = $api_source;
  532. // This field is for storing the raw conversation data
  533. $datarray['protocol'] = Conversation::PARCEL_DFRN;
  534. $conversation = DBA::selectFirst('conversation', ['conversation-uri', 'conversation-href'], ['item-uri' => $datarray['parent-uri']]);
  535. if (DBA::isResult($conversation)) {
  536. if ($conversation['conversation-uri'] != '') {
  537. $datarray['conversation-uri'] = $conversation['conversation-uri'];
  538. }
  539. if ($conversation['conversation-href'] != '') {
  540. $datarray['conversation-href'] = $conversation['conversation-href'];
  541. }
  542. }
  543. if ($orig_post) {
  544. $datarray['edit'] = true;
  545. } else {
  546. $datarray['edit'] = false;
  547. }
  548. // Check for hashtags in the body and repair or add hashtag links
  549. if ($preview || $orig_post) {
  550. Item::setHashtags($datarray);
  551. }
  552. // preview mode - prepare the body for display and send it via json
  553. if ($preview) {
  554. // We set the datarray ID to -1 because in preview mode the dataray
  555. // doesn't have an ID.
  556. $datarray["id"] = -1;
  557. $datarray["item_id"] = -1;
  558. $datarray["author-network"] = Protocol::DFRN;
  559. $o = conversation($a, [array_merge($contact_record, $datarray)], new Pager($a->query_string), 'search', false, true);
  560. Logger::log('preview: ' . $o);
  561. echo json_encode(['preview' => $o]);
  562. exit();
  563. }
  564. Addon::callHooks('post_local',$datarray);
  565. if (!empty($datarray['cancel'])) {
  566. Logger::log('mod_item: post cancelled by addon.');
  567. if ($return_path) {
  568. $a->internalRedirect($return_path);
  569. }
  570. $json = ['cancel' => 1];
  571. if (!empty($_REQUEST['jsreload'])) {
  572. $json['reload'] = System::baseUrl() . '/' . $_REQUEST['jsreload'];
  573. }
  574. echo json_encode($json);
  575. killme();
  576. }
  577. if ($orig_post) {
  578. // Fill the cache field
  579. // This could be done in Item::update as well - but we have to check for the existance of some fields.
  580. Item::putInCache($datarray);
  581. $fields = [
  582. 'title' => $datarray['title'],
  583. 'body' => $datarray['body'],
  584. 'tag' => $datarray['tag'],
  585. 'attach' => $datarray['attach'],
  586. 'file' => $datarray['file'],
  587. 'rendered-html' => $datarray['rendered-html'],
  588. 'rendered-hash' => $datarray['rendered-hash'],
  589. 'edited' => DateTimeFormat::utcNow(),
  590. 'changed' => DateTimeFormat::utcNow()];
  591. Item::update($fields, ['id' => $post_id]);
  592. // update filetags in pconfig
  593. FileTag::updatePconfig($uid, $categories_old, $categories_new, 'category');
  594. if (!empty($_REQUEST['return']) && strlen($return_path)) {
  595. Logger::log('return: ' . $return_path);
  596. $a->internalRedirect($return_path);
  597. }
  598. killme();
  599. } else {
  600. $post_id = 0;
  601. }
  602. unset($datarray['edit']);
  603. unset($datarray['self']);
  604. unset($datarray['api_source']);
  605. if ($origin) {
  606. $signed = Diaspora::createCommentSignature($uid, $datarray);
  607. if (!empty($signed)) {
  608. $datarray['diaspora_signed_text'] = json_encode($signed);
  609. }
  610. }
  611. $post_id = Item::insert($datarray);
  612. if (!$post_id) {
  613. Logger::log("Item wasn't stored.");
  614. $a->internalRedirect($return_path);
  615. }
  616. $datarray = Item::selectFirst(Item::ITEM_FIELDLIST, ['id' => $post_id]);
  617. if (!DBA::isResult($datarray)) {
  618. Logger::log("Item with id ".$post_id." couldn't be fetched.");
  619. $a->internalRedirect($return_path);
  620. }
  621. // update filetags in pconfig
  622. FileTag::updatePconfig($uid, $categories_old, $categories_new, 'category');
  623. // These notifications are sent if someone else is commenting other your wall
  624. if ($parent) {
  625. if ($contact_record != $author) {
  626. notification([
  627. 'type' => NOTIFY_COMMENT,
  628. 'notify_flags' => $user['notify-flags'],
  629. 'language' => $user['language'],
  630. 'to_name' => $user['username'],
  631. 'to_email' => $user['email'],
  632. 'uid' => $user['uid'],
  633. 'item' => $datarray,
  634. 'link' => System::baseUrl().'/display/'.urlencode($datarray['guid']),
  635. 'source_name' => $datarray['author-name'],
  636. 'source_link' => $datarray['author-link'],
  637. 'source_photo' => $datarray['author-avatar'],
  638. 'verb' => ACTIVITY_POST,
  639. 'otype' => 'item',
  640. 'parent' => $parent,
  641. 'parent_uri' => $parent_item['uri']
  642. ]);
  643. }
  644. } else {
  645. if (($contact_record != $author) && !count($forum_contact)) {
  646. notification([
  647. 'type' => NOTIFY_WALL,
  648. 'notify_flags' => $user['notify-flags'],
  649. 'language' => $user['language'],
  650. 'to_name' => $user['username'],
  651. 'to_email' => $user['email'],
  652. 'uid' => $user['uid'],
  653. 'item' => $datarray,
  654. 'link' => System::baseUrl().'/display/'.urlencode($datarray['guid']),
  655. 'source_name' => $datarray['author-name'],
  656. 'source_link' => $datarray['author-link'],
  657. 'source_photo' => $datarray['author-avatar'],
  658. 'verb' => ACTIVITY_POST,
  659. 'otype' => 'item'
  660. ]);
  661. }
  662. }
  663. Addon::callHooks('post_local_end', $datarray);
  664. if (strlen($emailcc) && $profile_uid == local_user()) {
  665. $erecips = explode(',', $emailcc);
  666. if (count($erecips)) {
  667. foreach ($erecips as $recip) {
  668. $addr = trim($recip);
  669. if (!strlen($addr)) {
  670. continue;
  671. }
  672. $disclaimer = '<hr />' . L10n::t('This message was sent to you by %s, a member of the Friendica social network.', $a->user['username'])
  673. . '<br />';
  674. $disclaimer .= L10n::t('You may visit them online at %s', System::baseUrl() . '/profile/' . $a->user['nickname']) . EOL;
  675. $disclaimer .= L10n::t('Please contact the sender by replying to this post if you do not wish to receive these messages.') . EOL;
  676. if (!$datarray['title']=='') {
  677. $subject = Email::encodeHeader($datarray['title'], 'UTF-8');
  678. } else {
  679. $subject = Email::encodeHeader('[Friendica]' . ' ' . L10n::t('%s posted an update.', $a->user['username']), 'UTF-8');
  680. }
  681. $link = '<a href="' . System::baseUrl() . '/profile/' . $a->user['nickname'] . '"><img src="' . $author['thumb'] . '" alt="' . $a->user['username'] . '" /></a><br /><br />';
  682. $html = Item::prepareBody($datarray);
  683. $message = '<html><body>' . $link . $html . $disclaimer . '</body></html>';
  684. $params = [
  685. 'fromName' => $a->user['username'],
  686. 'fromEmail' => $a->user['email'],
  687. 'toEmail' => $addr,
  688. 'replyTo' => $a->user['email'],
  689. 'messageSubject' => $subject,
  690. 'htmlVersion' => $message,
  691. 'textVersion' => HTML::toPlaintext($html.$disclaimer)
  692. ];
  693. Emailer::send($params);
  694. }
  695. }
  696. }
  697. // Insert an item entry for UID=0 for global entries.
  698. // We now do it in the background to save some time.
  699. // This is important in interactive environments like the frontend or the API.
  700. // We don't fork a new process since this is done anyway with the following command
  701. Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => true], "CreateShadowEntry", $post_id);
  702. // When we are doing some forum posting via ! we have to start the notifier manually.
  703. // These kind of posts don't initiate the notifier call in the item class.
  704. if ($only_to_forum) {
  705. Worker::add(PRIORITY_HIGH, "Notifier", $notify_type, $post_id);
  706. }
  707. Logger::log('post_complete');
  708. if ($api_source) {
  709. return $post_id;
  710. }
  711. item_post_return(System::baseUrl(), $api_source, $return_path);
  712. // NOTREACHED
  713. }
  714. function item_post_return($baseurl, $api_source, $return_path)
  715. {
  716. // figure out how to return, depending on from whence we came
  717. $a = get_app();
  718. if ($api_source) {
  719. return;
  720. }
  721. if ($return_path) {
  722. $a->internalRedirect($return_path);
  723. }
  724. $json = ['success' => 1];
  725. if (!empty($_REQUEST['jsreload'])) {
  726. $json['reload'] = $baseurl . '/' . $_REQUEST['jsreload'];
  727. }
  728. Logger::log('post_json: ' . print_r($json, true), Logger::DEBUG);
  729. echo json_encode($json);
  730. killme();
  731. }
  732. function item_content(App $a)
  733. {
  734. if (!local_user() && !remote_user()) {
  735. return;
  736. }
  737. $o = '';
  738. if (($a->argc >= 3) && ($a->argv[1] === 'drop') && intval($a->argv[2])) {
  739. if ($a->isAjax()) {
  740. $o = Item::deleteForUser(['id' => $a->argv[2]], local_user());
  741. } else {
  742. if (!empty($a->argv[3])) {
  743. $o = drop_item($a->argv[2], $a->argv[3]);
  744. }
  745. else {
  746. $o = drop_item($a->argv[2]);
  747. }
  748. }
  749. if ($a->isAjax()) {
  750. // ajax return: [<item id>, 0 (no perm) | <owner id>]
  751. echo json_encode([intval($a->argv[2]), intval($o)]);
  752. killme();
  753. }
  754. }
  755. return $o;
  756. }
  757. /**
  758. * This function removes the tag $tag from the text $body and replaces it with
  759. * the appropiate link.
  760. *
  761. * @param App $a Application instance @TODO is unused in this function's scope (excluding included files)
  762. * @param unknown_type $body the text to replace the tag in
  763. * @param string $inform a comma-seperated string containing everybody to inform
  764. * @param string $str_tags string to add the tag to
  765. * @param integer $profile_uid
  766. * @param string $tag the tag to replace
  767. * @param string $network The network of the post
  768. *
  769. * @return boolean true if replaced, false if not replaced
  770. */
  771. function handle_tag(App $a, &$body, &$inform, &$str_tags, $profile_uid, $tag, $network = "")
  772. {
  773. $replaced = false;
  774. $r = null;
  775. $tag_type = '@';
  776. //is it a person tag?
  777. if ((strpos($tag, '@') === 0) || (strpos($tag, '!') === 0)) {
  778. $tag_type = substr($tag, 0, 1);
  779. //is it already replaced?
  780. if (strpos($tag, '[url=')) {
  781. //append tag to str_tags
  782. if (!stristr($str_tags, $tag)) {
  783. if (strlen($str_tags)) {
  784. $str_tags .= ',';
  785. }
  786. $str_tags .= $tag;
  787. }
  788. // Checking for the alias that is used for OStatus
  789. $pattern = "/[@!]\[url\=(.*?)\](.*?)\[\/url\]/ism";
  790. if (preg_match($pattern, $tag, $matches)) {
  791. $data = Contact::getDetailsByURL($matches[1]);
  792. if ($data["alias"] != "") {
  793. $newtag = '@[url=' . $data["alias"] . ']' . $data["nick"] . '[/url]';
  794. if (!stripos($str_tags, '[url=' . $data["alias"] . ']')) {
  795. if (strlen($str_tags)) {
  796. $str_tags .= ',';
  797. }
  798. $str_tags .= $newtag;
  799. }
  800. }
  801. }
  802. return $replaced;
  803. }
  804. $stat = false;
  805. //get the person's name
  806. $name = substr($tag, 1);
  807. // Sometimes the tag detection doesn't seem to work right
  808. // This is some workaround
  809. $nameparts = explode(" ", $name);
  810. $name = $nameparts[0];
  811. // Try to detect the contact in various ways
  812. if (strpos($name, 'http://')) {
  813. // At first we have to ensure that the contact exists
  814. Contact::getIdForURL($name);
  815. // Now we should have something
  816. $contact = Contact::getDetailsByURL($name);
  817. } elseif (strpos($name, '@')) {
  818. // This function automatically probes when no entry was found
  819. $contact = Contact::getDetailsByAddr($name);
  820. } else {
  821. $contact = false;
  822. $fields = ['id', 'url', 'nick', 'name', 'alias', 'network', 'forum', 'prv'];
  823. if (strrpos($name, '+')) {
  824. // Is it in format @nick+number?
  825. $tagcid = intval(substr($name, strrpos($name, '+') + 1));
  826. $contact = DBA::selectFirst('contact', $fields, ['id' => $tagcid, 'uid' => $profile_uid]);
  827. }
  828. // select someone by nick or attag in the current network
  829. if (!DBA::isResult($contact) && ($network != "")) {
  830. $condition = ["(`nick` = ? OR `attag` = ?) AND `network` = ? AND `uid` = ?",
  831. $name, $name, $network, $profile_uid];
  832. $contact = DBA::selectFirst('contact', $fields, $condition);
  833. }
  834. //select someone by name in the current network
  835. if (!DBA::isResult($contact) && ($network != "")) {
  836. $condition = ['name' => $name, 'network' => $network, 'uid' => $profile_uid];
  837. $contact = DBA::selectFirst('contact', $fields, $condition);
  838. }
  839. // select someone by nick or attag in any network
  840. if (!DBA::isResult($contact)) {
  841. $condition = ["(`nick` = ? OR `attag` = ?) AND `uid` = ?", $name, $name, $profile_uid];
  842. $contact = DBA::selectFirst('contact', $fields, $condition);
  843. }
  844. // select someone by name in any network
  845. if (!DBA::isResult($contact)) {
  846. $condition = ['name' => $name, 'uid' => $profile_uid];
  847. $contact = DBA::selectFirst('contact', $fields, $condition);
  848. }
  849. }
  850. // Check if $contact has been successfully loaded
  851. if (DBA::isResult($contact)) {
  852. if (strlen($inform) && (isset($contact["notify"]) || isset($contact["id"]))) {
  853. $inform .= ',';
  854. }
  855. if (isset($contact["id"])) {
  856. $inform .= 'cid:' . $contact["id"];
  857. } elseif (isset($contact["notify"])) {
  858. $inform .= $contact["notify"];
  859. }
  860. $profile = $contact["url"];
  861. $alias = $contact["alias"];
  862. $newname = defaults($contact, "name", $contact["nick"]);
  863. }
  864. //if there is an url for this persons profile
  865. if (isset($profile) && ($newname != "")) {
  866. $replaced = true;
  867. // create profile link
  868. $profile = str_replace(',', '%2c', $profile);
  869. $newtag = $tag_type.'[url=' . $profile . ']' . $newname . '[/url]';
  870. $body = str_replace($tag_type . $name, $newtag, $body);
  871. // append tag to str_tags
  872. if (!stristr($str_tags, $newtag)) {
  873. if (strlen($str_tags)) {
  874. $str_tags .= ',';
  875. }
  876. $str_tags .= $newtag;
  877. }
  878. /*
  879. * Status.Net seems to require the numeric ID URL in a mention if the person isn't
  880. * subscribed to you. But the nickname URL is OK if they are. Grrr. We'll tag both.
  881. */
  882. if (strlen($alias)) {
  883. $newtag = '@[url=' . $alias . ']' . $newname . '[/url]';
  884. if (!stripos($str_tags, '[url=' . $alias . ']')) {
  885. if (strlen($str_tags)) {
  886. $str_tags .= ',';
  887. }
  888. $str_tags .= $newtag;
  889. }
  890. }
  891. }
  892. }
  893. return ['replaced' => $replaced, 'contact' => $contact];
  894. }