4 * @file include/items.php
7 use \Friendica\ParseUrl;
9 require_once('include/bbcode.php');
10 require_once('include/oembed.php');
11 require_once('include/salmon.php');
12 require_once('include/crypto.php');
13 require_once('include/Photo.php');
14 require_once('include/tags.php');
15 require_once('include/files.php');
16 require_once('include/text.php');
17 require_once('include/email.php');
18 require_once('include/threads.php');
19 require_once('include/socgraph.php');
20 require_once('include/plaintext.php');
21 require_once('include/ostatus.php');
22 require_once('include/feed.php');
23 require_once('include/Contact.php');
24 require_once('mod/share.php');
25 require_once('include/enotify.php');
26 require_once('include/dfrn.php');
27 require_once('include/group.php');
29 require_once('library/defuse/php-encryption-1.2.1/Crypto.php');
31 function construct_verb($item) {
39 * The purpose of this function is to apply system message length limits to
40 * imported messages without including any embedded photos in the length
42 if (! function_exists('limit_body_size')) {
43 function limit_body_size($body) {
45 // logger('limit_body_size: start', LOGGER_DEBUG);
47 $maxlen = get_max_import_size();
49 // If the length of the body, including the embedded images, is smaller
50 // than the maximum, then don't waste time looking for the images
51 if ($maxlen && (strlen($body) > $maxlen)) {
53 logger('limit_body_size: the total body length exceeds the limit', LOGGER_DEBUG);
60 $img_start = strpos($orig_body, '[img');
61 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
62 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
63 while(($img_st_close !== false) && ($img_end !== false)) {
65 $img_st_close++; // make it point to AFTER the closing bracket
66 $img_end += $img_start;
67 $img_end += strlen('[/img]');
69 if (! strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
70 // This is an embedded image
72 if ( ($textlen + $img_start) > $maxlen ) {
73 if ($textlen < $maxlen) {
74 logger('limit_body_size: the limit happens before an embedded image', LOGGER_DEBUG);
75 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
79 $new_body = $new_body . substr($orig_body, 0, $img_start);
80 $textlen += $img_start;
83 $new_body = $new_body . substr($orig_body, $img_start, $img_end - $img_start);
86 if ( ($textlen + $img_end) > $maxlen ) {
87 if ($textlen < $maxlen) {
88 logger('limit_body_size: the limit happens before the end of a non-embedded image', LOGGER_DEBUG);
89 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
93 $new_body = $new_body . substr($orig_body, 0, $img_end);
97 $orig_body = substr($orig_body, $img_end);
99 if ($orig_body === false) // in case the body ends on a closing image tag
102 $img_start = strpos($orig_body, '[img');
103 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
104 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
107 if ( ($textlen + strlen($orig_body)) > $maxlen) {
108 if ($textlen < $maxlen) {
109 logger('limit_body_size: the limit happens after the end of the last image', LOGGER_DEBUG);
110 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
114 logger('limit_body_size: the text size with embedded images extracted did not violate the limit', LOGGER_DEBUG);
115 $new_body = $new_body . $orig_body;
116 $textlen += strlen($orig_body);
124 function title_is_body($title, $body) {
126 $title = strip_tags($title);
127 $title = trim($title);
128 $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
129 $title = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $title);
131 $body = strip_tags($body);
133 $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
134 $body = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $body);
136 if (strlen($title) < strlen($body))
137 $body = substr($body, 0, strlen($title));
139 if (($title != $body) and (substr($title, -3) == "...")) {
140 $pos = strrpos($title, "...");
142 $title = substr($title, 0, $pos);
143 $body = substr($body, 0, $pos);
147 return($title == $body);
150 function add_page_info_data($data) {
151 call_hooks('page_info_data', $data);
153 // It maybe is a rich content, but if it does have everything that a link has,
154 // then treat it that way
155 if (($data["type"] == "rich") AND is_string($data["title"]) AND
156 is_string($data["text"]) AND (sizeof($data["images"]) > 0)) {
157 $data["type"] = "link";
160 if ((($data["type"] != "link") AND ($data["type"] != "video") AND ($data["type"] != "photo")) OR ($data["title"] == $data["url"])) {
164 if ($no_photos AND ($data["type"] == "photo")) {
168 if (sizeof($data["images"]) > 0) {
169 $preview = $data["images"][0];
174 // Escape some bad characters
175 $data["url"] = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["url"], ENT_QUOTES, 'UTF-8', false));
176 $data["title"] = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["title"], ENT_QUOTES, 'UTF-8', false));
178 $text = "[attachment type='".$data["type"]."'";
180 if ($data["text"] == "") {
181 $data["text"] = $data["title"];
184 if ($data["text"] == "") {
185 $data["text"] = $data["url"];
188 if ($data["url"] != "") {
189 $text .= " url='".$data["url"]."'";
192 if ($data["title"] != "") {
193 $text .= " title='".$data["title"]."'";
196 if (sizeof($data["images"]) > 0) {
197 $preview = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["images"][0]["src"], ENT_QUOTES, 'UTF-8', false));
198 // if the preview picture is larger than 500 pixels then show it in a larger mode
199 // But only, if the picture isn't higher than large (To prevent huge posts)
200 if (($data["images"][0]["width"] >= 500) AND ($data["images"][0]["width"] >= $data["images"][0]["height"])) {
201 $text .= " image='".$preview."'";
203 $text .= " preview='".$preview."'";
207 $text .= "]".$data["text"]."[/attachment]";
210 if (isset($data["keywords"]) AND count($data["keywords"])) {
213 foreach ($data["keywords"] AS $keyword) {
214 /// @todo make a positive list of allowed characters
215 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'", "’", "`", "(", ")", "„", "“"),
216 array("","", "", "", "", "", "", "", "", "", "", ""), $keyword);
217 $hashtags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url] ";
221 return "\n".$text.$hashtags;
224 function query_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
226 $data = ParseUrl::getSiteinfoCached($url, true);
229 $data["images"][0]["src"] = $photo;
231 logger('fetch page info for '.$url.' '.print_r($data, true), LOGGER_DEBUG);
233 if (!$keywords AND isset($data["keywords"]))
234 unset($data["keywords"]);
236 if (($keyword_blacklist != "") AND isset($data["keywords"])) {
237 $list = explode(",", $keyword_blacklist);
238 foreach ($list AS $keyword) {
239 $keyword = trim($keyword);
240 $index = array_search($keyword, $data["keywords"]);
241 if ($index !== false)
242 unset($data["keywords"][$index]);
249 function add_page_keywords($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
250 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
253 if (isset($data["keywords"]) AND count($data["keywords"])) {
255 foreach ($data["keywords"] AS $keyword) {
256 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'"),
257 array("","", "", "", "", ""), $keyword);
262 $tags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url]";
269 function add_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
270 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
272 $text = add_page_info_data($data);
277 function add_page_info_to_body($body, $texturl = false, $no_photos = false) {
279 logger('add_page_info_to_body: fetch page info for body '.$body, LOGGER_DEBUG);
281 $URLSearchString = "^\[\]";
283 // Fix for Mastodon where the mentions are in a different format
284 $body = preg_replace("/\[url\=([$URLSearchString]*)\]([#!@])(.*?)\[\/url\]/ism",
285 '$2[url=$1]$3[/url]', $body);
287 // Adding these spaces is a quick hack due to my problems with regular expressions :)
288 preg_match("/[^!#@]\[url\]([$URLSearchString]*)\[\/url\]/ism", " ".$body, $matches);
291 preg_match("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", " ".$body, $matches);
293 // Convert urls without bbcode elements
294 if (!$matches AND $texturl) {
295 preg_match("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", " ".$body, $matches);
297 // Yeah, a hack. I really hate regular expressions :)
299 $matches[1] = $matches[2];
303 $footer = add_page_info($matches[1], $no_photos);
305 // Remove the link from the body if the link is attached at the end of the post
306 if (isset($footer) AND (trim($footer) != "") AND (strpos($footer, $matches[1]))) {
307 $removedlink = trim(str_replace($matches[1], "", $body));
308 if (($removedlink == "") OR strstr($body, $removedlink))
309 $body = $removedlink;
311 $url = str_replace(array('/', '.'), array('\/', '\.'), $matches[1]);
312 $removedlink = preg_replace("/\[url\=".$url."\](.*?)\[\/url\]/ism", '', $body);
313 if (($removedlink == "") OR strstr($body, $removedlink))
314 $body = $removedlink;
317 // Add the page information to the bottom
318 if (isset($footer) AND (trim($footer) != ""))
325 * Adds a "lang" specification in a "postopts" element of given $arr,
326 * if possible and not already present.
327 * Expects "body" element to exist in $arr.
329 * @todo Add a parameter to request forcing override
331 function item_add_language_opt(&$arr) {
333 if (version_compare(PHP_VERSION, '5.3.0', '<')) return; // LanguageDetect.php not available ?
335 if ( x($arr, 'postopts') )
337 if ( strstr($arr['postopts'], 'lang=') )
340 /// @TODO Add parameter to request overriding
343 $postopts = $arr['postopts'];
348 require_once('library/langdet/Text/LanguageDetect.php');
349 $naked_body = preg_replace('/\[(.+?)\]/','',$arr['body']);
350 $l = new Text_LanguageDetect;
351 //$lng = $l->detectConfidence($naked_body);
352 //$arr['postopts'] = (($lng['language']) ? 'lang=' . $lng['language'] . ';' . $lng['confidence'] : '');
353 $lng = $l->detect($naked_body, 3);
355 if (sizeof($lng) > 0) {
356 if ($postopts != "") $postopts .= '&'; // arbitrary separator, to be reviewed
357 $postopts .= 'lang=';
359 foreach ($lng as $language => $score) {
360 $postopts .= $sep . $language.";".$score;
363 $arr['postopts'] = $postopts;
368 * @brief Creates an unique guid out of a given uri
370 * @param string $uri uri of an item entry
371 * @param string $host (Optional) hostname for the GUID prefix
372 * @return string unique guid
374 function uri_to_guid($uri, $host = "") {
376 // Our regular guid routine is using this kind of prefix as well
377 // We have to avoid that different routines could accidentally create the same value
378 $parsed = parse_url($uri);
381 $host = $parsed["host"];
384 $guid_prefix = hash("crc32", $host);
386 // Remove the scheme to make sure that "https" and "http" doesn't make a difference
387 unset($parsed["scheme"]);
389 $host_id = implode("/", $parsed);
391 // We could use any hash algorithm since it isn't a security issue
392 $host_hash = hash("ripemd128", $host_id);
394 return $guid_prefix.$host_hash;
397 function item_store($arr,$force_parent = false, $notify = false, $dontcache = false) {
401 // If it is a posting where users should get notifications, then define it as wall posting
404 $arr['type'] = 'wall';
406 $arr['last-child'] = 1;
407 $arr['network'] = NETWORK_DFRN;
409 // We have to avoid duplicates. So we create the GUID in form of a hash of the plink or uri.
410 // In difference to the call to "uri_to_guid" several lines below we add the hash of our own host.
411 // This is done because our host is the original creator of the post.
412 if (isset($arr['plink'])) {
413 $arr['guid'] = uri_to_guid($arr['plink'], $a->get_hostname());
414 } elseif (isset($arr['uri'])) {
415 $arr['guid'] = uri_to_guid($arr['uri'], $a->get_hostname());
419 // If a Diaspora signature structure was passed in, pull it out of the
420 // item array and set it aside for later storage.
423 if (x($arr,'dsprsig')) {
424 $encoded_signature = $arr['dsprsig'];
425 $dsprsig = json_decode(base64_decode($arr['dsprsig']));
426 unset($arr['dsprsig']);
429 // Converting the plink
430 if ($arr['network'] == NETWORK_OSTATUS) {
431 if (isset($arr['plink']))
432 $arr['plink'] = ostatus::convert_href($arr['plink']);
433 elseif (isset($arr['uri']))
434 $arr['plink'] = ostatus::convert_href($arr['uri']);
437 if (x($arr, 'gravity'))
438 $arr['gravity'] = intval($arr['gravity']);
439 elseif ($arr['parent-uri'] === $arr['uri'])
441 elseif (activity_match($arr['verb'],ACTIVITY_POST))
444 $arr['gravity'] = 6; // extensible catchall
446 if (! x($arr,'type'))
447 $arr['type'] = 'remote';
451 /* check for create date and expire time */
452 $uid = intval($arr['uid']);
453 $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
454 if (dbm::is_result($r)) {
455 $expire_interval = $r[0]['expire'];
456 if ($expire_interval>0) {
457 $expire_date = new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
458 $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
459 if ($created_date < $expire_date) {
460 logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
466 // Do we already have this item?
467 // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
468 if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
469 $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s') LIMIT 1",
470 dbesc(trim($arr['uri'])),
472 dbesc(NETWORK_DIASPORA),
474 dbesc(NETWORK_OSTATUS)
477 // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
479 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']);
484 // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
485 // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
486 //if ((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
487 // $arr['body'] = strip_tags($arr['body']);
489 item_add_language_opt($arr);
493 elseif ((trim($arr['guid']) == "") AND (trim($arr['plink']) != ""))
494 $arr['guid'] = uri_to_guid($arr['plink']);
495 elseif ((trim($arr['guid']) == "") AND (trim($arr['uri']) != ""))
496 $arr['guid'] = uri_to_guid($arr['uri']);
498 $parsed = parse_url($arr["author-link"]);
499 $guid_prefix = hash("crc32", $parsed["host"]);
502 $arr['wall'] = ((x($arr,'wall')) ? intval($arr['wall']) : 0);
503 $arr['guid'] = ((x($arr,'guid')) ? notags(trim($arr['guid'])) : get_guid(32, $guid_prefix));
504 $arr['uri'] = ((x($arr,'uri')) ? notags(trim($arr['uri'])) : item_new_uri($a->get_hostname(), $uid, $arr['guid']));
505 $arr['extid'] = ((x($arr,'extid')) ? notags(trim($arr['extid'])) : '');
506 $arr['author-name'] = ((x($arr,'author-name')) ? trim($arr['author-name']) : '');
507 $arr['author-link'] = ((x($arr,'author-link')) ? notags(trim($arr['author-link'])) : '');
508 $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
509 $arr['owner-name'] = ((x($arr,'owner-name')) ? trim($arr['owner-name']) : '');
510 $arr['owner-link'] = ((x($arr,'owner-link')) ? notags(trim($arr['owner-link'])) : '');
511 $arr['owner-avatar'] = ((x($arr,'owner-avatar')) ? notags(trim($arr['owner-avatar'])) : '');
512 $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert());
513 $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert());
514 $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert());
515 $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert());
516 $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert());
517 $arr['title'] = ((x($arr,'title')) ? trim($arr['title']) : '');
518 $arr['location'] = ((x($arr,'location')) ? trim($arr['location']) : '');
519 $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : '');
520 $arr['last-child'] = ((x($arr,'last-child')) ? intval($arr['last-child']) : 0 );
521 $arr['visible'] = ((x($arr,'visible') !== false) ? intval($arr['visible']) : 1 );
523 $arr['parent-uri'] = ((x($arr,'parent-uri')) ? notags(trim($arr['parent-uri'])) : $arr['uri']);
524 $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : '');
525 $arr['object-type'] = ((x($arr,'object-type')) ? notags(trim($arr['object-type'])) : '');
526 $arr['object'] = ((x($arr,'object')) ? trim($arr['object']) : '');
527 $arr['target-type'] = ((x($arr,'target-type')) ? notags(trim($arr['target-type'])) : '');
528 $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : '');
529 $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : '');
530 $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : '');
531 $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : '');
532 $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : '');
533 $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : '');
534 $arr['private'] = ((x($arr,'private')) ? intval($arr['private']) : 0 );
535 $arr['bookmark'] = ((x($arr,'bookmark')) ? intval($arr['bookmark']) : 0 );
536 $arr['body'] = ((x($arr,'body')) ? trim($arr['body']) : '');
537 $arr['tag'] = ((x($arr,'tag')) ? notags(trim($arr['tag'])) : '');
538 $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : '');
539 $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : '');
540 $arr['origin'] = ((x($arr,'origin')) ? intval($arr['origin']) : 0 );
541 $arr['network'] = ((x($arr,'network')) ? trim($arr['network']) : '');
542 $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : '');
543 $arr['resource-id'] = ((x($arr,'resource-id')) ? trim($arr['resource-id']) : '');
544 $arr['event-id'] = ((x($arr,'event-id')) ? intval($arr['event-id']) : 0 );
545 $arr['inform'] = ((x($arr,'inform')) ? trim($arr['inform']) : '');
546 $arr['file'] = ((x($arr,'file')) ? trim($arr['file']) : '');
548 // Items cannot be stored before they happen ...
549 if ($arr['created'] > datetime_convert())
550 $arr['created'] = datetime_convert();
552 // We haven't invented time travel by now.
553 if ($arr['edited'] > datetime_convert())
554 $arr['edited'] = datetime_convert();
556 if (($arr['author-link'] == "") AND ($arr['owner-link'] == ""))
557 logger("Both author-link and owner-link are empty. Called by: ".App::callstack(), LOGGER_DEBUG);
559 if ($arr['plink'] == "") {
561 $arr['plink'] = $a->get_baseurl().'/display/'.urlencode($arr['guid']);
564 if ($arr['network'] == "") {
565 $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
566 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
567 dbesc(normalise_link($arr['author-link'])),
571 if (!dbm::is_result($r))
572 $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
573 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
574 dbesc(normalise_link($arr['author-link']))
577 if (!dbm::is_result($r))
578 $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
579 intval($arr['contact-id']),
583 if (dbm::is_result($r))
584 $arr['network'] = $r[0]["network"];
586 // Fallback to friendica (why is it empty in some cases?)
587 if ($arr['network'] == "")
588 $arr['network'] = NETWORK_DFRN;
590 logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
593 // The contact-id should be set before "item_store" was called - but there seems to be some issues
594 if ($arr["contact-id"] == 0) {
595 // First we are looking for a suitable contact that matches with the author of the post
596 // This is done only for comments (See below explanation at "gcontact-id")
597 if ($arr['parent-uri'] != $arr['uri'])
598 $arr["contact-id"] = get_contact($arr['author-link'], $uid);
600 // If not present then maybe the owner was found
601 if ($arr["contact-id"] == 0)
602 $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
604 // Still missing? Then use the "self" contact of the current user
605 if ($arr["contact-id"] == 0) {
606 $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
608 $arr["contact-id"] = $r[0]["id"];
610 logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
613 if ($arr["gcontact-id"] == 0) {
614 // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
615 // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
616 // On comments the author is the better choice.
617 if ($arr['parent-uri'] === $arr['uri'])
618 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
619 "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
621 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
622 "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
625 if ($arr["author-id"] == 0)
626 $arr["author-id"] = get_contact($arr["author-link"], 0);
628 if ($arr["owner-id"] == 0)
629 $arr["owner-id"] = get_contact($arr["owner-link"], 0);
631 if ($arr['guid'] != "") {
632 // Checking if there is already an item with the same guid
633 logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
634 $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
635 dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
637 if (dbm::is_result($r)) {
638 logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
643 // Check for hashtags in the body and repair or add hashtag links
644 item_body_set_hashtags($arr);
646 $arr['thr-parent'] = $arr['parent-uri'];
647 if ($arr['parent-uri'] === $arr['uri']) {
650 $allow_cid = $arr['allow_cid'];
651 $allow_gid = $arr['allow_gid'];
652 $deny_cid = $arr['deny_cid'];
653 $deny_gid = $arr['deny_gid'];
654 $notify_type = 'wall-new';
657 // find the parent and snarf the item id and ACLs
658 // and anything else we need to inherit
660 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
661 dbesc($arr['parent-uri']),
665 if (dbm::is_result($r)) {
667 // is the new message multi-level threaded?
668 // even though we don't support it now, preserve the info
669 // and re-attach to the conversation parent.
671 if ($r[0]['uri'] != $r[0]['parent-uri']) {
672 $arr['parent-uri'] = $r[0]['parent-uri'];
673 $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
674 ORDER BY `id` ASC LIMIT 1",
675 dbesc($r[0]['parent-uri']),
676 dbesc($r[0]['parent-uri']),
683 $parent_id = $r[0]['id'];
684 $parent_deleted = $r[0]['deleted'];
685 $allow_cid = $r[0]['allow_cid'];
686 $allow_gid = $r[0]['allow_gid'];
687 $deny_cid = $r[0]['deny_cid'];
688 $deny_gid = $r[0]['deny_gid'];
689 $arr['wall'] = $r[0]['wall'];
690 $notify_type = 'comment-new';
692 // if the parent is private, force privacy for the entire conversation
693 // This differs from the above settings as it subtly allows comments from
694 // email correspondents to be private even if the overall thread is not.
696 if ($r[0]['private'])
697 $arr['private'] = $r[0]['private'];
699 // Edge case. We host a public forum that was originally posted to privately.
700 // The original author commented, but as this is a comment, the permissions
701 // weren't fixed up so it will still show the comment as private unless we fix it here.
703 if ((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
707 // If its a post from myself then tag the thread as "mention"
708 logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
709 $u = q("SELECT `nickname` FROM `user` WHERE `uid` = %d", intval($arr['uid']));
712 $self = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
713 logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
714 if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
715 q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
716 logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
721 // Allow one to see reply tweets from status.net even when
722 // we don't have or can't see the original post.
725 logger('item_store: $force_parent=true, reply converted to top-level post.');
727 $arr['parent-uri'] = $arr['uri'];
730 logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
738 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
740 dbesc($arr['network']),
744 if (dbm::is_result($r)) {
745 logger('duplicated item with the same uri found. '.print_r($arr,true));
749 // On Friendica and Diaspora the GUID is unique
750 if (in_array($arr['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) {
751 $r = q("SELECT `id` FROM `item` WHERE `guid` = '%s' AND `uid` = %d LIMIT 1",
755 if (dbm::is_result($r)) {
756 logger('duplicated item with the same guid found. '.print_r($arr,true));
760 // Check for an existing post with the same content. There seems to be a problem with OStatus.
761 $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
763 dbesc($arr['network']),
764 dbesc($arr['created']),
765 intval($arr['contact-id']),
768 if (dbm::is_result($r)) {
769 logger('duplicated item with the same body found. '.print_r($arr,true));
774 // Is this item available in the global items (with uid=0)?
775 if ($arr["uid"] == 0) {
776 $arr["global"] = true;
778 // Set the global flag on all items if this was a global item entry
779 q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
781 $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
783 $arr["global"] = (count($isglobal) > 0);
787 if (strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
790 $private = $arr['private'];
792 $arr["allow_cid"] = $allow_cid;
793 $arr["allow_gid"] = $allow_gid;
794 $arr["deny_cid"] = $deny_cid;
795 $arr["deny_gid"] = $deny_gid;
796 $arr["private"] = $private;
797 $arr["deleted"] = $parent_deleted;
799 // Fill the cache field
800 put_item_in_cache($arr);
803 call_hooks('post_local',$arr);
805 call_hooks('post_remote',$arr);
807 if (x($arr,'cancel')) {
808 logger('item_store: post cancelled by plugin.');
812 // Check for already added items.
813 // There is a timing issue here that sometimes creates double postings.
814 // An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this.
815 if ($arr["uid"] == 0) {
816 $r = qu("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = 0 LIMIT 1", dbesc(trim($arr['uri'])));
817 if (dbm::is_result($r)) {
818 logger('Global item already stored. URI: '.$arr['uri'].' on network '.$arr['network'], LOGGER_DEBUG);
823 // Store the unescaped version
828 logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
831 q("START TRANSACTION;");
833 $r = dbq("INSERT INTO `item` (`"
834 . implode("`, `", array_keys($arr))
836 . implode("', '", array_values($arr))
842 // When the item was successfully stored we fetch the ID of the item.
843 if (dbm::is_result($r)) {
844 $r = q("SELECT LAST_INSERT_ID() AS `item-id`");
845 if (dbm::is_result($r)) {
846 $current_post = $r[0]['item-id'];
848 // This shouldn't happen
852 // This can happen - for example - if there are locking timeouts.
855 // Store the data into a spool file so that we can try again later.
857 // At first we restore the Diaspora signature that we removed above.
858 if (isset($encoded_signature)) {
859 $arr['dsprsig'] = $encoded_signature;
862 // Now we store the data in the spool directory
863 $file = 'item-'.round(microtime(true) * 10000).".msg";
864 $spool = get_spoolpath().'/'.$file;
865 file_put_contents($spool, json_encode($arr));
866 logger("Item wasn't stored - Item was spooled into file ".$file, LOGGER_DEBUG);
870 if ($current_post == 0) {
871 // This is one of these error messages that never should occur.
872 logger("couldn't find created item - we better quit now.");
877 // How much entries have we created?
878 // We wouldn't need this query when we could use an unique index - but MySQL has length problems with them.
879 $r = q("SELECT COUNT(*) AS `entries` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s'",
882 dbesc($arr['network'])
885 if (!dbm::is_result($r)) {
886 // This shouldn't happen, since COUNT always works when the database connection is there.
887 logger("We couldn't count the stored entries. Very strange ...");
892 if ($r[0]["entries"] > 1) {
893 // There are duplicates. We delete our just created entry.
894 logger('Duplicated post occurred. uri = '.$arr['uri'].' uid = '.$arr['uid']);
896 // Yes, we could do a rollback here - but we are having many users with MyISAM.
897 q("DELETE FROM `item` WHERE `id` = %d", intval($current_post));
900 } elseif ($r[0]["entries"] == 0) {
901 // This really should never happen since we quit earlier if there were problems.
902 logger("Something is terribly wrong. We haven't found our created entry.");
907 logger('item_store: created item '.$current_post);
908 item_set_last_item($arr);
910 if (!$parent_id || ($arr['parent-uri'] === $arr['uri']))
911 $parent_id = $current_post;
914 $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
916 intval($current_post)
919 $arr['id'] = $current_post;
920 $arr['parent'] = $parent_id;
922 // update the commented timestamp on the parent
923 // Only update "commented" if it is really a comment
924 if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
925 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
926 dbesc(datetime_convert()),
927 dbesc(datetime_convert()),
931 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
932 dbesc(datetime_convert()),
938 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
939 // We can check for this condition when we decode and encode the stuff again.
940 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
941 $dsprsig->signature = base64_decode($dsprsig->signature);
942 logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
945 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
946 intval($current_post),
947 dbesc($dsprsig->signed_text),
948 dbesc($dsprsig->signature),
949 dbesc($dsprsig->signer)
953 $deleted = tag_deliver($arr['uid'],$current_post);
955 // current post can be deleted if is for a community page and no mention are
957 if (!$deleted AND !$dontcache) {
959 $r = q('SELECT * FROM `item` WHERE id = %d', intval($current_post));
960 if (count($r) == 1) {
962 call_hooks('post_local_end', $r[0]);
964 call_hooks('post_remote_end', $r[0]);
966 logger('item_store: new item not found in DB, id ' . $current_post);
969 if ($arr['parent-uri'] === $arr['uri']) {
970 add_thread($current_post);
972 update_thread($parent_id);
977 // Due to deadlock issues with the "term" table we are doing these steps after the commit.
978 // This is not perfect - but a workable solution until we found the reason for the problem.
979 create_tags_from_item($current_post);
980 create_files_from_item($current_post);
982 // If this is now the last-child, force all _other_ children of this parent to *not* be last-child
983 // It is done after the transaction to avoid dead locks.
984 if ($arr['last-child']) {
985 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
988 intval($current_post)
992 if ($arr['parent-uri'] === $arr['uri']) {
993 add_shadow_thread($current_post);
995 add_shadow_entry($current_post);
998 check_item_notification($current_post, $uid);
1001 proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
1003 return $current_post;
1007 * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
1009 * This can be used to filter for inactive contacts.
1010 * Only do this for public postings to avoid privacy problems, since poco data is public.
1011 * Don't set this value if it isn't from the owner (could be an author that we don't know)
1013 * @param array $arr Contains the just posted item record
1015 function item_set_last_item($arr) {
1017 $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
1019 // Is it a forum? Then we don't care about the rules from above
1020 if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
1021 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
1022 intval($arr['contact-id']));
1029 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1030 dbesc($arr['received']),
1031 dbesc($arr['received']),
1032 intval($arr['contact-id'])
1035 // Now do the same for the system wide contacts with uid=0
1036 if (!$arr['private']) {
1037 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1038 dbesc($arr['received']),
1039 dbesc($arr['received']),
1040 intval($arr['owner-id'])
1043 if ($arr['owner-id'] != $arr['author-id']) {
1044 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1045 dbesc($arr['received']),
1046 dbesc($arr['received']),
1047 intval($arr['author-id'])
1053 function item_body_set_hashtags(&$item) {
1055 $tags = get_tags($item["body"]);
1061 // This sorting is important when there are hashtags that are part of other hashtags
1062 // Otherwise there could be problems with hashtags like #test and #test2
1067 $URLSearchString = "^\[\]";
1069 // All hashtags should point to the home server
1070 //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1071 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
1073 //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1074 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
1076 // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
1077 $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1079 return("[url=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/url]");
1082 $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
1084 return("[bookmark=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/bookmark]");
1087 $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
1089 return("[attachment ".str_replace("#", "#", $match[1])."]".$match[2]."[/attachment]");
1092 // Repair recursive urls
1093 $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1094 "#$2", $item["body"]);
1097 foreach($tags as $tag) {
1098 if (strpos($tag,'#') !== 0)
1101 if (strpos($tag,'[url='))
1104 $basetag = str_replace('_',' ',substr($tag,1));
1106 $newtag = '#[url='.$a->get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1108 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1110 if (!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
1111 if (strlen($item["tag"]))
1112 $item["tag"] = ','.$item["tag"];
1113 $item["tag"] = $newtag.$item["tag"];
1117 // Convert back the masked hashtags
1118 $item["body"] = str_replace("#", "#", $item["body"]);
1121 function get_item_guid($id) {
1122 $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1123 if (dbm::is_result($r))
1124 return($r[0]["guid"]);
1129 function get_item_id($guid, $uid = 0) {
1135 $uid == local_user();
1137 // Does the given user have this item?
1139 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1140 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1141 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1142 if (dbm::is_result($r)) {
1144 $nick = $r[0]["nickname"];
1148 // Or is it anywhere on the server?
1150 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1151 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1152 AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = ''
1153 AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
1154 AND `item`.`private` = 0 AND `item`.`wall` = 1
1155 AND `item`.`guid` = '%s'", dbesc($guid));
1156 if (dbm::is_result($r)) {
1158 $nick = $r[0]["nickname"];
1161 return(array("nick" => $nick, "id" => $id));
1165 function get_item_contact($item,$contacts) {
1166 if (! count($contacts) || (! is_array($item)))
1168 foreach($contacts as $contact) {
1169 if ($contact['id'] == $item['contact-id']) {
1171 break; // NOTREACHED
1178 * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1180 * @param int $item_id
1181 * @return bool true if item was deleted, else false
1183 function tag_deliver($uid,$item_id) {
1191 $u = q("select * from user where uid = %d limit 1",
1197 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1198 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1201 $i = q("select * from item where id = %d and uid = %d limit 1",
1210 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1212 // Diaspora uses their own hardwired link URL in @-tags
1213 // instead of the one we supply with webfinger
1215 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1217 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1219 foreach($matches as $mtch) {
1220 if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1222 logger('tag_deliver: mention found: ' . $mtch[2]);
1228 if ( ($community_page || $prvgroup) &&
1229 (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1230 // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1232 logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1233 q("DELETE FROM item WHERE id = %d and uid = %d",
1242 $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1244 call_hooks('tagged', $arr);
1246 if ((! $community_page) && (! $prvgroup))
1250 // tgroup delivery - setup a second delivery chain
1251 // prevent delivery looping - only proceed
1252 // if the message originated elsewhere and is a top-level post
1254 if (($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1257 // now change this copy of the post to a forum head message and deliver to all the tgroup members
1260 $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1261 intval($u[0]['uid'])
1266 // also reset all the privacy bits to the forum default permissions
1268 $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1270 $forum_mode = (($prvgroup) ? 2 : 1);
1272 q("update item set wall = 1, origin = 1, forum_mode = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1273 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' where id = %d",
1274 intval($forum_mode),
1275 dbesc($c[0]['name']),
1276 dbesc($c[0]['url']),
1277 dbesc($c[0]['thumb']),
1279 dbesc($u[0]['allow_cid']),
1280 dbesc($u[0]['allow_gid']),
1281 dbesc($u[0]['deny_cid']),
1282 dbesc($u[0]['deny_gid']),
1285 update_thread($item_id);
1287 proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
1293 function tgroup_check($uid,$item) {
1299 // check that the message originated elsewhere and is a top-level post
1301 if (($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1305 $u = q("select * from user where uid = %d limit 1",
1311 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1312 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1315 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1317 // Diaspora uses their own hardwired link URL in @-tags
1318 // instead of the one we supply with webfinger
1320 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1322 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1324 foreach($matches as $mtch) {
1325 if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1327 logger('tgroup_check: mention found: ' . $mtch[2]);
1335 if ((! $community_page) && (! $prvgroup))
1342 This function returns true if $update has an edited timestamp newer
1343 than $existing, i.e. $update contains new data which should override
1344 what's already there. If there is no timestamp yet, the update is
1345 assumed to be newer. If the update has no timestamp, the existing
1346 item is assumed to be up-to-date. If the timestamps are equal it
1347 assumes the update has been seen before and should be ignored.
1349 function edited_timestamp_is_newer($existing, $update) {
1350 if (!x($existing,'edited') || !$existing['edited']) {
1353 if (!x($update,'edited') || !$update['edited']) {
1356 $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1357 $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1358 return (strcmp($existing_edited, $update_edited) < 0);
1363 * consume_feed - process atom feed and update anything/everything we might need to update
1365 * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1367 * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1368 * It is this person's stuff that is going to be updated.
1369 * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1370 * from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1371 * have a contact record.
1372 * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1373 * might not) try and subscribe to it.
1374 * $datedir sorts in reverse order
1375 * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1376 * imported prior to its children being seen in the stream unless we are certain
1377 * of how the feed is arranged/ordered.
1378 * With $pass = 1, we only pull parent items out of the stream.
1379 * With $pass = 2, we only pull children (comments/likes).
1381 * So running this twice, first with pass 1 and then with pass 2 will do the right
1382 * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1383 * model where comments can have sub-threads. That would require some massive sorting
1384 * to get all the feed items into a mostly linear ordering, and might still require
1388 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1389 if ($contact['network'] === NETWORK_OSTATUS) {
1391 // Test - remove before flight
1392 //$tempfile = tempnam(get_temppath(), "ostatus2");
1393 //file_put_contents($tempfile, $xml);
1394 logger("Consume OStatus messages ", LOGGER_DEBUG);
1395 ostatus::import($xml,$importer,$contact, $hub);
1400 if ($contact['network'] === NETWORK_FEED) {
1402 logger("Consume feeds", LOGGER_DEBUG);
1403 feed_import($xml,$importer,$contact, $hub);
1408 if ($contact['network'] === NETWORK_DFRN) {
1409 logger("Consume DFRN messages", LOGGER_DEBUG);
1411 $r = q("SELECT `contact`.*, `contact`.`uid` AS `importer_uid`,
1412 `contact`.`pubkey` AS `cpubkey`,
1413 `contact`.`prvkey` AS `cprvkey`,
1414 `contact`.`thumb` AS `thumb`,
1415 `contact`.`url` as `url`,
1416 `contact`.`name` as `senderName`,
1419 LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1420 WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1421 dbesc($contact["id"]), dbesc($importer["uid"])
1424 logger("Now import the DFRN feed");
1425 dfrn::import($xml,$r[0], true);
1431 function item_is_remote_self($contact, &$datarray) {
1434 if (!$contact['remote_self'])
1437 // Prevent the forwarding of posts that are forwarded
1438 if ($datarray["extid"] == NETWORK_DFRN)
1441 // Prevent to forward already forwarded posts
1442 if ($datarray["app"] == $a->get_hostname())
1445 // Only forward posts
1446 if ($datarray["verb"] != ACTIVITY_POST)
1449 if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1452 $datarray2 = $datarray;
1453 logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1454 if ($contact['remote_self'] == 2) {
1455 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1456 intval($contact['uid']));
1457 if (dbm::is_result($r)) {
1458 $datarray['contact-id'] = $r[0]["id"];
1460 $datarray['owner-name'] = $r[0]["name"];
1461 $datarray['owner-link'] = $r[0]["url"];
1462 $datarray['owner-avatar'] = $r[0]["thumb"];
1464 $datarray['author-name'] = $datarray['owner-name'];
1465 $datarray['author-link'] = $datarray['owner-link'];
1466 $datarray['author-avatar'] = $datarray['owner-avatar'];
1469 if ($contact['network'] != NETWORK_FEED) {
1470 $datarray["guid"] = get_guid(32);
1471 unset($datarray["plink"]);
1472 $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
1473 $datarray["parent-uri"] = $datarray["uri"];
1474 $datarray["extid"] = $contact['network'];
1475 $urlpart = parse_url($datarray2['author-link']);
1476 $datarray["app"] = $urlpart["host"];
1478 $datarray['private'] = 0;
1481 if ($contact['network'] != NETWORK_FEED) {
1482 // Store the original post
1483 $r = item_store($datarray2, false, false);
1484 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1486 $datarray["app"] = "Feed";
1491 function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
1492 $url = notags(trim($datarray['author-link']));
1493 $name = notags(trim($datarray['author-name']));
1494 $photo = notags(trim($datarray['author-avatar']));
1496 if (is_object($item)) {
1497 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1498 if ($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
1499 $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1503 if (is_array($contact)) {
1504 if (($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1505 || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1506 $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1507 intval(CONTACT_IS_FRIEND),
1508 intval($contact['id']),
1509 intval($importer['uid'])
1512 // send email notification to owner?
1515 // create contact record
1517 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1518 `blocked`, `readonly`, `pending`, `writable`)
1519 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1520 intval($importer['uid']),
1521 dbesc(datetime_convert()),
1523 dbesc(normalise_link($url)),
1527 dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1528 intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1530 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1531 intval($importer['uid']),
1534 if (dbm::is_result($r)) {
1535 $contact_record = $r[0];
1536 update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
1540 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1541 intval($importer['uid'])
1545 if (dbm::is_result($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1547 // create notification
1548 $hash = random_string();
1550 if (is_array($contact_record)) {
1551 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1552 VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1553 intval($importer['uid']),
1554 intval($contact_record['id']),
1556 dbesc(datetime_convert())
1560 $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1562 if (intval($def_gid))
1563 group_add_member($importer['uid'],'',$contact_record['id'],$def_gid);
1565 if (($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1566 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1569 'type' => NOTIFY_INTRO,
1570 'notify_flags' => $r[0]['notify-flags'],
1571 'language' => $r[0]['language'],
1572 'to_name' => $r[0]['username'],
1573 'to_email' => $r[0]['email'],
1574 'uid' => $r[0]['uid'],
1575 'link' => $a->get_baseurl() . '/notifications/intro',
1576 'source_name' => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1577 'source_link' => $contact_record['url'],
1578 'source_photo' => $contact_record['photo'],
1579 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1584 } elseif (dbm::is_result($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1585 $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1586 intval($importer['uid']),
1594 function lose_follower($importer,$contact,$datarray = array(),$item = "") {
1596 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1597 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1598 intval(CONTACT_IS_SHARING),
1599 intval($contact['id'])
1602 contact_remove($contact['id']);
1606 function lose_sharer($importer,$contact,$datarray = array(),$item = "") {
1608 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1609 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1610 intval(CONTACT_IS_FOLLOWER),
1611 intval($contact['id'])
1614 contact_remove($contact['id']);
1618 function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
1622 if (is_array($importer)) {
1623 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1624 intval($importer['uid'])
1628 // Diaspora has different message-ids in feeds than they do
1629 // through the direct Diaspora protocol. If we try and use
1630 // the feed, we'll get duplicates. So don't.
1632 if ((! dbm::is_result($r)) || $contact['network'] === NETWORK_DIASPORA)
1635 $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1637 // Use a single verify token, even if multiple hubs
1639 $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1641 $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1643 logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: ' . $push_url . ' with verifier ' . $verify_token);
1645 if (!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1646 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1647 dbesc($verify_token),
1648 intval($contact['id'])
1652 post_url($url,$params);
1654 logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1660 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1662 if (get_config('system','disable_embedded'))
1667 logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1668 $site = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://'));
1673 $img_start = strpos($orig_body, '[img');
1674 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1675 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1676 while( ($img_st_close !== false) && ($img_len !== false) ) {
1678 $img_st_close++; // make it point to AFTER the closing bracket
1679 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1681 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1684 if (stristr($image , $site . '/photo/')) {
1685 // Only embed locally hosted photos
1687 $i = basename($image);
1688 $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
1689 $x = strpos($i,'-');
1692 $res = substr($i,$x+1);
1693 $i = substr($i,0,$x);
1694 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1701 // Check to see if we should replace this photo link with an embedded image
1702 // 1. No need to do so if the photo is public
1703 // 2. If there's a contact-id provided, see if they're in the access list
1704 // for the photo. If so, embed it.
1705 // 3. Otherwise, if we have an item, see if the item permissions match the photo
1706 // permissions, regardless of order but first check to see if they're an exact
1707 // match to save some processing overhead.
1709 if (has_permissions($r[0])) {
1711 $recips = enumerate_permissions($r[0]);
1712 if (in_array($cid, $recips)) {
1716 if (compare_permissions($item,$r[0]))
1721 $data = $r[0]['data'];
1722 $type = $r[0]['type'];
1724 // If a custom width and height were specified, apply before embedding
1725 if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1726 logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1728 $width = intval($match[1]);
1729 $height = intval($match[2]);
1731 $ph = new Photo($data, $type);
1732 if ($ph->is_valid()) {
1733 $ph->scaleImage(max($width, $height));
1734 $data = $ph->imageString();
1735 $type = $ph->getType();
1739 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1740 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1741 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1747 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1748 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1749 if ($orig_body === false)
1752 $img_start = strpos($orig_body, '[img');
1753 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1754 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1757 $new_body = $new_body . $orig_body;
1762 function has_permissions($obj) {
1763 if (($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1768 function compare_permissions($obj1,$obj2) {
1769 // first part is easy. Check that these are exactly the same.
1770 if (($obj1['allow_cid'] == $obj2['allow_cid'])
1771 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1772 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1773 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1776 // This is harder. Parse all the permissions and compare the resulting set.
1778 $recipients1 = enumerate_permissions($obj1);
1779 $recipients2 = enumerate_permissions($obj2);
1782 if ($recipients1 == $recipients2)
1787 // returns an array of contact-ids that are allowed to see this object
1789 function enumerate_permissions($obj) {
1790 $allow_people = expand_acl($obj['allow_cid']);
1791 $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1792 $deny_people = expand_acl($obj['deny_cid']);
1793 $deny_groups = expand_groups(expand_acl($obj['deny_gid']));
1794 $recipients = array_unique(array_merge($allow_people,$allow_groups));
1795 $deny = array_unique(array_merge($deny_people,$deny_groups));
1796 $recipients = array_diff($recipients,$deny);
1800 function item_getfeedtags($item) {
1803 $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1805 for($x = 0; $x < $cnt; $x ++) {
1806 if ($matches[1][$x])
1807 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
1811 $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1813 for($x = 0; $x < $cnt; $x ++) {
1814 if ($matches[1][$x])
1815 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
1821 function item_expire($uid, $days, $network = "", $force = false) {
1823 if ((! $uid) || ($days < 1))
1826 // $expire_network_only = save your own wall posts
1827 // and just expire conversations started by others
1829 $expire_network_only = get_pconfig($uid,'expire','network_only');
1830 $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1832 if ($network != "") {
1833 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1834 // There is an index "uid_network_received" but not "uid_network_created"
1835 // This avoids the creation of another index just for one purpose.
1836 // And it doesn't really matter wether to look at "received" or "created"
1837 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1839 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1841 $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
1842 WHERE `uid` = %d $range
1850 if (! dbm::is_result($r))
1853 $expire_items = get_pconfig($uid, 'expire','items');
1854 $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1856 // Forcing expiring of items - but not notes and marked items
1858 $expire_items = true;
1860 $expire_notes = get_pconfig($uid, 'expire','notes');
1861 $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1863 $expire_starred = get_pconfig($uid, 'expire','starred');
1864 $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1866 $expire_photos = get_pconfig($uid, 'expire','photos');
1867 $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1869 logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1871 foreach($r as $item) {
1873 // don't expire filed items
1875 if (strpos($item['file'],'[') !== false)
1878 // Only expire posts, not photos and photo comments
1880 if ($expire_photos==0 && strlen($item['resource-id']))
1882 if ($expire_starred==0 && intval($item['starred']))
1884 if ($expire_notes==0 && $item['type']=='note')
1886 if ($expire_items==0 && $item['type']!='note')
1889 drop_item($item['id'],false);
1892 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1897 function drop_items($items) {
1900 if (! local_user() && ! remote_user())
1903 if (count($items)) {
1904 foreach($items as $item) {
1905 $owner = drop_item($item,false);
1906 if ($owner && ! $uid)
1911 // multiple threads may have been deleted, send an expire notification
1914 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1918 function drop_item($id,$interactive = true) {
1922 // locate item to be deleted
1924 $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1928 if (! dbm::is_result($r)) {
1931 notice( t('Item not found.') . EOL);
1932 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1937 $owner = $item['uid'];
1941 // check if logged in user is either the author or owner of this item
1943 if (is_array($_SESSION['remote'])) {
1944 foreach($_SESSION['remote'] as $visitor) {
1945 if ($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1946 $contact_id = $visitor['cid'];
1953 if ((local_user() == $item['uid']) || ($contact_id) || (! $interactive)) {
1955 // Check if we should do HTML-based delete confirmation
1956 if ($_REQUEST['confirm']) {
1957 // <form> can't take arguments in its "action" parameter
1958 // so add any arguments as hidden inputs
1959 $query = explode_querystring($a->query_string);
1961 foreach($query['args'] as $arg) {
1962 if (strpos($arg, 'confirm=') === false) {
1963 $arg_parts = explode('=', $arg);
1964 $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1968 return replace_macros(get_markup_template('confirm.tpl'), array(
1970 '$message' => t('Do you really want to delete this item?'),
1971 '$extra_inputs' => $inputs,
1972 '$confirm' => t('Yes'),
1973 '$confirm_url' => $query['base'],
1974 '$confirm_name' => 'confirmed',
1975 '$cancel' => t('Cancel'),
1978 // Now check how the user responded to the confirmation query
1979 if ($_REQUEST['canceled']) {
1980 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1983 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
1986 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
1987 dbesc(datetime_convert()),
1988 dbesc(datetime_convert()),
1991 create_tags_from_item($item['id']);
1992 create_files_from_item($item['id']);
1993 delete_thread($item['id'], $item['parent-uri']);
1995 // clean up categories and tags so they don't end up as orphans
1998 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
2000 foreach($matches as $mtch) {
2001 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
2007 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
2009 foreach($matches as $mtch) {
2010 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
2014 // If item is a link to a photo resource, nuke all the associated photos
2015 // (visitors will not have photo resources)
2016 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
2017 // generate a resource-id and therefore aren't intimately linked to the item.
2019 if (strlen($item['resource-id'])) {
2020 q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
2021 dbesc($item['resource-id']),
2022 intval($item['uid'])
2024 // ignore the result
2027 // If item is a link to an event, nuke the event record.
2029 if (intval($item['event-id'])) {
2030 q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
2031 intval($item['event-id']),
2032 intval($item['uid'])
2034 // ignore the result
2037 // If item has attachments, drop them
2039 foreach(explode(",",$item['attach']) as $attach){
2040 preg_match("|attach/(\d+)|", $attach, $matches);
2041 q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
2042 intval($matches[1]),
2045 // ignore the result
2049 // clean up item_id and sign meta-data tables
2052 // Old code - caused very long queries and warning entries in the mysql logfiles:
2054 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
2055 intval($item['id']),
2056 intval($item['uid'])
2059 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
2060 intval($item['id']),
2061 intval($item['uid'])
2065 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
2067 // Creating list of parents
2068 $r = q("select id from item where parent = %d and uid = %d",
2069 intval($item['id']),
2070 intval($item['uid'])
2075 foreach ($r AS $row) {
2076 if ($parentid != "")
2079 $parentid .= $row["id"];
2083 if ($parentid != "") {
2084 $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
2086 $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
2089 // If it's the parent of a comment thread, kill all the kids
2091 if ($item['uri'] == $item['parent-uri']) {
2092 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
2093 WHERE `parent-uri` = '%s' AND `uid` = %d ",
2094 dbesc(datetime_convert()),
2095 dbesc(datetime_convert()),
2096 dbesc($item['parent-uri']),
2097 intval($item['uid'])
2099 create_tags_from_itemuri($item['parent-uri'], $item['uid']);
2100 create_files_from_itemuri($item['parent-uri'], $item['uid']);
2101 delete_thread_uri($item['parent-uri'], $item['uid']);
2102 // ignore the result
2104 // ensure that last-child is set in case the comment that had it just got wiped.
2105 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2106 dbesc(datetime_convert()),
2107 dbesc($item['parent-uri']),
2108 intval($item['uid'])
2110 // who is the last child now?
2111 $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",
2112 dbesc($item['parent-uri']),
2113 intval($item['uid'])
2115 if (dbm::is_result($r)) {
2116 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2122 $drop_id = intval($item['id']);
2124 // send the notification upstream/downstream as the case may be
2126 proc_run(PRIORITY_HIGH,"include/notifier.php", "drop", $drop_id);
2130 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2135 notice( t('Permission denied.') . EOL);
2136 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2143 function first_post_date($uid,$wall = false) {
2144 $r = q("select id, created from item
2145 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
2147 order by created asc limit 1",
2149 intval($wall ? 1 : 0)
2151 if (dbm::is_result($r)) {
2152 // logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2153 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
2158 /* modified posted_dates() {below} to arrange the list in years */
2159 function list_post_dates($uid, $wall) {
2160 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2162 $dthen = first_post_date($uid, $wall);
2166 // Set the start and end date to the beginning of the month
2167 $dnow = substr($dnow,0,8).'01';
2168 $dthen = substr($dthen,0,8).'01';
2172 // Starting with the current month, get the first and last days of every
2173 // month down to and including the month of the first post
2174 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2175 $dyear = intval(substr($dnow,0,4));
2176 $dstart = substr($dnow,0,8) . '01';
2177 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2178 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2179 $end_month = datetime_convert('','',$dend,'Y-m-d');
2180 $str = day_translate(datetime_convert('','',$dnow,'F'));
2182 $ret[$dyear] = array();
2183 $ret[$dyear][] = array($str,$end_month,$start_month);
2184 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2189 function posted_dates($uid,$wall) {
2190 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2192 $dthen = first_post_date($uid,$wall);
2196 // Set the start and end date to the beginning of the month
2197 $dnow = substr($dnow,0,8).'01';
2198 $dthen = substr($dthen,0,8).'01';
2201 // Starting with the current month, get the first and last days of every
2202 // month down to and including the month of the first post
2203 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2204 $dstart = substr($dnow,0,8) . '01';
2205 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2206 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2207 $end_month = datetime_convert('','',$dend,'Y-m-d');
2208 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
2209 $ret[] = array($str,$end_month,$start_month);
2210 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2216 function posted_date_widget($url,$uid,$wall) {
2219 if (! feature_enabled($uid,'archives'))
2222 // For former Facebook folks that left because of "timeline"
2224 /* if ($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2227 $visible_years = get_pconfig($uid,'system','archive_visible_years');
2228 if (! $visible_years)
2231 $ret = list_post_dates($uid,$wall);
2236 $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2237 $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
2239 $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2240 '$title' => t('Archives'),
2241 '$size' => $visible_years,
2242 '$cutoff_year' => $cutoff_year,
2243 '$cutoff' => $cutoff,
2246 '$showmore' => t('show more')