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\Core\Session;
38 use Friendica\Database\DBA;
40 use Friendica\Model\Contact;
41 use Friendica\Model\Group;
43 use Friendica\Module\Response;
44 use Friendica\Network\HTTPException;
45 use Friendica\Util\DateTimeFormat;
46 use Friendica\Util\Profiler;
47 use Psr\Log\LoggerInterface;
50 * Show a contact profile
52 class Profile extends BaseModule
55 * @var Repository\LocalRelationship
57 private $localRelationship;
63 * @var IManageConfigValues
67 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 = [])
69 parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
71 $this->localRelationship = $localRelationship;
73 $this->config = $config;
76 protected function post(array $request = [])
78 if (!Session::getLocalUser()) {
82 $contact_id = $this->parameters['id'];
84 // Backward compatibility: The update still needs a user-specific contact ID
85 // Change to user-contact table check by version 2022.03
86 $cdata = Contact::getPublicAndUserContactID($contact_id, Session::getLocalUser());
87 if (empty($cdata['user']) || !DBA::exists('contact', ['id' => $cdata['user'], 'deleted' => false])) {
91 Hook::callAll('contact_edit_post', $_POST);
95 if (isset($_POST['hidden'])) {
96 $fields['hidden'] = !empty($_POST['hidden']);
99 if (isset($_POST['notify_new_posts'])) {
100 $fields['notify_new_posts'] = !empty($_POST['notify_new_posts']);
103 if (isset($_POST['fetch_further_information'])) {
104 $fields['fetch_further_information'] = intval($_POST['fetch_further_information']);
107 if (isset($_POST['remote_self'])) {
108 $fields['remote_self'] = intval($_POST['remote_self']);
111 if (isset($_POST['ffi_keyword_denylist'])) {
112 $fields['ffi_keyword_denylist'] = $_POST['ffi_keyword_denylist'];
115 if (isset($_POST['poll'])) {
116 $priority = intval($_POST['poll']);
117 if ($priority > 5 || $priority < 0) {
121 $fields['priority'] = $priority;
124 if (isset($_POST['info'])) {
125 $fields['info'] = $_POST['info'];
128 if (!Contact::update($fields, ['id' => $cdata['user'], 'uid' => Session::getLocalUser()])) {
129 DI::sysmsg()->addNotice($this->t('Failed to update contact record.'));
133 protected function content(array $request = []): string
135 if (!Session::getLocalUser()) {
136 return Module\Security\Login::form($_SERVER['REQUEST_URI']);
139 // Backward compatibility: Ensure to use the public contact when the user contact is provided
140 // Remove by version 2022.03
141 $data = Contact::getPublicAndUserContactID(intval($this->parameters['id']), Session::getLocalUser());
143 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
146 $contact = Contact::getById($data['public']);
147 if (!DBA::isResult($contact)) {
148 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
151 // Don't display contacts that are about to be deleted
152 if (DBA::isResult($contact) && (!empty($contact['deleted']) || !empty($contact['network']) && $contact['network'] == Protocol::PHANTOM)) {
153 throw new HTTPException\NotFoundException($this->t('Contact not found.'));
156 $localRelationship = $this->localRelationship->getForUserContact(Session::getLocalUser(), $contact['id']);
158 if ($localRelationship->rel === Contact::SELF) {
159 $this->baseUrl->redirect('profile/' . $contact['nick'] . '/profile');
162 if (isset($this->parameters['action'])) {
163 self::checkFormSecurityTokenRedirectOnError('contact/' . $contact['id'], 'contact_action', 't');
165 $cmd = $this->parameters['action'];
166 if ($cmd === 'update' && $localRelationship->rel !== Contact::NOTHING) {
167 Module\Contact::updateContactFromPoll($contact['id']);
170 if ($cmd === 'updateprofile') {
171 self::updateContactFromProbe($contact['id']);
174 if ($cmd === 'block') {
175 if ($localRelationship->blocked) {
176 // @TODO Backward compatibility, replace with $localRelationship->unblock()
177 Contact\User::setBlocked($contact['id'], Session::getLocalUser(), false);
179 $message = $this->t('Contact has been unblocked');
181 // @TODO Backward compatibility, replace with $localRelationship->block()
182 Contact\User::setBlocked($contact['id'], Session::getLocalUser(), true);
183 $message = $this->t('Contact has been blocked');
186 // @TODO: add $this->localRelationship->save($localRelationship);
187 DI::sysmsg()->addInfo($message);
190 if ($cmd === 'ignore') {
191 if ($localRelationship->ignored) {
192 // @TODO Backward compatibility, replace with $localRelationship->unblock()
193 Contact\User::setIgnored($contact['id'], Session::getLocalUser(), false);
195 $message = $this->t('Contact has been unignored');
197 // @TODO Backward compatibility, replace with $localRelationship->block()
198 Contact\User::setIgnored($contact['id'], Session::getLocalUser(), true);
199 $message = $this->t('Contact has been ignored');
202 // @TODO: add $this->localRelationship->save($localRelationship);
203 DI::sysmsg()->addInfo($message);
206 $this->baseUrl->redirect('contact/' . $contact['id']);
209 $vcard_widget = Widget\VCard::getHTML($contact);
212 if (!in_array($localRelationship->rel, [Contact::NOTHING, Contact::SELF])) {
213 $groups_widget = Group::sidebarWidget('contact', 'group', 'full', 'everyone', $data['user']);
216 $this->page['aside'] .= $vcard_widget . $groups_widget;
219 Nav::setSelected('contact');
221 $_SESSION['return_path'] = $this->args->getQueryString();
223 $this->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_head.tpl'), [
224 '$baseurl' => $this->baseUrl->get(true),
227 $contact['blocked'] = Contact\User::isBlocked($contact['id'], Session::getLocalUser());
228 $contact['readonly'] = Contact\User::isIgnored($contact['id'], Session::getLocalUser());
230 switch ($localRelationship->rel) {
231 case Contact::FRIEND: $relation_text = $this->t('You are mutual friends with %s', $contact['name']); break;
232 case Contact::FOLLOWER: $relation_text = $this->t('You are sharing with %s', $contact['name']); break;
233 case Contact::SHARING: $relation_text = $this->t('%s is sharing with you', $contact['name']); break;
238 if (!in_array($contact['network'], array_merge(Protocol::FEDERATED, [Protocol::TWITTER]))) {
242 $url = Contact::magicLinkByContact($contact);
243 if (strpos($url, 'redir/') === 0) {
244 $sparkle = ' class="sparkle" ';
249 $insecure = $this->t('Private communications are not available for this contact.');
251 $last_update = (($contact['last-update'] <= DBA::NULL_DATETIME) ? $this->t('Never') : DateTimeFormat::local($contact['last-update'], 'D, j M Y, g:i A'));
253 if ($contact['last-update'] > DBA::NULL_DATETIME) {
254 $last_update .= ' ' . ($contact['failed'] ? $this->t('(Update was not successful)') : $this->t('(Update was successful)'));
256 $lblsuggest = (($contact['network'] === Protocol::DFRN) ? $this->t('Suggest friends') : '');
258 $poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
260 $nettype = $this->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']));
263 $tab_str = Module\Contact::getTabsHTML($contact, Module\Contact::TAB_PROFILE);
265 $lost_contact = (($contact['archive'] && $contact['term-date'] > DBA::NULL_DATETIME && $contact['term-date'] < DateTimeFormat::utcNow()) ? $this->t('Communications lost with this contact!') : '');
267 $fetch_further_information = null;
268 if ($contact['network'] == Protocol::FEED) {
269 $fetch_further_information = [
270 'fetch_further_information',
271 $this->t('Fetch further information for feeds'),
272 $localRelationship->fetchFurtherInformation,
273 $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.'),
275 '0' => $this->t('Disabled'),
276 '1' => $this->t('Fetch information'),
277 '3' => $this->t('Fetch keywords'),
278 '2' => $this->t('Fetch information and keywords')
283 $allow_remote_self = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::FEED, Protocol::DFRN, Protocol::DIASPORA, Protocol::TWITTER])
284 && $this->config->get('system', 'allow_users_remote_self');
286 if ($contact['network'] == Protocol::FEED) {
287 $remote_self_options = [
288 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
289 Contact::MIRROR_FORWARDED => $this->t('Mirror as forwarded posting'),
290 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
292 } elseif ($contact['network'] == Protocol::ACTIVITYPUB) {
293 $remote_self_options = [
294 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
295 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
297 } elseif ($contact['network'] == Protocol::DFRN) {
298 $remote_self_options = [
299 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
300 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting'),
301 Contact::MIRROR_NATIVE_RESHARE => $this->t('Native reshare')
304 $remote_self_options = [
305 Contact::MIRROR_DEACTIVATED => $this->t('No mirroring'),
306 Contact::MIRROR_OWN_POST => $this->t('Mirror as my own posting')
310 $poll_interval = null;
311 if ((($contact['network'] == Protocol::FEED) && !$this->config->get('system', 'adjust_poll_frequency')) || ($contact['network'] == Protocol::MAIL)) {
312 $poll_interval = ContactSelector::pollInterval($localRelationship->priority, !$poll_enabled);
315 $contact_actions = $this->getContactActions($contact, $localRelationship);
317 if ($localRelationship->rel !== Contact::NOTHING) {
318 $lbl_info1 = $this->t('Contact Information / Notes');
319 $contact_settings_label = $this->t('Contact Settings');
322 $contact_settings_label = null;
325 $tpl = Renderer::getMarkupTemplate('contact_edit.tpl');
326 $o .= Renderer::replaceMacros($tpl, [
327 '$header' => $this->t('Contact'),
328 '$tab_str' => $tab_str,
329 '$submit' => $this->t('Submit'),
330 '$lbl_info1' => $lbl_info1,
331 '$lbl_info2' => $this->t('Their personal note'),
332 '$reason' => trim($contact['reason']),
333 '$infedit' => $this->t('Edit contact notes'),
334 '$common_link' => 'contact/' . $contact['id'] . '/contacts/common',
335 '$relation_text' => $relation_text,
336 '$visit' => $this->t('Visit %s\'s profile [%s]', $contact['name'], $contact['url']),
337 '$blockunblock' => $this->t('Block/Unblock contact'),
338 '$ignorecont' => $this->t('Ignore contact'),
339 '$lblrecent' => $this->t('View conversations'),
340 '$lblsuggest' => $lblsuggest,
341 '$nettype' => $nettype,
342 '$poll_interval' => $poll_interval,
343 '$poll_enabled' => $poll_enabled,
344 '$lastupdtext' => $this->t('Last update:'),
345 '$lost_contact' => $lost_contact,
346 '$updpub' => $this->t('Update public posts'),
347 '$last_update' => $last_update,
348 '$udnow' => $this->t('Update now'),
349 '$contact_id' => $contact['id'],
350 '$block_text' => ($contact['blocked'] ? $this->t('Unblock') : $this->t('Block')),
351 '$ignore_text' => ($contact['readonly'] ? $this->t('Unignore') : $this->t('Ignore')),
352 '$insecure' => (in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::MAIL, Protocol::DIASPORA]) ? '' : $insecure),
353 '$info' => $localRelationship->info,
354 '$cinfo' => ['info', '', $localRelationship->info, ''],
355 '$blocked' => ($contact['blocked'] ? $this->t('Currently blocked') : ''),
356 '$ignored' => ($contact['readonly'] ? $this->t('Currently ignored') : ''),
357 '$archived' => ($contact['archive'] ? $this->t('Currently archived') : ''),
358 '$pending' => ($contact['pending'] ? $this->t('Awaiting connection acknowledge') : ''),
359 '$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')],
360 '$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')],
361 '$fetch_further_information' => $fetch_further_information,
362 '$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')],
363 '$photo' => Contact::getPhoto($contact),
364 '$name' => $contact['name'],
365 '$sparkle' => $sparkle,
367 '$profileurllabel' => $this->t('Profile URL'),
368 '$profileurl' => $contact['url'],
369 '$account_type' => Contact::getAccountType($contact['contact-type']),
370 '$location' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['location']),
371 '$location_label' => $this->t('Location:'),
372 '$xmpp' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['xmpp']),
373 '$xmpp_label' => $this->t('XMPP:'),
374 '$matrix' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['matrix']),
375 '$matrix_label' => $this->t('Matrix:'),
376 '$about' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['about'], BBCode::EXTERNAL),
377 '$about_label' => $this->t('About:'),
378 '$keywords' => $contact['keywords'],
379 '$keywords_label' => $this->t('Tags:'),
380 '$contact_action_button' => $this->t('Actions'),
381 '$contact_actions' => $contact_actions,
382 '$contact_status' => $this->t('Status'),
383 '$contact_settings_label' => $contact_settings_label,
384 '$contact_profile_label' => $this->t('Profile'),
385 '$allow_remote_self' => $allow_remote_self,
388 $this->t('Mirror postings from this contact'),
389 $localRelationship->isRemoteSelf,
390 $this->t('Mark this contact as remote_self, this will cause friendica to repost new entries from this contact.'),
395 $arr = ['contact' => $contact, 'output' => $o];
397 Hook::callAll('contact_edit', $arr);
399 return $arr['output'];
403 * Returns the list of available actions that can performed on the provided contact
405 * This includes actions like e.g. 'block', 'hide', 'delete' and others
407 * @param array $contact Public contact row
408 * @param Entity\LocalRelationship $localRelationship
409 * @return array with contact related actions
410 * @throws HTTPException\InternalServerErrorException
412 private function getContactActions(array $contact, Entity\LocalRelationship $localRelationship): array
414 $poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
415 $contact_actions = [];
417 $formSecurityToken = self::getFormSecurityToken('contact_action');
419 // Provide friend suggestion only for Friendica contacts
420 if ($contact['network'] === Protocol::DFRN) {
421 $contact_actions['suggest'] = [
422 'label' => $this->t('Suggest friends'),
423 'url' => 'fsuggest/' . $contact['id'],
431 $contact_actions['update'] = [
432 'label' => $this->t('Update now'),
433 'url' => 'contact/' . $contact['id'] . '/update?t=' . $formSecurityToken,
440 if (in_array($contact['network'], Protocol::NATIVE_SUPPORT)) {
441 $contact_actions['updateprofile'] = [
442 'label' => $this->t('Refetch contact data'),
443 'url' => 'contact/' . $contact['id'] . '/updateprofile?t=' . $formSecurityToken,
446 'id' => 'updateprofile',
450 $contact_actions['block'] = [
451 'label' => $localRelationship->blocked ? $this->t('Unblock') : $this->t('Block'),
452 'url' => 'contact/' . $contact['id'] . '/block?t=' . $formSecurityToken,
453 'title' => $this->t('Toggle Blocked status'),
454 'sel' => $localRelationship->blocked ? 'active' : '',
455 'id' => 'toggle-block',
458 $contact_actions['ignore'] = [
459 'label' => $localRelationship->ignored ? $this->t('Unignore') : $this->t('Ignore'),
460 'url' => 'contact/' . $contact['id'] . '/ignore?t=' . $formSecurityToken,
461 'title' => $this->t('Toggle Ignored status'),
462 'sel' => $localRelationship->ignored ? 'active' : '',
463 'id' => 'toggle-ignore',
466 if (Protocol::supportsRevokeFollow($contact['network']) && in_array($localRelationship->rel, [Contact::FOLLOWER, Contact::FRIEND])) {
467 $contact_actions['revoke_follow'] = [
468 'label' => $this->t('Revoke Follow'),
469 'url' => 'contact/' . $contact['id'] . '/revoke',
470 'title' => $this->t('Revoke the follow from this contact'),
472 'id' => 'revoke_follow',
476 return $contact_actions;
480 * Updates contact from probing
482 * @param int $contact_id Id of the contact with uid != 0
484 * @throws HTTPException\InternalServerErrorException
485 * @throws \ImagickException
487 private static function updateContactFromProbe(int $contact_id)
489 $contact = DBA::selectFirst('contact', ['url'], ['id' => $contact_id, 'uid' => [0, Session::getLocalUser()], 'deleted' => false]);
490 if (!DBA::isResult($contact)) {
494 // Update the entry in the contact table
495 Contact::updateFromProbe($contact_id);