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 $dsprsig = json_decode(base64_decode($arr['dsprsig']));
421 unset($arr['dsprsig']);
424 // Converting the plink
425 if ($arr['network'] == NETWORK_OSTATUS) {
426 if (isset($arr['plink']))
427 $arr['plink'] = ostatus::convert_href($arr['plink']);
428 elseif (isset($arr['uri']))
429 $arr['plink'] = ostatus::convert_href($arr['uri']);
432 if (x($arr, 'gravity'))
433 $arr['gravity'] = intval($arr['gravity']);
434 elseif ($arr['parent-uri'] === $arr['uri'])
436 elseif (activity_match($arr['verb'],ACTIVITY_POST))
439 $arr['gravity'] = 6; // extensible catchall
441 if (! x($arr,'type'))
442 $arr['type'] = 'remote';
446 /* check for create date and expire time */
447 $uid = intval($arr['uid']);
448 $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
450 $expire_interval = $r[0]['expire'];
451 if ($expire_interval>0) {
452 $expire_date = new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
453 $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
454 if ($created_date < $expire_date) {
455 logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
461 // Do we already have this item?
462 // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
463 if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
464 $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s') LIMIT 1",
465 dbesc(trim($arr['uri'])),
467 dbesc(NETWORK_DIASPORA),
469 dbesc(NETWORK_OSTATUS)
472 // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
474 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']);
479 // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
480 // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
481 //if ((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
482 // $arr['body'] = strip_tags($arr['body']);
484 item_add_language_opt($arr);
488 elseif ((trim($arr['guid']) == "") AND (trim($arr['plink']) != ""))
489 $arr['guid'] = uri_to_guid($arr['plink']);
490 elseif ((trim($arr['guid']) == "") AND (trim($arr['uri']) != ""))
491 $arr['guid'] = uri_to_guid($arr['uri']);
493 $parsed = parse_url($arr["author-link"]);
494 $guid_prefix = hash("crc32", $parsed["host"]);
497 $arr['wall'] = ((x($arr,'wall')) ? intval($arr['wall']) : 0);
498 $arr['guid'] = ((x($arr,'guid')) ? notags(trim($arr['guid'])) : get_guid(32, $guid_prefix));
499 $arr['uri'] = ((x($arr,'uri')) ? notags(trim($arr['uri'])) : item_new_uri($a->get_hostname(), $uid, $arr['guid']));
500 $arr['extid'] = ((x($arr,'extid')) ? notags(trim($arr['extid'])) : '');
501 $arr['author-name'] = ((x($arr,'author-name')) ? trim($arr['author-name']) : '');
502 $arr['author-link'] = ((x($arr,'author-link')) ? notags(trim($arr['author-link'])) : '');
503 $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
504 $arr['owner-name'] = ((x($arr,'owner-name')) ? trim($arr['owner-name']) : '');
505 $arr['owner-link'] = ((x($arr,'owner-link')) ? notags(trim($arr['owner-link'])) : '');
506 $arr['owner-avatar'] = ((x($arr,'owner-avatar')) ? notags(trim($arr['owner-avatar'])) : '');
507 $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert());
508 $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert());
509 $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert());
510 $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert());
511 $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert());
512 $arr['title'] = ((x($arr,'title')) ? trim($arr['title']) : '');
513 $arr['location'] = ((x($arr,'location')) ? trim($arr['location']) : '');
514 $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : '');
515 $arr['last-child'] = ((x($arr,'last-child')) ? intval($arr['last-child']) : 0 );
516 $arr['visible'] = ((x($arr,'visible') !== false) ? intval($arr['visible']) : 1 );
518 $arr['parent-uri'] = ((x($arr,'parent-uri')) ? notags(trim($arr['parent-uri'])) : $arr['uri']);
519 $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : '');
520 $arr['object-type'] = ((x($arr,'object-type')) ? notags(trim($arr['object-type'])) : '');
521 $arr['object'] = ((x($arr,'object')) ? trim($arr['object']) : '');
522 $arr['target-type'] = ((x($arr,'target-type')) ? notags(trim($arr['target-type'])) : '');
523 $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : '');
524 $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : '');
525 $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : '');
526 $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : '');
527 $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : '');
528 $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : '');
529 $arr['private'] = ((x($arr,'private')) ? intval($arr['private']) : 0 );
530 $arr['bookmark'] = ((x($arr,'bookmark')) ? intval($arr['bookmark']) : 0 );
531 $arr['body'] = ((x($arr,'body')) ? trim($arr['body']) : '');
532 $arr['tag'] = ((x($arr,'tag')) ? notags(trim($arr['tag'])) : '');
533 $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : '');
534 $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : '');
535 $arr['origin'] = ((x($arr,'origin')) ? intval($arr['origin']) : 0 );
536 $arr['network'] = ((x($arr,'network')) ? trim($arr['network']) : '');
537 $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : '');
538 $arr['resource-id'] = ((x($arr,'resource-id')) ? trim($arr['resource-id']) : '');
539 $arr['event-id'] = ((x($arr,'event-id')) ? intval($arr['event-id']) : 0 );
540 $arr['inform'] = ((x($arr,'inform')) ? trim($arr['inform']) : '');
541 $arr['file'] = ((x($arr,'file')) ? trim($arr['file']) : '');
543 // Items cannot be stored before they happen ...
544 if ($arr['created'] > datetime_convert())
545 $arr['created'] = datetime_convert();
547 // We haven't invented time travel by now.
548 if ($arr['edited'] > datetime_convert())
549 $arr['edited'] = datetime_convert();
551 if (($arr['author-link'] == "") AND ($arr['owner-link'] == ""))
552 logger("Both author-link and owner-link are empty. Called by: ".App::callstack(), LOGGER_DEBUG);
554 if ($arr['plink'] == "") {
556 $arr['plink'] = $a->get_baseurl().'/display/'.urlencode($arr['guid']);
559 if ($arr['network'] == "") {
560 $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
561 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
562 dbesc(normalise_link($arr['author-link'])),
567 $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
568 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
569 dbesc(normalise_link($arr['author-link']))
573 $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
574 intval($arr['contact-id']),
579 $arr['network'] = $r[0]["network"];
581 // Fallback to friendica (why is it empty in some cases?)
582 if ($arr['network'] == "")
583 $arr['network'] = NETWORK_DFRN;
585 logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
588 // The contact-id should be set before "item_store" was called - but there seems to be some issues
589 if ($arr["contact-id"] == 0) {
590 // First we are looking for a suitable contact that matches with the author of the post
591 // This is done only for comments (See below explanation at "gcontact-id")
592 if ($arr['parent-uri'] != $arr['uri'])
593 $arr["contact-id"] = get_contact($arr['author-link'], $uid);
595 // If not present then maybe the owner was found
596 if ($arr["contact-id"] == 0)
597 $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
599 // Still missing? Then use the "self" contact of the current user
600 if ($arr["contact-id"] == 0) {
601 $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
603 $arr["contact-id"] = $r[0]["id"];
605 logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
608 if ($arr["gcontact-id"] == 0) {
609 // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
610 // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
611 // On comments the author is the better choice.
612 if ($arr['parent-uri'] === $arr['uri'])
613 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
614 "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
616 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
617 "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
620 if ($arr["author-id"] == 0)
621 $arr["author-id"] = get_contact($arr["author-link"], 0);
623 if ($arr["owner-id"] == 0)
624 $arr["owner-id"] = get_contact($arr["owner-link"], 0);
626 if ($arr['guid'] != "") {
627 // Checking if there is already an item with the same guid
628 logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
629 $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
630 dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
633 logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
638 // Check for hashtags in the body and repair or add hashtag links
639 item_body_set_hashtags($arr);
641 $arr['thr-parent'] = $arr['parent-uri'];
642 if ($arr['parent-uri'] === $arr['uri']) {
645 $allow_cid = $arr['allow_cid'];
646 $allow_gid = $arr['allow_gid'];
647 $deny_cid = $arr['deny_cid'];
648 $deny_gid = $arr['deny_gid'];
649 $notify_type = 'wall-new';
652 // find the parent and snarf the item id and ACLs
653 // and anything else we need to inherit
655 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
656 dbesc($arr['parent-uri']),
662 // is the new message multi-level threaded?
663 // even though we don't support it now, preserve the info
664 // and re-attach to the conversation parent.
666 if ($r[0]['uri'] != $r[0]['parent-uri']) {
667 $arr['parent-uri'] = $r[0]['parent-uri'];
668 $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
669 ORDER BY `id` ASC LIMIT 1",
670 dbesc($r[0]['parent-uri']),
671 dbesc($r[0]['parent-uri']),
678 $parent_id = $r[0]['id'];
679 $parent_deleted = $r[0]['deleted'];
680 $allow_cid = $r[0]['allow_cid'];
681 $allow_gid = $r[0]['allow_gid'];
682 $deny_cid = $r[0]['deny_cid'];
683 $deny_gid = $r[0]['deny_gid'];
684 $arr['wall'] = $r[0]['wall'];
685 $notify_type = 'comment-new';
687 // if the parent is private, force privacy for the entire conversation
688 // This differs from the above settings as it subtly allows comments from
689 // email correspondents to be private even if the overall thread is not.
691 if ($r[0]['private'])
692 $arr['private'] = $r[0]['private'];
694 // Edge case. We host a public forum that was originally posted to privately.
695 // The original author commented, but as this is a comment, the permissions
696 // weren't fixed up so it will still show the comment as private unless we fix it here.
698 if ((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
702 // If its a post from myself then tag the thread as "mention"
703 logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
704 $u = q("SELECT `nickname` FROM `user` WHERE `uid` = %d", intval($arr['uid']));
707 $self = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
708 logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
709 if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
710 q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
711 logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
716 // Allow one to see reply tweets from status.net even when
717 // we don't have or can't see the original post.
720 logger('item_store: $force_parent=true, reply converted to top-level post.');
722 $arr['parent-uri'] = $arr['uri'];
725 logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
733 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
735 dbesc($arr['network']),
739 if (dbm::is_result($r)) {
740 logger('duplicated item with the same uri found. '.print_r($arr,true));
744 // On Friendica and Diaspora the GUID is unique
745 if (in_array($arr['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) {
746 $r = q("SELECT `id` FROM `item` WHERE `guid` = '%s' AND `uid` = %d LIMIT 1",
750 if (dbm::is_result($r)) {
751 logger('duplicated item with the same guid found. '.print_r($arr,true));
755 // Check for an existing post with the same content. There seems to be a problem with OStatus.
756 $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
758 dbesc($arr['network']),
759 dbesc($arr['created']),
760 intval($arr['contact-id']),
763 if (dbm::is_result($r)) {
764 logger('duplicated item with the same body found. '.print_r($arr,true));
769 // Is this item available in the global items (with uid=0)?
770 if ($arr["uid"] == 0) {
771 $arr["global"] = true;
773 // Set the global flag on all items if this was a global item entry
774 q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
776 $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
778 $arr["global"] = (count($isglobal) > 0);
782 if (strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
785 $private = $arr['private'];
787 $arr["allow_cid"] = $allow_cid;
788 $arr["allow_gid"] = $allow_gid;
789 $arr["deny_cid"] = $deny_cid;
790 $arr["deny_gid"] = $deny_gid;
791 $arr["private"] = $private;
792 $arr["deleted"] = $parent_deleted;
794 // Fill the cache field
795 put_item_in_cache($arr);
798 call_hooks('post_local',$arr);
800 call_hooks('post_remote',$arr);
802 if (x($arr,'cancel')) {
803 logger('item_store: post cancelled by plugin.');
807 // Check for already added items.
808 // There is a timing issue here that sometimes creates double postings.
809 // An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this.
810 if ($arr["uid"] == 0) {
811 $r = qu("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = 0 LIMIT 1", dbesc(trim($arr['uri'])));
812 if (dbm::is_result($r)) {
813 logger('Global item already stored. URI: '.$arr['uri'].' on network '.$arr['network'], LOGGER_DEBUG);
818 // Store the unescaped version
823 logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
826 q("START TRANSACTION;");
828 $r = dbq("INSERT INTO `item` (`"
829 . implode("`, `", array_keys($arr))
831 . implode("', '", array_values($arr))
837 // When the item was successfully stored we fetch the ID of the item.
838 if (dbm::is_result($r)) {
839 $r = q("SELECT LAST_INSERT_ID() AS `item-id`");
840 if (dbm::is_result($r)) {
841 $current_post = $r[0]['item-id'];
843 // This shouldn't happen
847 // This can happen - for example - if there are locking timeouts.
848 logger("Item wasn't stored - we quit here.");
853 if ($current_post == 0) {
854 // This is one of these error messages that never should occur.
855 logger("couldn't find created item - we better quit now.");
860 // How much entries have we created?
861 // We wouldn't need this query when we could use an unique index - but MySQL has length problems with them.
862 $r = q("SELECT COUNT(*) AS `entries` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s'",
865 dbesc($arr['network'])
868 if (!dbm::is_result($r)) {
869 // This shouldn't happen, since COUNT always works when the database connection is there.
870 logger("We couldn't count the stored entries. Very strange ...");
875 if ($r[0]["entries"] > 1) {
876 // There are duplicates. We delete our just created entry.
877 logger('Duplicated post occurred. uri = '.$arr['uri'].' uid = '.$arr['uid']);
879 // Yes, we could do a rollback here - but we are having many users with MyISAM.
880 q("DELETE FROM `item` WHERE `id` = %d", intval($current_post));
883 } elseif ($r[0]["entries"] == 0) {
884 // This really should never happen since we quit earlier if there were problems.
885 logger("Something is terribly wrong. We haven't found our created entry.");
890 logger('item_store: created item '.$current_post);
891 item_set_last_item($arr);
893 if (!$parent_id || ($arr['parent-uri'] === $arr['uri']))
894 $parent_id = $current_post;
897 $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
899 intval($current_post)
902 $arr['id'] = $current_post;
903 $arr['parent'] = $parent_id;
905 // update the commented timestamp on the parent
906 // Only update "commented" if it is really a comment
907 if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
908 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
909 dbesc(datetime_convert()),
910 dbesc(datetime_convert()),
914 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
915 dbesc(datetime_convert()),
921 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
922 // We can check for this condition when we decode and encode the stuff again.
923 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
924 $dsprsig->signature = base64_decode($dsprsig->signature);
925 logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
928 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
929 intval($current_post),
930 dbesc($dsprsig->signed_text),
931 dbesc($dsprsig->signature),
932 dbesc($dsprsig->signer)
936 $deleted = tag_deliver($arr['uid'],$current_post);
938 // current post can be deleted if is for a community page and no mention are
940 if (!$deleted AND !$dontcache) {
942 $r = q('SELECT * FROM `item` WHERE id = %d', intval($current_post));
943 if (count($r) == 1) {
945 call_hooks('post_local_end', $r[0]);
947 call_hooks('post_remote_end', $r[0]);
949 logger('item_store: new item not found in DB, id ' . $current_post);
952 if ($arr['parent-uri'] === $arr['uri']) {
953 add_thread($current_post);
955 update_thread($parent_id);
960 // Due to deadlock issues with the "term" table we are doing these steps after the commit.
961 // This is not perfect - but a workable solution until we found the reason for the problem.
962 create_tags_from_item($current_post);
963 create_files_from_item($current_post);
965 // If this is now the last-child, force all _other_ children of this parent to *not* be last-child
966 // It is done after the transaction to avoid dead locks.
967 if ($arr['last-child']) {
968 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
971 intval($current_post)
975 if ($arr['parent-uri'] === $arr['uri']) {
976 add_shadow_thread($current_post);
978 add_shadow_entry($current_post);
981 check_item_notification($current_post, $uid);
984 proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
986 return $current_post;
990 * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
992 * This can be used to filter for inactive contacts.
993 * Only do this for public postings to avoid privacy problems, since poco data is public.
994 * Don't set this value if it isn't from the owner (could be an author that we don't know)
996 * @param array $arr Contains the just posted item record
998 function item_set_last_item($arr) {
1000 $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
1002 // Is it a forum? Then we don't care about the rules from above
1003 if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
1004 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
1005 intval($arr['contact-id']));
1012 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1013 dbesc($arr['received']),
1014 dbesc($arr['received']),
1015 intval($arr['contact-id'])
1018 // Now do the same for the system wide contacts with uid=0
1019 if (!$arr['private']) {
1020 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1021 dbesc($arr['received']),
1022 dbesc($arr['received']),
1023 intval($arr['owner-id'])
1026 if ($arr['owner-id'] != $arr['author-id']) {
1027 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1028 dbesc($arr['received']),
1029 dbesc($arr['received']),
1030 intval($arr['author-id'])
1036 function item_body_set_hashtags(&$item) {
1038 $tags = get_tags($item["body"]);
1044 // This sorting is important when there are hashtags that are part of other hashtags
1045 // Otherwise there could be problems with hashtags like #test and #test2
1050 $URLSearchString = "^\[\]";
1052 // All hashtags should point to the home server
1053 //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1054 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
1056 //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1057 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
1059 // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
1060 $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1062 return("[url=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/url]");
1065 $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
1067 return("[bookmark=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/bookmark]");
1070 $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
1072 return("[attachment ".str_replace("#", "#", $match[1])."]".$match[2]."[/attachment]");
1075 // Repair recursive urls
1076 $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1077 "#$2", $item["body"]);
1080 foreach($tags as $tag) {
1081 if (strpos($tag,'#') !== 0)
1084 if (strpos($tag,'[url='))
1087 $basetag = str_replace('_',' ',substr($tag,1));
1089 $newtag = '#[url='.$a->get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1091 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1093 if (!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
1094 if (strlen($item["tag"]))
1095 $item["tag"] = ','.$item["tag"];
1096 $item["tag"] = $newtag.$item["tag"];
1100 // Convert back the masked hashtags
1101 $item["body"] = str_replace("#", "#", $item["body"]);
1104 function get_item_guid($id) {
1105 $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1107 return($r[0]["guid"]);
1112 function get_item_id($guid, $uid = 0) {
1118 $uid == local_user();
1120 // Does the given user have this item?
1122 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1123 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1124 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1127 $nick = $r[0]["nickname"];
1131 // Or is it anywhere on the server?
1133 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1134 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1135 AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = ''
1136 AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
1137 AND `item`.`private` = 0 AND `item`.`wall` = 1
1138 AND `item`.`guid` = '%s'", dbesc($guid));
1141 $nick = $r[0]["nickname"];
1144 return(array("nick" => $nick, "id" => $id));
1148 function get_item_contact($item,$contacts) {
1149 if (! count($contacts) || (! is_array($item)))
1151 foreach($contacts as $contact) {
1152 if ($contact['id'] == $item['contact-id']) {
1154 break; // NOTREACHED
1161 * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1163 * @param int $item_id
1164 * @return bool true if item was deleted, else false
1166 function tag_deliver($uid,$item_id) {
1174 $u = q("select * from user where uid = %d limit 1",
1180 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1181 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1184 $i = q("select * from item where id = %d and uid = %d limit 1",
1193 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1195 // Diaspora uses their own hardwired link URL in @-tags
1196 // instead of the one we supply with webfinger
1198 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1200 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1202 foreach($matches as $mtch) {
1203 if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1205 logger('tag_deliver: mention found: ' . $mtch[2]);
1211 if ( ($community_page || $prvgroup) &&
1212 (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1213 // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1215 logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1216 q("DELETE FROM item WHERE id = %d and uid = %d",
1225 $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1227 call_hooks('tagged', $arr);
1229 if ((! $community_page) && (! $prvgroup))
1233 // tgroup delivery - setup a second delivery chain
1234 // prevent delivery looping - only proceed
1235 // if the message originated elsewhere and is a top-level post
1237 if (($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1240 // now change this copy of the post to a forum head message and deliver to all the tgroup members
1243 $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1244 intval($u[0]['uid'])
1249 // also reset all the privacy bits to the forum default permissions
1251 $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1253 $forum_mode = (($prvgroup) ? 2 : 1);
1255 q("update item set wall = 1, origin = 1, forum_mode = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1256 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' where id = %d",
1257 intval($forum_mode),
1258 dbesc($c[0]['name']),
1259 dbesc($c[0]['url']),
1260 dbesc($c[0]['thumb']),
1262 dbesc($u[0]['allow_cid']),
1263 dbesc($u[0]['allow_gid']),
1264 dbesc($u[0]['deny_cid']),
1265 dbesc($u[0]['deny_gid']),
1268 update_thread($item_id);
1270 proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
1276 function tgroup_check($uid,$item) {
1282 // check that the message originated elsewhere and is a top-level post
1284 if (($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1288 $u = q("select * from user where uid = %d limit 1",
1294 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1295 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1298 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1300 // Diaspora uses their own hardwired link URL in @-tags
1301 // instead of the one we supply with webfinger
1303 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1305 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1307 foreach($matches as $mtch) {
1308 if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1310 logger('tgroup_check: mention found: ' . $mtch[2]);
1318 if ((! $community_page) && (! $prvgroup))
1325 This function returns true if $update has an edited timestamp newer
1326 than $existing, i.e. $update contains new data which should override
1327 what's already there. If there is no timestamp yet, the update is
1328 assumed to be newer. If the update has no timestamp, the existing
1329 item is assumed to be up-to-date. If the timestamps are equal it
1330 assumes the update has been seen before and should be ignored.
1332 function edited_timestamp_is_newer($existing, $update) {
1333 if (!x($existing,'edited') || !$existing['edited']) {
1336 if (!x($update,'edited') || !$update['edited']) {
1339 $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1340 $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1341 return (strcmp($existing_edited, $update_edited) < 0);
1346 * consume_feed - process atom feed and update anything/everything we might need to update
1348 * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1350 * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1351 * It is this person's stuff that is going to be updated.
1352 * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1353 * from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1354 * have a contact record.
1355 * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1356 * might not) try and subscribe to it.
1357 * $datedir sorts in reverse order
1358 * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1359 * imported prior to its children being seen in the stream unless we are certain
1360 * of how the feed is arranged/ordered.
1361 * With $pass = 1, we only pull parent items out of the stream.
1362 * With $pass = 2, we only pull children (comments/likes).
1364 * So running this twice, first with pass 1 and then with pass 2 will do the right
1365 * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1366 * model where comments can have sub-threads. That would require some massive sorting
1367 * to get all the feed items into a mostly linear ordering, and might still require
1371 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1372 if ($contact['network'] === NETWORK_OSTATUS) {
1374 // Test - remove before flight
1375 //$tempfile = tempnam(get_temppath(), "ostatus2");
1376 //file_put_contents($tempfile, $xml);
1377 logger("Consume OStatus messages ", LOGGER_DEBUG);
1378 ostatus::import($xml,$importer,$contact, $hub);
1383 if ($contact['network'] === NETWORK_FEED) {
1385 logger("Consume feeds", LOGGER_DEBUG);
1386 feed_import($xml,$importer,$contact, $hub);
1391 if ($contact['network'] === NETWORK_DFRN) {
1392 logger("Consume DFRN messages", LOGGER_DEBUG);
1394 $r = q("SELECT `contact`.*, `contact`.`uid` AS `importer_uid`,
1395 `contact`.`pubkey` AS `cpubkey`,
1396 `contact`.`prvkey` AS `cprvkey`,
1397 `contact`.`thumb` AS `thumb`,
1398 `contact`.`url` as `url`,
1399 `contact`.`name` as `senderName`,
1402 LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1403 WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1404 dbesc($contact["id"]), dbesc($importer["uid"])
1407 logger("Now import the DFRN feed");
1408 dfrn::import($xml,$r[0], true);
1414 function item_is_remote_self($contact, &$datarray) {
1417 if (!$contact['remote_self'])
1420 // Prevent the forwarding of posts that are forwarded
1421 if ($datarray["extid"] == NETWORK_DFRN)
1424 // Prevent to forward already forwarded posts
1425 if ($datarray["app"] == $a->get_hostname())
1428 // Only forward posts
1429 if ($datarray["verb"] != ACTIVITY_POST)
1432 if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1435 $datarray2 = $datarray;
1436 logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1437 if ($contact['remote_self'] == 2) {
1438 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1439 intval($contact['uid']));
1441 $datarray['contact-id'] = $r[0]["id"];
1443 $datarray['owner-name'] = $r[0]["name"];
1444 $datarray['owner-link'] = $r[0]["url"];
1445 $datarray['owner-avatar'] = $r[0]["thumb"];
1447 $datarray['author-name'] = $datarray['owner-name'];
1448 $datarray['author-link'] = $datarray['owner-link'];
1449 $datarray['author-avatar'] = $datarray['owner-avatar'];
1452 if ($contact['network'] != NETWORK_FEED) {
1453 $datarray["guid"] = get_guid(32);
1454 unset($datarray["plink"]);
1455 $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
1456 $datarray["parent-uri"] = $datarray["uri"];
1457 $datarray["extid"] = $contact['network'];
1458 $urlpart = parse_url($datarray2['author-link']);
1459 $datarray["app"] = $urlpart["host"];
1461 $datarray['private'] = 0;
1464 if ($contact['network'] != NETWORK_FEED) {
1465 // Store the original post
1466 $r = item_store($datarray2, false, false);
1467 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1469 $datarray["app"] = "Feed";
1474 function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
1475 $url = notags(trim($datarray['author-link']));
1476 $name = notags(trim($datarray['author-name']));
1477 $photo = notags(trim($datarray['author-avatar']));
1479 if (is_object($item)) {
1480 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1481 if ($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
1482 $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1486 if (is_array($contact)) {
1487 if (($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1488 || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1489 $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1490 intval(CONTACT_IS_FRIEND),
1491 intval($contact['id']),
1492 intval($importer['uid'])
1495 // send email notification to owner?
1498 // create contact record
1500 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1501 `blocked`, `readonly`, `pending`, `writable`)
1502 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1503 intval($importer['uid']),
1504 dbesc(datetime_convert()),
1506 dbesc(normalise_link($url)),
1510 dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1511 intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1513 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1514 intval($importer['uid']),
1518 $contact_record = $r[0];
1519 update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
1523 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1524 intval($importer['uid'])
1527 if (count($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1529 // create notification
1530 $hash = random_string();
1532 if (is_array($contact_record)) {
1533 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1534 VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1535 intval($importer['uid']),
1536 intval($contact_record['id']),
1538 dbesc(datetime_convert())
1542 $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1544 if (intval($def_gid))
1545 group_add_member($importer['uid'],'',$contact_record['id'],$def_gid);
1547 if (($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1548 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1551 'type' => NOTIFY_INTRO,
1552 'notify_flags' => $r[0]['notify-flags'],
1553 'language' => $r[0]['language'],
1554 'to_name' => $r[0]['username'],
1555 'to_email' => $r[0]['email'],
1556 'uid' => $r[0]['uid'],
1557 'link' => $a->get_baseurl() . '/notifications/intro',
1558 'source_name' => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1559 'source_link' => $contact_record['url'],
1560 'source_photo' => $contact_record['photo'],
1561 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1566 } elseif (count($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1567 $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1568 intval($importer['uid']),
1576 function lose_follower($importer,$contact,$datarray = array(),$item = "") {
1578 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1579 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1580 intval(CONTACT_IS_SHARING),
1581 intval($contact['id'])
1584 contact_remove($contact['id']);
1588 function lose_sharer($importer,$contact,$datarray = array(),$item = "") {
1590 if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1591 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1592 intval(CONTACT_IS_FOLLOWER),
1593 intval($contact['id'])
1596 contact_remove($contact['id']);
1600 function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
1604 if (is_array($importer)) {
1605 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1606 intval($importer['uid'])
1610 // Diaspora has different message-ids in feeds than they do
1611 // through the direct Diaspora protocol. If we try and use
1612 // the feed, we'll get duplicates. So don't.
1614 if ((! count($r)) || $contact['network'] === NETWORK_DIASPORA)
1617 $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1619 // Use a single verify token, even if multiple hubs
1621 $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1623 $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1625 logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: ' . $push_url . ' with verifier ' . $verify_token);
1627 if (!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1628 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1629 dbesc($verify_token),
1630 intval($contact['id'])
1634 post_url($url,$params);
1636 logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1642 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1644 if (get_config('system','disable_embedded'))
1649 logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1650 $site = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://'));
1655 $img_start = strpos($orig_body, '[img');
1656 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1657 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1658 while( ($img_st_close !== false) && ($img_len !== false) ) {
1660 $img_st_close++; // make it point to AFTER the closing bracket
1661 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1663 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1666 if (stristr($image , $site . '/photo/')) {
1667 // Only embed locally hosted photos
1669 $i = basename($image);
1670 $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
1671 $x = strpos($i,'-');
1674 $res = substr($i,$x+1);
1675 $i = substr($i,0,$x);
1676 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1683 // Check to see if we should replace this photo link with an embedded image
1684 // 1. No need to do so if the photo is public
1685 // 2. If there's a contact-id provided, see if they're in the access list
1686 // for the photo. If so, embed it.
1687 // 3. Otherwise, if we have an item, see if the item permissions match the photo
1688 // permissions, regardless of order but first check to see if they're an exact
1689 // match to save some processing overhead.
1691 if (has_permissions($r[0])) {
1693 $recips = enumerate_permissions($r[0]);
1694 if (in_array($cid, $recips)) {
1698 if (compare_permissions($item,$r[0]))
1703 $data = $r[0]['data'];
1704 $type = $r[0]['type'];
1706 // If a custom width and height were specified, apply before embedding
1707 if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1708 logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1710 $width = intval($match[1]);
1711 $height = intval($match[2]);
1713 $ph = new Photo($data, $type);
1714 if ($ph->is_valid()) {
1715 $ph->scaleImage(max($width, $height));
1716 $data = $ph->imageString();
1717 $type = $ph->getType();
1721 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1722 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1723 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1729 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1730 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1731 if ($orig_body === false)
1734 $img_start = strpos($orig_body, '[img');
1735 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1736 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1739 $new_body = $new_body . $orig_body;
1744 function has_permissions($obj) {
1745 if (($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1750 function compare_permissions($obj1,$obj2) {
1751 // first part is easy. Check that these are exactly the same.
1752 if (($obj1['allow_cid'] == $obj2['allow_cid'])
1753 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1754 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1755 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1758 // This is harder. Parse all the permissions and compare the resulting set.
1760 $recipients1 = enumerate_permissions($obj1);
1761 $recipients2 = enumerate_permissions($obj2);
1764 if ($recipients1 == $recipients2)
1769 // returns an array of contact-ids that are allowed to see this object
1771 function enumerate_permissions($obj) {
1772 $allow_people = expand_acl($obj['allow_cid']);
1773 $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1774 $deny_people = expand_acl($obj['deny_cid']);
1775 $deny_groups = expand_groups(expand_acl($obj['deny_gid']));
1776 $recipients = array_unique(array_merge($allow_people,$allow_groups));
1777 $deny = array_unique(array_merge($deny_people,$deny_groups));
1778 $recipients = array_diff($recipients,$deny);
1782 function item_getfeedtags($item) {
1785 $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1787 for($x = 0; $x < $cnt; $x ++) {
1788 if ($matches[1][$x])
1789 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
1793 $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1795 for($x = 0; $x < $cnt; $x ++) {
1796 if ($matches[1][$x])
1797 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
1803 function item_expire($uid, $days, $network = "", $force = false) {
1805 if ((! $uid) || ($days < 1))
1808 // $expire_network_only = save your own wall posts
1809 // and just expire conversations started by others
1811 $expire_network_only = get_pconfig($uid,'expire','network_only');
1812 $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1814 if ($network != "") {
1815 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1816 // There is an index "uid_network_received" but not "uid_network_created"
1817 // This avoids the creation of another index just for one purpose.
1818 // And it doesn't really matter wether to look at "received" or "created"
1819 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1821 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1823 $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
1824 WHERE `uid` = %d $range
1835 $expire_items = get_pconfig($uid, 'expire','items');
1836 $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1838 // Forcing expiring of items - but not notes and marked items
1840 $expire_items = true;
1842 $expire_notes = get_pconfig($uid, 'expire','notes');
1843 $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1845 $expire_starred = get_pconfig($uid, 'expire','starred');
1846 $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1848 $expire_photos = get_pconfig($uid, 'expire','photos');
1849 $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1851 logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1853 foreach($r as $item) {
1855 // don't expire filed items
1857 if (strpos($item['file'],'[') !== false)
1860 // Only expire posts, not photos and photo comments
1862 if ($expire_photos==0 && strlen($item['resource-id']))
1864 if ($expire_starred==0 && intval($item['starred']))
1866 if ($expire_notes==0 && $item['type']=='note')
1868 if ($expire_items==0 && $item['type']!='note')
1871 drop_item($item['id'],false);
1874 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1879 function drop_items($items) {
1882 if (! local_user() && ! remote_user())
1885 if (count($items)) {
1886 foreach($items as $item) {
1887 $owner = drop_item($item,false);
1888 if ($owner && ! $uid)
1893 // multiple threads may have been deleted, send an expire notification
1896 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1900 function drop_item($id,$interactive = true) {
1904 // locate item to be deleted
1906 $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1913 notice( t('Item not found.') . EOL);
1914 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1919 $owner = $item['uid'];
1923 // check if logged in user is either the author or owner of this item
1925 if (is_array($_SESSION['remote'])) {
1926 foreach($_SESSION['remote'] as $visitor) {
1927 if ($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1928 $contact_id = $visitor['cid'];
1935 if ((local_user() == $item['uid']) || ($contact_id) || (! $interactive)) {
1937 // Check if we should do HTML-based delete confirmation
1938 if ($_REQUEST['confirm']) {
1939 // <form> can't take arguments in its "action" parameter
1940 // so add any arguments as hidden inputs
1941 $query = explode_querystring($a->query_string);
1943 foreach($query['args'] as $arg) {
1944 if (strpos($arg, 'confirm=') === false) {
1945 $arg_parts = explode('=', $arg);
1946 $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1950 return replace_macros(get_markup_template('confirm.tpl'), array(
1952 '$message' => t('Do you really want to delete this item?'),
1953 '$extra_inputs' => $inputs,
1954 '$confirm' => t('Yes'),
1955 '$confirm_url' => $query['base'],
1956 '$confirm_name' => 'confirmed',
1957 '$cancel' => t('Cancel'),
1960 // Now check how the user responded to the confirmation query
1961 if ($_REQUEST['canceled']) {
1962 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1965 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
1968 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
1969 dbesc(datetime_convert()),
1970 dbesc(datetime_convert()),
1973 create_tags_from_item($item['id']);
1974 create_files_from_item($item['id']);
1975 delete_thread($item['id'], $item['parent-uri']);
1977 // clean up categories and tags so they don't end up as orphans
1980 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
1982 foreach($matches as $mtch) {
1983 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
1989 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
1991 foreach($matches as $mtch) {
1992 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
1996 // If item is a link to a photo resource, nuke all the associated photos
1997 // (visitors will not have photo resources)
1998 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
1999 // generate a resource-id and therefore aren't intimately linked to the item.
2001 if (strlen($item['resource-id'])) {
2002 q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
2003 dbesc($item['resource-id']),
2004 intval($item['uid'])
2006 // ignore the result
2009 // If item is a link to an event, nuke the event record.
2011 if (intval($item['event-id'])) {
2012 q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
2013 intval($item['event-id']),
2014 intval($item['uid'])
2016 // ignore the result
2019 // If item has attachments, drop them
2021 foreach(explode(",",$item['attach']) as $attach){
2022 preg_match("|attach/(\d+)|", $attach, $matches);
2023 q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
2024 intval($matches[1]),
2027 // ignore the result
2031 // clean up item_id and sign meta-data tables
2034 // Old code - caused very long queries and warning entries in the mysql logfiles:
2036 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
2037 intval($item['id']),
2038 intval($item['uid'])
2041 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
2042 intval($item['id']),
2043 intval($item['uid'])
2047 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
2049 // Creating list of parents
2050 $r = q("select id from item where parent = %d and uid = %d",
2051 intval($item['id']),
2052 intval($item['uid'])
2057 foreach ($r AS $row) {
2058 if ($parentid != "")
2061 $parentid .= $row["id"];
2065 if ($parentid != "") {
2066 $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
2068 $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
2071 // If it's the parent of a comment thread, kill all the kids
2073 if ($item['uri'] == $item['parent-uri']) {
2074 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
2075 WHERE `parent-uri` = '%s' AND `uid` = %d ",
2076 dbesc(datetime_convert()),
2077 dbesc(datetime_convert()),
2078 dbesc($item['parent-uri']),
2079 intval($item['uid'])
2081 create_tags_from_itemuri($item['parent-uri'], $item['uid']);
2082 create_files_from_itemuri($item['parent-uri'], $item['uid']);
2083 delete_thread_uri($item['parent-uri'], $item['uid']);
2084 // ignore the result
2086 // ensure that last-child is set in case the comment that had it just got wiped.
2087 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2088 dbesc(datetime_convert()),
2089 dbesc($item['parent-uri']),
2090 intval($item['uid'])
2092 // who is the last child now?
2093 $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",
2094 dbesc($item['parent-uri']),
2095 intval($item['uid'])
2098 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2104 $drop_id = intval($item['id']);
2106 // send the notification upstream/downstream as the case may be
2108 proc_run(PRIORITY_HIGH,"include/notifier.php", "drop", $drop_id);
2112 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2117 notice( t('Permission denied.') . EOL);
2118 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2125 function first_post_date($uid,$wall = false) {
2126 $r = q("select id, created from item
2127 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
2129 order by created asc limit 1",
2131 intval($wall ? 1 : 0)
2134 // logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2135 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
2140 /* modified posted_dates() {below} to arrange the list in years */
2141 function list_post_dates($uid, $wall) {
2142 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2144 $dthen = first_post_date($uid, $wall);
2148 // Set the start and end date to the beginning of the month
2149 $dnow = substr($dnow,0,8).'01';
2150 $dthen = substr($dthen,0,8).'01';
2154 // Starting with the current month, get the first and last days of every
2155 // month down to and including the month of the first post
2156 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2157 $dyear = intval(substr($dnow,0,4));
2158 $dstart = substr($dnow,0,8) . '01';
2159 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2160 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2161 $end_month = datetime_convert('','',$dend,'Y-m-d');
2162 $str = day_translate(datetime_convert('','',$dnow,'F'));
2164 $ret[$dyear] = array();
2165 $ret[$dyear][] = array($str,$end_month,$start_month);
2166 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2171 function posted_dates($uid,$wall) {
2172 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2174 $dthen = first_post_date($uid,$wall);
2178 // Set the start and end date to the beginning of the month
2179 $dnow = substr($dnow,0,8).'01';
2180 $dthen = substr($dthen,0,8).'01';
2183 // Starting with the current month, get the first and last days of every
2184 // month down to and including the month of the first post
2185 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2186 $dstart = substr($dnow,0,8) . '01';
2187 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2188 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2189 $end_month = datetime_convert('','',$dend,'Y-m-d');
2190 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
2191 $ret[] = array($str,$end_month,$start_month);
2192 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2198 function posted_date_widget($url,$uid,$wall) {
2201 if (! feature_enabled($uid,'archives'))
2204 // For former Facebook folks that left because of "timeline"
2206 /* if ($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2209 $visible_years = get_pconfig($uid,'system','archive_visible_years');
2210 if (! $visible_years)
2213 $ret = list_post_dates($uid,$wall);
2218 $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2219 $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
2221 $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2222 '$title' => t('Archives'),
2223 '$size' => $visible_years,
2224 '$cutoff_year' => $cutoff_year,
2225 '$cutoff' => $cutoff,
2228 '$showmore' => t('show more')