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.

1062 lines
34 KiB

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