3 * @copyright Copyright (C) 2010-2021, the Friendica project
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;
24 use Friendica\BaseModule;
25 use Friendica\Content\ContactSelector;
26 use Friendica\Content\Nav;
27 use Friendica\Content\Pager;
28 use Friendica\Content\Text\BBCode;
29 use Friendica\Content\Widget;
30 use Friendica\Core\ACL;
31 use Friendica\Core\Hook;
32 use Friendica\Core\Protocol;
33 use Friendica\Core\Renderer;
34 use Friendica\Core\Theme;
35 use Friendica\Core\Worker;
36 use Friendica\Database\DBA;
39 use Friendica\Model\User;
40 use Friendica\Module\Security\Login;
41 use Friendica\Network\HTTPException\BadRequestException;
42 use Friendica\Network\HTTPException\NotFoundException;
43 use Friendica\Util\DateTimeFormat;
44 use Friendica\Util\Strings;
47 * Manages and show Contacts and their content
49 class Contact extends BaseModule
51 const TAB_CONVERSATIONS = 1;
53 const TAB_PROFILE = 3;
54 const TAB_CONTACTS = 4;
55 const TAB_ADVANCED = 5;
57 private static function batchActions()
59 if (empty($_POST['contact_batch']) || !is_array($_POST['contact_batch'])) {
63 $contacts_id = $_POST['contact_batch'];
65 $stmt = DBA::select('contact', ['id', 'archive'], ['id' => $contacts_id, 'uid' => local_user(), 'self' => false, 'deleted' => false]);
66 $orig_records = DBA::toArray($stmt);
69 foreach ($orig_records as $orig_record) {
70 $contact_id = $orig_record['id'];
71 if (!empty($_POST['contacts_batch_update'])) {
72 self::updateContactFromPoll($contact_id);
75 if (!empty($_POST['contacts_batch_block'])) {
76 self::blockContact($contact_id);
79 if (!empty($_POST['contacts_batch_ignore'])) {
80 self::ignoreContact($contact_id);
83 if (!empty($_POST['contacts_batch_archive'])
84 && self::archiveContact($contact_id, $orig_record)
88 if (!empty($_POST['contacts_batch_drop'])) {
89 self::dropContact($orig_record);
93 if ($count_actions > 0) {
94 info(DI::l10n()->tt('%d contact edited.', '%d contacts edited.', $count_actions));
97 DI::baseUrl()->redirect('contact');
100 public static function post(array $parameters = [])
108 // @TODO: Replace with parameter from router
109 if (DI::args()->getArgv()[1] === 'batch') {
110 self::batchActions();
114 // @TODO: Replace with parameter from router
115 $contact_id = intval(DI::args()->getArgv()[1]);
120 if (!DBA::exists('contact', ['id' => $contact_id, 'uid' => local_user(), 'deleted' => false])) {
121 notice(DI::l10n()->t('Could not access contact record.'));
122 DI::baseUrl()->redirect('contact');
123 return; // NOTREACHED
126 Hook::callAll('contact_edit_post', $_POST);
128 $hidden = !empty($_POST['hidden']);
130 $notify = !empty($_POST['notify']);
132 $fetch_further_information = intval($_POST['fetch_further_information'] ?? 0);
134 $remote_self = $_POST['remote_self'] ?? false;
136 $ffi_keyword_denylist = Strings::escapeHtml(trim($_POST['ffi_keyword_denylist'] ?? ''));
138 $priority = intval($_POST['poll'] ?? 0);
139 if ($priority > 5 || $priority < 0) {
143 $info = Strings::escapeHtml(trim($_POST['info'] ?? ''));
145 $r = DBA::update('contact', [
146 'priority' => $priority,
149 'notify_new_posts' => $notify,
150 'fetch_further_information' => $fetch_further_information,
151 'remote_self' => $remote_self,
152 'ffi_keyword_denylist' => $ffi_keyword_denylist],
153 ['id' => $contact_id, 'uid' => local_user()]
156 if (!DBA::isResult($r)) {
157 notice(DI::l10n()->t('Failed to update contact record.'));
162 /* contact actions */
164 private static function updateContactFromPoll($contact_id)
166 $contact = DBA::selectFirst('contact', ['uid', 'url', 'network'], ['id' => $contact_id, 'uid' => local_user(), 'deleted' => false]);
167 if (!DBA::isResult($contact)) {
171 if ($contact['network'] == Protocol::OSTATUS) {
172 $result = Model\Contact::createFromProbe($contact['uid'], $contact['url'], $contact['network']);
174 if ($result['success']) {
175 DBA::update('contact', ['subhub' => 1], ['id' => $contact_id]);
178 // pull feed and consume it, which should subscribe to the hub.
179 Worker::add(PRIORITY_HIGH, 'OnePoll', $contact_id, 'force');
181 Worker::add(PRIORITY_HIGH, 'UpdateContact', $contact_id);
185 private static function updateContactFromProbe($contact_id)
187 $contact = DBA::selectFirst('contact', ['url'], ['id' => $contact_id, 'uid' => [0, local_user()], 'deleted' => false]);
188 if (!DBA::isResult($contact)) {
192 // Update the entry in the contact table
193 Model\Contact::updateFromProbe($contact_id);
197 * Toggles the blocked status of a contact identified by id.
202 private static function blockContact($contact_id)
204 $blocked = !Model\Contact\User::isBlocked($contact_id, local_user());
205 Model\Contact\User::setBlocked($contact_id, local_user(), $blocked);
209 * Toggles the ignored status of a contact identified by id.
214 private static function ignoreContact($contact_id)
216 $ignored = !Model\Contact\User::isIgnored($contact_id, local_user());
217 Model\Contact\User::setIgnored($contact_id, local_user(), $ignored);
221 * Toggles the archived status of a contact identified by id.
222 * If the current status isn't provided, this will always archive the contact.
225 * @param $orig_record
229 private static function archiveContact($contact_id, $orig_record)
231 $archived = empty($orig_record['archive']);
232 $r = DBA::update('contact', ['archive' => $archived], ['id' => $contact_id, 'uid' => local_user()]);
234 return DBA::isResult($r);
237 private static function dropContact($orig_record)
239 $owner = Model\User::getOwnerDataById(local_user());
240 if (!DBA::isResult($owner)) {
244 Model\Contact::terminateFriendship($owner, $orig_record, true);
245 Model\Contact::remove($orig_record['id']);
248 public static function content(array $parameters = [], $update = 0)
251 return Login::form($_SERVER['REQUEST_URI']);
256 $search = Strings::escapeTags(trim($_GET['search'] ?? ''));
257 $nets = Strings::escapeTags(trim($_GET['nets'] ?? ''));
258 $rel = Strings::escapeTags(trim($_GET['rel'] ?? ''));
259 $group = Strings::escapeTags(trim($_GET['group'] ?? ''));
261 $accounttype = $_GET['accounttype'] ?? '';
262 $accounttypeid = User::getAccountTypeByString($accounttype);
266 $page->registerFooterScript(Theme::getPathForFile('asset/typeahead.js/dist/typeahead.bundle.js'));
267 $page->registerFooterScript(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.js'));
268 $page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css'));
269 $page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput-typeahead.css'));
272 // @TODO: Replace with parameter from router
273 if (DI::args()->getArgc() == 2 && intval(DI::args()->getArgv()[1])
274 || DI::args()->getArgc() == 3 && intval(DI::args()->getArgv()[1]) && in_array(DI::args()->getArgv()[2], ['posts', 'conversations'])
276 $contact_id = intval(DI::args()->getArgv()[1]);
278 // Ensure to use the user contact when the public contact was provided
279 $data = Model\Contact::getPublicAndUserContactID($contact_id, local_user());
280 if (!empty($data['user']) && ($contact_id == $data['public'])) {
281 $contact_id = $data['user'];
284 $contact = DBA::selectFirst('contact', [], ['id' => $contact_id, 'uid' => [0, local_user()], 'deleted' => false]);
286 // Don't display contacts that are about to be deleted
287 if ($contact['network'] == Protocol::PHANTOM) {
292 if (DBA::isResult($contact)) {
293 if ($contact['self']) {
294 // @TODO: Replace with parameter from router
295 if ((DI::args()->getArgc() == 3) && intval(DI::args()->getArgv()[1]) && in_array(DI::args()->getArgv()[2], ['posts', 'conversations'])) {
296 DI::baseUrl()->redirect('profile/' . $contact['nick']);
298 DI::baseUrl()->redirect('profile/' . $contact['nick'] . '/profile');
302 $vcard_widget = Widget\VCard::getHTML($contact);
304 $findpeople_widget = '';
306 $account_widget = '';
307 $networks_widget = '';
310 if ($contact['uid'] != 0) {
311 $groups_widget = Model\Group::sidebarWidget('contact', 'group', 'full', 'everyone', $contact_id);
317 $findpeople_widget = Widget::findPeople();
318 if (isset($_GET['add'])) {
319 $follow_widget = Widget::follow($_GET['add']);
321 $follow_widget = Widget::follow();
324 $account_widget = Widget::accounttypes($_SERVER['REQUEST_URI'], $accounttype);
325 $networks_widget = Widget::networks($_SERVER['REQUEST_URI'], $nets);
326 $rel_widget = Widget::contactRels($_SERVER['REQUEST_URI'], $rel);
327 $groups_widget = Widget::groups($_SERVER['REQUEST_URI'], $group);
330 DI::page()['aside'] .= $vcard_widget . $findpeople_widget . $follow_widget . $account_widget . $groups_widget . $networks_widget . $rel_widget;
332 $tpl = Renderer::getMarkupTemplate('contacts-head.tpl');
333 DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
334 '$baseurl' => DI::baseUrl()->get(true),
338 Nav::setSelected('contact');
341 notice(DI::l10n()->t('Permission denied.'));
342 return Login::form();
345 if (DI::args()->getArgc() == 3) {
346 $contact_id = intval(DI::args()->getArgv()[1]);
348 throw new BadRequestException();
351 // @TODO: Replace with parameter from router
352 $cmd = DI::args()->getArgv()[2];
354 $orig_record = DBA::selectFirst('contact', [], ['id' => $contact_id, 'uid' => [0, local_user()], 'self' => false, 'deleted' => false]);
355 if (!DBA::isResult($orig_record)) {
356 throw new NotFoundException(DI::l10n()->t('Contact not found'));
359 if ($cmd === 'update' && ($orig_record['uid'] != 0)) {
360 self::updateContactFromPoll($contact_id);
361 DI::baseUrl()->redirect('contact/' . $contact_id);
365 if ($cmd === 'updateprofile') {
366 self::updateContactFromProbe($contact_id);
367 DI::baseUrl()->redirect('contact/' . $contact_id);
371 if ($cmd === 'block') {
372 if (public_contact() === $contact_id) {
373 throw new BadRequestException(DI::l10n()->t('You can\'t block yourself'));
376 self::blockContact($contact_id);
378 $blocked = Model\Contact\User::isBlocked($contact_id, local_user());
379 info(($blocked ? DI::l10n()->t('Contact has been blocked') : DI::l10n()->t('Contact has been unblocked')));
381 DI::baseUrl()->redirect('contact/' . $contact_id);
385 if ($cmd === 'ignore') {
386 if (public_contact() === $contact_id) {
387 throw new BadRequestException(DI::l10n()->t('You can\'t ignore yourself'));
390 self::ignoreContact($contact_id);
392 $ignored = Model\Contact\User::isIgnored($contact_id, local_user());
393 info(($ignored ? DI::l10n()->t('Contact has been ignored') : DI::l10n()->t('Contact has been unignored')));
395 DI::baseUrl()->redirect('contact/' . $contact_id);
399 if ($cmd === 'archive' && ($orig_record['uid'] != 0)) {
400 $r = self::archiveContact($contact_id, $orig_record);
402 $archived = (($orig_record['archive']) ? 0 : 1);
403 info((($archived) ? DI::l10n()->t('Contact has been archived') : DI::l10n()->t('Contact has been unarchived')));
406 DI::baseUrl()->redirect('contact/' . $contact_id);
410 if ($cmd === 'drop' && ($orig_record['uid'] != 0)) {
411 // Check if we should do HTML-based delete confirmation
412 if (!empty($_REQUEST['confirm'])) {
413 DI::page()['aside'] = '';
415 return Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_drop_confirm.tpl'), [
416 '$header' => DI::l10n()->t('Drop contact'),
417 '$contact' => self::getContactTemplateVars($orig_record),
419 '$message' => DI::l10n()->t('Do you really want to delete this contact?'),
420 '$confirm' => DI::l10n()->t('Yes'),
421 '$confirm_url' => DI::args()->getCommand(),
422 '$confirm_name' => 'confirmed',
423 '$cancel' => DI::l10n()->t('Cancel'),
426 // Now check how the user responded to the confirmation query
427 if (!empty($_REQUEST['canceled'])) {
428 DI::baseUrl()->redirect('contact');
431 self::dropContact($orig_record);
432 info(DI::l10n()->t('Contact has been removed.'));
434 DI::baseUrl()->redirect('contact');
437 if ($cmd === 'posts') {
438 return self::getPostsHTML($a, $contact_id);
440 if ($cmd === 'conversations') {
441 return self::getConversationsHMTL($a, $contact_id, $update);
445 $_SESSION['return_path'] = DI::args()->getQueryString();
447 if (!empty($contact)) {
448 DI::page()['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_head.tpl'), [
449 '$baseurl' => DI::baseUrl()->get(true),
452 $contact['blocked'] = Model\Contact\User::isBlocked($contact['id'], local_user());
453 $contact['readonly'] = Model\Contact\User::isIgnored($contact['id'], local_user());
456 switch ($contact['rel']) {
457 case Model\Contact::FRIEND:
458 $relation_text = DI::l10n()->t('You are mutual friends with %s');
461 case Model\Contact::FOLLOWER;
462 $relation_text = DI::l10n()->t('You are sharing with %s');
465 case Model\Contact::SHARING;
466 $relation_text = DI::l10n()->t('%s is sharing with you');
473 if ($contact['uid'] == 0) {
477 if (!in_array($contact['network'], array_merge(Protocol::FEDERATED, [Protocol::TWITTER]))) {
481 $relation_text = sprintf($relation_text, $contact['name']);
483 $url = Model\Contact::magicLinkByContact($contact);
484 if (strpos($url, 'redir/') === 0) {
485 $sparkle = ' class="sparkle" ';
490 $insecure = DI::l10n()->t('Private communications are not available for this contact.');
492 $last_update = (($contact['last-update'] <= DBA::NULL_DATETIME) ? DI::l10n()->t('Never') : DateTimeFormat::local($contact['last-update'], 'D, j M Y, g:i A'));
494 if ($contact['last-update'] > DBA::NULL_DATETIME) {
495 $last_update .= ' ' . ($contact['failed'] ? DI::l10n()->t('(Update was not successful)') : DI::l10n()->t('(Update was successful)'));
497 $lblsuggest = (($contact['network'] === Protocol::DFRN) ? DI::l10n()->t('Suggest friends') : '');
499 $poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
501 $nettype = DI::l10n()->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']));
504 $tab_str = self::getTabsHTML($contact, self::TAB_PROFILE);
506 $lost_contact = (($contact['archive'] && $contact['term-date'] > DBA::NULL_DATETIME && $contact['term-date'] < DateTimeFormat::utcNow()) ? DI::l10n()->t('Communications lost with this contact!') : '');
508 $fetch_further_information = null;
509 if ($contact['network'] == Protocol::FEED) {
510 $fetch_further_information = [
511 'fetch_further_information',
512 DI::l10n()->t('Fetch further information for feeds'),
513 $contact['fetch_further_information'],
514 DI::l10n()->t('Fetch information like preview pictures, title and teaser from the feed item. You can activate this if the feed doesn\'t contain much text. Keywords are taken from the meta header in the feed item and are posted as hash tags.'),
516 '0' => DI::l10n()->t('Disabled'),
517 '1' => DI::l10n()->t('Fetch information'),
518 '3' => DI::l10n()->t('Fetch keywords'),
519 '2' => DI::l10n()->t('Fetch information and keywords')
524 // Disable remote self for everything except feeds.
525 // There is an issue when you repeat an item from maybe twitter and you got comments from friendica and twitter
526 // Problem is, you couldn't reply to both networks.
527 $allow_remote_self = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::FEED, Protocol::DFRN, Protocol::DIASPORA, Protocol::TWITTER])
528 && DI::config()->get('system', 'allow_users_remote_self');
530 if ($contact['network'] == Protocol::FEED) {
531 $remote_self_options = [Model\Contact::MIRROR_DEACTIVATED => DI::l10n()->t('No mirroring'),
532 Model\Contact::MIRROR_FORWARDED => DI::l10n()->t('Mirror as forwarded posting'),
533 Model\Contact::MIRROR_OWN_POST => DI::l10n()->t('Mirror as my own posting')];
534 } elseif (in_array($contact['network'], [Protocol::ACTIVITYPUB])) {
535 $remote_self_options = [Model\Contact::MIRROR_DEACTIVATED => DI::l10n()->t('No mirroring'),
536 Model\Contact::MIRROR_NATIVE_RESHARE => DI::l10n()->t('Native reshare')];
537 } elseif (in_array($contact['network'], [Protocol::DFRN])) {
538 $remote_self_options = [Model\Contact::MIRROR_DEACTIVATED => DI::l10n()->t('No mirroring'),
539 Model\Contact::MIRROR_OWN_POST => DI::l10n()->t('Mirror as my own posting'),
540 Model\Contact::MIRROR_NATIVE_RESHARE => DI::l10n()->t('Native reshare')];
542 $remote_self_options = [Model\Contact::MIRROR_DEACTIVATED => DI::l10n()->t('No mirroring'),
543 Model\Contact::MIRROR_OWN_POST => DI::l10n()->t('Mirror as my own posting')];
546 $poll_interval = null;
547 if ((($contact['network'] == Protocol::FEED) && !DI::config()->get('system', 'adjust_poll_frequency')) || ($contact['network']== Protocol::MAIL)) {
548 $poll_interval = ContactSelector::pollInterval($contact['priority'], !$poll_enabled);
551 // Load contactact related actions like hide, suggest, delete and others
552 $contact_actions = self::getContactActions($contact);
554 if ($contact['uid'] != 0) {
555 $lbl_info1 = DI::l10n()->t('Contact Information / Notes');
556 $contact_settings_label = DI::l10n()->t('Contact Settings');
559 $contact_settings_label = null;
562 $tpl = Renderer::getMarkupTemplate('contact_edit.tpl');
563 $o .= Renderer::replaceMacros($tpl, [
564 '$header' => DI::l10n()->t('Contact'),
565 '$tab_str' => $tab_str,
566 '$submit' => DI::l10n()->t('Submit'),
567 '$lbl_info1' => $lbl_info1,
568 '$lbl_info2' => DI::l10n()->t('Their personal note'),
569 '$reason' => trim(Strings::escapeTags($contact['reason'])),
570 '$infedit' => DI::l10n()->t('Edit contact notes'),
571 '$common_link' => 'contact/' . $contact['id'] . '/contacts/common',
572 '$relation_text' => $relation_text,
573 '$visit' => DI::l10n()->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
574 '$blockunblock' => DI::l10n()->t('Block/Unblock contact'),
575 '$ignorecont' => DI::l10n()->t('Ignore contact'),
576 '$lblrecent' => DI::l10n()->t('View conversations'),
577 '$lblsuggest' => $lblsuggest,
578 '$nettype' => $nettype,
579 '$poll_interval' => $poll_interval,
580 '$poll_enabled' => $poll_enabled,
581 '$lastupdtext' => DI::l10n()->t('Last update:'),
582 '$lost_contact' => $lost_contact,
583 '$updpub' => DI::l10n()->t('Update public posts'),
584 '$last_update' => $last_update,
585 '$udnow' => DI::l10n()->t('Update now'),
586 '$contact_id' => $contact['id'],
587 '$block_text' => ($contact['blocked'] ? DI::l10n()->t('Unblock') : DI::l10n()->t('Block')),
588 '$ignore_text' => ($contact['readonly'] ? DI::l10n()->t('Unignore') : DI::l10n()->t('Ignore')),
589 '$insecure' => (in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::MAIL, Protocol::DIASPORA]) ? '' : $insecure),
590 '$info' => $contact['info'],
591 '$cinfo' => ['info', '', $contact['info'], ''],
592 '$blocked' => ($contact['blocked'] ? DI::l10n()->t('Currently blocked') : ''),
593 '$ignored' => ($contact['readonly'] ? DI::l10n()->t('Currently ignored') : ''),
594 '$archived' => ($contact['archive'] ? DI::l10n()->t('Currently archived') : ''),
595 '$pending' => ($contact['pending'] ? DI::l10n()->t('Awaiting connection acknowledge') : ''),
596 '$hidden' => ['hidden', DI::l10n()->t('Hide this contact from others'), ($contact['hidden'] == 1), DI::l10n()->t('Replies/likes to your public posts <strong>may</strong> still be visible')],
597 '$notify' => ['notify', DI::l10n()->t('Notification for new posts'), ($contact['notify_new_posts'] == 1), DI::l10n()->t('Send a notification of every new post of this contact')],
598 '$fetch_further_information' => $fetch_further_information,
599 '$ffi_keyword_denylist' => ['ffi_keyword_denylist', DI::l10n()->t('Keyword Deny List'), $contact['ffi_keyword_denylist'], DI::l10n()->t('Comma separated list of keywords that should not be converted to hashtags, when "Fetch information and keywords" is selected')],
600 '$photo' => Model\Contact::getPhoto($contact),
601 '$name' => $contact['name'],
602 '$sparkle' => $sparkle,
604 '$profileurllabel'=> DI::l10n()->t('Profile URL'),
605 '$profileurl' => $contact['url'],
606 '$account_type' => Model\Contact::getAccountType($contact),
607 '$location' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['location']),
608 '$location_label' => DI::l10n()->t('Location:'),
609 '$xmpp' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['xmpp']),
610 '$xmpp_label' => DI::l10n()->t('XMPP:'),
611 '$about' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['about'], BBCode::EXTERNAL),
612 '$about_label' => DI::l10n()->t('About:'),
613 '$keywords' => $contact['keywords'],
614 '$keywords_label' => DI::l10n()->t('Tags:'),
615 '$contact_action_button' => DI::l10n()->t('Actions'),
616 '$contact_actions'=> $contact_actions,
617 '$contact_status' => DI::l10n()->t('Status'),
618 '$contact_settings_label' => $contact_settings_label,
619 '$contact_profile_label' => DI::l10n()->t('Profile'),
620 '$allow_remote_self' => $allow_remote_self,
621 '$remote_self' => ['remote_self',
622 DI::l10n()->t('Mirror postings from this contact'),
623 $contact['remote_self'],
624 DI::l10n()->t('Mark this contact as remote_self, this will cause friendica to repost new entries from this contact.'),
629 $arr = ['contact' => $contact, 'output' => $o];
631 Hook::callAll('contact_edit', $arr);
633 return $arr['output'];
636 $sql_values = [local_user()];
638 // @TODO: Replace with parameter from router
639 $type = DI::args()->getArgv()[1] ?? '';
643 $sql_extra = " AND EXISTS(SELECT `id` from `user-contact` WHERE `contact`.`id` = `user-contact`.`cid` and `user-contact`.`uid` = ? and `user-contact`.`blocked`)";
644 // This makes the query look for contact.uid = 0
645 array_unshift($sql_values, 0);
648 $sql_extra = " AND `hidden` AND NOT `blocked` AND NOT `pending`";
651 $sql_extra = " AND EXISTS(SELECT `id` from `user-contact` WHERE `contact`.`id` = `user-contact`.`cid` and `user-contact`.`uid` = ? and `user-contact`.`ignored`)";
652 // This makes the query look for contact.uid = 0
653 array_unshift($sql_values, 0);
656 $sql_extra = " AND `archive` AND NOT `blocked` AND NOT `pending`";
659 $sql_extra = " AND `pending` AND NOT `archive` AND NOT `failed` AND ((`rel` = ?)
660 OR EXISTS (SELECT `id` FROM `intro` WHERE `contact-id` = `contact`.`id` AND NOT `ignore`))";
661 $sql_values[] = Model\Contact::SHARING;
664 $sql_extra = " AND NOT `archive` AND NOT `blocked` AND NOT `pending`";
668 if (isset($accounttypeid)) {
669 $sql_extra .= " AND `contact-type` = ?";
670 $sql_values[] = $accounttypeid;
677 $search_hdr = $search;
678 $search_txt = preg_quote($search);
679 $sql_extra .= " AND (name REGEXP ? OR url REGEXP ? OR nick REGEXP ?)";
680 $sql_values[] = $search_txt;
681 $sql_values[] = $search_txt;
682 $sql_values[] = $search_txt;
686 $sql_extra .= " AND network = ? ";
687 $sql_values[] = $nets;
692 $sql_extra .= " AND `rel` IN (?, ?)";
693 $sql_values[] = Model\Contact::FOLLOWER;
694 $sql_values[] = Model\Contact::FRIEND;
697 $sql_extra .= " AND `rel` IN (?, ?)";
698 $sql_values[] = Model\Contact::SHARING;
699 $sql_values[] = Model\Contact::FRIEND;
702 $sql_extra .= " AND `rel` = ?";
703 $sql_values[] = Model\Contact::FRIEND;
708 $sql_extra = " AND EXISTS(SELECT `id` FROM `group_member` WHERE `gid` = ? AND `contact`.`id` = `contact-id`)";
709 $sql_values[] = $group;
713 $stmt = DBA::p("SELECT COUNT(*) AS `total`
719 " . Widget::unavailableNetworks(),
722 if (DBA::isResult($stmt)) {
723 $total = DBA::fetch($stmt)['total'];
727 $pager = new Pager(DI::l10n(), DI::args()->getQueryString());
729 $sql_values[] = $pager->getStart();
730 $sql_values[] = $pager->getItemsPerPage();
734 $stmt = DBA::p("SELECT *
744 while ($contact = DBA::fetch($stmt)) {
745 $contact['blocked'] = Model\Contact\User::isBlocked($contact['id'], local_user());
746 $contact['readonly'] = Model\Contact\User::isIgnored($contact['id'], local_user());
747 $contacts[] = self::getContactTemplateVars($contact);
753 'label' => DI::l10n()->t('All Contacts'),
755 'sel' => !$type ? 'active' : '',
756 'title' => DI::l10n()->t('Show all contacts'),
757 'id' => 'showall-tab',
761 'label' => DI::l10n()->t('Pending'),
762 'url' => 'contact/pending',
763 'sel' => $type == 'pending' ? 'active' : '',
764 'title' => DI::l10n()->t('Only show pending contacts'),
765 'id' => 'showpending-tab',
769 'label' => DI::l10n()->t('Blocked'),
770 'url' => 'contact/blocked',
771 'sel' => $type == 'blocked' ? 'active' : '',
772 'title' => DI::l10n()->t('Only show blocked contacts'),
773 'id' => 'showblocked-tab',
777 'label' => DI::l10n()->t('Ignored'),
778 'url' => 'contact/ignored',
779 'sel' => $type == 'ignored' ? 'active' : '',
780 'title' => DI::l10n()->t('Only show ignored contacts'),
781 'id' => 'showignored-tab',
785 'label' => DI::l10n()->t('Archived'),
786 'url' => 'contact/archived',
787 'sel' => $type == 'archived' ? 'active' : '',
788 'title' => DI::l10n()->t('Only show archived contacts'),
789 'id' => 'showarchived-tab',
793 'label' => DI::l10n()->t('Hidden'),
794 'url' => 'contact/hidden',
795 'sel' => $type == 'hidden' ? 'active' : '',
796 'title' => DI::l10n()->t('Only show hidden contacts'),
797 'id' => 'showhidden-tab',
801 'label' => DI::l10n()->t('Groups'),
804 'title' => DI::l10n()->t('Organize your contact groups'),
805 'id' => 'contactgroups-tab',
810 $tabs_tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
811 $tabs_html = Renderer::replaceMacros($tabs_tpl, ['$tabs' => $tabs]);
814 case 'followers': $header = DI::l10n()->t('Followers'); break;
815 case 'following': $header = DI::l10n()->t('Following'); break;
816 case 'mutuals': $header = DI::l10n()->t('Mutual friends'); break;
817 default: $header = DI::l10n()->t('Contacts');
821 case 'pending': $header .= ' - ' . DI::l10n()->t('Pending'); break;
822 case 'blocked': $header .= ' - ' . DI::l10n()->t('Blocked'); break;
823 case 'hidden': $header .= ' - ' . DI::l10n()->t('Hidden'); break;
824 case 'ignored': $header .= ' - ' . DI::l10n()->t('Ignored'); break;
825 case 'archived': $header .= ' - ' . DI::l10n()->t('Archived'); break;
828 $header .= $nets ? ' - ' . ContactSelector::networkToName($nets) : '';
830 $tpl = Renderer::getMarkupTemplate('contacts-template.tpl');
831 $o .= Renderer::replaceMacros($tpl, [
832 '$header' => $header,
833 '$tabs' => $tabs_html,
835 '$search' => $search_hdr,
836 '$desc' => DI::l10n()->t('Search your contacts'),
837 '$finding' => $searching ? DI::l10n()->t('Results for: %s', $search) : '',
838 '$submit' => DI::l10n()->t('Find'),
839 '$cmd' => DI::args()->getCommand(),
840 '$contacts' => $contacts,
841 '$contact_drop_confirm' => DI::l10n()->t('Do you really want to delete this contact?'),
843 '$batch_actions' => [
844 'contacts_batch_update' => DI::l10n()->t('Update'),
845 'contacts_batch_block' => DI::l10n()->t('Block') . '/' . DI::l10n()->t('Unblock'),
846 'contacts_batch_ignore' => DI::l10n()->t('Ignore') . '/' . DI::l10n()->t('Unignore'),
847 'contacts_batch_archive' => DI::l10n()->t('Archive') . '/' . DI::l10n()->t('Unarchive'),
848 'contacts_batch_drop' => DI::l10n()->t('Delete'),
850 '$h_batch_actions' => DI::l10n()->t('Batch Actions'),
851 '$paginate' => $pager->renderFull($total),
858 * List of pages for the Contact TabBar
860 * Available Pages are 'Status', 'Profile', 'Contacts' and 'Common Friends'
862 * @param array $contact The contact array
863 * @param int $active_tab 1 if tab should be marked as active
865 * @return string HTML string of the contact page tabs buttons.
866 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
867 * @throws \ImagickException
869 public static function getTabsHTML(array $contact, int $active_tab)
871 $cid = $pcid = $contact['id'];
872 $data = Model\Contact::getPublicAndUserContactID($contact['id'], local_user());
873 if (!empty($data['user']) && ($contact['id'] == $data['public'])) {
874 $cid = $data['user'];
875 } elseif (!empty($data['public'])) {
876 $pcid = $data['public'];
882 'label' => DI::l10n()->t('Status'),
883 'url' => 'contact/' . $pcid . '/conversations',
884 'sel' => (($active_tab == self::TAB_CONVERSATIONS) ? 'active' : ''),
885 'title' => DI::l10n()->t('Conversations started by this contact'),
886 'id' => 'status-tab',
890 'label' => DI::l10n()->t('Posts and Comments'),
891 'url' => 'contact/' . $pcid . '/posts',
892 'sel' => (($active_tab == self::TAB_POSTS) ? 'active' : ''),
893 'title' => DI::l10n()->t('Status Messages and Posts'),
898 'label' => DI::l10n()->t('Profile'),
899 'url' => 'contact/' . $cid,
900 'sel' => (($active_tab == self::TAB_PROFILE) ? 'active' : ''),
901 'title' => DI::l10n()->t('Profile Details'),
902 'id' => 'profile-tab',
905 ['label' => DI::l10n()->t('Contacts'),
906 'url' => 'contact/' . $pcid . '/contacts',
907 'sel' => (($active_tab == self::TAB_CONTACTS) ? 'active' : ''),
908 'title' => DI::l10n()->t('View all known contacts'),
909 'id' => 'contacts-tab',
914 if (!empty($contact['network']) && in_array($contact['network'], [Protocol::FEED, Protocol::MAIL]) && ($cid != $pcid)) {
915 $tabs[] = ['label' => DI::l10n()->t('Advanced'),
916 'url' => 'contact/' . $cid . '/advanced/',
917 'sel' => (($active_tab == self::TAB_ADVANCED) ? 'active' : ''),
918 'title' => DI::l10n()->t('Advanced Contact Settings'),
919 'id' => 'advanced-tab',
924 $tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
925 $tab_str = Renderer::replaceMacros($tab_tpl, ['$tabs' => $tabs]);
930 public static function getConversationsHMTL($a, $contact_id, $update, $parent = 0)
935 // We need the editor here to be able to reshare an item.
937 $o = status_editor($a, [], 0, true);
941 $contact = DBA::selectFirst('contact', ['uid', 'url', 'id'], ['id' => $contact_id, 'deleted' => false]);
944 $o .= self::getTabsHTML($contact, self::TAB_CONVERSATIONS);
947 if (DBA::isResult($contact)) {
949 $profiledata = Model\Contact::getByURLForUser($contact['url'], local_user());
950 DI::page()['aside'] = Widget\VCard::getHTML($profiledata);
952 DI::page()['aside'] = '';
955 if ($contact['uid'] == 0) {
956 $o .= Model\Contact::getPostsFromId($contact['id'], true, $update, $parent);
958 $o .= Model\Contact::getPostsFromUrl($contact['url'], true, $update, $parent);
965 private static function getPostsHTML($a, $contact_id)
967 $contact = DBA::selectFirst('contact', ['uid', 'url', 'id'], ['id' => $contact_id, 'deleted' => false]);
969 $o = self::getTabsHTML($contact, self::TAB_POSTS);
971 if (DBA::isResult($contact)) {
972 $profiledata = Model\Contact::getByURLForUser($contact['url'], local_user());
974 if (local_user() && in_array($profiledata['network'], Protocol::FEDERATED)) {
975 $profiledata['remoteconnect'] = DI::baseUrl() . '/follow?url=' . urlencode($profiledata['url']);
978 DI::page()['aside'] = Widget\VCard::getHTML($profiledata);
980 if ($contact['uid'] == 0) {
981 $o .= Model\Contact::getPostsFromId($contact['id']);
983 $o .= Model\Contact::getPostsFromUrl($contact['url']);
991 * Return the fields for the contact template
993 * @param array $contact Contact array
994 * @return array Template fields
996 public static function getContactTemplateVars(array $contact)
1000 if (!empty($contact['url']) && isset($contact['uid']) && ($contact['uid'] == 0) && local_user()) {
1001 $personal = Model\Contact::getByURL($contact['url'], false, ['uid', 'rel', 'self'], local_user());
1002 if (!empty($personal)) {
1003 $contact['uid'] = $personal['uid'];
1004 $contact['rel'] = $personal['rel'];
1005 $contact['self'] = $personal['self'];
1009 if (!empty($contact['uid']) && !empty($contact['rel']) && local_user() == $contact['uid']) {
1010 switch ($contact['rel']) {
1011 case Model\Contact::FRIEND:
1012 $alt_text = DI::l10n()->t('Mutual Friendship');
1015 case Model\Contact::FOLLOWER;
1016 $alt_text = DI::l10n()->t('is a fan of yours');
1019 case Model\Contact::SHARING;
1020 $alt_text = DI::l10n()->t('you are a fan of');
1028 $url = Model\Contact::magicLinkByContact($contact);
1030 if (strpos($url, 'redir/') === 0) {
1031 $sparkle = ' class="sparkle" ';
1036 if ($contact['pending']) {
1037 if (in_array($contact['rel'], [Model\Contact::FRIEND, Model\Contact::SHARING])) {
1038 $alt_text = DI::l10n()->t('Pending outgoing contact request');
1040 $alt_text = DI::l10n()->t('Pending incoming contact request');
1044 if ($contact['self']) {
1045 $alt_text = DI::l10n()->t('This is you');
1046 $url = $contact['url'];
1051 'id' => $contact['id'],
1053 'img_hover' => DI::l10n()->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
1054 'photo_menu' => Model\Contact::photoMenu($contact),
1055 'thumb' => Model\Contact::getThumb($contact, true),
1056 'alt_text' => $alt_text,
1057 'name' => $contact['name'],
1058 'nick' => $contact['nick'],
1059 'details' => $contact['location'],
1060 'tags' => $contact['keywords'],
1061 'about' => $contact['about'],
1062 'account_type' => Model\Contact::getAccountType($contact),
1063 'sparkle' => $sparkle,
1064 'itemurl' => ($contact['addr'] ?? '') ?: $contact['url'],
1065 'network' => ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']),
1070 * Gives a array with actions which can performed to a given contact
1072 * This includes actions like e.g. 'block', 'hide', 'archive', 'delete' and others
1074 * @param array $contact Data about the Contact
1075 * @return array with contact related actions
1077 private static function getContactActions($contact)
1079 $poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
1080 $contact_actions = [];
1082 // Provide friend suggestion only for Friendica contacts
1083 if ($contact['network'] === Protocol::DFRN) {
1084 $contact_actions['suggest'] = [
1085 'label' => DI::l10n()->t('Suggest friends'),
1086 'url' => 'fsuggest/' . $contact['id'],
1093 if ($poll_enabled) {
1094 $contact_actions['update'] = [
1095 'label' => DI::l10n()->t('Update now'),
1096 'url' => 'contact/' . $contact['id'] . '/update',
1103 if (in_array($contact['network'], Protocol::NATIVE_SUPPORT)) {
1104 $contact_actions['updateprofile'] = [
1105 'label' => DI::l10n()->t('Refetch contact data'),
1106 'url' => 'contact/' . $contact['id'] . '/updateprofile',
1109 'id' => 'updateprofile',
1113 $contact_actions['block'] = [
1114 'label' => (intval($contact['blocked']) ? DI::l10n()->t('Unblock') : DI::l10n()->t('Block')),
1115 'url' => 'contact/' . $contact['id'] . '/block',
1116 'title' => DI::l10n()->t('Toggle Blocked status'),
1117 'sel' => (intval($contact['blocked']) ? 'active' : ''),
1118 'id' => 'toggle-block',
1121 $contact_actions['ignore'] = [
1122 'label' => (intval($contact['readonly']) ? DI::l10n()->t('Unignore') : DI::l10n()->t('Ignore')),
1123 'url' => 'contact/' . $contact['id'] . '/ignore',
1124 'title' => DI::l10n()->t('Toggle Ignored status'),
1125 'sel' => (intval($contact['readonly']) ? 'active' : ''),
1126 'id' => 'toggle-ignore',
1129 if ($contact['uid'] != 0) {
1130 $contact_actions['archive'] = [
1131 'label' => (intval($contact['archive']) ? DI::l10n()->t('Unarchive') : DI::l10n()->t('Archive')),
1132 'url' => 'contact/' . $contact['id'] . '/archive',
1133 'title' => DI::l10n()->t('Toggle Archive status'),
1134 'sel' => (intval($contact['archive']) ? 'active' : ''),
1135 'id' => 'toggle-archive',
1138 $contact_actions['delete'] = [
1139 'label' => DI::l10n()->t('Delete'),
1140 'url' => 'contact/' . $contact['id'] . '/drop',
1141 'title' => DI::l10n()->t('Delete contact'),
1147 return $contact_actions;