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\Settings\Profile;
25 use Friendica\Core\ACL;
26 use Friendica\Core\Hook;
27 use Friendica\Core\L10n;
28 use Friendica\Core\Protocol;
29 use Friendica\Core\Renderer;
30 use Friendica\Core\Session\Capability\IHandleUserSessions;
31 use Friendica\Core\Theme;
32 use Friendica\Database\DBA;
33 use Friendica\Model\Contact;
34 use Friendica\Model\Profile;
35 use Friendica\Module\Response;
36 use Friendica\Navigation\SystemMessages;
37 use Friendica\Profile\ProfileField;
38 use Friendica\Model\User;
39 use Friendica\Module\BaseSettings;
40 use Friendica\Module\Security\Login;
41 use Friendica\Network\HTTPException;
42 use Friendica\Security\PermissionSet;
43 use Friendica\Util\ACLFormatter;
44 use Friendica\Util\DateTimeFormat;
45 use Friendica\Util\Profiler;
46 use Friendica\Util\Temporal;
47 use Friendica\Core\Worker;
48 use Psr\Log\LoggerInterface;
50 class Index extends BaseSettings
52 /** @var ProfileField\Repository\ProfileField */
53 private $profileFieldRepo;
54 /** @var ProfileField\Factory\ProfileField */
55 private $profileFieldFactory;
56 /** @var SystemMessages */
57 private $systemMessages;
58 /** @var PermissionSet\Repository\PermissionSet */
59 private $permissionSetRepo;
60 /** @var PermissionSet\Factory\PermissionSet */
61 private $permissionSetFactory;
62 /** @var ACLFormatter */
63 private $aclFormatter;
65 public function __construct(ACLFormatter $aclFormatter, PermissionSet\Factory\PermissionSet $permissionSetFactory, PermissionSet\Repository\PermissionSet $permissionSetRepo, SystemMessages $systemMessages, ProfileField\Factory\ProfileField $profileFieldFactory, ProfileField\Repository\ProfileField $profileFieldRepo, IHandleUserSessions $session, App\Page $page, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
67 parent::__construct($session, $page, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
69 $this->profileFieldRepo = $profileFieldRepo;
70 $this->profileFieldFactory = $profileFieldFactory;
71 $this->systemMessages = $systemMessages;
72 $this->permissionSetRepo = $permissionSetRepo;
73 $this->permissionSetFactory = $permissionSetFactory;
74 $this->aclFormatter = $aclFormatter;
77 protected function post(array $request = [])
79 if (!$this->session->getLocalUserId()) {
83 $profile = Profile::getByUID($this->session->getLocalUserId());
88 self::checkFormSecurityTokenRedirectOnError('/settings/profile', 'settings_profile');
90 Hook::callAll('profile_post', $request);
92 $dob = trim($request['dob'] ?? '');
94 if ($dob && !in_array($dob, ['0000-00-00', DBA::NULL_DATE])) {
95 $y = substr($dob, 0, 4);
96 if ((!ctype_digit($y)) || ($y < 1900)) {
102 if (strpos($dob, '0000-') === 0 || strpos($dob, '0001-') === 0) {
104 $dob = substr($dob, 5);
108 $dob = '0000-' . DateTimeFormat::utc('1900-' . $dob, 'm-d');
110 $dob = DateTimeFormat::utc($dob, 'Y-m-d');
114 $username = trim($request['username'] ?? '');
116 $this->systemMessages->addNotice($this->t('Display Name is required.'));
120 $about = trim($request['about']);
121 $address = trim($request['address']);
122 $locality = trim($request['locality']);
123 $region = trim($request['region']);
124 $postal_code = trim($request['postal_code']);
125 $country_name = trim($request['country_name']);
126 $pub_keywords = self::cleanKeywords(trim($request['pub_keywords']));
127 $prv_keywords = self::cleanKeywords(trim($request['prv_keywords']));
128 $xmpp = trim($request['xmpp']);
129 $matrix = trim($request['matrix']);
130 $homepage = trim($request['homepage']);
131 if ((strpos($homepage, 'http') !== 0) && (strlen($homepage))) {
132 // neither http nor https in URL, add them
133 $homepage = 'http://' . $homepage;
136 $profileFieldsNew = $this->getProfileFieldsFromInput(
137 $this->session->getLocalUserId(),
138 $request['profile_field'],
139 $request['profile_field_order']
142 $this->profileFieldRepo->saveCollectionForUser($this->session->getLocalUserId(), $profileFieldsNew);
144 User::update(['username' => $username], $this->session->getLocalUserId());
146 $result = Profile::update(
150 'address' => $address,
151 'locality' => $locality,
153 'postal-code' => $postal_code,
154 'country-name' => $country_name,
157 'homepage' => $homepage,
158 'pub_keywords' => $pub_keywords,
159 'prv_keywords' => $prv_keywords,
161 $this->session->getLocalUserId()
164 Worker::add(Worker::PRIORITY_MEDIUM, 'CheckRelMeProfileLink', $this->session->getLocalUserId());
167 $this->systemMessages->addNotice($this->t("Profile couldn't be updated."));
171 $this->baseUrl->redirect('settings/profile');
174 protected function content(array $request = []): string
176 if (!$this->session->getLocalUserId()) {
177 $this->systemMessages->addNotice($this->t('You must be logged in to use this module'));
178 return Login::form();
185 $owner = User::getOwnerDataById($this->session->getLocalUserId());
187 throw new HTTPException\NotFoundException();
190 $this->page->registerFooterScript('view/asset/es-jquery-sortable/source/js/jquery-sortable-min.js');
191 $this->page->registerFooterScript(Theme::getPathForFile('js/module/settings/profile/index.js'));
195 $profileFields = $this->profileFieldRepo->selectByUserId($this->session->getLocalUserId());
196 foreach ($profileFields as $profileField) {
197 $defaultPermissions = $profileField->permissionSet->withAllowedContacts(
198 Contact::pruneUnavailable($profileField->permissionSet->allow_cid)
202 'id' => $profileField->id,
203 'legend' => $profileField->label,
205 'label' => ['profile_field[' . $profileField->id . '][label]', $this->t('Label:'), $profileField->label],
206 'value' => ['profile_field[' . $profileField->id . '][value]', $this->t('Value:'), $profileField->value],
207 'acl' => ACL::getFullSelectorHTML(
209 $this->session->getLocalUserId(),
211 $defaultPermissions->toArray(),
212 ['network' => Protocol::DFRN],
213 'profile_field[' . $profileField->id . ']'
217 'permissions' => $this->t('Field Permissions'),
218 'permdesc' => $this->t("(click to open/close)"),
224 'legend' => $this->t('Add a new profile field'),
226 'label' => ['profile_field[new][label]', $this->t('Label:')],
227 'value' => ['profile_field[new][value]', $this->t('Value:')],
228 'acl' => ACL::getFullSelectorHTML(
230 $this->session->getLocalUserId(),
233 ['network' => Protocol::DFRN],
238 'permissions' => $this->t('Field Permissions'),
239 'permdesc' => $this->t("(click to open/close)"),
242 $this->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/profile/index_head.tpl'));
244 $personal_account = ($owner['account-type'] != User::ACCOUNT_TYPE_COMMUNITY);
246 if ($owner['homepage_verified']) {
247 $homepage_help_text = $this->t('The homepage is verified. A rel="me" link back to your Friendica profile page was found on the homepage.');
249 $homepage_help_text = $this->t('To verify your homepage, add a rel="me" link to it, pointing to your profile URL (%s).', $owner['url']);
252 $tpl = Renderer::getMarkupTemplate('settings/profile/index.tpl');
253 $o .= Renderer::replaceMacros($tpl, [
255 'profile_action' => $this->t('Profile Actions'),
256 'banner' => $this->t('Edit Profile Details'),
257 'submit' => $this->t('Submit'),
258 'profpic' => $this->t('Change Profile Photo'),
259 'viewprof' => $this->t('View Profile'),
260 'personal_section' => $this->t('Personal'),
261 'picture_section' => $this->t('Profile picture'),
262 'location_section' => $this->t('Location'),
263 'miscellaneous_section' => $this->t('Miscellaneous'),
264 'custom_fields_section' => $this->t('Custom Profile Fields'),
265 'profile_photo' => $this->t('Upload Profile Photo'),
266 'custom_fields_description' => $this->t('<p>Custom fields appear on <a href="%s">your profile page</a>.</p>
267 <p>You can use BBCodes in the field values.</p>
268 <p>Reorder by dragging the field title.</p>
269 <p>Empty the label field to remove a custom field.</p>
270 <p>Non-public fields can only be seen by the selected Friendica contacts or the Friendica contacts in the selected circles.</p>',
271 'profile/' . $owner['nickname'] . '/profile'
275 '$personal_account' => $personal_account,
277 '$form_security_token' => self::getFormSecurityToken('settings_profile'),
278 '$form_security_token_photo' => self::getFormSecurityToken('settings_profile_photo'),
280 '$profpiclink' => '/profile/' . $owner['nickname'] . '/photos',
282 '$nickname' => $owner['nickname'],
283 '$username' => ['username', $this->t('Display name:'), $owner['name']],
284 '$about' => ['about', $this->t('Description:'), $owner['about']],
285 '$dob' => Temporal::getDateofBirthField($owner['dob'], $owner['timezone']),
286 '$address' => ['address', $this->t('Street Address:'), $owner['address']],
287 '$locality' => ['locality', $this->t('Locality/City:'), $owner['locality']],
288 '$region' => ['region', $this->t('Region/State:'), $owner['region']],
289 '$postal_code' => ['postal_code', $this->t('Postal/Zip Code:'), $owner['postal-code']],
290 '$country_name' => ['country_name', $this->t('Country:'), $owner['country-name']],
291 '$age' => ((intval($owner['dob'])) ? '(' . $this->t('Age: ') . $this->tt('%d year old', '%d years old', Temporal::getAgeByTimezone($owner['dob'], $owner['timezone'])) . ')' : ''),
292 '$xmpp' => ['xmpp', $this->t('XMPP (Jabber) address:'), $owner['xmpp'], $this->t('The XMPP address will be published so that people can follow you there.')],
293 '$matrix' => ['matrix', $this->t('Matrix (Element) address:'), $owner['matrix'], $this->t('The Matrix address will be published so that people can follow you there.')],
294 '$homepage' => ['homepage', $this->t('Homepage URL:'), $owner['homepage'], $homepage_help_text],
295 '$pub_keywords' => ['pub_keywords', $this->t('Public Keywords:'), $owner['pub_keywords'], $this->t('(Used for suggesting potential friends, can be seen by others)')],
296 '$prv_keywords' => ['prv_keywords', $this->t('Private Keywords:'), $owner['prv_keywords'], $this->t('(Used for searching profiles, never shown to others)')],
297 '$custom_fields' => $custom_fields,
300 $arr = ['profile' => $owner, 'entry' => $o];
301 Hook::callAll('profile_edit', $arr);
306 private function getProfileFieldsFromInput(int $uid, array $profileFieldInputs, array $profileFieldOrder): ProfileField\Collection\ProfileFields
308 $profileFields = new ProfileField\Collection\ProfileFields();
310 // Returns an associative array of id => order values
311 $profileFieldOrder = array_flip($profileFieldOrder);
313 // Creation of the new field
314 if (!empty($profileFieldInputs['new']['label'])) {
315 $permissionSet = $this->permissionSetRepo->selectOrCreate($this->permissionSetFactory->createFromString(
317 $this->aclFormatter->toString($profileFieldInputs['new']['contact_allow'] ?? ''),
318 $this->aclFormatter->toString($profileFieldInputs['new']['circle_allow'] ?? ''),
319 $this->aclFormatter->toString($profileFieldInputs['new']['contact_deny'] ?? ''),
320 $this->aclFormatter->toString($profileFieldInputs['new']['circle_deny'] ?? '')
323 $profileFields->append($this->profileFieldFactory->createFromValues(
325 $profileFieldOrder['new'],
326 $profileFieldInputs['new']['label'],
327 $profileFieldInputs['new']['value'],
332 unset($profileFieldInputs['new']);
333 unset($profileFieldOrder['new']);
335 foreach ($profileFieldInputs as $id => $profileFieldInput) {
336 $permissionSet = $this->permissionSetRepo->selectOrCreate($this->permissionSetFactory->createFromString(
338 $this->aclFormatter->toString($profileFieldInput['contact_allow'] ?? ''),
339 $this->aclFormatter->toString($profileFieldInput['circle_allow'] ?? ''),
340 $this->aclFormatter->toString($profileFieldInput['contact_deny'] ?? ''),
341 $this->aclFormatter->toString($profileFieldInput['circle_deny'] ?? '')
344 $profileFields->append($this->profileFieldFactory->createFromValues(
346 $profileFieldOrder[$id],
347 $profileFieldInput['label'],
348 $profileFieldInput['value'],
353 return $profileFields;
356 private static function cleanKeywords($keywords): string
358 $keywords = str_replace(',', ' ', $keywords);
359 $keywords = explode(' ', $keywords);
362 foreach ($keywords as $keyword) {
363 $keyword = trim($keyword);
364 $keyword = trim($keyword, '#');
365 if ($keyword != '') {
366 $cleaned[] = $keyword;
370 return implode(', ', $cleaned);