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"])) {
212 foreach ($data["keywords"] AS $keyword) {
213 /// @todo make a positive list of allowed characters
214 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'", "’", "`", "(", ")", "„", "“"),
215 array("","", "", "", "", "", "", "", "", "", "", ""), $keyword);
216 $hashtags .= "#[url=".App::get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url] ";
220 return "\n".$text.$hashtags;
223 function query_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
225 $data = ParseUrl::getSiteinfoCached($url, true);
228 $data["images"][0]["src"] = $photo;
230 logger('fetch page info for '.$url.' '.print_r($data, true), LOGGER_DEBUG);
232 if (!$keywords AND isset($data["keywords"]))
233 unset($data["keywords"]);
235 if (($keyword_blacklist != "") AND isset($data["keywords"])) {
236 $list = explode(",", $keyword_blacklist);
237 foreach ($list AS $keyword) {
238 $keyword = trim($keyword);
239 $index = array_search($keyword, $data["keywords"]);
240 if ($index !== false)
241 unset($data["keywords"][$index]);
248 function add_page_keywords($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
249 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
252 if (isset($data["keywords"]) AND count($data["keywords"])) {
253 foreach ($data["keywords"] AS $keyword) {
254 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'"),
255 array("","", "", "", "", ""), $keyword);
260 $tags .= "#[url=".App::get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url]";
267 function add_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
268 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
270 $text = add_page_info_data($data);
275 function add_page_info_to_body($body, $texturl = false, $no_photos = false) {
277 logger('add_page_info_to_body: fetch page info for body '.$body, LOGGER_DEBUG);
279 $URLSearchString = "^\[\]";
281 // Fix for Mastodon where the mentions are in a different format
282 $body = preg_replace("/\[url\=([$URLSearchString]*)\]([#!@])(.*?)\[\/url\]/ism",
283 '$2[url=$1]$3[/url]', $body);
285 // Adding these spaces is a quick hack due to my problems with regular expressions :)
286 preg_match("/[^!#@]\[url\]([$URLSearchString]*)\[\/url\]/ism", " ".$body, $matches);
289 preg_match("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", " ".$body, $matches);
291 // Convert urls without bbcode elements
292 if (!$matches AND $texturl) {
293 preg_match("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", " ".$body, $matches);
295 // Yeah, a hack. I really hate regular expressions :)
297 $matches[1] = $matches[2];
301 $footer = add_page_info($matches[1], $no_photos);
303 // Remove the link from the body if the link is attached at the end of the post
304 if (isset($footer) AND (trim($footer) != "") AND (strpos($footer, $matches[1]))) {
305 $removedlink = trim(str_replace($matches[1], "", $body));
306 if (($removedlink == "") OR strstr($body, $removedlink))
307 $body = $removedlink;
309 $url = str_replace(array('/', '.'), array('\/', '\.'), $matches[1]);
310 $removedlink = preg_replace("/\[url\=".$url."\](.*?)\[\/url\]/ism", '', $body);
311 if (($removedlink == "") OR strstr($body, $removedlink))
312 $body = $removedlink;
315 // Add the page information to the bottom
316 if (isset($footer) AND (trim($footer) != ""))
323 * Adds a "lang" specification in a "postopts" element of given $arr,
324 * if possible and not already present.
325 * Expects "body" element to exist in $arr.
327 * @todo Add a parameter to request forcing override
329 function item_add_language_opt(&$arr) {
331 if (version_compare(PHP_VERSION, '5.3.0', '<')) return; // LanguageDetect.php not available ?
333 if ( x($arr, 'postopts') )
335 if ( strstr($arr['postopts'], 'lang=') )
338 /// @TODO Add parameter to request overriding
341 $postopts = $arr['postopts'];
346 require_once('library/langdet/Text/LanguageDetect.php');
347 $naked_body = preg_replace('/\[(.+?)\]/','',$arr['body']);
348 $l = new Text_LanguageDetect;
349 //$lng = $l->detectConfidence($naked_body);
350 //$arr['postopts'] = (($lng['language']) ? 'lang=' . $lng['language'] . ';' . $lng['confidence'] : '');
351 $lng = $l->detect($naked_body, 3);
353 if (sizeof($lng) > 0) {
354 if ($postopts != "") $postopts .= '&'; // arbitrary separator, to be reviewed
355 $postopts .= 'lang=';
357 foreach ($lng as $language => $score) {
358 $postopts .= $sep . $language.";".$score;
361 $arr['postopts'] = $postopts;
366 * @brief Creates an unique guid out of a given uri
368 * @param string $uri uri of an item entry
369 * @param string $host (Optional) hostname for the GUID prefix
370 * @return string unique guid
372 function uri_to_guid($uri, $host = "") {
374 // Our regular guid routine is using this kind of prefix as well
375 // We have to avoid that different routines could accidentally create the same value
376 $parsed = parse_url($uri);
379 $host = $parsed["host"];
382 $guid_prefix = hash("crc32", $host);
384 // Remove the scheme to make sure that "https" and "http" doesn't make a difference
385 unset($parsed["scheme"]);
387 $host_id = implode("/", $parsed);
389 // We could use any hash algorithm since it isn't a security issue
390 $host_hash = hash("ripemd128", $host_id);
392 return $guid_prefix.$host_hash;
395 function item_store($arr,$force_parent = false, $notify = false, $dontcache = false) {
399 // If it is a posting where users should get notifications, then define it as wall posting
402 $arr['type'] = 'wall';
404 $arr['last-child'] = 1;
405 $arr['network'] = NETWORK_DFRN;
407 // We have to avoid duplicates. So we create the GUID in form of a hash of the plink or uri.
408 // In difference to the call to "uri_to_guid" several lines below we add the hash of our own host.
409 // This is done because our host is the original creator of the post.
410 if (isset($arr['plink'])) {
411 $arr['guid'] = uri_to_guid($arr['plink'], $a->get_hostname());
412 } elseif (isset($arr['uri'])) {
413 $arr['guid'] = uri_to_guid($arr['uri'], $a->get_hostname());
417 // If a Diaspora signature structure was passed in, pull it out of the
418 // item array and set it aside for later storage.
421 if (x($arr,'dsprsig')) {
422 $encoded_signature = $arr['dsprsig'];
423 $dsprsig = json_decode(base64_decode($arr['dsprsig']));
424 unset($arr['dsprsig']);
427 // Converting the plink
428 if ($arr['network'] == NETWORK_OSTATUS) {
429 if (isset($arr['plink']))
430 $arr['plink'] = ostatus::convert_href($arr['plink']);
431 elseif (isset($arr['uri']))
432 $arr['plink'] = ostatus::convert_href($arr['uri']);
435 if (x($arr, 'gravity'))
436 $arr['gravity'] = intval($arr['gravity']);
437 elseif ($arr['parent-uri'] === $arr['uri'])
439 elseif (activity_match($arr['verb'],ACTIVITY_POST))
442 $arr['gravity'] = 6; // extensible catchall
444 if (! x($arr,'type'))
445 $arr['type'] = 'remote';
449 /* check for create date and expire time */
450 $uid = intval($arr['uid']);
451 $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
452 if (dbm::is_result($r)) {
453 $expire_interval = $r[0]['expire'];
454 if ($expire_interval>0) {
455 $expire_date = new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
456 $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
457 if ($created_date < $expire_date) {
458 logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
464 // Do we already have this item?
465 // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
466 if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
467 $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s') LIMIT 1",
468 dbesc(trim($arr['uri'])),
470 dbesc(NETWORK_DIASPORA),
472 dbesc(NETWORK_OSTATUS)
475 // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
477 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']);
482 // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
483 // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
484 //if ((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
485 // $arr['body'] = strip_tags($arr['body']);
487 item_add_language_opt($arr);
491 elseif ((trim($arr['guid']) == "") AND (trim($arr['plink']) != ""))
492 $arr['guid'] = uri_to_guid($arr['plink']);
493 elseif ((trim($arr['guid']) == "") AND (trim($arr['uri']) != ""))
494 $arr['guid'] = uri_to_guid($arr['uri']);
496 $parsed = parse_url($arr["author-link"]);
497 $guid_prefix = hash("crc32", $parsed["host"]);
500 $arr['wall'] = ((x($arr,'wall')) ? intval($arr['wall']) : 0);
501 $arr['guid'] = ((x($arr,'guid')) ? notags(trim($arr['guid'])) : get_guid(32, $guid_prefix));
502 $arr['uri'] = ((x($arr,'uri')) ? notags(trim($arr['uri'])) : item_new_uri($a->get_hostname(), $uid, $arr['guid']));
503 $arr['extid'] = ((x($arr,'extid')) ? notags(trim($arr['extid'])) : '');
504 $arr['author-name'] = ((x($arr,'author-name')) ? trim($arr['author-name']) : '');
505 $arr['author-link'] = ((x($arr,'author-link')) ? notags(trim($arr['author-link'])) : '');
506 $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
507 $arr['owner-name'] = ((x($arr,'owner-name')) ? trim($arr['owner-name']) : '');
508 $arr['owner-link'] = ((x($arr,'owner-link')) ? notags(trim($arr['owner-link'])) : '');
509 $arr['owner-avatar'] = ((x($arr,'owner-avatar')) ? notags(trim($arr['owner-avatar'])) : '');
510 $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert());
511 $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert());
512 $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert());
513 $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert());
514 $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert());
515 $arr['title'] = ((x($arr,'title')) ? trim($arr['title']) : '');
516 $arr['location'] = ((x($arr,'location')) ? trim($arr['location']) : '');
517 $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : '');
518 $arr['last-child'] = ((x($arr,'last-child')) ? intval($arr['last-child']) : 0 );
519 $arr['visible'] = ((x($arr,'visible') !== false) ? intval($arr['visible']) : 1 );
521 $arr['parent-uri'] = ((x($arr,'parent-uri')) ? notags(trim($arr['parent-uri'])) : $arr['uri']);
522 $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : '');
523 $arr['object-type'] = ((x($arr,'object-type')) ? notags(trim($arr['object-type'])) : '');
524 $arr['object'] = ((x($arr,'object')) ? trim($arr['object']) : '');
525 $arr['target-type'] = ((x($arr,'target-type')) ? notags(trim($arr['target-type'])) : '');
526 $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : '');
527 $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : '');
528 $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : '');
529 $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : '');
530 $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : '');
531 $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : '');
532 $arr['private'] = ((x($arr,'private')) ? intval($arr['private']) : 0 );
533 $arr['bookmark'] = ((x($arr,'bookmark')) ? intval($arr['bookmark']) : 0 );
534 $arr['body'] = ((x($arr,'body')) ? trim($arr['body']) : '');
535 $arr['tag'] = ((x($arr,'tag')) ? notags(trim($arr['tag'])) : '');
536 $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : '');
537 $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : '');
538 $arr['origin'] = ((x($arr,'origin')) ? intval($arr['origin']) : 0 );
539 $arr['network'] = ((x($arr,'network')) ? trim($arr['network']) : '');
540 $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : '');
541 $arr['resource-id'] = ((x($arr,'resource-id')) ? trim($arr['resource-id']) : '');
542 $arr['event-id'] = ((x($arr,'event-id')) ? intval($arr['event-id']) : 0 );
543 $arr['inform'] = ((x($arr,'inform')) ? trim($arr['inform']) : '');
544 $arr['file'] = ((x($arr,'file')) ? trim($arr['file']) : '');
546 // Items cannot be stored before they happen ...
547 if ($arr['created'] > datetime_convert())
548 $arr['created'] = datetime_convert();
550 // We haven't invented time travel by now.
551 if ($arr['edited'] > datetime_convert())
552 $arr['edited'] = datetime_convert();
554 if (($arr['author-link'] == "") AND ($arr['owner-link'] == ""))
555 logger("Both author-link and owner-link are empty. Called by: ".App::callstack(), LOGGER_DEBUG);
557 if ($arr['plink'] == "") {
558 $arr['plink'] = App::get_baseurl().'/display/'.urlencode($arr['guid']);
561 if ($arr['network'] == "") {
562 $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
563 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
564 dbesc(normalise_link($arr['author-link'])),
568 if (!dbm::is_result($r))
569 $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
570 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
571 dbesc(normalise_link($arr['author-link']))
574 if (!dbm::is_result($r))
575 $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
576 intval($arr['contact-id']),
580 if (dbm::is_result($r))
581 $arr['network'] = $r[0]["network"];
583 // Fallback to friendica (why is it empty in some cases?)
584 if ($arr['network'] == "")
585 $arr['network'] = NETWORK_DFRN;
587 logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
590 // The contact-id should be set before "item_store" was called - but there seems to be some issues
591 if ($arr["contact-id"] == 0) {
592 // First we are looking for a suitable contact that matches with the author of the post
593 // This is done only for comments (See below explanation at "gcontact-id")
594 if ($arr['parent-uri'] != $arr['uri'])
595 $arr["contact-id"] = get_contact($arr['author-link'], $uid);
597 // If not present then maybe the owner was found
598 if ($arr["contact-id"] == 0)
599 $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
601 // Still missing? Then use the "self" contact of the current user
602 if ($arr["contact-id"] == 0) {
603 $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
605 $arr["contact-id"] = $r[0]["id"];
607 logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
610 if ($arr["gcontact-id"] == 0) {
611 // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
612 // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
613 // On comments the author is the better choice.
614 if ($arr['parent-uri'] === $arr['uri'])
615 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
616 "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
618 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
619 "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
622 if ($arr["author-id"] == 0)
623 $arr["author-id"] = get_contact($arr["author-link"], 0);
625 if ($arr["owner-id"] == 0)
626 $arr["owner-id"] = get_contact($arr["owner-link"], 0);
628 if ($arr['guid'] != "") {
629 // Checking if there is already an item with the same guid
630 logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
631 $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
632 dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
634 if (dbm::is_result($r)) {
635 logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
640 // Check for hashtags in the body and repair or add hashtag links
641 item_body_set_hashtags($arr);
643 $arr['thr-parent'] = $arr['parent-uri'];
644 if ($arr['parent-uri'] === $arr['uri']) {
647 $allow_cid = $arr['allow_cid'];
648 $allow_gid = $arr['allow_gid'];
649 $deny_cid = $arr['deny_cid'];
650 $deny_gid = $arr['deny_gid'];
651 $notify_type = 'wall-new';
654 // find the parent and snarf the item id and ACLs
655 // and anything else we need to inherit
657 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
658 dbesc($arr['parent-uri']),
662 if (dbm::is_result($r)) {
664 // is the new message multi-level threaded?
665 // even though we don't support it now, preserve the info
666 // and re-attach to the conversation parent.
668 if ($r[0]['uri'] != $r[0]['parent-uri']) {
669 $arr['parent-uri'] = $r[0]['parent-uri'];
670 $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
671 ORDER BY `id` ASC LIMIT 1",
672 dbesc($r[0]['parent-uri']),
673 dbesc($r[0]['parent-uri']),
680 $parent_id = $r[0]['id'];
681 $parent_deleted = $r[0]['deleted'];
682 $allow_cid = $r[0]['allow_cid'];
683 $allow_gid = $r[0]['allow_gid'];
684 $deny_cid = $r[0]['deny_cid'];
685 $deny_gid = $r[0]['deny_gid'];
686 $arr['wall'] = $r[0]['wall'];
687 $notify_type = 'comment-new';
689 // if the parent is private, force privacy for the entire conversation
690 // This differs from the above settings as it subtly allows comments from
691 // email correspondents to be private even if the overall thread is not.
693 if ($r[0]['private'])
694 $arr['private'] = $r[0]['private'];
696 // Edge case. We host a public forum that was originally posted to privately.
697 // The original author commented, but as this is a comment, the permissions
698 // weren't fixed up so it will still show the comment as private unless we fix it here.
700 if ((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
704 // If its a post from myself then tag the thread as "mention"
705 logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
706 $u = q("SELECT `nickname` FROM `user` WHERE `uid` = %d", intval($arr['uid']));
707 if (dbm::is_result($u)) {
709 $self = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
710 logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
711 if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
712 q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
713 logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
718 // Allow one to see reply tweets from status.net even when
719 // we don't have or can't see the original post.
722 logger('item_store: $force_parent=true, reply converted to top-level post.');
724 $arr['parent-uri'] = $arr['uri'];
727 logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
735 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
737 dbesc($arr['network']),
741 if (dbm::is_result($r)) {
742 logger('duplicated item with the same uri found. '.print_r($arr,true));
746 // On Friendica and Diaspora the GUID is unique
747 if (in_array($arr['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) {
748 $r = q("SELECT `id` FROM `item` WHERE `guid` = '%s' AND `uid` = %d LIMIT 1",
752 if (dbm::is_result($r)) {
753 logger('duplicated item with the same guid found. '.print_r($arr,true));
757 // Check for an existing post with the same content. There seems to be a problem with OStatus.
758 $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
760 dbesc($arr['network']),
761 dbesc($arr['created']),
762 intval($arr['contact-id']),
765 if (dbm::is_result($r)) {
766 logger('duplicated item with the same body found. '.print_r($arr,true));
771 // Is this item available in the global items (with uid=0)?
772 if ($arr["uid"] == 0) {
773 $arr["global"] = true;
775 // Set the global flag on all items if this was a global item entry
776 q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
778 $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
780 $arr["global"] = (count($isglobal) > 0);
784 if (strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
787 $private = $arr['private'];
789 $arr["allow_cid"] = $allow_cid;
790 $arr["allow_gid"] = $allow_gid;
791 $arr["deny_cid"] = $deny_cid;
792 $arr["deny_gid"] = $deny_gid;
793 $arr["private"] = $private;
794 $arr["deleted"] = $parent_deleted;
796 // Fill the cache field
797 put_item_in_cache($arr);
800 call_hooks('post_local',$arr);
802 call_hooks('post_remote',$arr);
804 if (x($arr,'cancel')) {
805 logger('item_store: post cancelled by plugin.');
809 // Check for already added items.
810 // There is a timing issue here that sometimes creates double postings.
811 // An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this.
812 if ($arr["uid"] == 0) {
813 $r = qu("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = 0 LIMIT 1", dbesc(trim($arr['uri'])));
814 if (dbm::is_result($r)) {
815 logger('Global item already stored. URI: '.$arr['uri'].' on network '.$arr['network'], LOGGER_DEBUG);
820 // Store the unescaped version
823 dbm::esc_array($arr, true);
825 logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
828 q("START TRANSACTION;");
830 $r = dbq("INSERT INTO `item` (`"
831 . implode("`, `", array_keys($arr))
833 . implode(", ", array_values($arr))
839 // When the item was successfully stored we fetch the ID of the item.
840 if (dbm::is_result($r)) {
841 $r = q("SELECT LAST_INSERT_ID() AS `item-id`");
842 if (dbm::is_result($r)) {
843 $current_post = $r[0]['item-id'];
845 // This shouldn't happen
849 // This can happen - for example - if there are locking timeouts.
852 // Store the data into a spool file so that we can try again later.
854 // At first we restore the Diaspora signature that we removed above.
855 if (isset($encoded_signature)) {
856 $arr['dsprsig'] = $encoded_signature;
859 // Now we store the data in the spool directory
860 // We use "microtime" to keep the arrival order and "mt_rand" to avoid duplicates
861 $file = 'item-'.round(microtime(true) * 10000).'-'.mt_rand().'.msg';
863 $spoolpath = get_spoolpath();
864 if ($spoolpath != "") {
865 $spool = $spoolpath.'/'.$file;
866 file_put_contents($spool, json_encode($arr));
867 logger("Item wasn't stored - Item was spooled into file ".$file, LOGGER_DEBUG);
872 if ($current_post == 0) {
873 // This is one of these error messages that never should occur.
874 logger("couldn't find created item - we better quit now.");
879 // How much entries have we created?
880 // We wouldn't need this query when we could use an unique index - but MySQL has length problems with them.
881 $r = q("SELECT COUNT(*) AS `entries` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s'",
884 dbesc($arr['network'])
887 if (!dbm::is_result($r)) {
888 // This shouldn't happen, since COUNT always works when the database connection is there.
889 logger("We couldn't count the stored entries. Very strange ...");
894 if ($r[0]["entries"] > 1) {
895 // There are duplicates. We delete our just created entry.
896 logger('Duplicated post occurred. uri = '.$arr['uri'].' uid = '.$arr['uid']);
898 // Yes, we could do a rollback here - but we are having many users with MyISAM.
899 q("DELETE FROM `item` WHERE `id` = %d", intval($current_post));
902 } elseif ($r[0]["entries"] == 0) {
903 // This really should never happen since we quit earlier if there were problems.
904 logger("Something is terribly wrong. We haven't found our created entry.");
909 logger('item_store: created item '.$current_post);
910 item_set_last_item($arr);
912 if (!$parent_id || ($arr['parent-uri'] === $arr['uri']))
913 $parent_id = $current_post;
916 $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
918 intval($current_post)
921 $arr['id'] = $current_post;
922 $arr['parent'] = $parent_id;
924 // update the commented timestamp on the parent
925 // Only update "commented" if it is really a comment
926 if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
927 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
928 dbesc(datetime_convert()),
929 dbesc(datetime_convert()),
933 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
934 dbesc(datetime_convert()),
940 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
941 // We can check for this condition when we decode and encode the stuff again.
942 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
943 $dsprsig->signature = base64_decode($dsprsig->signature);
944 logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
947 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
948 intval($current_post),
949 dbesc($dsprsig->signed_text),
950 dbesc($dsprsig->signature),
951 dbesc($dsprsig->signer)
955 $deleted = tag_deliver($arr['uid'],$current_post);
957 // current post can be deleted if is for a community page and no mention are
959 if (!$deleted AND !$dontcache) {
961 $r = q('SELECT * FROM `item` WHERE `id` = %d', intval($current_post));
962 if ((dbm::is_result($r)) && (count($r) == 1)) {
964 call_hooks('post_local_end', $r[0]);
966 call_hooks('post_remote_end', $r[0]);
969 logger('item_store: new item not found in DB, id ' . $current_post);
973 if ($arr['parent-uri'] === $arr['uri']) {
974 add_thread($current_post);
976 update_thread($parent_id);
981 // Due to deadlock issues with the "term" table we are doing these steps after the commit.
982 // This is not perfect - but a workable solution until we found the reason for the problem.
983 create_tags_from_item($current_post);
984 create_files_from_item($current_post);
986 // If this is now the last-child, force all _other_ children of this parent to *not* be last-child
987 // It is done after the transaction to avoid dead locks.
988 if ($arr['last-child']) {
989 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
992 intval($current_post)
996 if ($arr['parent-uri'] === $arr['uri']) {
997 add_shadow_thread($current_post);
999 add_shadow_entry($current_post);
1002 check_item_notification($current_post, $uid);
1005 proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
1008 return $current_post;
1012 * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
1014 * This can be used to filter for inactive contacts.
1015 * Only do this for public postings to avoid privacy problems, since poco data is public.
1016 * Don't set this value if it isn't from the owner (could be an author that we don't know)
1018 * @param array $arr Contains the just posted item record
1020 function item_set_last_item($arr) {
1022 $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
1024 // Is it a forum? Then we don't care about the rules from above
1025 if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
1026 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
1027 intval($arr['contact-id']));
1034 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1035 dbesc($arr['received']),
1036 dbesc($arr['received']),
1037 intval($arr['contact-id'])
1040 // Now do the same for the system wide contacts with uid=0
1041 if (!$arr['private']) {
1042 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1043 dbesc($arr['received']),
1044 dbesc($arr['received']),
1045 intval($arr['owner-id'])
1048 if ($arr['owner-id'] != $arr['author-id']) {
1049 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1050 dbesc($arr['received']),
1051 dbesc($arr['received']),
1052 intval($arr['author-id'])
1058 function item_body_set_hashtags(&$item) {
1060 $tags = get_tags($item["body"]);
1066 // This sorting is important when there are hashtags that are part of other hashtags
1067 // Otherwise there could be problems with hashtags like #test and #test2
1072 $URLSearchString = "^\[\]";
1074 // All hashtags should point to the home server
1075 //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1076 // "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
1078 //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1079 // "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
1081 // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
1082 $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1084 return("[url=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/url]");
1087 $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
1089 return("[bookmark=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/bookmark]");
1092 $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
1094 return("[attachment ".str_replace("#", "#", $match[1])."]".$match[2]."[/attachment]");
1097 // Repair recursive urls
1098 $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1099 "#$2", $item["body"]);
1102 foreach($tags as $tag) {
1103 if (strpos($tag,'#') !== 0)
1106 if (strpos($tag,'[url='))
1109 $basetag = str_replace('_',' ',substr($tag,1));
1111 $newtag = '#[url='.App::get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1113 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1115 if (!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
1116 if (strlen($item["tag"]))
1117 $item["tag"] = ','.$item["tag"];
1118 $item["tag"] = $newtag.$item["tag"];
1122 // Convert back the masked hashtags
1123 $item["body"] = str_replace("#", "#", $item["body"]);
1126 function get_item_guid($id) {
1127 $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1128 if (dbm::is_result($r))
1129 return($r[0]["guid"]);
1134 function get_item_id($guid, $uid = 0) {
1140 $uid == local_user();
1142 // Does the given user have this item?
1144 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1145 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1146 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1147 if (dbm::is_result($r)) {
1149 $nick = $r[0]["nickname"];
1153 // Or is it anywhere on the server?
1155 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1156 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1157 AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = ''
1158 AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
1159 AND `item`.`private` = 0 AND `item`.`wall` = 1
1160 AND `item`.`guid` = '%s'", dbesc($guid));
1161 if (dbm::is_result($r)) {
1163 $nick = $r[0]["nickname"];
1166 return(array("nick" => $nick, "id" => $id));
1170 function get_item_contact($item,$contacts) {
1171 if (! count($contacts) || (! is_array($item)))
1173 foreach($contacts as $contact) {
1174 if ($contact['id'] == $item['contact-id']) {
1176 break; // NOTREACHED
1183 * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1185 * @param int $item_id
1186 * @return bool true if item was deleted, else false
1188 function tag_deliver($uid,$item_id) {
1196 $u = q("select * from user where uid = %d limit 1",
1200 if (! dbm::is_result($u)) {
1204 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1205 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1208 $i = q("SELECT * FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1212 if (! dbm::is_result($i)) {
1218 $link = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
1220 // Diaspora uses their own hardwired link URL in @-tags
1221 // instead of the one we supply with webfinger
1223 $dlink = normalise_link(App::get_baseurl() . '/u/' . $u[0]['nickname']);
1225 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1227 foreach($matches as $mtch) {
1228 if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1230 logger('tag_deliver: mention found: ' . $mtch[2]);
1236 if ( ($community_page || $prvgroup) &&
1237 (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1238 // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1240 logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1241 q("DELETE FROM item WHERE id = %d and uid = %d",
1250 $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1252 call_hooks('tagged', $arr);
1254 if ((! $community_page) && (! $prvgroup))
1258 // tgroup delivery - setup a second delivery chain
1259 // prevent delivery looping - only proceed
1260 // if the message originated elsewhere and is a top-level post
1262 if (($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1265 // now change this copy of the post to a forum head message and deliver to all the tgroup members
1268 $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1269 intval($u[0]['uid'])
1271 if (! dbm::is_result($c)) {
1275 // also reset all the privacy bits to the forum default permissions
1277 $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1279 $forum_mode = (($prvgroup) ? 2 : 1);
1281 q("UPDATE `item` SET `wall` = 1, `origin` = 1, `forum_mode` = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1282 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' WHERE `id` = %d",
1283 intval($forum_mode),
1284 dbesc($c[0]['name']),
1285 dbesc($c[0]['url']),
1286 dbesc($c[0]['thumb']),
1288 dbesc($u[0]['allow_cid']),
1289 dbesc($u[0]['allow_gid']),
1290 dbesc($u[0]['deny_cid']),
1291 dbesc($u[0]['deny_gid']),
1294 update_thread($item_id);
1296 proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
1302 function tgroup_check($uid,$item) {
1306 // check that the message originated elsewhere and is a top-level post
1308 if (($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1311 /// @TODO Encapsulate this or find it encapsulated and replace all occurrances
1312 $u = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1315 if (! dbm::is_result($u)) {
1319 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1320 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1323 $link = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
1325 // Diaspora uses their own hardwired link URL in @-tags
1326 // instead of the one we supply with webfinger
1328 $dlink = normalise_link(App::get_baseurl() . '/u/' . $u[0]['nickname']);
1330 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1332 foreach ($matches as $mtch) {
1333 if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1335 logger('tgroup_check: mention found: ' . $mtch[2]);
1344 /// @TODO Combines both return statements into one
1345 return (($community_page) || ($prvgroup));
1349 This function returns true if $update has an edited timestamp newer
1350 than $existing, i.e. $update contains new data which should override
1351 what's already there. If there is no timestamp yet, the update is
1352 assumed to be newer. If the update has no timestamp, the existing
1353 item is assumed to be up-to-date. If the timestamps are equal it
1354 assumes the update has been seen before and should be ignored.
1356 function edited_timestamp_is_newer($existing, $update) {
1357 if (!x($existing,'edited') || !$existing['edited']) {
1360 if (!x($update,'edited') || !$update['edited']) {
1364 $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1365 $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1366 return (strcmp($existing_edited, $update_edited) < 0);
1371 * consume_feed - process atom feed and update anything/everything we might need to update
1373 * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1375 * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1376 * It is this person's stuff that is going to be updated.
1377 * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1378 * from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1379 * have a contact record.
1380 * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1381 * might not) try and subscribe to it.
1382 * $datedir sorts in reverse order
1383 * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1384 * imported prior to its children being seen in the stream unless we are certain
1385 * of how the feed is arranged/ordered.
1386 * With $pass = 1, we only pull parent items out of the stream.
1387 * With $pass = 2, we only pull children (comments/likes).
1389 * So running this twice, first with pass 1 and then with pass 2 will do the right
1390 * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1391 * model where comments can have sub-threads. That would require some massive sorting
1392 * to get all the feed items into a mostly linear ordering, and might still require
1396 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1397 if ($contact['network'] === NETWORK_OSTATUS) {
1399 // Test - remove before flight
1400 //$tempfile = tempnam(get_temppath(), "ostatus2");
1401 //file_put_contents($tempfile, $xml);
1402 logger("Consume OStatus messages ", LOGGER_DEBUG);
1403 ostatus::import($xml,$importer,$contact, $hub);
1408 if ($contact['network'] === NETWORK_FEED) {
1410 logger("Consume feeds", LOGGER_DEBUG);
1411 feed_import($xml,$importer,$contact, $hub);
1416 if ($contact['network'] === NETWORK_DFRN) {
1417 logger("Consume DFRN messages", LOGGER_DEBUG);
1419 $r = q("SELECT `contact`.*, `contact`.`uid` AS `importer_uid`,
1420 `contact`.`pubkey` AS `cpubkey`,
1421 `contact`.`prvkey` AS `cprvkey`,
1422 `contact`.`thumb` AS `thumb`,
1423 `contact`.`url` as `url`,
1424 `contact`.`name` as `senderName`,
1427 LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1428 WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1429 dbesc($contact["id"]), dbesc($importer["uid"])
1432 logger("Now import the DFRN feed");
1433 dfrn::import($xml,$r[0], true);
1439 function item_is_remote_self($contact, &$datarray) {
1442 if (!$contact['remote_self'])
1445 // Prevent the forwarding of posts that are forwarded
1446 if ($datarray["extid"] == NETWORK_DFRN)
1449 // Prevent to forward already forwarded posts
1450 if ($datarray["app"] == $a->get_hostname())
1453 // Only forward posts
1454 if ($datarray["verb"] != ACTIVITY_POST)
1457 if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1460 $datarray2 = $datarray;
1461 logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1462 if ($contact['remote_self'] == 2) {
1463 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1464 intval($contact['uid']));
1465 if (dbm::is_result($r)) {
1466 $datarray['contact-id'] = $r[0]["id"];
1468 $datarray['owner-name'] = $r[0]["name"];
1469 $datarray['owner-link'] = $r[0]["url"];
1470 $datarray['owner-avatar'] = $r[0]["thumb"];
1472 $datarray['author-name'] = $datarray['owner-name'];
1473 $datarray['author-link'] = $datarray['owner-link'];
1474 $datarray['author-avatar'] = $datarray['owner-avatar'];
1477 if ($contact['network'] != NETWORK_FEED) {
1478 $datarray["guid"] = get_guid(32);
1479 unset($datarray["plink"]);
1480 $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
1481 $datarray["parent-uri"] = $datarray["uri"];
1482 $datarray["extid"] = $contact['network'];
1483 $urlpart = parse_url($datarray2['author-link']);
1484 $datarray["app"] = $urlpart["host"];
1486 $datarray['private'] = 0;
1489 if ($contact['network'] != NETWORK_FEED) {
1490 // Store the original post
1491 $r = item_store($datarray2, false, false);
1492 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1494 $datarray["app"] = "Feed";
1499 function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
1500 $url = notags(trim($datarray['author-link']));
1501 $name = notags(trim($datarray['author-name']));
1502 $photo = notags(trim($datarray['author-avatar']));
1504 if (is_object($item)) {
1505 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1506 if ($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
1507 $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1511 if (is_array($contact)) {
1512 if (($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1513 || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1514 $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1515 intval(CONTACT_IS_FRIEND),
1516 intval($contact['id']),
1517 intval($importer['uid'])
1520 // send email notification to owner?
1523 // create contact record
1525 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1526 `blocked`, `readonly`, `pending`, `writable`)
1527 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1528 intval($importer['uid']),
1529 dbesc(datetime_convert()),
1531 dbesc(normalise_link($url)),
1535 dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1536 intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1538 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1539 intval($importer['uid']),
1542 if (dbm::is_result($r)) {
1543 $contact_record = $r[0];
1544 update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
1548 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1549 intval($importer['uid'])
1553 if (dbm::is_result($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1555 // create notification
1556 $hash = random_string();
1558 if (is_array($contact_record)) {
1559 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1560 VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1561 intval($importer['uid']),
1562 intval($contact_record['id']),
1564 dbesc(datetime_convert())
1568 $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1570 if (intval($def_gid))
1571 group_add_member($importer['uid'],'',$contact_record['id'],$def_gid);
1573 if (($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1574 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1577 'type' => NOTIFY_INTRO,
1578 'notify_flags' => $r[0]['notify-flags'],
1579 'language' => $r[0]['language'],
1580 'to_name' => $r[0]['username'],
1581 'to_email' => $r[0]['email'],
1582 'uid' => $r[0]['uid'],
1583 'link' => App::get_baseurl() . '/notifications/intro',
1584 'source_name' => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1585 'source_link' => $contact_record['url'],
1586 'source_photo' => $contact_record['photo'],
1587 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1592 } elseif (dbm::is_result($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1593 $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1594 intval($importer['uid']),
1602 function lose_follower($importer,$contact,$datarray = array(),$item = "") {
1604 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1605 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1606 intval(CONTACT_IS_SHARING),
1607 intval($contact['id'])
1610 contact_remove($contact['id']);
1614 function lose_sharer($importer,$contact,$datarray = array(),$item = "") {
1616 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1617 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1618 intval(CONTACT_IS_FOLLOWER),
1619 intval($contact['id'])
1622 contact_remove($contact['id']);
1626 function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
1630 if (is_array($importer)) {
1631 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1632 intval($importer['uid'])
1636 // Diaspora has different message-ids in feeds than they do
1637 // through the direct Diaspora protocol. If we try and use
1638 // the feed, we'll get duplicates. So don't.
1640 if ((! dbm::is_result($r)) || $contact['network'] === NETWORK_DIASPORA)
1643 $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1645 // Use a single verify token, even if multiple hubs
1647 $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1649 $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1651 logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: ' . $push_url . ' with verifier ' . $verify_token);
1653 if (!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1654 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1655 dbesc($verify_token),
1656 intval($contact['id'])
1660 post_url($url,$params);
1662 logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1668 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1670 if (get_config('system','disable_embedded'))
1675 logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1676 $site = substr(App::get_baseurl(),strpos(App::get_baseurl(),'://'));
1681 $img_start = strpos($orig_body, '[img');
1682 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1683 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1684 while( ($img_st_close !== false) && ($img_len !== false) ) {
1686 $img_st_close++; // make it point to AFTER the closing bracket
1687 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1689 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1692 if (stristr($image , $site . '/photo/')) {
1693 // Only embed locally hosted photos
1695 $i = basename($image);
1696 $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
1697 $x = strpos($i,'-');
1700 $res = substr($i,$x+1);
1701 $i = substr($i,0,$x);
1702 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1709 // Check to see if we should replace this photo link with an embedded image
1710 // 1. No need to do so if the photo is public
1711 // 2. If there's a contact-id provided, see if they're in the access list
1712 // for the photo. If so, embed it.
1713 // 3. Otherwise, if we have an item, see if the item permissions match the photo
1714 // permissions, regardless of order but first check to see if they're an exact
1715 // match to save some processing overhead.
1717 if (has_permissions($r[0])) {
1719 $recips = enumerate_permissions($r[0]);
1720 if (in_array($cid, $recips)) {
1724 if (compare_permissions($item,$r[0]))
1729 $data = $r[0]['data'];
1730 $type = $r[0]['type'];
1732 // If a custom width and height were specified, apply before embedding
1733 if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1734 logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1736 $width = intval($match[1]);
1737 $height = intval($match[2]);
1739 $ph = new Photo($data, $type);
1740 if ($ph->is_valid()) {
1741 $ph->scaleImage(max($width, $height));
1742 $data = $ph->imageString();
1743 $type = $ph->getType();
1747 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1748 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1749 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1755 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1756 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1757 if ($orig_body === false)
1760 $img_start = strpos($orig_body, '[img');
1761 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1762 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1765 $new_body = $new_body . $orig_body;
1770 function has_permissions($obj) {
1771 if (($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1776 function compare_permissions($obj1,$obj2) {
1777 // first part is easy. Check that these are exactly the same.
1778 if (($obj1['allow_cid'] == $obj2['allow_cid'])
1779 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1780 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1781 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1784 // This is harder. Parse all the permissions and compare the resulting set.
1786 $recipients1 = enumerate_permissions($obj1);
1787 $recipients2 = enumerate_permissions($obj2);
1790 if ($recipients1 == $recipients2)
1795 // returns an array of contact-ids that are allowed to see this object
1797 function enumerate_permissions($obj) {
1798 $allow_people = expand_acl($obj['allow_cid']);
1799 $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1800 $deny_people = expand_acl($obj['deny_cid']);
1801 $deny_groups = expand_groups(expand_acl($obj['deny_gid']));
1802 $recipients = array_unique(array_merge($allow_people,$allow_groups));
1803 $deny = array_unique(array_merge($deny_people,$deny_groups));
1804 $recipients = array_diff($recipients,$deny);
1808 function item_getfeedtags($item) {
1811 $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1813 for($x = 0; $x < $cnt; $x ++) {
1814 if ($matches[1][$x])
1815 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
1819 $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1821 for($x = 0; $x < $cnt; $x ++) {
1822 if ($matches[1][$x])
1823 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
1829 function item_expire($uid, $days, $network = "", $force = false) {
1831 if ((! $uid) || ($days < 1))
1834 // $expire_network_only = save your own wall posts
1835 // and just expire conversations started by others
1837 $expire_network_only = get_pconfig($uid,'expire','network_only');
1838 $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1840 if ($network != "") {
1841 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1842 // There is an index "uid_network_received" but not "uid_network_created"
1843 // This avoids the creation of another index just for one purpose.
1844 // And it doesn't really matter wether to look at "received" or "created"
1845 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1847 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1849 $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
1850 WHERE `uid` = %d $range
1858 if (! dbm::is_result($r))
1861 $expire_items = get_pconfig($uid, 'expire','items');
1862 $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1864 // Forcing expiring of items - but not notes and marked items
1866 $expire_items = true;
1868 $expire_notes = get_pconfig($uid, 'expire','notes');
1869 $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1871 $expire_starred = get_pconfig($uid, 'expire','starred');
1872 $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1874 $expire_photos = get_pconfig($uid, 'expire','photos');
1875 $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1877 logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1879 foreach($r as $item) {
1881 // don't expire filed items
1883 if (strpos($item['file'],'[') !== false)
1886 // Only expire posts, not photos and photo comments
1888 if ($expire_photos==0 && strlen($item['resource-id']))
1890 if ($expire_starred==0 && intval($item['starred']))
1892 if ($expire_notes==0 && $item['type']=='note')
1894 if ($expire_items==0 && $item['type']!='note')
1897 drop_item($item['id'],false);
1900 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1905 function drop_items($items) {
1908 if (! local_user() && ! remote_user())
1911 if (count($items)) {
1912 foreach($items as $item) {
1913 $owner = drop_item($item,false);
1914 if ($owner && ! $uid)
1919 // multiple threads may have been deleted, send an expire notification
1922 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1926 function drop_item($id,$interactive = true) {
1930 // locate item to be deleted
1932 $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1936 if (! dbm::is_result($r)) {
1939 notice( t('Item not found.') . EOL);
1940 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
1945 $owner = $item['uid'];
1949 // check if logged in user is either the author or owner of this item
1951 if (is_array($_SESSION['remote'])) {
1952 foreach($_SESSION['remote'] as $visitor) {
1953 if ($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1954 $contact_id = $visitor['cid'];
1961 if ((local_user() == $item['uid']) || ($contact_id) || (! $interactive)) {
1963 // Check if we should do HTML-based delete confirmation
1964 if ($_REQUEST['confirm']) {
1965 // <form> can't take arguments in its "action" parameter
1966 // so add any arguments as hidden inputs
1967 $query = explode_querystring($a->query_string);
1969 foreach($query['args'] as $arg) {
1970 if (strpos($arg, 'confirm=') === false) {
1971 $arg_parts = explode('=', $arg);
1972 $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1976 return replace_macros(get_markup_template('confirm.tpl'), array(
1978 '$message' => t('Do you really want to delete this item?'),
1979 '$extra_inputs' => $inputs,
1980 '$confirm' => t('Yes'),
1981 '$confirm_url' => $query['base'],
1982 '$confirm_name' => 'confirmed',
1983 '$cancel' => t('Cancel'),
1986 // Now check how the user responded to the confirmation query
1987 if ($_REQUEST['canceled']) {
1988 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
1991 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
1994 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
1995 dbesc(datetime_convert()),
1996 dbesc(datetime_convert()),
1999 create_tags_from_item($item['id']);
2000 create_files_from_item($item['id']);
2001 delete_thread($item['id'], $item['parent-uri']);
2003 // clean up categories and tags so they don't end up as orphans
2006 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
2008 foreach($matches as $mtch) {
2009 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
2015 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
2017 foreach($matches as $mtch) {
2018 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
2022 // If item is a link to a photo resource, nuke all the associated photos
2023 // (visitors will not have photo resources)
2024 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
2025 // generate a resource-id and therefore aren't intimately linked to the item.
2027 if (strlen($item['resource-id'])) {
2028 q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
2029 dbesc($item['resource-id']),
2030 intval($item['uid'])
2032 // ignore the result
2035 // If item is a link to an event, nuke the event record.
2037 if (intval($item['event-id'])) {
2038 q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
2039 intval($item['event-id']),
2040 intval($item['uid'])
2042 // ignore the result
2045 // If item has attachments, drop them
2047 foreach(explode(",",$item['attach']) as $attach){
2048 preg_match("|attach/(\d+)|", $attach, $matches);
2049 q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
2050 intval($matches[1]),
2053 // ignore the result
2057 // clean up item_id and sign meta-data tables
2060 // Old code - caused very long queries and warning entries in the mysql logfiles:
2062 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
2063 intval($item['id']),
2064 intval($item['uid'])
2067 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
2068 intval($item['id']),
2069 intval($item['uid'])
2073 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
2075 // Creating list of parents
2076 $r = q("select id from item where parent = %d and uid = %d",
2077 intval($item['id']),
2078 intval($item['uid'])
2083 foreach ($r AS $row) {
2084 if ($parentid != "")
2087 $parentid .= $row["id"];
2091 if ($parentid != "") {
2092 $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
2094 $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
2097 // If it's the parent of a comment thread, kill all the kids
2099 if ($item['uri'] == $item['parent-uri']) {
2100 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
2101 WHERE `parent-uri` = '%s' AND `uid` = %d ",
2102 dbesc(datetime_convert()),
2103 dbesc(datetime_convert()),
2104 dbesc($item['parent-uri']),
2105 intval($item['uid'])
2107 create_tags_from_itemuri($item['parent-uri'], $item['uid']);
2108 create_files_from_itemuri($item['parent-uri'], $item['uid']);
2109 delete_thread_uri($item['parent-uri'], $item['uid']);
2110 // ignore the result
2112 // ensure that last-child is set in case the comment that had it just got wiped.
2113 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2114 dbesc(datetime_convert()),
2115 dbesc($item['parent-uri']),
2116 intval($item['uid'])
2118 // who is the last child now?
2119 $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",
2120 dbesc($item['parent-uri']),
2121 intval($item['uid'])
2123 if (dbm::is_result($r)) {
2124 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2130 $drop_id = intval($item['id']);
2132 // send the notification upstream/downstream as the case may be
2134 proc_run(PRIORITY_HIGH,"include/notifier.php", "drop", $drop_id);
2138 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2143 notice( t('Permission denied.') . EOL);
2144 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2151 function first_post_date($uid,$wall = false) {
2152 $r = q("select id, created from item
2153 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
2155 order by created asc limit 1",
2157 intval($wall ? 1 : 0)
2159 if (dbm::is_result($r)) {
2160 // logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2161 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
2166 /* modified posted_dates() {below} to arrange the list in years */
2167 function list_post_dates($uid, $wall) {
2168 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2170 $dthen = first_post_date($uid, $wall);
2174 // Set the start and end date to the beginning of the month
2175 $dnow = substr($dnow,0,8).'01';
2176 $dthen = substr($dthen,0,8).'01';
2180 // Starting with the current month, get the first and last days of every
2181 // month down to and including the month of the first post
2182 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2183 $dyear = intval(substr($dnow,0,4));
2184 $dstart = substr($dnow,0,8) . '01';
2185 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2186 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2187 $end_month = datetime_convert('','',$dend,'Y-m-d');
2188 $str = day_translate(datetime_convert('','',$dnow,'F'));
2190 $ret[$dyear] = array();
2191 $ret[$dyear][] = array($str,$end_month,$start_month);
2192 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2197 function posted_dates($uid,$wall) {
2198 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2200 $dthen = first_post_date($uid,$wall);
2204 // Set the start and end date to the beginning of the month
2205 $dnow = substr($dnow,0,8).'01';
2206 $dthen = substr($dthen,0,8).'01';
2209 // Starting with the current month, get the first and last days of every
2210 // month down to and including the month of the first post
2211 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2212 $dstart = substr($dnow,0,8) . '01';
2213 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2214 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2215 $end_month = datetime_convert('','',$dend,'Y-m-d');
2216 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
2217 $ret[] = array($str,$end_month,$start_month);
2218 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2224 function posted_date_widget($url,$uid,$wall) {
2227 if (! feature_enabled($uid,'archives'))
2230 // For former Facebook folks that left because of "timeline"
2232 /* if ($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2235 $visible_years = get_pconfig($uid,'system','archive_visible_years');
2236 if (! $visible_years)
2239 $ret = list_post_dates($uid,$wall);
2241 if (! dbm::is_result($ret))
2244 $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2245 $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
2247 $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2248 '$title' => t('Archives'),
2249 '$size' => $visible_years,
2250 '$cutoff_year' => $cutoff_year,
2251 '$cutoff' => $cutoff,
2254 '$showmore' => t('show more')