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');
20 require_once('library/defuse/php-encryption-1.2.1/Crypto.php');
22 function construct_verb($item) {
30 * The purpose of this function is to apply system message length limits to
31 * imported messages without including any embedded photos in the length
33 if(! function_exists('limit_body_size')) {
34 function limit_body_size($body) {
36 // logger('limit_body_size: start', LOGGER_DEBUG);
38 $maxlen = get_max_import_size();
40 // If the length of the body, including the embedded images, is smaller
41 // than the maximum, then don't waste time looking for the images
42 if($maxlen && (strlen($body) > $maxlen)) {
44 logger('limit_body_size: the total body length exceeds the limit', LOGGER_DEBUG);
51 $img_start = strpos($orig_body, '[img');
52 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
53 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
54 while(($img_st_close !== false) && ($img_end !== false)) {
56 $img_st_close++; // make it point to AFTER the closing bracket
57 $img_end += $img_start;
58 $img_end += strlen('[/img]');
60 if(! strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
61 // This is an embedded image
63 if( ($textlen + $img_start) > $maxlen ) {
64 if($textlen < $maxlen) {
65 logger('limit_body_size: the limit happens before an embedded image', LOGGER_DEBUG);
66 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
71 $new_body = $new_body . substr($orig_body, 0, $img_start);
72 $textlen += $img_start;
75 $new_body = $new_body . substr($orig_body, $img_start, $img_end - $img_start);
79 if( ($textlen + $img_end) > $maxlen ) {
80 if($textlen < $maxlen) {
81 logger('limit_body_size: the limit happens before the end of a non-embedded image', LOGGER_DEBUG);
82 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
87 $new_body = $new_body . substr($orig_body, 0, $img_end);
91 $orig_body = substr($orig_body, $img_end);
93 if($orig_body === false) // in case the body ends on a closing image tag
96 $img_start = strpos($orig_body, '[img');
97 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
98 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
101 if( ($textlen + strlen($orig_body)) > $maxlen) {
102 if($textlen < $maxlen) {
103 logger('limit_body_size: the limit happens after the end of the last image', LOGGER_DEBUG);
104 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
109 logger('limit_body_size: the text size with embedded images extracted did not violate the limit', LOGGER_DEBUG);
110 $new_body = $new_body . $orig_body;
111 $textlen += strlen($orig_body);
120 function title_is_body($title, $body) {
122 $title = strip_tags($title);
123 $title = trim($title);
124 $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
125 $title = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $title);
127 $body = strip_tags($body);
129 $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
130 $body = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $body);
132 if (strlen($title) < strlen($body))
133 $body = substr($body, 0, strlen($title));
135 if (($title != $body) and (substr($title, -3) == "...")) {
136 $pos = strrpos($title, "...");
138 $title = substr($title, 0, $pos);
139 $body = substr($body, 0, $pos);
143 return($title == $body);
146 function get_atom_elements($feed, $item, $contact = array()) {
148 require_once('library/HTMLPurifier.auto.php');
149 require_once('include/html2bbcode.php');
151 $best_photo = array();
155 $author = $item->get_author();
157 $res['author-name'] = unxmlify($author->get_name());
158 $res['author-link'] = unxmlify($author->get_link());
161 $res['author-name'] = unxmlify($feed->get_title());
162 $res['author-link'] = unxmlify($feed->get_permalink());
164 $res['uri'] = unxmlify($item->get_id());
165 $res['title'] = unxmlify($item->get_title());
166 $res['body'] = unxmlify($item->get_content());
167 $res['plink'] = unxmlify($item->get_link(0));
170 $base_url = implode('/', array_slice(explode('/',$res['plink']),0,3));
174 // look for a photo. We should check media size and find the best one,
175 // but for now let's just find any author photo
176 // Additionally we look for an alternate author link. On OStatus this one is the one we want.
178 $authorlinks = $item->feed->data["child"][SIMPLEPIE_NAMESPACE_ATOM_10]["feed"][0]["child"][SIMPLEPIE_NAMESPACE_ATOM_10]["author"][0]["child"]["http://www.w3.org/2005/Atom"]["link"];
179 if (is_array($authorlinks)) {
180 foreach ($authorlinks as $link) {
181 $linkdata = array_shift($link["attribs"]);
183 if ($linkdata["rel"] == "alternate")
184 $res["author-link"] = $linkdata["href"];
188 $rawauthor = $item->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10,'author');
190 if($rawauthor && $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']) {
191 $base = $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link'];
192 foreach($base as $link) {
193 if($link['attribs']['']['rel'] === 'alternate')
194 $res['author-link'] = unxmlify($link['attribs']['']['href']);
196 if(!x($res, 'author-avatar') || !$res['author-avatar']) {
197 if($link['attribs']['']['rel'] === 'photo' || $link['attribs']['']['rel'] === 'avatar')
198 $res['author-avatar'] = unxmlify($link['attribs']['']['href']);
203 $rawactor = $item->get_item_tags(NAMESPACE_ACTIVITY, 'actor');
205 if($rawactor && activity_match($rawactor[0]['child'][NAMESPACE_ACTIVITY]['object-type'][0]['data'],ACTIVITY_OBJ_PERSON)) {
206 $base = $rawactor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link'];
207 if($base && count($base)) {
208 foreach($base as $link) {
209 if($link['attribs']['']['rel'] === 'alternate' && (! $res['author-link']))
210 $res['author-link'] = unxmlify($link['attribs']['']['href']);
211 if(!x($res, 'author-avatar') || !$res['author-avatar']) {
212 if($link['attribs']['']['rel'] === 'avatar' || $link['attribs']['']['rel'] === 'photo')
213 $res['author-avatar'] = unxmlify($link['attribs']['']['href']);
219 // No photo/profile-link on the item - look at the feed level
221 if((! (x($res,'author-link'))) || (! (x($res,'author-avatar')))) {
222 $rawauthor = $feed->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10,'author');
223 if($rawauthor && $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']) {
224 $base = $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link'];
225 foreach($base as $link) {
226 if($link['attribs']['']['rel'] === 'alternate' && (! $res['author-link']))
227 $res['author-link'] = unxmlify($link['attribs']['']['href']);
228 if(! $res['author-avatar']) {
229 if($link['attribs']['']['rel'] === 'photo' || $link['attribs']['']['rel'] === 'avatar')
230 $res['author-avatar'] = unxmlify($link['attribs']['']['href']);
235 $rawactor = $feed->get_feed_tags(NAMESPACE_ACTIVITY, 'subject');
237 if($rawactor && activity_match($rawactor[0]['child'][NAMESPACE_ACTIVITY]['object-type'][0]['data'],ACTIVITY_OBJ_PERSON)) {
238 $base = $rawactor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link'];
240 if($base && count($base)) {
241 foreach($base as $link) {
242 if($link['attribs']['']['rel'] === 'alternate' && (! $res['author-link']))
243 $res['author-link'] = unxmlify($link['attribs']['']['href']);
244 if(! (x($res,'author-avatar'))) {
245 if($link['attribs']['']['rel'] === 'avatar' || $link['attribs']['']['rel'] === 'photo')
246 $res['author-avatar'] = unxmlify($link['attribs']['']['href']);
253 $apps = $item->get_item_tags(NAMESPACE_STATUSNET,'notice_info');
254 if($apps && $apps[0]['attribs']['']['source']) {
255 $res['app'] = strip_tags(unxmlify($apps[0]['attribs']['']['source']));
256 if($res['app'] === 'web')
257 $res['app'] = 'OStatus';
260 // base64 encoded json structure representing Diaspora signature
262 $dsig = $item->get_item_tags(NAMESPACE_DFRN,'diaspora_signature');
264 $res['dsprsig'] = unxmlify($dsig[0]['data']);
267 $dguid = $item->get_item_tags(NAMESPACE_DFRN,'diaspora_guid');
269 $res['guid'] = unxmlify($dguid[0]['data']);
271 $bm = $item->get_item_tags(NAMESPACE_DFRN,'bookmark');
273 $res['bookmark'] = ((unxmlify($bm[0]['data']) === 'true') ? 1 : 0);
277 * If there's a copy of the body content which is guaranteed to have survived mangling in transit, use it.
280 $have_real_body = false;
282 $rawenv = $item->get_item_tags(NAMESPACE_DFRN, 'env');
284 $have_real_body = true;
285 $res['body'] = $rawenv[0]['data'];
286 $res['body'] = str_replace(array(' ',"\t","\r","\n"), array('','','',''),$res['body']);
287 // make sure nobody is trying to sneak some html tags by us
288 $res['body'] = notags(base64url_decode($res['body']));
292 $res['body'] = limit_body_size($res['body']);
294 // It isn't certain at this point whether our content is plaintext or html and we'd be foolish to trust
295 // the content type. Our own network only emits text normally, though it might have been converted to
296 // html if we used a pubsubhubbub transport. But if we see even one html tag in our text, we will
297 // have to assume it is all html and needs to be purified.
299 // It doesn't matter all that much security wise - because before this content is used anywhere, we are
300 // going to escape any tags we find regardless, but this lets us import a limited subset of html from
301 // the wild, by sanitising it and converting supported tags to bbcode before we rip out any remaining
304 if((strpos($res['body'],'<') !== false) && (strpos($res['body'],'>') !== false)) {
306 $res['body'] = reltoabs($res['body'],$base_url);
308 $res['body'] = html2bb_video($res['body']);
310 $res['body'] = oembed_html2bbcode($res['body']);
312 $config = HTMLPurifier_Config::createDefault();
313 $config->set('Cache.DefinitionImpl', null);
315 // we shouldn't need a whitelist, because the bbcode converter
316 // will strip out any unsupported tags.
318 $purifier = new HTMLPurifier($config);
319 $res['body'] = $purifier->purify($res['body']);
321 $res['body'] = @html2bbcode($res['body']);
325 elseif(! $have_real_body) {
327 // it's not one of our messages and it has no tags
328 // so it's probably just text. We'll escape it just to be safe.
330 $res['body'] = escape_tags($res['body']);
334 // this tag is obsolete but we keep it for really old sites
336 $allow = $item->get_item_tags(NAMESPACE_DFRN,'comment-allow');
337 if($allow && $allow[0]['data'] == 1)
338 $res['last-child'] = 1;
340 $res['last-child'] = 0;
342 $private = $item->get_item_tags(NAMESPACE_DFRN,'private');
343 if($private && intval($private[0]['data']) > 0)
344 $res['private'] = intval($private[0]['data']);
348 $extid = $item->get_item_tags(NAMESPACE_DFRN,'extid');
349 if($extid && $extid[0]['data'])
350 $res['extid'] = $extid[0]['data'];
352 $rawlocation = $item->get_item_tags(NAMESPACE_DFRN, 'location');
354 $res['location'] = unxmlify($rawlocation[0]['data']);
357 $rawcreated = $item->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10,'published');
359 $res['created'] = unxmlify($rawcreated[0]['data']);
362 $rawedited = $item->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10,'updated');
364 $res['edited'] = unxmlify($rawedited[0]['data']);
366 if((x($res,'edited')) && (! (x($res,'created'))))
367 $res['created'] = $res['edited'];
369 if(! $res['created'])
370 $res['created'] = $item->get_date('c');
373 $res['edited'] = $item->get_date('c');
376 // Disallow time travelling posts
378 $d1 = strtotime($res['created']);
379 $d2 = strtotime($res['edited']);
380 $d3 = strtotime('now');
383 $res['created'] = datetime_convert();
385 $res['edited'] = datetime_convert();
387 $rawowner = $item->get_item_tags(NAMESPACE_DFRN, 'owner');
388 if($rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'])
389 $res['owner-name'] = unxmlify($rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data']);
390 elseif($rawowner[0]['child'][NAMESPACE_DFRN]['name'][0]['data'])
391 $res['owner-name'] = unxmlify($rawowner[0]['child'][NAMESPACE_DFRN]['name'][0]['data']);
392 if($rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'])
393 $res['owner-link'] = unxmlify($rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data']);
394 elseif($rawowner[0]['child'][NAMESPACE_DFRN]['uri'][0]['data'])
395 $res['owner-link'] = unxmlify($rawowner[0]['child'][NAMESPACE_DFRN]['uri'][0]['data']);
397 if($rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']) {
398 $base = $rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link'];
400 foreach($base as $link) {
401 if(!x($res, 'owner-avatar') || !$res['owner-avatar']) {
402 if($link['attribs']['']['rel'] === 'photo' || $link['attribs']['']['rel'] === 'avatar')
403 $res['owner-avatar'] = unxmlify($link['attribs']['']['href']);
408 $rawgeo = $item->get_item_tags(NAMESPACE_GEORSS,'point');
410 $res['coord'] = unxmlify($rawgeo[0]['data']);
412 if ($contact["network"] == NETWORK_FEED) {
413 $res['verb'] = ACTIVITY_POST;
414 $res['object-type'] = ACTIVITY_OBJ_NOTE;
417 $rawverb = $item->get_item_tags(NAMESPACE_ACTIVITY, 'verb');
419 // select between supported verbs
422 $res['verb'] = unxmlify($rawverb[0]['data']);
425 // translate OStatus unfollow to activity streams if it happened to get selected
427 if((x($res,'verb')) && ($res['verb'] === 'http://ostatus.org/schema/1.0/unfollow'))
428 $res['verb'] = ACTIVITY_UNFOLLOW;
430 $cats = $item->get_categories();
433 foreach($cats as $cat) {
434 $term = $cat->get_term();
436 $term = $cat->get_label();
437 $scheme = $cat->get_scheme();
438 if($scheme && $term && stristr($scheme,'X-DFRN:'))
439 $tag_arr[] = substr($scheme,7,1) . '[url=' . unxmlify(substr($scheme,9)) . ']' . unxmlify($term) . '[/url]';
441 $tag_arr[] = notags(trim($term));
443 $res['tag'] = implode(',', $tag_arr);
446 $attach = $item->get_enclosures();
449 foreach($attach as $att) {
450 $len = intval($att->get_length());
451 $link = str_replace(array(',','"'),array('%2D','%22'),notags(trim(unxmlify($att->get_link()))));
452 $title = str_replace(array(',','"'),array('%2D','%22'),notags(trim(unxmlify($att->get_title()))));
453 $type = str_replace(array(',','"'),array('%2D','%22'),notags(trim(unxmlify($att->get_type()))));
454 if(strpos($type,';'))
455 $type = substr($type,0,strpos($type,';'));
456 if((! $link) || (strpos($link,'http') !== 0))
462 $type = 'application/octet-stream';
464 $att_arr[] = '[attach]href="' . $link . '" length="' . $len . '" type="' . $type . '" title="' . $title . '"[/attach]';
466 $res['attach'] = implode(',', $att_arr);
469 $rawobj = $item->get_item_tags(NAMESPACE_ACTIVITY, 'object');
472 $res['object'] = '<object>' . "\n";
473 $child = $rawobj[0]['child'];
474 if($child[NAMESPACE_ACTIVITY]['object-type'][0]['data']) {
475 $res['object-type'] = $child[NAMESPACE_ACTIVITY]['object-type'][0]['data'];
476 $res['object'] .= '<type>' . $child[NAMESPACE_ACTIVITY]['object-type'][0]['data'] . '</type>' . "\n";
478 if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'id') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['id'][0]['data'])
479 $res['object'] .= '<id>' . $child[SIMPLEPIE_NAMESPACE_ATOM_10]['id'][0]['data'] . '</id>' . "\n";
480 if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'link') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['link'])
481 $res['object'] .= '<link>' . encode_rel_links($child[SIMPLEPIE_NAMESPACE_ATOM_10]['link']) . '</link>' . "\n";
482 if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'title') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['title'][0]['data'])
483 $res['object'] .= '<title>' . $child[SIMPLEPIE_NAMESPACE_ATOM_10]['title'][0]['data'] . '</title>' . "\n";
484 if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'content') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['content'][0]['data']) {
485 $body = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['content'][0]['data'];
487 $body = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['summary'][0]['data'];
488 // preserve a copy of the original body content in case we later need to parse out any microformat information, e.g. events
489 $res['object'] .= '<orig>' . xmlify($body) . '</orig>' . "\n";
490 if((strpos($body,'<') !== false) || (strpos($body,'>') !== false)) {
492 $body = html2bb_video($body);
494 $config = HTMLPurifier_Config::createDefault();
495 $config->set('Cache.DefinitionImpl', null);
497 $purifier = new HTMLPurifier($config);
498 $body = $purifier->purify($body);
499 $body = html2bbcode($body);
502 $res['object'] .= '<content>' . $body . '</content>' . "\n";
505 $res['object'] .= '</object>' . "\n";
508 $rawobj = $item->get_item_tags(NAMESPACE_ACTIVITY, 'target');
511 $res['target'] = '<target>' . "\n";
512 $child = $rawobj[0]['child'];
513 if($child[NAMESPACE_ACTIVITY]['object-type'][0]['data']) {
514 $res['target'] .= '<type>' . $child[NAMESPACE_ACTIVITY]['object-type'][0]['data'] . '</type>' . "\n";
516 if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'id') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['id'][0]['data'])
517 $res['target'] .= '<id>' . $child[SIMPLEPIE_NAMESPACE_ATOM_10]['id'][0]['data'] . '</id>' . "\n";
518 if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'link') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['link'])
519 $res['target'] .= '<link>' . encode_rel_links($child[SIMPLEPIE_NAMESPACE_ATOM_10]['link']) . '</link>' . "\n";
520 if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'data') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['title'][0]['data'])
521 $res['target'] .= '<title>' . $child[SIMPLEPIE_NAMESPACE_ATOM_10]['title'][0]['data'] . '</title>' . "\n";
522 if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'data') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['content'][0]['data']) {
523 $body = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['content'][0]['data'];
525 $body = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['summary'][0]['data'];
526 // preserve a copy of the original body content in case we later need to parse out any microformat information, e.g. events
527 $res['target'] .= '<orig>' . xmlify($body) . '</orig>' . "\n";
528 if((strpos($body,'<') !== false) || (strpos($body,'>') !== false)) {
530 $body = html2bb_video($body);
532 $config = HTMLPurifier_Config::createDefault();
533 $config->set('Cache.DefinitionImpl', null);
535 $purifier = new HTMLPurifier($config);
536 $body = $purifier->purify($body);
537 $body = html2bbcode($body);
540 $res['target'] .= '<content>' . $body . '</content>' . "\n";
543 $res['target'] .= '</target>' . "\n";
546 $arr = array('feed' => $feed, 'item' => $item, 'result' => $res);
548 call_hooks('parse_atom', $arr);
553 function add_page_info_data($data) {
554 call_hooks('page_info_data', $data);
556 // It maybe is a rich content, but if it does have everything that a link has,
557 // then treat it that way
558 if (($data["type"] == "rich") AND is_string($data["title"]) AND
559 is_string($data["text"]) AND (sizeof($data["images"]) > 0))
560 $data["type"] = "link";
562 if ((($data["type"] != "link") AND ($data["type"] != "video") AND ($data["type"] != "photo")) OR ($data["title"] == $url))
565 if ($no_photos AND ($data["type"] == "photo"))
568 // If the link contains BBCode stuff, make a short link out of this to avoid parsing problems
569 if (strpos($data["url"], '[') OR strpos($data["url"], ']')) {
570 require_once("include/network.php");
571 $data["url"] = short_link($data["url"]);
574 if (($data["type"] != "photo") AND is_string($data["title"]))
575 $text .= "[bookmark=".$data["url"]."]".trim($data["title"])."[/bookmark]";
577 if (($data["type"] != "video") AND ($photo != ""))
578 $text .= '[img]'.$photo.'[/img]';
579 elseif (($data["type"] != "video") AND (sizeof($data["images"]) > 0)) {
580 $imagedata = $data["images"][0];
581 $text .= '[img]'.$imagedata["src"].'[/img]';
584 if (($data["type"] != "photo") AND is_string($data["text"]))
585 $text .= "[quote]".$data["text"]."[/quote]";
588 if (isset($data["keywords"]) AND count($data["keywords"])) {
591 foreach ($data["keywords"] AS $keyword) {
592 /// @todo make a positive list of allowed characters
593 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'", "’", "`", "(", ")", "„", "“"),
594 array("","", "", "", "", "", "", "", "", "", "", ""), $keyword);
595 $hashtags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url] ";
599 return("\n[class=type-".$data["type"]."]".$text."[/class]".$hashtags);
602 function query_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
603 require_once("mod/parse_url.php");
605 $data = parseurl_getsiteinfo_cached($url, true);
608 $data["images"][0]["src"] = $photo;
610 logger('fetch page info for '.$url.' '.print_r($data, true), LOGGER_DEBUG);
612 if (!$keywords AND isset($data["keywords"]))
613 unset($data["keywords"]);
615 if (($keyword_blacklist != "") AND isset($data["keywords"])) {
616 $list = explode(",", $keyword_blacklist);
617 foreach ($list AS $keyword) {
618 $keyword = trim($keyword);
619 $index = array_search($keyword, $data["keywords"]);
620 if ($index !== false)
621 unset($data["keywords"][$index]);
628 function add_page_keywords($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
629 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
632 if (isset($data["keywords"]) AND count($data["keywords"])) {
634 foreach ($data["keywords"] AS $keyword) {
635 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'"),
636 array("","", "", "", "", ""), $keyword);
641 $tags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url]";
648 function add_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
649 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
651 $text = add_page_info_data($data);
656 function add_page_info_to_body($body, $texturl = false, $no_photos = false) {
658 logger('add_page_info_to_body: fetch page info for body '.$body, LOGGER_DEBUG);
660 $URLSearchString = "^\[\]";
662 // Adding these spaces is a quick hack due to my problems with regular expressions :)
663 preg_match("/[^!#@]\[url\]([$URLSearchString]*)\[\/url\]/ism", " ".$body, $matches);
666 preg_match("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", " ".$body, $matches);
668 // Convert urls without bbcode elements
669 if (!$matches AND $texturl) {
670 preg_match("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", " ".$body, $matches);
672 // Yeah, a hack. I really hate regular expressions :)
674 $matches[1] = $matches[2];
678 $footer = add_page_info($matches[1], $no_photos);
680 // Remove the link from the body if the link is attached at the end of the post
681 if (isset($footer) AND (trim($footer) != "") AND (strpos($footer, $matches[1]))) {
682 $removedlink = trim(str_replace($matches[1], "", $body));
683 if (($removedlink == "") OR strstr($body, $removedlink))
684 $body = $removedlink;
686 $url = str_replace(array('/', '.'), array('\/', '\.'), $matches[1]);
687 $removedlink = preg_replace("/\[url\=".$url."\](.*?)\[\/url\]/ism", '', $body);
688 if (($removedlink == "") OR strstr($body, $removedlink))
689 $body = $removedlink;
692 // Add the page information to the bottom
693 if (isset($footer) AND (trim($footer) != ""))
699 function encode_rel_links($links) {
701 if(! ((is_array($links)) && (count($links))))
703 foreach($links as $link) {
705 if($link['attribs']['']['rel'])
706 $o .= 'rel="' . $link['attribs']['']['rel'] . '" ';
707 if($link['attribs']['']['type'])
708 $o .= 'type="' . $link['attribs']['']['type'] . '" ';
709 if($link['attribs']['']['href'])
710 $o .= 'href="' . $link['attribs']['']['href'] . '" ';
711 if( (x($link['attribs'],NAMESPACE_MEDIA)) && $link['attribs'][NAMESPACE_MEDIA]['width'])
712 $o .= 'media:width="' . $link['attribs'][NAMESPACE_MEDIA]['width'] . '" ';
713 if( (x($link['attribs'],NAMESPACE_MEDIA)) && $link['attribs'][NAMESPACE_MEDIA]['height'])
714 $o .= 'media:height="' . $link['attribs'][NAMESPACE_MEDIA]['height'] . '" ';
720 function add_guid($item) {
721 $r = q("SELECT `guid` FROM `guid` WHERE `guid` = '%s' LIMIT 1", dbesc($item["guid"]));
725 q("INSERT INTO `guid` (`guid`,`plink`,`uri`,`network`) VALUES ('%s','%s','%s','%s')",
726 dbesc($item["guid"]), dbesc($item["plink"]),
727 dbesc($item["uri"]), dbesc($item["network"]));
731 * Adds a "lang" specification in a "postopts" element of given $arr,
732 * if possible and not already present.
733 * Expects "body" element to exist in $arr.
735 * @todo Add a parameter to request forcing override
737 function item_add_language_opt(&$arr) {
739 if (version_compare(PHP_VERSION, '5.3.0', '<')) return; // LanguageDetect.php not available ?
741 if ( x($arr, 'postopts') )
743 if ( strstr($arr['postopts'], 'lang=') )
746 /// @TODO Add parameter to request overriding
749 $postopts = $arr['postopts'];
756 require_once('library/langdet/Text/LanguageDetect.php');
757 $naked_body = preg_replace('/\[(.+?)\]/','',$arr['body']);
758 $l = new Text_LanguageDetect;
759 //$lng = $l->detectConfidence($naked_body);
760 //$arr['postopts'] = (($lng['language']) ? 'lang=' . $lng['language'] . ';' . $lng['confidence'] : '');
761 $lng = $l->detect($naked_body, 3);
763 if (sizeof($lng) > 0) {
764 if ($postopts != "") $postopts .= '&'; // arbitrary separator, to be reviewed
765 $postopts .= 'lang=';
767 foreach ($lng as $language => $score) {
768 $postopts .= $sep . $language.";".$score;
771 $arr['postopts'] = $postopts;
775 function item_store($arr,$force_parent = false, $notify = false, $dontcache = false) {
777 // If it is a posting where users should get notifications, then define it as wall posting
780 $arr['type'] = 'wall';
782 $arr['last-child'] = 1;
783 $arr['network'] = NETWORK_DFRN;
786 // If a Diaspora signature structure was passed in, pull it out of the
787 // item array and set it aside for later storage.
790 if(x($arr,'dsprsig')) {
791 $dsprsig = json_decode(base64_decode($arr['dsprsig']));
792 unset($arr['dsprsig']);
795 // Converting the plink
796 if ($arr['network'] == NETWORK_OSTATUS) {
797 if (isset($arr['plink']))
798 $arr['plink'] = ostatus_convert_href($arr['plink']);
799 elseif (isset($arr['uri']))
800 $arr['plink'] = ostatus_convert_href($arr['uri']);
803 if(x($arr, 'gravity'))
804 $arr['gravity'] = intval($arr['gravity']);
805 elseif($arr['parent-uri'] === $arr['uri'])
807 elseif(activity_match($arr['verb'],ACTIVITY_POST))
810 $arr['gravity'] = 6; // extensible catchall
813 $arr['type'] = 'remote';
817 /* check for create date and expire time */
818 $uid = intval($arr['uid']);
819 $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
821 $expire_interval = $r[0]['expire'];
822 if ($expire_interval>0) {
823 $expire_date = new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
824 $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
825 if ($created_date < $expire_date) {
826 logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
832 // Do we already have this item?
833 // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
834 if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
835 $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s') LIMIT 1",
836 dbesc(trim($arr['uri'])),
838 dbesc(NETWORK_DIASPORA),
840 dbesc(NETWORK_OSTATUS)
843 // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
845 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']);
850 // If there is no guid then take the same guid that was taken before for the same uri
851 if ((trim($arr['guid']) == "") AND (trim($arr['uri']) != "") AND (trim($arr['network']) != "")) {
852 logger('item_store: checking for an existing guid for uri '.$arr['uri'], LOGGER_DEBUG);
853 $r = q("SELECT `guid` FROM `guid` WHERE `uri` = '%s' AND `network` = '%s' LIMIT 1",
854 dbesc(trim($arr['uri'])), dbesc(trim($arr['network'])));
857 $arr['guid'] = $r[0]["guid"];
858 logger('item_store: found guid '.$arr['guid'].' for uri '.$arr['uri'], LOGGER_DEBUG);
862 // If there is no guid then take the same guid that was taken before for the same plink
863 if ((trim($arr['guid']) == "") AND (trim($arr['plink']) != "") AND (trim($arr['network']) != "")) {
864 logger('item_store: checking for an existing guid for plink '.$arr['plink'], LOGGER_DEBUG);
865 $r = q("SELECT `guid`, `uri` FROM `guid` WHERE `plink` = '%s' AND `network` = '%s' LIMIT 1",
866 dbesc(trim($arr['plink'])), dbesc(trim($arr['network'])));
869 $arr['guid'] = $r[0]["guid"];
870 logger('item_store: found guid '.$arr['guid'].' for plink '.$arr['plink'], LOGGER_DEBUG);
872 if ($r[0]["uri"] != $arr['uri'])
873 logger('Different uri for same guid: '.$arr['uri'].' and '.$r[0]["uri"].' - this shouldnt happen!', LOGGER_DEBUG);
877 // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
878 // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
879 //if((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
880 // $arr['body'] = strip_tags($arr['body']);
882 item_add_language_opt($arr);
887 $parsed = parse_url($arr["author-link"]);
888 $guid_prefix = hash("crc32", $parsed["host"]);
891 $arr['wall'] = ((x($arr,'wall')) ? intval($arr['wall']) : 0);
892 $arr['guid'] = ((x($arr,'guid')) ? notags(trim($arr['guid'])) : get_guid(32, $guid_prefix));
893 $arr['uri'] = ((x($arr,'uri')) ? notags(trim($arr['uri'])) : $arr['guid']);
894 $arr['extid'] = ((x($arr,'extid')) ? notags(trim($arr['extid'])) : '');
895 $arr['author-name'] = ((x($arr,'author-name')) ? trim($arr['author-name']) : '');
896 $arr['author-link'] = ((x($arr,'author-link')) ? notags(trim($arr['author-link'])) : '');
897 $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
898 $arr['owner-name'] = ((x($arr,'owner-name')) ? trim($arr['owner-name']) : '');
899 $arr['owner-link'] = ((x($arr,'owner-link')) ? notags(trim($arr['owner-link'])) : '');
900 $arr['owner-avatar'] = ((x($arr,'owner-avatar')) ? notags(trim($arr['owner-avatar'])) : '');
901 $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert());
902 $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert());
903 $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert());
904 $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert());
905 $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert());
906 $arr['title'] = ((x($arr,'title')) ? trim($arr['title']) : '');
907 $arr['location'] = ((x($arr,'location')) ? trim($arr['location']) : '');
908 $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : '');
909 $arr['last-child'] = ((x($arr,'last-child')) ? intval($arr['last-child']) : 0 );
910 $arr['visible'] = ((x($arr,'visible') !== false) ? intval($arr['visible']) : 1 );
912 $arr['parent-uri'] = ((x($arr,'parent-uri')) ? notags(trim($arr['parent-uri'])) : '');
913 $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : '');
914 $arr['object-type'] = ((x($arr,'object-type')) ? notags(trim($arr['object-type'])) : '');
915 $arr['object'] = ((x($arr,'object')) ? trim($arr['object']) : '');
916 $arr['target-type'] = ((x($arr,'target-type')) ? notags(trim($arr['target-type'])) : '');
917 $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : '');
918 $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : '');
919 $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : '');
920 $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : '');
921 $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : '');
922 $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : '');
923 $arr['private'] = ((x($arr,'private')) ? intval($arr['private']) : 0 );
924 $arr['bookmark'] = ((x($arr,'bookmark')) ? intval($arr['bookmark']) : 0 );
925 $arr['body'] = ((x($arr,'body')) ? trim($arr['body']) : '');
926 $arr['tag'] = ((x($arr,'tag')) ? notags(trim($arr['tag'])) : '');
927 $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : '');
928 $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : '');
929 $arr['origin'] = ((x($arr,'origin')) ? intval($arr['origin']) : 0 );
930 $arr['network'] = ((x($arr,'network')) ? trim($arr['network']) : '');
931 $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : '');
932 $arr['resource-id'] = ((x($arr,'resource-id')) ? trim($arr['resource-id']) : '');
933 $arr['event-id'] = ((x($arr,'event-id')) ? intval($arr['event-id']) : 0 );
934 $arr['inform'] = ((x($arr,'inform')) ? trim($arr['inform']) : '');
935 $arr['file'] = ((x($arr,'file')) ? trim($arr['file']) : '');
937 if ($arr['plink'] == "") {
939 $arr['plink'] = $a->get_baseurl().'/display/'.urlencode($arr['guid']);
942 if ($arr['network'] == "") {
943 $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
944 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
945 dbesc(normalise_link($arr['author-link'])),
950 $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
951 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
952 dbesc(normalise_link($arr['author-link']))
956 $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
957 intval($arr['contact-id']),
962 $arr['network'] = $r[0]["network"];
964 // Fallback to friendica (why is it empty in some cases?)
965 if ($arr['network'] == "")
966 $arr['network'] = NETWORK_DFRN;
968 logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
971 // The contact-id should be set before "item_store" was called - but there seems to be some issues
972 if ($arr["contact-id"] == 0) {
973 // First we are looking for a suitable contact that matches with the author of the post
974 $arr["contact-id"] = get_contact($arr['author-link'], $uid);
976 // If not present then maybe the owner was found
977 if ($arr["contact-id"] == 0)
978 $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
980 // Still missing? Then use the "self" contact of the current user
981 if ($arr["contact-id"] == 0) {
982 $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
984 $arr["contact-id"] = $r[0]["id"];
986 logger("Contact-id was missing for post ".$arr["guid"]." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
989 if ($arr["gcontact-id"] == 0)
990 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
991 "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
993 if ($arr['guid'] != "") {
994 // Checking if there is already an item with the same guid
995 logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
996 $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
997 dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
1000 logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
1005 // Check for hashtags in the body and repair or add hashtag links
1006 item_body_set_hashtags($arr);
1008 $arr['thr-parent'] = $arr['parent-uri'];
1009 if($arr['parent-uri'] === $arr['uri']) {
1011 $parent_deleted = 0;
1012 $allow_cid = $arr['allow_cid'];
1013 $allow_gid = $arr['allow_gid'];
1014 $deny_cid = $arr['deny_cid'];
1015 $deny_gid = $arr['deny_gid'];
1016 $notify_type = 'wall-new';
1020 // find the parent and snarf the item id and ACLs
1021 // and anything else we need to inherit
1023 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
1024 dbesc($arr['parent-uri']),
1030 // is the new message multi-level threaded?
1031 // even though we don't support it now, preserve the info
1032 // and re-attach to the conversation parent.
1034 if($r[0]['uri'] != $r[0]['parent-uri']) {
1035 $arr['parent-uri'] = $r[0]['parent-uri'];
1036 $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
1037 ORDER BY `id` ASC LIMIT 1",
1038 dbesc($r[0]['parent-uri']),
1039 dbesc($r[0]['parent-uri']),
1046 $parent_id = $r[0]['id'];
1047 $parent_deleted = $r[0]['deleted'];
1048 $allow_cid = $r[0]['allow_cid'];
1049 $allow_gid = $r[0]['allow_gid'];
1050 $deny_cid = $r[0]['deny_cid'];
1051 $deny_gid = $r[0]['deny_gid'];
1052 $arr['wall'] = $r[0]['wall'];
1053 $notify_type = 'comment-new';
1055 // if the parent is private, force privacy for the entire conversation
1056 // This differs from the above settings as it subtly allows comments from
1057 // email correspondents to be private even if the overall thread is not.
1059 if($r[0]['private'])
1060 $arr['private'] = $r[0]['private'];
1062 // Edge case. We host a public forum that was originally posted to privately.
1063 // The original author commented, but as this is a comment, the permissions
1064 // weren't fixed up so it will still show the comment as private unless we fix it here.
1066 if((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
1067 $arr['private'] = 0;
1070 // If its a post from myself then tag the thread as "mention"
1071 logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
1072 $u = q("select * from user where uid = %d limit 1", intval($arr['uid']));
1075 $self = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1076 logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
1077 if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
1078 q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
1079 logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
1085 // Allow one to see reply tweets from status.net even when
1086 // we don't have or can't see the original post.
1089 logger('item_store: $force_parent=true, reply converted to top-level post.');
1091 $arr['parent-uri'] = $arr['uri'];
1092 $arr['gravity'] = 0;
1095 logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
1099 $parent_deleted = 0;
1103 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
1105 dbesc($arr['network']),
1106 dbesc(NETWORK_DFRN),
1109 if($r && count($r)) {
1110 logger('duplicated item with the same uri found. ' . print_r($arr,true));
1114 // Check for an existing post with the same content. There seems to be a problem with OStatus.
1115 $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
1116 dbesc($arr['body']),
1117 dbesc($arr['network']),
1118 dbesc($arr['created']),
1119 intval($arr['contact-id']),
1122 if($r && count($r)) {
1123 logger('duplicated item with the same body found. ' . print_r($arr,true));
1127 // Is this item available in the global items (with uid=0)?
1128 if ($arr["uid"] == 0) {
1129 $arr["global"] = true;
1131 q("UPDATE `item` SET `global` = 1 WHERE `guid` = '%s'", dbesc($arr["guid"]));
1133 $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `guid` = '%s'", dbesc($arr["guid"]));
1135 $arr["global"] = (count($isglobal) > 0);
1138 // Fill the cache field
1139 put_item_in_cache($arr);
1142 call_hooks('post_local',$arr);
1144 call_hooks('post_remote',$arr);
1146 if(x($arr,'cancel')) {
1147 logger('item_store: post cancelled by plugin.');
1151 // Store the unescaped version
1156 logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
1158 $r = dbq("INSERT INTO `item` (`"
1159 . implode("`, `", array_keys($arr))
1161 . implode("', '", array_values($arr))
1167 // find the item that we just created
1168 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s' ORDER BY `id` ASC",
1170 intval($arr['uid']),
1171 dbesc($arr['network'])
1175 // There are duplicates. Keep the oldest one, delete the others
1176 logger('item_store: duplicated post occurred. Removing newer duplicates. uri = '.$arr['uri'].' uid = '.$arr['uid']);
1177 q("DELETE FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s' AND `id` > %d",
1179 intval($arr['uid']),
1180 dbesc($arr['network']),
1184 } elseif(count($r)) {
1186 // Store the guid and other relevant data
1189 $current_post = $r[0]['id'];
1190 logger('item_store: created item ' . $current_post);
1192 // Set "success_update" and "last-item" to the date of the last time we heard from this contact
1193 // This can be used to filter for inactive contacts.
1194 // Only do this for public postings to avoid privacy problems, since poco data is public.
1195 // Don't set this value if it isn't from the owner (could be an author that we don't know)
1197 $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
1199 // Is it a forum? Then we don't care about the rules from above
1200 if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
1201 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
1202 intval($arr['contact-id']));
1208 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1209 dbesc($arr['received']),
1210 dbesc($arr['received']),
1211 intval($arr['contact-id'])
1214 logger('item_store: could not locate created item');
1218 if((! $parent_id) || ($arr['parent-uri'] === $arr['uri']))
1219 $parent_id = $current_post;
1221 if(strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
1224 $private = $arr['private'];
1226 // Set parent id - and also make sure to inherit the parent's ACLs.
1228 $r = q("UPDATE `item` SET `parent` = %d, `allow_cid` = '%s', `allow_gid` = '%s',
1229 `deny_cid` = '%s', `deny_gid` = '%s', `private` = %d, `deleted` = %d WHERE `id` = %d",
1236 intval($parent_deleted),
1237 intval($current_post)
1240 $arr['id'] = $current_post;
1241 $arr['parent'] = $parent_id;
1242 $arr['allow_cid'] = $allow_cid;
1243 $arr['allow_gid'] = $allow_gid;
1244 $arr['deny_cid'] = $deny_cid;
1245 $arr['deny_gid'] = $deny_gid;
1246 $arr['private'] = $private;
1247 $arr['deleted'] = $parent_deleted;
1249 // update the commented timestamp on the parent
1250 // Only update "commented" if it is really a comment
1251 if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
1252 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
1253 dbesc(datetime_convert()),
1254 dbesc(datetime_convert()),
1258 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
1259 dbesc(datetime_convert()),
1265 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
1266 // We can check for this condition when we decode and encode the stuff again.
1267 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
1268 $dsprsig->signature = base64_decode($dsprsig->signature);
1269 logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
1272 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
1273 intval($current_post),
1274 dbesc($dsprsig->signed_text),
1275 dbesc($dsprsig->signature),
1276 dbesc($dsprsig->signer)
1282 * If this is now the last-child, force all _other_ children of this parent to *not* be last-child
1285 if($arr['last-child']) {
1286 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
1288 intval($arr['uid']),
1289 intval($current_post)
1293 $deleted = tag_deliver($arr['uid'],$current_post);
1295 // current post can be deleted if is for a community page and no mention are
1297 if (!$deleted AND !$dontcache) {
1299 $r = q('SELECT * FROM `item` WHERE id = %d', intval($current_post));
1300 if (count($r) == 1) {
1302 call_hooks('post_local_end', $r[0]);
1304 call_hooks('post_remote_end', $r[0]);
1306 logger('item_store: new item not found in DB, id ' . $current_post);
1309 // Add every contact of the post to the global contact table
1312 create_tags_from_item($current_post);
1313 create_files_from_item($current_post);
1315 // Only check for notifications on start posts
1316 if ($arr['parent-uri'] === $arr['uri']) {
1317 add_thread($current_post);
1318 logger('item_store: Check notification for contact '.$arr['contact-id'].' and post '.$current_post, LOGGER_DEBUG);
1320 // Send a notification for every new post?
1321 $r = q("SELECT `notify_new_posts` FROM `contact` WHERE `id` = %d AND `uid` = %d AND `notify_new_posts` LIMIT 1",
1322 intval($arr['contact-id']),
1325 $send_notification = count($r);
1327 if (!$send_notification) {
1328 $tags = q("SELECT `url` FROM `term` WHERE `otype` = %d AND `oid` = %d AND `type` = %d AND `uid` = %d",
1329 intval(TERM_OBJ_POST), intval($current_post), intval(TERM_MENTION), intval($arr['uid']));
1332 foreach ($tags AS $tag) {
1333 $r = q("SELECT `id` FROM `contact` WHERE `nurl` = '%s' AND `uid` = %d AND `notify_new_posts`",
1334 normalise_link($tag["url"]), intval($arr['uid']));
1336 $send_notification = true;
1341 if ($send_notification) {
1342 logger('item_store: Send notification for contact '.$arr['contact-id'].' and post '.$current_post, LOGGER_DEBUG);
1343 $u = q("SELECT * FROM user WHERE uid = %d LIMIT 1",
1344 intval($arr['uid']));
1346 $item = q("SELECT * FROM `item` WHERE `id` = %d AND `uid` = %d",
1347 intval($current_post),
1353 require_once('include/enotify.php');
1355 'type' => NOTIFY_SHARE,
1356 'notify_flags' => $u[0]['notify-flags'],
1357 'language' => $u[0]['language'],
1358 'to_name' => $u[0]['username'],
1359 'to_email' => $u[0]['email'],
1360 'uid' => $u[0]['uid'],
1362 'link' => $a->get_baseurl().'/display/'.urlencode($arr['guid']),
1363 'source_name' => $item[0]['author-name'],
1364 'source_link' => $item[0]['author-link'],
1365 'source_photo' => $item[0]['author-avatar'],
1366 'verb' => ACTIVITY_TAG,
1368 'parent' => $arr['parent']
1370 logger('item_store: Notification sent for contact '.$arr['contact-id'].' and post '.$current_post, LOGGER_DEBUG);
1373 update_thread($parent_id);
1374 add_shadow_entry($arr);
1378 proc_run('php', "include/notifier.php", $notify_type, $current_post);
1380 return $current_post;
1383 function item_body_set_hashtags(&$item) {
1385 $tags = get_tags($item["body"]);
1391 // This sorting is important when there are hashtags that are part of other hashtags
1392 // Otherwise there could be problems with hashtags like #test and #test2
1397 $URLSearchString = "^\[\]";
1399 // All hashtags should point to the home server
1400 //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1401 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
1403 //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1404 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
1406 // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
1407 $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1409 return("[url=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/url]");
1412 $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
1414 return("[bookmark=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/bookmark]");
1417 $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
1419 return("[attachment ".str_replace("#", "#", $match[1])."]".$match[2]."[/attachment]");
1422 // Repair recursive urls
1423 $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1424 "#$2", $item["body"]);
1427 foreach($tags as $tag) {
1428 if(strpos($tag,'#') !== 0)
1431 if(strpos($tag,'[url='))
1434 $basetag = str_replace('_',' ',substr($tag,1));
1436 $newtag = '#[url='.$a->get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1438 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1440 if(!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
1441 if(strlen($item["tag"]))
1442 $item["tag"] = ','.$item["tag"];
1443 $item["tag"] = $newtag.$item["tag"];
1447 // Convert back the masked hashtags
1448 $item["body"] = str_replace("#", "#", $item["body"]);
1451 function get_item_guid($id) {
1452 $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1454 return($r[0]["guid"]);
1459 function get_item_id($guid, $uid = 0) {
1465 $uid == local_user();
1467 // Does the given user have this item?
1469 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1470 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1471 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1474 $nick = $r[0]["nickname"];
1478 // Or is it anywhere on the server?
1480 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1481 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1482 AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = ''
1483 AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
1484 AND `item`.`private` = 0 AND `item`.`wall` = 1
1485 AND `item`.`guid` = '%s'", dbesc($guid));
1488 $nick = $r[0]["nickname"];
1491 return(array("nick" => $nick, "id" => $id));
1495 function get_item_contact($item,$contacts) {
1496 if(! count($contacts) || (! is_array($item)))
1498 foreach($contacts as $contact) {
1499 if($contact['id'] == $item['contact-id']) {
1501 break; // NOTREACHED
1508 * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1510 * @param int $item_id
1511 * @return bool true if item was deleted, else false
1513 function tag_deliver($uid,$item_id) {
1521 $u = q("select * from user where uid = %d limit 1",
1527 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1528 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1531 $i = q("select * from item where id = %d and uid = %d limit 1",
1540 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1542 // Diaspora uses their own hardwired link URL in @-tags
1543 // instead of the one we supply with webfinger
1545 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1547 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1549 foreach($matches as $mtch) {
1550 if(link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1552 logger('tag_deliver: mention found: ' . $mtch[2]);
1558 if ( ($community_page || $prvgroup) &&
1559 (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1560 // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1562 logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1563 q("DELETE FROM item WHERE id = %d and uid = %d",
1573 // send a notification
1575 // use a local photo if we have one
1577 $r = q("select * from contact where uid = %d and nurl = '%s' limit 1",
1578 intval($u[0]['uid']),
1579 dbesc(normalise_link($item['author-link']))
1581 $photo = (($r && count($r)) ? $r[0]['thumb'] : $item['author-avatar']);
1584 require_once('include/enotify.php');
1586 'type' => NOTIFY_TAGSELF,
1587 'notify_flags' => $u[0]['notify-flags'],
1588 'language' => $u[0]['language'],
1589 'to_name' => $u[0]['username'],
1590 'to_email' => $u[0]['email'],
1591 'uid' => $u[0]['uid'],
1593 'link' => $a->get_baseurl() . '/display/'.urlencode(get_item_guid($item['id'])),
1594 'source_name' => $item['author-name'],
1595 'source_link' => $item['author-link'],
1596 'source_photo' => $photo,
1597 'verb' => ACTIVITY_TAG,
1599 'parent' => $item['parent']
1603 $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1605 call_hooks('tagged', $arr);
1607 if((! $community_page) && (! $prvgroup))
1611 // tgroup delivery - setup a second delivery chain
1612 // prevent delivery looping - only proceed
1613 // if the message originated elsewhere and is a top-level post
1615 if(($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1618 // now change this copy of the post to a forum head message and deliver to all the tgroup members
1621 $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1622 intval($u[0]['uid'])
1627 // also reset all the privacy bits to the forum default permissions
1629 $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1631 $forum_mode = (($prvgroup) ? 2 : 1);
1633 q("update item set wall = 1, origin = 1, forum_mode = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1634 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' where id = %d",
1635 intval($forum_mode),
1636 dbesc($c[0]['name']),
1637 dbesc($c[0]['url']),
1638 dbesc($c[0]['thumb']),
1640 dbesc($u[0]['allow_cid']),
1641 dbesc($u[0]['allow_gid']),
1642 dbesc($u[0]['deny_cid']),
1643 dbesc($u[0]['deny_gid']),
1646 update_thread($item_id);
1648 proc_run('php','include/notifier.php','tgroup',$item_id);
1654 function tgroup_check($uid,$item) {
1660 // check that the message originated elsewhere and is a top-level post
1662 if(($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1666 $u = q("select * from user where uid = %d limit 1",
1672 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1673 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1676 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1678 // Diaspora uses their own hardwired link URL in @-tags
1679 // instead of the one we supply with webfinger
1681 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1683 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1685 foreach($matches as $mtch) {
1686 if(link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1688 logger('tgroup_check: mention found: ' . $mtch[2]);
1696 if((! $community_page) && (! $prvgroup))
1710 function dfrn_deliver($owner,$contact,$atom, $dissolve = false) {
1714 $idtosend = $orig_id = (($contact['dfrn-id']) ? $contact['dfrn-id'] : $contact['issued-id']);
1716 if($contact['duplex'] && $contact['dfrn-id'])
1717 $idtosend = '0:' . $orig_id;
1718 if($contact['duplex'] && $contact['issued-id'])
1719 $idtosend = '1:' . $orig_id;
1722 $rino = get_config('system','rino_encrypt');
1723 $rino = intval($rino);
1724 // use RINO1 if mcrypt isn't installed and RINO2 was selected
1725 if ($rino==2 and !function_exists('mcrypt_create_iv')) $rino=1;
1727 logger("Local rino version: ". $rino, LOGGER_DEBUG);
1729 $ssl_val = intval(get_config('system','ssl_policy'));
1733 case SSL_POLICY_FULL:
1734 $ssl_policy = 'full';
1736 case SSL_POLICY_SELFSIGN:
1737 $ssl_policy = 'self';
1739 case SSL_POLICY_NONE:
1741 $ssl_policy = 'none';
1745 $url = $contact['notify'] . '&dfrn_id=' . $idtosend . '&dfrn_version=' . DFRN_PROTOCOL_VERSION . (($rino) ? '&rino='.$rino : '');
1747 logger('dfrn_deliver: ' . $url);
1749 $xml = fetch_url($url);
1751 $curl_stat = $a->get_curl_code();
1753 return(-1); // timed out
1755 logger('dfrn_deliver: ' . $xml, LOGGER_DATA);
1760 if(strpos($xml,'<?xml') === false) {
1761 logger('dfrn_deliver: no valid XML returned');
1762 logger('dfrn_deliver: returned XML: ' . $xml, LOGGER_DATA);
1766 $res = parse_xml_string($xml);
1768 if((intval($res->status) != 0) || (! strlen($res->challenge)) || (! strlen($res->dfrn_id)))
1769 return (($res->status) ? $res->status : 3);
1771 $postvars = array();
1772 $sent_dfrn_id = hex2bin((string) $res->dfrn_id);
1773 $challenge = hex2bin((string) $res->challenge);
1774 $perm = (($res->perm) ? $res->perm : null);
1775 $dfrn_version = (float) (($res->dfrn_version) ? $res->dfrn_version : 2.0);
1776 $rino_remote_version = intval($res->rino);
1777 $page = (($owner['page-flags'] == PAGE_COMMUNITY) ? 1 : 0);
1779 logger("Remote rino version: ".$rino_remote_version." for ".$contact["url"], LOGGER_DEBUG);
1781 if($owner['page-flags'] == PAGE_PRVGROUP)
1784 $final_dfrn_id = '';
1787 if((($perm == 'rw') && (! intval($contact['writable'])))
1788 || (($perm == 'r') && (intval($contact['writable'])))) {
1789 q("update contact set writable = %d where id = %d",
1790 intval(($perm == 'rw') ? 1 : 0),
1791 intval($contact['id'])
1793 $contact['writable'] = (string) 1 - intval($contact['writable']);
1797 if(($contact['duplex'] && strlen($contact['pubkey']))
1798 || ($owner['page-flags'] == PAGE_COMMUNITY && strlen($contact['pubkey']))
1799 || ($contact['rel'] == CONTACT_IS_SHARING && strlen($contact['pubkey']))) {
1800 openssl_public_decrypt($sent_dfrn_id,$final_dfrn_id,$contact['pubkey']);
1801 openssl_public_decrypt($challenge,$postvars['challenge'],$contact['pubkey']);
1804 openssl_private_decrypt($sent_dfrn_id,$final_dfrn_id,$contact['prvkey']);
1805 openssl_private_decrypt($challenge,$postvars['challenge'],$contact['prvkey']);
1808 $final_dfrn_id = substr($final_dfrn_id, 0, strpos($final_dfrn_id, '.'));
1810 if(strpos($final_dfrn_id,':') == 1)
1811 $final_dfrn_id = substr($final_dfrn_id,2);
1813 if($final_dfrn_id != $orig_id) {
1814 logger('dfrn_deliver: wrong dfrn_id.');
1815 // did not decode properly - cannot trust this site
1819 $postvars['dfrn_id'] = $idtosend;
1820 $postvars['dfrn_version'] = DFRN_PROTOCOL_VERSION;
1822 $postvars['dissolve'] = '1';
1825 if((($contact['rel']) && ($contact['rel'] != CONTACT_IS_SHARING) && (! $contact['blocked'])) || ($owner['page-flags'] == PAGE_COMMUNITY)) {
1826 $postvars['data'] = $atom;
1827 $postvars['perm'] = 'rw';
1830 $postvars['data'] = str_replace('<dfrn:comment-allow>1','<dfrn:comment-allow>0',$atom);
1831 $postvars['perm'] = 'r';
1834 $postvars['ssl_policy'] = $ssl_policy;
1837 $postvars['page'] = $page;
1840 if($rino>0 && $rino_remote_version>0 && (! $dissolve)) {
1841 logger('rino version: '. $rino_remote_version);
1843 switch($rino_remote_version) {
1845 // Deprecated rino version!
1846 $key = substr(random_string(),0,16);
1847 $data = aes_encrypt($postvars['data'],$key);
1850 // RINO 2 based on php-encryption
1852 $key = Crypto::createNewRandomKey();
1853 } catch (CryptoTestFailed $ex) {
1854 logger('Cannot safely create a key');
1856 } catch (CannotPerformOperation $ex) {
1857 logger('Cannot safely create a key');
1861 $data = Crypto::encrypt($postvars['data'], $key);
1862 } catch (CryptoTestFailed $ex) {
1863 logger('Cannot safely perform encryption');
1865 } catch (CannotPerformOperation $ex) {
1866 logger('Cannot safely perform encryption');
1871 logger("rino: invalid requested verision '$rino_remote_version'");
1875 $postvars['rino'] = $rino_remote_version;
1876 $postvars['data'] = bin2hex($data);
1878 #logger('rino: sent key = ' . $key, LOGGER_DEBUG);
1881 if($dfrn_version >= 2.1) {
1882 if(($contact['duplex'] && strlen($contact['pubkey']))
1883 || ($owner['page-flags'] == PAGE_COMMUNITY && strlen($contact['pubkey']))
1884 || ($contact['rel'] == CONTACT_IS_SHARING && strlen($contact['pubkey']))) {
1886 openssl_public_encrypt($key,$postvars['key'],$contact['pubkey']);
1889 openssl_private_encrypt($key,$postvars['key'],$contact['prvkey']);
1893 if(($contact['duplex'] && strlen($contact['prvkey'])) || ($owner['page-flags'] == PAGE_COMMUNITY)) {
1894 openssl_private_encrypt($key,$postvars['key'],$contact['prvkey']);
1897 openssl_public_encrypt($key,$postvars['key'],$contact['pubkey']);
1901 logger('md5 rawkey ' . md5($postvars['key']));
1903 $postvars['key'] = bin2hex($postvars['key']);
1907 logger('dfrn_deliver: ' . "SENDING: " . print_r($postvars,true), LOGGER_DATA);
1909 $xml = post_url($contact['notify'],$postvars);
1911 logger('dfrn_deliver: ' . "RECEIVED: " . $xml, LOGGER_DATA);
1913 $curl_stat = $a->get_curl_code();
1914 if((! $curl_stat) || (! strlen($xml)))
1915 return(-1); // timed out
1917 if(($curl_stat == 503) && (stristr($a->get_curl_headers(),'retry-after')))
1920 if(strpos($xml,'<?xml') === false) {
1921 logger('dfrn_deliver: phase 2: no valid XML returned');
1922 logger('dfrn_deliver: phase 2: returned XML: ' . $xml, LOGGER_DATA);
1926 if($contact['term-date'] != '0000-00-00 00:00:00') {
1927 logger("dfrn_deliver: $url back from the dead - removing mark for death");
1928 require_once('include/Contact.php');
1929 unmark_for_death($contact);
1932 $res = parse_xml_string($xml);
1934 return $res->status;
1939 This function returns true if $update has an edited timestamp newer
1940 than $existing, i.e. $update contains new data which should override
1941 what's already there. If there is no timestamp yet, the update is
1942 assumed to be newer. If the update has no timestamp, the existing
1943 item is assumed to be up-to-date. If the timestamps are equal it
1944 assumes the update has been seen before and should be ignored.
1946 function edited_timestamp_is_newer($existing, $update) {
1947 if (!x($existing,'edited') || !$existing['edited']) {
1950 if (!x($update,'edited') || !$update['edited']) {
1953 $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1954 $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1955 return (strcmp($existing_edited, $update_edited) < 0);
1960 * consume_feed - process atom feed and update anything/everything we might need to update
1962 * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1964 * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1965 * It is this person's stuff that is going to be updated.
1966 * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1967 * from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1968 * have a contact record.
1969 * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1970 * might not) try and subscribe to it.
1971 * $datedir sorts in reverse order
1972 * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1973 * imported prior to its children being seen in the stream unless we are certain
1974 * of how the feed is arranged/ordered.
1975 * With $pass = 1, we only pull parent items out of the stream.
1976 * With $pass = 2, we only pull children (comments/likes).
1978 * So running this twice, first with pass 1 and then with pass 2 will do the right
1979 * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1980 * model where comments can have sub-threads. That would require some massive sorting
1981 * to get all the feed items into a mostly linear ordering, and might still require
1985 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1986 if ($contact['network'] === NETWORK_OSTATUS) {
1988 // Test - remove before flight
1989 //$tempfile = tempnam(get_temppath(), "ostatus2");
1990 //file_put_contents($tempfile, $xml);
1991 logger("Consume OStatus messages ", LOGGER_DEBUG);
1992 ostatus_import($xml,$importer,$contact, $hub);
1997 if ($contact['network'] === NETWORK_FEED) {
1999 logger("Consume feeds", LOGGER_DEBUG);
2000 feed_import($xml,$importer,$contact, $hub);
2005 require_once('library/simplepie/simplepie.inc');
2006 require_once('include/contact_selectors.php');
2008 if(! strlen($xml)) {
2009 logger('consume_feed: empty input');
2013 $feed = new SimplePie();
2014 $feed->set_raw_data($xml);
2016 $feed->enable_order_by_date(true);
2018 $feed->enable_order_by_date(false);
2022 logger('consume_feed: Error parsing XML: ' . $feed->error());
2024 $permalink = $feed->get_permalink();
2026 // Check at the feed level for updated contact name and/or photo
2030 $photo_timestamp = '';
2033 $contact_updated = '';
2035 $hubs = $feed->get_links('hub');
2036 logger('consume_feed: hubs: ' . print_r($hubs,true), LOGGER_DATA);
2039 $hub = implode(',', $hubs);
2041 $rawtags = $feed->get_feed_tags( NAMESPACE_DFRN, 'owner');
2043 $rawtags = $feed->get_feed_tags( SIMPLEPIE_NAMESPACE_ATOM_10, 'author');
2045 $elems = $rawtags[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10];
2046 if($elems['name'][0]['attribs'][NAMESPACE_DFRN]['updated']) {
2047 $name_updated = $elems['name'][0]['attribs'][NAMESPACE_DFRN]['updated'];
2048 $new_name = $elems['name'][0]['data'];
2050 // Manually checking for changed contact names
2051 if (($new_name != $contact['name']) AND ($new_name != "") AND ($name_updated <= $contact['name-date'])) {
2052 $name_updated = date("c");
2053 $photo_timestamp = date("c");
2056 if((x($elems,'link')) && ($elems['link'][0]['attribs']['']['rel'] === 'photo') && ($elems['link'][0]['attribs'][NAMESPACE_DFRN]['updated'])) {
2057 if ($photo_timestamp == "")
2058 $photo_timestamp = datetime_convert('UTC','UTC',$elems['link'][0]['attribs'][NAMESPACE_DFRN]['updated']);
2059 $photo_url = $elems['link'][0]['attribs']['']['href'];
2062 if((x($rawtags[0]['child'], NAMESPACE_DFRN)) && (x($rawtags[0]['child'][NAMESPACE_DFRN],'birthday'))) {
2063 $birthday = datetime_convert('UTC','UTC', $rawtags[0]['child'][NAMESPACE_DFRN]['birthday'][0]['data']);
2067 if((is_array($contact)) && ($photo_timestamp) && (strlen($photo_url)) && ($photo_timestamp > $contact['avatar-date'])) {
2068 logger('consume_feed: Updating photo for '.$contact['name'].' from '.$photo_url.' uid: '.$contact['uid']);
2070 $contact_updated = $photo_timestamp;
2072 require_once("include/Photo.php");
2073 $photos = import_profile_photo($photo_url,$contact['uid'],$contact['id']);
2075 q("UPDATE `contact` SET `avatar-date` = '%s', `photo` = '%s', `thumb` = '%s', `micro` = '%s'
2076 WHERE `uid` = %d AND `id` = %d AND NOT `self`",
2077 dbesc(datetime_convert()),
2081 intval($contact['uid']),
2082 intval($contact['id'])
2086 if((is_array($contact)) && ($name_updated) && (strlen($new_name)) && ($name_updated > $contact['name-date'])) {
2087 if ($name_updated > $contact_updated)
2088 $contact_updated = $name_updated;
2090 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `id` = %d LIMIT 1",
2091 intval($contact['uid']),
2092 intval($contact['id'])
2095 $x = q("UPDATE `contact` SET `name` = '%s', `name-date` = '%s' WHERE `uid` = %d AND `id` = %d AND `name` != '%s' AND NOT `self`",
2096 dbesc(notags(trim($new_name))),
2097 dbesc(datetime_convert()),
2098 intval($contact['uid']),
2099 intval($contact['id']),
2100 dbesc(notags(trim($new_name)))
2103 // do our best to update the name on content items
2105 if(count($r) AND (notags(trim($new_name)) != $r[0]['name'])) {
2106 q("UPDATE `item` SET `author-name` = '%s' WHERE `author-name` = '%s' AND `author-link` = '%s' AND `uid` = %d AND `author-name` != '%s'",
2107 dbesc(notags(trim($new_name))),
2108 dbesc($r[0]['name']),
2109 dbesc($r[0]['url']),
2110 intval($contact['uid']),
2111 dbesc(notags(trim($new_name)))
2116 if ($contact_updated AND $new_name AND $photo_url)
2117 poco_check($contact['url'], $new_name, NETWORK_DFRN, $photo_url, "", "", "", "", "", $contact_updated, 2, $contact['id'], $contact['uid']);
2119 if(strlen($birthday)) {
2120 if(substr($birthday,0,4) != $contact['bdyear']) {
2121 logger('consume_feed: updating birthday: ' . $birthday);
2125 * Add new birthday event for this person
2127 * $bdtext is just a readable placeholder in case the event is shared
2128 * with others. We will replace it during presentation to our $importer
2129 * to contain a sparkle link and perhaps a photo.
2133 $bdtext = sprintf( t('%s\'s birthday'), $contact['name']);
2134 $bdtext2 = sprintf( t('Happy Birthday %s'), ' [url=' . $contact['url'] . ']' . $contact['name'] . '[/url]' ) ;
2137 $r = q("INSERT INTO `event` (`uid`,`cid`,`created`,`edited`,`start`,`finish`,`summary`,`desc`,`type`)
2138 VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) ",
2139 intval($contact['uid']),
2140 intval($contact['id']),
2141 dbesc(datetime_convert()),
2142 dbesc(datetime_convert()),
2143 dbesc(datetime_convert('UTC','UTC', $birthday)),
2144 dbesc(datetime_convert('UTC','UTC', $birthday . ' + 1 day ')),
2153 q("UPDATE `contact` SET `bdyear` = '%s' WHERE `uid` = %d AND `id` = %d",
2154 dbesc(substr($birthday,0,4)),
2155 intval($contact['uid']),
2156 intval($contact['id'])
2159 // This function is called twice without reloading the contact
2160 // Make sure we only create one event. This is why &$contact
2161 // is a reference var in this function
2163 $contact['bdyear'] = substr($birthday,0,4);
2167 $community_page = 0;
2168 $rawtags = $feed->get_feed_tags( NAMESPACE_DFRN, 'community');
2170 $community_page = intval($rawtags[0]['data']);
2172 if(is_array($contact) && intval($contact['forum']) != $community_page) {
2173 q("update contact set forum = %d where id = %d",
2174 intval($community_page),
2175 intval($contact['id'])
2177 $contact['forum'] = (string) $community_page;
2181 // process any deleted entries
2183 $del_entries = $feed->get_feed_tags(NAMESPACE_TOMB, 'deleted-entry');
2184 if(is_array($del_entries) && count($del_entries) && $pass != 2) {
2185 foreach($del_entries as $dentry) {
2187 if(isset($dentry['attribs']['']['ref'])) {
2188 $uri = $dentry['attribs']['']['ref'];
2190 if(isset($dentry['attribs']['']['when'])) {
2191 $when = $dentry['attribs']['']['when'];
2192 $when = datetime_convert('UTC','UTC', $when, 'Y-m-d H:i:s');
2195 $when = datetime_convert('UTC','UTC','now','Y-m-d H:i:s');
2197 if($deleted && is_array($contact)) {
2198 $r = q("SELECT `item`.*, `contact`.`self` FROM `item` INNER JOIN `contact` on `item`.`contact-id` = `contact`.`id`
2199 WHERE `uri` = '%s' AND `item`.`uid` = %d AND `contact-id` = %d AND NOT `item`.`file` LIKE '%%[%%' LIMIT 1",
2201 intval($importer['uid']),
2202 intval($contact['id'])
2207 if(! $item['deleted'])
2208 logger('consume_feed: deleting item ' . $item['id'] . ' uri=' . $item['uri'], LOGGER_DEBUG);
2210 if($item['object-type'] === ACTIVITY_OBJ_EVENT) {
2211 logger("Deleting event ".$item['event-id'], LOGGER_DEBUG);
2212 event_delete($item['event-id']);
2215 if(($item['verb'] === ACTIVITY_TAG) && ($item['object-type'] === ACTIVITY_OBJ_TAGTERM)) {
2216 $xo = parse_xml_string($item['object'],false);
2217 $xt = parse_xml_string($item['target'],false);
2218 if($xt->type === ACTIVITY_OBJ_NOTE) {
2219 $i = q("select * from `item` where uri = '%s' and uid = %d limit 1",
2221 intval($importer['importer_uid'])
2225 // For tags, the owner cannot remove the tag on the author's copy of the post.
2227 $owner_remove = (($item['contact-id'] == $i[0]['contact-id']) ? true: false);
2228 $author_remove = (($item['origin'] && $item['self']) ? true : false);
2229 $author_copy = (($item['origin']) ? true : false);
2231 if($owner_remove && $author_copy)
2233 if($author_remove || $owner_remove) {
2234 $tags = explode(',',$i[0]['tag']);
2237 foreach($tags as $tag)
2238 if(trim($tag) !== trim($xo->body))
2239 $newtags[] = trim($tag);
2241 q("update item set tag = '%s' where id = %d",
2242 dbesc(implode(',',$newtags)),
2245 create_tags_from_item($i[0]['id']);
2251 if($item['uri'] == $item['parent-uri']) {
2252 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s',
2253 `body` = '', `title` = ''
2254 WHERE `parent-uri` = '%s' AND `uid` = %d",
2256 dbesc(datetime_convert()),
2257 dbesc($item['uri']),
2258 intval($importer['uid'])
2260 create_tags_from_itemuri($item['uri'], $importer['uid']);
2261 create_files_from_itemuri($item['uri'], $importer['uid']);
2262 update_thread_uri($item['uri'], $importer['uid']);
2265 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s',
2266 `body` = '', `title` = ''
2267 WHERE `uri` = '%s' AND `uid` = %d",
2269 dbesc(datetime_convert()),
2271 intval($importer['uid'])
2273 create_tags_from_itemuri($uri, $importer['uid']);
2274 create_files_from_itemuri($uri, $importer['uid']);
2275 if($item['last-child']) {
2276 // ensure that last-child is set in case the comment that had it just got wiped.
2277 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2278 dbesc(datetime_convert()),
2279 dbesc($item['parent-uri']),
2280 intval($item['uid'])
2282 // who is the last child now?
2283 $r = q("SELECT `id` FROM `item` WHERE `parent-uri` = '%s' AND `type` != 'activity' AND `deleted` = 0 AND `moderated` = 0 AND `uid` = %d
2284 ORDER BY `created` DESC LIMIT 1",
2285 dbesc($item['parent-uri']),
2286 intval($importer['uid'])
2289 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2300 // Now process the feed
2302 if($feed->get_item_quantity()) {
2304 logger('consume_feed: feed item count = ' . $feed->get_item_quantity());
2306 // in inverse date order
2308 $items = array_reverse($feed->get_items());
2310 $items = $feed->get_items();
2313 foreach($items as $item) {
2316 $item_id = $item->get_id();
2317 $rawthread = $item->get_item_tags( NAMESPACE_THREAD,'in-reply-to');
2318 if(isset($rawthread[0]['attribs']['']['ref'])) {
2320 $parent_uri = $rawthread[0]['attribs']['']['ref'];
2323 if(($is_reply) && is_array($contact)) {
2328 // not allowed to post
2330 if($contact['rel'] == CONTACT_IS_FOLLOWER)
2334 // Have we seen it? If not, import it.
2336 $item_id = $item->get_id();
2337 $datarray = get_atom_elements($feed, $item, $contact);
2339 if((! x($datarray,'author-name')) && ($contact['network'] != NETWORK_DFRN))
2340 $datarray['author-name'] = $contact['name'];
2341 if((! x($datarray,'author-link')) && ($contact['network'] != NETWORK_DFRN))
2342 $datarray['author-link'] = $contact['url'];
2343 if((! x($datarray,'author-avatar')) && ($contact['network'] != NETWORK_DFRN))
2344 $datarray['author-avatar'] = $contact['thumb'];
2346 if((! x($datarray,'author-name')) || (! x($datarray,'author-link'))) {
2347 logger('consume_feed: no author information! ' . print_r($datarray,true));
2351 $force_parent = false;
2352 if($contact['network'] === NETWORK_OSTATUS || stristr($contact['url'],'twitter.com')) {
2353 if($contact['network'] === NETWORK_OSTATUS)
2354 $force_parent = true;
2355 if(strlen($datarray['title']))
2356 unset($datarray['title']);
2357 $r = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d",
2358 dbesc(datetime_convert()),
2360 intval($importer['uid'])
2362 $datarray['last-child'] = 1;
2363 update_thread_uri($parent_uri, $importer['uid']);
2367 $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
2369 intval($importer['uid'])
2372 // Update content if 'updated' changes
2375 if (edited_timestamp_is_newer($r[0], $datarray)) {
2377 // do not accept (ignore) an earlier edit than one we currently have.
2378 if(datetime_convert('UTC','UTC',$datarray['edited']) < $r[0]['edited'])
2381 $r = q("UPDATE `item` SET `title` = '%s', `body` = '%s', `tag` = '%s', `edited` = '%s', `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d",
2382 dbesc($datarray['title']),
2383 dbesc($datarray['body']),
2384 dbesc($datarray['tag']),
2385 dbesc(datetime_convert('UTC','UTC',$datarray['edited'])),
2386 dbesc(datetime_convert()),
2388 intval($importer['uid'])
2390 create_tags_from_itemuri($item_id, $importer['uid']);
2391 update_thread_uri($item_id, $importer['uid']);
2394 // update last-child if it changes
2396 $allow = $item->get_item_tags( NAMESPACE_DFRN, 'comment-allow');
2397 if(($allow) && ($allow[0]['data'] != $r[0]['last-child'])) {
2398 $r = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d",
2399 dbesc(datetime_convert()),
2401 intval($importer['uid'])
2403 $r = q("UPDATE `item` SET `last-child` = %d , `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d",
2404 intval($allow[0]['data']),
2405 dbesc(datetime_convert()),
2407 intval($importer['uid'])
2409 update_thread_uri($item_id, $importer['uid']);
2415 if(($contact['network'] === NETWORK_FEED) || (! strlen($contact['notify']))) {
2416 // one way feed - no remote comment ability
2417 $datarray['last-child'] = 0;
2419 $datarray['parent-uri'] = $parent_uri;
2420 $datarray['uid'] = $importer['uid'];
2421 $datarray['contact-id'] = $contact['id'];
2422 if(($datarray['verb'] === ACTIVITY_LIKE)
2423 || ($datarray['verb'] === ACTIVITY_DISLIKE)
2424 || ($datarray['verb'] === ACTIVITY_ATTEND)
2425 || ($datarray['verb'] === ACTIVITY_ATTENDNO)
2426 || ($datarray['verb'] === ACTIVITY_ATTENDMAYBE)) {
2427 $datarray['type'] = 'activity';
2428 $datarray['gravity'] = GRAVITY_LIKE;
2429 // only one like or dislike per person
2430 // splitted into two queries for performance issues
2431 $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `author-link` = '%s' AND `verb` = '%s' AND `parent-uri` = '%s' AND NOT `deleted` LIMIT 1",
2432 intval($datarray['uid']),
2433 dbesc($datarray['author-link']),
2434 dbesc($datarray['verb']),
2435 dbesc($datarray['parent-uri'])
2440 $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `author-link` = '%s' AND `verb` = '%s' AND `thr-parent` = '%s' AND NOT `deleted` LIMIT 1",
2441 intval($datarray['uid']),
2442 dbesc($datarray['author-link']),
2443 dbesc($datarray['verb']),
2444 dbesc($datarray['parent-uri'])
2450 if(($datarray['verb'] === ACTIVITY_TAG) && ($datarray['object-type'] === ACTIVITY_OBJ_TAGTERM)) {
2451 $xo = parse_xml_string($datarray['object'],false);
2452 $xt = parse_xml_string($datarray['target'],false);
2454 if($xt->type == ACTIVITY_OBJ_NOTE) {
2455 $r = q("select * from item where `uri` = '%s' AND `uid` = %d limit 1",
2457 intval($importer['importer_uid'])
2462 // extract tag, if not duplicate, add to parent item
2463 if($xo->id && $xo->content) {
2464 $newtag = '#[url=' . $xo->id . ']'. $xo->content . '[/url]';
2465 if(! (stristr($r[0]['tag'],$newtag))) {
2466 q("UPDATE item SET tag = '%s' WHERE id = %d",
2467 dbesc($r[0]['tag'] . (strlen($r[0]['tag']) ? ',' : '') . $newtag),
2470 create_tags_from_item($r[0]['id']);
2476 $r = item_store($datarray,$force_parent);
2482 // Head post of a conversation. Have we seen it? If not, import it.
2484 $item_id = $item->get_id();
2486 $datarray = get_atom_elements($feed, $item, $contact);
2488 if(is_array($contact)) {
2489 if((! x($datarray,'author-name')) && ($contact['network'] != NETWORK_DFRN))
2490 $datarray['author-name'] = $contact['name'];
2491 if((! x($datarray,'author-link')) && ($contact['network'] != NETWORK_DFRN))
2492 $datarray['author-link'] = $contact['url'];
2493 if((! x($datarray,'author-avatar')) && ($contact['network'] != NETWORK_DFRN))
2494 $datarray['author-avatar'] = $contact['thumb'];
2497 if((! x($datarray,'author-name')) || (! x($datarray,'author-link'))) {
2498 logger('consume_feed: no author information! ' . print_r($datarray,true));
2502 // special handling for events
2504 if((x($datarray,'object-type')) && ($datarray['object-type'] === ACTIVITY_OBJ_EVENT)) {
2505 $ev = bbtoevent($datarray['body']);
2506 if((x($ev,'desc') || x($ev,'summary')) && x($ev,'start')) {
2507 $ev['uid'] = $importer['uid'];
2508 $ev['uri'] = $item_id;
2509 $ev['edited'] = $datarray['edited'];
2510 $ev['private'] = $datarray['private'];
2511 $ev['guid'] = $datarray['guid'];
2513 if(is_array($contact))
2514 $ev['cid'] = $contact['id'];
2515 $r = q("SELECT * FROM `event` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
2517 intval($importer['uid'])
2520 $ev['id'] = $r[0]['id'];
2521 $xyz = event_store($ev);
2526 if($contact['network'] === NETWORK_OSTATUS || stristr($contact['url'],'twitter.com')) {
2527 if(strlen($datarray['title']))
2528 unset($datarray['title']);
2529 $datarray['last-child'] = 1;
2533 $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
2535 intval($importer['uid'])
2538 // Update content if 'updated' changes
2541 if (edited_timestamp_is_newer($r[0], $datarray)) {
2543 // do not accept (ignore) an earlier edit than one we currently have.
2544 if(datetime_convert('UTC','UTC',$datarray['edited']) < $r[0]['edited'])
2547 $r = q("UPDATE `item` SET `title` = '%s', `body` = '%s', `tag` = '%s', `edited` = '%s', `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d",
2548 dbesc($datarray['title']),
2549 dbesc($datarray['body']),
2550 dbesc($datarray['tag']),
2551 dbesc(datetime_convert('UTC','UTC',$datarray['edited'])),
2552 dbesc(datetime_convert()),
2554 intval($importer['uid'])
2556 create_tags_from_itemuri($item_id, $importer['uid']);
2557 update_thread_uri($item_id, $importer['uid']);
2560 // update last-child if it changes
2562 $allow = $item->get_item_tags( NAMESPACE_DFRN, 'comment-allow');
2563 if($allow && $allow[0]['data'] != $r[0]['last-child']) {
2564 $r = q("UPDATE `item` SET `last-child` = %d , `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d",
2565 intval($allow[0]['data']),
2566 dbesc(datetime_convert()),
2568 intval($importer['uid'])
2570 update_thread_uri($item_id, $importer['uid']);
2575 if(activity_match($datarray['verb'],ACTIVITY_FOLLOW)) {
2576 logger('consume-feed: New follower');
2577 new_follower($importer,$contact,$datarray,$item);
2580 if(activity_match($datarray['verb'],ACTIVITY_UNFOLLOW)) {
2581 lose_follower($importer,$contact,$datarray,$item);
2585 if(activity_match($datarray['verb'],ACTIVITY_REQ_FRIEND)) {
2586 logger('consume-feed: New friend request');
2587 new_follower($importer,$contact,$datarray,$item,true);
2590 if(activity_match($datarray['verb'],ACTIVITY_UNFRIEND)) {
2591 lose_sharer($importer,$contact,$datarray,$item);
2596 if(! is_array($contact))
2600 if(($contact['network'] === NETWORK_FEED) || (! strlen($contact['notify']))) {
2601 // one way feed - no remote comment ability
2602 $datarray['last-child'] = 0;
2604 if($contact['network'] === NETWORK_FEED)
2605 $datarray['private'] = 2;
2607 $datarray['parent-uri'] = $item_id;
2608 $datarray['uid'] = $importer['uid'];
2609 $datarray['contact-id'] = $contact['id'];
2611 if(! link_compare($datarray['owner-link'],$contact['url'])) {
2612 // The item owner info is not our contact. It's OK and is to be expected if this is a tgroup delivery,
2613 // but otherwise there's a possible data mixup on the sender's system.
2614 // the tgroup delivery code called from item_store will correct it if it's a forum,
2615 // but we're going to unconditionally correct it here so that the post will always be owned by our contact.
2616 logger('consume_feed: Correcting item owner.', LOGGER_DEBUG);
2617 $datarray['owner-name'] = $contact['name'];
2618 $datarray['owner-link'] = $contact['url'];
2619 $datarray['owner-avatar'] = $contact['thumb'];
2622 // We've allowed "followers" to reach this point so we can decide if they are
2623 // posting an @-tag delivery, which followers are allowed to do for certain
2624 // page types. Now that we've parsed the post, let's check if it is legit. Otherwise ignore it.
2626 if(($contact['rel'] == CONTACT_IS_FOLLOWER) && (! tgroup_check($importer['uid'],$datarray)))
2629 // This is my contact on another system, but it's really me.
2630 // Turn this into a wall post.
2631 $notify = item_is_remote_self($contact, $datarray);
2633 $r = item_store($datarray, false, $notify);
2634 logger('Stored - Contact '.$contact['url'].' Notify '.$notify.' return '.$r.' Item '.print_r($datarray, true), LOGGER_DEBUG);
2642 function item_is_remote_self($contact, &$datarray) {
2645 if (!$contact['remote_self'])
2648 // Prevent the forwarding of posts that are forwarded
2649 if ($datarray["extid"] == NETWORK_DFRN)
2652 // Prevent to forward already forwarded posts
2653 if ($datarray["app"] == $a->get_hostname())
2656 // Only forward posts
2657 if ($datarray["verb"] != ACTIVITY_POST)
2660 if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
2663 $datarray2 = $datarray;
2664 logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
2665 if ($contact['remote_self'] == 2) {
2666 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
2667 intval($contact['uid']));
2669 $datarray['contact-id'] = $r[0]["id"];
2671 $datarray['owner-name'] = $r[0]["name"];
2672 $datarray['owner-link'] = $r[0]["url"];
2673 $datarray['owner-avatar'] = $r[0]["thumb"];
2675 $datarray['author-name'] = $datarray['owner-name'];
2676 $datarray['author-link'] = $datarray['owner-link'];
2677 $datarray['author-avatar'] = $datarray['owner-avatar'];
2680 if ($contact['network'] != NETWORK_FEED) {
2681 $datarray["guid"] = get_guid(32);
2682 unset($datarray["plink"]);
2683 $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
2684 $datarray["parent-uri"] = $datarray["uri"];
2685 $datarray["extid"] = $contact['network'];
2686 $urlpart = parse_url($datarray2['author-link']);
2687 $datarray["app"] = $urlpart["host"];
2689 $datarray['private'] = 0;
2692 if ($contact['network'] != NETWORK_FEED) {
2693 // Store the original post
2694 $r = item_store($datarray2, false, false);
2695 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
2697 $datarray["app"] = "Feed";
2702 function local_delivery($importer,$data) {
2704 require_once('library/simplepie/simplepie.inc');
2708 logger(__function__, LOGGER_TRACE);
2710 if($importer['readonly']) {
2711 // We aren't receiving stuff from this person. But we will quietly ignore them
2712 // rather than a blatant "go away" message.
2713 logger('local_delivery: ignoring');
2718 // Consume notification feed. This may differ from consuming a public feed in several ways
2719 // - might contain email or friend suggestions
2720 // - might contain remote followup to our message
2721 // - in which case we need to accept it and then notify other conversants
2722 // - we may need to send various email notifications
2724 $feed = new SimplePie();
2725 $feed->set_raw_data($data);
2726 $feed->enable_order_by_date(false);
2731 logger('local_delivery: Error parsing XML: ' . $feed->error());
2734 // Check at the feed level for updated contact name and/or photo
2738 $photo_timestamp = '';
2740 $contact_updated = '';
2743 $rawtags = $feed->get_feed_tags( NAMESPACE_DFRN, 'owner');
2745 // Fallback should not be needed here. If it isn't DFRN it won't have DFRN updated tags
2747 // $rawtags = $feed->get_feed_tags( SIMPLEPIE_NAMESPACE_ATOM_10, 'author');
2750 $elems = $rawtags[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10];
2751 if($elems['name'][0]['attribs'][NAMESPACE_DFRN]['updated']) {
2752 $name_updated = $elems['name'][0]['attribs'][NAMESPACE_DFRN]['updated'];
2753 $new_name = $elems['name'][0]['data'];
2755 // Manually checking for changed contact names
2756 if (($new_name != $importer['name']) AND ($new_name != "") AND ($name_updated <= $importer['name-date'])) {
2757 $name_updated = date("c");
2758 $photo_timestamp = date("c");
2761 if((x($elems,'link')) && ($elems['link'][0]['attribs']['']['rel'] === 'photo') && ($elems['link'][0]['attribs'][NAMESPACE_DFRN]['updated'])) {
2762 if ($photo_timestamp == "")
2763 $photo_timestamp = datetime_convert('UTC','UTC',$elems['link'][0]['attribs'][NAMESPACE_DFRN]['updated']);
2764 $photo_url = $elems['link'][0]['attribs']['']['href'];
2768 if(($photo_timestamp) && (strlen($photo_url)) && ($photo_timestamp > $importer['avatar-date'])) {
2770 $contact_updated = $photo_timestamp;
2772 logger('local_delivery: Updating photo for ' . $importer['name']);
2773 require_once("include/Photo.php");
2775 $photos = import_profile_photo($photo_url,$importer['importer_uid'],$importer['id']);
2777 q("UPDATE `contact` SET `avatar-date` = '%s', `photo` = '%s', `thumb` = '%s', `micro` = '%s'
2778 WHERE `uid` = %d AND `id` = %d AND NOT `self`",
2779 dbesc(datetime_convert()),
2783 intval($importer['importer_uid']),
2784 intval($importer['id'])
2788 if(($name_updated) && (strlen($new_name)) && ($name_updated > $importer['name-date'])) {
2789 if ($name_updated > $contact_updated)
2790 $contact_updated = $name_updated;
2792 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `id` = %d LIMIT 1",
2793 intval($importer['importer_uid']),
2794 intval($importer['id'])
2797 $x = q("UPDATE `contact` SET `name` = '%s', `name-date` = '%s' WHERE `uid` = %d AND `id` = %d AND `name` != '%s' AND NOT `self`",
2798 dbesc(notags(trim($new_name))),
2799 dbesc(datetime_convert()),
2800 intval($importer['importer_uid']),
2801 intval($importer['id']),
2802 dbesc(notags(trim($new_name)))
2805 // do our best to update the name on content items
2807 if(count($r) AND (notags(trim($new_name)) != $r[0]['name'])) {
2808 q("UPDATE `item` SET `author-name` = '%s' WHERE `author-name` = '%s' AND `author-link` = '%s' AND `uid` = %d AND `author-name` != '%s'",
2809 dbesc(notags(trim($new_name))),
2810 dbesc($r[0]['name']),
2811 dbesc($r[0]['url']),
2812 intval($importer['importer_uid']),
2813 dbesc(notags(trim($new_name)))
2818 if ($contact_updated AND $new_name AND $photo_url)
2819 poco_check($importer['url'], $new_name, NETWORK_DFRN, $photo_url, "", "", "", "", "", $contact_updated, 2, $importer['id'], $importer['importer_uid']);
2821 // Currently unsupported - needs a lot of work
2822 $reloc = $feed->get_feed_tags( NAMESPACE_DFRN, 'relocate' );
2823 if(isset($reloc[0]['child'][NAMESPACE_DFRN])) {
2824 $base = $reloc[0]['child'][NAMESPACE_DFRN];
2826 $newloc['uid'] = $importer['importer_uid'];
2827 $newloc['cid'] = $importer['id'];
2828 $newloc['name'] = notags(unxmlify($base['name'][0]['data']));
2829 $newloc['photo'] = notags(unxmlify($base['photo'][0]['data']));
2830 $newloc['thumb'] = notags(unxmlify($base['thumb'][0]['data']));
2831 $newloc['micro'] = notags(unxmlify($base['micro'][0]['data']));
2832 $newloc['url'] = notags(unxmlify($base['url'][0]['data']));
2833 $newloc['request'] = notags(unxmlify($base['request'][0]['data']));
2834 $newloc['confirm'] = notags(unxmlify($base['confirm'][0]['data']));
2835 $newloc['notify'] = notags(unxmlify($base['notify'][0]['data']));
2836 $newloc['poll'] = notags(unxmlify($base['poll'][0]['data']));
2837 $newloc['sitepubkey'] = notags(unxmlify($base['sitepubkey'][0]['data']));
2838 /** relocated user must have original key pair */
2839 /*$newloc['pubkey'] = notags(unxmlify($base['pubkey'][0]['data']));
2840 $newloc['prvkey'] = notags(unxmlify($base['prvkey'][0]['data']));*/
2842 logger("items:relocate contact ".print_r($newloc, true).print_r($importer, true), LOGGER_DEBUG);
2845 $r = q("SELECT photo, url FROM contact WHERE id=%d AND uid=%d;",
2846 intval($importer['id']),
2847 intval($importer['importer_uid']));
2852 $x = q("UPDATE contact SET
2863 `site-pubkey` = '%s'
2864 WHERE id=%d AND uid=%d;",
2865 dbesc($newloc['name']),
2866 dbesc($newloc['photo']),
2867 dbesc($newloc['thumb']),
2868 dbesc($newloc['micro']),
2869 dbesc($newloc['url']),
2870 dbesc(normalise_link($newloc['url'])),
2871 dbesc($newloc['request']),
2872 dbesc($newloc['confirm']),
2873 dbesc($newloc['notify']),
2874 dbesc($newloc['poll']),
2875 dbesc($newloc['sitepubkey']),
2876 intval($importer['id']),
2877 intval($importer['importer_uid']));
2883 'owner-link' => array($old['url'], $newloc['url']),
2884 'author-link' => array($old['url'], $newloc['url']),
2885 'owner-avatar' => array($old['photo'], $newloc['photo']),
2886 'author-avatar' => array($old['photo'], $newloc['photo']),
2888 foreach ($fields as $n=>$f){
2889 $x = q("UPDATE `item` SET `%s`='%s' WHERE `%s`='%s' AND uid=%d",
2892 intval($importer['importer_uid']));
2898 /// merge with current record, current contents have priority
2899 /// update record, set url-updated
2900 /// update profile photos
2901 /// schedule a scan?
2906 // handle friend suggestion notification
2908 $sugg = $feed->get_feed_tags( NAMESPACE_DFRN, 'suggest' );
2909 if(isset($sugg[0]['child'][NAMESPACE_DFRN])) {
2910 $base = $sugg[0]['child'][NAMESPACE_DFRN];
2912 $fsugg['uid'] = $importer['importer_uid'];
2913 $fsugg['cid'] = $importer['id'];
2914 $fsugg['name'] = notags(unxmlify($base['name'][0]['data']));
2915 $fsugg['photo'] = notags(unxmlify($base['photo'][0]['data']));
2916 $fsugg['url'] = notags(unxmlify($base['url'][0]['data']));
2917 $fsugg['request'] = notags(unxmlify($base['request'][0]['data']));
2918 $fsugg['body'] = escape_tags(unxmlify($base['note'][0]['data']));
2920 // Does our member already have a friend matching this description?
2922 $r = q("SELECT * FROM `contact` WHERE `name` = '%s' AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
2923 dbesc($fsugg['name']),
2924 dbesc(normalise_link($fsugg['url'])),
2925 intval($fsugg['uid'])
2930 // Do we already have an fcontact record for this person?
2933 $r = q("SELECT * FROM `fcontact` WHERE `url` = '%s' AND `name` = '%s' AND `request` = '%s' LIMIT 1",
2934 dbesc($fsugg['url']),
2935 dbesc($fsugg['name']),
2936 dbesc($fsugg['request'])
2941 // OK, we do. Do we already have an introduction for this person ?
2942 $r = q("select id from intro where uid = %d and fid = %d limit 1",
2943 intval($fsugg['uid']),
2950 $r = q("INSERT INTO `fcontact` ( `name`,`url`,`photo`,`request` ) VALUES ( '%s', '%s', '%s', '%s' ) ",
2951 dbesc($fsugg['name']),
2952 dbesc($fsugg['url']),
2953 dbesc($fsugg['photo']),
2954 dbesc($fsugg['request'])
2956 $r = q("SELECT * FROM `fcontact` WHERE `url` = '%s' AND `name` = '%s' AND `request` = '%s' LIMIT 1",
2957 dbesc($fsugg['url']),
2958 dbesc($fsugg['name']),
2959 dbesc($fsugg['request'])
2964 // database record did not get created. Quietly give up.
2969 $hash = random_string();
2971 $r = q("INSERT INTO `intro` ( `uid`, `fid`, `contact-id`, `note`, `hash`, `datetime`, `blocked` )
2972 VALUES( %d, %d, %d, '%s', '%s', '%s', %d )",
2973 intval($fsugg['uid']),
2975 intval($fsugg['cid']),
2976 dbesc($fsugg['body']),
2978 dbesc(datetime_convert()),
2983 'type' => NOTIFY_SUGGEST,
2984 'notify_flags' => $importer['notify-flags'],
2985 'language' => $importer['language'],
2986 'to_name' => $importer['username'],
2987 'to_email' => $importer['email'],
2988 'uid' => $importer['importer_uid'],
2990 'link' => $a->get_baseurl() . '/notifications/intros',
2991 'source_name' => $importer['name'],
2992 'source_link' => $importer['url'],
2993 'source_photo' => $importer['photo'],
2994 'verb' => ACTIVITY_REQ_FRIEND,
3003 $rawmail = $feed->get_feed_tags( NAMESPACE_DFRN, 'mail' );
3004 if(isset($rawmail[0]['child'][NAMESPACE_DFRN])) {
3006 logger('local_delivery: private message received');
3009 $base = $rawmail[0]['child'][NAMESPACE_DFRN];
3012 $msg['uid'] = $importer['importer_uid'];
3013 $msg['from-name'] = notags(unxmlify($base['sender'][0]['child'][NAMESPACE_DFRN]['name'][0]['data']));
3014 $msg['from-photo'] = notags(unxmlify($base['sender'][0]['child'][NAMESPACE_DFRN]['avatar'][0]['data']));
3015 $msg['from-url'] = notags(unxmlify($base['sender'][0]['child'][NAMESPACE_DFRN]['uri'][0]['data']));
3016 $msg['contact-id'] = $importer['id'];
3017 $msg['title'] = notags(unxmlify($base['subject'][0]['data']));
3018 $msg['body'] = escape_tags(unxmlify($base['content'][0]['data']));
3020 $msg['replied'] = 0;
3021 $msg['uri'] = notags(unxmlify($base['id'][0]['data']));
3022 $msg['parent-uri'] = notags(unxmlify($base['in-reply-to'][0]['data']));
3023 $msg['created'] = datetime_convert(notags(unxmlify('UTC','UTC',$base['sentdate'][0]['data'])));
3027 $r = dbq("INSERT INTO `mail` (`" . implode("`, `", array_keys($msg))
3028 . "`) VALUES ('" . implode("', '", array_values($msg)) . "')" );
3030 // send notifications.
3032 require_once('include/enotify.php');
3034 $notif_params = array(
3035 'type' => NOTIFY_MAIL,
3036 'notify_flags' => $importer['notify-flags'],
3037 'language' => $importer['language'],
3038 'to_name' => $importer['username'],
3039 'to_email' => $importer['email'],
3040 'uid' => $importer['importer_uid'],
3042 'source_name' => $msg['from-name'],
3043 'source_link' => $importer['url'],
3044 'source_photo' => $importer['thumb'],
3045 'verb' => ACTIVITY_POST,
3049 notification($notif_params);
3055 $community_page = 0;
3056 $rawtags = $feed->get_feed_tags( NAMESPACE_DFRN, 'community');
3058 $community_page = intval($rawtags[0]['data']);
3060 if(intval($importer['forum']) != $community_page) {
3061 q("update contact set forum = %d where id = %d",
3062 intval($community_page),
3063 intval($importer['id'])
3065 $importer['forum'] = (string) $community_page;
3068 logger('local_delivery: feed item count = ' . $feed->get_item_quantity());
3070 // process any deleted entries
3072 $del_entries = $feed->get_feed_tags(NAMESPACE_TOMB, 'deleted-entry');
3073 if(is_array($del_entries) && count($del_entries)) {
3074 foreach($del_entries as $dentry) {
3076 if(isset($dentry['attribs']['']['ref'])) {
3077 $uri = $dentry['attribs']['']['ref'];
3079 if(isset($dentry['attribs']['']['when'])) {
3080 $when = $dentry['attribs']['']['when'];
3081 $when = datetime_convert('UTC','UTC', $when, 'Y-m-d H:i:s');
3084 $when = datetime_convert('UTC','UTC','now','Y-m-d H:i:s');
3088 // check for relayed deletes to our conversation
3091 $r = q("select * from item where uri = '%s' and uid = %d limit 1",
3093 intval($importer['importer_uid'])
3096 $parent_uri = $r[0]['parent-uri'];
3097 if($r[0]['id'] != $r[0]['parent'])
3104 if($importer['page-flags'] == PAGE_COMMUNITY || $importer['page-flags'] == PAGE_PRVGROUP ) {
3107 logger('local_delivery: possible community delete');
3110 $sql_extra = " and contact.self = 1 and item.wall = 1 ";
3112 // was the top-level post for this reply written by somebody on this site?
3113 // Specifically, the recipient?
3115 $is_a_remote_delete = false;
3117 // POSSIBLE CLEANUP --> Why select so many fields when only forum_mode and wall are used?
3118 $r = q("select `item`.`id`, `item`.`uri`, `item`.`tag`, `item`.`forum_mode`,`item`.`origin`,`item`.`wall`,
3119 `contact`.`name`, `contact`.`url`, `contact`.`thumb` from `item`
3120 INNER JOIN `contact` ON `contact`.`id` = `item`.`contact-id`
3121 WHERE `item`.`uri` = '%s' AND (`item`.`parent-uri` = '%s' or `item`.`thr-parent` = '%s')
3122 AND `item`.`uid` = %d
3128 intval($importer['importer_uid'])
3131 $is_a_remote_delete = true;
3133 // Does this have the characteristics of a community or private group comment?
3134 // If it's a reply to a wall post on a community/prvgroup page it's a
3135 // valid community comment. Also forum_mode makes it valid for sure.
3136 // If neither, it's not.
3138 if($is_a_remote_delete && $community) {
3139 if((! $r[0]['forum_mode']) && (! $r[0]['wall'])) {
3140 $is_a_remote_delete = false;
3141 logger('local_delivery: not a community delete');
3145 if($is_a_remote_delete) {
3146 logger('local_delivery: received remote delete');
3150 $r = q("SELECT `item`.*, `contact`.`self` FROM `item` INNER JOIN contact on `item`.`contact-id` = `contact`.`id`
3151 WHERE `uri` = '%s' AND `item`.`uid` = %d AND `contact-id` = %d AND NOT `item`.`file` LIKE '%%[%%' LIMIT 1",
3153 intval($importer['importer_uid']),
3154 intval($importer['id'])
3160 if($item['deleted'])
3163 logger('local_delivery: deleting item ' . $item['id'] . ' uri=' . $item['uri'], LOGGER_DEBUG);
3165 if($item['object-type'] === ACTIVITY_OBJ_EVENT) {
3166 logger("Deleting event ".$item['event-id'], LOGGER_DEBUG);
3167 event_delete($item['event-id']);
3170 if(($item['verb'] === ACTIVITY_TAG) && ($item['object-type'] === ACTIVITY_OBJ_TAGTERM)) {
3171 $xo = parse_xml_string($item['object'],false);
3172 $xt = parse_xml_string($item['target'],false);
3174 if($xt->type === ACTIVITY_OBJ_NOTE) {
3175 $i = q("select * from `item` where uri = '%s' and uid = %d limit 1",
3177 intval($importer['importer_uid'])
3181 // For tags, the owner cannot remove the tag on the author's copy of the post.
3183 $owner_remove = (($item['contact-id'] == $i[0]['contact-id']) ? true: false);
3184 $author_remove = (($item['origin'] && $item['self']) ? true : false);
3185 $author_copy = (($item['origin']) ? true : false);
3187 if($owner_remove && $author_copy)
3189 if($author_remove || $owner_remove) {
3190 $tags = explode(',',$i[0]['tag']);
3193 foreach($tags as $tag)
3194 if(trim($tag) !== trim($xo->body))
3195 $newtags[] = trim($tag);
3197 q("update item set tag = '%s' where id = %d",
3198 dbesc(implode(',',$newtags)),
3201 create_tags_from_item($i[0]['id']);
3207 if($item['uri'] == $item['parent-uri']) {
3208 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s',
3209 `body` = '', `title` = ''
3210 WHERE `parent-uri` = '%s' AND `uid` = %d",
3212 dbesc(datetime_convert()),
3213 dbesc($item['uri']),
3214 intval($importer['importer_uid'])
3216 create_tags_from_itemuri($item['uri'], $importer['importer_uid']);
3217 create_files_from_itemuri($item['uri'], $importer['importer_uid']);
3218 update_thread_uri($item['uri'], $importer['importer_uid']);
3221 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s',
3222 `body` = '', `title` = ''
3223 WHERE `uri` = '%s' AND `uid` = %d",
3225 dbesc(datetime_convert()),
3227 intval($importer['importer_uid'])
3229 create_tags_from_itemuri($uri, $importer['importer_uid']);
3230 create_files_from_itemuri($uri, $importer['importer_uid']);
3231 update_thread_uri($uri, $importer['importer_uid']);
3232 if($item['last-child']) {
3233 // ensure that last-child is set in case the comment that had it just got wiped.
3234 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
3235 dbesc(datetime_convert()),
3236 dbesc($item['parent-uri']),
3237 intval($item['uid'])
3239 // who is the last child now?
3240 $r = q("SELECT `id` FROM `item` WHERE `parent-uri` = '%s' AND `type` != 'activity' AND `deleted` = 0 AND `uid` = %d
3241 ORDER BY `created` DESC LIMIT 1",
3242 dbesc($item['parent-uri']),
3243 intval($importer['importer_uid'])
3246 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
3251 // if this is a relayed delete, propagate it to other recipients
3253 if($is_a_remote_delete)
3254 proc_run('php',"include/notifier.php","drop",$item['id']);
3262 foreach($feed->get_items() as $item) {
3265 $item_id = $item->get_id();
3266 $rawthread = $item->get_item_tags( NAMESPACE_THREAD, 'in-reply-to');
3267 if(isset($rawthread[0]['attribs']['']['ref'])) {
3269 $parent_uri = $rawthread[0]['attribs']['']['ref'];
3275 if($importer['page-flags'] == PAGE_COMMUNITY || $importer['page-flags'] == PAGE_PRVGROUP ) {
3278 logger('local_delivery: possible community reply');
3281 $sql_extra = " and contact.self = 1 and item.wall = 1 ";
3283 // was the top-level post for this reply written by somebody on this site?
3284 // Specifically, the recipient?
3286 $is_a_remote_comment = false;
3287 $top_uri = $parent_uri;
3289 $r = q("select `item`.`parent-uri` from `item`
3290 WHERE `item`.`uri` = '%s'
3294 if($r && count($r)) {
3295 $top_uri = $r[0]['parent-uri'];
3297 // POSSIBLE CLEANUP --> Why select so many fields when only forum_mode and wall are used?
3298 $r = q("select `item`.`id`, `item`.`uri`, `item`.`tag`, `item`.`forum_mode`,`item`.`origin`,`item`.`wall`,
3299 `contact`.`name`, `contact`.`url`, `contact`.`thumb` from `item`
3300 INNER JOIN `contact` ON `contact`.`id` = `item`.`contact-id`
3301 WHERE `item`.`uri` = '%s' AND (`item`.`parent-uri` = '%s' or `item`.`thr-parent` = '%s')
3302 AND `item`.`uid` = %d
3308 intval($importer['importer_uid'])
3311 $is_a_remote_comment = true;
3314 // Does this have the characteristics of a community or private group comment?
3315 // If it's a reply to a wall post on a community/prvgroup page it's a
3316 // valid community comment. Also forum_mode makes it valid for sure.
3317 // If neither, it's not.
3319 if($is_a_remote_comment && $community) {
3320 if((! $r[0]['forum_mode']) && (! $r[0]['wall'])) {
3321 $is_a_remote_comment = false;
3322 logger('local_delivery: not a community reply');
3326 if($is_a_remote_comment) {
3327 logger('local_delivery: received remote comment');
3329 // remote reply to our post. Import and then notify everybody else.
3331 $datarray = get_atom_elements($feed, $item);
3333 $r = q("SELECT `id`, `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
3335 intval($importer['importer_uid'])
3338 // Update content if 'updated' changes
3342 if (edited_timestamp_is_newer($r[0], $datarray)) {
3344 // do not accept (ignore) an earlier edit than one we currently have.
3345 if(datetime_convert('UTC','UTC',$datarray['edited']) < $r[0]['edited'])
3348 logger('received updated comment' , LOGGER_DEBUG);
3349 $r = q("UPDATE `item` SET `title` = '%s', `body` = '%s', `tag` = '%s', `edited` = '%s', `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d",
3350 dbesc($datarray['title']),
3351 dbesc($datarray['body']),
3352 dbesc($datarray['tag']),
3353 dbesc(datetime_convert('UTC','UTC',$datarray['edited'])),
3354 dbesc(datetime_convert()),
3356 intval($importer['importer_uid'])
3358 create_tags_from_itemuri($item_id, $importer['importer_uid']);
3360 proc_run('php',"include/notifier.php","comment-import",$iid);
3369 $own = q("select name,url,thumb from contact where uid = %d and self = 1 limit 1",
3370 intval($importer['importer_uid'])
3374 $datarray['type'] = 'remote-comment';
3375 $datarray['wall'] = 1;
3376 $datarray['parent-uri'] = $parent_uri;
3377 $datarray['uid'] = $importer['importer_uid'];
3378 $datarray['owner-name'] = $own[0]['name'];
3379 $datarray['owner-link'] = $own[0]['url'];
3380 $datarray['owner-avatar'] = $own[0]['thumb'];
3381 $datarray['contact-id'] = $importer['id'];
3383 if(($datarray['verb'] === ACTIVITY_LIKE)
3384 || ($datarray['verb'] === ACTIVITY_DISLIKE)
3385 || ($datarray['verb'] === ACTIVITY_ATTEND)
3386 || ($datarray['verb'] === ACTIVITY_ATTENDNO)
3387 || ($datarray['verb'] === ACTIVITY_ATTENDMAYBE)) {
3389 $datarray['type'] = 'activity';
3390 $datarray['gravity'] = GRAVITY_LIKE;
3391 $datarray['last-child'] = 0;
3392 // only one like or dislike per person
3393 // splitted into two queries for performance issues
3394 $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `author-link` = '%s' AND `verb` = '%s' AND `parent-uri` = '%s' AND NOT `deleted` LIMIT 1",
3395 intval($datarray['uid']),
3396 dbesc($datarray['author-link']),
3397 dbesc($datarray['verb']),
3398 dbesc($datarray['parent-uri'])
3403 $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `author-link` = '%s' AND `verb` = '%s' AND `thr-parent` = '%s' AND NOT `deleted` LIMIT 1",
3404 intval($datarray['uid']),
3405 dbesc($datarray['author-link']),
3406 dbesc($datarray['verb']),
3407 dbesc($datarray['parent-uri'])
3414 if(($datarray['verb'] === ACTIVITY_TAG) && ($datarray['object-type'] === ACTIVITY_OBJ_TAGTERM)) {
3416 $xo = parse_xml_string($datarray['object'],false);
3417 $xt = parse_xml_string($datarray['target'],false);
3419 if(($xt->type == ACTIVITY_OBJ_NOTE) && ($xt->id)) {
3421 // fetch the parent item
3423 $tagp = q("select * from item where uri = '%s' and uid = %d limit 1",
3425 intval($importer['importer_uid'])
3430 // extract tag, if not duplicate, and this user allows tags, add to parent item
3432 if($xo->id && $xo->content) {
3433 $newtag = '#[url=' . $xo->id . ']'. $xo->content . '[/url]';
3434 if(! (stristr($tagp[0]['tag'],$newtag))) {
3435 $i = q("SELECT `blocktags` FROM `user` where `uid` = %d LIMIT 1",
3436 intval($importer['importer_uid'])
3438 if(count($i) && ! intval($i[0]['blocktags'])) {
3439 q("UPDATE item SET tag = '%s', `edited` = '%s', `changed` = '%s' WHERE id = %d",
3440 dbesc($tagp[0]['tag'] . (strlen($tagp[0]['tag']) ? ',' : '') . $newtag),
3441 intval($tagp[0]['id']),
3442 dbesc(datetime_convert()),
3443 dbesc(datetime_convert())
3445 create_tags_from_item($tagp[0]['id']);
3453 $posted_id = item_store($datarray);
3458 $datarray["id"] = $posted_id;
3460 $r = q("SELECT `parent`, `parent-uri` FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1",
3462 intval($importer['importer_uid'])
3465 $parent = $r[0]['parent'];
3466 $parent_uri = $r[0]['parent-uri'];
3470 $r1 = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `uid` = %d AND `parent` = %d",
3471 dbesc(datetime_convert()),
3472 intval($importer['importer_uid']),
3473 intval($r[0]['parent'])
3476 $r2 = q("UPDATE `item` SET `last-child` = 1, `changed` = '%s' WHERE `uid` = %d AND `id` = %d",
3477 dbesc(datetime_convert()),
3478 intval($importer['importer_uid']),
3483 if($posted_id && $parent) {
3485 proc_run('php',"include/notifier.php","comment-import","$posted_id");
3487 if((! $is_like) && (! $importer['self'])) {
3489 require_once('include/enotify.php');
3492 'type' => NOTIFY_COMMENT,
3493 'notify_flags' => $importer['notify-flags'],
3494 'language' => $importer['language'],
3495 'to_name' => $importer['username'],
3496 'to_email' => $importer['email'],
3497 'uid' => $importer['importer_uid'],
3498 'item' => $datarray,
3499 'link' => $a->get_baseurl().'/display/'.urlencode(get_item_guid($posted_id)),
3500 'source_name' => stripslashes($datarray['author-name']),
3501 'source_link' => $datarray['author-link'],
3502 'source_photo' => ((link_compare($datarray['author-link'],$importer['url']))
3503 ? $importer['thumb'] : $datarray['author-avatar']),
3504 'verb' => ACTIVITY_POST,
3506 'parent' => $parent,
3507 'parent_uri' => $parent_uri,
3519 // regular comment that is part of this total conversation. Have we seen it? If not, import it.
3521 $item_id = $item->get_id();
3522 $datarray = get_atom_elements($feed,$item);
3524 if($importer['rel'] == CONTACT_IS_FOLLOWER)
3527 $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
3529 intval($importer['importer_uid'])
3532 // Update content if 'updated' changes
3535 if (edited_timestamp_is_newer($r[0], $datarray)) {
3537 // do not accept (ignore) an earlier edit than one we currently have.
3538 if(datetime_convert('UTC','UTC',$datarray['edited']) < $r[0]['edited'])
3541 $r = q("UPDATE `item` SET `title` = '%s', `body` = '%s', `tag` = '%s', `edited` = '%s', `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d",
3542 dbesc($datarray['title']),
3543 dbesc($datarray['body']),
3544 dbesc($datarray['tag']),
3545 dbesc(datetime_convert('UTC','UTC',$datarray['edited'])),
3546 dbesc(datetime_convert()),
3548 intval($importer['importer_uid'])
3550 create_tags_from_itemuri($item_id, $importer['importer_uid']);
3553 // update last-child if it changes
3555 $allow = $item->get_item_tags( NAMESPACE_DFRN, 'comment-allow');
3556 if(($allow) && ($allow[0]['data'] != $r[0]['last-child'])) {
3557 $r = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d",
3558 dbesc(datetime_convert()),
3560 intval($importer['importer_uid'])
3562 $r = q("UPDATE `item` SET `last-child` = %d , `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d",
3563 intval($allow[0]['data']),
3564 dbesc(datetime_convert()),
3566 intval($importer['importer_uid'])
3572 $datarray['parent-uri'] = $parent_uri;
3573 $datarray['uid'] = $importer['importer_uid'];
3574 $datarray['contact-id'] = $importer['id'];
3575 if(($datarray['verb'] === ACTIVITY_LIKE)
3576 || ($datarray['verb'] === ACTIVITY_DISLIKE)
3577 || ($datarray['verb'] === ACTIVITY_ATTEND)
3578 || ($datarray['verb'] === ACTIVITY_ATTENDNO)
3579 || ($datarray['verb'] === ACTIVITY_ATTENDMAYBE)) {
3580 $datarray['type'] = 'activity';
3581 $datarray['gravity'] = GRAVITY_LIKE;
3582 // only one like or dislike per person
3583 // splitted into two queries for performance issues
3584 $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `author-link` = '%s' AND `verb` = '%s' AND `parent-uri` = '%s' AND NOT `deleted` LIMIT 1",
3585 intval($datarray['uid']),
3586 dbesc($datarray['author-link']),
3587 dbesc($datarray['verb']),
3588 dbesc($datarray['parent-uri'])
3593 $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `author-link` = '%s' AND `verb` = '%s' AND `thr-parent` = '%s' AND NOT `deleted` LIMIT 1",
3594 intval($datarray['uid']),
3595 dbesc($datarray['author-link']),
3596 dbesc($datarray['verb']),
3597 dbesc($datarray['parent-uri'])
3604 if(($datarray['verb'] === ACTIVITY_TAG) && ($datarray['object-type'] === ACTIVITY_OBJ_TAGTERM)) {
3606 $xo = parse_xml_string($datarray['object'],false);
3607 $xt = parse_xml_string($datarray['target'],false);
3609 if($xt->type == ACTIVITY_OBJ_NOTE) {
3610 $r = q("select * from item where `uri` = '%s' AND `uid` = %d limit 1",
3612 intval($importer['importer_uid'])
3617 // extract tag, if not duplicate, add to parent item
3619 if(! (stristr($r[0]['tag'],trim($xo->content)))) {
3620 q("UPDATE item SET tag = '%s' WHERE id = %d",
3621 dbesc($r[0]['tag'] . (strlen($r[0]['tag']) ? ',' : '') . '#[url=' . $xo->id . ']'. $xo->content . '[/url]'),
3624 create_tags_from_item($r[0]['id']);
3630 $posted_id = item_store($datarray);
3632 // find out if our user is involved in this conversation and wants to be notified.
3634 if(!x($datarray['type']) || $datarray['type'] != 'activity') {
3636 $myconv = q("SELECT `author-link`, `author-avatar`, `parent` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 AND `deleted` = 0",
3638 intval($importer['importer_uid'])
3641 if(count($myconv)) {
3642 $importer_url = $a->get_baseurl() . '/profile/' . $importer['nickname'];
3644 // first make sure this isn't our own post coming back to us from a wall-to-wall event
3645 if(! link_compare($datarray['author-link'],$importer_url)) {
3648 foreach($myconv as $conv) {
3650 // now if we find a match, it means we're in this conversation
3652 if(! link_compare($conv['author-link'],$importer_url))
3655 require_once('include/enotify.php');
3657 $conv_parent = $conv['parent'];
3660 'type' => NOTIFY_COMMENT,
3661 'notify_flags' => $importer['notify-flags'],
3662 'language' => $importer['language'],
3663 'to_name' => $importer['username'],
3664 'to_email' => $importer['email'],
3665 'uid' => $importer['importer_uid'],
3666 'item' => $datarray,
3667 'link' => $a->get_baseurl().'/display/'.urlencode(get_item_guid($posted_id)),
3668 'source_name' => stripslashes($datarray['author-name']),
3669 'source_link' => $datarray['author-link'],
3670 'source_photo' => ((link_compare($datarray['author-link'],$importer['url']))
3671 ? $importer['thumb'] : $datarray['author-avatar']),
3672 'verb' => ACTIVITY_POST,
3674 'parent' => $conv_parent,
3675 'parent_uri' => $parent_uri
3679 // only send one notification
3691 // Head post of a conversation. Have we seen it? If not, import it.
3694 $item_id = $item->get_id();
3695 $datarray = get_atom_elements($feed,$item);
3697 if((x($datarray,'object-type')) && ($datarray['object-type'] === ACTIVITY_OBJ_EVENT)) {
3698 $ev = bbtoevent($datarray['body']);
3699 if((x($ev,'desc') || x($ev,'summary')) && x($ev,'start')) {
3700 $ev['cid'] = $importer['id'];
3701 $ev['uid'] = $importer['uid'];
3702 $ev['uri'] = $item_id;
3703 $ev['edited'] = $datarray['edited'];
3704 $ev['private'] = $datarray['private'];
3705 $ev['guid'] = $datarray['guid'];
3707 $r = q("SELECT * FROM `event` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
3709 intval($importer['uid'])
3712 $ev['id'] = $r[0]['id'];
3713 $xyz = event_store($ev);
3718 $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
3720 intval($importer['importer_uid'])
3723 // Update content if 'updated' changes
3726 if (edited_timestamp_is_newer($r[0], $datarray)) {
3728 // do not accept (ignore) an earlier edit than one we currently have.
3729 if(datetime_convert('UTC','UTC',$datarray['edited']) < $r[0]['edited'])
3732 $r = q("UPDATE `item` SET `title` = '%s', `body` = '%s', `tag` = '%s', `edited` = '%s', `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d",
3733 dbesc($datarray['title']),
3734 dbesc($datarray['body']),
3735 dbesc($datarray['tag']),
3736 dbesc(datetime_convert('UTC','UTC',$datarray['edited'])),
3737 dbesc(datetime_convert()),
3739 intval($importer['importer_uid'])
3741 create_tags_from_itemuri($item_id, $importer['importer_uid']);
3742 update_thread_uri($item_id, $importer['importer_uid']);
3745 // update last-child if it changes
3747 $allow = $item->get_item_tags( NAMESPACE_DFRN, 'comment-allow');
3748 if($allow && $allow[0]['data'] != $r[0]['last-child']) {
3749 $r = q("UPDATE `item` SET `last-child` = %d , `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d",
3750 intval($allow[0]['data']),
3751 dbesc(datetime_convert()),
3753 intval($importer['importer_uid'])
3759 $datarray['parent-uri'] = $item_id;
3760 $datarray['uid'] = $importer['importer_uid'];
3761 $datarray['contact-id'] = $importer['id'];
3764 if(! link_compare($datarray['owner-link'],$importer['url'])) {
3765 // The item owner info is not our contact. It's OK and is to be expected if this is a tgroup delivery,
3766 // but otherwise there's a possible data mixup on the sender's system.
3767 // the tgroup delivery code called from item_store will correct it if it's a forum,
3768 // but we're going to unconditionally correct it here so that the post will always be owned by our contact.
3769 logger('local_delivery: Correcting item owner.', LOGGER_DEBUG);
3770 $datarray['owner-name'] = $importer['senderName'];
3771 $datarray['owner-link'] = $importer['url'];
3772 $datarray['owner-avatar'] = $importer['thumb'];
3775 if(($importer['rel'] == CONTACT_IS_FOLLOWER) && (! tgroup_check($importer['importer_uid'],$datarray)))
3778 // This is my contact on another system, but it's really me.
3779 // Turn this into a wall post.
3780 $notify = item_is_remote_self($importer, $datarray);
3782 $posted_id = item_store($datarray, false, $notify);
3784 if(stristr($datarray['verb'],ACTIVITY_POKE)) {
3785 $verb = urldecode(substr($datarray['verb'],strpos($datarray['verb'],'#')+1));
3788 $xo = parse_xml_string($datarray['object'],false);
3790 if(($xo->type == ACTIVITY_OBJ_PERSON) && ($xo->id)) {
3792 // somebody was poked/prodded. Was it me?
3794 $links = parse_xml_string("<links>".unxmlify($xo->link)."</links>",false);
3796 foreach($links->link as $l) {
3797 $atts = $l->attributes();
3798 switch($atts['rel']) {
3800 $Blink = $atts['href'];
3806 if($Blink && link_compare($Blink,$a->get_baseurl() . '/profile/' . $importer['nickname'])) {
3808 // send a notification
3809 require_once('include/enotify.php');
3812 'type' => NOTIFY_POKE,
3813 'notify_flags' => $importer['notify-flags'],
3814 'language' => $importer['language'],
3815 'to_name' => $importer['username'],
3816 'to_email' => $importer['email'],
3817 'uid' => $importer['importer_uid'],
3818 'item' => $datarray,
3819 'link' => $a->get_baseurl().'/display/'.urlencode(get_item_guid($posted_id)),
3820 'source_name' => stripslashes($datarray['author-name']),
3821 'source_link' => $datarray['author-link'],
3822 'source_photo' => ((link_compare($datarray['author-link'],$importer['url']))
3823 ? $importer['thumb'] : $datarray['author-avatar']),
3824 'verb' => $datarray['verb'],
3825 'otype' => 'person',
3826 'activity' => $verb,
3827 'parent' => $datarray['parent']
3843 function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
3844 $url = notags(trim($datarray['author-link']));
3845 $name = notags(trim($datarray['author-name']));
3846 $photo = notags(trim($datarray['author-avatar']));
3848 if (is_object($item)) {
3849 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
3850 if($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
3851 $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
3855 if(is_array($contact)) {
3856 if(($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
3857 || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
3858 $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
3859 intval(CONTACT_IS_FRIEND),
3860 intval($contact['id']),
3861 intval($importer['uid'])
3864 // send email notification to owner?
3867 // create contact record
3869 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
3870 `blocked`, `readonly`, `pending`, `writable`)
3871 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
3872 intval($importer['uid']),
3873 dbesc(datetime_convert()),
3875 dbesc(normalise_link($url)),
3879 dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
3880 intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
3882 $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
3883 intval($importer['uid']),
3887 $contact_record = $r[0];
3889 $photos = import_profile_photo($photo,$importer["uid"],$contact_record["id"]);
3891 q("UPDATE `contact` SET `photo` = '%s', `thumb` = '%s', `micro` = '%s' WHERE `id` = %d",
3895 intval($contact_record["id"])
3900 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
3901 intval($importer['uid'])
3904 if(count($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
3906 // create notification
3907 $hash = random_string();
3909 if(is_array($contact_record)) {
3910 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
3911 VALUES ( %d, %d, 0, 0, '%s', '%s' )",
3912 intval($importer['uid']),
3913 intval($contact_record['id']),
3915 dbesc(datetime_convert())
3919 if(intval($r[0]['def_gid'])) {
3920 require_once('include/group.php');
3921 group_add_member($r[0]['uid'],'',$contact_record['id'],$r[0]['def_gid']);
3924 if(($r[0]['notify-flags'] & NOTIFY_INTRO) &&
3925 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
3928 'type' => NOTIFY_INTRO,
3929 'notify_flags' => $r[0]['notify-flags'],
3930 'language' => $r[0]['language'],
3931 'to_name' => $r[0]['username'],
3932 'to_email' => $r[0]['email'],
3933 'uid' => $r[0]['uid'],
3934 'link' => $a->get_baseurl() . '/notifications/intro',
3935 'source_name' => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
3936 'source_link' => $contact_record['url'],
3937 'source_photo' => $contact_record['photo'],
3938 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
3943 } elseif (count($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
3944 $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
3945 intval($importer['uid']),
3953 function lose_follower($importer,$contact,$datarray,$item) {
3955 if(($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
3956 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
3957 intval(CONTACT_IS_SHARING),
3958 intval($contact['id'])
3962 contact_remove($contact['id']);
3966 function lose_sharer($importer,$contact,$datarray,$item) {
3968 if(($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
3969 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
3970 intval(CONTACT_IS_FOLLOWER),
3971 intval($contact['id'])
3975 contact_remove($contact['id']);
3979 function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
3983 if(is_array($importer)) {
3984 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
3985 intval($importer['uid'])
3989 // Diaspora has different message-ids in feeds than they do
3990 // through the direct Diaspora protocol. If we try and use
3991 // the feed, we'll get duplicates. So don't.
3993 if((! count($r)) || $contact['network'] === NETWORK_DIASPORA)
3996 $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
3998 // Use a single verify token, even if multiple hubs
4000 $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
4002 $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
4004 logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: ' . $push_url . ' with verifier ' . $verify_token);
4006 if(!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
4007 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
4008 dbesc($verify_token),
4009 intval($contact['id'])
4013 post_url($url,$params);
4015 logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
4021 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
4023 if(get_config('system','disable_embedded'))
4028 logger('fix_private_photos: check for photos', LOGGER_DEBUG);
4029 $site = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://'));
4034 $img_start = strpos($orig_body, '[img');
4035 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
4036 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
4037 while( ($img_st_close !== false) && ($img_len !== false) ) {
4039 $img_st_close++; // make it point to AFTER the closing bracket
4040 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
4042 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
4045 if(stristr($image , $site . '/photo/')) {
4046 // Only embed locally hosted photos
4048 $i = basename($image);
4049 $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
4050 $x = strpos($i,'-');
4053 $res = substr($i,$x+1);
4054 $i = substr($i,0,$x);
4055 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
4062 // Check to see if we should replace this photo link with an embedded image
4063 // 1. No need to do so if the photo is public
4064 // 2. If there's a contact-id provided, see if they're in the access list
4065 // for the photo. If so, embed it.
4066 // 3. Otherwise, if we have an item, see if the item permissions match the photo
4067 // permissions, regardless of order but first check to see if they're an exact
4068 // match to save some processing overhead.
4070 if(has_permissions($r[0])) {
4072 $recips = enumerate_permissions($r[0]);
4073 if(in_array($cid, $recips)) {
4078 if(compare_permissions($item,$r[0]))
4083 $data = $r[0]['data'];
4084 $type = $r[0]['type'];
4086 // If a custom width and height were specified, apply before embedding
4087 if(preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
4088 logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
4090 $width = intval($match[1]);
4091 $height = intval($match[2]);
4093 $ph = new Photo($data, $type);
4094 if($ph->is_valid()) {
4095 $ph->scaleImage(max($width, $height));
4096 $data = $ph->imageString();
4097 $type = $ph->getType();
4101 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
4102 $image = 'data:' . $type . ';base64,' . base64_encode($data);
4103 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
4109 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
4110 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
4111 if($orig_body === false)
4114 $img_start = strpos($orig_body, '[img');
4115 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
4116 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
4119 $new_body = $new_body . $orig_body;
4124 function has_permissions($obj) {
4125 if(($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
4130 function compare_permissions($obj1,$obj2) {
4131 // first part is easy. Check that these are exactly the same.
4132 if(($obj1['allow_cid'] == $obj2['allow_cid'])
4133 && ($obj1['allow_gid'] == $obj2['allow_gid'])
4134 && ($obj1['deny_cid'] == $obj2['deny_cid'])
4135 && ($obj1['deny_gid'] == $obj2['deny_gid']))
4138 // This is harder. Parse all the permissions and compare the resulting set.
4140 $recipients1 = enumerate_permissions($obj1);
4141 $recipients2 = enumerate_permissions($obj2);
4144 if($recipients1 == $recipients2)
4149 // returns an array of contact-ids that are allowed to see this object
4151 function enumerate_permissions($obj) {
4152 require_once('include/group.php');
4153 $allow_people = expand_acl($obj['allow_cid']);
4154 $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
4155 $deny_people = expand_acl($obj['deny_cid']);
4156 $deny_groups = expand_groups(expand_acl($obj['deny_gid']));
4157 $recipients = array_unique(array_merge($allow_people,$allow_groups));
4158 $deny = array_unique(array_merge($deny_people,$deny_groups));
4159 $recipients = array_diff($recipients,$deny);
4163 function item_getfeedtags($item) {
4166 $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
4168 for($x = 0; $x < $cnt; $x ++) {
4170 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
4174 $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
4176 for($x = 0; $x < $cnt; $x ++) {
4178 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
4184 function item_expire($uid, $days, $network = "", $force = false) {
4186 if((! $uid) || ($days < 1))
4189 // $expire_network_only = save your own wall posts
4190 // and just expire conversations started by others
4192 $expire_network_only = get_pconfig($uid,'expire','network_only');
4193 $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
4195 if ($network != "") {
4196 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
4197 // There is an index "uid_network_received" but not "uid_network_created"
4198 // This avoids the creation of another index just for one purpose.
4199 // And it doesn't really matter wether to look at "received" or "created"
4200 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
4202 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
4204 $r = q("SELECT * FROM `item`
4205 WHERE `uid` = %d $range
4216 $expire_items = get_pconfig($uid, 'expire','items');
4217 $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
4219 // Forcing expiring of items - but not notes and marked items
4221 $expire_items = true;
4223 $expire_notes = get_pconfig($uid, 'expire','notes');
4224 $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
4226 $expire_starred = get_pconfig($uid, 'expire','starred');
4227 $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
4229 $expire_photos = get_pconfig($uid, 'expire','photos');
4230 $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
4232 logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
4234 foreach($r as $item) {
4236 // don't expire filed items
4238 if(strpos($item['file'],'[') !== false)
4241 // Only expire posts, not photos and photo comments
4243 if($expire_photos==0 && strlen($item['resource-id']))
4245 if($expire_starred==0 && intval($item['starred']))
4247 if($expire_notes==0 && $item['type']=='note')
4249 if($expire_items==0 && $item['type']!='note')
4252 drop_item($item['id'],false);
4255 proc_run('php',"include/notifier.php","expire","$uid");
4260 function drop_items($items) {
4263 if(! local_user() && ! remote_user())
4267 foreach($items as $item) {
4268 $owner = drop_item($item,false);
4269 if($owner && ! $uid)
4274 // multiple threads may have been deleted, send an expire notification
4277 proc_run('php',"include/notifier.php","expire","$uid");
4281 function drop_item($id,$interactive = true) {
4285 // locate item to be deleted
4287 $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
4294 notice( t('Item not found.') . EOL);
4295 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
4300 $owner = $item['uid'];
4304 // check if logged in user is either the author or owner of this item
4306 if(is_array($_SESSION['remote'])) {
4307 foreach($_SESSION['remote'] as $visitor) {
4308 if($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
4309 $cid = $visitor['cid'];
4316 if((local_user() == $item['uid']) || ($cid) || (! $interactive)) {
4318 // Check if we should do HTML-based delete confirmation
4319 if($_REQUEST['confirm']) {
4320 // <form> can't take arguments in its "action" parameter
4321 // so add any arguments as hidden inputs
4322 $query = explode_querystring($a->query_string);
4324 foreach($query['args'] as $arg) {
4325 if(strpos($arg, 'confirm=') === false) {
4326 $arg_parts = explode('=', $arg);
4327 $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
4331 return replace_macros(get_markup_template('confirm.tpl'), array(
4333 '$message' => t('Do you really want to delete this item?'),
4334 '$extra_inputs' => $inputs,
4335 '$confirm' => t('Yes'),
4336 '$confirm_url' => $query['base'],
4337 '$confirm_name' => 'confirmed',
4338 '$cancel' => t('Cancel'),
4341 // Now check how the user responded to the confirmation query
4342 if($_REQUEST['canceled']) {
4343 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
4346 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
4349 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
4350 dbesc(datetime_convert()),
4351 dbesc(datetime_convert()),
4354 create_tags_from_item($item['id']);
4355 create_files_from_item($item['id']);
4356 delete_thread($item['id'], $item['parent-uri']);
4358 // clean up categories and tags so they don't end up as orphans
4361 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
4363 foreach($matches as $mtch) {
4364 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
4370 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
4372 foreach($matches as $mtch) {
4373 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
4377 // If item is a link to a photo resource, nuke all the associated photos
4378 // (visitors will not have photo resources)
4379 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
4380 // generate a resource-id and therefore aren't intimately linked to the item.
4382 if(strlen($item['resource-id'])) {
4383 q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
4384 dbesc($item['resource-id']),
4385 intval($item['uid'])
4387 // ignore the result
4390 // If item is a link to an event, nuke the event record.
4392 if(intval($item['event-id'])) {
4393 q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
4394 intval($item['event-id']),
4395 intval($item['uid'])
4397 // ignore the result
4400 // If item has attachments, drop them
4402 foreach(explode(",",$item['attach']) as $attach){
4403 preg_match("|attach/(\d+)|", $attach, $matches);
4404 q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
4405 intval($matches[1]),
4408 // ignore the result
4412 // clean up item_id and sign meta-data tables
4415 // Old code - caused very long queries and warning entries in the mysql logfiles:
4417 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
4418 intval($item['id']),
4419 intval($item['uid'])
4422 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
4423 intval($item['id']),
4424 intval($item['uid'])
4428 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
4430 // Creating list of parents
4431 $r = q("select id from item where parent = %d and uid = %d",
4432 intval($item['id']),
4433 intval($item['uid'])
4438 foreach ($r AS $row) {
4439 if ($parentid != "")
4442 $parentid .= $row["id"];
4446 if ($parentid != "") {
4447 $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
4449 $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
4452 // If it's the parent of a comment thread, kill all the kids
4454 if($item['uri'] == $item['parent-uri']) {
4455 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
4456 WHERE `parent-uri` = '%s' AND `uid` = %d ",
4457 dbesc(datetime_convert()),
4458 dbesc(datetime_convert()),
4459 dbesc($item['parent-uri']),
4460 intval($item['uid'])
4462 create_tags_from_itemuri($item['parent-uri'], $item['uid']);
4463 create_files_from_itemuri($item['parent-uri'], $item['uid']);
4464 delete_thread_uri($item['parent-uri'], $item['uid']);
4465 // ignore the result
4468 // ensure that last-child is set in case the comment that had it just got wiped.
4469 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
4470 dbesc(datetime_convert()),
4471 dbesc($item['parent-uri']),
4472 intval($item['uid'])
4474 // who is the last child now?
4475 $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",
4476 dbesc($item['parent-uri']),
4477 intval($item['uid'])
4480 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
4485 // Add a relayable_retraction signature for Diaspora.
4486 store_diaspora_retract_sig($item, $a->user, $a->get_baseurl());
4489 $drop_id = intval($item['id']);
4491 // send the notification upstream/downstream as the case may be
4493 proc_run('php',"include/notifier.php","drop","$drop_id");
4497 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
4503 notice( t('Permission denied.') . EOL);
4504 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
4511 function first_post_date($uid,$wall = false) {
4512 $r = q("select id, created from item
4513 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
4515 order by created asc limit 1",
4517 intval($wall ? 1 : 0)
4520 // logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
4521 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
4526 /* modified posted_dates() {below} to arrange the list in years */
4527 function list_post_dates($uid, $wall) {
4528 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
4530 $dthen = first_post_date($uid, $wall);
4534 // Set the start and end date to the beginning of the month
4535 $dnow = substr($dnow,0,8).'01';
4536 $dthen = substr($dthen,0,8).'01';
4540 // Starting with the current month, get the first and last days of every
4541 // month down to and including the month of the first post
4542 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
4543 $dyear = intval(substr($dnow,0,4));
4544 $dstart = substr($dnow,0,8) . '01';
4545 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
4546 $start_month = datetime_convert('','',$dstart,'Y-m-d');
4547 $end_month = datetime_convert('','',$dend,'Y-m-d');
4548 $str = day_translate(datetime_convert('','',$dnow,'F'));
4550 $ret[$dyear] = array();
4551 $ret[$dyear][] = array($str,$end_month,$start_month);
4552 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
4557 function posted_dates($uid,$wall) {
4558 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
4560 $dthen = first_post_date($uid,$wall);
4564 // Set the start and end date to the beginning of the month
4565 $dnow = substr($dnow,0,8).'01';
4566 $dthen = substr($dthen,0,8).'01';
4569 // Starting with the current month, get the first and last days of every
4570 // month down to and including the month of the first post
4571 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
4572 $dstart = substr($dnow,0,8) . '01';
4573 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
4574 $start_month = datetime_convert('','',$dstart,'Y-m-d');
4575 $end_month = datetime_convert('','',$dend,'Y-m-d');
4576 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
4577 $ret[] = array($str,$end_month,$start_month);
4578 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
4584 function posted_date_widget($url,$uid,$wall) {
4587 if(! feature_enabled($uid,'archives'))
4590 // For former Facebook folks that left because of "timeline"
4592 /* if($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
4595 $visible_years = get_pconfig($uid,'system','archive_visible_years');
4596 if(! $visible_years)
4599 $ret = list_post_dates($uid,$wall);
4604 $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
4605 $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
4607 $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
4608 '$title' => t('Archives'),
4609 '$size' => $visible_years,
4610 '$cutoff_year' => $cutoff_year,
4611 '$cutoff' => $cutoff,
4614 '$showmore' => t('show more')
4620 function store_diaspora_retract_sig($item, $user, $baseurl) {
4621 // Note that we can't add a target_author_signature
4622 // if the comment was deleted by a remote user. That should be ok, because if a remote user is deleting
4623 // the comment, that means we're the home of the post, and Diaspora will only
4624 // check the parent_author_signature of retractions that it doesn't have to relay further
4626 // I don't think this function gets called for an "unlike," but I'll check anyway
4628 $enabled = intval(get_config('system','diaspora_enabled'));
4630 logger('drop_item: diaspora support disabled, not storing retraction signature', LOGGER_DEBUG);
4634 logger('drop_item: storing diaspora retraction signature');
4636 $signed_text = $item['guid'] . ';' . ( ($item['verb'] === ACTIVITY_LIKE) ? 'Like' : 'Comment');
4638 if(local_user() == $item['uid']) {
4640 $handle = $user['nickname'] . '@' . substr($baseurl, strpos($baseurl,'://') + 3);
4641 $authorsig = base64_encode(rsa_sign($signed_text,$user['prvkey'],'sha256'));
4644 $r = q("SELECT `nick`, `url` FROM `contact` WHERE `id` = '%d' LIMIT 1",
4645 $item['contact-id'] // If this function gets called, drop_item() has already checked remote_user() == $item['contact-id']
4648 // The below handle only works for NETWORK_DFRN. I think that's ok, because this function
4649 // only handles DFRN deletes
4650 $handle_baseurl_start = strpos($r['url'],'://') + 3;
4651 $handle_baseurl_length = strpos($r['url'],'/profile') - $handle_baseurl_start;
4652 $handle = $r['nick'] . '@' . substr($r['url'], $handle_baseurl_start, $handle_baseurl_length);
4658 q("insert into sign (`retract_iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
4659 intval($item['id']),
4660 dbesc($signed_text),