diff --git a/doc/Developers-Intro.md b/doc/Developers-Intro.md index 639e1fea74..5dbf1def4a 100644 --- a/doc/Developers-Intro.md +++ b/doc/Developers-Intro.md @@ -170,6 +170,7 @@ This is called the Backward Compatibility Promise. Inspired by the [Symonfy BC promise](https://symfony.com/doc/current/contributing/code/bc.html) we promise BC for every class, interface, trait, enum, function, constant, etc., but with the exception of: - Classes, interfaces, traits, enums, functions, methods, properties and constants marked as `@internal` or `@private` +- Extending or modifying any non-abstract class or method in any way - Extending or modifying a `final` class or method in any way - Calling `private` methods (via Reflection) - Accessing `private` properties (via Reflection) diff --git a/doc/StrategyHooks.md b/doc/StrategyHooks.md index 2960ceeaad..440728783c 100644 --- a/doc/StrategyHooks.md +++ b/doc/StrategyHooks.md @@ -83,6 +83,8 @@ return [ ## Addons +> ⚠️ Since Friendica 2025.02 the strategy hooks for addons are deprecated, please use PHP hooks instead. + The hook logic is useful for decoupling the Friendica core logic, but its primary goal is to modularize Friendica in creating addons. Therefor you can either use the interfaces directly as shown above, or you can place your own `hooks.config.php` file inside a `static` directory directly under your addon core directory. diff --git a/src/Content/OEmbed.php b/src/Content/OEmbed.php index 69a08526e1..72e621a116 100644 --- a/src/Content/OEmbed.php +++ b/src/Content/OEmbed.php @@ -184,13 +184,16 @@ class OEmbed $eventDispatcher = DI::eventDispatcher(); - $oembed_data = ['url' => $embedurl]; + $oembed_data = [ + 'url' => $embedurl, + 'data' => $oembed, + ]; $oembed_data = $eventDispatcher->dispatch( new ArrayFilterEvent(ArrayFilterEvent::OEMBED_FETCH_END, $oembed_data), )->getArray(); - return $oembed_data['url'] ?? $embedurl; + return $oembed_data['data']; } /** diff --git a/src/Core/Addon/Model/AddonLoader.php b/src/Core/Addon/Model/AddonLoader.php index 4686f7e74b..a608a66e3d 100644 --- a/src/Core/Addon/Model/AddonLoader.php +++ b/src/Core/Addon/Model/AddonLoader.php @@ -10,7 +10,9 @@ namespace Friendica\Core\Addon\Model; use Friendica\Core\Addon\Capability\ICanLoadAddons; use Friendica\Core\Addon\Exception\AddonInvalidConfigFileException; use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\Logger\Factory\LoggerFactory; use Friendica\Util\Strings; +use Psr\Log\LoggerInterface; class AddonLoader implements ICanLoadAddons { @@ -48,6 +50,25 @@ class AddonLoader implements ICanLoadAddons throw new AddonInvalidConfigFileException('Error loading config file ' . $configFile); } + if ($configName === 'strategies') { + foreach ($config as $classname => $rule) { + if ($classname === LoggerInterface::class) { + @trigger_error(sprintf( + 'Providing a strategy for `%s` is deprecated since 2025.02 and will stop working in 5 months, please provide an implementation for `%s` via `dependency.config.php` and remove the `strategies.config.php` file in the `%s` addon.', + $classname, + LoggerFactory::class, + $addonName, + ), \E_USER_DEPRECATED); + } else { + @trigger_error(sprintf( + 'Providing strategies for `%s` via addons is deprecated since 2025.02 and will stop working in 5 months, please stop using this and remove the `strategies.config.php` file in the `%s` addon.', + $classname, + $addonName, + ), \E_USER_DEPRECATED); + } + } + } + $returnConfig = array_merge_recursive($returnConfig, $config); } diff --git a/src/Core/Hooks/Util/StrategiesFileManager.php b/src/Core/Hooks/Util/StrategiesFileManager.php index a39aabc85b..a876dc832c 100644 --- a/src/Core/Hooks/Util/StrategiesFileManager.php +++ b/src/Core/Hooks/Util/StrategiesFileManager.php @@ -21,8 +21,8 @@ class StrategiesFileManager * -> it's an empty string to cover empty/missing config values */ const STRATEGY_DEFAULT_KEY = ''; - const STATIC_DIR = 'static'; - const CONFIG_NAME = 'strategies'; + const STATIC_DIR = 'static'; + const CONFIG_NAME = 'strategies'; /** @var ICanLoadAddons */ protected $addonLoader; @@ -81,6 +81,9 @@ class StrategiesFileManager throw new HookConfigException(sprintf('Error loading config file %s.', $configFile)); } + /** + * @deprecated 2025.02 Providing strategies via addons is deprecated and will be removed in 5 months. + */ $this->config = array_merge_recursive($config, $this->addonLoader->getActiveAddonConfig(static::CONFIG_NAME)); } } diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 2f98a0d3e4..cced0ed626 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -154,6 +154,46 @@ class Contact return DBA::selectFirst('account-user-view', $fields, $condition, $params); } + /** + * Fetch data from the "account-user-view" for a given contact id. Creates missing data if needed. + * @param int $id Contact id + * @param array $fields selected fields + * @return array|bool + */ + public static function selectAccountUserById(int $id, array $fields = []) + { + $data = self::selectFirstAccountUser($fields, ['id' => $id]); + if (!empty($data) || !self::createPublicContactFromUserContact($id)) { + return $data; + } + + return self::selectFirstAccountUser($fields, ['id' => $id]); + } + + /** + * Add missing public contact for a given user contact. + * @param int $cid ID of the user contact + * @return bool true if the public user had been created + */ + public static function createPublicContactFromUserContact(int $cid): bool + { + $fields = [ + 'created', 'updated', 'network', 'name', 'nick', 'location', 'about', 'keywords', 'xmpp', + 'matrix', 'avatar', 'blurhash', 'header', 'url', 'nurl', 'uri-id', 'addr', 'alias', 'pubkey', + 'batch', 'notify', 'poll', 'subscribe', 'last-update', 'next-update', 'success_update', + 'failure_update', 'failed', 'term-date', 'last-item', 'last-discovery', 'local-data', + 'readonly', 'contact-type', 'manually-approve', 'archive', 'unsearchable', 'sensitive', + 'baseurl', 'gsid', 'bd', 'photo', 'thumb', 'micro', 'name-date', 'uri-date', 'avatar-date', + 'request', 'confirm', 'poco', 'writable', 'forum', 'prv', 'bdyear' + ]; + $contact = self::selectFirst($fields, ['id' => $cid]); + if (empty($contact)) { + return false; + } + $contact['uid'] = 0; + return (bool)self::insert($contact); + } + /** * Insert a row into the contact table * Important: You can't use DBA::lastInsertId() after this call since it will be set to 0. diff --git a/src/Model/User.php b/src/Model/User.php index 8ee52ee922..927a1a82bd 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -405,7 +405,7 @@ class User */ public static function getIdForContactId(int $cid): int { - $account = Contact::selectFirstAccountUser(['pid', 'self', 'uid'], ['id' => $cid]); + $account = Contact::selectAccountUserById($cid, ['pid', 'self', 'uid']); if (empty($account['pid'])) { return 0; } diff --git a/src/Protocol/ATProtocol/Processor.php b/src/Protocol/ATProtocol/Processor.php index 3a21ff3dd4..ba838817a9 100755 --- a/src/Protocol/ATProtocol/Processor.php +++ b/src/Protocol/ATProtocol/Processor.php @@ -363,7 +363,7 @@ class Processor return []; } - $account = Contact::selectFirstAccountUser(['pid'], ['id' => $contact['id']]); + $account = Contact::selectAccountUserById($contact['id'], ['pid']); $item['owner-id'] = $item['author-id'] = $account['pid']; $item['uri-id'] = ItemURI::getIdByURI($item['uri']); @@ -424,7 +424,7 @@ class Processor return []; } - $account = Contact::selectFirstAccountUser(['pid'], ['id' => $contact['id']]); + $account = Contact::selectAccountUserById($contact['id'], ['pid']); $item['owner-id'] = $item['author-id'] = $account['pid']; $item['uri-id'] = ItemURI::getIdByURI($uri); diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index 2f9be305f1..48780e6181 100644 --- a/src/Protocol/Feed.php +++ b/src/Protocol/Feed.php @@ -335,7 +335,7 @@ class Feed private static function getTitleFromItemOrEntry(array $item, DOMXPath $xpath, string $atomns, ?DOMNode $entry): string { - $title = (string) $item['title']; + $title = (string) ($item['title'] ?? ''); if (empty($title)) { $title = XML::getFirstNodeValue($xpath, $atomns . ':title/text()', $entry); @@ -1040,34 +1040,44 @@ class Feed $authorid = Contact::getIdForURL($owner['url']); $condition = [ - "`uid` = ? AND `received` > ? AND NOT `deleted` AND `gravity` IN (?, ?) - AND `private` != ? AND `visible` AND `wall` AND `parent-network` IN (?, ?, ?)", + "`uid` = ? AND `received` > ? AND NOT `deleted` + AND ((`gravity` IN (?, ?) AND `wall`) OR (`gravity` = ? AND `verb` = ?)) + AND `origin` AND `private` != ? AND `visible` AND `parent-network` IN (?, ?, ?) + AND `author-id` = ?", $owner['uid'], $check_date, Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT, - Item::PRIVATE, Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA + Item::GRAVITY_ACTIVITY, Activity::ANNOUNCE, + Item::PRIVATE, Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, + $authorid ]; if ($filter === 'comments') { - $condition[0] .= " AND `gravity` = ? "; - $condition[] = Item::GRAVITY_COMMENT; - } - - if ($owner['account-type'] != User::ACCOUNT_TYPE_COMMUNITY) { - $condition[0] .= " AND `contact-id` = ? AND `author-id` = ?"; - $condition[] = $owner['id']; - $condition[] = $authorid; + $condition = DBA::mergeConditions($condition, ['gravity' => Item::GRAVITY_COMMENT]); + } elseif ($filter === 'posts') { + $condition = DBA::mergeConditions($condition, ['gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_ACTIVITY]]); } $params = ['order' => ['received' => true], 'limit' => $max_items]; - if ($filter === 'posts') { - $ret = Post::selectOriginThread(Item::DELIVER_FIELDLIST, $condition, $params); - } else { - $ret = Post::selectOrigin(Item::DELIVER_FIELDLIST, $condition, $params); - } + $ret = Post::selectOrigin(Item::DELIVER_FIELDLIST, $condition, $params); $items = Post::toArray($ret); - $doc = new DOMDocument('1.0', 'utf-8'); + $reshares = []; + foreach ($items as $index => $item) { + if ($item['verb'] == Activity::ANNOUNCE) { + $reshares[$item['thr-parent-id']] = $index; + } + } + + if (!empty($reshares)) { + $posts = Post::selectToArray(Item::DELIVER_FIELDLIST, ['uri-id' => array_keys($reshares), 'uid' => $owner['uid']]); + foreach ($posts as $post) { + $items[$reshares[$post['uri-id']]] = $post; + } + } + + $doc = new DOMDocument('1.0', 'utf-8'); + $doc->formatOutput = true; $root = self::addHeader($doc, $owner, $filter); diff --git a/src/Worker/ExpirePosts.php b/src/Worker/ExpirePosts.php index 1d4e546a37..e4c1b231d6 100644 --- a/src/Worker/ExpirePosts.php +++ b/src/Worker/ExpirePosts.php @@ -199,31 +199,56 @@ class ExpirePosts return; } DI::logger()->notice('Start collecting orphaned URI-ID', ['last-id' => $item['uri-id']]); - $condition = [ - "`id` < ? - AND NOT EXISTS(SELECT `uri-id` FROM `post-user` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `parent-uri-id` FROM `post-user` WHERE `parent-uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `thr-parent-id` FROM `post-user` WHERE `thr-parent-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `external-id` FROM `post-user` WHERE `external-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `replies-id` FROM `post-user` WHERE `replies-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `context-id` FROM `post-thread` WHERE `context-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `conversation-id` FROM `post-thread` WHERE `conversation-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `mail` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `event` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `user-contact` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `contact` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `apcontact` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `diaspora-contact` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `inbox-status` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `post-delivery` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `post-delivery` WHERE `inbox-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `parent-uri-id` FROM `mail` WHERE `parent-uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `thr-parent-id` FROM `mail` WHERE `thr-parent-id` = `item-uri`.`id`)", $item['uri-id'] + + $sql = [ + 'SELECT i.id + FROM `item-uri` i + LEFT JOIN `post-user` pu1 ON i.id = pu1.`uri-id` + LEFT JOIN `post-user` pu2 ON i.id = pu2.`parent-uri-id` + LEFT JOIN `post-user` pu3 ON i.id = pu3.`thr-parent-id` + LEFT JOIN `post-user` pu4 ON i.id = pu4.`external-id` + LEFT JOIN `post-user` pu5 ON i.id = pu5.`replies-id` + LEFT JOIN `post-thread` pt1 ON i.id = pt1.`context-id` + LEFT JOIN `post-thread` pt2 ON i.id = pt2.`conversation-id` + LEFT JOIN `mail` m1 ON i.id = m1.`uri-id` + LEFT JOIN `event` e ON i.id = e.`uri-id` + LEFT JOIN `user-contact` uc ON i.id = uc.`uri-id` + LEFT JOIN `contact` c ON i.id = c.`uri-id` + LEFT JOIN `apcontact` ac ON i.id = ac.`uri-id` + LEFT JOIN `diaspora-contact` dc ON i.id = dc.`uri-id` + LEFT JOIN `inbox-status` ins ON i.id = ins.`uri-id` + LEFT JOIN `post-delivery` pd1 ON i.id = pd1.`uri-id` + LEFT JOIN `post-delivery` pd2 ON i.id = pd2.`inbox-id` + LEFT JOIN `mail` m2 ON i.id = m2.`parent-uri-id` + LEFT JOIN `mail` m3 ON i.id = m3.`thr-parent-id` + WHERE + i.id < ? AND + pu1.`uri-id` IS NULL AND + pu2.`parent-uri-id` IS NULL AND + pu3.`thr-parent-id` IS NULL AND + pu4.`external-id` IS NULL AND + pu5.`replies-id` IS NULL AND + pt1.`context-id` IS NULL AND + pt2.`conversation-id` IS NULL AND + m1.`uri-id` IS NULL AND + e.`uri-id` IS NULL AND + uc.`uri-id` IS NULL AND + c.`uri-id` IS NULL AND + ac.`uri-id` IS NULL AND + dc.`uri-id` IS NULL AND + ins.`uri-id` IS NULL AND + pd1.`uri-id` IS NULL AND + pd2.`inbox-id` IS NULL AND + m2.`parent-uri-id` IS NULL AND + m3.`thr-parent-id` IS NULL + LIMIT ?', + $item['uri-id'], + $limit ]; $pass = 0; do { ++$pass; - $uris = DBA::select('item-uri', ['id'], $condition, ['limit' => $limit]); + $uris = DBA::p(...$sql); $total = DBA::numRows($uris); DI::logger()->notice('Start deleting orphaned URI-ID', ['pass' => $pass, 'last-id' => $item['uri-id']]); $affected_count = 0; diff --git a/view/theme/frio/css/style.css b/view/theme/frio/css/style.css index 7bd9a165ea..e20add6d67 100644 --- a/view/theme/frio/css/style.css +++ b/view/theme/frio/css/style.css @@ -629,6 +629,14 @@ nav.navbar .nav > li > button:focus { text-align: center; z-index: 1; } +/* Workaround for Safari bug where the notification icon jumps on the next line */ +#topbar-first .topbar-nav .nav > li + li { + margin-left: 1px; +} +#topbar-first .topbar-nav .nav > li + li > a { + margin-left: -1px; +} +/* End workaround */ #topbar-first .topbar-nav .nav-segment { position: relative; text-align: left;