Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2309 lines
76 KiB

<?php
/**
* @file src/Protocol/OStatus.php
*/
namespace Friendica\Protocol;
use DOMDocument;
use DOMXPath;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML;
use Friendica\Core\Cache;
use Friendica\Core\Config;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\Lock;
use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Model\Conversation;
use Friendica\Model\GContact;
use Friendica\Model\Item;
use Friendica\Model\User;
use Friendica\Network\Probe;
use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network;
use Friendica\Util\Proxy as ProxyUtils;
use Friendica\Util\Strings;
use Friendica\Util\XML;
require_once 'mod/share.php';
require_once 'include/api.php';
/**
* @brief This class contain functions for the OStatus protocol
*/
class OStatus
{
4 years ago
private static $itemlist;
private static $conv_list = [];
4 years ago
/**
* @brief Fetches author data
*
* @param DOMXPath $xpath The xpath object
* @param object $context The xml context of the author details
* @param array $importer user record of the importing user
* @param array $contact Called by reference, will contain the fetched contact
* @param bool $onlyfetch Only fetch the header without updating the contact entries
*
* @return array Array of author related entries for the item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function fetchAuthor(DOMXPath $xpath, $context, array $importer, array &$contact, $onlyfetch)
{
$author = [];
$author["author-link"] = XML::getFirstNodeValue($xpath, 'atom:author/atom:uri/text()', $context);
$author["author-name"] = XML::getFirstNodeValue($xpath, 'atom:author/atom:name/text()', $context);
$addr = XML::getFirstNodeValue($xpath, 'atom:author/atom:email/text()', $context);
$aliaslink = $author["author-link"];
$alternate_item = $xpath->query("atom:author/atom:link[@rel='alternate']", $context)->item(0);
if (is_object($alternate_item)) {
foreach ($alternate_item->attributes as $attributes) {
if (($attributes->name == "href") && ($attributes->textContent != "")) {
$author["author-link"] = $attributes->textContent;
}
}
}
$author["author-id"] = Contact::getIdForURL($author["author-link"]);
$author["contact-id"] = $contact["id"];
$contact = null;
/*
This here would be better, but we would get problems with contacts from the statusnet addon
This is kept here as a reminder for the future
$cid = Contact::getIdForURL($author["author-link"], $importer["uid"]);
if ($cid) {
$contact = DBA::selectFirst('contact', [], ['id' => $cid]);
}
*/
if ($aliaslink != '') {
$condition = ["`uid` = ? AND `alias` = ? AND `network` != ? AND `rel` IN (?, ?)",
$importer["uid"], $aliaslink, Protocol::STATUSNET,
Contact::SHARING, Contact::FRIEND];
$contact = DBA::selectFirst('contact', [], $condition);
}
if (!DBA::isResult($contact) && $author["author-link"] != '') {
if ($aliaslink == "") {
$aliaslink = $author["author-link"];
}
$condition = ["`uid` = ? AND `nurl` IN (?, ?) AND `network` != ? AND `rel` IN (?, ?)",
$importer["uid"], Strings::normaliseLink($author["author-link"]), Strings::normaliseLink($aliaslink),
Protocol::STATUSNET, Contact::SHARING, Contact::FRIEND];
$contact = DBA::selectFirst('contact', [], $condition);
}
if (!DBA::isResult($contact) && ($addr != '')) {
$condition = ["`uid` = ? AND `addr` = ? AND `network` != ? AND `rel` IN (?, ?)",
$importer["uid"], $addr, Protocol::STATUSNET,
Contact::SHARING, Contact::FRIEND];
$contact = DBA::selectFirst('contact', [], $condition);
}
if (DBA::isResult($contact)) {
if ($contact['blocked']) {
$contact['id'] = -1;
}
$author["contact-id"] = $contact["id"];
}
$avatarlist = [];
$avatars = $xpath->query("atom:author/atom:link[@rel='avatar']", $context);
foreach ($avatars as $avatar) {
$href = "";
$width = 0;
foreach ($avatar->attributes as $attributes) {
if ($attributes->name == "href") {
$href = $attributes->textContent;
}
if ($attributes->name == "width") {
$width = $attributes->textContent;
}
}
if ($href != "") {
$avatarlist[$width] = $href;
}
}
if (count($avatarlist) > 0) {
krsort($avatarlist);
$author["author-avatar"] = Probe::fixAvatar(current($avatarlist), $author["author-link"]);
}
$displayname = XML::getFirstNodeValue($xpath, 'atom:author/poco:displayName/text()', $context);
if ($displayname != "") {
$author["author-name"] = $displayname;
}
$author["owner-id"] = $author["author-id"];
// Only update the contacts if it is an OStatus contact
if (DBA::isResult($contact) && ($contact['id'] > 0) && !$onlyfetch && ($contact["network"] == Protocol::OSTATUS)) {
// Update contact data
$current = $contact;
unset($current['name-date']);
// This query doesn't seem to work
// $value = $xpath->query("atom:link[@rel='salmon']", $context)->item(0)->nodeValue;
// if ($value != "")
// $contact["notify"] = $value;
// This query doesn't seem to work as well - I hate these queries
// $value = $xpath->query("atom:link[@rel='self' and @type='application/atom+xml']", $context)->item(0)->nodeValue;
// if ($value != "")
// $contact["poll"] = $value;
$contact['url'] = $author["author-link"];
$contact['nurl'] = Strings::normaliseLink($contact['url']);
$value = XML::getFirstNodeValue($xpath, 'atom:author/atom:uri/text()', $context);
if ($value != "") {
$contact["alias"] = $value;
}
$value = XML::getFirstNodeValue($xpath, 'atom:author/poco:displayName/text()', $context);
if ($value != "") {
$contact["name"] = $value;
}
$value = XML::getFirstNodeValue($xpath, 'atom:author/poco:preferredUsername/text()', $context);
if ($value != "") {
$contact["nick"] = $value;
}
$value = XML::getFirstNodeValue($xpath, 'atom:author/poco:note/text()', $context);
if ($value != "") {
$contact["about"] = HTML::toBBCode($value);
}
$value = XML::getFirstNodeValue($xpath, 'atom:author/poco:address/poco:formatted/text()', $context);
if ($value != "") {
$contact["location"] = $value;
}
$contact['name-date'] = DateTimeFormat::utcNow();
DBA::update('contact', $contact, ['id' => $contact["id"]], $current);
if (!empty($author["author-avatar"]) && ($author["author-avatar"] != $current['avatar'])) {
Logger::log("Update profile picture for contact ".$contact["id"], Logger::DEBUG);
Contact::updateAvatar($author["author-avatar"], $importer["uid"], $contact["id"]);
}
// Ensure that we are having this contact (with uid=0)
$cid = Contact::getIdForURL($aliaslink, 0, true);
if ($cid) {
$fields = ['url', 'nurl', 'name', 'nick', 'alias', 'about', 'location'];
$old_contact = DBA::selectFirst('contact', $fields, ['id' => $cid]);
// Update it with the current values
$fields = ['url' => $author["author-link"], 'name' => $contact["name"],
'nurl' => Strings::normaliseLink($author["author-link"]),
'nick' => $contact["nick"], 'alias' => $contact["alias"],
'about' => $contact["about"], 'location' => $contact["location"],
'success_update' => DateTimeFormat::utcNow(), 'last-update' => DateTimeFormat::utcNow()];
DBA::update('contact', $fields, ['id' => $cid], $old_contact);
// Update the avatar
if (!empty($author["author-avatar"])) {
Contact::updateAvatar($author["author-avatar"], 0, $cid);
}
}
$contact["generation"] = 2;
$contact["hide"] = false; // OStatus contacts are never hidden
if (!empty($author["author-avatar"])) {
$contact["photo"] = $author["author-avatar"];
}
$gcid = GContact::update($contact);
GContact::link($gcid, $contact["uid"], $contact["id"]);
} elseif ($contact["network"] != Protocol::DFRN) {
$contact = null;
}
return $author;
}
/**
* @brief Fetches author data from a given XML string
*
* @param string $xml The XML
* @param array $importer user record of the importing user
*
* @return array Array of author related entries for the item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function salmonAuthor($xml, array $importer)
{
if ($xml == "") {
return;
}
$doc = new DOMDocument();
@$doc->loadXML($xml);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('atom', NAMESPACE_ATOM1);
$xpath->registerNamespace('thr', NAMESPACE_THREAD);
$xpath->registerNamespace('georss', NAMESPACE_GEORSS);
$xpath->registerNamespace('activity', NAMESPACE_ACTIVITY);
$xpath->registerNamespace('media', NAMESPACE_MEDIA);
$xpath->registerNamespace('poco', NAMESPACE_POCO);
$xpath->registerNamespace('ostatus', NAMESPACE_OSTATUS);
$xpath->registerNamespace('statusnet', NAMESPACE_STATUSNET);
$contact = ["id" => 0];
// Fetch the first author
$authordata = $xpath->query('//author')->item(0);
$author = self::fetchAuthor($xpath, $authordata, $importer, $contact, true);
return $author;
}
/**
* @brief Read attributes from element
*
* @param object $element Element object
*
* @return array attributes
*/
private static function readAttributes($element)
{
$attribute = [];
foreach ($element->attributes as $attributes) {
$attribute[$attributes->name] = $attributes->textContent;
}
return $attribute;
}
4 years ago
/**
* @brief Imports an XML string containing OStatus elements
*
* @param string $xml The XML
* @param array $importer user record of the importing user
* @param array $contact contact
* @param string $hub Called by reference, returns the fetched hub data
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
4 years ago
*/
public static function import($xml, array $importer, array &$contact, &$hub)
{
self::process($xml, $importer, $contact, $hub);
}
4 years ago
/**
* @brief Internal feed processing
4 years ago
*
* @param string $xml The XML
* @param array $importer user record of the importing user
* @param array $contact contact
* @param string $hub Called by reference, returns the fetched hub data
* @param boolean $stored Is the post fresh imported or from the database?
* @param boolean $initialize Is it the leading post so that data has to be initialized?
*
* @return boolean Could the XML be processed?
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
4 years ago
*/
private static function process($xml, array $importer, array &$contact, &$hub, $stored = false, $initialize = true)
{
if ($initialize) {
self::$itemlist = [];
self::$conv_list = [];
}
Logger::log('Import OStatus message for user ' . $importer['uid'], Logger::DEBUG);
if ($xml == "") {
return false;
}
$doc = new DOMDocument();
@$doc->loadXML($xml);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('atom', NAMESPACE_ATOM1);
$xpath->registerNamespace('thr', NAMESPACE_THREAD);
$xpath->registerNamespace('georss', NAMESPACE_GEORSS);
$xpath->registerNamespace('activity', NAMESPACE_ACTIVITY);
$xpath->registerNamespace('media', NAMESPACE_MEDIA);
$xpath->registerNamespace('poco', NAMESPACE_POCO);
$xpath->registerNamespace('ostatus', NAMESPACE_OSTATUS);
$xpath->registerNamespace('statusnet', NAMESPACE_STATUSNET);
4 years ago
$hub = "";
$hub_items = $xpath->query("/atom:feed/atom:link[@rel='hub']")->item(0);
if (is_object($hub_items)) {
$hub_attributes = $hub_items->attributes;
if (is_object($hub_attributes)) {
foreach ($hub_attributes as $hub_attribute) {
if ($hub_attribute->name == "href") {
$hub = $hub_attribute->textContent;
Logger::log("Found hub ".$hub, Logger::DEBUG);
}
4 years ago
}
}
}
$header = [];
$header["uid"] = $importer["uid"];
$header["network"] = Protocol::OSTATUS;
$header["wall"] = 0;
$header["origin"] = 0;
$header["gravity"] = GRAVITY_COMMENT;
if (!is_object($doc->firstChild) || empty($doc->firstChild->tagName)) {
return false;
}
$first_child = $doc->firstChild->tagName;
if ($first_child == "feed") {
$entries = $xpath->query('/atom:feed/atom:entry');
} else {
$entries = $xpath->query('/atom:entry');
}
if ($entries->length == 1) {
// We reformat the XML to make it better readable
$doc2 = new DOMDocument();
$doc2->loadXML($xml);
$doc2->preserveWhiteSpace = false;
$doc2->formatOutput = true;
$xml2 = $doc2->saveXML();
$header["protocol"] = Conversation::PARCEL_SALMON;
$header["source"] = $xml2;
} elseif (!$initialize) {
return false;
}
// Fetch the first author
$authordata = $xpath->query('//author')->item(0);
$author = self::fetchAuthor($xpath, $authordata, $importer, $contact, $stored);
// Reverse the order of the entries
$entrylist = [];
foreach ($entries as $entry) {
$entrylist[] = $entry;
}
foreach (array_reverse($entrylist) as $entry) {
// fetch the author
$authorelement = $xpath->query('/atom:entry/atom:author', $entry);
if ($authorelement->length == 0) {
$authorelement = $xpath->query('atom:author', $entry);
}
if ($authorelement->length > 0) {
$author = self::fetchAuthor($xpath, $entry, $importer, $contact, $stored);
}
$value = XML::getFirstNodeValue($xpath, 'atom:author/poco:preferredUsername/text()', $entry);
if ($value != "") {
$nickname = $value;
} else {
$nickname = $author["author-name"];
}
$item = array_merge($header, $author);
$item["uri"] = XML::getFirstNodeValue($xpath, 'atom:id/text()', $entry);
$item["verb"] = XML::getFirstNodeValue($xpath, 'activity:verb/text()', $entry);
// Delete a message
if (in_array($item["verb"], ['qvitter-delete-notice', ACTIVITY_DELETE, 'delete'])) {
self::deleteNotice($item);
continue;
}
if (in_array($item["verb"], [NAMESPACE_OSTATUS."/unfavorite", ACTIVITY_UNFAVORITE])) {
// Ignore "Unfavorite" message
Logger::log("Ignore unfavorite message ".print_r($item, true), Logger::DEBUG);
continue;
}
// Deletions come with the same uri, so we check for duplicates after processing deletions
if (Item::exists(['uid' => $importer["uid"], 'uri' => $item["uri"]])) {
Logger::log('Post with URI '.$item["uri"].' already existed for user '.$importer["uid"].'.', Logger::DEBUG);
continue;
} else {
Logger::log('Processing post with URI '.$item["uri"].' for user '.$importer["uid"].'.', Logger::DEBUG);
}
if ($item["verb"] == ACTIVITY_JOIN) {
// ignore "Join" messages
Logger::log("Ignore join message ".print_r($item, true), Logger::DEBUG);
continue;
}
if ($item["verb"] == "http://mastodon.social/schema/1.0/block") {
// ignore mastodon "block" messages
Logger::log("Ignore block message ".print_r($item, true), Logger::DEBUG);
continue;
}
if ($item["verb"] == ACTIVITY_FOLLOW) {
Contact::addRelationship($importer, $contact, $item, $nickname);
continue;
}
if ($item["verb"] == NAMESPACE_OSTATUS."/unfollow") {
$dummy = null;
Contact::removeFollower($importer, $contact, $item, $dummy);
continue;
}
if ($item["verb"] == ACTIVITY_FAVORITE) {
$orig_uri = $xpath->query("activity:object/atom:id", $entry)->item(0)->nodeValue;
Logger::log("Favorite ".$orig_uri." ".print_r($item, true));
$item["verb"] = ACTIVITY_LIKE;
$item["parent-uri"] = $orig_uri;
$item["gravity"] = GRAVITY_ACTIVITY;
$item["object-type"] = ACTIVITY_OBJ_NOTE;
}
// http://activitystrea.ms/schema/1.0/rsvp-yes
if (!in_array($item["verb"], [ACTIVITY_POST, ACTIVITY_LIKE, ACTIVITY_SHARE])) {
Logger::log("Unhandled verb ".$item["verb"]." ".print_r($item, true), Logger::DEBUG);
}
self::processPost($xpath, $entry, $item, $importer);
if ($initialize && (count(self::$itemlist) > 0)) {
if (self::$itemlist[0]['uri'] == self::$itemlist[0]['parent-uri']) {
// We will import it everytime, when it is started by our contacts
$valid = !empty(self::$itemlist[0]['contact-id']);
if (!$valid) {
// If not, then it depends on this setting
$valid = !Config::get('system', 'ostatus_full_threads');
if ($valid) {
Logger::log("Item with uri ".self::$itemlist[0]['uri']." will be imported due to the system settings.", Logger::DEBUG);
}
} else {
Logger::log("Item with uri ".self::$itemlist[0]['uri']." belongs to a contact (".self::$itemlist[0]['contact-id']."). It will be imported.", Logger::DEBUG);
}
if ($valid) {
// Never post a thread when the only interaction by our contact was a like
$valid = false;
$verbs = [ACTIVITY_POST, ACTIVITY_SHARE];
foreach (self::$itemlist as $item) {
if (!empty($item['contact-id']) && in_array($item['verb'], $verbs)) {
$valid = true;
}
}
if ($valid) {
Logger::log("Item with uri ".self::$itemlist[0]['uri']." will be imported since the thread contains posts or shares.", Logger::DEBUG);
}
}
} else {
// But we will only import complete threads
$valid = Item::exists(['uid' => $importer["uid"], 'uri' => self::$itemlist[0]['parent-uri']]);
if ($valid) {
Logger::log("Item with uri ".self::$itemlist[0]["uri"]." belongs to parent ".self::$itemlist[0]['parent-uri']." of user ".$importer["uid"].". It will be imported.", Logger::DEBUG);
}
}
if ($valid) {
$default_contact = 0;
for ($key = count(self::$itemlist) - 1; $key >= 0; $key--) {
if (empty(self::$itemlist[$key]['contact-id'])) {
self::$itemlist[$key]['contact-id'] = $default_contact;
} else {
$default_contact = $item['contact-id'];
}
}
foreach (self::$itemlist as $item) {
$found = Item::exists(['uid' => $importer["uid"], 'uri' => $item["uri"]]);
if ($found) {
Logger::log("Item with uri ".$item["uri"]." for user ".$importer["uid"]." already exists.", Logger::DEBUG);
} elseif ($item['contact-id'] < 0) {
Logger::log("Item with uri ".$item["uri"]." is from a blocked contact.", Logger::DEBUG);
} else {
// We are having duplicated entries. Hopefully this solves it.
if (Lock::acquire('ostatus_process_item_insert')) {
$ret = Item::insert($item);
Lock::release('ostatus_process_item_insert');
Logger::log("Item with uri ".$item["uri"]." for user ".$importer["uid"].' stored. Return value: '.$ret);
} else {
$ret = Item::insert($item);
Logger::log("We couldn't lock - but tried to store the item anyway. Return value is ".$ret);
}
}
}
}
self::$itemlist = [];
}
Logger::log('Processing done for post with URI '.$item["uri"].' for user '.$importer["uid"].'.', Logger::DEBUG);
}
return true;
}
/**
* Removes notice item from database
*
* @param array $item item
* @return void
* @throws \Exception
*/
private static function deleteNotice(array $item)
{
$condition = ['uid' => $item['uid'], 'author-id' => $item['author-id'], 'uri' => $item['uri']];
if (!Item::exists($condition)) {
Logger::log('Item from '.$item['author-link'].' with uri '.$item['uri'].' for user '.$item['uid']." wasn't found. We don't delete it.");
return;
}
Item::delete($condition);
Logger::log('Deleted item with uri '.$item['uri'].' for user '.$item['uid']);
}
/**
* @brief Processes the XML for a post
*
* @param DOMXPath $xpath The xpath object
* @param object $entry The xml entry that is processed
* @param array $item The item array
* @param array $importer user record of the importing user
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function processPost(DOMXPath $xpath, $entry, array &$item, array $importer)
{
$item["body"] = HTML::toBBCode(XML::getFirstNodeValue($xpath, 'atom:content/text()', $entry));
$item["object-type"] = XML::getFirstNodeValue($xpath, 'activity:object-type/text()', $entry);
if (($item["object-type"] == ACTIVITY_OBJ_BOOKMARK) || ($item["object-type"] == ACTIVITY_OBJ_EVENT)) {
$item["title"] = XML::getFirstNodeValue($xpath, 'atom:title/text()', $entry);
$item["body"] = XML::getFirstNodeValue($xpath, 'atom:summary/text()', $entry);
} elseif ($item["object-type"] == ACTIVITY_OBJ_QUESTION) {
$item["title"] = XML::getFirstNodeValue($xpath, 'atom:title/text()', $entry);
}
$item["created"] = XML::getFirstNodeValue($xpath, 'atom:published/text()', $entry);
$item["edited"] = XML::getFirstNodeValue($xpath, 'atom:updated/text()', $entry);
$item['conversation-uri'] = XML::getFirstNodeValue($xpath, 'ostatus:conversation/text()', $entry);
$conv = $xpath->query('ostatus:conversation', $entry);
if (is_object($conv->item(0))) {
foreach ($conv->item(0)->attributes as $attributes) {
if ($attributes->name == "ref") {
$item['conversation-uri'] = $attributes->textContent;
}
if ($attributes->name == "href") {
$item['conversation-href'] = $attributes->textContent;
}
}
}
$related = "";
$inreplyto = $xpath->query('thr:in-reply-to', $entry);
if (is_object($inreplyto->item(0))) {
foreach ($inreplyto->item(0)->attributes as $attributes) {
if ($attributes->name == "ref") {
$item["parent-uri"] = $attributes->textContent;
}
if ($attributes->name == "href") {
$related = $attributes->textContent;
}
}
}
$georsspoint = $xpath->query('georss:point', $entry);
if (!empty($georsspoint) && ($georsspoint->length > 0)) {
$item["coord"] = $georsspoint->item(0)->nodeValue;
}
$categories = $xpath->query('atom:category', $entry);
if ($categories) {
foreach ($categories as $category) {
foreach ($category->attributes as $attributes) {
if ($attributes->name == 'term') {
$term = $attributes->textContent;
if (!empty($item['tag'])) {
$item['tag'] .= ',';
} else {
$item['tag'] = '';
}
$item['tag'] .= '#[url=' . System::baseUrl() . '/search?tag=' . $term . ']' . $term . '[/url]';
}
}
}
}
$self = '';
$add_body = '';
$links = $xpath->query('atom:link', $entry);
if ($links) {
$link_data = self::processLinks($links, $item);
$self = $link_data['self'];
$add_body = $link_data['add_body'];
}
$repeat_of = "";
$notice_info = $xpath->query('statusnet:notice_info', $entry);
if ($notice_info && ($notice_info->length > 0)) {
foreach ($notice_info->item(0)->attributes as $attributes) {
if ($attributes->name == "source") {
$item["app"] = strip_tags($attributes->textContent);
}
if ($attributes->name == "repeat_of") {
$repeat_of = $attributes->textContent;
}
}
}
// Is it a repeated post?
if (($repeat_of != "") || ($item["verb"] == ACTIVITY_SHARE)) {
$link_data = self::processRepeatedItem($xpath, $entry, $item, $importer);
if (!empty($link_data['add_body'])) {
$add_body .= $link_data['add_body'];
}
}
$item["body"] .= $add_body;
// Only add additional data when there is no picture in the post
if (!strstr($item["body"], '[/img]')) {
$item["body"] = add_page_info_to_body($item["body"]);
}
// Mastodon Content Warning
if (($item["verb"] == ACTIVITY_POST) && $xpath->evaluate('boolean(atom:summary)', $entry)) {
$clear_text = XML::getFirstNodeValue($xpath, 'atom:summary/text()', $entry);
if (!empty($clear_text)) {
$item['content-warning'] = HTML::toBBCode($clear_text);
}
}
if (($self != '') && empty($item['protocol'])) {
self::fetchSelf($self, $item);
}
if (!empty($item["conversation-href"])) {
self::fetchConversation($item['conversation-href'], $item['conversation-uri']);
}
if (isset($item["parent-uri"])) {
if (!Item::exists(['uid' => $importer["uid"], 'uri' => $item['parent-uri']])) {
if ($related != '') {
self::fetchRelated($related, $item["parent-uri"], $importer);
}
} else {
Logger::log('Reply with URI '.$item["uri"].' already existed for user '.$importer["uid"].'.', Logger::DEBUG);
}
} else {
$item["parent-uri"] = $item["uri"];
$item["gravity"] = GRAVITY_PARENT;
}
if (($item['author-link'] != '') && !empty($item['protocol'])) {
$item = Conversation::insert($item);
}
self::$itemlist[] = $item;
}
/**
* @brief Fetch the conversation for posts
*
* @param string $conversation The link to the conversation
* @param string $conversation_uri The conversation in "uri" format
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function fetchConversation($conversation, $conversation_uri)
{
// Ensure that we only store a conversation once in a process
if (isset(self::$conv_list[$conversation])) {
return;
}
self::$conv_list[$conversation] = true;
$curlResult = Network::curl($conversation, false, $redirects, ['accept_content' => 'application/atom+xml, text/html']);
if (!$curlResult->isSuccess()) {
return;
}
$xml = '';
if (stristr($curlResult->getHeader(), 'Content-Type: application/atom+xml')) {
$xml = $curlResult->getBody();
}
if ($xml == '') {
$doc = new DOMDocument();
if (!@$doc->loadHTML($curlResult->getBody())) {
return;
}
$xpath = new DOMXPath($doc);
$links = $xpath->query('//link');
if ($links) {
$file = '';
foreach ($links as $link) {
$attribute = self::readAttributes($link);
if (($attribute['rel'] == 'alternate') && ($attribute['type'] == 'application/atom+xml')) {
$file = $attribute['href'];
}
}
if ($file != '') {
$conversation_atom = Network::curl($attribute['href']);
if ($conversation_atom->isSuccess()) {
$xml = $conversation_atom->getBody();
}
}
}
}
if ($xml == '') {
return;
}
self::storeConversation($xml, $conversation, $conversation_uri);
}
/**
* @brief Store a feed in several conversation entries
*
* @param string $xml The feed
* @param string $conversation conversation
* @param string $conversation_uri conversation uri
* @return void
* @throws \Exception
*/
private static function storeConversation($xml, $conversation = '', $conversation_uri = '')
{
$doc = new DOMDocument();
@$doc->loadXML($xml);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('atom', NAMESPACE_ATOM1);
$xpath->registerNamespace('thr', NAMESPACE_THREAD);
$xpath->registerNamespace('ostatus', NAMESPACE_OSTATUS);
$entries = $xpath->query('/atom:feed/atom:entry');
// Now store the entries
foreach ($entries as $entry) {
$doc2 = new DOMDocument();
$doc2->preserveWhiteSpace = false;
$doc2->formatOutput = true;
$conv_data = [];