]> git.mxchange.org Git - friendica.git/blobdiff - src/Model/Item.php
Fix for: empty posts and comments that hadn't been transmitted to Diaspora
[friendica.git] / src / Model / Item.php
index 647c37d89da538261b806387b7dd8f8b31b253ac..8a5264a7bcd49e4c3a7892d0865686de5feb58e6 100644 (file)
@@ -24,6 +24,7 @@ use Friendica\Protocol\Diaspora;
 use Friendica\Protocol\OStatus;
 use Friendica\Util\DateTimeFormat;
 use Friendica\Util\XML;
+use Friendica\Util\Lock;
 use dba;
 use Text_LanguageDetect;
 
@@ -34,15 +35,15 @@ require_once 'include/text.php';
 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',
+       const DISPLAY_FIELDLIST = ['uid', 'id', 'parent', 'uri', 'thr-parent', 'parent-uri', 'guid', 'network',
                        'commented', 'created', 'edited', 'received', 'verb', 'object-type', 'postopts', 'plink',
                        'wall', 'private', 'starred', 'origin', 'title', 'body', 'file', 'attach',
                        'content-warning', 'location', 'coord', 'app', 'rendered-hash', 'rendered-html', 'object',
-                       'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'item_id', 'item_network',
+                       'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'item_id',
                        'author-id', 'author-link', 'author-name', 'author-avatar',
                        'owner-id', 'owner-link', 'owner-name', 'owner-avatar',
                        'contact-id', 'contact-link', 'contact-name', 'contact-avatar',
-                       'network', 'url', 'name', 'writable', 'self', 'cid', 'alias',
+                       'writable', 'self', 'cid', 'alias',
                        'event-id', 'event-created', 'event-edited', 'event-start', 'event-finish',
                        'event-summary', 'event-desc', 'event-location', 'event-type',
                        'event-nofinish', 'event-adjust', 'event-ignore', 'event-id'];
@@ -56,8 +57,116 @@ class Item extends BaseObject
                        'author-id', 'author-link', 'owner-link', 'contact-uid',
                        'signed_text', 'signature', 'signer'];
 
+       // Field list for "item-content" table that is mixed with the item table
+       const CONTENT_FIELDLIST = ['title', 'content-warning', 'body', 'location',
+                       'coord', 'app', 'rendered-hash', 'rendered-html', 'verb',
+                       'object-type', 'object', 'target-type', 'target', 'plink'];
+
+       // All fields in the item table
+       const ITEM_FIELDLIST = ['id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', 'guid',
+                       'contact-id', 'type', 'wall', 'gravity', 'extid', 'icid',
+                       'created', 'edited', 'commented', 'received', 'changed', 'verb',
+                       'postopts', 'plink', 'resource-id', 'event-id', 'tag', 'attach', 'inform',
+                       'file', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
+                       'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark',
+                       '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',
+                       'owner-id', 'owner-link', 'owner-name', 'owner-avatar'];
+
        /**
-        * Retrieve a single record from the item table and returns it in an associative array
+        * @brief Fetch a single item row
+        *
+        * @param mixed $stmt statement object
+        * @return array current row
+        */
+       public static function fetch($stmt)
+       {
+               $row = dba::fetch($stmt);
+
+               // Fetch data from the item-content table whenever there is content there
+               foreach (self::CONTENT_FIELDLIST as $field) {
+                       if (empty($row[$field]) && !empty($row['item-' . $field])) {
+                               $row[$field] = $row['item-' . $field];
+                       }
+                       unset($row['item-' . $field]);
+               }
+
+               // We prefer the data from the user's contact over the public one
+               if (!empty($row['author-link']) && !empty($row['contact-link']) &&
+                       ($row['author-link'] == $row['contact-link'])) {
+                       if (isset($row['author-avatar']) && !empty($row['contact-avatar'])) {
+                               $row['author-avatar'] = $row['contact-avatar'];
+                       }
+                       if (isset($row['author-name']) && !empty($row['contact-name'])) {
+                               $row['author-name'] = $row['contact-name'];
+                       }
+               }
+
+               if (!empty($row['owner-link']) && !empty($row['contact-link']) &&
+                       ($row['owner-link'] == $row['contact-link'])) {
+                       if (isset($row['owner-avatar']) && !empty($row['contact-avatar'])) {
+                               $row['owner-avatar'] = $row['contact-avatar'];
+                       }
+                       if (isset($row['owner-name']) && !empty($row['contact-name'])) {
+                               $row['owner-name'] = $row['contact-name'];
+                       }
+               }
+
+               // We can always comment on posts from these networks
+               if (isset($row['writable']) && !empty($row['network']) &&
+                       in_array($row['network'], [NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS])) {
+                       $row['writable'] = true;
+               }
+
+               return $row;
+       }
+
+       /**
+        * @brief Fills an array with data from an item query
+        *
+        * @param object $stmt statement object
+        * @return array Data array
+        */
+       public static function inArray($stmt, $do_close = true) {
+               if (is_bool($stmt)) {
+                       return $stmt;
+               }
+
+               $data = [];
+               while ($row = self::fetch($stmt)) {
+                       $data[] = $row;
+               }
+               if ($do_close) {
+                       dba::close($stmt);
+               }
+               return $data;
+       }
+
+       /**
+        * @brief Check if item data exists
+        *
+        * @param array $condition array of fields for condition
+        *
+        * @return boolean Are there rows for that condition?
+        */
+       public static function exists($condition) {
+               $stmt = self::select(['id'], $condition, ['limit' => 1]);
+
+               if (is_bool($stmt)) {
+                       $retval = $stmt;
+               } else {
+                       $retval = (dba::num_rows($stmt) > 0);
+               }
+
+               dba::close($stmt);
+
+               return $retval;
+       }
+
+       /**
+        * Retrieve a single record from the item table for a given user and returns it in an associative array
         *
         * @brief Retrieve a single record from a table
         * @param integer $uid User ID
@@ -67,15 +176,58 @@ class Item extends BaseObject
         * @return bool|array
         * @see dba::select
         */
-       public static function selectFirst($uid, array $fields = [], array $condition = [], $params = [])
+       public static function selectFirstForUser($uid, array $selected = [], array $condition = [], $params = [])
+       {
+               $params['uid'] = $uid;
+
+               if (empty($selected)) {
+                       $selected = Item::DISPLAY_FIELDLIST;
+               }
+
+               return self::selectFirst($selected, $condition, $params);
+       }
+
+       /**
+        * @brief Select rows from the item table for a given user
+        *
+        * @param integer $uid User ID
+        * @param array  $selected  Array of selected fields, empty for all
+        * @param array  $condition Array of fields for condition
+        * @param array  $params    Array of several parameters
+        *
+        * @return boolean|object
+        */
+       public static function selectForUser($uid, array $selected = [], array $condition = [], $params = [])
+       {
+               $params['uid'] = $uid;
+
+               if (empty($selected)) {
+                       $selected = Item::DISPLAY_FIELDLIST;
+               }
+
+               return self::select($selected, $condition, $params);
+       }
+
+       /**
+        * Retrieve a single record from the item table and returns it in an associative array
+        *
+        * @brief Retrieve a single record from a table
+        * @param array  $fields
+        * @param array  $condition
+        * @param array  $params
+        * @return bool|array
+        * @see dba::select
+        */
+       public static function selectFirst(array $fields = [], array $condition = [], $params = [])
        {
                $params['limit'] = 1;
-               $result = self::select($uid, $fields, $condition, $params);
+
+               $result = self::select($fields, $condition, $params);
 
                if (is_bool($result)) {
                        return $result;
                } else {
-                       $row = dba::fetch($result);
+                       $row = self::fetch($result);
                        dba::close($result);
                        return $row;
                }
@@ -84,15 +236,22 @@ class Item extends BaseObject
        /**
         * @brief Select rows from the item table
         *
-        * @param integer $uid User ID
-        * @param array  $fields    Array of selected fields, empty for all
+        * @param array  $selected  Array of selected fields, empty for all
         * @param array  $condition Array of fields for condition
         * @param array  $params    Array of several parameters
         *
         * @return boolean|object
         */
-       public static function select($uid, array $selected = [], array $condition = [], $params = [])
+       public static function select(array $selected = [], array $condition = [], $params = [])
        {
+               $uid = 0;
+               $usermode = false;
+
+               if (isset($params['uid'])) {
+                       $uid = $params['uid'];
+                       $usermode = true;
+               }
+
                $fields = self::fieldlist($selected);
 
                $select_fields = self::constructSelectFields($fields, $selected);
@@ -101,7 +260,9 @@ class Item extends BaseObject
 
                $condition_string = self::addTablesToFields($condition_string, $fields);
 
-               $condition_string = $condition_string . ' AND ' . self::condition(false);
+               if ($usermode) {
+                       $condition_string = $condition_string . ' AND ' . self::condition(false);
+               }
 
                $param_string = self::addTablesToFields(dba::buildParameter($params), $fields);
 
@@ -112,26 +273,68 @@ class Item extends BaseObject
                return dba::p($sql, $condition);
        }
 
+       /**
+        * @brief Select rows from the starting post in the item table
+        *
+        * @param integer $uid User ID
+        * @param array  $fields    Array of selected fields, empty for all
+        * @param array  $condition Array of fields for condition
+        * @param array  $params    Array of several parameters
+        *
+        * @return boolean|object
+        */
+       public static function selectThreadForUser($uid, array $selected = [], array $condition = [], $params = [])
+       {
+               $params['uid'] = $uid;
+
+               if (empty($selected)) {
+                       $selected = Item::DISPLAY_FIELDLIST;
+               }
+
+               return self::selectThread($selected, $condition, $params);
+       }
+
        /**
         * Retrieve a single record from the starting post in the item table and returns it in an associative array
         *
         * @brief Retrieve a single record from a table
         * @param integer $uid User ID
+        * @param array  $selected
+        * @param array  $condition
+        * @param array  $params
+        * @return bool|array
+        * @see dba::select
+        */
+       public static function selectFirstThreadForUser($uid, array $selected = [], array $condition = [], $params = [])
+       {
+               $params['uid'] = $uid;
+
+               if (empty($selected)) {
+                       $selected = Item::DISPLAY_FIELDLIST;
+               }
+
+               return self::selectFirstThread($selected, $condition, $params);
+       }
+
+       /**
+        * Retrieve a single record from the starting post in the item table and returns it in an associative array
+        *
+        * @brief Retrieve a single record from a table
         * @param array  $fields
         * @param array  $condition
         * @param array  $params
         * @return bool|array
         * @see dba::select
         */
-       public static function selectFirstThread($uid, array $fields = [], array $condition = [], $params = [])
+       public static function selectFirstThread(array $fields = [], array $condition = [], $params = [])
        {
                $params['limit'] = 1;
-               $result = self::selectThread($uid, $fields, $condition, $params);
+               $result = self::selectThread($fields, $condition, $params);
 
                if (is_bool($result)) {
                        return $result;
                } else {
-                       $row = dba::fetch($result);
+                       $row = self::fetch($result);
                        dba::close($result);
                        return $row;
                }
@@ -140,15 +343,22 @@ class Item extends BaseObject
        /**
         * @brief Select rows from the starting post in the item table
         *
-        * @param integer $uid User ID
-        * @param array  $fields    Array of selected fields, empty for all
+        * @param array  $selected  Array of selected fields, empty for all
         * @param array  $condition Array of fields for condition
         * @param array  $params    Array of several parameters
         *
         * @return boolean|object
         */
-       public static function selectThread($uid, array $selected = [], array $condition = [], $params = [])
+       public static function selectThread(array $selected = [], array $condition = [], $params = [])
        {
+               $uid = 0;
+               $usermode = false;
+
+               if (isset($params['uid'])) {
+                       $uid = $params['uid'];
+                       $usermode = true;
+               }
+
                $fields = self::fieldlist($selected);
 
                $threadfields = ['thread' => ['iid', 'uid', 'contact-id', 'owner-id', 'author-id',
@@ -163,7 +373,9 @@ class Item extends BaseObject
                $condition_string = self::addTablesToFields($condition_string, $threadfields);
                $condition_string = self::addTablesToFields($condition_string, $fields);
 
-               $condition_string = $condition_string . ' AND ' . self::condition(true);
+               if ($usermode) {
+                       $condition_string = $condition_string . ' AND ' . self::condition(true);
+               }
 
                $param_string = dba::buildParameter($params);
                $param_string = self::addTablesToFields($param_string, $threadfields);
@@ -183,39 +395,40 @@ class Item extends BaseObject
         */
        private static function fieldlist($selected)
        {
-               /*
-               These Fields are not added below. They are here to for bug search.
-               */
+               $fields = [];
 
-               $item_fields = ['author-id', 'owner-id', 'contact-id', 'uid', 'id', 'parent',
-                       'uri', 'thr-parent', 'parent-uri', 'content-warning',
-                       'commented', 'created', 'edited', 'received', 'verb', 'object-type', 'postopts', 'plink',
-                       'guid', 'wall', 'private', 'starred', 'origin', 'title', 'body', 'file', 'event-id',
-                       'location', 'coord', 'app', 'attach', 'rendered-hash', 'rendered-html', 'object',
-                       'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
-                       'id' => 'item_id', 'network' => 'item_network',
-                       'type', 'extid', 'changed', 'moderated', 'target-type', 'target',
-                       'resource-id', 'tag', 'inform', 'pubmail', 'visible', 'bookmark', 'unseen', 'deleted',
-                       'forum_mode', 'mention', 'global', 'shadow'];
-
-               $author_fields = ['url' => 'author-link', 'name' => 'author-name', 'thumb' => 'author-avatar'];
-               $owner_fields = ['url' => 'owner-link', 'name' => 'owner-name', 'thumb' => 'owner-avatar'];
-               $contact_fields = ['url' => 'contact-link', 'name' => 'contact-name', 'thumb' => 'contact-avatar',
-                       'network', 'url', 'name', 'writable', 'self', 'id' => 'cid', 'alias', 'uid' => 'contact-uid',
+               $fields['item'] = ['id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', 'guid',
+                       'contact-id', 'owner-id', 'author-id', 'type', 'wall', 'gravity', 'extid',
+                       'created', 'edited', 'commented', 'received', 'changed', 'postopts',
+                       'resource-id', 'event-id', 'tag', 'attach', 'inform',
+                       'file', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
+                       'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark',
+                       'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global',
+                       'id' => 'item_id', 'network', 'icid'];
+
+               $fields['item-content'] = self::CONTENT_FIELDLIST;
+
+               $fields['author'] = ['url' => 'author-link', 'name' => 'author-name',
+                       'thumb' => 'author-avatar', 'nick' => 'author-nick'];
+
+               $fields['owner'] = ['url' => 'owner-link', 'name' => 'owner-name',
+                       'thumb' => 'owner-avatar', 'nick' => 'owner-nick'];
+
+               $fields['contact'] = ['url' => 'contact-link', 'name' => 'contact-name', 'thumb' => 'contact-avatar',
+                       'writable', 'self', 'id' => 'cid', 'alias', 'uid' => 'contact-uid',
                        'photo', 'name-date', 'uri-date', 'avatar-date', 'thumb', 'dfrn-id'];
 
-               $event_fields = ['created' => 'event-created', 'edited' => 'event-edited',
+               $fields['parent-item'] = ['guid' => 'parent-guid', 'network' => 'parent-network'];
+
+               $fields['parent-item-author'] = ['url' => 'parent-author-link', 'name' => 'parent-author-name'];
+
+               $fields['event'] = ['created' => 'event-created', 'edited' => 'event-edited',
                        'start' => 'event-start','finish' => 'event-finish',
                        'summary' => 'event-summary','desc' => 'event-desc',
                        'location' => 'event-location', 'type' => 'event-type',
                        'nofinish' => 'event-nofinish','adjust' => 'event-adjust',
                        'ignore' => 'event-ignore', 'id' => 'event-id'];
 
-               $fields = ['item' => $item_fields, 'author' => $author_fields, 'owner' => $owner_fields,
-                       'contact' => $contact_fields, 'event' => $event_fields];
-
-               $fields['parent-item'] = ['guid' => 'parent-guid'];
-               $fields['parent-item-author'] = ['url' => 'parent-author-link', 'name' => 'parent-author-name'];
                $fields['sign'] = ['signed_text', 'signature', 'signer'];
 
                return $fields;
@@ -284,6 +497,10 @@ class Item extends BaseObject
                        $joins .= " LEFT JOIN `sign` ON `sign`.`iid` = `item`.`id`";
                }
 
+               if (strpos($sql_commands, "`item-content`.") !== false) {
+                       $joins .= " LEFT JOIN `item-content` ON `item-content`.`id` = `item`.`icid`";
+               }
+
                if ((strpos($sql_commands, "`parent-item`.") !== false) || (strpos($sql_commands, "`parent-author`.") !== false)) {
                        $joins .= " STRAIGHT_JOIN `item` AS `parent-item` ON `parent-item`.`id` = `item`.`parent`";
                }
@@ -309,10 +526,13 @@ class Item extends BaseObject
                foreach ($fields as $table => $table_fields) {
                        foreach ($table_fields as $field => $select) {
                                if (empty($selected) || in_array($select, $selected)) {
+                                       if (in_array($select, self::CONTENT_FIELDLIST)) {
+                                               $selection[] = "`item`.`".$select."` AS `item-" . $select . "`";
+                                       }
                                        if (is_int($field)) {
-                                               $selection[] = "`" . $table . "`.`".$select."`";
+                                               $selection[] = "`" . $table . "`.`" . $select . "`";
                                        } else {
-                                               $selection[] = "`" . $table . "`.`" . $field . "` AS `".$select ."`";
+                                               $selection[] = "`" . $table . "`.`" . $field . "` AS `" . $select . "`";
                                        }
                                }
                        }
@@ -371,19 +591,34 @@ class Item extends BaseObject
                // We cannot simply expand the condition to check for origin entries
                // The condition needn't to be a simple array but could be a complex condition.
                // And we have to execute this query before the update to ensure to fetch the same data.
-               $items = dba::select('item', ['id', 'origin'], $condition);
+               $items = dba::select('item', ['id', 'origin', 'uri', 'plink'], $condition);
 
-               $success = dba::update('item', $fields, $condition);
+               $content_fields = [];
+               foreach (self::CONTENT_FIELDLIST as $field) {
+                       if (isset($fields[$field])) {
+                               $content_fields[$field] = $fields[$field];
+                               unset($fields[$field]);
+                       }
+               }
 
-               if (!$success) {
-                       dba::close($items);
-                       dba::rollback();
-                       return false;
+               if (!empty($fields)) {
+                       $success = dba::update('item', $fields, $condition);
+
+                       if (!$success) {
+                               dba::close($items);
+                               dba::rollback();
+                               return false;
+                       }
                }
 
+               // When there is no content for the "old" item table, this will count the fetched items
                $rows = dba::affected_rows();
 
                while ($item = dba::fetch($items)) {
+                       if (!empty($item['plink'])) {
+                               $content_fields['plink'] =  $item['plink'];
+                       }
+                       self::updateContent($content_fields, ['uri' => $item['uri']]);
                        Term::insertFromTagFieldByItemId($item['id']);
                        Term::insertFromFileFieldByItemId($item['id']);
                        self::updateThread($item['id']);
@@ -515,15 +750,14 @@ class Item extends BaseObject
                self::deleteTagsFromItem($item);
 
                // Set the item to "deleted"
-               dba::update('item', ['deleted' => true, 'title' => '', 'body' => '',
-                                       'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()],
+               dba::update('item', ['deleted' => true, 'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()],
                                ['id' => $item['id']]);
 
                Term::insertFromTagFieldByItemId($item['id']);
                Term::insertFromFileFieldByItemId($item['id']);
                self::deleteThread($item['id'], $item['parent-uri']);
 
-               if (!dba::exists('item', ["`uri` = ? AND `uid` != 0 AND NOT `deleted`", $item['uri']])) {
+               if (!self::exists(["`uri` = ? AND `uid` != 0 AND NOT `deleted`", $item['uri']])) {
                        self::delete(['uri' => $item['uri'], 'uid' => 0, 'deleted' => false], $priority);
                }
 
@@ -740,18 +974,21 @@ class Item extends BaseObject
                        $item['parent-uri'] = $item['thr-parent'];
                }
 
-               if (x($item, 'gravity')) {
+               $item['type'] = defaults($item, 'type', 'remote');
+
+               if (isset($item['gravity'])) {
                        $item['gravity'] = intval($item['gravity']);
                } elseif ($item['parent-uri'] === $item['uri']) {
-                       $item['gravity'] = 0;
-               } elseif (activity_match($item['verb'],ACTIVITY_POST)) {
-                       $item['gravity'] = 6;
+                       $item['gravity'] = GRAVITY_PARENT;
+               } elseif (activity_match($item['verb'], ACTIVITY_POST)) {
+                       $item['gravity'] = GRAVITY_COMMENT;
+               } elseif ($item['type'] == 'activity') {
+                       $item['gravity'] = GRAVITY_ACTIVITY;
                } else {
-                       $item['gravity'] = 6;   // extensible catchall
+                       $item['gravity'] = GRAVITY_UNKNOWN;   // Should not happen
+                       logger('Unknown gravity for verb: ' . $item['verb'] . ' - type: ' . $item['type'], LOGGER_DEBUG);
                }
 
-               $item['type'] = defaults($item, 'type', 'remote');
-
                $uid = intval($item['uid']);
 
                // check for create date and expire time
@@ -837,6 +1074,7 @@ class Item extends BaseObject
 
                // When there is no content then we don't post it
                if ($item['body'].$item['title'] == '') {
+                       logger('No body, no title.');
                        return 0;
                }
 
@@ -879,11 +1117,12 @@ class Item extends BaseObject
                        return 0;
                }
 
-               //unset($item['author-link']);
+               // These fields aren't stored anymore in the item table, they are fetched upon request
+               unset($item['author-link']);
                unset($item['author-name']);
                unset($item['author-avatar']);
 
-               //unset($item['owner-link']);
+               unset($item['owner-link']);
                unset($item['owner-name']);
                unset($item['owner-avatar']);
 
@@ -902,7 +1141,7 @@ class Item extends BaseObject
                // Checking if there is already an item with the same guid
                logger('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 (dba::exists('item', $condition)) {
+               if (self::exists($condition)) {
                        logger('found item with guid '.$item['guid'].' for user '.$item['uid'].' on network '.$item['network'], LOGGER_DEBUG);
                        return 0;
                }
@@ -988,8 +1227,9 @@ class Item extends BaseObject
                                $user = dba::selectFirst('user', ['nickname'], ['uid' => $item['uid']]);
                                if (DBM::is_result($user)) {
                                        $self = normalise_link(System::baseUrl() . '/profile/' . $user['nickname']);
-                                       logger("'myself' is ".$self." for parent ".$parent_id." checking against ".$item['author-link']." and ".$item['owner-link'], LOGGER_DEBUG);
-                                       if ((normalise_link($item['author-link']) == $self) || (normalise_link($item['owner-link']) == $self)) {
+                                       $self_id = Contact::getIdForURL($self, 0, true);
+                                       logger("'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("tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
                                        }
@@ -1003,7 +1243,7 @@ class Item extends BaseObject
                                        logger('$force_parent=true, reply converted to top-level post.');
                                        $parent_id = 0;
                                        $item['parent-uri'] = $item['uri'];
-                                       $item['gravity'] = 0;
+                                       $item['gravity'] = GRAVITY_PARENT;
                                } else {
                                        logger('item parent '.$item['parent-uri'].' for '.$item['uid'].' was not found - ignoring item');
                                        return 0;
@@ -1015,7 +1255,7 @@ class Item extends BaseObject
 
                $condition = ["`uri` = ? AND `network` IN (?, ?) AND `uid` = ?",
                        $item['uri'], $item['network'], NETWORK_DFRN, $item['uid']];
-               if (dba::exists('item', $condition)) {
+               if (self::exists($condition)) {
                        logger('duplicated item with the same uri found. '.print_r($item,true));
                        return 0;
                }
@@ -1023,7 +1263,7 @@ class Item extends BaseObject
                // On Friendica and Diaspora the GUID is unique
                if (in_array($item['network'], [NETWORK_DFRN, NETWORK_DIASPORA])) {
                        $condition = ['guid' => $item['guid'], 'uid' => $item['uid']];
-                       if (dba::exists('item', $condition)) {
+                       if (self::exists($condition)) {
                                logger('duplicated item with the same guid found. '.print_r($item,true));
                                return 0;
                        }
@@ -1031,7 +1271,7 @@ class Item extends BaseObject
                        // Check for an existing post with the same content. There seems to be a problem with OStatus.
                        $condition = ["`body` = ? AND `network` = ? AND `created` = ? AND `contact-id` = ? AND `uid` = ?",
                                        $item['body'], $item['network'], $item['created'], $item['contact-id'], $item['uid']];
-                       if (dba::exists('item', $condition)) {
+                       if (self::exists($condition)) {
                                logger('duplicated item with the same body found. '.print_r($item,true));
                                return 0;
                        }
@@ -1044,7 +1284,7 @@ class Item extends BaseObject
                        // Set the global flag on all items if this was a global item entry
                        dba::update('item', ['global' => true], ['uri' => $item["uri"]]);
                } else {
-                       $item["global"] = dba::exists('item', ['uid' => 0, 'uri' => $item["uri"]]);
+                       $item["global"] = self::exists(['uid' => 0, 'uri' => $item["uri"]]);
                }
 
                // ACL settings
@@ -1085,7 +1325,7 @@ class Item extends BaseObject
                 * An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this.
                 */
                if ($item["uid"] == 0) {
-                       if (dba::exists('item', ['uri' => trim($item['uri']), 'uid' => 0])) {
+                       if (self::exists(['uri' => trim($item['uri']), 'uid' => 0])) {
                                logger('Global item already stored. URI: '.$item['uri'].' on network '.$item['network'], LOGGER_DEBUG);
                                return 0;
                        }
@@ -1094,6 +1334,7 @@ class Item extends BaseObject
                logger('' . print_r($item,true), LOGGER_DATA);
 
                dba::transaction();
+               self::insertContent($item);
                $ret = dba::insert('item', $item);
 
                // When the item was successfully stored we fetch the ID of the item.
@@ -1164,7 +1405,7 @@ class Item extends BaseObject
 
                // update the commented timestamp on the parent
                // Only update "commented" if it is really a comment
-               if (($item['verb'] == ACTIVITY_POST) || !Config::get("system", "like_no_comment")) {
+               if (($item['gravity'] != GRAVITY_ACTIVITY) || !Config::get("system", "like_no_comment")) {
                        dba::update('item', ['commented' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
                } else {
                        dba::update('item', ['changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
@@ -1241,6 +1482,95 @@ class Item extends BaseObject
                return $current_post;
        }
 
+       /**
+        * @brief Insert a new item content entry
+        *
+        * @param array $item The item fields that are to be inserted
+        */
+       private static function insertContent(&$item)
+       {
+               $fields = ['uri' => $item['uri'], 'plink' => $item['plink'],
+                       'uri-plink-hash' => hash('sha1', $item['plink']).hash('sha1', $item['uri'])];
+
+               foreach (self::CONTENT_FIELDLIST as $field) {
+                       if (isset($item[$field])) {
+                               $fields[$field] = $item[$field];
+                               unset($item[$field]);
+                       }
+               }
+
+               // To avoid timing problems, we are using locks.
+               $locked = Lock::set('item_insert_content');
+               if (!$locked) {
+                       logger("Couldn't acquire lock for URI " . $item['uri'] . " - proceeding anyway.");
+               }
+
+               // Do we already have this content?
+               $item_content = dba::selectFirst('item-content', ['id'], ['uri' => $item['uri']]);
+               if (DBM::is_result($item_content)) {
+                       $item['icid'] = $item_content['id'];
+                       logger('Fetched content for URI ' . $item['uri'] . ' (' . $item['icid'] . ')');
+               } elseif (dba::insert('item-content', $fields)) {
+                       $item['icid'] = dba::lastInsertId();
+                       logger('Inserted content for URI ' . $item['uri'] . ' (' . $item['icid'] . ')');
+               } else {
+                       // By setting the ICID value through the worker we should avoid timing problems.
+                       // When the locking works, this shouldn't be needed. But better be prepared.
+                       Worker::add(PRIORITY_HIGH, 'SetItemContentID', $uri);
+               }
+               if ($locked) {
+                       Lock::remove('item_insert_content');
+               }
+       }
+
+       /**
+        * @brief Set the item content id for a given URI
+        *
+        * @param string $uri The item URI
+        */
+       public static function setICIDforURI($uri)
+       {
+               $item_content = dba::selectFirst('item-content', ['id'], ['uri' => $uri]);
+               if (DBM::is_result($item_content)) {
+                       dba::update('item', ['icid' => $item_content['id']], ['icid' => 0, 'uri' => $uri]);
+                       logger('Asynchronously fetched content id for URI ' . $uri . ' (' . $item_content['id'] . ')');
+               } else {
+                       logger('No item-content found for URI ' . $uri);
+               }
+       }
+
+       /**
+        * @brief Update existing item content entries
+        *
+        * @param array $item The item fields that are to be changed
+        * @param array $condition The condition for finding the item content entries
+        */
+       private static function updateContent($item, $condition)
+       {
+               // We have to select only the fields from the "item-content" table
+               $fields = [];
+               foreach (self::CONTENT_FIELDLIST as $field) {
+                       if (isset($item[$field])) {
+                               $fields[$field] = $item[$field];
+                       }
+               }
+
+               if (empty($fields)) {
+                       return;
+               }
+
+               if (!empty($item['plink'])) {
+                       $fields['uri-plink-hash'] = hash('sha1', $item['plink']) . hash('sha1', $condition['uri']);
+               } else {
+                       // Ensure that we don't delete the plink
+                       unset($fields['plink']);
+               }
+
+               logger('Update content for URI ' . $condition['uri']);
+
+               dba::update('item-content', $fields, $condition, true);
+       }
+
        /**
         * @brief Distributes public items to the receivers
         *
@@ -1259,7 +1589,7 @@ class Item extends BaseObject
                $condition = ['id' => $itemid, 'uid' => 0,
                        'network' => [NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""],
                        'visible' => true, 'deleted' => false, 'moderated' => false, 'private' => false];
-               $item = dba::selectFirst('item', [], ['id' => $itemid]);
+               $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]);
                if (!DBM::is_result($item)) {
                        return;
                }
@@ -1270,8 +1600,6 @@ class Item extends BaseObject
                unset($item['wall']);
                unset($item['origin']);
                unset($item['starred']);
-               unset($item['rendered-hash']);
-               unset($item['rendered-html']);
 
                $users = [];
 
@@ -1387,12 +1715,12 @@ class Item extends BaseObject
                        return;
                }
 
-               $item = dba::selectFirst('item', [], ['id' => $itemid]);
+               $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]);
 
                if (DBM::is_result($item) && ($item["allow_cid"] == '') && ($item["allow_gid"] == '') &&
                        ($item["deny_cid"] == '') && ($item["deny_gid"] == '')) {
 
-                       if (!dba::exists('item', ['uri' => $item['uri'], 'uid' => 0])) {
+                       if (!self::exists(['uri' => $item['uri'], 'uid' => 0])) {
                                // Preparing public shadow (removing user specific data)
                                $item['uid'] = 0;
                                unset($item['id']);
@@ -1401,12 +1729,10 @@ class Item extends BaseObject
                                unset($item['mention']);
                                unset($item['origin']);
                                unset($item['starred']);
-                               unset($item['rendered-hash']);
-                               unset($item['rendered-html']);
                                if ($item['uri'] == $item['parent-uri']) {
-                                       $item['contact-id'] = Contact::getIdForURL($item['owner-link']);
+                                       $item['contact-id'] = $item['owner-id'];
                                } else {
-                                       $item['contact-id'] = Contact::getIdForURL($item['author-link']);
+                                       $item['contact-id'] = $item['author-id'];
                                }
 
                                if (in_array($item['type'], ["net-comment", "wall-comment"])) {
@@ -1431,7 +1757,7 @@ class Item extends BaseObject
         */
        public static function addShadowPost($itemid)
        {
-               $item = dba::selectFirst('item', [], ['id' => $itemid]);
+               $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]);
                if (!DBM::is_result($item)) {
                        return;
                }
@@ -1448,12 +1774,12 @@ class Item extends BaseObject
                }
 
                // Is there a shadow parent?
-               if (!dba::exists('item', ['uri' => $item['parent-uri'], 'uid' => 0])) {
+               if (!self::exists(['uri' => $item['parent-uri'], 'uid' => 0])) {
                        return;
                }
 
                // Is there already a shadow entry?
-               if (dba::exists('item', ['uri' => $item['uri'], 'uid' => 0])) {
+               if (self::exists(['uri' => $item['uri'], 'uid' => 0])) {
                        return;
                }
 
@@ -1469,8 +1795,6 @@ class Item extends BaseObject
                unset($item['mention']);
                unset($item['origin']);
                unset($item['starred']);
-               unset($item['rendered-hash']);
-               unset($item['rendered-html']);
                $item['contact-id'] = Contact::getIdForURL($item['author-link']);
 
                if (in_array($item['type'], ["net-comment", "wall-comment"])) {
@@ -1485,7 +1809,7 @@ class Item extends BaseObject
 
                // If this was a comment to a Diaspora post we don't get our comment back.
                // This means that we have to distribute the comment by ourselves.
-               if ($origin && dba::exists('item', ['id' => $parent, 'network' => NETWORK_DIASPORA])) {
+               if ($origin && self::exists(['id' => $parent, 'network' => NETWORK_DIASPORA])) {
                        self::distribute($public_shadow);
                }
        }
@@ -2295,30 +2619,32 @@ class Item extends BaseObject
                // event participation are essentially radio toggles. If you make a subsequent choice,
                // we need to eradicate your first choice.
                if ($event_verb_flag) {
-                       $verbs = "'" . dbesc(ACTIVITY_ATTEND) . "', '" . dbesc(ACTIVITY_ATTENDNO) . "', '" . dbesc(ACTIVITY_ATTENDMAYBE) . "'";
+                       $verbs = [ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE];
                } else {
-                       $verbs = "'".dbesc($activity)."'";
-               }
-
-               /// @todo This query is expected to be a performance eater due to the "OR" - it has to be changed totally
-               $existing_like = q("SELECT `id`, `guid`, `verb` FROM `item`
-                       WHERE `verb` IN ($verbs)
-                       AND `deleted` = 0
-                       AND `author-id` = %d
-                       AND `uid` = %d
-                       AND (`parent` = '%s' OR `parent-uri` = '%s' OR `thr-parent` = '%s')
-                       LIMIT 1",
-                       intval($author_contact['id']),
-                       intval($item['uid']),
-                       dbesc($item_id), dbesc($item_id), dbesc($item['uri'])
-               );
+                       $verbs = $activity;
+               }
 
-               // If it exists, mark it as deleted
-               if (DBM::is_result($existing_like)) {
-                       $like_item = $existing_like[0];
+               $base_condition = ['verb' => $verbs, 'deleted' => false, 'gravity' => GRAVITY_ACTIVITY,
+                       'author-id' => $author_contact['id'], 'uid' => item['uid']];
 
+               $condition = array_merge($base_condition, ['parent' => $item_id]);
+               $like_item = self::selectFirst(['id', 'guid', 'verb'], $condition);
+
+               if (!DBM::is_result($like_item)) {
+                       $condition = array_merge($base_condition, ['parent-uri' => $item_id]);
+                       $like_item = self::selectFirst(['id', 'guid', 'verb'], $condition);
+               }
+
+               if (!DBM::is_result($like_item)) {
+                       $condition = array_merge($base_condition, ['thr-parent' => $item_id]);
+                       $like_item = self::selectFirst(['id', 'guid', 'verb'], $condition);
+               }
+
+               // If it exists, mark it as deleted
+               if (DBM::is_result($like_item)) {
                        // Already voted, undo it
                        $fields = ['deleted' => true, 'unseen' => true, 'changed' => DateTimeFormat::utcNow()];
+                       /// @todo Consider using self::update - but before doing so, check the side effects
                        dba::update('item', $fields, ['id' => $like_item['id']]);
 
                        // Clean up the Diaspora signatures for this like
@@ -2372,7 +2698,7 @@ EOT;
                        'type'          => 'activity',
                        'wall'          => $item['wall'],
                        'origin'        => 1,
-                       'gravity'       => GRAVITY_LIKE,
+                       'gravity'       => GRAVITY_ACTIVITY,
                        'parent'        => $item['id'],
                        'parent-uri'    => $item['uri'],
                        'thr-parent'    => $item['uri'],
@@ -2438,9 +2764,9 @@ EOT;
 
        private static function updateThread($itemid, $setmention = false)
        {
-               $fields = ['uid', 'guid', 'title', 'body', 'created', 'edited', 'commented', 'received', 'changed',
+               $fields = ['uid', 'guid', 'created', 'edited', 'commented', 'received', 'changed',
                        'wall', 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark', 'contact-id',
-                       'deleted', 'origin', 'forum_mode', 'network', 'author-id', 'owner-id', 'rendered-html', 'rendered-hash'];
+                       'deleted', 'origin', 'forum_mode', 'network', 'author-id', 'owner-id'];
                $condition = ["`id` = ? AND (`parent` = ? OR `parent` = 0)", $itemid, $itemid];
 
                $item = dba::selectFirst('item', $fields, $condition);
@@ -2457,7 +2783,7 @@ EOT;
                $fields = [];
 
                foreach ($item as $field => $data) {
-                       if (!in_array($field, ["guid", "title", "body", "rendered-html", "rendered-hash"])) {
+                       if (!in_array($field, ["guid"])) {
                                $fields[$field] = $data;
                        }
                }
@@ -2465,19 +2791,6 @@ EOT;
                $result = dba::update('thread', $fields, ['iid' => $itemid]);
 
                logger("Update thread for item ".$itemid." - guid ".$item["guid"]." - ".(int)$result, LOGGER_DEBUG);
-
-               // Updating a shadow item entry
-               $items = dba::selectFirst('item', ['id'], ['guid' => $item['guid'], 'uid' => 0]);
-
-               if (!DBM::is_result($items)) {
-                       return;
-               }
-
-               $fields = ['title' => $item['title'], 'body' => $item['body'],
-                       'rendered-html' => $item['rendered-html'], 'rendered-hash' => $item['rendered-hash']];
-               $result = dba::update('item', $fields, ['id' => $items['id']]);
-
-               logger("Updating public shadow for post ".$items["id"]." - guid ".$item["guid"]." Result: ".print_r($result, true), LOGGER_DEBUG);
        }
 
        private static function deleteThread($itemid, $itemuri = "")
@@ -2495,7 +2808,7 @@ EOT;
 
                if ($itemuri != "") {
                        $condition = ["`uri` = ? AND NOT `deleted` AND NOT (`uid` IN (?, 0))", $itemuri, $item["uid"]];
-                       if (!dba::exists('item', $condition)) {
+                       if (!self::exists($condition)) {
                                dba::delete('item', ['uri' => $itemuri, 'uid' => 0]);
                                logger("deleteThread: Deleted shadow for item ".$itemuri, LOGGER_DEBUG);
                        }