Merge branch 'develop' into phpstan-level-3

This commit is contained in:
Art4 2025-03-12 15:51:14 +00:00
commit 9ea4f591c7
11 changed files with 159 additions and 46 deletions

View file

@ -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: 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` - 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 - Extending or modifying a `final` class or method in any way
- Calling `private` methods (via Reflection) - Calling `private` methods (via Reflection)
- Accessing `private` properties (via Reflection) - Accessing `private` properties (via Reflection)

View file

@ -83,6 +83,8 @@ return [
## Addons ## 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. 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. 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.

View file

@ -184,13 +184,16 @@ class OEmbed
$eventDispatcher = DI::eventDispatcher(); $eventDispatcher = DI::eventDispatcher();
$oembed_data = ['url' => $embedurl]; $oembed_data = [
'url' => $embedurl,
'data' => $oembed,
];
$oembed_data = $eventDispatcher->dispatch( $oembed_data = $eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::OEMBED_FETCH_END, $oembed_data), new ArrayFilterEvent(ArrayFilterEvent::OEMBED_FETCH_END, $oembed_data),
)->getArray(); )->getArray();
return $oembed_data['url'] ?? $embedurl; return $oembed_data['data'];
} }
/** /**

View file

@ -10,7 +10,9 @@ namespace Friendica\Core\Addon\Model;
use Friendica\Core\Addon\Capability\ICanLoadAddons; use Friendica\Core\Addon\Capability\ICanLoadAddons;
use Friendica\Core\Addon\Exception\AddonInvalidConfigFileException; use Friendica\Core\Addon\Exception\AddonInvalidConfigFileException;
use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Logger\Factory\LoggerFactory;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
class AddonLoader implements ICanLoadAddons class AddonLoader implements ICanLoadAddons
{ {
@ -48,6 +50,25 @@ class AddonLoader implements ICanLoadAddons
throw new AddonInvalidConfigFileException('Error loading config file ' . $configFile); 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); $returnConfig = array_merge_recursive($returnConfig, $config);
} }

View file

@ -21,8 +21,8 @@ class StrategiesFileManager
* -> it's an empty string to cover empty/missing config values * -> it's an empty string to cover empty/missing config values
*/ */
const STRATEGY_DEFAULT_KEY = ''; const STRATEGY_DEFAULT_KEY = '';
const STATIC_DIR = 'static'; const STATIC_DIR = 'static';
const CONFIG_NAME = 'strategies'; const CONFIG_NAME = 'strategies';
/** @var ICanLoadAddons */ /** @var ICanLoadAddons */
protected $addonLoader; protected $addonLoader;
@ -81,6 +81,9 @@ class StrategiesFileManager
throw new HookConfigException(sprintf('Error loading config file %s.', $configFile)); 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)); $this->config = array_merge_recursive($config, $this->addonLoader->getActiveAddonConfig(static::CONFIG_NAME));
} }
} }

View file

@ -154,6 +154,46 @@ class Contact
return DBA::selectFirst('account-user-view', $fields, $condition, $params); 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 * Insert a row into the contact table
* Important: You can't use DBA::lastInsertId() after this call since it will be set to 0. * Important: You can't use DBA::lastInsertId() after this call since it will be set to 0.

View file

@ -405,7 +405,7 @@ class User
*/ */
public static function getIdForContactId(int $cid): int 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'])) { if (empty($account['pid'])) {
return 0; return 0;
} }

View file

@ -363,7 +363,7 @@ class Processor
return []; return [];
} }
$account = Contact::selectFirstAccountUser(['pid'], ['id' => $contact['id']]); $account = Contact::selectAccountUserById($contact['id'], ['pid']);
$item['owner-id'] = $item['author-id'] = $account['pid']; $item['owner-id'] = $item['author-id'] = $account['pid'];
$item['uri-id'] = ItemURI::getIdByURI($item['uri']); $item['uri-id'] = ItemURI::getIdByURI($item['uri']);
@ -424,7 +424,7 @@ class Processor
return []; return [];
} }
$account = Contact::selectFirstAccountUser(['pid'], ['id' => $contact['id']]); $account = Contact::selectAccountUserById($contact['id'], ['pid']);
$item['owner-id'] = $item['author-id'] = $account['pid']; $item['owner-id'] = $item['author-id'] = $account['pid'];
$item['uri-id'] = ItemURI::getIdByURI($uri); $item['uri-id'] = ItemURI::getIdByURI($uri);

View file

@ -335,7 +335,7 @@ class Feed
private static function getTitleFromItemOrEntry(array $item, DOMXPath $xpath, string $atomns, ?DOMNode $entry): string private static function getTitleFromItemOrEntry(array $item, DOMXPath $xpath, string $atomns, ?DOMNode $entry): string
{ {
$title = (string) $item['title']; $title = (string) ($item['title'] ?? '');
if (empty($title)) { if (empty($title)) {
$title = XML::getFirstNodeValue($xpath, $atomns . ':title/text()', $entry); $title = XML::getFirstNodeValue($xpath, $atomns . ':title/text()', $entry);
@ -1040,34 +1040,44 @@ class Feed
$authorid = Contact::getIdForURL($owner['url']); $authorid = Contact::getIdForURL($owner['url']);
$condition = [ $condition = [
"`uid` = ? AND `received` > ? AND NOT `deleted` AND `gravity` IN (?, ?) "`uid` = ? AND `received` > ? AND NOT `deleted`
AND `private` != ? AND `visible` AND `wall` AND `parent-network` IN (?, ?, ?)", 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, $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') { if ($filter === 'comments') {
$condition[0] .= " AND `gravity` = ? "; $condition = DBA::mergeConditions($condition, ['gravity' => Item::GRAVITY_COMMENT]);
$condition[] = Item::GRAVITY_COMMENT; } elseif ($filter === 'posts') {
} $condition = DBA::mergeConditions($condition, ['gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_ACTIVITY]]);
if ($owner['account-type'] != User::ACCOUNT_TYPE_COMMUNITY) {
$condition[0] .= " AND `contact-id` = ? AND `author-id` = ?";
$condition[] = $owner['id'];
$condition[] = $authorid;
} }
$params = ['order' => ['received' => true], 'limit' => $max_items]; $params = ['order' => ['received' => true], 'limit' => $max_items];
if ($filter === 'posts') { $ret = Post::selectOrigin(Item::DELIVER_FIELDLIST, $condition, $params);
$ret = Post::selectOriginThread(Item::DELIVER_FIELDLIST, $condition, $params);
} else {
$ret = Post::selectOrigin(Item::DELIVER_FIELDLIST, $condition, $params);
}
$items = Post::toArray($ret); $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; $doc->formatOutput = true;
$root = self::addHeader($doc, $owner, $filter); $root = self::addHeader($doc, $owner, $filter);

View file

@ -199,31 +199,56 @@ class ExpirePosts
return; return;
} }
DI::logger()->notice('Start collecting orphaned URI-ID', ['last-id' => $item['uri-id']]); DI::logger()->notice('Start collecting orphaned URI-ID', ['last-id' => $item['uri-id']]);
$condition = [
"`id` < ? $sql = [
AND NOT EXISTS(SELECT `uri-id` FROM `post-user` WHERE `uri-id` = `item-uri`.`id`) 'SELECT i.id
AND NOT EXISTS(SELECT `parent-uri-id` FROM `post-user` WHERE `parent-uri-id` = `item-uri`.`id`) FROM `item-uri` i
AND NOT EXISTS(SELECT `thr-parent-id` FROM `post-user` WHERE `thr-parent-id` = `item-uri`.`id`) LEFT JOIN `post-user` pu1 ON i.id = pu1.`uri-id`
AND NOT EXISTS(SELECT `external-id` FROM `post-user` WHERE `external-id` = `item-uri`.`id`) LEFT JOIN `post-user` pu2 ON i.id = pu2.`parent-uri-id`
AND NOT EXISTS(SELECT `replies-id` FROM `post-user` WHERE `replies-id` = `item-uri`.`id`) LEFT JOIN `post-user` pu3 ON i.id = pu3.`thr-parent-id`
AND NOT EXISTS(SELECT `context-id` FROM `post-thread` WHERE `context-id` = `item-uri`.`id`) LEFT JOIN `post-user` pu4 ON i.id = pu4.`external-id`
AND NOT EXISTS(SELECT `conversation-id` FROM `post-thread` WHERE `conversation-id` = `item-uri`.`id`) LEFT JOIN `post-user` pu5 ON i.id = pu5.`replies-id`
AND NOT EXISTS(SELECT `uri-id` FROM `mail` WHERE `uri-id` = `item-uri`.`id`) LEFT JOIN `post-thread` pt1 ON i.id = pt1.`context-id`
AND NOT EXISTS(SELECT `uri-id` FROM `event` WHERE `uri-id` = `item-uri`.`id`) LEFT JOIN `post-thread` pt2 ON i.id = pt2.`conversation-id`
AND NOT EXISTS(SELECT `uri-id` FROM `user-contact` WHERE `uri-id` = `item-uri`.`id`) LEFT JOIN `mail` m1 ON i.id = m1.`uri-id`
AND NOT EXISTS(SELECT `uri-id` FROM `contact` WHERE `uri-id` = `item-uri`.`id`) LEFT JOIN `event` e ON i.id = e.`uri-id`
AND NOT EXISTS(SELECT `uri-id` FROM `apcontact` WHERE `uri-id` = `item-uri`.`id`) LEFT JOIN `user-contact` uc ON i.id = uc.`uri-id`
AND NOT EXISTS(SELECT `uri-id` FROM `diaspora-contact` WHERE `uri-id` = `item-uri`.`id`) LEFT JOIN `contact` c ON i.id = c.`uri-id`
AND NOT EXISTS(SELECT `uri-id` FROM `inbox-status` WHERE `uri-id` = `item-uri`.`id`) LEFT JOIN `apcontact` ac ON i.id = ac.`uri-id`
AND NOT EXISTS(SELECT `uri-id` FROM `post-delivery` WHERE `uri-id` = `item-uri`.`id`) LEFT JOIN `diaspora-contact` dc ON i.id = dc.`uri-id`
AND NOT EXISTS(SELECT `uri-id` FROM `post-delivery` WHERE `inbox-id` = `item-uri`.`id`) LEFT JOIN `inbox-status` ins ON i.id = ins.`uri-id`
AND NOT EXISTS(SELECT `parent-uri-id` FROM `mail` WHERE `parent-uri-id` = `item-uri`.`id`) LEFT JOIN `post-delivery` pd1 ON i.id = pd1.`uri-id`
AND NOT EXISTS(SELECT `thr-parent-id` FROM `mail` WHERE `thr-parent-id` = `item-uri`.`id`)", $item['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; $pass = 0;
do { do {
++$pass; ++$pass;
$uris = DBA::select('item-uri', ['id'], $condition, ['limit' => $limit]); $uris = DBA::p(...$sql);
$total = DBA::numRows($uris); $total = DBA::numRows($uris);
DI::logger()->notice('Start deleting orphaned URI-ID', ['pass' => $pass, 'last-id' => $item['uri-id']]); DI::logger()->notice('Start deleting orphaned URI-ID', ['pass' => $pass, 'last-id' => $item['uri-id']]);
$affected_count = 0; $affected_count = 0;

View file

@ -629,6 +629,14 @@ nav.navbar .nav > li > button:focus {
text-align: center; text-align: center;
z-index: 1; 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 { #topbar-first .topbar-nav .nav-segment {
position: relative; position: relative;
text-align: left; text-align: left;