diff --git a/src/Module/BaseSearchModule.php b/src/Module/BaseSearchModule.php index 9766c000cc..96692b0b2d 100644 --- a/src/Module/BaseSearchModule.php +++ b/src/Module/BaseSearchModule.php @@ -22,7 +22,7 @@ use Friendica\Util\Strings; class BaseSearchModule extends BaseModule { /** - * Performs a search with an optional prefix + * Performs a contact search with an optional prefix * * @param string $search Search query * @param string $prefix A optional prefix (e.g. @ or !) for searching @@ -31,7 +31,7 @@ class BaseSearchModule extends BaseModule * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function performSearch($search, $prefix = '') + public static function performContactSearch($search, $prefix = '') { $a = self::getApp(); $config = $a->getConfig(); diff --git a/src/Module/Search/Directory.php b/src/Module/Search/Directory.php index b18847afe9..e3515713f5 100644 --- a/src/Module/Search/Directory.php +++ b/src/Module/Search/Directory.php @@ -31,6 +31,6 @@ class Directory extends BaseSearchModule $a->page['aside'] .= Widget::findPeople(); $a->page['aside'] .= Widget::follow(); - return self::performSearch($search); + return self::performContactSearch($search); } } diff --git a/src/Module/Search/Index.php b/src/Module/Search/Index.php index 7c52c7e79d..010f0a790a 100644 --- a/src/Module/Search/Index.php +++ b/src/Module/Search/Index.php @@ -3,6 +3,7 @@ namespace Friendica\Module\Search; use Friendica\App\Arguments; +use Friendica\App\BaseURL; use Friendica\Content\Nav; use Friendica\Content\Pager; use Friendica\Content\Text\HTML; @@ -15,6 +16,7 @@ use Friendica\Core\Logger; use Friendica\Core\Renderer; use Friendica\Core\Session; use Friendica\Database\DBA; +use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\Term; use Friendica\Module\BaseSearchModule; @@ -37,6 +39,9 @@ class Index extends BaseSearchModule throw $e; } + /** @var BaseURL $baseURL */ + $baseURL = self::getClass(BaseURL::class); + if (Config::get('system', 'permit_crawling') && !Session::isAuthenticated()) { // Default values: // 10 requests are "free", after the 11th only a call per minute is allowed @@ -82,24 +87,22 @@ class Index extends BaseSearchModule '$content' => HTML::search($search, 'search-box', false) ]); + if (!$search) { + return $o; + } + if (strpos($search, '#') === 0) { $tag = true; $search = substr($search, 1); } + self::tryRedirectToProfile($baseURL, $search); + if (strpos($search, '@') === 0 || strpos($search, '!') === 0) { - return self::performSearch($search); + return self::performContactSearch($search); } - if (parse_url($search, PHP_URL_SCHEME) != '') { - $id = Item::fetchByLink($search); - if (!empty($id)) { - $item = Item::selectFirst(['guid'], ['id' => $id]); - if (DBA::isResult($item)) { - self::getApp()->internalRedirect('display/' . $item['guid']); - } - } - } + self::tryRedirectToPost($baseURL, $search); if (!empty($_GET['search-option'])) { switch ($_GET['search-option']) { @@ -109,16 +112,12 @@ class Index extends BaseSearchModule $tag = true; break; case 'contacts': - return self::performSearch($search, '@'); + return self::performContactSearch($search, '@'); case 'forums': - return self::performSearch($search, '!'); + return self::performContactSearch($search, '!'); } } - if (!$search) { - return $o; - } - $tag = $tag || Config::get('system', 'only_tag_search'); // Here is the way permissions work in the search module... @@ -197,4 +196,91 @@ class Index extends BaseSearchModule return $o; } + + /** + * Tries to redirect to a local profile page based on the input. + * + * This method separates logged in and anonymous users. Logged in users can trigger contact probes to import + * non-existing contacts while anonymous users can only trigger a local lookup. + * + * Formats matched: + * - @user@domain + * - user@domain + * - Any fully-formed URL + * + * @param BaseURL $baseURL + * @param string $search + * @throws HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + private static function tryRedirectToProfile(BaseURL $baseURL, string $search) + { + $isUrl = parse_url($search, PHP_URL_SCHEME) !== ''; + $isAddr = preg_match('/^@?([a-z0-9.-_]+@[a-z0-9.-_:]+)$/i', trim($search), $matches); + + if (!$isUrl && !$isAddr) { + return; + } + + if ($isAddr) { + $search = $matches[1]; + } + + if (local_user()) { + // User-specific contact URL/address search + $contact_id = Contact::getIdForURL($search, local_user()); + if (!$contact_id) { + // User-specific contact URL/address search and probe + $contact_id = Contact::getIdForURL($search); + } + } else { + // Cheaper local lookup for anonymous users, no probe + if ($isAddr) { + $contact = Contact::selectFirst(['id' => 'cid'], ['addr' => $search, 'uid' => 0]); + } else { + $contact = Contact::getDetailsByURL($search, 0, ['cid' => 0]); + } + + if (DBA::isResult($contact)) { + $contact_id = $contact['cid']; + } + } + + if (!empty($contact_id)) { + $baseURL->redirect('contact/' . $contact_id); + } + } + + /** + * Fetch/search a post by URL and redirects to its local representation if it was found. + * + * @param BaseURL $baseURL + * @param string $search + * @throws HTTPException\InternalServerErrorException + */ + private static function tryRedirectToPost(BaseURL $baseURL, string $search) + { + if (parse_url($search, PHP_URL_SCHEME) == '') { + return; + } + + if (local_user()) { + // Post URL search + $item_id = Item::fetchByLink($search, local_user()); + if (!$item_id) { + // If the user-specific search failed, we search and probe a public post + $item_id = Item::fetchByLink($search); + } + } else { + // Cheaper local lookup for anonymous users, no probe + $item_id = Item::searchByLink($search); + } + + if (!empty($item_id)) { + $item = Item::selectFirst(['guid'], ['id' => $item_id]); + if (DBA::isResult($item)) { + $baseURL->redirect('display/' . $item['guid']); + } + } + } }