]> git.mxchange.org Git - friendica.git/blob - src/Module/Search/Index.php
Merge pull request #8209 from nupplaphil/task/mod_worker
[friendica.git] / src / Module / Search / Index.php
1 <?php
2
3 namespace Friendica\Module\Search;
4
5 use Friendica\Content\Nav;
6 use Friendica\Content\Pager;
7 use Friendica\Content\Text\HTML;
8 use Friendica\Content\Widget;
9 use Friendica\Core\Cache\Duration;
10 use Friendica\Core\Logger;
11 use Friendica\Core\Renderer;
12 use Friendica\Core\Session;
13 use Friendica\Database\DBA;
14 use Friendica\DI;
15 use Friendica\Model\Contact;
16 use Friendica\Model\Item;
17 use Friendica\Model\Term;
18 use Friendica\Module\BaseSearch;
19 use Friendica\Network\HTTPException;
20 use Friendica\Util\Strings;
21
22 class Index extends BaseSearch
23 {
24         public static function content(array $parameters = [])
25         {
26                 $search = (!empty($_GET['q']) ? Strings::escapeTags(trim(rawurldecode($_GET['q']))) : '');
27
28                 if (DI::config()->get('system', 'block_public') && !Session::isAuthenticated()) {
29                         throw new HTTPException\ForbiddenException(DI::l10n()->t('Public access denied.'));
30                 }
31
32                 if (DI::config()->get('system', 'local_search') && !Session::isAuthenticated()) {
33                         $e = new HTTPException\ForbiddenException(DI::l10n()->t('Only logged in users are permitted to perform a search.'));
34                         $e->httpdesc = DI::l10n()->t('Public access denied.');
35                         throw $e;
36                 }
37
38                 if (DI::config()->get('system', 'permit_crawling') && !Session::isAuthenticated()) {
39                         // Default values:
40                         // 10 requests are "free", after the 11th only a call per minute is allowed
41
42                         $free_crawls = intval(DI::config()->get('system', 'free_crawls'));
43                         if ($free_crawls == 0)
44                                 $free_crawls = 10;
45
46                         $crawl_permit_period = intval(DI::config()->get('system', 'crawl_permit_period'));
47                         if ($crawl_permit_period == 0)
48                                 $crawl_permit_period = 10;
49
50                         $remote = $_SERVER['REMOTE_ADDR'];
51                         $result = DI::cache()->get('remote_search:' . $remote);
52                         if (!is_null($result)) {
53                                 $resultdata = json_decode($result);
54                                 if (($resultdata->time > (time() - $crawl_permit_period)) && ($resultdata->accesses > $free_crawls)) {
55                                         throw new HTTPException\TooManyRequestsException(DI::l10n()->t('Only one search per minute is permitted for not logged in users.'));
56                                 }
57                                 DI::cache()->set('remote_search:' . $remote, json_encode(['time' => time(), 'accesses' => $resultdata->accesses + 1]), Duration::HOUR);
58                         } else {
59                                 DI::cache()->set('remote_search:' . $remote, json_encode(['time' => time(), 'accesses' => 1]), Duration::HOUR);
60                         }
61                 }
62
63                 if (local_user()) {
64                         DI::page()['aside'] .= Widget\SavedSearches::getHTML('search?q=' . urlencode($search), $search);
65                 }
66
67                 Nav::setSelected('search');
68
69                 $tag = false;
70                 if (!empty($_GET['tag'])) {
71                         $tag = true;
72                         $search = '#' . Strings::escapeTags(trim(rawurldecode($_GET['tag'])));
73                 }
74
75                 // contruct a wrapper for the search header
76                 $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('content_wrapper.tpl'), [
77                         'name' => 'search-header',
78                         '$title' => DI::l10n()->t('Search'),
79                         '$title_size' => 3,
80                         '$content' => HTML::search($search, 'search-box', false)
81                 ]);
82
83                 if (!$search) {
84                         return $o;
85                 }
86
87                 if (strpos($search, '#') === 0) {
88                         $tag = true;
89                         $search = substr($search, 1);
90                 }
91
92                 self::tryRedirectToProfile($search);
93
94                 if (strpos($search, '@') === 0 || strpos($search, '!') === 0) {
95                         return self::performContactSearch($search);
96                 }
97
98                 self::tryRedirectToPost($search);
99
100                 if (!empty($_GET['search-option'])) {
101                         switch ($_GET['search-option']) {
102                                 case 'fulltext':
103                                         break;
104                                 case 'tags':
105                                         $tag = true;
106                                         break;
107                                 case 'contacts':
108                                         return self::performContactSearch($search, '@');
109                                 case 'forums':
110                                         return self::performContactSearch($search, '!');
111                         }
112                 }
113
114                 $tag = $tag || DI::config()->get('system', 'only_tag_search');
115
116                 // Here is the way permissions work in the search module...
117                 // Only public posts can be shown
118                 // OR your own posts if you are a logged in member
119                 // No items will be shown if the member has a blocked profile wall.
120
121                 $pager = new Pager(DI::args()->getQueryString());
122
123                 if ($tag) {
124                         Logger::info('Start tag search.', ['q' => $search]);
125
126                         $condition = [
127                                 "(`uid` = 0 OR (`uid` = ? AND NOT `global`))
128                                 AND `otype` = ? AND `type` = ? AND `term` = ?",
129                                 local_user(), Term::OBJECT_TYPE_POST, Term::HASHTAG, $search
130                         ];
131                         $params = [
132                                 'order' => ['received' => true],
133                                 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]
134                         ];
135                         $terms = DBA::select('term', ['oid'], $condition, $params);
136
137                         $itemids = [];
138                         while ($term = DBA::fetch($terms)) {
139                                 $itemids[] = $term['oid'];
140                         }
141
142                         DBA::close($terms);
143
144                         if (!empty($itemids)) {
145                                 $params = ['order' => ['id' => true]];
146                                 $items = Item::selectForUser(local_user(), [], ['id' => $itemids], $params);
147                                 $r = Item::inArray($items);
148                         } else {
149                                 $r = [];
150                         }
151                 } else {
152                         Logger::info('Start fulltext search.', ['q' => $search]);
153
154                         $condition = [
155                                 "(`uid` = 0 OR (`uid` = ? AND NOT `global`))
156                                 AND `body` LIKE CONCAT('%',?,'%')",
157                                 local_user(), $search
158                         ];
159                         $params = [
160                                 'order' => ['id' => true],
161                                 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]
162                         ];
163                         $items = Item::selectForUser(local_user(), [], $condition, $params);
164                         $r = Item::inArray($items);
165                 }
166
167                 if (!DBA::isResult($r)) {
168                         info(DI::l10n()->t('No results.'));
169                         return $o;
170                 }
171
172                 if ($tag) {
173                         $title = DI::l10n()->t('Items tagged with: %s', $search);
174                 } else {
175                         $title = DI::l10n()->t('Results for: %s', $search);
176                 }
177
178                 $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), [
179                         '$title' => $title
180                 ]);
181
182                 Logger::info('Start Conversation.', ['q' => $search]);
183
184                 $o .= conversation(DI::app(), $r, $pager, 'search', false, false, 'commented', local_user());
185
186                 $o .= $pager->renderMinimal(count($r));
187
188                 return $o;
189         }
190
191         /**
192          * Tries to redirect to a local profile page based on the input.
193          *
194          * This method separates logged in and anonymous users. Logged in users can trigger contact probes to import
195          * non-existing contacts while anonymous users can only trigger a local lookup.
196          *
197          * Formats matched:
198          * - @user@domain
199          * - user@domain
200          * - Any fully-formed URL
201          *
202          * @param string  $search
203          * @throws HTTPException\InternalServerErrorException
204          * @throws \ImagickException
205          */
206         private static function tryRedirectToProfile(string $search)
207         {
208                 $isUrl = !empty(parse_url($search, PHP_URL_SCHEME));
209                 $isAddr = (bool)preg_match('/^@?([a-z0-9.-_]+@[a-z0-9.-_:]+)$/i', trim($search), $matches);
210
211                 if (!$isUrl && !$isAddr) {
212                         return;
213                 }
214
215                 if ($isAddr) {
216                         $search = $matches[1];
217                 }
218
219                 if (local_user()) {
220                         // User-specific contact URL/address search
221                         $contact_id = Contact::getIdForURL($search, local_user());
222                         if (!$contact_id) {
223                                 // User-specific contact URL/address search and probe
224                                 $contact_id = Contact::getIdForURL($search);
225                         }
226                 } else {
227                         // Cheaper local lookup for anonymous users, no probe
228                         if ($isAddr) {
229                                 $contact = Contact::selectFirst(['id' => 'cid'], ['addr' => $search, 'uid' => 0]);
230                         } else {
231                                 $contact = Contact::getDetailsByURL($search, 0, ['cid' => 0]);
232                         }
233
234                         if (DBA::isResult($contact)) {
235                                 $contact_id = $contact['cid'];
236                         }
237                 }
238
239                 if (!empty($contact_id)) {
240                         DI::baseUrl()->redirect('contact/' . $contact_id);
241                 }
242         }
243
244         /**
245          * Fetch/search a post by URL and redirects to its local representation if it was found.
246          *
247          * @param string  $search
248          * @throws HTTPException\InternalServerErrorException
249          */
250         private static function tryRedirectToPost(string $search)
251         {
252                 if (parse_url($search, PHP_URL_SCHEME) == '') {
253                         return;
254                 }
255
256                 if (local_user()) {
257                         // Post URL search
258                         $item_id = Item::fetchByLink($search, local_user());
259                         if (!$item_id) {
260                                 // If the user-specific search failed, we search and probe a public post
261                                 $item_id = Item::fetchByLink($search);
262                         }
263                 } else {
264                         // Cheaper local lookup for anonymous users, no probe
265                         $item_id = Item::searchByLink($search);
266                 }
267
268                 if (!empty($item_id)) {
269                         $item = Item::selectFirst(['guid'], ['id' => $item_id]);
270                         if (DBA::isResult($item)) {
271                                 DI::baseUrl()->redirect('display/' . $item['guid']);
272                         }
273                 }
274         }
275 }