diff --git a/mod/dirfind.php b/mod/dirfind.php deleted file mode 100644 index a5b77312f..000000000 --- a/mod/dirfind.php +++ /dev/null @@ -1,265 +0,0 @@ -page['aside'])) { - $a->page['aside'] = ''; - } - - $a->page['aside'] .= Widget::findPeople(); - - $a->page['aside'] .= Widget::follow(); -} - -function dirfind_content(App $a, $prefix = "") { - - $community = false; - $discover_user = false; - - $local = Config::get('system','poco_local_search'); - - $search = $prefix.Strings::escapeTags(trim(defaults($_REQUEST, 'search', ''))); - - $header = ''; - - if (strpos($search,'@') === 0) { - $search = substr($search,1); - $header = L10n::t('People Search - %s', $search); - if ((filter_var($search, FILTER_VALIDATE_EMAIL) && Network::isEmailDomainValid($search)) || - (substr(Strings::normaliseLink($search), 0, 7) == "http://")) { - $user_data = Probe::uri($search); - $discover_user = (in_array($user_data["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])); - } - } - - if (strpos($search,'!') === 0) { - $search = substr($search,1); - $community = true; - $header = L10n::t('Forum Search - %s', $search); - } - - $o = ''; - - if ($search) { - $pager = new Pager($a->query_string); - - if ($discover_user) { - $j = new stdClass(); - $j->total = 1; - $j->items_page = 1; - $j->page = $pager->getPage(); - - $objresult = new stdClass(); - $objresult->cid = 0; - $objresult->name = $user_data["name"]; - $objresult->addr = $user_data["addr"]; - $objresult->url = $user_data["url"]; - $objresult->photo = $user_data["photo"]; - $objresult->tags = ""; - $objresult->network = $user_data["network"]; - - $contact = Model\Contact::getDetailsByURL($user_data["url"], local_user()); - $objresult->cid = $contact["cid"]; - $objresult->pcid = $contact["zid"]; - - $j->results[] = $objresult; - - // Add the contact to the global contacts if it isn't already in our system - if (($contact["cid"] == 0) && ($contact["zid"] == 0) && ($contact["gid"] == 0)) { - Model\GContact::update($user_data); - } - } elseif ($local) { - if ($community) { - $extra_sql = " AND `community`"; - } else { - $extra_sql = ""; - } - - $pager->setItemsPerPage(80); - - if (Config::get('system','diaspora_enabled')) { - $diaspora = Protocol::DIASPORA; - } else { - $diaspora = Protocol::DFRN; - } - - if (!Config::get('system','ostatus_disabled')) { - $ostatus = Protocol::OSTATUS; - } else { - $ostatus = Protocol::DFRN; - } - - $search2 = "%".$search."%"; - - /// @TODO These 2 SELECTs are not checked on validity with DBA::isResult() - $count = q("SELECT count(*) AS `total` FROM `gcontact` - WHERE NOT `hide` AND `network` IN ('%s', '%s', '%s', '%s') AND - ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) AND - (`url` LIKE '%s' OR `name` LIKE '%s' OR `location` LIKE '%s' OR - `addr` LIKE '%s' OR `about` LIKE '%s' OR `keywords` LIKE '%s') $extra_sql", - DBA::escape(Protocol::ACTIVITYPUB), DBA::escape(Protocol::DFRN), DBA::escape($ostatus), DBA::escape($diaspora), - DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), - DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2))); - - $results = q("SELECT `nurl` - FROM `gcontact` - WHERE NOT `hide` AND `network` IN ('%s', '%s', '%s', '%s') AND - ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) AND - (`url` LIKE '%s' OR `name` LIKE '%s' OR `location` LIKE '%s' OR - `addr` LIKE '%s' OR `about` LIKE '%s' OR `keywords` LIKE '%s') $extra_sql - GROUP BY `nurl` - ORDER BY `updated` DESC LIMIT %d, %d", - DBA::escape(Protocol::ACTIVITYPUB), DBA::escape(Protocol::DFRN), DBA::escape($ostatus), DBA::escape($diaspora), - DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), - DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), - $pager->getStart(), $pager->getItemsPerPage()); - $j = new stdClass(); - $j->total = $count[0]["total"]; - $j->items_page = $pager->getItemsPerPage(); - $j->page = $pager->getPage(); - foreach ($results AS $result) { - if (PortableContact::alternateOStatusUrl($result["nurl"])) { - continue; - } - - $urlparts = parse_url($result["nurl"]); - - // Ignore results that look strange. - // For historic reasons the gcontact table does contain some garbage. - if (!empty($urlparts['query']) || !empty($urlparts['fragment'])) { - continue; - } - - $result = Model\Contact::getDetailsByURL($result["nurl"], local_user()); - - if ($result["name"] == "") { - $result["name"] = end(explode("/", $urlparts["path"])); - } - - $objresult = new stdClass(); - $objresult->cid = $result["cid"]; - $objresult->pcid = $result["zid"]; - $objresult->name = $result["name"]; - $objresult->addr = $result["addr"]; - $objresult->url = $result["url"]; - $objresult->photo = $result["photo"]; - $objresult->tags = $result["keywords"]; - $objresult->network = $result["network"]; - - $j->results[] = $objresult; - } - - // Add found profiles from the global directory to the local directory - Worker::add(PRIORITY_LOW, 'DiscoverPoCo', "dirsearch", urlencode($search)); - } elseif (strlen(Config::get('system','directory'))) { - $p = (($pager->getPage() != 1) ? '&p=' . $pager->getPage() : ''); - - $x = Network::fetchUrl(get_server() . '/lsearch?f=' . $p . '&search=' . urlencode($search)); - - $j = json_decode($x); - $pager->setItemsPerPage($j->items_page); - } - - if (!empty($j->results)) { - $id = 0; - - $entries = []; - foreach ($j->results as $jj) { - - $alt_text = ""; - - $contact_details = Model\Contact::getDetailsByURL($jj->url, local_user()); - - $itemurl = (($contact_details["addr"] != "") ? $contact_details["addr"] : $jj->url); - - // If We already know this contact then don't show the "connect" button - if ($jj->cid > 0) { - $connlnk = ""; - $conntxt = ""; - $contact = DBA::selectFirst('contact', [], ['id' => $jj->cid]); - if (DBA::isResult($contact)) { - $photo_menu = Model\Contact::photoMenu($contact); - $details = Module\Contact::getContactTemplateVars($contact); - $alt_text = $details['alt_text']; - } else { - $photo_menu = []; - } - } else { - $connlnk = System::baseUrl().'/follow/?url='.(!empty($jj->connect) ? $jj->connect : $jj->url); - $conntxt = L10n::t('Connect'); - - $contact = DBA::selectFirst('contact', [], ['id' => $jj->pcid]); - if (DBA::isResult($contact)) { - $photo_menu = Model\Contact::photoMenu($contact); - } else { - $photo_menu = []; - } - - $photo_menu['profile'] = [L10n::t("View Profile"), Model\Contact::magicLink($jj->url)]; - $photo_menu['follow'] = [L10n::t("Connect/Follow"), $connlnk]; - } - - $jj->photo = str_replace("http:///photo/", get_server()."/photo/", $jj->photo); - - $entry = [ - 'alt_text' => $alt_text, - 'url' => Model\Contact::magicLink($jj->url), - 'itemurl' => $itemurl, - 'name' => $jj->name, - 'thumb' => ProxyUtils::proxifyUrl($jj->photo, false, ProxyUtils::SIZE_THUMB), - 'img_hover' => $jj->tags, - 'conntxt' => $conntxt, - 'connlnk' => $connlnk, - 'photo_menu' => $photo_menu, - 'details' => $contact_details['location'], - 'tags' => $contact_details['keywords'], - 'about' => $contact_details['about'], - 'account_type' => Model\Contact::getAccountType($contact_details), - 'network' => ContactSelector::networkToName($jj->network, $jj->url), - 'id' => ++$id, - ]; - $entries[] = $entry; - } - - $tpl = Renderer::getMarkupTemplate('viewcontact_template.tpl'); - $o .= Renderer::replaceMacros($tpl,[ - 'title' => $header, - '$contacts' => $entries, - '$paginate' => $pager->renderFull($j->total), - ]); - } else { - info(L10n::t('No matches') . EOL); - } - - } - - return $o; -} diff --git a/mod/search.php b/mod/search.php index 1416f1d89..4144e2608 100644 --- a/mod/search.php +++ b/mod/search.php @@ -12,13 +12,11 @@ use Friendica\Core\Config; use Friendica\Core\L10n; use Friendica\Core\Logger; use Friendica\Core\Renderer; -use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\Model\Item; +use Friendica\Module\BaseSearchModule; use Friendica\Util\Strings; -require_once 'mod/dirfind.php'; - function search_saved_searches() { $o = ''; @@ -150,10 +148,10 @@ function search_content(App $a) { $search = substr($search,1); } if (strpos($search,'@') === 0) { - return dirfind_content($a); + return BaseSearchModule::performSearch(); } if (strpos($search,'!') === 0) { - return dirfind_content($a); + return BaseSearchModule::performSearch(); } if (!empty($_GET['search-option'])) @@ -164,11 +162,9 @@ function search_content(App $a) { $tag = true; break; case 'contacts': - return dirfind_content($a, "@"); - break; + return BaseSearchModule::performSearch('@'); case 'forums': - return dirfind_content($a, "!"); - break; + return BaseSearchModule::performSearch('!'); } if (!$search) diff --git a/src/App/Router.php b/src/App/Router.php index f7d2a2640..be81c143d 100644 --- a/src/App/Router.php +++ b/src/App/Router.php @@ -112,6 +112,7 @@ class Router $collector->addRoute(['GET'], '/ignored', Module\Contact::class); }); $this->routeCollector->addRoute(['GET'], '/credits', Module\Credits::class); + $this->routeCollector->addRoute(['GET'], '/dirfind', Module\Search\Directory::class); $this->routeCollector->addRoute(['GET'], '/directory', Module\Directory::class); $this->routeCollector->addGroup('/feed', function (RouteCollector $collector) { $collector->addRoute(['GET'], '/{nickname}', Module\Feed::class); diff --git a/src/Core/Search.php b/src/Core/Search.php new file mode 100644 index 000000000..bd4a81a5f --- /dev/null +++ b/src/Core/Search.php @@ -0,0 +1,244 @@ +getConfig(); + $server = $config->get('system', 'directory', self::DEFAULT_DIRECTORY); + + $searchUrl = $server . '/search'; + + switch ($type) { + case self::TYPE_FORUM: + $searchUrl .= '/forum'; + break; + case self::TYPE_PEOPLE: + $searchUrl .= '/people'; + break; + } + $searchUrl .= '?q=' . urlencode($search); + + if ($page > 1) { + $searchUrl .= '&page=' . $page; + } + + $red = 0; + $resultJson = Network::fetchUrl($searchUrl, false, $red, 0, 'application/json'); + + $results = json_decode($resultJson, true); + + $resultList = new ResultList( + defaults($results, 'page', 1), + defaults($results, 'count', 1), + defaults($results, 'itemsperpage', 1) + ); + + $profiles = defaults($results, 'profiles', []); + + foreach ($profiles as $profile) { + $contactDetails = Contact::getDetailsByURL(defaults($profile, 'profile_url', ''), local_user()); + $itemUrl = defaults($contactDetails, 'addr', defaults($profile, 'profile_url', '')); + + $result = new ContactResult( + defaults($profile, 'name', ''), + defaults($profile, 'addr', ''), + $itemUrl, + defaults($profile, 'profile_url', ''), + defaults($profile, 'photo', ''), + Protocol::DFRN, + defaults($contactDetails, 'cid', 0), + 0, + defaults($profile, 'tags', '')); + + $resultList->addResult($result); + } + + return $resultList; + } + + /** + * Search in the local database for occurrences of the search string + * + * @param string $search + * @param int $type + * @param int $start + * @param int $itemPage + * + * @return ResultList|null + * @throws HTTPException\InternalServerErrorException + */ + public static function getContactsFromLocalDirectory($search, $type = self::TYPE_ALL, $start = 0, $itemPage = 80) + { + $config = self::getApp()->getConfig(); + + $diaspora = $config->get('system', 'diaspora_enabled') ? Protocol::DIASPORA : Protocol::DFRN; + $ostatus = !$config->get('system', 'ostatus_disabled') ? Protocol::OSTATUS : Protocol::DFRN; + + $wildcard = Strings::escapeHtml('%' . $search . '%'); + + $count = DBA::count('gcontact', [ + 'NOT `hide` + AND `network` IN (?, ?, ?, ?) + AND ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) + AND (`url` LIKE ? OR `name` LIKE ? OR `location` LIKE ? + OR `addr` LIKE ? OR `about` LIKE ? OR `keywords` LIKE ?) + AND `community` = ?', + Protocol::ACTIVITYPUB, Protocol::DFRN, $ostatus, $diaspora, + $wildcard, $wildcard, $wildcard, + $wildcard, $wildcard, $wildcard, + ($type === self::TYPE_FORUM), + ]); + + if (empty($count)) { + return null; + } + + $data = DBA::select('gcontact', ['nurl'], [ + 'NOT `hide` + AND `network` IN (?, ?, ?, ?) + AND ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) + AND (`url` LIKE ? OR `name` LIKE ? OR `location` LIKE ? + OR `addr` LIKE ? OR `about` LIKE ? OR `keywords` LIKE ?) + AND `community` = ?', + Protocol::ACTIVITYPUB, Protocol::DFRN, $ostatus, $diaspora, + $wildcard, $wildcard, $wildcard, + $wildcard, $wildcard, $wildcard, + ($type === self::TYPE_FORUM), + ], [ + 'group_by' => ['nurl', 'updated'], + 'limit' => [$start, $itemPage], + 'order' => ['updated' => 'DESC'] + ]); + + if (!DBA::isResult($data)) { + return null; + } + + $resultList = new ResultList($start, $itemPage, $count); + + while ($row = DBA::fetch($data)) { + if (PortableContact::alternateOStatusUrl($row["nurl"])) { + continue; + } + + $urlParts = parse_url($row["nurl"]); + + // Ignore results that look strange. + // For historic reasons the gcontact table does contain some garbage. + if (!empty($urlParts['query']) || !empty($urlParts['fragment'])) { + continue; + } + + $contact = Contact::getDetailsByURL($row["nurl"], local_user()); + + if ($contact["name"] == "") { + $contact["name"] = end(explode("/", $urlParts["path"])); + } + + $result = new ContactResult( + $contact["name"], + $contact["addr"], + $contact["addr"], + $contact["url"], + $contact["photo"], + $contact["network"], + $contact["cid"], + $contact["zid"], + $contact["keywords"] + ); + + $resultList->addResult($result); + } + + DBA::close($data); + + // Add found profiles from the global directory to the local directory + Worker::add(PRIORITY_LOW, 'DiscoverPoCo', "dirsearch", urlencode($search)); + + return $resultList; + } +} diff --git a/src/Database/DBA.php b/src/Database/DBA.php index 85bdbbb62..2327e4a7f 100644 --- a/src/Database/DBA.php +++ b/src/Database/DBA.php @@ -1507,6 +1507,15 @@ class DBA */ public static function buildParameter(array $params = []) { + $groupby_string = ''; + if (isset($params['group_by'])) { + $groupby_string = " GROUP BY "; + foreach ($params['group_by'] as $fields) { + $groupby_string .= "`" . $fields . "`, "; + } + $groupby_string = substr($groupby_string, 0, -2); + } + $order_string = ''; if (isset($params['order'])) { $order_string = " ORDER BY "; @@ -1531,7 +1540,7 @@ class DBA $limit_string = " LIMIT " . intval($params['limit'][0]) . ", " . intval($params['limit'][1]); } - return $order_string.$limit_string; + return $groupby_string . $order_string . $limit_string; } /** diff --git a/src/Module/BaseSearchModule.php b/src/Module/BaseSearchModule.php new file mode 100644 index 000000000..226a4da4d --- /dev/null +++ b/src/Module/BaseSearchModule.php @@ -0,0 +1,165 @@ +getConfig(); + + $type = Search::TYPE_ALL; + + $localSearch = $config->get('system', 'poco_local_search'); + + $search = $prefix . Strings::escapeTags(trim(defaults($_REQUEST, 'search', ''))); + + if (!$search) { + return ''; + } + + $header = ''; + + if (strpos($search, '@') === 0) { + $search = substr($search, 1); + $type = Search::TYPE_PEOPLE; + $header = L10n::t('People Search - %s', $search); + $results = Search::getContactsFromProbe($search); + } + + if (strpos($search, '!') === 0) { + $search = substr($search, 1); + $type = Search::TYPE_FORUM; + $header = L10n::t('Forum Search - %s', $search); + } + + $pager = new Pager($a->query_string); + + if ($localSearch && empty($results)) { + $pager->setItemsPerPage(80); + $results = Search::getContactsFromLocalDirectory($search, $type, $pager->getStart(), $pager->getItemsPerPage()); + + } elseif (strlen($config->get('system', 'directory')) && empty($results)) { + $results = Search::getContactsFromGlobalDirectory($search, $type, $pager->getPage()); + $pager->setItemsPerPage($results->getItemsPage()); + } + + return self::printResult($results, $pager, $header); + } + + /** + * Prints a human readable search result + * + * @param ResultList $results + * @param Pager $pager + * @param string $header + * + * @return string The result + * @throws HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + protected static function printResult(ResultList $results, Pager $pager, $header = '') + { + if (empty($results) || empty($results->getResults())) { + info(L10n::t('No matches') . EOL); + return ''; + } + + $a = self::getApp(); + + $id = 0; + $entries = []; + foreach ($results->getResults() as $result) { + + // in case the result is a contact result, add a contact-specific entry + if ($result instanceof ContactResult) { + + $alt_text = ''; + $location = ''; + $about = ''; + $accountType = ''; + $photo_menu = []; + + // If We already know this contact then don't show the "connect" button + if ($result->getCid() > 0 || $result->getPCid() > 0) { + $connLink = ""; + $connTxt = ""; + $contact = Model\Contact::getById( + ($result->getCid() > 0) ? $result->getCid() : $result->getPCid() + ); + + if (!empty($contact)) { + $photo_menu = Model\Contact::photoMenu($contact); + $details = Contact::getContactTemplateVars($contact); + $alt_text = $details['alt_text']; + $location = $contact['location']; + $about = $contact['about']; + $accountType = Model\Contact::getAccountType($contact); + } else { + $photo_menu = []; + } + } else { + $connLink = $a->getBaseURL() . '/follow/?url=' . $result->getUrl(); + $connTxt = L10n::t('Connect'); + + $photo_menu['profile'] = [L10n::t("View Profile"), Model\Contact::magicLink($result->getUrl())]; + $photo_menu['follow'] = [L10n::t("Connect/Follow"), $connLink]; + } + + $photo = str_replace("http:///photo/", get_server() . "/photo/", $result->getPhoto()); + + $entry = [ + 'alt_text' => $alt_text, + 'url' => Model\Contact::magicLink($result->getUrl()), + 'itemurl' => $result->getItem(), + 'name' => $result->getName(), + 'thumb' => ProxyUtils::proxifyUrl($photo, false, ProxyUtils::SIZE_THUMB), + 'img_hover' => $result->getTags(), + 'conntxt' => $connTxt, + 'connlnk' => $connLink, + 'photo_menu' => $photo_menu, + 'details' => $location, + 'tags' => $result->getTags(), + 'about' => $about, + 'account_type' => $accountType, + 'network' => ContactSelector::networkToName($result->getNetwork(), $result->getUrl()), + 'id' => ++$id, + ]; + $entries[] = $entry; + } + } + + $tpl = Renderer::getMarkupTemplate('viewcontact_template.tpl'); + return Renderer::replaceMacros($tpl, [ + 'title' => $header, + '$contacts' => $entries, + '$paginate' => $pager->renderFull($results->getTotal()), + ]); + } +} diff --git a/src/Module/Search/Directory.php b/src/Module/Search/Directory.php new file mode 100644 index 000000000..76ada50d5 --- /dev/null +++ b/src/Module/Search/Directory.php @@ -0,0 +1,32 @@ +page['aside'])) { + $a->page['aside'] = ''; + } + + $a->page['aside'] .= Widget::findPeople(); + $a->page['aside'] .= Widget::follow(); + + return self::performSearch(); + } +} diff --git a/src/Object/Search/ContactResult.php b/src/Object/Search/ContactResult.php new file mode 100644 index 000000000..ccfb5f4cf --- /dev/null +++ b/src/Object/Search/ContactResult.php @@ -0,0 +1,147 @@ +cid; + } + + /** + * @return int + */ + public function getPCid() + { + return $this->pCid; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getAddr() + { + return $this->addr; + } + + /** + * @return string + */ + public function getItem() + { + return $this->item; + } + + /** + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @return string + */ + public function getPhoto() + { + return $this->photo; + } + + /** + * @return string + */ + public function getTags() + { + return $this->tags; + } + + /** + * @return string + */ + public function getNetwork() + { + return $this->network; + } + + /** + * @param string $name + * @param string $addr + * @param string $item + * @param string $url + * @param string $photo + * @param string $network + * @param int $cid + * @param int $pCid + * @param string $tags + */ + public function __construct($name, $addr, $item, $url, $photo, $network, $cid = 0, $pCid = 0, $tags = '') + { + $this->name = $name; + $this->addr = $addr; + $this->item = $item; + $this->url = $url; + $this->photo = $photo; + $this->network = $network; + + $this->cid = $cid; + $this->pCid = $pCid; + $this->tags = $tags; + } +} diff --git a/src/Object/Search/IResult.php b/src/Object/Search/IResult.php new file mode 100644 index 000000000..64fb71c85 --- /dev/null +++ b/src/Object/Search/IResult.php @@ -0,0 +1,11 @@ +page; + } + + /** + * @return int + */ + public function getTotal() + { + return $this->total; + } + + /** + * @return int + */ + public function getItemsPage() + { + return $this->itemsPage; + } + + /** + * @return IResult[] + */ + public function getResults() + { + return $this->results; + } + + /** + * @param int $page + * @param int $total + * @param int $itemsPage + * @param IResult[] $results + */ + public function __construct($page = 0, $total = 0, $itemsPage = 0, array $results = []) + { + $this->page = $page; + $this->total = $total; + $this->itemsPage = $itemsPage; + + $this->results = $results; + } + + /** + * Adds a result to the result list + * + * @param IResult $result + */ + public function addResult(IResult $result) + { + $this->results[] = $result; + } +}