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;
24 use Friendica\Core\ACL;
25 use Friendica\Core\Hook;
26 use Friendica\Core\Protocol;
27 use Friendica\Core\Renderer;
28 use Friendica\Core\Theme;
29 use Friendica\Database\DBA;
31 use Friendica\Model\Contact;
32 use Friendica\Model\Profile;
33 use Friendica\Profile\ProfileField\Collection\ProfileFields;
34 use Friendica\Profile\ProfileField\Entity\ProfileField;
35 use Friendica\Model\User;
36 use Friendica\Module\BaseSettings;
37 use Friendica\Module\Security\Login;
38 use Friendica\Network\HTTPException;
39 use Friendica\Util\DateTimeFormat;
40 use Friendica\Util\Temporal;
41 use Friendica\Core\Worker;
43 class Index extends BaseSettings
45 protected function post(array $request = [])
47 if (!DI::userSession()->getLocalUserId()) {
51 $profile = Profile::getByUID(DI::userSession()->getLocalUserId());
52 if (!DBA::isResult($profile)) {
56 self::checkFormSecurityTokenRedirectOnError('/settings/profile', 'settings_profile');
58 Hook::callAll('profile_post', $_POST);
60 $dob = trim($_POST['dob'] ?? '');
62 if ($dob && !in_array($dob, ['0000-00-00', DBA::NULL_DATE])) {
63 $y = substr($dob, 0, 4);
64 if ((!ctype_digit($y)) || ($y < 1900)) {
70 if (strpos($dob, '0000-') === 0 || strpos($dob, '0001-') === 0) {
72 $dob = substr($dob, 5);
76 $dob = '0000-' . DateTimeFormat::utc('1900-' . $dob, 'm-d');
78 $dob = DateTimeFormat::utc($dob, 'Y-m-d');
82 $name = trim($_POST['name'] ?? '');
84 DI::sysmsg()->addNotice(DI::l10n()->t('Profile Name is required.'));
88 $about = trim($_POST['about']);
89 $address = trim($_POST['address']);
90 $locality = trim($_POST['locality']);
91 $region = trim($_POST['region']);
92 $postal_code = trim($_POST['postal_code']);
93 $country_name = trim($_POST['country_name']);
94 $pub_keywords = self::cleanKeywords(trim($_POST['pub_keywords']));
95 $prv_keywords = self::cleanKeywords(trim($_POST['prv_keywords']));
96 $xmpp = trim($_POST['xmpp']);
97 $matrix = trim($_POST['matrix']);
98 $homepage = trim($_POST['homepage']);
99 if ((strpos($homepage, 'http') !== 0) && (strlen($homepage))) {
100 // neither http nor https in URL, add them
101 $homepage = 'http://' . $homepage;
104 $profileFieldsNew = self::getProfileFieldsFromInput(
105 DI::userSession()->getLocalUserId(),
106 $_REQUEST['profile_field'],
107 $_REQUEST['profile_field_order']
110 DI::profileField()->saveCollectionForUser(DI::userSession()->getLocalUserId(), $profileFieldsNew);
112 $result = Profile::update(
117 'address' => $address,
118 'locality' => $locality,
120 'postal-code' => $postal_code,
121 'country-name' => $country_name,
124 'homepage' => $homepage,
125 'pub_keywords' => $pub_keywords,
126 'prv_keywords' => $prv_keywords,
128 DI::userSession()->getLocalUserId()
131 Worker::add(Worker::PRIORITY_MEDIUM, 'CheckRelMeProfileLink', DI::userSession()->getLocalUserId());
134 DI::sysmsg()->addNotice(DI::l10n()->t('Profile couldn\'t be updated.'));
138 DI::baseUrl()->redirect('settings/profile');
141 protected function content(array $request = []): string
143 if (!DI::userSession()->getLocalUserId()) {
144 DI::sysmsg()->addNotice(DI::l10n()->t('You must be logged in to use this module'));
145 return Login::form();
152 $profile = User::getOwnerDataById(DI::userSession()->getLocalUserId());
153 if (!DBA::isResult($profile)) {
154 throw new HTTPException\NotFoundException();
159 DI::page()->registerFooterScript('view/asset/es-jquery-sortable/source/js/jquery-sortable-min.js');
160 DI::page()->registerFooterScript(Theme::getPathForFile('js/module/settings/profile/index.js'));
164 $profileFields = DI::profileField()->selectByUserId(DI::userSession()->getLocalUserId());
165 foreach ($profileFields as $profileField) {
166 /** @var ProfileField $profileField */
167 $defaultPermissions = $profileField->permissionSet->withAllowedContacts(
168 Contact::pruneUnavailable($profileField->permissionSet->allow_cid)
172 'id' => $profileField->id,
173 'legend' => $profileField->label,
175 'label' => ['profile_field[' . $profileField->id . '][label]', DI::l10n()->t('Label:'), $profileField->label],
176 'value' => ['profile_field[' . $profileField->id . '][value]', DI::l10n()->t('Value:'), $profileField->value],
177 'acl' => ACL::getFullSelectorHTML(
179 $a->getLoggedInUserId(),
181 $defaultPermissions->toArray(),
182 ['network' => Protocol::DFRN],
183 'profile_field[' . $profileField->id . ']'
186 'permissions' => DI::l10n()->t('Field Permissions'),
187 'permdesc' => DI::l10n()->t("(click to open/close)"),
193 'legend' => DI::l10n()->t('Add a new profile field'),
195 'label' => ['profile_field[new][label]', DI::l10n()->t('Label:')],
196 'value' => ['profile_field[new][value]', DI::l10n()->t('Value:')],
197 'acl' => ACL::getFullSelectorHTML(
199 $a->getLoggedInUserId(),
202 ['network' => Protocol::DFRN],
206 'permissions' => DI::l10n()->t('Field Permissions'),
207 'permdesc' => DI::l10n()->t("(click to open/close)"),
210 DI::page()['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/profile/index_head.tpl'), [
213 $personal_account = ($profile['account-type'] != User::ACCOUNT_TYPE_COMMUNITY);
215 if ($profile['homepage_verified']) {
216 $homepage_help_text = DI::l10n()->t('The homepage is verified. A rel="me" link back to your Friendica profile page was found on the homepage.');
218 $homepage_help_text = DI::l10n()->t('To verify your homepage, add a rel="me" link to it, pointing to your profile URL (%s).', $profile['url']);
221 $tpl = Renderer::getMarkupTemplate('settings/profile/index.tpl');
222 $o .= Renderer::replaceMacros($tpl, [
223 '$personal_account' => $personal_account,
225 '$form_security_token' => self::getFormSecurityToken('settings_profile'),
226 '$form_security_token_photo' => self::getFormSecurityToken('settings_profile_photo'),
228 '$profile_action' => DI::l10n()->t('Profile Actions'),
229 '$banner' => DI::l10n()->t('Edit Profile Details'),
230 '$submit' => DI::l10n()->t('Submit'),
231 '$profpic' => DI::l10n()->t('Change Profile Photo'),
232 '$profpiclink' => '/profile/' . $profile['nickname'] . '/photos',
233 '$viewprof' => DI::l10n()->t('View Profile'),
235 '$lbl_personal_section' => DI::l10n()->t('Personal'),
236 '$lbl_picture_section' => DI::l10n()->t('Profile picture'),
237 '$lbl_location_section' => DI::l10n()->t('Location'),
238 '$lbl_miscellaneous_section' => DI::l10n()->t('Miscellaneous'),
239 '$lbl_custom_fields_section' => DI::l10n()->t('Custom Profile Fields'),
241 '$lbl_profile_photo' => DI::l10n()->t('Upload Profile Photo'),
243 '$baseurl' => DI::baseUrl(),
244 '$nickname' => $profile['nickname'],
245 '$name' => ['name', DI::l10n()->t('Display name:'), $profile['name']],
246 '$about' => ['about', DI::l10n()->t('Description:'), $profile['about']],
247 '$dob' => Temporal::getDateofBirthField($profile['dob'], $profile['timezone']),
248 '$address' => ['address', DI::l10n()->t('Street Address:'), $profile['address']],
249 '$locality' => ['locality', DI::l10n()->t('Locality/City:'), $profile['locality']],
250 '$region' => ['region', DI::l10n()->t('Region/State:'), $profile['region']],
251 '$postal_code' => ['postal_code', DI::l10n()->t('Postal/Zip Code:'), $profile['postal-code']],
252 '$country_name' => ['country_name', DI::l10n()->t('Country:'), $profile['country-name']],
253 '$age' => ((intval($profile['dob'])) ? '(' . DI::l10n()->t('Age: ') . DI::l10n()->tt('%d year old', '%d years old', Temporal::getAgeByTimezone($profile['dob'], $profile['timezone'])) . ')' : ''),
254 '$xmpp' => ['xmpp', DI::l10n()->t('XMPP (Jabber) address:'), $profile['xmpp'], DI::l10n()->t('The XMPP address will be published so that people can follow you there.')],
255 '$matrix' => ['matrix', DI::l10n()->t('Matrix (Element) address:'), $profile['matrix'], DI::l10n()->t('The Matrix address will be published so that people can follow you there.')],
256 '$homepage' => ['homepage', DI::l10n()->t('Homepage URL:'), $profile['homepage'], $homepage_help_text],
257 '$pub_keywords' => ['pub_keywords', DI::l10n()->t('Public Keywords:'), $profile['pub_keywords'], DI::l10n()->t('(Used for suggesting potential friends, can be seen by others)')],
258 '$prv_keywords' => ['prv_keywords', DI::l10n()->t('Private Keywords:'), $profile['prv_keywords'], DI::l10n()->t('(Used for searching profiles, never shown to others)')],
259 '$custom_fields_description' => DI::l10n()->t("<p>Custom fields appear on <a href=\"%s\">your profile page</a>.</p>
260 <p>You can use BBCodes in the field values.</p>
261 <p>Reorder by dragging the field title.</p>
262 <p>Empty the label field to remove a custom field.</p>
263 <p>Non-public fields can only be seen by the selected Friendica contacts or the Friendica contacts in the selected groups.</p>",
264 'profile/' . $profile['nickname'] . '/profile'
266 '$custom_fields' => $custom_fields,
269 $arr = ['profile' => $profile, 'entry' => $o];
270 Hook::callAll('profile_edit', $arr);
275 private static function getProfileFieldsFromInput(int $uid, array $profileFieldInputs, array $profileFieldOrder): ProfileFields
277 $profileFields = new ProfileFields();
279 // Returns an associative array of id => order values
280 $profileFieldOrder = array_flip($profileFieldOrder);
282 // Creation of the new field
283 if (!empty($profileFieldInputs['new']['label'])) {
284 $permissionSet = DI::permissionSet()->selectOrCreate(DI::permissionSetFactory()->createFromString(
286 DI::aclFormatter()->toString($profileFieldInputs['new']['contact_allow'] ?? ''),
287 DI::aclFormatter()->toString($profileFieldInputs['new']['group_allow'] ?? ''),
288 DI::aclFormatter()->toString($profileFieldInputs['new']['contact_deny'] ?? ''),
289 DI::aclFormatter()->toString($profileFieldInputs['new']['group_deny'] ?? '')
292 $profileFields->append(DI::profileFieldFactory()->createFromValues(
294 $profileFieldOrder['new'],
295 $profileFieldInputs['new']['label'],
296 $profileFieldInputs['new']['value'],
301 unset($profileFieldInputs['new']);
302 unset($profileFieldOrder['new']);
304 foreach ($profileFieldInputs as $id => $profileFieldInput) {
305 $permissionSet = DI::permissionSet()->selectOrCreate(DI::permissionSetFactory()->createFromString(
307 DI::aclFormatter()->toString($profileFieldInput['contact_allow'] ?? ''),
308 DI::aclFormatter()->toString($profileFieldInput['group_allow'] ?? ''),
309 DI::aclFormatter()->toString($profileFieldInput['contact_deny'] ?? ''),
310 DI::aclFormatter()->toString($profileFieldInput['group_deny'] ?? '')
313 $profileFields->append(DI::profileFieldFactory()->createFromValues(
315 $profileFieldOrder[$id],
316 $profileFieldInput['label'],
317 $profileFieldInput['value'],
322 return $profileFields;
325 private static function cleanKeywords($keywords)
327 $keywords = str_replace(',', ' ', $keywords);
328 $keywords = explode(' ', $keywords);
331 foreach ($keywords as $keyword) {
332 $keyword = trim(strtolower($keyword));
333 $keyword = trim($keyword, '#');
334 if ($keyword != '') {
335 $cleaned[] = $keyword;
339 $keywords = implode(', ', $cleaned);