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\Network\HTTPException;
35 use Friendica\Util\Strings;
38 * ACL selector json backend
40 * @package Friendica\Module\Search
42 class Acl extends BaseModule
44 const TYPE_GLOBAL_CONTACT = 'x';
45 const TYPE_MENTION_CONTACT = 'c';
46 const TYPE_MENTION_GROUP = 'g';
47 const TYPE_MENTION_CONTACT_GROUP = '';
48 const TYPE_MENTION_FORUM = 'f';
49 const TYPE_PRIVATE_MESSAGE = 'm';
50 const TYPE_ANY_CONTACT = 'a';
52 public static function rawContent(array $parameters = [])
55 throw new HTTPException\UnauthorizedException(DI::l10n()->t('You must be logged in to use this module.'));
58 $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 $r = Search::searchGlobalContact($search, $mode, $page);
81 if (empty($g['name'])) {
82 DI::logger()->warning('Wrong result item from Search::searchGlobalContact', ['$g' => $g, '$search' => $search, '$mode' => $mode, '$page' => $page]);
85 $contact = Contact::getByURL($g['url']);
87 'photo' => Contact::getMicro($contact, $g['photo']),
88 'name' => htmlspecialchars($contact['name'] ?? $g['name']),
89 'nick' => $contact['nick'] ?? ($g['addr'] ?: $g['url']),
90 'network' => $contact['network'] ?? $g['network'],
92 'forum' => !empty($g['community']),
97 'start' => ($page - 1) * 20,
105 private static function regularContactSearch(string $type)
107 $start = $_REQUEST['start'] ?? 0;
108 $count = $_REQUEST['count'] ?? 100;
109 $search = $_REQUEST['search'] ?? '';
110 $conv_id = $_REQUEST['conversation'] ?? null;
112 // For use with jquery.textcomplete for private mail completion
113 if (!empty($_REQUEST['query'])) {
115 $type = self::TYPE_PRIVATE_MESSAGE;
117 $search = $_REQUEST['query'];
120 Logger::info('ACL {action} - {subaction}', ['module' => 'acl', 'action' => 'content', 'subaction' => 'search', 'search' => $search, 'type' => $type, 'conversation' => $conv_id]);
126 $sql_extra = "AND `name` LIKE '%%" . DBA::escape($search) . "%%'";
127 $sql_extra2 = "AND (`attag` LIKE '%%" . DBA::escape($search) . "%%' OR `name` LIKE '%%" . DBA::escape($search) . "%%' OR `nick` LIKE '%%" . DBA::escape($search) . "%%')";
130 // count groups and contacts
132 if ($type == self::TYPE_MENTION_CONTACT_GROUP || $type == self::TYPE_MENTION_GROUP) {
133 $r = q("SELECT COUNT(*) AS g FROM `group` WHERE NOT `deleted` AND `uid` = %d $sql_extra",
136 $group_count = (int) $r[0]['g'];
139 $sql_extra2 .= ' ' . Widget::unavailableNetworks();
143 case self::TYPE_MENTION_CONTACT_GROUP:
144 case self::TYPE_MENTION_CONTACT:
145 // autocomplete for editor mentions
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 `notify` != '' $sql_extra2",
152 $contact_count = (int) $r[0]['c'];
155 case self::TYPE_MENTION_FORUM:
156 // autocomplete for editor mentions of forums
157 $r = q("SELECT COUNT(*) AS c FROM `contact`
158 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
159 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
160 AND (`forum` OR `prv`)
161 AND `notify` != '' $sql_extra2",
164 $contact_count = (int) $r[0]['c'];
167 case self::TYPE_PRIVATE_MESSAGE:
168 // autocomplete for Private Messages
169 $r = q("SELECT COUNT(*) AS c FROM `contact`
170 WHERE `uid` = %d AND NOT `self` AND NOT `deleted`
171 AND NOT `blocked` AND NOT `pending` AND NOT `archive`
172 AND `network` IN ('%s', '%s', '%s') $sql_extra2",
173 intval(local_user()),
174 DBA::escape(Protocol::ACTIVITYPUB),
175 DBA::escape(Protocol::DFRN),
176 DBA::escape(Protocol::DIASPORA)
178 $contact_count = (int) $r[0]['c'];
181 case self::TYPE_ANY_CONTACT:
183 // autocomplete for Contacts
184 $r = q("SELECT COUNT(*) AS c FROM `contact`
185 WHERE `uid` = %d AND NOT `self`
186 AND NOT `pending` AND NOT `deleted` $sql_extra2",
189 $contact_count = (int) $r[0]['c'];
193 $tot = $group_count + $contact_count;
198 if ($type == self::TYPE_MENTION_CONTACT_GROUP || $type == self::TYPE_MENTION_GROUP) {
199 /// @todo We should cache this query.
200 // This can be done when we can delete cache entries via wildcard
201 $r = q("SELECT `group`.`id`, `group`.`name`, GROUP_CONCAT(DISTINCT `group_member`.`contact-id` SEPARATOR ',') AS uids
203 INNER JOIN `group_member` ON `group_member`.`gid`=`group`.`id`
204 WHERE NOT `group`.`deleted` AND `group`.`uid` = %d
206 GROUP BY `group`.`name`, `group`.`id`
207 ORDER BY `group`.`name`
209 intval(local_user()),
217 'photo' => 'images/twopeople.png',
218 'name' => htmlspecialchars($g['name']),
219 'id' => intval($g['id']),
220 'uids' => array_map('intval', explode(',', $g['uids'])),
225 if ((count($groups) > 0) && ($search == '')) {
226 $groups[] = ['separator' => true];
232 case self::TYPE_MENTION_CONTACT_GROUP:
233 $r = q("SELECT `id`, `name`, `nick`, `avatar`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv`, (`prv` OR `forum`) AS `frm` 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', '%s'))
238 intval(local_user()),
239 DBA::escape(Protocol::OSTATUS),
240 DBA::escape(Protocol::STATUSNET)
244 case self::TYPE_MENTION_CONTACT:
245 $r = q("SELECT `id`, `name`, `nick`, `avatar`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact`
246 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
247 AND NOT (`network` IN ('%s'))
250 intval(local_user()),
251 DBA::escape(Protocol::STATUSNET)
255 case self::TYPE_MENTION_FORUM:
256 $r = q("SELECT `id`, `name`, `nick`, `avatar`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact`
257 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
258 AND NOT (`network` IN ('%s'))
259 AND (`forum` OR `prv`)
262 intval(local_user()),
263 DBA::escape(Protocol::STATUSNET)
267 case self::TYPE_PRIVATE_MESSAGE:
268 $r = q("SELECT `id`, `name`, `nick`, `avatar`, `micro`, `network`, `url`, `attag`, `addr` FROM `contact`
269 WHERE `uid` = %d AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `archive`
270 AND `network` IN ('%s', '%s', '%s')
273 intval(local_user()),
274 DBA::escape(Protocol::ACTIVITYPUB),
275 DBA::escape(Protocol::DFRN),
276 DBA::escape(Protocol::DIASPORA)
280 case self::TYPE_ANY_CONTACT:
282 $r = q("SELECT `id`, `name`, `nick`, `avatar`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv`, `avatar` FROM `contact`
283 WHERE `uid` = %d AND NOT `deleted` AND NOT `pending` AND NOT `archive`
291 if (DBA::isResult($r)) {
296 'photo' => Contact::getMicro($g),
297 'name' => htmlspecialchars($g['name']),
298 'id' => intval($g['id']),
299 'network' => $g['network'],
301 'nick' => htmlentities(($g['attag'] ?? '') ?: $g['nick']),
302 'addr' => htmlentities(($g['addr'] ?? '') ?: $g['url']),
303 'forum' => !empty($g['forum']) || !empty($g['prv']) ? 1 : 0,
305 if ($entry['forum']) {
308 $contacts[] = $entry;
311 if (count($forums) > 0) {
313 $forums[] = ['separator' => true];
315 $contacts = array_merge($forums, $contacts);
319 $items = array_merge($groups, $contacts);
322 // In multi threaded posts the conv_id is not the parent of the whole thread
323 $parent_item = Item::selectFirst(['parent'], ['id' => $conv_id]);
324 if (DBA::isResult($parent_item)) {
325 $conv_id = $parent_item['parent'];
329 * if $conv_id is set, get unknown contacts in thread
330 * but first get known contacts url to filter them out
332 $known_contacts = array_map(function ($i) {
336 $unknown_contacts = [];
338 $condition = ["`parent` = ?", $conv_id];
339 $params = ['order' => ['author-name' => true]];
340 $authors = Item::selectForUser(local_user(), ['author-link'], $condition, $params);
342 while ($author = Item::fetch($authors)) {
343 $item_authors[$author['author-link']] = $author['author-link'];
345 DBA::close($authors);
347 foreach ($item_authors as $author) {
348 if (in_array($author, $known_contacts)) {
352 $contact = Contact::getByURL($author, false, ['micro', 'name', 'id', 'network', 'nick', 'addr', 'url', 'forum', 'avatar']);
354 if (count($contact) > 0) {
355 $unknown_contacts[] = [
357 'photo' => Contact::getMicro($contact),
358 'name' => htmlspecialchars($contact['name']),
359 'id' => intval($contact['cid']),
360 'network' => $contact['network'],
361 'link' => $contact['url'],
362 'nick' => htmlentities(($contact['nick'] ?? '') ?: $contact['addr']),
363 'addr' => htmlentities(($contact['addr'] ?? '') ?: $contact['url']),
364 'forum' => $contact['forum']
369 $items = array_merge($items, $unknown_contacts);
370 $tot += count($unknown_contacts);
378 'contacts' => $contacts,
384 Hook::callAll('acl_lookup_end', $results);
387 'tot' => $results['tot'],
388 'start' => $results['start'],
389 'count' => $results['count'],
390 'items' => $results['items'],