Merge branch 'develop' into phpstan-level-1

This commit is contained in:
Art4 2024-11-16 19:59:32 +00:00
commit 8bbf9d25b6
561 changed files with 65016 additions and 61395 deletions

View file

@ -51,7 +51,7 @@ class App
{
const PLATFORM = 'Friendica';
const CODENAME = 'Yellow Archangel';
const VERSION = '2024.09-dev';
const VERSION = '2024.12-dev';
// Allow themes to control internal parameters
// by changing App values in theme.php

View file

@ -273,6 +273,13 @@ class Page implements ArrayAccess
'$max_imagesize' => round(Images::getMaxUploadBytes() / 1000000, 0),
]) . $this->page['htmlhead'];
if ($pConfig->get($localUID, 'accessibility', 'hide_empty_descriptions')) {
$this->page['htmlhead'] .= "<style>.empty-description {display: none;}</style>\n";
}
if ($pConfig->get($localUID, 'accessibility', 'hide_custom_emojis')) {
$this->page['htmlhead'] .= "<style>span.emoji.mastodon img {display: none;}</style>\n";
}
}
/**

View file

@ -12,15 +12,20 @@ use Friendica\Core\Protocol;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Util\Strings;
use GuzzleHttp\Psr7\Uri;
/**
* ContactSelector class
*/
class ContactSelector
{
const SVG_DISABLED = -1;
const SVG_COLOR_BLACK = 0;
const SVG_BLACK = 1;
const SVG_COLOR_WHITE = 2;
const SVG_WHITE = 3;
static $serverdata = [];
static $server_url = [];
static $server_id = [];
/**
* @param string $current current
@ -50,50 +55,51 @@ class ContactSelector
return $o;
}
private static function getServerForProfile(string $profile)
/**
* Fetches the server id for a given profile
*
* @param string $profile
* @return integer
*/
public static function getServerIdForProfile(string $profile): int
{
$server_url = self::getServerURLForProfile($profile);
if (!empty(self::$serverdata[$server_url])) {
return self::$serverdata[$server_url];
if (!empty(self::$server_id[$profile])) {
return self::$server_id[$profile];
}
// Now query the GServer for the platform name
$gserver = DBA::selectFirst('gserver', ['platform', 'network'], ['nurl' => $server_url]);
$contact = DBA::selectFirst('contact', ['gsid'], ['uid' => 0, 'nurl' => Strings::normaliseLink($profile)]);
if (empty($contact['gsid'])) {
return 0;
}
self::$serverdata[$server_url] = $gserver;
return $gserver;
self::$server_id[$profile] = $contact['gsid'];
return $contact['gsid'];
}
/**
* @param string $profile Profile URL
* @return string Server URL
* @throws \Exception
* Get server array for a given server id
*
* @param integer $gsid
* @return array
*/
private static function getServerURLForProfile(string $profile): string
private static function getServerForId(int $gsid = null): array
{
if (!empty(self::$server_url[$profile])) {
return self::$server_url[$profile];
if (empty($gsid)) {
return [];
}
$server_url = '';
// Fetch the server url from the contact table
$contact = DBA::selectFirst('contact', ['baseurl'], ['uid' => 0, 'nurl' => Strings::normaliseLink($profile)]);
if (DBA::isResult($contact) && !empty($contact['baseurl'])) {
$server_url = Strings::normaliseLink($contact['baseurl']);
if (!empty(self::$serverdata[$gsid])) {
return self::$serverdata[$gsid];
}
if (empty($server_url)) {
// Create the server url out of the profile url
$parts = parse_url($profile);
unset($parts['path']);
$server_url = Strings::normaliseLink((string)Uri::fromParts((array)$parts));
$gserver = DBA::selectFirst('gserver', ['id', 'url', 'platform', 'network'], ['id' => $gsid]);
if (empty($gserver)) {
return [];
}
self::$server_url[$profile] = $server_url;
return $server_url;
self::$serverdata[$gserver['id']] = $gserver;
return $gserver;
}
/**
@ -106,7 +112,7 @@ class ContactSelector
* @return string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function networkToName(string $network, string $profile = '', string $protocol = '', int $gsid = null): string
public static function networkToName(string $network, string $protocol = '', int $gsid = null): string
{
$nets = [
Protocol::DFRN => DI::l10n()->t('DFRN'),
@ -137,15 +143,8 @@ class ContactSelector
$networkname = str_replace($search, $replace, $network);
if ((in_array($network, Protocol::FEDERATED)) && ($profile != "")) {
if (!empty($gsid) && !empty(self::$serverdata[$gsid])) {
$gserver = self::$serverdata[$gsid];
} elseif (!empty($gsid)) {
$gserver = DBA::selectFirst('gserver', ['platform', 'network'], ['id' => $gsid]);
self::$serverdata[$gsid] = $gserver;
} else {
$gserver = self::getServerForProfile($profile);
}
if (in_array($network, Protocol::FEDERATED) && !empty($gsid)) {
$gserver = self::getServerForId($gsid);
if (!empty($gserver['platform'])) {
$platform = $gserver['platform'];
@ -155,80 +154,132 @@ class ContactSelector
if (!empty($platform)) {
$networkname = $platform;
if ($network == Protocol::ACTIVITYPUB) {
$networkname .= ' (AP)';
}
}
}
if (!empty($protocol) && ($protocol != $network)) {
if (!empty($protocol) && ($protocol != $network) && $network != Protocol::DFRN) {
$networkname = DI::l10n()->t('%s (via %s)', $networkname, self::networkToName($protocol));
} elseif (in_array($network, ['', $protocol]) && ($network == Protocol::DFRN)) {
$networkname .= ' (DFRN)';
} elseif (in_array($network, ['', $protocol]) && ($network == Protocol::DIASPORA) && ($platform != 'diaspora')) {
$networkname .= ' (Diaspora)';
}
return $networkname;
}
/**
* Determines network's icon name
* Fetch the platform SVG of a given system
* @see https://codeberg.org/FediverseIconography/pages
* @see https://github.com/simple-icons/simple-icons
* @see https://icon-sets.iconify.design
*
* @param string $network network
* @param string $profile optional, default empty
* @param int $gsid Server id
* @return string Name for network icon
* @throws \Exception
* @param string $network
* @param integer|null $gsid
* @param string $platform
* @param integer $uid
* @return string
*/
public static function networkToIcon(string $network, string $profile = "", int $gsid = null): string
public static function networkToSVG(string $network, int $gsid = null, string $platform = '', int $uid = 0): string
{
$nets = [
Protocol::DFRN => 'friendica',
Protocol::OSTATUS => 'gnu-social', // There is no generic OStatus icon
Protocol::FEED => 'rss',
Protocol::MAIL => 'inbox',
Protocol::DIASPORA => 'diaspora',
Protocol::ZOT => 'hubzilla',
Protocol::LINKEDIN => 'linkedin',
Protocol::XMPP => 'xmpp',
Protocol::MYSPACE => 'file-text-o', /// @todo
Protocol::GPLUS => 'google-plus',
Protocol::PUMPIO => 'file-text-o', /// @todo
Protocol::TWITTER => 'twitter',
Protocol::DISCOURSE => 'dot-circle-o', /// @todo
Protocol::DIASPORA2 => 'diaspora',
Protocol::STATUSNET => 'gnu-social',
Protocol::ACTIVITYPUB => 'activitypub',
Protocol::PNUT => 'file-text-o', /// @todo
Protocol::TUMBLR => 'tumblr',
Protocol::BLUESKY => 'circle', /// @todo
];
$platform_icon_style = $uid ? (DI::pConfig()->get($uid, 'accessibility', 'platform_icon_style') ?? self::SVG_COLOR_BLACK) : self::SVG_COLOR_BLACK;
$platform_icons = ['diaspora' => 'diaspora', 'friendica' => 'friendica', 'friendika' => 'friendica',
'GNU Social' => 'gnu-social', 'gnusocial' => 'gnu-social', 'hubzilla' => 'hubzilla',
'mastodon' => 'mastodon', 'hometown' => 'mastodon', 'peertube' => 'peertube', 'pixelfed' => 'pixelfed',
'pleroma' => 'pleroma', 'akkoma' => 'pleroma', 'red' => 'hubzilla', 'redmatrix' => 'hubzilla',
'socialhome' => 'social-home', 'wordpress' => 'wordpress', 'lemmy' => 'users',
'plume' => 'plume', 'funkwhale' => 'funkwhale', 'nextcloud' => 'nextcloud', 'drupal' => 'drupal',
'firefish' => 'fire', 'calckey' => 'calculator', 'kbin' => 'check', 'threads' => 'instagram'];
if ($platform_icon_style == self::SVG_DISABLED) {
return '';
}
$nets = [
Protocol::ACTIVITYPUB => 'activitypub', // https://commons.wikimedia.org/wiki/File:ActivityPub-logo-symbol.svg
Protocol::BLUESKY => 'bluesky', // https://commons.wikimedia.org/wiki/File:Bluesky_Logo.svg
Protocol::DFRN => 'friendica',
Protocol::DIASPORA => 'diaspora', // https://www.svgrepo.com/svg/362315/diaspora
Protocol::DIASPORA2 => 'diaspora', // https://www.svgrepo.com/svg/362315/diaspora
Protocol::DISCOURSE => 'discourse', // https://commons.wikimedia.org/wiki/File:Discourse_icon.svg
Protocol::FEED => 'rss', // https://commons.wikimedia.org/wiki/File:Generic_Feed-icon.svg
Protocol::MAIL => 'email', // https://www.svgrepo.com/svg/501173/email
Protocol::OSTATUS => '',
Protocol::PNUT => '',
Protocol::PUMPIO => 'pump-io', // https://commons.wikimedia.org/wiki/File:Pump.io_Logo.svg
Protocol::STATUSNET => '',
Protocol::TUMBLR => 'tumblr', // https://commons.wikimedia.org/wiki/File:Tumblr.svg
Protocol::TWITTER => '',
Protocol::ZOT => 'hubzilla', // https://www.svgrepo.com/svg/362219/hubzilla
];
$search = array_keys($nets);
$replace = array_values($nets);
$network_icon = str_replace($search, $replace, $network);
$network_svg = str_replace($search, $replace, $network);
if ((in_array($network, Protocol::FEDERATED)) && ($profile != "")) {
if (!empty($gsid) && !empty(self::$serverdata[$gsid])) {
$gserver = self::$serverdata[$gsid];
} elseif (!empty($gsid)) {
$gserver = DBA::selectFirst('gserver', ['platform', 'network'], ['id' => $gsid]);
self::$serverdata[$gsid] = $gserver;
} else {
$gserver = self::getServerForProfile($profile);
}
if (!empty($gserver['platform'])) {
$network_icon = $platform_icons[strtolower($gserver['platform'])] ?? $network_icon;
}
if (in_array($network, Protocol::FEDERATED) && !empty($gsid)) {
$gserver = self::getServerForId($gsid);
$platform = $gserver['platform'];
}
return $network_icon;
$svg = ['aardwolf', 'activitypods', 'activitypub', 'akkoma', 'anfora', 'awakari', 'azorius',
'bluesky', 'bonfire', 'bookwyrm', 'bridgy_fed', 'brighteon_social', 'brutalinks', 'calckey',
'castopod', 'catodon', 'chatter_net', 'chuckya', 'clubsall', 'communecter', 'decodon',
'diaspora', 'discourse', 'dolphin', 'drupal', 'email', 'emissary', 'epicyon', 'f2ap',
'fedibird', 'fedify', 'firefish', 'flipboard', 'flohmarkt', 'forgefriends', 'forgejo',
'forte', 'foundkey', 'friendica', 'funkwhale', 'gancio', 'gath.io', 'ghost', 'gitlab',
'glitch-soc', 'glitchsoc', 'gnu_social', 'gnusocial', 'goblin', 'go-fed', 'gotosocial',
'greatape', 'guppe', 'hollo', 'hometown', 'honk', 'hubzilla', 'iceshrimp', 'juick', 'kazarma',
'kbin', 'kepi', 'kitsune', 'kmyblue', 'kookie', 'ktistec', 'lemmy', 'loops', 'mastodon',
'mbin', 'micro.blog', 'minds', 'misskey', 'mistpark', 'mitra', 'mobilizon', 'neodb',
'newsmast', 'nextcloud_social', 'nodebb', 'osada', 'owncast', 'peertube', 'piefed', 'pinetta',
'pixelfed', 'pleroma', 'plume', 'postmarks', 'prismo', 'pump-io', 'rebased', 'redmatrix',
'reel2bits', 'rss', 'ruffy', 'sakura', 'seppo', 'shadowfacts', 'sharky', 'shuttlecraft',
'smilodon', 'smithereen', 'snac', 'soapbox', 'socialhome', 'streams', 'sublinks', 'sutty',
'takahē', 'takesama', 'threads', 'tumblr', 'vernissage', 'vervis', 'vidzy', 'vocata', 'wafrn',
'wildebeest', 'wordpress', 'write.as', 'writefreely', 'wxwclub', 'xwiki', 'zap'];
if (in_array($platform_icon_style,[self::SVG_WHITE, self::SVG_COLOR_WHITE])) {
$svg = ['activitypub', 'akkoma', 'andstatus', 'bluesky', 'bonfire', 'bookwyrm', 'bridgy_fed',
'calckey', 'castopod', 'diaspora', 'discourse', 'dolphin', 'drupal', 'email', 'firefish',
'flipboard', 'flohmarkt', 'forgejo', 'friendica', 'funkwhale', 'ghost', 'gitlab',
'glitch-soc', 'gnusocial', 'gotosocial', 'guppe', 'hollo', 'hubzilla', 'iceshrimp', 'kbin',
'lemmy', 'loforo', 'loops', 'mastodon', 'mbin', 'microblog', 'minds', 'misskey', 'mobilizon',
'nextcloud', 'owncast', 'peertube', 'phanpy', 'pixelfed', 'pleroma', 'plume', 'rss', 'shark',
'soapbox', 'socialhome', 'streams', 'takahē', 'threads', 'tumblr', 'wordpress', 'write.as',
'writefreely', 'xwiki', 'zap'];
}
if (!empty($platform)) {
$aliases = [
'brighteon' => 'brighteon_social',
'bridgy-fed' => 'bridgy_fed',
'friendika' => 'friendica',
'gathio' => 'gath.io',
'GNU Social' => 'gnu_social',
'gnusocial' => 'gnu_social',
'guppe groups' => 'guppe',
'microblog' => 'micro.blog',
'microblogpub' => 'micro.blog',
'nextcloud' => 'nextcloud_social',
'red' => 'redmatrix',
'sharkey' => 'sharky',
'sutty-distributed-press' => 'sutty',
];
$platform = str_replace(array_keys($aliases), array_values($aliases), $platform);
$network_svg = in_array($platform, $svg) ? $platform : $network_svg;
}
if (empty($network_svg)) {
return '';
}
$color = ['aardwolf', 'activitypods', 'activitypub', 'akkoma', 'bluesky', 'chuckya', 'decodon',
'discourse', 'fedify', 'firefish', 'flipboard', 'friendica', 'gitlab', 'gnusocial', 'kookie',
'loops', 'mastodon', 'mbin', 'misskey', 'neodb', 'newsmast', 'nodebb', 'peertube', 'pixelfed',
'pleroma', 'rss', 'sharky', 'tumblr', 'vervis', 'vocata', 'wordpress'];
if (in_array($platform_icon_style, [self::SVG_COLOR_BLACK, self::SVG_COLOR_WHITE]) && in_array($network_svg, $color)) {
return 'images/platforms/color/' . $network_svg . '.svg';
} elseif (in_array($platform_icon_style, [self::SVG_WHITE, self::SVG_COLOR_WHITE])) {
return 'images/platforms/white/' . $network_svg . '.svg';
} else {
return 'images/platforms/black/' . $network_svg . '.svg';
}
}
}

View file

@ -1516,8 +1516,8 @@ class Conversation
'uriid' => $item['uri-id'],
'author_gsid' => $item['author-gsid'],
'network' => $item['network'],
'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network'], $item['author-gsid']),
'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link'], $item['author-gsid']),
'network_name' => ContactSelector::networkToName($item['author-network'], $item['network'], $item['author-gsid']),
'network_svg' => ContactSelector::networkToSVG($item['network'], $item['author-gsid'], '', $this->session->getLocalUserId()),
'linktitle' => $this->l10n->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
'profile_url' => $profile_link,
'item_photo_menu_html' => $this->item->photoMenu($item, $formSecurityToken),

View file

@ -686,7 +686,7 @@ class BBCode
// to the last element
$newbody = str_replace(
'[$#saved_image' . $cnt . '#$]',
'<img src="' . self::proxyUrl($image, self::INTERNAL, $uriid) . '" alt="' . DI::l10n()->t('Image/photo') . '" />',
'<img src="' . self::proxyUrl($image, self::INTERNAL, $uriid) . '" alt="" class="empty-description"/>',
$newbody
);
$cnt++;
@ -849,6 +849,7 @@ class BBCode
$img_str .= ' ' . $key . '="' . htmlspecialchars($value, ENT_COMPAT) . '"';
}
}
$img_str .= ' ' . empty($attributes['alt']) ? 'class="empty-description"' : 'class="has-alt-description"';
return $img_str . '>';
},
$text
@ -920,6 +921,7 @@ class BBCode
$contact = Contact::getByURL($attributes['profile'], false, ['network']);
$network = $contact['network'] ?? Protocol::PHANTOM;
$gsid = ContactSelector::getServerIdForProfile($attributes['profile']);
$tpl = Renderer::getMarkupTemplate('shared_content.tpl');
$text .= self::SHARED_ANCHOR . Renderer::replaceMacros($tpl, [
'$profile' => $attributes['profile'],
@ -929,8 +931,8 @@ class BBCode
'$link_title' => DI::l10n()->t('Link to source'),
'$posted' => $attributes['posted'],
'$guid' => $attributes['guid'],
'$network_name' => ContactSelector::networkToName($network, $attributes['profile']),
'$network_icon' => ContactSelector::networkToIcon($network, $attributes['profile']),
'$network_name' => ContactSelector::networkToName($network, '', $gsid),
'$network_svg' => ContactSelector::networkToSVG($network, $gsid),
'$content' => self::TOP_ANCHOR . self::setMentions(trim($content), 0, $network) . self::BOTTOM_ANCHOR,
]);
break;
@ -1826,8 +1828,8 @@ class BBCode
$text
);
$text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '<img src="$3" style="width: $1px;" >', $text);
$text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*)\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: $1px;" >', $text);
$text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '<img src="$3" style="width: $1px;" alt="" class="empty-description">', $text);
$text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*)\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: $1px;" alt="" class="empty-description">', $text);
$text = preg_replace_callback(
"/\[[iz]mg\=(.*?)\](.*?)\[\/[iz]mg\]/ism",
@ -1836,7 +1838,7 @@ class BBCode
$alt = htmlspecialchars($matches[2], ENT_COMPAT);
// Fix for Markdown problems with Diaspora, see issue #12701
if (($simple_html != self::DIASPORA) || strpos($matches[2], '"') === false) {
return '<img src="' . $matches[1] . '" alt="' . $alt . '" title="' . $alt . '">';
return '<img src="' . $matches[1] . '" alt="' . $alt . '" title="' . $alt . '" class="' . (empty($alt) ? 'empty-description' : 'has-alt-description') . '">';
} else {
return '<img src="' . $matches[1] . '" alt="' . $alt . '">';
}
@ -1859,8 +1861,8 @@ class BBCode
$text
);
$text = preg_replace("/\[img\](.*?)\[\/img\]/ism", '<img src="$1" alt="' . DI::l10n()->t('Image/photo') . '" />', $text);
$text = preg_replace("/\[zmg\](.*?)\[\/zmg\]/ism", '<img src="$1" alt="' . DI::l10n()->t('Image/photo') . '" />', $text);
$text = preg_replace("/\[img\](.*?)\[\/img\]/ism", '<img src="$1" alt="" class="empty-description"/>', $text);
$text = preg_replace("/\[zmg\](.*?)\[\/zmg\]/ism", '<img src="$1" alt="" class="empty-description" />', $text);
$text = self::convertImages($text, $simple_html, $uriid);

View file

@ -12,6 +12,7 @@ use Friendica\DI;
use Friendica\Model\Photo;
use Friendica\Model\Post;
use Friendica\Util\Network;
use IntlChar;
class Plaintext
{
@ -237,39 +238,62 @@ class Plaintext
*/
private static function getParts(string $message, int $baselimit): array
{
$parts = [];
$part = '';
$parts = [];
$part = '';
$break_pos = 0;
$comma_pos = 0;
$limit = $baselimit;
while ($message) {
$pos1 = strpos($message, ' ');
$pos2 = strpos($message, "\n");
$pos_word = mb_strpos($message, ' ');
$pos_paragraph = mb_strpos($message, "\n");
if (($pos1 !== false) && ($pos2 !== false)) {
$pos = min($pos1, $pos2) + 1;
} elseif ($pos1 !== false) {
$pos = $pos1 + 1;
} elseif ($pos2 !== false) {
$pos = $pos2 + 1;
if (($pos_word !== false) && ($pos_paragraph !== false)) {
$pos = min($pos_word, $pos_paragraph) + 1;
} elseif ($pos_word !== false) {
$pos = $pos_word + 1;
} elseif ($pos_paragraph !== false) {
$pos = $pos_paragraph + 1;
} else {
$word = $message;
$message = '';
}
if (trim($message)) {
$word = substr($message, 0, $pos);
$message = trim(substr($message, $pos));
$word = mb_substr($message, 0, $pos);
$message = trim(mb_substr($message, $pos));
}
if (Network::isValidHttpUrl(trim($word))) {
$limit += mb_strlen(trim($word)) - self::URL_LENGTH;
}
$break = mb_strrpos($word, "\n") !== false;
if (!$break && (mb_strrpos($word, '. ') !== false || mb_strrpos($word, '? ') !== false || mb_strrpos($word, '! ') !== false)) {
$break = IntlChar::isupper(mb_substr($message, 0, 1));
}
$comma = (mb_strrpos($word, ', ') !== false) && IntlChar::isalpha(mb_substr($message, 0, 1));
if ((mb_strlen($part . $word) > $limit - 8) && ($parts || (mb_strlen($part . $word . $message) > $limit))) {
$parts[] = trim($part);
$part = '';
$limit = $baselimit;
if ($break_pos) {
$parts[] = trim(mb_substr($part, 0, $break_pos));
$part = mb_substr($part, $break_pos);
} elseif ($comma_pos) {
$parts[] = trim(mb_substr($part, 0, $comma_pos));
$part = mb_substr($part, $comma_pos);
} else {
$parts[] = trim($part);
$part = '';
}
$limit = $baselimit;
$break_pos = 0;
$comma_pos = 0;
} elseif ($break) {
$break_pos = $pos + mb_strlen($part);
} elseif ($comma) {
$comma_pos = $pos + mb_strlen($part);
}
$part .= $word;
}

View file

@ -91,23 +91,59 @@ class Widget
$networks = [Protocol::PHANTOM, Protocol::FACEBOOK, Protocol::APPNET, Protocol::TWITTER, Protocol::ZOT, Protocol::OSTATUS, Protocol::STATUSNET];
Addon::loadAddons();
if (!Addon::isEnabled("discourse")) {
if (!Addon::isEnabled('discourse')) {
$networks[] = Protocol::DISCOURSE;
}
if (!Addon::isEnabled("pumpio")) {
if (!Addon::isEnabled('pumpio')) {
$networks[] = Protocol::PUMPIO;
}
if (!Addon::isEnabled("tumblr")) {
if (!Addon::isEnabled('tumblr')) {
$networks[] = Protocol::TUMBLR;
}
if (!DI::config()->get("system", "diaspora_enabled")) {
if (!DI::config()->get('system', 'diaspora_enabled')) {
$networks[] = Protocol::DIASPORA;
}
if (!Addon::isEnabled("pnut")) {
if (!Addon::isEnabled('pnut')) {
$networks[] = Protocol::PNUT;
}
return $networks;
}
/**
* Return available networks as array
*
* @return array Supported networks
*/
public static function availableNetworks(): array
{
$networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::FEED];
Addon::loadAddons();
if (Addon::isEnabled('discourse')) {
$networks[] = Protocol::DISCOURSE;
}
if (Addon::isEnabled('pumpio')) {
$networks[] = Protocol::PUMPIO;
}
if (Addon::isEnabled('tumblr')) {
$networks[] = Protocol::TUMBLR;
}
if (DI::config()->get('system', 'diaspora_enabled')) {
$networks[] = Protocol::DIASPORA;
}
if (function_exists('imap_open') && !DI::config()->get('system', 'imap_disabled')) {
$networks[] = Protocol::MAIL;
}
if (Addon::isEnabled('pnut')) {
$networks[] = Protocol::PNUT;
}
return $networks;
@ -382,7 +418,7 @@ class Widget
$tpl = Renderer::getMarkupTemplate('widget/remote_friends_common.tpl');
return Renderer::replaceMacros($tpl, [
'$desc' => DI::l10n()->tt("%d contact in common", "%d contacts in common", $total),
'$desc' => DI::l10n()->tt('%d contact in common', '%d contacts in common', $total),
'$base' => DI::baseUrl(),
'$nickname' => $nickname,
'$linkmore' => $total > 5 ? 'true' : '',
@ -576,4 +612,4 @@ class Widget
$channelname
);
}
}
}

View file

@ -41,10 +41,10 @@ class VCard
if ($contact['network'] != '') {
$network_link = Strings::formatNetworkName($contact['network'], $contact_url);
$network_avatar = ContactSelector::networkToIcon($contact['network'], $contact_url);
$network_svg = ContactSelector::networkToSVG($contact['network'], $contact['gsid'], '', DI::userSession()->getLocalUserId());
} else {
$network_link = '';
$network_avatar = '';
$network_svg = '';
}
$follow_link = '';
@ -57,6 +57,11 @@ class VCard
$photo = Contact::getPhoto($contact);
if (DI::userSession()->getLocalUserId()) {
if (Contact\User::isIsBlocked($contact['id'], DI::userSession()->getLocalUserId())) {
$hide_follow = true;
$hide_mention = true;
}
if ($contact['uid']) {
$id = $contact['id'];
$rel = $contact['rel'];
@ -77,7 +82,7 @@ class VCard
if (in_array($rel, [Contact::SHARING, Contact::FRIEND])) {
$unfollow_link = 'contact/unfollow?url=' . urlencode($contact_url) . '&auto=1';
} elseif (!$pending) {
$follow_link = 'contact/follow?url=' . urlencode($contact_url) . '&auto=1';
$follow_link = 'contact/follow?binurl=' . bin2hex($contact_url) . '&auto=1';
}
}
@ -106,7 +111,7 @@ class VCard
'$matrix' => DI::l10n()->t('Matrix:'),
'$location' => DI::l10n()->t('Location:'),
'$network_link' => $network_link,
'$network_avatar' => $network_avatar,
'$network_svg' => $network_svg,
'$network' => DI::l10n()->t('Network:'),
'$account_type' => Contact::getAccountType($contact['contact-type']),
'$follow' => DI::l10n()->t('Follow'),

View file

@ -11,6 +11,7 @@ use Friendica\Database\DBA;
use Friendica\Model\User;
use Friendica\Network\HTTPException;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\ActivityPub\Transmitter;
use Friendica\Protocol\Diaspora;
/**
@ -32,26 +33,26 @@ class Protocol
const SUPPORT_PRIVATE = [self::DFRN, self::DIASPORA, self::MAIL, self::ACTIVITYPUB, self::PUMPIO];
// Supported through a connector
const DIASPORA2 = 'dspc'; // Diaspora connector
const PUMPIO = 'pump'; // pump.io
const TWITTER = 'twit'; // Twitter
const DISCOURSE = 'dscs'; // Discourse
const TUMBLR = 'tmbl'; // Tumblr
const BLUESKY = 'bsky'; // Bluesky
const DIASPORA2 = 'dspc'; // Diaspora connector
const DISCOURSE = 'dscs'; // Discourse
const PNUT = 'pnut'; // pnut.io
const PUMPIO = 'pump'; // pump.io
const TUMBLR = 'tmbl'; // Tumblr
const TWITTER = 'twit'; // Twitter
// Dead protocols
const OSTATUS = 'stat'; // GNU Social and other OStatus implementations
const STATUSNET = 'stac'; // Statusnet connector
const APPNET = 'apdn'; // app.net - Dead protocol
const FACEBOOK = 'face'; // Facebook API - Not working anymore, API is closed
const GPLUS = 'goog'; // Google+ - Dead in 2019
const OSTATUS = 'stat'; // GNU Social and other OStatus implementations
const STATUSNET = 'stac'; // Statusnet connector
// Currently unsupported
const ICALENDAR = 'ical'; // iCalendar
const MYSPACE = 'mysp'; // MySpace
const LINKEDIN = 'lnkd'; // LinkedIn
const MYSPACE = 'mysp'; // MySpace
const NEWS = 'nntp'; // Network News Transfer Protocol
const PNUT = 'pnut'; // pnut.io
const XMPP = 'xmpp'; // XMPP
const ZOT = 'zot!'; // Zot!
@ -211,7 +212,7 @@ class Protocol
}
/**
* Send a block message to a remote server. Only useful for connector addons.
* Send a block message to a remote server.
*
* @param array $contact Public contact record to block
* @param int $uid User issuing the block
@ -220,6 +221,23 @@ class Protocol
*/
public static function block(array $contact, int $uid): ?bool
{
if (empty($contact['network'])) {
throw new \InvalidArgumentException('Missing network key in contact array');
}
$protocol = $contact['network'];
if ($protocol == self::DFRN && !empty($contact['protocol'])) {
$protocol = $contact['protocol'];
}
if ($protocol == self::ACTIVITYPUB) {
$activity_id = Transmitter::activityIDFromContact($contact['id'], $uid);
if (empty($activity_id)) {
return false;
}
return ActivityPub\Transmitter::sendActivity('Block', $contact['url'], $uid, $activity_id);
}
// Catch-all hook for connector addons
$hook_data = [
'contact' => $contact,
@ -232,7 +250,7 @@ class Protocol
}
/**
* Send an unblock message to a remote server. Only useful for connector addons.
* Send an unblock message to a remote server.
*
* @param array $contact Public contact record to unblock
* @param int $uid User revoking the block
@ -241,6 +259,24 @@ class Protocol
*/
public static function unblock(array $contact, int $uid): ?bool
{
$owner = User::getOwnerDataById($uid);
if (!DBA::isResult($owner)) {
return false;
}
if (empty($contact['network'])) {
throw new \InvalidArgumentException('Missing network key in contact array');
}
$protocol = $contact['network'];
if ($protocol == self::DFRN && !empty($contact['protocol'])) {
$protocol = $contact['protocol'];
}
if ($protocol == self::ACTIVITYPUB) {
return ActivityPub\Transmitter::sendContactUnblock($contact['url'], $contact['id'], $owner);
}
// Catch-all hook for connector addons
$hook_data = [
'contact' => $contact,

View file

@ -24,6 +24,44 @@ class Update
const NEW_TABLE_STRUCTURE_VERSION = 1288;
/**
* Returns the status of the current update
*
* @return int
*/
public static function getStatus(): int
{
return (int)DI::config()->get('system', 'update') ?? static::SUCCESS;
}
/**
* Returns the latest Version of the Friendica git repository and null, if this node doesn't check updates automatically
*
* @return string
*/
public static function getAvailableVersion(): ?string
{
return DI::keyValue()->get('git_friendica_version') ?? null;
}
/**
* Returns true, if there's a new update and null if this node doesn't check updates automatically
*
* @return bool|null
*/
public static function isAvailable(): ?bool
{
if (DI::config()->get('system', 'check_new_version_url', 'none') != 'none') {
if (version_compare(App::VERSION, static::getAvailableVersion()) < 0) {
return true;
} else {
return false;
}
}
return null;
}
/**
* Function to check if the Database structure needs an update.
*

View file

@ -188,6 +188,16 @@ class DBStructure
return $status;
}
/**
* Returns the current status of the Update
*
* @return int
*/
public static function getUpdateStatus(): int
{
return (int)DI::config()->get('system', 'dbupdate') ?? static::UPDATE_NOT_CHECKED;
}
/**
* Updates DB structure from the installation and returns eventual errors messages
*

View file

@ -10,8 +10,10 @@ namespace Friendica\Factory\Api\Mastodon;
use Friendica\App\BaseURL;
use Friendica\BaseFactory;
use Friendica\Collection\Api\Mastodon\Fields;
use Friendica\Content\Widget;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Model\User;
use Friendica\Network\HTTPException;
use Friendica\Profile\ProfileField\Repository\ProfileField as ProfileFieldRepository;
use ImagickException;
@ -75,10 +77,15 @@ class Account extends BaseFactory
$fields = new Fields();
if (Contact::isLocal($account['url'])) {
$self_contact = Contact::selectFirst(['uid'], ['nurl' => $account['nurl'], 'self' => true]);
if (!empty($self_contact['uid'])) {
$profileFields = $this->profileFieldRepo->selectPublicFieldsByUserId($self_contact['uid']);
$profile_uid = User::getIdForContactId($account['id']);
if ($profile_uid) {
$profileFields = $this->profileFieldRepo->selectPublicFieldsByUserId($profile_uid);
$fields = $this->mstdnFieldFactory->createFromProfileFields($profileFields);
if ($profile_uid == $uid) {
$account['ap-followers_count'] = $this->getContactRelationCountForUid($uid, [Contact::FOLLOWER, Contact::FRIEND]);
$account['ap-following_count'] = $this->getContactRelationCountForUid($uid, [Contact::SHARING, Contact::FRIEND]);
}
}
}
@ -98,4 +105,20 @@ class Account extends BaseFactory
return new \Friendica\Object\Api\Mastodon\Account($this->baseUrl, $account, $fields);
}
private function getContactRelationCountForUid(int $uid, array $rel): int
{
$condition = [
'uid' => $uid,
'rel' => $rel,
'self' => false,
'deleted' => false,
'archive' => false,
'pending' => false,
'blocked' => false,
'network' => Widget::availableNetworks(),
];
return DBA::count('contact', $condition);
}
}

View file

@ -216,7 +216,7 @@ class Status extends BaseFactory
}
if ($platform == '') {
$platform = ContactSelector::networkToName($item['network'], $item['author-link'], $item['network'], $item['author-gsid']);
$platform = ContactSelector::networkToName($item['network'], $item['network'], $item['author-gsid']);
}
$application = new \Friendica\Object\Api\Mastodon\Application($item['app'] ?: $platform);
@ -328,7 +328,7 @@ class Status extends BaseFactory
$delivery_data = $uid != $item['uid'] ? null : new FriendicaDeliveryData($item['delivery_queue_count'], $item['delivery_queue_done'], $item['delivery_queue_failed']);
$visibility_data = $uid != $item['uid'] ? null : new FriendicaVisibility($this->aclFormatter->expand($item['allow_cid']), $this->aclFormatter->expand($item['deny_cid']), $this->aclFormatter->expand($item['allow_gid']), $this->aclFormatter->expand($item['deny_gid']));
$friendica = new FriendicaExtension($item['title'] ?? '', $item['changed'], $item['commented'], $item['received'], $counts->dislikes, $origin_dislike, $network, $platform, $version, $sitename, $delivery_data, $visibility_data);
$friendica = new FriendicaExtension($item['title'] ?? '', $item['changed'], $item['commented'], $item['received'], $counts->dislikes, $origin_dislike, $network, $platform, $version, $sitename, $delivery_data, $visibility_data, BBCode::convertForUriId($item['uri-id'], $item['body'], BBCode::EXTERNAL));
return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $friendica, $quote, $poll, $emojis);
}
@ -399,7 +399,7 @@ class Status extends BaseFactory
$attachments = [];
$in_reply = [];
$reshare = [];
$friendica = new FriendicaExtension('', null, null, null, 0, false, null, null, null, null, null, null);
$friendica = new FriendicaExtension('', null, null, null, 0, false, null, null, null, null, null, null, BBCode::convertForUriId($item['uri-id'], $item['body'], BBCode::EXTERNAL));
return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $friendica);
}

View file

@ -68,7 +68,7 @@ class Status extends BaseFactory
*/
public function createFromItemId(int $id, int $uid, bool $include_entities = false): \Friendica\Object\Api\Twitter\Status
{
$fields = ['parent-uri-id', 'uri-id', 'uid', 'author-id', 'author-link', 'author-network', 'owner-id', 'causer-id',
$fields = ['parent-uri-id', 'uri-id', 'uid', 'author-id', 'author-link', 'author-network', 'author-gsid', 'owner-id', 'causer-id',
'starred', 'app', 'title', 'body', 'raw-body', 'created', 'network','post-reason', 'language', 'gravity',
'thr-parent-id', 'parent-author-id', 'parent-author-nick', 'uri', 'plink', 'private', 'vid', 'coord', 'quote-uri-id'];
$item = Post::selectFirst($fields, ['id' => $id], ['order' => ['uid' => true]]);
@ -89,7 +89,7 @@ class Status extends BaseFactory
*/
public function createFromUriId(int $uriId, int $uid = 0, bool $include_entities = false): \Friendica\Object\Api\Twitter\Status
{
$fields = ['parent-uri-id', 'uri-id', 'uid', 'author-id', 'author-link', 'author-network', 'owner-id', 'causer-id',
$fields = ['parent-uri-id', 'uri-id', 'uid', 'author-id', 'author-link', 'author-network', 'author-gsid', 'owner-id', 'causer-id',
'starred', 'app', 'title', 'body', 'raw-body', 'created', 'network','post-reason', 'language', 'gravity',
'thr-parent-id', 'parent-author-id', 'parent-author-nick', 'uri', 'plink', 'private', 'vid', 'coord'];
$item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => [0, $uid]], ['order' => ['uid' => true]]);

View file

@ -1082,17 +1082,17 @@ class Contact
*/
public static function markForArchival(array $contact)
{
if (!isset($contact['url']) && !empty($contact['id'])) {
$fields = ['id', 'url', 'archive', 'self', 'term-date'];
if ((!isset($contact['uri-id']) || !isset($contact['url']) || !isset($contact['archive']) || !isset($contact['self']) || !isset($contact['term-date'])) && !empty($contact['id'])) {
$fields = ['id', 'uri-id', 'url', 'archive', 'self', 'term-date'];
$contact = DBA::selectFirst('contact', $fields, ['id' => $contact['id']]);
if (!DBA::isResult($contact)) {
return;
}
} elseif (!isset($contact['url'])) {
} elseif (!isset($contact['url']) || !isset($contact['uri-id'])) {
Logger::info('Empty contact', ['contact' => $contact]);
}
Logger::info('Contact is marked for archival', ['id' => $contact['id'], 'term-date' => $contact['term-date']]);
Logger::info('Contact is marked for archival', ['id' => $contact['id'], 'archive' => $contact['archive'], 'term-date' => $contact['term-date'], 'url' => $contact['url']]);
// Contact already archived or "self" contact? => nothing to do
if ($contact['archive'] || $contact['self']) {
@ -1100,8 +1100,7 @@ class Contact
}
if ($contact['term-date'] <= DBA::NULL_DATETIME) {
self::update(['term-date' => DateTimeFormat::utcNow()], ['id' => $contact['id']]);
self::update(['term-date' => DateTimeFormat::utcNow()], ['`nurl` = ? AND `term-date` <= ? AND NOT `self`', Strings::normaliseLink($contact['url']), DBA::NULL_DATETIME]);
self::update(['term-date' => DateTimeFormat::utcNow()], ['uri-id' => $contact['uri-id'], 'self' => false]);
} else {
/* @todo
* We really should send a notification to the owner after 2-3 weeks
@ -1118,8 +1117,7 @@ class Contact
* delete, though if the owner tries to unarchive them we'll start
* the whole process over again.
*/
self::update(['archive' => true], ['id' => $contact['id']]);
self::update(['archive' => true], ['nurl' => Strings::normaliseLink($contact['url']), 'self' => false]);
self::update(['archive' => true], ['uri-id' => $contact['uri-id'], 'self' => false]);
}
}
}
@ -1144,28 +1142,25 @@ class Contact
}
}
$condition = ['`id` = ? AND (`term-date` > ? OR `archive`)', $contact['id'], DBA::NULL_DATETIME];
$exists = DBA::exists('contact', $condition);
// We don't need to update, we never marked this contact for archival
if (!$exists) {
$condition = ['`id` = ? AND (`term-date` > ? OR `archive`)', $contact['id'], DBA::NULL_DATETIME];
if (!DBA::exists('contact', $condition)) {
return;
}
Logger::info('Contact is marked as vital again', ['id' => $contact['id'], 'term-date' => $contact['term-date']]);
if (!isset($contact['url']) && !empty($contact['id'])) {
$fields = ['id', 'url', 'batch'];
if ((!isset($contact['url']) || !isset($contact['uri-id'])) && !empty($contact['id'])) {
$fields = ['id', 'uri-id', 'url', 'batch', 'term-date'];
$contact = DBA::selectFirst('contact', $fields, ['id' => $contact['id']]);
if (!DBA::isResult($contact)) {
return;
}
}
Logger::info('Contact is marked as vital again', ['id' => $contact['id'], 'term-date' => $contact['term-date'], 'url' => $contact['url']]);
// It's a miracle. Our dead contact has inexplicably come back to life.
$fields = ['failed' => false, 'term-date' => DBA::NULL_DATETIME, 'archive' => false];
self::update($fields, ['id' => $contact['id']]);
self::update($fields, ['nurl' => Strings::normaliseLink($contact['url']), 'self' => false]);
self::update($fields, ['uri-id' => $contact['uri-id'], 'self' => false]);
}
/**
@ -1225,7 +1220,7 @@ class Contact
if ($contact['uid'] && in_array($contact['rel'], [self::SHARING, self::FRIEND])) {
$unfollow_link = 'contact/unfollow?url=' . urlencode($contact['url']) . '&auto=1';
} elseif (!$contact['pending']) {
$follow_link = 'contact/follow?url=' . urlencode($contact['url']) . '&auto=1';
$follow_link = 'contact/follow?binurl=' . bin2hex($contact['url']) . '&auto=1';
}
}
@ -1372,7 +1367,7 @@ class Contact
$personal_contact = DBA::selectFirst('contact', $fields, ["`nurl` = ? AND `uid` != 0", Strings::normaliseLink($url)]);
}
if (DBA::isResult($personal_contact)) {
if (DBA::isResult($personal_contact) && !Probe::isProbable($personal_contact['network'])) {
Logger::info('Take contact data from personal contact', ['url' => $url, 'update' => $update, 'contact' => $personal_contact]);
$data = $personal_contact;
$data['photo'] = $personal_contact['avatar'];
@ -1590,11 +1585,15 @@ class Contact
*/
public static function getPostsFromId(int $cid, int $uid, bool $only_media = false, string $last_created = null): string
{
$contact = DBA::selectFirst('contact', ['contact-type', 'network'], ['id' => $cid]);
$contact = DBA::selectFirst('contact', ['contact-type', 'network', 'name', 'nick'], ['id' => $cid]);
if (!DBA::isResult($contact)) {
return '';
}
if (Contact\User::isIsBlocked($cid, $uid)) {
return DI::l10n()->t('%s has blocked you', $contact['name'] ?: $contact['nick']);
}
if (empty($contact["network"]) || in_array($contact["network"], Protocol::FEDERATED)) {
$condition = ["(`uid` = 0 OR (`uid` = ? AND NOT `global`))", $uid];
} else {
@ -1658,11 +1657,15 @@ class Contact
*/
public static function getThreadsFromId(int $cid, int $uid, int $update = 0, int $parent = 0, string $last_created = ''): string
{
$contact = DBA::selectFirst('contact', ['contact-type', 'network'], ['id' => $cid]);
$contact = DBA::selectFirst('contact', ['contact-type', 'network', 'name', 'nick'], ['id' => $cid]);
if (!DBA::isResult($contact)) {
return '';
}
if (Contact\User::isIsBlocked($cid, $uid)) {
return DI::l10n()->t('%s has blocked you', $contact['name'] ?: $contact['nick']);
}
if (empty($contact["network"]) || in_array($contact["network"], Protocol::FEDERATED)) {
$condition = ["(`uid` = 0 OR (`uid` = ? AND NOT `global`))", $uid];
} else {
@ -2298,7 +2301,7 @@ class Contact
return;
}
if (!Network::isValidHttpUrl($avatar)) {
if (!empty($avatar) && !Network::isValidHttpUrl($avatar)) {
Logger::warning('Invalid avatar', ['cid' => $cid, 'avatar' => $avatar]);
$avatar = '';
}

View file

@ -9,7 +9,7 @@ namespace Friendica\Model\Contact;
use Exception;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Worker;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\DI;
@ -140,13 +140,21 @@ class User
$contact = Contact::getById($cdata['public']);
if ($blocked) {
Protocol::block($contact, $uid);
Worker::add(Worker::PRIORITY_HIGH, 'Contact\Block', $cid, $uid);
} else {
Protocol::unblock($contact, $uid);
Worker::add(Worker::PRIORITY_HIGH, 'Contact\Unblock', $cid, $uid);
}
if ($cdata['user'] != 0) {
DBA::update('contact', ['blocked' => $blocked], ['id' => $cdata['user'], 'pending' => false]);
if ($blocked) {
$contact = Contact::getById($cdata['user']);
if (!empty($contact)) {
// Mastodon-expected behavior: relationship is severed on block
Contact::terminateFriendship($contact);
}
}
}
DBA::update('user-contact', ['blocked' => $blocked], ['cid' => $cdata['public'], 'uid' => $uid], true);

View file

@ -30,6 +30,7 @@ class Conversation
const PARCEL_ATOM03 = 15;
const PARCEL_OPML = 16;
const PARCEL_TWITTER = 67;
const PARCEL_CONNECTOR = 68;
const PARCEL_UNKNOWN = 255;
/**

View file

@ -753,6 +753,10 @@ class GServer
$serverdata = self::detectNetworkViaContacts($url, $serverdata);
}
if ($serverdata['platform'] == 'mastodon') {
$serverdata = self::detectMastodonForks($serverdata);
}
if (($serverdata['network'] == Protocol::PHANTOM) && in_array($serverdata['detection-method'], [self::DETECT_MANUAL, self::DETECT_BODY])) {
self::setFailureByUrl($url);
return false;
@ -1792,6 +1796,23 @@ class GServer
return $serverdata;
}
private static function detectMastodonForks(array $serverdata): array
{
if (strpos($serverdata['version'], 'glitch') !== false) {
$serverdata['platform'] = 'glitchsoc';
}
if (strpos($serverdata['version'], 'chuckya') !== false) {
$serverdata['platform'] = 'chuckya';
}
if (strpos($serverdata['version'], 'sakura') !== false) {
$serverdata['platform'] = 'sakura';
}
return $serverdata;
}
/**
* Checks if the given server does have a '/poco' endpoint.
* This is used for the 'PortableContact' functionality,

View file

@ -8,6 +8,7 @@
namespace Friendica\Model;
use Friendica\Contact\LocalRelationship\Entity\LocalRelationship;
use Friendica\Content\ContactSelector;
use Friendica\Content\Image;
use Friendica\Content\Post\Collection\PostMedias;
use Friendica\Content\Post\Entity\PostMedia;
@ -3375,7 +3376,7 @@ class Item
$item['body'] = preg_replace("#\s*\[attachment .*?].*?\[/attachment]\s*#ism", "\n", $item['body']);
}
$fields = ['uri-id', 'uri', 'body', 'title', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink', 'network', 'has-media', 'quote-uri-id', 'post-type'];
$fields = ['uri-id', 'uri', 'body', 'title', 'author-name', 'author-link', 'author-avatar', 'author-gsid', 'guid', 'created', 'plink', 'network', 'has-media', 'quote-uri-id', 'post-type'];
$shared_uri_id = 0;
$shared_links = [];
@ -3383,7 +3384,7 @@ class Item
$shared = DI::contentItem()->getSharedPost($item, $fields);
if (!empty($shared['post'])) {
$shared_item = $shared['post'];
$shared_item = $shared['post'];
$shared_item['body'] = Post\Media::removeFromEndOfBody($shared_item['body']);
$shared_item['body'] = Post\Media::replaceImage($shared_item['body']);
$quote_uri_id = $shared['post']['uri-id'];
@ -3470,6 +3471,10 @@ class Item
unset($hook_data);
}
if (!empty($shared_item['uri-id'])) {
$s = self::replacePlatformIcon($s, $shared_item, $uid);
}
$hook_data = [
'item' => $item,
'html' => $s,
@ -3533,6 +3538,39 @@ class Item
return $hook_data['html'];
}
/**
* Replace the platform icon with the icon in the style selected by the user
*
* @param string $html
* @param array $item
* @param integer $uid
* @return string
*/
private static function replacePlatformIcon(string $html, array $item, int $uid): string
{
$dom = new \DOMDocument();
if (!@$dom->loadHTML($html)) {
return $html;
}
$svg = ContactSelector::networkToSVG($item['network'], $item['author-gsid'], '', $uid);
if (empty($svg)) {
return $html;
}
$xpath = new \DOMXPath($dom);
/** @var DOMElement $element */
foreach ($xpath->query("//img[@class='network-svg']") as $element) {
$src = $element->getAttributeNode('src')->nodeValue;
if ($src == $svg) {
continue;
}
$element_html = $element->ownerDocument->saveHTML($element);
$html = str_replace($element_html, str_replace($src, $svg, $element_html), $html);
}
return $html;
}
/**
* Modify links to pictures to links for the "Fancybox" gallery
*
@ -3733,6 +3771,9 @@ class Item
continue;
}
if (empty($PostMedia->description) && DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'accessibility', 'hide_empty_descriptions')) {
continue;
}
$images[] = $PostMedia->withUrl(new Uri($src_url))->withPreview(new Uri($preview_url), $preview_size);
}
}
@ -4152,6 +4193,10 @@ class Item
try {
$curlResult = DI::httpClient()->head($uri, [HttpClientOptions::ACCEPT_CONTENT => HttpClientAccept::JSON_AS, HttpClientOptions::REQUEST => HttpClientRequest::ACTIVITYPUB]);
if (!HTTPSignature::isValidContentType($curlResult->getContentType(), $uri) && (current(explode(';', $curlResult->getContentType())) == 'application/json')) {
// Issue 14126: Workaround for Mastodon servers that return "application/json" on a "head" request.
$curlResult = HTTPSignature::fetchRaw($uri, $uid);
}
if (HTTPSignature::isValidContentType($curlResult->getContentType(), $uri)) {
$fetched_uri = ActivityPub\Processor::fetchMissingActivity($uri, [], '', $completion, $uid);
}

View file

@ -465,7 +465,7 @@ class Post
AND ((NOT `contact-readonly` AND NOT `contact-pending` AND (`contact-rel` IN (?, ?)))
OR `self` OR `contact-uid` = ?)
AND NOT EXISTS(SELECT `uri-id` FROM `post-user` WHERE `uid` = ? AND `uri-id` = " . DBA::quoteIdentifier($view) . ".`uri-id` AND `hidden`)
AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` IN (`author-id`, `owner-id`) AND (`blocked` OR `ignored`))
AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` IN (`author-id`, `owner-id`) AND (`blocked` OR `ignored` OR `is-blocked`))
AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = ? AND `gsid` IN (`author-gsid`, `owner-gsid`, `causer-gsid`) AND `ignored`)",
0, Contact::SHARING, Contact::FRIEND, 0, $uid, $uid, $uid]);

View file

@ -325,7 +325,7 @@ class Profile
if ($visitor_is_following) {
$unfollow_link = $visitor_base_path . '/contact/unfollow?url=' . urlencode($profile_url) . '&auto=1';
} else {
$follow_link = $visitor_base_path . '/contact/follow?url=' . urlencode($profile_url) . '&auto=1';
$follow_link = $visitor_base_path . '/contact/follow?binurl=' . bin2hex($profile_url) . '&auto=1';
}
}

View file

@ -63,7 +63,7 @@ class Index extends BaseAdmin
'$function' => 'addons',
'$addons' => $addons,
'$pcount' => count($addons),
'$noplugshint' => DI::l10n()->t('There are currently no addons available on your node. You can find the official addon repository at %1$s and might find other interesting addons in the open addon registry at %2$s', 'https://github.com/friendica/friendica-addons', 'http://addons.friendi.ca'),
'$noplugshint' => DI::l10n()->t('There are currently no addons available on your node. You can find the official addon repository at %1$s.', 'https://git.friendi.ca/friendica/friendica-addons'),
'$form_security_token' => self::getFormSecurityToken('admin_addons'),
]);
}

View file

@ -8,6 +8,7 @@
namespace Friendica\Module\Admin;
use Friendica\App;
use Friendica\Content\ContactSelector;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Database\DBA;
@ -36,6 +37,7 @@ class Federation extends BaseAdmin
'foundkey' => ['name' => 'Foundkey', 'color' => '#609926'], // Some random color from the repository
'funkwhale' => ['name' => 'Funkwhale', 'color' => '#4082B4'], // From the homepage
'gancio' => ['name' => 'Gancio', 'color' => '#7253ed'], // Fontcolor from the page
'glitchsoc' => ['name' => 'Mastodon Glitch Edition', 'color' => '#82dcb9'], // Color from their site
'gnusocial' => ['name' => 'GNU Social/Statusnet', 'color' => '#a22430'], // dark red from the logo
'gotosocial' => ['name' => 'GoToSocial', 'color' => '#df8958'], // Some color from their mascot
'hometown' => ['name' => 'Hometown', 'color' => '#1f70c1'], // Color from the Patreon page
@ -128,6 +130,10 @@ class Federation extends BaseAdmin
$platform = 'nomad';
} elseif(stristr($platform, 'pleroma')) {
$platform = 'pleroma';
} elseif(stristr($platform, 'glitchsoc')) {
$platform = 'glitchsoc';
} elseif(stristr($platform, 'iceshrimp.net')) {
$platform = 'iceshrimp';
} elseif(stristr($platform, 'statusnet')) {
$platform = 'gnusocial';
} elseif(stristr($platform, 'nextcloud')) {
@ -176,6 +182,7 @@ class Federation extends BaseAdmin
}
$gserver['platform'] = $systems[$platform]['name'];
$gserver['svg'] = ContactSelector::networkToSVG($gserver['network'], null, $platform, DI::userSession()->getLocalUserId());
$gserver['totallbl'] = DI::l10n()->tt('%2$s total system' , '%2$s total systems' , $gserver['total'], number_format($gserver['total']));
$gserver['monthlbl'] = DI::l10n()->tt('%2$s active user last month' , '%2$s active users last month' , $gserver['month'] ?? 0, number_format($gserver['month'] ?? 0));
$gserver['halfyearlbl'] = DI::l10n()->tt('%2$s active user last six months' , '%2$s active users last six months' , $gserver['halfyear'] ?? 0, number_format($gserver['halfyear'] ?? 0));

View file

@ -64,23 +64,19 @@ class Summary extends BaseAdmin
// Check if github.com/friendica/stable/VERSION is higher then
// the local version of Friendica. Check is opt-in, source may be stable or develop branch
if (DI::config()->get('system', 'check_new_version_url', 'none') != 'none') {
$gitversion = DI::keyValue()->get('git_friendica_version') ?? '';
if (version_compare(App::VERSION, $gitversion) < 0) {
$warningtext[] = DI::l10n()->t('There is a new version of Friendica available for download. Your current version is %1$s, upstream version is %2$s', App::VERSION, $gitversion);
}
if (Update::isAvailable()) {
$warningtext[] = DI::l10n()->t('There is a new version of Friendica available for download. Your current version is %1$s, upstream version is %2$s', App::VERSION, Update::getAvailableVersion());
}
if (DI::config()->get('system', 'dbupdate', DBStructure::UPDATE_NOT_CHECKED) == DBStructure::UPDATE_NOT_CHECKED) {
if (DBStructure::getUpdateStatus() == DBStructure::UPDATE_NOT_CHECKED) {
DBStructure::performUpdate();
}
if (DI::config()->get('system', 'dbupdate') == DBStructure::UPDATE_FAILED) {
if (DBStructure::getUpdateStatus() == DBStructure::UPDATE_FAILED) {
$warningtext[] = DI::l10n()->t('The database update failed. Please run "php bin/console.php dbstructure update" from the command line and have a look at the errors that might appear.');
}
if (DI::config()->get('system', 'update') == Update::FAILED) {
if (Update::getStatus() == Update::FAILED) {
$warningtext[] = DI::l10n()->t('The last update failed. Please run "php bin/console.php dbstructure update" from the command line and have a look at the errors that might appear. (Some of the errors are possibly inside the logfile.)');
}
@ -160,9 +156,6 @@ class Summary extends BaseAdmin
// We can do better, but this is a quick queue status
$queues = ['label' => DI::l10n()->t('Message queues'), 'deferred' => $deferred, 'workerq' => $workerqueue];
$variables = DBA::toArray(DBA::p('SHOW variables LIKE "max_allowed_packet"'));
$max_allowed_packet = $variables ? $variables[0]['Value'] : 0;
$server_settings = [
'label' => DI::l10n()->t('Server Settings'),
'php' => [
@ -173,7 +166,7 @@ class Summary extends BaseAdmin
'memory_limit' => ini_get('memory_limit')
],
'mysql' => [
'max_allowed_packet' => $max_allowed_packet
'max_allowed_packet' => DBA::getVariable('max_allowed_packet'),
]
];

View file

@ -7,10 +7,8 @@
namespace Friendica\Module\Api\Mastodon\Accounts;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\User;
use Friendica\Module\BaseApi;
/**
@ -29,15 +27,6 @@ class Block extends BaseApi
Contact\User::setBlocked($this->parameters['id'], $uid, true);
$ucid = Contact::getUserContactId($this->parameters['id'], $uid);
if ($ucid) {
$contact = Contact::getById($ucid);
if (!empty($contact)) {
// Mastodon-expected behavior: relationship is severed on block
Contact::terminateFriendship($contact);
}
}
$this->jsonExit(DI::mstdnRelationship()->createFromContactId($this->parameters['id'], $uid)->toArray());
}
}

View file

@ -59,28 +59,34 @@ class Apps extends BaseApi
$this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity($this->t('Missing parameters')));
}
$client_id = bin2hex(random_bytes(32));
$client_secret = bin2hex(random_bytes(32));
$fields = ['client_id' => $client_id, 'client_secret' => $client_secret, 'name' => $request['client_name'], 'redirect_uri' => $request['redirect_uris']];
$fields = ['name' => $request['client_name'], 'redirect_uri' => $request['redirect_uris']];
if (!empty($request['scopes'])) {
$fields['scopes'] = $request['scopes'];
}
$fields['read'] = (stripos($request['scopes'], self::SCOPE_READ) !== false);
$fields['write'] = (stripos($request['scopes'], self::SCOPE_WRITE) !== false);
$fields['follow'] = (stripos($request['scopes'], self::SCOPE_FOLLOW) !== false);
$fields['push'] = (stripos($request['scopes'], self::SCOPE_PUSH) !== false);
if (!empty($request['website'])) {
$fields['website'] = $request['website'];
}
$application = DBA::selectFirst('application', ['id'], $fields);
if (!empty($application['id'])) {
$this->logger->debug('Found existing application', ['request' => $request, 'id' => $application['id']]);
$this->jsonExit(DI::mstdnApplication()->createFromApplicationId($application['id'])->toArray());
}
$fields['read'] = (stripos($request['scopes'], self::SCOPE_READ) !== false);
$fields['write'] = (stripos($request['scopes'], self::SCOPE_WRITE) !== false);
$fields['follow'] = (stripos($request['scopes'], self::SCOPE_FOLLOW) !== false);
$fields['push'] = (stripos($request['scopes'], self::SCOPE_PUSH) !== false);
$fields['client_id'] = bin2hex(random_bytes(32));
$fields['client_secret'] = bin2hex(random_bytes(32));
if (!DBA::insert('application', $fields)) {
$this->logAndJsonError(500, $this->errorFactory->InternalError());
}
$this->logger->debug('Create new application', ['request' => $request, 'id' => DBA::lastInsertId()]);
$this->jsonExit(DI::mstdnApplication()->createFromApplicationId(DBA::lastInsertId())->toArray());
}
}

View file

@ -42,38 +42,37 @@ class Media extends BaseApi
$type = Post\Media::getType($request['file']['type']);
if (in_array($type, [Post\Media::IMAGE, Post\Media::UNKNOWN])) {
if (in_array($type, [Post\Media::IMAGE, Post\Media::UNKNOWN, Post\Media::APPLICATION])) {
$media = Photo::upload($uid, $request['file'], '', null, null, '', '', $request['description']);
if (empty($media)) {
$this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity());
if (!empty($media)) {
Logger::info('Uploaded photo', ['media' => $media]);
$this->jsonExit(DI::mstdnAttachment()->createFromPhoto($media['id']));
} elseif ($type == Post\Media::IMAGE) {
$this->jsonExit(DI::mstdnAttachment()->createFromPhoto($media['id']));
}
Logger::info('Uploaded photo', ['media' => $media]);
$this->jsonExit(DI::mstdnAttachment()->createFromPhoto($media['id']));
} else {
$tempFileName = $request['file']['tmp_name'];
$fileName = basename($request['file']['name']);
$fileSize = intval($request['file']['size']);
$maxFileSize = Strings::getBytesFromShorthand(DI::config()->get('system', 'maxfilesize'));
if ($fileSize <= 0) {
Logger::notice('Filesize is invalid', ['size' => $fileSize, 'request' => $request]);
@unlink($tempFileName);
$this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity());
}
if ($maxFileSize && $fileSize > $maxFileSize) {
Logger::notice('Filesize is too large', ['size' => $fileSize, 'max' => $maxFileSize, 'request' => $request]);
@unlink($tempFileName);
$this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity());
}
$id = Attach::storeFile($tempFileName, self::getCurrentUserID(), $fileName, $request['file']['type'], '<' . Contact::getPublicIdByUserId(self::getCurrentUserID()) . '>');
@unlink($tempFileName);
Logger::info('Uploaded media', ['id' => $id]);
$this->jsonExit(DI::mstdnAttachment()->createFromAttach($id));
}
$tempFileName = $request['file']['tmp_name'];
$fileName = basename($request['file']['name']);
$fileSize = intval($request['file']['size']);
$maxFileSize = Strings::getBytesFromShorthand(DI::config()->get('system', 'maxfilesize'));
if ($fileSize <= 0) {
Logger::notice('Filesize is invalid', ['size' => $fileSize, 'request' => $request]);
@unlink($tempFileName);
$this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity());
}
if ($maxFileSize && $fileSize > $maxFileSize) {
Logger::notice('Filesize is too large', ['size' => $fileSize, 'max' => $maxFileSize, 'request' => $request]);
@unlink($tempFileName);
$this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity());
}
$id = Attach::storeFile($tempFileName, self::getCurrentUserID(), $fileName, $request['file']['type'], '<' . Contact::getPublicIdByUserId(self::getCurrentUserID()) . '>');
@unlink($tempFileName);
Logger::info('Uploaded media', ['id' => $id]);
$this->jsonExit(DI::mstdnAttachment()->createFromAttach($id));
}
public function put(array $request = [])

View file

@ -66,7 +66,7 @@ class Context extends BaseApi
if (!empty($uid) && !$request['show_all']) {
$condition = DBA::mergeConditions(
$condition,
["NOT `author-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND (`blocked` OR `ignored`))", $uid]
["NOT `author-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND (`blocked` OR `ignored` OR `is-blocked`))", $uid]
);
}

View file

@ -55,7 +55,7 @@ class Direct extends BaseApi
if (!empty($uid)) {
$condition = DBA::mergeConditions(
$condition,
["NOT `parent-author-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND (`blocked` OR `ignored`) AND `cid` = `parent-author-id`)", $uid]
["NOT `parent-author-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND (`blocked` OR `ignored` OR `is-blocked`) AND `cid` = `parent-author-id`)", $uid]
);
}

View file

@ -66,6 +66,8 @@ class Home extends BaseApi
$condition = DBA::mergeConditions($condition, ['gravity' => Item::GRAVITY_PARENT]);
}
$condition = DBA::mergeConditions($condition, ["NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` IN (`parent-owner-id`, `parent-author-id`) AND (`blocked` OR `ignored` OR `is-blocked` OR `channel-only`))", $uid]);
$items = Post::selectTimelineForUser($uid, ['uri-id'], $condition, $params);
$display_quotes = self::appSupportsQuotes();

View file

@ -93,7 +93,7 @@ class Tag extends BaseApi
if (!empty($uid)) {
$condition = DBA::mergeConditions(
$condition,
["NOT `author-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND (`blocked` OR `ignored`) AND `cid` = `author-id`)", $uid]
["NOT `author-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND (`blocked` OR `ignored` OR `is-blocked`) AND `cid` = `author-id`)", $uid]
);
}

View file

@ -617,7 +617,7 @@ class Contact extends BaseModule
'account_type' => Model\Contact::getAccountType($contact['contact-type']),
'sparkle' => $sparkle,
'itemurl' => ($contact['addr'] ?? '') ?: $contact['url'],
'network' => ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']),
'network' => ContactSelector::networkToName($contact['network'], $contact['protocol'], $contact['gsid']),
];
}
}

View file

@ -56,11 +56,15 @@ class Follow extends BaseModule
throw new ForbiddenException($this->t('Access denied.'));
}
if (isset($request['cancel']) || empty($request['url'])) {
$this->baseUrl->redirect('contact');
if (!empty($request['follow-url'])) {
$this->baseUrl->redirect('contact/follow?binurl=' . bin2hex($request['follow-url']));
}
$url = Probe::cleanURI($request['url']);
$url = $this->getUrl($request);
if (isset($request['cancel']) || empty($url)) {
$this->baseUrl->redirect('contact');
}
$this->process($url);
}
@ -77,7 +81,7 @@ class Follow extends BaseModule
$uid = $this->session->getLocalUserId();
// uri is used by the /authorize_interaction Mastodon route
$url = Probe::cleanURI(trim($request['uri'] ?? $request['url'] ?? ''));
$url = $this->getUrl($request);
// Issue 6874: Allow remote following from Peertube
if (strpos($url, 'acct:') === 0) {
@ -182,7 +186,7 @@ class Follow extends BaseModule
protected function process(string $url)
{
$returnPath = 'contact/follow?url=' . urlencode($url);
$returnPath = 'contact/follow?binurl=' . bin2hex($url);
$result = Contact::createFromProbeForUser($this->session->getLocalUserId(), $url);
@ -227,4 +231,14 @@ class Follow extends BaseModule
return;
}
}
private function getUrl(array $request): string
{
if (!empty($request['binurl']) && Strings::isHex($request['binurl'])) {
$url = hex2bin($request['binurl']);
} else {
$url = $request['url'] ?? '';
}
return Probe::cleanURI($url);
}
}

View file

@ -297,7 +297,7 @@ class Profile extends BaseModule
$poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::FEED, Protocol::MAIL]);
$nettype = $this->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']));
$nettype = $this->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['protocol'], $contact['gsid']));
// tabs
$tab_str = Module\Contact::getTabsHTML($contact, Module\Contact::TAB_PROFILE);
@ -356,6 +356,11 @@ class Profile extends BaseModule
$contact_actions = $this->getContactActions($contact, $localRelationship);
if (Contact\User::isIsBlocked($contact['id'], $this->session->getLocalUserId())) {
$relation_text = $this->t('%s has blocked you', $contact['name'] ?: $contact['nick']);
unset($contact_actions['follow']);
}
if ($localRelationship->rel !== Contact::NOTHING) {
$lbl_info1 = $this->t('Contact Information / Notes');
$contact_settings_label = $this->t('Contact Settings');
@ -477,7 +482,7 @@ class Profile extends BaseModule
} else {
$contact_actions['follow'] = [
'label' => $this->t('Follow'),
'url' => 'contact/follow?url=' . urlencode($contact['url']) . '&auto=1',
'url' => 'contact/follow?binurl=' . bin2hex($contact['url']) . '&auto=1',
'title' => '',
'sel' => '',
'id' => 'follow',

View file

@ -87,7 +87,7 @@ class Install extends BaseModule
// so we may not have a css at all. Here we set a static css file for the install procedure pages
Renderer::$theme['stylesheet'] = $this->baseUrl . '/view/install/style.css';
$this->currentWizardStep = ($_POST['pass'] ?? '') ?: self::SYSTEM_CHECK;
$this->currentWizardStep = ($_REQUEST['pass'] ?? '') ?: self::SYSTEM_CHECK;
}
protected function post(array $request = [])
@ -164,6 +164,7 @@ class Install extends BaseModule
break;
}
DI::baseUrl()->redirect('install?pass=' . $this->currentWizardStep);
}
protected function content(array $request = []): string

View file

@ -96,7 +96,7 @@ class Add extends BaseModeration
array_walk($gservers, function (array &$gserver) {
$gserver['domain'] = (new Uri($gserver['url']))->getHost();
$gserver['network_icon'] = ContactSelector::networkToIcon($gserver['network']);
$gserver['network_svg'] = ContactSelector::networkToSVG($gserver['network']);
$gserver['network_name'] = ContactSelector::networkToName($gserver['network']);
});

View file

@ -76,7 +76,7 @@ class Source extends BaseModeration
'urllbl' => $this->t('URL'),
'mentionlbl' => $this->t('Mention'),
'implicitlbl' => $this->t('Implicit Mention'),
'error' => $this->t('Error'),
'error' => $this->tt('Error','Errors', 1),
'notfound' => $this->t('Item not found'),
'nosource' => $this->t('No source recorded'),
'noconfig' => !$this->config->get('debug', 'store_source') ? $this->t('Please make sure the <code>debug.store_source</code> config key is set in <code>config/local.config.php</code> for future items to have sources.') : '',

View file

@ -156,7 +156,8 @@ class Introductions extends BaseNotifications
$header .= ' <' . $Introduction->getAddr() . '>';
}
$header .= ' (' . ContactSelector::networkToName($Introduction->getNetwork(), $Introduction->getUrl()) . ')';
$gsid = ContactSelector::getServerIdForProfile($Introduction->getUrl());
$header .= ' (' . ContactSelector::networkToName($Introduction->getNetwork(), '', $gsid) . ')';
if ($Introduction->getNetwork() != Protocol::DIASPORA) {
$discard = $this->t('Discard');
@ -191,7 +192,7 @@ class Introductions extends BaseNotifications
'$addr' => $Introduction->getAddr(),
'$lbl_knowyou' => $lbl_knowyou,
'$lbl_network' => $this->t('Network:'),
'$network' => ContactSelector::networkToName($Introduction->getNetwork(), $Introduction->getUrl()),
'$network' => ContactSelector::networkToName($Introduction->getNetwork(), '', $gsid),
'$knowyou' => $knowyou,
'$approve' => $this->t('Approve'),
'$note' => $Introduction->getNote(),

View file

@ -36,17 +36,18 @@ class Authorize extends BaseApi
], $request);
if ($request['response_type'] != 'code') {
Logger::warning('Unsupported or missing response type', ['request' => $_REQUEST]);
Logger::warning('Unsupported or missing response type', ['request' => $request]);
$this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity($this->t('Unsupported or missing response type')));
}
if (empty($request['client_id']) || empty($request['redirect_uri'])) {
Logger::warning('Incomplete request data', ['request' => $_REQUEST]);
Logger::warning('Incomplete request data', ['request' => $request]);
$this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity($this->t('Incomplete request data')));
}
$application = OAuth::getApplication($request['client_id'], $request['client_secret'], $request['redirect_uri']);
if (empty($application)) {
Logger::warning('An application could not be fetched.', ['request' => $request]);
$this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity());
}

View file

@ -9,10 +9,14 @@ namespace Friendica\Module;
use DOMDocument;
use DOMElement;
use Friendica\App;
use Friendica\BaseModule;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Friendica\Util\BasePath;
use Friendica\Util\Profiler;
use Friendica\Util\XML;
use Psr\Log\LoggerInterface;
/**
* Prints the opensearch description document
@ -20,22 +24,37 @@ use Friendica\Util\XML;
*/
class OpenSearch extends BaseModule
{
/** @var IManageConfigValues */
private $config;
/** @var App\baseUrl */
protected $baseUrl;
/** @var string */
private $basePath;
public function __construct(BasePath $basePath, IManageConfigValues $config, L10n $l10n, App\baseUrl $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->config = $config;
$this->basePath = $basePath->getPath();
$this->baseUrl = $baseUrl;
}
/**
* @throws \Exception
*/
protected function rawContent(array $request = [])
{
$hostname = DI::baseUrl()->getHost();
$baseUrl = (string)DI::baseUrl();
/** @var DOMDocument $xml */
XML::fromArray([
'OpenSearchDescription' => [
'@attributes' => [
'xmlns' => 'http://a9.com/-/spec/opensearch/1.1',
'xmlns' => 'http://a9.com/-/spec/opensearch/1.1/',
],
'ShortName' => "Friendica $hostname",
'Description' => "Search in Friendica $hostname",
'ShortName' => $this->baseUrl->getHost(),
'Description' => $this->l10n->t('Search in Friendica %s', $this->baseUrl->getHost()),
'Contact' => 'https://github.com/friendica/friendica/issues',
'InputEncoding' => 'UTF-8',
'OutputEncoding' => 'UTF-8',
@ -46,29 +65,41 @@ class OpenSearch extends BaseModule
/** @var DOMElement $parent */
$parent = $xml->getElementsByTagName('OpenSearchDescription')[0];
XML::addElement($xml, $parent, 'Image',
"$baseUrl/images/friendica-16.png", [
if (file_exists($this->basePath . '/favicon.ico')) {
$shortcut_icon = '/favicon.ico';
} else {
$shortcut_icon = $this->config->get('system', 'shortcut_icon');
}
if (!empty($shortcut_icon)) {
$imagedata = getimagesize($this->baseUrl . $shortcut_icon);
}
if (!empty($imagedata)) {
XML::addElement($xml, $parent, 'Image', $this->baseUrl . $shortcut_icon, [
'width' => $imagedata[0],
'height' => $imagedata[1],
'type' => $imagedata['mime'],
]);
} else {
XML::addElement($xml, $parent, 'Image',
$this->baseUrl . '/images/friendica-16.png', [
'height' => 16,
'width' => 16,
'type' => 'image/png',
]);
XML::addElement($xml, $parent, 'Image',
"$baseUrl/images/friendica-64.png", [
'height' => 64,
'width' => 64,
'type' => 'image/png',
]);
}
XML::addElement($xml, $parent, 'Url', '', [
'type' => 'text/html',
'template' => "$baseUrl/search?search={searchTerms}",
'method' => 'get',
'template' => $this->baseUrl . '/search?q={searchTerms}',
]);
XML::addElement($xml, $parent, 'Url', '', [
'type' => 'application/opensearchdescription+xml',
'rel' => 'self',
'template' => "$baseUrl/opensearch",
'template' => $this->baseUrl . '/opensearch',
]);
$this->httpExit($xml->saveXML(), Response::TYPE_XML, 'application/opensearchdescription+xml');

View file

@ -225,7 +225,7 @@ class Conversations extends BaseProfile
$items = array_merge($items, $pinned);
}
$o .= $this->conversation->render($items, Conversation::MODE_PROFILE, false, false, 'pinned_received', $profile['uid']);
$o .= $this->conversation->render($items, Conversation::MODE_PROFILE, false, false, 'pinned_received', $this->session->getLocalUserId());
$o .= $pager->renderMinimal(count($items));

View file

@ -8,6 +8,7 @@
namespace Friendica\Module\Settings;
use Friendica\App;
use Friendica\Content\ContactSelector;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Conversation\Factory\Channel as ChannelFactory;
@ -80,23 +81,26 @@ class Display extends BaseSettings
$user = User::getById($uid);
$theme = trim($request['theme']);
$mobile_theme = trim($request['mobile_theme'] ?? '');
$enable_smile = (bool)$request['enable_smile'];
$enable = (array)$request['enable'];
$bookmark = (array)$request['bookmark'];
$channel_languages = (array)$request['channel_languages'];
$first_day_of_week = (bool)$request['first_day_of_week'];
$calendar_default_view = trim($request['calendar_default_view']);
$infinite_scroll = (bool)$request['infinite_scroll'];
$enable_smart_threading = (bool)$request['enable_smart_threading'];
$enable_dislike = (bool)$request['enable_dislike'];
$display_resharer = (bool)$request['display_resharer'];
$stay_local = (bool)$request['stay_local'];
$show_page_drop = (bool)$request['show_page_drop'];
$display_eventlist = (bool)$request['display_eventlist'];
$preview_mode = (int)$request['preview_mode'];
$browser_update = (int)$request['browser_update'];
$theme = trim($request['theme']);
$mobile_theme = trim($request['mobile_theme'] ?? '');
$enable_smile = (bool)$request['enable_smile'];
$enable = (array)$request['enable'];
$bookmark = (array)$request['bookmark'];
$channel_languages = (array)$request['channel_languages'];
$first_day_of_week = (int)$request['first_day_of_week'];
$calendar_default_view = trim($request['calendar_default_view']);
$infinite_scroll = (bool)$request['infinite_scroll'];
$enable_smart_threading = (bool)$request['enable_smart_threading'];
$enable_dislike = (bool)$request['enable_dislike'];
$display_resharer = (bool)$request['display_resharer'];
$stay_local = (bool)$request['stay_local'];
$hide_empty_descriptions = (bool)$request['hide_empty_descriptions'];
$hide_custom_emojis = (bool)$request['hide_custom_emojis'];
$platform_icon_style = (int)$request['platform_icon_style'];
$show_page_drop = (bool)$request['show_page_drop'];
$display_eventlist = (bool)$request['display_eventlist'];
$preview_mode = (int)$request['preview_mode'];
$browser_update = (int)$request['browser_update'];
if ($browser_update != -1) {
$browser_update = $browser_update * 1000;
if ($browser_update < 10000) {
@ -135,25 +139,29 @@ class Display extends BaseSettings
$this->pConfig->set($uid, 'system', 'mobile_theme', $mobile_theme);
}
$this->pConfig->set($uid, 'system', 'itemspage_network' , $itemspage_network);
$this->pConfig->set($uid, 'system', 'itemspage_network', $itemspage_network);
$this->pConfig->set($uid, 'system', 'itemspage_mobile_network', $itemspage_mobile_network);
$this->pConfig->set($uid, 'system', 'update_interval' , $browser_update);
$this->pConfig->set($uid, 'system', 'no_smilies' , !$enable_smile);
$this->pConfig->set($uid, 'system', 'infinite_scroll' , $infinite_scroll);
$this->pConfig->set($uid, 'system', 'no_smart_threading' , !$enable_smart_threading);
$this->pConfig->set($uid, 'system', 'hide_dislike' , !$enable_dislike);
$this->pConfig->set($uid, 'system', 'display_resharer' , $display_resharer);
$this->pConfig->set($uid, 'system', 'stay_local' , $stay_local);
$this->pConfig->set($uid, 'system', 'show_page_drop' , $show_page_drop);
$this->pConfig->set($uid, 'system', 'display_eventlist' , $display_eventlist);
$this->pConfig->set($uid, 'system', 'preview_mode' , $preview_mode);
$this->pConfig->set($uid, 'system', 'update_interval', $browser_update);
$this->pConfig->set($uid, 'system', 'no_smilies', !$enable_smile);
$this->pConfig->set($uid, 'system', 'infinite_scroll', $infinite_scroll);
$this->pConfig->set($uid, 'system', 'no_smart_threading', !$enable_smart_threading);
$this->pConfig->set($uid, 'system', 'hide_dislike', !$enable_dislike);
$this->pConfig->set($uid, 'system', 'display_resharer', $display_resharer);
$this->pConfig->set($uid, 'system', 'stay_local', $stay_local);
$this->pConfig->set($uid, 'system', 'show_page_drop', $show_page_drop);
$this->pConfig->set($uid, 'system', 'display_eventlist', $display_eventlist);
$this->pConfig->set($uid, 'system', 'preview_mode', $preview_mode);
$this->pConfig->set($uid, 'system', 'network_timelines' , $network_timelines);
$this->pConfig->set($uid, 'system', 'enabled_timelines' , $enabled_timelines);
$this->pConfig->set($uid, 'channel', 'languages' , $channel_languages);
$this->pConfig->set($uid, 'system', 'network_timelines', $network_timelines);
$this->pConfig->set($uid, 'system', 'enabled_timelines', $enabled_timelines);
$this->pConfig->set($uid, 'channel', 'languages', $channel_languages);
$this->pConfig->set($uid, 'calendar', 'first_day_of_week' , $first_day_of_week);
$this->pConfig->set($uid, 'calendar', 'default_view' , $calendar_default_view);
$this->pConfig->set($uid, 'accessibility', 'hide_empty_descriptions', $hide_empty_descriptions);
$this->pConfig->set($uid, 'accessibility', 'hide_custom_emojis', $hide_custom_emojis);
$this->pConfig->set($uid, 'accessibility', 'platform_icon_style', $platform_icon_style);
$this->pConfig->set($uid, 'calendar', 'first_day_of_week', $first_day_of_week);
$this->pConfig->set($uid, 'calendar', 'default_view', $calendar_default_view);
if (in_array($theme, Theme::getAllowedList())) {
if ($theme == $user['theme']) {
@ -241,6 +249,17 @@ class Display extends BaseSettings
$show_page_drop = $this->pConfig->get($uid, 'system', 'show_page_drop', true);
$display_eventlist = $this->pConfig->get($uid, 'system', 'display_eventlist', true);
$hide_empty_descriptions = $this->pConfig->get($uid, 'accessibility', 'hide_empty_descriptions', false);
$hide_custom_emojis = $this->pConfig->get($uid, 'accessibility', 'hide_custom_emojis', false);
$platform_icon_style = $this->pConfig->get($uid, 'accessibility', 'platform_icon_style', ContactSelector::SVG_COLOR_BLACK);
$platform_icon_styles = [
ContactSelector::SVG_DISABLED => $this->t('Disabled'),
ContactSelector::SVG_COLOR_BLACK => $this->t('Color/Black'),
ContactSelector::SVG_BLACK => $this->t('Black'),
ContactSelector::SVG_COLOR_WHITE => $this->t('Color/White'),
ContactSelector::SVG_WHITE => $this->t('White'),
];
$preview_mode = $this->pConfig->get($uid, 'system', 'preview_mode', BBCode::PREVIEW_LARGE);
$preview_modes = [
BBCode::PREVIEW_NONE => $this->t('No preview'),
@ -308,18 +327,21 @@ class Display extends BaseSettings
'$mobile_theme' => ['mobile_theme', $this->t('Mobile Theme:'), $mobile_theme_selected, '', $mobile_themes, false],
'$theme_config' => $theme_config,
'$itemspage_network' => ['itemspage_network' , $this->t('Number of items to display per page:'), $itemspage_network, $this->t('Maximum of 100 items')],
'$itemspage_network' => ['itemspage_network', $this->t('Number of items to display per page:'), $itemspage_network, $this->t('Maximum of 100 items')],
'$itemspage_mobile_network' => ['itemspage_mobile_network', $this->t('Number of items to display per page when viewed from mobile device:'), $itemspage_mobile_network, $this->t('Maximum of 100 items')],
'$ajaxint' => ['browser_update' , $this->t('Update browser every xx seconds'), $browser_update, $this->t('Minimum of 10 seconds. Enter -1 to disable it.')],
'$enable_smile' => ['enable_smile' , $this->t('Display emoticons'), $enable_smile, $this->t('When enabled, emoticons are replaced with matching symbols.')],
'$infinite_scroll' => ['infinite_scroll' , $this->t('Infinite scroll'), $infinite_scroll, $this->t('Automatic fetch new items when reaching the page end.')],
'$enable_smart_threading' => ['enable_smart_threading' , $this->t('Enable Smart Threading'), $enable_smart_threading, $this->t('Enable the automatic suppression of extraneous thread indentation.')],
'$enable_dislike' => ['enable_dislike' , $this->t('Display the Dislike feature'), $enable_dislike, $this->t('Display the Dislike button and dislike reactions on posts and comments.')],
'$display_resharer' => ['display_resharer' , $this->t('Display the resharer'), $display_resharer, $this->t('Display the first resharer as icon and text on a reshared item.')],
'$stay_local' => ['stay_local' , $this->t('Stay local'), $stay_local, $this->t("Don't go to a remote system when following a contact link.")],
'$show_page_drop' => ['show_page_drop' , $this->t('Show the post deletion checkbox'), $show_page_drop, $this->t("Display the checkbox for the post deletion on the network page.")],
'$display_eventlist' => ['display_eventlist' , $this->t('DIsplay the event list'), $display_eventlist, $this->t("Display the birthday reminder and event list on the network page.")],
'$preview_mode' => ['preview_mode' , $this->t('Link preview mode'), $preview_mode, $this->t('Appearance of the link preview that is added to each post with a link.'), $preview_modes, false],
'$ajaxint' => ['browser_update', $this->t('Update browser every xx seconds'), $browser_update, $this->t('Minimum of 10 seconds. Enter -1 to disable it.')],
'$enable_smile' => ['enable_smile', $this->t('Display emoticons'), $enable_smile, $this->t('When enabled, emoticons are replaced with matching symbols.')],
'$infinite_scroll' => ['infinite_scroll', $this->t('Infinite scroll'), $infinite_scroll, $this->t('Automatic fetch new items when reaching the page end.')],
'$enable_smart_threading' => ['enable_smart_threading', $this->t('Enable Smart Threading'), $enable_smart_threading, $this->t('Enable the automatic suppression of extraneous thread indentation.')],
'$enable_dislike' => ['enable_dislike', $this->t('Display the Dislike feature'), $enable_dislike, $this->t('Display the Dislike button and dislike reactions on posts and comments.')],
'$display_resharer' => ['display_resharer', $this->t('Display the resharer'), $display_resharer, $this->t('Display the first resharer as icon and text on a reshared item.')],
'$stay_local' => ['stay_local', $this->t('Stay local'), $stay_local, $this->t("Don't go to a remote system when following a contact link.")],
'$show_page_drop' => ['show_page_drop', $this->t('Show the post deletion checkbox'), $show_page_drop, $this->t("Display the checkbox for the post deletion on the network page.")],
'$display_eventlist' => ['display_eventlist', $this->t('DIsplay the event list'), $display_eventlist, $this->t("Display the birthday reminder and event list on the network page.")],
'$preview_mode' => ['preview_mode', $this->t('Link preview mode'), $preview_mode, $this->t('Appearance of the link preview that is added to each post with a link.'), $preview_modes, false],
'$hide_empty_descriptions' => ['hide_empty_descriptions', $this->t('Hide pictures with empty alternative text'), $hide_empty_descriptions, $this->t("Don't display pictures that are missing the alternative text.")],
'$hide_custom_emojis' => ['hide_custom_emojis', $this->t('Hide custom emojis'), $hide_custom_emojis, $this->t("Don't display custom emojis.")],
'$platform_icon_style' => ['platform_icon_style', $this->t('Platform icons style'), $platform_icon_style, $this->t('Style of the platform icons'), $platform_icon_styles, false],
'$timeline_label' => $this->t('Label'),
'$timeline_descriptiom' => $this->t('Description'),
@ -330,7 +352,7 @@ class Display extends BaseSettings
'$channel_languages' => ['channel_languages[]', $this->t('Channel languages:'), $channel_languages, $this->t('Select all languages that you want to see in your channels.'), $languages, 'multiple'],
'$first_day_of_week' => ['first_day_of_week' , $this->t('Beginning of week:') , $first_day_of_week , '', $weekdays , false],
'$first_day_of_week' => ['first_day_of_week', $this->t('Beginning of week:'), $first_day_of_week, '', $weekdays, false],
'$calendar_default_view' => ['calendar_default_view', $this->t('Default calendar view:'), $calendar_default_view, '', $calendarViews, false],
]);
}

View file

@ -77,7 +77,8 @@ class AppSpecific extends BaseSettings
$this->baseUrl->redirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password'));
} else {
$this->appSpecificPassword = AppSpecificPassword::generateForUser($this->session->getLocalUserId(), $request['description'] ?? '');
$this->systemMessages->addInfo($this->t('New app-specific password generated.'));
$this->systemMessages->addInfo($this->t('New app-specific password generated: %s', $this->appSpecificPassword['plaintext_password']));
$this->baseUrl->redirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password'));
}
break;

View file

@ -14,8 +14,11 @@ use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\KeyValueStorage\Capability\IManageKeyValuePairs;
use Friendica\Core\L10n;
use Friendica\Core\Protocol;
use Friendica\Core\Update;
use Friendica\Core\Worker;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\Database\DBStructure;
use Friendica\Model\Register;
use Friendica\Moderation\Entity\Report;
use Friendica\Util\DateTimeFormat;
@ -23,6 +26,10 @@ use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
use Friendica\Network\HTTPException;
/**
* Returns statistics of the current node for administration use
* Like for monitoring
*/
class Stats extends BaseModule
{
/** @var IManageConfigValues */
@ -129,7 +136,25 @@ class Stats extends BaseModule
],
'open' => $this->dba->count('report', ['status' => Report::STATUS_OPEN]),
'closed' => $this->dba->count('report', ['status' => Report::STATUS_CLOSED]),
]
],
'update' => [
'available' => Update::isAvailable(),
'available_version' => Update::getAvailableVersion(),
'status' => Update::getStatus(),
'db_status' => DBStructure::getUpdateStatus(),
],
'server' => [
'version' => App::VERSION,
'php' => [
'version' => phpversion(),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'memory_limit' => ini_get('memory_limit'),
],
'database' => [
'max_allowed_packet' => DBA::getVariable('max_allowed_packet'),
],
],
];
if (Addon::isEnabled('bluesky')) {

View file

@ -33,14 +33,14 @@ class Xrd extends BaseModule
}
$uri = urldecode(trim($_GET['uri']));
$mode = self::getAcceptedContentType($_SERVER['HTTP_ACCEPT'] ?? '', Response::TYPE_JSON);
$mode = self::getAcceptedContentType($_SERVER['HTTP_ACCEPT'] ?? '', Response::TYPE_XML);
} else {
if (empty($_GET['resource'])) {
throw new BadRequestException();
}
$uri = urldecode(trim($_GET['resource']));
$mode = self::getAcceptedContentType($_SERVER['HTTP_ACCEPT'] ?? '', Response::TYPE_XML);
$mode = self::getAcceptedContentType($_SERVER['HTTP_ACCEPT'] ?? '', Response::TYPE_JSON);
}
if (Network::isValidHttpUrl($uri)) {

View file

@ -57,6 +57,9 @@ class FriendicaExtension extends BaseDataTransferObject
*/
protected $visibility;
/** @var string|null */
protected $content;
/**
* Creates a FriendicaExtension object
*
@ -86,7 +89,8 @@ class FriendicaExtension extends BaseDataTransferObject
?string $version,
?string $sitename,
?FriendicaDeliveryData $delivery_data,
?FriendicaVisibility $visibility
?FriendicaVisibility $visibility,
?string $content
) {
$this->title = $title;
$this->changed_at = $changed_at ? DateTimeFormat::utc($changed_at, DateTimeFormat::JSON) : null;
@ -100,6 +104,7 @@ class FriendicaExtension extends BaseDataTransferObject
$this->version = $version;
$this->sitename = $sitename;
$this->visibility = $visibility;
$this->content = $content;
}
/**

View file

@ -121,7 +121,7 @@ class Status extends BaseDataTransferObject
$this->entities = $entities;
$this->extended_entities = $entities;
$origin = ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']);
$origin = ContactSelector::networkToName($item['author-network'], $item['network'], $item['author-gsid']);
if (empty($this->source)) {
$this->source = $origin;

View file

@ -131,7 +131,7 @@ class User extends BaseDataTransferObject
$this->name = $publicContact['name'] ?: $publicContact['nick'];
$this->screen_name = $publicContact['nick'] ?: $publicContact['name'];
$this->location = $publicContact['location'] ?:
ContactSelector::networkToName($publicContact['network'], $publicContact['url'], $publicContact['protocol']);
ContactSelector::networkToName($publicContact['network'], $publicContact['protocol'], $publicContact['gsid']);
$this->derived = [];
$this->url = $publicContact['url'];
// No entities needed since we don't perform any shortening in the URL or description

View file

@ -606,8 +606,8 @@ class Post
'edited' => $edited,
'author_gsid' => $item['author-gsid'],
'network' => $item['network'],
'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network'], $item['author-gsid']),
'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link'], $item['author-gsid']),
'network_name' => ContactSelector::networkToName($item['author-network'], $item['network'], $item['author-gsid']),
'network_svg' => ContactSelector::networkToSVG($item['network'], $item['author-gsid'], '', DI::userSession()->getLocalUserId()),
'received' => $item['received'],
'commented' => $item['commented'],
'created_date' => $item['created'],

View file

@ -877,10 +877,10 @@ class Processor
{
if (!empty($activity['mediatype']) && ($activity['mediatype'] == 'text/markdown')) {
$item['title'] = strip_tags($activity['name'] ?? '');
$content = Markdown::toBBCode($activity['content']);
$content = Markdown::toBBCode($activity['content'] ?? '');
} elseif (!empty($activity['mediatype']) && ($activity['mediatype'] == 'text/bbcode')) {
$item['title'] = $activity['name'];
$content = $activity['content'];
$item['title'] = $activity['name'] ?? '';
$content = $activity['content'] ?? '';
} else {
// By default assume "text/html"
$item['title'] = HTML::toBBCode($activity['name'] ?? '');

View file

@ -637,14 +637,6 @@ class Transmitter
$audience[] = $owner['url'];
}
if (self::isAnnounce($item) || self::isAPPost($last_id)) {
// Will be activated in a later step
$networks = Protocol::FEDERATED;
} else {
// For now only send to these contacts:
$networks = [Protocol::ACTIVITYPUB];
}
$data = ['to' => [], 'cc' => [], 'bto' => [], 'bcc' => [], 'audience' => $audience];
if ($item['gravity'] == Item::GRAVITY_PARENT) {
@ -704,7 +696,7 @@ class Transmitter
$cid = Contact::getIdForURL($term['url'], $item['uid']);
if (!empty($cid) && in_array($cid, $receiver_list)) {
$contact = DBA::selectFirst('contact', ['url', 'network', 'protocol', 'gsid'], ['id' => $cid, 'network' => Protocol::FEDERATED]);
if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) {
if (!DBA::isResult($contact)) {
continue;
}
@ -741,7 +733,7 @@ class Transmitter
}
$contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol', 'gsid'], ['id' => $receiver, 'network' => Protocol::FEDERATED]);
if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) {
if (!DBA::isResult($contact)) {
continue;
}
@ -984,44 +976,16 @@ class Transmitter
return DBA::exists('inbox-status', ['url' => $url, 'archive' => true]);
}
/**
* Check if a given contact should be delivered via AP
*
* @param array $contact Contact array
* @param array $networks Array with networks
* @return bool Whether the used protocol matches ACTIVITYPUB
* @throws Exception
*/
private static function isAPContact(array $contact, array $networks): bool
{
if (in_array($contact['network'], $networks) || ($contact['protocol'] == Protocol::ACTIVITYPUB)) {
return true;
}
return GServer::getProtocol($contact['gsid'] ?? 0) == Post\DeliveryData::ACTIVITYPUB;
}
/**
* Fetches a list of inboxes of followers of a given user
*
* @param integer $uid User ID
* @param boolean $all_ap Retrieve all AP enabled inboxes
* @return array of follower inboxes
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function fetchTargetInboxesforUser(int $uid, bool $all_ap = false): array
public static function fetchTargetInboxesforUser(int $uid): array
{
$inboxes = [];
if ($all_ap) {
// Will be activated in a later step
$networks = Protocol::FEDERATED;
} else {
// For now only send to these contacts:
$networks = [Protocol::ACTIVITYPUB];
}
$condition = [
'uid' => $uid,
'self' => false,
@ -1029,37 +993,51 @@ class Transmitter
'pending' => false,
'blocked' => false,
'network' => Protocol::FEDERATED,
'contact-type' => [Contact::TYPE_UNKNOWN, Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION],
];
if (!empty($uid)) {
$condition['rel'] = [Contact::FOLLOWER, Contact::FRIEND];
}
$contacts = DBA::select('contact', ['id', 'url', 'network', 'protocol', 'gsid'], $condition);
while ($contact = DBA::fetch($contacts)) {
if (!self::isAPContact($contact, $networks)) {
return self::addInboxesForCondition($condition, []);
}
/**
* Fetch inboxes for a list of contacts
*
* @param array $recipients
* @param array $inboxes
* @return array
*/
public static function addInboxesForRecipients(array $recipients, array $inboxes): array
{
return self::addInboxesForCondition(['id' => $recipients], $inboxes);
}
/**
* Get a list of inboxes for a given contact condition
*
* @param array $condition
* @param array $inboxes
* @return array
*/
private static function addInboxesForCondition(array $condition, array $inboxes): array
{
$condition = DBA::mergeConditions($condition, ["(`ap-inbox` IS NOT NULL OR `ap-sharedinbox` IS NOT NULL)"]);
$accounts = DBA::select('account-user-view', ['id', 'url', 'ap-inbox', 'ap-sharedinbox'], $condition);
while ($account = DBA::fetch($accounts)) {
if (!empty($account['ap-sharedinbox']) && !Contact::isLocal($account['url'])) {
$target = $account['ap-sharedinbox'];
} elseif (!empty($account['ap-inbox'])) {
$target = $account['ap-inbox'];
} else {
continue;
}
if (Network::isUrlBlocked($contact['url'])) {
continue;
}
$profile = APContact::getByURL($contact['url'], false);
if (!empty($profile)) {
if (empty($profile['sharedinbox']) || Contact::isLocal($contact['url'])) {
$target = $profile['inbox'];
} else {
$target = $profile['sharedinbox'];
}
if (!self::archivedInbox($target)) {
$inboxes[$target][] = $contact['id'];
}
if (!Transmitter::archivedInbox($target) && (empty($inboxes[$target]) || !in_array($account['id'], $inboxes[$target]))) {
$inboxes[$target][] = $account['id'];
}
}
DBA::close($contacts);
return $inboxes;
}
@ -1106,7 +1084,7 @@ class Transmitter
}
if ($item_profile && ($receiver == $item_profile['followers']) && ($uid == $profile_uid)) {
$inboxes = array_merge_recursive($inboxes, self::fetchTargetInboxesforUser($uid, true));
$inboxes = array_merge_recursive($inboxes, self::fetchTargetInboxesforUser($uid));
} else {
$profile = APContact::getByURL($receiver, false);
if (!empty($profile)) {
@ -1413,7 +1391,7 @@ class Transmitter
}
}
if (!$api_mode && !$item['origin']) {
if (!$api_mode && !$item['deleted'] && !$item['origin']) {
Logger::debug('Post is not ours and is not stored', ['id' => $item['id'], 'uri-id' => $item['uri-id']]);
return false;
}
@ -2103,17 +2081,18 @@ class Transmitter
* Creates an activity id for a given contact id
*
* @param integer $cid Contact ID of target
* @param integer $uid Optional user id. if empty, the contact uid is used.
*
* @return bool|string activity id
*/
public static function activityIDFromContact(int $cid)
public static function activityIDFromContact(int $cid, int $uid = 0)
{
$contact = DBA::selectFirst('contact', ['uid', 'id', 'created'], ['id' => $cid]);
if (!DBA::isResult($contact)) {
return false;
}
$hash = hash('ripemd128', $contact['uid'] . '-' . $contact['id'] . '-' . $contact['created']);
$hash = hash('ripemd128', $uid ?: $contact['uid'] . '-' . $contact['id'] . '-' . $contact['created']);
$uuid = substr($hash, 0, 8) . '-' . substr($hash, 8, 4) . '-' . substr($hash, 12, 4) . '-' . substr($hash, 16, 4) . '-' . substr($hash, 20, 12);
return DI::baseUrl() . '/activity/' . $uuid;
}
@ -2477,6 +2456,53 @@ class Transmitter
return HTTPSignature::transmit($signed, $profile['inbox'], $owner);
}
/**
* Transmits a message that we don't want to block this contact anymore
*
* @param string $target Target profile
* @param integer $cid Contact id
* @param array $owner Sender owner-view record
* @return bool success
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
* @throws \Exception
*/
public static function sendContactUnblock(string $target, int $cid, array $owner): bool
{
$profile = APContact::getByURL($target);
if (empty($profile['inbox'])) {
Logger::warning('No inbox found for target', ['target' => $target, 'profile' => $profile]);
return false;
}
$object_id = self::activityIDFromContact($cid, $owner['uid']);
if (empty($object_id)) {
return false;
}
$objectId = DI::baseUrl() . '/activity/' . System::createGUID();
$data = [
'@context' => ActivityPub::CONTEXT,
'id' => $objectId,
'type' => 'Undo',
'actor' => $owner['url'],
'object' => [
'id' => $object_id,
'type' => 'Block',
'actor' => $owner['url'],
'object' => $profile['url']
],
'instrument' => self::getService(),
'to' => [$profile['url']],
];
Logger::info('Sending undo to ' . $target . ' for user ' . $owner['uid'] . ' with id ' . $objectId);
$signed = LDSignature::sign($data, $owner);
return HTTPSignature::transmit($signed, $profile['inbox'], $owner);
}
/**
* Prepends mentions (@) to $body variable
*

View file

@ -986,7 +986,7 @@ class DFRN
Logger::notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]);
return -25;
}
Item::incrementOutbound(Protocol::DFRN);
$xml = $postResult->getBodyString();
$curl_stat = $postResult->getReturnCode();
@ -1017,6 +1017,7 @@ class DFRN
if (!empty($contact['gsid'])) {
GServer::setReachableById($contact['gsid'], Protocol::DFRN);
Item::incrementOutbound(Protocol::DFRN);
}
if (!empty($res->message)) {

View file

@ -2962,7 +2962,6 @@ class Diaspora
return 0;
}
$return_code = $postResult->getReturnCode();
Item::incrementOutbound(Protocol::DIASPORA);
} else {
Logger::notice('test_mode');
return 200;
@ -2971,6 +2970,7 @@ class Diaspora
if (!empty($contact['gsid']) && (empty($return_code) || $postResult->isTimeout())) {
GServer::setFailureById($contact['gsid']);
} elseif (!empty($contact['gsid']) && ($return_code >= 200) && ($return_code <= 299)) {
Item::incrementOutbound(Protocol::DIASPORA);
GServer::setReachableById($contact['gsid'], Protocol::DIASPORA);
}

View file

@ -129,8 +129,10 @@ class OAuth
return [];
}
// The redirect_uri could contain several URI that are separated by spaces.
if (($application['redirect_uri'] != $redirect_uri) && !in_array($redirect_uri, explode(' ', $application['redirect_uri']))) {
// The redirect_uri could contain several URI that are separated by spaces or new lines.
$uris = explode(' ', str_replace(["\n", "\r", "\t"], ' ', $application['redirect_uri']));
if (!in_array($redirect_uri, $uris)) {
Logger::warning('Redirection uri does not match', ['redirect_uri' => $redirect_uri, 'application-redirect_uri' => $application['redirect_uri']]);
return [];
}

View file

@ -297,7 +297,9 @@ class HTTPSignature
self::setInboxStatus($target, ($return_code >= 200) && ($return_code <= 299));
Item::incrementOutbound(Protocol::ACTIVITYPUB);
if (($return_code >= 200) && ($return_code <= 299)) {
Item::incrementOutbound(Protocol::ACTIVITYPUB);
}
return $postResult;
}
@ -757,9 +759,9 @@ class HTTPSignature
}
if (in_array('(expires)', $sig_block['headers']) && !empty($sig_block['expires'])) {
$expired = min($sig_block['expires'], $created + 300);
$expired = min($sig_block['expires'], $created + 3600);
} else {
$expired = $created + 300;
$expired = $created + 3600;
}
// Check if the signed date field is in an acceptable range

View file

@ -58,6 +58,9 @@ class JsonLD
case 'http://joinmastodon.org/ns':
$url = DI::basePath() . '/static/joinmastodon.jsonld';
break;
case 'https://purl.archive.org/socialweb/webfinger':
$url = DI::basePath() . '/static/socialweb-webfinger.jsonld';
break;
default:
switch (parse_url($url, PHP_URL_PATH)) {
case '/schemas/litepub-0.1.jsonld';

View file

@ -155,7 +155,8 @@ class Strings
{
if ($network != '') {
if ($url != '') {
$network_name = '<a href="' . $url . '">' . ContactSelector::networkToName($network, $url) . '</a>';
$gsid = ContactSelector::getServerIdForProfile($url);
$network_name = '<a href="' . $url . '">' . ContactSelector::networkToName($network, '', $gsid) . '</a>';
} else {
$network_name = ContactSelector::networkToName($network);
}

View file

@ -0,0 +1,38 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Worker\Contact;
use Friendica\Core\Protocol;
use Friendica\Core\Worker;
use Friendica\Model\Contact;
class Block
{
const WORKER_DEFER_LIMIT = 5;
/**
* Issue asynchronous block message to remote servers.
*
* @param int $cid Target public contact (uid = 0) id
* @param int $uid Source local user id
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function execute(int $cid, int $uid)
{
$contact = Contact::getById($cid);
if (empty($contact)) {
return;
}
$result = Protocol::block($contact, $uid);
if ($result === false) {
Worker::defer(self::WORKER_DEFER_LIMIT);
}
}
}

View file

@ -0,0 +1,38 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Worker\Contact;
use Friendica\Core\Protocol;
use Friendica\Core\Worker;
use Friendica\Model\Contact;
class Unblock
{
const WORKER_DEFER_LIMIT = 5;
/**
* Issue asynchronous unblock message to remote servers.
*
* @param int $cid Target public contact (uid = 0) id
* @param int $uid Source local user id
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function execute(int $cid, int $uid)
{
$contact = Contact::getById($cid);
if (empty($contact)) {
return;
}
$result = Protocol::unblock($contact, $uid);
if ($result === false) {
Worker::defer(self::WORKER_DEFER_LIMIT);
}
}
}

View file

@ -23,6 +23,7 @@ use Friendica\Model\Tag;
use Friendica\Model\User;
use Friendica\Protocol\Activity;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\ActivityPub\Transmitter;
use Friendica\Protocol\Diaspora;
use Friendica\Protocol\Delivery;
use Friendica\Util\LDSignature;
@ -46,7 +47,6 @@ class Notifier
Logger::info('Invoked', ['cmd' => $cmd, 'target' => $post_uriid, 'sender_uid' => $sender_uid]);
$target_id = $post_uriid;
$top_level = false;
$recipients = [];
$delivery_contacts_stmt = null;
@ -126,8 +126,6 @@ class Notifier
$item['deleted'] = 1;
}
}
$top_level = $target_item['gravity'] == Item::GRAVITY_PARENT;
}
$owner = User::getOwnerDataById($uid);
@ -161,16 +159,17 @@ class Notifier
Logger::info('Got post', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'network' => $target_item['network'], 'parent-network' => $parent['network'], 'thread-parent-network' => $thr_parent['network']]);
if (!self::isRemovalActivity($cmd, $owner, Protocol::ACTIVITYPUB)) {
$apdelivery = self::activityPubDelivery($cmd, $target_item, $parent, $thr_parent, $a->getQueueValue('priority'), $a->getQueueValue('created'), $owner);
$ap_contacts = $apdelivery['contacts'];
$delivery_queue_count += $apdelivery['count'];
// Restrict distribution to AP, when there are no permissions.
if (($target_item['private'] == Item::PRIVATE) && empty($target_item['allow_cid']) && empty($target_item['allow_gid']) && empty($target_item['deny_cid']) && empty($target_item['deny_gid'])) {
$only_ap_delivery = true;
$public_message = false;
$diaspora_delivery = false;
}
// Restrict distribution to AP, when there are no permissions.
if (!self::isRemovalActivity($cmd, $owner, Protocol::ACTIVITYPUB) && ($target_item['private'] == Item::PRIVATE) && empty($target_item['allow_cid']) && empty($target_item['allow_gid']) && empty($target_item['deny_cid']) && empty($target_item['deny_gid'])) {
$only_ap_delivery = true;
$public_message = false;
$diaspora_delivery = false;
}
if (!$target_item['origin'] && $target_item['network'] == Protocol::ACTIVITYPUB) {
$only_ap_delivery = true;
$diaspora_delivery = false;
Logger::debug('Remote post arrived via AP', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'network' => $target_item['network'], 'parent-network' => $parent['network'], 'thread-parent-network' => $thr_parent['network']]);
}
// Only deliver threaded replies (comment to a comment) to Diaspora
@ -194,45 +193,19 @@ class Notifier
// if $parent['wall'] == 1 we will already have the parent message in our array
// and we will relay the whole lot.
$localhost = str_replace('www.','', DI::baseUrl()->getHost());
if (strpos($localhost,':')) {
$localhost = substr($localhost,0,strpos($localhost,':'));
}
/**
*
* Be VERY CAREFUL if you make any changes to the following several lines. Seemingly innocuous changes
* have been known to cause runaway conditions which affected several servers, along with
* permissions issues.
*
*/
$relay_to_owner = false;
if (!$top_level && ($parent['wall'] == 0) && (stristr($target_item['uri'],$localhost))) {
if (($target_item['gravity'] != Item::GRAVITY_PARENT) && !$parent['wall'] && $target_item['origin']) {
$relay_to_owner = true;
}
// until the 'origin' flag has been in use for several months
// we will just use it as a fallback test
// later we will be able to use it as the primary test of whether or not to relay.
if (!$target_item['origin']) {
$relay_to_owner = false;
}
if ($parent['origin']) {
if (!$target_item['origin'] || $parent['origin']) {
$relay_to_owner = false;
}
// Special treatment for group posts
if (Item::isGroupPost($target_item['uri-id'])) {
$relay_to_owner = true;
$direct_group_delivery = true;
}
// Avoid that comments in a group thread are sent to OStatus
if (Item::isGroupPost($parent['uri-id'])) {
$direct_group_delivery = true;
}
$exclusive_delivery = false;
@ -279,17 +252,19 @@ class Notifier
return;
}
if (strlen($parent['allow_cid'])
if (
strlen($parent['allow_cid'])
|| strlen($parent['allow_gid'])
|| strlen($parent['deny_cid'])
|| strlen($parent['deny_gid'])) {
|| strlen($parent['deny_gid'])
) {
$public_message = false; // private recipients, not public
}
$aclFormatter = DI::aclFormatter();
$allow_people = $aclFormatter->expand($parent['allow_cid']);
$allow_circles = Circle::expand($uid, $aclFormatter->expand($parent['allow_gid']),true);
$allow_circles = Circle::expand($uid, $aclFormatter->expand($parent['allow_gid']), true);
$deny_people = $aclFormatter->expand($parent['deny_cid']);
$deny_circles = Circle::expand($uid, $aclFormatter->expand($parent['deny_gid']));
@ -297,10 +272,10 @@ class Notifier
$recipients[] = $item['contact-id'];
// pull out additional tagged people to notify (if public message)
if ($public_message && $item['inform']) {
$people = explode(',',$item['inform']);
$people = explode(',', $item['inform']);
foreach ($people as $person) {
if (substr($person,0,4) === 'cid:') {
$recipients[] = intval(substr($person,4));
if (substr($person, 0, 4) === 'cid:') {
$recipients[] = intval(substr($person, 4));
}
}
}
@ -313,7 +288,7 @@ class Notifier
// If this is a public message and pubmail is set on the parent, include all your email contacts
if (
function_exists('imap_open')
&& !DI::config()->get('system','imap_disabled')
&& !DI::config()->get('system', 'imap_disabled')
&& $public_message
&& intval($target_item['pubmail'])
) {
@ -338,51 +313,65 @@ class Notifier
$public_message = false;
}
if (empty($delivery_contacts_stmt)) {
if ($only_ap_delivery) {
$recipients = $ap_contacts;
} elseif ($followup) {
$recipients = $recipients_followup;
}
$condition = ['id' => $recipients, 'self' => false, 'uid' => [0, $uid],
'blocked' => false, 'pending' => false, 'archive' => false];
if (!empty($networks)) {
$condition['network'] = $networks;
}
$delivery_contacts_stmt = DBA::select('contact', ['id', 'uri-id', 'addr', 'url', 'network', 'protocol', 'baseurl', 'gsid', 'batch'], $condition);
if ($only_ap_delivery) {
$recipients = [];
} elseif ($followup) {
$recipients = $recipients_followup;
}
$conversants = [];
$batch_delivery = false;
$apdelivery = self::activityPubDelivery($cmd, $target_item, $parent, $thr_parent, $a->getQueueValue('priority'), $a->getQueueValue('created'), $recipients);
$ap_contacts = $apdelivery['contacts'];
$delivery_queue_count += $apdelivery['count'];
if ($public_message && !in_array($cmd, [Delivery::MAIL, Delivery::SUGGESTION]) && !$followup) {
$participants = [];
if ($diaspora_delivery && !$unlisted) {
$batch_delivery = true;
$participants = DBA::selectToArray('contact', ['batch', 'network', 'protocol', 'baseurl', 'gsid', 'id', 'url', 'name'],
["`network` = ? AND `batch` != '' AND `uid` = ? AND `rel` != ? AND NOT `blocked` AND NOT `pending` AND NOT `archive`", Protocol::DIASPORA, $owner['uid'], Contact::SHARING],
['group_by' => ['batch', 'network', 'protocol']]);
// Fetch the participation list
// The function will ensure that there are no duplicates
$participants = Diaspora::participantsForThread($target_item, $participants);
if (!$only_ap_delivery) {
if (empty($delivery_contacts_stmt)) {
$condition = ['id' => $recipients, 'self' => false, 'uid' => [0, $uid],
'blocked' => false, 'pending' => false, 'archive' => false];
if (!empty($networks)) {
$condition['network'] = $networks;
}
$delivery_contacts_stmt = DBA::select('contact', ['id', 'uri-id', 'addr', 'url', 'network', 'protocol', 'baseurl', 'gsid', 'batch'], $condition);
}
$condition = ['network' => Protocol::DFRN, 'uid' => $owner['uid'], 'blocked' => false,
'pending' => false, 'archive' => false, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]];
$conversants = [];
$batch_delivery = false;
$contacts = DBA::selectToArray('contact', ['id', 'uri-id', 'url', 'addr', 'name', 'network', 'protocol', 'baseurl', 'gsid'], $condition);
if ($public_message && !in_array($cmd, [Delivery::MAIL, Delivery::SUGGESTION]) && !$followup) {
$participants = [];
$conversants = array_merge($contacts, $participants);
if ($diaspora_delivery && !$unlisted) {
$batch_delivery = true;
$delivery_queue_count += self::delivery($cmd, $post_uriid, $sender_uid, $target_item, $thr_parent, $owner, $batch_delivery, true, $conversants, $ap_contacts, []);
$participants = DBA::selectToArray('contact', ['batch', 'network', 'protocol', 'baseurl', 'gsid', 'id', 'url', 'name'],
["`network` = ? AND `batch` != '' AND `uid` = ? AND `rel` != ? AND NOT `blocked` AND NOT `pending` AND NOT `archive`", Protocol::DIASPORA, $owner['uid'], Contact::SHARING],
['group_by' => ['batch', 'network', 'protocol']]);
// Fetch the participation list
// The function will ensure that there are no duplicates
$participants = Diaspora::participantsForThread($target_item, $participants);
}
$condition = [
'network' => Protocol::DFRN,
'uid' => $owner['uid'],
'self' => false,
'blocked' => false,
'pending' => false,
'archive' => false,
'rel' => [Contact::FOLLOWER, Contact::FRIEND]
];
$contacts = DBA::selectToArray('contact', ['id', 'uri-id', 'url', 'addr', 'name', 'network', 'protocol', 'baseurl', 'gsid'], $condition);
$conversants = array_merge($contacts, $participants);
$delivery_queue_count += self::delivery($cmd, $post_uriid, $sender_uid, $target_item, $parent, $thr_parent, $owner, $batch_delivery, true, $conversants, $ap_contacts, []);
}
$contacts = DBA::toArray($delivery_contacts_stmt);
$delivery_queue_count += self::delivery($cmd, $post_uriid, $sender_uid, $target_item, $parent, $thr_parent, $owner, $batch_delivery, false, $contacts, $ap_contacts, $conversants);
}
$contacts = DBA::toArray($delivery_contacts_stmt);
$delivery_queue_count += self::delivery($cmd, $post_uriid, $sender_uid, $target_item, $thr_parent, $owner, $batch_delivery, false, $contacts, $ap_contacts, $conversants);
if (!empty($target_item)) {
Logger::info('Calling hooks for ' . $cmd . ' ' . $target_id);
@ -411,6 +400,7 @@ class Notifier
* @param int $post_uriid
* @param int $sender_uid
* @param array $target_item
* @param array $parent
* @param array $thr_parent
* @param array $owner
* @param bool $batch_delivery
@ -422,7 +412,7 @@ class Notifier
* @throws InternalServerErrorException
* @throws Exception
*/
private static function delivery(string $cmd, int $post_uriid, int $sender_uid, array $target_item, array $thr_parent, array $owner, bool $batch_delivery, bool $in_batch, array $contacts, array $ap_contacts, array $conversants = []): int
private static function delivery(string $cmd, int $post_uriid, int $sender_uid, array $target_item, array $parent, array $thr_parent, array $owner, bool $batch_delivery, bool $in_batch, array $contacts, array $ap_contacts, array $conversants = []): int
{
$a = DI::app();
$delivery_queue_count = 0;
@ -433,6 +423,13 @@ class Notifier
}
foreach ($contacts as $contact) {
// Transmit via Diaspora if the thread had started as Diaspora post.
// Also transmit via Diaspora if this is a direct answer to a Diaspora comment.
if (($contact['network'] != Protocol::DIASPORA) && in_array(Protocol::DIASPORA, [$parent['network'] ?? '', $thr_parent['network'] ?? '', $target_item['network'] ?? ''])) {
Logger::info('Enforcing the Diaspora protocol', ['id' => $contact['id'], 'network' => $contact['network'], 'parent' => $parent['network'], 'thread-parent' => $thr_parent['network'], 'post' => $target_item['network']]);
$contact['network'] = Protocol::DIASPORA;
}
// Direct delivery of local contacts
if (!in_array($cmd, [Delivery::RELOCATION, Delivery::SUGGESTION, Delivery::MAIL]) && $target_uid = User::getIdForURL($contact['url'])) {
if ($cmd == Delivery::DELETION) {
@ -453,14 +450,19 @@ class Notifier
continue;
}
// Deletions are always sent via DFRN as well.
// This is done until we can perform deletions of foreign comments on our own threads via AP.
if (($cmd != Delivery::DELETION) && in_array($contact['id'], $ap_contacts)) {
Logger::info('Contact is already delivered via AP, so skip delivery via legacy DFRN/Diaspora', ['target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]);
$cdata = Contact::getPublicAndUserContactID($contact['id'], $sender_uid);
if (in_array($cdata['public'] ?: $contact['id'], $ap_contacts)) {
Logger::info('The public contact is already delivered via AP, so skip delivery via legacy DFRN/Diaspora', ['batch' => $in_batch, 'target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]);
continue;
} elseif (in_array($cdata['user'] ?: $contact['id'], $ap_contacts)) {
Logger::info('The user contact is already delivered via AP, so skip delivery via legacy DFRN/Diaspora', ['batch' => $in_batch, 'target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]);
continue;
}
if (!empty($contact['id']) && Contact::isArchived($contact['id'])) {
// We mark the contact here, since we could have only got here, when the "archived" value on this
// specific contact hadn't been set.
Contact::markForArchival($contact);
Logger::info('Contact is archived, so skip delivery', ['target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]);
continue;
}
@ -505,7 +507,7 @@ class Notifier
continue;
}
Logger::info('Delivery', ['batch' => $in_batch, 'target' => $post_uriid, 'uid' => $sender_uid, 'guid' => $target_item['guid'] ?? '', 'to' => $contact]);
Logger::info('Delivery', ['cmd' => $cmd, 'batch' => $in_batch, 'target' => $post_uriid, 'uid' => $sender_uid, 'guid' => $target_item['guid'] ?? '', 'to' => $contact]);
// Ensure that posts with our own protocol arrives before Diaspora posts arrive.
// Situation is that sometimes Friendica servers receive Friendica posts over the Diaspora protocol first.
@ -578,7 +580,7 @@ class Notifier
*/
private static function isRemovalActivity(string $cmd, array $owner, string $network): bool
{
return ($cmd == Delivery::DELETION) && $owner['account_removed'] && in_array($network, [Protocol::ACTIVITYPUB, Protocol::DIASPORA]);
return ($cmd == Delivery::REMOVAL) && $owner['account_removed'] && in_array($network, [Protocol::ACTIVITYPUB, Protocol::DIASPORA]);
}
/**
@ -602,7 +604,7 @@ class Notifier
return false;
}
while($contact = DBA::fetch($contacts_stmt)) {
while ($contact = DBA::fetch($contacts_stmt)) {
Contact::terminateFriendship($contact);
}
DBA::close($contacts_stmt);
@ -623,15 +625,16 @@ class Notifier
* @param array $target_item
* @param array $parent
* @param array $thr_parent
* @param int $priority The priority the Notifier queue item was created with
* @param string $created The date the Notifier queue item was created on
* @param int $priority The priority the Notifier queue item was created with
* @param string $created The date the Notifier queue item was created on
* @param array $recipients Array of receivers
*
* @return array 'count' => The number of delivery tasks created, 'contacts' => their contact ids
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
* @todo Unused parameter $owner
*/
private static function activityPubDelivery($cmd, array $target_item, array $parent, array $thr_parent, int $priority, string $created, $owner): array
private static function activityPubDelivery($cmd, array $target_item, array $parent, array $thr_parent, int $priority, string $created, array $recipients): array
{
// Don't deliver via AP when the starting post isn't from a federated network
if (!in_array($parent['network'], Protocol::FEDERATED)) {
@ -680,10 +683,10 @@ class Notifier
Logger::info('Origin item will be distributed', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]);
$check_signature = false;
} elseif (!Post\Activity::exists($target_item['uri-id'])) {
Logger::info('Remote item is no AP post. It will not be distributed.', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]);
} elseif (!$target_item['deleted'] && !Post\Activity::exists($target_item['uri-id'])) {
Logger::info('Remote activity not found. It will not be distributed.', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]);
return ['count' => 0, 'contacts' => []];
} elseif ($parent['origin'] && (($target_item['gravity'] != Item::GRAVITY_ACTIVITY) || DI::config()->get('system', 'redistribute_activities'))) {
} elseif ($parent['origin'] && ($target_item['private'] != Item::PRIVATE) && (($target_item['gravity'] != Item::GRAVITY_ACTIVITY) || DI::config()->get('system', 'redistribute_activities'))) {
$inboxes = ActivityPub\Transmitter::fetchTargetInboxes($parent, $uid);
if (in_array($target_item['private'], [Item::PUBLIC])) {
@ -697,6 +700,10 @@ class Notifier
return ['count' => 0, 'contacts' => []];
}
if ($target_item['private'] != Item::PRIVATE) {
$inboxes = Transmitter::addInboxesForRecipients($recipients, $inboxes);
}
if (empty($inboxes) && empty($relay_inboxes)) {
Logger::info('No inboxes found for item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . '. It will not be distributed.');
return ['count' => 0, 'contacts' => []];

View file

@ -76,7 +76,7 @@ class UpdateServerDirectory
}
$accounts = json_decode($result, true);
if (empty($accounts)) {
if (!is_array($accounts)) {
Logger::info('No contacts', ['url' => $gserver['url']]);
return;
}