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;
12 use Friendica\Model\Contact;
13 use Friendica\Model\Item;
14 use Friendica\Network\HTTPException;
15 use Friendica\Util\Proxy as ProxyUtils;
16 use Friendica\Util\Strings;
19 * ACL selector json backend
21 * @package Friendica\Module\Search
23 class Acl extends BaseModule
25 const TYPE_GLOBAL_CONTACT = 'x';
26 const TYPE_MENTION_CONTACT = 'c';
27 const TYPE_MENTION_GROUP = 'g';
28 const TYPE_MENTION_CONTACT_GROUP = '';
29 const TYPE_MENTION_FORUM = 'f';
30 const TYPE_PRIVATE_MESSAGE = 'm';
31 const TYPE_ANY_CONTACT = 'a';
33 public static function rawContent(array $parameters = [])
36 throw new HTTPException\UnauthorizedException(DI::l10n()->t('You must be logged in to use this module.'));
39 $type = $_REQUEST['type'] ?? self::TYPE_MENTION_CONTACT_GROUP;
41 if ($type === self::TYPE_GLOBAL_CONTACT) {
42 $o = self::globalContactSearch();
44 $o = self::regularContactSearch($type);
51 private static function globalContactSearch()
53 // autocomplete for global contact search (e.g. navbar search)
54 $search = Strings::escapeTags(trim($_REQUEST['search']));
55 $mode = $_REQUEST['smode'];
56 $page = $_REQUEST['page'] ?? 1;
58 $r = Search::searchGlobalContact($search, $mode, $page);
63 'photo' => ProxyUtils::proxifyUrl($g['photo'], false, ProxyUtils::SIZE_MICRO),
64 'name' => htmlspecialchars($g['name']),
65 'nick' => $g['addr'] ?: $g['url'],
66 'network' => $g['network'],
68 'forum' => !empty($g['community']) ? 1 : 0,
73 'start' => ($page - 1) * 20,
81 private static function regularContactSearch(string $type)
83 $start = $_REQUEST['start'] ?? 0;
84 $count = $_REQUEST['count'] ?? 100;
85 $search = $_REQUEST['search'] ?? '';
86 $conv_id = $_REQUEST['conversation'] ?? null;
88 // For use with jquery.textcomplete for private mail completion
89 if (!empty($_REQUEST['query'])) {
91 $type = self::TYPE_PRIVATE_MESSAGE;
93 $search = $_REQUEST['query'];
96 Logger::info('ACL {action} - {subaction}', ['module' => 'acl', 'action' => 'content', 'subaction' => 'search', 'search' => $search, 'type' => $type, 'conversation' => $conv_id]);
102 $sql_extra = "AND `name` LIKE '%%" . DBA::escape($search) . "%%'";
103 $sql_extra2 = "AND (`attag` LIKE '%%" . DBA::escape($search) . "%%' OR `name` LIKE '%%" . DBA::escape($search) . "%%' OR `nick` LIKE '%%" . DBA::escape($search) . "%%')";
106 // count groups and contacts
108 if ($type == self::TYPE_MENTION_CONTACT_GROUP || $type == self::TYPE_MENTION_GROUP) {
109 $r = q("SELECT COUNT(*) AS g FROM `group` WHERE NOT `deleted` AND `uid` = %d $sql_extra",
112 $group_count = (int) $r[0]['g'];
115 $sql_extra2 .= ' ' . Widget::unavailableNetworks();
119 case self::TYPE_MENTION_CONTACT_GROUP:
120 case self::TYPE_MENTION_CONTACT:
121 // autocomplete for editor mentions
122 $r = q("SELECT COUNT(*) AS c FROM `contact`
123 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
124 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
125 AND `notify` != '' $sql_extra2",
128 $contact_count = (int) $r[0]['c'];
131 case self::TYPE_MENTION_FORUM:
132 // autocomplete for editor mentions of forums
133 $r = q("SELECT COUNT(*) AS c FROM `contact`
134 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
135 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
136 AND (`forum` OR `prv`)
137 AND `notify` != '' $sql_extra2",
140 $contact_count = (int) $r[0]['c'];
143 case self::TYPE_PRIVATE_MESSAGE:
144 // autocomplete for Private Messages
145 $r = q("SELECT COUNT(*) AS c FROM `contact`
146 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
147 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
148 AND `network` IN ('%s', '%s', '%s') $sql_extra2",
149 intval(local_user()),
150 DBA::escape(Protocol::ACTIVITYPUB),
151 DBA::escape(Protocol::DFRN),
152 DBA::escape(Protocol::DIASPORA)
154 $contact_count = (int) $r[0]['c'];
157 case self::TYPE_ANY_CONTACT:
159 // autocomplete for Contacts
160 $r = q("SELECT COUNT(*) AS c FROM `contact`
161 WHERE `uid` = %d AND NOT `self`
162 AND NOT `pending` AND NOT `deleted` $sql_extra2",
165 $contact_count = (int) $r[0]['c'];
169 $tot = $group_count + $contact_count;
174 if ($type == self::TYPE_MENTION_CONTACT_GROUP || $type == self::TYPE_MENTION_GROUP) {
175 /// @todo We should cache this query.
176 // This can be done when we can delete cache entries via wildcard
177 $r = q("SELECT `group`.`id`, `group`.`name`, GROUP_CONCAT(DISTINCT `group_member`.`contact-id` SEPARATOR ',') AS uids
179 INNER JOIN `group_member` ON `group_member`.`gid`=`group`.`id`
180 WHERE NOT `group`.`deleted` AND `group`.`uid` = %d
182 GROUP BY `group`.`name`, `group`.`id`
183 ORDER BY `group`.`name`
185 intval(local_user()),
193 'photo' => 'images/twopeople.png',
194 'name' => htmlspecialchars($g['name']),
195 'id' => intval($g['id']),
196 'uids' => array_map('intval', explode(',', $g['uids'])),
201 if ((count($groups) > 0) && ($search == '')) {
202 $groups[] = ['separator' => true];
208 case self::TYPE_MENTION_CONTACT_GROUP:
209 $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv`, (`prv` OR `forum`) AS `frm` FROM `contact`
210 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
211 AND NOT (`network` IN ('%s', '%s'))
214 intval(local_user()),
215 DBA::escape(Protocol::OSTATUS),
216 DBA::escape(Protocol::STATUSNET)
220 case self::TYPE_MENTION_CONTACT:
221 $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact`
222 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
223 AND NOT (`network` IN ('%s'))
226 intval(local_user()),
227 DBA::escape(Protocol::STATUSNET)
231 case self::TYPE_MENTION_FORUM:
232 $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact`
233 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
234 AND NOT (`network` IN ('%s'))
235 AND (`forum` OR `prv`)
238 intval(local_user()),
239 DBA::escape(Protocol::STATUSNET)
243 case self::TYPE_PRIVATE_MESSAGE:
244 $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr` FROM `contact`
245 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive`
246 AND `network` IN ('%s', '%s', '%s')
249 intval(local_user()),
250 DBA::escape(Protocol::ACTIVITYPUB),
251 DBA::escape(Protocol::DFRN),
252 DBA::escape(Protocol::DIASPORA)
256 case self::TYPE_ANY_CONTACT:
258 $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact`
259 WHERE `uid` = %d AND NOT `deleted` AND NOT `pending` AND NOT `archive`
267 if (DBA::isResult($r)) {
272 'photo' => ProxyUtils::proxifyUrl($g['micro'], false, ProxyUtils::SIZE_MICRO),
273 'name' => htmlspecialchars($g['name']),
274 'id' => intval($g['id']),
275 'network' => $g['network'],
277 'nick' => htmlentities(($g['attag'] ?? '') ?: $g['nick']),
278 'addr' => htmlentities(($g['addr'] ?? '') ?: $g['url']),
279 'forum' => !empty($g['forum']) || !empty($g['prv']) ? 1 : 0,
281 if ($entry['forum']) {
284 $contacts[] = $entry;
287 if (count($forums) > 0) {
289 $forums[] = ['separator' => true];
291 $contacts = array_merge($forums, $contacts);
295 $items = array_merge($groups, $contacts);
298 // In multi threaded posts the conv_id is not the parent of the whole thread
299 $parent_item = Item::selectFirst(['parent'], ['id' => $conv_id]);
300 if (DBA::isResult($parent_item)) {
301 $conv_id = $parent_item['parent'];
305 * if $conv_id is set, get unknown contacts in thread
306 * but first get known contacts url to filter them out
308 $known_contacts = array_map(function ($i) {
312 $unknown_contacts = [];
314 $condition = ["`parent` = ?", $conv_id];
315 $params = ['order' => ['author-name' => true]];
316 $authors = Item::selectForUser(local_user(), ['author-link'], $condition, $params);
318 while ($author = Item::fetch($authors)) {
319 $item_authors[$author['author-link']] = $author['author-link'];
321 DBA::close($authors);
323 foreach ($item_authors as $author) {
324 if (in_array($author, $known_contacts)) {
328 $contact = Contact::getDetailsByURL($author);
330 if (count($contact) > 0) {
331 $unknown_contacts[] = [
333 'photo' => ProxyUtils::proxifyUrl($contact['micro'], false, ProxyUtils::SIZE_MICRO),
334 'name' => htmlspecialchars($contact['name']),
335 'id' => intval($contact['cid']),
336 'network' => $contact['network'],
337 'link' => $contact['url'],
338 'nick' => htmlentities(($contact['nick'] ?? '') ?: $contact['addr']),
339 'addr' => htmlentities(($contact['addr'] ?? '') ?: $contact['url']),
340 'forum' => $contact['forum']
345 $items = array_merge($items, $unknown_contacts);
346 $tot += count($unknown_contacts);
354 'contacts' => $contacts,
360 Hook::callAll('acl_lookup_end', $results);
363 'tot' => $results['tot'],
364 'start' => $results['start'],
365 'count' => $results['count'],
366 'items' => $results['items'],