]> git.mxchange.org Git - friendica.git/blob - src/Module/Contact.php
Move contact posts to their own module class
[friendica.git] / src / Module / Contact.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
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.
11  *
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.
16  *
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/>.
19  *
20  */
21
22 namespace Friendica\Module;
23
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\Hook;
31 use Friendica\Core\Protocol;
32 use Friendica\Core\Renderer;
33 use Friendica\Core\Theme;
34 use Friendica\Core\Worker;
35 use Friendica\Database\DBA;
36 use Friendica\DI;
37 use Friendica\Model;
38 use Friendica\Model\User;
39 use Friendica\Module\Security\Login;
40 use Friendica\Network\HTTPException\BadRequestException;
41 use Friendica\Network\HTTPException\NotFoundException;
42 use Friendica\Util\DateTimeFormat;
43 use Friendica\Util\Strings;
44
45 /**
46  *  Manages and show Contacts and their content
47  */
48 class Contact extends BaseModule
49 {
50         const TAB_CONVERSATIONS = 1;
51         const TAB_POSTS = 2;
52         const TAB_PROFILE = 3;
53         const TAB_CONTACTS = 4;
54         const TAB_ADVANCED = 5;
55         const TAB_MEDIA = 6;
56
57         private static function batchActions()
58         {
59                 if (empty($_POST['contact_batch']) || !is_array($_POST['contact_batch'])) {
60                         return;
61                 }
62
63                 $redirectUrl = $_POST['redirect_url'] ?? 'contact';
64
65                 self::checkFormSecurityTokenRedirectOnError($redirectUrl, 'contact_batch_actions');
66
67                 $orig_records = Model\Contact::selectToArray(['id', 'uid'], ['id' => $_POST['contact_batch'], 'uid' => [0, local_user()], 'self' => false, 'deleted' => false]);
68
69                 $count_actions = 0;
70                 foreach ($orig_records as $orig_record) {
71                         $cdata = Model\Contact::getPublicAndUserContactID($orig_record['id'], local_user());
72                         if (empty($cdata) || public_contact() === $cdata['public']) {
73                                 // No action available on your own contact
74                                 continue;
75                         }
76
77                         if (!empty($_POST['contacts_batch_update']) && $cdata['user']) {
78                                 self::updateContactFromPoll($cdata['user']);
79                                 $count_actions++;
80                         }
81
82                         if (!empty($_POST['contacts_batch_block'])) {
83                                 self::toggleBlockContact($cdata['public'], local_user());
84                                 $count_actions++;
85                         }
86
87                         if (!empty($_POST['contacts_batch_ignore'])) {
88                                 self::toggleIgnoreContact($cdata['public']);
89                                 $count_actions++;
90                         }
91                 }
92                 if ($count_actions > 0) {
93                         info(DI::l10n()->tt('%d contact edited.', '%d contacts edited.', $count_actions));
94                 }
95
96                 DI::baseUrl()->redirect($redirectUrl);
97         }
98
99         public function post()
100         {
101                 if (!local_user()) {
102                         return;
103                 }
104
105                 // @TODO: Replace with parameter from router
106                 if (DI::args()->getArgv()[1] === 'batch') {
107                         self::batchActions();
108                         return;
109                 }
110
111                 // @TODO: Replace with parameter from router
112                 $contact_id = intval(DI::args()->getArgv()[1]);
113                 if (!$contact_id) {
114                         return;
115                 }
116
117                 if (!DBA::exists('contact', ['id' => $contact_id, 'uid' => local_user(), 'deleted' => false])) {
118                         notice(DI::l10n()->t('Could not access contact record.'));
119                         DI::baseUrl()->redirect('contact');
120                         return; // NOTREACHED
121                 }
122
123                 Hook::callAll('contact_edit_post', $_POST);
124
125                 $hidden = !empty($_POST['hidden']);
126
127                 $notify = !empty($_POST['notify']);
128
129                 $fetch_further_information = intval($_POST['fetch_further_information'] ?? 0);
130
131                 $remote_self = $_POST['remote_self'] ?? false;
132
133                 $ffi_keyword_denylist = Strings::escapeHtml(trim($_POST['ffi_keyword_denylist'] ?? ''));
134
135                 $priority = intval($_POST['poll'] ?? 0);
136                 if ($priority > 5 || $priority < 0) {
137                         $priority = 0;
138                 }
139
140                 $info = Strings::escapeHtml(trim($_POST['info'] ?? ''));
141
142                 $r = Model\Contact::update([
143                         'priority'   => $priority,
144                         'info'       => $info,
145                         'hidden'     => $hidden,
146                         'notify_new_posts' => $notify,
147                         'fetch_further_information' => $fetch_further_information,
148                         'remote_self' => $remote_self,
149                         'ffi_keyword_denylist'     => $ffi_keyword_denylist],
150                         ['id' => $contact_id, 'uid' => local_user()]
151                 );
152
153                 if (!DBA::isResult($r)) {
154                         notice(DI::l10n()->t('Failed to update contact record.'));
155                 }
156                 return;
157         }
158
159         /* contact actions */
160
161         /**
162          * @param int $contact_id Id of contact with uid != 0
163          * @throws NotFoundException
164          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
165          * @throws \ImagickException
166          */
167         private static function updateContactFromPoll(int $contact_id)
168         {
169                 $contact = DBA::selectFirst('contact', ['uid', 'url', 'network'], ['id' => $contact_id, 'uid' => local_user(), 'deleted' => false]);
170                 if (!DBA::isResult($contact)) {
171                         return;
172                 }
173
174                 if ($contact['network'] == Protocol::OSTATUS) {
175                         $result = Model\Contact::createFromProbeForUser($contact['uid'], $contact['url'], $contact['network']);
176
177                         if ($result['success']) {
178                                 Model\Contact::update(['subhub' => 1], ['id' => $contact_id]);
179                         }
180
181                         // pull feed and consume it, which should subscribe to the hub.
182                         Worker::add(PRIORITY_HIGH, 'OnePoll', $contact_id, 'force');
183                 } else {
184                         Worker::add(PRIORITY_HIGH, 'UpdateContact', $contact_id);
185                 }
186         }
187
188         /**
189          * @param int $contact_id Id of the contact with uid != 0
190          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
191          * @throws \ImagickException
192          */
193         private static function updateContactFromProbe(int $contact_id)
194         {
195                 $contact = DBA::selectFirst('contact', ['url'], ['id' => $contact_id, 'uid' => local_user(), 'deleted' => false]);
196                 if (!DBA::isResult($contact)) {
197                         return;
198                 }
199
200                 // Update the entry in the contact table
201                 Model\Contact::updateFromProbe($contact_id);
202         }
203
204         /**
205          * Toggles the blocked status of a contact identified by id.
206          *
207          * @param int $contact_id Id of the contact with uid = 0
208          * @param int $owner_id   Id of the user we want to block the contact for
209          * @throws \Exception
210          */
211         private static function toggleBlockContact(int $contact_id, int $owner_id)
212         {
213                 $blocked = !Model\Contact\User::isBlocked($contact_id, $owner_id);
214                 Model\Contact\User::setBlocked($contact_id, $owner_id, $blocked);
215         }
216
217         /**
218          * Toggles the ignored status of a contact identified by id.
219          *
220          * @param int $contact_id Id of the contact with uid = 0
221          * @throws \Exception
222          */
223         private static function toggleIgnoreContact(int $contact_id)
224         {
225                 $ignored = !Model\Contact\User::isIgnored($contact_id, local_user());
226                 Model\Contact\User::setIgnored($contact_id, local_user(), $ignored);
227         }
228
229         public function content($update = 0): string
230         {
231                 if (!local_user()) {
232                         return Login::form($_SERVER['REQUEST_URI']);
233                 }
234
235                 $a = DI::app();
236
237                 $search = trim($_GET['search'] ?? '');
238                 $nets   = trim($_GET['nets']   ?? '');
239                 $rel    = trim($_GET['rel']    ?? '');
240                 $group  = trim($_GET['group']  ?? '');
241
242                 $accounttype = $_GET['accounttype'] ?? '';
243                 $accounttypeid = User::getAccountTypeByString($accounttype);
244
245                 $page = DI::page();
246
247                 $page->registerFooterScript(Theme::getPathForFile('asset/typeahead.js/dist/typeahead.bundle.js'));
248                 $page->registerFooterScript(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.js'));
249                 $page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css'));
250                 $page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput-typeahead.css'));
251
252                 $contact = null;
253                 // @TODO: Replace with parameter from router
254                 if (DI::args()->getArgc() == 2 && intval(DI::args()->getArgv()[1])
255                         || DI::args()->getArgc() == 3 && intval(DI::args()->getArgv()[1]) && in_array(DI::args()->getArgv()[2], ['conversations'])
256                 ) {
257                         $contact_id = intval(DI::args()->getArgv()[1]);
258
259                         // Ensure to use the user contact when the public contact was provided
260                         $data = Model\Contact::getPublicAndUserContactID($contact_id, local_user());
261                         if (!empty($data['user']) && ($contact_id == $data['public'])) {
262                                 $contact_id = $data['user'];
263                         }
264
265                         if (!empty($data)) {
266                                 $contact = DBA::selectFirst('contact', [], [
267                                         'id'      => $contact_id,
268                                         'uid'     => [0, local_user()],
269                                         'deleted' => false
270                                 ]);
271
272                                 // Don't display contacts that are about to be deleted
273                                 if (DBA::isResult($contact) && !empty($contact['network']) && $contact['network'] == Protocol::PHANTOM) {
274                                         $contact = false;
275                                 }
276                         }
277                 }
278
279                 if (DBA::isResult($contact)) {
280                         if ($contact['self']) {
281                                 // @TODO: Replace with parameter from router
282                                 if ((DI::args()->getArgc() == 3) && intval(DI::args()->getArgv()[1]) && in_array(DI::args()->getArgv()[2], ['conversations'])) {
283                                         DI::baseUrl()->redirect('profile/' . $contact['nick']);
284                                 } else {
285                                         DI::baseUrl()->redirect('profile/' . $contact['nick'] . '/profile');
286                                 }
287                         }
288
289                         $vcard_widget = Widget\VCard::getHTML($contact);
290
291                         $findpeople_widget = '';
292                         $follow_widget = '';
293                         $account_widget = '';
294                         $networks_widget = '';
295                         $rel_widget = '';
296
297                         if ($contact['uid'] != 0) {
298                                 $groups_widget = Model\Group::sidebarWidget('contact', 'group', 'full', 'everyone', $contact_id);
299                         } else {
300                                 $groups_widget = '';
301                         }
302                 } else {
303                         $vcard_widget = '';
304                         $findpeople_widget = Widget::findPeople();
305                         if (isset($_GET['add'])) {
306                                 $follow_widget = Widget::follow($_GET['add']);
307                         } else {
308                                 $follow_widget = Widget::follow();
309                         }
310
311                         $account_widget = Widget::accounttypes($_SERVER['REQUEST_URI'], $accounttype);
312                         $networks_widget = Widget::networks($_SERVER['REQUEST_URI'], $nets);
313                         $rel_widget = Widget::contactRels($_SERVER['REQUEST_URI'], $rel);
314                         $groups_widget = Widget::groups($_SERVER['REQUEST_URI'], $group);
315                 }
316
317                 DI::page()['aside'] .= $vcard_widget . $findpeople_widget . $follow_widget . $account_widget . $groups_widget . $networks_widget . $rel_widget;
318
319                 $tpl = Renderer::getMarkupTemplate('contacts-head.tpl');
320                 DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
321                         '$baseurl' => DI::baseUrl()->get(true),
322                 ]);
323
324                 $o = '';
325                 Nav::setSelected('contact');
326
327                 if (DI::args()->getArgc() == 3) {
328                         $contact_id = intval(DI::args()->getArgv()[1]);
329                         if (!$contact_id) {
330                                 throw new BadRequestException();
331                         }
332
333                         // @TODO: Replace with parameter from router
334                         $cmd = DI::args()->getArgv()[2];
335
336                         $orig_record = DBA::selectFirst('contact', [], ['id' => $contact_id, 'uid' => [0, local_user()], 'self' => false, 'deleted' => false]);
337                         if (!DBA::isResult($orig_record)) {
338                                 throw new NotFoundException(DI::l10n()->t('Contact not found'));
339                         }
340
341                         if ($cmd === 'conversations') {
342                                 return self::getConversationsHMTL($a, $contact_id, $update);
343                         }
344
345                         self::checkFormSecurityTokenRedirectOnError('contact/' . $contact_id, 'contact_action', 't');
346
347                         $cdata = Model\Contact::getPublicAndUserContactID($orig_record['id'], local_user());
348                         if (empty($cdata)) {
349                                 throw new NotFoundException(DI::l10n()->t('Contact not found'));
350                         }
351
352                         if ($cmd === 'update' && $cdata['user']) {
353                                 self::updateContactFromPoll($cdata['user']);
354                                 DI::baseUrl()->redirect('contact/' . $contact_id);
355                                 // NOTREACHED
356                         }
357
358                         if ($cmd === 'updateprofile' && $cdata['user']) {
359                                 self::updateContactFromProbe($cdata['user']);
360                                 DI::baseUrl()->redirect('contact/' . $contact_id);
361                                 // NOTREACHED
362                         }
363
364                         if ($cmd === 'block') {
365                                 if (public_contact() === $cdata['public']) {
366                                         throw new BadRequestException(DI::l10n()->t('You can\'t block yourself'));
367                                 }
368
369                                 self::toggleBlockContact($cdata['public'], local_user());
370
371                                 $blocked = Model\Contact\User::isBlocked($contact_id, local_user());
372                                 info(($blocked ? DI::l10n()->t('Contact has been blocked') : DI::l10n()->t('Contact has been unblocked')));
373
374                                 DI::baseUrl()->redirect('contact/' . $contact_id);
375                                 // NOTREACHED
376                         }
377
378                         if ($cmd === 'ignore') {
379                                 if (public_contact() === $cdata['public']) {
380                                         throw new BadRequestException(DI::l10n()->t('You can\'t ignore yourself'));
381                                 }
382
383                                 self::toggleIgnoreContact($cdata['public']);
384
385                                 $ignored = Model\Contact\User::isIgnored($cdata['public'], local_user());
386                                 info(($ignored ? DI::l10n()->t('Contact has been ignored') : DI::l10n()->t('Contact has been unignored')));
387
388                                 DI::baseUrl()->redirect('contact/' . $contact_id);
389                                 // NOTREACHED
390                         }
391                 }
392
393                 $_SESSION['return_path'] = DI::args()->getQueryString();
394
395                 if (!empty($contact)) {
396                         DI::page()['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_head.tpl'), [
397                                 '$baseurl' => DI::baseUrl()->get(true),
398                         ]);
399
400                         $contact['blocked']  = Model\Contact\User::isBlocked($contact['id'], local_user());
401                         $contact['readonly'] = Model\Contact\User::isIgnored($contact['id'], local_user());
402
403                         $relation_text = '';
404                         switch ($contact['rel']) {
405                                 case Model\Contact::FRIEND:
406                                         $relation_text = DI::l10n()->t('You are mutual friends with %s');
407                                         break;
408
409                                 case Model\Contact::FOLLOWER;
410                                         $relation_text = DI::l10n()->t('You are sharing with %s');
411                                         break;
412
413                                 case Model\Contact::SHARING;
414                                         $relation_text = DI::l10n()->t('%s is sharing with you');
415                                         break;
416
417                                 default:
418                                         break;
419                         }
420
421                         if ($contact['uid'] == 0) {
422                                 $relation_text = '';
423                         }
424
425                         if (!in_array($contact['network'], array_merge(Protocol::FEDERATED, [Protocol::TWITTER]))) {
426                                 $relation_text = '';
427                         }
428
429                         $relation_text = sprintf($relation_text, $contact['name']);
430
431                         $url = Model\Contact::magicLinkByContact($contact);
432                         if (strpos($url, 'redir/') === 0) {
433                                 $sparkle = ' class="sparkle" ';
434                         } else {
435                                 $sparkle = '';
436                         }
437
438                         $insecure = DI::l10n()->t('Private communications are not available for this contact.');
439
440                         $last_update = (($contact['last-update'] <= DBA::NULL_DATETIME) ? DI::l10n()->t('Never') : DateTimeFormat::local($contact['last-update'], 'D, j M Y, g:i A'));
441
442                         if ($contact['last-update'] > DBA::NULL_DATETIME) {
443                                 $last_update .= ' ' . ($contact['failed'] ? DI::l10n()->t('(Update was not successful)') : DI::l10n()->t('(Update was successful)'));
444                         }
445                         $lblsuggest = (($contact['network'] === Protocol::DFRN) ? DI::l10n()->t('Suggest friends') : '');
446
447                         $poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
448
449                         $nettype = DI::l10n()->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']));
450
451                         // tabs
452                         $tab_str = self::getTabsHTML($contact, self::TAB_PROFILE);
453
454                         $lost_contact = (($contact['archive'] && $contact['term-date'] > DBA::NULL_DATETIME && $contact['term-date'] < DateTimeFormat::utcNow()) ? DI::l10n()->t('Communications lost with this contact!') : '');
455
456                         $fetch_further_information = null;
457                         if ($contact['network'] == Protocol::FEED) {
458                                 $fetch_further_information = [
459                                         'fetch_further_information',
460                                         DI::l10n()->t('Fetch further information for feeds'),
461                                         $contact['fetch_further_information'],
462                                         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.'),
463                                         [
464                                                 '0' => DI::l10n()->t('Disabled'),
465                                                 '1' => DI::l10n()->t('Fetch information'),
466                                                 '3' => DI::l10n()->t('Fetch keywords'),
467                                                 '2' => DI::l10n()->t('Fetch information and keywords')
468                                         ]
469                                 ];
470                         }
471
472                         // Disable remote self for everything except feeds.
473                         // There is an issue when you repeat an item from maybe twitter and you got comments from friendica and twitter
474                         // Problem is, you couldn't reply to both networks.
475                         $allow_remote_self = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::FEED, Protocol::DFRN, Protocol::DIASPORA, Protocol::TWITTER])
476                                 && DI::config()->get('system', 'allow_users_remote_self');
477
478                         if ($contact['network'] == Protocol::FEED) {
479                                 $remote_self_options = [Model\Contact::MIRROR_DEACTIVATED => DI::l10n()->t('No mirroring'),
480                                         Model\Contact::MIRROR_FORWARDED => DI::l10n()->t('Mirror as forwarded posting'),
481                                         Model\Contact::MIRROR_OWN_POST => DI::l10n()->t('Mirror as my own posting')];
482                         } elseif (in_array($contact['network'], [Protocol::ACTIVITYPUB])) {
483                                 $remote_self_options = [Model\Contact::MIRROR_DEACTIVATED => DI::l10n()->t('No mirroring'),
484                                 Model\Contact::MIRROR_NATIVE_RESHARE => DI::l10n()->t('Native reshare')];
485                         } elseif (in_array($contact['network'], [Protocol::DFRN])) {
486                                 $remote_self_options = [Model\Contact::MIRROR_DEACTIVATED => DI::l10n()->t('No mirroring'),
487                                 Model\Contact::MIRROR_OWN_POST => DI::l10n()->t('Mirror as my own posting'),
488                                 Model\Contact::MIRROR_NATIVE_RESHARE => DI::l10n()->t('Native reshare')];
489                         } else {
490                                 $remote_self_options = [Model\Contact::MIRROR_DEACTIVATED => DI::l10n()->t('No mirroring'),
491                                         Model\Contact::MIRROR_OWN_POST => DI::l10n()->t('Mirror as my own posting')];
492                         }
493
494                         $poll_interval = null;
495                         if ((($contact['network'] == Protocol::FEED) && !DI::config()->get('system', 'adjust_poll_frequency')) || ($contact['network'] == Protocol::MAIL)) {
496                                 $poll_interval = ContactSelector::pollInterval($contact['priority'], !$poll_enabled);
497                         }
498
499                         // Load contactact related actions like hide, suggest, delete and others
500                         $contact_actions = self::getContactActions($contact);
501
502                         if ($contact['uid'] != 0) {
503                                 $lbl_info1 = DI::l10n()->t('Contact Information / Notes');
504                                 $contact_settings_label = DI::l10n()->t('Contact Settings');
505                         } else {
506                                 $lbl_info1 = null;
507                                 $contact_settings_label = null;
508                         }
509
510                         $tpl = Renderer::getMarkupTemplate('contact_edit.tpl');
511                         $o .= Renderer::replaceMacros($tpl, [
512                                 '$header'         => DI::l10n()->t('Contact'),
513                                 '$tab_str'        => $tab_str,
514                                 '$submit'         => DI::l10n()->t('Submit'),
515                                 '$lbl_info1'      => $lbl_info1,
516                                 '$lbl_info2'      => DI::l10n()->t('Their personal note'),
517                                 '$reason'         => trim($contact['reason']),
518                                 '$infedit'        => DI::l10n()->t('Edit contact notes'),
519                                 '$common_link'    => 'contact/' . $contact['id'] . '/contacts/common',
520                                 '$relation_text'  => $relation_text,
521                                 '$visit'          => DI::l10n()->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
522                                 '$blockunblock'   => DI::l10n()->t('Block/Unblock contact'),
523                                 '$ignorecont'     => DI::l10n()->t('Ignore contact'),
524                                 '$lblrecent'      => DI::l10n()->t('View conversations'),
525                                 '$lblsuggest'     => $lblsuggest,
526                                 '$nettype'        => $nettype,
527                                 '$poll_interval'  => $poll_interval,
528                                 '$poll_enabled'   => $poll_enabled,
529                                 '$lastupdtext'    => DI::l10n()->t('Last update:'),
530                                 '$lost_contact'   => $lost_contact,
531                                 '$updpub'         => DI::l10n()->t('Update public posts'),
532                                 '$last_update'    => $last_update,
533                                 '$udnow'          => DI::l10n()->t('Update now'),
534                                 '$contact_id'     => $contact['id'],
535                                 '$block_text'     => ($contact['blocked'] ? DI::l10n()->t('Unblock') : DI::l10n()->t('Block')),
536                                 '$ignore_text'    => ($contact['readonly'] ? DI::l10n()->t('Unignore') : DI::l10n()->t('Ignore')),
537                                 '$insecure'       => (in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::MAIL, Protocol::DIASPORA]) ? '' : $insecure),
538                                 '$info'           => $contact['info'],
539                                 '$cinfo'          => ['info', '', $contact['info'], ''],
540                                 '$blocked'        => ($contact['blocked'] ? DI::l10n()->t('Currently blocked') : ''),
541                                 '$ignored'        => ($contact['readonly'] ? DI::l10n()->t('Currently ignored') : ''),
542                                 '$archived'       => ($contact['archive'] ? DI::l10n()->t('Currently archived') : ''),
543                                 '$pending'        => ($contact['pending'] ? DI::l10n()->t('Awaiting connection acknowledge') : ''),
544                                 '$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')],
545                                 '$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')],
546                                 '$fetch_further_information' => $fetch_further_information,
547                                 '$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')],
548                                 '$photo'          => Model\Contact::getPhoto($contact),
549                                 '$name'           => $contact['name'],
550                                 '$sparkle'        => $sparkle,
551                                 '$url'            => $url,
552                                 '$profileurllabel'=> DI::l10n()->t('Profile URL'),
553                                 '$profileurl'     => $contact['url'],
554                                 '$account_type'   => Model\Contact::getAccountType($contact),
555                                 '$location'       => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['location']),
556                                 '$location_label' => DI::l10n()->t('Location:'),
557                                 '$xmpp'           => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['xmpp']),
558                                 '$xmpp_label'     => DI::l10n()->t('XMPP:'),
559                                 '$matrix'         => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['matrix']),
560                                 '$matrix_label'   => DI::l10n()->t('Matrix:'),
561                                 '$about'          => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['about'], BBCode::EXTERNAL),
562                                 '$about_label'    => DI::l10n()->t('About:'),
563                                 '$keywords'       => $contact['keywords'],
564                                 '$keywords_label' => DI::l10n()->t('Tags:'),
565                                 '$contact_action_button' => DI::l10n()->t('Actions'),
566                                 '$contact_actions'=> $contact_actions,
567                                 '$contact_status' => DI::l10n()->t('Status'),
568                                 '$contact_settings_label' => $contact_settings_label,
569                                 '$contact_profile_label' => DI::l10n()->t('Profile'),
570                                 '$allow_remote_self' => $allow_remote_self,
571                                 '$remote_self'       => ['remote_self',
572                                         DI::l10n()->t('Mirror postings from this contact'),
573                                         $contact['remote_self'],
574                                         DI::l10n()->t('Mark this contact as remote_self, this will cause friendica to repost new entries from this contact.'),
575                                         $remote_self_options
576                                 ],
577                         ]);
578
579                         $arr = ['contact' => $contact, 'output' => $o];
580
581                         Hook::callAll('contact_edit', $arr);
582
583                         return $arr['output'];
584                 }
585
586                 $sql_values = [local_user()];
587
588                 // @TODO: Replace with parameter from router
589                 $type = DI::args()->getArgv()[1] ?? '';
590
591                 switch ($type) {
592                         case 'blocked':
593                                 $sql_extra = " AND EXISTS(SELECT `id` from `user-contact` WHERE `contact`.`id` = `user-contact`.`cid` and `user-contact`.`uid` = ? and `user-contact`.`blocked`)";
594                                 // This makes the query look for contact.uid = 0
595                                 array_unshift($sql_values, 0);
596                                 break;
597                         case 'hidden':
598                                 $sql_extra = " AND `hidden` AND NOT `blocked` AND NOT `pending`";
599                                 break;
600                         case 'ignored':
601                                 $sql_extra = " AND EXISTS(SELECT `id` from `user-contact` WHERE `contact`.`id` = `user-contact`.`cid` and `user-contact`.`uid` = ? and `user-contact`.`ignored`)";
602                                 // This makes the query look for contact.uid = 0
603                                 array_unshift($sql_values, 0);
604                                 break;
605                         case 'archived':
606                                 $sql_extra = " AND `archive` AND NOT `blocked` AND NOT `pending`";
607                                 break;
608                         case 'pending':
609                                 $sql_extra = " AND `pending` AND NOT `archive` AND NOT `failed` AND ((`rel` = ?)
610                                         OR EXISTS (SELECT `id` FROM `intro` WHERE `contact-id` = `contact`.`id` AND NOT `ignore`))";
611                                 $sql_values[] = Model\Contact::SHARING;
612                                 break;
613                         default:
614                                 $sql_extra = " AND NOT `archive` AND NOT `blocked` AND NOT `pending`";
615                                 break;
616                 }
617
618                 if (isset($accounttypeid)) {
619                         $sql_extra .= " AND `contact-type` = ?";
620                         $sql_values[] = $accounttypeid;
621                 }
622
623                 $searching = false;
624                 $search_hdr = null;
625                 if ($search) {
626                         $searching = true;
627                         $search_hdr = $search;
628                         $search_txt = preg_quote($search);
629                         $sql_extra .= " AND (name REGEXP ? OR url REGEXP ? OR nick REGEXP ?)";
630                         $sql_values[] = $search_txt;
631                         $sql_values[] = $search_txt;
632                         $sql_values[] = $search_txt;
633                 }
634
635                 if ($nets) {
636                         $sql_extra .= " AND network = ? ";
637                         $sql_values[] = $nets;
638                 }
639
640                 switch ($rel) {
641                         case 'followers':
642                                 $sql_extra .= " AND `rel` IN (?, ?)";
643                                 $sql_values[] = Model\Contact::FOLLOWER;
644                                 $sql_values[] = Model\Contact::FRIEND;
645                                 break;
646                         case 'following':
647                                 $sql_extra .= " AND `rel` IN (?, ?)";
648                                 $sql_values[] = Model\Contact::SHARING;
649                                 $sql_values[] = Model\Contact::FRIEND;
650                                 break;
651                         case 'mutuals':
652                                 $sql_extra .= " AND `rel` = ?";
653                                 $sql_values[] = Model\Contact::FRIEND;
654                                 break;
655                 }
656
657                 if ($group) {
658                         $sql_extra .= " AND EXISTS(SELECT `id` FROM `group_member` WHERE `gid` = ? AND `contact`.`id` = `contact-id`)";
659                         $sql_values[] = $group;
660                 }
661
662                 $networks = Widget::unavailableNetworks();
663                 $sql_extra .= " AND NOT `network` IN (" . substr(str_repeat("?, ", count($networks)), 0, -2) . ")";
664                 $sql_values = array_merge($sql_values, $networks);
665
666                 $condition = ["`uid` = ? AND NOT `self` AND NOT `deleted`" . $sql_extra];
667                 $condition = array_merge($condition, $sql_values);
668
669                 $total = DBA::count('contact', $condition);
670
671                 $pager = new Pager(DI::l10n(), DI::args()->getQueryString());
672
673                 $contacts = [];
674
675                 $stmt = DBA::select('contact', [], $condition, ['order' => ['name'], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]]);
676
677                 while ($contact = DBA::fetch($stmt)) {
678                         $contact['blocked'] = Model\Contact\User::isBlocked($contact['id'], local_user());
679                         $contact['readonly'] = Model\Contact\User::isIgnored($contact['id'], local_user());
680                         $contacts[] = self::getContactTemplateVars($contact);
681                 }
682                 DBA::close($stmt);
683
684                 $tabs = [
685                         [
686                                 'label' => DI::l10n()->t('All Contacts'),
687                                 'url'   => 'contact',
688                                 'sel'   => !$type ? 'active' : '',
689                                 'title' => DI::l10n()->t('Show all contacts'),
690                                 'id'    => 'showall-tab',
691                                 'accesskey' => 'l',
692                         ],
693                         [
694                                 'label' => DI::l10n()->t('Pending'),
695                                 'url'   => 'contact/pending',
696                                 'sel'   => $type == 'pending' ? 'active' : '',
697                                 'title' => DI::l10n()->t('Only show pending contacts'),
698                                 'id'    => 'showpending-tab',
699                                 'accesskey' => 'p',
700                         ],
701                         [
702                                 'label' => DI::l10n()->t('Blocked'),
703                                 'url'   => 'contact/blocked',
704                                 'sel'   => $type == 'blocked' ? 'active' : '',
705                                 'title' => DI::l10n()->t('Only show blocked contacts'),
706                                 'id'    => 'showblocked-tab',
707                                 'accesskey' => 'b',
708                         ],
709                         [
710                                 'label' => DI::l10n()->t('Ignored'),
711                                 'url'   => 'contact/ignored',
712                                 'sel'   => $type == 'ignored' ? 'active' : '',
713                                 'title' => DI::l10n()->t('Only show ignored contacts'),
714                                 'id'    => 'showignored-tab',
715                                 'accesskey' => 'i',
716                         ],
717                         [
718                                 'label' => DI::l10n()->t('Archived'),
719                                 'url'   => 'contact/archived',
720                                 'sel'   => $type == 'archived' ? 'active' : '',
721                                 'title' => DI::l10n()->t('Only show archived contacts'),
722                                 'id'    => 'showarchived-tab',
723                                 'accesskey' => 'y',
724                         ],
725                         [
726                                 'label' => DI::l10n()->t('Hidden'),
727                                 'url'   => 'contact/hidden',
728                                 'sel'   => $type == 'hidden' ? 'active' : '',
729                                 'title' => DI::l10n()->t('Only show hidden contacts'),
730                                 'id'    => 'showhidden-tab',
731                                 'accesskey' => 'h',
732                         ],
733                         [
734                                 'label' => DI::l10n()->t('Groups'),
735                                 'url'   => 'group',
736                                 'sel'   => '',
737                                 'title' => DI::l10n()->t('Organize your contact groups'),
738                                 'id'    => 'contactgroups-tab',
739                                 'accesskey' => 'e',
740                         ],
741                 ];
742
743                 $tabs_tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
744                 $tabs_html = Renderer::replaceMacros($tabs_tpl, ['$tabs' => $tabs]);
745
746                 switch ($rel) {
747                         case 'followers': $header = DI::l10n()->t('Followers'); break;
748                         case 'following': $header = DI::l10n()->t('Following'); break;
749                         case 'mutuals':   $header = DI::l10n()->t('Mutual friends'); break;
750                         default:          $header = DI::l10n()->t('Contacts');
751                 }
752
753                 switch ($type) {
754                         case 'pending':  $header .= ' - ' . DI::l10n()->t('Pending'); break;
755                         case 'blocked':  $header .= ' - ' . DI::l10n()->t('Blocked'); break;
756                         case 'hidden':   $header .= ' - ' . DI::l10n()->t('Hidden'); break;
757                         case 'ignored':  $header .= ' - ' . DI::l10n()->t('Ignored'); break;
758                         case 'archived': $header .= ' - ' . DI::l10n()->t('Archived'); break;
759                 }
760
761                 $header .= $nets ? ' - ' . ContactSelector::networkToName($nets) : '';
762
763                 $tpl = Renderer::getMarkupTemplate('contacts-template.tpl');
764                 $o .= Renderer::replaceMacros($tpl, [
765                         '$header'     => $header,
766                         '$tabs'       => $tabs_html,
767                         '$total'      => $total,
768                         '$search'     => $search_hdr,
769                         '$desc'       => DI::l10n()->t('Search your contacts'),
770                         '$finding'    => $searching ? DI::l10n()->t('Results for: %s', $search) : '',
771                         '$submit'     => DI::l10n()->t('Find'),
772                         '$cmd'        => DI::args()->getCommand(),
773                         '$contacts'   => $contacts,
774                         '$form_security_token'  => BaseModule::getFormSecurityToken('contact_batch_actions'),
775                         'multiselect' => 1,
776                         '$batch_actions' => [
777                                 'contacts_batch_update'  => DI::l10n()->t('Update'),
778                                 'contacts_batch_block'   => DI::l10n()->t('Block') . '/' . DI::l10n()->t('Unblock'),
779                                 'contacts_batch_ignore'  => DI::l10n()->t('Ignore') . '/' . DI::l10n()->t('Unignore'),
780                         ],
781                         '$h_batch_actions' => DI::l10n()->t('Batch Actions'),
782                         '$paginate'   => $pager->renderFull($total),
783                 ]);
784
785                 return $o;
786         }
787
788         /**
789          * List of pages for the Contact TabBar
790          *
791          * Available Pages are 'Status', 'Profile', 'Contacts' and 'Common Friends'
792          *
793          * @param array $contact    The contact array
794          * @param int   $active_tab 1 if tab should be marked as active
795          *
796          * @return string HTML string of the contact page tabs buttons.
797          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
798          * @throws \ImagickException
799          */
800         public static function getTabsHTML(array $contact, int $active_tab)
801         {
802                 $cid = $pcid = $contact['id'];
803                 $data = Model\Contact::getPublicAndUserContactID($contact['id'], local_user());
804                 if (!empty($data['user']) && ($contact['id'] == $data['public'])) {
805                         $cid = $data['user'];
806                 } elseif (!empty($data['public'])) {
807                         $pcid = $data['public'];
808                 }
809
810                 // tabs
811                 $tabs = [
812                         [
813                                 'label' => DI::l10n()->t('Status'),
814                                 'url'   => 'contact/' . $pcid . '/conversations',
815                                 'sel'   => (($active_tab == self::TAB_CONVERSATIONS) ? 'active' : ''),
816                                 'title' => DI::l10n()->t('Conversations started by this contact'),
817                                 'id'    => 'status-tab',
818                                 'accesskey' => 'm',
819                         ],
820                         [
821                                 'label' => DI::l10n()->t('Posts and Comments'),
822                                 'url'   => 'contact/' . $pcid . '/posts',
823                                 'sel'   => (($active_tab == self::TAB_POSTS) ? 'active' : ''),
824                                 'title' => DI::l10n()->t('Status Messages and Posts'),
825                                 'id'    => 'posts-tab',
826                                 'accesskey' => 'p',
827                         ],
828                         [
829                                 'label' => DI::l10n()->t('Media'),
830                                 'url'   => 'contact/' . $pcid . '/media',
831                                 'sel'   => (($active_tab == self::TAB_MEDIA) ? 'active' : ''),
832                                 'title' => DI::l10n()->t('Posts containing media objects'),
833                                 'id'    => 'media-tab',
834                                 'accesskey' => 'd',
835                         ],
836                         [
837                                 'label' => DI::l10n()->t('Profile'),
838                                 'url'   => 'contact/' . $cid,
839                                 'sel'   => (($active_tab == self::TAB_PROFILE) ? 'active' : ''),
840                                 'title' => DI::l10n()->t('Profile Details'),
841                                 'id'    => 'profile-tab',
842                                 'accesskey' => 'o',
843                         ],
844                         ['label' => DI::l10n()->t('Contacts'),
845                                 'url'   => 'contact/' . $pcid . '/contacts',
846                                 'sel'   => (($active_tab == self::TAB_CONTACTS) ? 'active' : ''),
847                                 'title' => DI::l10n()->t('View all known contacts'),
848                                 'id'    => 'contacts-tab',
849                                 'accesskey' => 't'
850                         ],
851                 ];
852
853                 if (!empty($contact['network']) && in_array($contact['network'], [Protocol::FEED, Protocol::MAIL]) && ($cid != $pcid)) {
854                         $tabs[] = ['label' => DI::l10n()->t('Advanced'),
855                                 'url'   => 'contact/' . $cid . '/advanced/',
856                                 'sel'   => (($active_tab == self::TAB_ADVANCED) ? 'active' : ''),
857                                 'title' => DI::l10n()->t('Advanced Contact Settings'),
858                                 'id'    => 'advanced-tab',
859                                 'accesskey' => 'r'
860                         ];
861                 }
862
863                 $tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
864                 $tab_str = Renderer::replaceMacros($tab_tpl, ['$tabs' => $tabs]);
865
866                 return $tab_str;
867         }
868
869         public static function getConversationsHMTL($a, $contact_id, $update, $parent = 0)
870         {
871                 $o = '';
872
873                 if (!$update) {
874                         // We need the editor here to be able to reshare an item.
875                         if (local_user()) {
876                                 $o = DI::conversation()->statusEditor([], 0, true);
877                         }
878                 }
879
880                 $contact = DBA::selectFirst('contact', ['uid', 'url', 'id'], ['id' => $contact_id, 'deleted' => false]);
881
882                 if (!$update) {
883                         $o .= self::getTabsHTML($contact, self::TAB_CONVERSATIONS);
884                 }
885
886                 if (DBA::isResult($contact)) {
887                         if (!$update) {
888                                 $profiledata = Model\Contact::getByURLForUser($contact['url'], local_user());
889                                 DI::page()['aside'] = Widget\VCard::getHTML($profiledata);
890                         } else {
891                                 DI::page()['aside'] = '';
892                         }
893
894                         if ($contact['uid'] == 0) {
895                                 $o .= Model\Contact::getPostsFromId($contact['id'], true, $update, $parent);
896                         } else {
897                                 $o .= Model\Contact::getPostsFromUrl($contact['url'], true, $update, $parent);
898                         }
899                 }
900
901                 return $o;
902         }
903
904         /**
905          * Return the fields for the contact template
906          *
907          * @param array $contact Contact array
908          * @return array Template fields
909          */
910         public static function getContactTemplateVars(array $contact)
911         {
912                 $alt_text = '';
913
914                 if (!empty($contact['url']) && isset($contact['uid']) && ($contact['uid'] == 0) && local_user()) {
915                         $personal = Model\Contact::getByURL($contact['url'], false, ['uid', 'rel', 'self'], local_user());
916                         if (!empty($personal)) {
917                                 $contact['uid'] = $personal['uid'];
918                                 $contact['rel'] = $personal['rel'];
919                                 $contact['self'] = $personal['self'];
920                         }
921                 }
922
923                 if (!empty($contact['uid']) && !empty($contact['rel']) && local_user() == $contact['uid']) {
924                         switch ($contact['rel']) {
925                                 case Model\Contact::FRIEND:
926                                         $alt_text = DI::l10n()->t('Mutual Friendship');
927                                         break;
928
929                                 case Model\Contact::FOLLOWER;
930                                         $alt_text = DI::l10n()->t('is a fan of yours');
931                                         break;
932
933                                 case Model\Contact::SHARING;
934                                         $alt_text = DI::l10n()->t('you are a fan of');
935                                         break;
936
937                                 default:
938                                         break;
939                         }
940                 }
941
942                 $url = Model\Contact::magicLinkByContact($contact);
943
944                 if (strpos($url, 'redir/') === 0) {
945                         $sparkle = ' class="sparkle" ';
946                 } else {
947                         $sparkle = '';
948                 }
949
950                 if ($contact['pending']) {
951                         if (in_array($contact['rel'], [Model\Contact::FRIEND, Model\Contact::SHARING])) {
952                                 $alt_text = DI::l10n()->t('Pending outgoing contact request');
953                         } else {
954                                 $alt_text = DI::l10n()->t('Pending incoming contact request');
955                         }
956                 }
957
958                 if ($contact['self']) {
959                         $alt_text = DI::l10n()->t('This is you');
960                         $url = $contact['url'];
961                         $sparkle = '';
962                 }
963
964                 return [
965                         'id'           => $contact['id'],
966                         'url'          => $url,
967                         'img_hover'    => DI::l10n()->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
968                         'photo_menu'   => Model\Contact::photoMenu($contact),
969                         'thumb'        => Model\Contact::getThumb($contact, true),
970                         'alt_text'     => $alt_text,
971                         'name'         => $contact['name'],
972                         'nick'         => $contact['nick'],
973                         'details'      => $contact['location'],
974                         'tags'         => $contact['keywords'],
975                         'about'        => $contact['about'],
976                         'account_type' => Model\Contact::getAccountType($contact),
977                         'sparkle'      => $sparkle,
978                         'itemurl'      => ($contact['addr'] ?? '') ?: $contact['url'],
979                         'network'      => ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']),
980                 ];
981         }
982
983         /**
984          * Gives a array with actions which can performed to a given contact
985          *
986          * This includes actions like e.g. 'block', 'hide', 'delete' and others
987          *
988          * @param array $contact Data about the Contact
989          * @return array with contact related actions
990          */
991         private static function getContactActions($contact)
992         {
993                 $poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
994                 $contact_actions = [];
995
996                 $formSecurityToken = self::getFormSecurityToken('contact_action');
997
998                 // Provide friend suggestion only for Friendica contacts
999                 if ($contact['network'] === Protocol::DFRN) {
1000                         $contact_actions['suggest'] = [
1001                                 'label' => DI::l10n()->t('Suggest friends'),
1002                                 'url'   => 'fsuggest/' . $contact['id'],
1003                                 'title' => '',
1004                                 'sel'   => '',
1005                                 'id'    => 'suggest',
1006                         ];
1007                 }
1008
1009                 if ($poll_enabled) {
1010                         $contact_actions['update'] = [
1011                                 'label' => DI::l10n()->t('Update now'),
1012                                 'url'   => 'contact/' . $contact['id'] . '/update?t=' . $formSecurityToken,
1013                                 'title' => '',
1014                                 'sel'   => '',
1015                                 'id'    => 'update',
1016                         ];
1017                 }
1018
1019                 if (in_array($contact['network'], Protocol::NATIVE_SUPPORT)) {
1020                         $contact_actions['updateprofile'] = [
1021                                 'label' => DI::l10n()->t('Refetch contact data'),
1022                                 'url'   => 'contact/' . $contact['id'] . '/updateprofile?t=' . $formSecurityToken,
1023                                 'title' => '',
1024                                 'sel'   => '',
1025                                 'id'    => 'updateprofile',
1026                         ];
1027                 }
1028
1029                 $contact_actions['block'] = [
1030                         'label' => (intval($contact['blocked']) ? DI::l10n()->t('Unblock') : DI::l10n()->t('Block')),
1031                         'url'   => 'contact/' . $contact['id'] . '/block?t=' . $formSecurityToken,
1032                         'title' => DI::l10n()->t('Toggle Blocked status'),
1033                         'sel'   => (intval($contact['blocked']) ? 'active' : ''),
1034                         'id'    => 'toggle-block',
1035                 ];
1036
1037                 $contact_actions['ignore'] = [
1038                         'label' => (intval($contact['readonly']) ? DI::l10n()->t('Unignore') : DI::l10n()->t('Ignore')),
1039                         'url'   => 'contact/' . $contact['id'] . '/ignore?t=' . $formSecurityToken,
1040                         'title' => DI::l10n()->t('Toggle Ignored status'),
1041                         'sel'   => (intval($contact['readonly']) ? 'active' : ''),
1042                         'id'    => 'toggle-ignore',
1043                 ];
1044
1045                 if ($contact['uid'] != 0 && Protocol::supportsRevokeFollow($contact['network']) && in_array($contact['rel'], [Model\Contact::FOLLOWER, Model\Contact::FRIEND])) {
1046                         $contact_actions['revoke_follow'] = [
1047                                 'label' => DI::l10n()->t('Revoke Follow'),
1048                                 'url'   => 'contact/' . $contact['id'] . '/revoke',
1049                                 'title' => DI::l10n()->t('Revoke the follow from this contact'),
1050                                 'sel'   => '',
1051                                 'id'    => 'revoke_follow',
1052                         ];
1053                 }
1054
1055                 return $contact_actions;
1056         }
1057 }