3 namespace Friendica\Module\Search;
5 use Friendica\BaseModule;
6 use Friendica\Content\Widget;
7 use Friendica\Core\Hook;
8 use Friendica\Core\Logger;
9 use Friendica\Core\Protocol;
10 use Friendica\Core\Search;
11 use Friendica\Database\DBA;
13 use Friendica\Model\Contact;
14 use Friendica\Model\Item;
15 use Friendica\Network\HTTPException;
16 use Friendica\Util\Proxy as ProxyUtils;
17 use Friendica\Util\Strings;
20 * ACL selector json backend
22 * @package Friendica\Module\Search
24 class Acl extends BaseModule
26 const TYPE_GLOBAL_CONTACT = 'x';
27 const TYPE_MENTION_CONTACT = 'c';
28 const TYPE_MENTION_GROUP = 'g';
29 const TYPE_MENTION_CONTACT_GROUP = '';
30 const TYPE_MENTION_FORUM = 'f';
31 const TYPE_PRIVATE_MESSAGE = 'm';
32 const TYPE_ANY_CONTACT = 'a';
34 public static function rawContent(array $parameters = [])
37 throw new HTTPException\UnauthorizedException(DI::l10n()->t('You must be logged in to use this module.'));
40 $type = $_REQUEST['type'] ?? self::TYPE_MENTION_CONTACT_GROUP;
42 if ($type === self::TYPE_GLOBAL_CONTACT) {
43 $o = self::globalContactSearch();
45 $o = self::regularContactSearch($type);
52 private static function globalContactSearch()
54 // autocomplete for global contact search (e.g. navbar search)
55 $search = Strings::escapeTags(trim($_REQUEST['search']));
56 $mode = $_REQUEST['smode'];
57 $page = $_REQUEST['page'] ?? 1;
59 $r = Search::searchGlobalContact($search, $mode, $page);
64 'photo' => ProxyUtils::proxifyUrl($g['photo'], false, ProxyUtils::SIZE_MICRO),
65 'name' => htmlspecialchars($g['name']),
66 'nick' => $g['addr'] ?: $g['url'],
67 'network' => $g['network'],
69 'forum' => !empty($g['community']) ? 1 : 0,
74 'start' => ($page - 1) * 20,
82 private static function regularContactSearch(string $type)
84 $start = $_REQUEST['start'] ?? 0;
85 $count = $_REQUEST['count'] ?? 100;
86 $search = $_REQUEST['search'] ?? '';
87 $conv_id = $_REQUEST['conversation'] ?? null;
89 // For use with jquery.textcomplete for private mail completion
90 if (!empty($_REQUEST['query'])) {
92 $type = self::TYPE_PRIVATE_MESSAGE;
94 $search = $_REQUEST['query'];
97 Logger::info('ACL {action} - {subaction}', ['module' => 'acl', 'action' => 'content', 'subaction' => 'search', 'search' => $search, 'type' => $type, 'conversation' => $conv_id]);
103 $sql_extra = "AND `name` LIKE '%%" . DBA::escape($search) . "%%'";
104 $sql_extra2 = "AND (`attag` LIKE '%%" . DBA::escape($search) . "%%' OR `name` LIKE '%%" . DBA::escape($search) . "%%' OR `nick` LIKE '%%" . DBA::escape($search) . "%%')";
107 // count groups and contacts
109 if ($type == self::TYPE_MENTION_CONTACT_GROUP || $type == self::TYPE_MENTION_GROUP) {
110 $r = q("SELECT COUNT(*) AS g FROM `group` WHERE NOT `deleted` AND `uid` = %d $sql_extra",
113 $group_count = (int) $r[0]['g'];
116 $sql_extra2 .= ' ' . Widget::unavailableNetworks();
120 case self::TYPE_MENTION_CONTACT_GROUP:
121 case self::TYPE_MENTION_CONTACT:
122 // autocomplete for editor mentions
123 $r = q("SELECT COUNT(*) AS c FROM `contact`
124 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
125 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
126 AND `notify` != '' $sql_extra2",
129 $contact_count = (int) $r[0]['c'];
132 case self::TYPE_MENTION_FORUM:
133 // autocomplete for editor mentions of forums
134 $r = q("SELECT COUNT(*) AS c FROM `contact`
135 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
136 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
137 AND (`forum` OR `prv`)
138 AND `notify` != '' $sql_extra2",
141 $contact_count = (int) $r[0]['c'];
144 case self::TYPE_PRIVATE_MESSAGE:
145 // autocomplete for Private Messages
146 $r = q("SELECT COUNT(*) AS c FROM `contact`
147 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
148 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
149 AND `network` IN ('%s', '%s', '%s') $sql_extra2",
150 intval(local_user()),
151 DBA::escape(Protocol::ACTIVITYPUB),
152 DBA::escape(Protocol::DFRN),
153 DBA::escape(Protocol::DIASPORA)
155 $contact_count = (int) $r[0]['c'];
158 case self::TYPE_ANY_CONTACT:
160 // autocomplete for Contacts
161 $r = q("SELECT COUNT(*) AS c FROM `contact`
162 WHERE `uid` = %d AND NOT `self`
163 AND NOT `pending` AND NOT `deleted` $sql_extra2",
166 $contact_count = (int) $r[0]['c'];
170 $tot = $group_count + $contact_count;
175 if ($type == self::TYPE_MENTION_CONTACT_GROUP || $type == self::TYPE_MENTION_GROUP) {
176 /// @todo We should cache this query.
177 // This can be done when we can delete cache entries via wildcard
178 $r = q("SELECT `group`.`id`, `group`.`name`, GROUP_CONCAT(DISTINCT `group_member`.`contact-id` SEPARATOR ',') AS uids
180 INNER JOIN `group_member` ON `group_member`.`gid`=`group`.`id`
181 WHERE NOT `group`.`deleted` AND `group`.`uid` = %d
183 GROUP BY `group`.`name`, `group`.`id`
184 ORDER BY `group`.`name`
186 intval(local_user()),
194 'photo' => 'images/twopeople.png',
195 'name' => htmlspecialchars($g['name']),
196 'id' => intval($g['id']),
197 'uids' => array_map('intval', explode(',', $g['uids'])),
202 if ((count($groups) > 0) && ($search == '')) {
203 $groups[] = ['separator' => true];
209 case self::TYPE_MENTION_CONTACT_GROUP:
210 $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv`, (`prv` OR `forum`) AS `frm` FROM `contact`
211 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
212 AND NOT (`network` IN ('%s', '%s'))
215 intval(local_user()),
216 DBA::escape(Protocol::OSTATUS),
217 DBA::escape(Protocol::STATUSNET)
221 case self::TYPE_MENTION_CONTACT:
222 $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact`
223 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
224 AND NOT (`network` IN ('%s'))
227 intval(local_user()),
228 DBA::escape(Protocol::STATUSNET)
232 case self::TYPE_MENTION_FORUM:
233 $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact`
234 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
235 AND NOT (`network` IN ('%s'))
236 AND (`forum` OR `prv`)
239 intval(local_user()),
240 DBA::escape(Protocol::STATUSNET)
244 case self::TYPE_PRIVATE_MESSAGE:
245 $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr` FROM `contact`
246 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive`
247 AND `network` IN ('%s', '%s', '%s')
250 intval(local_user()),
251 DBA::escape(Protocol::ACTIVITYPUB),
252 DBA::escape(Protocol::DFRN),
253 DBA::escape(Protocol::DIASPORA)
257 case self::TYPE_ANY_CONTACT:
259 $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact`
260 WHERE `uid` = %d AND NOT `deleted` AND NOT `pending` AND NOT `archive`
268 if (DBA::isResult($r)) {
273 'photo' => ProxyUtils::proxifyUrl($g['micro'], false, ProxyUtils::SIZE_MICRO),
274 'name' => htmlspecialchars($g['name']),
275 'id' => intval($g['id']),
276 'network' => $g['network'],
278 'nick' => htmlentities(($g['attag'] ?? '') ?: $g['nick']),
279 'addr' => htmlentities(($g['addr'] ?? '') ?: $g['url']),
280 'forum' => !empty($g['forum']) || !empty($g['prv']) ? 1 : 0,
282 if ($entry['forum']) {
285 $contacts[] = $entry;
288 if (count($forums) > 0) {
290 $forums[] = ['separator' => true];
292 $contacts = array_merge($forums, $contacts);
296 $items = array_merge($groups, $contacts);
299 // In multi threaded posts the conv_id is not the parent of the whole thread
300 $parent_item = Item::selectFirst(['parent'], ['id' => $conv_id]);
301 if (DBA::isResult($parent_item)) {
302 $conv_id = $parent_item['parent'];
306 * if $conv_id is set, get unknown contacts in thread
307 * but first get known contacts url to filter them out
309 $known_contacts = array_map(function ($i) {
313 $unknown_contacts = [];
315 $condition = ["`parent` = ?", $conv_id];
316 $params = ['order' => ['author-name' => true]];
317 $authors = Item::selectForUser(local_user(), ['author-link'], $condition, $params);
319 while ($author = Item::fetch($authors)) {
320 $item_authors[$author['author-link']] = $author['author-link'];
322 DBA::close($authors);
324 foreach ($item_authors as $author) {
325 if (in_array($author, $known_contacts)) {
329 $contact = Contact::getDetailsByURL($author);
331 if (count($contact) > 0) {
332 $unknown_contacts[] = [
334 'photo' => ProxyUtils::proxifyUrl($contact['micro'], false, ProxyUtils::SIZE_MICRO),
335 'name' => htmlspecialchars($contact['name']),
336 'id' => intval($contact['cid']),
337 'network' => $contact['network'],
338 'link' => $contact['url'],
339 'nick' => htmlentities(($contact['nick'] ?? '') ?: $contact['addr']),
340 'addr' => htmlentities(($contact['addr'] ?? '') ?: $contact['url']),
341 'forum' => $contact['forum']
346 $items = array_merge($items, $unknown_contacts);
347 $tot += count($unknown_contacts);
355 'contacts' => $contacts,
361 Hook::callAll('acl_lookup_end', $results);
364 'tot' => $results['tot'],
365 'start' => $results['start'],
366 'count' => $results['count'],
367 'items' => $results['items'],