/** * @brief Called when we deliver things that might be tagged in ways that require delivery processing. * * Handles community tagging of posts and also look for mention tags and sets up * a second delivery chain if appropriate. * * @param int $uid * @param int $item_id */ function tag_deliver($uid, $item_id) { $mention = false; /* * Fetch stuff we need - a channel and an item */ $u = q("select * from channel left join xchan on channel_hash = xchan_hash where channel_id = %d limit 1", intval($uid)); if (!$u) { return; } $i = q("select * from item where id = %d and uid = %d limit 1", intval($item_id), intval($uid)); if (!$i) { return; } $i = fetch_post_tags($i); $item = $i[0]; if ($item['source_xchan'] && $item['item_flags'] & ITEM_UPLINK && $item['item_flags'] & ITEM_THREAD_TOP && $item['edited'] != $item['created']) { // this is an update (edit) to a post which was already processed by us and has a second delivery chain // Just start the second delivery chain to deliver the updated post proc_run('php', 'include/notifier.php', 'tgroup', $item['id']); return; } /* * Seems like a good place to plug in a poke notification. */ if (stristr($item['verb'], ACTIVITY_POKE)) { $poke_notify = true; if ($item['obj_type'] == "" || $item['obj_type'] !== ACTIVITY_OBJ_PERSON || !$item['object']) { $poke_notify = false; } $obj = json_decode_plus($item['object']); if ($obj) { if ($obj['id'] !== $u[0]['channel_hash']) { $poke_notify = false; } } if ($item['item_restrict'] & ITEM_DELETED) { $poke_notify = false; } $verb = urldecode(substr($item['verb'], strpos($item['verb'], '#') + 1)); if ($poke_notify) { require_once 'include/enotify.php'; notification(array('to_xchan' => $u[0]['channel_hash'], 'from_xchan' => $item['author_xchan'], 'type' => NOTIFY_POKE, 'item' => $item, 'link' => $i[0]['llink'], 'verb' => ACTIVITY_POKE, 'activity' => $verb, 'otype' => 'item')); } } /* * Do community tagging */ if ($item['obj_type'] === ACTIVITY_OBJ_TAGTERM) { // We received a community tag activity for a post. // See if we are the owner of the parent item and have given permission to tag our posts. // If so tag the parent post. logger('tag_deliver: community tag activity received'); if ($item['owner_xchan'] === $u[0]['channel_hash'] && !get_pconfig($u[0]['channel_id'], 'system', 'blocktags')) { logger('tag_deliver: community tag recipient: ' . $u[0]['channel_name']); $j_tgt = json_decode_plus($item['target']); if ($j_tgt && $j_tgt['id']) { $p = q("select * from item where mid = '%s' and uid = %d limit 1", dbesc($j_tgt['id']), intval($u[0]['channel_id'])); if ($p) { $j_obj = json_decode_plus($item['object']); logger('tag_deliver: tag object: ' . print_r($j_obj, true), LOGGER_DATA); if ($j_obj && $j_obj['id'] && $j_obj['title']) { if (is_array($j_obj['link'])) { $taglink = get_rel_link($j_obj['link'], 'alternate'); } store_item_tag($u[0]['channel_id'], $p[0]['id'], TERM_OBJ_POST, TERM_HASHTAG, $j_obj['title'], $j_obj['id']); $x = q("update item set edited = '%s', received = '%s', changed = '%s' where mid = '%s' and uid = %d", dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($j_tgt['id']), intval($u[0]['channel_id'])); proc_run('php', 'include/notifier.php', 'edit_post', $p[0]['id']); } } } } else { logger('tag_deliver: tag permission denied for ' . $u[0]['channel_address']); } } /* * A "union" is a message which our channel has sourced from another channel. * This sets up a second delivery chain just like forum tags do. * Find out if this is a source-able post. */ $union = check_item_source($uid, $item); if ($union) { logger('check_item_source returns true'); } // This might be a followup (e.g. comment) by the original post author to a tagged forum // If so setup a second delivery chain if (!($item['item_flags'] & ITEM_THREAD_TOP)) { $x = q("select * from item where id = parent and parent = %d and uid = %d limit 1", intval($item['parent']), intval($uid)); if ($x && $x[0]['item_flags'] & ITEM_UPLINK) { start_delivery_chain($u[0], $item, $item_id, $x[0]); } } /* * Now we've got those out of the way. Let's see if this is a post that's tagged for re-delivery */ $terms = get_terms_oftype($item['term'], TERM_MENTION); if ($terms) { logger('tag_deliver: post mentions: ' . print_r($terms, true), LOGGER_DATA); } $link = normalise_link($u[0]['xchan_url']); if ($terms) { foreach ($terms as $term) { if (link_compare($term['url'], $link)) { $mention = true; break; } } } if ($mention) { logger('tag_deliver: mention found for ' . $u[0]['channel_name']); $r = q("update item set item_flags = ( item_flags | %d ) where id = %d", intval(ITEM_MENTIONSME), intval($item_id)); // At this point we've determined that the person receiving this post was mentioned in it or it is a union. // Now let's check if this mention was inside a reshare so we don't spam a forum // If it's private we may have to unobscure it momentarily so that we can parse it. $body = ''; if ($item['item_flags'] & ITEM_OBSCURED) { $key = get_config('system', 'prvkey'); if ($item['body']) { $body = crypto_unencapsulate(json_decode_plus($item['body']), $key); } } else { $body = $item['body']; } $body = preg_replace('/\\[share(.*?)\\[\\/share\\]/', '', $body); $tagged = false; $plustagged = false; $matches = array(); $pattern = '/@\\!?\\[zrl\\=' . preg_quote($term['url'], '/') . '\\]' . preg_quote($term['term'], '/') . '\\[\\/zrl\\]/'; if (preg_match($pattern, $body, $matches)) { $tagged = true; } $pattern = '/@\\!?\\[zrl\\=([^\\]]*?)\\]((?:.(?!\\[zrl\\=))*?)\\+\\[\\/zrl\\]/'; if (preg_match_all($pattern, $body, $matches, PREG_SET_ORDER)) { $max_forums = get_config('system', 'max_tagged_forums'); if (!$max_forums) { $max_forums = 2; } $matched_forums = 0; foreach ($matches as $match) { $matched_forums++; if ($term['url'] === $match[1] && $term['term'] === $match[2]) { if ($matched_forums <= $max_forums) { $plustagged = true; break; } logger('forum ' . $term['term'] . ' exceeded max_tagged_forums - ignoring'); } } } if (!($tagged || $plustagged)) { logger('tag_deliver: mention was in a reshare or exceeded max_tagged_forums - ignoring'); return; } $arr = array('channel_id' => $uid, 'item' => $item, 'body' => $body); call_hooks('tagged', $arr); /* * Kill two birds with one stone. As long as we're here, send a mention notification. */ require_once 'include/enotify.php'; notification(array('to_xchan' => $u[0]['channel_hash'], 'from_xchan' => $item['author_xchan'], 'type' => NOTIFY_TAGSELF, 'item' => $item, 'link' => $i[0]['llink'], 'verb' => ACTIVITY_TAG, 'otype' => 'item')); // Just a normal tag? if (!$plustagged) { logger('tag_deliver: not a plus tag', LOGGER_DEBUG); return; } // plustagged - keep going, next check permissions if (!perm_is_allowed($uid, $item['author_xchan'], 'tag_deliver')) { logger('tag_delivery denied for uid ' . $uid . ' and xchan ' . $item['author_xchan']); return; } } if (!$mention && !$union) { logger('tag_deliver: no mention and no union.'); return; } // tgroup delivery - setup a second delivery chain // prevent delivery looping - only proceed // if the message originated elsewhere and is a top-level post if ($item['item_flags'] & ITEM_WALL || $item['item_flags'] & ITEM_ORIGIN || !($item['item_flags'] & ITEM_THREAD_TOP) || $item['id'] != $item['parent']) { logger('tag_deliver: item was local or a comment. rejected.'); return; } logger('tag_deliver: creating second delivery chain.'); start_delivery_chain($u[0], $item, $item_id, null); }
/** * @brief * * @param array $sender * @param array $arr * @param array $deliveries * @param boolean $relay * @param boolean $public (optional) default false * @param boolean $request (optional) default false * @return array */ function process_delivery($sender, $arr, $deliveries, $relay, $public = false, $request = false) { $result = array(); $result['site'] = z_root(); // We've validated the sender. Now make sure that the sender is the owner or author if (!$public) { if ($sender['hash'] != $arr['owner_xchan'] && $sender['hash'] != $arr['author_xchan']) { logger("process_delivery: sender {$sender['hash']} is not owner {$arr['owner_xchan']} or author {$arr['author_xchan']} - mid {$arr['mid']}"); return; } } foreach ($deliveries as $d) { $local_public = $public; $DR = new DReport(z_root(), $sender['hash'], $d['hash'], $arr['mid']); $r = q("select * from channel where channel_hash = '%s' limit 1", dbesc($d['hash'])); if (!$r) { $DR->update('recipient not found'); $result[] = $DR->get(); continue; } $channel = $r[0]; $DR->addto_recipient($channel['channel_name'] . ' <' . $channel['channel_address'] . '@' . get_app()->get_hostname() . '>'); /** * @FIXME: Somehow we need to block normal message delivery from our clones, as the delivered * message doesn't have ACL information in it as the cloned copy does. That copy * will normally arrive first via sync delivery, but this isn't guaranteed. * There's a chance the current delivery could take place before the cloned copy arrives * hence the item could have the wrong ACL and *could* be used in subsequent deliveries or * access checks. So far all attempts at identifying this situation precisely * have caused issues with delivery of relayed comments. */ // if(($d['hash'] === $sender['hash']) && ($sender['url'] !== z_root()) && (! $relay)) { // $DR->update('self delivery ignored'); // $result[] = $DR->get(); // continue; // } // allow public postings to the sys channel regardless of permissions, but not // for comments travelling upstream. Wait and catch them on the way down. // They may have been blocked by the owner. if (intval($channel['channel_system']) && !$arr['item_private'] && !$relay) { $local_public = true; $r = q("select xchan_selfcensored from xchan where xchan_hash = '%s' limit 1", dbesc($sender['hash'])); // don't import sys channel posts from selfcensored authors if ($r && intval($r[0]['xchan_selfcensored'])) { $local_public = false; continue; } } $tag_delivery = tgroup_check($channel['channel_id'], $arr); $perm = 'send_stream'; if ($arr['mid'] !== $arr['parent_mid'] && $relay) { $perm = 'post_comments'; } // This is our own post, possibly coming from a channel clone if ($arr['owner_xchan'] == $d['hash']) { $arr['item_wall'] = 1; } else { $arr['item_wall'] = 0; } if (!perm_is_allowed($channel['channel_id'], $sender['hash'], $perm) && !$tag_delivery && !$local_public) { logger("permission denied for delivery to channel {$channel['channel_id']} {$channel['channel_address']}"); $DR->update('permission denied'); $result[] = $DR->get(); continue; } if ($arr['mid'] != $arr['parent_mid']) { // check source route. // We are only going to accept comments from this sender if the comment has the same route as the top-level-post, // this is so that permissions mismatches between senders apply to the entire conversation // As a side effect we will also do a preliminary check that we have the top-level-post, otherwise // processing it is pointless. $r = q("select route, id from item where mid = '%s' and uid = %d limit 1", dbesc($arr['parent_mid']), intval($channel['channel_id'])); if (!$r) { $DR->update('comment parent not found'); $result[] = $DR->get(); // We don't seem to have a copy of this conversation or at least the parent // - so request a copy of the entire conversation to date. // Don't do this if it's a relay post as we're the ones who are supposed to // have the copy and we don't want the request to loop. // Also don't do this if this comment came from a conversation request packet. // It's possible that comments are allowed but posting isn't and that could // cause a conversation fetch loop. We can detect these packets since they are // delivered via a 'notify' packet type that has a message_id element in the // initial zot packet (just like the corresponding 'request' packet type which // makes the request). // We'll also check the send_stream permission - because if it isn't allowed, // the top level post is unlikely to be imported and // this is just an exercise in futility. if (!$relay && !$request && !$local_public && perm_is_allowed($channel['channel_id'], $sender['hash'], 'send_stream')) { proc_run('php', 'include/notifier.php', 'request', $channel['channel_id'], $sender['hash'], $arr['parent_mid']); } continue; } if ($relay) { // reset the route in case it travelled a great distance upstream // use our parent's route so when we go back downstream we'll match // with whatever route our parent has. $arr['route'] = $r[0]['route']; } else { // going downstream check that we have the same upstream provider that // sent it to us originally. Ignore it if it came from another source // (with potentially different permissions). // only compare the last hop since it could have arrived at the last location any number of ways. // Always accept empty routes and firehose items (route contains 'undefined') . $existing_route = explode(',', $r[0]['route']); $routes = count($existing_route); if ($routes) { $last_hop = array_pop($existing_route); $last_prior_route = implode(',', $existing_route); } else { $last_hop = ''; $last_prior_route = ''; } if (in_array('undefined', $existing_route) || $last_hop == 'undefined' || $sender['hash'] == 'undefined') { $last_hop = ''; } $current_route = ($arr['route'] ? $arr['route'] . ',' : '') . $sender['hash']; if ($last_hop && $last_hop != $sender['hash']) { logger('comment route mismatch: parent route = ' . $r[0]['route'] . ' expected = ' . $current_route, LOGGER_DEBUG); logger('comment route mismatch: parent msg = ' . $r[0]['id'], LOGGER_DEBUG); $DR->update('comment route mismatch'); $result[] = $DR->get(); continue; } // we'll add sender['hash'] onto this when we deliver it. $last_prior_route now has the previously stored route // *except* for the sender['hash'] which would've been the last hop before it got to us. $arr['route'] = $last_prior_route; } } $ab = q("select * from abook where abook_channel = %d and abook_xchan = '%s'", intval($channel['channel_id']), dbesc($arr['owner_xchan'])); $abook = $ab ? $ab[0] : null; if (intval($arr['item_deleted'])) { // remove_community_tag is a no-op if this isn't a community tag activity remove_community_tag($sender, $arr, $channel['channel_id']); // set these just in case we need to store a fresh copy of the deleted post. // This could happen if the delete got here before the original post did. $arr['aid'] = $channel['channel_account_id']; $arr['uid'] = $channel['channel_id']; $item_id = delete_imported_item($sender, $arr, $channel['channel_id'], $relay); $DR->update($item_id ? 'deleted' : 'delete_failed'); $result[] = $DR->get(); if ($relay && $item_id) { logger('process_delivery: invoking relay'); proc_run('php', 'include/notifier.php', 'relay', intval($item_id)); $DR->update('relayed'); $result[] = $DR->get(); } continue; } $r = q("select * from item where mid = '%s' and uid = %d limit 1", dbesc($arr['mid']), intval($channel['channel_id'])); if ($r) { // We already have this post. $item_id = $r[0]['id']; if (intval($r[0]['item_deleted'])) { // It was deleted locally. $DR->update('update ignored'); $result[] = $DR->get(); continue; } elseif ($arr['edited'] > $r[0]['edited']) { $arr['id'] = $r[0]['id']; $arr['uid'] = $channel['channel_id']; if ($arr['mid'] == $arr['parent_mid'] && !post_is_importable($arr, $abook)) { $DR->update('update ignored'); $result[] = $DR->get(); } else { update_imported_item($sender, $arr, $r[0], $channel['channel_id']); $DR->update('updated'); $result[] = $DR->get(); if (!$relay) { add_source_route($item_id, $sender['hash']); } } } else { $DR->update('update ignored'); $result[] = $DR->get(); // We need this line to ensure wall-to-wall comments are relayed (by falling through to the relay bit), // and at the same time not relay any other relayable posts more than once, because to do so is very wasteful. if (!intval($r[0]['item_origin'])) { continue; } } } else { $arr['aid'] = $channel['channel_account_id']; $arr['uid'] = $channel['channel_id']; // if it's a sourced post, call the post_local hooks as if it were // posted locally so that crosspost connectors will be triggered. if (check_item_source($arr['uid'], $arr)) { call_hooks('post_local', $arr); } $item_id = 0; if ($arr['mid'] == $arr['parent_mid'] && !post_is_importable($arr, $abook)) { $DR->update('post ignored'); $result[] = $DR->get(); } else { $item_result = item_store($arr); if ($item_result['success']) { $item_id = $item_result['item_id']; $parr = array('item_id' => $item_id, 'item' => $arr, 'sender' => $sender, 'channel' => $channel); call_hooks('activity_received', $parr); // don't add a source route if it's a relay or later recipients will get a route mismatch if (!$relay) { add_source_route($item_id, $sender['hash']); } } $DR->update($item_id ? 'posted' : 'storage failed: ' . $item_result['message']); $result[] = $DR->get(); } } if ($relay && $item_id) { logger('process_delivery: invoking relay'); proc_run('php', 'include/notifier.php', 'relay', intval($item_id)); $DR->addto_update('relayed'); $result[] = $DR->get(); } } if (!$deliveries) { $result[] = array('', 'no recipients', '', $arr['mid']); } logger('process_delivery: local results: ' . print_r($result, true), LOGGER_DEBUG); return $result; }
/** * Sourced and tag-delivered posts are re-targetted for delivery to the connections of the channel * receiving the post. This starts the second delivery chain, by resetting permissions and ensuring * that ITEM_UPLINK is set on the parent post, and storing the current owner_xchan as the source_xchan. * We'll become the new owner. If called without $parent, this *is* the parent post. * * @param array $channel * @param array $item * @param int $item_id * @param boolean $parent */ function start_delivery_chain($channel, $item, $item_id, $parent) { $sourced = check_item_source($channel['channel_id'], $item); if ($sourced) { $r = q("select * from source where src_channel_id = %d and ( src_xchan = '%s' or src_xchan = '*' ) limit 1", intval($channel['channel_id']), dbesc($item['source_xchan'] ? $item['source_xchan'] : $item['owner_xchan'])); if ($r) { $t = trim($r[0]['src_tag']); if ($t) { $tags = explode(',', $t); if ($tags) { foreach ($tags as $tt) { $tt = trim($tt); if ($tt) { q("insert into term (uid,oid,otype,ttype,term,url)\n \t\t\t\tvalues(%d,%d,%d,%d,'%s','%s') ", intval($channel['channel_id']), intval($item_id), intval(TERM_OBJ_POST), intval(TERM_CATEGORY), dbesc($tt), dbesc(z_root() . '/channel/' . $channel['channel_address'] . '?f=&cat=' . urlencode($tt))); } } } } } } // Change this copy of the post to a forum head message and deliver to all the tgroup members // also reset all the privacy bits to the forum default permissions $private = $channel['channel_allow_cid'] || $channel['channel_allow_gid'] || $channel['channel_deny_cid'] || $channel['channel_deny_gid'] ? 1 : 0; $new_public_policy = map_scope(\Zotlabs\Access\PermissionLimits::Get($channel['channel_id'], 'view_stream'), true); if (!$private && $new_public_policy) { $private = 1; } $item_wall = 1; $item_origin = 1; $item_uplink = 0; $item_nocomment = 0; $item_obscured = 0; $flag_bits = $item['item_flags']; // maintain the original source, which will be the original item owner and was stored in source_xchan // when we created the delivery fork if ($parent) { $r = q("update item set source_xchan = '%s' where id = %d", dbesc($parent['source_xchan']), intval($item_id)); } else { $item_uplink = 1; $r = q("update item set source_xchan = owner_xchan where id = %d", intval($item_id)); } $title = $item['title']; $body = $item['body']; $r = q("update item set item_uplink = %d, item_nocomment = %d, item_obscured = %d, item_flags = %d, owner_xchan = '%s', allow_cid = '%s', allow_gid = '%s', \n\t\tdeny_cid = '%s', deny_gid = '%s', item_private = %d, public_policy = '%s', comment_policy = '%s', title = '%s', body = '%s', item_wall = %d, item_origin = %d where id = %d", intval($item_uplink), intval($item_nocomment), intval($item_obscured), intval($flag_bits), dbesc($channel['channel_hash']), dbesc($channel['channel_allow_cid']), dbesc($channel['channel_allow_gid']), dbesc($channel['channel_deny_cid']), dbesc($channel['channel_deny_gid']), intval($private), dbesc($new_public_policy), dbesc(map_scope(\Zotlabs\Access\PermissionLimits::Get($channel['channel_id'], 'post_comments'))), dbesc($title), dbesc($body), intval($item_wall), intval($item_origin), intval($item_id)); if ($r) { Zotlabs\Daemon\Master::Summon(array('Notifier', 'tgroup', $item_id)); } else { logger('start_delivery_chain: failed to update item'); // reset the source xchan to prevent loops $r = q("update item set source_xchan = '' where id = %d", intval($item_id)); } }