X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FItem.php;h=d41e84c5b9f44c5e6ff0b1d61cb15c9d542c56a3;hb=f9994548c1f1110c7f548e00fcf1b6ee42b9de3b;hp=e67b862346ab0bbd8d8603afb7bdaa4c7fa9e103;hpb=0c3a5c815e7067aa587ac8f0bb8e32bb5892082e;p=friendica.git diff --git a/src/Model/Item.php b/src/Model/Item.php index e67b862346..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,8 +71,6 @@ 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', @@ -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']); } } @@ -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,6 +969,8 @@ 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']); @@ -996,7 +1000,7 @@ 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']]); } @@ -1056,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 { @@ -1151,14 +1154,11 @@ class Item } } - // 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']])) { @@ -1167,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. @@ -1184,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]); @@ -1384,27 +1384,6 @@ class Item 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; @@ -1415,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; @@ -1430,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']]); - 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']]); + if (!empty($item['uid']) && !self::isAllowedByUser($item, $item['uid'])) { 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. @@ -1465,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 * @@ -1513,7 +1507,7 @@ class Item $parent = self::selectFirst($fields, $condition, $params); if (!DBA::isResult($parent)) { - Logger::info('item parent was not found - ignoring item', ['thr-parent' => $item['thr-parent'], 'uid' => $item['uid']]); + Logger::notice('item parent was not found - ignoring item', ['thr-parent' => $item['thr-parent'], 'uid' => $item['uid']]); return []; } @@ -1521,15 +1515,13 @@ class Item return $parent; } - $condition = ['uri' => $item['parent-uri'], - 'parent-uri' => $item['parent-uri'], - 'uid' => $item['uid']]; - // We select wall = 1 in priority for top level permission checks - $params = ['order' => ['wall' => true]]; + $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::info('item parent was not found - ignoring item', ['parent-uri' => $item['parent-uri'], 'uid' => $item['uid']]); + Logger::notice('item top level parent was not found - ignoring item', ['parent-uri' => $parent['parent-uri'], 'uid' => $parent['uid']]); return []; } @@ -1574,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; @@ -1586,7 +1578,7 @@ 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']]); @@ -1695,12 +1687,19 @@ class Item return 0; } - if ($item['thr-parent'] != $item['uri']) { + 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']; @@ -1744,7 +1743,6 @@ class Item // Update the contact relations Contact\Relation::store($toplevel_parent['author-id'], $item['author-id'], $item['created']); - unset($item['parent']); unset($item['parent_origin']); } else { $parent_id = 0; @@ -1787,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; @@ -1873,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']); @@ -1892,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; } @@ -1911,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)) { @@ -1927,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; } @@ -1952,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); @@ -1980,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); @@ -1995,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) { @@ -2799,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']) { @@ -2830,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)) { @@ -3042,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; @@ -3088,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 @@ -3107,7 +3134,7 @@ class Item continue; } - self::markForDeletionById($item['id'], PRIORITY_LOW); + self::markForDeletionById($item['id'], $priority); ++$expired; } @@ -3502,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); @@ -3524,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; } /** @@ -3955,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; + } }