Example #1
0
/**
 * @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);
}
Example #2
0
File: zot.php Project: 23n/hubzilla
/**
 * @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;
}
Example #3
0
/**
 * 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));
    }
}