X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FItem.php;h=d41e84c5b9f44c5e6ff0b1d61cb15c9d542c56a3;hb=f9994548c1f1110c7f548e00fcf1b6ee42b9de3b;hp=46d28ee8227f7baf5db9d5b42f4284468fa95839;hpb=f91ab57543d1a5d1e492606a0ebefe48d8be71f3;p=friendica.git diff --git a/src/Model/Item.php b/src/Model/Item.php index 46d28ee822..d41e84c5b9 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -34,7 +34,7 @@ use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\DI; -use Friendica\Model\Post\Category; +use Friendica\Model\Post; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Diaspora; @@ -71,13 +71,11 @@ class Item const PT_FETCHED = 75; const PT_PERSONAL_NOTE = 128; - const LOCK_INSERT = 'item-insert'; - // Field list that is used to display the items const DISPLAY_FIELDLIST = [ 'uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', 'network', 'gravity', 'commented', 'created', 'edited', 'received', 'verb', 'object-type', 'postopts', 'plink', - 'wall', 'private', 'starred', 'origin', 'title', 'body', 'file', 'attach', 'language', + 'wall', 'private', 'starred', 'origin', 'title', 'body', 'file', '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', @@ -95,7 +93,7 @@ class Item const DELIVER_FIELDLIST = ['uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', 'parent-guid', 'created', 'edited', 'verb', 'object-type', 'object', 'target', 'private', 'title', 'body', 'location', 'coord', 'app', - 'attach', 'deleted', 'extid', 'post-type', 'gravity', + 'deleted', 'extid', 'post-type', 'gravity', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'author-id', 'author-link', 'owner-link', 'contact-uid', 'signed_text', 'signature', 'signer', 'network']; @@ -113,7 +111,7 @@ class Item 'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'vid', 'contact-id', 'type', 'wall', 'gravity', 'extid', 'icid', 'psid', 'created', 'edited', 'commented', 'received', 'changed', 'verb', - 'postopts', 'plink', 'resource-id', 'event-id', 'attach', 'inform', + 'postopts', 'plink', 'resource-id', 'event-id', 'inform', 'file', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'post-type', 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark', 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'network', @@ -235,6 +233,8 @@ class Item return $row; } + $row = DBA::castFields('item', $row); + // ---------------------- Transform item structure data ---------------------- // We prefer the data from the user's contact over the public one @@ -310,7 +310,7 @@ class Item if (!array_key_exists('verb', $row) || in_array($row['verb'], ['', Activity::POST, Activity::SHARE])) { // Build the file string out of the term entries if (array_key_exists('file', $row) && empty($row['file'])) { - $row['file'] = Category::getTextByURIId($row['internal-uri-id'], $row['internal-uid']); + $row['file'] = Post\Category::getTextByURIId($row['internal-uri-id'], $row['internal-uid']); } } @@ -659,7 +659,7 @@ class Item 'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'vid', 'causer-id', 'contact-id', 'owner-id', 'author-id', 'type', 'wall', 'gravity', 'extid', 'created', 'edited', 'commented', 'received', 'changed', 'psid', - 'resource-id', 'event-id', 'attach', 'post-type', 'file', + 'resource-id', 'event-id', 'post-type', 'file', 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark', 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'id' => 'item_id', 'network', 'icid', @@ -911,6 +911,8 @@ class Item return false; } + $data_fields = $fields; + // To ensure the data integrity we do it in an transaction DBA::transaction(); @@ -967,7 +969,17 @@ class Item $notify_items = []; while ($item = DBA::fetch($items)) { + Post\User::update($item['uri-id'], $item['uid'], $data_fields); + if (empty($content_fields['verb']) || !in_array($content_fields['verb'], self::ACTIVITIES)) { + if (!empty($content_fields['body'])) { + $content_fields['raw-body'] = trim($content_fields['raw-body'] ?? $content_fields['body']); + + // Remove all media attachments from the body and store them in the post-media table + $content_fields['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $content_fields['raw-body']); + $content_fields['raw-body'] = self::setHashtags($content_fields['raw-body']); + } + self::updateContent($content_fields, ['uri-id' => $item['uri-id']]); if (empty($item['icid'])) { @@ -988,12 +1000,16 @@ class Item } if (!is_null($files)) { - Category::storeTextByURIId($item['uri-id'], $item['uid'], $files); + Post\Category::storeTextByURIId($item['uri-id'], $item['uid'], $files); if (!empty($item['file'])) { DBA::update('item', ['file' => ''], ['id' => $item['id']]); } } + if (!empty($fields['attach'])) { + Post\Media::insertFromAttachment($item['uri-id'], $fields['attach']); + } + Post\DeliveryData::update($item['uri-id'], $delivery_data); self::updateThread($item['id']); @@ -1044,14 +1060,13 @@ class Item return; } - $items = self::select(['id', 'uid'], $condition); + $items = self::select(['id', 'uid', 'uri-id'], $condition); while ($item = self::fetch($items)) { + Post\User::update($item['uri-id'], $item['uid'], ['hidden' => true]); + // "Deleting" global items just means hiding them if ($item['uid'] == 0) { DBA::update('user-item', ['hidden' => true], ['iid' => $item['id'], 'uid' => $uid], true); - - // Delete notifications - DBA::delete('notify', ['iid' => $item['id'], 'uid' => $uid]); } elseif ($item['uid'] == $uid) { self::markForDeletionById($item['id'], PRIORITY_HIGH); } else { @@ -1075,7 +1090,7 @@ class Item Logger::info('Mark item for deletion by id', ['id' => $item_id, 'callstack' => System::callstack()]); // locate item to be deleted $fields = ['id', 'uri', 'uri-id', 'uid', 'parent', 'parent-uri', 'origin', - 'deleted', 'file', 'resource-id', 'event-id', 'attach', + 'deleted', 'file', 'resource-id', 'event-id', 'verb', 'object-type', 'object', 'target', 'contact-id', 'icid', 'psid', 'gravity']; $item = self::selectFirst($fields, ['id' => $item_id]); @@ -1132,22 +1147,18 @@ class Item } // If item has attachments, drop them - /// @TODO: this should first check if attachment is used elsewhere - foreach (explode(",", $item['attach']) as $attach) { - preg_match("|attach/(\d+)|", $attach, $matches); - if (is_array($matches) && count($matches) > 1) { + $attachments = Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT]); + foreach($attachments as $attachment) { + if (preg_match("|attach/(\d+)|", $attachment['url'], $matches)) { Attach::delete(['id' => $matches[1], 'uid' => $item['uid']]); } } - // Delete notifications - DBA::delete('notify', ['iid' => $item['id'], 'uid' => $item['uid']]); - // Set the item to "deleted" $item_fields = ['deleted' => true, 'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()]; DBA::update('item', $item_fields, ['id' => $item['id']]); - Category::storeTextByURIId($item['uri-id'], $item['uid'], ''); + Post\Category::storeTextByURIId($item['uri-id'], $item['uid'], ''); self::deleteThread($item['id'], $item['parent-uri']); if (!self::exists(["`uri` = ? AND `uid` != 0 AND NOT `deleted`", $item['uri']])) { @@ -1156,9 +1167,6 @@ class Item Post\DeliveryData::delete($item['uri-id']); - if (!empty($item['icid']) && !self::exists(['icid' => $item['icid'], 'deleted' => false])) { - DBA::delete('item-content', ['id' => $item['icid']], ['cascade' => false]); - } // When the permission set will be used in photo and events as well, // this query here needs to be extended. // @todo Currently deactivated. We need the permission set in the deletion process. @@ -1173,13 +1181,16 @@ class Item } // Is it our comment and/or our thread? - if ($item['origin'] || $parent['origin']) { + if (($item['origin'] || $parent['origin']) && ($item['uid'] != 0)) { // When we delete the original post we will delete all existing copies on the server as well self::markForDeletion(['uri' => $item['uri'], 'deleted' => false], $priority); // send the notification upstream/downstream - Worker::add(['priority' => $priority, 'dont_fork' => true], "Notifier", Delivery::DELETION, intval($item['id'])); + if ($priority) { + Worker::add(['priority' => $priority, 'dont_fork' => true], "Notifier", Delivery::DELETION, intval($item['id'])); + } } elseif ($item['uid'] != 0) { + Post\User::update($item['uri-id'], $item['uid'], ['hidden' => true]); // When we delete just our local user copy of an item, we have to set a marker to hide it $global_item = self::selectFirst(['id'], ['uri' => $item['uri'], 'uid' => 0, 'deleted' => false]); @@ -1368,32 +1379,11 @@ class Item public static function isValid(array $item) { // When there is no content then we don't post it - if ($item['body'].$item['title'] == '') { + if ($item['body'] . $item['title'] == '') { Logger::notice('No body, no title.'); return false; } - // check for create date and expire time - $expire_interval = DI::config()->get('system', 'dbclean-expire-days', 0); - - $user = DBA::selectFirst('user', ['expire'], ['uid' => $item['uid']]); - if (DBA::isResult($user) && ($user['expire'] > 0) && (($user['expire'] < $expire_interval) || ($expire_interval == 0))) { - $expire_interval = $user['expire']; - } - - if (($expire_interval > 0) && !empty($item['created'])) { - $expire_date = time() - ($expire_interval * 86400); - $created_date = strtotime($item['created']); - if ($created_date < $expire_date) { - Logger::notice('Item created before expiration interval.', [ - 'created' => date('c', $created_date), - 'expired' => date('c', $expire_date), - '$item' => $item - ]); - return false; - } - } - if (!empty($item['author-id']) && Contact::isBlocked($item['author-id'])) { Logger::notice('Author is blocked node-wide', ['author-link' => $item['author-link'], 'item-uri' => $item['uri']]); return false; @@ -1404,11 +1394,6 @@ class Item return false; } - if (!empty($item['uid']) && !empty($item['author-id']) && Contact\User::isBlocked($item['author-id'], $item['uid'])) { - Logger::notice('Author is blocked by user', ['author-link' => $item['author-link'], 'uid' => $item['uid'], 'item-uri' => $item['uri']]); - return false; - } - if (!empty($item['owner-id']) && Contact::isBlocked($item['owner-id'])) { Logger::notice('Owner is blocked node-wide', ['owner-link' => $item['owner-link'], 'item-uri' => $item['uri']]); return false; @@ -1419,22 +1404,10 @@ class Item return false; } - if (!empty($item['uid']) && !empty($item['owner-id']) && Contact\User::isBlocked($item['owner-id'], $item['uid'])) { - Logger::notice('Owner is blocked by user', ['owner-link' => $item['owner-link'], 'uid' => $item['uid'], 'item-uri' => $item['uri']]); + if (!empty($item['uid']) && !self::isAllowedByUser($item, $item['uid'])) { return false; } - // The causer is set during a thread completion, for example because of a reshare. It countains the responsible actor. - if (!empty($item['uid']) && !empty($item['causer-id']) && Contact\User::isBlocked($item['causer-id'], $item['uid'])) { - Logger::notice('Causer is blocked by user', ['causer-link' => $item['causer-link'], 'uid' => $item['uid'], 'item-uri' => $item['uri']]); - return false; - } - - if (!empty($item['uid']) && !empty($item['causer-id']) && ($item['parent-uri'] == $item['uri']) && Contact\User::isIgnored($item['causer-id'], $item['uid'])) { - Logger::notice('Causer is ignored by user', ['causer-link' => $item['causer-link'], 'uid' => $item['uid'], 'item-uri' => $item['uri']]); - return false; - } - if ($item['verb'] == Activity::FOLLOW) { if (!$item['origin'] && ($item['author-id'] == Contact::getPublicIdByUserId($item['uid']))) { // Our own follow request can be relayed to us. We don't store it to avoid notification chaos. @@ -1454,6 +1427,38 @@ class Item return true; } + /** + * Check if the item array is too old + * + * @param array $item + * @return boolean item is too old + */ + public static function isTooOld(array $item) + { + // check for create date and expire time + $expire_interval = DI::config()->get('system', 'dbclean-expire-days', 0); + + $user = DBA::selectFirst('user', ['expire'], ['uid' => $item['uid']]); + if (DBA::isResult($user) && ($user['expire'] > 0) && (($user['expire'] < $expire_interval) || ($expire_interval == 0))) { + $expire_interval = $user['expire']; + } + + if (($expire_interval > 0) && !empty($item['created'])) { + $expire_date = time() - ($expire_interval * 86400); + $created_date = strtotime($item['created']); + if ($created_date < $expire_date) { + Logger::notice('Item created before expiration interval.', [ + 'created' => date('c', $created_date), + 'expired' => date('c', $expire_date), + '$item' => $item + ]); + return true; + } + } + + return false; + } + /** * Return the id of the given item array if it has been stored before * @@ -1486,88 +1491,41 @@ class Item } /** - * Fetch parent data for the given item array + * Fetch top-level parent data for the given item array * * @param array $item * @return array item array with parent data + * @throws \Exception */ - private static function getParentData(array $item) + private static function getTopLevelParent(array $item) { - // find the parent and snarf the item id and ACLs - // and anything else we need to inherit - - $fields = ['uri', 'parent-uri', 'id', 'deleted', + $fields = ['uid', 'uri', 'parent-uri', 'id', 'deleted', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'wall', 'private', 'forum_mode', 'origin', 'author-id']; - $condition = ['uri' => $item['parent-uri'], 'uid' => $item['uid']]; + $condition = ['uri' => $item['thr-parent'], 'uid' => $item['uid']]; $params = ['order' => ['id' => false]]; $parent = self::selectFirst($fields, $condition, $params); if (!DBA::isResult($parent)) { - Logger::info('item parent was not found - ignoring item', ['parent-uri' => $item['parent-uri'], 'uid' => $item['uid']]); + Logger::notice('item parent was not found - ignoring item', ['thr-parent' => $item['thr-parent'], 'uid' => $item['uid']]); return []; - } else { - // is the new message multi-level threaded? - // even though we don't support it now, preserve the info - // and re-attach to the conversation parent. - if ($parent['uri'] != $parent['parent-uri']) { - $item['parent-uri'] = $parent['parent-uri']; - - $condition = ['uri' => $item['parent-uri'], - 'parent-uri' => $item['parent-uri'], - 'uid' => $item['uid']]; - $params = ['order' => ['id' => false]]; - $toplevel_parent = self::selectFirst($fields, $condition, $params); - - if (DBA::isResult($toplevel_parent)) { - $parent = $toplevel_parent; - } - } - - $item['parent'] = $parent['id']; - $item["deleted"] = $parent['deleted']; - $item["allow_cid"] = $parent['allow_cid']; - $item['allow_gid'] = $parent['allow_gid']; - $item['deny_cid'] = $parent['deny_cid']; - $item['deny_gid'] = $parent['deny_gid']; - $item['parent_origin'] = $parent['origin']; - - // Don't federate received participation messages - if ($item['verb'] != Activity::FOLLOW) { - $item['wall'] = $parent['wall']; - } else { - $item['wall'] = false; - } - - /* - * If the parent is private, force privacy for the entire conversation - * This differs from the above settings as it subtly allows comments from - * email correspondents to be private even if the overall thread is not. - */ - if ($parent['private']) { - $item['private'] = $parent['private']; - } - - /* - * Edge case. We host a public forum that was originally posted to privately. - * The original author commented, but as this is a comment, the permissions - * weren't fixed up so it will still show the comment as private unless we fix it here. - */ - if ((intval($parent['forum_mode']) == 1) && ($parent['private'] != self::PUBLIC)) { - $item['private'] = self::PUBLIC; - } + } - // If its a post that originated here then tag the thread as "mention" - if ($item['origin'] && $item['uid']) { - DBA::update('thread', ['mention' => true], ['iid' => $item['parent']]); - Logger::info('tagged thread as mention', ['parent' => $item['parent'], 'uid' => $item['uid']]); - } + if ($parent['uri'] == $parent['parent-uri']) { + return $parent; + } - // Update the contact relations - Contact\Relation::store($parent['author-id'], $item['author-id'], $item['created']); + $condition = ['uri' => $parent['parent-uri'], + 'parent-uri' => $parent['parent-uri'], + 'uid' => $parent['uid']]; + $params = ['order' => ['id' => false]]; + $toplevel_parent = self::selectFirst($fields, $condition, $params); + if (!DBA::isResult($toplevel_parent)) { + Logger::notice('item top level parent was not found - ignoring item', ['parent-uri' => $parent['parent-uri'], 'uid' => $parent['uid']]); + return []; } - return $item; + return $toplevel_parent; } /** @@ -1608,7 +1566,7 @@ class Item $item['wall'] = 1; $item['origin'] = 1; $item['network'] = Protocol::DFRN; - $item['protocol'] = Conversation::PARCEL_DFRN; + $item['protocol'] = Conversation::PARCEL_DIRECT; if (is_int($notify)) { $priority = $notify; @@ -1620,18 +1578,19 @@ class Item $uid = intval($item['uid']); $item['guid'] = self::guid($item, $notify); - $item['uri'] = substr(Strings::escapeTags(trim(($item['uri'] ?? '') ?: self::newURI($item['uid'], $item['guid']))), 0, 255); + $item['uri'] = substr(trim($item['uri'] ?? '') ?: self::newURI($item['uid'], $item['guid']), 0, 255); // Store URI data $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]); + // Backward compatibility: parent-uri used to be the direct parent uri. + // If it is provided without a thr-parent, it probably is the old behavior. + $item['thr-parent'] = trim($item['thr-parent'] ?? $item['parent-uri'] ?? $item['uri']); + $item['parent-uri'] = $item['thr-parent']; + // Store conversation data $item = Conversation::insert($item); - if (!empty($item['thr-parent'])) { - $item['parent-uri'] = $item['thr-parent']; - } - /* * Do we already have this item? * We have to check several networks since Friendica posts could be repeated @@ -1666,7 +1625,6 @@ class Item $item['coord'] = trim($item['coord'] ?? ''); $item['visible'] = (isset($item['visible']) ? intval($item['visible']) : 1); $item['deleted'] = 0; - $item['parent-uri'] = trim(($item['parent-uri'] ?? '') ?: $item['uri']); $item['post-type'] = ($item['post-type'] ?? '') ?: self::PT_ARTICLE; $item['verb'] = trim($item['verb'] ?? ''); $item['object-type'] = trim($item['object-type'] ?? ''); @@ -1681,7 +1639,6 @@ class Item $item['private'] = intval($item['private'] ?? self::PUBLIC); $item['body'] = trim($item['body'] ?? ''); $item['raw-body'] = trim($item['raw-body'] ?? $item['body']); - $item['attach'] = trim($item['attach'] ?? ''); $item['app'] = trim($item['app'] ?? ''); $item['origin'] = intval($item['origin'] ?? 0); $item['postopts'] = trim($item['postopts'] ?? ''); @@ -1730,6 +1687,68 @@ class Item return 0; } + if ($item['gravity'] !== GRAVITY_PARENT) { + $toplevel_parent = self::getTopLevelParent($item); + if (empty($toplevel_parent)) { + return 0; + } + + // If the thread originated from this node, we check the permission against the thread starter + $condition = ['uri' => $toplevel_parent['uri'], 'wall' => true]; + $localTopLevelParent = self::selectFirst(['uid'], $condition); + if (!empty($localTopLevelParent['uid']) && !self::isAllowedByUser($item, $localTopLevelParent['uid'])) { + return 0; + } + + $parent_id = $toplevel_parent['id']; + $item['parent-uri'] = $toplevel_parent['uri']; + $item['deleted'] = $toplevel_parent['deleted']; + $item['allow_cid'] = $toplevel_parent['allow_cid']; + $item['allow_gid'] = $toplevel_parent['allow_gid']; + $item['deny_cid'] = $toplevel_parent['deny_cid']; + $item['deny_gid'] = $toplevel_parent['deny_gid']; + $parent_origin = $toplevel_parent['origin']; + + // Don't federate received participation messages + if ($item['verb'] != Activity::FOLLOW) { + $item['wall'] = $toplevel_parent['wall']; + } else { + $item['wall'] = false; + } + + /* + * If the parent is private, force privacy for the entire conversation + * This differs from the above settings as it subtly allows comments from + * email correspondents to be private even if the overall thread is not. + */ + if ($toplevel_parent['private']) { + $item['private'] = $toplevel_parent['private']; + } + + /* + * Edge case. We host a public forum that was originally posted to privately. + * The original author commented, but as this is a comment, the permissions + * weren't fixed up so it will still show the comment as private unless we fix it here. + */ + if ((intval($toplevel_parent['forum_mode']) == 1) && ($toplevel_parent['private'] != self::PUBLIC)) { + $item['private'] = self::PUBLIC; + } + + // If its a post that originated here then tag the thread as "mention" + if ($item['origin'] && $item['uid']) { + DBA::update('thread', ['mention' => true], ['iid' => $parent_id]); + Logger::info('tagged thread as mention', ['parent' => $parent_id, 'uid' => $item['uid']]); + } + + // Update the contact relations + Contact\Relation::store($toplevel_parent['author-id'], $item['author-id'], $item['created']); + + unset($item['parent_origin']); + } else { + $parent_id = 0; + $parent_origin = $item['origin']; + } + // We don't store the causer link, only the id unset($item['causer-link']); @@ -1743,23 +1762,6 @@ class Item unset($item['owner-name']); unset($item['owner-avatar']); - $item['thr-parent'] = $item['parent-uri']; - - if ($item['parent-uri'] != $item['uri']) { - $item = self::getParentData($item); - if (empty($item)) { - return 0; - } - - $parent_id = $item['parent']; - unset($item['parent']); - $parent_origin = $item['parent_origin']; - unset($item['parent_origin']); - } else { - $parent_id = 0; - $parent_origin = $item['origin']; - } - $item['parent-uri-id'] = ItemURI::getIdByURI($item['parent-uri']); $item['thr-parent-id'] = ItemURI::getIdByURI($item['thr-parent']); @@ -1783,11 +1785,13 @@ class Item $item['parent'] = $parent_id; Hook::callAll('post_local', $item); unset($item['edit']); - unset($item['parent']); } else { Hook::callAll('post_remote', $item); } + // Set after the insert because top-level posts are self-referencing + unset($item['parent']); + if (!empty($item['cancel'])) { Logger::log('post cancelled by addon.'); return 0; @@ -1826,6 +1830,10 @@ class Item // Check for hashtags in the body and repair or add hashtag links $item['body'] = self::setHashtags($item['body']); + if (!empty($item['attach'])) { + Post\Media::insertFromAttachment($item['uri-id'], $item['attach']); + } + // Fill the cache field self::putInCache($item); @@ -1865,7 +1873,7 @@ class Item // Attached file links if (!empty($item['file'])) { - Category::storeTextByURIId($item['uri-id'], $item['uid'], $item['file']); + Post\Category::storeTextByURIId($item['uri-id'], $item['uid'], $item['file']); } unset($item['file']); @@ -1884,17 +1892,16 @@ class Item Tag::storeFromBody($item['uri-id'], $body); } - // Remove all fields that aren't part of the item table - foreach ($item as $field => $value) { - if (!in_array($field, $structure['item'])) { - unset($item[$field]); + if (Post\User::insert($item['uri-id'], $item['uid'], $item)) { + // Remove all fields that aren't part of the item table + foreach ($item as $field => $value) { + if (!in_array($field, $structure['item'])) { + unset($item[$field]); + } } - } - if (DI::lock()->acquire(self::LOCK_INSERT, 0)) { $condition = ['uri-id' => $item['uri-id'], 'uid' => $item['uid'], 'network' => $item['network']]; if (DBA::exists('item', $condition)) { - DI::lock()->release(self::LOCK_INSERT); Logger::notice('Item is already inserted - aborting', $condition); return 0; } @@ -1903,11 +1910,9 @@ class Item // When the item was successfully stored we fetch the ID of the item. $current_post = DBA::lastInsertId(); - DI::lock()->release(self::LOCK_INSERT); } else { - Logger::warning('Item lock had not been acquired'); - $result = false; - $current_post = 0; + Logger::notice('Post-User is already inserted - aborting', ['uid' => $item['uid'], 'uri-id' => $item['uri-id']]); + return 0; } if (empty($current_post) || !DBA::isResult($result)) { @@ -1919,7 +1924,7 @@ class Item Logger::notice('created item', ['id' => $current_post, 'uid' => $item['uid'], 'network' => $item['network'], 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]); - if (!$parent_id || ($item['parent-uri'] === $item['uri'])) { + if (!$parent_id || ($item['gravity'] === GRAVITY_PARENT)) { $parent_id = $current_post; } @@ -1944,7 +1949,7 @@ class Item DBA::update('item', ['changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]); } - if ($item['parent-uri'] === $item['uri']) { + if ($item['gravity'] === GRAVITY_PARENT) { self::addThread($current_post); } else { self::updateThread($parent_id); @@ -1972,7 +1977,7 @@ class Item } } - if ($item['parent-uri'] === $item['uri']) { + if ($item['gravity'] === GRAVITY_PARENT) { self::addShadow($current_post); } else { self::addShadowPost($current_post); @@ -1987,6 +1992,9 @@ class Item // Distribute items to users who subscribed to their tags self::distributeByTags($item); + // Automatically reshare the item if the "remote_self" option is selected + self::autoReshare($item); + $transmit = $notify || ($item['visible'] && ($parent_origin || $item['origin'])); if ($transmit) { @@ -2465,12 +2473,15 @@ class Item */ private static function getLanguage(array $item) { - if (!in_array($item['gravity'], [GRAVITY_PARENT, GRAVITY_COMMENT])) { + if (!in_array($item['gravity'], [GRAVITY_PARENT, GRAVITY_COMMENT]) || empty($item['body'])) { return ''; } // Convert attachments to links $naked_body = BBCode::removeAttachment($item['body']); + if (empty($naked_body)) { + return ''; + } // Remove links and pictures $naked_body = BBCode::removeLinks($naked_body); @@ -2788,6 +2799,31 @@ class Item return false; } + /** + * Automatically reshare the item if the "remote_self" option is selected + * + * @param array $item + * @return void + */ + private static function autoReshare(array $item) + { + if ($item['gravity'] != GRAVITY_PARENT) { + return; + } + + if (!DBA::exists('contact', ['id' => $item['contact-id'], 'remote_self' => Contact::MIRROR_NATIVE_RESHARE])) { + return; + } + + if (!in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN])) { + return; + } + + Logger::info('Automatically reshare item', ['uid' => $item['uid'], 'id' => $item['id'], 'guid' => $item['guid'], 'uri-id' => $item['uri-id']]); + + Item::performActivity($item['id'], 'announce', $item['uid']); + } + public static function isRemoteSelf($contact, &$datarray) { if (!$contact['remote_self']) { @@ -2819,7 +2855,7 @@ class Item $datarray2 = $datarray; Logger::info('remote-self start', ['contact' => $contact['url'], 'remote_self'=> $contact['remote_self'], 'item' => $datarray]); - if ($contact['remote_self'] == 2) { + if ($contact['remote_self'] == Contact::MIRROR_OWN_POST) { $self = DBA::selectFirst('contact', ['id', 'name', 'url', 'thumb'], ['uid' => $contact['uid'], 'self' => true]); if (DBA::isResult($self)) { @@ -2841,14 +2877,20 @@ class Item } if ($contact['network'] != Protocol::FEED) { + $old_uri_id = $datarray["uri-id"] ?? 0; $datarray["guid"] = System::createUUID(); unset($datarray["plink"]); $datarray["uri"] = self::newURI($contact['uid'], $datarray["guid"]); - $datarray["parent-uri"] = $datarray["uri"]; - $datarray["thr-parent"] = $datarray["uri"]; + $datarray["uri-id"] = ItemURI::getIdByURI($datarray["uri"]); $datarray["extid"] = Protocol::DFRN; $urlpart = parse_url($datarray2['author-link']); $datarray["app"] = $urlpart["host"]; + if (!empty($old_uri_id)) { + Post\Media::copy($old_uri_id, $datarray["uri-id"]); + } + + unset($datarray["parent-uri"]); + unset($datarray["thr-parent"]); } else { $datarray['private'] = self::PUBLIC; } @@ -3025,7 +3067,7 @@ class Item return $recipients; } - public static function expire($uid, $days, $network = "", $force = false) + public static function expire(int $uid, int $days, string $network = "", bool $force = false) { if (!$uid || ($days < 1)) { return; @@ -3071,6 +3113,8 @@ class Item $expired = 0; + $priority = DI::config()->get('system', 'expire-notify-priority'); + while ($item = Item::fetch($items)) { // don't expire filed items @@ -3090,7 +3134,7 @@ class Item continue; } - self::markForDeletionById($item['id'], PRIORITY_LOW); + self::markForDeletionById($item['id'], $priority); ++$expired; } @@ -3280,7 +3324,6 @@ class Item 'network' => Protocol::DFRN, 'gravity' => GRAVITY_ACTIVITY, 'parent' => $item['id'], - 'parent-uri' => $item['uri'], 'thr-parent' => $item['uri'], 'owner-id' => $author_id, 'author-id' => $author_id, @@ -3384,6 +3427,37 @@ class Item } } + /** + * Fetch the SQL condition for the given user id + * + * @param integer $owner_id User ID for which the permissions should be fetched + * @return array condition + */ + public static function getPermissionsConditionArrayByUserId(int $owner_id) + { + $local_user = local_user(); + $remote_user = Session::getRemoteContactID($owner_id); + + // default permissions - anonymous user + $condition = ["`private` != ?", self::PRIVATE]; + + if ($local_user && ($local_user == $owner_id)) { + // Profile owner - everything is visible + $condition = []; + } elseif ($remote_user) { + // Authenticated visitor - fetch the matching permissionsets + $set = PermissionSet::get($owner_id, $remote_user); + if (!empty($set)) { + $condition = ["(`private` != ? OR (`private` = ? AND `wall` + AND `psid` IN (" . implode(', ', array_fill(0, count($set), '?')) . ")))", + Item::PRIVATE, Item::PRIVATE]; + $condition = array_merge($condition, $set); + } + } + + return $condition; + } + public static function getPermissionsSQLByUserId($owner_id) { $local_user = local_user(); @@ -3455,20 +3529,21 @@ class Item */ public static function putInCache(&$item, $update = false) { - $body = $item["body"]; + // Save original body to prevent addons to modify it + $body = $item['body']; $rendered_hash = $item['rendered-hash'] ?? ''; $rendered_html = $item['rendered-html'] ?? ''; if ($rendered_hash == '' - || $rendered_html == "" - || $rendered_hash != hash("md5", $item["body"]) - || DI::config()->get("system", "ignore_cache") + || $rendered_html == '' + || $rendered_hash != hash('md5', BBCode::VERSION . '::' . $body) + || DI::config()->get('system', 'ignore_cache') ) { self::addRedirToImageTags($item); - $item["rendered-html"] = BBCode::convert($item["body"]); - $item["rendered-hash"] = hash("md5", $item["body"]); + $item['rendered-html'] = BBCode::convert($item['body']); + $item['rendered-hash'] = hash('md5', BBCode::VERSION . '::' . $body); $hook_data = ['item' => $item, 'rendered-html' => $item['rendered-html'], 'rendered-hash' => $item['rendered-hash']]; Hook::callAll('put_item_in_cache', $hook_data); @@ -3477,27 +3552,27 @@ class Item unset($hook_data); // Force an update if the generated values differ from the existing ones - if ($rendered_hash != $item["rendered-hash"]) { + if ($rendered_hash != $item['rendered-hash']) { $update = true; } // Only compare the HTML when we forcefully ignore the cache - if (DI::config()->get("system", "ignore_cache") && ($rendered_html != $item["rendered-html"])) { + if (DI::config()->get('system', 'ignore_cache') && ($rendered_html != $item['rendered-html'])) { $update = true; } - if ($update && !empty($item["id"])) { + if ($update && !empty($item['id'])) { self::update( [ - 'rendered-html' => $item["rendered-html"], - 'rendered-hash' => $item["rendered-hash"] + 'rendered-html' => $item['rendered-html'], + 'rendered-hash' => $item['rendered-hash'] ], - ['id' => $item["id"]] + ['id' => $item['id']] ); } } - $item["body"] = $body; + $item['body'] = $body; } /** @@ -3604,12 +3679,10 @@ class Item $as = ''; $vhead = false; - $matches = []; - preg_match_all('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\"(?: title=\"(.*?)\")?|', $item['attach'], $matches, PREG_SET_ORDER); - foreach ($matches as $mtch) { - $mime = $mtch[3]; + foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT, Post\Media::TORRENT, Post\Media::UNKNOWN]) as $attachment) { + $mime = $attachment['mimetype']; - $the_url = Contact::magicLinkById($item['author-id'], $mtch[1]); + $the_url = Contact::magicLinkById($item['author-id'], $attachment['url']); if (strpos($mime, 'video') !== false) { if (!$vhead) { @@ -3636,8 +3709,8 @@ class Item $filesubtype = 'unkn'; } - $title = Strings::escapeHtml(trim(($mtch[4] ?? '') ?: $mtch[1])); - $title .= ' ' . $mtch[2] . ' ' . DI::l10n()->t('bytes'); + $title = Strings::escapeHtml(trim(($attachment['description'] ?? '') ?: $attachment['url'])); + $title .= ' ' . ($attachment['size'] ?? 0) . ' ' . DI::l10n()->t('bytes'); $icon = '
'; $as .= '' . $icon . ''; @@ -3876,7 +3949,7 @@ class Item $uid = $item['uid'] ?? 0; // first try to fetch the item via the GUID. This will work for all reshares that had been created on this system - $shared_item = self::selectFirst(['title', 'body', 'attach'], ['guid' => $shared['guid'], 'uid' => [0, $uid]]); + $shared_item = self::selectFirst(['title', 'body'], ['guid' => $shared['guid'], 'uid' => [0, $uid]]); if (!DBA::isResult($shared_item)) { if (empty($shared['link'])) { return $item; @@ -3889,7 +3962,7 @@ class Item return $item; } - $shared_item = self::selectFirst(['title', 'body', 'attach'], ['id' => $id]); + $shared_item = self::selectFirst(['title', 'body'], ['id' => $id]); if (!DBA::isResult($shared_item)) { return $item; } @@ -3910,4 +3983,41 @@ class Item return array_merge($item, $shared_item); } + + /** + * Check a prospective item array against user-level permissions + * + * @param array $item Expected keys: uri, gravity, and + * author-link if is author-id is set, + * owner-link if is owner-id is set, + * causer-link if is causer-id is set. + * @param int $user_id Local user ID + * @return bool + * @throws \Exception + */ + protected static function isAllowedByUser(array $item, int $user_id) + { + if (!empty($item['author-id']) && Contact\User::isBlocked($item['author-id'], $user_id)) { + Logger::notice('Author is blocked by user', ['author-link' => $item['author-link'], 'uid' => $user_id, 'item-uri' => $item['uri']]); + return false; + } + + if (!empty($item['owner-id']) && Contact\User::isBlocked($item['owner-id'], $user_id)) { + Logger::notice('Owner is blocked by user', ['owner-link' => $item['owner-link'], 'uid' => $user_id, 'item-uri' => $item['uri']]); + return false; + } + + // The causer is set during a thread completion, for example because of a reshare. It countains the responsible actor. + if (!empty($item['causer-id']) && Contact\User::isBlocked($item['causer-id'], $user_id)) { + Logger::notice('Causer is blocked by user', ['causer-link' => $item['causer-link'] ?? $item['causer-id'], 'uid' => $user_id, 'item-uri' => $item['uri']]); + return false; + } + + if (!empty($item['causer-id']) && ($item['gravity'] === GRAVITY_PARENT) && Contact\User::isIgnored($item['causer-id'], $user_id)) { + Logger::notice('Causer is ignored by user', ['causer-link' => $item['causer-link'] ?? $item['causer-id'], 'uid' => $user_id, 'item-uri' => $item['uri']]); + return false; + } + + return true; + } }