3 * @copyright Copyright (C) 2010-2023, the Friendica project
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 namespace Friendica\Module\Contact;
25 use Friendica\BaseModule;
26 use Friendica\Contact\LocalRelationship;
27 use Friendica\Content\ContactSelector;
28 use Friendica\Content\Nav;
29 use Friendica\Content\Text\BBCode;
30 use Friendica\Content\Widget;
31 use Friendica\Core\Config\Capability\IManageConfigValues;
32 use Friendica\Core\Hook;
33 use Friendica\Core\L10n;
34 use Friendica\Core\Protocol;
35 use Friendica\Core\Renderer;
36 use Friendica\Core\Session\Capability\IHandleUserSessions;
37 use Friendica\Database\Database;
38 use Friendica\Database\DBA;
39 use Friendica\Model\Circle;
40 use Friendica\Model\Contact;
42 use Friendica\Module\Response;
43 use Friendica\Navigation\SystemMessages;
44 use Friendica\Network\HTTPException;
45 use Friendica\User\Settings;
46 use Friendica\Util\DateTimeFormat;
47 use Friendica\Util\Profiler;
48 use Psr\Log\LoggerInterface;
51 * Show a contact profile
53 class Profile extends BaseModule
55 /** @var LocalRelationship\Repository\LocalRelationship */
56 private $localRelationship;
59 /** @var IManageConfigValues */
61 /** @var IHandleUserSessions */
63 /** @var SystemMessages */
64 private $systemMessages;
67 /** @var Settings\Repository\UserGServer */
70 public function __construct(Settings\Repository\UserGServer $userGServer, Database $db, SystemMessages $systemMessages, IHandleUserSessions $session, L10n $l10n, LocalRelationship\Repository\LocalRelationship $localRelationship, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, App\Page $page, IManageConfigValues $config, array $server, array $parameters = [])
72 parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
74 $this->localRelationship = $localRelationship;
76 $this->config = $config;
77 $this->session = $session;
78 $this->systemMessages = $systemMessages;
80 $this->userGServer = $userGServer;
83 protected function post(array $request = [])
85 if (!$this->session->getLocalUserId()) {
89 $contact_id = $this->parameters['id'];
91 // Backward compatibility: The update still needs a user-specific contact ID
92 // Change to user-contact table check by version 2022.03
93 $cdata = Contact::getPublicAndUserContactID($contact_id, $this->session->getLocalUserId());
94 if (empty($cdata['user']) || !$this->db->exists('contact', ['id' => $cdata['user'], 'deleted' => false])) {
98 Hook::callAll('contact_edit_post', $_POST);
102 if (isset($_POST['hidden'])) {
103 $fields['hidden'] = !empty($_POST['hidden']);
106 if (isset($_POST['notify_new_posts'])) {
107 $fields['notify_new_posts'] = !empty($_POST['notify_new_posts']);
110 if (isset($_POST['fetch_further_information'])) {
111 $fields['fetch_further_information'] = intval($_POST['fetch_further_information']);
114 if (isset($_POST['remote_self'])) {
115 $fields['remote_self'] = intval($_POST['remote_self']);
118 if (isset($_POST['ffi_keyword_denylist'])) {
119 $fields['ffi_keyword_denylist'] = $_POST['ffi_keyword_denylist'];
122 if (isset($_POST['poll'])) {
123 $priority = intval($_POST['poll']);
124 if ($priority > 5 || $priority < 0) {
128 $fields['priority'] = $priority;
131 if (isset($_POST['info'])) {
132 $fields['info'] = $_POST['info'];
135 if (!Contact::update($fields, ['id' => $cdata['user'], 'uid' => $this->session->getLocalUserId()])) {
136 $this->systemMessages->addNotice($this->t('Failed to update contact record.'));
140 protected function content(array $request = []): string
142 if (!$this->session->getLocalUserId()) {
143 return Module\Security\Login::form($_SERVER['REQUEST_URI']);
146 // Backward compatibility: Ensure to use the public contact when the user contact is provided
147 // Remove by version 2022.03
148 $data = Contact::getPublicAndUserContactID(intval($this->parameters['id']), $this->session->getLocalUserId());
150 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
153 $contact = Contact::getById($data['public']);
154 if (!$this->db->isResult($contact)) {
155 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
158 // Don't display contacts that are about to be deleted
159 if ($this->db->isResult($contact) && (!empty($contact['deleted']) || !empty($contact['network']) && $contact['network'] == Protocol::PHANTOM)) {
160 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
163 $localRelationship = $this->localRelationship->getForUserContact($this->session->getLocalUserId(), $contact['id']);
165 if ($localRelationship->rel === Contact::SELF) {
166 $this->baseUrl->redirect('profile/' . $contact['nick'] . '/profile');
169 if (isset($this->parameters['action'])) {
170 self::checkFormSecurityTokenRedirectOnError('contact/' . $contact['id'], 'contact_action', 't');
172 $cmd = $this->parameters['action'];
173 if ($cmd === 'update' && $localRelationship->rel !== Contact::NOTHING) {
174 Module\Contact::updateContactFromPoll($contact['id']);
177 if ($cmd === 'updateprofile') {
178 $this->updateContactFromProbe($contact['id']);
181 if ($cmd === 'block') {
182 if ($localRelationship->blocked) {
183 // @TODO Backward compatibility, replace with $localRelationship->unblock()
184 Contact\User::setBlocked($contact['id'], $this->session->getLocalUserId(), false);
186 $message = $this->t('Contact has been unblocked');
188 // @TODO Backward compatibility, replace with $localRelationship->block()
189 Contact\User::setBlocked($contact['id'], $this->session->getLocalUserId(), true);
190 $message = $this->t('Contact has been blocked');
193 // @TODO: add $this->localRelationship->save($localRelationship);
194 $this->systemMessages->addInfo($message);
197 if ($cmd === 'ignore') {
198 if ($localRelationship->ignored) {
199 // @TODO Backward compatibility, replace with $localRelationship->unblock()
200 Contact\User::setIgnored($contact['id'], $this->session->getLocalUserId(), false);
202 $message = $this->t('Contact has been unignored');
204 // @TODO Backward compatibility, replace with $localRelationship->block()
205 Contact\User::setIgnored($contact['id'], $this->session->getLocalUserId(), true);
206 $message = $this->t('Contact has been ignored');
209 // @TODO: add $this->localRelationship->save($localRelationship);
210 $this->systemMessages->addInfo($message);
213 if ($cmd === 'collapse') {
214 if ($localRelationship->collapsed) {
215 // @TODO Backward compatibility, replace with $localRelationship->unblock()
216 Contact\User::setCollapsed($contact['id'], $this->session->getLocalUserId(), false);
218 $message = $this->t('Contact has been uncollapsed');
220 // @TODO Backward compatibility, replace with $localRelationship->block()
221 Contact\User::setCollapsed($contact['id'], $this->session->getLocalUserId(), true);
222 $message = $this->t('Contact has been collapsed');
225 // @TODO: add $this->localRelationship->save($localRelationship);
226 $this->systemMessages->addInfo($message);
229 $this->baseUrl->redirect('contact/' . $contact['id']);
232 $vcard_widget = Widget\VCard::getHTML($contact);
233 $circles_widget = '';
235 if (!in_array($localRelationship->rel, [Contact::NOTHING, Contact::SELF])) {
236 $circles_widget = Circle::sidebarWidget('contact', 'circle', 'full', 'everyone', $data['user']);
239 $this->page['aside'] .= $vcard_widget . $circles_widget;
242 Nav::setSelected('contact');
244 $_SESSION['return_path'] = $this->args->getQueryString();
246 $this->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_head.tpl'), [
249 switch ($localRelationship->rel) {
250 case Contact::FRIEND: $relation_text = $this->t('You are mutual friends with %s', $contact['name']); break;
251 case Contact::FOLLOWER: $relation_text = $this->t('You are sharing with %s', $contact['name']); break;
252 case Contact::SHARING: $relation_text = $this->t('%s is sharing with you', $contact['name']); break;
257 if (!Protocol::supportsFollow($contact['network'])) {
261 $url = Contact::magicLinkByContact($contact);
262 if (strpos($url, 'contact/redir/') === 0) {
263 $sparkle = ' class="sparkle" ';
268 $insecure = $this->t('Private communications are not available for this contact.');
270 // @TODO: Figure out why gsid can be empty
271 if (empty($contact['gsid'])) {
272 $this->logger->notice('Empty gsid for contact', ['contact' => $contact]);
277 $this->userGServer->isIgnoredByUser($this->session->getLocalUserId(), $contact['gsid']) ?
278 $this->t('This contact is on a server you ignored.')
281 $last_update = (($contact['last-update'] <= DBA::NULL_DATETIME) ? $this->t('Never') : DateTimeFormat::local($contact['last-update'], 'D, j M Y, g:i A'));
283 if ($contact['last-update'] > DBA::NULL_DATETIME) {
284 $last_update .= ' ' . ($contact['failed'] ? $this->t('(Update was not successful)') : $this->t('(Update was successful)'));
286 $lblsuggest = (($contact['network'] === Protocol::DFRN) ? $this->t('Suggest friends') : '');
288 $poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
290 $nettype = $this->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']));
293 $tab_str = Module\Contact::getTabsHTML($contact, Module\Contact::TAB_PROFILE);
295 $lost_contact = (($contact['archive'] && $contact['term-date'] > DBA::NULL_DATETIME && $contact['term-date'] < DateTimeFormat::utcNow()) ? $this->t('Communications lost with this contact!') : '');
297 $fetch_further_information = null;
298 if ($contact['network'] == Protocol::FEED) {
299 $fetch_further_information = [
300 'fetch_further_information',
301 $this->t('Fetch further information for feeds'),
302 $localRelationship->fetchFurtherInformation,
303 $this->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.'),
305 Entity\LocalRelationship::FFI_NONE => $this->t('Disabled'),
306 Entity\LocalRelationship::FFI_INFORMATION => $this->t('Fetch information'),
307 Entity\LocalRelationship::FFI_KEYWORD => $this->t('Fetch keywords'),
308 Entity\LocalRelationship::FFI_BOTH => $this->t('Fetch information and keywords')
313 $allow_remote_self = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::FEED, Protocol::DFRN, Protocol::DIASPORA, Protocol::TWITTER])
314 && $this->config->get('system', 'allow_users_remote_self');
316 if ($contact['network'] == Protocol::FEED) {
317 $remote_self_options = [
318 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
319 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
321 } elseif ($contact['network'] == Protocol::ACTIVITYPUB) {
322 $remote_self_options = [
323 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
324 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
326 } elseif ($contact['network'] == Protocol::DFRN) {
327 $remote_self_options = [
328 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
329 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting'),
330 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
333 $remote_self_options = [
334 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
335 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
339 $poll_interval = null;
340 if ((($contact['network'] == Protocol::FEED) && !$this->config->get('system', 'adjust_poll_frequency')) || ($contact['network'] == Protocol::MAIL)) {
341 $poll_interval = ContactSelector::pollInterval($localRelationship->priority, !$poll_enabled);
344 $contact_actions = $this->getContactActions($contact, $localRelationship);
346 if ($localRelationship->rel !== Contact::NOTHING) {
347 $lbl_info1 = $this->t('Contact Information / Notes');
348 $contact_settings_label = $this->t('Contact Settings');
351 $contact_settings_label = null;
354 $tpl = Renderer::getMarkupTemplate('contact_edit.tpl');
355 $o .= Renderer::replaceMacros($tpl, [
356 '$header' => $this->t('Contact'),
357 '$tab_str' => $tab_str,
358 '$submit' => $this->t('Submit'),
359 '$lbl_info1' => $lbl_info1,
360 '$lbl_info2' => $this->t('Their personal note'),
361 '$reason' => trim($contact['reason'] ?? ''),
362 '$infedit' => $this->t('Edit contact notes'),
363 '$common_link' => 'contact/' . $contact['id'] . '/contacts/common',
364 '$relation_text' => $relation_text,
365 '$visit' => $this->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
366 '$blockunblock' => $this->t('Block/Unblock contact'),
367 '$ignorecont' => $this->t('Ignore contact'),
368 '$lblrecent' => $this->t('View conversations'),
369 '$lblsuggest' => $lblsuggest,
370 '$nettype' => $nettype,
371 '$poll_interval' => $poll_interval,
372 '$poll_enabled' => $poll_enabled,
373 '$lastupdtext' => $this->t('Last update:'),
374 '$lost_contact' => $lost_contact,
375 '$updpub' => $this->t('Update public posts'),
376 '$last_update' => $last_update,
377 '$udnow' => $this->t('Update now'),
378 '$contact_id' => $contact['id'],
379 '$pending' => $localRelationship->pending ? $this->t('Awaiting connection acknowledge') : '',
380 '$blocked' => $localRelationship->blocked ? $this->t('Currently blocked') : '',
381 '$ignored' => $localRelationship->ignored ? $this->t('Currently ignored') : '',
382 '$collapsed' => $localRelationship->collapsed ? $this->t('Currently collapsed') : '',
383 '$archived' => ($contact['archive'] ? $this->t('Currently archived') : ''),
384 '$insecure' => (in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::MAIL, Protocol::DIASPORA]) ? '' : $insecure),
385 '$serverIgnored' => $serverIgnored,
386 '$manageServers' => $this->t('Manage remote servers'),
387 '$cinfo' => ['info', '', $localRelationship->info, ''],
388 '$hidden' => ['hidden', $this->t('Hide this contact from others'), $localRelationship->hidden, $this->t('Replies/likes to your public posts <strong>may</strong> still be visible')],
389 '$notify_new_posts' => ['notify_new_posts', $this->t('Notification for new posts'), ($localRelationship->notifyNewPosts), $this->t('Send a notification of every new post of this contact')],
390 '$fetch_further_information' => $fetch_further_information,
391 '$ffi_keyword_denylist' => ['ffi_keyword_denylist', $this->t('Keyword Deny List'), $localRelationship->ffiKeywordDenylist, $this->t('Comma separated list of keywords that should not be converted to hashtags, when "Fetch information and keywords" is selected')],
392 '$photo' => Contact::getPhoto($contact),
393 '$name' => $contact['name'],
394 '$sparkle' => $sparkle,
396 '$profileurllabel' => $this->t('Profile URL'),
397 '$profileurl' => $contact['url'],
398 '$account_type' => Contact::getAccountType($contact['contact-type']),
399 '$location' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['location']),
400 '$location_label' => $this->t('Location:'),
401 '$xmpp' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['xmpp']),
402 '$xmpp_label' => $this->t('XMPP:'),
403 '$matrix' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['matrix']),
404 '$matrix_label' => $this->t('Matrix:'),
405 '$about' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['about'], BBCode::EXTERNAL),
406 '$about_label' => $this->t('About:'),
407 '$keywords' => $contact['keywords'],
408 '$keywords_label' => $this->t('Tags:'),
409 '$contact_action_button' => $this->t('Actions'),
410 '$contact_actions' => $contact_actions,
411 '$contact_status' => $this->t('Status'),
412 '$contact_settings_label' => $contact_settings_label,
413 '$contact_profile_label' => $this->t('Profile'),
414 '$allow_remote_self' => $allow_remote_self,
417 $this->t('Mirror postings from this contact'),
418 $localRelationship->remoteSelf,
419 $this->t('Mark this contact as remote_self, this will cause friendica to repost new entries from this contact.'),
424 $arr = ['contact' => $contact, 'output' => $o];
426 Hook::callAll('contact_edit', $arr);
428 return $arr['output'];
432 * Returns the list of available actions that can performed on the provided contact
434 * This includes actions like e.g. 'block', 'hide', 'delete' and others
436 * @param array $contact Public contact row
437 * @param LocalRelationship\Entity\LocalRelationship $localRelationship
438 * @return array with contact related actions
439 * @throws HTTPException\InternalServerErrorException
441 private function getContactActions(array $contact, LocalRelationship\Entity\LocalRelationship $localRelationship): array
443 $poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
444 $contact_actions = [];
446 $formSecurityToken = self::getFormSecurityToken('contact_action');
448 if ($localRelationship->rel & Contact::SHARING) {
449 $contact_actions['unfollow'] = [
450 'label' => $this->t('Unfollow'),
451 'url' => 'contact/unfollow?url=' . urlencode($contact['url']) . '&auto=1',
457 $contact_actions['follow'] = [
458 'label' => $this->t('Follow'),
459 'url' => 'contact/follow?url=' . urlencode($contact['url']) . '&auto=1',
466 // Provide friend suggestion only for Friendica contacts
467 if ($contact['network'] === Protocol::DFRN) {
468 $contact_actions['suggest'] = [
469 'label' => $this->t('Suggest friends'),
470 'url' => 'fsuggest/' . $contact['id'],
478 $contact_actions['update'] = [
479 'label' => $this->t('Update now'),
480 'url' => 'contact/' . $contact['id'] . '/update?t=' . $formSecurityToken,
487 if (Protocol::supportsProbe($contact['network'])) {
488 $contact_actions['updateprofile'] = [
489 'label' => $this->t('Refetch contact data'),
490 'url' => 'contact/' . $contact['id'] . '/updateprofile?t=' . $formSecurityToken,
493 'id' => 'updateprofile',
497 $contact_actions['block'] = [
498 'label' => $localRelationship->blocked ? $this->t('Unblock') : $this->t('Block'),
499 'url' => 'contact/' . $contact['id'] . '/block?t=' . $formSecurityToken,
500 'title' => $this->t('Toggle Blocked status'),
501 'sel' => $localRelationship->blocked ? 'active' : '',
502 'id' => 'toggle-block',
505 $contact_actions['ignore'] = [
506 'label' => $localRelationship->ignored ? $this->t('Unignore') : $this->t('Ignore'),
507 'url' => 'contact/' . $contact['id'] . '/ignore?t=' . $formSecurityToken,
508 'title' => $this->t('Toggle Ignored status'),
509 'sel' => $localRelationship->ignored ? 'active' : '',
510 'id' => 'toggle-ignore',
513 $contact_actions['collapse'] = [
514 'label' => $localRelationship->collapsed ? $this->t('Uncollapse') : $this->t('Collapse'),
515 'url' => 'contact/' . $contact['id'] . '/collapse?t=' . $formSecurityToken,
516 'title' => $this->t('Toggle Collapsed status'),
517 'sel' => $localRelationship->collapsed ? 'active' : '',
518 'id' => 'toggle-collapse',
521 if (Protocol::supportsRevokeFollow($contact['network']) && in_array($localRelationship->rel, [Contact::FOLLOWER, Contact::FRIEND])) {
522 $contact_actions['revoke_follow'] = [
523 'label' => $this->t('Revoke Follow'),
524 'url' => 'contact/' . $contact['id'] . '/revoke',
525 'title' => $this->t('Revoke the follow from this contact'),
527 'id' => 'revoke_follow',
531 return $contact_actions;
535 * Updates contact from probing
537 * @param int $contact_id Id of the contact with uid != 0
539 * @throws HTTPException\InternalServerErrorException
540 * @throws \ImagickException
542 private function updateContactFromProbe(int $contact_id)
544 if (!$this->db->exists('contact', ['id' => $contact_id, 'uid' => [0, $this->session->getLocalUserId()], 'deleted' => false])) {
548 // Update the entry in the contact table
549 Contact::updateFromProbe($contact_id);