Merge pull request #14618 from Art4/merge-2024.09-rc-into-develop

Merge `2024.09-rc` into `develop`
This commit is contained in:
Hypolite Petovan 2024-12-25 22:38:07 -05:00 committed by GitHub
commit a8963c3ba2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 635 additions and 628 deletions

View file

@ -558,15 +558,17 @@ class Worker
if ($method_call) {
try {
call_user_func_array(sprintf('Friendica\Worker\%s::execute', $funcname), $argv);
} catch (\TypeError $e) {
// No need to defer a worker queue entry if the arguments are invalid
Logger::notice('Wrong worker arguments', ['class' => $funcname, 'argv' => $argv, 'queue' => $queue, 'message' => $e->getMessage()]);
} catch (\Throwable $e) {
Logger::error('Uncaught exception in worker execution', ['class' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile() . ':' . $e->getLine(), 'trace' => $e->getTraceAsString(), 'previous' => $e->getPrevious()]);
Logger::error('Uncaught exception in worker method execution', ['class' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile() . ':' . $e->getLine(), 'trace' => $e->getTraceAsString(), 'previous' => $e->getPrevious()]);
Worker::defer();
}
} else {
$funcname($argv, count($argv));
try {
$funcname($argv, count($argv));
} catch (\Throwable $e) {
Logger::error('Uncaught exception in worker execution', ['message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile() . ':' . $e->getLine(), 'trace' => $e->getTraceAsString(), 'previous' => $e->getPrevious()]);
Worker::defer();
}
}
Logger::disableWorker();

View file

@ -24,11 +24,13 @@ use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\ATProtocol;
use Friendica\Util\Images;
use Friendica\Util\Network;
use Friendica\Util\ParseUrl;
use Friendica\Util\Proxy;
use Friendica\Util\Strings;
use GuzzleHttp\Psr7\Uri;
/**
* Class Media
@ -237,7 +239,7 @@ class Media
$media = self::addAccount($media);
}
if (in_array($media['type'], [self::ACTIVITY, self::LD, self::JSON])) {
if (in_array($media['type'], [self::ACTIVITY, self::LD, self::JSON]) || self::isFederatedServer($media['url'])) {
$media = self::addActivity($media);
}
@ -248,6 +250,20 @@ class Media
return $media;
}
private static function isFederatedServer(string $url): bool
{
$baseurl = Network::getBaseUrl(new Uri($url));
if (empty($baseurl)) {
return false;
}
if (Strings::compareLink($baseurl, ATProtocol::WEB)) {
return true;
}
return DBA::exists('gserver', ['nurl' => Strings::normaliseLink($baseurl), 'network' => Protocol::FEDERATED]);
}
private static function addPreviewData(array $media): array
{
if (!empty($media['preview-width']) && !empty($media['preview-height'])) {

View file

@ -151,6 +151,13 @@ class BaseSettings extends BaseModule
'accesskey' => 's',
];
$tabs[] = [
'label' => $this->t('Import Contacts'),
'url' => 'settings/importcontacts',
'selected' => static::class == Settings\UserExport::class ? 'active' : '',
'accesskey' => '',
];
$tabs[] = [
'label' => $this->t('Export personal data'),
'url' => 'settings/userexport',

View file

@ -9,7 +9,6 @@ namespace Friendica\Module\Settings;
use Exception;
use Friendica\Core\ACL;
use Friendica\Core\Logger;
use Friendica\Core\Renderer;
use Friendica\Core\Search;
use Friendica\Core\Worker;
@ -25,7 +24,6 @@ use Friendica\Module\BaseSettings;
use Friendica\Network\HTTPException;
use Friendica\Protocol\Activity;
use Friendica\Protocol\Delivery;
use Friendica\Util\Network;
use Friendica\Util\Temporal;
class Account extends BaseSettings
@ -322,42 +320,6 @@ class Account extends BaseSettings
DI::baseUrl()->redirect($redirectUrl);
}
// Import Contacts from CSV file
if (!empty($request['importcontact-submit'])) {
if (isset($_FILES['importcontact-filename'])) {
// was there an error
if ($_FILES['importcontact-filename']['error'] > 0) {
Logger::notice('Contact CSV file upload error', ['error' => $_FILES['importcontact-filename']['error']]);
DI::sysmsg()->addNotice(DI::l10n()->t('Contact CSV file upload error'));
} else {
$csvArray = array_map('str_getcsv', file($_FILES['importcontact-filename']['tmp_name']));
Logger::notice('Import started', ['lines' => count($csvArray)]);
// import contacts
foreach ($csvArray as $csvRow) {
// The 1st row may, or may not contain the headers of the table
// We expect the 1st field of the row to contain either the URL
// or the handle of the account, therefore we check for either
// "http" or "@" to be present in the string.
// All other fields from the row will be ignored
if ((strpos($csvRow[0], '@') !== false) || Network::isValidHttpUrl($csvRow[0])) {
Worker::add(Worker::PRIORITY_MEDIUM, 'AddContact', DI::userSession()->getLocalUserId(), trim($csvRow[0], '@'));
} else {
Logger::notice('Invalid account', ['url' => $csvRow[0]]);
}
}
Logger::notice('Import done');
DI::sysmsg()->addInfo(DI::l10n()->t('Importing Contacts done'));
// delete temp file
unlink($_FILES['importcontact-filename']['tmp_name']);
}
} else {
Logger::notice('Import triggered, but no import file was found.');
}
DI::baseUrl()->redirect($redirectUrl);
}
if (!empty($request['relocate-submit'])) {
Worker::add(Worker::PRIORITY_HIGH, 'Notifier', Delivery::RELOCATION, DI::userSession()->getLocalUserId());
DI::sysmsg()->addInfo(DI::l10n()->t("Relocate message has been send to your contacts"));
@ -633,11 +595,6 @@ class Account extends BaseSettings
'$h_descadvn' => DI::l10n()->t('Change the behaviour of this account for special situations'),
'$pagetype' => $pagetype,
'$importcontact' => DI::l10n()->t('Import Contacts'),
'$importcontact_text' => DI::l10n()->t('Upload a CSV file that contains the handle of your followed accounts in the first column you exported from the old account.'),
'$importcontact_button' => DI::l10n()->t('Upload File'),
'$importcontact_maxsize' => DI::config()->get('system', 'max_csv_file_size', 30720),
'$relocate' => DI::l10n()->t('Relocate'),
'$relocate_text' => DI::l10n()->t("If you have moved this profile from another server, and some of your contacts don't receive your updates, try pushing this button."),
'$relocate_button' => DI::l10n()->t("Resend relocate message to contacts"),

View file

@ -62,7 +62,6 @@ class Connectors extends BaseSettings
$this->pconfig->set($this->session->getLocalUserId(), 'system', 'api_spoiler_title', intval($request['api_spoiler_title']));
$this->pconfig->set($this->session->getLocalUserId(), 'system', 'api_auto_attach', intval($request['api_auto_attach']));
$this->pconfig->set($this->session->getLocalUserId(), 'system', 'article_mode', intval($request['article_mode']));
$this->pconfig->set($this->session->getLocalUserId(), 'ostatus', 'legacy_contact', $request['legacy_contact']);
} elseif (!empty($request['mail-submit']) && function_exists('imap_open') && !$this->config->get('system', 'imap_disabled')) {
$mail_server = $request['mail_server'] ?? '';
$mail_port = $request['mail_port'] ?? '';
@ -127,11 +126,6 @@ class Connectors extends BaseSettings
$api_spoiler_title = intval($this->pconfig->get($this->session->getLocalUserId(), 'system', 'api_spoiler_title', true));
$api_auto_attach = intval($this->pconfig->get($this->session->getLocalUserId(), 'system', 'api_auto_attach', false));
$article_mode = intval($this->pconfig->get($this->session->getLocalUserId(), 'system', 'article_mode'));
$legacy_contact = $this->pconfig->get($this->session->getLocalUserId(), 'ostatus', 'legacy_contact');
if (!empty($legacy_contact)) {
$this->baseUrl->redirect('ostatus/subscribe?url=' . urlencode($legacy_contact));
}
$connector_settings_forms = [];
foreach ($this->database->selectToArray('hook', ['file', 'function'], ['hook' => 'connector_settings']) as $hook) {
@ -215,7 +209,6 @@ class Connectors extends BaseSettings
'$api_spoiler_title' => ['api_spoiler_title', $this->t('API: Use spoiler field as title'), $api_spoiler_title, $this->t('When activated, the "spoiler_text" field in the API will be used for the title on standalone posts. When deactivated it will be used for spoiler text. For comments it will always be used for spoiler text.')],
'$api_auto_attach' => ['api_auto_attach', $this->t('API: Automatically links at the end of the post as attached posts'), $api_auto_attach, $this->t('When activated, added links at the end of the post react the same way as added links in the web interface.')],
'$article_mode' => ['article_mode', $this->t('Article Mode'), $article_mode, $this->t("Controls how posts with titles are transmitted. Mastodon and its forks don't display the content of these posts if the post is created in the correct (default) way."), $article_modes],
'$legacy_contact' => ['legacy_contact', $this->t('Your legacy ActivityPub/GNU Social account'), $legacy_contact, $this->t('If you enter your old account name from an ActivityPub based system or your GNU Social/Statusnet account name here (in the format user@domain.tld), your contacts will be added automatically. The field will be emptied when done.')],
'$connector_settings_forms' => $connector_settings_forms,

View file

@ -0,0 +1,118 @@
<?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\Module\Settings;
use Friendica\App;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues;
use Friendica\Core\Renderer;
use Friendica\Core\Session\Capability\IHandleUserSessions;
use Friendica\Core\Worker;
use Friendica\Module\BaseSettings;
use Friendica\Module\Response;
use Friendica\Navigation\SystemMessages;
use Friendica\Network\HTTPException;
use Friendica\Util\Network;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
/**
* Module to export user data
**/
class ContactImport extends BaseSettings
{
/** @var IManageConfigValues */
private $config;
/** @var IManagePersonalConfigValues */
private $pconfig;
/** @var SystemMessages */
protected $systemMessages;
public function __construct(SystemMessages $systemMessages, IManagePersonalConfigValues $pconfig, IManageConfigValues $config, IHandleUserSessions $session, App\Page $page, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($session, $page, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->config = $config;
$this->pconfig = $pconfig;
$this->systemMessages = $systemMessages;
}
protected function post(array $request = [])
{
if (!$this->session->getLocalUserId()) {
throw new HTTPException\ForbiddenException($this->l10n->t('Permission denied.'));
}
self::checkFormSecurityTokenRedirectOnError($this->args->getQueryString(), 'contactimport');
parent::post();
// Import Contacts from CSV file
if (!empty($request['importcontact-submit'])) {
$this->pconfig->set($this->session->getLocalUserId(), 'ostatus', 'legacy_contact', $request['legacy_contact']);
if (isset($_FILES['importcontact-filename']) && !empty($_FILES['importcontact-filename']['tmp_name'])) {
// was there an error
if ($_FILES['importcontact-filename']['error'] > 0) {
$this->logger->notice('Contact CSV file upload error', ['error' => $_FILES['importcontact-filename']['error']]);
$this->systemMessages->addNotice($this->l10n->t('Contact CSV file upload error'));
} else {
$csvArray = array_map('str_getcsv', file($_FILES['importcontact-filename']['tmp_name']));
$this->logger->notice('Import started', ['lines' => count($csvArray)]);
// import contacts
foreach ($csvArray as $csvRow) {
// The 1st row may, or may not contain the headers of the table
// We expect the 1st field of the row to contain either the URL
// or the handle of the account, therefore we check for either
// "http" or "@" to be present in the string.
// All other fields from the row will be ignored
if ((strpos($csvRow[0], '@') !== false) || Network::isValidHttpUrl($csvRow[0])) {
Worker::add(Worker::PRIORITY_MEDIUM, 'AddContact', $this->session->getLocalUserId(), trim($csvRow[0], '@'));
} else {
$this->logger->notice('Invalid account', ['url' => $csvRow[0]]);
}
}
$this->logger->notice('Import done');
$this->systemMessages->addInfo($this->l10n->t('Importing Contacts done'));
// delete temp file
unlink($_FILES['importcontact-filename']['tmp_name']);
}
} else {
$this->logger->notice('Import triggered, but no import file was found.');
}
}
$this->baseUrl->redirect($this->args->getQueryString());
}
protected function content(array $request = []): string
{
if (!$this->session->getLocalUserId()) {
throw new HTTPException\ForbiddenException($this->l10n->t('Permission denied.'));
}
parent::content();
$legacy_contact = $this->pconfig->get($this->session->getLocalUserId(), 'ostatus', 'legacy_contact');
if (!empty($legacy_contact)) {
$this->baseUrl->redirect('ostatus/subscribe?url=' . urlencode($legacy_contact));
}
$tpl = Renderer::getMarkupTemplate('settings/contactimport.tpl');
return Renderer::replaceMacros($tpl, [
'$title' => $this->l10n->t('Import Contacts'),
'$submit' => $this->l10n->t('Save Settings'),
'$form_security_token' => self::getFormSecurityToken('contactimport'),
'$importcontact_text' => $this->l10n->t('Upload a CSV file that contains the handle of your followed accounts in the first column you exported from the old account.'),
'$importcontact_button' => $this->l10n->t('Upload File'),
'$importcontact_maxsize' => $this->config->get('system', 'max_csv_file_size', 30720),
'$legacy_contact' => ['legacy_contact', $this->t('Your legacy ActivityPub/GNU Social account'), $legacy_contact, $this->t('If you enter your old account name from an ActivityPub based system or your GNU Social/Statusnet account name here (in the format user@domain.tld), your contacts will be added automatically. The field will be emptied when done.')],
]);
}
}

View file

@ -129,7 +129,26 @@ final class ATProtocol
}
$data = $this->get($pds . '/xrpc/' . $url, [HttpClientOptions::HEADERS => $headers]);
$this->pConfig->set($uid, 'bluesky', 'status', is_null($data) ? self::STATUS_API_FAIL : self::STATUS_SUCCESS);
if ($data === null) {
$this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_API_FAIL);
return null;
}
if (!empty($data->code) && ($data->code < 200 || $data->code >= 400)) {
if (!empty($data->message)) {
$this->pConfig->set($uid, 'bluesky', 'status-message', $data->message);
} elseif (!empty($data->code)) {
$this->pConfig->set($uid, 'bluesky', 'status-message', 'Error Code: ' . $data->code);
}
return $data;
}
$this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_SUCCESS);
$this->pConfig->set($uid, 'bluesky', 'status-message', '');
return $data;
}
@ -156,6 +175,9 @@ final class ATProtocol
return null;
}
$data->code = $curlResult->getReturnCode();
} elseif (($curlResult->getReturnCode() < 200) || ($curlResult->getReturnCode() >= 400)) {
$this->logger->notice('Unexpected return code', ['url' => $url, 'code' => $curlResult->getReturnCode(), 'error' => $data ?: $curlResult->getBodyString()]);
$data->code = $curlResult->getReturnCode();
}
Item::incrementInbound(Protocol::BLUESKY);
@ -197,14 +219,17 @@ final class ATProtocol
} catch (\Exception $e) {
$this->logger->notice('Exception on post', ['exception' => $e]);
$this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_API_FAIL);
$this->pConfig->set($uid, 'bluesky', 'status-message', $e->getMessage());
return null;
}
$data = json_decode($curlResult->getBodyString());
$data = json_decode($curlResult->getBodyString(), false);
if (!$curlResult->isSuccess()) {
$this->logger->notice('API Error', ['url' => $url, 'code' => $curlResult->getReturnCode(), 'error' => $data ?: $curlResult->getBodyString()]);
if (!$data) {
$this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_API_FAIL);
return null;
}
$data->code = $curlResult->getReturnCode();
@ -212,8 +237,14 @@ final class ATProtocol
if (!empty($data->code) && ($data->code >= 200) && ($data->code < 400)) {
$this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_SUCCESS);
$this->pConfig->set($uid, 'bluesky', 'status-message', '');
} else {
$this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_API_FAIL);
if (!empty($data->message)) {
$this->pConfig->set($uid, 'bluesky', 'status-message', $data->message);
} elseif (!empty($data->code)) {
$this->pConfig->set($uid, 'bluesky', 'status-message', 'Error Code: ' . $data->code);
}
}
return $data;
}
@ -501,10 +532,6 @@ final class ATProtocol
$data = $this->post($uid, '/xrpc/com.atproto.server.refreshSession', '', ['Authorization' => ['Bearer ' . $token]]);
if (empty($data) || empty($data->accessJwt)) {
$this->logger->debug('Refresh failed', ['return' => $data]);
$password = $this->pConfig->get($uid, 'bluesky', 'password');
if (!empty($password)) {
return $this->createUserToken($uid, $password);
}
$this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_TOKEN_FAIL);
return '';
}
@ -541,6 +568,7 @@ final class ATProtocol
$this->pConfig->set($uid, 'bluesky', 'refresh_token', $data->refreshJwt);
$this->pConfig->set($uid, 'bluesky', 'token_created', time());
$this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_TOKEN_OK);
$this->pConfig->set($uid, 'bluesky', 'status-message', '');
return $data->accessJwt;
}
}

View file

@ -1,5 +1,5 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project

View file

@ -1,5 +1,5 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project

View file

@ -1,5 +1,5 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project
@ -178,7 +178,7 @@ class Processor
if ($id) {
$this->logger->info('Post inserted', ['id' => $id, 'guid' => $item['guid']]);
} elseif (Post::exists(['uid' => $uid, 'uri-id' => $item['uri-id']])) {
$this->logger->warning('Post was found', ['guid' => $item['guid'], 'uri' => $item['uri']]);
$this->logger->notice('Post was found', ['guid' => $item['guid'], 'uri' => $item['uri']]);
} else {
$this->logger->warning('Post was not inserted', ['guid' => $item['guid'], 'uri' => $item['uri']]);
}
@ -208,7 +208,7 @@ class Processor
if ($id) {
$this->logger->info('Repost inserted', ['id' => $id]);
} elseif (Post::exists(['uid' => $uid, 'uri-id' => $item['uri-id']])) {
$this->logger->warning('Repost was found', ['uri' => $item['uri']]);
$this->logger->notice('Repost was found', ['uri' => $item['uri']]);
} else {
$this->logger->warning('Repost was not inserted', ['uri' => $item['uri']]);
}
@ -237,7 +237,7 @@ class Processor
if ($id) {
$this->logger->info('Like inserted', ['id' => $id]);
} elseif (Post::exists(['uid' => $uid, 'uri-id' => $item['uri-id']])) {
$this->logger->warning('Like was found', ['uri' => $item['uri']]);
$this->logger->notice('Like was found', ['uri' => $item['uri']]);
} else {
$this->logger->warning('Like was not inserted', ['uri' => $item['uri']]);
}
@ -317,7 +317,7 @@ class Processor
if ($id) {
$this->logger->info('Fetched post inserted', ['id' => $id, 'guid' => $item['guid']]);
} elseif (Post::exists(['uid' => $uid, 'uri-id' => $item['uri-id']])) {
$this->logger->warning('Fetched post was found', ['guid' => $item['guid'], 'uri' => $item['uri']]);
$this->logger->notice('Fetched post was found', ['guid' => $item['guid'], 'uri' => $item['uri']]);
} else {
$this->logger->warning('Fetched post was not inserted', ['guid' => $item['guid'], 'uri' => $item['uri']]);
}

View file

@ -164,7 +164,7 @@ class Delivery
}
Logger::notice('Delivery failed', ['retcode' => $response->getReturnCode(), 'serverfailure' => $serverfail, 'drop' => $drop, 'runtime' => round($runtime, 3), 'uri-id' => $uri_id, 'uid' => $uid, 'item_id' => $item_id, 'cmd' => $cmd, 'inbox' => $inbox]);
Logger::notice('Delivery failed', ['retcode' => $response->getReturnCode() ?? 0, 'serverfailure' => $serverfail, 'drop' => $drop, 'runtime' => round($runtime, 3), 'uri-id' => $uri_id, 'uid' => $uid, 'item_id' => $item_id, 'cmd' => $cmd, 'inbox' => $inbox]);
}
if ($uri_id) {
if ($success) {

View file

@ -127,7 +127,6 @@ class JsonLD
$messages[] = $currentException->getMessage();
} while ($currentException = $currentException->getPrevious());
Logger::warning('JsonLD normalize error');
Logger::notice('JsonLD normalize error', ['messages' => $messages]);
Logger::info('JsonLD normalize error', ['trace' => $e->getTraceAsString()]);
Logger::debug('JsonLD normalize error', ['jsonobj' => $jsonobj]);

View file

@ -682,4 +682,19 @@ class Network
return (string)Uri::fromParts($parts);
}
/**
* Get base url without a path, fragment or query
*
* @param UriInterface $uri
* @return string baseurl
*/
public static function getBaseUrl(UriInterface $uri): string
{
return $uri
->withUserInfo('')
->withQuery('')
->withFragment('')
->withPath('');
}
}

View file

@ -453,6 +453,10 @@ class Notifier
}
$cdata = Contact::getPublicAndUserContactID($contact['id'], $sender_uid);
if (empty($cdata)) {
Logger::info('No contact entry found', ['id' => $contact['id'], 'uid' => $sender_uid]);
continue;
}
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;
@ -639,8 +643,8 @@ class Notifier
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)) {
Logger::info('Parent network is no federated network, so no AP delivery', ['network' => $parent['network']]);
if (!in_array($parent['network'] ?? '', Protocol::FEDERATED)) {
Logger::info('Parent network is no federated network, so no AP delivery', ['network' => $parent['network'] ?? '']);
return ['count' => 0, 'contacts' => []];
}