Merge branch 'stable' into develop

This commit is contained in:
Tobias Diekershoff 2024-08-17 17:29:18 +02:00
commit 95229140f8
194 changed files with 11224 additions and 9691 deletions

View file

@ -287,6 +287,7 @@ class APContact
} elseif ($apcontact['type'] == 'Tombstone') {
// The "inbox" field must have a content
$apcontact['inbox'] = '';
$apcontact['addr'] = '';
}
// Quit if this doesn't seem to be an account at all
@ -294,7 +295,7 @@ class APContact
return $fetched_contact;
}
if (empty($apcontact['addr'])) {
if (empty($apcontact['addr']) && ($apcontact['type'] != 'Tombstone')) {
try {
$apcontact['addr'] = $apcontact['nick'] . '@' . (new Uri($apcontact['url']))->getAuthority();
} catch (\Throwable $e) {

View file

@ -245,6 +245,7 @@ class Attach
* @param string $src Source file name
* @param int $uid User id
* @param string $filename Optional file name
* @param string $filetype Optional file type
* @param string $allow_cid
* @param string $allow_gid
* @param string $deny_cid
@ -252,7 +253,7 @@ class Attach
* @return boolean|int Insert id or false on failure
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function storeFile(string $src, int $uid, string $filename = '', string $allow_cid = '', string $allow_gid = '', string $deny_cid = '', string $deny_gid = '')
public static function storeFile(string $src, int $uid, string $filename = '', string $filetype = '', string $allow_cid = '', string $allow_gid = '', string $deny_cid = '', string $deny_gid = '')
{
if ($filename === '') {
$filename = basename($src);
@ -260,7 +261,7 @@ class Attach
$data = @file_get_contents($src);
return self::store($data, $uid, $filename, '', null, $allow_cid, $allow_gid, $deny_cid, $deny_gid);
return self::store($data, $uid, $filename, $filetype, null, $allow_cid, $allow_gid, $deny_cid, $deny_gid);
}
@ -345,6 +346,16 @@ class Attach
}
}
public static function setPermissionForId(int $id, int $uid, string $str_contact_allow, string $str_circle_allow, string $str_contact_deny, string $str_circle_deny)
{
$fields = [
'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow,
'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny,
];
self::update($fields, ['id' => $id, 'uid' => $uid]);
}
public static function addAttachmentToBody(string $body, int $uid): string
{
preg_match_all("/\[attachment\](.*?)\[\/attachment\]/ism", $body, $matches, PREG_SET_ORDER);

View file

@ -290,12 +290,12 @@ class Circle
throw new HTTPException\NotFoundException('Circle not found.');
}
$cdata = Contact::getPublicAndUserContactID($cid, $circle['uid']);
if (empty($cdata['user'])) {
$ucid = Contact::getUserContactId($cid, $circle['uid']);
if (!$ucid) {
throw new HTTPException\NotFoundException('Invalid contact.');
}
return DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $cdata['user']], Database::INSERT_IGNORE);
return DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $ucid], Database::INSERT_IGNORE);
}
/**
@ -318,12 +318,12 @@ class Circle
throw new HTTPException\NotFoundException('Circle not found.');
}
$cdata = Contact::getPublicAndUserContactID($cid, $circle['uid']);
if (empty($cdata['user'])) {
$ucid = Contact::getUserContactId($cid, $circle['uid']);
if (!$ucid) {
throw new HTTPException\NotFoundException('Invalid contact.');
}
return DBA::delete('group_member', ['gid' => $gid, 'contact-id' => $cid]);
return DBA::delete('group_member', ['gid' => $gid, 'contact-id' => $ucid]);
}
/**
@ -347,12 +347,12 @@ class Circle
}
foreach ($contacts as $cid) {
$cdata = Contact::getPublicAndUserContactID($cid, $circle['uid']);
if (empty($cdata['user'])) {
$ucid = Contact::getUserContactId($cid, $circle['uid']);
if (!$ucid) {
throw new HTTPException\NotFoundException('Invalid contact.');
}
DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $cdata['user']], Database::INSERT_IGNORE);
DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $ucid], Database::INSERT_IGNORE);
}
}
@ -379,12 +379,12 @@ class Circle
$contactIds = [];
foreach ($contacts as $cid) {
$cdata = Contact::getPublicAndUserContactID($cid, $circle['uid']);
if (empty($cdata['user'])) {
$ucid = Contact::getUserContactId($cid, $circle['uid']);
if (!$ucid) {
throw new HTTPException\NotFoundException('Invalid contact.');
}
$contactIds[] = $cdata['user'];
$contactIds[] = $ucid;
}
// Return status of deletion

View file

@ -444,12 +444,12 @@ class Contact
return false;
}
$cdata = self::getPublicAndUserContactID($cid, $uid);
if (empty($cdata['user'])) {
$ucid = self::getUserContactId($cid, $uid);
if (!$ucid) {
return false;
}
$condition = ['id' => $cdata['user'], 'rel' => [self::FOLLOWER, self::FRIEND]];
$condition = ['id' => $ucid, 'rel' => [self::FOLLOWER, self::FRIEND]];
if ($strict) {
$condition = array_merge($condition, ['pending' => false, 'readonly' => false, 'blocked' => false]);
}
@ -495,12 +495,12 @@ class Contact
return false;
}
$cdata = self::getPublicAndUserContactID($cid, $uid);
if (empty($cdata['user'])) {
$ucid = self::getUserContactId($cid, $uid);
if (!$ucid) {
return false;
}
$condition = ['id' => $cdata['user'], 'rel' => [self::SHARING, self::FRIEND]];
$condition = ['id' => $ucid, 'rel' => [self::SHARING, self::FRIEND]];
if ($strict) {
$condition = array_merge($condition, ['pending' => false, 'readonly' => false, 'blocked' => false]);
}
@ -671,6 +671,32 @@ class Contact
return ['public' => $pcid, 'user' => $ucid];
}
/**
* Returns the public contact id of a provided contact id
*
* @param integer $cid
* @param integer $uid
* @return integer
*/
public static function getPublicContactId(int $cid, int $uid): int
{
$contact = DBA::selectFirst('account-user-view', ['pid'], ['id' => $cid, 'uid' => [0, $uid]]);
return $contact['pid'] ?? 0;
}
/**
* Returns the user contact id of a provided contact id
*
* @param integer $cid
* @param integer $uid
* @return integer
*/
public static function getUserContactId(int $cid, int $uid): int
{
$data = self::getPublicAndUserContactID($cid, $uid);
return $data['user'] ?? 0;
}
/**
* Helper function for "getPublicAndUserContactID"
*
@ -968,13 +994,13 @@ class Contact
}
if (in_array($contact['rel'], [self::SHARING, self::FRIEND])) {
$cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']);
if (!empty($cdata['public'])) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']);
$pcid = self::getPublicContactId($contact['id'], $contact['uid']);
if ($pcid) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\Unfollow', $pcid, $contact['uid']);
}
}
self::removeSharer($contact);
self::removeSharer($contact, false);
}
/**
@ -998,13 +1024,13 @@ class Contact
}
if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND])) {
$cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']);
if (!empty($cdata['public'])) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']);
$pcid = self::getPublicContactId($contact['id'], $contact['uid']);
if ($pcid) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\RevokeFollow', $pcid, $contact['uid']);
}
}
self::removeFollower($contact);
self::removeFollower($contact, false);
}
/**
@ -1025,14 +1051,14 @@ class Contact
throw new \InvalidArgumentException('Unexpected public contact record');
}
$cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']);
$pcid = self::getPublicContactId($contact['id'], $contact['uid']);
if (in_array($contact['rel'], [self::SHARING, self::FRIEND]) && !empty($cdata['public'])) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']);
if (in_array($contact['rel'], [self::SHARING, self::FRIEND]) && $pcid) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\Unfollow', $pcid, $contact['uid']);
}
if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND]) && !empty($cdata['public'])) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']);
if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND]) && $pcid) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\RevokeFollow', $pcid, $contact['uid']);
}
self::remove($contact['id']);
@ -1547,24 +1573,25 @@ class Contact
/**
* Returns posts from a given contact url
*
* @param string $contact_url Contact URL
* @param bool $thread_mode
* @param int $update Update mode
* @param int $parent Item parent ID for the update mode
* @param bool $only_media Only display media content
* @param string $contact_url Contact URL
* @param int $uid User ID
* @param bool $only_media Only display media content
* @param string $last_created Newest creation date, used for paging
* @return string posts in HTML
* @throws \Exception
*/
public static function getPostsFromUrl(string $contact_url, int $uid, bool $only_media = false): string
public static function getPostsFromUrl(string $contact_url, int $uid, bool $only_media = false, string $last_created = null): string
{
return self::getPostsFromId(self::getIdForURL($contact_url), $uid, $only_media);
return self::getPostsFromId(self::getIdForURL($contact_url), $uid, $only_media, $last_created);
}
/**
* Returns posts from a given contact id
*
* @param int $cid Contact ID
* @param bool $only_media Only display media content
* @param int $cid Contact ID
* @param int $uid User ID
* @param bool $only_media Only display media content
* @param string $last_created Newest creation date, used for paging
* @return string posts in HTML
* @throws \Exception
*/
@ -2666,6 +2693,14 @@ class Contact
$data = Probe::uri($contact['url'], $network, $contact['uid']);
if (in_array($data['network'], Protocol::FEDERATED) && (parse_url($data['url'], PHP_URL_SCHEME) == 'http')) {
$ssl_url = str_replace('http://', 'https://', $contact['url']);
$ssl_data = Probe::uri($ssl_url, $network, $contact['uid']);
if (($ssl_data['network'] == $data['network']) && (parse_url($ssl_data['url'], PHP_URL_SCHEME) != 'http')) {
$data = $ssl_data;
}
}
if ($data['network'] == Protocol::DIASPORA) {
try {
DI::dsprContact()->updateFromProbeArray($data);
@ -2824,7 +2859,7 @@ class Contact
// We must not try to update relay contacts via probe. They are no real contacts.
// See Relay::updateContact() for more details.
// We check after the probing to be able to correct falsely detected contact types.
if (($contact['contact-type'] == self::TYPE_RELAY) && Strings::compareLink($contact['url'], $contact['baseurl']) &&
if (($contact['contact-type'] == self::TYPE_RELAY) && Strings::compareLink($contact['url'], $contact['baseurl'] ?? '') &&
(!Strings::compareLink($ret['url'], $contact['url']) || in_array($ret['network'], [Protocol::FEED, Protocol::PHANTOM]))
) {
if (GServer::reachable($contact)) {
@ -3013,6 +3048,10 @@ class Contact
*/
public static function getProtocol(string $url, string $network): string
{
if (self::isLocal($url)) {
return Protocol::ACTIVITYPUB;
}
if ($network != Protocol::DFRN) {
return $network;
}
@ -3404,16 +3443,21 @@ class Contact
* Update the local relationship when a local user loses a follower
*
* @param array $contact User-specific contact (uid != 0) array
* @param bool $delete Delete if set, otherwise set relation to "nothing" when contact had been a follower
* @return void
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function removeFollower(array $contact)
public static function removeFollower(array $contact, bool $delete = true)
{
if (in_array($contact['rel'] ?? [], [self::FRIEND, self::SHARING])) {
self::update(['rel' => self::SHARING], ['id' => $contact['id']]);
} elseif (!empty($contact['id'])) {
self::remove($contact['id']);
if ($delete) {
self::remove($contact['id']);
} else {
self::update(['rel' => self::NOTHING, 'pending' => false], ['id' => $contact['id']]);
}
} else {
DI::logger()->info('Couldn\'t remove follower because of invalid contact array', ['contact' => $contact]);
return;
@ -3423,9 +3467,9 @@ class Contact
self::clearFollowerFollowingEndpointCache($contact['uid']);
$cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']);
if (!empty($cdata['public'])) {
DI::notification()->deleteForUserByVerb($contact['uid'], Activity::FOLLOW, ['actor-id' => $cdata['public']]);
$pcid = self::getPublicContactId($contact['id'], $contact['uid']);
if ($pcid) {
DI::notification()->deleteForUserByVerb($contact['uid'], Activity::FOLLOW, ['actor-id' => $pcid]);
}
}
@ -3434,14 +3478,19 @@ class Contact
* Removes the contact for sharing-only protocols (feed and mail).
*
* @param array $contact User-specific contact (uid != 0) array
* @param bool $delete Delete if set, otherwise set relation to "nothing" when contact had been a sharer
* @throws HTTPException\InternalServerErrorException
*/
public static function removeSharer(array $contact)
public static function removeSharer(array $contact, bool $delete = true)
{
self::clearFollowerFollowingEndpointCache($contact['uid']);
if ($contact['rel'] == self::SHARING || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) {
self::remove($contact['id']);
if (in_array($contact['rel'], [self::SHARING, self::NOTHING]) || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) {
if ($delete) {
self::remove($contact['id']);
} else {
self::update(['rel' => self::NOTHING, 'pending' => false], ['id' => $contact['id']]);
}
} else {
self::update(['rel' => self::FOLLOWER, 'pending' => false], ['id' => $contact['id']]);
}
@ -3572,6 +3621,9 @@ class Contact
}
$contact = DBA::selectFirst('contact', ['id', 'network', 'url', 'alias', 'uid'], ['id' => $cid]);
if (empty($contact)) {
return $url;
}
return self::magicLinkByContact($contact, $url);
}

View file

@ -281,12 +281,12 @@ class User
*/
public static function setCollapsed(int $cid, int $uid, bool $collapsed)
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (empty($cdata)) {
$pcid = Contact::getPublicContactId($cid, $uid);
if (!$pcid) {
return;
}
DBA::update('user-contact', ['collapsed' => $collapsed], ['cid' => $cdata['public'], 'uid' => $uid], true);
DBA::update('user-contact', ['collapsed' => $collapsed], ['cid' => $pcid, 'uid' => $uid], true);
}
/**
@ -300,21 +300,13 @@ class User
*/
public static function isCollapsed(int $cid, int $uid): bool
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (empty($cdata)) {
$pcid = Contact::getPublicContactId($cid, $uid);
if (!$pcid) {
return false;
}
$collapsed = false;
if (!empty($cdata['public'])) {
$public_contact = DBA::selectFirst('user-contact', ['collapsed'], ['cid' => $cdata['public'], 'uid' => $uid]);
if (DBA::isResult($public_contact)) {
$collapsed = (bool) $public_contact['collapsed'];
}
}
return $collapsed;
$public_contact = DBA::selectFirst('user-contact', ['collapsed'], ['cid' => $pcid, 'uid' => $uid]);
return $public_contact['collapsed'] ?? false;
}
/**
@ -328,12 +320,12 @@ class User
*/
public static function setChannelFrequency(int $cid, int $uid, int $frequency)
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (empty($cdata)) {
$pcid = Contact::getPublicContactId($cid, $uid);
if (!$pcid) {
return;
}
DBA::update('user-contact', ['channel-frequency' => $frequency], ['cid' => $cdata['public'], 'uid' => $uid], true);
DBA::update('user-contact', ['channel-frequency' => $frequency], ['cid' => $pcid, 'uid' => $uid], true);
}
/**
@ -347,21 +339,13 @@ class User
*/
public static function getChannelFrequency(int $cid, int $uid): int
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (empty($cdata)) {
$pcid = Contact::getPublicContactId($cid, $uid);
if (!$pcid) {
return false;
}
$frequency = self::FREQUENCY_DEFAULT;
if (!empty($cdata['public'])) {
$public_contact = DBA::selectFirst('user-contact', ['channel-frequency'], ['cid' => $cdata['public'], 'uid' => $uid]);
if (DBA::isResult($public_contact)) {
$frequency = $public_contact['channel-frequency'] ?? self::FREQUENCY_DEFAULT;
}
}
return $frequency;
$public_contact = DBA::selectFirst('user-contact', ['channel-frequency'], ['cid' => $pcid, 'uid' => $uid]);
return $public_contact['channel-frequency'] ?? self::FREQUENCY_DEFAULT;
}
/**
@ -375,12 +359,12 @@ class User
*/
public static function setChannelOnly(int $cid, int $uid, bool $isChannelOnly)
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (empty($cdata)) {
$pcid = Contact::getPublicContactId($cid, $uid);
if (!$pcid) {
return;
}
DBA::update('user-contact', ['channel-only' => $isChannelOnly], ['cid' => $cdata['public'], 'uid' => $uid], true);
DBA::update('user-contact', ['channel-only' => $isChannelOnly], ['cid' => $pcid, 'uid' => $uid], true);
}
/**
@ -394,21 +378,13 @@ class User
*/
public static function getChannelOnly(int $cid, int $uid): bool
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (empty($cdata)) {
$pcid = Contact::getPublicContactId($cid, $uid);
if (!$pcid) {
return false;
}
$isChannelOnly = false;
if (!empty($cdata['public'])) {
$public_contact = DBA::selectFirst('user-contact', ['channel-only'], ['cid' => $cdata['public'], 'uid' => $uid]);
if (DBA::isResult($public_contact)) {
$isChannelOnly = $public_contact['channel-only'] ?? false;
}
}
return $isChannelOnly;
$public_contact = DBA::selectFirst('user-contact', ['channel-only'], ['cid' => $pcid, 'uid' => $uid]);
return $public_contact['channel-only'] ?? false;
}
/**
@ -422,12 +398,12 @@ class User
*/
public static function setIsBlocked(int $cid, int $uid, bool $blocked)
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (empty($cdata)) {
$pcid = Contact::getPublicContactId($cid, $uid);
if (!$pcid) {
return;
}
DBA::update('user-contact', ['is-blocked' => $blocked], ['cid' => $cdata['public'], 'uid' => $uid], true);
DBA::update('user-contact', ['is-blocked' => $blocked], ['cid' => $pcid, 'uid' => $uid], true);
}
/**
@ -440,18 +416,12 @@ class User
*/
public static function isIsBlocked(int $cid, int $uid): bool
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (empty($cdata)) {
$pcid = Contact::getPublicContactId($cid, $uid);
if (!$pcid) {
return false;
}
if (!empty($cdata['public'])) {
$public_contact = DBA::selectFirst('user-contact', ['is-blocked'], ['cid' => $cdata['public'], 'uid' => $uid]);
if (DBA::isResult($public_contact)) {
return $public_contact['is-blocked'];
}
}
return false;
$public_contact = DBA::selectFirst('user-contact', ['is-blocked'], ['cid' => $pcid, 'uid' => $uid]);
return $public_contact['is-blocked'] ?? false;
}
}

View file

@ -87,9 +87,11 @@ class GServer
// Standardized endpoints
const DETECT_STATISTICS_JSON = 100;
const DETECT_NODEINFO_1 = 101;
const DETECT_NODEINFO_2 = 102;
const DETECT_NODEINFO_210 = 103;
const DETECT_NODEINFO_10 = 101; // Nodeinfo Version 1.0
const DETECT_NODEINFO_20 = 102; // Nodeinfo Version 2.0
const DETECT_NODEINFO2_10 = 103; // Nodeinfo2 Version 1.0
const DETECT_NODEINFO_21 = 104; // Nodeinfo Version 2.1
const DETECT_NODEINFO_22 = 105; // Nodeinfo Version 2.2
/**
* Check for the existence of a server and adds it in the background if not existant
@ -612,7 +614,7 @@ class GServer
$in_webroot = empty(parse_url($url, PHP_URL_PATH));
// When a nodeinfo is present, we don't need to dig further
$curlResult = DI::httpClient()->get($url . '/.well-known/x-nodeinfo2', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
$curlResult = DI::httpClient()->get($url . '/.well-known/nodeinfo', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if ($curlResult->isTimeout()) {
self::setFailureByUrl($url);
return false;
@ -621,10 +623,11 @@ class GServer
if (!empty($network) && !in_array($network, Protocol::NATIVE_SUPPORT)) {
$serverdata = ['detection-method' => self::DETECT_MANUAL, 'network' => $network, 'platform' => '', 'version' => '', 'site_name' => '', 'info' => ''];
} else {
$serverdata = self::parseNodeinfo210($curlResult);
if (empty($serverdata)) {
$curlResult = DI::httpClient()->get($url . '/.well-known/nodeinfo', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
$serverdata = self::fetchNodeinfo($url, $curlResult);
$serverdata = self::parseNodeinfo($url, $curlResult);
if (empty($serverdata) || !in_array($serverdata['detection-method'], [self::DETECT_NODEINFO_20, self::DETECT_NODEINFO_21, self::DETECT_NODEINFO_22])) {
$curlResult = DI::httpClient()->get($url . '/.well-known/x-nodeinfo2', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
$serverdata = self::parseNodeinfo2($curlResult) ?: $serverdata;
}
}
@ -1049,7 +1052,9 @@ class GServer
}
/**
* Detect server type by using the nodeinfo data
* Parses Nodeinfo
*
* @see https://github.com/jhass/nodeinfo
*
* @param string $url address of the server
* @param ICanHandleHttpResponses $httpResult
@ -1058,7 +1063,7 @@ class GServer
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function fetchNodeinfo(string $url, ICanHandleHttpResponses $httpResult): array
private static function parseNodeinfo(string $url, ICanHandleHttpResponses $httpResult): array
{
if (!$httpResult->isSuccess()) {
return [];
@ -1072,6 +1077,7 @@ class GServer
$nodeinfo1_url = '';
$nodeinfo2_url = '';
$detection_method = self::DETECT_MANUAL;
foreach ($nodeinfo['links'] as $link) {
if (!is_array($link) || empty($link['rel']) || empty($link['href'])) {
@ -1081,8 +1087,15 @@ class GServer
if ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/1.0') {
$nodeinfo1_url = Network::addBasePath($link['href'], $httpResult->getUrl());
} elseif ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0') {
} elseif (($detection_method < self::DETECT_NODEINFO_20) && ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0')) {
$nodeinfo2_url = Network::addBasePath($link['href'], $httpResult->getUrl());
$detection_method = self::DETECT_NODEINFO_20;
} elseif (($detection_method < self::DETECT_NODEINFO_21) && ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.1')) {
$nodeinfo2_url = Network::addBasePath($link['href'], $httpResult->getUrl());
$detection_method = self::DETECT_NODEINFO_21;
} elseif (($detection_method < self::DETECT_NODEINFO_22) && ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.2')) {
$nodeinfo2_url = Network::addBasePath($link['href'], $httpResult->getUrl());
$detection_method = self::DETECT_NODEINFO_22;
}
}
@ -1093,18 +1106,20 @@ class GServer
$server = [];
if (!empty($nodeinfo2_url)) {
$server = self::parseNodeinfo2($nodeinfo2_url);
$server = self::parseNodeinfo_2($nodeinfo2_url, $detection_method);
}
if (empty($server) && !empty($nodeinfo1_url)) {
$server = self::parseNodeinfo1($nodeinfo1_url);
$server = self::parseNodeinfo_1($nodeinfo1_url);
}
return $server;
}
/**
* Parses Nodeinfo 1
* Parses Nodeinfo with the version 1.0
*
* @see https://github.com/jhass/nodeinfo/tree/main/schemas/1.0
*
* @param string $nodeinfo_url address of the nodeinfo path
*
@ -1112,7 +1127,7 @@ class GServer
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function parseNodeinfo1(string $nodeinfo_url): array
private static function parseNodeinfo_1(string $nodeinfo_url): array
{
$curlResult = DI::httpClient()->get($nodeinfo_url, HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) {
@ -1125,8 +1140,10 @@ class GServer
return [];
}
$server = ['detection-method' => self::DETECT_NODEINFO_1,
'register_policy' => Register::CLOSED];
$server = [
'detection-method' => self::DETECT_NODEINFO_10,
'register_policy' => Register::CLOSED
];
if (!empty($nodeinfo['openRegistrations'])) {
$server['register_policy'] = Register::OPEN;
@ -1202,17 +1219,20 @@ class GServer
}
/**
* Parses Nodeinfo 2
* Parses Nodeinfo with the versions 2.0, 2.1 and 2.2
*
* @see https://git.feneas.org/jaywink/nodeinfo2
* @see https://github.com/jhass/nodeinfo/tree/main/schemas/2.0
* @see https://github.com/jhass/nodeinfo/tree/main/schemas/2.1
* @see https://github.com/jhass/nodeinfo/tree/main/schemas/2.2
*
* @param string $nodeinfo_url address of the nodeinfo path
* @param string $nodeinfo_url address of the nodeinfo path
* @param int $detection_method nodeinfo version
*
* @return array Server data
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function parseNodeinfo2(string $nodeinfo_url): array
private static function parseNodeinfo_2(string $nodeinfo_url, int $detection_method): array
{
$curlResult = DI::httpClient()->get($nodeinfo_url, HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) {
@ -1225,7 +1245,7 @@ class GServer
}
$server = [
'detection-method' => self::DETECT_NODEINFO_2,
'detection-method' => $detection_method,
'register_policy' => Register::CLOSED,
'platform' => 'unknown',
];
@ -1234,6 +1254,15 @@ class GServer
$server['register_policy'] = Register::OPEN;
}
if (!empty($nodeinfo['instance'])) {
if (!empty($nodeinfo['instance']['name'])) {
$server['site_name'] = $nodeinfo['instance']['name'];
}
if (!empty($nodeinfo['instance']['description'])) {
$server['info'] = $nodeinfo['instance']['description'];
}
}
if (!empty($nodeinfo['software'])) {
if (isset($nodeinfo['software']['name'])) {
$server['platform'] = strtolower($nodeinfo['software']['name']);
@ -1249,6 +1278,13 @@ class GServer
if (($server['platform'] == 'mastodon') && substr($nodeinfo['software']['version'], -5) == '-qoto') {
$server['platform'] = 'qoto';
}
if (isset($nodeinfo['software']['repository'])) {
$server['repository'] = strtolower($nodeinfo['software']['repository']);
}
if (isset($nodeinfo['software']['homepage'])) {
$server['homepage'] = strtolower($nodeinfo['software']['homepage']);
}
}
}
@ -1260,6 +1296,9 @@ class GServer
if (!empty($nodeinfo['metadata']['nodeName'])) {
$server['site_name'] = $nodeinfo['metadata']['nodeName'];
}
if (!empty($nodeinfo['metadata']['nodeDescription'])) {
$server['info'] = $nodeinfo['metadata']['nodeDescription'];
}
if (!empty($nodeinfo['usage']['users']['total'])) {
$server['registered-users'] = max($nodeinfo['usage']['users']['total'], 1);
@ -1320,9 +1359,9 @@ class GServer
}
/**
* Parses NodeInfo2 protocol 1.0
* Parses NodeInfo2
*
* @see https://github.com/jaywink/nodeinfo2/blob/master/PROTOCOL.md
* @see https://github.com/jaywink/nodeinfo2
*
* @param string $nodeinfo_url address of the nodeinfo path
*
@ -1330,7 +1369,7 @@ class GServer
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function parseNodeinfo210(ICanHandleHttpResponses $httpResult): array
private static function parseNodeinfo2(ICanHandleHttpResponses $httpResult): array
{
if (!$httpResult->isSuccess()) {
return [];
@ -1342,8 +1381,10 @@ class GServer
return [];
}
$server = ['detection-method' => self::DETECT_NODEINFO_210,
'register_policy' => Register::CLOSED];
$server = [
'detection-method' => self::DETECT_NODEINFO2_10,
'register_policy' => Register::CLOSED
];
if (!empty($nodeinfo['openRegistrations'])) {
$server['register_policy'] = Register::OPEN;
@ -2570,6 +2611,10 @@ class GServer
return;
}
if ($data['openwebauth'] == $gserver['openwebauth']) {
return;
}
$serverdata = self::getZotData($gserver['url'], []);
if (empty($serverdata)) {
$serverdata = ['openwebauth' => $data['openwebauth']];

View file

@ -198,6 +198,10 @@ class Item
$fields['external-id'] = ItemURI::getIdByURI($fields['extid']);
}
if (!empty($fields['replies'])) {
$fields['replies-id'] = ItemURI::getIdByURI($fields['replies']);
}
if (!empty($fields['verb'])) {
$fields['vid'] = Verb::getID($fields['verb']);
}
@ -415,8 +419,24 @@ class Item
self::markForDeletion(['parent' => $item['parent'], 'deleted' => false], $priority);
}
if ($item['uid'] == 0 && $item['gravity'] == self::GRAVITY_PARENT) {
$posts = DI::keyValue()->get('nodeinfo_total_posts') ?? 0;
DI::keyValue()->set('nodeinfo_total_posts', $posts - 1);
} elseif ($item['uid'] == 0 && $item['gravity'] == self::GRAVITY_COMMENT) {
$comments = DI::keyValue()->get('nodeinfo_total_comments') ?? 0;
DI::keyValue()->set('nodeinfo_total_comments', $comments - 1);
}
// Is it our comment and/or our thread?
if (($item['origin'] || $parent['origin']) && ($item['uid'] != 0)) {
if ($item['origin'] && $item['gravity'] == self::GRAVITY_PARENT) {
$posts = DI::keyValue()->get('nodeinfo_local_posts') ?? 0;
DI::keyValue()->set('nodeinfo_local_posts', $posts - 1);
} elseif ($item['origin'] && $item['gravity'] == self::GRAVITY_COMMENT) {
$comments = DI::keyValue()->get('nodeinfo_local_comments') ?? 0;
DI::keyValue()->set('nodeinfo_local_comments', $comments - 1);
}
// When we delete the original post we will delete all existing copies on the server as well
self::markForDeletion(['uri-id' => $item['uri-id'], 'deleted' => false], $priority);
@ -536,9 +556,9 @@ class Item
}
if (!empty($item['causer-id']) && Contact::isSharing($item['causer-id'], $item['uid'], true)) {
$cdata = Contact::getPublicAndUserContactID($item['causer-id'], $item['uid']);
if (!empty($cdata['user'])) {
return $cdata['user'];
$ucid = Contact::getUserContactId($item['causer-id'], $item['uid']);
if ($ucid) {
return $ucid;
}
}
@ -1077,6 +1097,10 @@ class Item
$parent_id = 0;
$parent_origin = $item['origin'];
if ($item['wall'] && empty($item['context'])) {
$item['context'] = $item['parent-uri'] . '#context';
}
if ($item['wall'] && empty($item['conversation'])) {
$item['conversation'] = $item['parent-uri'] . '#context';
}
@ -1098,6 +1122,10 @@ class Item
$item['conversation-id'] = ItemURI::getIdByURI($item['conversation']);
}
if (!empty($item['context']) && empty($item['context-id'])) {
$item['context-id'] = ItemURI::getIdByURI($item['context']);
}
// Is this item available in the global items (with uid=0)?
if ($item['uid'] == 0) {
$item['global'] = true;
@ -1165,6 +1193,10 @@ class Item
$item['external-id'] = ItemURI::getIdByURI($item['extid']);
}
if (!empty($item['replies'])) {
$item['replies-id'] = ItemURI::getIdByURI($item['replies']);
}
if ($item['verb'] == Activity::ANNOUNCE) {
self::setOwnerforResharedItem($item);
}
@ -1334,6 +1366,14 @@ class Item
return 0;
}
if ($posted_item['origin'] && $posted_item['gravity'] == self::GRAVITY_PARENT) {
$posts = (int)(DI::keyValue()->get('nodeinfo_local_posts') ?? 0);
DI::keyValue()->set('nodeinfo_local_posts', $posts + 1);
} elseif ($posted_item['origin'] && $posted_item['gravity'] == self::GRAVITY_COMMENT) {
$comments = (int)(DI::keyValue()->get('nodeinfo_local_comments') ?? 0);
DI::keyValue()->set('nodeinfo_local_comments', $comments + 1);
}
Post\Origin::insert($posted_item);
// update the commented timestamp on the parent
@ -1423,6 +1463,14 @@ class Item
}
if ($inserted) {
if ($posted_item['gravity'] == self::GRAVITY_PARENT) {
$posts = (int)(DI::keyValue()->get('nodeinfo_total_posts') ?? 0);
DI::keyValue()->set('nodeinfo_total_posts', $posts + 1);
} elseif ($posted_item['gravity'] == self::GRAVITY_COMMENT) {
$comments = (int)(DI::keyValue()->get('nodeinfo_total_comments') ?? 0);
DI::keyValue()->set('nodeinfo_total_comments', $comments + 1);
}
// Fill the cache with the rendered content.
if (in_array($posted_item['gravity'], [self::GRAVITY_PARENT, self::GRAVITY_COMMENT])) {
self::updateDisplayCache($posted_item['uri-id']);
@ -2584,12 +2632,12 @@ class Item
return;
}
$cdata = Contact::getPublicAndUserContactID($item['author-id'], $item['uid']);
if (empty($cdata['user']) || ($cdata['user'] != $item['contact-id'])) {
$ucid = Contact::getUserContactId($item['author-id'], $item['uid']);
if (!$ucid || ($ucid != $item['contact-id'])) {
return;
}
if (!DBA::exists('contact', ['id' => $cdata['user'], 'remote_self' => LocalRelationship::MIRROR_NATIVE_RESHARE])) {
if (!DBA::exists('contact', ['id' => $ucid, 'remote_self' => LocalRelationship::MIRROR_NATIVE_RESHARE])) {
return;
}
@ -4118,6 +4166,10 @@ class Item
return $item_id;
}
if (ActivityPub\Processor::alreadyKnown($uri, '')) {
return 0;
}
$hookData = [
'uri' => $uri,
'uid' => $uid,
@ -4213,4 +4265,22 @@ class Item
Logger::warning('Post does not exist although it was supposed to had been fetched.', ['id' => $id, 'url' => $url, 'uid' => $uid]);
return 0;
}
public static function incrementInbound(string $network)
{
$packets = (int)(DI::keyValue()->get('stats_packets_inbound_' . $network) ?? 0);
if ($packets >= PHP_INT_MAX) {
$packets = 0;
}
DI::keyValue()->set('stats_packets_inbound_' . $network, $packets + 1);
}
public static function incrementOutbound(string $network)
{
$packets = (int)(DI::keyValue()->get('stats_packets_outbound_' . $network) ?? 0);
if ($packets >= PHP_INT_MAX) {
$packets = 0;
}
DI::keyValue()->set('stats_packets_outbound_' . $network, $packets + 1);
}
}

View file

@ -53,6 +53,8 @@ class Nodeinfo
return;
}
$logger->info('User statistics - start');
$userStats = User::getStatistics();
DI::keyValue()->set('nodeinfo_total_users', $userStats['total_users']);
@ -60,21 +62,26 @@ class Nodeinfo
DI::keyValue()->set('nodeinfo_active_users_monthly', $userStats['active_users_monthly']);
DI::keyValue()->set('nodeinfo_active_users_weekly', $userStats['active_users_weekly']);
$logger->info('user statistics', $userStats);
$logger->info('user statistics - done', $userStats);
$posts = DBA::count('post-thread', ["`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE NOT `deleted` AND `origin`)"]);
$comments = DBA::count('post', ["NOT `deleted` AND `gravity` = ? AND `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)", Item::GRAVITY_COMMENT]);
DI::keyValue()->set('nodeinfo_local_posts', $posts);
DI::keyValue()->set('nodeinfo_local_comments', $comments);
$logger->info('User activity', ['posts' => $posts, 'comments' => $comments]);
$posts = DBA::count('post', ['deleted' => false, 'gravity' => Item::GRAVITY_COMMENT]);
$comments = DBA::count('post', ['deleted' => false, 'gravity' => Item::GRAVITY_COMMENT]);
DI::keyValue()->set('nodeinfo_total_posts', $posts);
DI::keyValue()->set('nodeinfo_total_comments', $comments);
$logger->info('Post statistics - done', ['posts' => $posts, 'comments' => $comments]);
}
/**
* Return the supported services
*
* @return Object with supported services
*/
*/
public static function getUsage(bool $version2 = false)
{
$config = DI::config();
@ -101,7 +108,7 @@ class Nodeinfo
* Return the supported services
*
* @return array with supported services
*/
*/
public static function getServices(): array
{
$services = [

View file

@ -141,7 +141,7 @@ class Content
if ($uid != 0) {
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND (NOT `restricted` OR `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `uid` = ?))", $search, $uid];
} else {
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND NOT `restricted", $search];
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND NOT `restricted`", $search];
}
return DBA::count(SearchIndex::getSearchTable(), $condition);
}

View file

@ -88,8 +88,8 @@ class Media
// "document" has got the lowest priority. So when the same file is both attached as document
// and embedded as picture then we only store the picture or replace the document
$found = DBA::selectFirst('post-media', ['type'], ['uri-id' => $media['uri-id'], 'url' => $media['url']]);
if (!$force && !empty($found) && (($found['type'] != self::DOCUMENT) || ($media['type'] == self::DOCUMENT))) {
Logger::info('Media already exists', ['uri-id' => $media['uri-id'], 'url' => $media['url']]);
if (!$force && !empty($found) && (!in_array($found['type'], [self::UNKNOWN, self::DOCUMENT]) || ($media['type'] == self::DOCUMENT))) {
Logger::info('Media already exists', ['uri-id' => $media['uri-id'], 'url' => $media['url'], 'found' => $found['type'], 'new' => $media['type']]);
return false;
}
@ -444,42 +444,46 @@ class Media
return $data;
}
$type = explode('/', current(explode(';', $data['mimetype'])));
$data['type'] = self::getType($data['mimetype']);
return $data;
}
public static function getType(string $mimeType): int
{
$type = explode('/', current(explode(';', $mimeType)));
if (count($type) < 2) {
Logger::info('Unknown MimeType', ['type' => $type, 'media' => $data]);
$data['type'] = self::UNKNOWN;
return $data;
Logger::info('Unknown MimeType', ['type' => $type, 'media' => $mimeType]);
return self::UNKNOWN;
}
$filetype = strtolower($type[0]);
$subtype = strtolower($type[1]);
if ($filetype == 'image') {
$data['type'] = self::IMAGE;
$type = self::IMAGE;
} elseif ($filetype == 'video') {
$data['type'] = self::VIDEO;
$type = self::VIDEO;
} elseif ($filetype == 'audio') {
$data['type'] = self::AUDIO;
$type = self::AUDIO;
} elseif (($filetype == 'text') && ($subtype == 'html')) {
$data['type'] = self::HTML;
$type = self::HTML;
} elseif (($filetype == 'text') && ($subtype == 'xml')) {
$data['type'] = self::XML;
$type = self::XML;
} elseif (($filetype == 'text') && ($subtype == 'plain')) {
$data['type'] = self::PLAIN;
$type = self::PLAIN;
} elseif ($filetype == 'text') {
$data['type'] = self::TEXT;
$type = self::TEXT;
} elseif (($filetype == 'application') && ($subtype == 'x-bittorrent')) {
$data['type'] = self::TORRENT;
$type = self::TORRENT;
} elseif ($filetype == 'application') {
$data['type'] = self::APPLICATION;
$type = self::APPLICATION;
} else {
$data['type'] = self::UNKNOWN;
Logger::info('Unknown type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $data]);
return $data;
$type = self::UNKNOWN;
Logger::info('Unknown type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $mimeType]);
}
Logger::debug('Detected type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $data]);
return $data;
Logger::debug('Detected type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $mimeType]);
return $type;
}
/**

View file

@ -622,8 +622,10 @@ class Profile
$bd_format = DI::l10n()->t('g A l F d'); // 8 AM Friday January 18
$classtoday = '';
$condition = ["`uid` = ? AND `type` != 'birthday' AND `start` < ? AND `start` >= ?",
$uid, DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utc('now - 1 days')];
$condition = [
"`uid` = ? AND `type` != 'birthday' AND `start` < ? AND `start` >= ?",
$uid, DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utc('now - 1 days')
];
$s = DBA::select('event', [], $condition, ['order' => ['start']]);
$r = [];
@ -633,9 +635,11 @@ class Profile
$total = 0;
while ($rr = DBA::fetch($s)) {
$condition = ['parent-uri' => $rr['uri'], 'uid' => $rr['uid'], 'author-id' => $pcid,
$condition = [
'parent-uri' => $rr['uri'], 'uid' => $rr['uid'], 'author-id' => $pcid,
'vid' => [Verb::getID(Activity::ATTEND), Verb::getID(Activity::ATTENDMAYBE)],
'visible' => true, 'deleted' => false];
'visible' => true, 'deleted' => false
];
if (!Post::exists($condition)) {
continue;
}
@ -724,7 +728,8 @@ class Profile
if (!empty($search)) {
$publish = (DI::config()->get('system', 'publish_all') ? '' : "AND `publish` ");
$searchTerm = '%' . $search . '%';
$condition = ["`verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`
$condition = [
"`verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`
$publish
AND ((`name` LIKE ?) OR
(`nickname` LIKE ?) OR
@ -735,7 +740,8 @@ class Profile
(`pub_keywords` LIKE ?) OR
(`prv_keywords` LIKE ?))",
$searchTerm, $searchTerm, $searchTerm, $searchTerm,
$searchTerm, $searchTerm, $searchTerm, $searchTerm];
$searchTerm, $searchTerm, $searchTerm, $searchTerm
];
} else {
$condition = ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false];
if (!DI::config()->get('system', 'publish_all')) {
@ -838,4 +844,44 @@ class Profile
DBA::delete('profile', ['id' => $profile['id']]);
}
}
/**
* Get "about" field with the added responsible relay contact if appropriate.
*
* @param string $about
* @param integer|null $parent_uid
* @param integer $account_type
* @param string $language
* @return string
*/
public static function addResponsibleRelayContact(string $about = null, int $parent_uid = null, int $account_type, string $language): ?string
{
if (($account_type != User::ACCOUNT_TYPE_RELAY) || empty($parent_uid)) {
return $about;
}
$parent = User::getOwnerDataById($parent_uid);
if (strpos($about, $parent['addr']) || strpos($about, $parent['url'])) {
return $about;
}
$l10n = DI::l10n()->withLang($language);
return $about .= "\n" . $l10n->t('Responsible account: %s', $parent['addr']);
}
/**
* Set "about" field with the added responsible relay contact if appropriate.
*
* @param integer $uid
* @return void
*/
public static function setResponsibleRelayContact(int $uid)
{
$owner = User::getOwnerDataById($uid);
$about = self::addResponsibleRelayContact($owner['about'], $owner['parent-uid'], $owner['account-type'], $owner['language']);
if ($about != $owner['about']) {
self::update(['about' => $about], $uid);
}
}
}

View file

@ -412,6 +412,29 @@ class User
return 0;
}
/**
* Returns the user id of a given contact id
*
* @param int $cid
*
* @return integer user id
* @throws Exception
*/
public static function getIdForContactId(int $cid): int
{
$account = Contact::selectFirstAccountUser(['pid', 'self', 'uid'], ['id' => $cid]);
if (empty($account['pid'])) {
return 0;
}
if ($account['self']) {
return $account['uid'];
}
$self = Contact::selectFirstAccountUser(['uid'], ['pid' => $cid, 'self' => true]);
return $self['uid'] ?? 0;
}
/**
* Get a user based on its email
*