X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FItem.php;h=8a5264a7bcd49e4c3a7892d0865686de5feb58e6;hb=6b31e72905a826b4e25c50a2be87657178634056;hp=4e67083f99a8a6354fb88d3c7db99d50f455f957;hpb=a50805c37103fc6c44ce2f1562dee4fc91479b89;p=friendica.git diff --git a/src/Model/Item.php b/src/Model/Item.php index 4e67083f99..8a5264a7bc 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -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; @@ -33,6 +34,538 @@ 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', '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', + 'author-id', 'author-link', 'author-name', 'author-avatar', + 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', + 'contact-id', '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', + 'event-nofinish', 'event-adjust', 'event-ignore', 'event-id']; + + // 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', + 'private', 'title', 'body', 'location', 'coord', 'app', + 'attach', 'tag', 'bookmark', 'deleted', 'extid', + 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', + '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']; + + /** + * @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 + * @param array $fields + * @param array $condition + * @param array $params + * @return bool|array + * @see dba::select + */ + 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($fields, $condition, $params); + + if (is_bool($result)) { + return $result; + } else { + $row = self::fetch($result); + dba::close($result); + return $row; + } + } + + /** + * @brief Select rows from the item table + * + * @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(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); + + $condition_string = dba::buildCondition($condition); + + $condition_string = self::addTablesToFields($condition_string, $fields); + + if ($usermode) { + $condition_string = $condition_string . ' AND ' . self::condition(false); + } + + $param_string = self::addTablesToFields(dba::buildParameter($params), $fields); + + $table = "`item` " . self::constructJoins($uid, $select_fields . $condition_string . $param_string, false); + + $sql = "SELECT " . $select_fields . " FROM " . $table . $condition_string . $param_string; + + 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(array $fields = [], array $condition = [], $params = []) + { + $params['limit'] = 1; + $result = self::selectThread($fields, $condition, $params); + + if (is_bool($result)) { + return $result; + } else { + $row = self::fetch($result); + dba::close($result); + return $row; + } + } + + /** + * @brief Select rows from the starting post in the item table + * + * @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(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', + 'created', 'edited', 'commented', 'received', 'changed', 'wall', 'private', + 'pubmail', 'moderated', 'visible', 'starred', 'ignored', 'bookmark', + 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'network']]; + + $select_fields = self::constructSelectFields($fields, $selected); + + $condition_string = dba::buildCondition($condition); + + $condition_string = self::addTablesToFields($condition_string, $threadfields); + $condition_string = self::addTablesToFields($condition_string, $fields); + + if ($usermode) { + $condition_string = $condition_string . ' AND ' . self::condition(true); + } + + $param_string = dba::buildParameter($params); + $param_string = self::addTablesToFields($param_string, $threadfields); + $param_string = self::addTablesToFields($param_string, $fields); + + $table = "`thread` " . self::constructJoins($uid, $select_fields . $condition_string . $param_string, true); + + $sql = "SELECT " . $select_fields . " FROM " . $table . $condition_string . $param_string; + + return dba::p($sql, $condition); + } + + /** + * @brief Returns a list of fields that are associated with the item table + * + * @return array field list + */ + private static function fieldlist($selected) + { + $fields = []; + + $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']; + + $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['sign'] = ['signed_text', 'signature', 'signer']; + + return $fields; + } + + /** + * @brief Returns SQL condition for the "select" functions + * + * @param boolean $thread_mode Called for the items (false) or for the threads (true) + * + * @return string SQL condition + */ + private static function condition($thread_mode) + { + if ($thread_mode) { + $master_table = "`thread`"; + } else { + $master_table = "`item`"; + } + return "$master_table.`visible` AND NOT $master_table.`deleted` AND NOT $master_table.`moderated` AND (`user-item`.`hidden` IS NULL OR NOT `user-item`.`hidden`) "; + } + + /** + * @brief Returns all needed "JOIN" commands for the "select" functions + * + * @param integer $uid User ID + * @param string $sql_commands The parts of the built SQL commands in the "select" functions + * @param boolean $thread_mode Called for the items (false) or for the threads (true) + * + * @return string The SQL joins for the "select" functions + */ + private static function constructJoins($uid, $sql_commands, $thread_mode) + { + if ($thread_mode) { + $master_table = "`thread`"; + $master_table_key = "`thread`.`iid`"; + $joins = "STRAIGHT_JOIN `item` ON `item`.`id` = `thread`.`iid` "; + } else { + $master_table = "`item`"; + $master_table_key = "`item`.`id`"; + $joins = ''; + } + + $joins .= sprintf("STRAIGHT_JOIN `contact` ON `contact`.`id` = $master_table.`contact-id` + AND NOT `contact`.`blocked` + AND ((NOT `contact`.`readonly` AND NOT `contact`.`pending` AND (`contact`.`rel` IN (%s, %s))) + OR `contact`.`self` OR (`item`.`id` != `item`.`parent`) OR `contact`.`uid` = 0) + STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = $master_table.`author-id` AND NOT `author`.`blocked` + STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = $master_table.`owner-id` AND NOT `owner`.`blocked` + LEFT JOIN `user-item` ON `user-item`.`iid` = $master_table_key AND `user-item`.`uid` = %d", + CONTACT_IS_SHARING, CONTACT_IS_FRIEND, intval($uid)); + + if (strpos($sql_commands, "`group_member`.") !== false) { + $joins .= " STRAIGHT_JOIN `group_member` ON `group_member`.`contact-id` = $master_table.`contact-id`"; + } + + if (strpos($sql_commands, "`user`.") !== false) { + $joins .= " STRAIGHT_JOIN `user` ON `user`.`uid` = $master_table.`uid`"; + } + + if (strpos($sql_commands, "`event`.") !== false) { + $joins .= " LEFT JOIN `event` ON `event-id` = `event`.`id`"; + } + + if (strpos($sql_commands, "`sign`.") !== false) { + $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`"; + } + + if (strpos($sql_commands, "`parent-item-author`.") !== false) { + $joins .= " STRAIGHT_JOIN `contact` AS `parent-item-author` ON `parent-item-author`.`id` = `parent-item`.`author-id`"; + } + + return $joins; + } + + /** + * @brief Add the field list for the "select" functions + * + * @param array $fields The field definition array + * @param array $selected The array with the selected fields from the "select" functions + * + * @return string The field list + */ + private static function constructSelectFields($fields, $selected) + { + $selection = []; + 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 . "`"; + } else { + $selection[] = "`" . $table . "`.`" . $field . "` AS `" . $select . "`"; + } + } + } + } + return implode(", ", $selection); + } + + /** + * @brief add table definition to fields in an SQL query + * + * @param string $query SQL query + * @param array $fields The field definition array + * + * @return string the changed SQL query + */ + private static function addTablesToFields($query, $fields) + { + foreach ($fields as $table => $table_fields) { + foreach ($table_fields as $alias => $field) { + if (is_int($alias)) { + $replace_field = $field; + } else { + $replace_field = $alias; + } + + $search = "/([^\.])`" . $field . "`/i"; + $replace = "$1`" . $table . "`.`" . $replace_field . "`"; + $query = preg_replace($search, $replace, $query); + } + } + return $query; + } + /** * @brief Update existing item entries * @@ -58,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); + + $content_fields = []; + foreach (self::CONTENT_FIELDLIST as $field) { + if (isset($fields[$field])) { + $content_fields[$field] = $fields[$field]; + unset($fields[$field]); + } + } - $success = dba::update('item', $fields, $condition); + if (!empty($fields)) { + $success = dba::update('item', $fields, $condition); - if (!$success) { - dba::close($items); - dba::rollback(); - return false; + 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']); @@ -102,6 +650,32 @@ class Item extends BaseObject dba::close($items); } + /** + * @brief Delete an item for an user and notify others about it - if it was ours + * + * @param array $condition The condition for finding the item entries + * @param integer $uid User who wants to delete this item + */ + public static function deleteForUser($condition, $uid) + { + if ($uid == 0) { + return; + } + + $items = dba::select('item', ['id', 'uid'], $condition); + while ($item = dba::fetch($items)) { + // "Deleting" global items just means hiding them + if ($item['uid'] == 0) { + dba::update('user-item', ['hidden' => true], ['iid' => $item['id'], 'uid' => $uid], true); + } elseif ($item['uid'] == $uid) { + self::deleteById($item['id'], PRIORITY_HIGH); + } else { + logger('Wrong ownership. Not deleting item ' . $item['id']); + } + } + dba::close($items); + } + /** * @brief Delete an item and notify others about it - if it was ours * @@ -110,7 +684,7 @@ class Item extends BaseObject * * @return boolean success */ - public static function deleteById($item_id, $priority = PRIORITY_HIGH) + private static function deleteById($item_id, $priority = PRIORITY_HIGH) { // locate item to be deleted $fields = ['id', 'uri', 'uid', 'parent', 'parent-uri', 'origin', @@ -176,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); } @@ -210,7 +783,6 @@ class Item extends BaseObject } } - logger('Item with ID ' . $item_id . " has been deleted.", LOGGER_DEBUG); return true; @@ -364,7 +936,7 @@ class Item extends BaseObject } $item['guid'] = self::guid($item, $notify); - $item['uri'] = notags(trim(defaults($item, 'uri', item_new_uri($a->get_hostname(), $item['uid'], $item['guid'])))); + $item['uri'] = notags(trim(defaults($item, 'uri', self::newURI($item['uid'], $item['guid'])))); // Store conversation data $item = Conversation::insert($item); @@ -402,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 @@ -499,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; } @@ -541,6 +1117,15 @@ class Item extends BaseObject return 0; } + // 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-name']); + unset($item['owner-avatar']); + if ($item['network'] == NETWORK_PHANTOM) { logger('Missing network. Called by: '.System::callstack(), LOGGER_DEBUG); @@ -556,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; } @@ -642,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); } @@ -657,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; @@ -669,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; } @@ -677,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; } @@ -685,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; } @@ -698,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 @@ -739,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; } @@ -748,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. @@ -818,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]); @@ -895,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 * @@ -913,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; } @@ -924,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 = []; @@ -1041,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']); @@ -1055,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"])) { @@ -1085,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; } @@ -1097,16 +1769,17 @@ class Item extends BaseObject } // Is this a shadow entry? - if ($item['uid'] == 0) + if ($item['uid'] == 0) { return; + } // 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; } @@ -1122,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"])) { @@ -1138,10 +1809,8 @@ 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) { - if (dba::exists('item', ['id' => $parent, 'network' => NETWORK_DIASPORA])) { - self::distribute($public_shadow); - } + if ($origin && self::exists(['id' => $parent, 'network' => NETWORK_DIASPORA])) { + self::distribute($public_shadow); } } @@ -1152,14 +1821,14 @@ class Item extends BaseObject */ private static function addLanguageInPostopts(&$item) { + $postopts = ""; + if (!empty($item['postopts'])) { if (strstr($item['postopts'], 'lang=')) { // do not override return; } $postopts = $item['postopts']; - } else { - $postopts = ""; } $naked_body = Text\BBCode::toPlaintext($item['body'], false); @@ -1210,6 +1879,29 @@ class Item extends BaseObject return $guid_prefix.$host_hash; } + /** + * generate an unique URI + * + * @param integer $uid User id + * @param string $guid An existing GUID (Otherwise it will be generated) + * + * @return string + */ + public static function newURI($uid, $guid = "") + { + if ($guid == "") { + $guid = get_guid(32); + } + + $hostname = self::getApp()->get_hostname(); + + $user = dba::selectFirst('user', ['nickname'], ['uid' => $uid]); + + $uri = "urn:X-dfrn:" . $hostname . ':' . $user['nickname'] . ':' . $guid; + + return $uri; + } + /** * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact * @@ -1459,8 +2151,7 @@ class Item extends BaseObject $forum_mode = ($prvgroup ? 2 : 1); $fields = ['wall' => true, 'origin' => true, 'forum_mode' => $forum_mode, 'contact-id' => $self['id'], - 'owner-id' => $owner_id, 'owner-name' => $self['name'], 'owner-link' => $self['url'], - 'owner-avatar' => $self['thumb'], 'private' => $private, 'allow_cid' => $user['allow_cid'], + 'owner-id' => $owner_id, 'owner-link' => $self['url'], 'private' => $private, 'allow_cid' => $user['allow_cid'], 'allow_gid' => $user['allow_gid'], 'deny_cid' => $user['deny_cid'], 'deny_gid' => $user['deny_gid']]; dba::update('item', $fields, ['id' => $item_id]); @@ -1527,7 +2218,7 @@ class Item extends BaseObject if ($contact['network'] != NETWORK_FEED) { $datarray["guid"] = get_guid(32); unset($datarray["plink"]); - $datarray["uri"] = item_new_uri($a->get_hostname(), $contact['uid'], $datarray["guid"]); + $datarray["uri"] = self::newURI($contact['uid'], $datarray["guid"]); $datarray["parent-uri"] = $datarray["uri"]; $datarray["thr-parent"] = $datarray["uri"]; $datarray["extid"] = NETWORK_DFRN; @@ -1928,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 @@ -1999,13 +2692,13 @@ EOT; $new_item = [ 'guid' => get_guid(32), - 'uri' => item_new_uri(self::getApp()->get_hostname(), $item['uid']), + 'uri' => self::newURI($item['uid']), 'uid' => $item['uid'], 'contact-id' => $item_contact_id, 'type' => 'activity', 'wall' => $item['wall'], 'origin' => 1, - 'gravity' => GRAVITY_LIKE, + 'gravity' => GRAVITY_ACTIVITY, 'parent' => $item['id'], 'parent-uri' => $item['uri'], 'thr-parent' => $item['uri'], @@ -2051,7 +2744,7 @@ EOT; private static function addThread($itemid, $onlyshadow = false) { $fields = ['uid', 'created', 'edited', 'commented', 'received', 'changed', 'wall', 'private', 'pubmail', - 'moderated', 'visible', 'spam', 'starred', 'bookmark', 'contact-id', + 'moderated', 'visible', 'starred', 'bookmark', 'contact-id', 'deleted', 'origin', 'forum_mode', 'mention', 'network', 'author-id', 'owner-id']; $condition = ["`id` = ? AND (`parent` = ? OR `parent` = 0)", $itemid, $itemid]; $item = dba::selectFirst('item', $fields, $condition); @@ -2071,9 +2764,9 @@ EOT; private static function updateThread($itemid, $setmention = false) { - $fields = ['uid', 'guid', 'title', 'body', 'created', 'edited', 'commented', 'received', 'changed', - 'wall', 'private', 'pubmail', 'moderated', 'visible', 'spam', 'starred', 'bookmark', 'contact-id', - 'deleted', 'origin', 'forum_mode', 'network', 'author-id', 'owner-id', 'rendered-html', 'rendered-hash']; + $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']; $condition = ["`id` = ? AND (`parent` = ? OR `parent` = 0)", $itemid, $itemid]; $item = dba::selectFirst('item', $fields, $condition); @@ -2090,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; } } @@ -2098,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 = "") @@ -2128,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); }