+ /**
+ * @brief Send a "poke"
+ *
+ * @param array $item the new item record
+ * @param array $importer Record of the importer user mixed with contact of the content
+ * @param int $posted_id The record number of item record that was just posted
+ */
+ private function do_poke($item, $importer, $posted_id) {
+ $verb = urldecode(substr($item["verb"],strpos($item["verb"], "#")+1));
+ if(!$verb)
+ return;
+ $xo = parse_xml_string($item["object"],false);
+
+ if(($xo->type == ACTIVITY_OBJ_PERSON) && ($xo->id)) {
+
+ // somebody was poked/prodded. Was it me?
+ foreach($xo->link as $l) {
+ $atts = $l->attributes();
+ switch($atts["rel"]) {
+ case "alternate":
+ $Blink = $atts["href"];
+ break;
+ default:
+ break;
+ }
+ }
+
+ if($Blink && link_compare($Blink,App::get_baseurl()."/profile/".$importer["nickname"])) {
+
+ // send a notification
+ notification(array(
+ "type" => NOTIFY_POKE,
+ "notify_flags" => $importer["notify-flags"],
+ "language" => $importer["language"],
+ "to_name" => $importer["username"],
+ "to_email" => $importer["email"],
+ "uid" => $importer["importer_uid"],
+ "item" => $item,
+ "link" => App::get_baseurl()."/display/".urlencode(get_item_guid($posted_id)),
+ "source_name" => stripslashes($item["author-name"]),
+ "source_link" => $item["author-link"],
+ "source_photo" => ((link_compare($item["author-link"],$importer["url"]))
+ ? $importer["thumb"] : $item["author-avatar"]),
+ "verb" => $item["verb"],
+ "otype" => "person",
+ "activity" => $verb,
+ "parent" => $item["parent"]
+ ));
+ }
+ }
+ }
+
+ /**
+ * @brief Processes several actions, depending on the verb
+ *
+ * @param int $entrytype Is it a toplevel entry, a comment or a relayed comment?
+ * @param array $importer Record of the importer user mixed with contact of the content
+ * @param array $item the new item record
+ * @param bool $is_like Is the verb a "like"?
+ *
+ * @return bool Should the processing of the entries be continued?
+ */
+ private function process_verbs($entrytype, $importer, &$item, &$is_like) {
+
+ logger("Process verb ".$item["verb"]." and object-type ".$item["object-type"]." for entrytype ".$entrytype, LOGGER_DEBUG);
+
+ if (($entrytype == DFRN_TOP_LEVEL)) {
+ // The filling of the the "contact" variable is done for legcy reasons
+ // The functions below are partly used by ostatus.php as well - where we have this variable
+ $r = q("SELECT * FROM `contact` WHERE `id` = %d", intval($importer["id"]));
+ $contact = $r[0];
+ $nickname = $contact["nick"];
+
+ // Big question: Do we need these functions? They were part of the "consume_feed" function.
+ // This function once was responsible for DFRN and OStatus.
+ if(activity_match($item["verb"],ACTIVITY_FOLLOW)) {
+ logger("New follower");
+ new_follower($importer, $contact, $item, $nickname);
+ return false;
+ }
+ if(activity_match($item["verb"],ACTIVITY_UNFOLLOW)) {
+ logger("Lost follower");
+ lose_follower($importer, $contact, $item);
+ return false;
+ }
+ if(activity_match($item["verb"],ACTIVITY_REQ_FRIEND)) {
+ logger("New friend request");
+ new_follower($importer, $contact, $item, $nickname, true);
+ return false;
+ }
+ if(activity_match($item["verb"],ACTIVITY_UNFRIEND)) {
+ logger("Lost sharer");
+ lose_sharer($importer, $contact, $item);
+ return false;
+ }
+ } else {
+ if(($item["verb"] == ACTIVITY_LIKE)
+ || ($item["verb"] == ACTIVITY_DISLIKE)
+ || ($item["verb"] == ACTIVITY_ATTEND)
+ || ($item["verb"] == ACTIVITY_ATTENDNO)
+ || ($item["verb"] == ACTIVITY_ATTENDMAYBE)) {
+ $is_like = true;
+ $item["type"] = "activity";
+ $item["gravity"] = GRAVITY_LIKE;
+ // only one like or dislike per person
+ // splitted into two queries for performance issues
+ $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",
+ intval($item["uid"]),
+ dbesc($item["author-link"]),
+ dbesc($item["verb"]),
+ dbesc($item["parent-uri"])
+ );
+ if($r && count($r))
+ return false;
+
+ $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",
+ intval($item["uid"]),
+ dbesc($item["author-link"]),
+ dbesc($item["verb"]),
+ dbesc($item["parent-uri"])
+ );
+ if($r && count($r))
+ return false;
+ } else
+ $is_like = false;
+
+ if(($item["verb"] == ACTIVITY_TAG) && ($item["object-type"] == ACTIVITY_OBJ_TAGTERM)) {
+
+ $xo = parse_xml_string($item["object"],false);
+ $xt = parse_xml_string($item["target"],false);
+
+ if($xt->type == ACTIVITY_OBJ_NOTE) {
+ $r = q("SELECT `id`, `tag` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
+ dbesc($xt->id),
+ intval($importer["importer_uid"])
+ );
+
+ if(!count($r))
+ return false;
+
+ // extract tag, if not duplicate, add to parent item
+ if($xo->content) {
+ if(!(stristr($r[0]["tag"],trim($xo->content)))) {
+ q("UPDATE `item` SET `tag` = '%s' WHERE `id` = %d",
+ dbesc($r[0]["tag"] . (strlen($r[0]["tag"]) ? ',' : '') . '#[url=' . $xo->id . ']'. $xo->content . '[/url]'),
+ intval($r[0]["id"])
+ );
+ create_tags_from_item($r[0]["id"]);
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @brief Processes the link elements
+ *
+ * @param object $links link elements
+ * @param array $item the item record
+ */
+ private function parse_links($links, &$item) {
+ $rel = "";
+ $href = "";
+ $type = "";
+ $length = "0";
+ $title = "";
+ foreach ($links AS $link) {
+ foreach($link->attributes AS $attributes) {
+ if ($attributes->name == "href")
+ $href = $attributes->textContent;
+ if ($attributes->name == "rel")
+ $rel = $attributes->textContent;
+ if ($attributes->name == "type")
+ $type = $attributes->textContent;
+ if ($attributes->name == "length")
+ $length = $attributes->textContent;
+ if ($attributes->name == "title")
+ $title = $attributes->textContent;
+ }
+ if (($rel != "") AND ($href != ""))
+ switch($rel) {
+ case "alternate":
+ $item["plink"] = $href;
+ break;
+ case "enclosure":
+ $enclosure = $href;
+ if(strlen($item["attach"]))
+ $item["attach"] .= ",";
+
+ $item["attach"] .= '[attach]href="'.$href.'" length="'.$length.'" type="'.$type.'" title="'.$title.'"[/attach]';
+ break;
+ }
+ }
+ }
+
+ /**
+ * @brief Processes the entry elements which contain the items and comments
+ *
+ * @param array $header Array of the header elements that always stay the same
+ * @param object $xpath XPath object
+ * @param object $entry entry elements
+ * @param array $importer Record of the importer user mixed with contact of the content
+ */
+ private function process_entry($header, $xpath, $entry, $importer) {
+
+ logger("Processing entries");
+
+ $item = $header;
+
+ // Get the uri
+ $item["uri"] = $xpath->query("atom:id/text()", $entry)->item(0)->nodeValue;
+
+ // Fetch the owner
+ $owner = self::fetchauthor($xpath, $entry, $importer, "dfrn:owner", true);
+
+ $item["owner-name"] = $owner["name"];
+ $item["owner-link"] = $owner["link"];
+ $item["owner-avatar"] = $owner["avatar"];
+
+ // fetch the author
+ $author = self::fetchauthor($xpath, $entry, $importer, "atom:author", true);
+
+ $item["author-name"] = $author["name"];
+ $item["author-link"] = $author["link"];
+ $item["author-avatar"] = $author["avatar"];
+
+ $item["title"] = $xpath->query("atom:title/text()", $entry)->item(0)->nodeValue;
+
+ $item["created"] = $xpath->query("atom:published/text()", $entry)->item(0)->nodeValue;
+ $item["edited"] = $xpath->query("atom:updated/text()", $entry)->item(0)->nodeValue;
+
+ $item["body"] = $xpath->query("dfrn:env/text()", $entry)->item(0)->nodeValue;
+ $item["body"] = str_replace(array(' ',"\t","\r","\n"), array('','','',''),$item["body"]);
+ // make sure nobody is trying to sneak some html tags by us
+ $item["body"] = notags(base64url_decode($item["body"]));
+
+ $item["body"] = limit_body_size($item["body"]);
+
+ /// @todo Do we really need this check for HTML elements? (It was copied from the old function)
+ if((strpos($item['body'],'<') !== false) && (strpos($item['body'],'>') !== false)) {
+
+ $item['body'] = reltoabs($item['body'],$base_url);
+
+ $item['body'] = html2bb_video($item['body']);
+
+ $item['body'] = oembed_html2bbcode($item['body']);
+
+ $config = HTMLPurifier_Config::createDefault();
+ $config->set('Cache.DefinitionImpl', null);
+
+ // we shouldn't need a whitelist, because the bbcode converter
+ // will strip out any unsupported tags.
+
+ $purifier = new HTMLPurifier($config);
+ $item['body'] = $purifier->purify($item['body']);
+
+ $item['body'] = @html2bbcode($item['body']);
+ }
+
+ /// @todo We should check for a repeated post and if we know the repeated author.
+
+ // We don't need the content element since "dfrn:env" is always present
+ //$item["body"] = $xpath->query("atom:content/text()", $entry)->item(0)->nodeValue;
+
+ $item["last-child"] = $xpath->query("dfrn:comment-allow/text()", $entry)->item(0)->nodeValue;
+ $item["location"] = $xpath->query("dfrn:location/text()", $entry)->item(0)->nodeValue;
+
+ $georsspoint = $xpath->query("georss:point", $entry);
+ if ($georsspoint)
+ $item["coord"] = $georsspoint->item(0)->nodeValue;
+
+ $item["private"] = $xpath->query("dfrn:private/text()", $entry)->item(0)->nodeValue;
+
+ $item["extid"] = $xpath->query("dfrn:extid/text()", $entry)->item(0)->nodeValue;
+
+ if ($xpath->query("dfrn:bookmark/text()", $entry)->item(0)->nodeValue == "true")
+ $item["bookmark"] = true;
+
+ $notice_info = $xpath->query("statusnet:notice_info", $entry);
+ if ($notice_info AND ($notice_info->length > 0)) {
+ foreach($notice_info->item(0)->attributes AS $attributes) {
+ if ($attributes->name == "source")
+ $item["app"] = strip_tags($attributes->textContent);
+ }
+ }
+
+ $item["guid"] = $xpath->query("dfrn:diaspora_guid/text()", $entry)->item(0)->nodeValue;
+
+ // We store the data from "dfrn:diaspora_signature" in a different table, this is done in "item_store"
+ $dsprsig = unxmlify($xpath->query("dfrn:diaspora_signature/text()", $entry)->item(0)->nodeValue);
+ if ($dsprsig != "")
+ $item["dsprsig"] = $dsprsig;
+
+ $item["verb"] = $xpath->query("activity:verb/text()", $entry)->item(0)->nodeValue;
+
+ if ($xpath->query("activity:object-type/text()", $entry)->item(0)->nodeValue != "")
+ $item["object-type"] = $xpath->query("activity:object-type/text()", $entry)->item(0)->nodeValue;
+
+ $object = $xpath->query("activity:object", $entry)->item(0);
+ $item["object"] = self::transform_activity($xpath, $object, "object");
+
+ if (trim($item["object"]) != "") {
+ $r = parse_xml_string($item["object"], false);
+ if (isset($r->type))
+ $item["object-type"] = $r->type;
+ }
+
+ $target = $xpath->query("activity:target", $entry)->item(0);
+ $item["target"] = self::transform_activity($xpath, $target, "target");
+
+ $categories = $xpath->query("atom:category", $entry);
+ if ($categories) {
+ foreach ($categories AS $category) {
+ $term = "";
+ $scheme = "";
+ foreach($category->attributes AS $attributes) {
+ if ($attributes->name == "term")
+ $term = $attributes->textContent;
+
+ if ($attributes->name == "scheme")
+ $scheme = $attributes->textContent;
+ }
+
+ if (($term != "") AND ($scheme != "")) {
+ $parts = explode(":", $scheme);
+ if ((count($parts) >= 4) AND (array_shift($parts) == "X-DFRN")) {
+ $termhash = array_shift($parts);
+ $termurl = implode(":", $parts);
+
+ if(strlen($item["tag"]))
+ $item["tag"] .= ",";
+
+ $item["tag"] .= $termhash."[url=".$termurl."]".$term."[/url]";
+ }
+ }
+ }
+ }
+
+ $enclosure = "";
+
+ $links = $xpath->query("atom:link", $entry);
+ if ($links)
+ self::parse_links($links, $item);
+
+ // Is it a reply or a top level posting?
+ $item["parent-uri"] = $item["uri"];
+
+ $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;
+
+ // Get the type of the item (Top level post, reply or remote reply)
+ $entrytype = self::get_entry_type($importer, $item);
+
+ // Now assign the rest of the values that depend on the type of the message
+ if (in_array($entrytype, array(DFRN_REPLY, DFRN_REPLY_RC))) {
+ if (!isset($item["object-type"]))
+ $item["object-type"] = ACTIVITY_OBJ_COMMENT;
+
+ if ($item["contact-id"] != $owner["contact-id"])
+ $item["contact-id"] = $owner["contact-id"];
+
+ if (($item["network"] != $owner["network"]) AND ($owner["network"] != ""))
+ $item["network"] = $owner["network"];
+
+ if ($item["contact-id"] != $author["contact-id"])
+ $item["contact-id"] = $author["contact-id"];
+
+ if (($item["network"] != $author["network"]) AND ($author["network"] != ""))
+ $item["network"] = $author["network"];
+
+ // This code was taken from the old DFRN code
+ // When activated, forums don't work.
+ // And: Why should we disallow commenting by followers?
+ // the behaviour is now similar to the Diaspora part.
+ //if($importer["rel"] == CONTACT_IS_FOLLOWER) {
+ // logger("Contact ".$importer["id"]." is only follower. Quitting", LOGGER_DEBUG);
+ // return;
+ //}
+ }
+
+ if ($entrytype == DFRN_REPLY_RC) {
+ $item["type"] = "remote-comment";
+ $item["wall"] = 1;
+ } elseif ($entrytype == DFRN_TOP_LEVEL) {
+ if (!isset($item["object-type"]))
+ $item["object-type"] = ACTIVITY_OBJ_NOTE;
+
+ // Is it an event?
+ if ($item["object-type"] == ACTIVITY_OBJ_EVENT) {
+ logger("Item ".$item["uri"]." seems to contain an event.", LOGGER_DEBUG);
+ $ev = bbtoevent($item["body"]);
+ if((x($ev, "desc") || x($ev, "summary")) && x($ev, "start")) {
+ logger("Event in item ".$item["uri"]." was found.", LOGGER_DEBUG);
+ $ev["cid"] = $importer["id"];
+ $ev["uid"] = $importer["uid"];
+ $ev["uri"] = $item["uri"];
+ $ev["edited"] = $item["edited"];
+ $ev['private'] = $item['private'];
+ $ev["guid"] = $item["guid"];
+
+ $r = q("SELECT `id` FROM `event` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
+ dbesc($item["uri"]),
+ intval($importer["uid"])
+ );
+ if(count($r))
+ $ev["id"] = $r[0]["id"];
+
+ $event_id = event_store($ev);
+ logger("Event ".$event_id." was stored", LOGGER_DEBUG);
+ return;
+ }
+ }
+ }
+
+ $r = q("SELECT `id`, `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
+ dbesc($item["uri"]),
+ intval($importer["importer_uid"])
+ );
+
+ if (!self::process_verbs($entrytype, $importer, $item, $is_like)) {
+ logger("Exiting because 'process_verbs' told us so", LOGGER_DEBUG);
+ return;
+ }
+
+ // Update content if 'updated' changes
+ if(count($r)) {
+ if (self::update_content($r[0], $item, $importer, $entrytype))
+ logger("Item ".$item["uri"]." was updated.", LOGGER_DEBUG);
+ else
+ logger("Item ".$item["uri"]." already existed.", LOGGER_DEBUG);
+ return;
+ }
+
+ if (in_array($entrytype, array(DFRN_REPLY, DFRN_REPLY_RC))) {
+ $posted_id = item_store($item);
+ $parent = 0;
+
+ if($posted_id) {
+
+ logger("Reply from contact ".$item["contact-id"]." was stored with id ".$posted_id, LOGGER_DEBUG);
+
+ $item["id"] = $posted_id;
+
+ $r = q("SELECT `parent`, `parent-uri` FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1",
+ intval($posted_id),
+ intval($importer["importer_uid"])
+ );
+ if(count($r)) {
+ $parent = $r[0]["parent"];
+ $parent_uri = $r[0]["parent-uri"];
+ }
+
+ if(!$is_like) {
+ $r1 = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `uid` = %d AND `parent` = %d",
+ dbesc(datetime_convert()),
+ intval($importer["importer_uid"]),
+ intval($r[0]["parent"])
+ );
+
+ $r2 = q("UPDATE `item` SET `last-child` = 1, `changed` = '%s' WHERE `uid` = %d AND `id` = %d",
+ dbesc(datetime_convert()),
+ intval($importer["importer_uid"]),
+ intval($posted_id)
+ );
+ }
+
+ if($posted_id AND $parent AND ($entrytype == DFRN_REPLY_RC)) {
+ logger("Notifying followers about comment ".$posted_id, LOGGER_DEBUG);
+ proc_run("php", "include/notifier.php", "comment-import", $posted_id);
+ }
+
+ return true;
+ }
+ } else { // $entrytype == DFRN_TOP_LEVEL
+ if(!link_compare($item["owner-link"],$importer["url"])) {
+ // The item owner info is not our contact. It's OK and is to be expected if this is a tgroup delivery,
+ // but otherwise there's a possible data mixup on the sender's system.
+ // the tgroup delivery code called from item_store will correct it if it's a forum,
+ // but we're going to unconditionally correct it here so that the post will always be owned by our contact.
+ logger('Correcting item owner.', LOGGER_DEBUG);
+ $item["owner-name"] = $importer["senderName"];
+ $item["owner-link"] = $importer["url"];
+ $item["owner-avatar"] = $importer["thumb"];
+ }
+
+ if(($importer["rel"] == CONTACT_IS_FOLLOWER) && (!tgroup_check($importer["importer_uid"], $item))) {
+ logger("Contact ".$importer["id"]." is only follower and tgroup check was negative.", LOGGER_DEBUG);
+ return;
+ }
+
+ // This is my contact on another system, but it's really me.
+ // Turn this into a wall post.
+ $notify = item_is_remote_self($importer, $item);
+
+ $posted_id = item_store($item, false, $notify);
+
+ logger("Item was stored with id ".$posted_id, LOGGER_DEBUG);
+
+ if(stristr($item["verb"],ACTIVITY_POKE))
+ self::do_poke($item, $importer, $posted_id);
+ }
+ }
+
+ /**
+ * @brief Deletes items
+ *
+ * @param object $xpath XPath object
+ * @param object $deletion deletion elements
+ * @param array $importer Record of the importer user mixed with contact of the content
+ */
+ private function process_deletion($xpath, $deletion, $importer) {
+
+ logger("Processing deletions");
+
+ foreach($deletion->attributes AS $attributes) {
+ if ($attributes->name == "ref")
+ $uri = $attributes->textContent;
+ if ($attributes->name == "when")
+ $when = $attributes->textContent;
+ }
+ if ($when)
+ $when = datetime_convert("UTC", "UTC", $when, "Y-m-d H:i:s");