3 * @copyright Copyright (C) 2020, Friendica
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 namespace Friendica\Module\Search;
24 use Friendica\BaseModule;
25 use Friendica\Content\Widget;
26 use Friendica\Core\Hook;
27 use Friendica\Core\Logger;
28 use Friendica\Core\Protocol;
29 use Friendica\Core\Search;
30 use Friendica\Database\DBA;
32 use Friendica\Model\Contact;
33 use Friendica\Model\Item;
34 use Friendica\Model\Post;
35 use Friendica\Network\HTTPException;
36 use Friendica\Util\Strings;
39 * ACL selector json backend
41 * @package Friendica\Module\Search
43 class Acl extends BaseModule
45 const TYPE_GLOBAL_CONTACT = 'x';
46 const TYPE_MENTION_CONTACT = 'c';
47 const TYPE_MENTION_GROUP = 'g';
48 const TYPE_MENTION_CONTACT_GROUP = '';
49 const TYPE_MENTION_FORUM = 'f';
50 const TYPE_PRIVATE_MESSAGE = 'm';
51 const TYPE_ANY_CONTACT = 'a';
53 public static function rawContent(array $parameters = [])
56 throw new HTTPException\UnauthorizedException(DI::l10n()->t('You must be logged in to use this module.'));
59 $type = $_REQUEST['type'] ?? self::TYPE_MENTION_CONTACT_GROUP;
60 if ($type === self::TYPE_GLOBAL_CONTACT) {
61 $o = self::globalContactSearch();
63 $o = self::regularContactSearch($type);
70 private static function globalContactSearch()
72 // autocomplete for global contact search (e.g. navbar search)
73 $search = Strings::escapeTags(trim($_REQUEST['search']));
74 $mode = $_REQUEST['smode'];
75 $page = $_REQUEST['page'] ?? 1;
77 $result = Search::searchContact($search, $mode, $page);
80 foreach ($result as $contact) {
82 'photo' => Contact::getMicro($contact),
83 'name' => htmlspecialchars($contact['name']),
84 'nick' => $contact['addr'] ?: $contact['url'],
85 'network' => $contact['network'],
86 'link' => $contact['url'],
87 'forum' => $contact['contact-type'] == Contact::TYPE_COMMUNITY,
92 'start' => ($page - 1) * 20,
100 private static function regularContactSearch(string $type)
102 $start = $_REQUEST['start'] ?? 0;
103 $count = $_REQUEST['count'] ?? 100;
104 $search = $_REQUEST['search'] ?? '';
105 $conv_id = $_REQUEST['conversation'] ?? null;
107 // For use with jquery.textcomplete for private mail completion
108 if (!empty($_REQUEST['query'])) {
110 $type = self::TYPE_PRIVATE_MESSAGE;
112 $search = $_REQUEST['query'];
115 Logger::info('ACL {action} - {subaction}', ['module' => 'acl', 'action' => 'content', 'subaction' => 'search', 'search' => $search, 'type' => $type, 'conversation' => $conv_id]);
121 $sql_extra = "AND `name` LIKE '%%" . DBA::escape($search) . "%%'";
122 $sql_extra2 = "AND (`attag` LIKE '%%" . DBA::escape($search) . "%%' OR `name` LIKE '%%" . DBA::escape($search) . "%%' OR `nick` LIKE '%%" . DBA::escape($search) . "%%')";
125 // count groups and contacts
127 if ($type == self::TYPE_MENTION_CONTACT_GROUP || $type == self::TYPE_MENTION_GROUP) {
128 $r = q("SELECT COUNT(*) AS g FROM `group` WHERE NOT `deleted` AND `uid` = %d $sql_extra",
131 $group_count = (int) $r[0]['g'];
134 $sql_extra2 .= ' ' . Widget::unavailableNetworks();
138 case self::TYPE_MENTION_CONTACT_GROUP:
139 case self::TYPE_MENTION_CONTACT:
140 // autocomplete for editor mentions
141 $r = q("SELECT COUNT(*) AS c FROM `contact`
142 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
143 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
144 AND `notify` != '' $sql_extra2",
147 $contact_count = (int) $r[0]['c'];
150 case self::TYPE_MENTION_FORUM:
151 // autocomplete for editor mentions of forums
152 $r = q("SELECT COUNT(*) AS c FROM `contact`
153 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
154 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
155 AND (`forum` OR `prv`)
156 AND `notify` != '' $sql_extra2",
159 $contact_count = (int) $r[0]['c'];
162 case self::TYPE_PRIVATE_MESSAGE:
163 // autocomplete for Private Messages
164 $r = q("SELECT COUNT(*) AS c FROM `contact`
165 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
166 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
167 AND `network` IN ('%s', '%s', '%s') $sql_extra2",
168 intval(local_user()),
169 DBA::escape(Protocol::ACTIVITYPUB),
170 DBA::escape(Protocol::DFRN),
171 DBA::escape(Protocol::DIASPORA)
173 $contact_count = (int) $r[0]['c'];
176 case self::TYPE_ANY_CONTACT:
178 // autocomplete for Contacts
179 $r = q("SELECT COUNT(*) AS c FROM `contact`
180 WHERE `uid` = %d AND NOT `self`
181 AND NOT `pending` AND NOT `deleted` $sql_extra2",
184 $contact_count = (int) $r[0]['c'];
188 $tot = $group_count + $contact_count;
193 if ($type == self::TYPE_MENTION_CONTACT_GROUP || $type == self::TYPE_MENTION_GROUP) {
194 /// @todo We should cache this query.
195 // This can be done when we can delete cache entries via wildcard
196 $r = q("SELECT `group`.`id`, `group`.`name`, GROUP_CONCAT(DISTINCT `group_member`.`contact-id` SEPARATOR ',') AS uids
198 INNER JOIN `group_member` ON `group_member`.`gid`=`group`.`id`
199 WHERE NOT `group`.`deleted` AND `group`.`uid` = %d
201 GROUP BY `group`.`name`, `group`.`id`
202 ORDER BY `group`.`name`
204 intval(local_user()),
212 'photo' => 'images/twopeople.png',
213 'name' => htmlspecialchars($g['name']),
214 'id' => intval($g['id']),
215 'uids' => array_map('intval', explode(',', $g['uids'])),
220 if ((count($groups) > 0) && ($search == '')) {
221 $groups[] = ['separator' => true];
227 case self::TYPE_MENTION_CONTACT_GROUP:
228 $r = q("SELECT `id`, `name`, `nick`, `avatar`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv`, (`prv` OR `forum`) AS `frm` FROM `contact`
229 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
230 AND NOT (`network` IN ('%s', '%s'))
233 intval(local_user()),
234 DBA::escape(Protocol::OSTATUS),
235 DBA::escape(Protocol::STATUSNET)
239 case self::TYPE_MENTION_CONTACT:
240 $r = q("SELECT `id`, `name`, `nick`, `avatar`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact`
241 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
242 AND NOT (`network` IN ('%s'))
245 intval(local_user()),
246 DBA::escape(Protocol::STATUSNET)
250 case self::TYPE_MENTION_FORUM:
251 $r = q("SELECT `id`, `name`, `nick`, `avatar`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact`
252 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
253 AND NOT (`network` IN ('%s'))
254 AND (`forum` OR `prv`)
257 intval(local_user()),
258 DBA::escape(Protocol::STATUSNET)
262 case self::TYPE_PRIVATE_MESSAGE:
263 $r = q("SELECT `id`, `name`, `nick`, `avatar`, `micro`, `network`, `url`, `attag`, `addr` FROM `contact`
264 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive`
265 AND `network` IN ('%s', '%s', '%s')
268 intval(local_user()),
269 DBA::escape(Protocol::ACTIVITYPUB),
270 DBA::escape(Protocol::DFRN),
271 DBA::escape(Protocol::DIASPORA)
275 case self::TYPE_ANY_CONTACT:
277 $r = q("SELECT `id`, `name`, `nick`, `avatar`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv`, `avatar` FROM `contact`
278 WHERE `uid` = %d AND NOT `deleted` AND NOT `pending` AND NOT `archive`
286 if (DBA::isResult($r)) {
291 'photo' => Contact::getMicro($g),
292 'name' => htmlspecialchars($g['name']),
293 'id' => intval($g['id']),
294 'network' => $g['network'],
296 'nick' => htmlentities(($g['attag'] ?? '') ?: $g['nick']),
297 'addr' => htmlentities(($g['addr'] ?? '') ?: $g['url']),
298 'forum' => !empty($g['forum']) || !empty($g['prv']) ? 1 : 0,
300 if ($entry['forum']) {
303 $contacts[] = $entry;
306 if (count($forums) > 0) {
308 $forums[] = ['separator' => true];
310 $contacts = array_merge($forums, $contacts);
314 $items = array_merge($groups, $contacts);
317 // In multi threaded posts the conv_id is not the parent of the whole thread
318 $parent_item = Post::selectFirst(['parent'], ['id' => $conv_id]);
319 if (DBA::isResult($parent_item)) {
320 $conv_id = $parent_item['parent'];
324 * if $conv_id is set, get unknown contacts in thread
325 * but first get known contacts url to filter them out
327 $known_contacts = array_map(function ($i) {
331 $unknown_contacts = [];
333 $condition = ["`parent` = ?", $conv_id];
334 $params = ['order' => ['author-name' => true]];
335 $authors = Post::selectForUser(local_user(), ['author-link'], $condition, $params);
337 while ($author = Post::fetch($authors)) {
338 $item_authors[$author['author-link']] = $author['author-link'];
340 DBA::close($authors);
342 foreach ($item_authors as $author) {
343 if (in_array($author, $known_contacts)) {
347 $contact = Contact::getByURL($author, false, ['micro', 'name', 'id', 'network', 'nick', 'addr', 'url', 'forum', 'avatar']);
349 if (count($contact) > 0) {
350 $unknown_contacts[] = [
352 'photo' => Contact::getMicro($contact),
353 'name' => htmlspecialchars($contact['name']),
354 'id' => intval($contact['id']),
355 'network' => $contact['network'],
356 'link' => $contact['url'],
357 'nick' => htmlentities(($contact['nick'] ?? '') ?: $contact['addr']),
358 'addr' => htmlentities(($contact['addr'] ?? '') ?: $contact['url']),
359 'forum' => $contact['forum']
364 $items = array_merge($items, $unknown_contacts);
365 $tot += count($unknown_contacts);
373 'contacts' => $contacts,
379 Hook::callAll('acl_lookup_end', $results);
382 'tot' => $results['tot'],
383 'start' => $results['start'],
384 'count' => $results['count'],
385 'items' => $results['items'],