X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FItem.php;h=3086a869be8dfaefb3a5df454dc2b4eabc555253;hb=75aa831b32ec55b635fdbb3631b10f3cd0248aca;hp=9fbe973eeddda1225a5c1cd0d91439cc72125cf9;hpb=64cea5ce43fef4291f154a230d86e3cd93d621bc;p=friendica.git diff --git a/src/Model/Item.php b/src/Model/Item.php index 9fbe973eed..3086a869be 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -67,7 +67,7 @@ class Item extends BaseObject // 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', + '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', @@ -78,6 +78,42 @@ class Item extends BaseObject '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]; + + /** + * @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 (is_null($index) || !array_key_exists($index, self::ACTIVITIES)) { + return ''; + } + + return self::ACTIVITIES[$index]; + } + /** * @brief Fetch a single item row * @@ -88,14 +124,12 @@ class Item extends BaseObject { $row = dba::fetch($stmt); - // 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['item-' . $field])) { - $row[$field] = $row['item-' . $field]; - } - unset($row['item-' . $field]); + 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'])) { @@ -117,22 +151,64 @@ class Item extends BaseObject } } - // Build the tag string out of the term entries - if (isset($row['id']) && array_key_exists('tag', $row)) { - $row['tag'] = Term::tagTextFromItemId($row['id']); + // 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; } - // Build the file string out of the term entries - if (isset($row['id']) && array_key_exists('file', $row)) { - $row['file'] = Term::fileTextFromItemId($row['id']); + // ---------------------- 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]); } - // 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; + 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) && empty($row['tag'])) { + $row['tag'] = Term::tagTextFromItemId($row['internal-iid']); + } + + // Build the file string out of the term entries + if (array_key_exists('file', $row) && empty($row['file'])) { + $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; } @@ -279,7 +355,7 @@ class Item extends BaseObject $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; @@ -394,7 +470,7 @@ class Item extends BaseObject $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; @@ -417,7 +493,11 @@ class Item extends BaseObject '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']; + 'id' => 'item_id', 'network', 'icid', 'iaid', 'id' => 'internal-iid', + 'network' => 'internal-network', 'icid' => 'internal-icid', + 'iaid' => 'internal-iaid']; + + $fields['item-activity'] = ['activity', 'activity' => 'internal-activity']; $fields['item-content'] = array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST); @@ -473,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`"; @@ -485,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`"; @@ -510,6 +602,10 @@ class Item extends BaseObject $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`"; } @@ -535,14 +631,15 @@ class Item extends BaseObject */ private static function constructSelectFields($fields, $selected) { - // To be able to fetch the tags we need the item id - if (in_array('tag', $selected) && !in_array('id', $selected)) { - $selected[] = 'id'; + if (!empty($selected)) { + $selected[] = 'internal-iid'; + $selected[] = 'internal-iaid'; + $selected[] = 'internal-icid'; + $selected[] = 'internal-network'; } - // To be able to fetch the files we need the item id - if (in_array('file', $selected) && !in_array('id', $selected)) { - $selected[] = 'id'; + if (in_array('verb', $selected)) { + $selected[] = 'internal-activity'; } $selection = []; @@ -550,7 +647,7 @@ class Item extends BaseObject 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 `item-" . $select . "`"; + $selection[] = "`item`.`".$select."` AS `internal-item-" . $select . "`"; } if (is_int($field)) { $selection[] = "`" . $table . "`.`" . $select . "`"; @@ -614,13 +711,24 @@ 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', 'uri', 'plink', 'icid'], $condition); + $items = dba::select('item', ['id', 'origin', 'uri', 'plink', 'iaid', 'icid', 'tag', 'file'], $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 (in_array($field, self::CONTENT_FIELDLIST)) { + unset($fields[$field]); + } else { + $fields[$field] = null; + } + } + } + + $author_owner_fields = ['author-name', 'author-avatar', 'author-link', 'owner-name', 'owner-avatar', 'owner-link']; + foreach ($author_owner_fields as $field) { + if (isset($fields[$field])) { + $fields[$field] = null; } } @@ -653,30 +761,61 @@ class Item extends BaseObject while ($item = dba::fetch($items)) { if (!empty($item['plink'])) { - $content_fields['plink'] = $item['plink']; + $content_fields['plink'] = $item['plink']; } - self::updateContent($content_fields, ['uri' => $item['uri']]); + if (!empty($item['iaid']) || (!empty($content_fields['verb']) && (self::activityToIndex($content_fields['verb']) >= 0))) { + self::updateActivity($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])) { + 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']]); + } + } + } elseif (!empty($item['icid'])) { + dba::update('item', ['icid' => null], ['id' => $item['id']]); + + if (!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']]); } - dba::update('item', $item_fields, ['id' => $item['id']]); } } if (!empty($tags)) { Term::insertFromTagFieldByItemId($item['id'], $tags); + if (!empty($item['tag'])) { + dba::update('item', ['tag' => ''], ['id' => $item['id']]); + } } if (!empty($files)) { Term::insertFromFileFieldByItemId($item['id'], $files); + if (!empty($item['file'])) { + dba::update('item', ['file' => ''], ['id' => $item['id']]); + } } self::updateThread($item['id']); @@ -1410,7 +1549,9 @@ class Item extends BaseObject } // We are doing this outside of the transaction to avoid timing problems - self::insertContent($item); + if (!self::insertActivity($item)) { + self::insertContent($item); + } dba::transaction(); $ret = dba::insert('item', $item); @@ -1565,6 +1706,55 @@ 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 * @@ -1623,6 +1813,33 @@ class Item extends BaseObject } } + /** + * @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 * @@ -1640,7 +1857,9 @@ class Item extends BaseObject } if (empty($fields)) { - return; + // when there are no fields at all, just use the condition + // This is to ensure that we always store content. + $fields = $condition; } if (!empty($item['plink'])) { @@ -2595,27 +2814,22 @@ class Item extends BaseObject switch ($verb) { case 'like': case 'unlike': - $bodyverb = L10n::t('%1$s likes %2$s\'s %3$s'); $activity = ACTIVITY_LIKE; break; case 'dislike': case 'undislike': - $bodyverb = L10n::t('%1$s doesn\'t like %2$s\'s %3$s'); $activity = ACTIVITY_DISLIKE; break; case 'attendyes': case 'unattendyes': - $bodyverb = L10n::t('%1$s is attending %2$s\'s %3$s'); $activity = ACTIVITY_ATTEND; break; case 'attendno': case 'unattendno': - $bodyverb = L10n::t('%1$s is not attending %2$s\'s %3$s'); $activity = ACTIVITY_ATTENDNO; break; case 'attendmaybe': case 'unattendmaybe': - $bodyverb = L10n::t('%1$s may attend %2$s\'s %3$s'); $activity = ACTIVITY_ATTENDMAYBE; break; default: @@ -2634,6 +2848,8 @@ class Item extends BaseObject return false; } + $item_uri = $item['uri']; + $uid = $item['uid']; if (($uid == 0) && local_user()) { $uid = local_user(); @@ -2654,7 +2870,7 @@ class Item extends BaseObject // Retrieve the current logged in user's public contact $author_id = public_contact(); - $author_contact = dba::selectFirst('contact', [], ['id' => $author_id]); + $author_contact = dba::selectFirst('contact', ['url'], ['id' => $author_id]); if (!DBM::is_result($author_contact)) { logger('like: unknown author ' . $author_id); return false; @@ -2678,26 +2894,21 @@ class Item extends BaseObject // we need to eradicate your first choice. if ($event_verb_flag) { $verbs = [ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE]; + + // Translate to the index based activity index + $activities = []; + foreach ($verbs as $verb) { + $activities[] = self::activityToIndex($verb); + } } else { - $verbs = $activity; + $activities = self::activityToIndex($activity); } - $base_condition = ['verb' => $verbs, 'deleted' => false, 'gravity' => GRAVITY_ACTIVITY, - 'author-id' => $author_contact['id'], 'uid' => item['uid']]; + $condition = ['activity' => $activities, 'deleted' => false, 'gravity' => GRAVITY_ACTIVITY, + 'author-id' => $author_id, 'uid' => $item['uid'], 'thr-parent' => $item_uri]; - $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 @@ -2706,12 +2917,9 @@ class Item extends BaseObject dba::update('item', $fields, ['id' => $like_item['id']]); // Clean up the Diaspora signatures for this like - // Go ahead and do it even if Diaspora support is disabled. We still want to clean up - // if it had been enabled in the past dba::delete('sign', ['iid' => $like_item['id']]); - $like_item_id = $like_item['id']; - Worker::add(PRIORITY_HIGH, "Notifier", "like", $like_item_id); + Worker::add(PRIORITY_HIGH, "Notifier", "like", $like_item['id']); if (!$event_verb_flag || $like_item['verb'] == $activity) { return true; @@ -2723,30 +2931,7 @@ class Item extends BaseObject return true; } - // Else or if event verb different from existing row, create a new item row - $post_type = (($item['resource-id']) ? L10n::t('photo') : L10n::t('status')); - if ($item['object-type'] === ACTIVITY_OBJ_EVENT) { - $post_type = L10n::t('event'); - } $objtype = $item['resource-id'] ? ACTIVITY_OBJ_IMAGE : ACTIVITY_OBJ_NOTE ; - $link = xmlify('' . "\n") ; - $body = $item['body']; - - $obj = <<< EOT - - - $objtype - 1 - {$item['uri']} - $link - - $body - -EOT; - - $ulink = '[url=' . $author_contact['url'] . ']' . $author_contact['name'] . '[/url]'; - $alink = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]'; - $plink = '[url=' . System::baseUrl() . '/display/' . $owner_self_contact['nick'] . '/' . $item['id'] . ']' . $post_type . '[/url]'; $new_item = [ 'guid' => get_guid(32), @@ -2761,17 +2946,10 @@ EOT; 'parent-uri' => $item['uri'], 'thr-parent' => $item['uri'], 'owner-id' => $item['owner-id'], - 'owner-name' => $item['owner-name'], - 'owner-link' => $item['owner-link'], - 'owner-avatar' => $item['owner-avatar'], - 'author-id' => $author_contact['id'], - 'author-name' => $author_contact['name'], - 'author-link' => $author_contact['url'], - 'author-avatar' => $author_contact['thumb'], - 'body' => sprintf($bodyverb, $ulink, $alink, $plink), + 'author-id' => $author_id, + 'body' => $activity, 'verb' => $activity, 'object-type' => $objtype, - 'object' => $obj, 'allow_cid' => $item['allow_cid'], 'allow_gid' => $item['allow_gid'], 'deny_cid' => $item['deny_cid'],