]> git.mxchange.org Git - friendica.git/blob - src/Module/Contact.php
Use rawContent for Special Options to avoid a protected options() method
[friendica.git] / src / Module / Contact.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Module;
23
24 use Friendica\BaseModule;
25 use Friendica\Content\ContactSelector;
26 use Friendica\Content\Nav;
27 use Friendica\Content\Pager;
28 use Friendica\Content\Widget;
29 use Friendica\Core\Protocol;
30 use Friendica\Core\Renderer;
31 use Friendica\Core\Theme;
32 use Friendica\Core\Worker;
33 use Friendica\Database\DBA;
34 use Friendica\DI;
35 use Friendica\Model;
36 use Friendica\Model\User;
37 use Friendica\Module\Security\Login;
38 use Friendica\Network\HTTPException\NotFoundException;
39
40 /**
41  *  Manages and show Contacts and their content
42  */
43 class Contact extends BaseModule
44 {
45         const TAB_CONVERSATIONS = 1;
46         const TAB_POSTS = 2;
47         const TAB_PROFILE = 3;
48         const TAB_CONTACTS = 4;
49         const TAB_ADVANCED = 5;
50         const TAB_MEDIA = 6;
51
52         private static function batchActions()
53         {
54                 if (empty($_POST['contact_batch']) || !is_array($_POST['contact_batch'])) {
55                         return;
56                 }
57
58                 $redirectUrl = $_POST['redirect_url'] ?? 'contact';
59
60                 self::checkFormSecurityTokenRedirectOnError($redirectUrl, 'contact_batch_actions');
61
62                 $orig_records = Model\Contact::selectToArray(['id', 'uid'], ['id' => $_POST['contact_batch'], 'uid' => [0, local_user()], 'self' => false, 'deleted' => false]);
63
64                 $count_actions = 0;
65                 foreach ($orig_records as $orig_record) {
66                         $cdata = Model\Contact::getPublicAndUserContactID($orig_record['id'], local_user());
67                         if (empty($cdata) || public_contact() === $cdata['public']) {
68                                 // No action available on your own contact
69                                 continue;
70                         }
71
72                         if (!empty($_POST['contacts_batch_update']) && $cdata['user']) {
73                                 self::updateContactFromPoll($cdata['user']);
74                                 $count_actions++;
75                         }
76
77                         if (!empty($_POST['contacts_batch_block'])) {
78                                 self::toggleBlockContact($cdata['public'], local_user());
79                                 $count_actions++;
80                         }
81
82                         if (!empty($_POST['contacts_batch_ignore'])) {
83                                 self::toggleIgnoreContact($cdata['public']);
84                                 $count_actions++;
85                         }
86                 }
87                 if ($count_actions > 0) {
88                         info(DI::l10n()->tt('%d contact edited.', '%d contacts edited.', $count_actions));
89                 }
90
91                 DI::baseUrl()->redirect($redirectUrl);
92         }
93
94         protected function post(array $request = [])
95         {
96                 if (!local_user()) {
97                         return;
98                 }
99
100                 // @TODO: Replace with parameter from router
101                 if (DI::args()->getArgv()[1] === 'batch') {
102                         self::batchActions();
103                 }
104         }
105
106         /* contact actions */
107
108         /**
109          * @param int $contact_id Id of contact with uid != 0
110          * @throws NotFoundException
111          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
112          * @throws \ImagickException
113          */
114         public static function updateContactFromPoll(int $contact_id)
115         {
116                 $contact = DBA::selectFirst('contact', ['uid', 'url', 'network'], ['id' => $contact_id, 'uid' => local_user(), 'deleted' => false]);
117                 if (!DBA::isResult($contact)) {
118                         return;
119                 }
120
121                 if ($contact['network'] == Protocol::OSTATUS) {
122                         $result = Model\Contact::createFromProbeForUser($contact['uid'], $contact['url'], $contact['network']);
123
124                         if ($result['success']) {
125                                 Model\Contact::update(['subhub' => 1], ['id' => $contact_id]);
126                         }
127
128                         // pull feed and consume it, which should subscribe to the hub.
129                         Worker::add(PRIORITY_HIGH, 'OnePoll', $contact_id, 'force');
130                 } else {
131                         Worker::add(PRIORITY_HIGH, 'UpdateContact', $contact_id);
132                 }
133         }
134
135         /**
136          * Toggles the blocked status of a contact identified by id.
137          *
138          * @param int $contact_id Id of the contact with uid = 0
139          * @param int $owner_id   Id of the user we want to block the contact for
140          * @throws \Exception
141          */
142         private static function toggleBlockContact(int $contact_id, int $owner_id)
143         {
144                 $blocked = !Model\Contact\User::isBlocked($contact_id, $owner_id);
145                 Model\Contact\User::setBlocked($contact_id, $owner_id, $blocked);
146         }
147
148         /**
149          * Toggles the ignored status of a contact identified by id.
150          *
151          * @param int $contact_id Id of the contact with uid = 0
152          * @throws \Exception
153          */
154         private static function toggleIgnoreContact(int $contact_id)
155         {
156                 $ignored = !Model\Contact\User::isIgnored($contact_id, local_user());
157                 Model\Contact\User::setIgnored($contact_id, local_user(), $ignored);
158         }
159
160         protected function content(array $request = []): string
161         {
162                 if (!local_user()) {
163                         return Login::form($_SERVER['REQUEST_URI']);
164                 }
165
166                 $search = trim($_GET['search'] ?? '');
167                 $nets   = trim($_GET['nets']   ?? '');
168                 $rel    = trim($_GET['rel']    ?? '');
169                 $group  = trim($_GET['group']  ?? '');
170
171                 $accounttype = $_GET['accounttype'] ?? '';
172                 $accounttypeid = User::getAccountTypeByString($accounttype);
173
174                 $page = DI::page();
175
176                 $page->registerFooterScript(Theme::getPathForFile('asset/typeahead.js/dist/typeahead.bundle.js'));
177                 $page->registerFooterScript(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.js'));
178                 $page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css'));
179                 $page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput-typeahead.css'));
180
181                 $vcard_widget = '';
182                 $findpeople_widget = Widget::findPeople();
183                 if (isset($_GET['add'])) {
184                         $follow_widget = Widget::follow($_GET['add']);
185                 } else {
186                         $follow_widget = Widget::follow();
187                 }
188
189                 $account_widget = Widget::accounttypes($_SERVER['REQUEST_URI'], $accounttype);
190                 $networks_widget = Widget::networks($_SERVER['REQUEST_URI'], $nets);
191                 $rel_widget = Widget::contactRels($_SERVER['REQUEST_URI'], $rel);
192                 $groups_widget = Widget::groups($_SERVER['REQUEST_URI'], $group);
193
194                 DI::page()['aside'] .= $vcard_widget . $findpeople_widget . $follow_widget . $account_widget . $groups_widget . $networks_widget . $rel_widget;
195
196                 $tpl = Renderer::getMarkupTemplate('contacts-head.tpl');
197                 DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
198                         '$baseurl' => DI::baseUrl()->get(true),
199                 ]);
200
201                 $o = '';
202                 Nav::setSelected('contact');
203
204                 $_SESSION['return_path'] = DI::args()->getQueryString();
205
206                 $sql_values = [local_user()];
207
208                 // @TODO: Replace with parameter from router
209                 $type = DI::args()->getArgv()[1] ?? '';
210
211                 switch ($type) {
212                         case 'blocked':
213                                 $sql_extra = " AND EXISTS(SELECT `id` from `user-contact` WHERE `contact`.`id` = `user-contact`.`cid` and `user-contact`.`uid` = ? and `user-contact`.`blocked`)";
214                                 // This makes the query look for contact.uid = 0
215                                 array_unshift($sql_values, 0);
216                                 break;
217                         case 'hidden':
218                                 $sql_extra = " AND `hidden` AND NOT `blocked` AND NOT `pending`";
219                                 break;
220                         case 'ignored':
221                                 $sql_extra = " AND EXISTS(SELECT `id` from `user-contact` WHERE `contact`.`id` = `user-contact`.`cid` and `user-contact`.`uid` = ? and `user-contact`.`ignored`)";
222                                 // This makes the query look for contact.uid = 0
223                                 array_unshift($sql_values, 0);
224                                 break;
225                         case 'archived':
226                                 $sql_extra = " AND `archive` AND NOT `blocked` AND NOT `pending`";
227                                 break;
228                         case 'pending':
229                                 $sql_extra = " AND `pending` AND NOT `archive` AND NOT `failed` AND ((`rel` = ?)
230                                         OR EXISTS (SELECT `id` FROM `intro` WHERE `contact-id` = `contact`.`id` AND NOT `ignore`))";
231                                 $sql_values[] = Model\Contact::SHARING;
232                                 break;
233                         default:
234                                 $sql_extra = " AND NOT `archive` AND NOT `blocked` AND NOT `pending`";
235                                 break;
236                 }
237
238                 if (isset($accounttypeid)) {
239                         $sql_extra .= " AND `contact-type` = ?";
240                         $sql_values[] = $accounttypeid;
241                 }
242
243                 $searching = false;
244                 $search_hdr = null;
245                 if ($search) {
246                         $searching = true;
247                         $search_hdr = $search;
248                         $search_txt = preg_quote($search);
249                         $sql_extra .= " AND (name REGEXP ? OR url REGEXP ? OR nick REGEXP ?)";
250                         $sql_values[] = $search_txt;
251                         $sql_values[] = $search_txt;
252                         $sql_values[] = $search_txt;
253                 }
254
255                 if ($nets) {
256                         $sql_extra .= " AND network = ? ";
257                         $sql_values[] = $nets;
258                 }
259
260                 switch ($rel) {
261                         case 'followers':
262                                 $sql_extra .= " AND `rel` IN (?, ?)";
263                                 $sql_values[] = Model\Contact::FOLLOWER;
264                                 $sql_values[] = Model\Contact::FRIEND;
265                                 break;
266                         case 'following':
267                                 $sql_extra .= " AND `rel` IN (?, ?)";
268                                 $sql_values[] = Model\Contact::SHARING;
269                                 $sql_values[] = Model\Contact::FRIEND;
270                                 break;
271                         case 'mutuals':
272                                 $sql_extra .= " AND `rel` = ?";
273                                 $sql_values[] = Model\Contact::FRIEND;
274                                 break;
275                 }
276
277                 if ($group) {
278                         $sql_extra .= " AND EXISTS(SELECT `id` FROM `group_member` WHERE `gid` = ? AND `contact`.`id` = `contact-id`)";
279                         $sql_values[] = $group;
280                 }
281
282                 $networks = Widget::unavailableNetworks();
283                 $sql_extra .= " AND NOT `network` IN (" . substr(str_repeat("?, ", count($networks)), 0, -2) . ")";
284                 $sql_values = array_merge($sql_values, $networks);
285
286                 $condition = ["`uid` = ? AND NOT `self` AND NOT `deleted`" . $sql_extra];
287                 $condition = array_merge($condition, $sql_values);
288
289                 $total = DBA::count('contact', $condition);
290
291                 $pager = new Pager(DI::l10n(), DI::args()->getQueryString());
292
293                 $contacts = [];
294
295                 $stmt = DBA::select('contact', [], $condition, ['order' => ['name'], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]]);
296
297                 while ($contact = DBA::fetch($stmt)) {
298                         $contact['blocked'] = Model\Contact\User::isBlocked($contact['id'], local_user());
299                         $contact['readonly'] = Model\Contact\User::isIgnored($contact['id'], local_user());
300                         $contacts[] = self::getContactTemplateVars($contact);
301                 }
302                 DBA::close($stmt);
303
304                 $tabs = [
305                         [
306                                 'label' => DI::l10n()->t('All Contacts'),
307                                 'url'   => 'contact',
308                                 'sel'   => !$type ? 'active' : '',
309                                 'title' => DI::l10n()->t('Show all contacts'),
310                                 'id'    => 'showall-tab',
311                                 'accesskey' => 'l',
312                         ],
313                         [
314                                 'label' => DI::l10n()->t('Pending'),
315                                 'url'   => 'contact/pending',
316                                 'sel'   => $type == 'pending' ? 'active' : '',
317                                 'title' => DI::l10n()->t('Only show pending contacts'),
318                                 'id'    => 'showpending-tab',
319                                 'accesskey' => 'p',
320                         ],
321                         [
322                                 'label' => DI::l10n()->t('Blocked'),
323                                 'url'   => 'contact/blocked',
324                                 'sel'   => $type == 'blocked' ? 'active' : '',
325                                 'title' => DI::l10n()->t('Only show blocked contacts'),
326                                 'id'    => 'showblocked-tab',
327                                 'accesskey' => 'b',
328                         ],
329                         [
330                                 'label' => DI::l10n()->t('Ignored'),
331                                 'url'   => 'contact/ignored',
332                                 'sel'   => $type == 'ignored' ? 'active' : '',
333                                 'title' => DI::l10n()->t('Only show ignored contacts'),
334                                 'id'    => 'showignored-tab',
335                                 'accesskey' => 'i',
336                         ],
337                         [
338                                 'label' => DI::l10n()->t('Archived'),
339                                 'url'   => 'contact/archived',
340                                 'sel'   => $type == 'archived' ? 'active' : '',
341                                 'title' => DI::l10n()->t('Only show archived contacts'),
342                                 'id'    => 'showarchived-tab',
343                                 'accesskey' => 'y',
344                         ],
345                         [
346                                 'label' => DI::l10n()->t('Hidden'),
347                                 'url'   => 'contact/hidden',
348                                 'sel'   => $type == 'hidden' ? 'active' : '',
349                                 'title' => DI::l10n()->t('Only show hidden contacts'),
350                                 'id'    => 'showhidden-tab',
351                                 'accesskey' => 'h',
352                         ],
353                         [
354                                 'label' => DI::l10n()->t('Groups'),
355                                 'url'   => 'group',
356                                 'sel'   => '',
357                                 'title' => DI::l10n()->t('Organize your contact groups'),
358                                 'id'    => 'contactgroups-tab',
359                                 'accesskey' => 'e',
360                         ],
361                 ];
362
363                 $tabs_tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
364                 $tabs_html = Renderer::replaceMacros($tabs_tpl, ['$tabs' => $tabs]);
365
366                 switch ($rel) {
367                         case 'followers': $header = DI::l10n()->t('Followers'); break;
368                         case 'following': $header = DI::l10n()->t('Following'); break;
369                         case 'mutuals':   $header = DI::l10n()->t('Mutual friends'); break;
370                         default:          $header = DI::l10n()->t('Contacts');
371                 }
372
373                 switch ($type) {
374                         case 'pending':  $header .= ' - ' . DI::l10n()->t('Pending'); break;
375                         case 'blocked':  $header .= ' - ' . DI::l10n()->t('Blocked'); break;
376                         case 'hidden':   $header .= ' - ' . DI::l10n()->t('Hidden'); break;
377                         case 'ignored':  $header .= ' - ' . DI::l10n()->t('Ignored'); break;
378                         case 'archived': $header .= ' - ' . DI::l10n()->t('Archived'); break;
379                 }
380
381                 $header .= $nets ? ' - ' . ContactSelector::networkToName($nets) : '';
382
383                 $tpl = Renderer::getMarkupTemplate('contacts-template.tpl');
384                 $o .= Renderer::replaceMacros($tpl, [
385                         '$header'     => $header,
386                         '$tabs'       => $tabs_html,
387                         '$total'      => $total,
388                         '$search'     => $search_hdr,
389                         '$desc'       => DI::l10n()->t('Search your contacts'),
390                         '$finding'    => $searching ? DI::l10n()->t('Results for: %s', $search) : '',
391                         '$submit'     => DI::l10n()->t('Find'),
392                         '$cmd'        => DI::args()->getCommand(),
393                         '$contacts'   => $contacts,
394                         '$form_security_token'  => BaseModule::getFormSecurityToken('contact_batch_actions'),
395                         'multiselect' => 1,
396                         '$batch_actions' => [
397                                 'contacts_batch_update'  => DI::l10n()->t('Update'),
398                                 'contacts_batch_block'   => DI::l10n()->t('Block') . '/' . DI::l10n()->t('Unblock'),
399                                 'contacts_batch_ignore'  => DI::l10n()->t('Ignore') . '/' . DI::l10n()->t('Unignore'),
400                         ],
401                         '$h_batch_actions' => DI::l10n()->t('Batch Actions'),
402                         '$paginate'   => $pager->renderFull($total),
403                 ]);
404
405                 return $o;
406         }
407
408         /**
409          * List of pages for the Contact TabBar
410          *
411          * Available Pages are 'Status', 'Profile', 'Contacts' and 'Common Friends'
412          *
413          * @param array $contact    The contact array
414          * @param int   $active_tab 1 if tab should be marked as active
415          *
416          * @return string HTML string of the contact page tabs buttons.
417          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
418          * @throws \ImagickException
419          */
420         public static function getTabsHTML(array $contact, int $active_tab)
421         {
422                 $cid = $pcid = $contact['id'];
423                 $data = Model\Contact::getPublicAndUserContactID($contact['id'], local_user());
424                 if (!empty($data['user']) && ($contact['id'] == $data['public'])) {
425                         $cid = $data['user'];
426                 } elseif (!empty($data['public'])) {
427                         $pcid = $data['public'];
428                 }
429
430                 // tabs
431                 $tabs = [
432                         [
433                                 'label' => DI::l10n()->t('Status'),
434                                 'url'   => 'contact/' . $pcid . '/conversations',
435                                 'sel'   => (($active_tab == self::TAB_CONVERSATIONS) ? 'active' : ''),
436                                 'title' => DI::l10n()->t('Conversations started by this contact'),
437                                 'id'    => 'status-tab',
438                                 'accesskey' => 'm',
439                         ],
440                         [
441                                 'label' => DI::l10n()->t('Posts and Comments'),
442                                 'url'   => 'contact/' . $pcid . '/posts',
443                                 'sel'   => (($active_tab == self::TAB_POSTS) ? 'active' : ''),
444                                 'title' => DI::l10n()->t('Status Messages and Posts'),
445                                 'id'    => 'posts-tab',
446                                 'accesskey' => 'p',
447                         ],
448                         [
449                                 'label' => DI::l10n()->t('Media'),
450                                 'url'   => 'contact/' . $pcid . '/media',
451                                 'sel'   => (($active_tab == self::TAB_MEDIA) ? 'active' : ''),
452                                 'title' => DI::l10n()->t('Posts containing media objects'),
453                                 'id'    => 'media-tab',
454                                 'accesskey' => 'd',
455                         ],
456                         [
457                                 'label' => DI::l10n()->t('Profile'),
458                                 'url'   => 'contact/' . $cid,
459                                 'sel'   => (($active_tab == self::TAB_PROFILE) ? 'active' : ''),
460                                 'title' => DI::l10n()->t('Profile Details'),
461                                 'id'    => 'profile-tab',
462                                 'accesskey' => 'o',
463                         ],
464                         ['label' => DI::l10n()->t('Contacts'),
465                                 'url'   => 'contact/' . $pcid . '/contacts',
466                                 'sel'   => (($active_tab == self::TAB_CONTACTS) ? 'active' : ''),
467                                 'title' => DI::l10n()->t('View all known contacts'),
468                                 'id'    => 'contacts-tab',
469                                 'accesskey' => 't'
470                         ],
471                 ];
472
473                 if (!empty($contact['network']) && in_array($contact['network'], [Protocol::FEED, Protocol::MAIL]) && ($cid != $pcid)) {
474                         $tabs[] = ['label' => DI::l10n()->t('Advanced'),
475                                 'url'   => 'contact/' . $cid . '/advanced/',
476                                 'sel'   => (($active_tab == self::TAB_ADVANCED) ? 'active' : ''),
477                                 'title' => DI::l10n()->t('Advanced Contact Settings'),
478                                 'id'    => 'advanced-tab',
479                                 'accesskey' => 'r'
480                         ];
481                 }
482
483                 $tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
484                 $tab_str = Renderer::replaceMacros($tab_tpl, ['$tabs' => $tabs]);
485
486                 return $tab_str;
487         }
488
489         /**
490          * Return the fields for the contact template
491          *
492          * @param array $contact Contact array
493          * @return array Template fields
494          */
495         public static function getContactTemplateVars(array $contact)
496         {
497                 $alt_text = '';
498
499                 if (!empty($contact['url']) && isset($contact['uid']) && ($contact['uid'] == 0) && local_user()) {
500                         $personal = Model\Contact::getByURL($contact['url'], false, ['uid', 'rel', 'self'], local_user());
501                         if (!empty($personal)) {
502                                 $contact['uid'] = $personal['uid'];
503                                 $contact['rel'] = $personal['rel'];
504                                 $contact['self'] = $personal['self'];
505                         }
506                 }
507
508                 if (!empty($contact['uid']) && !empty($contact['rel']) && local_user() == $contact['uid']) {
509                         switch ($contact['rel']) {
510                                 case Model\Contact::FRIEND:
511                                         $alt_text = DI::l10n()->t('Mutual Friendship');
512                                         break;
513
514                                 case Model\Contact::FOLLOWER;
515                                         $alt_text = DI::l10n()->t('is a fan of yours');
516                                         break;
517
518                                 case Model\Contact::SHARING;
519                                         $alt_text = DI::l10n()->t('you are a fan of');
520                                         break;
521
522                                 default:
523                                         break;
524                         }
525                 }
526
527                 $url = Model\Contact::magicLinkByContact($contact);
528
529                 if (strpos($url, 'redir/') === 0) {
530                         $sparkle = ' class="sparkle" ';
531                 } else {
532                         $sparkle = '';
533                 }
534
535                 if ($contact['pending']) {
536                         if (in_array($contact['rel'], [Model\Contact::FRIEND, Model\Contact::SHARING])) {
537                                 $alt_text = DI::l10n()->t('Pending outgoing contact request');
538                         } else {
539                                 $alt_text = DI::l10n()->t('Pending incoming contact request');
540                         }
541                 }
542
543                 if ($contact['self']) {
544                         $alt_text = DI::l10n()->t('This is you');
545                         $url = $contact['url'];
546                         $sparkle = '';
547                 }
548
549                 return [
550                         'id'           => $contact['id'],
551                         'url'          => $url,
552                         'img_hover'    => DI::l10n()->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
553                         'photo_menu'   => Model\Contact::photoMenu($contact),
554                         'thumb'        => Model\Contact::getThumb($contact, true),
555                         'alt_text'     => $alt_text,
556                         'name'         => $contact['name'],
557                         'nick'         => $contact['nick'],
558                         'details'      => $contact['location'],
559                         'tags'         => $contact['keywords'],
560                         'about'        => $contact['about'],
561                         'account_type' => Model\Contact::getAccountType($contact),
562                         'sparkle'      => $sparkle,
563                         'itemurl'      => ($contact['addr'] ?? '') ?: $contact['url'],
564                         'network'      => ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']),
565                 ];
566         }
567 }