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 /// @TODO one day with composer autoloader no more needed
30 require_once 'library/defuse/php-encryption-1.2.1/Crypto.php';
32 function construct_verb($item) {
41 * The purpose of this function is to apply system message length limits to
42 * imported messages without including any embedded photos in the length
44 if (! function_exists('limit_body_size')) {
45 function limit_body_size($body) {
47 // logger('limit_body_size: start', LOGGER_DEBUG);
49 $maxlen = get_max_import_size();
51 // If the length of the body, including the embedded images, is smaller
52 // than the maximum, then don't waste time looking for the images
53 if ($maxlen && (strlen($body) > $maxlen)) {
55 logger('limit_body_size: the total body length exceeds the limit', LOGGER_DEBUG);
62 $img_start = strpos($orig_body, '[img');
63 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
64 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
65 while (($img_st_close !== false) && ($img_end !== false)) {
67 $img_st_close++; // make it point to AFTER the closing bracket
68 $img_end += $img_start;
69 $img_end += strlen('[/img]');
71 if (! strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
72 // This is an embedded image
74 if (($textlen + $img_start) > $maxlen ) {
75 if ($textlen < $maxlen) {
76 logger('limit_body_size: the limit happens before an embedded image', LOGGER_DEBUG);
77 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
81 $new_body = $new_body . substr($orig_body, 0, $img_start);
82 $textlen += $img_start;
85 $new_body = $new_body . substr($orig_body, $img_start, $img_end - $img_start);
88 if (($textlen + $img_end) > $maxlen ) {
89 if ($textlen < $maxlen) {
90 logger('limit_body_size: the limit happens before the end of a non-embedded image', LOGGER_DEBUG);
91 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
95 $new_body = $new_body . substr($orig_body, 0, $img_end);
99 $orig_body = substr($orig_body, $img_end);
101 if ($orig_body === false) {
102 // in case the body ends on a closing image tag
106 $img_start = strpos($orig_body, '[img');
107 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
108 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
111 if (($textlen + strlen($orig_body)) > $maxlen) {
112 if ($textlen < $maxlen) {
113 logger('limit_body_size: the limit happens after the end of the last image', LOGGER_DEBUG);
114 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
118 logger('limit_body_size: the text size with embedded images extracted did not violate the limit', LOGGER_DEBUG);
119 $new_body = $new_body . $orig_body;
120 $textlen += strlen($orig_body);
129 function title_is_body($title, $body) {
131 $title = strip_tags($title);
132 $title = trim($title);
133 $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
134 $title = str_replace(array("\n", "\r", "\t", " "), array("", "", "", ""), $title);
136 $body = strip_tags($body);
138 $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
139 $body = str_replace(array("\n", "\r", "\t", " "), array("", "", "", ""), $body);
141 if (strlen($title) < strlen($body))
142 $body = substr($body, 0, strlen($title));
144 if (($title != $body) and (substr($title, -3) == "...")) {
145 $pos = strrpos($title, "...");
147 $title = substr($title, 0, $pos);
148 $body = substr($body, 0, $pos);
152 return($title == $body);
155 function add_page_info_data($data) {
156 call_hooks('page_info_data', $data);
158 // It maybe is a rich content, but if it does have everything that a link has,
159 // then treat it that way
160 if (($data["type"] == "rich") AND is_string($data["title"]) AND
161 is_string($data["text"]) AND (sizeof($data["images"]) > 0)) {
162 $data["type"] = "link";
165 if ((($data["type"] != "link") AND ($data["type"] != "video") AND ($data["type"] != "photo")) OR ($data["title"] == $data["url"])) {
169 if ($no_photos AND ($data["type"] == "photo")) {
173 if (sizeof($data["images"]) > 0) {
174 $preview = $data["images"][0];
179 // Escape some bad characters
180 $data["url"] = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["url"], ENT_QUOTES, 'UTF-8', false));
181 $data["title"] = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["title"], ENT_QUOTES, 'UTF-8', false));
183 $text = "[attachment type='".$data["type"]."'";
185 if ($data["text"] == "") {
186 $data["text"] = $data["title"];
189 if ($data["text"] == "") {
190 $data["text"] = $data["url"];
193 if ($data["url"] != "") {
194 $text .= " url='".$data["url"]."'";
197 if ($data["title"] != "") {
198 $text .= " title='".$data["title"]."'";
201 if (sizeof($data["images"]) > 0) {
202 $preview = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["images"][0]["src"], ENT_QUOTES, 'UTF-8', false));
203 // if the preview picture is larger than 500 pixels then show it in a larger mode
204 // But only, if the picture isn't higher than large (To prevent huge posts)
205 if (($data["images"][0]["width"] >= 500) AND ($data["images"][0]["width"] >= $data["images"][0]["height"])) {
206 $text .= " image='".$preview."'";
208 $text .= " preview='".$preview."'";
212 $text .= "]".$data["text"]."[/attachment]";
215 if (isset($data["keywords"]) AND count($data["keywords"])) {
217 foreach ($data["keywords"] AS $keyword) {
218 /// @todo make a positive list of allowed characters
219 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'", "’", "`", "(", ")", "„", "“"),
220 array("", "", "", "", "", "", "", "", "", "", "", ""), $keyword);
221 $hashtags .= "#[url=".App::get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url] ";
225 return "\n".$text.$hashtags;
228 function query_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
230 $data = ParseUrl::getSiteinfoCached($url, true);
233 $data["images"][0]["src"] = $photo;
236 logger('fetch page info for '.$url.' '.print_r($data, true), LOGGER_DEBUG);
238 if (!$keywords AND isset($data["keywords"])) {
239 unset($data["keywords"]);
242 if (($keyword_blacklist != "") AND isset($data["keywords"])) {
243 $list = explode(", ", $keyword_blacklist);
244 foreach ($list AS $keyword) {
245 $keyword = trim($keyword);
246 $index = array_search($keyword, $data["keywords"]);
247 if ($index !== false) {
248 unset($data["keywords"][$index]);
256 function add_page_keywords($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
257 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
260 if (isset($data["keywords"]) AND count($data["keywords"])) {
261 foreach ($data["keywords"] AS $keyword) {
262 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'"),
263 array("", "", "", "", "", ""), $keyword);
269 $tags .= "#[url=" . App::get_baseurl() . "/search?tag=" . rawurlencode($hashtag) . "]" . $hashtag . "[/url]";
276 function add_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
277 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
279 $text = add_page_info_data($data);
284 function add_page_info_to_body($body, $texturl = false, $no_photos = false) {
286 logger('add_page_info_to_body: fetch page info for body '.$body, LOGGER_DEBUG);
288 $URLSearchString = "^\[\]";
290 // Fix for Mastodon where the mentions are in a different format
291 $body = preg_replace("/\[url\=([$URLSearchString]*)\]([#!@])(.*?)\[\/url\]/ism",
292 '$2[url=$1]$3[/url]', $body);
294 // Adding these spaces is a quick hack due to my problems with regular expressions :)
295 preg_match("/[^!#@]\[url\]([$URLSearchString]*)\[\/url\]/ism", " ".$body, $matches);
298 preg_match("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", " ".$body, $matches);
300 // Convert urls without bbcode elements
301 if (!$matches AND $texturl) {
302 preg_match("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", " ".$body, $matches);
304 // Yeah, a hack. I really hate regular expressions :)
306 $matches[1] = $matches[2];
310 $footer = add_page_info($matches[1], $no_photos);
312 // Remove the link from the body if the link is attached at the end of the post
313 if (isset($footer) AND (trim($footer) != "") AND (strpos($footer, $matches[1]))) {
314 $removedlink = trim(str_replace($matches[1], "", $body));
315 if (($removedlink == "") OR strstr($body, $removedlink))
316 $body = $removedlink;
318 $url = str_replace(array('/', '.'), array('\/', '\.'), $matches[1]);
319 $removedlink = preg_replace("/\[url\=".$url."\](.*?)\[\/url\]/ism", '', $body);
320 if (($removedlink == "") OR strstr($body, $removedlink))
321 $body = $removedlink;
324 // Add the page information to the bottom
325 if (isset($footer) AND (trim($footer) != ""))
332 * Adds a "lang" specification in a "postopts" element of given $arr,
333 * if possible and not already present.
334 * Expects "body" element to exist in $arr.
336 * @todo Add a parameter to request forcing override
338 function item_add_language_opt(&$arr) {
340 if (version_compare(PHP_VERSION, '5.3.0', '<')) return; // LanguageDetect.php not available ?
342 if (x($arr, 'postopts') )
344 if (strstr($arr['postopts'], 'lang=') )
347 /// @TODO Add parameter to request overriding
350 $postopts = $arr['postopts'];
355 require_once('library/langdet/Text/LanguageDetect.php');
356 $naked_body = preg_replace('/\[(.+?)\]/','', $arr['body']);
357 $l = new Text_LanguageDetect;
358 //$lng = $l->detectConfidence($naked_body);
359 //$arr['postopts'] = (($lng['language']) ? 'lang=' . $lng['language'] . ';' . $lng['confidence'] : '');
360 $lng = $l->detect($naked_body, 3);
362 if (sizeof($lng) > 0) {
363 if ($postopts != "") $postopts .= '&'; // arbitrary separator, to be reviewed
364 $postopts .= 'lang=';
366 foreach ($lng as $language => $score) {
367 $postopts .= $sep . $language.";".$score;
370 $arr['postopts'] = $postopts;
375 * @brief Creates an unique guid out of a given uri
377 * @param string $uri uri of an item entry
378 * @param string $host (Optional) hostname for the GUID prefix
379 * @return string unique guid
381 function uri_to_guid($uri, $host = "") {
383 // Our regular guid routine is using this kind of prefix as well
384 // We have to avoid that different routines could accidentally create the same value
385 $parsed = parse_url($uri);
388 $host = $parsed["host"];
391 $guid_prefix = hash("crc32", $host);
393 // Remove the scheme to make sure that "https" and "http" doesn't make a difference
394 unset($parsed["scheme"]);
396 $host_id = implode("/", $parsed);
398 // We could use any hash algorithm since it isn't a security issue
399 $host_hash = hash("ripemd128", $host_id);
401 return $guid_prefix.$host_hash;
404 function item_store($arr, $force_parent = false, $notify = false, $dontcache = false) {
408 // If it is a posting where users should get notifications, then define it as wall posting
411 $arr['type'] = 'wall';
413 $arr['last-child'] = 1;
414 $arr['network'] = NETWORK_DFRN;
416 // We have to avoid duplicates. So we create the GUID in form of a hash of the plink or uri.
417 // In difference to the call to "uri_to_guid" several lines below we add the hash of our own host.
418 // This is done because our host is the original creator of the post.
419 if (!isset($arr['guid'])) {
420 if (isset($arr['plink'])) {
421 $arr['guid'] = uri_to_guid($arr['plink'], $a->get_hostname());
422 } elseif (isset($arr['uri'])) {
423 $arr['guid'] = uri_to_guid($arr['uri'], $a->get_hostname());
428 // If a Diaspora signature structure was passed in, pull it out of the
429 // item array and set it aside for later storage.
432 if (x($arr,'dsprsig')) {
433 $encoded_signature = $arr['dsprsig'];
434 $dsprsig = json_decode(base64_decode($arr['dsprsig']));
435 unset($arr['dsprsig']);
438 // Converting the plink
439 if ($arr['network'] == NETWORK_OSTATUS) {
440 if (isset($arr['plink']))
441 $arr['plink'] = ostatus::convert_href($arr['plink']);
442 elseif (isset($arr['uri']))
443 $arr['plink'] = ostatus::convert_href($arr['uri']);
446 if (x($arr, 'gravity'))
447 $arr['gravity'] = intval($arr['gravity']);
448 elseif ($arr['parent-uri'] === $arr['uri'])
450 elseif (activity_match($arr['verb'],ACTIVITY_POST))
453 $arr['gravity'] = 6; // extensible catchall
455 if (! x($arr,'type'))
456 $arr['type'] = 'remote';
460 /* check for create date and expire time */
461 $uid = intval($arr['uid']);
462 $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
463 if (dbm::is_result($r)) {
464 $expire_interval = $r[0]['expire'];
465 if ($expire_interval>0) {
466 $expire_date = new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
467 $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
468 if ($created_date < $expire_date) {
469 logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
475 // Do we already have this item?
476 // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
477 if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
478 $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s') LIMIT 1",
479 dbesc(trim($arr['uri'])),
481 dbesc(NETWORK_DIASPORA),
483 dbesc(NETWORK_OSTATUS)
486 // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
488 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']);
493 // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
494 // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
495 //if ((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
496 // $arr['body'] = strip_tags($arr['body']);
498 item_add_language_opt($arr);
502 elseif ((trim($arr['guid']) == "") AND (trim($arr['plink']) != ""))
503 $arr['guid'] = uri_to_guid($arr['plink']);
504 elseif ((trim($arr['guid']) == "") AND (trim($arr['uri']) != ""))
505 $arr['guid'] = uri_to_guid($arr['uri']);
507 $parsed = parse_url($arr["author-link"]);
508 $guid_prefix = hash("crc32", $parsed["host"]);
511 $arr['wall'] = ((x($arr,'wall')) ? intval($arr['wall']) : 0);
512 $arr['guid'] = ((x($arr,'guid')) ? notags(trim($arr['guid'])) : get_guid(32, $guid_prefix));
513 $arr['uri'] = ((x($arr,'uri')) ? notags(trim($arr['uri'])) : item_new_uri($a->get_hostname(), $uid, $arr['guid']));
514 $arr['extid'] = ((x($arr,'extid')) ? notags(trim($arr['extid'])) : '');
515 $arr['author-name'] = ((x($arr,'author-name')) ? trim($arr['author-name']) : '');
516 $arr['author-link'] = ((x($arr,'author-link')) ? notags(trim($arr['author-link'])) : '');
517 $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
518 $arr['owner-name'] = ((x($arr,'owner-name')) ? trim($arr['owner-name']) : '');
519 $arr['owner-link'] = ((x($arr,'owner-link')) ? notags(trim($arr['owner-link'])) : '');
520 $arr['owner-avatar'] = ((x($arr,'owner-avatar')) ? notags(trim($arr['owner-avatar'])) : '');
521 $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC', $arr['created']) : datetime_convert());
522 $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC', $arr['edited']) : datetime_convert());
523 $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC', $arr['commented']) : datetime_convert());
524 $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC', $arr['received']) : datetime_convert());
525 $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC', $arr['changed']) : datetime_convert());
526 $arr['title'] = ((x($arr,'title')) ? trim($arr['title']) : '');
527 $arr['location'] = ((x($arr,'location')) ? trim($arr['location']) : '');
528 $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : '');
529 $arr['last-child'] = ((x($arr,'last-child')) ? intval($arr['last-child']) : 0 );
530 $arr['visible'] = ((x($arr,'visible') !== false) ? intval($arr['visible']) : 1 );
532 $arr['parent-uri'] = ((x($arr,'parent-uri')) ? notags(trim($arr['parent-uri'])) : $arr['uri']);
533 $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : '');
534 $arr['object-type'] = ((x($arr,'object-type')) ? notags(trim($arr['object-type'])) : '');
535 $arr['object'] = ((x($arr,'object')) ? trim($arr['object']) : '');
536 $arr['target-type'] = ((x($arr,'target-type')) ? notags(trim($arr['target-type'])) : '');
537 $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : '');
538 $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : '');
539 $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : '');
540 $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : '');
541 $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : '');
542 $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : '');
543 $arr['private'] = ((x($arr,'private')) ? intval($arr['private']) : 0 );
544 $arr['bookmark'] = ((x($arr,'bookmark')) ? intval($arr['bookmark']) : 0 );
545 $arr['body'] = ((x($arr,'body')) ? trim($arr['body']) : '');
546 $arr['tag'] = ((x($arr,'tag')) ? notags(trim($arr['tag'])) : '');
547 $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : '');
548 $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : '');
549 $arr['origin'] = ((x($arr,'origin')) ? intval($arr['origin']) : 0 );
550 $arr['network'] = ((x($arr,'network')) ? trim($arr['network']) : '');
551 $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : '');
552 $arr['resource-id'] = ((x($arr,'resource-id')) ? trim($arr['resource-id']) : '');
553 $arr['event-id'] = ((x($arr,'event-id')) ? intval($arr['event-id']) : 0 );
554 $arr['inform'] = ((x($arr,'inform')) ? trim($arr['inform']) : '');
555 $arr['file'] = ((x($arr,'file')) ? trim($arr['file']) : '');
557 // Items cannot be stored before they happen ...
558 if ($arr['created'] > datetime_convert())
559 $arr['created'] = datetime_convert();
561 // We haven't invented time travel by now.
562 if ($arr['edited'] > datetime_convert())
563 $arr['edited'] = datetime_convert();
565 if (($arr['author-link'] == "") AND ($arr['owner-link'] == ""))
566 logger("Both author-link and owner-link are empty. Called by: ".App::callstack(), LOGGER_DEBUG);
568 if ($arr['plink'] == "") {
569 $arr['plink'] = App::get_baseurl().'/display/'.urlencode($arr['guid']);
572 if ($arr['network'] == "") {
573 $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
574 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
575 dbesc(normalise_link($arr['author-link'])),
579 if (!dbm::is_result($r))
580 $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
581 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
582 dbesc(normalise_link($arr['author-link']))
585 if (!dbm::is_result($r))
586 $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
587 intval($arr['contact-id']),
591 if (dbm::is_result($r))
592 $arr['network'] = $r[0]["network"];
594 // Fallback to friendica (why is it empty in some cases?)
595 if ($arr['network'] == "")
596 $arr['network'] = NETWORK_DFRN;
598 logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
601 // The contact-id should be set before "item_store" was called - but there seems to be some issues
602 if ($arr["contact-id"] == 0) {
603 // First we are looking for a suitable contact that matches with the author of the post
604 // This is done only for comments (See below explanation at "gcontact-id")
605 if ($arr['parent-uri'] != $arr['uri'])
606 $arr["contact-id"] = get_contact($arr['author-link'], $uid);
608 // If not present then maybe the owner was found
609 if ($arr["contact-id"] == 0)
610 $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
612 // Still missing? Then use the "self" contact of the current user
613 if ($arr["contact-id"] == 0) {
614 $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
616 $arr["contact-id"] = $r[0]["id"];
618 logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
621 if ($arr["gcontact-id"] == 0) {
622 // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
623 // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
624 // On comments the author is the better choice.
625 if ($arr['parent-uri'] === $arr['uri'])
626 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
627 "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
629 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
630 "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
633 if ($arr["author-id"] == 0)
634 $arr["author-id"] = get_contact($arr["author-link"], 0);
636 if ($arr["owner-id"] == 0)
637 $arr["owner-id"] = get_contact($arr["owner-link"], 0);
639 if ($arr['guid'] != "") {
640 // Checking if there is already an item with the same guid
641 logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
642 $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
643 dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
645 if (dbm::is_result($r)) {
646 logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
651 // Check for hashtags in the body and repair or add hashtag links
652 item_body_set_hashtags($arr);
654 $arr['thr-parent'] = $arr['parent-uri'];
655 if ($arr['parent-uri'] === $arr['uri']) {
658 $allow_cid = $arr['allow_cid'];
659 $allow_gid = $arr['allow_gid'];
660 $deny_cid = $arr['deny_cid'];
661 $deny_gid = $arr['deny_gid'];
662 $notify_type = 'wall-new';
665 // find the parent and snarf the item id and ACLs
666 // and anything else we need to inherit
668 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
669 dbesc($arr['parent-uri']),
673 if (dbm::is_result($r)) {
675 // is the new message multi-level threaded?
676 // even though we don't support it now, preserve the info
677 // and re-attach to the conversation parent.
679 if ($r[0]['uri'] != $r[0]['parent-uri']) {
680 $arr['parent-uri'] = $r[0]['parent-uri'];
681 $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
682 ORDER BY `id` ASC LIMIT 1",
683 dbesc($r[0]['parent-uri']),
684 dbesc($r[0]['parent-uri']),
691 $parent_id = $r[0]['id'];
692 $parent_deleted = $r[0]['deleted'];
693 $allow_cid = $r[0]['allow_cid'];
694 $allow_gid = $r[0]['allow_gid'];
695 $deny_cid = $r[0]['deny_cid'];
696 $deny_gid = $r[0]['deny_gid'];
697 $arr['wall'] = $r[0]['wall'];
698 $notify_type = 'comment-new';
700 // if the parent is private, force privacy for the entire conversation
701 // This differs from the above settings as it subtly allows comments from
702 // email correspondents to be private even if the overall thread is not.
704 if ($r[0]['private'])
705 $arr['private'] = $r[0]['private'];
707 // Edge case. We host a public forum that was originally posted to privately.
708 // The original author commented, but as this is a comment, the permissions
709 // weren't fixed up so it will still show the comment as private unless we fix it here.
711 if ((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
715 // If its a post from myself then tag the thread as "mention"
716 logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
717 $u = q("SELECT `nickname` FROM `user` WHERE `uid` = %d", intval($arr['uid']));
718 if (dbm::is_result($u)) {
720 $self = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
721 logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
722 if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
723 q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
724 logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
729 // Allow one to see reply tweets from status.net even when
730 // we don't have or can't see the original post.
733 logger('item_store: $force_parent=true, reply converted to top-level post.');
735 $arr['parent-uri'] = $arr['uri'];
738 logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
746 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
748 dbesc($arr['network']),
752 if (dbm::is_result($r)) {
753 logger('duplicated item with the same uri found. '.print_r($arr,true));
757 // On Friendica and Diaspora the GUID is unique
758 if (in_array($arr['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) {
759 $r = q("SELECT `id` FROM `item` WHERE `guid` = '%s' AND `uid` = %d LIMIT 1",
763 if (dbm::is_result($r)) {
764 logger('duplicated item with the same guid found. '.print_r($arr,true));
768 // Check for an existing post with the same content. There seems to be a problem with OStatus.
769 $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
771 dbesc($arr['network']),
772 dbesc($arr['created']),
773 intval($arr['contact-id']),
776 if (dbm::is_result($r)) {
777 logger('duplicated item with the same body found. '.print_r($arr,true));
782 // Is this item available in the global items (with uid=0)?
783 if ($arr["uid"] == 0) {
784 $arr["global"] = true;
786 // Set the global flag on all items if this was a global item entry
787 q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
789 $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
791 $arr["global"] = (count($isglobal) > 0);
795 if (strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
798 $private = $arr['private'];
800 $arr["allow_cid"] = $allow_cid;
801 $arr["allow_gid"] = $allow_gid;
802 $arr["deny_cid"] = $deny_cid;
803 $arr["deny_gid"] = $deny_gid;
804 $arr["private"] = $private;
805 $arr["deleted"] = $parent_deleted;
807 // Fill the cache field
808 put_item_in_cache($arr);
811 call_hooks('post_local', $arr);
813 call_hooks('post_remote', $arr);
815 if (x($arr,'cancel')) {
816 logger('item_store: post cancelled by plugin.');
820 // Check for already added items.
821 // There is a timing issue here that sometimes creates double postings.
822 // An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this.
823 if ($arr["uid"] == 0) {
824 $r = qu("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = 0 LIMIT 1", dbesc(trim($arr['uri'])));
825 if (dbm::is_result($r)) {
826 logger('Global item already stored. URI: '.$arr['uri'].' on network '.$arr['network'], LOGGER_DEBUG);
831 // Store the unescaped version
834 dbm::esc_array($arr, true);
836 logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
839 q("START TRANSACTION;");
841 $r = dbq("INSERT INTO `item` (`"
842 . implode("`, `", array_keys($arr))
844 . implode(", ", array_values($arr))
850 // When the item was successfully stored we fetch the ID of the item.
851 if (dbm::is_result($r)) {
852 $r = q("SELECT LAST_INSERT_ID() AS `item-id`");
853 if (dbm::is_result($r)) {
854 $current_post = $r[0]['item-id'];
856 // This shouldn't happen
860 // This can happen - for example - if there are locking timeouts.
863 // Store the data into a spool file so that we can try again later.
865 // At first we restore the Diaspora signature that we removed above.
866 if (isset($encoded_signature)) {
867 $arr['dsprsig'] = $encoded_signature;
870 // Now we store the data in the spool directory
871 // We use "microtime" to keep the arrival order and "mt_rand" to avoid duplicates
872 $file = 'item-'.round(microtime(true) * 10000).'-'.mt_rand().'.msg';
874 $spoolpath = get_spoolpath();
875 if ($spoolpath != "") {
876 $spool = $spoolpath.'/'.$file;
877 file_put_contents($spool, json_encode($arr));
878 logger("Item wasn't stored - Item was spooled into file ".$file, LOGGER_DEBUG);
883 if ($current_post == 0) {
884 // This is one of these error messages that never should occur.
885 logger("couldn't find created item - we better quit now.");
890 // How much entries have we created?
891 // We wouldn't need this query when we could use an unique index - but MySQL has length problems with them.
892 $r = q("SELECT COUNT(*) AS `entries` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s'",
895 dbesc($arr['network'])
898 if (!dbm::is_result($r)) {
899 // This shouldn't happen, since COUNT always works when the database connection is there.
900 logger("We couldn't count the stored entries. Very strange ...");
905 if ($r[0]["entries"] > 1) {
906 // There are duplicates. We delete our just created entry.
907 logger('Duplicated post occurred. uri = '.$arr['uri'].' uid = '.$arr['uid']);
909 // Yes, we could do a rollback here - but we are having many users with MyISAM.
910 q("DELETE FROM `item` WHERE `id` = %d", intval($current_post));
913 } elseif ($r[0]["entries"] == 0) {
914 // This really should never happen since we quit earlier if there were problems.
915 logger("Something is terribly wrong. We haven't found our created entry.");
920 logger('item_store: created item '.$current_post);
921 item_set_last_item($arr);
923 if (!$parent_id || ($arr['parent-uri'] === $arr['uri']))
924 $parent_id = $current_post;
927 $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
929 intval($current_post)
932 $arr['id'] = $current_post;
933 $arr['parent'] = $parent_id;
935 // update the commented timestamp on the parent
936 // Only update "commented" if it is really a comment
937 if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
938 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
939 dbesc(datetime_convert()),
940 dbesc(datetime_convert()),
944 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
945 dbesc(datetime_convert()),
951 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
952 // We can check for this condition when we decode and encode the stuff again.
953 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
954 $dsprsig->signature = base64_decode($dsprsig->signature);
955 logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
958 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
959 intval($current_post),
960 dbesc($dsprsig->signed_text),
961 dbesc($dsprsig->signature),
962 dbesc($dsprsig->signer)
966 $deleted = tag_deliver($arr['uid'], $current_post);
968 // current post can be deleted if is for a community page and no mention are
970 if (!$deleted AND !$dontcache) {
972 $r = q('SELECT * FROM `item` WHERE `id` = %d', intval($current_post));
973 if ((dbm::is_result($r)) && (count($r) == 1)) {
975 call_hooks('post_local_end', $r[0]);
977 call_hooks('post_remote_end', $r[0]);
980 logger('item_store: new item not found in DB, id ' . $current_post);
984 if ($arr['parent-uri'] === $arr['uri']) {
985 add_thread($current_post);
987 update_thread($parent_id);
992 // Due to deadlock issues with the "term" table we are doing these steps after the commit.
993 // This is not perfect - but a workable solution until we found the reason for the problem.
994 create_tags_from_item($current_post);
995 create_files_from_item($current_post);
997 // If this is now the last-child, force all _other_ children of this parent to *not* be last-child
998 // It is done after the transaction to avoid dead locks.
999 if ($arr['last-child']) {
1000 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
1002 intval($arr['uid']),
1003 intval($current_post)
1007 if ($arr['parent-uri'] === $arr['uri']) {
1008 add_shadow_thread($current_post);
1010 add_shadow_entry($current_post);
1013 check_item_notification($current_post, $uid);
1016 proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
1019 return $current_post;
1023 * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
1025 * This can be used to filter for inactive contacts.
1026 * Only do this for public postings to avoid privacy problems, since poco data is public.
1027 * Don't set this value if it isn't from the owner (could be an author that we don't know)
1029 * @param array $arr Contains the just posted item record
1031 function item_set_last_item($arr) {
1033 $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
1035 // Is it a forum? Then we don't care about the rules from above
1036 if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
1037 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
1038 intval($arr['contact-id']));
1045 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1046 dbesc($arr['received']),
1047 dbesc($arr['received']),
1048 intval($arr['contact-id'])
1051 // Now do the same for the system wide contacts with uid=0
1052 if (!$arr['private']) {
1053 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1054 dbesc($arr['received']),
1055 dbesc($arr['received']),
1056 intval($arr['owner-id'])
1059 if ($arr['owner-id'] != $arr['author-id']) {
1060 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1061 dbesc($arr['received']),
1062 dbesc($arr['received']),
1063 intval($arr['author-id'])
1069 function item_body_set_hashtags(&$item) {
1071 $tags = get_tags($item["body"]);
1077 // This sorting is important when there are hashtags that are part of other hashtags
1078 // Otherwise there could be problems with hashtags like #test and #test2
1083 $URLSearchString = "^\[\]";
1085 // All hashtags should point to the home server
1086 //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1087 // "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
1089 //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1090 // "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
1092 // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
1093 $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1095 return("[url=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/url]");
1098 $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
1100 return("[bookmark=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/bookmark]");
1103 $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
1105 return("[attachment ".str_replace("#", "#", $match[1])."]".$match[2]."[/attachment]");
1108 // Repair recursive urls
1109 $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1110 "#$2", $item["body"]);
1113 foreach($tags as $tag) {
1114 if (strpos($tag,'#') !== 0)
1117 if (strpos($tag,'[url='))
1120 $basetag = str_replace('_',' ',substr($tag,1));
1122 $newtag = '#[url='.App::get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1124 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1126 if (!stristr($item["tag"], "/search?tag=".$basetag."]".$basetag."[/url]")) {
1127 if (strlen($item["tag"]))
1128 $item["tag"] = ','.$item["tag"];
1129 $item["tag"] = $newtag.$item["tag"];
1133 // Convert back the masked hashtags
1134 $item["body"] = str_replace("#", "#", $item["body"]);
1137 function get_item_guid($id) {
1138 $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1139 if (dbm::is_result($r))
1140 return($r[0]["guid"]);
1145 function get_item_id($guid, $uid = 0) {
1151 $uid == local_user();
1153 // Does the given user have this item?
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`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1158 if (dbm::is_result($r)) {
1160 $nick = $r[0]["nickname"];
1164 // Or is it anywhere on the server?
1166 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1167 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1168 AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = ''
1169 AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
1170 AND `item`.`private` = 0 AND `item`.`wall` = 1
1171 AND `item`.`guid` = '%s'", dbesc($guid));
1172 if (dbm::is_result($r)) {
1174 $nick = $r[0]["nickname"];
1177 return(array("nick" => $nick, "id" => $id));
1181 function get_item_contact($item, $contacts) {
1182 if (! count($contacts) || (! is_array($item)))
1184 foreach($contacts as $contact) {
1185 if ($contact['id'] == $item['contact-id']) {
1187 break; // NOTREACHED
1194 * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1196 * @param int $item_id
1197 * @return bool true if item was deleted, else false
1199 function tag_deliver($uid, $item_id) {
1207 $u = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1211 if (! dbm::is_result($u)) {
1215 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1216 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1219 $i = q("SELECT * FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1223 if (! dbm::is_result($i)) {
1229 $link = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
1231 // Diaspora uses their own hardwired link URL in @-tags
1232 // instead of the one we supply with webfinger
1234 $dlink = normalise_link(App::get_baseurl() . '/u/' . $u[0]['nickname']);
1236 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism', $item['body'], $matches,PREG_SET_ORDER);
1238 foreach($matches as $mtch) {
1239 if (link_compare($link, $mtch[1]) || link_compare($dlink, $mtch[1])) {
1241 logger('tag_deliver: mention found: ' . $mtch[2]);
1247 if (($community_page || $prvgroup) &&
1248 (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])) {
1249 // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1251 logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1252 q("DELETE FROM item WHERE id = %d and uid = %d",
1261 $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1263 call_hooks('tagged', $arr);
1265 if ((! $community_page) && (! $prvgroup))
1269 // tgroup delivery - setup a second delivery chain
1270 // prevent delivery looping - only proceed
1271 // if the message originated elsewhere and is a top-level post
1273 if (($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent'])) {
1277 // now change this copy of the post to a forum head message and deliver to all the tgroup members
1280 $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1281 intval($u[0]['uid'])
1283 if (! dbm::is_result($c)) {
1287 // also reset all the privacy bits to the forum default permissions
1289 $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1291 $forum_mode = (($prvgroup) ? 2 : 1);
1293 q("UPDATE `item` SET `wall` = 1, `origin` = 1, `forum_mode` = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1294 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' WHERE `id` = %d",
1295 intval($forum_mode),
1296 dbesc($c[0]['name']),
1297 dbesc($c[0]['url']),
1298 dbesc($c[0]['thumb']),
1300 dbesc($u[0]['allow_cid']),
1301 dbesc($u[0]['allow_gid']),
1302 dbesc($u[0]['deny_cid']),
1303 dbesc($u[0]['deny_gid']),
1306 update_thread($item_id);
1308 proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
1314 function tgroup_check($uid, $item) {
1318 // check that the message originated elsewhere and is a top-level post
1320 if (($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri'])) {
1324 /// @TODO Encapsulate this or find it encapsulated and replace all occurrances
1325 $u = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1328 if (! dbm::is_result($u)) {
1332 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1333 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1336 $link = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
1338 // Diaspora uses their own hardwired link URL in @-tags
1339 // instead of the one we supply with webfinger
1341 $dlink = normalise_link(App::get_baseurl() . '/u/' . $u[0]['nickname']);
1343 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism', $item['body'], $matches,PREG_SET_ORDER);
1345 foreach ($matches as $mtch) {
1346 if (link_compare($link, $mtch[1]) || link_compare($dlink, $mtch[1])) {
1348 logger('tgroup_check: mention found: ' . $mtch[2]);
1357 /// @TODO Combines both return statements into one
1358 return (($community_page) || ($prvgroup));
1362 This function returns true if $update has an edited timestamp newer
1363 than $existing, i.e. $update contains new data which should override
1364 what's already there. If there is no timestamp yet, the update is
1365 assumed to be newer. If the update has no timestamp, the existing
1366 item is assumed to be up-to-date. If the timestamps are equal it
1367 assumes the update has been seen before and should be ignored.
1369 function edited_timestamp_is_newer($existing, $update) {
1370 if (!x($existing,'edited') || !$existing['edited']) {
1373 if (!x($update,'edited') || !$update['edited']) {
1377 $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1378 $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1379 return (strcmp($existing_edited, $update_edited) < 0);
1384 * consume_feed - process atom feed and update anything/everything we might need to update
1386 * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1388 * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1389 * It is this person's stuff that is going to be updated.
1390 * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1391 * from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1392 * have a contact record.
1393 * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1394 * might not) try and subscribe to it.
1395 * $datedir sorts in reverse order
1396 * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1397 * imported prior to its children being seen in the stream unless we are certain
1398 * of how the feed is arranged/ordered.
1399 * With $pass = 1, we only pull parent items out of the stream.
1400 * With $pass = 2, we only pull children (comments/likes).
1402 * So running this twice, first with pass 1 and then with pass 2 will do the right
1403 * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1404 * model where comments can have sub-threads. That would require some massive sorting
1405 * to get all the feed items into a mostly linear ordering, and might still require
1409 function consume_feed($xml, $importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1410 if ($contact['network'] === NETWORK_OSTATUS) {
1412 // Test - remove before flight
1413 //$tempfile = tempnam(get_temppath(), "ostatus2");
1414 //file_put_contents($tempfile, $xml);
1415 logger("Consume OStatus messages ", LOGGER_DEBUG);
1416 ostatus::import($xml, $importer, $contact, $hub);
1421 if ($contact['network'] === NETWORK_FEED) {
1423 logger("Consume feeds", LOGGER_DEBUG);
1424 feed_import($xml, $importer, $contact, $hub);
1429 if ($contact['network'] === NETWORK_DFRN) {
1430 logger("Consume DFRN messages", LOGGER_DEBUG);
1432 $r = q("SELECT `contact`.*, `contact`.`uid` AS `importer_uid`,
1433 `contact`.`pubkey` AS `cpubkey`,
1434 `contact`.`prvkey` AS `cprvkey`,
1435 `contact`.`thumb` AS `thumb`,
1436 `contact`.`url` as `url`,
1437 `contact`.`name` as `senderName`,
1440 LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1441 WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1442 dbesc($contact["id"]), dbesc($importer["uid"])
1445 logger("Now import the DFRN feed");
1446 dfrn::import($xml, $r[0], true);
1452 function item_is_remote_self($contact, &$datarray) {
1455 if (!$contact['remote_self'])
1458 // Prevent the forwarding of posts that are forwarded
1459 if ($datarray["extid"] == NETWORK_DFRN)
1462 // Prevent to forward already forwarded posts
1463 if ($datarray["app"] == $a->get_hostname())
1466 // Only forward posts
1467 if ($datarray["verb"] != ACTIVITY_POST)
1470 if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1473 $datarray2 = $datarray;
1474 logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1475 if ($contact['remote_self'] == 2) {
1476 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1477 intval($contact['uid']));
1478 if (dbm::is_result($r)) {
1479 $datarray['contact-id'] = $r[0]["id"];
1481 $datarray['owner-name'] = $r[0]["name"];
1482 $datarray['owner-link'] = $r[0]["url"];
1483 $datarray['owner-avatar'] = $r[0]["thumb"];
1485 $datarray['author-name'] = $datarray['owner-name'];
1486 $datarray['author-link'] = $datarray['owner-link'];
1487 $datarray['author-avatar'] = $datarray['owner-avatar'];
1490 if ($contact['network'] != NETWORK_FEED) {
1491 $datarray["guid"] = get_guid(32);
1492 unset($datarray["plink"]);
1493 $datarray["uri"] = item_new_uri($a->get_hostname(), $contact['uid'], $datarray["guid"]);
1494 $datarray["parent-uri"] = $datarray["uri"];
1495 $datarray["extid"] = $contact['network'];
1496 $urlpart = parse_url($datarray2['author-link']);
1497 $datarray["app"] = $urlpart["host"];
1499 $datarray['private'] = 0;
1502 if ($contact['network'] != NETWORK_FEED) {
1503 // Store the original post
1504 $r = item_store($datarray2, false, false);
1505 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1507 $datarray["app"] = "Feed";
1512 function new_follower($importer, $contact, $datarray, $item, $sharing = false) {
1513 $url = notags(trim($datarray['author-link']));
1514 $name = notags(trim($datarray['author-name']));
1515 $photo = notags(trim($datarray['author-avatar']));
1517 if (is_object($item)) {
1518 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1519 if ($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data']) {
1520 $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1526 if (is_array($contact)) {
1527 if (($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1528 || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1529 $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1530 intval(CONTACT_IS_FRIEND),
1531 intval($contact['id']),
1532 intval($importer['uid'])
1535 // send email notification to owner?
1538 // create contact record
1540 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1541 `blocked`, `readonly`, `pending`, `writable`)
1542 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1543 intval($importer['uid']),
1544 dbesc(datetime_convert()),
1546 dbesc(normalise_link($url)),
1550 dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1551 intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1553 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1554 intval($importer['uid']),
1557 if (dbm::is_result($r)) {
1558 $contact_record = $r[0];
1559 update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
1562 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1563 intval($importer['uid'])
1566 if (dbm::is_result($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1568 // create notification
1569 $hash = random_string();
1571 if (is_array($contact_record)) {
1572 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1573 VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1574 intval($importer['uid']),
1575 intval($contact_record['id']),
1577 dbesc(datetime_convert())
1581 $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1583 if (intval($def_gid)) {
1584 group_add_member($importer['uid'], '', $contact_record['id'], $def_gid);
1587 if (($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1588 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1591 'type' => NOTIFY_INTRO,
1592 'notify_flags' => $r[0]['notify-flags'],
1593 'language' => $r[0]['language'],
1594 'to_name' => $r[0]['username'],
1595 'to_email' => $r[0]['email'],
1596 'uid' => $r[0]['uid'],
1597 'link' => App::get_baseurl() . '/notifications/intro',
1598 'source_name' => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1599 'source_link' => $contact_record['url'],
1600 'source_photo' => $contact_record['photo'],
1601 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1606 } elseif (dbm::is_result($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1607 $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1608 intval($importer['uid']),
1616 function lose_follower($importer, $contact, array $datarray = array(), $item = "") {
1618 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1619 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1620 intval(CONTACT_IS_SHARING),
1621 intval($contact['id'])
1624 contact_remove($contact['id']);
1628 function lose_sharer($importer, $contact, array $datarray = array(), $item = "") {
1630 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1631 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1632 intval(CONTACT_IS_FOLLOWER),
1633 intval($contact['id'])
1636 contact_remove($contact['id']);
1640 function subscribe_to_hub($url, $importer, $contact, $hubmode = 'subscribe') {
1644 if (is_array($importer)) {
1645 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1646 intval($importer['uid'])
1650 // Diaspora has different message-ids in feeds than they do
1651 // through the direct Diaspora protocol. If we try and use
1652 // the feed, we'll get duplicates. So don't.
1654 if ((! dbm::is_result($r)) || $contact['network'] === NETWORK_DIASPORA)
1657 $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1659 // Use a single verify token, even if multiple hubs
1661 $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1663 $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1665 logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: ' . $push_url . ' with verifier ' . $verify_token);
1667 if (!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1668 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1669 dbesc($verify_token),
1670 intval($contact['id'])
1674 post_url($url, $params);
1676 logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1682 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1684 if (get_config('system','disable_embedded'))
1689 logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1690 $site = substr(App::get_baseurl(),strpos(App::get_baseurl(),'://'));
1695 $img_start = strpos($orig_body, '[img');
1696 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1697 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1698 while ( ($img_st_close !== false) && ($img_len !== false) ) {
1700 $img_st_close++; // make it point to AFTER the closing bracket
1701 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1703 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1706 if (stristr($image , $site . '/photo/')) {
1707 // Only embed locally hosted photos
1709 $i = basename($image);
1710 $i = str_replace(array('.jpg','.png','.gif'),array('', '',''), $i);
1711 $x = strpos($i,'-');
1714 $res = substr($i, $x+1);
1715 $i = substr($i,0, $x);
1716 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1723 // Check to see if we should replace this photo link with an embedded image
1724 // 1. No need to do so if the photo is public
1725 // 2. If there's a contact-id provided, see if they're in the access list
1726 // for the photo. If so, embed it.
1727 // 3. Otherwise, if we have an item, see if the item permissions match the photo
1728 // permissions, regardless of order but first check to see if they're an exact
1729 // match to save some processing overhead.
1731 if (has_permissions($r[0])) {
1733 $recips = enumerate_permissions($r[0]);
1734 if (in_array($cid, $recips)) {
1738 if (compare_permissions($item, $r[0]))
1743 $data = $r[0]['data'];
1744 $type = $r[0]['type'];
1746 // If a custom width and height were specified, apply before embedding
1747 if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1748 logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1750 $width = intval($match[1]);
1751 $height = intval($match[2]);
1753 $ph = new Photo($data, $type);
1754 if ($ph->is_valid()) {
1755 $ph->scaleImage(max($width, $height));
1756 $data = $ph->imageString();
1757 $type = $ph->getType();
1761 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1762 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1763 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1769 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1770 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1771 if ($orig_body === false) {
1775 $img_start = strpos($orig_body, '[img');
1776 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1777 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1780 $new_body = $new_body . $orig_body;
1785 function has_permissions($obj) {
1786 if (($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1791 function compare_permissions($obj1, $obj2) {
1792 // first part is easy. Check that these are exactly the same.
1793 if (($obj1['allow_cid'] == $obj2['allow_cid'])
1794 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1795 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1796 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1799 // This is harder. Parse all the permissions and compare the resulting set.
1801 $recipients1 = enumerate_permissions($obj1);
1802 $recipients2 = enumerate_permissions($obj2);
1805 if ($recipients1 == $recipients2)
1810 // returns an array of contact-ids that are allowed to see this object
1812 function enumerate_permissions($obj) {
1813 $allow_people = expand_acl($obj['allow_cid']);
1814 $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1815 $deny_people = expand_acl($obj['deny_cid']);
1816 $deny_groups = expand_groups(expand_acl($obj['deny_gid']));
1817 $recipients = array_unique(array_merge($allow_people, $allow_groups));
1818 $deny = array_unique(array_merge($deny_people, $deny_groups));
1819 $recipients = array_diff($recipients, $deny);
1823 function item_getfeedtags($item) {
1826 $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|', $item['tag'], $matches);
1828 for($x = 0; $x < $cnt; $x ++) {
1829 if ($matches[1][$x])
1830 $ret[$matches[2][$x]] = array('#', $matches[1][$x], $matches[2][$x]);
1834 $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|', $item['tag'], $matches);
1836 for($x = 0; $x < $cnt; $x ++) {
1837 if ($matches[1][$x])
1838 $ret[] = array('@', $matches[1][$x], $matches[2][$x]);
1844 function item_expire($uid, $days, $network = "", $force = false) {
1846 if ((! $uid) || ($days < 1))
1849 // $expire_network_only = save your own wall posts
1850 // and just expire conversations started by others
1852 $expire_network_only = get_pconfig($uid,'expire','network_only');
1853 $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1855 if ($network != "") {
1856 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1857 // There is an index "uid_network_received" but not "uid_network_created"
1858 // This avoids the creation of another index just for one purpose.
1859 // And it doesn't really matter wether to look at "received" or "created"
1860 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1862 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1864 $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
1865 WHERE `uid` = %d $range
1873 if (! dbm::is_result($r))
1876 $expire_items = get_pconfig($uid, 'expire','items');
1877 $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1879 // Forcing expiring of items - but not notes and marked items
1881 $expire_items = true;
1883 $expire_notes = get_pconfig($uid, 'expire','notes');
1884 $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1886 $expire_starred = get_pconfig($uid, 'expire','starred');
1887 $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1889 $expire_photos = get_pconfig($uid, 'expire','photos');
1890 $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1892 logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1894 foreach($r as $item) {
1896 // don't expire filed items
1898 if (strpos($item['file'],'[') !== false)
1901 // Only expire posts, not photos and photo comments
1903 if ($expire_photos==0 && strlen($item['resource-id']))
1905 if ($expire_starred==0 && intval($item['starred']))
1907 if ($expire_notes==0 && $item['type']=='note')
1909 if ($expire_items==0 && $item['type']!='note')
1912 drop_item($item['id'],false);
1915 proc_run(PRIORITY_HIGH, "include/notifier.php", "expire", $uid);
1920 function drop_items($items) {
1923 if (! local_user() && ! remote_user())
1926 if (count($items)) {
1927 foreach($items as $item) {
1928 $owner = drop_item($item,false);
1929 if ($owner && ! $uid)
1934 // multiple threads may have been deleted, send an expire notification
1937 proc_run(PRIORITY_HIGH, "include/notifier.php", "expire", $uid);
1942 function drop_item($id, $interactive = true) {
1946 // locate item to be deleted
1948 $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1952 if (! dbm::is_result($r)) {
1953 if (! $interactive) {
1956 notice( t('Item not found.') . EOL);
1957 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
1962 $owner = $item['uid'];
1966 // check if logged in user is either the author or owner of this item
1968 if (is_array($_SESSION['remote'])) {
1969 foreach($_SESSION['remote'] as $visitor) {
1970 if ($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1971 $contact_id = $visitor['cid'];
1978 if ((local_user() == $item['uid']) || ($contact_id) || (! $interactive)) {
1980 // Check if we should do HTML-based delete confirmation
1981 if ($_REQUEST['confirm']) {
1982 // <form> can't take arguments in its "action" parameter
1983 // so add any arguments as hidden inputs
1984 $query = explode_querystring($a->query_string);
1986 foreach($query['args'] as $arg) {
1987 if (strpos($arg, 'confirm=') === false) {
1988 $arg_parts = explode('=', $arg);
1989 $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1993 return replace_macros(get_markup_template('confirm.tpl'), array(
1995 '$message' => t('Do you really want to delete this item?'),
1996 '$extra_inputs' => $inputs,
1997 '$confirm' => t('Yes'),
1998 '$confirm_url' => $query['base'],
1999 '$confirm_name' => 'confirmed',
2000 '$cancel' => t('Cancel'),
2003 // Now check how the user responded to the confirmation query
2004 if ($_REQUEST['canceled']) {
2005 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2008 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
2011 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
2012 dbesc(datetime_convert()),
2013 dbesc(datetime_convert()),
2016 create_tags_from_item($item['id']);
2017 create_files_from_item($item['id']);
2018 delete_thread($item['id'], $item['parent-uri']);
2020 // clean up categories and tags so they don't end up as orphans
2023 $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches,PREG_SET_ORDER);
2025 foreach($matches as $mtch) {
2026 file_tag_unsave_file($item['uid'], $item['id'], $mtch[1],true);
2032 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches,PREG_SET_ORDER);
2034 foreach($matches as $mtch) {
2035 file_tag_unsave_file($item['uid'], $item['id'], $mtch[1],false);
2039 // If item is a link to a photo resource, nuke all the associated photos
2040 // (visitors will not have photo resources)
2041 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
2042 // generate a resource-id and therefore aren't intimately linked to the item.
2044 if (strlen($item['resource-id'])) {
2045 q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
2046 dbesc($item['resource-id']),
2047 intval($item['uid'])
2049 // ignore the result
2052 // If item is a link to an event, nuke the event record.
2054 if (intval($item['event-id'])) {
2055 q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
2056 intval($item['event-id']),
2057 intval($item['uid'])
2059 // ignore the result
2062 // If item has attachments, drop them
2064 foreach(explode(", ", $item['attach']) as $attach) {
2065 preg_match("|attach/(\d+)|", $attach, $matches);
2066 q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
2067 intval($matches[1]),
2070 // ignore the result
2074 // clean up item_id and sign meta-data tables
2077 // Old code - caused very long queries and warning entries in the mysql logfiles:
2079 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
2080 intval($item['id']),
2081 intval($item['uid'])
2084 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
2085 intval($item['id']),
2086 intval($item['uid'])
2090 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
2092 // Creating list of parents
2093 $r = q("SELECT `id` FROM `item` WHERE `parent` = %d AND `uid` = %d",
2094 intval($item['id']),
2095 intval($item['uid'])
2100 foreach ($r AS $row) {
2101 if ($parentid != "")
2104 $parentid .= $row["id"];
2108 if ($parentid != "") {
2109 $r = q("DELETE FROM `item_id` WHERE `iid` IN (%s)", dbesc($parentid));
2110 $r = q("DELETE FROM `sign` WHERE `iid` IN (%s)", dbesc($parentid));
2113 // If it's the parent of a comment thread, kill all the kids
2114 if ($item['uri'] == $item['parent-uri']) {
2115 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
2116 WHERE `parent-uri` = '%s' AND `uid` = %d ",
2117 dbesc(datetime_convert()),
2118 dbesc(datetime_convert()),
2119 dbesc($item['parent-uri']),
2120 intval($item['uid'])
2122 create_tags_from_itemuri($item['parent-uri'], $item['uid']);
2123 create_files_from_itemuri($item['parent-uri'], $item['uid']);
2124 delete_thread_uri($item['parent-uri'], $item['uid']);
2125 // ignore the result
2127 // ensure that last-child is set in case the comment that had it just got wiped.
2128 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2129 dbesc(datetime_convert()),
2130 dbesc($item['parent-uri']),
2131 intval($item['uid'])
2133 // who is the last child now?
2134 $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",
2135 dbesc($item['parent-uri']),
2136 intval($item['uid'])
2138 if (dbm::is_result($r)) {
2139 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2145 $drop_id = intval($item['id']);
2147 // send the notification upstream/downstream as the case may be
2149 proc_run(PRIORITY_HIGH, "include/notifier.php", "drop", $drop_id);
2151 if (! $interactive) {
2154 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2157 if (! $interactive) {
2160 notice( t('Permission denied.') . EOL);
2161 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2168 function first_post_date($uid, $wall = false) {
2169 $r = q("SELECT `id`, `created` FROM `item`
2170 WHERE `uid` = %d AND `wall` = %d AND `deleted` = 0 AND `visible` = 1 AND `moderated` = 0
2172 ORDER BY `created` ASC LIMIT 1",
2174 intval($wall ? 1 : 0)
2176 if (dbm::is_result($r)) {
2177 // logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2178 return substr(datetime_convert('',date_default_timezone_get(), $r[0]['created']),0,10);
2183 /* modified posted_dates() {below} to arrange the list in years */
2184 function list_post_dates($uid, $wall) {
2185 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2187 $dthen = first_post_date($uid, $wall);
2192 // Set the start and end date to the beginning of the month
2193 $dnow = substr($dnow,0,8).'01';
2194 $dthen = substr($dthen,0,8).'01';
2199 * Starting with the current month, get the first and last days of every
2200 * month down to and including the month of the first post
2202 while (substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2203 $dyear = intval(substr($dnow,0,4));
2204 $dstart = substr($dnow,0,8) . '01';
2205 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2206 $start_month = datetime_convert('', '', $dstart,'Y-m-d');
2207 $end_month = datetime_convert('', '', $dend,'Y-m-d');
2208 $str = day_translate(datetime_convert('', '', $dnow,'F'));
2209 if (!$ret[$dyear]) {
2210 $ret[$dyear] = array();
2212 $ret[$dyear][] = array($str, $end_month, $start_month);
2213 $dnow = datetime_convert('', '', $dnow . ' -1 month', 'Y-m-d');
2218 function posted_dates($uid, $wall) {
2219 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2221 $dthen = first_post_date($uid, $wall);
2226 // Set the start and end date to the beginning of the month
2227 $dnow = substr($dnow,0,8).'01';
2228 $dthen = substr($dthen,0,8).'01';
2231 // Starting with the current month, get the first and last days of every
2232 // month down to and including the month of the first post
2233 while (substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2234 $dstart = substr($dnow,0,8) . '01';
2235 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2236 $start_month = datetime_convert('', '', $dstart,'Y-m-d');
2237 $end_month = datetime_convert('', '', $dend,'Y-m-d');
2238 $str = day_translate(datetime_convert('', '', $dnow,'F Y'));
2239 $ret[] = array($str, $end_month, $start_month);
2240 $dnow = datetime_convert('', '', $dnow . ' -1 month', 'Y-m-d');
2246 function posted_date_widget($url, $uid, $wall) {
2249 if (! feature_enabled($uid, 'archives')) {
2253 // For former Facebook folks that left because of "timeline"
2255 /* if ($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2258 $visible_years = get_pconfig($uid,'system','archive_visible_years');
2259 if (! $visible_years) {
2263 $ret = list_post_dates($uid, $wall);
2265 if (! dbm::is_result($ret)) {
2269 $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2270 $cutoff = ((array_key_exists($cutoff_year, $ret))? true : false);
2272 $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2273 '$title' => t('Archives'),
2274 '$size' => $visible_years,
2275 '$cutoff_year' => $cutoff_year,
2276 '$cutoff' => $cutoff,
2279 '$showmore' => t('show more')