+
+ /******************************************************************************************
+ * Here are all the functions that are needed to transmit data with the Diaspora protocol *
+ ******************************************************************************************/
+
+ private function my_handle($me) {
+ if ($contact["addr"] != "")
+ return $contact["addr"];
+
+ // Normally we should have a filled "addr" field - but in the past this wasn't the case
+ // So - just in case - we build the the address here.
+ return $me["nickname"]."@".substr(App::get_baseurl(), strpos(App::get_baseurl(),"://") + 3);
+ }
+
+ private function build_public_message($msg, $user, $contact, $prvkey, $pubkey) {
+
+ logger("Message: ".$msg, LOGGER_DATA);
+
+ $handle = self::my_handle($user);
+
+ $b64url_data = base64url_encode($msg);
+
+ $data = str_replace(array("\n", "\r", " ", "\t"), array("", "", "", ""), $b64url_data);
+
+ $type = "application/xml";
+ $encoding = "base64url";
+ $alg = "RSA-SHA256";
+
+ $signable_data = $data.".".base64url_encode($type).".".base64url_encode($encoding).".".base64url_encode($alg);
+
+ $signature = rsa_sign($signable_data,$prvkey);
+ $sig = base64url_encode($signature);
+
+ $xmldata = array("diaspora" => array("header" => array("author_id" => $handle),
+ "me:env" => array("me:encoding" => "base64url",
+ "me:alg" => "RSA-SHA256",
+ "me:data" => $data,
+ "@attributes" => array("type" => "application/xml"),
+ "me:sig" => $sig)));
+
+ $namespaces = array("" => "https://joindiaspora.com/protocol",
+ "me" => "http://salmon-protocol.org/ns/magic-env");
+
+ $magic_env = xml::from_array($xmldata, $xml, false, $namespaces);
+
+ logger("magic_env: ".$magic_env, LOGGER_DATA);
+ return $magic_env;
+ }
+
+ private function build_private_message($msg, $user, $contact, $prvkey, $pubkey) {
+
+ logger("Message: ".$msg, LOGGER_DATA);
+
+ // without a public key nothing will work
+
+ if (!$pubkey) {
+ logger("pubkey missing: contact id: ".$contact["id"]);
+ return false;
+ }
+
+ $inner_aes_key = random_string(32);
+ $b_inner_aes_key = base64_encode($inner_aes_key);
+ $inner_iv = random_string(16);
+ $b_inner_iv = base64_encode($inner_iv);
+
+ $outer_aes_key = random_string(32);
+ $b_outer_aes_key = base64_encode($outer_aes_key);
+ $outer_iv = random_string(16);
+ $b_outer_iv = base64_encode($outer_iv);
+
+ $handle = self::my_handle($user);
+
+ $padded_data = pkcs5_pad($msg,16);
+ $inner_encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $inner_aes_key, $padded_data, MCRYPT_MODE_CBC, $inner_iv);
+
+ $b64_data = base64_encode($inner_encrypted);
+
+
+ $b64url_data = base64url_encode($b64_data);
+ $data = str_replace(array("\n", "\r", " ", "\t"), array("", "", "", ""), $b64url_data);
+
+ $type = "application/xml";
+ $encoding = "base64url";
+ $alg = "RSA-SHA256";
+
+ $signable_data = $data.".".base64url_encode($type).".".base64url_encode($encoding).".".base64url_encode($alg);
+
+ $signature = rsa_sign($signable_data,$prvkey);
+ $sig = base64url_encode($signature);
+
+ $xmldata = array("decrypted_header" => array("iv" => $b_inner_iv,
+ "aes_key" => $b_inner_aes_key,
+ "author_id" => $handle));
+
+ $decrypted_header = xml::from_array($xmldata, $xml, true);
+ $decrypted_header = pkcs5_pad($decrypted_header,16);
+
+ $ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $outer_aes_key, $decrypted_header, MCRYPT_MODE_CBC, $outer_iv);
+
+ $outer_json = json_encode(array("iv" => $b_outer_iv, "key" => $b_outer_aes_key));
+
+ $encrypted_outer_key_bundle = "";
+ openssl_public_encrypt($outer_json, $encrypted_outer_key_bundle, $pubkey);
+
+ $b64_encrypted_outer_key_bundle = base64_encode($encrypted_outer_key_bundle);
+
+ logger("outer_bundle: ".$b64_encrypted_outer_key_bundle." key: ".$pubkey, LOGGER_DATA);
+
+ $encrypted_header_json_object = json_encode(array("aes_key" => base64_encode($encrypted_outer_key_bundle),
+ "ciphertext" => base64_encode($ciphertext)));
+ $cipher_json = base64_encode($encrypted_header_json_object);
+
+ $xmldata = array("diaspora" => array("encrypted_header" => $cipher_json,
+ "me:env" => array("me:encoding" => "base64url",
+ "me:alg" => "RSA-SHA256",
+ "me:data" => $data,
+ "@attributes" => array("type" => "application/xml"),
+ "me:sig" => $sig)));
+
+ $namespaces = array("" => "https://joindiaspora.com/protocol",
+ "me" => "http://salmon-protocol.org/ns/magic-env");
+
+ $magic_env = xml::from_array($xmldata, $xml, false, $namespaces);
+
+ logger("magic_env: ".$magic_env, LOGGER_DATA);
+ return $magic_env;
+ }
+
+ private function build_message($msg, $user, $contact, $prvkey, $pubkey, $public = false) {
+
+ if ($public)
+ $magic_env = self::build_public_message($msg,$user,$contact,$prvkey,$pubkey);
+ else
+ $magic_env = self::build_private_message($msg,$user,$contact,$prvkey,$pubkey);
+
+ // The data that will be transmitted is double encoded via "urlencode", strange ...
+ $slap = "xml=".urlencode(urlencode($magic_env));
+ return $slap;
+ }
+
+ private function signature($owner, $message) {
+ $sigmsg = $message;
+ unset($sigmsg["author_signature"]);
+ unset($sigmsg["parent_author_signature"]);
+
+ $signed_text = implode(";", $sigmsg);
+
+ return base64_encode(rsa_sign($signed_text, $owner["uprvkey"], "sha256"));
+ }
+
+ public static function transmit($owner, $contact, $slap, $public_batch, $queue_run=false, $guid = "") {
+
+ $a = get_app();
+
+ $enabled = intval(get_config("system", "diaspora_enabled"));
+ if(!$enabled)
+ return 200;
+
+ $logid = random_string(4);
+ $dest_url = (($public_batch) ? $contact["batch"] : $contact["notify"]);
+ if (!$dest_url) {
+ logger("no url for contact: ".$contact["id"]." batch mode =".$public_batch);
+ return 0;
+ }
+
+ logger("transmit: ".$logid."-".$guid." ".$dest_url);
+
+ if (!$queue_run && was_recently_delayed($contact["id"])) {
+ $return_code = 0;
+ } else {
+ if (!intval(get_config("system", "diaspora_test"))) {
+ post_url($dest_url."/", $slap);
+ $return_code = $a->get_curl_code();
+ } else {
+ logger("test_mode");
+ return 200;
+ }
+ }
+
+ logger("transmit: ".$logid."-".$guid." returns: ".$return_code);
+
+ if(!$return_code || (($return_code == 503) && (stristr($a->get_curl_headers(), "retry-after")))) {
+ logger("queue message");
+
+ $r = q("SELECT `id` FROM `queue` WHERE `cid` = %d AND `network` = '%s' AND `content` = '%s' AND `batch` = %d LIMIT 1",
+ intval($contact["id"]),
+ dbesc(NETWORK_DIASPORA),
+ dbesc($slap),
+ intval($public_batch)
+ );
+ if($r) {
+ logger("add_to_queue ignored - identical item already in queue");
+ } else {
+ // queue message for redelivery
+ add_to_queue($contact["id"], NETWORK_DIASPORA, $slap, $public_batch);
+ }
+ }
+
+ return(($return_code) ? $return_code : (-1));
+ }
+
+
+ private function build_and_transmit($owner, $contact, $type, $message, $public_batch = false, $guid = "") {
+
+ $data = array("XML" => array("post" => array($type => $message)));
+
+ $msg = xml::from_array($data, $xml);
+
+ logger('message: '.$msg, LOGGER_DATA);
+ logger('send guid '.$guid, LOGGER_DEBUG);
+
+ $slap = self::build_message($msg, $owner, $contact, $owner['uprvkey'], $contact['pubkey'], $public_batch);
+
+ $return_code = self::transmit($owner, $contact, $slap, $public_batch, false, $guid);
+
+ logger("guid: ".$item["guid"]." result ".$return_code, LOGGER_DEBUG);
+
+ return $return_code;
+ }
+
+ public static function send_share($owner,$contact) {
+
+ $message = array("sender_handle" => self::my_handle($owner),
+ "recipient_handle" => $contact["addr"]);
+
+ return self::build_and_transmit($owner, $contact, "request", $message);
+ }
+
+ public static function send_unshare($owner,$contact) {
+
+ $message = array("post_guid" => $owner["guid"],
+ "diaspora_handle" => self::my_handle($owner),
+ "type" => "Person");
+
+ return self::build_and_transmit($owner, $contact, "retraction", $message);
+ }
+
+ private function is_reshare($body) {
+ $body = trim($body);
+
+ // Skip if it isn't a pure repeated messages
+ // Does it start with a share?
+ if (strpos($body, "[share") > 0)
+ return(false);
+
+ // Does it end with a share?
+ if (strlen($body) > (strrpos($body, "[/share]") + 8))
+ return(false);
+
+ $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body);
+ // Skip if there is no shared message in there
+ if ($body == $attributes)
+ return(false);
+
+ $guid = "";
+ preg_match("/guid='(.*?)'/ism", $attributes, $matches);
+ if ($matches[1] != "")
+ $guid = $matches[1];
+
+ preg_match('/guid="(.*?)"/ism', $attributes, $matches);
+ if ($matches[1] != "")
+ $guid = $matches[1];
+
+ if ($guid != "") {
+ $r = q("SELECT `contact-id` FROM `item` WHERE `guid` = '%s' AND `network` IN ('%s', '%s') LIMIT 1",
+ dbesc($guid), NETWORK_DFRN, NETWORK_DIASPORA);
+ if ($r) {
+ $ret= array();
+ $ret["root_handle"] = self::handle_from_contact($r[0]["contact-id"]);
+ $ret["root_guid"] = $guid;
+ return($ret);
+ }
+ }
+
+ $profile = "";
+ preg_match("/profile='(.*?)'/ism", $attributes, $matches);
+ if ($matches[1] != "")
+ $profile = $matches[1];
+
+ preg_match('/profile="(.*?)"/ism', $attributes, $matches);
+ if ($matches[1] != "")
+ $profile = $matches[1];
+
+ $ret= array();
+
+ $ret["root_handle"] = preg_replace("=https?://(.*)/u/(.*)=ism", "$2@$1", $profile);
+ if (($ret["root_handle"] == $profile) OR ($ret["root_handle"] == ""))
+ return(false);
+
+ $link = "";
+ preg_match("/link='(.*?)'/ism", $attributes, $matches);
+ if ($matches[1] != "")
+ $link = $matches[1];
+
+ preg_match('/link="(.*?)"/ism', $attributes, $matches);
+ if ($matches[1] != "")
+ $link = $matches[1];
+
+ $ret["root_guid"] = preg_replace("=https?://(.*)/posts/(.*)=ism", "$2", $link);
+ if (($ret["root_guid"] == $link) OR ($ret["root_guid"] == ""))
+ return(false);
+ return($ret);
+ }
+
+ public static function send_status($item, $owner, $contact, $public_batch = false) {
+
+ $myaddr = self::my_handle($owner);
+
+ $public = (($item["private"]) ? "false" : "true");
+
+ $created = datetime_convert("UTC", "UTC", $item["created"], 'Y-m-d H:i:s \U\T\C');
+
+ // Detect a share element and do a reshare
+ if (!$item['private'] AND ($ret = self::is_reshare($item["body"]))) {
+ $message = array("root_diaspora_id" => $ret["root_handle"],
+ "root_guid" => $ret["root_guid"],
+ "guid" => $item["guid"],
+ "diaspora_handle" => $myaddr,
+ "public" => $public,
+ "created_at" => $created,
+ "provider_display_name" => $item["app"]);
+
+ $type = "reshare";
+ } else {
+ $title = $item["title"];
+ $body = $item["body"];
+
+ // convert to markdown
+ $body = html_entity_decode(bb2diaspora($body));
+
+ // Adding the title
+ if(strlen($title))
+ $body = "## ".html_entity_decode($title)."\n\n".$body;
+
+ if ($item["attach"]) {
+ $cnt = preg_match_all('/href=\"(.*?)\"(.*?)title=\"(.*?)\"/ism', $item["attach"], $matches, PREG_SET_ORDER);
+ if(cnt) {
+ $body .= "\n".t("Attachments:")."\n";
+ foreach($matches as $mtch)
+ $body .= "[".$mtch[3]."](".$mtch[1].")\n";
+ }
+ }
+
+ $location = array();
+
+ if ($item["location"] != "")
+ $location["address"] = $item["location"];
+
+ if ($item["coord"] != "") {
+ $coord = explode(" ", $item["coord"]);
+ $location["lat"] = $coord[0];
+ $location["lng"] = $coord[1];
+ }
+
+ $message = array("raw_message" => $body,
+ "location" => $location,
+ "guid" => $item["guid"],
+ "diaspora_handle" => $myaddr,
+ "public" => $public,
+ "created_at" => $created,
+ "provider_display_name" => $item["app"]);
+
+ if (count($location) == 0)
+ unset($message["location"]);
+
+ $type = "status_message";
+ }
+
+ return self::build_and_transmit($owner, $contact, $type, $message, $public_batch, $item["guid"]);
+ }
+
+ private function construct_like($item, $owner) {
+
+ $myaddr = self::my_handle($owner);
+
+ $p = q("SELECT `guid`, `uri`, `parent-uri` FROM `item` WHERE `uri` = '%s' LIMIT 1",
+ dbesc($item["thr-parent"]));
+ if(!$p)
+ return false;
+
+ $parent = $p[0];
+
+ $target_type = ($parent["uri"] === $parent["parent-uri"] ? "Post" : "Comment");
+ $positive = "true";
+
+ return(array("positive" => $positive,
+ "guid" => $item["guid"],
+ "target_type" => $target_type,
+ "parent_guid" => $parent["guid"],
+ "author_signature" => $authorsig,
+ "diaspora_handle" => $myaddr));
+ }
+
+ private function construct_comment($item, $owner) {
+
+ $myaddr = self::my_handle($owner);
+
+ $p = q("SELECT `guid` FROM `item` WHERE `parent` = %d AND `id` = %d LIMIT 1",
+ intval($item["parent"]),
+ intval($item["parent"])
+ );
+
+ if (!$p)
+ return false;
+
+ $parent = $p[0];
+
+ $text = html_entity_decode(bb2diaspora($item["body"]));
+
+ return(array("guid" => $item["guid"],
+ "parent_guid" => $parent["guid"],
+ "author_signature" => "",
+ "text" => $text,
+ "diaspora_handle" => $myaddr));
+ }
+
+ public static function send_followup($item,$owner,$contact,$public_batch = false) {
+
+ if($item['verb'] === ACTIVITY_LIKE) {
+ $message = self::construct_like($item, $owner);
+ $type = "like";
+ } else {
+ $message = self::construct_comment($item, $owner);
+ $type = "comment";
+ }
+
+ if (!$message)
+ return false;
+
+ $message["author_signature"] = self::signature($owner, $message);
+
+ return self::build_and_transmit($owner, $contact, $type, $message, $public_batch, $item["guid"]);
+ }
+
+ private function message_from_signatur($item, $signature) {
+
+ // Split the signed text
+ $signed_parts = explode(";", $signature['signed_text']);
+
+ if ($item["deleted"])
+ $message = array("parent_author_signature" => "",
+ "target_guid" => $signed_parts[0],
+ "target_type" => $signed_parts[1],
+ "sender_handle" => $signature['signer'],
+ "target_author_signature" => $signature['signature']);
+ elseif ($item['verb'] === ACTIVITY_LIKE)
+ $message = array("positive" => $signed_parts[0],
+ "guid" => $signed_parts[1],
+ "target_type" => $signed_parts[2],
+ "parent_guid" => $signed_parts[3],
+ "parent_author_signature" => "",
+ "author_signature" => $signature['signature'],
+ "diaspora_handle" => $signed_parts[4]);
+ else {
+ // Remove the comment guid
+ $guid = array_shift($signed_parts);
+
+ // Remove the parent guid
+ $parent_guid = array_shift($signed_parts);
+
+ // Remove the handle
+ $handle = array_pop($signed_parts);
+
+ // Glue the parts together
+ $text = implode(";", $signed_parts);
+
+ $message = array("guid" => $guid,
+ "parent_guid" => $parent_guid,
+ "parent_author_signature" => "",
+ "author_signature" => $signature['signature'],
+ "text" => implode(";", $signed_parts),
+ "diaspora_handle" => $handle);
+ }
+ return $message;
+ }
+
+ public static function send_relay($item, $owner, $contact, $public_batch = false) {
+
+ if ($item["deleted"]) {
+ $sql_sign_id = "retract_iid";
+ $type = "relayable_retraction";
+ } elseif ($item['verb'] === ACTIVITY_LIKE) {
+ $sql_sign_id = "iid";
+ $type = "like";
+ } else {
+ $sql_sign_id = "iid";
+ $type = "comment";
+ }
+
+ // fetch the original signature
+
+ $r = q("SELECT `signed_text`, `signature`, `signer` FROM `sign` WHERE `".$sql_sign_id."` = %d LIMIT 1",
+ intval($item["id"]));
+
+ if (!$r)
+ return self::send_followup($item, $owner, $contact, $public_batch);
+
+ $signature = $r[0];
+
+ // Old way - is used by the internal Friendica functions
+ /// @todo Change all signatur storing functions to the new format
+ if ($signature['signed_text'] AND $signature['signature'] AND $signature['signer'])
+ $message = self::message_from_signatur($item, $signature);
+ else {// New way
+ $msg = json_decode($signature['signed_text'], true);
+
+ $message = array();
+ foreach ($msg AS $field => $data) {
+ if (!$item["deleted"]) {
+ if ($field == "author")
+ $field = "diaspora_handle";
+ if ($field == "parent_type")
+ $field = "target_type";
+ }
+
+ $message[$field] = $data;
+ }
+ }
+
+ if ($item["deleted"]) {
+ $signed_text = $message["target_guid"].';'.$message["target_type"];
+ $message["parent_author_signature"] = base64_encode(rsa_sign($signed_text, $owner["uprvkey"], "sha256"));
+ } else
+ $message["parent_author_signature"] = self::signature($owner, $message);
+
+ logger("Relayed data ".print_r($message, true), LOGGER_DEBUG);
+
+ return self::build_and_transmit($owner, $contact, $type, $message, $public_batch, $item["guid"]);
+ }
+
+ public static function send_retraction($item, $owner, $contact, $public_batch = false) {
+
+ $myaddr = self::my_handle($owner);
+
+ // Check whether the retraction is for a top-level post or whether it's a relayable
+ if ($item["uri"] !== $item["parent-uri"]) {
+ $msg_type = "relayable_retraction";
+ $target_type = (($item["verb"] === ACTIVITY_LIKE) ? "Like" : "Comment");
+ } else {
+ $msg_type = "signed_retraction";
+ $target_type = "StatusMessage";
+ }
+
+ $signed_text = $item["guid"].";".$target_type;
+
+ $message = array("target_guid" => $item['guid'],
+ "target_type" => $target_type,
+ "sender_handle" => $myaddr,
+ "target_author_signature" => base64_encode(rsa_sign($signed_text,$owner['uprvkey'],'sha256')));
+
+ return self::build_and_transmit($owner, $contact, $msg_type, $message, $public_batch, $item["guid"]);
+ }
+
+ public static function send_mail($item, $owner, $contact) {
+
+ $myaddr = self::my_handle($owner);
+
+ $r = q("SELECT * FROM `conv` WHERE `id` = %d AND `uid` = %d LIMIT 1",
+ intval($item["convid"]),
+ intval($item["uid"])
+ );
+
+ if (!$r) {
+ logger("conversation not found.");
+ return;
+ }
+ $cnv = $r[0];
+
+ $conv = array(
+ "guid" => $cnv["guid"],
+ "subject" => $cnv["subject"],
+ "created_at" => datetime_convert("UTC", "UTC", $cnv['created'], 'Y-m-d H:i:s \U\T\C'),
+ "diaspora_handle" => $cnv["creator"],
+ "participant_handles" => $cnv["recips"]
+ );
+
+ $body = bb2diaspora($item["body"]);
+ $created = datetime_convert("UTC", "UTC", $item["created"], 'Y-m-d H:i:s \U\T\C');
+
+ $signed_text = $item["guid"].";".$cnv["guid"].";".$body.";".$created.";".$myaddr.";".$cnv['guid'];
+ $sig = base64_encode(rsa_sign($signed_text, $owner["uprvkey"], "sha256"));
+
+ $msg = array(
+ "guid" => $item["guid"],
+ "parent_guid" => $cnv["guid"],
+ "parent_author_signature" => $sig,
+ "author_signature" => $sig,
+ "text" => $body,
+ "created_at" => $created,
+ "diaspora_handle" => $myaddr,
+ "conversation_guid" => $cnv["guid"]
+ );
+
+ if ($item["reply"]) {
+ $message = $msg;
+ $type = "message";
+ } else {
+ $message = array("guid" => $cnv["guid"],
+ "subject" => $cnv["subject"],
+ "created_at" => datetime_convert("UTC", "UTC", $cnv['created'], 'Y-m-d H:i:s \U\T\C'),
+ "message" => $msg,
+ "diaspora_handle" => $cnv["creator"],
+ "participant_handles" => $cnv["recips"]);
+
+ $type = "conversation";
+ }
+
+ return self::build_and_transmit($owner, $contact, $type, $message, false, $item["guid"]);
+ }