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.

2258 lines
74 KiB

11 years ago
11 years ago
9 years ago
4 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
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. <?php
  2. /**
  3. * @file include/items.php
  4. */
  5. use \Friendica\ParseUrl;
  6. require_once('include/bbcode.php');
  7. require_once('include/oembed.php');
  8. require_once('include/salmon.php');
  9. require_once('include/crypto.php');
  10. require_once('include/Photo.php');
  11. require_once('include/tags.php');
  12. require_once('include/files.php');
  13. require_once('include/text.php');
  14. require_once('include/email.php');
  15. require_once('include/threads.php');
  16. require_once('include/socgraph.php');
  17. require_once('include/plaintext.php');
  18. require_once('include/ostatus.php');
  19. require_once('include/feed.php');
  20. require_once('include/Contact.php');
  21. require_once('mod/share.php');
  22. require_once('include/enotify.php');
  23. require_once('include/dfrn.php');
  24. require_once('include/group.php');
  25. require_once('library/defuse/php-encryption-1.2.1/Crypto.php');
  26. function construct_verb($item) {
  27. if ($item['verb'])
  28. return $item['verb'];
  29. return ACTIVITY_POST;
  30. }
  31. /* limit_body_size()
  32. *
  33. * The purpose of this function is to apply system message length limits to
  34. * imported messages without including any embedded photos in the length
  35. */
  36. if (! function_exists('limit_body_size')) {
  37. function limit_body_size($body) {
  38. // logger('limit_body_size: start', LOGGER_DEBUG);
  39. $maxlen = get_max_import_size();
  40. // If the length of the body, including the embedded images, is smaller
  41. // than the maximum, then don't waste time looking for the images
  42. if ($maxlen && (strlen($body) > $maxlen)) {
  43. logger('limit_body_size: the total body length exceeds the limit', LOGGER_DEBUG);
  44. $orig_body = $body;
  45. $new_body = '';
  46. $textlen = 0;
  47. $max_found = false;
  48. $img_start = strpos($orig_body, '[img');
  49. $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
  50. $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
  51. while(($img_st_close !== false) && ($img_end !== false)) {
  52. $img_st_close++; // make it point to AFTER the closing bracket
  53. $img_end += $img_start;
  54. $img_end += strlen('[/img]');
  55. if (! strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
  56. // This is an embedded image
  57. if ( ($textlen + $img_start) > $maxlen ) {
  58. if ($textlen < $maxlen) {
  59. logger('limit_body_size: the limit happens before an embedded image', LOGGER_DEBUG);
  60. $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
  61. $textlen = $maxlen;
  62. }
  63. } else {
  64. $new_body = $new_body . substr($orig_body, 0, $img_start);
  65. $textlen += $img_start;
  66. }
  67. $new_body = $new_body . substr($orig_body, $img_start, $img_end - $img_start);
  68. } else {
  69. if ( ($textlen + $img_end) > $maxlen ) {
  70. if ($textlen < $maxlen) {
  71. logger('limit_body_size: the limit happens before the end of a non-embedded image', LOGGER_DEBUG);
  72. $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
  73. $textlen = $maxlen;
  74. }
  75. } else {
  76. $new_body = $new_body . substr($orig_body, 0, $img_end);
  77. $textlen += $img_end;
  78. }
  79. }
  80. $orig_body = substr($orig_body, $img_end);
  81. if ($orig_body === false) // in case the body ends on a closing image tag
  82. $orig_body = '';
  83. $img_start = strpos($orig_body, '[img');
  84. $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
  85. $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
  86. }
  87. if ( ($textlen + strlen($orig_body)) > $maxlen) {
  88. if ($textlen < $maxlen) {
  89. logger('limit_body_size: the limit happens after the end of the last image', LOGGER_DEBUG);
  90. $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
  91. $textlen = $maxlen;
  92. }
  93. } else {
  94. logger('limit_body_size: the text size with embedded images extracted did not violate the limit', LOGGER_DEBUG);
  95. $new_body = $new_body . $orig_body;
  96. $textlen += strlen($orig_body);
  97. }
  98. return $new_body;
  99. } else
  100. return $body;
  101. }}
  102. function title_is_body($title, $body) {
  103. $title = strip_tags($title);
  104. $title = trim($title);
  105. $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
  106. $title = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $title);
  107. $body = strip_tags($body);
  108. $body = trim($body);
  109. $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
  110. $body = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $body);
  111. if (strlen($title) < strlen($body))
  112. $body = substr($body, 0, strlen($title));
  113. if (($title != $body) and (substr($title, -3) == "...")) {
  114. $pos = strrpos($title, "...");
  115. if ($pos > 0) {
  116. $title = substr($title, 0, $pos);
  117. $body = substr($body, 0, $pos);
  118. }
  119. }
  120. return($title == $body);
  121. }
  122. function add_page_info_data($data) {
  123. call_hooks('page_info_data', $data);
  124. // It maybe is a rich content, but if it does have everything that a link has,
  125. // then treat it that way
  126. if (($data["type"] == "rich") AND is_string($data["title"]) AND
  127. is_string($data["text"]) AND (sizeof($data["images"]) > 0)) {
  128. $data["type"] = "link";
  129. }
  130. if ((($data["type"] != "link") AND ($data["type"] != "video") AND ($data["type"] != "photo")) OR ($data["title"] == $data["url"])) {
  131. return "";
  132. }
  133. if ($no_photos AND ($data["type"] == "photo")) {
  134. return "";
  135. }
  136. if (sizeof($data["images"]) > 0) {
  137. $preview = $data["images"][0];
  138. } else {
  139. $preview = "";
  140. }
  141. // Escape some bad characters
  142. $data["url"] = str_replace(array("[", "]"), array("&#91;", "&#93;"), htmlentities($data["url"], ENT_QUOTES, 'UTF-8', false));
  143. $data["title"] = str_replace(array("[", "]"), array("&#91;", "&#93;"), htmlentities($data["title"], ENT_QUOTES, 'UTF-8', false));
  144. $text = "[attachment type='".$data["type"]."'";
  145. if ($data["text"] == "") {
  146. $data["text"] = $data["title"];
  147. }
  148. if ($data["text"] == "") {
  149. $data["text"] = $data["url"];
  150. }
  151. if ($data["url"] != "") {
  152. $text .= " url='".$data["url"]."'";
  153. }
  154. if ($data["title"] != "") {
  155. $text .= " title='".$data["title"]."'";
  156. }
  157. if (sizeof($data["images"]) > 0) {
  158. $preview = str_replace(array("[", "]"), array("&#91;", "&#93;"), htmlentities($data["images"][0]["src"], ENT_QUOTES, 'UTF-8', false));
  159. // if the preview picture is larger than 500 pixels then show it in a larger mode
  160. // But only, if the picture isn't higher than large (To prevent huge posts)
  161. if (($data["images"][0]["width"] >= 500) AND ($data["images"][0]["width"] >= $data["images"][0]["height"])) {
  162. $text .= " image='".$preview."'";
  163. } else {
  164. $text .= " preview='".$preview."'";
  165. }
  166. }
  167. $text .= "]".$data["text"]."[/attachment]";
  168. $hashtags = "";
  169. if (isset($data["keywords"]) AND count($data["keywords"])) {
  170. $hashtags = "\n";
  171. foreach ($data["keywords"] AS $keyword) {
  172. /// @todo make a positive list of allowed characters
  173. $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'", "", "`", "(", ")", "", ""),
  174. array("","", "", "", "", "", "", "", "", "", "", ""), $keyword);
  175. $hashtags .= "#[url=".App::get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url] ";
  176. }
  177. }
  178. return "\n".$text.$hashtags;
  179. }
  180. function query_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
  181. $data = ParseUrl::getSiteinfoCached($url, true);
  182. if ($photo != "")
  183. $data["images"][0]["src"] = $photo;
  184. logger('fetch page info for '.$url.' '.print_r($data, true), LOGGER_DEBUG);
  185. if (!$keywords AND isset($data["keywords"]))
  186. unset($data["keywords"]);
  187. if (($keyword_blacklist != "") AND isset($data["keywords"])) {
  188. $list = explode(",", $keyword_blacklist);
  189. foreach ($list AS $keyword) {
  190. $keyword = trim($keyword);
  191. $index = array_search($keyword, $data["keywords"]);
  192. if ($index !== false)
  193. unset($data["keywords"][$index]);
  194. }
  195. }
  196. return($data);
  197. }
  198. function add_page_keywords($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
  199. $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
  200. $tags = "";
  201. if (isset($data["keywords"]) AND count($data["keywords"])) {
  202. foreach ($data["keywords"] AS $keyword) {
  203. $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'"),
  204. array("","", "", "", "", ""), $keyword);
  205. if ($tags != "")
  206. $tags .= ",";
  207. $tags .= "#[url=".App::get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url]";
  208. }
  209. }
  210. return($tags);
  211. }
  212. function add_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
  213. $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
  214. $text = add_page_info_data($data);
  215. return($text);
  216. }
  217. function add_page_info_to_body($body, $texturl = false, $no_photos = false) {
  218. logger('add_page_info_to_body: fetch page info for body '.$body, LOGGER_DEBUG);
  219. $URLSearchString = "^\[\]";
  220. // Fix for Mastodon where the mentions are in a different format
  221. $body = preg_replace("/\[url\=([$URLSearchString]*)\]([#!@])(.*?)\[\/url\]/ism",
  222. '$2[url=$1]$3[/url]', $body);
  223. // Adding these spaces is a quick hack due to my problems with regular expressions :)
  224. preg_match("/[^!#@]\[url\]([$URLSearchString]*)\[\/url\]/ism", " ".$body, $matches);
  225. if (!$matches)
  226. preg_match("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", " ".$body, $matches);
  227. // Convert urls without bbcode elements
  228. if (!$matches AND $texturl) {
  229. preg_match("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", " ".$body, $matches);
  230. // Yeah, a hack. I really hate regular expressions :)
  231. if ($matches)
  232. $matches[1] = $matches[2];
  233. }
  234. if ($matches)
  235. $footer = add_page_info($matches[1], $no_photos);
  236. // Remove the link from the body if the link is attached at the end of the post
  237. if (isset($footer) AND (trim($footer) != "") AND (strpos($footer, $matches[1]))) {
  238. $removedlink = trim(str_replace($matches[1], "", $body));
  239. if (($removedlink == "") OR strstr($body, $removedlink))
  240. $body = $removedlink;
  241. $url = str_replace(array('/', '.'), array('\/', '\.'), $matches[1]);
  242. $removedlink = preg_replace("/\[url\=".$url."\](.*?)\[\/url\]/ism", '', $body);
  243. if (($removedlink == "") OR strstr($body, $removedlink))
  244. $body = $removedlink;
  245. }
  246. // Add the page information to the bottom
  247. if (isset($footer) AND (trim($footer) != ""))
  248. $body .= $footer;
  249. return $body;
  250. }
  251. /**
  252. * Adds a "lang" specification in a "postopts" element of given $arr,
  253. * if possible and not already present.
  254. * Expects "body" element to exist in $arr.
  255. *
  256. * @todo Add a parameter to request forcing override
  257. */
  258. function item_add_language_opt(&$arr) {
  259. if (version_compare(PHP_VERSION, '5.3.0', '<')) return; // LanguageDetect.php not available ?
  260. if ( x($arr, 'postopts') )
  261. {
  262. if ( strstr($arr['postopts'], 'lang=') )
  263. {
  264. // do not override
  265. /// @TODO Add parameter to request overriding
  266. return;
  267. }
  268. $postopts = $arr['postopts'];
  269. } else {
  270. $postopts = "";
  271. }
  272. require_once('library/langdet/Text/LanguageDetect.php');
  273. $naked_body = preg_replace('/\[(.+?)\]/','',$arr['body']);
  274. $l = new Text_LanguageDetect;
  275. //$lng = $l->detectConfidence($naked_body);
  276. //$arr['postopts'] = (($lng['language']) ? 'lang=' . $lng['language'] . ';' . $lng['confidence'] : '');
  277. $lng = $l->detect($naked_body, 3);
  278. if (sizeof($lng) > 0) {
  279. if ($postopts != "") $postopts .= '&'; // arbitrary separator, to be reviewed
  280. $postopts .= 'lang=';
  281. $sep = "";
  282. foreach ($lng as $language => $score) {
  283. $postopts .= $sep . $language.";".$score;
  284. $sep = ':';
  285. }
  286. $arr['postopts'] = $postopts;
  287. }
  288. }
  289. /**
  290. * @brief Creates an unique guid out of a given uri
  291. *
  292. * @param string $uri uri of an item entry
  293. * @param string $host (Optional) hostname for the GUID prefix
  294. * @return string unique guid
  295. */
  296. function uri_to_guid($uri, $host = "") {
  297. // Our regular guid routine is using this kind of prefix as well
  298. // We have to avoid that different routines could accidentally create the same value
  299. $parsed = parse_url($uri);
  300. if ($host == "") {
  301. $host = $parsed["host"];
  302. }
  303. $guid_prefix = hash("crc32", $host);
  304. // Remove the scheme to make sure that "https" and "http" doesn't make a difference
  305. unset($parsed["scheme"]);
  306. $host_id = implode("/", $parsed);
  307. // We could use any hash algorithm since it isn't a security issue
  308. $host_hash = hash("ripemd128", $host_id);
  309. return $guid_prefix.$host_hash;
  310. }
  311. function item_store($arr,$force_parent = false, $notify = false, $dontcache = false) {
  312. $a = get_app();
  313. // If it is a posting where users should get notifications, then define it as wall posting
  314. if ($notify) {
  315. $arr['wall'] = 1;
  316. $arr['type'] = 'wall';
  317. $arr['origin'] = 1;
  318. $arr['last-child'] = 1;
  319. $arr['network'] = NETWORK_DFRN;
  320. // We have to avoid duplicates. So we create the GUID in form of a hash of the plink or uri.
  321. // In difference to the call to "uri_to_guid" several lines below we add the hash of our own host.
  322. // This is done because our host is the original creator of the post.
  323. if (isset($arr['plink'])) {
  324. $arr['guid'] = uri_to_guid($arr['plink'], $a->get_hostname());
  325. } elseif (isset($arr['uri'])) {
  326. $arr['guid'] = uri_to_guid($arr['uri'], $a->get_hostname());
  327. }
  328. }
  329. // If a Diaspora signature structure was passed in, pull it out of the
  330. // item array and set it aside for later storage.
  331. $dsprsig = null;
  332. if (x($arr,'dsprsig')) {
  333. $encoded_signature = $arr['dsprsig'];
  334. $dsprsig = json_decode(base64_decode($arr['dsprsig']));
  335. unset($arr['dsprsig']);
  336. }
  337. // Converting the plink
  338. if ($arr['network'] == NETWORK_OSTATUS) {
  339. if (isset($arr['plink']))
  340. $arr['plink'] = ostatus::convert_href($arr['plink']);
  341. elseif (isset($arr['uri']))
  342. $arr['plink'] = ostatus::convert_href($arr['uri']);
  343. }
  344. if (x($arr, 'gravity'))
  345. $arr['gravity'] = intval($arr['gravity']);
  346. elseif ($arr['parent-uri'] === $arr['uri'])
  347. $arr['gravity'] = 0;
  348. elseif (activity_match($arr['verb'],ACTIVITY_POST))
  349. $arr['gravity'] = 6;
  350. else
  351. $arr['gravity'] = 6; // extensible catchall
  352. if (! x($arr,'type'))
  353. $arr['type'] = 'remote';
  354. /* check for create date and expire time */
  355. $uid = intval($arr['uid']);
  356. $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
  357. if (dbm::is_result($r)) {
  358. $expire_interval = $r[0]['expire'];
  359. if ($expire_interval>0) {
  360. $expire_date = new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
  361. $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
  362. if ($created_date < $expire_date) {
  363. logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
  364. return 0;
  365. }
  366. }
  367. }
  368. // Do we already have this item?
  369. // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
  370. if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
  371. $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s') LIMIT 1",
  372. dbesc(trim($arr['uri'])),
  373. intval($uid),
  374. dbesc(NETWORK_DIASPORA),
  375. dbesc(NETWORK_DFRN),
  376. dbesc(NETWORK_OSTATUS)
  377. );
  378. if ($r) {
  379. // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
  380. if ($uid != 0)
  381. logger("Item with uri ".$arr['uri']." already existed for user ".$uid." with id ".$r[0]["id"]." target network ".$r[0]["network"]." - new network: ".$arr['network']);
  382. return($r[0]["id"]);
  383. }
  384. }
  385. // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
  386. // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
  387. //if ((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
  388. // $arr['body'] = strip_tags($arr['body']);
  389. item_add_language_opt($arr);
  390. if ($notify)
  391. $guid_prefix = "";
  392. elseif ((trim($arr['guid']) == "") AND (trim($arr['plink']) != ""))
  393. $arr['guid'] = uri_to_guid($arr['plink']);
  394. elseif ((trim($arr['guid']) == "") AND (trim($arr['uri']) != ""))
  395. $arr['guid'] = uri_to_guid($arr['uri']);
  396. else {
  397. $parsed = parse_url($arr["author-link"]);
  398. $guid_prefix = hash("crc32", $parsed["host"]);
  399. }
  400. $arr['wall'] = ((x($arr,'wall')) ? intval($arr['wall']) : 0);
  401. $arr['guid'] = ((x($arr,'guid')) ? notags(trim($arr['guid'])) : get_guid(32, $guid_prefix));
  402. $arr['uri'] = ((x($arr,'uri')) ? notags(trim($arr['uri'])) : item_new_uri($a->get_hostname(), $uid, $arr['guid']));
  403. $arr['extid'] = ((x($arr,'extid')) ? notags(trim($arr['extid'])) : '');
  404. $arr['author-name'] = ((x($arr,'author-name')) ? trim($arr['author-name']) : '');
  405. $arr['author-link'] = ((x($arr,'author-link')) ? notags(trim($arr['author-link'])) : '');
  406. $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
  407. $arr['owner-name'] = ((x($arr,'owner-name')) ? trim($arr['owner-name']) : '');
  408. $arr['owner-link'] = ((x($arr,'owner-link')) ? notags(trim($arr['owner-link'])) : '');
  409. $arr['owner-avatar'] = ((x($arr,'owner-avatar')) ? notags(trim($arr['owner-avatar'])) : '');
  410. $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert());
  411. $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert());
  412. $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert());
  413. $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert());
  414. $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert());
  415. $arr['title'] = ((x($arr,'title')) ? trim($arr['title']) : '');
  416. $arr['location'] = ((x($arr,'location')) ? trim($arr['location']) : '');
  417. $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : '');
  418. $arr['last-child'] = ((x($arr,'last-child')) ? intval($arr['last-child']) : 0 );
  419. $arr['visible'] = ((x($arr,'visible') !== false) ? intval($arr['visible']) : 1 );
  420. $arr['deleted'] = 0;
  421. $arr['parent-uri'] = ((x($arr,'parent-uri')) ? notags(trim($arr['parent-uri'])) : $arr['uri']);
  422. $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : '');
  423. $arr['object-type'] = ((x($arr,'object-type')) ? notags(trim($arr['object-type'])) : '');
  424. $arr['object'] = ((x($arr,'object')) ? trim($arr['object']) : '');
  425. $arr['target-type'] = ((x($arr,'target-type')) ? notags(trim($arr['target-type'])) : '');
  426. $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : '');
  427. $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : '');
  428. $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : '');
  429. $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : '');
  430. $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : '');
  431. $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : '');
  432. $arr['private'] = ((x($arr,'private')) ? intval($arr['private']) : 0 );
  433. $arr['bookmark'] = ((x($arr,'bookmark')) ? intval($arr['bookmark']) : 0 );
  434. $arr['body'] = ((x($arr,'body')) ? trim($arr['body']) : '');
  435. $arr['tag'] = ((x($arr,'tag')) ? notags(trim($arr['tag'])) : '');
  436. $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : '');
  437. $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : '');
  438. $arr['origin'] = ((x($arr,'origin')) ? intval($arr['origin']) : 0 );
  439. $arr['network'] = ((x($arr,'network')) ? trim($arr['network']) : '');
  440. $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : '');
  441. $arr['resource-id'] = ((x($arr,'resource-id')) ? trim($arr['resource-id']) : '');
  442. $arr['event-id'] = ((x($arr,'event-id')) ? intval($arr['event-id']) : 0 );
  443. $arr['inform'] = ((x($arr,'inform')) ? trim($arr['inform']) : '');
  444. $arr['file'] = ((x($arr,'file')) ? trim($arr['file']) : '');
  445. // Items cannot be stored before they happen ...
  446. if ($arr['created'] > datetime_convert())
  447. $arr['created'] = datetime_convert();
  448. // We haven't invented time travel by now.
  449. if ($arr['edited'] > datetime_convert())
  450. $arr['edited'] = datetime_convert();
  451. if (($arr['author-link'] == "") AND ($arr['owner-link'] == ""))
  452. logger("Both author-link and owner-link are empty. Called by: ".App::callstack(), LOGGER_DEBUG);
  453. if ($arr['plink'] == "") {
  454. $arr['plink'] = App::get_baseurl().'/display/'.urlencode($arr['guid']);
  455. }
  456. if ($arr['network'] == "") {
  457. $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
  458. dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
  459. dbesc(normalise_link($arr['author-link'])),
  460. intval($arr['uid'])
  461. );
  462. if (!dbm::is_result($r))
  463. $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
  464. dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
  465. dbesc(normalise_link($arr['author-link']))
  466. );
  467. if (!dbm::is_result($r))
  468. $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
  469. intval($arr['contact-id']),
  470. intval($arr['uid'])
  471. );
  472. if (dbm::is_result($r))
  473. $arr['network'] = $r[0]["network"];
  474. // Fallback to friendica (why is it empty in some cases?)
  475. if ($arr['network'] == "")
  476. $arr['network'] = NETWORK_DFRN;
  477. logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
  478. }
  479. // The contact-id should be set before "item_store" was called - but there seems to be some issues
  480. if ($arr["contact-id"] == 0) {
  481. // First we are looking for a suitable contact that matches with the author of the post
  482. // This is done only for comments (See below explanation at "gcontact-id")
  483. if ($arr['parent-uri'] != $arr['uri'])
  484. $arr["contact-id"] = get_contact($arr['author-link'], $uid);
  485. // If not present then maybe the owner was found
  486. if ($arr["contact-id"] == 0)
  487. $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
  488. // Still missing? Then use the "self" contact of the current user
  489. if ($arr["contact-id"] == 0) {
  490. $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
  491. if ($r)
  492. $arr["contact-id"] = $r[0]["id"];
  493. }
  494. logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
  495. }
  496. if ($arr["gcontact-id"] == 0) {
  497. // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
  498. // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
  499. // On comments the author is the better choice.
  500. if ($arr['parent-uri'] === $arr['uri'])
  501. $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
  502. "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
  503. else
  504. $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
  505. "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
  506. }
  507. if ($arr["author-id"] == 0)
  508. $arr["author-id"] = get_contact($arr["author-link"], 0);
  509. if ($arr["owner-id"] == 0)
  510. $arr["owner-id"] = get_contact($arr["owner-link"], 0);
  511. if ($arr['guid'] != "") {
  512. // Checking if there is already an item with the same guid
  513. logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
  514. $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
  515. dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
  516. if (dbm::is_result($r)) {
  517. logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
  518. return 0;
  519. }
  520. }
  521. // Check for hashtags in the body and repair or add hashtag links
  522. item_body_set_hashtags($arr);
  523. $arr['thr-parent'] = $arr['parent-uri'];
  524. if ($arr['parent-uri'] === $arr['uri']) {
  525. $parent_id = 0;
  526. $parent_deleted = 0;
  527. $allow_cid = $arr['allow_cid'];
  528. $allow_gid = $arr['allow_gid'];
  529. $deny_cid = $arr['deny_cid'];
  530. $deny_gid = $arr['deny_gid'];
  531. $notify_type = 'wall-new';
  532. } else {
  533. // find the parent and snarf the item id and ACLs
  534. // and anything else we need to inherit
  535. $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
  536. dbesc($arr['parent-uri']),
  537. intval($arr['uid'])
  538. );
  539. if (dbm::is_result($r)) {
  540. // is the new message multi-level threaded?
  541. // even though we don't support it now, preserve the info
  542. // and re-attach to the conversation parent.
  543. if ($r[0]['uri'] != $r[0]['parent-uri']) {
  544. $arr['parent-uri'] = $r[0]['parent-uri'];
  545. $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
  546. ORDER BY `id` ASC LIMIT 1",
  547. dbesc($r[0]['parent-uri']),
  548. dbesc($r[0]['parent-uri']),
  549. intval($arr['uid'])
  550. );
  551. if ($z && count($z))
  552. $r = $z;
  553. }
  554. $parent_id = $r[0]['id'];
  555. $parent_deleted = $r[0]['deleted'];
  556. $allow_cid = $r[0]['allow_cid'];
  557. $allow_gid = $r[0]['allow_gid'];
  558. $deny_cid = $r[0]['deny_cid'];
  559. $deny_gid = $r[0]['deny_gid'];
  560. $arr['wall'] = $r[0]['wall'];
  561. $notify_type = 'comment-new';
  562. // if the parent is private, force privacy for the entire conversation
  563. // This differs from the above settings as it subtly allows comments from
  564. // email correspondents to be private even if the overall thread is not.
  565. if ($r[0]['private'])
  566. $arr['private'] = $r[0]['private'];
  567. // Edge case. We host a public forum that was originally posted to privately.
  568. // The original author commented, but as this is a comment, the permissions
  569. // weren't fixed up so it will still show the comment as private unless we fix it here.
  570. if ((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
  571. $arr['private'] = 0;
  572. // If its a post from myself then tag the thread as "mention"
  573. logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
  574. $u = q("SELECT `nickname` FROM `user` WHERE `uid` = %d", intval($arr['uid']));
  575. if (dbm::is_result($u)) {
  576. $a = get_app();
  577. $self = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
  578. logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
  579. if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
  580. q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
  581. logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
  582. }
  583. }
  584. } else {
  585. // Allow one to see reply tweets from status.net even when
  586. // we don't have or can't see the original post.
  587. if ($force_parent) {
  588. logger('item_store: $force_parent=true, reply converted to top-level post.');
  589. $parent_id = 0;
  590. $arr['parent-uri'] = $arr['uri'];
  591. $arr['gravity'] = 0;
  592. } else {
  593. logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
  594. return 0;
  595. }
  596. $parent_deleted = 0;
  597. }
  598. }
  599. $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
  600. dbesc($arr['uri']),
  601. dbesc($arr['network']),
  602. dbesc(NETWORK_DFRN),
  603. intval($arr['uid'])
  604. );
  605. if (dbm::is_result($r)) {
  606. logger('duplicated item with the same uri found. '.print_r($arr,true));
  607. return 0;
  608. }
  609. // On Friendica and Diaspora the GUID is unique
  610. if (in_array($arr['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) {
  611. $r = q("SELECT `id` FROM `item` WHERE `guid` = '%s' AND `uid` = %d LIMIT 1",
  612. dbesc($arr['guid']),
  613. intval($arr['uid'])
  614. );
  615. if (dbm::is_result($r)) {
  616. logger('duplicated item with the same guid found. '.print_r($arr,true));
  617. return 0;
  618. }
  619. } else {
  620. // Check for an existing post with the same content. There seems to be a problem with OStatus.
  621. $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
  622. dbesc($arr['body']),
  623. dbesc($arr['network']),
  624. dbesc($arr['created']),
  625. intval($arr['contact-id']),
  626. intval($arr['uid'])
  627. );
  628. if (dbm::is_result($r)) {
  629. logger('duplicated item with the same body found. '.print_r($arr,true));
  630. return 0;
  631. }
  632. }
  633. // Is this item available in the global items (with uid=0)?
  634. if ($arr["uid"] == 0) {
  635. $arr["global"] = true;
  636. // Set the global flag on all items if this was a global item entry
  637. q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
  638. } else {
  639. $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
  640. $arr["global"] = (count($isglobal) > 0);
  641. }
  642. // ACL settings
  643. if (strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
  644. $private = 1;
  645. else
  646. $private = $arr['private'];
  647. $arr["allow_cid"] = $allow_cid;
  648. $arr["allow_gid"] = $allow_gid;
  649. $arr["deny_cid"] = $deny_cid;
  650. $arr["deny_gid"] = $deny_gid;
  651. $arr["private"] = $private;
  652. $arr["deleted"] = $parent_deleted;
  653. // Fill the cache field
  654. put_item_in_cache($arr);
  655. if ($notify)
  656. call_hooks('post_local',$arr);
  657. else
  658. call_hooks('post_remote',$arr);
  659. if (x($arr,'cancel')) {
  660. logger('item_store: post cancelled by plugin.');
  661. return 0;
  662. }
  663. // Check for already added items.
  664. // There is a timing issue here that sometimes creates double postings.
  665. // An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this.
  666. if ($arr["uid"] == 0) {
  667. $r = qu("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = 0 LIMIT 1", dbesc(trim($arr['uri'])));
  668. if (dbm::is_result($r)) {
  669. logger('Global item already stored. URI: '.$arr['uri'].' on network '.$arr['network'], LOGGER_DEBUG);
  670. return 0;
  671. }
  672. }
  673. // Store the unescaped version
  674. $unescaped = $arr;
  675. dbm::esc_array($arr, true);
  676. logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
  677. q("COMMIT");
  678. q("START TRANSACTION;");
  679. $r = dbq("INSERT INTO `item` (`"
  680. . implode("`, `", array_keys($arr))
  681. . "`) VALUES ("
  682. . implode(", ", array_values($arr))
  683. . ")");
  684. // And restore it
  685. $arr = $unescaped;
  686. // When the item was successfully stored we fetch the ID of the item.
  687. if (dbm::is_result($r)) {
  688. $r = q("SELECT LAST_INSERT_ID() AS `item-id`");
  689. if (dbm::is_result($r)) {
  690. $current_post = $r[0]['item-id'];
  691. } else {
  692. // This shouldn't happen
  693. $current_post = 0;
  694. }
  695. } else {
  696. // This can happen - for example - if there are locking timeouts.
  697. q("ROLLBACK");
  698. // Store the data into a spool file so that we can try again later.
  699. // At first we restore the Diaspora signature that we removed above.
  700. if (isset($encoded_signature)) {
  701. $arr['dsprsig'] = $encoded_signature;
  702. }
  703. // Now we store the data in the spool directory
  704. // We use "microtime" to keep the arrival order and "mt_rand" to avoid duplicates
  705. $file = 'item-'.round(microtime(true) * 10000).'-'.mt_rand().'.msg';
  706. $spoolpath = get_spoolpath();
  707. if ($spoolpath != "") {
  708. $spool = $spoolpath.'/'.$file;
  709. file_put_contents($spool, json_encode($arr));
  710. logger("Item wasn't stored - Item was spooled into file ".$file, LOGGER_DEBUG);
  711. }
  712. return 0;
  713. }
  714. if ($current_post == 0) {
  715. // This is one of these error messages that never should occur.
  716. logger("couldn't find created item - we better quit now.");
  717. q("ROLLBACK");
  718. return 0;
  719. }
  720. // How much entries have we created?
  721. // We wouldn't need this query when we could use an unique index - but MySQL has length problems with them.
  722. $r = q("SELECT COUNT(*) AS `entries` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s'",
  723. dbesc($arr['uri']),
  724. intval($arr['uid']),
  725. dbesc($arr['network'])
  726. );
  727. if (!dbm::is_result($r)) {
  728. // This shouldn't happen, since COUNT always works when the database connection is there.
  729. logger("We couldn't count the stored entries. Very strange ...");
  730. q("ROLLBACK");
  731. return 0;
  732. }
  733. if ($r[0]["entries"] > 1) {
  734. // There are duplicates. We delete our just created entry.
  735. logger('Duplicated post occurred. uri = '.$arr['uri'].' uid = '.$arr['uid']);
  736. // Yes, we could do a rollback here - but we are having many users with MyISAM.
  737. q("DELETE FROM `item` WHERE `id` = %d", intval($current_post));
  738. q("COMMIT");
  739. return 0;
  740. } elseif ($r[0]["entries"] == 0) {
  741. // This really should never happen since we quit earlier if there were problems.
  742. logger("Something is terribly wrong. We haven't found our created entry.");
  743. q("ROLLBACK");
  744. return 0;
  745. }
  746. logger('item_store: created item '.$current_post);
  747. item_set_last_item($arr);
  748. if (!$parent_id || ($arr['parent-uri'] === $arr['uri']))
  749. $parent_id = $current_post;
  750. // Set parent id
  751. $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
  752. intval($parent_id),
  753. intval($current_post)
  754. );
  755. $arr['id'] = $current_post;
  756. $arr['parent'] = $parent_id;
  757. // update the commented timestamp on the parent
  758. // Only update "commented" if it is really a comment
  759. if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
  760. q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
  761. dbesc(datetime_convert()),
  762. dbesc(datetime_convert()),
  763. intval($parent_id)
  764. );
  765. else
  766. q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
  767. dbesc(datetime_convert()),
  768. intval($parent_id)
  769. );
  770. if ($dsprsig) {
  771. // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
  772. // We can check for this condition when we decode and encode the stuff again.
  773. if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
  774. $dsprsig->signature = base64_decode($dsprsig->signature);
  775. logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
  776. }
  777. q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
  778. intval($current_post),
  779. dbesc($dsprsig->signed_text),
  780. dbesc($dsprsig->signature),
  781. dbesc($dsprsig->signer)
  782. );
  783. }
  784. $deleted = tag_deliver($arr['uid'],$current_post);
  785. // current post can be deleted if is for a community page and no mention are
  786. // in it.
  787. if (!$deleted AND !$dontcache) {
  788. $r = q('SELECT * FROM `item` WHERE `id` = %d', intval($current_post));
  789. if ((dbm::is_result($r)) && (count($r) == 1)) {
  790. if ($notify) {
  791. call_hooks('post_local_end', $r[0]);
  792. } else {
  793. call_hooks('post_remote_end', $r[0]);
  794. }
  795. } else {
  796. logger('item_store: new item not found in DB, id ' . $current_post);
  797. }
  798. }
  799. if ($arr['parent-uri'] === $arr['uri']) {
  800. add_thread($current_post);
  801. } else {
  802. update_thread($parent_id);
  803. }
  804. q("COMMIT");
  805. // Due to deadlock issues with the "term" table we are doing these steps after the commit.
  806. // This is not perfect - but a workable solution until we found the reason for the problem.
  807. create_tags_from_item($current_post);
  808. create_files_from_item($current_post);
  809. // If this is now the last-child, force all _other_ children of this parent to *not* be last-child
  810. // It is done after the transaction to avoid dead locks.
  811. if ($arr['last-child']) {
  812. $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
  813. dbesc($arr['uri']),
  814. intval($arr['uid']),
  815. intval($current_post)
  816. );
  817. }
  818. if ($arr['parent-uri'] === $arr['uri']) {
  819. add_shadow_thread($current_post);
  820. } else {
  821. add_shadow_entry($current_post);
  822. }
  823. check_item_notification($current_post, $uid);
  824. if ($notify) {
  825. proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
  826. }
  827. return $current_post;
  828. }
  829. /**
  830. * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
  831. *
  832. * This can be used to filter for inactive contacts.
  833. * Only do this for public postings to avoid privacy problems, since poco data is public.
  834. * Don't set this value if it isn't from the owner (could be an author that we don't know)
  835. *
  836. * @param array $arr Contains the just posted item record
  837. */
  838. function item_set_last_item($arr) {
  839. $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
  840. // Is it a forum? Then we don't care about the rules from above
  841. if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
  842. $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
  843. intval($arr['contact-id']));
  844. if ($isforum) {
  845. $update = true;
  846. }
  847. }
  848. if ($update) {
  849. q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
  850. dbesc($arr['received']),
  851. dbesc($arr['received']),
  852. intval($arr['contact-id'])
  853. );
  854. }
  855. // Now do the same for the system wide contacts with uid=0
  856. if (!$arr['private']) {
  857. q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
  858. dbesc($arr['received']),
  859. dbesc($arr['received']),
  860. intval($arr['owner-id'])
  861. );
  862. if ($arr['owner-id'] != $arr['author-id']) {
  863. q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
  864. dbesc($arr['received']),
  865. dbesc($arr['received']),
  866. intval($arr['author-id'])
  867. );
  868. }
  869. }
  870. }
  871. function item_body_set_hashtags(&$item) {
  872. $tags = get_tags($item["body"]);
  873. // No hashtags?
  874. if (!count($tags))
  875. return(false);
  876. // This sorting is important when there are hashtags that are part of other hashtags
  877. // Otherwise there could be problems with hashtags like #test and #test2
  878. rsort($tags);
  879. $a = get_app();
  880. $URLSearchString = "^\[\]";
  881. // All hashtags should point to the home server
  882. //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  883. // "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
  884. //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  885. // "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
  886. // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
  887. $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  888. function ($match){
  889. return("[url=".str_replace("#", "&num;", $match[1])."]"<