Issue 5983: Central check for blocked and ignored contacts added
[friendica.git/.git] / src / Model / Item.php
index db9fbf4..67d051d 100644 (file)
@@ -11,9 +11,9 @@ use Friendica\Content\Text\BBCode;
 use Friendica\Content\Text\HTML;
 use Friendica\Core\Config;
 use Friendica\Core\Hook;
+use Friendica\Core\L10n;
 use Friendica\Core\Lock;
 use Friendica\Core\Logger;
-use Friendica\Core\L10n;
 use Friendica\Core\PConfig;
 use Friendica\Core\Protocol;
 use Friendica\Core\Renderer;
@@ -24,9 +24,11 @@ use Friendica\Protocol\Diaspora;
 use Friendica\Protocol\OStatus;
 use Friendica\Util\DateTimeFormat;
 use Friendica\Util\Map;
-use Friendica\Util\XML;
+use Friendica\Util\Network;
 use Friendica\Util\Security;
 use Friendica\Util\Strings;
+use Friendica\Util\XML;
+use Friendica\Worker\Delivery;
 use Text_LanguageDetect;
 
 class Item extends BaseObject
@@ -44,14 +46,14 @@ class Item extends BaseObject
 
        // Field list that is used to display the items
        const DISPLAY_FIELDLIST = [
-               'uid', 'id', 'parent', 'uri', 'thr-parent', 'parent-uri', 'guid', 'network',
+               'uid', 'id', 'parent', 'uri', 'thr-parent', 'parent-uri', 'guid', 'network', 'gravity',
                'commented', 'created', 'edited', 'received', 'verb', 'object-type', 'postopts', 'plink',
                'wall', 'private', 'starred', 'origin', 'title', 'body', 'file', 'attach', 'language',
                'content-warning', 'location', 'coord', 'app', 'rendered-hash', 'rendered-html', 'object',
                'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'item_id',
                'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network',
                'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'owner-network',
-               'contact-id', 'contact-link', 'contact-name', 'contact-avatar',
+               'contact-id', 'contact-uid', 'contact-link', 'contact-name', 'contact-avatar',
                'writable', 'self', 'cid', 'alias',
                'event-id', 'event-created', 'event-edited', 'event-start', 'event-finish',
                'event-summary', 'event-desc', 'event-location', 'event-type',
@@ -61,7 +63,7 @@ class Item extends BaseObject
 
        // Field list that is used to deliver items via the protocols
        const DELIVER_FIELDLIST = ['uid', 'id', 'parent', 'uri', 'thr-parent', 'parent-uri', 'guid',
-                       'created', 'edited', 'verb', 'object-type', 'object', 'target',
+                       'parent-guid', 'created', 'edited', 'verb', 'object-type', 'object', 'target',
                        'private', 'title', 'body', 'location', 'coord', 'app',
                        'attach', 'tag', 'deleted', 'extid', 'post-type',
                        'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
@@ -86,12 +88,12 @@ class Item extends BaseObject
                        'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'network',
                        'title', 'content-warning', 'body', 'location', 'coord', 'app',
                        'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target',
-                       'author-id', 'author-link', 'author-name', 'author-avatar',
+                       'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network',
                        'owner-id', 'owner-link', 'owner-name', 'owner-avatar'];
 
        // Never reorder or remove entries from this list. Just add new ones at the end, if needed.
        // The item-activity table only stores the index and needs this array to know the matching activity.
-       const ACTIVITIES = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE];
+       const ACTIVITIES = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE, ACTIVITY_FOLLOW, ACTIVITY2_ANNOUNCE];
 
        private static $legacy_mode = null;
 
@@ -924,7 +926,7 @@ class Item extends BaseObject
                        // We only need to notfiy others when it is an original entry from us.
                        // Only call the notifier when the item has some content relevant change.
                        if ($item['origin'] && in_array('edited', array_keys($fields))) {
-                               Worker::add(PRIORITY_HIGH, "Notifier", 'edit_post', $item['id']);
+                               Worker::add(PRIORITY_HIGH, "Notifier", Delivery::POST, $item['id']);
                        }
                }
 
@@ -1094,7 +1096,7 @@ class Item extends BaseObject
                        self::delete(['uri' => $item['uri'], 'deleted' => false], $priority);
 
                        // send the notification upstream/downstream
-                       Worker::add(['priority' => $priority, 'dont_fork' => true], "Notifier", "drop", intval($item['id']));
+                       Worker::add(['priority' => $priority, 'dont_fork' => true], "Notifier", Delivery::DELETION, intval($item['id']));
                } elseif ($item['uid'] != 0) {
 
                        // When we delete just our local user copy of an item, we have to set a marker to hide it
@@ -1315,6 +1317,8 @@ class Item extends BaseObject
                        $item['gravity'] = GRAVITY_PARENT;
                } elseif (activity_match($item['verb'], ACTIVITY_POST)) {
                        $item['gravity'] = GRAVITY_COMMENT;
+               } elseif (activity_match($item['verb'], ACTIVITY_FOLLOW)) {
+                       $item['gravity'] = GRAVITY_ACTIVITY;
                } else {
                        $item['gravity'] = GRAVITY_UNKNOWN;   // Should not happen
                        Logger::log('Unknown gravity for verb: ' . $item['verb'], Logger::DEBUG);
@@ -1334,7 +1338,11 @@ class Item extends BaseObject
                        $expire_date = time() - ($expire_interval * 86400);
                        $created_date = strtotime($item['created']);
                        if ($created_date < $expire_date) {
-                               Logger::log('item-store: item created ('.date('c', $created_date).') before expiration time ('.date('c', $expire_date).'). ignored. ' . print_r($item,true), Logger::DEBUG);
+                               Logger::notice('Item created before expiration interval.', [
+                                       'created' => date('c', $created_date),
+                                       'expired' => date('c', $expire_date),
+                                       '$item' => $item
+                               ]);
                                return 0;
                        }
                }
@@ -1352,7 +1360,13 @@ class Item extends BaseObject
                        if (DBA::isResult($existing)) {
                                // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
                                if ($uid != 0) {
-                                       Logger::log("Item with uri ".$item['uri']." already existed for user ".$uid." with id ".$existing["id"]." target network ".$existing["network"]." - new network: ".$item['network']);
+                                       Logger::notice('Item already existed for user', [
+                                               'uri' => $item['uri'],
+                                               'uid' => $uid,
+                                               'network' => $item['network'],
+                                               'existing_id' => $existing["id"],
+                                               'existing_network' => $existing["network"]
+                                       ]);
                                }
 
                                return $existing["id"];
@@ -1403,7 +1417,7 @@ class Item extends BaseObject
 
                // When there is no content then we don't post it
                if ($item['body'].$item['title'] == '') {
-                       Logger::log('No body, no title.');
+                       Logger::notice('No body, no title.');
                        return 0;
                }
 
@@ -1427,44 +1441,95 @@ class Item extends BaseObject
                $default = ['url' => $item['author-link'], 'name' => $item['author-name'],
                        'photo' => $item['author-avatar'], 'network' => $item['network']];
 
-               $item['author-id'] = defaults($item, 'author-id', Contact::getIdForURL($item["author-link"], 0, false, $default));
+               $item['author-id'] = defaults($item, 'author-id', Contact::getIdForURL($item['author-link'], 0, false, $default));
+
+               if (Contact::isBlocked($item['author-id'])) {
+                       Logger::notice('Author is blocked node-wide', ['author-link' => $item['author-link'], 'item-uri' => $item['uri']]);
+                       return 0;
+               }
+
+               if (!empty($item['author-link']) && Network::isUrlBlocked($item['author-link'])) {
+                       Logger::notice('Author server is blocked', ['author-link' => $item['author-link'], 'item-uri' => $item['uri']]);
+                       return 0;
+               }
+
+               if (!empty($uid) && Contact::isBlockedByUser($item['author-link'], $uid)) {
+                       Logger::notice('Author is blocked by user', ['author-link' => $item['author-link'], 'uid' => $uid, 'item-uri' => $item['uri']]);
+                       return 0;
+               }
 
-               if (Contact::isBlocked($item["author-id"])) {
-                       Logger::log('Contact '.$item["author-id"].' is blocked, item '.$item["uri"].' will not be stored');
+               if (!empty($uid) && ($item['parent-uri'] === $item['uri']) && Contact::isIgnoredByUser($item['author-link'], $uid)) {
+                       Logger::notice('Author is ignored by user', ['author-link' => $item['author-link'], 'uid' => $uid, 'item-uri' => $item['uri']]);
                        return 0;
                }
 
                $default = ['url' => $item['owner-link'], 'name' => $item['owner-name'],
                        'photo' => $item['owner-avatar'], 'network' => $item['network']];
 
-               $item['owner-id'] = defaults($item, 'owner-id', Contact::getIdForURL($item["owner-link"], 0, false, $default));
+               $item['owner-id'] = defaults($item, 'owner-id', Contact::getIdForURL($item['owner-link'], 0, false, $default));
 
-               if (Contact::isBlocked($item["owner-id"])) {
-                       Logger::log('Contact '.$item["owner-id"].' is blocked, item '.$item["uri"].' will not be stored');
+               if (Contact::isBlocked($item['owner-id'])) {
+                       Logger::notice('Owner is blocked node-wide', ['owner-link' => $item['owner-link'], 'item-uri' => $item['uri']]);
                        return 0;
                }
 
-               if ($item['network'] == Protocol::PHANTOM) {
-                       Logger::log('Missing network. Called by: '.System::callstack(), Logger::DEBUG);
+               if (!empty($item['owner-link']) && Network::isUrlBlocked($item['owner-link'])) {
+                       Logger::notice('Owner server is blocked', ['owner-link' => $item['owner-link'], 'item-uri' => $item['uri']]);
+                       return 0;
+               }
+
+               if (!empty($uid) && Contact::isBlockedByUser($item['owner-link'], $uid)) {
+                       Logger::notice('Owner is blocked by user', ['owner-link' => $item['owner-link'], 'uid' => $uid, 'item-uri' => $item['uri']]);
+                       return 0;
+               }
+
+               if (!empty($uid) && ($item['parent-uri'] === $item['uri']) && Contact::isIgnoredByUser($item['owner-link'], $uid)) {
+                       Logger::notice('Owner is ignored by user', ['owner-link' => $item['owner-link'], 'uid' => $uid, 'item-uri' => $item['uri']]);
+                       return 0;
+               }
 
+               if ($item['network'] == Protocol::PHANTOM) {
                        $item['network'] = Protocol::DFRN;
-                       Logger::log("Set network to " . $item["network"] . " for " . $item["uri"], Logger::DEBUG);
+                       Logger::notice('Missing network, setting to {network}.', [
+                               'uri' => $item["uri"],
+                               'network' => $item['network'],
+                               'callstack' => System::callstack()
+                       ]);
                }
 
                // Checking if there is already an item with the same guid
-               Logger::log('Checking for an item for user '.$item['uid'].' on network '.$item['network'].' with the guid '.$item['guid'], Logger::DEBUG);
                $condition = ['guid' => $item['guid'], 'network' => $item['network'], 'uid' => $item['uid']];
                if (self::exists($condition)) {
-                       Logger::log('found item with guid '.$item['guid'].' for user '.$item['uid'].' on network '.$item['network'], Logger::DEBUG);
+                       Logger::notice('Found already existing item', [
+                               'guid' => $item['guid'],
+                               'uid' => $item['uid'],
+                               'network' => $item['network']
+                       ]);
                        return 0;
                }
 
+               if ($item['verb'] == ACTIVITY_FOLLOW) {
+                       if (!$item['origin'] && ($item['author-id'] == Contact::getPublicIdByUserId($uid))) {
+                               // Our own follow request can be relayed to us. We don't store it to avoid notification chaos.
+                               Logger::log("Follow: Don't store not origin follow request from us for " . $item['parent-uri'], Logger::DEBUG);
+                               return 0;
+                       }
+
+                       $condition = ['verb' => ACTIVITY_FOLLOW, 'uid' => $item['uid'],
+                               'parent-uri' => $item['parent-uri'], 'author-id' => $item['author-id']];
+                       if (self::exists($condition)) {
+                               // It happens that we receive multiple follow requests by the same author - we only store one.
+                               Logger::log('Follow: Found existing follow request from author ' . $item['author-id'] . ' for ' . $item['parent-uri'], Logger::DEBUG);
+                               return 0;
+                       }
+               }
+
                // Check for hashtags in the body and repair or add hashtag links
                self::setHashtags($item);
 
                $item['thr-parent'] = $item['parent-uri'];
 
-               $notify_type = '';
+               $notify_type = Delivery::POST;
                $allow_cid = '';
                $allow_gid = '';
                $deny_cid  = '';
@@ -1477,7 +1542,6 @@ class Item extends BaseObject
                        $allow_gid = $item['allow_gid'];
                        $deny_cid  = $item['deny_cid'];
                        $deny_gid  = $item['deny_gid'];
-                       $notify_type = 'wall-new';
                } else {
                        // find the parent and snarf the item id and ACLs
                        // and anything else we need to inherit
@@ -1514,8 +1578,7 @@ class Item extends BaseObject
                                $allow_gid      = $parent['allow_gid'];
                                $deny_cid       = $parent['deny_cid'];
                                $deny_gid       = $parent['deny_gid'];
-                               $item['wall']    = $parent['wall'];
-                               $notify_type    = 'comment-new';
+                               $item['wall']   = $parent['wall'];
 
                                /*
                                 * If the parent is private, force privacy for the entire conversation
@@ -1535,17 +1598,10 @@ class Item extends BaseObject
                                        $item['private'] = 0;
                                }
 
-                               // If its a post from myself then tag the thread as "mention"
-                               Logger::log("Checking if parent ".$parent_id." has to be tagged as mention for user ".$item['uid'], Logger::DEBUG);
-                               $user = DBA::selectFirst('user', ['nickname'], ['uid' => $item['uid']]);
-                               if (DBA::isResult($user)) {
-                                       $self = Strings::normaliseLink(System::baseUrl() . '/profile/' . $user['nickname']);
-                                       $self_id = Contact::getIdForURL($self, 0, true);
-                                       Logger::log("'myself' is ".$self_id." for parent ".$parent_id." checking against ".$item['author-id']." and ".$item['owner-id'], Logger::DEBUG);
-                                       if (($item['author-id'] == $self_id) || ($item['owner-id'] == $self_id)) {
-                                               DBA::update('thread', ['mention' => true], ['iid' => $parent_id]);
-                                               Logger::log("tagged thread ".$parent_id." as mention for user ".$self, Logger::DEBUG);
-                                       }
+                               // If its a post that originated here then tag the thread as "mention"
+                               if ($item['origin'] && $item['uid']) {
+                                       DBA::update('thread', ['mention' => true], ['iid' => $parent_id]);
+                                       Logger::log('tagged thread ' . $parent_id . ' as mention for user ' . $item['uid'], Logger::DEBUG);
                                }
                        } else {
                                /*
@@ -1566,6 +1622,10 @@ class Item extends BaseObject
                        }
                }
 
+               if (stristr($item['verb'], ACTIVITY_POKE)) {
+                       $notify_type = Delivery::POKE;
+               }
+
                $item['parent-uri-id'] = ItemURI::getIdByURI($item['parent-uri']);
                $item['thr-parent-id'] = ItemURI::getIdByURI($item['thr-parent']);
 
@@ -1598,7 +1658,7 @@ class Item extends BaseObject
                        $item["global"] = true;
 
                        // Set the global flag on all items if this was a global item entry
-                       self::update(['global' => true], ['uri' => $item["uri"]]);
+                       DBA::update('item', ['global' => true], ['uri' => $item["uri"]]);
                } else {
                        $item["global"] = self::exists(['uid' => 0, 'uri' => $item["uri"]]);
                }
@@ -1684,6 +1744,7 @@ class Item extends BaseObject
                unset($item['author-link']);
                unset($item['author-name']);
                unset($item['author-avatar']);
+               unset($item['author-network']);
 
                unset($item['owner-link']);
                unset($item['owner-name']);
@@ -1754,7 +1815,7 @@ class Item extends BaseObject
                }
 
                // Set parent id
-               self::update(['parent' => $parent_id], ['id' => $current_post]);
+               DBA::update('item', ['parent' => $parent_id], ['id' => $current_post]);
 
                $item['id'] = $current_post;
                $item['parent'] = $parent_id;
@@ -1762,9 +1823,9 @@ class Item extends BaseObject
                // update the commented timestamp on the parent
                // Only update "commented" if it is really a comment
                if (($item['gravity'] != GRAVITY_ACTIVITY) || !Config::get("system", "like_no_comment")) {
-                       self::update(['commented' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
+                       DBA::update('item', ['commented' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
                } else {
-                       self::update(['changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
+                       DBA::update('item', ['changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
                }
 
                if ($dsprsig) {
@@ -1839,18 +1900,8 @@ class Item extends BaseObject
 
                check_user_notification($current_post);
 
-               if ($notify) {
+               if ($notify || ($item['visible'] && ((!empty($parent) && $parent['origin']) || $item['origin']))) {
                        Worker::add(['priority' => $priority, 'dont_fork' => true], 'Notifier', $notify_type, $current_post);
-               } elseif ($item['visible'] && ((!empty($parent) && $parent['origin']) || $item['origin'])) {
-                       if ($item['gravity'] == GRAVITY_ACTIVITY) {
-                               $cmd = $item['origin'] ? 'activity-new' : 'activity-import';
-                       } elseif ($item['gravity'] == GRAVITY_COMMENT) {
-                               $cmd = $item['origin'] ? 'comment-new' : 'comment-import';
-                       } else {
-                               $cmd = 'wall-new';
-                       }
-
-                       Worker::add(['priority' => $priority, 'dont_fork' => true], 'Notifier', $cmd, $current_post);
                }
 
                return $current_post;
@@ -2386,7 +2437,6 @@ class Item extends BaseObject
 
        public static function setHashtags(&$item)
        {
-
                $tags = BBCode::getTags($item["body"]);
 
                // No hashtags?
@@ -2394,6 +2444,17 @@ class Item extends BaseObject
                        return false;
                }
 
+               // What happens in [code], stays in [code]!
+               // escape the # and the [
+               // hint: we will also get in trouble with #tags, when we want markdown in posts -> ### Headline 3
+               $item["body"] = preg_replace_callback("/\[code(.*?)\](.*?)\[\/code\]/ism",
+                       function ($match) {
+                               // we truly ESCape all # and [ to prevent gettin weird tags in [code] blocks
+                               $find = ['#', '['];
+                               $replace = [chr(27).'sharp', chr(27).'leftsquarebracket'];
+                               return ("[code" . $match[1] . "]" . str_replace($find, $replace, $match[2]) . "[/code]");
+                       }, $item["body"]);
+
                // This sorting is important when there are hashtags that are part of other hashtags
                // Otherwise there could be problems with hashtags like #test and #test2
                rsort($tags);
@@ -2430,12 +2491,11 @@ class Item extends BaseObject
                                "&num;$2", $item["body"]);
 
                foreach ($tags as $tag) {
-                       if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=')) {
+                       if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=') || $tag[1] == '#') {
                                continue;
                        }
 
                        $basetag = str_replace('_',' ',substr($tag,1));
-
                        $newtag = '#[url=' . System::baseUrl() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
 
                        $item["body"] = str_replace($tag, $newtag, $item["body"]);
@@ -2450,62 +2510,16 @@ class Item extends BaseObject
 
                // Convert back the masked hashtags
                $item["body"] = str_replace("&num;", "#", $item["body"]);
-       }
-
-       public static function getGuidById($id)
-       {
-               $item = self::selectFirst(['guid'], ['id' => $id]);
-               if (DBA::isResult($item)) {
-                       return $item['guid'];
-               } else {
-                       return '';
-               }
-       }
-
-       /**
-        * This function is only used for the old Friendica app on Android that doesn't like paths with guid
-        *
-        * @param string $guid item guid
-        * @param int    $uid  user id
-        * @return array with id and nick of the item with the given guid
-        * @throws \Exception
-        */
-       public static function getIdAndNickByGuid($guid, $uid = 0)
-       {
-               $nick = "";
-               $id = 0;
-
-               if ($uid == 0) {
-                       $uid = local_user();
-               }
-
-               // Does the given user have this item?
-               if ($uid) {
-                       $item = self::selectFirst(['id'], ['guid' => $guid, 'uid' => $uid]);
-                       if (DBA::isResult($item)) {
-                               $user = DBA::selectFirst('user', ['nickname'], ['uid' => $uid]);
-                               if (!DBA::isResult($user)) {
-                                       return;
-                               }
-                               $id = $item['id'];
-                               $nick = $user['nickname'];
-                       }
-               }
 
-               // Or is it anywhere on the server?
-               if ($nick == "") {
-                       $condition = ["`guid` = ? AND `uid` != 0", $guid];
-                       $item = self::selectFirst(['id', 'uid'], $condition);
-                       if (DBA::isResult($item)) {
-                               $user = DBA::selectFirst('user', ['nickname'], ['uid' => $item['uid']]);
-                               if (!DBA::isResult($user)) {
-                                       return;
-                               }
-                               $id = $item['id'];
-                               $nick = $user['nickname'];
-                       }
-               }
-               return ["nick" => $nick, "id" => $id];
+               // Remember! What happens in [code], stays in [code]
+               // roleback the # and [
+               $item["body"] = preg_replace_callback("/\[code(.*?)\](.*?)\[\/code\]/ism",
+                       function ($match) {
+                               // we truly unESCape all sharp and leftsquarebracket
+                               $find = [chr(27).'sharp', chr(27).'leftsquarebracket'];
+                               $replace = ['#', '['];
+                               return ("[code" . $match[1] . "]" . str_replace($find, $replace, $match[2]) . "[/code]");
+                       }, $item["body"]);
        }
 
        /**
@@ -2603,7 +2617,7 @@ class Item extends BaseObject
 
                self::updateThread($item_id);
 
-               Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => true], 'Notifier', 'tgroup', $item_id);
+               Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => true], 'Notifier', Delivery::POST, $item_id);
        }
 
        public static function isRemoteSelf($contact, &$datarray)
@@ -2653,7 +2667,6 @@ class Item extends BaseObject
                                $datarray['author-link']   = $datarray['owner-link'];
                                $datarray['author-avatar'] = $datarray['owner-avatar'];
 
-                               unset($datarray['created']);
                                unset($datarray['edited']);
 
                                unset($datarray['network']);
@@ -3410,9 +3423,7 @@ class Item extends BaseObject
                        if (strpos($mime, 'video') !== false) {
                                if (!$vhead) {
                                        $vhead = true;
-                                       $a->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('videos_head.tpl'), [
-                                               '$baseurl' => System::baseUrl(),
-                                       ]);
+                                       $a->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('videos_head.tpl'));
                                }
 
                                $url_parts = explode('/', $the_url);
@@ -3536,4 +3547,31 @@ class Item extends BaseObject
 
                return $ret;
        }
+
+       /**
+        * Is the given item array a post that is sent as starting post to a forum?
+        *
+        * @param array $item
+        * @param array $owner
+        *
+        * @return boolean "true" when it is a forum post
+        */
+       public static function isForumPost(array $item, array $owner = [])
+       {
+               if (empty($owner)) {
+                       $owner = User::getOwnerDataById($item['uid']);
+                       if (empty($owner)) {
+                               return false;
+                       }
+               }
+
+               if (($item['author-id'] == $item['owner-id']) ||
+                       ($owner['id'] == $item['contact-id']) ||
+                       ($item['uri'] != $item['parent-uri']) ||
+                       $item['origin']) {
+                       return false;
+               }
+
+               return Contact::isForum($item['contact-id']);
+       }
 }