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