xmlify(Zotlabs\Lib\System::get_project_version()), '$generator' => xmlify(Zotlabs\Lib\System::get_platform_name()), '$generator_uri' => 'https://hubzilla.org', '$feed_id' => xmlify($channel['xchan_url']), '$feed_title' => xmlify($channel['channel_name']), '$feed_updated' => xmlify(datetime_convert('UTC', 'UTC', 'now', ATOM_TIME)), '$author' => $feed_author, '$owner' => $owner, '$profile_page' => xmlify($channel['xchan_url']), )); $x = [ 'xml' => $atom, 'channel' => $channel, 'observer_hash' => $observer_hash, 'params' => $params ]; /** * @hooks atom_feed_top * * \e string \b xml - the generated feed and what will get returned from the hook * * \e array \b channel * * \e string \b observer_hash * * \e array \b params */ call_hooks('atom_feed_top', $x); $atom = $x['xml']; /** * @hooks atom_feed * A much simpler interface than atom_feed_top. * * \e string - the feed after atom_feed_top hook */ call_hooks('atom_feed', $atom); $items = items_fetch( [ 'wall' => '1', 'datequery' => $params['end'], 'datequery2' => $params['begin'], 'start' => intval($params['start']), 'records' => intval($params['records']), 'direction' => dbesc($params['direction']), 'pages' => $params['pages'], 'order' => dbesc('post'), 'top' => $params['top'], 'cat' => $params['cat'], 'compat' => $params['compat'] ], $channel, $observer_hash, CLIENT_MODE_NORMAL, App::$module ); if($items) { $type = 'html'; foreach($items as $item) { if($item['item_private']) continue; $atom .= atom_entry($item, $type, null, $channel, true, '', $params['compat']); } } /** * @hooks atom_feed_end * \e string - The created XML feed as a string without closing tag */ call_hooks('atom_feed_end', $atom); $atom .= '' . "\r\n"; return $atom; } /** * @brief Return the verb for an item, or fall back to ACTIVITY_POST. * * @param array $item an associative array with * * \e string \b verb * @return string item's verb if set, default ACTIVITY_POST see boot.php */ function construct_verb($item) { if ($item['verb']) return $item['verb']; return 'Create'; } function construct_activity_object($item) { if($item['obj']) { $o = '' . "\r\n"; $r = json_decode($item['obj'],false); if(! $r) return EMPTY_STR; if(isset($r->type)) $o .= '' . xmlify($r->type) . '' . "\r\n"; if(isset($r->id)) $o .= '' . xmlify($r->id) . '' . "\r\n"; if(isset($r->title)) $o .= '' . xmlify($r->title) . '' . "\r\n"; if(isset($r->links)) { /** @FIXME!! */ if(substr($r->link,0,1) === '<') { $r->link = preg_replace('/\/','',$r->link); $o .= $r->link; } else $o .= '' . "\r\n"; } if(isset($r->content)) { $o .= '' . xmlify(bbcode($r->content)) . '' . "\r\n"; } $o .= '' . "\r\n"; return $o; } return EMPTY_STR; } function construct_activity_target($item) { if($item['target']) { $o = '' . "\r\n"; $r = json_decode($item['target'],false); if(! $r) return ''; if($r->type) $o .= '' . xmlify($r->type) . '' . "\r\n"; if($r->id) $o .= '' . xmlify($r->id) . '' . "\r\n"; if($r->title) $o .= '' . xmlify($r->title) . '' . "\r\n"; if($r->links) { /** @FIXME !!! */ if(substr($r->link,0,1) === '<') { if(strstr($r->link,'&') && (! strstr($r->link,'&'))) $r->link = str_replace('&','&', $r->link); $r->link = preg_replace('/\/','',$r->link); $o .= $r->link; } else $o .= '' . "\r\n"; } if($r->content) $o .= '' . xmlify(bbcode($r->content)) . '' . "\r\n"; $o .= '' . "\r\n"; return $o; } return ''; } /** * @brief Return an array with a parsed atom author. * * @param SimplePie $feed * @param SimplePie $item * @return array $author */ function get_atom_author($feed, $item) { $author = []; $found_author = $item->get_author(); if($found_author) { $rawauthor = $feed->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'author'); if(isset($rawauthor[0]['child'][NAMESPACE_POCO]['displayName'][0]['data'])) { $author['full_name'] = unxmlify($rawauthor[0]['child'][NAMESPACE_POCO]['displayName'][0]['data']); } $author['author_name'] = unxmlify($found_author->get_name()); $author['author_link'] = unxmlify($found_author->get_link()); $author['author_is_feed'] = false; } else { $author['author_name'] = unxmlify($feed->get_title()); $author['author_link'] = unxmlify($feed->get_permalink()); $author['author_is_feed'] = true; } if(substr($author['author_link'],-1,1) == '/') $author['author_link'] = substr($author['author_link'],0,-1); // look for a photo. We should check media size and find the best one, // but for now let's just find any author photo $rawauthor = $item->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10,'author'); if($rawauthor && isset($rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link'])) { $base = $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']; foreach($base as $link) { if(!x($author, 'author_photo') || ! $author['author_photo']) { if($link['attribs']['']['rel'] === 'photo' || $link['attribs']['']['rel'] === 'avatar') $author['author_photo'] = unxmlify($link['attribs']['']['href']); } } } $rawactor = $item->get_item_tags(NAMESPACE_ACTIVITY, 'actor'); if($rawactor && activity_match($rawactor[0]['child'][NAMESPACE_ACTIVITY]['obj_type'][0]['data'], ['Person', ACTIVITY_OBJ_PERSON])) { $base = $rawactor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']; if($base && count($base)) { foreach($base as $link) { if($link['attribs']['']['rel'] === 'alternate' && (! $res['author_link'])) $author['author_link'] = unxmlify($link['attribs']['']['href']); if(!x($author, 'author_photo') || ! $author['author_photo']) { if($link['attribs']['']['rel'] === 'avatar' || $link['attribs']['']['rel'] === 'photo') $author['author_photo'] = unxmlify($link['attribs']['']['href']); } } } } // check for a yahoo media element (github etc.) if(! x($author,'author_photo') || ! $author['author_photo']) { $rawmedia = $item->get_item_tags(NAMESPACE_YMEDIA,'thumbnail'); if($rawmedia && $rawmedia[0]['attribs']['']['url']) { $author['author_photo'] = strip_tags(unxmlify($rawmedia[0]['attribs']['']['url'])); } } // No photo/profile-link on the item - look at the feed level if(! x($author,'author_link') || ! x($author,'author_photo')) { $rawauthor = $feed->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10,'author'); if($rawauthor && isset($rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link'])) { $base = $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']; foreach($base as $link) { if($link['attribs']['']['rel'] === 'alternate' && (! $author['author_link'])) { $author['author_link'] = unxmlify($link['attribs']['']['href']); $author['author_is_feed'] = true; } if(! $author['author_photo']) { if($link['attribs']['']['rel'] === 'photo' || $link['attribs']['']['rel'] === 'avatar') $author['author_photo'] = unxmlify($link['attribs']['']['href']); } } } $rawactor = $feed->get_feed_tags(NAMESPACE_ACTIVITY, 'subject'); if($rawactor && activity_match($rawactor[0]['child'][NAMESPACE_ACTIVITY]['obj_type'][0]['data'], ['Person', ACTIVITY_OBJ_PERSON])) { $base = $rawactor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']; if($base && count($base)) { foreach($base as $link) { if($link['attribs']['']['rel'] === 'alternate' && (! $res['author_link'])) $author['author_link'] = unxmlify($link['attribs']['']['href']); if(! (x($author,'author_photo'))) { if($link['attribs']['']['rel'] === 'avatar' || $link['attribs']['']['rel'] === 'photo') $author['author_photo'] = unxmlify($link['attribs']['']['href']); } } } } } $rawowner = $item->get_item_tags(NAMESPACE_DFRN, 'owner'); if(! $rawowner) $rawowner = $item->get_item_tags(NAMESPACE_ZOT, 'owner'); if(isset($rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'])) $author['owner_name'] = unxmlify($rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data']); elseif(isset($rawowner[0]['child'][NAMESPACE_DFRN]['name'][0]['data'])) $author['owner_name'] = unxmlify($rawowner[0]['child'][NAMESPACE_DFRN]['name'][0]['data']); if(isset($rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'])) $author['owner_link'] = unxmlify($rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data']); elseif(isset($rawowner[0]['child'][NAMESPACE_DFRN]['uri'][0]['data'])) $author['owner_link'] = unxmlify($rawowner[0]['child'][NAMESPACE_DFRN]['uri'][0]['data']); if(isset($rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link'])) { $base = $rawowner[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']; foreach($base as $link) { if(!x($author, 'owner_photo') || ! $author['owner_photo']) { if($link['attribs']['']['rel'] === 'photo' || $link['attribs']['']['rel'] === 'avatar') $author['owner_photo'] = unxmlify($link['attribs']['']['href']); } } } // build array to pass to hook $arr = [ 'feed' => $feed, 'item' => $item, 'author' => $author ]; /** * @hooks parse_atom_author * * \e SimplePie \b feed - The original SimplePie feed * * \e SimplePie \b item * * \e array \b author - the result array that will also get returned */ call_hooks('parse_atom_author', $arr); logger('author: ' . print_r($arr['author'], true), LOGGER_DATA); return $arr['author']; } /** * @brief Return an array with a parsed atom item. * * @param SimplePie $feed * @param SimplePie $item * @return array Associative array with the parsed item data */ function get_atom_elements($feed, $item) { require_once('include/html2bbcode.php'); $res = array(); $res['mid'] = normalise_id(unxmlify($item->get_id())); $res['title'] = unxmlify($item->get_title()); $res['body'] = unxmlify($item->get_content()); $res['plink'] = unxmlify($item->get_link(0)); $res['item_rss'] = 1; $res['uuid'] = uuid_from_url($res['plink']); $summary = unxmlify($item->get_description(true)); if($summary === $res['body']) $summary = ''; if(($summary) && ((strpos($summary,'<') !== false) || (strpos($summary,'>') !== false))) { $summary = html2plain($summary); } // removing the content of the title if its identically to the body // This helps with auto generated titles e.g. from tumblr if (title_is_body($res['title'], $res['body'])) $res['title'] = ""; else { $res['title'] = bbcode($res['title'], [ 'tryoembed' => false ]); $res['title'] = html2plain($res['title'], 0, true); $res['title'] = strip_tags($res['title']); $res['title'] = html_entity_decode($res['title'], ENT_QUOTES, 'UTF-8'); $res['title'] = preg_replace("/https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,\@]+/", "", $res['title']); while (strpos($res['title'], "\n") !== false) $res['title'] = str_replace("\n", " ", $res['title']); while (strpos($res['title'], " ") !== false) $res['title'] = str_replace(" ", " ", $res['title']); $res['title'] = trim($res['title']); } if($res['plink']) $base_url = implode('/', array_slice(explode('/',$res['plink']),0,3)); else $base_url = ''; $rawcreated = $item->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'published'); if($rawcreated) $res['created'] = unxmlify($rawcreated[0]['data']); $rawedited = $item->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'updated'); if($rawedited) $res['edited'] = unxmlify($rawedited[0]['data']); if(x($res,'edited') && ! x($res,'created')) $res['created'] = $res['edited']; if(! x($res,'created')) $res['created'] = $item->get_date('c'); if(! x($res,'edited')) $res['edited'] = $item->get_date('c'); $rawverb = $item->get_item_tags(NAMESPACE_ACTIVITY, 'verb'); // select between supported verbs if($rawverb) { $res['verb'] = unxmlify($rawverb[0]['data']); } $ostatus_conversation = ''; $rawcnv = $item->get_item_tags(NAMESPACE_OSTATUS, 'conversation'); if($rawcnv) { // new style $ostatus_conversation = normalise_id(unxmlify($rawcnv[0]['attribs']['']['ref'])); if(! $ostatus_conversation) { // old style $ostatus_conversation = normalise_id(unxmlify($rawcnv[0]['data'])); } if($ostatus_conversation) { set_iconfig($res,'ostatus','conversation',$ostatus_conversation,true); logger('ostatus_conversation: ' . $ostatus_conversation, LOGGER_DATA, LOG_INFO); } } $mastodon = (($item->get_item_tags('http://mastodon.social/schema/1.0','scope')) ? true : false); if($mastodon) { if(($mastodon[0]['data']) && ($mastodon[0]['data'] !== 'public')) $res['item_private'] = 1; } $apps = $item->get_item_tags(NAMESPACE_STATUSNET, 'notice_info'); if($apps && $apps[0]['attribs']['']['source']) { $res['app'] = strip_tags(unxmlify($apps[0]['attribs']['']['source'])); } /* * If there's a copy of the body content which is guaranteed to have survived mangling in transit, use it. */ $have_real_body = false; $rawenv = $item->get_item_tags(NAMESPACE_DFRN, 'env'); if(! $rawenv) $rawenv = $item->get_item_tags(NAMESPACE_ZOT, 'source'); if($rawenv) { $have_real_body = true; $res['body'] = $rawenv[0]['data']; $res['body'] = str_replace(array(' ',"\t","\r","\n"), array('','','',''),$res['body']); // make sure nobody is trying to sneak some html tags by us $res['body'] = notags(base64url_decode($res['body'])); // We could probably turn these old Friendica bbcode bookmarks into bookmark tags but we'd have to // create a term table item for them. For now just make sure they stay as links. $res['body'] = preg_replace('/\[bookmark(.*?)\](.*?)\[\/bookmark\]/','[url$1]$2[/url]',$res['body']); } $res['body'] = limit_body_size($res['body']); // It isn't certain at this point whether our content is plaintext or html and we'd be foolish to trust // the content type. Our own network only emits text normally, though it might have been converted to // html if we used a pubsubhubbub transport. But if we see even one html tag in our text, we will // have to assume it is all html and needs to be purified. // It doesn't matter all that much security wise - because before this content is used anywhere, we are // going to escape any tags we find regardless, but this lets us import a limited subset of html from // the wild, by sanitising it and converting supported tags to bbcode before we rip out any remaining // html. if((strpos($res['body'],'<') !== false) && (strpos($res['body'],'>') !== false)) { $res['body'] = reltoabs($res['body'],$base_url); $res['body'] = html2bb_video($res['body']); $res['body'] = oembed_html2bbcode($res['body']); $res['body'] = purify_html($res['body']); $res['body'] = @html2bbcode($res['body']); } elseif(! $have_real_body) { // it's not one of our messages and it has no tags // so it's probably just text. We'll escape it just to be safe. $res['body'] = escape_tags($res['body']); } // strip title and don't apply "title-in-body" if the feed involved // uses the OStatus stack. We need a more generalised way for the calling // function to specify this behaviour or for plugins to alter it. $terms = []; if($res['plink'] && $res['title']) { $res['body'] = '#^[url=' . $res['plink'] . ']' . $res['title'] . '[/url]' . "\n\n" . $res['body']; $terms[] = array( 'otype' => TERM_OBJ_POST, 'ttype' => TERM_BOOKMARK, 'url' => $res['plink'], 'term' => $res['title'], ); } elseif($res['plink']) { $res['body'] = '#^[url]' . $res['plink'] . '[/url]' . "\n\n" . $res['body']; $terms[] = array( 'otype' => TERM_OBJ_POST, 'ttype' => TERM_BOOKMARK, 'url' => $res['plink'], 'term' => $res['plink'], ); } if($summary && $res['body']) { $res['body'] = '[summary]' . $summary . '[/summary]' . $res['body']; } $private = $item->get_item_tags(NAMESPACE_DFRN, 'private'); if($private && intval($private[0]['data']) > 0) $res['item_private'] = ((intval($private[0]['data'])) ? 1 : 0); else $res['item_private'] = 0; $rawlocation = $item->get_item_tags(NAMESPACE_DFRN, 'location'); if($rawlocation) $res['location'] = unxmlify($rawlocation[0]['data']); // Disallow time travelling posts $d1 = strtotime($res['created']); $d2 = strtotime($res['edited']); $d3 = strtotime('now'); if($d1 > $d3) $res['created'] = datetime_convert(); if($d2 > $d3) $res['edited'] = datetime_convert(); $res['created'] = datetime_convert('UTC','UTC',$res['created']); $res['edited'] = datetime_convert('UTC','UTC',$res['edited']); $rawgeo = $item->get_item_tags(NAMESPACE_GEORSS, 'point'); if($rawgeo) $res['coord'] = unxmlify($rawgeo[0]['data']); $cats = $item->get_categories(); if($cats) { if(is_null($terms)) $terms = array(); foreach($cats as $cat) { $term = $cat->get_term(); if(! $term) $term = $cat->get_label(); $scheme = $cat->get_scheme(); $termurl = ''; if($scheme && $term && stristr($scheme,'X-DFRN:')) { $termtype = ((substr($scheme,7,1) === '#') ? TERM_HASHTAG : TERM_MENTION); $termurl = unxmlify(substr($scheme,9)); } else { $termtype = TERM_CATEGORY; } $termterm = notags(trim(unxmlify($term))); // Mastodon auto generates an nsfw category tag for any 'content-warning' message. // Most people use CW and use both summary/content as a spoiler and we honour that // construct so the post will already be collapsed. The generated tag is almost // always wrong and even if it isn't we would already be doing the right thing. if($mastodon && $termterm === 'nsfw' && $summary && $res['body']) continue; if($termterm) { $terms[] = array( 'otype' => TERM_OBJ_POST, 'ttype' => $termtype, 'url' => $termurl, 'term' => $termterm, ); } } } if($terms) $res['term'] = $terms; $attach = $item->get_enclosures(); if($attach) { $res['attach'] = array(); foreach($attach as $att) { $len = intval($att->get_length()); $link = str_replace(array(',','"'),array('%2D','%22'),notags(trim(unxmlify($att->get_link())))); $title = str_replace(array(',','"'),array('%2D','%22'),notags(trim(unxmlify($att->get_title())))); $type = str_replace(array(',','"'),array('%2D','%22'),notags(trim(unxmlify($att->get_type())))); if(strpos($type,';')) $type = substr($type,0,strpos($type,';')); if((! $link) || (strpos($link,'http') !== 0)) continue; if(! $title) $title = ' '; if(! $type) $type = 'application/octet-stream'; // put media enclosures in bbcode markup if((strpos($type,'image') === 0) && (strpos($res['body'], ']' . $link . '[/img]') === false) && (strpos($link,'http') === 0)) { $res['body'] .= "\n\n" . '[img]' . $link . '[/img]'; } if((strpos($type,'video') === 0) && (strpos($res['body'], ']' . $link . '[/video]') === false) && (strpos($link,'http') === 0)) { $res['body'] .= "\n\n" . '[video]' . $link . '[/video]'; } if((strpos($type,'audio') === 0) && (strpos($res['body'], ']' . $link . '[/audio]') === false) && (strpos($link,'http') === 0)) { $res['body'] .= "\n\n" . '[audio]' . $link . '[/audio]'; } $res['attach'][] = array('href' => $link, 'length' => $len, 'type' => $type, 'title' => $title ); } } $rawobj = $item->get_item_tags(NAMESPACE_ACTIVITY, 'object'); if($rawobj) { $obj = array(); $child = $rawobj[0]['child']; if($child[NAMESPACE_ACTIVITY]['obj_type'][0]['data']) { $res['obj_type'] = $child[NAMESPACE_ACTIVITY]['obj_type'][0]['data']; $obj['type'] = $child[NAMESPACE_ACTIVITY]['obj_type'][0]['data']; } if($child[NAMESPACE_ACTIVITY]['object-type'][0]['data']) { $res['obj_type'] = $child[NAMESPACE_ACTIVITY]['object-type'][0]['data']; $obj['type'] = $child[NAMESPACE_ACTIVITY]['object-type'][0]['data']; } if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'id') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['id'][0]['data']) $obj['id'] = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['id'][0]['data']; if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'link') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['link']) $obj['link'] = encode_rel_links($child[SIMPLEPIE_NAMESPACE_ATOM_10]['link']); if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'title') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['title'][0]['data']) $obj['title'] = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['title'][0]['data']; if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'content') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['content'][0]['data']) { $body = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['content'][0]['data']; if(! $body) $body = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['summary'][0]['data']; // preserve a copy of the original body content in case we later need to parse out any microformat information, e.g. events $obj['orig'] = xmlify($body); if((strpos($body,'<') !== false) || (strpos($body,'>') !== false)) { $body = purify_html($body); $body = html2bbcode($body); } $obj['content'] = $body; } $res['obj'] = $obj; } $rawobj = $item->get_item_tags(NAMESPACE_ACTIVITY, 'target'); if($rawobj) { $obj = array(); $child = $rawobj[0]['child']; if($child[NAMESPACE_ACTIVITY]['obj_type'][0]['data']) { $res['tgt_type'] = $child[NAMESPACE_ACTIVITY]['obj_type'][0]['data']; $obj['type'] = $child[NAMESPACE_ACTIVITY]['obj_type'][0]['data']; } if($child[NAMESPACE_ACTIVITY]['object-type'][0]['data']) { $res['tgt_type'] = $child[NAMESPACE_ACTIVITY]['object-type'][0]['data']; $obj['type'] = $child[NAMESPACE_ACTIVITY]['object-type'][0]['data']; } if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'id') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['id'][0]['data']) $obj['id'] = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['id'][0]['data']; if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'link') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['link']) $obj['link'] = encode_rel_links($child[SIMPLEPIE_NAMESPACE_ATOM_10]['link']); if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'title') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['title'][0]['data']) $obj['title'] = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['title'][0]['data']; if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'content') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['content'][0]['data']) { $body = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['content'][0]['data']; if(! $body) $body = $child[SIMPLEPIE_NAMESPACE_ATOM_10]['summary'][0]['data']; // preserve a copy of the original body content in case we later need to parse out any microformat information, e.g. events $obj['orig'] = xmlify($body); if((strpos($body,'<') !== false) || (strpos($body,'>') !== false)) { $body = purify_html($body); $body = html2bbcode($body); } $obj['content'] = $body; } $res['target'] = $obj; } if(array_key_exists('verb',$res) && $res['verb'] === ACTIVITY_SHARE && array_key_exists('obj_type',$res) && in_array($res['obj_type'], ['Note', ACTIVITY_OBJ_NOTE, ACTIVITY_OBJ_COMMENT])) { feed_get_reshare($res,$item); } // build array to pass to hook $arr = [ 'feed' => $feed, 'item' => $item, 'result' => $res ]; /** * @hooks parse_atom * * \e SimplePie \b feed - The original SimplePie feed * * \e SimplePie \b item * * \e array \b result - the result array that will also get returned */ call_hooks('parse_atom', $arr); logger('result: ' . print_r($arr['result'], true), LOGGER_DATA); return $arr['result']; } function feed_get_reshare(&$res,$item) { $share = []; // For Mastodon shares ("boosts"), we need to parse the original author information // from the activity:object -> author structure $rawobj = $item->get_item_tags(NAMESPACE_ACTIVITY, 'object'); if($rawobj) { $rawauthor = $rawobj[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['author']; if($rawauthor && $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name']) { $share['author'] = unxmlify($rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data']); } if($rawauthor && $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri']) { $share['profile'] = unxmlify($rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data']); } if($rawauthor && $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']) { $base = $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']; foreach($base as $link) { if(! (array_key_exists('avatar',$share) && $share['avatar'])) { if($link['attribs']['']['rel'] === 'photo' || $link['attribs']['']['rel'] === 'avatar') $share['avatar'] = unxmlify($link['attribs']['']['href']); } } } if(! $share['author']) $share['author'] = t('unknown'); if(! $share['avatar']) $share['avatar'] = z_root() . '/' . get_default_profile_photo(80); if(! $share['profile']) $share['profile'] = z_root(); $child = $rawobj[0]['child']; if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'link') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['link']) $share['links'] = encode_rel_links($child[SIMPLEPIE_NAMESPACE_ATOM_10]['link']); $rawcreated = $rawobj[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['published']; if($rawcreated) $share['created'] = unxmlify($rawcreated[0]['data']); else $share['created'] = $res['created']; if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'id') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['id'][0]['data']) $share['message_id'] = unxmlify($child[SIMPLEPIE_NAMESPACE_ATOM_10]['id'][0]['data']); if(x($child[SIMPLEPIE_NAMESPACE_ATOM_10], 'content') && $child[SIMPLEPIE_NAMESPACE_ATOM_10]['content'][0]['data']) { $body = unxmlify($child[SIMPLEPIE_NAMESPACE_ATOM_10]['content'][0]['data']); if(! $body) $body = unxmlify($child[SIMPLEPIE_NAMESPACE_ATOM_10]['summary'][0]['data']); if((strpos($body,'<') !== false) || (strpos($body,'>') !== false)) { $body = purify_html($body); $body = html2bbcode($body); } } $attach = $share['links']; if($attach) { foreach($attach as $att) { if($att['rel'] === 'alternate') { $share['alternate'] = str_replace(array(',','"'),array('%2D','%22'),notags(trim(unxmlify($att['href'])))); continue; } if($att['rel'] !== 'enclosure') continue; $len = intval($att['length']); $link = str_replace(array(',','"'),array('%2D','%22'),notags(trim(unxmlify($att['href'])))); $title = str_replace(array(',','"'),array('%2D','%22'),notags(trim(unxmlify($att['title'])))); $type = str_replace(array(',','"'),array('%2D','%22'),notags(trim(unxmlify($att['type'])))); if(strpos($type,';')) $type = substr($type,0,strpos($type,';')); if((! $link) || (strpos($link,'http') !== 0)) continue; if(! $title) $title = ' '; if(! $type) $type = 'application/octet-stream'; if((strpos($type,'image') === 0) && (strpos($body, ']' . $link . '[/img]') === false) && (strpos($link,'http') === 0)) { $body .= "\n\n" . '[img]' . $link . '[/img]'; } if((strpos($type,'video') === 0) && (strpos($body, ']' . $link . '[/video]') === false) && (strpos($link,'http') === 0)) { $body .= "\n\n" . '[video]' . $link . '[/video]'; } if((strpos($type,'audio') === 0) && (strpos($body, ']' . $link . '[/audio]') === false) && (strpos($link,'http') === 0)) { $body .= "\n\n" . '[audio]' . $link . '[/audio]'; } } } if((! $body) && ($share['alternate'])) { $body = $share['alternate']; } $res['body'] = "[share author='" . urlencode($share['author']) . "' profile='" . $share['profile'] . "' avatar='" . $share['avatar'] . "' link='" . $share['alternate'] . "' auth='" . 'false' . "' posted='" . $share['created'] . "' message_id='" . $share['message_id'] . "']"; $res['body'] .= $body; $res['body'] .= "[/share]"; } } /** * @brief Encodes SimplePie_Item link arrays. * * @param array $links Array with SimplePie_Item link tags * @return array */ function encode_rel_links($links) { $o = array(); if(! ((is_array($links)) && (count($links)))) return $o; foreach($links as $link) { $l = array(); if($link['attribs']['']['rel']) $l['rel'] = $link['attribs']['']['rel']; if($link['attribs']['']['length']) $l['length'] = $link['attribs']['']['length']; if($link['attribs']['']['title']) $l['title'] = $link['attribs']['']['title']; if($link['attribs']['']['type']) $l['type'] = $link['attribs']['']['type']; if($link['attribs']['']['href']) $l['href'] = $link['attribs']['']['href']; if( (x($link['attribs'], NAMESPACE_MEDIA)) && $link['attribs'][NAMESPACE_MEDIA]['width']) $l['width'] = $link['attribs'][NAMESPACE_MEDIA]['width']; if( (x($link['attribs'], NAMESPACE_MEDIA)) && $link['attribs'][NAMESPACE_MEDIA]['height']) $l['height'] = $link['attribs'][NAMESPACE_MEDIA]['height']; if($l) $o[] = $l; } return $o; } function process_feed_tombstones($feed,$importer,$contact,$pass) { $arr_deleted = []; $del_entries = $feed->get_feed_tags(NAMESPACE_TOMB, 'deleted-entry'); if(is_array($del_entries) && count($del_entries) && $pass != 2) { foreach($del_entries as $dentry) { if(isset($dentry['attribs']['']['ref'])) { $arr_deleted[] = normalise_id($dentry['attribs']['']['ref']); } } } if($arr_deleted && is_array($contact)) { foreach($arr_deleted as $mid) { $r = q("SELECT * from item where mid = '%s' and author_xchan = '%s' and uid = %d limit 1", dbesc($mid), dbesc($contact['xchan_hash']), intval($importer['channel_id']) ); if($r) { $item = $r[0]; if(! intval($item['item_deleted'])) { logger('deleting item ' . $item['id'] . ' mid=' . $item['mid'], LOGGER_DEBUG); drop_item($item['id'],false); } } } } } /** * @brief Process atom feed and update anything/everything we might need to update. * * @param string $xml * The (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds. * @param array $importer * The channel record of the local user who owns this * relationship. It is this person's stuff that is going to be updated. * @param[in,out] array $contact (abook record joined to xchan record) * The person who is sending us stuff. If not set, we MAY be processing a "follow" activity * from an external network and MAY create an appropriate contact record. Otherwise, we MUST * have a contact record. * @param int $pass by default ($pass = 0) we cannot guarantee that a parent item has been * imported prior to its children being seen in the stream unless we are certain * of how the feed is arranged/ordered. * * With $pass = 1, we only pull parent items out of the stream. * * With $pass = 2, we only pull children (comments/likes). * * So running this twice, first with pass 1 and then with pass 2 will do the right * thing regardless of feed ordering. This won't be adequate in a fully-threaded * model where comments can have sub-threads. That would require some massive sorting * to get all the feed items into a mostly linear ordering, and might still require * recursion. */ function consume_feed($xml, $importer, &$contact, $pass = 0) { if(! strlen($xml)) { logger('Empty input'); return; } $sys_expire = intval(get_config('system', 'default_expire_days')); $chn_expire = intval($importer['channel_expire_days']); $expire_days = $sys_expire; if(($chn_expire != 0) && ($chn_expire < $sys_expire)) $expire_days = $chn_expire; $feed = new SimplePie(); $feed->set_raw_data($xml); // We can preserve iframes because we will strip them in the purifier after // checking for supported video sources. $strip_htmltags = $feed->strip_htmltags; array_splice($strip_htmltags, array_search('iframe', $strip_htmltags), 1); $feed->strip_htmltags($strip_htmltags); $feed->init(); if($feed->error()) logger('Error parsing XML: ' . $feed->error()); $permalink = $feed->get_permalink(); // Check at the feed level for tombstones process_feed_tombstones($feed,$importer,$contact,$pass); // Now process the feed if($feed->get_item_quantity()) { logger('feed item count = ' . $feed->get_item_quantity(), LOGGER_DEBUG); $items = $feed->get_items(); foreach($items as $item) { $is_reply = false; $send_downstream = false; $parent_link = ''; logger('processing ' . $item->get_id(), LOGGER_DEBUG); $rawthread = $item->get_item_tags( NAMESPACE_THREAD,'in-reply-to'); if(isset($rawthread[0]['attribs']['']['ref'])) { $is_reply = true; $parent_mid = normalise_id($rawthread[0]['attribs']['']['ref']); } if(isset($rawthread[0]['attribs']['']['href'])) { $parent_link = $rawthread[0]['attribs']['']['href']; } if(isset($parent_mid)) logger('in-reply-to: ' . $parent_mid, LOGGER_DEBUG); if($is_reply) { if($pass == 1) continue; // Have we seen it? If not, import it. $author = get_atom_author($feed,$item); $datarray = get_atom_elements($feed,$item); if(! $datarray['mid']) continue; $item_parent_mid = q("select parent_mid from item where mid = '%s' and uid = %d limit 1", dbesc($parent_mid), intval($importer['channel_id']) ); // This probably isn't an appropriate default but we're about to change it // if it's wrong. $datarray['comment_policy'] = 'authenticated'; // A Mastodon privacy tag has been found. We cannot send private comments // through the OStatus protocol, so block commenting. if(array_key_exists('item_private',$datarray) && intval($datarray['item_private'])) { $datarray['public_policy'] = 'specific'; $datarray['comment_policy'] = 'none'; } if($contact['xchan_network'] === 'rss') { $datarray['public_policy'] = 'specific'; $datarray['comment_policy'] = 'none'; } // if we have everything but a photo, provide the default profile photo if($author['author_name'] && $author['author_link'] && (! $author['author_photo'])) $author['author_photo'] = z_root() . '/' . get_default_profile_photo(80); if((! x($author,'author_name')) || ($author['author_is_feed'])) $author['author_name'] = $contact['xchan_name']; if((! x($author,'author_link')) || ($author['author_is_feed'])) $author['author_link'] = $contact['xchan_url']; if((! x($author,'author_photo'))|| ($author['author_is_feed'])) $author['author_photo'] = $contact['xchan_photo_m']; $datarray['author_xchan'] = ''; if($author['author_link'] != $contact['xchan_url']) { $name = ''; if($author['full_name']) { $name = $author['full_name']; if($author['author_name']) $name .= ' (' . $author['author_name'] . ')'; } else { $name = $author['author_name']; } $x = import_author_rss(array('name' => $name,'url' => $author['author_link'],'photo' => array('src' => $author['author_photo']))); if($x) $datarray['author_xchan'] = $x; } if(! $datarray['author_xchan']) $datarray['author_xchan'] = $contact['xchan_hash']; $datarray['owner_xchan'] = $contact['xchan_hash']; $r = q("SELECT id, edited, author_xchan, item_deleted FROM item WHERE mid = '%s' AND uid = %d LIMIT 1", dbesc($datarray['mid']), intval($importer['channel_id']) ); // Update content if 'updated' changes if($r) { if(activity_match($datarray['verb'], ['Delete', ACTIVITY_DELETE]) && $datarray['author_xchan'] === $r[0]['author_xchan']) { if(! intval($r[0]['item_deleted'])) { logger('deleting item ' . $r[0]['id'] . ' mid=' . $datarray['mid'], LOGGER_DEBUG); drop_item($r[0]['id'],false); } continue; } if((x($datarray,'edited') !== false) && (datetime_convert('UTC','UTC',$datarray['edited']) !== $r[0]['edited'])) { // do not accept (ignore) an earlier edit than one we currently have. if(datetime_convert('UTC','UTC',$datarray['edited']) < $r[0]['edited']) continue; $datarray['uid'] = $importer['channel_id']; $datarray['aid'] = $importer['channel_account_id']; $datarray['id'] = $r[0]['id']; update_feed_item($importer['channel_id'],$datarray); } continue; } $pmid = ''; $conv_id = get_iconfig($datarray,'ostatus','conversation'); // match conversations - first try ostatus:conversation // next try thr:in_reply_to if($conv_id) { logger('find_parent: conversation_id: ' . $conv_id, LOGGER_DEBUG); $c = q("select parent_mid from item left join iconfig on item.id = iconfig.iid where iconfig.cat = 'ostatus' and iconfig.k = 'conversation' and iconfig.v = '%s' and item.uid = %d order by item.id limit 1", dbesc($conv_id), intval($importer['channel_id']) ); if($c) { logger('find_parent: matched conversation: ' . $conv_id, LOGGER_DEBUG); $pmid = $c[0]['parent_mid']; $datarray['parent_mid'] = $pmid; } } if(($item_parent_mid) && (! $pmid)) { logger('find_parent: matched in-reply-to: ' . $parent_mid, LOGGER_DEBUG); $pmid = $item_parent_mid[0]['parent_mid']; $datarray['parent_mid'] = $pmid; } if((! $pmid) && $parent_link !== '') { $f = feed_conversation_fetch($importer,$contact,$parent_link); if($f) { // check both potential conversation parents again if($conv_id) { $c = q("select parent_mid from item left join iconfig on item.id = iconfig.iid where iconfig.cat = 'ostatus' and iconfig.k = 'conversation' and iconfig.v = '%s' and item.uid = %d order by item.id limit 1", dbesc($conv_id), intval($importer['channel_id']) ); if($c) { $pmid = $c[0]['parent_mid']; $datarray['parent_mid'] = $pmid; } } if(! $pmid) { $x = q("select parent_mid from item where mid = '%s' and uid = %d limit 1", dbesc($parent_mid), intval($importer['channel_id']) ); if($x) { $item_parent_mid = $x; $pmid = $x[0]['parent_mid']; $datarray['parent_mid'] = $pmid; } } } // the conversation parent might just be the post we are trying to import. // check existence again in case it was just delivered. $r = q("SELECT id FROM item WHERE mid = '%s' AND uid = %d LIMIT 1", dbesc($datarray['mid']), intval($importer['channel_id']) ); if($r) { continue; } } if($pmid) { // check comment permissions on the parent $parent_item = 0; $r = q("select * from item where parent_mid = '%s' and parent_mid = mid and uid = %d limit 1", dbesc($pmid), intval($importer['channel_id']) ); if($r) { $parent_item = $r[0]; if(intval($parent_item['item_nocomment']) || $parent_item['comment_policy'] === 'none' || ($parent_item['comments_closed'] > NULL_DATE && $parent_item['comments_closed'] < datetime_convert())) { logger('comments disabled for post ' . $parent_item['mid']); continue; } } $allowed = false; if($parent_item) { if($parent_item['owner_xchan'] == $importer['channel_hash']) $allowed = perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'post_comments'); else $allowed = true; if(! $allowed) { logger('Ignoring this comment author.'); $status = 202; continue; } // The salmon endpoint sets this to indicate that we should send comments from // interactive feeds (such as OStatus) downstream to our followers // We do not want to set it for non-interactive feeds or conversations we do not own if(array_key_exists('send_downstream',$importer) && intval($importer['send_downstream']) && ($parent_item['owner_xchan'] == $importer['channel_hash'])) { $send_downstream = true; } } else { if((! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'send_stream')) && (! $importer['system'])) { /** * @fixme check for and process ostatus autofriend * otherwise ignore this author. */ logger('Ignoring this author.'); continue; } } } else { // immediate parent wasn't found. Turn into a top-level post if permissions allow // but save the thread_parent in case we need to refer to it later. if($importer['channel_system']) { if( ! \Zotlabs\Lib\MessageFilter::evaluate($datarray,get_config('system','pubstream_incl'),get_config('system','pubstream_excl'))) { continue; } } if(! post_is_importable($importer['channel_id'], $datarray, [$contact])) continue; $datarray['parent_mid'] = $datarray['mid']; set_iconfig($datarray,'system','parent_mid',$parent_mid,true); } // allow likes of comments if($item_parent_mid && activity_match($datarray['verb'], ['Like', ACTIVITY_LIKE])) { $datarray['thr_parent'] = $item_parent_mid[0]['parent_mid']; } $datarray['aid'] = $importer['channel_account_id']; $datarray['uid'] = $importer['channel_id']; logger('data: ' . print_r($datarray, true), LOGGER_DATA); $xx = item_store($datarray); $r = $xx['item_id']; if($send_downstream) { \Zotlabs\Daemon\Master::Summon(array('Notifier', 'comment', $r)); } continue; } else { // Head post of a conversation. Have we seen it? If not, import it. $author = get_atom_author($feed,$item); $datarray = get_atom_elements($feed,$item); if(! $datarray['mid']) continue; // This probably isn't an appropriate default but we're about to change it // if it's wrong. $datarray['comment_policy'] = 'authenticated'; // A Mastodon privacy tag has been found. We cannot send private comments // through the OStatus protocol, so block commenting. if(array_key_exists('item_private',$datarray) && intval($datarray['item_private'])) { $datarray['public_policy'] = 'specific'; $datarray['comment_policy'] = 'none'; } if($contact['xchan_network'] === 'rss') { $datarray['public_policy'] = 'specific'; $datarray['comment_policy'] = 'none'; } // if we have everything but a photo, provide the default profile photo if($author['author_name'] && $author['author_link'] && (! x($author,'author_photo') || ! $author['author_photo'])) $author['author_photo'] = z_root() . '/' . get_default_profile_photo(80); if(is_array($contact)) { if((! x($author,'author_name')) || ($author['author_is_feed'])) $author['author_name'] = $contact['xchan_name']; if((! x($author,'author_link')) || ($author['author_is_feed'])) $author['author_link'] = $contact['xchan_url']; if((! x($author,'author_photo'))|| ($author['author_is_feed'])) $author['author_photo'] = $contact['xchan_photo_m']; } if((! x($author,'author_name')) || (! x($author,'author_link'))) { logger('No author information! ' . print_r($author,true)); continue; } $datarray['author_xchan'] = ''; if($author['author_link'] != $contact['xchan_url']) { $name = ''; if(isset($author['full_name'])) { $name = $author['full_name']; if(isset($author['author_name'])) $name .= ' (' . $author['author_name'] . ')'; } else { $name = $author['author_name']; } $x = import_author_rss(array('name' => $name,'url' => $author['author_link'],'photo' => array('src' => $author['author_photo']))); if($x) $datarray['author_xchan'] = $x; } if(! $datarray['author_xchan']) $datarray['author_xchan'] = $contact['xchan_hash']; $datarray['owner_xchan'] = $contact['xchan_hash']; if(array_key_exists('created',$datarray) && $datarray['created'] > NULL_DATE && $expire_days) { $t1 = $datarray['created']; $t2 = datetime_convert('UTC','UTC','now - ' . $expire_days . 'days'); if($t1 < $t2) { logger('feed content older than expiration. Ignoring.', LOGGER_DEBUG, LOG_INFO); continue; } } $r = q("SELECT id, edited, author_xchan, item_deleted FROM item WHERE mid = '%s' AND uid = %d LIMIT 1", dbesc($datarray['mid']), intval($importer['channel_id']) ); // Update content if 'updated' changes if($r) { if(isset($datarray['verb']) && activity_match($datarray['verb'], ['Delete', ACTIVITY_DELETE]) && isset($datarray['author_xchan']) && $datarray['author_xchan'] === $r[0]['author_xchan']) { if(! intval($r[0]['item_deleted'])) { logger('deleting item ' . $r[0]['id'] . ' mid=' . $datarray['mid'], LOGGER_DEBUG); drop_item($r[0]['id'],false); } continue; } if((x($datarray,'edited') !== false) && (datetime_convert('UTC','UTC',$datarray['edited']) !== $r[0]['edited'])) { // do not accept (ignore) an earlier edit than one we currently have. if(datetime_convert('UTC','UTC',$datarray['edited']) < $r[0]['edited']) continue; $datarray['uid'] = $importer['channel_id']; $datarray['aid'] = $importer['channel_account_id']; $datarray['id'] = $r[0]['id']; update_feed_item($importer['channel_id'],$datarray); } continue; } $datarray['parent_mid'] = $datarray['mid']; $datarray['uid'] = $importer['channel_id']; $datarray['aid'] = $importer['channel_account_id']; if(isset($author['owner_link']) && !link_compare($author['owner_link'], $contact['xchan_url'])) { logger('Correcting item owner.', LOGGER_DEBUG); $author['owner_name'] = $contact['xchan_name']; $author['owner_link'] = $contact['xchan_url']; $author['owner_avatar'] = $contact['xchan_photo_m']; } if($importer['channel_system']) { if( ! \Zotlabs\Lib\MessageFilter::evaluate($datarray,get_config('system','pubstream_incl'),get_config('system','pubstream_excl'))) { continue; } } if(! post_is_importable($importer['channel_id'], $datarray, [$contact])) continue; logger('author: ' . print_r($author, true), LOGGER_DEBUG); logger('data: ' . print_r($datarray, true), LOGGER_DATA); $xx = item_store($datarray); $r = $xx['item_id']; continue; } } } } function feed_conversation_fetch($importer,$contact,$parent_link) { logger('parent_link: ' . $parent_link, LOGGER_DEBUG, LOG_INFO); $link = ''; // GNU-Social flavoured feeds if(strpos($parent_link,'/notice/')) { $link = str_replace('/notice/','/api/statuses/show/',$parent_link) . '.atom'; } // Mastodon flavoured feeds if(strpos($parent_link,'/users/') && strpos($parent_link,'/updates/')) { $link = $parent_link . '.atom'; } if(! $link) return false; logger('fetching: ' . $link, LOGGER_DEBUG, LOG_INFO); $fetch = z_fetch_url($link); if(! $fetch['success']) return false; $data = $fetch['body']; // We will probably receive an atom 'entry' and not an atom 'feed'. Unfortunately // our parser is a bit strict about compliance so we'll insert just enough of a feed // tag to trick it into believing it's a compliant feed. if(! strstr($data,'set_raw_data($xml); // We can preserve iframes because we will strip them in the purifier after // checking for supported video sources. $strip_htmltags = $feed->strip_htmltags; array_splice($strip_htmltags, array_search('iframe', $strip_htmltags), 1); $feed->strip_htmltags($strip_htmltags); $feed->init(); if($feed->error()) { logger('Error parsing XML: ' . $feed->error()); return $ret; } $permalink = $feed->get_permalink(); if($feed->get_item_quantity()) { // this should be exactly one logger('feed item count = ' . $feed->get_item_quantity(), LOGGER_DEBUG); $items = $feed->get_items(); foreach($items as $item) { $item_id = normalise_id($item->get_id()); logger('processing ' . $item_id, LOGGER_DEBUG); $rawthread = $item->get_item_tags( NAMESPACE_THREAD, 'in-reply-to'); if(isset($rawthread[0]['attribs']['']['ref'])) { $is_reply = true; $parent_mid = normalise_id($rawthread[0]['attribs']['']['ref']); } if($is_reply) $ret['parent_mid'] = $parent_mid; $ret['author'] = get_atom_author($feed,$item); $ret['item'] = get_atom_elements($feed,$item); // reset policies which are restricted by default for RSS connections // This item is likely coming from GNU-social via salmon and allows public interaction $ret['item']['public_policy'] = ''; $ret['item']['comment_policy'] = 'authenticated'; } } return $ret; } /** * @brief Given an xml (atom) feed, find author and hub links. * * @param string $xml * @return array */ function feed_meta($xml) { $ret = array(); if(! strlen($xml)) { logger('empty input'); return $ret; } $feed = new SimplePie(); $feed->set_raw_data($xml); $feed->init(); if($feed->error()) { logger('Error parsing XML: ' . $feed->error()); return $ret; } $ret['hubs'] = $feed->get_links('hub'); //logger('hubs: ' . print_r($hubs,true), LOGGER_DATA); $author = array(); $found_author = $feed->get_author(); if($found_author) { $author['author_name'] = unxmlify($found_author->get_name()); $author['author_link'] = unxmlify($found_author->get_link()); $rawauthor = $feed->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'author'); logger('rawauthor: ' . print_r($rawauthor, true)); if($rawauthor) { if($rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']) { $base = $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']; foreach($base as $link) { if(!x($author, 'author_photo') || ! $author['author_photo']) { if($link['attribs']['']['rel'] === 'photo' || $link['attribs']['']['rel'] === 'avatar') { $author['author_photo'] = unxmlify($link['attribs']['']['href']); break; } } } } if($rawauthor[0]['child'][NAMESPACE_POCO]['displayName'][0]['data']) $author['full_name'] = unxmlify($rawauthor[0]['child'][NAMESPACE_POCO]['displayName'][0]['data']); if($rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data']) $author['author_uri'] = unxmlify($rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data']); } } if(! $author['author_photo']) $author['author_photo'] = $feed->get_image_url(); if(substr($author['author_link'],-1,1) == '/') $author['author_link'] = substr($author['author_link'],0,-1); $ret['author'] = $author; return $ret; } /** * @brief Not yet implemented function to update feed item. * * @param int $uid * @param array $datarray */ function update_feed_item($uid, $datarray) { item_store_update($datarray); } /** * @brief Fetch the content of a feed and further consume it. * * It will first process parent items and in a second run child items. * @see consume_feed() * * @param int $uid * @param int $abook_id * @param string $url URL of the feed */ function handle_feed($uid, $abook_id, $url) { $channel = channelx_by_n($uid); if(! $channel) return; $x = q("select * from abook left join xchan on abook_xchan = xchan_hash where abook_id = %d and abook_channel = %d limit 1", dbesc($abook_id), intval($uid) ); $recurse = 0; $z = z_fetch_url($url, false, $recurse, array('novalidate' => true)); //logger('data:' . print_r($z, true), LOGGER_DATA); if($z['success']) { consume_feed($z['body'], $channel, $x[0], 1); consume_feed($z['body'], $channel, $x[0], 2); return true; } return false; } /** * @brief Return a XML tag with author information. * * @param string $tag The XML tag to create * @param string $nick preferred username * @param string $name displayed name of the author * @param string $uri * @param int $h image height * @param int $w image width * @param string $type profile photo mime type * @param string $photo Fully qualified URL to a profile/avator photo * @return string XML tag */ function atom_author($tag, $nick, $name, $uri, $h, $w, $type, $photo) { $o = ''; if(! $tag) return $o; $nick = xmlify($nick); $name = xmlify($name); $uri = xmlify($uri); $h = intval($h); $w = intval($w); $photo = xmlify($photo); $o .= "<$tag>\r\n"; $o .= " $uri\r\n"; $o .= " $nick\r\n"; $o .= " $uri\r\n"; $o .= ' ' . "\r\n"; $o .= ' ' . "\r\n"; $o .= ' ' . $nick . '' . "\r\n"; $o .= ' ' . $name . '' . "\r\n"; /** * @hooks atom_author * Possibility to add further tags to returned XML string * * \e string - The created XML tag as a string without closing tag */ call_hooks('atom_author', $o); $o .= "\r\n"; return $o; } /** * @brief Return an atom tag with author information from an xchan. * * @param string $tag * @param array $xchan * @return string */ function atom_render_author($tag, $xchan) { $nick = ((!empty($xchan['xchan_addr'])) ? xmlify(substr($xchan['xchan_addr'], 0, strpos($xchan['xchan_addr'], '@'))) : ''); $id = ((!empty($xchan['xchan_url'])) ? xmlify($xchan['xchan_url']) : ''); $name = ((!empty($xchan['xchan_name'])) ? xmlify($xchan['xchan_name']) : ''); $photo = ((!empty($xchan['xchan_photo_l'])) ? xmlify($xchan['xchan_photo_l']) : ''); $type = ((!empty($xchan['xchan_photo_mimetype'])) ? xmlify($xchan['xchan_photo_mimetype']) : ''); $w = $h = 300; $o = "<$tag>\r\n"; $o .= " http://activitystrea.ms/schema/1.0/person\r\n"; $o .= " $id\r\n"; $o .= " $nick\r\n"; $o .= " $id\r\n"; $o .= ' ' . "\r\n"; $o .= ' ' . "\r\n"; $o .= ' ' . "\r\n"; $o .= ' ' . $nick . '' . "\r\n"; $o .= ' ' . $name . '' . "\r\n"; /** * @hooks atom_render_author * Possibility to add further tags to returned XML string. * * \e string The created XML tag as a string without closing tag */ call_hooks('atom_render_author', $o); $o .= "\r\n"; return $o; } function compat_photos_list($s) { $ret = []; $found = preg_match_all('/\[[zi]mg(.*?)\](.*?)\[/ism',$s,$matches,PREG_SET_ORDER); if($found) { foreach($matches as $match) { $entry = [ 'href' => $match[2], 'type' => guess_image_type($match[2]) ]; $sizer = new \Zotlabs\Lib\Img_filesize($match[2]); $size = $sizer->getSize(); if(intval($size)) { $entry['length'] = intval($size); } $ret[] = $entry; } } return $ret; } /** * @brief Create an item for the Atom feed. * * @see get_feed_for() * * @param array $item * @param string $type * @param array $author * @param array $owner * @param string $comment default false * @param number $cid default 0 * @param boolean $compat default false * @return void|string */ function atom_entry($item, $type, $author, $owner, $comment = false, $cid = 0, $compat = false) { if(! $item['parent']) return; if($item['item_deleted']) return '' . "\r\n"; create_export_photo_body($item); // provide separate summary and content unless compat is true; as summary represents a content-warning on some networks $matches = false; if(preg_match('|\[summary\](.*?)\[/summary\]|ism',$item['body'],$matches)) $summary = $matches[1]; else $summary = ''; $body = $item['body']; if($summary) $body = preg_replace('|^(.*?)\[summary\](.*?)\[/summary\](.*?)$|ism','$1$3',$item['body']); if($compat) $summary = ''; if($item['allow_cid'] || $item['allow_gid'] || $item['deny_cid'] || $item['deny_gid']) $body = fix_private_photos($body,$owner['channel_id'],$item,$cid); if($compat) { $compat_photos = compat_photos_list($body); } else { $compat_photos = null; } $o = "\r\n\r\n\r\n"; if(is_array($author)) { $o .= atom_render_author('author',$author); } else { $o .= atom_render_author('author',$item['author']); } $o .= atom_render_author('zot:owner',$item['owner']); if(($item['parent'] != $item['id']) || ($item['parent_mid'] !== $item['mid']) || (($item['thr_parent'] !== '') && ($item['thr_parent'] !== $item['mid']))) { $parent_item = (($item['thr_parent']) ? $item['thr_parent'] : $item['parent_mid']); // ensure it's a legal uri and not just a message-id if(! strpos($parent_item,':')) $parent_item = 'X-ZOT:' . $parent_item; $o .= '' . "\r\n"; } if((activity_match($item['obj_type'], ACTIVITY_OBJ_EVENT) || activity_match($item['obj_type'], 'Event')) && activity_match($item['verb'],['Create', ACTIVITY_POST])) { $obj = ((is_array($item['obj'])) ? $item['obj'] : json_decode($item['obj'],true)); $o .= '' . xmlify($item['title']) . '' . "\r\n"; $o .= '' . xmlify(bbcode($obj['title'])) . '' . "\r\n"; $o .= '' . datetime_convert('UTC','UTC', $obj['dtstart'],'Ymd\\THis' . (($obj['adjust']) ? '\\Z' : '')) . '' . "\r\n"; $o .= '' . datetime_convert('UTC','UTC', $obj['dtend'],'Ymd\\THis' . (($obj['adjust']) ? '\\Z' : '')) . '' . "\r\n"; $o .= '' . xmlify(bbcode($obj['location'])) . '' . "\r\n"; $o .= '' . xmlify(bbcode($obj['description'])) . '' . "\r\n"; } else { $o .= '' . xmlify($item['title']) . '' . "\r\n"; if($summary) $o .= '' . xmlify(prepare_text($summary,$item['mimetype'])) . '' . "\r\n"; $o .= '' . xmlify(prepare_text($body,$item['mimetype'])) . '' . "\r\n"; } $o .= '' . 'X-ZOT:' . xmlify($item['mid']) . '' . "\r\n"; $o .= '' . xmlify(datetime_convert('UTC','UTC',$item['created'] . '+00:00',ATOM_TIME)) . '' . "\r\n"; $o .= '' . xmlify(datetime_convert('UTC','UTC',$item['edited'] . '+00:00',ATOM_TIME)) . '' . "\r\n"; $o .= '' . "\r\n"; if($item['location']) { $o .= '' . xmlify($item['location']) . '' . "\r\n"; $o .= '' . xmlify($item['location']) . '' . "\r\n"; } if($item['coord']) $o .= '' . xmlify($item['coord']) . '' . "\r\n"; if(($item['item_private']) || strlen($item['allow_cid']) || strlen($item['allow_gid']) || strlen($item['deny_cid']) || strlen($item['deny_gid'])) $o .= '' . (($item['item_private']) ? $item['item_private'] : 1) . '' . "\r\n"; if($item['app']) $o .= '' . "\r\n"; $verb = construct_verb($item); $o .= '' . xmlify($verb) . '' . "\r\n"; $actobj = construct_activity_object($item); if(strlen($actobj)) $o .= $actobj; $actarg = construct_activity_target($item); if(strlen($actarg)) $o .= $actarg; if($item['attach']) { $enclosures = json_decode($item['attach'], true); if($enclosures) { foreach($enclosures as $enc) { $o .= '' . "\r\n"; } } } if($compat_photos) { foreach($compat_photos as $enc) { $o .= '' . "\r\n"; } } if (isset($item['term']) && $item['term']) { foreach($item['term'] as $term) { $scheme = ''; $label = ''; switch($term['ttype']) { case TERM_UNKNOWN: $scheme = NAMESPACE_ZOT . '/term/unknown'; $label = $term['term']; break; case TERM_HASHTAG: case TERM_COMMUNITYTAG: $scheme = NAMESPACE_ZOT . '/term/hashtag'; $label = '#' . $term['term']; break; case TERM_MENTION: $scheme = NAMESPACE_ZOT . '/term/mention'; $label = '@' . $term['term']; break; case TERM_CATEGORY: $scheme = NAMESPACE_ZOT . '/term/category'; $label = $term['term']; break; default: break; } if(! $scheme) continue; $o .= '' . "\r\n"; } } $o .= '' . "\r\n"; // build array to pass to hook $x = [ 'item' => $item, 'type' => $type, 'author' => $author, 'owner' => $owner, 'comment' => $comment, 'abook_id' => $cid, 'entry' => $o ]; /** * @hooks atom_entry * * \e array \b item * * \e string \b type * * \e array \b author * * \e array \b owner * * \e string \b comment * * \e number \b abook_id * * \e string \b entry - The generated entry and what will get returned */ call_hooks('atom_entry', $x); return $x['entry']; }