3 require_once('include/bbcode.php');
4 require_once('include/oembed.php');
5 require_once('include/salmon.php');
6 require_once('include/crypto.php');
7 require_once('include/Photo.php');
8 require_once('include/tags.php');
9 require_once('include/files.php');
10 require_once('include/text.php');
11 require_once('include/email.php');
12 require_once('include/threads.php');
13 require_once('include/socgraph.php');
14 require_once('include/plaintext.php');
15 require_once('include/ostatus.php');
16 require_once('include/feed.php');
17 require_once('include/Contact.php');
18 require_once('mod/share.php');
19 require_once('include/enotify.php');
20 require_once('include/dfrn.php');
21 require_once('include/group.php');
23 require_once('library/defuse/php-encryption-1.2.1/Crypto.php');
25 function construct_verb($item) {
33 * The purpose of this function is to apply system message length limits to
34 * imported messages without including any embedded photos in the length
36 if(! function_exists('limit_body_size')) {
37 function limit_body_size($body) {
39 // logger('limit_body_size: start', LOGGER_DEBUG);
41 $maxlen = get_max_import_size();
43 // If the length of the body, including the embedded images, is smaller
44 // than the maximum, then don't waste time looking for the images
45 if($maxlen && (strlen($body) > $maxlen)) {
47 logger('limit_body_size: the total body length exceeds the limit', LOGGER_DEBUG);
54 $img_start = strpos($orig_body, '[img');
55 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
56 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
57 while(($img_st_close !== false) && ($img_end !== false)) {
59 $img_st_close++; // make it point to AFTER the closing bracket
60 $img_end += $img_start;
61 $img_end += strlen('[/img]');
63 if(! strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
64 // This is an embedded image
66 if( ($textlen + $img_start) > $maxlen ) {
67 if($textlen < $maxlen) {
68 logger('limit_body_size: the limit happens before an embedded image', LOGGER_DEBUG);
69 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
74 $new_body = $new_body . substr($orig_body, 0, $img_start);
75 $textlen += $img_start;
78 $new_body = $new_body . substr($orig_body, $img_start, $img_end - $img_start);
82 if( ($textlen + $img_end) > $maxlen ) {
83 if($textlen < $maxlen) {
84 logger('limit_body_size: the limit happens before the end of a non-embedded image', LOGGER_DEBUG);
85 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
90 $new_body = $new_body . substr($orig_body, 0, $img_end);
94 $orig_body = substr($orig_body, $img_end);
96 if($orig_body === false) // in case the body ends on a closing image tag
99 $img_start = strpos($orig_body, '[img');
100 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
101 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
104 if( ($textlen + strlen($orig_body)) > $maxlen) {
105 if($textlen < $maxlen) {
106 logger('limit_body_size: the limit happens after the end of the last image', LOGGER_DEBUG);
107 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
112 logger('limit_body_size: the text size with embedded images extracted did not violate the limit', LOGGER_DEBUG);
113 $new_body = $new_body . $orig_body;
114 $textlen += strlen($orig_body);
123 function title_is_body($title, $body) {
125 $title = strip_tags($title);
126 $title = trim($title);
127 $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
128 $title = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $title);
130 $body = strip_tags($body);
132 $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
133 $body = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $body);
135 if (strlen($title) < strlen($body))
136 $body = substr($body, 0, strlen($title));
138 if (($title != $body) and (substr($title, -3) == "...")) {
139 $pos = strrpos($title, "...");
141 $title = substr($title, 0, $pos);
142 $body = substr($body, 0, $pos);
146 return($title == $body);
149 function add_page_info_data($data) {
150 call_hooks('page_info_data', $data);
152 // It maybe is a rich content, but if it does have everything that a link has,
153 // then treat it that way
154 if (($data["type"] == "rich") AND is_string($data["title"]) AND
155 is_string($data["text"]) AND (sizeof($data["images"]) > 0))
156 $data["type"] = "link";
158 if ((($data["type"] != "link") AND ($data["type"] != "video") AND ($data["type"] != "photo")) OR ($data["title"] == $url))
161 if ($no_photos AND ($data["type"] == "photo"))
164 // If the link contains BBCode stuff, make a short link out of this to avoid parsing problems
165 if (strpos($data["url"], '[') OR strpos($data["url"], ']')) {
166 require_once("include/network.php");
167 $data["url"] = short_link($data["url"]);
170 if (($data["type"] != "photo") AND is_string($data["title"]))
171 $text .= "[bookmark=".$data["url"]."]".trim($data["title"])."[/bookmark]";
173 if (($data["type"] != "video") AND ($photo != ""))
174 $text .= '[img]'.$photo.'[/img]';
175 elseif (($data["type"] != "video") AND (sizeof($data["images"]) > 0)) {
176 $imagedata = $data["images"][0];
177 $text .= '[img]'.$imagedata["src"].'[/img]';
180 if (($data["type"] != "photo") AND is_string($data["text"]))
181 $text .= "[quote]".$data["text"]."[/quote]";
184 if (isset($data["keywords"]) AND count($data["keywords"])) {
187 foreach ($data["keywords"] AS $keyword) {
188 /// @todo make a positive list of allowed characters
189 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'", "’", "`", "(", ")", "„", "“"),
190 array("","", "", "", "", "", "", "", "", "", "", ""), $keyword);
191 $hashtags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url] ";
195 return("\n[class=type-".$data["type"]."]".$text."[/class]".$hashtags);
198 function query_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
199 require_once("mod/parse_url.php");
201 $data = parseurl_getsiteinfo_cached($url, true);
204 $data["images"][0]["src"] = $photo;
206 logger('fetch page info for '.$url.' '.print_r($data, true), LOGGER_DEBUG);
208 if (!$keywords AND isset($data["keywords"]))
209 unset($data["keywords"]);
211 if (($keyword_blacklist != "") AND isset($data["keywords"])) {
212 $list = explode(",", $keyword_blacklist);
213 foreach ($list AS $keyword) {
214 $keyword = trim($keyword);
215 $index = array_search($keyword, $data["keywords"]);
216 if ($index !== false)
217 unset($data["keywords"][$index]);
224 function add_page_keywords($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
225 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
228 if (isset($data["keywords"]) AND count($data["keywords"])) {
230 foreach ($data["keywords"] AS $keyword) {
231 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'"),
232 array("","", "", "", "", ""), $keyword);
237 $tags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url]";
244 function add_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
245 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
247 $text = add_page_info_data($data);
252 function add_page_info_to_body($body, $texturl = false, $no_photos = false) {
254 logger('add_page_info_to_body: fetch page info for body '.$body, LOGGER_DEBUG);
256 $URLSearchString = "^\[\]";
258 // Adding these spaces is a quick hack due to my problems with regular expressions :)
259 preg_match("/[^!#@]\[url\]([$URLSearchString]*)\[\/url\]/ism", " ".$body, $matches);
262 preg_match("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", " ".$body, $matches);
264 // Convert urls without bbcode elements
265 if (!$matches AND $texturl) {
266 preg_match("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", " ".$body, $matches);
268 // Yeah, a hack. I really hate regular expressions :)
270 $matches[1] = $matches[2];
274 $footer = add_page_info($matches[1], $no_photos);
276 // Remove the link from the body if the link is attached at the end of the post
277 if (isset($footer) AND (trim($footer) != "") AND (strpos($footer, $matches[1]))) {
278 $removedlink = trim(str_replace($matches[1], "", $body));
279 if (($removedlink == "") OR strstr($body, $removedlink))
280 $body = $removedlink;
282 $url = str_replace(array('/', '.'), array('\/', '\.'), $matches[1]);
283 $removedlink = preg_replace("/\[url\=".$url."\](.*?)\[\/url\]/ism", '', $body);
284 if (($removedlink == "") OR strstr($body, $removedlink))
285 $body = $removedlink;
288 // Add the page information to the bottom
289 if (isset($footer) AND (trim($footer) != ""))
296 * Adds a "lang" specification in a "postopts" element of given $arr,
297 * if possible and not already present.
298 * Expects "body" element to exist in $arr.
300 * @todo Add a parameter to request forcing override
302 function item_add_language_opt(&$arr) {
304 if (version_compare(PHP_VERSION, '5.3.0', '<')) return; // LanguageDetect.php not available ?
306 if ( x($arr, 'postopts') )
308 if ( strstr($arr['postopts'], 'lang=') )
311 /// @TODO Add parameter to request overriding
314 $postopts = $arr['postopts'];
321 require_once('library/langdet/Text/LanguageDetect.php');
322 $naked_body = preg_replace('/\[(.+?)\]/','',$arr['body']);
323 $l = new Text_LanguageDetect;
324 //$lng = $l->detectConfidence($naked_body);
325 //$arr['postopts'] = (($lng['language']) ? 'lang=' . $lng['language'] . ';' . $lng['confidence'] : '');
326 $lng = $l->detect($naked_body, 3);
328 if (sizeof($lng) > 0) {
329 if ($postopts != "") $postopts .= '&'; // arbitrary separator, to be reviewed
330 $postopts .= 'lang=';
332 foreach ($lng as $language => $score) {
333 $postopts .= $sep . $language.";".$score;
336 $arr['postopts'] = $postopts;
341 * @brief Creates an unique guid out of a given uri
343 * @param string $uri uri of an item entry
344 * @return string unique guid
346 function uri_to_guid($uri) {
348 // Our regular guid routine is using this kind of prefix as well
349 // We have to avoid that different routines could accidentally create the same value
350 $parsed = parse_url($uri);
351 $guid_prefix = hash("crc32", $parsed["host"]);
353 // Remove the scheme to make sure that "https" and "http" doesn't make a difference
354 unset($parsed["scheme"]);
356 $host_id = implode("/", $parsed);
358 // We could use any hash algorithm since it isn't a security issue
359 $host_hash = hash("ripemd128", $host_id);
361 return $guid_prefix.$host_hash;
364 function item_store($arr,$force_parent = false, $notify = false, $dontcache = false) {
366 // If it is a posting where users should get notifications, then define it as wall posting
369 $arr['type'] = 'wall';
371 $arr['last-child'] = 1;
372 $arr['network'] = NETWORK_DFRN;
375 // If a Diaspora signature structure was passed in, pull it out of the
376 // item array and set it aside for later storage.
379 if(x($arr,'dsprsig')) {
380 $dsprsig = json_decode(base64_decode($arr['dsprsig']));
381 unset($arr['dsprsig']);
384 // Converting the plink
385 if ($arr['network'] == NETWORK_OSTATUS) {
386 if (isset($arr['plink']))
387 $arr['plink'] = ostatus::convert_href($arr['plink']);
388 elseif (isset($arr['uri']))
389 $arr['plink'] = ostatus::convert_href($arr['uri']);
392 if(x($arr, 'gravity'))
393 $arr['gravity'] = intval($arr['gravity']);
394 elseif($arr['parent-uri'] === $arr['uri'])
396 elseif(activity_match($arr['verb'],ACTIVITY_POST))
399 $arr['gravity'] = 6; // extensible catchall
402 $arr['type'] = 'remote';
406 /* check for create date and expire time */
407 $uid = intval($arr['uid']);
408 $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
410 $expire_interval = $r[0]['expire'];
411 if ($expire_interval>0) {
412 $expire_date = new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
413 $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
414 if ($created_date < $expire_date) {
415 logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
421 // Do we already have this item?
422 // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
423 if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
424 $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s') LIMIT 1",
425 dbesc(trim($arr['uri'])),
427 dbesc(NETWORK_DIASPORA),
429 dbesc(NETWORK_OSTATUS)
432 // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
434 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']);
439 // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
440 // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
441 //if((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
442 // $arr['body'] = strip_tags($arr['body']);
444 item_add_language_opt($arr);
448 elseif ((trim($arr['guid']) == "") AND (trim($arr['plink']) != ""))
449 $arr['guid'] = uri_to_guid($arr['plink']);
450 elseif ((trim($arr['guid']) == "") AND (trim($arr['uri']) != ""))
451 $arr['guid'] = uri_to_guid($arr['uri']);
453 $parsed = parse_url($arr["author-link"]);
454 $guid_prefix = hash("crc32", $parsed["host"]);
457 $arr['wall'] = ((x($arr,'wall')) ? intval($arr['wall']) : 0);
458 $arr['guid'] = ((x($arr,'guid')) ? notags(trim($arr['guid'])) : get_guid(32, $guid_prefix));
459 $arr['uri'] = ((x($arr,'uri')) ? notags(trim($arr['uri'])) : $arr['guid']);
460 $arr['extid'] = ((x($arr,'extid')) ? notags(trim($arr['extid'])) : '');
461 $arr['author-name'] = ((x($arr,'author-name')) ? trim($arr['author-name']) : '');
462 $arr['author-link'] = ((x($arr,'author-link')) ? notags(trim($arr['author-link'])) : '');
463 $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
464 $arr['owner-name'] = ((x($arr,'owner-name')) ? trim($arr['owner-name']) : '');
465 $arr['owner-link'] = ((x($arr,'owner-link')) ? notags(trim($arr['owner-link'])) : '');
466 $arr['owner-avatar'] = ((x($arr,'owner-avatar')) ? notags(trim($arr['owner-avatar'])) : '');
467 $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert());
468 $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert());
469 $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert());
470 $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert());
471 $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert());
472 $arr['title'] = ((x($arr,'title')) ? trim($arr['title']) : '');
473 $arr['location'] = ((x($arr,'location')) ? trim($arr['location']) : '');
474 $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : '');
475 $arr['last-child'] = ((x($arr,'last-child')) ? intval($arr['last-child']) : 0 );
476 $arr['visible'] = ((x($arr,'visible') !== false) ? intval($arr['visible']) : 1 );
478 $arr['parent-uri'] = ((x($arr,'parent-uri')) ? notags(trim($arr['parent-uri'])) : '');
479 $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : '');
480 $arr['object-type'] = ((x($arr,'object-type')) ? notags(trim($arr['object-type'])) : '');
481 $arr['object'] = ((x($arr,'object')) ? trim($arr['object']) : '');
482 $arr['target-type'] = ((x($arr,'target-type')) ? notags(trim($arr['target-type'])) : '');
483 $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : '');
484 $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : '');
485 $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : '');
486 $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : '');
487 $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : '');
488 $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : '');
489 $arr['private'] = ((x($arr,'private')) ? intval($arr['private']) : 0 );
490 $arr['bookmark'] = ((x($arr,'bookmark')) ? intval($arr['bookmark']) : 0 );
491 $arr['body'] = ((x($arr,'body')) ? trim($arr['body']) : '');
492 $arr['tag'] = ((x($arr,'tag')) ? notags(trim($arr['tag'])) : '');
493 $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : '');
494 $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : '');
495 $arr['origin'] = ((x($arr,'origin')) ? intval($arr['origin']) : 0 );
496 $arr['network'] = ((x($arr,'network')) ? trim($arr['network']) : '');
497 $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : '');
498 $arr['resource-id'] = ((x($arr,'resource-id')) ? trim($arr['resource-id']) : '');
499 $arr['event-id'] = ((x($arr,'event-id')) ? intval($arr['event-id']) : 0 );
500 $arr['inform'] = ((x($arr,'inform')) ? trim($arr['inform']) : '');
501 $arr['file'] = ((x($arr,'file')) ? trim($arr['file']) : '');
504 if (($arr['author-link'] == "") AND ($arr['owner-link'] == ""))
505 logger("Both author-link and owner-link are empty. Called by: ".App::callstack(), LOGGER_DEBUG);
507 if ($arr['plink'] == "") {
509 $arr['plink'] = $a->get_baseurl().'/display/'.urlencode($arr['guid']);
512 if ($arr['network'] == "") {
513 $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
514 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
515 dbesc(normalise_link($arr['author-link'])),
520 $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
521 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
522 dbesc(normalise_link($arr['author-link']))
526 $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
527 intval($arr['contact-id']),
532 $arr['network'] = $r[0]["network"];
534 // Fallback to friendica (why is it empty in some cases?)
535 if ($arr['network'] == "")
536 $arr['network'] = NETWORK_DFRN;
538 logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
541 // The contact-id should be set before "item_store" was called - but there seems to be some issues
542 if ($arr["contact-id"] == 0) {
543 // First we are looking for a suitable contact that matches with the author of the post
544 // This is done only for comments (See below explanation at "gcontact-id")
545 if($arr['parent-uri'] != $arr['uri'])
546 $arr["contact-id"] = get_contact($arr['author-link'], $uid);
548 // If not present then maybe the owner was found
549 if ($arr["contact-id"] == 0)
550 $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
552 // Still missing? Then use the "self" contact of the current user
553 if ($arr["contact-id"] == 0) {
554 $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
556 $arr["contact-id"] = $r[0]["id"];
558 logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
561 if ($arr["gcontact-id"] == 0) {
562 // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
563 // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
564 // On comments the author is the better choice.
565 if($arr['parent-uri'] === $arr['uri'])
566 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
567 "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
569 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
570 "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
573 if ($arr['guid'] != "") {
574 // Checking if there is already an item with the same guid
575 logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
576 $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
577 dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
580 logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
585 // Check for hashtags in the body and repair or add hashtag links
586 item_body_set_hashtags($arr);
588 $arr['thr-parent'] = $arr['parent-uri'];
589 if($arr['parent-uri'] === $arr['uri']) {
592 $allow_cid = $arr['allow_cid'];
593 $allow_gid = $arr['allow_gid'];
594 $deny_cid = $arr['deny_cid'];
595 $deny_gid = $arr['deny_gid'];
596 $notify_type = 'wall-new';
600 // find the parent and snarf the item id and ACLs
601 // and anything else we need to inherit
603 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
604 dbesc($arr['parent-uri']),
610 // is the new message multi-level threaded?
611 // even though we don't support it now, preserve the info
612 // and re-attach to the conversation parent.
614 if($r[0]['uri'] != $r[0]['parent-uri']) {
615 $arr['parent-uri'] = $r[0]['parent-uri'];
616 $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
617 ORDER BY `id` ASC LIMIT 1",
618 dbesc($r[0]['parent-uri']),
619 dbesc($r[0]['parent-uri']),
626 $parent_id = $r[0]['id'];
627 $parent_deleted = $r[0]['deleted'];
628 $allow_cid = $r[0]['allow_cid'];
629 $allow_gid = $r[0]['allow_gid'];
630 $deny_cid = $r[0]['deny_cid'];
631 $deny_gid = $r[0]['deny_gid'];
632 $arr['wall'] = $r[0]['wall'];
633 $notify_type = 'comment-new';
635 // if the parent is private, force privacy for the entire conversation
636 // This differs from the above settings as it subtly allows comments from
637 // email correspondents to be private even if the overall thread is not.
640 $arr['private'] = $r[0]['private'];
642 // Edge case. We host a public forum that was originally posted to privately.
643 // The original author commented, but as this is a comment, the permissions
644 // weren't fixed up so it will still show the comment as private unless we fix it here.
646 if((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
650 // If its a post from myself then tag the thread as "mention"
651 logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
652 $u = q("select * from user where uid = %d limit 1", intval($arr['uid']));
655 $self = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
656 logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
657 if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
658 q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
659 logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
665 // Allow one to see reply tweets from status.net even when
666 // we don't have or can't see the original post.
669 logger('item_store: $force_parent=true, reply converted to top-level post.');
671 $arr['parent-uri'] = $arr['uri'];
675 logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
683 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
685 dbesc($arr['network']),
689 if($r && count($r)) {
690 logger('duplicated item with the same uri found. ' . print_r($arr,true));
694 // Check for an existing post with the same content. There seems to be a problem with OStatus.
695 $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
697 dbesc($arr['network']),
698 dbesc($arr['created']),
699 intval($arr['contact-id']),
702 if($r && count($r)) {
703 logger('duplicated item with the same body found. ' . print_r($arr,true));
707 // Is this item available in the global items (with uid=0)?
708 if ($arr["uid"] == 0) {
709 $arr["global"] = true;
711 q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
713 $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
715 $arr["global"] = (count($isglobal) > 0);
718 // Fill the cache field
719 put_item_in_cache($arr);
722 call_hooks('post_local',$arr);
724 call_hooks('post_remote',$arr);
726 if(x($arr,'cancel')) {
727 logger('item_store: post cancelled by plugin.');
731 // Store the unescaped version
736 logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
738 $r = dbq("INSERT INTO `item` (`"
739 . implode("`, `", array_keys($arr))
741 . implode("', '", array_values($arr))
747 // find the item that we just created
748 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s' ORDER BY `id` ASC",
751 dbesc($arr['network'])
755 // There are duplicates. Keep the oldest one, delete the others
756 logger('item_store: duplicated post occurred. Removing newer duplicates. uri = '.$arr['uri'].' uid = '.$arr['uid']);
757 q("DELETE FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s' AND `id` > %d",
760 dbesc($arr['network']),
764 } elseif(count($r)) {
766 $current_post = $r[0]['id'];
767 logger('item_store: created item ' . $current_post);
769 // Set "success_update" and "last-item" to the date of the last time we heard from this contact
770 // This can be used to filter for inactive contacts.
771 // Only do this for public postings to avoid privacy problems, since poco data is public.
772 // Don't set this value if it isn't from the owner (could be an author that we don't know)
774 $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
776 // Is it a forum? Then we don't care about the rules from above
777 if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
778 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
779 intval($arr['contact-id']));
785 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
786 dbesc($arr['received']),
787 dbesc($arr['received']),
788 intval($arr['contact-id'])
791 logger('item_store: could not locate created item');
795 if((! $parent_id) || ($arr['parent-uri'] === $arr['uri']))
796 $parent_id = $current_post;
798 if(strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
801 $private = $arr['private'];
803 // Set parent id - and also make sure to inherit the parent's ACLs.
805 $r = q("UPDATE `item` SET `parent` = %d, `allow_cid` = '%s', `allow_gid` = '%s',
806 `deny_cid` = '%s', `deny_gid` = '%s', `private` = %d, `deleted` = %d WHERE `id` = %d",
813 intval($parent_deleted),
814 intval($current_post)
817 $arr['id'] = $current_post;
818 $arr['parent'] = $parent_id;
819 $arr['allow_cid'] = $allow_cid;
820 $arr['allow_gid'] = $allow_gid;
821 $arr['deny_cid'] = $deny_cid;
822 $arr['deny_gid'] = $deny_gid;
823 $arr['private'] = $private;
824 $arr['deleted'] = $parent_deleted;
826 // update the commented timestamp on the parent
827 // Only update "commented" if it is really a comment
828 if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
829 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
830 dbesc(datetime_convert()),
831 dbesc(datetime_convert()),
835 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
836 dbesc(datetime_convert()),
842 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
843 // We can check for this condition when we decode and encode the stuff again.
844 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
845 $dsprsig->signature = base64_decode($dsprsig->signature);
846 logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
849 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
850 intval($current_post),
851 dbesc($dsprsig->signed_text),
852 dbesc($dsprsig->signature),
853 dbesc($dsprsig->signer)
859 * If this is now the last-child, force all _other_ children of this parent to *not* be last-child
862 if($arr['last-child']) {
863 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
866 intval($current_post)
870 $deleted = tag_deliver($arr['uid'],$current_post);
872 // current post can be deleted if is for a community page and no mention are
874 if (!$deleted AND !$dontcache) {
876 $r = q('SELECT * FROM `item` WHERE id = %d', intval($current_post));
877 if (count($r) == 1) {
879 call_hooks('post_local_end', $r[0]);
881 call_hooks('post_remote_end', $r[0]);
883 logger('item_store: new item not found in DB, id ' . $current_post);
886 create_tags_from_item($current_post);
887 create_files_from_item($current_post);
889 // Only check for notifications on start posts
890 if ($arr['parent-uri'] === $arr['uri'])
891 add_thread($current_post);
893 update_thread($parent_id);
894 add_shadow_entry($arr);
897 check_item_notification($current_post, $uid);
900 proc_run('php', "include/notifier.php", $notify_type, $current_post);
902 return $current_post;
905 function item_body_set_hashtags(&$item) {
907 $tags = get_tags($item["body"]);
913 // This sorting is important when there are hashtags that are part of other hashtags
914 // Otherwise there could be problems with hashtags like #test and #test2
919 $URLSearchString = "^\[\]";
921 // All hashtags should point to the home server
922 //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
923 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
925 //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
926 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
928 // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
929 $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
931 return("[url=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/url]");
934 $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
936 return("[bookmark=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/bookmark]");
939 $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
941 return("[attachment ".str_replace("#", "#", $match[1])."]".$match[2]."[/attachment]");
944 // Repair recursive urls
945 $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
946 "#$2", $item["body"]);
949 foreach($tags as $tag) {
950 if(strpos($tag,'#') !== 0)
953 if(strpos($tag,'[url='))
956 $basetag = str_replace('_',' ',substr($tag,1));
958 $newtag = '#[url='.$a->get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
960 $item["body"] = str_replace($tag, $newtag, $item["body"]);
962 if(!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
963 if(strlen($item["tag"]))
964 $item["tag"] = ','.$item["tag"];
965 $item["tag"] = $newtag.$item["tag"];
969 // Convert back the masked hashtags
970 $item["body"] = str_replace("#", "#", $item["body"]);
973 function get_item_guid($id) {
974 $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
976 return($r[0]["guid"]);
981 function get_item_id($guid, $uid = 0) {
987 $uid == local_user();
989 // Does the given user have this item?
991 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
992 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
993 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
996 $nick = $r[0]["nickname"];
1000 // Or is it anywhere on the server?
1002 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1003 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1004 AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = ''
1005 AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
1006 AND `item`.`private` = 0 AND `item`.`wall` = 1
1007 AND `item`.`guid` = '%s'", dbesc($guid));
1010 $nick = $r[0]["nickname"];
1013 return(array("nick" => $nick, "id" => $id));
1017 function get_item_contact($item,$contacts) {
1018 if(! count($contacts) || (! is_array($item)))
1020 foreach($contacts as $contact) {
1021 if($contact['id'] == $item['contact-id']) {
1023 break; // NOTREACHED
1030 * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1032 * @param int $item_id
1033 * @return bool true if item was deleted, else false
1035 function tag_deliver($uid,$item_id) {
1043 $u = q("select * from user where uid = %d limit 1",
1049 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1050 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1053 $i = q("select * from item where id = %d and uid = %d limit 1",
1062 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1064 // Diaspora uses their own hardwired link URL in @-tags
1065 // instead of the one we supply with webfinger
1067 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1069 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1071 foreach($matches as $mtch) {
1072 if(link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1074 logger('tag_deliver: mention found: ' . $mtch[2]);
1080 if ( ($community_page || $prvgroup) &&
1081 (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1082 // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1084 logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1085 q("DELETE FROM item WHERE id = %d and uid = %d",
1094 $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1096 call_hooks('tagged', $arr);
1098 if((! $community_page) && (! $prvgroup))
1102 // tgroup delivery - setup a second delivery chain
1103 // prevent delivery looping - only proceed
1104 // if the message originated elsewhere and is a top-level post
1106 if(($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1109 // now change this copy of the post to a forum head message and deliver to all the tgroup members
1112 $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1113 intval($u[0]['uid'])
1118 // also reset all the privacy bits to the forum default permissions
1120 $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1122 $forum_mode = (($prvgroup) ? 2 : 1);
1124 q("update item set wall = 1, origin = 1, forum_mode = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1125 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' where id = %d",
1126 intval($forum_mode),
1127 dbesc($c[0]['name']),
1128 dbesc($c[0]['url']),
1129 dbesc($c[0]['thumb']),
1131 dbesc($u[0]['allow_cid']),
1132 dbesc($u[0]['allow_gid']),
1133 dbesc($u[0]['deny_cid']),
1134 dbesc($u[0]['deny_gid']),
1137 update_thread($item_id);
1139 proc_run('php','include/notifier.php','tgroup',$item_id);
1145 function tgroup_check($uid,$item) {
1151 // check that the message originated elsewhere and is a top-level post
1153 if(($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1157 $u = q("select * from user where uid = %d limit 1",
1163 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1164 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1167 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1169 // Diaspora uses their own hardwired link URL in @-tags
1170 // instead of the one we supply with webfinger
1172 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1174 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1176 foreach($matches as $mtch) {
1177 if(link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1179 logger('tgroup_check: mention found: ' . $mtch[2]);
1187 if((! $community_page) && (! $prvgroup))
1194 This function returns true if $update has an edited timestamp newer
1195 than $existing, i.e. $update contains new data which should override
1196 what's already there. If there is no timestamp yet, the update is
1197 assumed to be newer. If the update has no timestamp, the existing
1198 item is assumed to be up-to-date. If the timestamps are equal it
1199 assumes the update has been seen before and should be ignored.
1201 function edited_timestamp_is_newer($existing, $update) {
1202 if (!x($existing,'edited') || !$existing['edited']) {
1205 if (!x($update,'edited') || !$update['edited']) {
1208 $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1209 $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1210 return (strcmp($existing_edited, $update_edited) < 0);
1215 * consume_feed - process atom feed and update anything/everything we might need to update
1217 * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1219 * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1220 * It is this person's stuff that is going to be updated.
1221 * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1222 * from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1223 * have a contact record.
1224 * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1225 * might not) try and subscribe to it.
1226 * $datedir sorts in reverse order
1227 * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1228 * imported prior to its children being seen in the stream unless we are certain
1229 * of how the feed is arranged/ordered.
1230 * With $pass = 1, we only pull parent items out of the stream.
1231 * With $pass = 2, we only pull children (comments/likes).
1233 * So running this twice, first with pass 1 and then with pass 2 will do the right
1234 * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1235 * model where comments can have sub-threads. That would require some massive sorting
1236 * to get all the feed items into a mostly linear ordering, and might still require
1240 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1241 if ($contact['network'] === NETWORK_OSTATUS) {
1243 // Test - remove before flight
1244 //$tempfile = tempnam(get_temppath(), "ostatus2");
1245 //file_put_contents($tempfile, $xml);
1246 logger("Consume OStatus messages ", LOGGER_DEBUG);
1247 ostatus::import($xml,$importer,$contact, $hub);
1252 if ($contact['network'] === NETWORK_FEED) {
1254 logger("Consume feeds", LOGGER_DEBUG);
1255 feed_import($xml,$importer,$contact, $hub);
1260 if ($contact['network'] === NETWORK_DFRN) {
1261 logger("Consume DFRN messages", LOGGER_DEBUG);
1263 $r = q("SELECT `contact`.*, `contact`.`uid` AS `importer_uid`,
1264 `contact`.`pubkey` AS `cpubkey`,
1265 `contact`.`prvkey` AS `cprvkey`,
1266 `contact`.`thumb` AS `thumb`,
1267 `contact`.`url` as `url`,
1268 `contact`.`name` as `senderName`,
1271 LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1272 WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1273 dbesc($contact["id"]), dbesc($importer["uid"])
1276 logger("Now import the DFRN feed");
1277 dfrn::import($xml,$r[0], true);
1283 function item_is_remote_self($contact, &$datarray) {
1286 if (!$contact['remote_self'])
1289 // Prevent the forwarding of posts that are forwarded
1290 if ($datarray["extid"] == NETWORK_DFRN)
1293 // Prevent to forward already forwarded posts
1294 if ($datarray["app"] == $a->get_hostname())
1297 // Only forward posts
1298 if ($datarray["verb"] != ACTIVITY_POST)
1301 if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1304 $datarray2 = $datarray;
1305 logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1306 if ($contact['remote_self'] == 2) {
1307 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1308 intval($contact['uid']));
1310 $datarray['contact-id'] = $r[0]["id"];
1312 $datarray['owner-name'] = $r[0]["name"];
1313 $datarray['owner-link'] = $r[0]["url"];
1314 $datarray['owner-avatar'] = $r[0]["thumb"];
1316 $datarray['author-name'] = $datarray['owner-name'];
1317 $datarray['author-link'] = $datarray['owner-link'];
1318 $datarray['author-avatar'] = $datarray['owner-avatar'];
1321 if ($contact['network'] != NETWORK_FEED) {
1322 $datarray["guid"] = get_guid(32);
1323 unset($datarray["plink"]);
1324 $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
1325 $datarray["parent-uri"] = $datarray["uri"];
1326 $datarray["extid"] = $contact['network'];
1327 $urlpart = parse_url($datarray2['author-link']);
1328 $datarray["app"] = $urlpart["host"];
1330 $datarray['private'] = 0;
1333 if ($contact['network'] != NETWORK_FEED) {
1334 // Store the original post
1335 $r = item_store($datarray2, false, false);
1336 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1338 $datarray["app"] = "Feed";
1343 function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
1344 $url = notags(trim($datarray['author-link']));
1345 $name = notags(trim($datarray['author-name']));
1346 $photo = notags(trim($datarray['author-avatar']));
1348 if (is_object($item)) {
1349 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1350 if($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
1351 $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1355 if(is_array($contact)) {
1356 if(($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1357 || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1358 $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1359 intval(CONTACT_IS_FRIEND),
1360 intval($contact['id']),
1361 intval($importer['uid'])
1364 // send email notification to owner?
1367 // create contact record
1369 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1370 `blocked`, `readonly`, `pending`, `writable`)
1371 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1372 intval($importer['uid']),
1373 dbesc(datetime_convert()),
1375 dbesc(normalise_link($url)),
1379 dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1380 intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1382 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1383 intval($importer['uid']),
1387 $contact_record = $r[0];
1389 $photos = import_profile_photo($photo,$importer["uid"],$contact_record["id"]);
1391 q("UPDATE `contact` SET `photo` = '%s', `thumb` = '%s', `micro` = '%s' WHERE `id` = %d",
1395 intval($contact_record["id"])
1400 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1401 intval($importer['uid'])
1404 if(count($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1406 // create notification
1407 $hash = random_string();
1409 if(is_array($contact_record)) {
1410 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1411 VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1412 intval($importer['uid']),
1413 intval($contact_record['id']),
1415 dbesc(datetime_convert())
1419 $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1421 if(intval($def_gid))
1422 group_add_member($importer['uid'],'',$contact_record['id'],$def_gid);
1424 if(($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1425 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1428 'type' => NOTIFY_INTRO,
1429 'notify_flags' => $r[0]['notify-flags'],
1430 'language' => $r[0]['language'],
1431 'to_name' => $r[0]['username'],
1432 'to_email' => $r[0]['email'],
1433 'uid' => $r[0]['uid'],
1434 'link' => $a->get_baseurl() . '/notifications/intro',
1435 'source_name' => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1436 'source_link' => $contact_record['url'],
1437 'source_photo' => $contact_record['photo'],
1438 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1443 } elseif (count($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1444 $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1445 intval($importer['uid']),
1453 function lose_follower($importer,$contact,$datarray = array(),$item = "") {
1455 if(($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1456 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1457 intval(CONTACT_IS_SHARING),
1458 intval($contact['id'])
1462 contact_remove($contact['id']);
1466 function lose_sharer($importer,$contact,$datarray = array(),$item = "") {
1468 if(($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1469 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1470 intval(CONTACT_IS_FOLLOWER),
1471 intval($contact['id'])
1475 contact_remove($contact['id']);
1479 function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
1483 if(is_array($importer)) {
1484 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1485 intval($importer['uid'])
1489 // Diaspora has different message-ids in feeds than they do
1490 // through the direct Diaspora protocol. If we try and use
1491 // the feed, we'll get duplicates. So don't.
1493 if((! count($r)) || $contact['network'] === NETWORK_DIASPORA)
1496 $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1498 // Use a single verify token, even if multiple hubs
1500 $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1502 $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1504 logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: ' . $push_url . ' with verifier ' . $verify_token);
1506 if(!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1507 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1508 dbesc($verify_token),
1509 intval($contact['id'])
1513 post_url($url,$params);
1515 logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1521 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1523 if(get_config('system','disable_embedded'))
1528 logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1529 $site = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://'));
1534 $img_start = strpos($orig_body, '[img');
1535 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1536 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1537 while( ($img_st_close !== false) && ($img_len !== false) ) {
1539 $img_st_close++; // make it point to AFTER the closing bracket
1540 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1542 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1545 if(stristr($image , $site . '/photo/')) {
1546 // Only embed locally hosted photos
1548 $i = basename($image);
1549 $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
1550 $x = strpos($i,'-');
1553 $res = substr($i,$x+1);
1554 $i = substr($i,0,$x);
1555 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1562 // Check to see if we should replace this photo link with an embedded image
1563 // 1. No need to do so if the photo is public
1564 // 2. If there's a contact-id provided, see if they're in the access list
1565 // for the photo. If so, embed it.
1566 // 3. Otherwise, if we have an item, see if the item permissions match the photo
1567 // permissions, regardless of order but first check to see if they're an exact
1568 // match to save some processing overhead.
1570 if(has_permissions($r[0])) {
1572 $recips = enumerate_permissions($r[0]);
1573 if(in_array($cid, $recips)) {
1578 if(compare_permissions($item,$r[0]))
1583 $data = $r[0]['data'];
1584 $type = $r[0]['type'];
1586 // If a custom width and height were specified, apply before embedding
1587 if(preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1588 logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1590 $width = intval($match[1]);
1591 $height = intval($match[2]);
1593 $ph = new Photo($data, $type);
1594 if($ph->is_valid()) {
1595 $ph->scaleImage(max($width, $height));
1596 $data = $ph->imageString();
1597 $type = $ph->getType();
1601 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1602 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1603 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1609 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1610 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1611 if($orig_body === false)
1614 $img_start = strpos($orig_body, '[img');
1615 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1616 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1619 $new_body = $new_body . $orig_body;
1624 function has_permissions($obj) {
1625 if(($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1630 function compare_permissions($obj1,$obj2) {
1631 // first part is easy. Check that these are exactly the same.
1632 if(($obj1['allow_cid'] == $obj2['allow_cid'])
1633 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1634 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1635 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1638 // This is harder. Parse all the permissions and compare the resulting set.
1640 $recipients1 = enumerate_permissions($obj1);
1641 $recipients2 = enumerate_permissions($obj2);
1644 if($recipients1 == $recipients2)
1649 // returns an array of contact-ids that are allowed to see this object
1651 function enumerate_permissions($obj) {
1652 $allow_people = expand_acl($obj['allow_cid']);
1653 $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1654 $deny_people = expand_acl($obj['deny_cid']);
1655 $deny_groups = expand_groups(expand_acl($obj['deny_gid']));
1656 $recipients = array_unique(array_merge($allow_people,$allow_groups));
1657 $deny = array_unique(array_merge($deny_people,$deny_groups));
1658 $recipients = array_diff($recipients,$deny);
1662 function item_getfeedtags($item) {
1665 $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1667 for($x = 0; $x < $cnt; $x ++) {
1669 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
1673 $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1675 for($x = 0; $x < $cnt; $x ++) {
1677 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
1683 function item_expire($uid, $days, $network = "", $force = false) {
1685 if((! $uid) || ($days < 1))
1688 // $expire_network_only = save your own wall posts
1689 // and just expire conversations started by others
1691 $expire_network_only = get_pconfig($uid,'expire','network_only');
1692 $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1694 if ($network != "") {
1695 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1696 // There is an index "uid_network_received" but not "uid_network_created"
1697 // This avoids the creation of another index just for one purpose.
1698 // And it doesn't really matter wether to look at "received" or "created"
1699 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1701 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1703 $r = q("SELECT * FROM `item`
1704 WHERE `uid` = %d $range
1715 $expire_items = get_pconfig($uid, 'expire','items');
1716 $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1718 // Forcing expiring of items - but not notes and marked items
1720 $expire_items = true;
1722 $expire_notes = get_pconfig($uid, 'expire','notes');
1723 $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1725 $expire_starred = get_pconfig($uid, 'expire','starred');
1726 $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1728 $expire_photos = get_pconfig($uid, 'expire','photos');
1729 $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1731 logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1733 foreach($r as $item) {
1735 // don't expire filed items
1737 if(strpos($item['file'],'[') !== false)
1740 // Only expire posts, not photos and photo comments
1742 if($expire_photos==0 && strlen($item['resource-id']))
1744 if($expire_starred==0 && intval($item['starred']))
1746 if($expire_notes==0 && $item['type']=='note')
1748 if($expire_items==0 && $item['type']!='note')
1751 drop_item($item['id'],false);
1754 proc_run('php',"include/notifier.php","expire","$uid");
1759 function drop_items($items) {
1762 if(! local_user() && ! remote_user())
1766 foreach($items as $item) {
1767 $owner = drop_item($item,false);
1768 if($owner && ! $uid)
1773 // multiple threads may have been deleted, send an expire notification
1776 proc_run('php',"include/notifier.php","expire","$uid");
1780 function drop_item($id,$interactive = true) {
1784 // locate item to be deleted
1786 $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1793 notice( t('Item not found.') . EOL);
1794 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1799 $owner = $item['uid'];
1803 // check if logged in user is either the author or owner of this item
1805 if(is_array($_SESSION['remote'])) {
1806 foreach($_SESSION['remote'] as $visitor) {
1807 if($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1808 $cid = $visitor['cid'];
1815 if((local_user() == $item['uid']) || ($cid) || (! $interactive)) {
1817 // Check if we should do HTML-based delete confirmation
1818 if($_REQUEST['confirm']) {
1819 // <form> can't take arguments in its "action" parameter
1820 // so add any arguments as hidden inputs
1821 $query = explode_querystring($a->query_string);
1823 foreach($query['args'] as $arg) {
1824 if(strpos($arg, 'confirm=') === false) {
1825 $arg_parts = explode('=', $arg);
1826 $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1830 return replace_macros(get_markup_template('confirm.tpl'), array(
1832 '$message' => t('Do you really want to delete this item?'),
1833 '$extra_inputs' => $inputs,
1834 '$confirm' => t('Yes'),
1835 '$confirm_url' => $query['base'],
1836 '$confirm_name' => 'confirmed',
1837 '$cancel' => t('Cancel'),
1840 // Now check how the user responded to the confirmation query
1841 if($_REQUEST['canceled']) {
1842 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1845 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
1848 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
1849 dbesc(datetime_convert()),
1850 dbesc(datetime_convert()),
1853 create_tags_from_item($item['id']);
1854 create_files_from_item($item['id']);
1855 delete_thread($item['id'], $item['parent-uri']);
1857 // clean up categories and tags so they don't end up as orphans
1860 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
1862 foreach($matches as $mtch) {
1863 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
1869 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
1871 foreach($matches as $mtch) {
1872 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
1876 // If item is a link to a photo resource, nuke all the associated photos
1877 // (visitors will not have photo resources)
1878 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
1879 // generate a resource-id and therefore aren't intimately linked to the item.
1881 if(strlen($item['resource-id'])) {
1882 q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
1883 dbesc($item['resource-id']),
1884 intval($item['uid'])
1886 // ignore the result
1889 // If item is a link to an event, nuke the event record.
1891 if(intval($item['event-id'])) {
1892 q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
1893 intval($item['event-id']),
1894 intval($item['uid'])
1896 // ignore the result
1899 // If item has attachments, drop them
1901 foreach(explode(",",$item['attach']) as $attach){
1902 preg_match("|attach/(\d+)|", $attach, $matches);
1903 q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
1904 intval($matches[1]),
1907 // ignore the result
1911 // clean up item_id and sign meta-data tables
1914 // Old code - caused very long queries and warning entries in the mysql logfiles:
1916 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
1917 intval($item['id']),
1918 intval($item['uid'])
1921 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
1922 intval($item['id']),
1923 intval($item['uid'])
1927 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
1929 // Creating list of parents
1930 $r = q("select id from item where parent = %d and uid = %d",
1931 intval($item['id']),
1932 intval($item['uid'])
1937 foreach ($r AS $row) {
1938 if ($parentid != "")
1941 $parentid .= $row["id"];
1945 if ($parentid != "") {
1946 $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
1948 $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
1951 // If it's the parent of a comment thread, kill all the kids
1953 if($item['uri'] == $item['parent-uri']) {
1954 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
1955 WHERE `parent-uri` = '%s' AND `uid` = %d ",
1956 dbesc(datetime_convert()),
1957 dbesc(datetime_convert()),
1958 dbesc($item['parent-uri']),
1959 intval($item['uid'])
1961 create_tags_from_itemuri($item['parent-uri'], $item['uid']);
1962 create_files_from_itemuri($item['parent-uri'], $item['uid']);
1963 delete_thread_uri($item['parent-uri'], $item['uid']);
1964 // ignore the result
1967 // ensure that last-child is set in case the comment that had it just got wiped.
1968 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
1969 dbesc(datetime_convert()),
1970 dbesc($item['parent-uri']),
1971 intval($item['uid'])
1973 // who is the last child now?
1974 $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",
1975 dbesc($item['parent-uri']),
1976 intval($item['uid'])
1979 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
1985 $drop_id = intval($item['id']);
1987 // send the notification upstream/downstream as the case may be
1989 proc_run('php',"include/notifier.php","drop","$drop_id");
1993 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1999 notice( t('Permission denied.') . EOL);
2000 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2007 function first_post_date($uid,$wall = false) {
2008 $r = q("select id, created from item
2009 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
2011 order by created asc limit 1",
2013 intval($wall ? 1 : 0)
2016 // logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2017 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
2022 /* modified posted_dates() {below} to arrange the list in years */
2023 function list_post_dates($uid, $wall) {
2024 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2026 $dthen = first_post_date($uid, $wall);
2030 // Set the start and end date to the beginning of the month
2031 $dnow = substr($dnow,0,8).'01';
2032 $dthen = substr($dthen,0,8).'01';
2036 // Starting with the current month, get the first and last days of every
2037 // month down to and including the month of the first post
2038 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2039 $dyear = intval(substr($dnow,0,4));
2040 $dstart = substr($dnow,0,8) . '01';
2041 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2042 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2043 $end_month = datetime_convert('','',$dend,'Y-m-d');
2044 $str = day_translate(datetime_convert('','',$dnow,'F'));
2046 $ret[$dyear] = array();
2047 $ret[$dyear][] = array($str,$end_month,$start_month);
2048 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2053 function posted_dates($uid,$wall) {
2054 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2056 $dthen = first_post_date($uid,$wall);
2060 // Set the start and end date to the beginning of the month
2061 $dnow = substr($dnow,0,8).'01';
2062 $dthen = substr($dthen,0,8).'01';
2065 // Starting with the current month, get the first and last days of every
2066 // month down to and including the month of the first post
2067 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2068 $dstart = substr($dnow,0,8) . '01';
2069 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2070 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2071 $end_month = datetime_convert('','',$dend,'Y-m-d');
2072 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
2073 $ret[] = array($str,$end_month,$start_month);
2074 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2080 function posted_date_widget($url,$uid,$wall) {
2083 if(! feature_enabled($uid,'archives'))
2086 // For former Facebook folks that left because of "timeline"
2088 /* if($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2091 $visible_years = get_pconfig($uid,'system','archive_visible_years');
2092 if(! $visible_years)
2095 $ret = list_post_dates($uid,$wall);
2100 $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2101 $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
2103 $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2104 '$title' => t('Archives'),
2105 '$size' => $visible_years,
2106 '$cutoff_year' => $cutoff_year,
2107 '$cutoff' => $cutoff,
2110 '$showmore' => t('show more')