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.
 
 
 
 
 
 

2145 lines
70 KiB

  1. <?php
  2. require_once('include/bbcode.php');
  3. require_once('include/oembed.php');
  4. require_once('include/salmon.php');
  5. require_once('include/crypto.php');
  6. require_once('include/Photo.php');
  7. require_once('include/tags.php');
  8. require_once('include/files.php');
  9. require_once('include/text.php');
  10. require_once('include/email.php');
  11. require_once('include/threads.php');
  12. require_once('include/socgraph.php');
  13. require_once('include/plaintext.php');
  14. require_once('include/ostatus.php');
  15. require_once('include/feed.php');
  16. require_once('include/Contact.php');
  17. require_once('mod/share.php');
  18. require_once('include/enotify.php');
  19. require_once('include/dfrn.php');
  20. require_once('include/group.php');
  21. require_once('library/defuse/php-encryption-1.2.1/Crypto.php');
  22. function construct_verb($item) {
  23. if($item['verb'])
  24. return $item['verb'];
  25. return ACTIVITY_POST;
  26. }
  27. /* limit_body_size()
  28. *
  29. * The purpose of this function is to apply system message length limits to
  30. * imported messages without including any embedded photos in the length
  31. */
  32. if(! function_exists('limit_body_size')) {
  33. function limit_body_size($body) {
  34. // logger('limit_body_size: start', LOGGER_DEBUG);
  35. $maxlen = get_max_import_size();
  36. // If the length of the body, including the embedded images, is smaller
  37. // than the maximum, then don't waste time looking for the images
  38. if($maxlen && (strlen($body) > $maxlen)) {
  39. logger('limit_body_size: the total body length exceeds the limit', LOGGER_DEBUG);
  40. $orig_body = $body;
  41. $new_body = '';
  42. $textlen = 0;
  43. $max_found = false;
  44. $img_start = strpos($orig_body, '[img');
  45. $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
  46. $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
  47. while(($img_st_close !== false) && ($img_end !== false)) {
  48. $img_st_close++; // make it point to AFTER the closing bracket
  49. $img_end += $img_start;
  50. $img_end += strlen('[/img]');
  51. if(! strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
  52. // This is an embedded image
  53. if( ($textlen + $img_start) > $maxlen ) {
  54. if($textlen < $maxlen) {
  55. logger('limit_body_size: the limit happens before an embedded image', LOGGER_DEBUG);
  56. $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
  57. $textlen = $maxlen;
  58. }
  59. }
  60. else {
  61. $new_body = $new_body . substr($orig_body, 0, $img_start);
  62. $textlen += $img_start;
  63. }
  64. $new_body = $new_body . substr($orig_body, $img_start, $img_end - $img_start);
  65. }
  66. else {
  67. if( ($textlen + $img_end) > $maxlen ) {
  68. if($textlen < $maxlen) {
  69. logger('limit_body_size: the limit happens before the end of a non-embedded image', LOGGER_DEBUG);
  70. $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
  71. $textlen = $maxlen;
  72. }
  73. }
  74. else {
  75. $new_body = $new_body . substr($orig_body, 0, $img_end);
  76. $textlen += $img_end;
  77. }
  78. }
  79. $orig_body = substr($orig_body, $img_end);
  80. if($orig_body === false) // in case the body ends on a closing image tag
  81. $orig_body = '';
  82. $img_start = strpos($orig_body, '[img');
  83. $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
  84. $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
  85. }
  86. if( ($textlen + strlen($orig_body)) > $maxlen) {
  87. if($textlen < $maxlen) {
  88. logger('limit_body_size: the limit happens after the end of the last image', LOGGER_DEBUG);
  89. $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
  90. $textlen = $maxlen;
  91. }
  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. }
  100. else
  101. return $body;
  102. }}
  103. function title_is_body($title, $body) {
  104. $title = strip_tags($title);
  105. $title = trim($title);
  106. $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
  107. $title = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $title);
  108. $body = strip_tags($body);
  109. $body = trim($body);
  110. $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
  111. $body = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $body);
  112. if (strlen($title) < strlen($body))
  113. $body = substr($body, 0, strlen($title));
  114. if (($title != $body) and (substr($title, -3) == "...")) {
  115. $pos = strrpos($title, "...");
  116. if ($pos > 0) {
  117. $title = substr($title, 0, $pos);
  118. $body = substr($body, 0, $pos);
  119. }
  120. }
  121. return($title == $body);
  122. }
  123. function add_page_info_data($data) {
  124. call_hooks('page_info_data', $data);
  125. // It maybe is a rich content, but if it does have everything that a link has,
  126. // then treat it that way
  127. if (($data["type"] == "rich") AND is_string($data["title"]) AND
  128. is_string($data["text"]) AND (sizeof($data["images"]) > 0))
  129. $data["type"] = "link";
  130. if ((($data["type"] != "link") AND ($data["type"] != "video") AND ($data["type"] != "photo")) OR ($data["title"] == $url))
  131. return("");
  132. if ($no_photos AND ($data["type"] == "photo"))
  133. return("");
  134. if (sizeof($data["images"]) > 0)
  135. $preview = $data["images"][0];
  136. else
  137. $preview = "";
  138. // Escape some bad characters
  139. $data["url"] = str_replace(array("[", "]"), array("&#91;", "&#93;"), htmlentities($data["url"], ENT_QUOTES, 'UTF-8', false));
  140. $data["title"] = str_replace(array("[", "]"), array("&#91;", "&#93;"), htmlentities($data["title"], ENT_QUOTES, 'UTF-8', false));
  141. $text = "[attachment type='".$data["type"]."'";
  142. if ($data["url"] != "")
  143. $text .= " url='".$data["url"]."'";
  144. if ($data["title"] != "")
  145. $text .= " title='".$data["title"]."'";
  146. if (sizeof($data["images"]) > 0) {
  147. $preview = str_replace(array("[", "]"), array("&#91;", "&#93;"), htmlentities($data["images"][0]["src"], ENT_QUOTES, 'UTF-8', false));
  148. // if the preview picture is larger than 500 pixels then show it in a larger mode
  149. // But only, if the picture isn't higher than large (To prevent huge posts)
  150. if (($data["images"][0]["width"] >= 500) AND ($data["images"][0]["width"] >= $data["images"][0]["height"]))
  151. $text .= " image='".$preview."'";
  152. else
  153. $text .= " preview='".$preview."'";
  154. }
  155. $text .= "]".$data["text"]."[/attachment]";
  156. $hashtags = "";
  157. if (isset($data["keywords"]) AND count($data["keywords"])) {
  158. $a = get_app();
  159. $hashtags = "\n";
  160. foreach ($data["keywords"] AS $keyword) {
  161. /// @todo make a positive list of allowed characters
  162. $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'", "’", "`", "(", ")", "„", "“"),
  163. array("","", "", "", "", "", "", "", "", "", "", ""), $keyword);
  164. $hashtags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url] ";
  165. }
  166. }
  167. return "\n".$text.$hashtags;
  168. }
  169. function query_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
  170. require_once("mod/parse_url.php");
  171. $data = parseurl_getsiteinfo_cached($url, true);
  172. if ($photo != "")
  173. $data["images"][0]["src"] = $photo;
  174. logger('fetch page info for '.$url.' '.print_r($data, true), LOGGER_DEBUG);
  175. if (!$keywords AND isset($data["keywords"]))
  176. unset($data["keywords"]);
  177. if (($keyword_blacklist != "") AND isset($data["keywords"])) {
  178. $list = explode(",", $keyword_blacklist);
  179. foreach ($list AS $keyword) {
  180. $keyword = trim($keyword);
  181. $index = array_search($keyword, $data["keywords"]);
  182. if ($index !== false)
  183. unset($data["keywords"][$index]);
  184. }
  185. }
  186. return($data);
  187. }
  188. function add_page_keywords($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
  189. $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
  190. $tags = "";
  191. if (isset($data["keywords"]) AND count($data["keywords"])) {
  192. $a = get_app();
  193. foreach ($data["keywords"] AS $keyword) {
  194. $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'"),
  195. array("","", "", "", "", ""), $keyword);
  196. if ($tags != "")
  197. $tags .= ",";
  198. $tags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url]";
  199. }
  200. }
  201. return($tags);
  202. }
  203. function add_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
  204. $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
  205. $text = add_page_info_data($data);
  206. return($text);
  207. }
  208. function add_page_info_to_body($body, $texturl = false, $no_photos = false) {
  209. logger('add_page_info_to_body: fetch page info for body '.$body, LOGGER_DEBUG);
  210. $URLSearchString = "^\[\]";
  211. // Adding these spaces is a quick hack due to my problems with regular expressions :)
  212. preg_match("/[^!#@]\[url\]([$URLSearchString]*)\[\/url\]/ism", " ".$body, $matches);
  213. if (!$matches)
  214. preg_match("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", " ".$body, $matches);
  215. // Convert urls without bbcode elements
  216. if (!$matches AND $texturl) {
  217. preg_match("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", " ".$body, $matches);
  218. // Yeah, a hack. I really hate regular expressions :)
  219. if ($matches)
  220. $matches[1] = $matches[2];
  221. }
  222. if ($matches)
  223. $footer = add_page_info($matches[1], $no_photos);
  224. // Remove the link from the body if the link is attached at the end of the post
  225. if (isset($footer) AND (trim($footer) != "") AND (strpos($footer, $matches[1]))) {
  226. $removedlink = trim(str_replace($matches[1], "", $body));
  227. if (($removedlink == "") OR strstr($body, $removedlink))
  228. $body = $removedlink;
  229. $url = str_replace(array('/', '.'), array('\/', '\.'), $matches[1]);
  230. $removedlink = preg_replace("/\[url\=".$url."\](.*?)\[\/url\]/ism", '', $body);
  231. if (($removedlink == "") OR strstr($body, $removedlink))
  232. $body = $removedlink;
  233. }
  234. // Add the page information to the bottom
  235. if (isset($footer) AND (trim($footer) != ""))
  236. $body .= $footer;
  237. return $body;
  238. }
  239. /**
  240. * Adds a "lang" specification in a "postopts" element of given $arr,
  241. * if possible and not already present.
  242. * Expects "body" element to exist in $arr.
  243. *
  244. * @todo Add a parameter to request forcing override
  245. */
  246. function item_add_language_opt(&$arr) {
  247. if (version_compare(PHP_VERSION, '5.3.0', '<')) return; // LanguageDetect.php not available ?
  248. if ( x($arr, 'postopts') )
  249. {
  250. if ( strstr($arr['postopts'], 'lang=') )
  251. {
  252. // do not override
  253. /// @TODO Add parameter to request overriding
  254. return;
  255. }
  256. $postopts = $arr['postopts'];
  257. }
  258. else
  259. {
  260. $postopts = "";
  261. }
  262. require_once('library/langdet/Text/LanguageDetect.php');
  263. $naked_body = preg_replace('/\[(.+?)\]/','',$arr['body']);
  264. $l = new Text_LanguageDetect;
  265. //$lng = $l->detectConfidence($naked_body);
  266. //$arr['postopts'] = (($lng['language']) ? 'lang=' . $lng['language'] . ';' . $lng['confidence'] : '');
  267. $lng = $l->detect($naked_body, 3);
  268. if (sizeof($lng) > 0) {
  269. if ($postopts != "") $postopts .= '&'; // arbitrary separator, to be reviewed
  270. $postopts .= 'lang=';
  271. $sep = "";
  272. foreach ($lng as $language => $score) {
  273. $postopts .= $sep . $language.";".$score;
  274. $sep = ':';
  275. }
  276. $arr['postopts'] = $postopts;
  277. }
  278. }
  279. /**
  280. * @brief Creates an unique guid out of a given uri
  281. *
  282. * @param string $uri uri of an item entry
  283. * @return string unique guid
  284. */
  285. function uri_to_guid($uri) {
  286. // Our regular guid routine is using this kind of prefix as well
  287. // We have to avoid that different routines could accidentally create the same value
  288. $parsed = parse_url($uri);
  289. $guid_prefix = hash("crc32", $parsed["host"]);
  290. // Remove the scheme to make sure that "https" and "http" doesn't make a difference
  291. unset($parsed["scheme"]);
  292. $host_id = implode("/", $parsed);
  293. // We could use any hash algorithm since it isn't a security issue
  294. $host_hash = hash("ripemd128", $host_id);
  295. return $guid_prefix.$host_hash;
  296. }
  297. function item_store($arr,$force_parent = false, $notify = false, $dontcache = false) {
  298. // If it is a posting where users should get notifications, then define it as wall posting
  299. if ($notify) {
  300. $arr['wall'] = 1;
  301. $arr['type'] = 'wall';
  302. $arr['origin'] = 1;
  303. $arr['last-child'] = 1;
  304. $arr['network'] = NETWORK_DFRN;
  305. }
  306. // If a Diaspora signature structure was passed in, pull it out of the
  307. // item array and set it aside for later storage.
  308. $dsprsig = null;
  309. if(x($arr,'dsprsig')) {
  310. $dsprsig = json_decode(base64_decode($arr['dsprsig']));
  311. unset($arr['dsprsig']);
  312. }
  313. // Converting the plink
  314. if ($arr['network'] == NETWORK_OSTATUS) {
  315. if (isset($arr['plink']))
  316. $arr['plink'] = ostatus::convert_href($arr['plink']);
  317. elseif (isset($arr['uri']))
  318. $arr['plink'] = ostatus::convert_href($arr['uri']);
  319. }
  320. if(x($arr, 'gravity'))
  321. $arr['gravity'] = intval($arr['gravity']);
  322. elseif($arr['parent-uri'] === $arr['uri'])
  323. $arr['gravity'] = 0;
  324. elseif(activity_match($arr['verb'],ACTIVITY_POST))
  325. $arr['gravity'] = 6;
  326. else
  327. $arr['gravity'] = 6; // extensible catchall
  328. if(! x($arr,'type'))
  329. $arr['type'] = 'remote';
  330. /* check for create date and expire time */
  331. $uid = intval($arr['uid']);
  332. $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
  333. if(count($r)) {
  334. $expire_interval = $r[0]['expire'];
  335. if ($expire_interval>0) {
  336. $expire_date = new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
  337. $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
  338. if ($created_date < $expire_date) {
  339. logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
  340. return 0;
  341. }
  342. }
  343. }
  344. // Do we already have this item?
  345. // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
  346. if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
  347. $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s') LIMIT 1",
  348. dbesc(trim($arr['uri'])),
  349. intval($uid),
  350. dbesc(NETWORK_DIASPORA),
  351. dbesc(NETWORK_DFRN),
  352. dbesc(NETWORK_OSTATUS)
  353. );
  354. if ($r) {
  355. // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
  356. if ($uid != 0)
  357. 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']);
  358. return($r[0]["id"]);
  359. }
  360. }
  361. // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
  362. // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
  363. //if((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
  364. // $arr['body'] = strip_tags($arr['body']);
  365. item_add_language_opt($arr);
  366. if ($notify)
  367. $guid_prefix = "";
  368. elseif ((trim($arr['guid']) == "") AND (trim($arr['plink']) != ""))
  369. $arr['guid'] = uri_to_guid($arr['plink']);
  370. elseif ((trim($arr['guid']) == "") AND (trim($arr['uri']) != ""))
  371. $arr['guid'] = uri_to_guid($arr['uri']);
  372. else {
  373. $parsed = parse_url($arr["author-link"]);
  374. $guid_prefix = hash("crc32", $parsed["host"]);
  375. }
  376. $arr['wall'] = ((x($arr,'wall')) ? intval($arr['wall']) : 0);
  377. $arr['guid'] = ((x($arr,'guid')) ? notags(trim($arr['guid'])) : get_guid(32, $guid_prefix));
  378. $arr['uri'] = ((x($arr,'uri')) ? notags(trim($arr['uri'])) : $arr['guid']);
  379. $arr['extid'] = ((x($arr,'extid')) ? notags(trim($arr['extid'])) : '');
  380. $arr['author-name'] = ((x($arr,'author-name')) ? trim($arr['author-name']) : '');
  381. $arr['author-link'] = ((x($arr,'author-link')) ? notags(trim($arr['author-link'])) : '');
  382. $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
  383. $arr['owner-name'] = ((x($arr,'owner-name')) ? trim($arr['owner-name']) : '');
  384. $arr['owner-link'] = ((x($arr,'owner-link')) ? notags(trim($arr['owner-link'])) : '');
  385. $arr['owner-avatar'] = ((x($arr,'owner-avatar')) ? notags(trim($arr['owner-avatar'])) : '');
  386. $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert());
  387. $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert());
  388. $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert());
  389. $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert());
  390. $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert());
  391. $arr['title'] = ((x($arr,'title')) ? trim($arr['title']) : '');
  392. $arr['location'] = ((x($arr,'location')) ? trim($arr['location']) : '');
  393. $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : '');
  394. $arr['last-child'] = ((x($arr,'last-child')) ? intval($arr['last-child']) : 0 );
  395. $arr['visible'] = ((x($arr,'visible') !== false) ? intval($arr['visible']) : 1 );
  396. $arr['deleted'] = 0;
  397. $arr['parent-uri'] = ((x($arr,'parent-uri')) ? notags(trim($arr['parent-uri'])) : '');
  398. $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : '');
  399. $arr['object-type'] = ((x($arr,'object-type')) ? notags(trim($arr['object-type'])) : '');
  400. $arr['object'] = ((x($arr,'object')) ? trim($arr['object']) : '');
  401. $arr['target-type'] = ((x($arr,'target-type')) ? notags(trim($arr['target-type'])) : '');
  402. $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : '');
  403. $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : '');
  404. $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : '');
  405. $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : '');
  406. $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : '');
  407. $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : '');
  408. $arr['private'] = ((x($arr,'private')) ? intval($arr['private']) : 0 );
  409. $arr['bookmark'] = ((x($arr,'bookmark')) ? intval($arr['bookmark']) : 0 );
  410. $arr['body'] = ((x($arr,'body')) ? trim($arr['body']) : '');
  411. $arr['tag'] = ((x($arr,'tag')) ? notags(trim($arr['tag'])) : '');
  412. $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : '');
  413. $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : '');
  414. $arr['origin'] = ((x($arr,'origin')) ? intval($arr['origin']) : 0 );
  415. $arr['network'] = ((x($arr,'network')) ? trim($arr['network']) : '');
  416. $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : '');
  417. $arr['resource-id'] = ((x($arr,'resource-id')) ? trim($arr['resource-id']) : '');
  418. $arr['event-id'] = ((x($arr,'event-id')) ? intval($arr['event-id']) : 0 );
  419. $arr['inform'] = ((x($arr,'inform')) ? trim($arr['inform']) : '');
  420. $arr['file'] = ((x($arr,'file')) ? trim($arr['file']) : '');
  421. // Items cannot be stored before they happen ...
  422. if ($arr['created'] > datetime_convert())
  423. $arr['created'] = datetime_convert();
  424. // We haven't invented time travel by now.
  425. if ($arr['edited'] > datetime_convert())
  426. $arr['edited'] = datetime_convert();
  427. if (($arr['author-link'] == "") AND ($arr['owner-link'] == ""))
  428. logger("Both author-link and owner-link are empty. Called by: ".App::callstack(), LOGGER_DEBUG);
  429. if ($arr['plink'] == "") {
  430. $a = get_app();
  431. $arr['plink'] = $a->get_baseurl().'/display/'.urlencode($arr['guid']);
  432. }
  433. if ($arr['network'] == "") {
  434. $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
  435. dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
  436. dbesc(normalise_link($arr['author-link'])),
  437. intval($arr['uid'])
  438. );
  439. if(!count($r))
  440. $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
  441. dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
  442. dbesc(normalise_link($arr['author-link']))
  443. );
  444. if(!count($r))
  445. $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
  446. intval($arr['contact-id']),
  447. intval($arr['uid'])
  448. );
  449. if(count($r))
  450. $arr['network'] = $r[0]["network"];
  451. // Fallback to friendica (why is it empty in some cases?)
  452. if ($arr['network'] == "")
  453. $arr['network'] = NETWORK_DFRN;
  454. logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
  455. }
  456. // The contact-id should be set before "item_store" was called - but there seems to be some issues
  457. if ($arr["contact-id"] == 0) {
  458. // First we are looking for a suitable contact that matches with the author of the post
  459. // This is done only for comments (See below explanation at "gcontact-id")
  460. if($arr['parent-uri'] != $arr['uri'])
  461. $arr["contact-id"] = get_contact($arr['author-link'], $uid);
  462. // If not present then maybe the owner was found
  463. if ($arr["contact-id"] == 0)
  464. $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
  465. // Still missing? Then use the "self" contact of the current user
  466. if ($arr["contact-id"] == 0) {
  467. $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
  468. if ($r)
  469. $arr["contact-id"] = $r[0]["id"];
  470. }
  471. logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
  472. }
  473. if ($arr["gcontact-id"] == 0) {
  474. // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
  475. // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
  476. // On comments the author is the better choice.
  477. if($arr['parent-uri'] === $arr['uri'])
  478. $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
  479. "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
  480. else
  481. $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
  482. "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
  483. }
  484. if ($arr["author-id"] == 0)
  485. $arr["author-id"] = get_contact($arr["author-link"], 0);
  486. if ($arr["owner-id"] == 0)
  487. $arr["owner-id"] = get_contact($arr["owner-link"], 0);
  488. if ($arr['guid'] != "") {
  489. // Checking if there is already an item with the same guid
  490. logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
  491. $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
  492. dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
  493. if(count($r)) {
  494. logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
  495. return 0;
  496. }
  497. }
  498. // Check for hashtags in the body and repair or add hashtag links
  499. item_body_set_hashtags($arr);
  500. $arr['thr-parent'] = $arr['parent-uri'];
  501. if($arr['parent-uri'] === $arr['uri']) {
  502. $parent_id = 0;
  503. $parent_deleted = 0;
  504. $allow_cid = $arr['allow_cid'];
  505. $allow_gid = $arr['allow_gid'];
  506. $deny_cid = $arr['deny_cid'];
  507. $deny_gid = $arr['deny_gid'];
  508. $notify_type = 'wall-new';
  509. }
  510. else {
  511. // find the parent and snarf the item id and ACLs
  512. // and anything else we need to inherit
  513. $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
  514. dbesc($arr['parent-uri']),
  515. intval($arr['uid'])
  516. );
  517. if(count($r)) {
  518. // is the new message multi-level threaded?
  519. // even though we don't support it now, preserve the info
  520. // and re-attach to the conversation parent.
  521. if($r[0]['uri'] != $r[0]['parent-uri']) {
  522. $arr['parent-uri'] = $r[0]['parent-uri'];
  523. $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
  524. ORDER BY `id` ASC LIMIT 1",
  525. dbesc($r[0]['parent-uri']),
  526. dbesc($r[0]['parent-uri']),
  527. intval($arr['uid'])
  528. );
  529. if($z && count($z))
  530. $r = $z;
  531. }
  532. $parent_id = $r[0]['id'];
  533. $parent_deleted = $r[0]['deleted'];
  534. $allow_cid = $r[0]['allow_cid'];
  535. $allow_gid = $r[0]['allow_gid'];
  536. $deny_cid = $r[0]['deny_cid'];
  537. $deny_gid = $r[0]['deny_gid'];
  538. $arr['wall'] = $r[0]['wall'];
  539. $notify_type = 'comment-new';
  540. // if the parent is private, force privacy for the entire conversation
  541. // This differs from the above settings as it subtly allows comments from
  542. // email correspondents to be private even if the overall thread is not.
  543. if($r[0]['private'])
  544. $arr['private'] = $r[0]['private'];
  545. // Edge case. We host a public forum that was originally posted to privately.
  546. // The original author commented, but as this is a comment, the permissions
  547. // weren't fixed up so it will still show the comment as private unless we fix it here.
  548. if((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
  549. $arr['private'] = 0;
  550. // If its a post from myself then tag the thread as "mention"
  551. logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
  552. $u = q("SELECT `nickname` FROM `user` WHERE `uid` = %d", intval($arr['uid']));
  553. if(count($u)) {
  554. $a = get_app();
  555. $self = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
  556. logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
  557. if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
  558. q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
  559. logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
  560. }
  561. }
  562. } else {
  563. // Allow one to see reply tweets from status.net even when
  564. // we don't have or can't see the original post.
  565. if($force_parent) {
  566. logger('item_store: $force_parent=true, reply converted to top-level post.');
  567. $parent_id = 0;
  568. $arr['parent-uri'] = $arr['uri'];
  569. $arr['gravity'] = 0;
  570. }
  571. else {
  572. logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
  573. return 0;
  574. }
  575. $parent_deleted = 0;
  576. }
  577. }
  578. $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
  579. dbesc($arr['uri']),
  580. dbesc($arr['network']),
  581. dbesc(NETWORK_DFRN),
  582. intval($arr['uid'])
  583. );
  584. if($r && count($r)) {
  585. logger('duplicated item with the same uri found. ' . print_r($arr,true));
  586. return 0;
  587. }
  588. // Check for an existing post with the same content. There seems to be a problem with OStatus.
  589. $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
  590. dbesc($arr['body']),
  591. dbesc($arr['network']),
  592. dbesc($arr['created']),
  593. intval($arr['contact-id']),
  594. intval($arr['uid'])
  595. );
  596. if($r && count($r)) {
  597. logger('duplicated item with the same body found. ' . print_r($arr,true));
  598. return 0;
  599. }
  600. // Is this item available in the global items (with uid=0)?
  601. if ($arr["uid"] == 0) {
  602. $arr["global"] = true;
  603. q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
  604. } else {
  605. $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
  606. $arr["global"] = (count($isglobal) > 0);
  607. }
  608. // ACL settings
  609. if(strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
  610. $private = 1;
  611. else
  612. $private = $arr['private'];
  613. $arr["allow_cid"] = $allow_cid;
  614. $arr["allow_gid"] = $allow_gid;
  615. $arr["deny_cid"] = $deny_cid;
  616. $arr["deny_gid"] = $deny_gid;
  617. $arr["private"] = $private;
  618. $arr["deleted"] = $parent_deleted;
  619. // Fill the cache field
  620. put_item_in_cache($arr);
  621. if ($notify)
  622. call_hooks('post_local',$arr);
  623. else
  624. call_hooks('post_remote',$arr);
  625. if(x($arr,'cancel')) {
  626. logger('item_store: post cancelled by plugin.');
  627. return 0;
  628. }
  629. // Store the unescaped version
  630. $unescaped = $arr;
  631. dbesc_array($arr);
  632. logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
  633. $r = dbq("INSERT INTO `item` (`"
  634. . implode("`, `", array_keys($arr))
  635. . "`) VALUES ('"
  636. . implode("', '", array_values($arr))
  637. . "')" );
  638. // And restore it
  639. $arr = $unescaped;
  640. // find the item that we just created
  641. $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s' ORDER BY `id` ASC",
  642. dbesc($arr['uri']),
  643. intval($arr['uid']),
  644. dbesc($arr['network'])
  645. );
  646. if(count($r) > 1) {
  647. // There are duplicates. Keep the oldest one, delete the others
  648. logger('item_store: duplicated post occurred. Removing newer duplicates. uri = '.$arr['uri'].' uid = '.$arr['uid']);
  649. q("DELETE FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s' AND `id` > %d",
  650. dbesc($arr['uri']),
  651. intval($arr['uid']),
  652. dbesc($arr['network']),
  653. intval($r[0]["id"])
  654. );
  655. return 0;
  656. } elseif(count($r)) {
  657. $current_post = $r[0]['id'];
  658. logger('item_store: created item ' . $current_post);
  659. item_set_last_item($arr);
  660. } else {
  661. logger('item_store: could not locate created item');
  662. return 0;
  663. }
  664. if(!$parent_id || ($arr['parent-uri'] === $arr['uri']))
  665. $parent_id = $current_post;
  666. // Set parent id
  667. $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
  668. intval($parent_id),
  669. intval($current_post)
  670. );
  671. $arr['id'] = $current_post;
  672. $arr['parent'] = $parent_id;
  673. // update the commented timestamp on the parent
  674. // Only update "commented" if it is really a comment
  675. if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
  676. q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
  677. dbesc(datetime_convert()),
  678. dbesc(datetime_convert()),
  679. intval($parent_id)
  680. );
  681. else
  682. q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
  683. dbesc(datetime_convert()),
  684. intval($parent_id)
  685. );
  686. if($dsprsig) {
  687. // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
  688. // We can check for this condition when we decode and encode the stuff again.
  689. if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
  690. $dsprsig->signature = base64_decode($dsprsig->signature);
  691. logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
  692. }
  693. q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
  694. intval($current_post),
  695. dbesc($dsprsig->signed_text),
  696. dbesc($dsprsig->signature),
  697. dbesc($dsprsig->signer)
  698. );
  699. }
  700. /**
  701. * If this is now the last-child, force all _other_ children of this parent to *not* be last-child
  702. */
  703. if($arr['last-child']) {
  704. $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
  705. dbesc($arr['uri']),
  706. intval($arr['uid']),
  707. intval($current_post)
  708. );
  709. }
  710. $deleted = tag_deliver($arr['uid'],$current_post);
  711. // current post can be deleted if is for a community page and no mention are
  712. // in it.
  713. if (!$deleted AND !$dontcache) {
  714. $r = q('SELECT * FROM `item` WHERE id = %d', intval($current_post));
  715. if (count($r) == 1) {
  716. if ($notify)
  717. call_hooks('post_local_end', $r[0]);
  718. else
  719. call_hooks('post_remote_end', $r[0]);
  720. } else
  721. logger('item_store: new item not found in DB, id ' . $current_post);
  722. }
  723. create_tags_from_item($current_post);
  724. create_files_from_item($current_post);
  725. // Only check for notifications on start posts
  726. if ($arr['parent-uri'] === $arr['uri'])
  727. add_thread($current_post);
  728. else {
  729. update_thread($parent_id);
  730. add_shadow_entry($arr);
  731. }
  732. check_item_notification($current_post, $uid);
  733. if ($notify)
  734. proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
  735. return $current_post;
  736. }
  737. /**
  738. * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
  739. *
  740. * This can be used to filter for inactive contacts.
  741. * Only do this for public postings to avoid privacy problems, since poco data is public.
  742. * Don't set this value if it isn't from the owner (could be an author that we don't know)
  743. *
  744. * @param array $arr Contains the just posted item record
  745. */
  746. function item_set_last_item($arr) {
  747. $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
  748. // Is it a forum? Then we don't care about the rules from above
  749. if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
  750. $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
  751. intval($arr['contact-id']));
  752. if ($isforum)
  753. $update = true;
  754. }
  755. if ($update)
  756. q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
  757. dbesc($arr['received']),
  758. dbesc($arr['received']),
  759. intval($arr['contact-id'])
  760. );
  761. // Now do the same for the system wide contacts with uid=0
  762. if (!$arr['private']) {
  763. q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
  764. dbesc($arr['received']),
  765. dbesc($arr['received']),
  766. intval($arr['owner-id'])
  767. );
  768. if ($arr['owner-id'] != $arr['author-id'])
  769. q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
  770. dbesc($arr['received']),
  771. dbesc($arr['received']),
  772. intval($arr['author-id'])
  773. );
  774. }
  775. }
  776. function item_body_set_hashtags(&$item) {
  777. $tags = get_tags($item["body"]);
  778. // No hashtags?
  779. if(!count($tags))
  780. return(false);
  781. // This sorting is important when there are hashtags that are part of other hashtags
  782. // Otherwise there could be problems with hashtags like #test and #test2
  783. rsort($tags);
  784. $a = get_app();
  785. $URLSearchString = "^\[\]";
  786. // All hashtags should point to the home server
  787. //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  788. // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
  789. //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  790. // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
  791. // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
  792. $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  793. function ($match){
  794. return("[url=".str_replace("#", "&num;", $match[1])."]".str_replace("#", "&num;", $match[2])."[/url]");
  795. },$item["body"]);
  796. $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
  797. function ($match){
  798. return("[bookmark=".str_replace("#", "&num;", $match[1])."]".str_replace("#", "&num;", $match[2])."[/bookmark]");
  799. },$item["body"]);
  800. $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
  801. function ($match){
  802. return("[attachment ".str_replace("#", "&num;", $match[1])."]".$match[2]."[/attachment]");
  803. },$item["body"]);
  804. // Repair recursive urls
  805. $item["body"] = preg_replace("/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  806. "&num;$2", $item["body"]);
  807. foreach($tags as $tag) {
  808. if(strpos($tag,'#') !== 0)
  809. continue;
  810. if(strpos($tag,'[url='))
  811. continue;
  812. $basetag = str_replace('_',' ',substr($tag,1));
  813. $newtag = '#[url='.$a->get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
  814. $item["body"] = str_replace($tag, $newtag, $item["body"]);
  815. if(!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
  816. if(strlen($item["tag"]))
  817. $item["tag"] = ','.$item["tag"];
  818. $item["tag"] = $newtag.$item["tag"];
  819. }
  820. }
  821. // Convert back the masked hashtags
  822. $item["body"] = str_replace("&num;", "#", $item["body"]);
  823. }
  824. function get_item_guid($id) {
  825. $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
  826. if (count($r))
  827. return($r[0]["guid"]);
  828. else
  829. return("");
  830. }
  831. function get_item_id($guid, $uid = 0) {
  832. $nick = "";
  833. $id = 0;
  834. if ($uid == 0)
  835. $uid == local_user();
  836. // Does the given user have this item?
  837. if ($uid) {
  838. $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
  839. WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
  840. AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
  841. if (count($r)) {
  842. $id = $r[0]["id"];
  843. $nick = $r[0]["nickname"];
  844. }
  845. }
  846. // Or is it anywhere on the server?
  847. if ($nick == "") {
  848. $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
  849. WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
  850. AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = ''
  851. AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
  852. AND `item`.`private` = 0 AND `item`.`wall` = 1
  853. AND `item`.`guid` = '%s'", dbesc($guid));
  854. if (count($r)) {
  855. $id = $r[0]["id"];
  856. $nick = $r[0]["nickname"];
  857. }
  858. }
  859. return(array("nick" => $nick, "id" => $id));
  860. }
  861. // return - test
  862. function get_item_contact($item,$contacts) {
  863. if(! count($contacts) || (! is_array($item)))
  864. return false;
  865. foreach($contacts as $contact) {
  866. if($contact['id'] == $item['contact-id']) {
  867. return $contact;
  868. break; // NOTREACHED
  869. }
  870. }
  871. return false;
  872. }
  873. /**
  874. * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
  875. * @param int $uid
  876. * @param int $item_id
  877. * @return bool true if item was deleted, else false
  878. */
  879. function tag_deliver($uid,$item_id) {
  880. //
  881. $a = get_app();
  882. $mention = false;
  883. $u = q("select * from user where uid = %d limit 1",
  884. intval($uid)
  885. );
  886. if(! count($u))
  887. return;
  888. $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
  889. $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
  890. $i = q("select * from item where id = %d and uid = %d limit 1",
  891. intval($item_id),
  892. intval($uid)
  893. );
  894. if(! count($i))
  895. return;
  896. $item = $i[0];
  897. $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
  898. // Diaspora uses their own hardwired link URL in @-tags
  899. // instead of the one we supply with webfinger
  900. $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
  901. $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
  902. if($cnt) {
  903. foreach($matches as $mtch) {
  904. if(link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
  905. $mention = true;
  906. logger('tag_deliver: mention found: ' . $mtch[2]);
  907. }
  908. }
  909. }
  910. if(! $mention){
  911. if ( ($community_page || $prvgroup) &&
  912. (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
  913. // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
  914. // delete it!
  915. logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
  916. q("DELETE FROM item WHERE id = %d and uid = %d",
  917. intval($item_id),
  918. intval($uid)
  919. );
  920. return true;
  921. }
  922. return;
  923. }
  924. $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
  925. call_hooks('tagged', $arr);
  926. if((! $community_page) && (! $prvgroup))
  927. return;
  928. // tgroup delivery - setup a second delivery chain
  929. // prevent delivery looping - only proceed
  930. // if the message originated elsewhere and is a top-level post
  931. if(($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
  932. return;
  933. // now change this copy of the post to a forum head message and deliver to all the tgroup members
  934. $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
  935. intval($u[0]['uid'])
  936. );
  937. if(! count($c))
  938. return;
  939. // also reset all the privacy bits to the forum default permissions
  940. $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
  941. $forum_mode = (($prvgroup) ? 2 : 1);
  942. q("update item set wall = 1, origin = 1, forum_mode = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
  943. `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' where id = %d",
  944. intval($forum_mode),
  945. dbesc($c[0]['name']),
  946. dbesc($c[0]['url']),
  947. dbesc($c[0]['thumb']),
  948. intval($private),
  949. dbesc($u[0]['allow_cid']),
  950. dbesc($u[0]['allow_gid']),
  951. dbesc($u[0]['deny_cid']),
  952. dbesc($u[0]['deny_gid']),
  953. intval($item_id)
  954. );
  955. update_thread($item_id);
  956. proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
  957. }
  958. function tgroup_check($uid,$item) {
  959. $a = get_app();
  960. $mention = false;
  961. // check that the message originated elsewhere and is a top-level post
  962. if(($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
  963. return false;
  964. $u = q("select * from user where uid = %d limit 1",
  965. intval($uid)
  966. );
  967. if(! count($u))
  968. return false;
  969. $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
  970. $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
  971. $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
  972. // Diaspora uses their own hardwired link URL in @-tags
  973. // instead of the one we supply with webfinger
  974. $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
  975. $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
  976. if($cnt) {
  977. foreach($matches as $mtch) {
  978. if(link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
  979. $mention = true;
  980. logger('tgroup_check: mention found: ' . $mtch[2]);
  981. }
  982. }
  983. }
  984. if(! $mention)
  985. return false;
  986. if((! $community_page) && (! $prvgroup))
  987. return false;
  988. return true;
  989. }
  990. /*
  991. This function returns true if $update has an edited timestamp newer
  992. than $existing, i.e. $update contains new data which should override
  993. what's already there. If there is no timestamp yet, the update is
  994. assumed to be newer. If the update has no timestamp, the existing
  995. item is assumed to be up-to-date. If the timestamps are equal it
  996. assumes the update has been seen before and should be ignored.
  997. */
  998. function edited_timestamp_is_newer($existing, $update) {
  999. if (!x($existing,'edited') || !$existing['edited']) {
  1000. return true;
  1001. }
  1002. if (!x($update,'edited') || !$update['edited']) {
  1003. return false;
  1004. }
  1005. $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
  1006. $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
  1007. return (strcmp($existing_edited, $update_edited) < 0);
  1008. }
  1009. /**
  1010. *
  1011. * consume_feed - process atom feed and update anything/everything we might need to update
  1012. *
  1013. * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
  1014. *
  1015. * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
  1016. * It is this person's stuff that is going to be updated.
  1017. * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
  1018. * from an external network and MAY create an appropriate contact record. Otherwise, we MUST
  1019. * have a contact record.
  1020. * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
  1021. * might not) try and subscribe to it.
  1022. * $datedir sorts in reverse order
  1023. * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
  1024. * imported prior to its children being seen in the stream unless we are certain
  1025. * of how the feed is arranged/ordered.
  1026. * With $pass = 1, we only pull parent items out of the stream.
  1027. * With $pass = 2, we only pull children (comments/likes).
  1028. *
  1029. * So running this twice, first with pass 1 and then with pass 2 will do the right
  1030. * thing regardless of feed ordering. This won't be adequate in a fully-threaded
  1031. * model where comments can have sub-threads. That would require some massive sorting
  1032. * to get all the feed items into a mostly linear ordering, and might still require
  1033. * recursion.
  1034. */
  1035. function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
  1036. if ($contact['network'] === NETWORK_OSTATUS) {
  1037. if ($pass < 2) {
  1038. // Test - remove before flight
  1039. //$tempfile = tempnam(get_temppath(), "ostatus2");
  1040. //file_put_contents($tempfile, $xml);
  1041. logger("Consume OStatus messages ", LOGGER_DEBUG);
  1042. ostatus::import($xml,$importer,$contact, $hub);
  1043. }
  1044. return;
  1045. }
  1046. if ($contact['network'] === NETWORK_FEED) {
  1047. if ($pass < 2) {
  1048. logger("Consume feeds", LOGGER_DEBUG);
  1049. feed_import($xml,$importer,$contact, $hub);
  1050. }
  1051. return;
  1052. }
  1053. if ($contact['network'] === NETWORK_DFRN) {
  1054. logger("Consume DFRN messages", LOGGER_DEBUG);
  1055. $r = q("SELECT `contact`.*, `contact`.`uid` AS `importer_uid`,
  1056. `contact`.`pubkey` AS `cpubkey`,
  1057. `contact`.`prvkey` AS `cprvkey`,
  1058. `contact`.`thumb` AS `thumb`,
  1059. `contact`.`url` as `url`,
  1060. `contact`.`name` as `senderName`,
  1061. `user`.*
  1062. FROM `contact`
  1063. LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
  1064. WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
  1065. dbesc($contact["id"]), dbesc($importer["uid"])
  1066. );
  1067. if ($r) {
  1068. logger("Now import the DFRN feed");
  1069. dfrn::import($xml,$r[0], true);
  1070. return;
  1071. }
  1072. }
  1073. }
  1074. function item_is_remote_self($contact, &$datarray) {
  1075. $a = get_app();
  1076. if (!$contact['remote_self'])
  1077. return false;
  1078. // Prevent the forwarding of posts that are forwarded
  1079. if ($datarray["extid"] == NETWORK_DFRN)
  1080. return false;
  1081. // Prevent to forward already forwarded posts
  1082. if ($datarray["app"] == $a->get_hostname())
  1083. return false;
  1084. // Only forward posts
  1085. if ($datarray["verb"] != ACTIVITY_POST)
  1086. return false;
  1087. if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
  1088. return false;
  1089. $datarray2 = $datarray;
  1090. logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
  1091. if ($contact['remote_self'] == 2) {
  1092. $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
  1093. intval($contact['uid']));
  1094. if (count($r)) {
  1095. $datarray['contact-id'] = $r[0]["id"];
  1096. $datarray['owner-name'] = $r[0]["name"];
  1097. $datarray['owner-link'] = $r[0]["url"];
  1098. $datarray['owner-avatar'] = $r[0]["thumb"];
  1099. $datarray['author-name'] = $datarray['owner-name'];
  1100. $datarray['author-link'] = $datarray['owner-link'];
  1101. $datarray['author-avatar'] = $datarray['owner-avatar'];
  1102. }
  1103. if ($contact['network'] != NETWORK_FEED) {
  1104. $datarray["guid"] = get_guid(32);
  1105. unset($datarray["plink"]);
  1106. $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
  1107. $datarray["parent-uri"] = $datarray["uri"];
  1108. $datarray["extid"] = $contact['network'];
  1109. $urlpart = parse_url($datarray2['author-link']);
  1110. $datarray["app"] = $urlpart["host"];
  1111. } else
  1112. $datarray['private'] = 0;
  1113. }
  1114. if ($contact['network'] != NETWORK_FEED) {
  1115. // Store the original post
  1116. $r = item_store($datarray2, false, false);
  1117. logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
  1118. } else
  1119. $datarray["app"] = "Feed";
  1120. return true;
  1121. }
  1122. function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
  1123. $url = notags(trim($datarray['author-link']));
  1124. $name = notags(trim($datarray['author-name']));
  1125. $photo = notags(trim($datarray['author-avatar']));
  1126. if (is_object($item)) {
  1127. $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
  1128. if($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
  1129. $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
  1130. } else
  1131. $nick = $item;
  1132. if(is_array($contact)) {
  1133. if(($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
  1134. || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
  1135. $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
  1136. intval(CONTACT_IS_FRIEND),
  1137. intval($contact['id']),
  1138. intval($importer['uid'])
  1139. );
  1140. }
  1141. // send email notification to owner?
  1142. } else {
  1143. // create contact record
  1144. $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
  1145. `blocked`, `readonly`, `pending`, `writable`)
  1146. VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
  1147. intval($importer['uid']),
  1148. dbesc(datetime_convert()),
  1149. dbesc($url),
  1150. dbesc(normalise_link($url)),
  1151. dbesc($name),
  1152. dbesc($nick),
  1153. dbesc($photo),
  1154. dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
  1155. intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
  1156. );
  1157. $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
  1158. intval($importer['uid']),
  1159. dbesc($url)
  1160. );
  1161. if(count($r)) {
  1162. $contact_record = $r[0];
  1163. update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
  1164. }
  1165. $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
  1166. intval($importer['uid'])
  1167. );
  1168. $a = get_app();
  1169. if(count($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
  1170. // create notification
  1171. $hash = random_string();
  1172. if(is_array($contact_record)) {
  1173. $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
  1174. VALUES ( %d, %d, 0, 0, '%s', '%s' )",
  1175. intval($importer['uid']),
  1176. intval($contact_record['id']),
  1177. dbesc($hash),
  1178. dbesc(datetime_convert())
  1179. );
  1180. }
  1181. $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
  1182. if(intval($def_gid))
  1183. group_add_member($importer['uid'],'',$contact_record['id'],$def_gid);
  1184. if(($r[0]['notify-flags'] & NOTIFY_INTRO) &&
  1185. in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
  1186. notification(array(
  1187. 'type' => NOTIFY_INTRO,
  1188. 'notify_flags' => $r[0]['notify-flags'],
  1189. 'language' => $r[0]['language'],
  1190. 'to_name' => $r[0]['username'],
  1191. 'to_email' => $r[0]['email'],
  1192. 'uid' => $r[0]['uid'],
  1193. 'link' => $a->get_baseurl() . '/notifications/intro',
  1194. 'source_name' => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
  1195. 'source_link' => $contact_record['url'],
  1196. 'source_photo' => $contact_record['photo'],
  1197. 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
  1198. 'otype' => 'intro'
  1199. ));
  1200. }
  1201. } elseif (count($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
  1202. $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
  1203. intval($importer['uid']),
  1204. dbesc($url)
  1205. );
  1206. }
  1207. }
  1208. }
  1209. function lose_follower($importer,$contact,$datarray = array(),$item = "") {
  1210. if(($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
  1211. q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
  1212. intval(CONTACT_IS_SHARING),
  1213. intval($contact['id'])
  1214. );
  1215. }
  1216. else {
  1217. contact_remove($contact['id']);
  1218. }
  1219. }
  1220. function lose_sharer($importer,$contact,$datarray = array(),$item = "") {
  1221. if(($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
  1222. q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
  1223. intval(CONTACT_IS_FOLLOWER),
  1224. intval($contact['id'])
  1225. );
  1226. }
  1227. else {
  1228. contact_remove($contact['id']);
  1229. }
  1230. }
  1231. function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
  1232. $a = get_app();
  1233. if(is_array($importer)) {
  1234. $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
  1235. intval($importer['uid'])
  1236. );
  1237. }
  1238. // Diaspora has different message-ids in feeds than they do
  1239. // through the direct Diaspora protocol. If we try and use
  1240. // the feed, we'll get duplicates. So don't.
  1241. if((! count($r)) || $contact['network'] === NETWORK_DIASPORA)
  1242. return;
  1243. $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
  1244. // Use a single verify token, even if multiple hubs
  1245. $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
  1246. $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
  1247. logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: ' . $push_url . ' with verifier ' . $verify_token);
  1248. if(!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
  1249. $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
  1250. dbesc($verify_token),
  1251. intval($contact['id'])
  1252. );
  1253. }
  1254. post_url($url,$params);
  1255. logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
  1256. return;
  1257. }
  1258. function fix_private_photos($s, $uid, $item = null, $cid = 0) {
  1259. if(get_config('system','disable_embedded'))
  1260. return $s;
  1261. $a = get_app();
  1262. logger('fix_private_photos: check for photos', LOGGER_DEBUG);
  1263. $site = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://'));
  1264. $orig_body = $s;
  1265. $new_body = '';
  1266. $img_start = strpos($orig_body, '[img');
  1267. $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
  1268. $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
  1269. while( ($img_st_close !== false) && ($img_len !== false) ) {
  1270. $img_st_close++; // make it point to AFTER the closing bracket
  1271. $image = substr($orig_body, $img_start + $img_st_close, $img_len);
  1272. logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
  1273. if(stristr($image , $site . '/photo/')) {
  1274. // Only embed locally hosted photos
  1275. $replace = false;
  1276. $i = basename($image);
  1277. $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
  1278. $x = strpos($i,'-');
  1279. if($x) {
  1280. $res = substr($i,$x+1);
  1281. $i = substr($i,0,$x);
  1282. $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
  1283. dbesc($i),
  1284. intval($res),
  1285. intval($uid)
  1286. );
  1287. if($r) {
  1288. // Check to see if we should replace this photo link with an embedded image
  1289. // 1. No need to do so if the photo is public
  1290. // 2. If there's a contact-id provided, see if they're in the access list
  1291. // for the photo. If so, embed it.
  1292. // 3. Otherwise, if we have an item, see if the item permissions match the photo
  1293. // permissions, regardless of order but first check to see if they're an exact
  1294. // match to save some processing overhead.
  1295. if(has_permissions($r[0])) {
  1296. if($cid) {
  1297. $recips = enumerate_permissions($r[0]);
  1298. if(in_array($cid, $recips)) {
  1299. $replace = true;
  1300. }
  1301. }
  1302. elseif($item) {
  1303. if(compare_permissions($item,$r[0]))
  1304. $replace = true;
  1305. }
  1306. }
  1307. if($replace) {
  1308. $data = $r[0]['data'];
  1309. $type = $r[0]['type'];
  1310. // If a custom width and height were specified, apply before embedding
  1311. if(preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
  1312. logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
  1313. $width = intval($match[1]);
  1314. $height = intval($match[2]);
  1315. $ph = new Photo($data, $type);
  1316. if($ph->is_valid()) {
  1317. $ph->scaleImage(max($width, $height));
  1318. $data = $ph->imageString();
  1319. $type = $ph->getType();
  1320. }
  1321. }
  1322. logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
  1323. $image = 'data:' . $type . ';base64,' . base64_encode($data);
  1324. logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
  1325. }
  1326. }
  1327. }
  1328. }
  1329. $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
  1330. $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
  1331. if($orig_body === false)
  1332. $orig_body = '';
  1333. $img_start = strpos($orig_body, '[img');
  1334. $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
  1335. $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
  1336. }
  1337. $new_body = $new_body . $orig_body;
  1338. return($new_body);
  1339. }
  1340. function has_permissions($obj) {
  1341. if(($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
  1342. return true;
  1343. return false;
  1344. }
  1345. function compare_permissions($obj1,$obj2) {
  1346. // first part is easy. Check that these are exactly the same.
  1347. if(($obj1['allow_cid'] == $obj2['allow_cid'])
  1348. && ($obj1['allow_gid'] == $obj2['allow_gid'])
  1349. && ($obj1['deny_cid'] == $obj2['deny_cid'])
  1350. && ($obj1['deny_gid'] == $obj2['deny_gid']))
  1351. return true;
  1352. // This is harder. Parse all the permissions and compare the resulting set.
  1353. $recipients1 = enumerate_permissions($obj1);
  1354. $recipients2 = enumerate_permissions($obj2);
  1355. sort($recipients1);
  1356. sort($recipients2);
  1357. if($recipients1 == $recipients2)
  1358. return true;
  1359. return false;
  1360. }
  1361. // returns an array of contact-ids that are allowed to see this object
  1362. function enumerate_permissions($obj) {
  1363. $allow_people = expand_acl($obj['allow_cid']);
  1364. $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
  1365. $deny_people = expand_acl($obj['deny_cid']);
  1366. $deny_groups = expand_groups(expand_acl($obj['deny_gid']));
  1367. $recipients = array_unique(array_merge($allow_people,$allow_groups));
  1368. $deny = array_unique(array_merge($deny_people,$deny_groups));
  1369. $recipients = array_diff($recipients,$deny);
  1370. return $recipients;
  1371. }
  1372. function item_getfeedtags($item) {
  1373. $ret = array();
  1374. $matches = false;
  1375. $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
  1376. if($cnt) {
  1377. for($x = 0; $x < $cnt; $x ++) {
  1378. if($matches[1][$x])
  1379. $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
  1380. }
  1381. }
  1382. $matches = false;
  1383. $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
  1384. if($cnt) {
  1385. for($x = 0; $x < $cnt; $x ++) {
  1386. if($matches[1][$x])
  1387. $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
  1388. }
  1389. }
  1390. return $ret;
  1391. }
  1392. function item_expire($uid, $days, $network = "", $force = false) {
  1393. if((! $uid) || ($days < 1))
  1394. return;
  1395. // $expire_network_only = save your own wall posts
  1396. // and just expire conversations started by others
  1397. $expire_network_only = get_pconfig($uid,'expire','network_only');
  1398. $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
  1399. if ($network != "") {
  1400. $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
  1401. // There is an index "uid_network_received" but not "uid_network_created"
  1402. // This avoids the creation of another index just for one purpose.
  1403. // And it doesn't really matter wether to look at "received" or "created"
  1404. $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
  1405. } else
  1406. $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
  1407. $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
  1408. WHERE `uid` = %d $range
  1409. AND `id` = `parent`
  1410. $sql_extra
  1411. AND `deleted` = 0",
  1412. intval($uid),
  1413. intval($days)
  1414. );
  1415. if(! count($r))
  1416. return;
  1417. $expire_items = get_pconfig($uid, 'expire','items');
  1418. $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
  1419. // Forcing expiring of items - but not notes and marked items
  1420. if ($force)
  1421. $expire_items = true;
  1422. $expire_notes = get_pconfig($uid, 'expire','notes');
  1423. $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
  1424. $expire_starred = get_pconfig($uid, 'expire','starred');
  1425. $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
  1426. $expire_photos = get_pconfig($uid, 'expire','photos');
  1427. $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
  1428. logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
  1429. foreach($r as $item) {
  1430. // don't expire filed items
  1431. if(strpos($item['file'],'[') !== false)
  1432. continue;
  1433. // Only expire posts, not photos and photo comments
  1434. if($expire_photos==0 && strlen($item['resource-id']))
  1435. continue;
  1436. if($expire_starred==0 && intval($item['starred']))
  1437. continue;
  1438. if($expire_notes==0 && $item['type']=='note')
  1439. continue;
  1440. if($expire_items==0 && $item['type']!='note')
  1441. continue;
  1442. drop_item($item['id'],false);
  1443. }
  1444. proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
  1445. }
  1446. function drop_items($items) {
  1447. $uid = 0;
  1448. if(! local_user() && ! remote_user())
  1449. return;
  1450. if(count($items)) {
  1451. foreach($items as $item) {
  1452. $owner = drop_item($item,false);
  1453. if($owner && ! $uid)
  1454. $uid = $owner;
  1455. }
  1456. }
  1457. // multiple threads may have been deleted, send an expire notification
  1458. if($uid)
  1459. proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
  1460. }
  1461. function drop_item($id,$interactive = true) {
  1462. $a = get_app();
  1463. // locate item to be deleted
  1464. $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
  1465. intval($id)
  1466. );
  1467. if(! count($r)) {
  1468. if(! $interactive)
  1469. return 0;
  1470. notice( t('Item not found.') . EOL);
  1471. goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
  1472. }
  1473. $item = $r[0];
  1474. $owner = $item['uid'];
  1475. $cid = 0;
  1476. // check if logged in user is either the author or owner of this item
  1477. if(is_array($_SESSION['remote'])) {
  1478. foreach($_SESSION['remote'] as $visitor) {
  1479. if($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
  1480. $cid = $visitor['cid'];
  1481. break;
  1482. }
  1483. }
  1484. }
  1485. if((local_user() == $item['uid']) || ($cid) || (! $interactive)) {
  1486. // Check if we should do HTML-based delete confirmation
  1487. if($_REQUEST['confirm']) {
  1488. // <form> can't take arguments in its "action" parameter
  1489. // so add any arguments as hidden inputs
  1490. $query = explode_querystring($a->query_string);
  1491. $inputs = array();
  1492. foreach($query['args'] as $arg) {
  1493. if(strpos($arg, 'confirm=') === false) {
  1494. $arg_parts = explode('=', $arg);
  1495. $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
  1496. }
  1497. }
  1498. return replace_macros(get_markup_template('confirm.tpl'), array(
  1499. '$method' => 'get',
  1500. '$message' => t('Do you really want to delete this item?'),
  1501. '$extra_inputs' => $inputs,
  1502. '$confirm' => t('Yes'),
  1503. '$confirm_url' => $query['base'],
  1504. '$confirm_name' => 'confirmed',
  1505. '$cancel' => t('Cancel'),
  1506. ));
  1507. }
  1508. // Now check how the user responded to the confirmation query
  1509. if($_REQUEST['canceled']) {
  1510. goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
  1511. }
  1512. logger('delete item: ' . $item['id'], LOGGER_DEBUG);
  1513. // delete the item
  1514. $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
  1515. dbesc(datetime_convert()),
  1516. dbesc(datetime_convert()),
  1517. intval($item['id'])
  1518. );
  1519. create_tags_from_item($item['id']);
  1520. create_files_from_item($item['id']);
  1521. delete_thread($item['id'], $item['parent-uri']);
  1522. // clean up categories and tags so they don't end up as orphans
  1523. $matches = false;
  1524. $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
  1525. if($cnt) {
  1526. foreach($matches as $mtch) {
  1527. file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
  1528. }
  1529. }
  1530. $matches = false;
  1531. $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
  1532. if($cnt) {
  1533. foreach($matches as $mtch) {
  1534. file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
  1535. }
  1536. }
  1537. // If item is a link to a photo resource, nuke all the associated photos
  1538. // (visitors will not have photo resources)
  1539. // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
  1540. // generate a resource-id and therefore aren't intimately linked to the item.
  1541. if(strlen($item['resource-id'])) {
  1542. q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
  1543. dbesc($item['resource-id']),
  1544. intval($item['uid'])
  1545. );
  1546. // ignore the result
  1547. }
  1548. // If item is a link to an event, nuke the event record.
  1549. if(intval($item['event-id'])) {
  1550. q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
  1551. intval($item['event-id']),
  1552. intval($item['uid'])
  1553. );
  1554. // ignore the result
  1555. }
  1556. // If item has attachments, drop them
  1557. foreach(explode(",",$item['attach']) as $attach){
  1558. preg_match("|attach/(\d+)|", $attach, $matches);
  1559. q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
  1560. intval($matches[1]),
  1561. local_user()
  1562. );
  1563. // ignore the result
  1564. }
  1565. // clean up item_id and sign meta-data tables
  1566. /*
  1567. // Old code - caused very long queries and warning entries in the mysql logfiles:
  1568. $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
  1569. intval($item['id']),
  1570. intval($item['uid'])
  1571. );
  1572. $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
  1573. intval($item['id']),
  1574. intval($item['uid'])
  1575. );
  1576. */
  1577. // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
  1578. // Creating list of parents
  1579. $r = q("select id from item where parent = %d and uid = %d",
  1580. intval($item['id']),
  1581. intval($item['uid'])
  1582. );
  1583. $parentid = "";
  1584. foreach ($r AS $row) {
  1585. if ($parentid != "")
  1586. $parentid .= ", ";
  1587. $parentid .= $row["id"];
  1588. }
  1589. // Now delete them
  1590. if ($parentid != "") {
  1591. $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
  1592. $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
  1593. }
  1594. // If it's the parent of a comment thread, kill all the kids
  1595. if($item['uri'] == $item['parent-uri']) {
  1596. $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
  1597. WHERE `parent-uri` = '%s' AND `uid` = %d ",
  1598. dbesc(datetime_convert()),
  1599. dbesc(datetime_convert()),
  1600. dbesc($item['parent-uri']),
  1601. intval($item['uid'])
  1602. );
  1603. create_tags_from_itemuri($item['parent-uri'], $item['uid']);
  1604. create_files_from_itemuri($item['parent-uri'], $item['uid']);
  1605. delete_thread_uri($item['parent-uri'], $item['uid']);
  1606. // ignore the result
  1607. }
  1608. else {
  1609. // ensure that last-child is set in case the comment that had it just got wiped.
  1610. q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
  1611. dbesc(datetime_convert()),
  1612. dbesc($item['parent-uri']),
  1613. intval($item['uid'])
  1614. );
  1615. // who is the last child now?
  1616. $r = q("SELECT `id` FROM `item` WHERE `parent-uri` = '%s' AND `type` != 'activity' AND `deleted` = 0 AND `uid` = %d ORDER BY `edited` DESC LIMIT 1",
  1617. dbesc($item['parent-uri']),
  1618. intval($item['uid'])
  1619. );
  1620. if(count($r)) {
  1621. q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
  1622. intval($r[0]['id'])
  1623. );
  1624. }
  1625. }
  1626. $drop_id = intval($item['id']);
  1627. // send the notification upstream/downstream as the case may be
  1628. proc_run(PRIORITY_HIGH,"include/notifier.php", "drop", $drop_id);
  1629. if(! $interactive)
  1630. return $owner;
  1631. goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
  1632. //NOTREACHED
  1633. }
  1634. else {
  1635. if(! $interactive)
  1636. return 0;
  1637. notice( t('Permission denied.') . EOL);
  1638. goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
  1639. //NOTREACHED
  1640. }
  1641. }
  1642. function first_post_date($uid,$wall = false) {
  1643. $r = q("select id, created from item
  1644. where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
  1645. and id = parent
  1646. order by created asc limit 1",
  1647. intval($uid),
  1648. intval($wall ? 1 : 0)
  1649. );
  1650. if(count($r)) {
  1651. // logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
  1652. return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
  1653. }
  1654. return false;
  1655. }
  1656. /* modified posted_dates() {below} to arrange the list in years */
  1657. function list_post_dates($uid, $wall) {
  1658. $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
  1659. $dthen = first_post_date($uid, $wall);
  1660. if(! $dthen)
  1661. return array();
  1662. // Set the start and end date to the beginning of the month
  1663. $dnow = substr($dnow,0,8).'01';
  1664. $dthen = substr($dthen,0,8).'01';
  1665. $ret = array();
  1666. // Starting with the current month, get the first and last days of every
  1667. // month down to and including the month of the first post
  1668. while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
  1669. $dyear = intval(substr($dnow,0,4));
  1670. $dstart = substr($dnow,0,8) . '01';
  1671. $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
  1672. $start_month = datetime_convert('','',$dstart,'Y-m-d');
  1673. $end_month = datetime_convert('','',$dend,'Y-m-d');
  1674. $str = day_translate(datetime_convert('','',$dnow,'F'));
  1675. if(! $ret[$dyear])
  1676. $ret[$dyear] = array();
  1677. $ret[$dyear][] = array($str,$end_month,$start_month);
  1678. $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
  1679. }
  1680. return $ret;
  1681. }
  1682. function posted_dates($uid,$wall) {
  1683. $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
  1684. $dthen = first_post_date($uid,$wall);
  1685. if(! $dthen)
  1686. return array();
  1687. // Set the start and end date to the beginning of the month
  1688. $dnow = substr($dnow,0,8).'01';
  1689. $dthen = substr($dthen,0,8).'01';
  1690. $ret = array();
  1691. // Starting with the current month, get the first and last days of every
  1692. // month down to and including the month of the first post
  1693. while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
  1694. $dstart = substr($dnow,0,8) . '01';
  1695. $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
  1696. $start_month = datetime_convert('','',$dstart,'Y-m-d');
  1697. $end_month = datetime_convert('','',$dend,'Y-m-d');
  1698. $str = day_translate(datetime_convert('','',$dnow,'F Y'));
  1699. $ret[] = array($str,$end_month,$start_month);
  1700. $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
  1701. }
  1702. return $ret;
  1703. }
  1704. function posted_date_widget($url,$uid,$wall) {
  1705. $o = '';
  1706. if(! feature_enabled($uid,'archives'))
  1707. return $o;
  1708. // For former Facebook folks that left because of "timeline"
  1709. /* if($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
  1710. return $o;*/
  1711. $visible_years = get_pconfig($uid,'system','archive_visible_years');
  1712. if(! $visible_years)
  1713. $visible_years = 5;
  1714. $ret = list_post_dates($uid,$wall);
  1715. if(! count($ret))
  1716. return $o;
  1717. $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
  1718. $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
  1719. $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
  1720. '$title' => t('Archives'),
  1721. '$size' => $visible_years,
  1722. '$cutoff_year' => $cutoff_year,
  1723. '$cutoff' => $cutoff,
  1724. '$url' => $url,
  1725. '$dates' => $ret,
  1726. '$showmore' => t('show more')
  1727. ));
  1728. return $o;
  1729. }