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['guid'])) {
411 if (isset($arr['plink'])) {
412 $arr['guid'] = uri_to_guid($arr['plink'], $a->get_hostname());
413 } elseif (isset($arr['uri'])) {
414 $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'] == "") {
560 $arr['plink'] = App::get_baseurl().'/display/'.urlencode($arr['guid']);
563 if ($arr['network'] == "") {
564 $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
565 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
566 dbesc(normalise_link($arr['author-link'])),
570 if (!dbm::is_result($r))
571 $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
572 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
573 dbesc(normalise_link($arr['author-link']))
576 if (!dbm::is_result($r))
577 $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
578 intval($arr['contact-id']),
582 if (dbm::is_result($r))
583 $arr['network'] = $r[0]["network"];
585 // Fallback to friendica (why is it empty in some cases?)
586 if ($arr['network'] == "")
587 $arr['network'] = NETWORK_DFRN;
589 logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
592 // The contact-id should be set before "item_store" was called - but there seems to be some issues
593 if ($arr["contact-id"] == 0) {
594 // First we are looking for a suitable contact that matches with the author of the post
595 // This is done only for comments (See below explanation at "gcontact-id")
596 if ($arr['parent-uri'] != $arr['uri'])
597 $arr["contact-id"] = get_contact($arr['author-link'], $uid);
599 // If not present then maybe the owner was found
600 if ($arr["contact-id"] == 0)
601 $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
603 // Still missing? Then use the "self" contact of the current user
604 if ($arr["contact-id"] == 0) {
605 $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
607 $arr["contact-id"] = $r[0]["id"];
609 logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
612 if ($arr["gcontact-id"] == 0) {
613 // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
614 // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
615 // On comments the author is the better choice.
616 if ($arr['parent-uri'] === $arr['uri'])
617 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
618 "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
620 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
621 "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
624 if ($arr["author-id"] == 0)
625 $arr["author-id"] = get_contact($arr["author-link"], 0);
627 if ($arr["owner-id"] == 0)
628 $arr["owner-id"] = get_contact($arr["owner-link"], 0);
630 if ($arr['guid'] != "") {
631 // Checking if there is already an item with the same guid
632 logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
633 $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
634 dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
636 if (dbm::is_result($r)) {
637 logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
642 // Check for hashtags in the body and repair or add hashtag links
643 item_body_set_hashtags($arr);
645 $arr['thr-parent'] = $arr['parent-uri'];
646 if ($arr['parent-uri'] === $arr['uri']) {
649 $allow_cid = $arr['allow_cid'];
650 $allow_gid = $arr['allow_gid'];
651 $deny_cid = $arr['deny_cid'];
652 $deny_gid = $arr['deny_gid'];
653 $notify_type = 'wall-new';
656 // find the parent and snarf the item id and ACLs
657 // and anything else we need to inherit
659 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
660 dbesc($arr['parent-uri']),
664 if (dbm::is_result($r)) {
666 // is the new message multi-level threaded?
667 // even though we don't support it now, preserve the info
668 // and re-attach to the conversation parent.
670 if ($r[0]['uri'] != $r[0]['parent-uri']) {
671 $arr['parent-uri'] = $r[0]['parent-uri'];
672 $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
673 ORDER BY `id` ASC LIMIT 1",
674 dbesc($r[0]['parent-uri']),
675 dbesc($r[0]['parent-uri']),
682 $parent_id = $r[0]['id'];
683 $parent_deleted = $r[0]['deleted'];
684 $allow_cid = $r[0]['allow_cid'];
685 $allow_gid = $r[0]['allow_gid'];
686 $deny_cid = $r[0]['deny_cid'];
687 $deny_gid = $r[0]['deny_gid'];
688 $arr['wall'] = $r[0]['wall'];
689 $notify_type = 'comment-new';
691 // if the parent is private, force privacy for the entire conversation
692 // This differs from the above settings as it subtly allows comments from
693 // email correspondents to be private even if the overall thread is not.
695 if ($r[0]['private'])
696 $arr['private'] = $r[0]['private'];
698 // Edge case. We host a public forum that was originally posted to privately.
699 // The original author commented, but as this is a comment, the permissions
700 // weren't fixed up so it will still show the comment as private unless we fix it here.
702 if ((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
706 // If its a post from myself then tag the thread as "mention"
707 logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
708 $u = q("SELECT `nickname` FROM `user` WHERE `uid` = %d", intval($arr['uid']));
709 if (dbm::is_result($u)) {
711 $self = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
712 logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
713 if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
714 q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
715 logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
720 // Allow one to see reply tweets from status.net even when
721 // we don't have or can't see the original post.
724 logger('item_store: $force_parent=true, reply converted to top-level post.');
726 $arr['parent-uri'] = $arr['uri'];
729 logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
737 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
739 dbesc($arr['network']),
743 if (dbm::is_result($r)) {
744 logger('duplicated item with the same uri found. '.print_r($arr,true));
748 // On Friendica and Diaspora the GUID is unique
749 if (in_array($arr['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) {
750 $r = q("SELECT `id` FROM `item` WHERE `guid` = '%s' AND `uid` = %d LIMIT 1",
754 if (dbm::is_result($r)) {
755 logger('duplicated item with the same guid found. '.print_r($arr,true));
759 // Check for an existing post with the same content. There seems to be a problem with OStatus.
760 $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
762 dbesc($arr['network']),
763 dbesc($arr['created']),
764 intval($arr['contact-id']),
767 if (dbm::is_result($r)) {
768 logger('duplicated item with the same body found. '.print_r($arr,true));
773 // Is this item available in the global items (with uid=0)?
774 if ($arr["uid"] == 0) {
775 $arr["global"] = true;
777 // Set the global flag on all items if this was a global item entry
778 q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
780 $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
782 $arr["global"] = (count($isglobal) > 0);
786 if (strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
789 $private = $arr['private'];
791 $arr["allow_cid"] = $allow_cid;
792 $arr["allow_gid"] = $allow_gid;
793 $arr["deny_cid"] = $deny_cid;
794 $arr["deny_gid"] = $deny_gid;
795 $arr["private"] = $private;
796 $arr["deleted"] = $parent_deleted;
798 // Fill the cache field
799 put_item_in_cache($arr);
802 call_hooks('post_local',$arr);
804 call_hooks('post_remote',$arr);
806 if (x($arr,'cancel')) {
807 logger('item_store: post cancelled by plugin.');
811 // Check for already added items.
812 // There is a timing issue here that sometimes creates double postings.
813 // An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this.
814 if ($arr["uid"] == 0) {
815 $r = qu("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = 0 LIMIT 1", dbesc(trim($arr['uri'])));
816 if (dbm::is_result($r)) {
817 logger('Global item already stored. URI: '.$arr['uri'].' on network '.$arr['network'], LOGGER_DEBUG);
822 // Store the unescaped version
825 dbm::esc_array($arr, true);
827 logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
830 q("START TRANSACTION;");
832 $r = dbq("INSERT INTO `item` (`"
833 . implode("`, `", array_keys($arr))
835 . implode(", ", array_values($arr))
841 // When the item was successfully stored we fetch the ID of the item.
842 if (dbm::is_result($r)) {
843 $r = q("SELECT LAST_INSERT_ID() AS `item-id`");
844 if (dbm::is_result($r)) {
845 $current_post = $r[0]['item-id'];
847 // This shouldn't happen
851 // This can happen - for example - if there are locking timeouts.
854 // Store the data into a spool file so that we can try again later.
856 // At first we restore the Diaspora signature that we removed above.
857 if (isset($encoded_signature)) {
858 $arr['dsprsig'] = $encoded_signature;
861 // Now we store the data in the spool directory
862 // We use "microtime" to keep the arrival order and "mt_rand" to avoid duplicates
863 $file = 'item-'.round(microtime(true) * 10000).'-'.mt_rand().'.msg';
865 $spoolpath = get_spoolpath();
866 if ($spoolpath != "") {
867 $spool = $spoolpath.'/'.$file;
868 file_put_contents($spool, json_encode($arr));
869 logger("Item wasn't stored - Item was spooled into file ".$file, LOGGER_DEBUG);
874 if ($current_post == 0) {
875 // This is one of these error messages that never should occur.
876 logger("couldn't find created item - we better quit now.");
881 // How much entries have we created?
882 // We wouldn't need this query when we could use an unique index - but MySQL has length problems with them.
883 $r = q("SELECT COUNT(*) AS `entries` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s'",
886 dbesc($arr['network'])
889 if (!dbm::is_result($r)) {
890 // This shouldn't happen, since COUNT always works when the database connection is there.
891 logger("We couldn't count the stored entries. Very strange ...");
896 if ($r[0]["entries"] > 1) {
897 // There are duplicates. We delete our just created entry.
898 logger('Duplicated post occurred. uri = '.$arr['uri'].' uid = '.$arr['uid']);
900 // Yes, we could do a rollback here - but we are having many users with MyISAM.
901 q("DELETE FROM `item` WHERE `id` = %d", intval($current_post));
904 } elseif ($r[0]["entries"] == 0) {
905 // This really should never happen since we quit earlier if there were problems.
906 logger("Something is terribly wrong. We haven't found our created entry.");
911 logger('item_store: created item '.$current_post);
912 item_set_last_item($arr);
914 if (!$parent_id || ($arr['parent-uri'] === $arr['uri']))
915 $parent_id = $current_post;
918 $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
920 intval($current_post)
923 $arr['id'] = $current_post;
924 $arr['parent'] = $parent_id;
926 // update the commented timestamp on the parent
927 // Only update "commented" if it is really a comment
928 if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
929 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
930 dbesc(datetime_convert()),
931 dbesc(datetime_convert()),
935 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
936 dbesc(datetime_convert()),
942 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
943 // We can check for this condition when we decode and encode the stuff again.
944 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
945 $dsprsig->signature = base64_decode($dsprsig->signature);
946 logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
949 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
950 intval($current_post),
951 dbesc($dsprsig->signed_text),
952 dbesc($dsprsig->signature),
953 dbesc($dsprsig->signer)
957 $deleted = tag_deliver($arr['uid'],$current_post);
959 // current post can be deleted if is for a community page and no mention are
961 if (!$deleted AND !$dontcache) {
963 $r = q('SELECT * FROM `item` WHERE `id` = %d', intval($current_post));
964 if ((dbm::is_result($r)) && (count($r) == 1)) {
966 call_hooks('post_local_end', $r[0]);
968 call_hooks('post_remote_end', $r[0]);
971 logger('item_store: new item not found in DB, id ' . $current_post);
975 if ($arr['parent-uri'] === $arr['uri']) {
976 add_thread($current_post);
978 update_thread($parent_id);
983 // Due to deadlock issues with the "term" table we are doing these steps after the commit.
984 // This is not perfect - but a workable solution until we found the reason for the problem.
985 create_tags_from_item($current_post);
986 create_files_from_item($current_post);
988 // If this is now the last-child, force all _other_ children of this parent to *not* be last-child
989 // It is done after the transaction to avoid dead locks.
990 if ($arr['last-child']) {
991 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
994 intval($current_post)
998 if ($arr['parent-uri'] === $arr['uri']) {
999 add_shadow_thread($current_post);
1001 add_shadow_entry($current_post);
1004 check_item_notification($current_post, $uid);
1007 proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
1010 return $current_post;
1014 * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
1016 * This can be used to filter for inactive contacts.
1017 * Only do this for public postings to avoid privacy problems, since poco data is public.
1018 * Don't set this value if it isn't from the owner (could be an author that we don't know)
1020 * @param array $arr Contains the just posted item record
1022 function item_set_last_item($arr) {
1024 $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
1026 // Is it a forum? Then we don't care about the rules from above
1027 if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
1028 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
1029 intval($arr['contact-id']));
1036 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1037 dbesc($arr['received']),
1038 dbesc($arr['received']),
1039 intval($arr['contact-id'])
1042 // Now do the same for the system wide contacts with uid=0
1043 if (!$arr['private']) {
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['owner-id'])
1050 if ($arr['owner-id'] != $arr['author-id']) {
1051 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1052 dbesc($arr['received']),
1053 dbesc($arr['received']),
1054 intval($arr['author-id'])
1060 function item_body_set_hashtags(&$item) {
1062 $tags = get_tags($item["body"]);
1068 // This sorting is important when there are hashtags that are part of other hashtags
1069 // Otherwise there could be problems with hashtags like #test and #test2
1074 $URLSearchString = "^\[\]";
1076 // All hashtags should point to the home server
1077 //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1078 // "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
1080 //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1081 // "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
1083 // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
1084 $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1086 return("[url=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/url]");
1089 $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
1091 return("[bookmark=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/bookmark]");
1094 $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
1096 return("[attachment ".str_replace("#", "#", $match[1])."]".$match[2]."[/attachment]");
1099 // Repair recursive urls
1100 $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1101 "#$2", $item["body"]);
1104 foreach($tags as $tag) {
1105 if (strpos($tag,'#') !== 0)
1108 if (strpos($tag,'[url='))
1111 $basetag = str_replace('_',' ',substr($tag,1));
1113 $newtag = '#[url='.App::get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1115 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1117 if (!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
1118 if (strlen($item["tag"]))
1119 $item["tag"] = ','.$item["tag"];
1120 $item["tag"] = $newtag.$item["tag"];
1124 // Convert back the masked hashtags
1125 $item["body"] = str_replace("#", "#", $item["body"]);
1128 function get_item_guid($id) {
1129 $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1130 if (dbm::is_result($r))
1131 return($r[0]["guid"]);
1136 function get_item_id($guid, $uid = 0) {
1142 $uid == local_user();
1144 // Does the given user have this item?
1146 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1147 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1148 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1149 if (dbm::is_result($r)) {
1151 $nick = $r[0]["nickname"];
1155 // Or is it anywhere on the server?
1157 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1158 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1159 AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = ''
1160 AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
1161 AND `item`.`private` = 0 AND `item`.`wall` = 1
1162 AND `item`.`guid` = '%s'", dbesc($guid));
1163 if (dbm::is_result($r)) {
1165 $nick = $r[0]["nickname"];
1168 return(array("nick" => $nick, "id" => $id));
1172 function get_item_contact($item,$contacts) {
1173 if (! count($contacts) || (! is_array($item)))
1175 foreach($contacts as $contact) {
1176 if ($contact['id'] == $item['contact-id']) {
1178 break; // NOTREACHED
1185 * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1187 * @param int $item_id
1188 * @return bool true if item was deleted, else false
1190 function tag_deliver($uid,$item_id) {
1198 $u = q("select * from user where uid = %d limit 1",
1202 if (! dbm::is_result($u)) {
1206 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1207 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1210 $i = q("SELECT * FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1214 if (! dbm::is_result($i)) {
1220 $link = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
1222 // Diaspora uses their own hardwired link URL in @-tags
1223 // instead of the one we supply with webfinger
1225 $dlink = normalise_link(App::get_baseurl() . '/u/' . $u[0]['nickname']);
1227 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1229 foreach($matches as $mtch) {
1230 if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1232 logger('tag_deliver: mention found: ' . $mtch[2]);
1238 if ( ($community_page || $prvgroup) &&
1239 (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1240 // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1242 logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1243 q("DELETE FROM item WHERE id = %d and uid = %d",
1252 $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1254 call_hooks('tagged', $arr);
1256 if ((! $community_page) && (! $prvgroup))
1260 // tgroup delivery - setup a second delivery chain
1261 // prevent delivery looping - only proceed
1262 // if the message originated elsewhere and is a top-level post
1264 if (($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1267 // now change this copy of the post to a forum head message and deliver to all the tgroup members
1270 $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1271 intval($u[0]['uid'])
1273 if (! dbm::is_result($c)) {
1277 // also reset all the privacy bits to the forum default permissions
1279 $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1281 $forum_mode = (($prvgroup) ? 2 : 1);
1283 q("UPDATE `item` SET `wall` = 1, `origin` = 1, `forum_mode` = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1284 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' WHERE `id` = %d",
1285 intval($forum_mode),
1286 dbesc($c[0]['name']),
1287 dbesc($c[0]['url']),
1288 dbesc($c[0]['thumb']),
1290 dbesc($u[0]['allow_cid']),
1291 dbesc($u[0]['allow_gid']),
1292 dbesc($u[0]['deny_cid']),
1293 dbesc($u[0]['deny_gid']),
1296 update_thread($item_id);
1298 proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
1304 function tgroup_check($uid,$item) {
1308 // check that the message originated elsewhere and is a top-level post
1310 if (($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1313 /// @TODO Encapsulate this or find it encapsulated and replace all occurrances
1314 $u = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1317 if (! dbm::is_result($u)) {
1321 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1322 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1325 $link = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
1327 // Diaspora uses their own hardwired link URL in @-tags
1328 // instead of the one we supply with webfinger
1330 $dlink = normalise_link(App::get_baseurl() . '/u/' . $u[0]['nickname']);
1332 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1334 foreach ($matches as $mtch) {
1335 if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1337 logger('tgroup_check: mention found: ' . $mtch[2]);
1346 /// @TODO Combines both return statements into one
1347 return (($community_page) || ($prvgroup));
1351 This function returns true if $update has an edited timestamp newer
1352 than $existing, i.e. $update contains new data which should override
1353 what's already there. If there is no timestamp yet, the update is
1354 assumed to be newer. If the update has no timestamp, the existing
1355 item is assumed to be up-to-date. If the timestamps are equal it
1356 assumes the update has been seen before and should be ignored.
1358 function edited_timestamp_is_newer($existing, $update) {
1359 if (!x($existing,'edited') || !$existing['edited']) {
1362 if (!x($update,'edited') || !$update['edited']) {
1366 $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1367 $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1368 return (strcmp($existing_edited, $update_edited) < 0);
1373 * consume_feed - process atom feed and update anything/everything we might need to update
1375 * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1377 * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1378 * It is this person's stuff that is going to be updated.
1379 * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1380 * from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1381 * have a contact record.
1382 * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1383 * might not) try and subscribe to it.
1384 * $datedir sorts in reverse order
1385 * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1386 * imported prior to its children being seen in the stream unless we are certain
1387 * of how the feed is arranged/ordered.
1388 * With $pass = 1, we only pull parent items out of the stream.
1389 * With $pass = 2, we only pull children (comments/likes).
1391 * So running this twice, first with pass 1 and then with pass 2 will do the right
1392 * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1393 * model where comments can have sub-threads. That would require some massive sorting
1394 * to get all the feed items into a mostly linear ordering, and might still require
1398 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1399 if ($contact['network'] === NETWORK_OSTATUS) {
1401 // Test - remove before flight
1402 //$tempfile = tempnam(get_temppath(), "ostatus2");
1403 //file_put_contents($tempfile, $xml);
1404 logger("Consume OStatus messages ", LOGGER_DEBUG);
1405 ostatus::import($xml,$importer,$contact, $hub);
1410 if ($contact['network'] === NETWORK_FEED) {
1412 logger("Consume feeds", LOGGER_DEBUG);
1413 feed_import($xml,$importer,$contact, $hub);
1418 if ($contact['network'] === NETWORK_DFRN) {
1419 logger("Consume DFRN messages", LOGGER_DEBUG);
1421 $r = q("SELECT `contact`.*, `contact`.`uid` AS `importer_uid`,
1422 `contact`.`pubkey` AS `cpubkey`,
1423 `contact`.`prvkey` AS `cprvkey`,
1424 `contact`.`thumb` AS `thumb`,
1425 `contact`.`url` as `url`,
1426 `contact`.`name` as `senderName`,
1429 LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1430 WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1431 dbesc($contact["id"]), dbesc($importer["uid"])
1434 logger("Now import the DFRN feed");
1435 dfrn::import($xml,$r[0], true);
1441 function item_is_remote_self($contact, &$datarray) {
1444 if (!$contact['remote_self'])
1447 // Prevent the forwarding of posts that are forwarded
1448 if ($datarray["extid"] == NETWORK_DFRN)
1451 // Prevent to forward already forwarded posts
1452 if ($datarray["app"] == $a->get_hostname())
1455 // Only forward posts
1456 if ($datarray["verb"] != ACTIVITY_POST)
1459 if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1462 $datarray2 = $datarray;
1463 logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1464 if ($contact['remote_self'] == 2) {
1465 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1466 intval($contact['uid']));
1467 if (dbm::is_result($r)) {
1468 $datarray['contact-id'] = $r[0]["id"];
1470 $datarray['owner-name'] = $r[0]["name"];
1471 $datarray['owner-link'] = $r[0]["url"];
1472 $datarray['owner-avatar'] = $r[0]["thumb"];
1474 $datarray['author-name'] = $datarray['owner-name'];
1475 $datarray['author-link'] = $datarray['owner-link'];
1476 $datarray['author-avatar'] = $datarray['owner-avatar'];
1479 if ($contact['network'] != NETWORK_FEED) {
1480 $datarray["guid"] = get_guid(32);
1481 unset($datarray["plink"]);
1482 $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
1483 $datarray["parent-uri"] = $datarray["uri"];
1484 $datarray["extid"] = $contact['network'];
1485 $urlpart = parse_url($datarray2['author-link']);
1486 $datarray["app"] = $urlpart["host"];
1488 $datarray['private'] = 0;
1491 if ($contact['network'] != NETWORK_FEED) {
1492 // Store the original post
1493 $r = item_store($datarray2, false, false);
1494 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1496 $datarray["app"] = "Feed";
1501 function new_follower($importer, $contact, $datarray, $item, $sharing = false) {
1502 $url = notags(trim($datarray['author-link']));
1503 $name = notags(trim($datarray['author-name']));
1504 $photo = notags(trim($datarray['author-avatar']));
1506 if (is_object($item)) {
1507 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1508 if ($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data']) {
1509 $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1515 if (is_array($contact)) {
1516 if (($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1517 || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1518 $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1519 intval(CONTACT_IS_FRIEND),
1520 intval($contact['id']),
1521 intval($importer['uid'])
1524 // send email notification to owner?
1527 // create contact record
1529 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1530 `blocked`, `readonly`, `pending`, `writable`)
1531 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1532 intval($importer['uid']),
1533 dbesc(datetime_convert()),
1535 dbesc(normalise_link($url)),
1539 dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1540 intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1542 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1543 intval($importer['uid']),
1546 if (dbm::is_result($r)) {
1547 $contact_record = $r[0];
1548 update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
1551 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1552 intval($importer['uid'])
1555 if (dbm::is_result($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1557 // create notification
1558 $hash = random_string();
1560 if (is_array($contact_record)) {
1561 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1562 VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1563 intval($importer['uid']),
1564 intval($contact_record['id']),
1566 dbesc(datetime_convert())
1570 $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1572 if (intval($def_gid)) {
1573 group_add_member($importer['uid'], '', $contact_record['id'], $def_gid);
1576 if (($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1577 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1580 'type' => NOTIFY_INTRO,
1581 'notify_flags' => $r[0]['notify-flags'],
1582 'language' => $r[0]['language'],
1583 'to_name' => $r[0]['username'],
1584 'to_email' => $r[0]['email'],
1585 'uid' => $r[0]['uid'],
1586 'link' => App::get_baseurl() . '/notifications/intro',
1587 'source_name' => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1588 'source_link' => $contact_record['url'],
1589 'source_photo' => $contact_record['photo'],
1590 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1595 } elseif (dbm::is_result($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1596 $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1597 intval($importer['uid']),
1605 function lose_follower($importer, $contact, array $datarray = array(), $item = "") {
1607 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1608 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1609 intval(CONTACT_IS_SHARING),
1610 intval($contact['id'])
1613 contact_remove($contact['id']);
1617 function lose_sharer($importer, $contact, array $datarray = array(), $item = "") {
1619 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1620 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1621 intval(CONTACT_IS_FOLLOWER),
1622 intval($contact['id'])
1625 contact_remove($contact['id']);
1629 function subscribe_to_hub($url, $importer, $contact, $hubmode = 'subscribe') {
1633 if (is_array($importer)) {
1634 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1635 intval($importer['uid'])
1639 // Diaspora has different message-ids in feeds than they do
1640 // through the direct Diaspora protocol. If we try and use
1641 // the feed, we'll get duplicates. So don't.
1643 if ((! dbm::is_result($r)) || $contact['network'] === NETWORK_DIASPORA)
1646 $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1648 // Use a single verify token, even if multiple hubs
1650 $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1652 $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1654 logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: ' . $push_url . ' with verifier ' . $verify_token);
1656 if (!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1657 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1658 dbesc($verify_token),
1659 intval($contact['id'])
1663 post_url($url,$params);
1665 logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1671 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1673 if (get_config('system','disable_embedded'))
1678 logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1679 $site = substr(App::get_baseurl(),strpos(App::get_baseurl(),'://'));
1684 $img_start = strpos($orig_body, '[img');
1685 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1686 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1687 while( ($img_st_close !== false) && ($img_len !== false) ) {
1689 $img_st_close++; // make it point to AFTER the closing bracket
1690 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1692 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1695 if (stristr($image , $site . '/photo/')) {
1696 // Only embed locally hosted photos
1698 $i = basename($image);
1699 $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
1700 $x = strpos($i,'-');
1703 $res = substr($i,$x+1);
1704 $i = substr($i,0,$x);
1705 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1712 // Check to see if we should replace this photo link with an embedded image
1713 // 1. No need to do so if the photo is public
1714 // 2. If there's a contact-id provided, see if they're in the access list
1715 // for the photo. If so, embed it.
1716 // 3. Otherwise, if we have an item, see if the item permissions match the photo
1717 // permissions, regardless of order but first check to see if they're an exact
1718 // match to save some processing overhead.
1720 if (has_permissions($r[0])) {
1722 $recips = enumerate_permissions($r[0]);
1723 if (in_array($cid, $recips)) {
1727 if (compare_permissions($item,$r[0]))
1732 $data = $r[0]['data'];
1733 $type = $r[0]['type'];
1735 // If a custom width and height were specified, apply before embedding
1736 if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1737 logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1739 $width = intval($match[1]);
1740 $height = intval($match[2]);
1742 $ph = new Photo($data, $type);
1743 if ($ph->is_valid()) {
1744 $ph->scaleImage(max($width, $height));
1745 $data = $ph->imageString();
1746 $type = $ph->getType();
1750 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1751 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1752 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1758 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1759 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1760 if ($orig_body === false)
1763 $img_start = strpos($orig_body, '[img');
1764 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1765 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1768 $new_body = $new_body . $orig_body;
1773 function has_permissions($obj) {
1774 if (($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1779 function compare_permissions($obj1,$obj2) {
1780 // first part is easy. Check that these are exactly the same.
1781 if (($obj1['allow_cid'] == $obj2['allow_cid'])
1782 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1783 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1784 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1787 // This is harder. Parse all the permissions and compare the resulting set.
1789 $recipients1 = enumerate_permissions($obj1);
1790 $recipients2 = enumerate_permissions($obj2);
1793 if ($recipients1 == $recipients2)
1798 // returns an array of contact-ids that are allowed to see this object
1800 function enumerate_permissions($obj) {
1801 $allow_people = expand_acl($obj['allow_cid']);
1802 $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1803 $deny_people = expand_acl($obj['deny_cid']);
1804 $deny_groups = expand_groups(expand_acl($obj['deny_gid']));
1805 $recipients = array_unique(array_merge($allow_people,$allow_groups));
1806 $deny = array_unique(array_merge($deny_people,$deny_groups));
1807 $recipients = array_diff($recipients,$deny);
1811 function item_getfeedtags($item) {
1814 $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1816 for($x = 0; $x < $cnt; $x ++) {
1817 if ($matches[1][$x])
1818 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
1822 $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1824 for($x = 0; $x < $cnt; $x ++) {
1825 if ($matches[1][$x])
1826 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
1832 function item_expire($uid, $days, $network = "", $force = false) {
1834 if ((! $uid) || ($days < 1))
1837 // $expire_network_only = save your own wall posts
1838 // and just expire conversations started by others
1840 $expire_network_only = get_pconfig($uid,'expire','network_only');
1841 $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1843 if ($network != "") {
1844 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1845 // There is an index "uid_network_received" but not "uid_network_created"
1846 // This avoids the creation of another index just for one purpose.
1847 // And it doesn't really matter wether to look at "received" or "created"
1848 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1850 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1852 $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
1853 WHERE `uid` = %d $range
1861 if (! dbm::is_result($r))
1864 $expire_items = get_pconfig($uid, 'expire','items');
1865 $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1867 // Forcing expiring of items - but not notes and marked items
1869 $expire_items = true;
1871 $expire_notes = get_pconfig($uid, 'expire','notes');
1872 $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1874 $expire_starred = get_pconfig($uid, 'expire','starred');
1875 $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1877 $expire_photos = get_pconfig($uid, 'expire','photos');
1878 $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1880 logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1882 foreach($r as $item) {
1884 // don't expire filed items
1886 if (strpos($item['file'],'[') !== false)
1889 // Only expire posts, not photos and photo comments
1891 if ($expire_photos==0 && strlen($item['resource-id']))
1893 if ($expire_starred==0 && intval($item['starred']))
1895 if ($expire_notes==0 && $item['type']=='note')
1897 if ($expire_items==0 && $item['type']!='note')
1900 drop_item($item['id'],false);
1903 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1908 function drop_items($items) {
1911 if (! local_user() && ! remote_user())
1914 if (count($items)) {
1915 foreach($items as $item) {
1916 $owner = drop_item($item,false);
1917 if ($owner && ! $uid)
1922 // multiple threads may have been deleted, send an expire notification
1925 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1929 function drop_item($id,$interactive = true) {
1933 // locate item to be deleted
1935 $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1939 if (! dbm::is_result($r)) {
1942 notice( t('Item not found.') . EOL);
1943 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
1948 $owner = $item['uid'];
1952 // check if logged in user is either the author or owner of this item
1954 if (is_array($_SESSION['remote'])) {
1955 foreach($_SESSION['remote'] as $visitor) {
1956 if ($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1957 $contact_id = $visitor['cid'];
1964 if ((local_user() == $item['uid']) || ($contact_id) || (! $interactive)) {
1966 // Check if we should do HTML-based delete confirmation
1967 if ($_REQUEST['confirm']) {
1968 // <form> can't take arguments in its "action" parameter
1969 // so add any arguments as hidden inputs
1970 $query = explode_querystring($a->query_string);
1972 foreach($query['args'] as $arg) {
1973 if (strpos($arg, 'confirm=') === false) {
1974 $arg_parts = explode('=', $arg);
1975 $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1979 return replace_macros(get_markup_template('confirm.tpl'), array(
1981 '$message' => t('Do you really want to delete this item?'),
1982 '$extra_inputs' => $inputs,
1983 '$confirm' => t('Yes'),
1984 '$confirm_url' => $query['base'],
1985 '$confirm_name' => 'confirmed',
1986 '$cancel' => t('Cancel'),
1989 // Now check how the user responded to the confirmation query
1990 if ($_REQUEST['canceled']) {
1991 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
1994 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
1997 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
1998 dbesc(datetime_convert()),
1999 dbesc(datetime_convert()),
2002 create_tags_from_item($item['id']);
2003 create_files_from_item($item['id']);
2004 delete_thread($item['id'], $item['parent-uri']);
2006 // clean up categories and tags so they don't end up as orphans
2009 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
2011 foreach($matches as $mtch) {
2012 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
2018 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
2020 foreach($matches as $mtch) {
2021 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
2025 // If item is a link to a photo resource, nuke all the associated photos
2026 // (visitors will not have photo resources)
2027 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
2028 // generate a resource-id and therefore aren't intimately linked to the item.
2030 if (strlen($item['resource-id'])) {
2031 q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
2032 dbesc($item['resource-id']),
2033 intval($item['uid'])
2035 // ignore the result
2038 // If item is a link to an event, nuke the event record.
2040 if (intval($item['event-id'])) {
2041 q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
2042 intval($item['event-id']),
2043 intval($item['uid'])
2045 // ignore the result
2048 // If item has attachments, drop them
2050 foreach(explode(",",$item['attach']) as $attach){
2051 preg_match("|attach/(\d+)|", $attach, $matches);
2052 q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
2053 intval($matches[1]),
2056 // ignore the result
2060 // clean up item_id and sign meta-data tables
2063 // Old code - caused very long queries and warning entries in the mysql logfiles:
2065 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
2066 intval($item['id']),
2067 intval($item['uid'])
2070 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
2071 intval($item['id']),
2072 intval($item['uid'])
2076 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
2078 // Creating list of parents
2079 $r = q("select id from item where parent = %d and uid = %d",
2080 intval($item['id']),
2081 intval($item['uid'])
2086 foreach ($r AS $row) {
2087 if ($parentid != "")
2090 $parentid .= $row["id"];
2094 if ($parentid != "") {
2095 $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
2097 $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
2100 // If it's the parent of a comment thread, kill all the kids
2102 if ($item['uri'] == $item['parent-uri']) {
2103 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
2104 WHERE `parent-uri` = '%s' AND `uid` = %d ",
2105 dbesc(datetime_convert()),
2106 dbesc(datetime_convert()),
2107 dbesc($item['parent-uri']),
2108 intval($item['uid'])
2110 create_tags_from_itemuri($item['parent-uri'], $item['uid']);
2111 create_files_from_itemuri($item['parent-uri'], $item['uid']);
2112 delete_thread_uri($item['parent-uri'], $item['uid']);
2113 // ignore the result
2115 // ensure that last-child is set in case the comment that had it just got wiped.
2116 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2117 dbesc(datetime_convert()),
2118 dbesc($item['parent-uri']),
2119 intval($item['uid'])
2121 // who is the last child now?
2122 $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",
2123 dbesc($item['parent-uri']),
2124 intval($item['uid'])
2126 if (dbm::is_result($r)) {
2127 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2133 $drop_id = intval($item['id']);
2135 // send the notification upstream/downstream as the case may be
2137 proc_run(PRIORITY_HIGH,"include/notifier.php", "drop", $drop_id);
2141 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2146 notice( t('Permission denied.') . EOL);
2147 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2154 function first_post_date($uid,$wall = false) {
2155 $r = q("select id, created from item
2156 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
2158 order by created asc limit 1",
2160 intval($wall ? 1 : 0)
2162 if (dbm::is_result($r)) {
2163 // logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2164 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
2169 /* modified posted_dates() {below} to arrange the list in years */
2170 function list_post_dates($uid, $wall) {
2171 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2173 $dthen = first_post_date($uid, $wall);
2177 // Set the start and end date to the beginning of the month
2178 $dnow = substr($dnow,0,8).'01';
2179 $dthen = substr($dthen,0,8).'01';
2183 // Starting with the current month, get the first and last days of every
2184 // month down to and including the month of the first post
2185 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2186 $dyear = intval(substr($dnow,0,4));
2187 $dstart = substr($dnow,0,8) . '01';
2188 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2189 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2190 $end_month = datetime_convert('','',$dend,'Y-m-d');
2191 $str = day_translate(datetime_convert('','',$dnow,'F'));
2193 $ret[$dyear] = array();
2194 $ret[$dyear][] = array($str,$end_month,$start_month);
2195 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2200 function posted_dates($uid,$wall) {
2201 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2203 $dthen = first_post_date($uid,$wall);
2207 // Set the start and end date to the beginning of the month
2208 $dnow = substr($dnow,0,8).'01';
2209 $dthen = substr($dthen,0,8).'01';
2212 // Starting with the current month, get the first and last days of every
2213 // month down to and including the month of the first post
2214 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2215 $dstart = substr($dnow,0,8) . '01';
2216 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2217 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2218 $end_month = datetime_convert('','',$dend,'Y-m-d');
2219 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
2220 $ret[] = array($str,$end_month,$start_month);
2221 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2227 function posted_date_widget($url,$uid,$wall) {
2230 if (! feature_enabled($uid,'archives'))
2233 // For former Facebook folks that left because of "timeline"
2235 /* if ($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2238 $visible_years = get_pconfig($uid,'system','archive_visible_years');
2239 if (! $visible_years)
2242 $ret = list_post_dates($uid,$wall);
2244 if (! dbm::is_result($ret))
2247 $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2248 $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
2250 $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2251 '$title' => t('Archives'),
2252 '$size' => $visible_years,
2253 '$cutoff_year' => $cutoff_year,
2254 '$cutoff' => $cutoff,
2257 '$showmore' => t('show more')