function diaspora_send_downstream($item, $owner, $contact, $public_batch = false) { $a = get_app(); $myaddr = channel_reddress($owner); $text = bb2diaspora_itembody($item); $parentauthorsig = ''; $body = $text; // Diaspora doesn't support threaded comments, but some // versions of Diaspora (i.e. Diaspora-pistos) support // likes on comments // That version is now dead so detect a "sublike" and // just send it as an activity. $sublike = false; if ($item['verb'] === ACTIVITY_LIKE) { if ($item['thr_parent'] && $item['thr_parent'] !== $item['parent_mid']) { $sublike = true; } } // The first item in the `item` table with the parent id is the parent. However, MySQL doesn't always // return the items ordered by `item`.`id`, in which case the wrong item is chosen as the parent. // The only item with `parent` and `id` as the parent id is the parent item. $p = q("select * from item where parent = %d and id = %d limit 1", intval($item['parent']), intval($item['parent'])); if ($p) { $parent = $p[0]; } else { logger('diaspora_send_downstream: no parent'); return; } $diaspora_fields = get_iconfig($item, 'diaspora', 'fields'); if ($diaspora_fields) { $xmlout = diaspora_fields_to_xml($diaspora_fields); $parentauthorsig = diaspora_sign_fields($diaspora_fields, $owner['channel_prvkey']); } $like = false; $relay_retract = false; $sql_sign_id = 'iid'; if (intval($item['item_deleted'])) { $relay_retract = true; $target_type = $item['verb'] === ACTIVITY_LIKE && !$sublike ? 'Like' : 'Comment'; $sql_sign_id = 'retract_iid'; $tpl = get_markup_template('diaspora_relayable_retraction.tpl', 'addon/diaspora'); } elseif ($item['verb'] === ACTIVITY_LIKE && !$sublike && $xmlout) { $like = true; $target_type = $parent['mid'] === $parent['parent_mid'] ? 'Post' : 'Comment'; // $positive = (intval($item['item_deleted']) ? 'false' : 'true'); $positive = 'true'; $tpl = get_markup_template('diaspora_like_relay.tpl', 'addon/diaspora'); } else { // item is a comment $tpl = get_markup_template('diaspora_comment_relay.tpl', 'addon/diaspora'); } $diaspora_meta = $item['diaspora_meta'] ? json_decode($item['diaspora_meta'], true) : ''; if ($diaspora_meta) { if (array_key_exists('iv', $diaspora_meta)) { $key = get_config('system', 'prvkey'); $meta = json_decode(crypto_unencapsulate($diaspora_meta, $key), true); } else { $meta = $diaspora_meta; } $sender_signed_text = $meta['signed_text']; $authorsig = $meta['signature']; $handle = $meta['signer']; $text = $meta['body']; } else { logger('diaspora_send_downstream: original author signature not found'); } /* Since the author signature is only checked by the parent, not by the relay recipients, * I think it may not be necessary for us to do so much work to preserve all the original * signatures. The important thing that Diaspora DOES need is the original creator's handle. * Let's just generate that and forget about all the original author signature stuff. * * Note: this might be more of an problem if we want to support likes on comments for older * versions of Diaspora (diaspora-pistos), but since there are a number of problems with * doing that, let's ignore it for now. * * */ // bug - nomadic identity may/will affect diaspora_handle_from_contact if (!$handle) { $handle = channel_reddress($owner); } if (!$sender_signed_text) { if ($relay_retract) { $sender_signed_text = $item['mid'] . ';' . $target_type; } elseif ($like) { $sender_signed_text = $positive . ';' . $item['mid'] . ';' . $target_type . ';' . $parent['mid'] . ';' . $handle; } else { $sender_signed_text = $item['mid'] . ';' . $parent['mid'] . ';' . $text . ';' . $handle; } } // The relayable may have arrived from somebody who provided no Diaspora Comment Virus. // We check for this above in bb2diaspora_itembody. In that case we will have generated // the body as a "wall-to-wall" post, and the author_signature will now be our own. if (!$xmlout && !$authorsig) { $authorsig = base64_encode(rsa_sign($sender_signed_text, $owner['channel_prvkey'], 'sha256')); } // Sign the relayable with the top-level owner's signature if (!$parentauthorsig) { $parentauthorsig = base64_encode(rsa_sign($sender_signed_text, $owner['channel_prvkey'], 'sha256')); } if (!$text) { logger('diaspora_send_downstream: no text'); } $msg = replace_macros($tpl, array('$xml' => $xmlout, '$guid' => xmlify($item['mid']), '$parent_guid' => xmlify($parent['mid']), '$target_type' => xmlify($target_type), '$authorsig' => xmlify($authorsig), '$parentsig' => xmlify($parentauthorsig), '$body' => xmlify($text), '$positive' => xmlify($positive), '$handle' => xmlify($handle))); logger('diaspora_send_downstream: base message: ' . $msg, LOGGER_DATA); $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg, $owner, $contact, $owner['channel_prvkey'], $contact['xchan_pubkey'], $public_batch))); return diaspora_queue($owner, $contact, $slap, $public_batch, $item['mid']); }
function diaspora_post_local(&$a, &$item) { /** * If all the conditions are met, generate an instance of the Diaspora Comment Virus * * Previously all comments from any Hubzilla source (including those who have not opted in to * Diaspora federation), were required to locally generate a Diaspora comment signature. * The only exception was wall-to-wall posts which have no local signing authority. * * Going forward, if we are asked to propagate the virus and it is not present (due to the post author * not opting in to Diaspora federation); we will generate a "wall-to-wall" comment and not require * a source signature. This allows hubs and communities to opt-out of Diaspora federation and not be * forced to generate the comment virus regardless. This is necessary because Diaspora now requires * the virus not just to provide a stored signature and Diaspora formatted text body, but must also * include all XML fields presented by the Diaspora protocol when transmitting the comment, while * maintaining their source order. This is fine for federated communities using UNO, but it makes * no sense to require this low-level baggage in channels and communities that have chosen not to use * the Diaspora protocol and services. * */ require_once 'include/bb2diaspora.php'; if ($item['mid'] === $item['parent_mid']) { return; } if ($item['created'] != $item['edited']) { return; } $meta = null; $author = channelx_by_hash($item['author_xchan']); if ($author) { // The author has a local channel, If they have this connector installed, // sign the comment and create a Diaspora Comment Virus. $dspr_allowed = get_pconfig($author['channel_id'], 'system', 'diaspora_allowed'); if (!$dspr_allowed) { return; } $handle = channel_reddress($author); if ($item['verb'] === ACTIVITY_LIKE) { if ($item['thr_parent'] == $item['parent_mid'] && $item['obj_type'] == ACTIVITY_OBJ_NOTE) { $meta = ['positive' => 'true', 'guid' => $item['mid'], 'target_type' => 'Post', 'parent_guid' => $item['parent_mid'], 'diaspora_handle' => $handle]; } } else { $body = bb2diaspora_itembody($item, true, true); $meta = ['guid' => $item['mid'], 'parent_guid' => $item['parent_mid'], 'text' => $body, 'diaspora_handle' => $handle]; } $meta['author_signature'] = diaspora_sign_fields($meta, $author['channel_prvkey']); if ($item['author_xchan'] === $item['owner_xchan']) { $meta['parent_author_signature'] = diaspora_sign_fields($meta, $author['channel_prvkey']); } } if (!$meta && $item['author_xchan'] !== $item['owner_xchan']) { // A local comment arrived but the commenter does not have a local channel // or the commenter doesn't have the Diaspora plugin enabled. // The owner *should* have a local channel // Find the owner and if the owner has this addon installed, turn the comment into // a 'wall-to-wall' message containing the author attribution, // with the comment signed by the owner. $owner = channelx_by_hash($item['owner_xchan']); if (!$owner) { return; } $dspr_allowed = get_pconfig($owner['channel_id'], 'system', 'diaspora_allowed'); if (!$dspr_allowed) { return; } $handle = channel_reddress($owner); if ($item['verb'] === ACTIVITY_LIKE) { if ($item['thr_parent'] == $item['parent_mid'] && $item['obj_type'] == ACTIVITY_OBJ_NOTE) { $meta = ['positive' => 'true', 'guid' => $item['mid'], 'target_type' => 'Post', 'parent_guid' => $item['parent_mid'], 'diaspora_handle' => $handle]; } } else { $body = bb2diaspora_itembody($item, true, false); $meta = ['guid' => $item['mid'], 'parent_guid' => $item['parent_mid'], 'text' => $body, 'diaspora_handle' => $handle]; } $meta['author_signature'] = diaspora_sign_fields($meta, $owner['channel_prvkey']); $meta['parent_author_signature'] = diaspora_sign_fields($meta, $owner['channel_prvkey']); } if ($meta) { set_iconfig($item, 'diaspora', 'fields', $meta, true); } // otherwise, neither the author or owner have this plugin installed. Do nothing. // logger('ditem: ' . print_r($item,true)); }
function diaspora_like($importer, $xml, $msg) { $a = get_app(); $guid = notags(unxmlify($xml['guid'])); $parent_guid = notags(unxmlify($xml['parent_guid'])); $diaspora_handle = notags(diaspora_get_author($xml)); $target_type = notags(diaspora_get_ptype($xml)); $positive = notags(unxmlify($xml['positive'])); $author_signature = notags(unxmlify($xml['author_signature'])); $parent_author_signature = $xml['parent_author_signature'] ? notags(unxmlify($xml['parent_author_signature'])) : ''; // likes on comments not supported here and likes on photos not supported by Diaspora // if($target_type !== 'Post') // return; $contact = diaspora_get_contact_by_handle($importer['channel_id'], $msg['author']); if (!$contact) { logger('diaspora_like: cannot find contact: ' . $msg['author'] . ' for channel ' . $importer['channel_name']); return; } if (!$importer['system'] && !perm_is_allowed($importer['channel_id'], $contact['xchan_hash'], 'post_comments')) { logger('diaspora_like: Ignoring this author.'); return 202; } $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `mid` = '%s' LIMIT 1", intval($importer['channel_id']), dbesc($parent_guid)); if (!count($r)) { logger('diaspora_like: parent item not found: ' . $guid); return; } $parent_item = $r[0]; $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `mid` = '%s' LIMIT 1", intval($importer['channel_id']), dbesc($guid)); if (count($r)) { if ($positive === 'true') { logger('diaspora_like: duplicate like: ' . $guid); return; } // Note: I don't think "Like" objects with positive = "false" are ever actually used // It looks like "RelayableRetractions" are used for "unlike" instead if ($positive === 'false') { logger('diaspora_like: received a like with positive set to "false"...ignoring'); // perhaps call drop_item() // FIXME--actually don't unless it turns out that Diaspora does indeed send out "false" likes // send notification via proc_run() return; } } $i = q("select * from xchan where xchan_hash = '%s' limit 1", dbesc($parent_item['author_xchan'])); if ($i) { $item_author = $i[0]; } // Note: I don't think "Like" objects with positive = "false" are ever actually used // It looks like "RelayableRetractions" are used for "unlike" instead if ($positive === 'false') { logger('diaspora_like: received a like with positive set to "false"'); logger('diaspora_like: unlike received with no corresponding like...ignoring'); return; } /* How Diaspora performs "like" signature checking: - If an item has been sent by the like author to the top-level post owner to relay on to the rest of the contacts on the top-level post, the top-level post owner should check the author_signature, then create a parent_author_signature before relaying the like on - If an item has been relayed on by the top-level post owner, the contacts who receive it check only the parent_author_signature. Basically, they trust that the top-level post owner has already verified the authenticity of anything he/she sends out - In either case, the signature that get checked is the signature created by the person who sent the salmon */ // 2014-09-10 let's try this: signatures are failing. I'll try and make a signable string from // the parameters in the order they were presented in the post. This is how D* creates the signable string. $signed_data = $positive . ';' . $guid . ';' . $target_type . ';' . $parent_guid . ';' . $diaspora_handle; $key = $msg['key']; if ($parent_author_signature) { // If a parent_author_signature exists, then we've received the like // relayed from the top-level post owner. There's no need to check the // author_signature if the parent_author_signature is valid $x = diaspora_verify_fields($xml, $parent_author_signature, $key); if (!$x) { logger('diaspora_like: top-level owner verification failed.'); return; } //$parent_author_signature = base64_decode($parent_author_signature); //if(! rsa_verify($signed_data,$parent_author_signature,$key,'sha256')) { // if (intval(get_config('system','ignore_diaspora_like_signature'))) // logger('diaspora_like: top-level owner verification failed. Proceeding anyway.'); // else { // logger('diaspora_like: top-level owner verification failed.'); // return; // } //} } else { // If there's no parent_author_signature, then we've received the like // from the like creator. In that case, the person is "like"ing // our post, so he/she must be a contact of ours and his/her public key // should be in $msg['key'] $x = diaspora_verify_fields($xml, $author_signature, $key); if (!$x) { logger('diaspora_like: author verification failed.'); return; } //$author_signature = base64_decode($author_signature); //if(! rsa_verify($signed_data,$author_signature,$key,'sha256')) { // if (intval(get_config('system','ignore_diaspora_like_signature'))) // logger('diaspora_like: like creator verification failed. Proceeding anyway'); // else { // logger('diaspora_like: like creator verification failed.'); // return; // } //} $xml['parent_author_signature'] = diaspora_sign_fields($xml, $importer['channel_prvkey']); } logger('diaspora_like: signature check complete.', LOGGER_DEBUG); // Phew! Everything checks out. Now create an item. // Find the original comment author information. // We need this to make sure we display the comment author // information (name and avatar) correctly. if (strcasecmp($diaspora_handle, $msg['author']) == 0) { $person = $contact; } else { $person = find_diaspora_person_by_handle($diaspora_handle); if (!is_array($person)) { logger('diaspora_like: unable to find author details'); return; } } $uri = $diaspora_handle . ':' . $guid; $activity = ACTIVITY_LIKE; $post_type = $parent_item['resource_type'] === 'photo' ? t('photo') : t('status'); $links = array(array('rel' => 'alternate', 'type' => 'text/html', 'href' => $parent_item['plink'])); $objtype = $parent_item['resource_type'] === 'photo' ? ACTIVITY_OBJ_PHOTO : ACTIVITY_OBJ_NOTE; $body = $parent_item['body']; $object = json_encode(array('type' => $post_type, 'id' => $parent_item['mid'], 'parent' => $parent_item['thr_parent'] ? $parent_item['thr_parent'] : $parent_item['parent_mid'], 'link' => $links, 'title' => $parent_item['title'], 'content' => $parent_item['body'], 'created' => $parent_item['created'], 'edited' => $parent_item['edited'], 'author' => array('name' => $item_author['xchan_name'], 'address' => $item_author['xchan_addr'], 'guid' => $item_author['xchan_guid'], 'guid_sig' => $item_author['xchan_guid_sig'], 'link' => array(array('rel' => 'alternate', 'type' => 'text/html', 'href' => $item_author['xchan_url']), array('rel' => 'photo', 'type' => $item_author['xchan_photo_mimetype'], 'href' => $item_author['xchan_photo_m']))))); $bodyverb = t('%1$s likes %2$s\'s %3$s'); $arr = array(); $arr['uid'] = $importer['channel_id']; $arr['aid'] = $importer['channel_account_id']; $arr['mid'] = $guid; $arr['parent_mid'] = $parent_item['mid']; $arr['owner_xchan'] = $parent_item['owner_xchan']; $arr['author_xchan'] = $person['xchan_hash']; $ulink = '[url=' . $contact['url'] . ']' . $contact['name'] . '[/url]'; $alink = '[url=' . $parent_item['author-link'] . ']' . $parent_item['author-name'] . '[/url]'; $plink = '[url=' . z_root() . '/display/' . $guid . ']' . $post_type . '[/url]'; $arr['body'] = sprintf($bodyverb, $ulink, $alink, $plink); $arr['app'] = 'Diaspora'; // set the route to that of the parent so downstream hubs won't reject it. $arr['route'] = $parent_item['route']; $arr['item_private'] = $parent_item['item_private']; $arr['verb'] = $activity; $arr['obj_type'] = $objtype; $arr['obj'] = $object; if (!$parent_author_signature) { $key = get_config('system', 'pubkey'); $x = array('signer' => $diaspora_handle, 'body' => $text, 'signed_text' => $signed_data, 'signature' => base64_encode($author_signature)); $arr['diaspora_meta'] = json_encode($x); } set_iconfig($arr, 'diaspora', 'fields', array_map('unxmlify', $xml), true); $result = item_store($arr); if ($result['success']) { // if the message isn't already being relayed, notify others // the existence of parent_author_signature means the parent_author or owner // is already relaying. The parent_item['origin'] indicates the message was created on our system if (intval($parent_item['item_origin']) && !$parent_author_signature) { Zotlabs\Daemon\Master::Summon(array('Notifier', 'comment-import', $result['item_id'])); } sync_an_item($importer['channel_id'], $result['item_id']); } return; }