From eb6b03b555ebfe8145ab826336f0d24aa7fbe46d Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 10 Nov 2022 22:28:33 -0500 Subject: [PATCH] Add new OStatus\PortableContacts module class - Retain existing route /poco for backward compatibility - Remove unsupported links to /poco/{nickname} route --- src/Model/Contact.php | 2 - src/Module/Profile/Profile.php | 1 - src/Module/User/PortableContacts.php | 277 +++++++++++++++++++++++++++ src/Module/Xrd.php | 24 +-- static/routes.config.php | 7 +- 5 files changed, 288 insertions(+), 23 deletions(-) create mode 100644 src/Module/User/PortableContacts.php diff --git a/src/Model/Contact.php b/src/Model/Contact.php index ee74cb157..8239708e1 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -729,7 +729,6 @@ class Contact 'notify' => DI::baseUrl() . '/dfrn_notify/' . $user['nickname'], 'poll' => DI::baseUrl() . '/dfrn_poll/' . $user['nickname'], 'confirm' => DI::baseUrl() . '/dfrn_confirm/' . $user['nickname'], - 'poco' => DI::baseUrl() . '/poco/' . $user['nickname'], 'name-date' => DateTimeFormat::utcNow(), 'uri-date' => DateTimeFormat::utcNow(), 'avatar-date' => DateTimeFormat::utcNow(), @@ -811,7 +810,6 @@ class Contact 'notify' => DI::baseUrl() . '/dfrn_notify/' . $user['nickname'], 'poll' => DI::baseUrl() . '/dfrn_poll/'. $user['nickname'], 'confirm' => DI::baseUrl() . '/dfrn_confirm/' . $user['nickname'], - 'poco' => DI::baseUrl() . '/poco/' . $user['nickname'], ]; diff --git a/src/Module/Profile/Profile.php b/src/Module/Profile/Profile.php index a60266093..07db08259 100644 --- a/src/Module/Profile/Profile.php +++ b/src/Module/Profile/Profile.php @@ -334,7 +334,6 @@ class Profile extends BaseProfile foreach ($dfrn_pages as $dfrn) { $htmlhead .= '' . "\n"; } - $htmlhead .= '' . "\n"; return $htmlhead; } diff --git a/src/Module/User/PortableContacts.php b/src/Module/User/PortableContacts.php new file mode 100644 index 000000000..662999481 --- /dev/null +++ b/src/Module/User/PortableContacts.php @@ -0,0 +1,277 @@ +. + * + * @see https://web.archive.org/web/20160405005550/http://portablecontacts.net/draft-spec.html + */ + +namespace Friendica\Module\User; + +use Friendica\App; +use Friendica\BaseModule; +use Friendica\Content\Text\BBCode; +use Friendica\Core\Cache\Capability\ICanCache; +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\L10n; +use Friendica\Core\Protocol; +use Friendica\Core\System; +use Friendica\Database\Database; +use Friendica\Module\Response; +use Friendica\Network\HTTPException; +use Friendica\Util\DateTimeFormat; +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; + +/** + * Minimal implementation of the Portable Contacts protocol + * @see https://portablecontacts.github.io + */ +class PortableContacts extends BaseModule +{ + /** @var IManageConfigValues */ + private $config; + /** @var Database */ + private $database; + /** @var ICanCache */ + private $cache; + + public function __construct(ICanCache $cache, Database $database, 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->database = $database; + $this->cache = $cache; + } + + protected function rawContent(array $request = []) + { + if ($this->config->get('system', 'block_public') || $this->config->get('system', 'block_local_dir')) { + throw new HTTPException\ForbiddenException(); + } + + $format = $request['format'] ?? 'json'; + if ($format !== 'json') { + throw new HTTPException\UnsupportedMediaTypeException(); + } + + $totalResults = $this->database->count('profile', ['net-publish' => true]); + if (!$totalResults) { + throw new HTTPException\ForbiddenException(); + } + + if (!empty($request['startIndex']) && is_numeric($request['startIndex'])) { + $startIndex = intval($request['startIndex']); + } else { + $startIndex = 0; + } + + $itemsPerPage = !empty($request['count']) && is_numeric($request['count']) ? intval($request['count']) : $totalResults; + + $this->logger->info('Start system mode query'); + $contacts = $this->database->selectToArray('owner-view', [], ['net-publish' => true], ['limit' => [$startIndex, $itemsPerPage]]); + $this->logger->info('Query done'); + + $return = []; + if (!empty($request['sorted'])) { + $return['sorted'] = false; + } + + if (!empty($request['filtered'])) { + $return['filtered'] = false; + } + + if (!empty($request['updatedSince'])) { + $return['updatedSince'] = false; + } + + $return['startIndex'] = $startIndex; + $return['itemsPerPage'] = $itemsPerPage; + $return['totalResults'] = $totalResults; + + $return['entry'] = []; + + $selectedFields = [ + 'id' => false, + 'displayName' => false, + 'urls' => false, + 'updated' => false, + 'preferredUsername' => false, + 'photos' => false, + 'aboutMe' => false, + 'currentLocation' => false, + 'network' => false, + 'tags' => false, + 'address' => false, + 'contactType' => false, + 'generation' => false + ]; + + if (empty($request['fields']) || $request['fields'] == '@all') { + foreach ($selectedFields as $k => $v) { + $selectedFields[$k] = true; + } + } else { + $fields_req = explode(',', $request['fields']); + foreach ($fields_req as $f) { + $selectedFields[trim($f)] = true; + } + } + + if (!$contacts) { + $return['entry'][] = []; + } + + foreach ($contacts as $contact) { + if (!isset($contact['updated'])) { + $contact['updated'] = ''; + } + + if (!isset($contact['generation'])) { + $contact['generation'] = 1; + } + + if (empty($contact['keywords']) && isset($contact['pub_keywords'])) { + $contact['keywords'] = $contact['pub_keywords']; + } + + if (isset($contact['account-type'])) { + $contact['contact-type'] = $contact['account-type']; + } + + $cacheKey = 'about:' . $contact['nick'] . ':' . DateTimeFormat::utc($contact['updated'], DateTimeFormat::ATOM); + $about = $this->cache->get($cacheKey); + if (is_null($about)) { + $about = BBCode::convertForUriId($contact['uri-id'], $contact['about']); + $this->cache->set($cacheKey, $about); + } + + // Non connected persons can only see the keywords of a Diaspora account + if ($contact['network'] == Protocol::DIASPORA) { + $contact['location'] = ''; + $about = ''; + } + + $entry = []; + if ($selectedFields['id']) { + $entry['id'] = (int)$contact['id']; + } + + if ($selectedFields['displayName']) { + $entry['displayName'] = $contact['name']; + } + + if ($selectedFields['aboutMe']) { + $entry['aboutMe'] = $about; + } + + if ($selectedFields['currentLocation']) { + $entry['currentLocation'] = $contact['location']; + } + + if ($selectedFields['generation']) { + $entry['generation'] = (int)$contact['generation']; + } + + if ($selectedFields['urls']) { + $entry['urls'] = [['value' => $contact['url'], 'type' => 'profile']]; + if ($contact['addr'] && ($contact['network'] !== Protocol::MAIL)) { + $entry['urls'][] = ['value' => 'acct:' . $contact['addr'], 'type' => 'webfinger']; + } + } + + if ($selectedFields['preferredUsername']) { + $entry['preferredUsername'] = $contact['nick']; + } + + if ($selectedFields['updated']) { + $entry['updated'] = $contact['success_update']; + + if ($contact['name-date'] > $entry['updated']) { + $entry['updated'] = $contact['name-date']; + } + + if ($contact['uri-date'] > $entry['updated']) { + $entry['updated'] = $contact['uri-date']; + } + + if ($contact['avatar-date'] > $entry['updated']) { + $entry['updated'] = $contact['avatar-date']; + } + + $entry['updated'] = date('c', strtotime($entry['updated'])); + } + + if ($selectedFields['photos']) { + $entry['photos'] = [['value' => $contact['photo'], 'type' => 'profile']]; + } + + if ($selectedFields['network']) { + $entry['network'] = $contact['network']; + if ($entry['network'] == Protocol::STATUSNET) { + $entry['network'] = Protocol::OSTATUS; + } + + if (($entry['network'] == '') && ($contact['self'])) { + $entry['network'] = Protocol::DFRN; + } + } + + if ($selectedFields['tags']) { + $tags = str_replace(',', ' ', $contact['keywords']); + $tags = explode(' ', $tags); + + $cleaned = []; + foreach ($tags as $tag) { + $tag = trim(strtolower($tag)); + if ($tag != '') { + $cleaned[] = $tag; + } + } + + $entry['tags'] = [$cleaned]; + } + + if ($selectedFields['address']) { + $entry['address'] = []; + + if (isset($contact['locality'])) { + $entry['address']['locality'] = $contact['locality']; + } + + if (isset($contact['region'])) { + $entry['address']['region'] = $contact['region']; + } + + if (isset($contact['country'])) { + $entry['address']['country'] = $contact['country']; + } + } + + if ($selectedFields['contactType']) { + $entry['contactType'] = intval($contact['contact-type']); + } + + $return['entry'][] = $entry; + } + + $this->logger->info('End of poco'); + + System::jsonExit($return); + } +} diff --git a/src/Module/Xrd.php b/src/Module/Xrd.php index 4e4603fbd..29641482f 100644 --- a/src/Module/Xrd.php +++ b/src/Module/Xrd.php @@ -184,10 +184,6 @@ class Xrd extends BaseModule 'type' => 'text/html', 'href' => $baseURL . '/hcard/' . $owner['nickname'], ], - [ - 'rel' => ActivityNamespace::POCO, - 'href' => $owner['poco'], - ], [ 'rel' => 'http://webfinger.net/rel/avatar', 'type' => $avatar['type'], @@ -272,56 +268,50 @@ class Xrd extends BaseModule ] ], '5:link' => [ - '@attributes' => [ - 'rel' => 'http://portablecontacts.net/spec/1.0', - 'href' => $owner['poco'] - ] - ], - '6:link' => [ '@attributes' => [ 'rel' => 'http://webfinger.net/rel/avatar', 'type' => $avatar['type'], 'href' => User::getAvatarUrl($owner) ] ], - '7:link' => [ + '6:link' => [ '@attributes' => [ 'rel' => 'http://joindiaspora.com/seed_location', 'type' => 'text/html', 'href' => $baseURL ] ], - '8:link' => [ + '7:link' => [ '@attributes' => [ 'rel' => 'salmon', 'href' => $baseURL . '/salmon/' . $owner['nickname'] ] ], - '9:link' => [ + '8:link' => [ '@attributes' => [ 'rel' => 'http://salmon-protocol.org/ns/salmon-replies', 'href' => $baseURL . '/salmon/' . $owner['nickname'] ] ], - '10:link' => [ + '9:link' => [ '@attributes' => [ 'rel' => 'http://salmon-protocol.org/ns/salmon-mention', 'href' => $baseURL . '/salmon/' . $owner['nickname'] . '/mention' ] ], - '11:link' => [ + '10:link' => [ '@attributes' => [ 'rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => $baseURL . '/contact/follow?url={uri}' ] ], - '12:link' => [ + '11:link' => [ '@attributes' => [ 'rel' => 'magic-public-key', 'href' => 'data:application/magic-public-key,' . Salmon::salmonKey($owner['spubkey']) ] ], - '13:link' => [ + '12:link' => [ '@attributes' => [ 'rel' => 'http://purl.org/openwebauth/v1', 'type' => 'application/x-zot+json', diff --git a/static/routes.config.php b/static/routes.config.php index 0e9ce965e..927b01840 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -569,9 +569,10 @@ return [ '/{sub1}/{sub2}/{url}' => [Module\Proxy::class, [R::GET]], ], - '/pubsub/{nickname}/{cid:\d+}' => [Module\OStatus\PubSub::class, [R::GET, R::POST]], - '/pubsubhubbub/{nickname}' => [Module\OStatus\PubSubHubBub::class, [ R::POST]], - '/salmon/{nickname}' => [Module\OStatus\Salmon::class, [ R::POST]], + '/poco' => [Module\User\PortableContacts::class, [R::GET ]], + '/pubsub/{nickname}/{cid:\d+}' => [Module\OStatus\PubSub::class, [R::GET, R::POST]], + '/pubsubhubbub/{nickname}' => [Module\OStatus\PubSubHubBub::class, [ R::POST]], + '/salmon/{nickname}' => [Module\OStatus\Salmon::class, [ R::POST]], '/search' => [ '[/]' => [Module\Search\Index::class, [R::GET ]],