4 * @file include/items.php
7 use \Friendica\ParseUrl;
9 require_once('include/bbcode.php');
10 require_once('include/oembed.php');
11 require_once('include/salmon.php');
12 require_once('include/crypto.php');
13 require_once('include/Photo.php');
14 require_once('include/tags.php');
15 require_once('include/files.php');
16 require_once('include/text.php');
17 require_once('include/email.php');
18 require_once('include/threads.php');
19 require_once('include/socgraph.php');
20 require_once('include/plaintext.php');
21 require_once('include/ostatus.php');
22 require_once('include/feed.php');
23 require_once('include/Contact.php');
24 require_once('mod/share.php');
25 require_once('include/enotify.php');
26 require_once('include/dfrn.php');
27 require_once('include/group.php');
29 require_once('library/defuse/php-encryption-1.2.1/Crypto.php');
31 function construct_verb($item) {
39 * The purpose of this function is to apply system message length limits to
40 * imported messages without including any embedded photos in the length
42 if (! function_exists('limit_body_size')) {
43 function limit_body_size($body) {
45 // logger('limit_body_size: start', LOGGER_DEBUG);
47 $maxlen = get_max_import_size();
49 // If the length of the body, including the embedded images, is smaller
50 // than the maximum, then don't waste time looking for the images
51 if ($maxlen && (strlen($body) > $maxlen)) {
53 logger('limit_body_size: the total body length exceeds the limit', LOGGER_DEBUG);
60 $img_start = strpos($orig_body, '[img');
61 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
62 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
63 while(($img_st_close !== false) && ($img_end !== false)) {
65 $img_st_close++; // make it point to AFTER the closing bracket
66 $img_end += $img_start;
67 $img_end += strlen('[/img]');
69 if (! strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
70 // This is an embedded image
72 if ( ($textlen + $img_start) > $maxlen ) {
73 if ($textlen < $maxlen) {
74 logger('limit_body_size: the limit happens before an embedded image', LOGGER_DEBUG);
75 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
79 $new_body = $new_body . substr($orig_body, 0, $img_start);
80 $textlen += $img_start;
83 $new_body = $new_body . substr($orig_body, $img_start, $img_end - $img_start);
86 if ( ($textlen + $img_end) > $maxlen ) {
87 if ($textlen < $maxlen) {
88 logger('limit_body_size: the limit happens before the end of a non-embedded image', LOGGER_DEBUG);
89 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
93 $new_body = $new_body . substr($orig_body, 0, $img_end);
97 $orig_body = substr($orig_body, $img_end);
99 if ($orig_body === false) // in case the body ends on a closing image tag
102 $img_start = strpos($orig_body, '[img');
103 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
104 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
107 if ( ($textlen + strlen($orig_body)) > $maxlen) {
108 if ($textlen < $maxlen) {
109 logger('limit_body_size: the limit happens after the end of the last image', LOGGER_DEBUG);
110 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
114 logger('limit_body_size: the text size with embedded images extracted did not violate the limit', LOGGER_DEBUG);
115 $new_body = $new_body . $orig_body;
116 $textlen += strlen($orig_body);
124 function title_is_body($title, $body) {
126 $title = strip_tags($title);
127 $title = trim($title);
128 $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
129 $title = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $title);
131 $body = strip_tags($body);
133 $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
134 $body = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $body);
136 if (strlen($title) < strlen($body))
137 $body = substr($body, 0, strlen($title));
139 if (($title != $body) and (substr($title, -3) == "...")) {
140 $pos = strrpos($title, "...");
142 $title = substr($title, 0, $pos);
143 $body = substr($body, 0, $pos);
147 return($title == $body);
150 function add_page_info_data($data) {
151 call_hooks('page_info_data', $data);
153 // It maybe is a rich content, but if it does have everything that a link has,
154 // then treat it that way
155 if (($data["type"] == "rich") AND is_string($data["title"]) AND
156 is_string($data["text"]) AND (sizeof($data["images"]) > 0)) {
157 $data["type"] = "link";
160 if ((($data["type"] != "link") AND ($data["type"] != "video") AND ($data["type"] != "photo")) OR ($data["title"] == $data["url"])) {
164 if ($no_photos AND ($data["type"] == "photo")) {
168 if (sizeof($data["images"]) > 0) {
169 $preview = $data["images"][0];
174 // Escape some bad characters
175 $data["url"] = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["url"], ENT_QUOTES, 'UTF-8', false));
176 $data["title"] = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["title"], ENT_QUOTES, 'UTF-8', false));
178 $text = "[attachment type='".$data["type"]."'";
180 if ($data["text"] == "") {
181 $data["text"] = $data["title"];
184 if ($data["text"] == "") {
185 $data["text"] = $data["url"];
188 if ($data["url"] != "") {
189 $text .= " url='".$data["url"]."'";
192 if ($data["title"] != "") {
193 $text .= " title='".$data["title"]."'";
196 if (sizeof($data["images"]) > 0) {
197 $preview = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["images"][0]["src"], ENT_QUOTES, 'UTF-8', false));
198 // if the preview picture is larger than 500 pixels then show it in a larger mode
199 // But only, if the picture isn't higher than large (To prevent huge posts)
200 if (($data["images"][0]["width"] >= 500) AND ($data["images"][0]["width"] >= $data["images"][0]["height"])) {
201 $text .= " image='".$preview."'";
203 $text .= " preview='".$preview."'";
207 $text .= "]".$data["text"]."[/attachment]";
210 if (isset($data["keywords"]) AND count($data["keywords"])) {
213 foreach ($data["keywords"] AS $keyword) {
214 /// @todo make a positive list of allowed characters
215 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'", "’", "`", "(", ")", "„", "“"),
216 array("","", "", "", "", "", "", "", "", "", "", ""), $keyword);
217 $hashtags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url] ";
221 return "\n".$text.$hashtags;
224 function query_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
226 $data = ParseUrl::getSiteinfoCached($url, true);
229 $data["images"][0]["src"] = $photo;
231 logger('fetch page info for '.$url.' '.print_r($data, true), LOGGER_DEBUG);
233 if (!$keywords AND isset($data["keywords"]))
234 unset($data["keywords"]);
236 if (($keyword_blacklist != "") AND isset($data["keywords"])) {
237 $list = explode(",", $keyword_blacklist);
238 foreach ($list AS $keyword) {
239 $keyword = trim($keyword);
240 $index = array_search($keyword, $data["keywords"]);
241 if ($index !== false)
242 unset($data["keywords"][$index]);
249 function add_page_keywords($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
250 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
253 if (isset($data["keywords"]) AND count($data["keywords"])) {
255 foreach ($data["keywords"] AS $keyword) {
256 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'"),
257 array("","", "", "", "", ""), $keyword);
262 $tags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url]";
269 function add_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
270 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
272 $text = add_page_info_data($data);
277 function add_page_info_to_body($body, $texturl = false, $no_photos = false) {
279 logger('add_page_info_to_body: fetch page info for body '.$body, LOGGER_DEBUG);
281 $URLSearchString = "^\[\]";
283 // Adding these spaces is a quick hack due to my problems with regular expressions :)
284 preg_match("/[^!#@]\[url\]([$URLSearchString]*)\[\/url\]/ism", " ".$body, $matches);
287 preg_match("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", " ".$body, $matches);
289 // Convert urls without bbcode elements
290 if (!$matches AND $texturl) {
291 preg_match("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", " ".$body, $matches);
293 // Yeah, a hack. I really hate regular expressions :)
295 $matches[1] = $matches[2];
299 $footer = add_page_info($matches[1], $no_photos);
301 // Remove the link from the body if the link is attached at the end of the post
302 if (isset($footer) AND (trim($footer) != "") AND (strpos($footer, $matches[1]))) {
303 $removedlink = trim(str_replace($matches[1], "", $body));
304 if (($removedlink == "") OR strstr($body, $removedlink))
305 $body = $removedlink;
307 $url = str_replace(array('/', '.'), array('\/', '\.'), $matches[1]);
308 $removedlink = preg_replace("/\[url\=".$url."\](.*?)\[\/url\]/ism", '', $body);
309 if (($removedlink == "") OR strstr($body, $removedlink))
310 $body = $removedlink;
313 // Add the page information to the bottom
314 if (isset($footer) AND (trim($footer) != ""))
321 * Adds a "lang" specification in a "postopts" element of given $arr,
322 * if possible and not already present.
323 * Expects "body" element to exist in $arr.
325 * @todo Add a parameter to request forcing override
327 function item_add_language_opt(&$arr) {
329 if (version_compare(PHP_VERSION, '5.3.0', '<')) return; // LanguageDetect.php not available ?
331 if ( x($arr, 'postopts') )
333 if ( strstr($arr['postopts'], 'lang=') )
336 /// @TODO Add parameter to request overriding
339 $postopts = $arr['postopts'];
344 require_once('library/langdet/Text/LanguageDetect.php');
345 $naked_body = preg_replace('/\[(.+?)\]/','',$arr['body']);
346 $l = new Text_LanguageDetect;
347 //$lng = $l->detectConfidence($naked_body);
348 //$arr['postopts'] = (($lng['language']) ? 'lang=' . $lng['language'] . ';' . $lng['confidence'] : '');
349 $lng = $l->detect($naked_body, 3);
351 if (sizeof($lng) > 0) {
352 if ($postopts != "") $postopts .= '&'; // arbitrary separator, to be reviewed
353 $postopts .= 'lang=';
355 foreach ($lng as $language => $score) {
356 $postopts .= $sep . $language.";".$score;
359 $arr['postopts'] = $postopts;
364 * @brief Creates an unique guid out of a given uri
366 * @param string $uri uri of an item entry
367 * @param string $host (Optional) hostname for the GUID prefix
368 * @return string unique guid
370 function uri_to_guid($uri, $host = "") {
372 // Our regular guid routine is using this kind of prefix as well
373 // We have to avoid that different routines could accidentally create the same value
374 $parsed = parse_url($uri);
377 $host = $parsed["host"];
380 $guid_prefix = hash("crc32", $host);
382 // Remove the scheme to make sure that "https" and "http" doesn't make a difference
383 unset($parsed["scheme"]);
385 $host_id = implode("/", $parsed);
387 // We could use any hash algorithm since it isn't a security issue
388 $host_hash = hash("ripemd128", $host_id);
390 return $guid_prefix.$host_hash;
393 function item_store($arr,$force_parent = false, $notify = false, $dontcache = false) {
397 // If it is a posting where users should get notifications, then define it as wall posting
400 $arr['type'] = 'wall';
402 $arr['last-child'] = 1;
403 $arr['network'] = NETWORK_DFRN;
405 // We have to avoid duplicates. So we create the GUID in form of a hash of the plink or uri.
406 // In difference to the call to "uri_to_guid" several lines below we add the hash of our own host.
407 // This is done because our host is the original creator of the post.
408 if (isset($arr['plink'])) {
409 $arr['guid'] = uri_to_guid($arr['plink'], $a->get_hostname());
410 } elseif (isset($arr['uri'])) {
411 $arr['guid'] = uri_to_guid($arr['uri'], $a->get_hostname());
415 // If a Diaspora signature structure was passed in, pull it out of the
416 // item array and set it aside for later storage.
419 if (x($arr,'dsprsig')) {
420 $encoded_signature = $arr['dsprsig'];
421 $dsprsig = json_decode(base64_decode($arr['dsprsig']));
422 unset($arr['dsprsig']);
425 // Converting the plink
426 if ($arr['network'] == NETWORK_OSTATUS) {
427 if (isset($arr['plink']))
428 $arr['plink'] = ostatus::convert_href($arr['plink']);
429 elseif (isset($arr['uri']))
430 $arr['plink'] = ostatus::convert_href($arr['uri']);
433 if (x($arr, 'gravity'))
434 $arr['gravity'] = intval($arr['gravity']);
435 elseif ($arr['parent-uri'] === $arr['uri'])
437 elseif (activity_match($arr['verb'],ACTIVITY_POST))
440 $arr['gravity'] = 6; // extensible catchall
442 if (! x($arr,'type'))
443 $arr['type'] = 'remote';
447 /* check for create date and expire time */
448 $uid = intval($arr['uid']);
449 $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
451 $expire_interval = $r[0]['expire'];
452 if ($expire_interval>0) {
453 $expire_date = new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
454 $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
455 if ($created_date < $expire_date) {
456 logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
462 // Do we already have this item?
463 // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
464 if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
465 $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s') LIMIT 1",
466 dbesc(trim($arr['uri'])),
468 dbesc(NETWORK_DIASPORA),
470 dbesc(NETWORK_OSTATUS)
473 // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
475 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']);
480 // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
481 // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
482 //if ((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
483 // $arr['body'] = strip_tags($arr['body']);
485 item_add_language_opt($arr);
489 elseif ((trim($arr['guid']) == "") AND (trim($arr['plink']) != ""))
490 $arr['guid'] = uri_to_guid($arr['plink']);
491 elseif ((trim($arr['guid']) == "") AND (trim($arr['uri']) != ""))
492 $arr['guid'] = uri_to_guid($arr['uri']);
494 $parsed = parse_url($arr["author-link"]);
495 $guid_prefix = hash("crc32", $parsed["host"]);
498 $arr['wall'] = ((x($arr,'wall')) ? intval($arr['wall']) : 0);
499 $arr['guid'] = ((x($arr,'guid')) ? notags(trim($arr['guid'])) : get_guid(32, $guid_prefix));
500 $arr['uri'] = ((x($arr,'uri')) ? notags(trim($arr['uri'])) : item_new_uri($a->get_hostname(), $uid, $arr['guid']));
501 $arr['extid'] = ((x($arr,'extid')) ? notags(trim($arr['extid'])) : '');
502 $arr['author-name'] = ((x($arr,'author-name')) ? trim($arr['author-name']) : '');
503 $arr['author-link'] = ((x($arr,'author-link')) ? notags(trim($arr['author-link'])) : '');
504 $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
505 $arr['owner-name'] = ((x($arr,'owner-name')) ? trim($arr['owner-name']) : '');
506 $arr['owner-link'] = ((x($arr,'owner-link')) ? notags(trim($arr['owner-link'])) : '');
507 $arr['owner-avatar'] = ((x($arr,'owner-avatar')) ? notags(trim($arr['owner-avatar'])) : '');
508 $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert());
509 $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert());
510 $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert());
511 $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert());
512 $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert());
513 $arr['title'] = ((x($arr,'title')) ? trim($arr['title']) : '');
514 $arr['location'] = ((x($arr,'location')) ? trim($arr['location']) : '');
515 $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : '');
516 $arr['last-child'] = ((x($arr,'last-child')) ? intval($arr['last-child']) : 0 );
517 $arr['visible'] = ((x($arr,'visible') !== false) ? intval($arr['visible']) : 1 );
519 $arr['parent-uri'] = ((x($arr,'parent-uri')) ? notags(trim($arr['parent-uri'])) : $arr['uri']);
520 $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : '');
521 $arr['object-type'] = ((x($arr,'object-type')) ? notags(trim($arr['object-type'])) : '');
522 $arr['object'] = ((x($arr,'object')) ? trim($arr['object']) : '');
523 $arr['target-type'] = ((x($arr,'target-type')) ? notags(trim($arr['target-type'])) : '');
524 $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : '');
525 $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : '');
526 $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : '');
527 $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : '');
528 $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : '');
529 $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : '');
530 $arr['private'] = ((x($arr,'private')) ? intval($arr['private']) : 0 );
531 $arr['bookmark'] = ((x($arr,'bookmark')) ? intval($arr['bookmark']) : 0 );
532 $arr['body'] = ((x($arr,'body')) ? trim($arr['body']) : '');
533 $arr['tag'] = ((x($arr,'tag')) ? notags(trim($arr['tag'])) : '');
534 $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : '');
535 $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : '');
536 $arr['origin'] = ((x($arr,'origin')) ? intval($arr['origin']) : 0 );
537 $arr['network'] = ((x($arr,'network')) ? trim($arr['network']) : '');
538 $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : '');
539 $arr['resource-id'] = ((x($arr,'resource-id')) ? trim($arr['resource-id']) : '');
540 $arr['event-id'] = ((x($arr,'event-id')) ? intval($arr['event-id']) : 0 );
541 $arr['inform'] = ((x($arr,'inform')) ? trim($arr['inform']) : '');
542 $arr['file'] = ((x($arr,'file')) ? trim($arr['file']) : '');
544 // Items cannot be stored before they happen ...
545 if ($arr['created'] > datetime_convert())
546 $arr['created'] = datetime_convert();
548 // We haven't invented time travel by now.
549 if ($arr['edited'] > datetime_convert())
550 $arr['edited'] = datetime_convert();
552 if (($arr['author-link'] == "") AND ($arr['owner-link'] == ""))
553 logger("Both author-link and owner-link are empty. Called by: ".App::callstack(), LOGGER_DEBUG);
555 if ($arr['plink'] == "") {
557 $arr['plink'] = $a->get_baseurl().'/display/'.urlencode($arr['guid']);
560 if ($arr['network'] == "") {
561 $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
562 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
563 dbesc(normalise_link($arr['author-link'])),
568 $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
569 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
570 dbesc(normalise_link($arr['author-link']))
574 $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
575 intval($arr['contact-id']),
580 $arr['network'] = $r[0]["network"];
582 // Fallback to friendica (why is it empty in some cases?)
583 if ($arr['network'] == "")
584 $arr['network'] = NETWORK_DFRN;
586 logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
589 // The contact-id should be set before "item_store" was called - but there seems to be some issues
590 if ($arr["contact-id"] == 0) {
591 // First we are looking for a suitable contact that matches with the author of the post
592 // This is done only for comments (See below explanation at "gcontact-id")
593 if ($arr['parent-uri'] != $arr['uri'])
594 $arr["contact-id"] = get_contact($arr['author-link'], $uid);
596 // If not present then maybe the owner was found
597 if ($arr["contact-id"] == 0)
598 $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
600 // Still missing? Then use the "self" contact of the current user
601 if ($arr["contact-id"] == 0) {
602 $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
604 $arr["contact-id"] = $r[0]["id"];
606 logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
609 if ($arr["gcontact-id"] == 0) {
610 // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
611 // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
612 // On comments the author is the better choice.
613 if ($arr['parent-uri'] === $arr['uri'])
614 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
615 "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
617 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
618 "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
621 if ($arr["author-id"] == 0)
622 $arr["author-id"] = get_contact($arr["author-link"], 0);
624 if ($arr["owner-id"] == 0)
625 $arr["owner-id"] = get_contact($arr["owner-link"], 0);
627 if ($arr['guid'] != "") {
628 // Checking if there is already an item with the same guid
629 logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
630 $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
631 dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
634 logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
639 // Check for hashtags in the body and repair or add hashtag links
640 item_body_set_hashtags($arr);
642 $arr['thr-parent'] = $arr['parent-uri'];
643 if ($arr['parent-uri'] === $arr['uri']) {
646 $allow_cid = $arr['allow_cid'];
647 $allow_gid = $arr['allow_gid'];
648 $deny_cid = $arr['deny_cid'];
649 $deny_gid = $arr['deny_gid'];
650 $notify_type = 'wall-new';
653 // find the parent and snarf the item id and ACLs
654 // and anything else we need to inherit
656 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
657 dbesc($arr['parent-uri']),
663 // is the new message multi-level threaded?
664 // even though we don't support it now, preserve the info
665 // and re-attach to the conversation parent.
667 if ($r[0]['uri'] != $r[0]['parent-uri']) {
668 $arr['parent-uri'] = $r[0]['parent-uri'];
669 $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
670 ORDER BY `id` ASC LIMIT 1",
671 dbesc($r[0]['parent-uri']),
672 dbesc($r[0]['parent-uri']),
679 $parent_id = $r[0]['id'];
680 $parent_deleted = $r[0]['deleted'];
681 $allow_cid = $r[0]['allow_cid'];
682 $allow_gid = $r[0]['allow_gid'];
683 $deny_cid = $r[0]['deny_cid'];
684 $deny_gid = $r[0]['deny_gid'];
685 $arr['wall'] = $r[0]['wall'];
686 $notify_type = 'comment-new';
688 // if the parent is private, force privacy for the entire conversation
689 // This differs from the above settings as it subtly allows comments from
690 // email correspondents to be private even if the overall thread is not.
692 if ($r[0]['private'])
693 $arr['private'] = $r[0]['private'];
695 // Edge case. We host a public forum that was originally posted to privately.
696 // The original author commented, but as this is a comment, the permissions
697 // weren't fixed up so it will still show the comment as private unless we fix it here.
699 if ((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
703 // If its a post from myself then tag the thread as "mention"
704 logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
705 $u = q("SELECT `nickname` FROM `user` WHERE `uid` = %d", intval($arr['uid']));
708 $self = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
709 logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
710 if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
711 q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
712 logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
717 // Allow one to see reply tweets from status.net even when
718 // we don't have or can't see the original post.
721 logger('item_store: $force_parent=true, reply converted to top-level post.');
723 $arr['parent-uri'] = $arr['uri'];
726 logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
734 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
736 dbesc($arr['network']),
740 if (dbm::is_result($r)) {
741 logger('duplicated item with the same uri found. '.print_r($arr,true));
745 // On Friendica and Diaspora the GUID is unique
746 if (in_array($arr['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) {
747 $r = q("SELECT `id` FROM `item` WHERE `guid` = '%s' AND `uid` = %d LIMIT 1",
751 if (dbm::is_result($r)) {
752 logger('duplicated item with the same guid found. '.print_r($arr,true));
756 // Check for an existing post with the same content. There seems to be a problem with OStatus.
757 $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
759 dbesc($arr['network']),
760 dbesc($arr['created']),
761 intval($arr['contact-id']),
764 if (dbm::is_result($r)) {
765 logger('duplicated item with the same body found. '.print_r($arr,true));
770 // Is this item available in the global items (with uid=0)?
771 if ($arr["uid"] == 0) {
772 $arr["global"] = true;
774 // Set the global flag on all items if this was a global item entry
775 q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
777 $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
779 $arr["global"] = (count($isglobal) > 0);
783 if (strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
786 $private = $arr['private'];
788 $arr["allow_cid"] = $allow_cid;
789 $arr["allow_gid"] = $allow_gid;
790 $arr["deny_cid"] = $deny_cid;
791 $arr["deny_gid"] = $deny_gid;
792 $arr["private"] = $private;
793 $arr["deleted"] = $parent_deleted;
795 // Fill the cache field
796 put_item_in_cache($arr);
799 call_hooks('post_local',$arr);
801 call_hooks('post_remote',$arr);
803 if (x($arr,'cancel')) {
804 logger('item_store: post cancelled by plugin.');
808 // Check for already added items.
809 // There is a timing issue here that sometimes creates double postings.
810 // An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this.
811 if ($arr["uid"] == 0) {
812 $r = qu("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = 0 LIMIT 1", dbesc(trim($arr['uri'])));
813 if (dbm::is_result($r)) {
814 logger('Global item already stored. URI: '.$arr['uri'].' on network '.$arr['network'], LOGGER_DEBUG);
819 // Store the unescaped version
824 logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
827 q("START TRANSACTION;");
829 $r = dbq("INSERT INTO `item` (`"
830 . implode("`, `", array_keys($arr))
832 . implode("', '", array_values($arr))
838 // When the item was successfully stored we fetch the ID of the item.
839 if (dbm::is_result($r)) {
840 $r = q("SELECT LAST_INSERT_ID() AS `item-id`");
841 if (dbm::is_result($r)) {
842 $current_post = $r[0]['item-id'];
844 // This shouldn't happen
848 // This can happen - for example - if there are locking timeouts.
851 // Store the data into a spool file so that we can try again later.
853 // At first we restore the Diaspora signature that we removed above.
854 if (isset($encoded_signature)) {
855 $arr['dsprsig'] = $encoded_signature;
858 // Now we store the data in the spool directory
859 $file = 'item-'.round(microtime(true) * 10000).".msg";
860 $spool = get_spoolpath().'/'.$file;
861 file_put_contents($spool, json_encode($arr));
862 logger("Item wasn't stored - Item was spooled into file ".$file, LOGGER_DEBUG);
866 if ($current_post == 0) {
867 // This is one of these error messages that never should occur.
868 logger("couldn't find created item - we better quit now.");
873 // How much entries have we created?
874 // We wouldn't need this query when we could use an unique index - but MySQL has length problems with them.
875 $r = q("SELECT COUNT(*) AS `entries` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s'",
878 dbesc($arr['network'])
881 if (!dbm::is_result($r)) {
882 // This shouldn't happen, since COUNT always works when the database connection is there.
883 logger("We couldn't count the stored entries. Very strange ...");
888 if ($r[0]["entries"] > 1) {
889 // There are duplicates. We delete our just created entry.
890 logger('Duplicated post occurred. uri = '.$arr['uri'].' uid = '.$arr['uid']);
892 // Yes, we could do a rollback here - but we are having many users with MyISAM.
893 q("DELETE FROM `item` WHERE `id` = %d", intval($current_post));
896 } elseif ($r[0]["entries"] == 0) {
897 // This really should never happen since we quit earlier if there were problems.
898 logger("Something is terribly wrong. We haven't found our created entry.");
903 logger('item_store: created item '.$current_post);
904 item_set_last_item($arr);
906 if (!$parent_id || ($arr['parent-uri'] === $arr['uri']))
907 $parent_id = $current_post;
910 $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
912 intval($current_post)
915 $arr['id'] = $current_post;
916 $arr['parent'] = $parent_id;
918 // update the commented timestamp on the parent
919 // Only update "commented" if it is really a comment
920 if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
921 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
922 dbesc(datetime_convert()),
923 dbesc(datetime_convert()),
927 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
928 dbesc(datetime_convert()),
934 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
935 // We can check for this condition when we decode and encode the stuff again.
936 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
937 $dsprsig->signature = base64_decode($dsprsig->signature);
938 logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
941 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
942 intval($current_post),
943 dbesc($dsprsig->signed_text),
944 dbesc($dsprsig->signature),
945 dbesc($dsprsig->signer)
949 $deleted = tag_deliver($arr['uid'],$current_post);
951 // current post can be deleted if is for a community page and no mention are
953 if (!$deleted AND !$dontcache) {
955 $r = q('SELECT * FROM `item` WHERE id = %d', intval($current_post));
956 if (count($r) == 1) {
958 call_hooks('post_local_end', $r[0]);
960 call_hooks('post_remote_end', $r[0]);
962 logger('item_store: new item not found in DB, id ' . $current_post);
965 if ($arr['parent-uri'] === $arr['uri']) {
966 add_thread($current_post);
968 update_thread($parent_id);
973 // Due to deadlock issues with the "term" table we are doing these steps after the commit.
974 // This is not perfect - but a workable solution until we found the reason for the problem.
975 create_tags_from_item($current_post);
976 create_files_from_item($current_post);
978 // If this is now the last-child, force all _other_ children of this parent to *not* be last-child
979 // It is done after the transaction to avoid dead locks.
980 if ($arr['last-child']) {
981 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
984 intval($current_post)
988 if ($arr['parent-uri'] === $arr['uri']) {
989 add_shadow_thread($current_post);
991 add_shadow_entry($current_post);
994 check_item_notification($current_post, $uid);
997 proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
999 return $current_post;
1003 * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
1005 * This can be used to filter for inactive contacts.
1006 * Only do this for public postings to avoid privacy problems, since poco data is public.
1007 * Don't set this value if it isn't from the owner (could be an author that we don't know)
1009 * @param array $arr Contains the just posted item record
1011 function item_set_last_item($arr) {
1013 $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
1015 // Is it a forum? Then we don't care about the rules from above
1016 if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
1017 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
1018 intval($arr['contact-id']));
1025 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1026 dbesc($arr['received']),
1027 dbesc($arr['received']),
1028 intval($arr['contact-id'])
1031 // Now do the same for the system wide contacts with uid=0
1032 if (!$arr['private']) {
1033 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1034 dbesc($arr['received']),
1035 dbesc($arr['received']),
1036 intval($arr['owner-id'])
1039 if ($arr['owner-id'] != $arr['author-id']) {
1040 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1041 dbesc($arr['received']),
1042 dbesc($arr['received']),
1043 intval($arr['author-id'])
1049 function item_body_set_hashtags(&$item) {
1051 $tags = get_tags($item["body"]);
1057 // This sorting is important when there are hashtags that are part of other hashtags
1058 // Otherwise there could be problems with hashtags like #test and #test2
1063 $URLSearchString = "^\[\]";
1065 // All hashtags should point to the home server
1066 //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1067 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
1069 //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1070 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
1072 // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
1073 $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1075 return("[url=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/url]");
1078 $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
1080 return("[bookmark=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/bookmark]");
1083 $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
1085 return("[attachment ".str_replace("#", "#", $match[1])."]".$match[2]."[/attachment]");
1088 // Repair recursive urls
1089 $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1090 "#$2", $item["body"]);
1093 foreach($tags as $tag) {
1094 if (strpos($tag,'#') !== 0)
1097 if (strpos($tag,'[url='))
1100 $basetag = str_replace('_',' ',substr($tag,1));
1102 $newtag = '#[url='.$a->get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1104 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1106 if (!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
1107 if (strlen($item["tag"]))
1108 $item["tag"] = ','.$item["tag"];
1109 $item["tag"] = $newtag.$item["tag"];
1113 // Convert back the masked hashtags
1114 $item["body"] = str_replace("#", "#", $item["body"]);
1117 function get_item_guid($id) {
1118 $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1120 return($r[0]["guid"]);
1125 function get_item_id($guid, $uid = 0) {
1131 $uid == local_user();
1133 // Does the given user have this item?
1135 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1136 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1137 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1140 $nick = $r[0]["nickname"];
1144 // Or is it anywhere on the server?
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`.`allow_cid` = '' AND `item`.`allow_gid` = ''
1149 AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
1150 AND `item`.`private` = 0 AND `item`.`wall` = 1
1151 AND `item`.`guid` = '%s'", dbesc($guid));
1154 $nick = $r[0]["nickname"];
1157 return(array("nick" => $nick, "id" => $id));
1161 function get_item_contact($item,$contacts) {
1162 if (! count($contacts) || (! is_array($item)))
1164 foreach($contacts as $contact) {
1165 if ($contact['id'] == $item['contact-id']) {
1167 break; // NOTREACHED
1174 * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1176 * @param int $item_id
1177 * @return bool true if item was deleted, else false
1179 function tag_deliver($uid,$item_id) {
1187 $u = q("select * from user where uid = %d limit 1",
1193 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1194 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1197 $i = q("select * from item where id = %d and uid = %d limit 1",
1206 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1208 // Diaspora uses their own hardwired link URL in @-tags
1209 // instead of the one we supply with webfinger
1211 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1213 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1215 foreach($matches as $mtch) {
1216 if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1218 logger('tag_deliver: mention found: ' . $mtch[2]);
1224 if ( ($community_page || $prvgroup) &&
1225 (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1226 // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1228 logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1229 q("DELETE FROM item WHERE id = %d and uid = %d",
1238 $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1240 call_hooks('tagged', $arr);
1242 if ((! $community_page) && (! $prvgroup))
1246 // tgroup delivery - setup a second delivery chain
1247 // prevent delivery looping - only proceed
1248 // if the message originated elsewhere and is a top-level post
1250 if (($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1253 // now change this copy of the post to a forum head message and deliver to all the tgroup members
1256 $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1257 intval($u[0]['uid'])
1262 // also reset all the privacy bits to the forum default permissions
1264 $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1266 $forum_mode = (($prvgroup) ? 2 : 1);
1268 q("update item set wall = 1, origin = 1, forum_mode = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1269 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' where id = %d",
1270 intval($forum_mode),
1271 dbesc($c[0]['name']),
1272 dbesc($c[0]['url']),
1273 dbesc($c[0]['thumb']),
1275 dbesc($u[0]['allow_cid']),
1276 dbesc($u[0]['allow_gid']),
1277 dbesc($u[0]['deny_cid']),
1278 dbesc($u[0]['deny_gid']),
1281 update_thread($item_id);
1283 proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
1289 function tgroup_check($uid,$item) {
1295 // check that the message originated elsewhere and is a top-level post
1297 if (($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1301 $u = q("select * from user where uid = %d limit 1",
1307 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1308 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1311 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1313 // Diaspora uses their own hardwired link URL in @-tags
1314 // instead of the one we supply with webfinger
1316 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1318 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1320 foreach($matches as $mtch) {
1321 if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1323 logger('tgroup_check: mention found: ' . $mtch[2]);
1331 if ((! $community_page) && (! $prvgroup))
1338 This function returns true if $update has an edited timestamp newer
1339 than $existing, i.e. $update contains new data which should override
1340 what's already there. If there is no timestamp yet, the update is
1341 assumed to be newer. If the update has no timestamp, the existing
1342 item is assumed to be up-to-date. If the timestamps are equal it
1343 assumes the update has been seen before and should be ignored.
1345 function edited_timestamp_is_newer($existing, $update) {
1346 if (!x($existing,'edited') || !$existing['edited']) {
1349 if (!x($update,'edited') || !$update['edited']) {
1352 $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1353 $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1354 return (strcmp($existing_edited, $update_edited) < 0);
1359 * consume_feed - process atom feed and update anything/everything we might need to update
1361 * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1363 * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1364 * It is this person's stuff that is going to be updated.
1365 * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1366 * from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1367 * have a contact record.
1368 * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1369 * might not) try and subscribe to it.
1370 * $datedir sorts in reverse order
1371 * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1372 * imported prior to its children being seen in the stream unless we are certain
1373 * of how the feed is arranged/ordered.
1374 * With $pass = 1, we only pull parent items out of the stream.
1375 * With $pass = 2, we only pull children (comments/likes).
1377 * So running this twice, first with pass 1 and then with pass 2 will do the right
1378 * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1379 * model where comments can have sub-threads. That would require some massive sorting
1380 * to get all the feed items into a mostly linear ordering, and might still require
1384 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1385 if ($contact['network'] === NETWORK_OSTATUS) {
1387 // Test - remove before flight
1388 //$tempfile = tempnam(get_temppath(), "ostatus2");
1389 //file_put_contents($tempfile, $xml);
1390 logger("Consume OStatus messages ", LOGGER_DEBUG);
1391 ostatus::import($xml,$importer,$contact, $hub);
1396 if ($contact['network'] === NETWORK_FEED) {
1398 logger("Consume feeds", LOGGER_DEBUG);
1399 feed_import($xml,$importer,$contact, $hub);
1404 if ($contact['network'] === NETWORK_DFRN) {
1405 logger("Consume DFRN messages", LOGGER_DEBUG);
1407 $r = q("SELECT `contact`.*, `contact`.`uid` AS `importer_uid`,
1408 `contact`.`pubkey` AS `cpubkey`,
1409 `contact`.`prvkey` AS `cprvkey`,
1410 `contact`.`thumb` AS `thumb`,
1411 `contact`.`url` as `url`,
1412 `contact`.`name` as `senderName`,
1415 LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1416 WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1417 dbesc($contact["id"]), dbesc($importer["uid"])
1420 logger("Now import the DFRN feed");
1421 dfrn::import($xml,$r[0], true);
1427 function item_is_remote_self($contact, &$datarray) {
1430 if (!$contact['remote_self'])
1433 // Prevent the forwarding of posts that are forwarded
1434 if ($datarray["extid"] == NETWORK_DFRN)
1437 // Prevent to forward already forwarded posts
1438 if ($datarray["app"] == $a->get_hostname())
1441 // Only forward posts
1442 if ($datarray["verb"] != ACTIVITY_POST)
1445 if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1448 $datarray2 = $datarray;
1449 logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1450 if ($contact['remote_self'] == 2) {
1451 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1452 intval($contact['uid']));
1454 $datarray['contact-id'] = $r[0]["id"];
1456 $datarray['owner-name'] = $r[0]["name"];
1457 $datarray['owner-link'] = $r[0]["url"];
1458 $datarray['owner-avatar'] = $r[0]["thumb"];
1460 $datarray['author-name'] = $datarray['owner-name'];
1461 $datarray['author-link'] = $datarray['owner-link'];
1462 $datarray['author-avatar'] = $datarray['owner-avatar'];
1465 if ($contact['network'] != NETWORK_FEED) {
1466 $datarray["guid"] = get_guid(32);
1467 unset($datarray["plink"]);
1468 $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
1469 $datarray["parent-uri"] = $datarray["uri"];
1470 $datarray["extid"] = $contact['network'];
1471 $urlpart = parse_url($datarray2['author-link']);
1472 $datarray["app"] = $urlpart["host"];
1474 $datarray['private'] = 0;
1477 if ($contact['network'] != NETWORK_FEED) {
1478 // Store the original post
1479 $r = item_store($datarray2, false, false);
1480 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1482 $datarray["app"] = "Feed";
1487 function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
1488 $url = notags(trim($datarray['author-link']));
1489 $name = notags(trim($datarray['author-name']));
1490 $photo = notags(trim($datarray['author-avatar']));
1492 if (is_object($item)) {
1493 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1494 if ($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
1495 $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1499 if (is_array($contact)) {
1500 if (($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1501 || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1502 $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1503 intval(CONTACT_IS_FRIEND),
1504 intval($contact['id']),
1505 intval($importer['uid'])
1508 // send email notification to owner?
1511 // create contact record
1513 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1514 `blocked`, `readonly`, `pending`, `writable`)
1515 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1516 intval($importer['uid']),
1517 dbesc(datetime_convert()),
1519 dbesc(normalise_link($url)),
1523 dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1524 intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1526 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1527 intval($importer['uid']),
1531 $contact_record = $r[0];
1532 update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
1536 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1537 intval($importer['uid'])
1540 if (count($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1542 // create notification
1543 $hash = random_string();
1545 if (is_array($contact_record)) {
1546 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1547 VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1548 intval($importer['uid']),
1549 intval($contact_record['id']),
1551 dbesc(datetime_convert())
1555 $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1557 if (intval($def_gid))
1558 group_add_member($importer['uid'],'',$contact_record['id'],$def_gid);
1560 if (($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1561 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1564 'type' => NOTIFY_INTRO,
1565 'notify_flags' => $r[0]['notify-flags'],
1566 'language' => $r[0]['language'],
1567 'to_name' => $r[0]['username'],
1568 'to_email' => $r[0]['email'],
1569 'uid' => $r[0]['uid'],
1570 'link' => $a->get_baseurl() . '/notifications/intro',
1571 'source_name' => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1572 'source_link' => $contact_record['url'],
1573 'source_photo' => $contact_record['photo'],
1574 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1579 } elseif (count($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1580 $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1581 intval($importer['uid']),
1589 function lose_follower($importer,$contact,$datarray = array(),$item = "") {
1591 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1592 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1593 intval(CONTACT_IS_SHARING),
1594 intval($contact['id'])
1597 contact_remove($contact['id']);
1601 function lose_sharer($importer,$contact,$datarray = array(),$item = "") {
1603 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1604 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1605 intval(CONTACT_IS_FOLLOWER),
1606 intval($contact['id'])
1609 contact_remove($contact['id']);
1613 function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
1617 if (is_array($importer)) {
1618 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1619 intval($importer['uid'])
1623 // Diaspora has different message-ids in feeds than they do
1624 // through the direct Diaspora protocol. If we try and use
1625 // the feed, we'll get duplicates. So don't.
1627 if ((! count($r)) || $contact['network'] === NETWORK_DIASPORA)
1630 $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1632 // Use a single verify token, even if multiple hubs
1634 $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1636 $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1638 logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: ' . $push_url . ' with verifier ' . $verify_token);
1640 if (!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1641 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1642 dbesc($verify_token),
1643 intval($contact['id'])
1647 post_url($url,$params);
1649 logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1655 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1657 if (get_config('system','disable_embedded'))
1662 logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1663 $site = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://'));
1668 $img_start = strpos($orig_body, '[img');
1669 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1670 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1671 while( ($img_st_close !== false) && ($img_len !== false) ) {
1673 $img_st_close++; // make it point to AFTER the closing bracket
1674 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1676 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1679 if (stristr($image , $site . '/photo/')) {
1680 // Only embed locally hosted photos
1682 $i = basename($image);
1683 $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
1684 $x = strpos($i,'-');
1687 $res = substr($i,$x+1);
1688 $i = substr($i,0,$x);
1689 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1696 // Check to see if we should replace this photo link with an embedded image
1697 // 1. No need to do so if the photo is public
1698 // 2. If there's a contact-id provided, see if they're in the access list
1699 // for the photo. If so, embed it.
1700 // 3. Otherwise, if we have an item, see if the item permissions match the photo
1701 // permissions, regardless of order but first check to see if they're an exact
1702 // match to save some processing overhead.
1704 if (has_permissions($r[0])) {
1706 $recips = enumerate_permissions($r[0]);
1707 if (in_array($cid, $recips)) {
1711 if (compare_permissions($item,$r[0]))
1716 $data = $r[0]['data'];
1717 $type = $r[0]['type'];
1719 // If a custom width and height were specified, apply before embedding
1720 if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1721 logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1723 $width = intval($match[1]);
1724 $height = intval($match[2]);
1726 $ph = new Photo($data, $type);
1727 if ($ph->is_valid()) {
1728 $ph->scaleImage(max($width, $height));
1729 $data = $ph->imageString();
1730 $type = $ph->getType();
1734 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1735 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1736 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1742 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1743 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1744 if ($orig_body === false)
1747 $img_start = strpos($orig_body, '[img');
1748 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1749 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1752 $new_body = $new_body . $orig_body;
1757 function has_permissions($obj) {
1758 if (($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1763 function compare_permissions($obj1,$obj2) {
1764 // first part is easy. Check that these are exactly the same.
1765 if (($obj1['allow_cid'] == $obj2['allow_cid'])
1766 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1767 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1768 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1771 // This is harder. Parse all the permissions and compare the resulting set.
1773 $recipients1 = enumerate_permissions($obj1);
1774 $recipients2 = enumerate_permissions($obj2);
1777 if ($recipients1 == $recipients2)
1782 // returns an array of contact-ids that are allowed to see this object
1784 function enumerate_permissions($obj) {
1785 $allow_people = expand_acl($obj['allow_cid']);
1786 $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1787 $deny_people = expand_acl($obj['deny_cid']);
1788 $deny_groups = expand_groups(expand_acl($obj['deny_gid']));
1789 $recipients = array_unique(array_merge($allow_people,$allow_groups));
1790 $deny = array_unique(array_merge($deny_people,$deny_groups));
1791 $recipients = array_diff($recipients,$deny);
1795 function item_getfeedtags($item) {
1798 $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1800 for($x = 0; $x < $cnt; $x ++) {
1801 if ($matches[1][$x])
1802 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
1806 $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1808 for($x = 0; $x < $cnt; $x ++) {
1809 if ($matches[1][$x])
1810 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
1816 function item_expire($uid, $days, $network = "", $force = false) {
1818 if ((! $uid) || ($days < 1))
1821 // $expire_network_only = save your own wall posts
1822 // and just expire conversations started by others
1824 $expire_network_only = get_pconfig($uid,'expire','network_only');
1825 $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1827 if ($network != "") {
1828 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1829 // There is an index "uid_network_received" but not "uid_network_created"
1830 // This avoids the creation of another index just for one purpose.
1831 // And it doesn't really matter wether to look at "received" or "created"
1832 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1834 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1836 $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
1837 WHERE `uid` = %d $range
1848 $expire_items = get_pconfig($uid, 'expire','items');
1849 $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1851 // Forcing expiring of items - but not notes and marked items
1853 $expire_items = true;
1855 $expire_notes = get_pconfig($uid, 'expire','notes');
1856 $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1858 $expire_starred = get_pconfig($uid, 'expire','starred');
1859 $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1861 $expire_photos = get_pconfig($uid, 'expire','photos');
1862 $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1864 logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1866 foreach($r as $item) {
1868 // don't expire filed items
1870 if (strpos($item['file'],'[') !== false)
1873 // Only expire posts, not photos and photo comments
1875 if ($expire_photos==0 && strlen($item['resource-id']))
1877 if ($expire_starred==0 && intval($item['starred']))
1879 if ($expire_notes==0 && $item['type']=='note')
1881 if ($expire_items==0 && $item['type']!='note')
1884 drop_item($item['id'],false);
1887 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1892 function drop_items($items) {
1895 if (! local_user() && ! remote_user())
1898 if (count($items)) {
1899 foreach($items as $item) {
1900 $owner = drop_item($item,false);
1901 if ($owner && ! $uid)
1906 // multiple threads may have been deleted, send an expire notification
1909 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1913 function drop_item($id,$interactive = true) {
1917 // locate item to be deleted
1919 $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1926 notice( t('Item not found.') . EOL);
1927 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1932 $owner = $item['uid'];
1936 // check if logged in user is either the author or owner of this item
1938 if (is_array($_SESSION['remote'])) {
1939 foreach($_SESSION['remote'] as $visitor) {
1940 if ($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1941 $contact_id = $visitor['cid'];
1948 if ((local_user() == $item['uid']) || ($contact_id) || (! $interactive)) {
1950 // Check if we should do HTML-based delete confirmation
1951 if ($_REQUEST['confirm']) {
1952 // <form> can't take arguments in its "action" parameter
1953 // so add any arguments as hidden inputs
1954 $query = explode_querystring($a->query_string);
1956 foreach($query['args'] as $arg) {
1957 if (strpos($arg, 'confirm=') === false) {
1958 $arg_parts = explode('=', $arg);
1959 $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1963 return replace_macros(get_markup_template('confirm.tpl'), array(
1965 '$message' => t('Do you really want to delete this item?'),
1966 '$extra_inputs' => $inputs,
1967 '$confirm' => t('Yes'),
1968 '$confirm_url' => $query['base'],
1969 '$confirm_name' => 'confirmed',
1970 '$cancel' => t('Cancel'),
1973 // Now check how the user responded to the confirmation query
1974 if ($_REQUEST['canceled']) {
1975 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1978 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
1981 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
1982 dbesc(datetime_convert()),
1983 dbesc(datetime_convert()),
1986 create_tags_from_item($item['id']);
1987 create_files_from_item($item['id']);
1988 delete_thread($item['id'], $item['parent-uri']);
1990 // clean up categories and tags so they don't end up as orphans
1993 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
1995 foreach($matches as $mtch) {
1996 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
2002 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
2004 foreach($matches as $mtch) {
2005 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
2009 // If item is a link to a photo resource, nuke all the associated photos
2010 // (visitors will not have photo resources)
2011 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
2012 // generate a resource-id and therefore aren't intimately linked to the item.
2014 if (strlen($item['resource-id'])) {
2015 q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
2016 dbesc($item['resource-id']),
2017 intval($item['uid'])
2019 // ignore the result
2022 // If item is a link to an event, nuke the event record.
2024 if (intval($item['event-id'])) {
2025 q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
2026 intval($item['event-id']),
2027 intval($item['uid'])
2029 // ignore the result
2032 // If item has attachments, drop them
2034 foreach(explode(",",$item['attach']) as $attach){
2035 preg_match("|attach/(\d+)|", $attach, $matches);
2036 q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
2037 intval($matches[1]),
2040 // ignore the result
2044 // clean up item_id and sign meta-data tables
2047 // Old code - caused very long queries and warning entries in the mysql logfiles:
2049 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
2050 intval($item['id']),
2051 intval($item['uid'])
2054 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
2055 intval($item['id']),
2056 intval($item['uid'])
2060 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
2062 // Creating list of parents
2063 $r = q("select id from item where parent = %d and uid = %d",
2064 intval($item['id']),
2065 intval($item['uid'])
2070 foreach ($r AS $row) {
2071 if ($parentid != "")
2074 $parentid .= $row["id"];
2078 if ($parentid != "") {
2079 $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
2081 $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
2084 // If it's the parent of a comment thread, kill all the kids
2086 if ($item['uri'] == $item['parent-uri']) {
2087 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
2088 WHERE `parent-uri` = '%s' AND `uid` = %d ",
2089 dbesc(datetime_convert()),
2090 dbesc(datetime_convert()),
2091 dbesc($item['parent-uri']),
2092 intval($item['uid'])
2094 create_tags_from_itemuri($item['parent-uri'], $item['uid']);
2095 create_files_from_itemuri($item['parent-uri'], $item['uid']);
2096 delete_thread_uri($item['parent-uri'], $item['uid']);
2097 // ignore the result
2099 // ensure that last-child is set in case the comment that had it just got wiped.
2100 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2101 dbesc(datetime_convert()),
2102 dbesc($item['parent-uri']),
2103 intval($item['uid'])
2105 // who is the last child now?
2106 $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",
2107 dbesc($item['parent-uri']),
2108 intval($item['uid'])
2111 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2117 $drop_id = intval($item['id']);
2119 // send the notification upstream/downstream as the case may be
2121 proc_run(PRIORITY_HIGH,"include/notifier.php", "drop", $drop_id);
2125 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2130 notice( t('Permission denied.') . EOL);
2131 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2138 function first_post_date($uid,$wall = false) {
2139 $r = q("select id, created from item
2140 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
2142 order by created asc limit 1",
2144 intval($wall ? 1 : 0)
2147 // logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2148 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
2153 /* modified posted_dates() {below} to arrange the list in years */
2154 function list_post_dates($uid, $wall) {
2155 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2157 $dthen = first_post_date($uid, $wall);
2161 // Set the start and end date to the beginning of the month
2162 $dnow = substr($dnow,0,8).'01';
2163 $dthen = substr($dthen,0,8).'01';
2167 // Starting with the current month, get the first and last days of every
2168 // month down to and including the month of the first post
2169 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2170 $dyear = intval(substr($dnow,0,4));
2171 $dstart = substr($dnow,0,8) . '01';
2172 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2173 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2174 $end_month = datetime_convert('','',$dend,'Y-m-d');
2175 $str = day_translate(datetime_convert('','',$dnow,'F'));
2177 $ret[$dyear] = array();
2178 $ret[$dyear][] = array($str,$end_month,$start_month);
2179 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2184 function posted_dates($uid,$wall) {
2185 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2187 $dthen = first_post_date($uid,$wall);
2191 // Set the start and end date to the beginning of the month
2192 $dnow = substr($dnow,0,8).'01';
2193 $dthen = substr($dthen,0,8).'01';
2196 // Starting with the current month, get the first and last days of every
2197 // month down to and including the month of the first post
2198 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2199 $dstart = substr($dnow,0,8) . '01';
2200 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2201 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2202 $end_month = datetime_convert('','',$dend,'Y-m-d');
2203 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
2204 $ret[] = array($str,$end_month,$start_month);
2205 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2211 function posted_date_widget($url,$uid,$wall) {
2214 if (! feature_enabled($uid,'archives'))
2217 // For former Facebook folks that left because of "timeline"
2219 /* if ($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2222 $visible_years = get_pconfig($uid,'system','archive_visible_years');
2223 if (! $visible_years)
2226 $ret = list_post_dates($uid,$wall);
2231 $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2232 $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
2234 $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2235 '$title' => t('Archives'),
2236 '$size' => $visible_years,
2237 '$cutoff_year' => $cutoff_year,
2238 '$cutoff' => $cutoff,
2241 '$showmore' => t('show more')