X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FItem.php;h=7c4a3cbffe1f43a74579c1de94a9c8731e1673b6;hb=5a070425601987a5705689e701b131a5b8634489;hp=5fe096c8218647460bd46a174302fbc7b40bef66;hpb=133cd342d462b37bf4ea2d902e96733bba04190f;p=friendica.git diff --git a/src/Model/Item.php b/src/Model/Item.php index 5fe096c821..7c4a3cbffe 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,8 +34,228 @@ 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', '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', + '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 MIXED_CONTENT_FIELDLIST = ['title', 'content-warning', 'body', 'location', + 'coord', 'app', 'rendered-hash', 'rendered-html', 'verb', + 'object-type', 'object', 'target-type', 'target', 'plink']; + + // Field list for "item-content" table that is not present in the "item" table + const CONTENT_FIELDLIST = ['language']; + + // 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', 'iaid', + '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']; + + // 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]; + /** - * Retrieve a single record from the item table and returns it in an associative array + * @brief returns an activity index from an activity string + * + * @param string $activity activity string + * @return integer Activity index + */ + private static function activityToIndex($activity) + { + $index = array_search($activity, self::ACTIVITIES); + + if (is_bool($index)) { + $index = -1; + } + + return $index; + } + + /** + * @brief returns an activity string from an activity index + * + * @param integer $index activity index + * @return string Activity string + */ + private static function indexToActivity($index) + { + if (!isset(self::ACTIVITIES[$index])) { + return ''; + } + + return self::ACTIVITIES[$index]; + } + + /** + * @brief Fetch a single item row + * + * @param mixed $stmt statement object + * @return array current row + */ + public static function fetch($stmt) + { + $row = dba::fetch($stmt); + + if (is_bool($row)) { + return $row; + } + + // ---------------------- Transform item structure data ---------------------- + + // 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 (array_key_exists('writable', $row) && + in_array($row['internal-network'], [NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS])) { + $row['writable'] = true; + } + + // ---------------------- Transform item content data ---------------------- + + // Fetch data from the item-content table whenever there is content there + foreach (self::MIXED_CONTENT_FIELDLIST as $field) { + if (empty($row[$field]) && !empty($row['internal-item-' . $field])) { + $row[$field] = $row['internal-item-' . $field]; + } + unset($row['internal-item-' . $field]); + } + + if (!empty($row['internal-iaid']) && array_key_exists('verb', $row)) { + $row['verb'] = self::indexToActivity($row['internal-activity']); + if (array_key_exists('title', $row)) { + $row['title'] = ''; + } + if (array_key_exists('body', $row)) { + $row['body'] = $row['verb']; + } + if (array_key_exists('object', $row)) { + $row['object'] = ''; + } + if (array_key_exists('object-type', $row)) { + $row['object-type'] = ACTIVITY_OBJ_NOTE; + } + } elseif (in_array($row['verb'], ['', ACTIVITY_POST, ACTIVITY_SHARE])) { + // Posts don't have an object or target - but having tags or files. + // We safe some performance by building tag and file strings only here. + // We remove object and target since they aren't used for this type. + if (array_key_exists('object', $row)) { + $row['object'] = ''; + } + if (array_key_exists('target', $row)) { + $row['target'] = ''; + } + // Build the tag string out of the term entries + if (array_key_exists('tag', $row)) { + $row['tag'] = Term::tagTextFromItemId($row['internal-iid']); + } + + // Build the file string out of the term entries + if (array_key_exists('file', $row)) { + $row['file'] = Term::fileTextFromItemId($row['internal-iid']); + } + } + + // Remove internal fields + unset($row['internal-activity']); + unset($row['internal-network']); + unset($row['internal-iid']); + unset($row['internal-iaid']); + unset($row['internal-icid']); + + 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 @@ -44,15 +265,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; } @@ -61,15 +325,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); @@ -78,37 +349,81 @@ 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); - $table = "`item` " . self::constructJoins($uid, $select_fields . $condition_string . $param_string, false); + $table = "`item` " . self::constructJoins($uid, $select_fields . $condition_string . $param_string, false, $usermode); $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($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; } @@ -117,15 +432,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', @@ -140,13 +462,15 @@ 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); $param_string = self::addTablesToFields($param_string, $fields); - $table = "`thread` " . self::constructJoins($uid, $select_fields . $condition_string . $param_string, true); + $table = "`thread` " . self::constructJoins($uid, $select_fields . $condition_string . $param_string, true, $usermode); $sql = "SELECT " . $select_fields . " FROM " . $table . $condition_string . $param_string; @@ -160,40 +484,45 @@ class Item extends BaseObject */ private static function fieldlist($selected) { - /* - These Fields are not added below. They are here to for bug search. - 'type', 'extid', 'changed', 'moderated', 'target-type', 'target', 'resource-id', - 'tag', 'inform', 'pubmail', 'visible', 'bookmark', 'unseen', 'deleted', - 'forum_mode', 'mention', 'global', 'shadow', - */ - - $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', 'unseen', - 'id' => 'item_id', 'network' => 'item_network']; + $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', 'iaid', 'id' => 'internal-iid', + 'network' => 'internal-network', 'icid' => 'internal-icid', + 'iaid' => 'internal-iaid']; + + $fields['item-activity'] = ['activity' => 'internal-activity']; + + $fields['item-content'] = array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST); - $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']; + $fields['author'] = ['url' => 'author-link', 'name' => 'author-name', + 'thumb' => 'author-avatar', 'nick' => 'author-nick', 'network' => 'author-network']; - $event_fields = ['created' => 'event-created', 'edited' => 'event-edited', + $fields['owner'] = ['url' => 'owner-link', 'name' => 'owner-name', + 'thumb' => 'owner-avatar', 'nick' => 'owner-nick', 'network' => 'owner-network']; + + $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 = ['item' => $item_fields, 'author' => $author_fields, 'owner' => $owner_fields, - 'contact' => $contact_fields, 'event' => $event_fields]; - - if (!empty($selected)) { - $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; } @@ -224,7 +553,7 @@ class Item extends BaseObject * * @return string The SQL joins for the "select" functions */ - private static function constructJoins($uid, $sql_commands, $thread_mode) + private static function constructJoins($uid, $sql_commands, $thread_mode, $user_mode) { if ($thread_mode) { $master_table = "`thread`"; @@ -236,14 +565,26 @@ class Item extends BaseObject $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 ($user_mode) { + $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)); + } else { + if (strpos($sql_commands, "`contact`.") !== false) { + $joins .= "STRAIGHT_JOIN `contact` ON `contact`.`id` = $master_table.`contact-id`"; + } + if (strpos($sql_commands, "`author`.") !== false) { + $joins .= " STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = $master_table.`author-id`"; + } + if (strpos($sql_commands, "`owner`.") !== false) { + $joins .= " STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = $master_table.`owner-id`"; + } + } if (strpos($sql_commands, "`group_member`.") !== false) { $joins .= " STRAIGHT_JOIN `group_member` ON `group_member`.`contact-id` = $master_table.`contact-id`"; @@ -257,6 +598,18 @@ class Item extends BaseObject $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-activity`.") !== false) { + $joins .= " LEFT JOIN `item-activity` ON `item-activity`.`id` = `item`.`iaid`"; + } + + 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`"; } @@ -278,14 +631,28 @@ class Item extends BaseObject */ private static function constructSelectFields($fields, $selected) { + if (!empty($selected)) { + $selected[] = 'internal-iid'; + $selected[] = 'internal-iaid'; + $selected[] = 'internal-icid'; + $selected[] = 'internal-network'; + } + + if (in_array('verb', $selected)) { + $selected[] = 'internal-activity'; + } + $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::MIXED_CONTENT_FIELDLIST)) { + $selection[] = "`item`.`".$select."` AS `internal-item-" . $select . "`"; + } if (is_int($field)) { - $selection[] = "`" . $table . "`.`".$select."`"; + $selection[] = "`" . $table . "`.`" . $select . "`"; } else { - $selection[] = "`" . $table . "`.`" . $field . "` AS `".$select ."`"; + $selection[] = "`" . $table . "`.`" . $field . "` AS `" . $select . "`"; } } } @@ -344,21 +711,88 @@ 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', 'iaid', 'icid'], $condition); - $success = dba::update('item', $fields, $condition); + $content_fields = []; + foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_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 (array_key_exists('tag', $fields)) { + $tags = $fields['tag']; + unset($fields['tag']); + } else { + $tags = ''; + } + + if (array_key_exists('file', $fields)) { + $files = $fields['file']; + unset($fields['file']); + } else { + $files = ''; } + 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)) { - Term::insertFromTagFieldByItemId($item['id']); - Term::insertFromFileFieldByItemId($item['id']); + if (!empty($item['plink'])) { + $content_fields['plink'] = $item['plink']; + } + if (self::updateActivity($content_fields, ['uri' => $item['uri']])) { + if (empty($item['iaid'])) { + $item_activity = dba::selectFirst('item-activity', ['id'], ['uri' => $item['uri']]); + if (DBM::is_result($item_activity)) { + $item_fields = ['iaid' => $item_activity['id'], 'icid' => null]; + foreach (self::MIXED_CONTENT_FIELDLIST as $field) { + $item_fields[$field] = ''; + } + dba::update('item', $item_fields, ['id' => $item['id']]); + + if (!empty($item['icid']) && !dba::exists('item', ['icid' => $item['icid']])) { + dba::delete('item-content', ['id' => $item['icid']]); + } + } + } + } else { + self::updateContent($content_fields, ['uri' => $item['uri']]); + + if (empty($item['icid'])) { + $item_content = dba::selectFirst('item-content', [], ['uri' => $item['uri']]); + if (DBM::is_result($item_content)) { + $item_fields = ['icid' => $item_content['id']]; + // Clear all fields in the item table that have a content in the item-content table + foreach ($item_content as $field => $content) { + if (in_array($field, self::MIXED_CONTENT_FIELDLIST) && !empty($item_content[$field])) { + $item_fields[$field] = ''; + } + } + dba::update('item', $item_fields, ['id' => $item['id']]); + } + } + } + + if (!empty($tags)) { + Term::insertFromTagFieldByItemId($item['id'], $tags); + } + + if (!empty($files)) { + Term::insertFromFileFieldByItemId($item['id'], $files); + } + self::updateThread($item['id']); // We only need to notfiy others when it is an original entry from us. @@ -428,7 +862,7 @@ class Item extends BaseObject $fields = ['id', 'uri', 'uid', 'parent', 'parent-uri', 'origin', 'deleted', 'file', 'resource-id', 'event-id', 'attach', 'verb', 'object-type', 'object', 'target', 'contact-id']; - $item = dba::selectFirst('item', $fields, ['id' => $item_id]); + $item = self::selectFirst($fields, ['id' => $item_id]); if (!DBM::is_result($item)) { logger('Item with ID ' . $item_id . " hasn't been found.", LOGGER_DEBUG); return false; @@ -439,7 +873,7 @@ class Item extends BaseObject return false; } - $parent = dba::selectFirst('item', ['origin'], ['id' => $item['parent']]); + $parent = self::selectFirst(['origin'], ['id' => $item['parent']]); if (!DBM::is_result($parent)) { $parent = ['origin' => false]; } @@ -488,15 +922,18 @@ 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()], - ['id' => $item['id']]); - - Term::insertFromTagFieldByItemId($item['id']); - Term::insertFromFileFieldByItemId($item['id']); + // This erasing of item content is superfluous for items with a matching item-content. + // But for the next time we will still have old content in the item table. + $item_fields = ['deleted' => true, 'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow(), + 'body' => '', 'title' => '', 'content-warning' => '', 'rendered-hash' => '', 'rendered-html' => '', + 'object' => '', 'target' => '', 'tag' => '', 'postopts' => '', 'attach' => '', 'file' => '']; + dba::update('item', $item_fields, ['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); } @@ -516,7 +953,7 @@ class Item extends BaseObject } elseif ($item['uid'] != 0) { // When we delete just our local user copy of an item, we have to set a marker to hide it - $global_item = dba::selectFirst('item', ['id'], ['uri' => $item['uri'], 'uid' => 0, 'deleted' => false]); + $global_item = self::selectFirst(['id'], ['uri' => $item['uri'], 'uid' => 0, 'deleted' => false]); if (DBM::is_result($global_item)) { dba::update('user-item', ['hidden' => true], ['iid' => $global_item['id'], 'uid' => $item['uid']], true); } @@ -540,7 +977,7 @@ class Item extends BaseObject return; } - $i = dba::selectFirst('item', ['id', 'contact-id', 'tag'], ['uri' => $xt->id, 'uid' => $item['uid']]); + $i = self::selectFirst(['id', 'contact-id', 'tag'], ['uri' => $xt->id, 'uid' => $item['uid']]); if (!DBM::is_result($i)) { return; } @@ -675,7 +1112,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); @@ -713,18 +1150,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 @@ -753,7 +1193,7 @@ class Item extends BaseObject $condition = ["`uri` = ? AND `uid` = ? AND `network` IN (?, ?, ?)", trim($item['uri']), $item['uid'], NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS]; - $existing = dba::selectFirst('item', ['id', 'network'], $condition); + $existing = self::selectFirst(['id', 'network'], $condition); if (DBM::is_result($existing)) { // We only log the entries with a different user id than 0. Otherwise we would have too many false positives if ($uid != 0) { @@ -764,7 +1204,7 @@ class Item extends BaseObject } } - self::addLanguageInPostopts($item); + self::addLanguageToItemArray($item); $item['wall'] = intval(defaults($item, 'wall', 0)); $item['extid'] = trim(defaults($item, 'extid', '')); @@ -810,6 +1250,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; } @@ -852,11 +1293,12 @@ class Item extends BaseObject return 0; } - //unset($item['author-link']); - //unset($item['author-name']); - //unset($item['author-avatar']); + // 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']); @@ -875,7 +1317,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; } @@ -908,7 +1350,7 @@ class Item extends BaseObject 'wall', 'private', 'forum_mode', 'origin']; $condition = ['uri' => $item['parent-uri'], 'uid' => $item['uid']]; $params = ['order' => ['id' => false]]; - $parent = dba::selectFirst('item', $fields, $condition, $params); + $parent = self::selectFirst($fields, $condition, $params); if (DBM::is_result($parent)) { // is the new message multi-level threaded? @@ -922,7 +1364,7 @@ class Item extends BaseObject 'parent-uri' => $item['parent-uri'], 'uid' => $item['uid']]; $params = ['order' => ['id' => false]]; - $toplevel_parent = dba::selectFirst('item', $fields, $condition, $params); + $toplevel_parent = self::selectFirst($fields, $condition, $params); if (DBM::is_result($toplevel_parent)) { $parent = $toplevel_parent; @@ -961,8 +1403,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); } @@ -976,7 +1419,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; @@ -988,7 +1431,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; } @@ -996,7 +1439,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; } @@ -1004,7 +1447,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; } @@ -1017,7 +1460,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 @@ -1058,7 +1501,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; } @@ -1066,6 +1509,25 @@ class Item extends BaseObject logger('' . print_r($item,true), LOGGER_DATA); + if (array_key_exists('tag', $item)) { + $tags = $item['tag']; + unset($item['tag']); + } else { + $tags = ''; + } + + if (array_key_exists('file', $item)) { + $files = $item['file']; + unset($item['file']); + } else { + $files = ''; + } + + // We are doing this outside of the transaction to avoid timing problems + if (!self::insertActivity($item)) { + self::insertContent($item); + } + dba::transaction(); $ret = dba::insert('item', $item); @@ -1137,7 +1599,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]); @@ -1170,7 +1632,7 @@ class Item extends BaseObject * in it. */ if (!$deleted && !$dontcache) { - $posted_item = dba::selectFirst('item', [], ['id' => $current_post]); + $posted_item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $current_post]); if (DBM::is_result($posted_item)) { if ($notify) { Addon::callHooks('post_local_end', $posted_item); @@ -1194,8 +1656,13 @@ class Item extends BaseObject * Due to deadlock issues with the "term" table we are doing these steps after the commit. * This is not perfect - but a workable solution until we found the reason for the problem. */ - Term::insertFromTagFieldByItemId($current_post); - Term::insertFromFileFieldByItemId($current_post); + if (!empty($tags)) { + Term::insertFromTagFieldByItemId($current_post, $tags); + } + + if (!empty($files)) { + Term::insertFromFileFieldByItemId($current_post, $files); + } if ($item['parent-uri'] === $item['uri']) { self::addShadow($current_post); @@ -1214,6 +1681,172 @@ 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 insertActivity(&$item) + { + $activity_index = self::activityToIndex($item['verb']); + + if ($activity_index < 0) { + return false; + } + + $fields = ['uri' => $item['uri'], 'activity' => $activity_index, + 'uri-hash' => hash('sha1', $item['uri']) . hash('ripemd160', $item['uri'])]; + + $saved_item = $item; + + // We just remove everything that is content + foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST) as $field) { + unset($item[$field]); + } + + // To avoid timing problems, we are using locks. + $locked = Lock::set('item_insert_activity'); + if (!$locked) { + logger("Couldn't acquire lock for URI " . $item['uri'] . " - proceeding anyway."); + } + + // Do we already have this content? + $item_activity = dba::selectFirst('item-activity', ['id'], ['uri' => $item['uri']]); + if (DBM::is_result($item_activity)) { + $item['iaid'] = $item_activity['id']; + logger('Fetched activity for URI ' . $item['uri'] . ' (' . $item['iaid'] . ')'); + } elseif (dba::insert('item-activity', $fields)) { + $item['iaid'] = dba::lastInsertId(); + logger('Inserted activity for URI ' . $item['uri'] . ' (' . $item['iaid'] . ')'); + } else { + // This shouldn't happen. But if it does, we simply store it in the item-content table + logger('Could not insert activity for URI ' . $item['uri'] . ' - should not happen'); + $item = $saved_item; + return false; + } + if ($locked) { + Lock::remove('item_insert_activity'); + } + return true; + } + + /** + * @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 (array_merge(self::CONTENT_FIELDLIST, self::MIXED_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', $item['uri']); + logger('Could not insert content for URI ' . $item['uri'] . ' - trying asynchronously'); + } + 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 set item content id for URI ' . $uri . ' (' . $item_content['id'] . ') - Affected: '. (int)dba::affected_rows()); + } 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 updateActivity($item, $condition) + { + if (empty($item['verb'])) { + return false; + } + $activity_index = self::activityToIndex($item['verb']); + + if ($activity_index < 0) { + return false; + } + + $fields = ['activity' => $activity_index, + 'uri-hash' => hash('sha1', $condition['uri']) . hash('ripemd160', $condition['uri'])]; + + logger('Update activity for URI ' . $condition['uri']); + + dba::update('item-activity', $fields, $condition, true); + + return true; + } + + /** + * @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 (array_merge(self::CONTENT_FIELDLIST, self::MIXED_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 * @@ -1223,7 +1856,7 @@ class Item extends BaseObject public static function distribute($itemid, $signed_text = '') { $condition = ["`id` IN (SELECT `parent` FROM `item` WHERE `id` = ?)", $itemid]; - $parent = dba::selectFirst('item', ['owner-id'], $condition); + $parent = self::selectFirst(['owner-id'], $condition); if (!DBM::is_result($parent)) { return; } @@ -1232,7 +1865,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; } @@ -1243,8 +1876,6 @@ class Item extends BaseObject unset($item['wall']); unset($item['origin']); unset($item['starred']); - unset($item['rendered-hash']); - unset($item['rendered-html']); $users = []; @@ -1258,7 +1889,7 @@ class Item extends BaseObject $origin_uid = 0; if ($item['uri'] != $item['parent-uri']) { - $parents = dba::select('item', ['uid', 'origin'], ["`uri` = ? AND `uid` != 0", $item['parent-uri']]); + $parents = self::select(['uid', 'origin'], ["`uri` = ? AND `uid` != 0", $item['parent-uri']]); while ($parent = dba::fetch($parents)) { $users[$parent['uid']] = $parent['uid']; if ($parent['origin'] && !$item['origin']) { @@ -1337,9 +1968,9 @@ class Item extends BaseObject */ public static function addShadow($itemid) { - $fields = ['uid', 'private', 'moderated', 'visible', 'deleted', 'network']; + $fields = ['uid', 'private', 'moderated', 'visible', 'deleted', 'network', 'uri']; $condition = ['id' => $itemid, 'parent' => [0, $itemid]]; - $item = dba::selectFirst('item', $fields, $condition); + $item = self::selectFirst($fields, $condition); if (!DBM::is_result($item)) { return; @@ -1360,38 +1991,36 @@ class Item extends BaseObject return; } - $item = dba::selectFirst('item', [], ['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])) { - // Preparing public shadow (removing user specific data) - $item['uid'] = 0; - unset($item['id']); - unset($item['parent']); - unset($item['wall']); - 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']); - } else { - $item['contact-id'] = Contact::getIdForURL($item['author-link']); - } + if (self::exists(['uri' => $item['uri'], 'uid' => 0])) { + return; + } - if (in_array($item['type'], ["net-comment", "wall-comment"])) { - $item['type'] = 'remote-comment'; - } elseif ($item['type'] == 'wall') { - $item['type'] = 'remote'; - } + $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]); - $public_shadow = self::insert($item, false, false, true); + if (DBM::is_result($item)) { + // Preparing public shadow (removing user specific data) + $item['uid'] = 0; + unset($item['id']); + unset($item['parent']); + unset($item['wall']); + unset($item['mention']); + unset($item['origin']); + unset($item['starred']); + if ($item['uri'] == $item['parent-uri']) { + $item['contact-id'] = $item['owner-id']; + } else { + $item['contact-id'] = $item['author-id']; + } - logger("Stored public shadow for thread ".$itemid." under id ".$public_shadow, LOGGER_DEBUG); + if (in_array($item['type'], ["net-comment", "wall-comment"])) { + $item['type'] = 'remote-comment'; + } elseif ($item['type'] == 'wall') { + $item['type'] = 'remote'; } + + $public_shadow = self::insert($item, false, false, true); + + logger("Stored public shadow for thread ".$itemid." under id ".$public_shadow, LOGGER_DEBUG); } } @@ -1404,7 +2033,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; } @@ -1421,12 +2050,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; } @@ -1442,8 +2071,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"])) { @@ -1458,45 +2085,25 @@ 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); } } /** - * Adds a "lang" specification in a "postopts" element of given $arr, - * if possible and not already present. + * Adds a language specification in a "language" element of given $arr. * Expects "body" element to exist in $arr. */ - private static function addLanguageInPostopts(&$item) + private static function addLanguageToItemArray(&$item) { - $postopts = ""; - - if (!empty($item['postopts'])) { - if (strstr($item['postopts'], 'lang=')) { - // do not override - return; - } - $postopts = $item['postopts']; - } - $naked_body = Text\BBCode::toPlaintext($item['body'], false); - $languages = (new Text_LanguageDetect())->detect($naked_body, 3); + $ld = new Text_LanguageDetect(); + $ld->setNameMode(2); + $languages = $ld->detect($naked_body, 3); - if (sizeof($languages) > 0) { - if ($postopts != '') { - $postopts .= '&'; // arbitrary separator, to be reviewed - } - - $postopts .= 'lang='; - $sep = ""; - - foreach ($languages as $language => $score) { - $postopts .= $sep . $language . ";" . $score; - $sep = ':'; - } - $item['postopts'] = $postopts; + if (is_array($languages)) { + $item['language'] = json_encode($languages); } } @@ -1528,6 +2135,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 * @@ -1646,7 +2276,7 @@ class Item extends BaseObject public static function getGuidById($id) { - $item = dba::selectFirst('item', ['guid'], ['id' => $id]); + $item = self::selectFirst(['guid'], ['id' => $id]); if (DBM::is_result($item)) { return $item['guid']; } else { @@ -1680,8 +2310,6 @@ class Item extends BaseObject $item = dba::fetch_first("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid` WHERE `item`.`visible` AND NOT `item`.`deleted` AND NOT `item`.`moderated` - AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = '' - AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = '' AND NOT `item`.`private` AND `item`.`wall` AND `item`.`guid` = ?", $guid); if (DBM::is_result($item)) { @@ -1710,7 +2338,7 @@ class Item extends BaseObject $community_page = (($user['page-flags'] == PAGE_COMMUNITY) ? true : false); $prvgroup = (($user['page-flags'] == PAGE_PRVGROUP) ? true : false); - $item = dba::selectFirst('item', [], ['id' => $item_id]); + $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $item_id]); if (!DBM::is_result($item)) { return; } @@ -1844,7 +2472,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; @@ -1983,17 +2611,8 @@ class Item extends BaseObject private static function hasPermissions($obj) { - return ( - ( - x($obj, 'allow_cid') - ) || ( - x($obj, 'allow_gid') - ) || ( - x($obj, 'deny_cid') - ) || ( - x($obj, 'deny_gid') - ) - ); + return !empty($obj['allow_cid']) || !empty($obj['allow_gid']) || + !empty($obj['deny_cid']) || !empty($obj['deny_gid']); } private static function samePermissions($obj1, $obj2) @@ -2059,74 +2678,79 @@ class Item extends BaseObject return; } + $condition = ["`uid` = ? AND NOT `deleted` AND `id` = `parent` AND `gravity` = ?", + $uid, GRAVITY_PARENT]; + /* * $expire_network_only = save your own wall posts * and just expire conversations started by others */ - $expire_network_only = PConfig::get($uid,'expire', 'network_only'); - $sql_extra = (intval($expire_network_only) ? " AND wall = 0 " : ""); + $expire_network_only = PConfig::get($uid, 'expire', 'network_only', false); + + if ($expire_network_only) { + $condition[0] .= " AND NOT `wall`"; + } if ($network != "") { - $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network)); + $condition[0] .= " AND `network` = ?"; + $condition[] = $network; /* * There is an index "uid_network_received" but not "uid_network_created" * This avoids the creation of another index just for one purpose. * And it doesn't really matter wether to look at "received" or "created" */ - $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY "; + $condition[0] .= " AND `received` < UTC_TIMESTAMP() - INTERVAL ? DAY"; + $condition[] = $days; } else { - $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY "; + $condition[0] .= " AND `created` < UTC_TIMESTAMP() - INTERVAL ? DAY"; + $condition[] = $days; } - $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item` - WHERE `uid` = %d $range - AND `id` = `parent` - $sql_extra - AND `deleted` = 0", - intval($uid), - intval($days) - ); + $items = self::select(['file', 'resource-id', 'starred', 'type', 'id'], $condition); - if (!DBM::is_result($r)) { + if (!DBM::is_result($items)) { return; } - $expire_items = PConfig::get($uid, 'expire', 'items', 1); + $expire_items = PConfig::get($uid, 'expire', 'items', true); // Forcing expiring of items - but not notes and marked items if ($force) { $expire_items = true; } - $expire_notes = PConfig::get($uid, 'expire', 'notes', 1); - $expire_starred = PConfig::get($uid, 'expire', 'starred', 1); - $expire_photos = PConfig::get($uid, 'expire', 'photos', 0); - - logger('User '.$uid.': expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos"); + $expire_notes = PConfig::get($uid, 'expire', 'notes', true); + $expire_starred = PConfig::get($uid, 'expire', 'starred', true); + $expire_photos = PConfig::get($uid, 'expire', 'photos', false); - foreach ($r as $item) { + $expired = 0; + while ($item = Item::fetch($items)) { // don't expire filed items - if (strpos($item['file'],'[') !== false) { + if (strpos($item['file'], '[') !== false) { continue; } // Only expire posts, not photos and photo comments - if ($expire_photos == 0 && strlen($item['resource-id'])) { + if (!$expire_photos && strlen($item['resource-id'])) { continue; - } elseif ($expire_starred == 0 && intval($item['starred'])) { + } elseif (!$expire_starred && intval($item['starred'])) { continue; - } elseif ($expire_notes == 0 && $item['type'] == 'note') { + } elseif (!$expire_notes && $item['type'] == 'note') { continue; - } elseif ($expire_items == 0 && $item['type'] != 'note') { + } elseif (!$expire_items && $item['type'] != 'note') { continue; } self::deleteById($item['id'], PRIORITY_LOW); + + ++$expired; } + dba::close($items); + logger('User ' . $uid . ": expired $expired items; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos"); } public static function firstPostDate($uid, $wall = false) @@ -2196,7 +2820,7 @@ class Item extends BaseObject logger('like: verb ' . $verb . ' item ' . $item_id); - $item = dba::selectFirst('item', [], ['`id` = ? OR `uri` = ?', $item_id, $item_id]); + $item = self::selectFirst(self::ITEM_FIELDLIST, ['`id` = ? OR `uri` = ?', $item_id, $item_id]); if (!DBM::is_result($item)) { logger('like: unknown item ' . $item_id); return false; @@ -2245,30 +2869,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 @@ -2316,13 +2942,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'], @@ -2371,7 +2997,7 @@ EOT; '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); + $item = self::selectFirst($fields, $condition); if (!DBM::is_result($item)) { return; @@ -2388,12 +3014,12 @@ 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); + $item = self::selectFirst($fields, $condition); if (!DBM::is_result($item)) { return; } @@ -2407,7 +3033,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; } } @@ -2415,19 +3041,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 = "") @@ -2445,7 +3058,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); }