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