3 * @copyright Copyright (C) 2010-2022, 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\Module\Response;
42 use Friendica\Network\HTTPException;
43 use Friendica\Util\DateTimeFormat;
44 use Friendica\Util\Profiler;
45 use Psr\Log\LoggerInterface;
48 * Show a contact profile
50 class Profile extends BaseModule
53 * @var Repository\LocalRelationship
55 private $localRelationship;
61 * @var IManageConfigValues
65 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 = [])
67 parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
69 $this->localRelationship = $localRelationship;
71 $this->config = $config;
74 protected function post(array $request = [])
80 $contact_id = $this->parameters['id'];
82 // Backward compatibility: The update still needs a user-specific contact ID
83 // Change to user-contact table check by version 2022.03
84 $cdata = Contact::getPublicAndUserContactID($contact_id, local_user());
85 if (empty($cdata['user']) || !DBA::exists('contact', ['id' => $cdata['user'], 'deleted' => false])) {
89 Hook::callAll('contact_edit_post', $_POST);
93 if (isset($_POST['hidden'])) {
94 $fields['hidden'] = !empty($_POST['hidden']);
97 if (isset($_POST['notify_new_posts'])) {
98 $fields['notify_new_posts'] = !empty($_POST['notify_new_posts']);
101 if (isset($_POST['fetch_further_information'])) {
102 $fields['fetch_further_information'] = intval($_POST['fetch_further_information']);
105 if (isset($_POST['remote_self'])) {
106 $fields['remote_self'] = intval($_POST['remote_self']);
109 if (isset($_POST['ffi_keyword_denylist'])) {
110 $fields['ffi_keyword_denylist'] = $_POST['ffi_keyword_denylist'];
113 if (isset($_POST['poll'])) {
114 $priority = intval($_POST['poll']);
115 if ($priority > 5 || $priority < 0) {
119 $fields['priority'] = $priority;
122 if (isset($_POST['info'])) {
123 $fields['info'] = $_POST['info'];
126 if (!Contact::update($fields, ['id' => $cdata['user'], 'uid' => local_user()])) {
127 notice($this->t('Failed to update contact record.'));
131 protected function content(array $request = []): string
134 return Module\Security\Login::form($_SERVER['REQUEST_URI']);
137 // Backward compatibility: Ensure to use the public contact when the user contact is provided
138 // Remove by version 2022.03
139 $data = Contact::getPublicAndUserContactID(intval($this->parameters['id']), local_user());
141 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
144 $contact = Contact::getById($data['public']);
145 if (!DBA::isResult($contact)) {
146 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
149 // Don't display contacts that are about to be deleted
150 if (DBA::isResult($contact) && (!empty($contact['deleted']) || !empty($contact['network']) && $contact['network'] == Protocol::PHANTOM)) {
151 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
154 $localRelationship = $this->localRelationship->getForUserContact(local_user(), $contact['id']);
156 if ($localRelationship->rel === Contact::SELF) {
157 $this->baseUrl->redirect('profile/' . $contact['nick'] . '/profile');
160 if (isset($this->parameters['action'])) {
161 self::checkFormSecurityTokenRedirectOnError('contact/' . $contact['id'], 'contact_action', 't');
163 $cmd = $this->parameters['action'];
164 if ($cmd === 'update' && $localRelationship->rel !== Contact::NOTHING) {
165 Module\Contact::updateContactFromPoll($contact['id']);
168 if ($cmd === 'updateprofile' && $localRelationship->rel !== Contact::NOTHING) {
169 self::updateContactFromProbe($contact['id']);
172 if ($cmd === 'block') {
173 if ($localRelationship->blocked) {
174 // @TODO Backward compatibility, replace with $localRelationship->unblock()
175 Contact\User::setBlocked($contact['id'], local_user(), false);
177 $message = $this->t('Contact has been unblocked');
179 // @TODO Backward compatibility, replace with $localRelationship->block()
180 Contact\User::setBlocked($contact['id'], local_user(), true);
181 $message = $this->t('Contact has been blocked');
184 // @TODO: add $this->localRelationship->save($localRelationship);
188 if ($cmd === 'ignore') {
189 if ($localRelationship->ignored) {
190 // @TODO Backward compatibility, replace with $localRelationship->unblock()
191 Contact\User::setIgnored($contact['id'], local_user(), false);
193 $message = $this->t('Contact has been unignored');
195 // @TODO Backward compatibility, replace with $localRelationship->block()
196 Contact\User::setIgnored($contact['id'], local_user(), true);
197 $message = $this->t('Contact has been ignored');
200 // @TODO: add $this->localRelationship->save($localRelationship);
204 $this->baseUrl->redirect('contact/' . $contact['id']);
207 $vcard_widget = Widget\VCard::getHTML($contact);
210 if (!in_array($localRelationship->rel, [Contact::NOTHING, Contact::SELF])) {
211 $groups_widget = Group::sidebarWidget('contact', 'group', 'full', 'everyone', $data['user']);
214 $this->page['aside'] .= $vcard_widget . $groups_widget;
217 Nav::setSelected('contact');
219 $_SESSION['return_path'] = $this->args->getQueryString();
221 $this->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_head.tpl'), [
222 '$baseurl' => $this->baseUrl->get(true),
225 $contact['blocked'] = Contact\User::isBlocked($contact['id'], local_user());
226 $contact['readonly'] = Contact\User::isIgnored($contact['id'], local_user());
228 switch ($localRelationship->rel) {
229 case Contact::FRIEND: $relation_text = $this->t('You are mutual friends with %s', $contact['name']); break;
230 case Contact::FOLLOWER: $relation_text = $this->t('You are sharing with %s', $contact['name']); break;
231 case Contact::SHARING: $relation_text = $this->t('%s is sharing with you', $contact['name']); break;
236 if (!in_array($contact['network'], array_merge(Protocol::FEDERATED, [Protocol::TWITTER]))) {
240 $url = Contact::magicLinkByContact($contact);
241 if (strpos($url, 'redir/') === 0) {
242 $sparkle = ' class="sparkle" ';
247 $insecure = $this->t('Private communications are not available for this contact.');
249 $last_update = (($contact['last-update'] <= DBA::NULL_DATETIME) ? $this->t('Never') : DateTimeFormat::local($contact['last-update'], 'D, j M Y, g:i A'));
251 if ($contact['last-update'] > DBA::NULL_DATETIME) {
252 $last_update .= ' ' . ($contact['failed'] ? $this->t('(Update was not successful)') : $this->t('(Update was successful)'));
254 $lblsuggest = (($contact['network'] === Protocol::DFRN) ? $this->t('Suggest friends') : '');
256 $poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
258 $nettype = $this->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']));
261 $tab_str = Module\Contact::getTabsHTML($contact, Module\Contact::TAB_PROFILE);
263 $lost_contact = (($contact['archive'] && $contact['term-date'] > DBA::NULL_DATETIME && $contact['term-date'] < DateTimeFormat::utcNow()) ? $this->t('Communications lost with this contact!') : '');
265 $fetch_further_information = null;
266 if ($contact['network'] == Protocol::FEED) {
267 $fetch_further_information = [
268 'fetch_further_information',
269 $this->t('Fetch further information for feeds'),
270 $localRelationship->fetchFurtherInformation,
271 $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.'),
273 '0' => $this->t('Disabled'),
274 '1' => $this->t('Fetch information'),
275 '3' => $this->t('Fetch keywords'),
276 '2' => $this->t('Fetch information and keywords')
281 $allow_remote_self = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::FEED, Protocol::DFRN, Protocol::DIASPORA, Protocol::TWITTER])
282 && $this->config->get('system', 'allow_users_remote_self');
284 if ($contact['network'] == Protocol::FEED) {
285 $remote_self_options = [
286 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
287 Contact::MIRROR_FORWARDED => $this->t('Mirror as forwarded posting'),
288 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
290 } elseif ($contact['network'] == Protocol::ACTIVITYPUB) {
291 $remote_self_options = [
292 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
293 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
295 } elseif ($contact['network'] == Protocol::DFRN) {
296 $remote_self_options = [
297 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
298 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting'),
299 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
302 $remote_self_options = [
303 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
304 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
308 $poll_interval = null;
309 if ((($contact['network'] == Protocol::FEED) && !$this->config->get('system', 'adjust_poll_frequency')) || ($contact['network'] == Protocol::MAIL)) {
310 $poll_interval = ContactSelector::pollInterval($localRelationship->priority, !$poll_enabled);
313 $contact_actions = $this->getContactActions($contact, $localRelationship);
315 if ($localRelationship->rel !== Contact::NOTHING) {
316 $lbl_info1 = $this->t('Contact Information / Notes');
317 $contact_settings_label = $this->t('Contact Settings');
320 $contact_settings_label = null;
323 $tpl = Renderer::getMarkupTemplate('contact_edit.tpl');
324 $o .= Renderer::replaceMacros($tpl, [
325 '$header' => $this->t('Contact'),
326 '$tab_str' => $tab_str,
327 '$submit' => $this->t('Submit'),
328 '$lbl_info1' => $lbl_info1,
329 '$lbl_info2' => $this->t('Their personal note'),
330 '$reason' => trim($contact['reason']),
331 '$infedit' => $this->t('Edit contact notes'),
332 '$common_link' => 'contact/' . $contact['id'] . '/contacts/common',
333 '$relation_text' => $relation_text,
334 '$visit' => $this->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
335 '$blockunblock' => $this->t('Block/Unblock contact'),
336 '$ignorecont' => $this->t('Ignore contact'),
337 '$lblrecent' => $this->t('View conversations'),
338 '$lblsuggest' => $lblsuggest,
339 '$nettype' => $nettype,
340 '$poll_interval' => $poll_interval,
341 '$poll_enabled' => $poll_enabled,
342 '$lastupdtext' => $this->t('Last update:'),
343 '$lost_contact' => $lost_contact,
344 '$updpub' => $this->t('Update public posts'),
345 '$last_update' => $last_update,
346 '$udnow' => $this->t('Update now'),
347 '$contact_id' => $contact['id'],
348 '$block_text' => ($contact['blocked'] ? $this->t('Unblock') : $this->t('Block')),
349 '$ignore_text' => ($contact['readonly'] ? $this->t('Unignore') : $this->t('Ignore')),
350 '$insecure' => (in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::MAIL, Protocol::DIASPORA]) ? '' : $insecure),
351 '$info' => $localRelationship->info,
352 '$cinfo' => ['info', '', $localRelationship->info, ''],
353 '$blocked' => ($contact['blocked'] ? $this->t('Currently blocked') : ''),
354 '$ignored' => ($contact['readonly'] ? $this->t('Currently ignored') : ''),
355 '$archived' => ($contact['archive'] ? $this->t('Currently archived') : ''),
356 '$pending' => ($contact['pending'] ? $this->t('Awaiting connection acknowledge') : ''),
357 '$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')],
358 '$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')],
359 '$fetch_further_information' => $fetch_further_information,
360 '$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')],
361 '$photo' => Contact::getPhoto($contact),
362 '$name' => $contact['name'],
363 '$sparkle' => $sparkle,
365 '$profileurllabel' => $this->t('Profile URL'),
366 '$profileurl' => $contact['url'],
367 '$account_type' => Contact::getAccountType($contact['contact-type']),
368 '$location' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['location']),
369 '$location_label' => $this->t('Location:'),
370 '$xmpp' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['xmpp']),
371 '$xmpp_label' => $this->t('XMPP:'),
372 '$matrix' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['matrix']),
373 '$matrix_label' => $this->t('Matrix:'),
374 '$about' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['about'], BBCode::EXTERNAL),
375 '$about_label' => $this->t('About:'),
376 '$keywords' => $contact['keywords'],
377 '$keywords_label' => $this->t('Tags:'),
378 '$contact_action_button' => $this->t('Actions'),
379 '$contact_actions' => $contact_actions,
380 '$contact_status' => $this->t('Status'),
381 '$contact_settings_label' => $contact_settings_label,
382 '$contact_profile_label' => $this->t('Profile'),
383 '$allow_remote_self' => $allow_remote_self,
386 $this->t('Mirror postings from this contact'),
387 $localRelationship->isRemoteSelf,
388 $this->t('Mark this contact as remote_self, this will cause friendica to repost new entries from this contact.'),
393 $arr = ['contact' => $contact, 'output' => $o];
395 Hook::callAll('contact_edit', $arr);
397 return $arr['output'];
401 * Returns the list of available actions that can performed on the provided contact
403 * This includes actions like e.g. 'block', 'hide', 'delete' and others
405 * @param array $contact Public contact row
406 * @param Entity\LocalRelationship $localRelationship
407 * @return array with contact related actions
408 * @throws HTTPException\InternalServerErrorException
410 private function getContactActions(array $contact, Entity\LocalRelationship $localRelationship): array
412 $poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
413 $contact_actions = [];
415 $formSecurityToken = self::getFormSecurityToken('contact_action');
417 // Provide friend suggestion only for Friendica contacts
418 if ($contact['network'] === Protocol::DFRN) {
419 $contact_actions['suggest'] = [
420 'label' => $this->t('Suggest friends'),
421 'url' => 'fsuggest/' . $contact['id'],
429 $contact_actions['update'] = [
430 'label' => $this->t('Update now'),
431 'url' => 'contact/' . $contact['id'] . '/update?t=' . $formSecurityToken,
438 if (in_array($contact['network'], Protocol::NATIVE_SUPPORT)) {
439 $contact_actions['updateprofile'] = [
440 'label' => $this->t('Refetch contact data'),
441 'url' => 'contact/' . $contact['id'] . '/updateprofile?t=' . $formSecurityToken,
444 'id' => 'updateprofile',
448 $contact_actions['block'] = [
449 'label' => $localRelationship->blocked ? $this->t('Unblock') : $this->t('Block'),
450 'url' => 'contact/' . $contact['id'] . '/block?t=' . $formSecurityToken,
451 'title' => $this->t('Toggle Blocked status'),
452 'sel' => $localRelationship->blocked ? 'active' : '',
453 'id' => 'toggle-block',
456 $contact_actions['ignore'] = [
457 'label' => $localRelationship->ignored ? $this->t('Unignore') : $this->t('Ignore'),
458 'url' => 'contact/' . $contact['id'] . '/ignore?t=' . $formSecurityToken,
459 'title' => $this->t('Toggle Ignored status'),
460 'sel' => $localRelationship->ignored ? 'active' : '',
461 'id' => 'toggle-ignore',
464 if (Protocol::supportsRevokeFollow($contact['network']) && in_array($localRelationship->rel, [Contact::FOLLOWER, Contact::FRIEND])) {
465 $contact_actions['revoke_follow'] = [
466 'label' => $this->t('Revoke Follow'),
467 'url' => 'contact/' . $contact['id'] . '/revoke',
468 'title' => $this->t('Revoke the follow from this contact'),
470 'id' => 'revoke_follow',
474 return $contact_actions;
478 * @param int $contact_id Id of the contact with uid != 0
479 * @throws HTTPException\InternalServerErrorException
480 * @throws \ImagickException
482 private static function updateContactFromProbe(int $contact_id)
484 $contact = DBA::selectFirst('contact', ['url'], ['id' => $contact_id, 'uid' => local_user(), 'deleted' => false]);
485 if (!DBA::isResult($contact)) {
489 // Update the entry in the contact table
490 Contact::updateFromProbe($contact_id);