From e5416ca4a9b964ebdd454bbb864cb21c6e56059a Mon Sep 17 00:00:00 2001 From: Art4 Date: Fri, 7 Feb 2025 14:50:56 +0000 Subject: [PATCH 01/11] Deprecate providing LoggerInterface via addon strategies --- src/Core/Addon/Model/AddonLoader.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Core/Addon/Model/AddonLoader.php b/src/Core/Addon/Model/AddonLoader.php index 4686f7e74b..8ef27cb5a7 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,19 @@ 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, please provide an implementation for `%s` via `dependency.config.php` instead in %s addon.', + LoggerInterface::class, + LoggerFactory::class, + $addonName, + ), \E_USER_DEPRECATED); + } + } + } + $returnConfig = array_merge_recursive($returnConfig, $config); } From 1ea2df569e0ce1b0d6db996475691efbd8022999 Mon Sep 17 00:00:00 2001 From: Art4 Date: Fri, 7 Feb 2025 15:06:11 +0000 Subject: [PATCH 02/11] Deprecate strategies via addons --- src/Core/Addon/Model/AddonLoader.php | 10 ++++++++-- src/Core/Hooks/Util/StrategiesFileManager.php | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Core/Addon/Model/AddonLoader.php b/src/Core/Addon/Model/AddonLoader.php index 8ef27cb5a7..a608a66e3d 100644 --- a/src/Core/Addon/Model/AddonLoader.php +++ b/src/Core/Addon/Model/AddonLoader.php @@ -54,11 +54,17 @@ class AddonLoader implements ICanLoadAddons foreach ($config as $classname => $rule) { if ($classname === LoggerInterface::class) { @trigger_error(sprintf( - 'Providing a strategy for `%s` is deprecated since 2025.02, please provide an implementation for `%s` via `dependency.config.php` instead in %s addon.', - LoggerInterface::class, + '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); } } } diff --git a/src/Core/Hooks/Util/StrategiesFileManager.php b/src/Core/Hooks/Util/StrategiesFileManager.php index a39aabc85b..f5d2fe0224 100644 --- a/src/Core/Hooks/Util/StrategiesFileManager.php +++ b/src/Core/Hooks/Util/StrategiesFileManager.php @@ -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)); } } From c8dd900635a89d04398e89b3a2f7ec0dc01907f2 Mon Sep 17 00:00:00 2001 From: Art4 Date: Sat, 8 Feb 2025 07:01:54 +0000 Subject: [PATCH 03/11] Add deprecation note in docs --- doc/StrategyHooks.md | 2 ++ 1 file changed, 2 insertions(+) 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. From e9dae569ccfa037dc137331d295884866213d189 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Wed, 5 Mar 2025 19:07:10 -0500 Subject: [PATCH 04/11] [frio] Fix Safari bug where notification icon jumps to next line --- view/theme/frio/css/style.css | 8 ++++++++ 1 file changed, 8 insertions(+) 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; From 3b5ad05e47ddc26270e5bd3300c7d7d0d175a4f7 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 3 Mar 2025 22:30:19 +0000 Subject: [PATCH 05/11] Fixes fatal error "Return value must be of type Friendica\Object\OEmbed, string returned" --- src/Content/OEmbed.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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']; } /** From 4883035f891d30445ac7d92f730f8e289bf89ae3 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Fri, 7 Mar 2025 22:44:01 -0500 Subject: [PATCH 06/11] Replace NOT EXIST(SELECT) with LEFT JOIN WHERE IS NULL in ExpirePosts - Improves the query execution plan --- src/Worker/ExpirePosts.php | 67 ++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 21 deletions(-) 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; From d990026fb83eb37a9a73211ed7693d6d563fcae4 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 9 Mar 2025 07:52:58 +0000 Subject: [PATCH 07/11] Issue 14433: Display reshared content in feeds --- src/Protocol/Feed.php | 44 ++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index 2f9be305f1..6a2f5d42dd 100644 --- a/src/Protocol/Feed.php +++ b/src/Protocol/Feed.php @@ -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); From 9e4a69150c41bc7a428423570a4e4e06f51f873d Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 11 Mar 2025 04:43:44 +0000 Subject: [PATCH 08/11] Add public contact if missing --- src/Model/Contact.php | 40 +++++++++++++++++++++++++++ src/Model/User.php | 2 +- src/Protocol/ATProtocol/Processor.php | 4 +-- 3 files changed, 43 insertions(+), 3 deletions(-) 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 f551f6a273..502b31ea4a 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); From 24cbfd0953aa9f0fb856daf73385633fbf962008 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 11 Mar 2025 04:54:16 +0000 Subject: [PATCH 09/11] Fix codestyle --- src/Core/Hooks/Util/StrategiesFileManager.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Hooks/Util/StrategiesFileManager.php b/src/Core/Hooks/Util/StrategiesFileManager.php index a39aabc85b..0a887d31b5 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; From c1184698ee2c5856d80b1e1d355d8f0dabb68d58 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Sat, 8 Mar 2025 16:45:11 -0500 Subject: [PATCH 10/11] Ward against missing array key in Protocol\Feed - Address https://github.com/friendica/friendica/issues/14647#issuecomment-2667306693 --- src/Protocol/Feed.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index 6a2f5d42dd..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); From 6b7dd7bd12e586342a6a9f400794c655338bffa9 Mon Sep 17 00:00:00 2001 From: Artur Weigandt Date: Wed, 12 Mar 2025 11:06:28 +0100 Subject: [PATCH 11/11] remove BC for extending any non-abstract class --- doc/Developers-Intro.md | 1 + 1 file changed, 1 insertion(+) 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)