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\Entity;
27 use Friendica\Contact\LocalRelationship\Repository;
28 use Friendica\Content\ContactSelector;
29 use Friendica\Content\Nav;
30 use Friendica\Content\Text\BBCode;
31 use Friendica\Content\Widget;
32 use Friendica\Core\Config\Capability\IManageConfigValues;
33 use Friendica\Core\Hook;
34 use Friendica\Core\L10n;
35 use Friendica\Core\Protocol;
36 use Friendica\Core\Renderer;
37 use Friendica\Core\Session\Capability\IHandleUserSessions;
38 use Friendica\Database\Database;
39 use Friendica\Database\DBA;
40 use Friendica\Model\Circle;
41 use Friendica\Model\Contact;
43 use Friendica\Module\Response;
44 use Friendica\Navigation\SystemMessages;
45 use Friendica\Network\HTTPException;
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;
68 public function __construct(Database $db, SystemMessages $systemMessages, IHandleUserSessions $session, L10n $l10n, Repository\LocalRelationship $localRelationship, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, App\Page $page, IManageConfigValues $config, array $server, array $parameters = [])
70 parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
72 $this->localRelationship = $localRelationship;
74 $this->config = $config;
75 $this->session = $session;
76 $this->systemMessages = $systemMessages;
80 protected function post(array $request = [])
82 if (!$this->session->getLocalUserId()) {
86 $contact_id = $this->parameters['id'];
88 // Backward compatibility: The update still needs a user-specific contact ID
89 // Change to user-contact table check by version 2022.03
90 $cdata = Contact::getPublicAndUserContactID($contact_id, $this->session->getLocalUserId());
91 if (empty($cdata['user']) || !$this->db->exists('contact', ['id' => $cdata['user'], 'deleted' => false])) {
95 Hook::callAll('contact_edit_post', $_POST);
99 if (isset($_POST['hidden'])) {
100 $fields['hidden'] = !empty($_POST['hidden']);
103 if (isset($_POST['notify_new_posts'])) {
104 $fields['notify_new_posts'] = !empty($_POST['notify_new_posts']);
107 if (isset($_POST['fetch_further_information'])) {
108 $fields['fetch_further_information'] = intval($_POST['fetch_further_information']);
111 if (isset($_POST['remote_self'])) {
112 $fields['remote_self'] = intval($_POST['remote_self']);
115 if (isset($_POST['ffi_keyword_denylist'])) {
116 $fields['ffi_keyword_denylist'] = $_POST['ffi_keyword_denylist'];
119 if (isset($_POST['poll'])) {
120 $priority = intval($_POST['poll']);
121 if ($priority > 5 || $priority < 0) {
125 $fields['priority'] = $priority;
128 if (isset($_POST['info'])) {
129 $fields['info'] = $_POST['info'];
132 if (!Contact::update($fields, ['id' => $cdata['user'], 'uid' => $this->session->getLocalUserId()])) {
133 $this->systemMessages->addNotice($this->t('Failed to update contact record.'));
137 protected function content(array $request = []): string
139 if (!$this->session->getLocalUserId()) {
140 return Module\Security\Login::form($_SERVER['REQUEST_URI']);
143 // Backward compatibility: Ensure to use the public contact when the user contact is provided
144 // Remove by version 2022.03
145 $data = Contact::getPublicAndUserContactID(intval($this->parameters['id']), $this->session->getLocalUserId());
147 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
150 $contact = Contact::getById($data['public']);
151 if (!$this->db->isResult($contact)) {
152 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
155 // Don't display contacts that are about to be deleted
156 if ($this->db->isResult($contact) && (!empty($contact['deleted']) || !empty($contact['network']) && $contact['network'] == Protocol::PHANTOM)) {
157 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
160 $localRelationship = $this->localRelationship->getForUserContact($this->session->getLocalUserId(), $contact['id']);
162 if ($localRelationship->rel === Contact::SELF) {
163 $this->baseUrl->redirect('profile/' . $contact['nick'] . '/profile');
166 if (isset($this->parameters['action'])) {
167 self::checkFormSecurityTokenRedirectOnError('contact/' . $contact['id'], 'contact_action', 't');
169 $cmd = $this->parameters['action'];
170 if ($cmd === 'update' && $localRelationship->rel !== Contact::NOTHING) {
171 Module\Contact::updateContactFromPoll($contact['id']);
174 if ($cmd === 'updateprofile') {
175 $this->updateContactFromProbe($contact['id']);
178 if ($cmd === 'block') {
179 if ($localRelationship->blocked) {
180 // @TODO Backward compatibility, replace with $localRelationship->unblock()
181 Contact\User::setBlocked($contact['id'], $this->session->getLocalUserId(), false);
183 $message = $this->t('Contact has been unblocked');
185 // @TODO Backward compatibility, replace with $localRelationship->block()
186 Contact\User::setBlocked($contact['id'], $this->session->getLocalUserId(), true);
187 $message = $this->t('Contact has been blocked');
190 // @TODO: add $this->localRelationship->save($localRelationship);
191 $this->systemMessages->addInfo($message);
194 if ($cmd === 'ignore') {
195 if ($localRelationship->ignored) {
196 // @TODO Backward compatibility, replace with $localRelationship->unblock()
197 Contact\User::setIgnored($contact['id'], $this->session->getLocalUserId(), false);
199 $message = $this->t('Contact has been unignored');
201 // @TODO Backward compatibility, replace with $localRelationship->block()
202 Contact\User::setIgnored($contact['id'], $this->session->getLocalUserId(), true);
203 $message = $this->t('Contact has been ignored');
206 // @TODO: add $this->localRelationship->save($localRelationship);
207 $this->systemMessages->addInfo($message);
210 if ($cmd === 'collapse') {
211 if ($localRelationship->collapsed) {
212 // @TODO Backward compatibility, replace with $localRelationship->unblock()
213 Contact\User::setCollapsed($contact['id'], $this->session->getLocalUserId(), false);
215 $message = $this->t('Contact has been uncollapsed');
217 // @TODO Backward compatibility, replace with $localRelationship->block()
218 Contact\User::setCollapsed($contact['id'], $this->session->getLocalUserId(), true);
219 $message = $this->t('Contact has been collapsed');
222 // @TODO: add $this->localRelationship->save($localRelationship);
223 $this->systemMessages->addInfo($message);
226 $this->baseUrl->redirect('contact/' . $contact['id']);
229 $vcard_widget = Widget\VCard::getHTML($contact);
230 $circles_widget = '';
232 if (!in_array($localRelationship->rel, [Contact::NOTHING, Contact::SELF])) {
233 $circles_widget = Circle::sidebarWidget('contact', 'circle', 'full', 'everyone', $data['user']);
236 $this->page['aside'] .= $vcard_widget . $circles_widget;
239 Nav::setSelected('contact');
241 $_SESSION['return_path'] = $this->args->getQueryString();
243 $this->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_head.tpl'), [
246 switch ($localRelationship->rel) {
247 case Contact::FRIEND: $relation_text = $this->t('You are mutual friends with %s', $contact['name']); break;
248 case Contact::FOLLOWER: $relation_text = $this->t('You are sharing with %s', $contact['name']); break;
249 case Contact::SHARING: $relation_text = $this->t('%s is sharing with you', $contact['name']); break;
254 if (!Protocol::supportsFollow($contact['network'])) {
258 $url = Contact::magicLinkByContact($contact);
259 if (strpos($url, 'contact/redir/') === 0) {
260 $sparkle = ' class="sparkle" ';
265 $insecure = $this->t('Private communications are not available for this contact.');
267 $last_update = (($contact['last-update'] <= DBA::NULL_DATETIME) ? $this->t('Never') : DateTimeFormat::local($contact['last-update'], 'D, j M Y, g:i A'));
269 if ($contact['last-update'] > DBA::NULL_DATETIME) {
270 $last_update .= ' ' . ($contact['failed'] ? $this->t('(Update was not successful)') : $this->t('(Update was successful)'));
272 $lblsuggest = (($contact['network'] === Protocol::DFRN) ? $this->t('Suggest friends') : '');
274 $poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
276 $nettype = $this->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']));
279 $tab_str = Module\Contact::getTabsHTML($contact, Module\Contact::TAB_PROFILE);
281 $lost_contact = (($contact['archive'] && $contact['term-date'] > DBA::NULL_DATETIME && $contact['term-date'] < DateTimeFormat::utcNow()) ? $this->t('Communications lost with this contact!') : '');
283 $fetch_further_information = null;
284 if ($contact['network'] == Protocol::FEED) {
285 $fetch_further_information = [
286 'fetch_further_information',
287 $this->t('Fetch further information for feeds'),
288 $localRelationship->fetchFurtherInformation,
289 $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.'),
291 Entity\LocalRelationship::FFI_NONE => $this->t('Disabled'),
292 Entity\LocalRelationship::FFI_INFORMATION => $this->t('Fetch information'),
293 Entity\LocalRelationship::FFI_KEYWORD => $this->t('Fetch keywords'),
294 Entity\LocalRelationship::FFI_BOTH => $this->t('Fetch information and keywords')
299 $allow_remote_self = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::FEED, Protocol::DFRN, Protocol::DIASPORA, Protocol::TWITTER])
300 && $this->config->get('system', 'allow_users_remote_self');
302 if ($contact['network'] == Protocol::FEED) {
303 $remote_self_options = [
304 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
305 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
307 } elseif ($contact['network'] == Protocol::ACTIVITYPUB) {
308 $remote_self_options = [
309 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
310 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
312 } elseif ($contact['network'] == Protocol::DFRN) {
313 $remote_self_options = [
314 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
315 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting'),
316 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
319 $remote_self_options = [
320 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
321 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
325 $poll_interval = null;
326 if ((($contact['network'] == Protocol::FEED) && !$this->config->get('system', 'adjust_poll_frequency')) || ($contact['network'] == Protocol::MAIL)) {
327 $poll_interval = ContactSelector::pollInterval($localRelationship->priority, !$poll_enabled);
330 $contact_actions = $this->getContactActions($contact, $localRelationship);
332 if ($localRelationship->rel !== Contact::NOTHING) {
333 $lbl_info1 = $this->t('Contact Information / Notes');
334 $contact_settings_label = $this->t('Contact Settings');
337 $contact_settings_label = null;
340 $tpl = Renderer::getMarkupTemplate('contact_edit.tpl');
341 $o .= Renderer::replaceMacros($tpl, [
342 '$header' => $this->t('Contact'),
343 '$tab_str' => $tab_str,
344 '$submit' => $this->t('Submit'),
345 '$lbl_info1' => $lbl_info1,
346 '$lbl_info2' => $this->t('Their personal note'),
347 '$reason' => trim($contact['reason'] ?? ''),
348 '$infedit' => $this->t('Edit contact notes'),
349 '$common_link' => 'contact/' . $contact['id'] . '/contacts/common',
350 '$relation_text' => $relation_text,
351 '$visit' => $this->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
352 '$blockunblock' => $this->t('Block/Unblock contact'),
353 '$ignorecont' => $this->t('Ignore contact'),
354 '$lblrecent' => $this->t('View conversations'),
355 '$lblsuggest' => $lblsuggest,
356 '$nettype' => $nettype,
357 '$poll_interval' => $poll_interval,
358 '$poll_enabled' => $poll_enabled,
359 '$lastupdtext' => $this->t('Last update:'),
360 '$lost_contact' => $lost_contact,
361 '$updpub' => $this->t('Update public posts'),
362 '$last_update' => $last_update,
363 '$udnow' => $this->t('Update now'),
364 '$contact_id' => $contact['id'],
365 '$pending' => $localRelationship->pending ? $this->t('Awaiting connection acknowledge') : '',
366 '$blocked' => $localRelationship->blocked ? $this->t('Currently blocked') : '',
367 '$ignored' => $localRelationship->ignored ? $this->t('Currently ignored') : '',
368 '$collapsed' => $localRelationship->collapsed ? $this->t('Currently collapsed') : '',
369 '$archived' => ($contact['archive'] ? $this->t('Currently archived') : ''),
370 '$insecure' => (in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::MAIL, Protocol::DIASPORA]) ? '' : $insecure),
371 '$cinfo' => ['info', '', $localRelationship->info, ''],
372 '$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')],
373 '$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')],
374 '$fetch_further_information' => $fetch_further_information,
375 '$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')],
376 '$photo' => Contact::getPhoto($contact),
377 '$name' => $contact['name'],
378 '$sparkle' => $sparkle,
380 '$profileurllabel' => $this->t('Profile URL'),
381 '$profileurl' => $contact['url'],
382 '$account_type' => Contact::getAccountType($contact['contact-type']),
383 '$location' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['location']),
384 '$location_label' => $this->t('Location:'),
385 '$xmpp' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['xmpp']),
386 '$xmpp_label' => $this->t('XMPP:'),
387 '$matrix' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['matrix']),
388 '$matrix_label' => $this->t('Matrix:'),
389 '$about' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['about'], BBCode::EXTERNAL),
390 '$about_label' => $this->t('About:'),
391 '$keywords' => $contact['keywords'],
392 '$keywords_label' => $this->t('Tags:'),
393 '$contact_action_button' => $this->t('Actions'),
394 '$contact_actions' => $contact_actions,
395 '$contact_status' => $this->t('Status'),
396 '$contact_settings_label' => $contact_settings_label,
397 '$contact_profile_label' => $this->t('Profile'),
398 '$allow_remote_self' => $allow_remote_self,
401 $this->t('Mirror postings from this contact'),
402 $localRelationship->remoteSelf,
403 $this->t('Mark this contact as remote_self, this will cause friendica to repost new entries from this contact.'),
408 $arr = ['contact' => $contact, 'output' => $o];
410 Hook::callAll('contact_edit', $arr);
412 return $arr['output'];
416 * Returns the list of available actions that can performed on the provided contact
418 * This includes actions like e.g. 'block', 'hide', 'delete' and others
420 * @param array $contact Public contact row
421 * @param Entity\LocalRelationship $localRelationship
422 * @return array with contact related actions
423 * @throws HTTPException\InternalServerErrorException
425 private function getContactActions(array $contact, Entity\LocalRelationship $localRelationship): array
427 $poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
428 $contact_actions = [];
430 $formSecurityToken = self::getFormSecurityToken('contact_action');
432 if ($localRelationship->rel & Contact::SHARING) {
433 $contact_actions['unfollow'] = [
434 'label' => $this->t('Unfollow'),
435 'url' => 'contact/unfollow?url=' . urlencode($contact['url']) . '&auto=1',
441 $contact_actions['follow'] = [
442 'label' => $this->t('Follow'),
443 'url' => 'contact/follow?url=' . urlencode($contact['url']) . '&auto=1',
450 // Provide friend suggestion only for Friendica contacts
451 if ($contact['network'] === Protocol::DFRN) {
452 $contact_actions['suggest'] = [
453 'label' => $this->t('Suggest friends'),
454 'url' => 'fsuggest/' . $contact['id'],
462 $contact_actions['update'] = [
463 'label' => $this->t('Update now'),
464 'url' => 'contact/' . $contact['id'] . '/update?t=' . $formSecurityToken,
471 if (Protocol::supportsProbe($contact['network'])) {
472 $contact_actions['updateprofile'] = [
473 'label' => $this->t('Refetch contact data'),
474 'url' => 'contact/' . $contact['id'] . '/updateprofile?t=' . $formSecurityToken,
477 'id' => 'updateprofile',
481 $contact_actions['block'] = [
482 'label' => $localRelationship->blocked ? $this->t('Unblock') : $this->t('Block'),
483 'url' => 'contact/' . $contact['id'] . '/block?t=' . $formSecurityToken,
484 'title' => $this->t('Toggle Blocked status'),
485 'sel' => $localRelationship->blocked ? 'active' : '',
486 'id' => 'toggle-block',
489 $contact_actions['ignore'] = [
490 'label' => $localRelationship->ignored ? $this->t('Unignore') : $this->t('Ignore'),
491 'url' => 'contact/' . $contact['id'] . '/ignore?t=' . $formSecurityToken,
492 'title' => $this->t('Toggle Ignored status'),
493 'sel' => $localRelationship->ignored ? 'active' : '',
494 'id' => 'toggle-ignore',
497 $contact_actions['collapse'] = [
498 'label' => $localRelationship->collapsed ? $this->t('Uncollapse') : $this->t('Collapse'),
499 'url' => 'contact/' . $contact['id'] . '/collapse?t=' . $formSecurityToken,
500 'title' => $this->t('Toggle Collapsed status'),
501 'sel' => $localRelationship->collapsed ? 'active' : '',
502 'id' => 'toggle-collapse',
505 if (Protocol::supportsRevokeFollow($contact['network']) && in_array($localRelationship->rel, [Contact::FOLLOWER, Contact::FRIEND])) {
506 $contact_actions['revoke_follow'] = [
507 'label' => $this->t('Revoke Follow'),
508 'url' => 'contact/' . $contact['id'] . '/revoke',
509 'title' => $this->t('Revoke the follow from this contact'),
511 'id' => 'revoke_follow',
515 return $contact_actions;
519 * Updates contact from probing
521 * @param int $contact_id Id of the contact with uid != 0
523 * @throws HTTPException\InternalServerErrorException
524 * @throws \ImagickException
526 private function updateContactFromProbe(int $contact_id)
528 if (!$this->db->exists('contact', ['id' => $contact_id, 'uid' => [0, $this->session->getLocalUserId()], 'deleted' => false])) {
532 // Update the entry in the contact table
533 Contact::updateFromProbe($contact_id);