3 * @copyright Copyright (C) 2010-2021, 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;
38 use Friendica\Model\Contact;
39 use Friendica\Model\Group;
41 use Friendica\Network\HTTPException;
42 use Friendica\Util\DateTimeFormat;
45 * Show a contact profile
47 class Profile extends BaseModule
50 * @var Repository\LocalRelationship
52 private $localRelationship;
66 * @var IManageConfigValues
70 public function __construct(L10n $l10n, Repository\LocalRelationship $localRelationship, App\BaseURL $baseUrl, App\Page $page, App\Arguments $args, IManageConfigValues $config, array $parameters = [])
72 parent::__construct($l10n, $parameters);
74 $this->localRelationship = $localRelationship;
75 $this->baseUrl = $baseUrl;
78 $this->config = $config;
81 public function post()
87 $contact_id = $this->parameters['id'];
89 // Backward compatibility: The update still needs a user-specific contact ID
90 // Change to user-contact table check by version 2022.03
91 $cdata = Contact::getPublicAndUserContactID($contact_id, local_user());
92 if (empty($cdata['user']) || !DBA::exists('contact', ['id' => $cdata['user'], 'deleted' => false])) {
96 Hook::callAll('contact_edit_post', $_POST);
100 if (isset($_POST['hidden'])) {
101 $fields['hidden'] = !empty($_POST['hidden']);
104 if (isset($_POST['notify'])) {
105 $fields['notify'] = !empty($_POST['notify']);
108 if (isset($_POST['fetch_further_information'])) {
109 $fields['fetch_further_information'] = intval($_POST['fetch_further_information']);
112 if (isset($_POST['remote_self'])) {
113 $fields['remote_self'] = intval($_POST['remote_self']);
116 if (isset($_POST['ffi_keyword_denylist'])) {
117 $fields['ffi_keyword_denylist'] = $_POST['ffi_keyword_denylist'];
120 if (isset($_POST['poll'])) {
121 $priority = intval($_POST['poll']);
122 if ($priority > 5 || $priority < 0) {
126 $fields['priority'] = $priority;
129 if (isset($_POST['info'])) {
130 $fields['info'] = $_POST['info'];
133 if (!Contact::update($fields, ['id' => $cdata['user'], 'uid' => local_user()])) {
134 notice($this->t('Failed to update contact record.'));
138 public function content(): string
141 return Module\Security\Login::form($_SERVER['REQUEST_URI']);
144 // Backward compatibility: Ensure to use the public contact when the user contact is provided
145 // Remove by version 2022.03
146 $data = Contact::getPublicAndUserContactID(intval($this->parameters['id']), local_user());
148 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
151 $contact = Contact::getById($data['public']);
152 if (!DBA::isResult($contact)) {
153 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
156 // Don't display contacts that are about to be deleted
157 if (DBA::isResult($contact) && (!empty($contact['deleted']) || !empty($contact['network']) && $contact['network'] == Protocol::PHANTOM)) {
158 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
161 $localRelationship = $this->localRelationship->getForUserContact(local_user(), $contact['id']);
163 if ($localRelationship->rel === Contact::SELF) {
164 $this->baseUrl->redirect('profile/' . $contact['nick'] . '/profile');
167 if (isset($parameters['action'])) {
168 self::checkFormSecurityTokenRedirectOnError('contact/' . $contact['id'], 'contact_action', 't');
170 $cmd = $parameters['action'];
171 if ($cmd === 'update' && $localRelationship->rel !== Contact::NOTHING) {
172 Module\Contact::updateContactFromPoll($contact['id']);
175 if ($cmd === 'updateprofile' && $localRelationship->rel !== Contact::NOTHING) {
176 self::updateContactFromProbe($contact['id']);
179 if ($cmd === 'block') {
180 if ($localRelationship->blocked) {
181 // @TODO Backward compatibility, replace with $localRelationship->unblock()
182 Contact\User::setBlocked($contact['id'], local_user(), false);
184 $message = $this->t('Contact has been unblocked');
186 // @TODO Backward compatibility, replace with $localRelationship->block()
187 Contact\User::setBlocked($contact['id'], local_user(), true);
188 $message = $this->t('Contact has been blocked');
191 // @TODO: add $this->localRelationship->save($localRelationship);
195 if ($cmd === 'ignore') {
196 if ($localRelationship->ignored) {
197 // @TODO Backward compatibility, replace with $localRelationship->unblock()
198 Contact\User::setIgnored($contact['id'], local_user(), false);
200 $message = $this->t('Contact has been unignored');
202 // @TODO Backward compatibility, replace with $localRelationship->block()
203 Contact\User::setIgnored($contact['id'], local_user(), true);
204 $message = $this->t('Contact has been ignored');
207 // @TODO: add $this->localRelationship->save($localRelationship);
211 $this->baseUrl->redirect('contact/' . $contact['id']);
214 $vcard_widget = Widget\VCard::getHTML($contact);
217 if (!in_array($localRelationship->rel, [Contact::NOTHING, Contact::SELF])) {
218 $groups_widget = Group::sidebarWidget('contact', 'group', 'full', 'everyone', $contact['id']);
221 $this->page['aside'] .= $vcard_widget . $groups_widget;
224 Nav::setSelected('contact');
226 $_SESSION['return_path'] = $this->args->getQueryString();
228 $this->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_head.tpl'), [
229 '$baseurl' => $this->baseUrl->get(true),
232 $contact['blocked'] = Contact\User::isBlocked($contact['id'], local_user());
233 $contact['readonly'] = Contact\User::isIgnored($contact['id'], local_user());
235 switch ($localRelationship->rel) {
236 case Contact::FRIEND: $relation_text = $this->t('You are mutual friends with %s', $contact['name']); break;
237 case Contact::FOLLOWER: $relation_text = $this->t('You are sharing with %s', $contact['name']); break;
238 case Contact::SHARING: $relation_text = $this->t('%s is sharing with you', $contact['name']); break;
243 if (!in_array($contact['network'], array_merge(Protocol::FEDERATED, [Protocol::TWITTER]))) {
247 $url = Contact::magicLinkByContact($contact);
248 if (strpos($url, 'redir/') === 0) {
249 $sparkle = ' class="sparkle" ';
254 $insecure = $this->t('Private communications are not available for this contact.');
256 $last_update = (($contact['last-update'] <= DBA::NULL_DATETIME) ? $this->t('Never') : DateTimeFormat::local($contact['last-update'], 'D, j M Y, g:i A'));
258 if ($contact['last-update'] > DBA::NULL_DATETIME) {
259 $last_update .= ' ' . ($contact['failed'] ? $this->t('(Update was not successful)') : $this->t('(Update was successful)'));
261 $lblsuggest = (($contact['network'] === Protocol::DFRN) ? $this->t('Suggest friends') : '');
263 $poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
265 $nettype = $this->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']));
268 $tab_str = Module\Contact::getTabsHTML($contact, Module\Contact::TAB_PROFILE);
270 $lost_contact = (($contact['archive'] && $contact['term-date'] > DBA::NULL_DATETIME && $contact['term-date'] < DateTimeFormat::utcNow()) ? $this->t('Communications lost with this contact!') : '');
272 $fetch_further_information = null;
273 if ($contact['network'] == Protocol::FEED) {
274 $fetch_further_information = [
275 'fetch_further_information',
276 $this->t('Fetch further information for feeds'),
277 $localRelationship->fetchFurtherInformation,
278 $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.'),
280 '0' => $this->t('Disabled'),
281 '1' => $this->t('Fetch information'),
282 '3' => $this->t('Fetch keywords'),
283 '2' => $this->t('Fetch information and keywords')
288 $allow_remote_self = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::FEED, Protocol::DFRN, Protocol::DIASPORA, Protocol::TWITTER])
289 && $this->config->get('system', 'allow_users_remote_self');
291 if ($contact['network'] == Protocol::FEED) {
292 $remote_self_options = [
293 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
294 Contact::MIRROR_FORWARDED => $this->t('Mirror as forwarded posting'),
295 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
297 } elseif ($contact['network'] == Protocol::ACTIVITYPUB) {
298 $remote_self_options = [
299 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
300 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
302 } elseif ($contact['network'] == Protocol::DFRN) {
303 $remote_self_options = [
304 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
305 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting'),
306 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
309 $remote_self_options = [
310 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
311 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
315 $poll_interval = null;
316 if ((($contact['network'] == Protocol::FEED) && !$this->config->get('system', 'adjust_poll_frequency')) || ($contact['network'] == Protocol::MAIL)) {
317 $poll_interval = ContactSelector::pollInterval($localRelationship->priority, !$poll_enabled);
320 $contact_actions = $this->getContactActions($contact, $localRelationship);
322 if ($localRelationship->rel !== Contact::NOTHING) {
323 $lbl_info1 = $this->t('Contact Information / Notes');
324 $contact_settings_label = $this->t('Contact Settings');
327 $contact_settings_label = null;
330 $tpl = Renderer::getMarkupTemplate('contact_edit.tpl');
331 $o .= Renderer::replaceMacros($tpl, [
332 '$header' => $this->t('Contact'),
333 '$tab_str' => $tab_str,
334 '$submit' => $this->t('Submit'),
335 '$lbl_info1' => $lbl_info1,
336 '$lbl_info2' => $this->t('Their personal note'),
337 '$reason' => trim($contact['reason']),
338 '$infedit' => $this->t('Edit contact notes'),
339 '$common_link' => 'contact/' . $contact['id'] . '/contacts/common',
340 '$relation_text' => $relation_text,
341 '$visit' => $this->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
342 '$blockunblock' => $this->t('Block/Unblock contact'),
343 '$ignorecont' => $this->t('Ignore contact'),
344 '$lblrecent' => $this->t('View conversations'),
345 '$lblsuggest' => $lblsuggest,
346 '$nettype' => $nettype,
347 '$poll_interval' => $poll_interval,
348 '$poll_enabled' => $poll_enabled,
349 '$lastupdtext' => $this->t('Last update:'),
350 '$lost_contact' => $lost_contact,
351 '$updpub' => $this->t('Update public posts'),
352 '$last_update' => $last_update,
353 '$udnow' => $this->t('Update now'),
354 '$contact_id' => $contact['id'],
355 '$block_text' => ($contact['blocked'] ? $this->t('Unblock') : $this->t('Block')),
356 '$ignore_text' => ($contact['readonly'] ? $this->t('Unignore') : $this->t('Ignore')),
357 '$insecure' => (in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::MAIL, Protocol::DIASPORA]) ? '' : $insecure),
358 '$info' => $localRelationship->info,
359 '$cinfo' => ['info', '', $localRelationship->info, ''],
360 '$blocked' => ($contact['blocked'] ? $this->t('Currently blocked') : ''),
361 '$ignored' => ($contact['readonly'] ? $this->t('Currently ignored') : ''),
362 '$archived' => ($contact['archive'] ? $this->t('Currently archived') : ''),
363 '$pending' => ($contact['pending'] ? $this->t('Awaiting connection acknowledge') : ''),
364 '$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')],
365 '$notify' => ['notify', $this->t('Notification for new posts'), ($contact['notify_new_posts'] == 1), $this->t('Send a notification of every new post of this contact')],
366 '$fetch_further_information' => $fetch_further_information,
367 '$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')],
368 '$photo' => Contact::getPhoto($contact),
369 '$name' => $contact['name'],
370 '$sparkle' => $sparkle,
372 '$profileurllabel' => $this->t('Profile URL'),
373 '$profileurl' => $contact['url'],
374 '$account_type' => Contact::getAccountType($contact),
375 '$location' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['location']),
376 '$location_label' => $this->t('Location:'),
377 '$xmpp' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['xmpp']),
378 '$xmpp_label' => $this->t('XMPP:'),
379 '$matrix' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['matrix']),
380 '$matrix_label' => $this->t('Matrix:'),
381 '$about' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['about'], BBCode::EXTERNAL),
382 '$about_label' => $this->t('About:'),
383 '$keywords' => $contact['keywords'],
384 '$keywords_label' => $this->t('Tags:'),
385 '$contact_action_button' => $this->t('Actions'),
386 '$contact_actions' => $contact_actions,
387 '$contact_status' => $this->t('Status'),
388 '$contact_settings_label' => $contact_settings_label,
389 '$contact_profile_label' => $this->t('Profile'),
390 '$allow_remote_self' => $allow_remote_self,
393 $this->t('Mirror postings from this contact'),
394 $localRelationship->isRemoteSelf,
395 $this->t('Mark this contact as remote_self, this will cause friendica to repost new entries from this contact.'),
400 $arr = ['contact' => $contact, 'output' => $o];
402 Hook::callAll('contact_edit', $arr);
404 return $arr['output'];
408 * Returns the list of available actions that can performed on the provided contact
410 * This includes actions like e.g. 'block', 'hide', 'delete' and others
412 * @param array $contact Public contact row
413 * @param Entity\LocalRelationship $localRelationship
414 * @return array with contact related actions
415 * @throws HTTPException\InternalServerErrorException
417 private function getContactActions(array $contact, Entity\LocalRelationship $localRelationship): array
419 $poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
420 $contact_actions = [];
422 $formSecurityToken = self::getFormSecurityToken('contact_action');
424 // Provide friend suggestion only for Friendica contacts
425 if ($contact['network'] === Protocol::DFRN) {
426 $contact_actions['suggest'] = [
427 'label' => $this->t('Suggest friends'),
428 'url' => 'fsuggest/' . $contact['id'],
436 $contact_actions['update'] = [
437 'label' => $this->t('Update now'),
438 'url' => 'contact/' . $contact['id'] . '/update?t=' . $formSecurityToken,
445 if (in_array($contact['network'], Protocol::NATIVE_SUPPORT)) {
446 $contact_actions['updateprofile'] = [
447 'label' => $this->t('Refetch contact data'),
448 'url' => 'contact/' . $contact['id'] . '/updateprofile?t=' . $formSecurityToken,
451 'id' => 'updateprofile',
455 $contact_actions['block'] = [
456 'label' => $localRelationship->blocked ? $this->t('Unblock') : $this->t('Block'),
457 'url' => 'contact/' . $contact['id'] . '/block?t=' . $formSecurityToken,
458 'title' => $this->t('Toggle Blocked status'),
459 'sel' => $localRelationship->blocked ? 'active' : '',
460 'id' => 'toggle-block',
463 $contact_actions['ignore'] = [
464 'label' => $localRelationship->ignored ? $this->t('Unignore') : $this->t('Ignore'),
465 'url' => 'contact/' . $contact['id'] . '/ignore?t=' . $formSecurityToken,
466 'title' => $this->t('Toggle Ignored status'),
467 'sel' => $localRelationship->ignored ? 'active' : '',
468 'id' => 'toggle-ignore',
471 if (Protocol::supportsRevokeFollow($contact['network']) && in_array($localRelationship->rel, [Contact::FOLLOWER, Contact::FRIEND])) {
472 $contact_actions['revoke_follow'] = [
473 'label' => $this->t('Revoke Follow'),
474 'url' => 'contact/' . $contact['id'] . '/revoke',
475 'title' => $this->t('Revoke the follow from this contact'),
477 'id' => 'revoke_follow',
481 return $contact_actions;
485 * @param int $contact_id Id of the contact with uid != 0
486 * @throws HTTPException\InternalServerErrorException
487 * @throws \ImagickException
489 private static function updateContactFromProbe(int $contact_id)
491 $contact = DBA::selectFirst('contact', ['url'], ['id' => $contact_id, 'uid' => local_user(), 'deleted' => false]);
492 if (!DBA::isResult($contact)) {
496 // Update the entry in the contact table
497 Contact::updateFromProbe($contact_id);