diff --git a/src/Model/User.php b/src/Model/User.php index 990df65dbd..fe20857b95 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -99,6 +99,95 @@ class User private static $owner; + /** + * Fetch the system account + * + * @return return system account + */ + public static function getSystemAccount() + { + $system = Contact::selectFirst([], ['self' => true, 'uid' => 0]); + if (!DBA::isResult($system)) { + self::createSystemAccount(); + $system = Contact::selectFirst([], ['self' => true, 'uid' => 0]); + if (!DBA::isResult($system)) { + return []; + } + } + + $system['spubkey'] = $system['uprvkey'] = $system['prvkey']; + $system['username'] = $system['name']; + $system['nickname'] = $system['nick']; + return $system; + } + + /** + * Create the system account + * + * @return void + */ + private static function createSystemAccount() + { + $system_actor_name = DI::config()->get('system', 'actor_name'); + if (empty($system_actor_name)) { + $system_actor_name = self::getActorName(); + if (empty($system_actor_name)) { + return; + } + } + + $keys = Crypto::newKeypair(4096); + if ($keys === false) { + throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.')); + } + + $system = []; + $system['uid'] = 0; + $system['created'] = DateTimeFormat::utcNow(); + $system['self'] = true; + $system['network'] = Protocol::ACTIVITYPUB; + $system['name'] = 'System Account'; + $system['addr'] = $system_actor_name . '@' . DI::baseUrl()->getHostname(); + $system['nick'] = $system_actor_name; + $system['avatar'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO; + $system['photo'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO; + $system['thumb'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_THUMB; + $system['micro'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_MICRO; + $system['url'] = DI::baseUrl() . '/friendica'; + $system['nurl'] = Strings::normaliseLink($system['url']); + $system['pubkey'] = $keys['pubkey']; + $system['prvkey'] = $keys['prvkey']; + $system['blocked'] = 0; + $system['pending'] = 0; + $system['contact-type'] = Contact::TYPE_RELAY; // In AP this is translated to 'Application' + $system['name-date'] = DateTimeFormat::utcNow(); + $system['uri-date'] = DateTimeFormat::utcNow(); + $system['avatar-date'] = DateTimeFormat::utcNow(); + $system['closeness'] = 0; + $system['baseurl'] = DI::baseUrl(); + $system['gsid'] = GServer::getID($system['baseurl']); + DBA::insert('contact', $system); + } + + /** + * Detect a usable actor name + * + * @return string actor account name + */ + public static function getActorName() + { + // List of possible actor names + $possible_accounts = ['friendica', 'actor', 'system', 'internal']; + foreach ($possible_accounts as $name) { + if (!DBA::exists('user', ['nickname' => $name, 'account_removed' => false, 'expire']) && + !DBA::exists('userd', ['username' => $name])) { + DI::config()->set('system', 'actor_name', $name); + return $name; + } + } + return ''; + } + /** * Returns true if a user record exists with the provided id * @@ -588,15 +677,24 @@ class User public static function isNicknameBlocked($nickname) { $forbidden_nicknames = DI::config()->get('system', 'forbidden_nicknames', ''); + if (!empty($forbidden_nicknames)) { + // check if the nickname is in the list of blocked nicknames + $forbidden = explode(',', $forbidden_nicknames); + $forbidden = array_map('trim', $forbidden); + } else { + $forbidden = []; + } - // if the config variable is empty return false - if (empty($forbidden_nicknames)) { + // Add the name of the internal actor to the "forbidden" list + $actor_name = DI::config()->get('system', 'actor_name'); + if (!empty($actor_name)) { + $forbidden[] = $actor_name; + } + + if (empty($forbidden)) { return false; } - // check if the nickname is in the list of blocked nicknames - $forbidden = explode(',', $forbidden_nicknames); - $forbidden = array_map('trim', $forbidden); if (in_array(strtolower($nickname), $forbidden)) { return true; } diff --git a/src/Module/Admin/Site.php b/src/Module/Admin/Site.php index 7cc17fa036..7033f09b9c 100644 --- a/src/Module/Admin/Site.php +++ b/src/Module/Admin/Site.php @@ -29,6 +29,7 @@ use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; +use Friendica\Model\User; use Friendica\Module\BaseAdmin; use Friendica\Module\Register; use Friendica\Util\BasePath; @@ -148,6 +149,7 @@ class Site extends BaseAdmin $allowed_sites = (!empty($_POST['allowed_sites']) ? Strings::escapeTags(trim($_POST['allowed_sites'])) : ''); $allowed_email = (!empty($_POST['allowed_email']) ? Strings::escapeTags(trim($_POST['allowed_email'])) : ''); $forbidden_nicknames = (!empty($_POST['forbidden_nicknames']) ? strtolower(Strings::escapeTags(trim($_POST['forbidden_nicknames']))) : ''); + $system_actor_name = (!empty($_POST['system_actor_name']) ? Strings::escapeTags(trim($_POST['system_actor_name'])) : ''); $no_oembed_rich_content = !empty($_POST['no_oembed_rich_content']); $allowed_oembed = (!empty($_POST['allowed_oembed']) ? Strings::escapeTags(trim($_POST['allowed_oembed'])) : ''); $block_public = !empty($_POST['block_public']); @@ -355,6 +357,7 @@ class Site extends BaseAdmin DI::config()->set('system', 'allowed_sites' , $allowed_sites); DI::config()->set('system', 'allowed_email' , $allowed_email); DI::config()->set('system', 'forbidden_nicknames' , $forbidden_nicknames); + DI::config()->set('system', 'system_actor_name' , $system_actor_name); DI::config()->set('system', 'no_oembed_rich_content' , $no_oembed_rich_content); DI::config()->set('system', 'allowed_oembed' , $allowed_oembed); DI::config()->set('system', 'block_public' , $block_public); @@ -510,6 +513,11 @@ class Site extends BaseAdmin get_temppath(); get_itemcachepath(); + $system_actor_name = DI::config()->get('system', 'actor_name'); + if (empty($system_actor_name)) { + $system_actor_name = User::getActorName(); + } + /* Register policy */ $register_choices = [ Register::CLOSED => DI::l10n()->t('Closed'), @@ -600,6 +608,7 @@ class Site extends BaseAdmin // name, label, value, help string, extra data... '$sitename' => ['sitename', DI::l10n()->t('Site name'), DI::config()->get('config', 'sitename'), ''], '$sender_email' => ['sender_email', DI::l10n()->t('Sender Email'), DI::config()->get('config', 'sender_email'), DI::l10n()->t('The email address your server shall use to send notification emails from.'), '', '', 'email'], + '$system_actor_name' => ['system_actor_name', DI::l10n()->t('Name of the system actor'), $system_actor_name, DI::l10n()->t("Name of the internal system account that is used to perform ActivityPub requests. This must be an unused username. If set, this shouldn't be changed again.")], '$banner' => ['banner', DI::l10n()->t('Banner/Logo'), $banner, ''], '$email_banner' => ['email_banner', DI::l10n()->t('Email Banner/Logo'), $email_banner, ''], '$shortcut_icon' => ['shortcut_icon', DI::l10n()->t('Shortcut icon'), DI::config()->get('system', 'shortcut_icon'), DI::l10n()->t('Link to an icon that will be used for browsers.')], diff --git a/src/Module/Friendica.php b/src/Module/Friendica.php index ea693ce275..2d76f80d04 100644 --- a/src/Module/Friendica.php +++ b/src/Module/Friendica.php @@ -25,8 +25,10 @@ use Friendica\BaseModule; use Friendica\Core\Addon; use Friendica\Core\Hook; use Friendica\Core\Renderer; +use Friendica\Core\System; use Friendica\DI; use Friendica\Model\User; +use Friendica\Protocol\ActivityPub; /** * Prints information about the current node @@ -108,6 +110,15 @@ class Friendica extends BaseModule public static function rawContent(array $parameters = []) { + if (ActivityPub::isRequest()) { + $data = ActivityPub\Transmitter::getProfile(0); + if (!empty($data)) { + header('Access-Control-Allow-Origin: *'); + header('Cache-Control: max-age=23200, stale-while-revalidate=23200'); + System::jsonExit($data, 'application/activity+json'); + } + } + $app = DI::app(); // @TODO: Replace with parameter from router diff --git a/src/Module/Xrd.php b/src/Module/Xrd.php index 249c143ffb..87adef5cfc 100644 --- a/src/Module/Xrd.php +++ b/src/Module/Xrd.php @@ -24,6 +24,7 @@ namespace Friendica\Module; use Friendica\BaseModule; use Friendica\Core\Hook; use Friendica\Core\Renderer; +use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Photo; @@ -77,24 +78,30 @@ class Xrd extends BaseModule $name = substr($local, 0, strpos($local, '@')); } - $user = User::getByNickname($name); + if ($name == DI::config()->get('system', 'actor_name')) { + $owner = User::getSystemAccount(); + if (empty($owner)) { + throw new \Friendica\Network\HTTPException\NotFoundException(); + } + self::printSystemJSON($owner); + } else { + $user = User::getByNickname($name); + if (empty($user)) { + throw new \Friendica\Network\HTTPException\NotFoundException(); + } - if (empty($user)) { - throw new \Friendica\Network\HTTPException\NotFoundException(); + $owner = User::getOwnerDataById($user['uid']); + if (empty($owner)) { + DI::logger()->warning('No owner data for user id', ['uri' => $uri, 'name' => $name, 'user' => $user]); + throw new \Friendica\Network\HTTPException\NotFoundException(); + } + + $alias = str_replace('/profile/', '/~', $owner['url']); + + $avatar = Photo::selectFirst(['type'], ['uid' => $owner['uid'], 'profile' => true]); } - $owner = User::getOwnerDataById($user['uid']); - - if (empty($owner)) { - DI::logger()->warning('No owner data for user id', ['uri' => $uri, 'name' => $name, 'user' => $user]); - throw new \Friendica\Network\HTTPException\NotFoundException(); - } - - $alias = str_replace('/profile/', '/~', $owner['url']); - - $avatar = Photo::selectFirst(['type'], ['uid' => $owner['uid'], 'profile' => true]); - - if (!DBA::isResult($avatar)) { + if (empty($avatar)) { $avatar = ['type' => 'image/jpeg']; } @@ -105,6 +112,32 @@ class Xrd extends BaseModule } } + private static function printSystemJSON(array $owner) + { + $json = [ + 'subject' => 'acct:' . $owner['addr'], + 'aliases' => [$owner['url']], + 'links' => [ + [ + 'rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => $owner['url'], + ], + [ + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => $owner['url'], + ], + [ + 'rel' => 'http://ostatus.org/schema/1.0/subscribe', + 'template' => DI::baseUrl()->get() . '/follow?url={uri}', + ], + ] + ]; + header('Access-Control-Allow-Origin: *'); + System::jsonExit($json, 'application/jrd+json; charset=utf-8'); + } + private static function printJSON($alias, $baseURL, $owner, $avatar) { $salmon_key = Salmon::salmonKey($owner['spubkey']); diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index c04b9e592d..359d361b01 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -90,20 +90,6 @@ class ActivityPub */ public static function fetchContent(string $url, int $uid = 0) { - if (empty($uid)) { - $user = User::getFirstAdmin(['uid']); - - if (empty($user['uid'])) { - // When the system setup is missing an admin we just take the first user - $condition = ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]; - $user = DBA::selectFirst('user', ['uid'], $condition); - } - - if (!empty($user['uid'])) { - $uid = $user['uid']; - } - } - return HTTPSignature::fetch($url, $uid); } diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index f1b2d3acc7..259c8c2251 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -214,39 +214,63 @@ class Transmitter */ public static function getProfile($uid) { - $condition = ['uid' => $uid, 'blocked' => false, 'account_expired' => false, - 'account_removed' => false, 'verified' => true]; - $fields = ['guid', 'nickname', 'pubkey', 'account-type', 'page-flags']; - $user = DBA::selectFirst('user', $fields, $condition); - if (!DBA::isResult($user)) { - return []; - } + if ($uid != 0) { + $condition = ['uid' => $uid, 'blocked' => false, 'account_expired' => false, + 'account_removed' => false, 'verified' => true]; + $fields = ['guid', 'nickname', 'pubkey', 'account-type', 'page-flags']; + $user = DBA::selectFirst('user', $fields, $condition); + if (!DBA::isResult($user)) { + return []; + } - $fields = ['locality', 'region', 'country-name']; - $profile = DBA::selectFirst('profile', $fields, ['uid' => $uid]); - if (!DBA::isResult($profile)) { - return []; - } + $fields = ['locality', 'region', 'country-name']; + $profile = DBA::selectFirst('profile', $fields, ['uid' => $uid]); + if (!DBA::isResult($profile)) { + return []; + } - $fields = ['name', 'url', 'location', 'about', 'avatar', 'photo']; - $contact = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]); - if (!DBA::isResult($contact)) { - return []; + $fields = ['name', 'url', 'location', 'about', 'avatar', 'photo']; + $contact = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]); + if (!DBA::isResult($contact)) { + return []; + } + } else { + $contact = User::getSystemAccount(); + $user = ['guid' => '', 'nickname' => $contact['nick'], 'pubkey' => $contact['pubkey'], + 'account-type' => $contact['contact-type'], 'page-flags' => User::PAGE_FLAGS_NORMAL]; + $profile = ['locality' => '', 'region' => '', 'country-name' => '']; } $data = ['@context' => ActivityPub::CONTEXT]; $data['id'] = $contact['url']; - $data['diaspora:guid'] = $user['guid']; + + if (!empty($user['guid'])) { + $data['diaspora:guid'] = $user['guid']; + } + $data['type'] = ActivityPub::ACCOUNT_TYPES[$user['account-type']]; - $data['following'] = DI::baseUrl() . '/following/' . $user['nickname']; - $data['followers'] = DI::baseUrl() . '/followers/' . $user['nickname']; - $data['inbox'] = DI::baseUrl() . '/inbox/' . $user['nickname']; - $data['outbox'] = DI::baseUrl() . '/outbox/' . $user['nickname']; + + if ($uid != 0) { + $data['following'] = DI::baseUrl() . '/following/' . $user['nickname']; + $data['followers'] = DI::baseUrl() . '/followers/' . $user['nickname']; + $data['inbox'] = DI::baseUrl() . '/inbox/' . $user['nickname']; + $data['outbox'] = DI::baseUrl() . '/outbox/' . $user['nickname']; + } else { + $data['inbox'] = DI::baseUrl() . '/friendica/inbox'; + } + $data['preferredUsername'] = $user['nickname']; $data['name'] = $contact['name']; - $data['vcard:hasAddress'] = ['@type' => 'vcard:Home', 'vcard:country-name' => $profile['country-name'], - 'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']]; - $data['summary'] = BBCode::convert($contact['about'], false); + + if (!empty($profile['country-name'] . $profile['region'] . $profile['locality'])) { + $data['vcard:hasAddress'] = ['@type' => 'vcard:Home', 'vcard:country-name' => $profile['country-name'], + 'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']]; + } + + if (!empty($contact['about'])) { + $data['summary'] = BBCode::convert($contact['about'], false); + } + $data['url'] = $contact['url']; $data['manuallyApprovesFollowers'] = in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]); $data['publicKey'] = ['id' => $contact['url'] . '#main-key', @@ -652,6 +676,10 @@ class Transmitter $item_profile = APContact::getByURL($item['owner-link'], false); } + if (empty($item_profile)) { + return []; + } + $profile_uid = User::getIdForURL($item_profile['url']); foreach (['to', 'cc', 'bto', 'bcc'] as $element) { diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index bbd5cd9b15..5165f600fc 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -420,23 +420,28 @@ class HTTPSignature if (!$owner) { return; } - - if (!empty($owner['uprvkey'])) { - // Header data that is about to be signed. - $host = parse_url($request, PHP_URL_HOST); - $path = parse_url($request, PHP_URL_PATH); - $date = DateTimeFormat::utcNow(DateTimeFormat::HTTP); - - $headers = ['Date: ' . $date, 'Host: ' . $host]; - - $signed_data = "(request-target): get " . $path . "\ndate: ". $date . "\nhost: " . $host; - - $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); - - $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) date host",signature="' . $signature . '"'; + } else { + $owner = User::getSystemAccount(); + if (!$owner) { + return; } } + if (!empty($owner['uprvkey'])) { + // Header data that is about to be signed. + $host = parse_url($request, PHP_URL_HOST); + $path = parse_url($request, PHP_URL_PATH); + $date = DateTimeFormat::utcNow(DateTimeFormat::HTTP); + + $headers = ['Date: ' . $date, 'Host: ' . $host]; + + $signed_data = "(request-target): get " . $path . "\ndate: ". $date . "\nhost: " . $host; + + $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); + + $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) date host",signature="' . $signature . '"'; + } + if (!empty($opts['accept_content'])) { $headers[] = 'Accept: ' . $opts['accept_content']; } diff --git a/view/templates/admin/site.tpl b/view/templates/admin/site.tpl index 5c63970e9f..0c09741343 100644 --- a/view/templates/admin/site.tpl +++ b/view/templates/admin/site.tpl @@ -14,6 +14,7 @@ {{include file="field_input.tpl" field=$sitename}} {{include file="field_input.tpl" field=$sender_email}} + {{include file="field_input.tpl" field=$system_actor_name}} {{include file="field_textarea.tpl" field=$banner}} {{include file="field_input.tpl" field=$email_banner}} {{include file="field_input.tpl" field=$shortcut_icon}} diff --git a/view/theme/frio/templates/admin/site.tpl b/view/theme/frio/templates/admin/site.tpl index 0136b1cd44..a60cb06b63 100644 --- a/view/theme/frio/templates/admin/site.tpl +++ b/view/theme/frio/templates/admin/site.tpl @@ -41,6 +41,7 @@
{{include file="field_input.tpl" field=$sitename}} {{include file="field_input.tpl" field=$sender_email}} + {{include file="field_input.tpl" field=$system_actor_name}} {{include file="field_textarea.tpl" field=$banner}} {{include file="field_input.tpl" field=$shortcut_icon}} {{include file="field_input.tpl" field=$touch_icon}}