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\Database\DBA;
39 use Friendica\Model\Contact;
40 use Friendica\Model\Circle;
42 use Friendica\Module\Response;
43 use Friendica\Network\HTTPException;
44 use Friendica\Util\DateTimeFormat;
45 use Friendica\Util\Profiler;
46 use Psr\Log\LoggerInterface;
49 * Show a contact profile
51 class Profile extends BaseModule
54 * @var Repository\LocalRelationship
56 private $localRelationship;
62 * @var IManageConfigValues
66 public function __construct(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 = [])
68 parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
70 $this->localRelationship = $localRelationship;
72 $this->config = $config;
75 protected function post(array $request = [])
77 if (!DI::userSession()->getLocalUserId()) {
81 $contact_id = $this->parameters['id'];
83 // Backward compatibility: The update still needs a user-specific contact ID
84 // Change to user-contact table check by version 2022.03
85 $cdata = Contact::getPublicAndUserContactID($contact_id, DI::userSession()->getLocalUserId());
86 if (empty($cdata['user']) || !DBA::exists('contact', ['id' => $cdata['user'], 'deleted' => false])) {
90 Hook::callAll('contact_edit_post', $_POST);
94 if (isset($_POST['hidden'])) {
95 $fields['hidden'] = !empty($_POST['hidden']);
98 if (isset($_POST['notify_new_posts'])) {
99 $fields['notify_new_posts'] = !empty($_POST['notify_new_posts']);
102 if (isset($_POST['fetch_further_information'])) {
103 $fields['fetch_further_information'] = intval($_POST['fetch_further_information']);
106 if (isset($_POST['remote_self'])) {
107 $fields['remote_self'] = intval($_POST['remote_self']);
110 if (isset($_POST['ffi_keyword_denylist'])) {
111 $fields['ffi_keyword_denylist'] = $_POST['ffi_keyword_denylist'];
114 if (isset($_POST['poll'])) {
115 $priority = intval($_POST['poll']);
116 if ($priority > 5 || $priority < 0) {
120 $fields['priority'] = $priority;
123 if (isset($_POST['info'])) {
124 $fields['info'] = $_POST['info'];
127 if (!Contact::update($fields, ['id' => $cdata['user'], 'uid' => DI::userSession()->getLocalUserId()])) {
128 DI::sysmsg()->addNotice($this->t('Failed to update contact record.'));
132 protected function content(array $request = []): string
134 if (!DI::userSession()->getLocalUserId()) {
135 return Module\Security\Login::form($_SERVER['REQUEST_URI']);
138 // Backward compatibility: Ensure to use the public contact when the user contact is provided
139 // Remove by version 2022.03
140 $data = Contact::getPublicAndUserContactID(intval($this->parameters['id']), DI::userSession()->getLocalUserId());
142 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
145 $contact = Contact::getById($data['public']);
146 if (!DBA::isResult($contact)) {
147 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
150 // Don't display contacts that are about to be deleted
151 if (DBA::isResult($contact) && (!empty($contact['deleted']) || !empty($contact['network']) && $contact['network'] == Protocol::PHANTOM)) {
152 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
155 $localRelationship = $this->localRelationship->getForUserContact(DI::userSession()->getLocalUserId(), $contact['id']);
157 if ($localRelationship->rel === Contact::SELF) {
158 $this->baseUrl->redirect('profile/' . $contact['nick'] . '/profile');
161 if (isset($this->parameters['action'])) {
162 self::checkFormSecurityTokenRedirectOnError('contact/' . $contact['id'], 'contact_action', 't');
164 $cmd = $this->parameters['action'];
165 if ($cmd === 'update' && $localRelationship->rel !== Contact::NOTHING) {
166 Module\Contact::updateContactFromPoll($contact['id']);
169 if ($cmd === 'updateprofile') {
170 self::updateContactFromProbe($contact['id']);
173 if ($cmd === 'block') {
174 if ($localRelationship->blocked) {
175 // @TODO Backward compatibility, replace with $localRelationship->unblock()
176 Contact\User::setBlocked($contact['id'], DI::userSession()->getLocalUserId(), false);
178 $message = $this->t('Contact has been unblocked');
180 // @TODO Backward compatibility, replace with $localRelationship->block()
181 Contact\User::setBlocked($contact['id'], DI::userSession()->getLocalUserId(), true);
182 $message = $this->t('Contact has been blocked');
185 // @TODO: add $this->localRelationship->save($localRelationship);
186 DI::sysmsg()->addInfo($message);
189 if ($cmd === 'ignore') {
190 if ($localRelationship->ignored) {
191 // @TODO Backward compatibility, replace with $localRelationship->unblock()
192 Contact\User::setIgnored($contact['id'], DI::userSession()->getLocalUserId(), false);
194 $message = $this->t('Contact has been unignored');
196 // @TODO Backward compatibility, replace with $localRelationship->block()
197 Contact\User::setIgnored($contact['id'], DI::userSession()->getLocalUserId(), true);
198 $message = $this->t('Contact has been ignored');
201 // @TODO: add $this->localRelationship->save($localRelationship);
202 DI::sysmsg()->addInfo($message);
205 if ($cmd === 'collapse') {
206 if ($localRelationship->collapsed) {
207 // @TODO Backward compatibility, replace with $localRelationship->unblock()
208 Contact\User::setCollapsed($contact['id'], DI::userSession()->getLocalUserId(), false);
210 $message = $this->t('Contact has been uncollapsed');
212 // @TODO Backward compatibility, replace with $localRelationship->block()
213 Contact\User::setCollapsed($contact['id'], DI::userSession()->getLocalUserId(), true);
214 $message = $this->t('Contact has been collapsed');
217 // @TODO: add $this->localRelationship->save($localRelationship);
218 DI::sysmsg()->addInfo($message);
221 $this->baseUrl->redirect('contact/' . $contact['id']);
224 $vcard_widget = Widget\VCard::getHTML($contact);
225 $circles_widget = '';
227 if (!in_array($localRelationship->rel, [Contact::NOTHING, Contact::SELF])) {
228 $circles_widget = Circle::sidebarWidget('contact', 'circle', 'full', 'everyone', $data['user']);
231 $this->page['aside'] .= $vcard_widget . $circles_widget;
234 Nav::setSelected('contact');
236 $_SESSION['return_path'] = $this->args->getQueryString();
238 $this->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_head.tpl'), [
241 switch ($localRelationship->rel) {
242 case Contact::FRIEND: $relation_text = $this->t('You are mutual friends with %s', $contact['name']); break;
243 case Contact::FOLLOWER: $relation_text = $this->t('You are sharing with %s', $contact['name']); break;
244 case Contact::SHARING: $relation_text = $this->t('%s is sharing with you', $contact['name']); break;
249 if (!Protocol::supportsFollow($contact['network'])) {
253 $url = Contact::magicLinkByContact($contact);
254 if (strpos($url, 'contact/redir/') === 0) {
255 $sparkle = ' class="sparkle" ';
260 $insecure = $this->t('Private communications are not available for this contact.');
262 $last_update = (($contact['last-update'] <= DBA::NULL_DATETIME) ? $this->t('Never') : DateTimeFormat::local($contact['last-update'], 'D, j M Y, g:i A'));
264 if ($contact['last-update'] > DBA::NULL_DATETIME) {
265 $last_update .= ' ' . ($contact['failed'] ? $this->t('(Update was not successful)') : $this->t('(Update was successful)'));
267 $lblsuggest = (($contact['network'] === Protocol::DFRN) ? $this->t('Suggest friends') : '');
269 $poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
271 $nettype = $this->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']));
274 $tab_str = Module\Contact::getTabsHTML($contact, Module\Contact::TAB_PROFILE);
276 $lost_contact = (($contact['archive'] && $contact['term-date'] > DBA::NULL_DATETIME && $contact['term-date'] < DateTimeFormat::utcNow()) ? $this->t('Communications lost with this contact!') : '');
278 $fetch_further_information = null;
279 if ($contact['network'] == Protocol::FEED) {
280 $fetch_further_information = [
281 'fetch_further_information',
282 $this->t('Fetch further information for feeds'),
283 $localRelationship->fetchFurtherInformation,
284 $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.'),
286 Entity\LocalRelationship::FFI_NONE => $this->t('Disabled'),
287 Entity\LocalRelationship::FFI_INFORMATION => $this->t('Fetch information'),
288 Entity\LocalRelationship::FFI_KEYWORD => $this->t('Fetch keywords'),
289 Entity\LocalRelationship::FFI_BOTH => $this->t('Fetch information and keywords')
294 $allow_remote_self = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::FEED, Protocol::DFRN, Protocol::DIASPORA, Protocol::TWITTER])
295 && $this->config->get('system', 'allow_users_remote_self');
297 if ($contact['network'] == Protocol::FEED) {
298 $remote_self_options = [
299 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
300 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
302 } elseif ($contact['network'] == Protocol::ACTIVITYPUB) {
303 $remote_self_options = [
304 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
305 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
307 } elseif ($contact['network'] == Protocol::DFRN) {
308 $remote_self_options = [
309 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
310 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting'),
311 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
314 $remote_self_options = [
315 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
316 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
320 $poll_interval = null;
321 if ((($contact['network'] == Protocol::FEED) && !$this->config->get('system', 'adjust_poll_frequency')) || ($contact['network'] == Protocol::MAIL)) {
322 $poll_interval = ContactSelector::pollInterval($localRelationship->priority, !$poll_enabled);
325 $contact_actions = $this->getContactActions($contact, $localRelationship);
327 if ($localRelationship->rel !== Contact::NOTHING) {
328 $lbl_info1 = $this->t('Contact Information / Notes');
329 $contact_settings_label = $this->t('Contact Settings');
332 $contact_settings_label = null;
335 $tpl = Renderer::getMarkupTemplate('contact_edit.tpl');
336 $o .= Renderer::replaceMacros($tpl, [
337 '$header' => $this->t('Contact'),
338 '$tab_str' => $tab_str,
339 '$submit' => $this->t('Submit'),
340 '$lbl_info1' => $lbl_info1,
341 '$lbl_info2' => $this->t('Their personal note'),
342 '$reason' => trim($contact['reason'] ?? ''),
343 '$infedit' => $this->t('Edit contact notes'),
344 '$common_link' => 'contact/' . $contact['id'] . '/contacts/common',
345 '$relation_text' => $relation_text,
346 '$visit' => $this->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
347 '$blockunblock' => $this->t('Block/Unblock contact'),
348 '$ignorecont' => $this->t('Ignore contact'),
349 '$lblrecent' => $this->t('View conversations'),
350 '$lblsuggest' => $lblsuggest,
351 '$nettype' => $nettype,
352 '$poll_interval' => $poll_interval,
353 '$poll_enabled' => $poll_enabled,
354 '$lastupdtext' => $this->t('Last update:'),
355 '$lost_contact' => $lost_contact,
356 '$updpub' => $this->t('Update public posts'),
357 '$last_update' => $last_update,
358 '$udnow' => $this->t('Update now'),
359 '$contact_id' => $contact['id'],
360 '$pending' => $localRelationship->pending ? $this->t('Awaiting connection acknowledge') : '',
361 '$blocked' => $localRelationship->blocked ? $this->t('Currently blocked') : '',
362 '$ignored' => $localRelationship->ignored ? $this->t('Currently ignored') : '',
363 '$collapsed' => $localRelationship->collapsed ? $this->t('Currently collapsed') : '',
364 '$archived' => ($contact['archive'] ? $this->t('Currently archived') : ''),
365 '$insecure' => (in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::MAIL, Protocol::DIASPORA]) ? '' : $insecure),
366 '$cinfo' => ['info', '', $localRelationship->info, ''],
367 '$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')],
368 '$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')],
369 '$fetch_further_information' => $fetch_further_information,
370 '$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')],
371 '$photo' => Contact::getPhoto($contact),
372 '$name' => $contact['name'],
373 '$sparkle' => $sparkle,
375 '$profileurllabel' => $this->t('Profile URL'),
376 '$profileurl' => $contact['url'],
377 '$account_type' => Contact::getAccountType($contact['contact-type']),
378 '$location' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['location']),
379 '$location_label' => $this->t('Location:'),
380 '$xmpp' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['xmpp']),
381 '$xmpp_label' => $this->t('XMPP:'),
382 '$matrix' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['matrix']),
383 '$matrix_label' => $this->t('Matrix:'),
384 '$about' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['about'], BBCode::EXTERNAL),
385 '$about_label' => $this->t('About:'),
386 '$keywords' => $contact['keywords'],
387 '$keywords_label' => $this->t('Tags:'),
388 '$contact_action_button' => $this->t('Actions'),
389 '$contact_actions' => $contact_actions,
390 '$contact_status' => $this->t('Status'),
391 '$contact_settings_label' => $contact_settings_label,
392 '$contact_profile_label' => $this->t('Profile'),
393 '$allow_remote_self' => $allow_remote_self,
396 $this->t('Mirror postings from this contact'),
397 $localRelationship->remoteSelf,
398 $this->t('Mark this contact as remote_self, this will cause friendica to repost new entries from this contact.'),
403 $arr = ['contact' => $contact, 'output' => $o];
405 Hook::callAll('contact_edit', $arr);
407 return $arr['output'];
411 * Returns the list of available actions that can performed on the provided contact
413 * This includes actions like e.g. 'block', 'hide', 'delete' and others
415 * @param array $contact Public contact row
416 * @param Entity\LocalRelationship $localRelationship
417 * @return array with contact related actions
418 * @throws HTTPException\InternalServerErrorException
420 private function getContactActions(array $contact, Entity\LocalRelationship $localRelationship): array
422 $poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
423 $contact_actions = [];
425 $formSecurityToken = self::getFormSecurityToken('contact_action');
427 if ($localRelationship->rel & Contact::SHARING) {
428 $contact_actions['unfollow'] = [
429 'label' => $this->t('Unfollow'),
430 'url' => 'contact/unfollow?url=' . urlencode($contact['url']) . '&auto=1',
436 $contact_actions['follow'] = [
437 'label' => $this->t('Follow'),
438 'url' => 'contact/follow?url=' . urlencode($contact['url']) . '&auto=1',
445 // Provide friend suggestion only for Friendica contacts
446 if ($contact['network'] === Protocol::DFRN) {
447 $contact_actions['suggest'] = [
448 'label' => $this->t('Suggest friends'),
449 'url' => 'fsuggest/' . $contact['id'],
457 $contact_actions['update'] = [
458 'label' => $this->t('Update now'),
459 'url' => 'contact/' . $contact['id'] . '/update?t=' . $formSecurityToken,
466 if (Protocol::supportsProbe($contact['network'])) {
467 $contact_actions['updateprofile'] = [
468 'label' => $this->t('Refetch contact data'),
469 'url' => 'contact/' . $contact['id'] . '/updateprofile?t=' . $formSecurityToken,
472 'id' => 'updateprofile',
476 $contact_actions['block'] = [
477 'label' => $localRelationship->blocked ? $this->t('Unblock') : $this->t('Block'),
478 'url' => 'contact/' . $contact['id'] . '/block?t=' . $formSecurityToken,
479 'title' => $this->t('Toggle Blocked status'),
480 'sel' => $localRelationship->blocked ? 'active' : '',
481 'id' => 'toggle-block',
484 $contact_actions['ignore'] = [
485 'label' => $localRelationship->ignored ? $this->t('Unignore') : $this->t('Ignore'),
486 'url' => 'contact/' . $contact['id'] . '/ignore?t=' . $formSecurityToken,
487 'title' => $this->t('Toggle Ignored status'),
488 'sel' => $localRelationship->ignored ? 'active' : '',
489 'id' => 'toggle-ignore',
492 $contact_actions['collapse'] = [
493 'label' => $localRelationship->collapsed ? $this->t('Uncollapse') : $this->t('Collapse'),
494 'url' => 'contact/' . $contact['id'] . '/collapse?t=' . $formSecurityToken,
495 'title' => $this->t('Toggle Collapsed status'),
496 'sel' => $localRelationship->collapsed ? 'active' : '',
497 'id' => 'toggle-collapse',
500 if (Protocol::supportsRevokeFollow($contact['network']) && in_array($localRelationship->rel, [Contact::FOLLOWER, Contact::FRIEND])) {
501 $contact_actions['revoke_follow'] = [
502 'label' => $this->t('Revoke Follow'),
503 'url' => 'contact/' . $contact['id'] . '/revoke',
504 'title' => $this->t('Revoke the follow from this contact'),
506 'id' => 'revoke_follow',
510 return $contact_actions;
514 * Updates contact from probing
516 * @param int $contact_id Id of the contact with uid != 0
518 * @throws HTTPException\InternalServerErrorException
519 * @throws \ImagickException
521 private static function updateContactFromProbe(int $contact_id)
523 $contact = DBA::selectFirst('contact', ['url'], ['id' => $contact_id, 'uid' => [0, DI::userSession()->getLocalUserId()], 'deleted' => false]);
524 if (!DBA::isResult($contact)) {
528 // Update the entry in the contact table
529 Contact::updateFromProbe($contact_id);