diff --git a/mod/photos.php b/mod/photos.php index ccf0525cb0..95627b6da4 100644 --- a/mod/photos.php +++ b/mod/photos.php @@ -1408,7 +1408,7 @@ function photos_content(App $a) $author = ['uid' => 0, 'id' => $item['author-id'], 'network' => $item['author-network'], 'url' => $item['author-link']]; $profile_url = Contact::magicLinkByContact($author); - if (strpos($profile_url, 'redir/') === 0) { + if (strpos($profile_url, 'contact/redir/') === 0) { $sparkle = ' sparkle'; } else { $sparkle = ''; diff --git a/mod/redir.php b/mod/redir.php index 903c6b2a35..5f5c97ae5d 100644 --- a/mod/redir.php +++ b/mod/redir.php @@ -29,149 +29,3 @@ use Friendica\Model\Profile; use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Util\Strings; - -function redir_init(App $a) { - if (!DI::userSession()->isAuthenticated()) { - throw new \Friendica\Network\HTTPException\ForbiddenException(DI::l10n()->t('Access denied.')); - } - - $url = $_GET['url'] ?? ''; - - if (DI::args()->getArgc() > 1 && intval(DI::args()->getArgv()[1])) { - $cid = intval(DI::args()->getArgv()[1]); - } else { - $cid = 0; - } - - // Try magic auth before the legacy stuff - redir_magic($a, $cid, $url); - - if (empty($cid)) { - throw new \Friendica\Network\HTTPException\BadRequestException(DI::l10n()->t('Bad Request.')); - } - - $fields = ['id', 'uid', 'nurl', 'url', 'addr', 'name']; - $contact = DBA::selectFirst('contact', $fields, ['id' => $cid, 'uid' => [0, DI::userSession()->getLocalUserId()]]); - if (!DBA::isResult($contact)) { - throw new \Friendica\Network\HTTPException\NotFoundException(DI::l10n()->t('Contact not found.')); - } - - $contact_url = $contact['url']; - - if (!empty($a->getContactId()) && $a->getContactId() == $cid) { - // Local user is already authenticated. - redir_check_url($contact_url, $url); - $a->redirect($url ?: $contact_url); - } - - if ($contact['uid'] == 0 && DI::userSession()->getLocalUserId()) { - // Let's have a look if there is an established connection - // between the public contact we have found and the local user. - $contact = DBA::selectFirst('contact', $fields, ['nurl' => $contact['nurl'], 'uid' => DI::userSession()->getLocalUserId()]); - - if (DBA::isResult($contact)) { - $cid = $contact['id']; - } - - if (!empty($a->getContactId()) && $a->getContactId() == $cid) { - // Local user is already authenticated. - redir_check_url($contact_url, $url); - $target_url = $url ?: $contact_url; - Logger::info($contact['name'] . " is already authenticated. Redirecting to " . $target_url); - $a->redirect($target_url); - } - } - - if (DI::userSession()->getRemoteUserId()) { - $host = substr(DI::baseUrl()->getUrlPath() . (DI::baseUrl()->getUrlPath() ? '/' . DI::baseUrl()->getUrlPath() : ''), strpos(DI::baseUrl()->getUrlPath(), '://') + 3); - $remotehost = substr($contact['addr'], strpos($contact['addr'], '@') + 1); - - // On a local instance we have to check if the local user has already authenticated - // with the local contact. Otherwise the local user would ask the local contact - // for authentification everytime he/she is visiting a profile page of the local - // contact. - if (($host == $remotehost) && (DI::userSession()->getRemoteContactID(DI::session()->get('visitor_visiting')) == DI::session()->get('visitor_id'))) { - // Remote user is already authenticated. - redir_check_url($contact_url, $url); - $target_url = $url ?: $contact_url; - Logger::info($contact['name'] . " is already authenticated. Redirecting to " . $target_url); - $a->redirect($target_url); - } - } - - if (empty($url)) { - throw new \Friendica\Network\HTTPException\BadRequestException(DI::l10n()->t('Bad Request.')); - } - - // If we don't have a connected contact, redirect with - // the 'zrl' parameter. - $my_profile = Profile::getMyURL(); - - if (!empty($my_profile) && !Strings::compareLink($my_profile, $url)) { - $separator = strpos($url, '?') ? '&' : '?'; - - $url .= $separator . 'zrl=' . urlencode($my_profile); - } - - Logger::info('redirecting to ' . $url); - $a->redirect($url); -} - -function redir_magic($a, $cid, $url) -{ - $visitor = Profile::getMyURL(); - if (!empty($visitor)) { - Logger::info('Got my url', ['visitor' => $visitor]); - } - - $contact = DBA::selectFirst('contact', ['url'], ['id' => $cid]); - if (!DBA::isResult($contact)) { - Logger::info('Contact not found', ['id' => $cid]); - throw new \Friendica\Network\HTTPException\NotFoundException(DI::l10n()->t('Contact not found.')); - } else { - $contact_url = $contact['url']; - redir_check_url($contact_url, $url); - $target_url = $url ?: $contact_url; - } - - $basepath = Contact::getBasepath($contact_url); - - // We don't use magic auth when there is no visitor, we are on the same system or we visit our own stuff - if (empty($visitor) || Strings::compareLink($basepath, DI::baseUrl()) || Strings::compareLink($contact_url, $visitor)) { - Logger::info('Redirecting without magic', ['target' => $target_url, 'visitor' => $visitor, 'contact' => $contact_url]); - DI::app()->redirect($target_url); - } - - // Test for magic auth on the target system - $serverret = DI::httpClient()->head($basepath . '/magic', [HttpClientOptions::ACCEPT_CONTENT => HttpClientAccept::HTML]); - if ($serverret->isSuccess()) { - $separator = strpos($target_url, '?') ? '&' : '?'; - $target_url .= $separator . 'zrl=' . urlencode($visitor) . '&addr=' . urlencode($contact_url); - - Logger::info('Redirecting with magic', ['target' => $target_url, 'visitor' => $visitor, 'contact' => $contact_url]); - System::externalRedirect($target_url); - } else { - Logger::info('No magic for contact', ['contact' => $contact_url]); - } -} - -function redir_check_url(string $contact_url, string $url) -{ - if (empty($contact_url) || empty($url)) { - return; - } - - $url_host = parse_url($url, PHP_URL_HOST); - if (empty($url_host)) { - $url_host = parse_url(DI::baseUrl(), PHP_URL_HOST); - } - - $contact_url_host = parse_url($contact_url, PHP_URL_HOST); - - if ($url_host == $contact_url_host) { - return; - } - - Logger::error('URL check host mismatch', ['contact' => $contact_url, 'url' => $url]); - throw new \Friendica\Network\HTTPException\ForbiddenException(DI::l10n()->t('Access denied.')); -} diff --git a/src/Content/Conversation.php b/src/Content/Conversation.php index 7773cdd508..522de7b573 100644 --- a/src/Content/Conversation.php +++ b/src/Content/Conversation.php @@ -146,7 +146,7 @@ class Conversation 'url' => $activity['author-link'] ]; $url = Contact::magicLinkByContact($author); - if (strpos($url, 'redir/') === 0) { + if (strpos($url, 'contact/redir/') === 0) { $sparkle = ' class="sparkle" '; } @@ -612,7 +612,7 @@ class Conversation $profile_link = Contact::magicLinkByContact($author); $sparkle = ''; - if (strpos($profile_link, 'redir/') === 0) { + if (strpos($profile_link, 'contact/redir/') === 0) { $sparkle = ' sparkle'; } diff --git a/src/Content/Item.php b/src/Content/Item.php index 5321cf1464..0d4c3c180b 100644 --- a/src/Content/Item.php +++ b/src/Content/Item.php @@ -345,7 +345,7 @@ class Item 'url' => $item['author-link'], ]; $profile_link = Contact::magicLinkByContact($author, $item['author-link']); - $sparkle = (strpos($profile_link, 'redir/') === 0); + $sparkle = (strpos($profile_link, 'contact/redir/') === 0); $cid = 0; $pcid = $item['author-id']; diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index 9a13e3f097..69389cc5ff 100644 --- a/src/Content/Text/BBCode.php +++ b/src/Content/Text/BBCode.php @@ -2090,7 +2090,7 @@ class BBCode $text = preg_replace('/\<([^>]*?)(src|href)=(.*?)\&\;(.*?)\>/ism', '<$1$2=$3&$4>', $text); // sanitizes src attributes (http and redir URLs for displaying in a web page, cid used for inline images in emails) - $allowed_src_protocols = ['//', 'http://', 'https://', 'redir/', 'cid:']; + $allowed_src_protocols = ['//', 'http://', 'https://', 'contact/redir/', 'cid:']; array_walk($allowed_src_protocols, function(&$value) { $value = preg_quote($value, '#');}); @@ -2105,7 +2105,7 @@ class BBCode $allowed_link_protocols[] = '//'; $allowed_link_protocols[] = 'http://'; $allowed_link_protocols[] = 'https://'; - $allowed_link_protocols[] = 'redir/'; + $allowed_link_protocols[] = 'contact/redir/'; array_walk($allowed_link_protocols, function(&$value) { $value = preg_quote($value, '#');}); diff --git a/src/Content/Text/HTML.php b/src/Content/Text/HTML.php index 93afeac4d0..2e55c71d20 100644 --- a/src/Content/Text/HTML.php +++ b/src/Content/Text/HTML.php @@ -840,7 +840,7 @@ class HTML if ($redirect) { $url = Contact::magicLinkByContact($contact); - if (strpos($url, 'redir/') === 0) { + if (strpos($url, 'contact/redir/') === 0) { $sparkle = ' sparkle'; } } diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 64ce451e46..ee74cb1574 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -1149,7 +1149,7 @@ class Contact $sparkle = false; if (($contact['network'] === Protocol::DFRN) && !$contact['self'] && empty($contact['pending'])) { $sparkle = true; - $profile_link = 'redir/' . $contact['id']; + $profile_link = 'contact/redir/' . $contact['id']; } else { $profile_link = $contact['url']; } @@ -3324,7 +3324,7 @@ class Contact return $destination; } - $redirect = 'redir/' . $contact['id']; + $redirect = 'contact/redir/' . $contact['id']; if (($url != '') && !Strings::compareLink($contact['url'], $url)) { $redirect .= '?url=' . $url; diff --git a/src/Module/Contact.php b/src/Module/Contact.php index 13f48f350f..9ed2501771 100644 --- a/src/Module/Contact.php +++ b/src/Module/Contact.php @@ -529,7 +529,7 @@ class Contact extends BaseModule $url = Model\Contact::magicLinkByContact($contact); - if (strpos($url, 'redir/') === 0) { + if (strpos($url, 'contact/redir/') === 0) { $sparkle = ' class="sparkle" '; } else { $sparkle = ''; diff --git a/src/Module/Contact/Hovercard.php b/src/Module/Contact/Hovercard.php index 7f12bf5cb5..620b96095a 100644 --- a/src/Module/Contact/Hovercard.php +++ b/src/Module/Contact/Hovercard.php @@ -44,14 +44,12 @@ class Hovercard extends BaseModule throw new HTTPException\ForbiddenException(); } - // If a contact is connected the url is internally changed to 'redir/CID'. We need the pure url to search for + // If a contact is connected the url is internally changed to 'contact/redir/CID'. We need the pure url to search for // the contact. So we strip out the contact id from the internal url and look in the contact table for // the real url (nurl) - if (strpos($contact_url, 'redir/') === 0) { + if (strpos($contact_url, 'contact/redir/') === 0) { $cid = intval(substr($contact_url, 6)); - } - - if (strpos($contact_url, 'contact/') === 0) { + } elseif (strpos($contact_url, 'contact/') === 0) { $cid = intval(substr($contact_url, 8)); } diff --git a/src/Module/Contact/Profile.php b/src/Module/Contact/Profile.php index eaf9993fc1..ecb25dd607 100644 --- a/src/Module/Contact/Profile.php +++ b/src/Module/Contact/Profile.php @@ -239,7 +239,7 @@ class Profile extends BaseModule } $url = Contact::magicLinkByContact($contact); - if (strpos($url, 'redir/') === 0) { + if (strpos($url, 'contact/redir/') === 0) { $sparkle = ' class="sparkle" '; } else { $sparkle = ''; diff --git a/src/Module/Contact/Redir.php b/src/Module/Contact/Redir.php new file mode 100644 index 0000000000..4b7360e471 --- /dev/null +++ b/src/Module/Contact/Redir.php @@ -0,0 +1,224 @@ +. + * + */ + +namespace Friendica\Module\Contact; + +use Friendica\Core\L10n; +use Friendica\App; +use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Database\Database; +use Friendica\Model\Contact; +use Friendica\Module\Response; +use Friendica\Network\HTTPClient\Capability\ICanSendHttpRequests; +use Friendica\Network\HTTPClient\Client\HttpClientAccept; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPException; +use Friendica\Util\Profiler; +use Friendica\Util\Strings; +use Psr\Log\LoggerInterface; + +class Redir extends \Friendica\BaseModule +{ + /** @var IHandleUserSessions */ + private $session; + /** @var Database */ + private $database; + /** @var App */ + private $app; + /** @var ICanSendHttpRequests */ + private $httpClient; + + public function __construct(ICanSendHttpRequests $httpClient, App $app, Database $database, IHandleUserSessions $session, 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->session = $session; + $this->database = $database; + $this->app = $app; + $this->httpClient = $httpClient; + } + + protected function rawContent(array $request = []) + { + if (!$this->session->isAuthenticated()) { + throw new HTTPException\ForbiddenException($this->t('Access denied.')); + } + + $url = $request['url'] ?? ''; + + $cid = $this->parameters['id'] ?? 0; + + // Try magic auth before the legacy stuff + $this->magic($cid, $url); + + $this->legacy($cid, $url); + } + + /** + * @param int $cid + * @param string $url + * @return void + * @throws HTTPException\ForbiddenException + * @throws HTTPException\InternalServerErrorException + * @throws HTTPException\NotFoundException + * @throws \ImagickException + */ + private function magic(int $cid, string $url) + { + $visitor = $this->session->getMyUrl(); + if (!empty($visitor)) { + $this->logger->info('Got my url', ['visitor' => $visitor]); + } + + $contact = $this->database->selectFirst('contact', ['url'], ['id' => $cid]); + if (!$contact) { + $this->logger->info('Contact not found', ['id' => $cid]); + throw new HTTPException\NotFoundException($this->t('Contact not found.')); + } else { + $contact_url = $contact['url']; + $this->checkUrl($contact_url, $url); + $target_url = $url ?: $contact_url; + } + + $basepath = Contact::getBasepath($contact_url); + + // We don't use magic auth when there is no visitor, we are on the same system, or we visit our own stuff + if (empty($visitor) || Strings::compareLink($basepath, $this->baseUrl) || Strings::compareLink($contact_url, $visitor)) { + $this->logger->info('Redirecting without magic', ['target' => $target_url, 'visitor' => $visitor, 'contact' => $contact_url]); + $this->app->redirect($target_url); + } + + // Test for magic auth on the target system + $response = $this->httpClient->head($basepath . '/magic', [HttpClientOptions::ACCEPT_CONTENT => HttpClientAccept::HTML]); + if ($response->isSuccess()) { + $separator = strpos($target_url, '?') ? '&' : '?'; + $target_url .= $separator . 'zrl=' . urlencode($visitor) . '&addr=' . urlencode($contact_url); + + $this->logger->info('Redirecting with magic', ['target' => $target_url, 'visitor' => $visitor, 'contact' => $contact_url]); + $this->app->redirect($target_url); + } else { + $this->logger->info('No magic for contact', ['contact' => $contact_url]); + } + } + + /** + * @param int $cid + * @param string $url + * @return void + * @throws HTTPException\BadRequestException + * @throws HTTPException\ForbiddenException + * @throws HTTPException\InternalServerErrorException + * @throws HTTPException\NotFoundException + */ + private function legacy(int $cid, string $url): void + { + if (empty($cid)) { + throw new HTTPException\BadRequestException($this->t('Bad Request.')); + } + + $fields = ['id', 'uid', 'nurl', 'url', 'addr', 'name']; + $contact = $this->database->selectFirst('contact', $fields, ['id' => $cid, 'uid' => [0, $this->session->getLocalUserId()]]); + if (!$contact) { + throw new HTTPException\NotFoundException($this->t('Contact not found.')); + } + + $contact_url = $contact['url']; + + if (!empty($this->app->getContactId()) && $this->app->getContactId() == $cid) { + // Local user is already authenticated. + $this->checkUrl($contact_url, $url); + $this->app->redirect($url ?: $contact_url); + } + + if ($contact['uid'] == 0 && $this->session->getLocalUserId()) { + // Let's have a look if there is an established connection + // between the public contact we have found and the local user. + $contact = $this->database->selectFirst('contact', $fields, ['nurl' => $contact['nurl'], 'uid' => $this->session->getLocalUserId()]); + if ($contact) { + $cid = $contact['id']; + } + + if (!empty($this->app->getContactId()) && $this->app->getContactId() == $cid) { + // Local user is already authenticated. + $this->checkUrl($contact_url, $url); + $target_url = $url ?: $contact_url; + $this->logger->info($contact['name'] . " is already authenticated. Redirecting to " . $target_url); + $this->app->redirect($target_url); + } + } + + if ($this->session->getRemoteUserId()) { + $host = substr($this->baseUrl->getUrlPath() . ($this->baseUrl->getUrlPath() ? '/' . $this->baseUrl->getUrlPath() : ''), strpos($this->baseUrl->getUrlPath(), '://') + 3); + $remotehost = substr($contact['addr'], strpos($contact['addr'], '@') + 1); + + // On a local instance we have to check if the local user has already authenticated + // with the local contact. Otherwise, the local user would ask the local contact + // for authentification everytime he/she is visiting a profile page of the local + // contact. + if (($host == $remotehost) && ($this->session->getRemoteContactID($this->session->get('visitor_visiting')) == $this->session->get('visitor_id'))) { + // Remote user is already authenticated. + $this->checkUrl($contact_url, $url); + $target_url = $url ?: $contact_url; + $this->logger->info($contact['name'] . " is already authenticated. Redirecting to " . $target_url); + $this->app->redirect($target_url); + } + } + + if (empty($url)) { + throw new HTTPException\BadRequestException($this->t('Bad Request.')); + } + + // If we don't have a connected contact, redirect with + // the 'zrl' parameter. + $my_profile = $this->session->getMyUrl(); + + if (!empty($my_profile) && !Strings::compareLink($my_profile, $url)) { + $separator = strpos($url, '?') ? '&' : '?'; + + $url .= $separator . 'zrl=' . urlencode($my_profile); + } + + $this->logger->info('redirecting to ' . $url); + $this->app->redirect($url); + } + + + private function checkUrl(string $contact_url, string $url) + { + if (empty($contact_url) || empty($url)) { + return; + } + + $url_host = parse_url($url, PHP_URL_HOST); + if (empty($url_host)) { + $url_host = parse_url($this->baseUrl, PHP_URL_HOST); + } + + $contact_url_host = parse_url($contact_url, PHP_URL_HOST); + + if ($url_host == $contact_url_host) { + return; + } + + $this->logger->error('URL check host mismatch', ['contact' => $contact_url, 'url' => $url]); + throw new HTTPException\ForbiddenException($this->t('Access denied.')); + } +} diff --git a/src/Object/Post.php b/src/Object/Post.php index 459405cdbf..87951c19cc 100644 --- a/src/Object/Post.php +++ b/src/Object/Post.php @@ -280,7 +280,7 @@ class Post $profile_link = $item['author-link']; } - if (strpos($profile_link, 'redir/') === 0) { + if (strpos($profile_link, 'contact/redir/') === 0) { $sparkle = ' sparkle'; } diff --git a/static/routes.config.php b/static/routes.config.php index 12d68f4016..d281f252b9 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -385,6 +385,7 @@ return [ '/hovercard' => [Module\Contact\Hovercard::class, [R::GET]], '/ignored' => [Module\Contact::class, [R::GET]], '/pending' => [Module\Contact::class, [R::GET]], + '/redir/{id:\d+}' => [Module\Contact\Redir::class, [R::GET]], '/suggestions' => [Module\Contact\Suggestions::class, [R::GET]], '/unfollow' => [Module\Contact\Unfollow::class, [R::GET, R::POST]], ], diff --git a/view/theme/frio/theme.php b/view/theme/frio/theme.php index f4daed4d6c..1079a08c86 100644 --- a/view/theme/frio/theme.php +++ b/view/theme/frio/theme.php @@ -107,7 +107,7 @@ function frio_item_photo_links(App $a, &$body_info) $newlink = str_replace($matches[0], "/photo/{$matches[1]}", $link); // Add a "quiet" parameter to any redir links to prevent the "XX welcomes YY" info boxes - $newlink = preg_replace('/href="([^"]+)\/redir\/([^"]+)&url=([^"]+)"/', 'href="$1/redir/$2&quiet=1&url=$3"', $newlink); + $newlink = preg_replace('#href="([^"]+)/contact/redir/(\d+)&url=([^"]+)"#', 'href="$1/contact/redir/$2&quiet=1&url=$3"', $newlink); // Having any arguments to the link for Colorbox causes it to fetch base64 code instead of the image $newlink = preg_replace('/\/[?&]zrl=([^&"]+)/', '', $newlink);