ProfileFields::postProcessOutputHtml()   A
last analyzed

Complexity

Conditions 6
Paths 9

Size

Total Lines 36
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 9
nop 3
dl 0
loc 36
rs 9.1111
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Handles the loading of custom and standard fields
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * @version 2.0 Beta 1
11
 *
12
 */
13
14
namespace ElkArte\Profile;
15
16
use BBC\ParserWrapper;
17
use ElkArte\Errors\ErrorContext;
18
use ElkArte\Exceptions\Exception;
19
use ElkArte\Helper\DataValidator;
20
use ElkArte\Helper\HttpReq;
21
use ElkArte\Helper\Util;
22
use ElkArte\Languages\Txt;
23
use ElkArte\MembersList;
24
use ElkArte\User;
25
use ElkArte\PersonalMessage\PmHelper;
26
27
/**
28
 * The ProfileFields class is responsible for loading and rendering profile fields.
29
 */
30
class ProfileFields
31
{
32
	/**
33
	 * Load any custom fields for this area.
34
	 * No area means load all; 'summary' loads all public ones.
35
	 *
36
	 * @param int $memID
37
	 * @param string $area = 'summary'
38
	 * @param array $custom_fields = array()
39
	 */
40
	public function loadCustomFields($memID, $area = 'summary', array $custom_fields = []): void
41
	{
42
		global $context;
43
44
		$context['custom_fields'] = [];
45
		$context['custom_fields_required'] = false;
46
47
		require_once(SUBSDIR . '/Profile.subs.php');
48
		$where = $this->getProfileFieldWhereClause($area, $memID);
49
		$data = getCustomFieldData($where, $area);
50
		foreach ($data as $row)
51
		{
52
			// Shortcut.
53
			$options = MembersList::get($memID)->options;
0 ignored issues
show
Bug Best Practice introduced by
The property options does not exist on anonymous//sources/ElkArte/MembersList.php$0. Since you implemented __get, consider adding a @property annotation.
Loading history...
54
			$value = $options[$row['col_name']] ?? $row['default_value'];
55
56
			// If this was submitted already, then make the value the posted version.
57
			if (!empty($custom_fields) && isset($custom_fields[$row['col_name']]))
58
			{
59
				$value = Util::htmlspecialchars($custom_fields[$row['col_name']]);
60
				if (in_array($row['field_type'], ['select', 'radio']))
61
				{
62
					$options = explode(',', $row['field_options']);
63
					$value = $options[$value] ?? '';
64
				}
65
			}
66
67
			// Generate HTML for the various form inputs.
68
			[$input_html, $output_html, $key] = $this->generateFormFieldHtml($row, $value);
69
			$output_html = $this->postProcessOutputHtml($row, $output_html, $key);
70
71
			$context['custom_fields_required'] = $context['custom_fields_required'] || $row['show_reg'];
72
			$valid_areas = ['register', 'account', 'forumprofile', 'theme'];
73
74
			if (($value === '' || $value === 'no_default') && !in_array($area, $valid_areas))
75
			{
76
				continue;
77
			}
78
79
			$context['custom_fields'][] = [
80
				'name' => $row['field_name'],
81
				'desc' => $row['field_desc'],
82
				'field_type' => $row['field_type'],
83
				'input_html' => $input_html,
84
				'output_html' => $output_html,
85
				'placement' => $row['placement'],
86
				'colname' => $row['col_name'],
87
				'value' => $value,
88
				'show_reg' => $row['show_reg'],
89
				'field_length' => $row['field_length'],
90
				'mask' => $row['mask'],
91
			];
92
		}
93
94
		call_integration_hook('integrate_load_custom_profile_fields', [$memID, $area]);
95
	}
96
97
	/**
98
	 * Generate HTML for a form field based on the given row and value.
99
	 *
100
	 * @param array $row - The row containing information about the field.
101
	 * @param mixed $value - The current value of the field.
102
	 *
103
	 * @return array - An array containing the generated input HTML and the corresponding output HTML.
104
	 */
105
	private function generateFormFieldHtml($row, $value): array
106
	{
107
		global $txt;
108
109
		$output_html = $value;
110
		$key = null;
111
112
		// Implement form field HTML generation based on field_type
113
		switch ($row['field_type'])
114
		{
115
			case 'check':
116
				// generate HTML for checkbox
117
				$true = (bool) $value;
118
				$input_html = '<input id="' . $row['col_name'] . '" type="checkbox" name="customfield[' . $row['col_name'] . ']" ' . ($true ? 'checked="checked"' : '') . ' class="input_check" />';
119
				$output_html = $true ? $txt['yes'] : $txt['no'];
120
				break;
121
			case 'select':
122
				// generate HTML for select
123
				$input_html = '<select id="' . $row['col_name'] . '" name="customfield[' . $row['col_name'] . ']"><option value=""' . ($row['default_value'] === 'no_default' ? ' selected="selected"' : '') . '></option>';
124
				$options = explode(',', $row['field_options']);
125
126
				foreach ($options as $k => $v)
127
				{
128
					$true = ($value === $v);
129
					$input_html .= '<option value="' . $k . '"' . ($true ? ' selected="selected"' : '') . '>' . $v . '</option>';
130
					if ($true)
131
					{
132
						$key = $k;
133
						$output_html = $v;
134
					}
135
				}
136
137
				$input_html .= '</select>';
138
				break;
139
			case 'radio':
140
				// generate HTML for radio
141
				$input_html = '<fieldset><legend>' . $row['field_name'] . '</legend>';
142
				$options = explode(',', $row['field_options']);
143
144
				foreach ($options as $k => $v)
145
				{
146
					$true = ($value === $v);
147
					$input_html .= '<label for="customfield_' . $row['col_name'] . '_' . $k . '"><input type="radio" name="customfield[' . $row['col_name'] . ']" class="input_radio" id="customfield_' . $row['col_name'] . '_' . $k . '" value="' . $k . '" ' . ($true ? 'checked="checked"' : '') . ' />' . $v . '</label><br />';
148
					if ($true)
149
					{
150
						$key = $k;
151
						$output_html = $v;
152
					}
153
				}
154
155
				$input_html .= '</fieldset>';
156
				break;
157
			case 'text':
158
			case 'url':
159
			case 'search':
160
			case 'date':
161
			case 'email':
162
			case 'color':
163
				// A standard input field, including some html5 variants
164
				$row['field_length'] = (int) $row['field_length'];
165
				$input_html = '<input id="' . $row['col_name'] . '" type="' . $row['field_type'] . '" name="customfield[' . $row['col_name'] . ']"';
166
167
				if ($row['field_length'] !== 0)
168
				{
169
					$input_html .= ' maxlength="' . $row['field_length'] . '"';
170
				}
171
172
				if ($row['field_length'] === 0 || $row['field_length'] >= 50)
173
				{
174
					$input_html .= ' size="50"';
175
				}
176
				elseif ($row['field_length'] > 30)
177
				{
178
					$input_html .= ' size="30"';
179
				}
180
				elseif ($row['field_length'] > 10)
181
				{
182
					$input_html .= ' size="20"';
183
				}
184
				else
185
				{
186
					$input_html .= ' size="10"';
187
				}
188
189
				$input_html .= ' value="' . $value . '" placeholder="' . $row['field_name'] . '" class="input_text" />';
190
				break;
191
			default:
192
				// generate HTML for textarea
193
				$input_html = '<textarea id="' . $row['col_name'] . '" name="customfield[' . $row['col_name'] . ']"';
194
195
				if (!empty($row['rows']))
196
				{
197
					$input_html .= ' rows="' . $row['rows'] . '"';
198
				}
199
200
				if (!empty($row['cols']))
201
				{
202
					$input_html .= ' cols="' . $row['cols'] . '"';
203
				}
204
205
				$input_html .= '>' . $value . '</textarea>';
206
				break;
207
		}
208
209
		return [$input_html, $output_html, $key];
210
	}
211
212
	/**
213
	 * This defines every profile field known to man.
214
	 *
215
	 * @param bool $force_reload = false
216
	 */
217
	public function loadProfileFields($force_reload = false): void
218
	{
219
		global $context, $profile_fields, $txt, $scripturl, $modSettings, $cur_profile, $language, $settings;
220
221
		// Don't load this twice!
222
		if (!empty($profile_fields) && !$force_reload)
223
		{
224
			return;
225
		}
226
227
		/**
228
		 * This horrific array defines all the profile fields in the whole world!
229
		 * In general, each "field" has one array - the key of which is the database
230
		 * column name associated with said field.
231
		 *
232
		 * Each item can have the following attributes:
233
		 *
234
		 * string $type: The type of field this is - valid types are:
235
		 *   - callback: This is a field that has its own callback mechanism for templating.
236
		 *   - check:    A simple checkbox.
237
		 *   - hidden:   This doesn't have any visual aspects but may have some validity.
238
		 *   - password: A password box.
239
		 *   - select:   A select box.
240
		 *   - text:     A string of some description.
241
		 *
242
		 * string $label:       The label for this item - default will be $txt[$key] if this isn't set.
243
		 * string $subtext:     The subtext (Small label) for this item.
244
		 * int $size:           Optional size for a text area.
245
		 * array $input_attr:   An array of text strings to be added to the input box for this item.
246
		 * string $value:       The value of the item. If not set $cur_profile[$key] is assumed.
247
		 * string $permission:  Permission required for this item (Excluded _any/_own suffix which is applied automatically).
248
		 * func $input_validate: A runtime function which validates the element before going to the database. It is passed
249
		 *                       the relevant $_POST element if it exists and should be treated like a reference.
250
		 *
251
		 * Return types:
252
		 *   - true:          Element can be stored.
253
		 *   - false:         Skip this element.
254
		 *   - a text string: An error occurred - this is the error message.
255
		 *
256
		 * function $preload: A function that is used to load data required for this element to be displayed. Must return
257
		 *                    true to be completely displayed.
258
		 *
259
		 * string $cast_type: If set casts the element to a certain type. Valid types (bool, int, float).
260
		 * string $save_key:  If the index of this element isn't the database column name it can be overridden with this string.
261
		 * bool $is_dummy:    If set then nothing is acted upon for this element.
262
		 * bool $enabled:     A test to determine whether this is even available - if not is unset.
263
		 * string $link_with: Key which links this field to an overall set.
264
		 *
265
		 * string $js_submit: javascript to add inside the function checkProfileSubmit() in the template
266
		 * string $js:        javascript to add to the page in general
267
		 * string $js_load:   filename of js to be loaded with "loadJavasciptFile"
268
		 *
269
		 * Note that all elements that have a custom input_validate must ensure they set the value of $cur_profile correct to enable
270
		 * the changes to be displayed correctly on submitting of the form.
271
		 */
272
273
		$profile_fields = [
274
			'avatar_choice' => [
275
				'type' => 'callback',
276
				'callback_func' => 'avatar_select',
277
				// This handles the permissions too.
278
				'preload' => 'profileLoadAvatarData',
279
				'input_validate' => 'profileSaveAvatarData',
280
				'save_key' => 'avatar',
281
			],
282
			'bday1' => [
283
				'type' => 'callback',
284
				'callback_func' => 'birthdate',
285
				'permission' => 'profile_extra',
286
				'preload' => static function () {
287
					global $cur_profile, $context;
288
289
					// Split up the birthdate...
290
					[$uyear, $umonth, $uday] = explode('-', empty($cur_profile['birthdate']) || $cur_profile['birthdate'] === '0001-01-01' ? '0000-00-00' : $cur_profile['birthdate']);
291
					$context['member']['birth_date'] = [
292
						'year' => $uyear === '0004' ? '0000' : $uyear,
293
						'month' => $umonth,
294
						'day' => $uday,
295
					];
296
297
					return true;
298
				},
299
				'input_validate' => static function (&$value) {
300
					global $profile_vars, $cur_profile;
301
302
					if (isset($_POST['bday1']))
303
					{
304
						$date_parts = explode('-', $_POST['bday1']);
305
						$bday3 = isset($date_parts[0]) ? (int) $date_parts[0] : 1; // Year
306
						$bday1 = isset($date_parts[1]) ? (int) $date_parts[1] : 1; // Month
307
						$bday2 = isset($date_parts[2]) ? (int) $date_parts[2] : 1; // Day
308
309
						// Set to blank?
310
						if ($bday3 === 1 && $bday2 === 1 && $bday1 === 1)
311
						{
312
							$value = '0001-01-01';
313
						}
314
						else
315
						{
316
							$value = checkdate($bday1, $bday2, max($bday3, 4)) ? sprintf('%04d-%02d-%02d', max($bday3, 4), $bday1, $bday2) : '0001-01-01';
317
						}
318
					}
319
					else
320
					{
321
						$value = '0001-01-01';
322
					}
323
					$profile_vars['birthdate'] = $value;
324
					$cur_profile['birthdate'] = $value;
325
326
					return false;
327
				},
328
			],
329
			'date_registered' => [
330
				'type' => 'date',
331
				'value' => empty($cur_profile['date_registered']) ? $txt['not_applicable'] : Util::strftime('%Y-%m-%d', $cur_profile['date_registered'] + (User::$info->time_offset + $modSettings['time_offset']) * 3600),
0 ignored issues
show
Bug Best Practice introduced by
The property time_offset does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
332
				'label' => $txt['date_registered'],
333
				'log_change' => true,
334
				'permission' => 'moderate_forum',
335
				'input_validate' => static function (&$value) {
336
					global $txt, $modSettings, $cur_profile;
337
338
					// Bad date!  Go try again - please?
339
					if (($value = strtotime($value)) === -1)
340
					{
341
						$value = $cur_profile['date_registered'];
342
343
						return $txt['invalid_registration'] . ' ' . Util::strftime('%d %b %Y ' . (str_contains(User::$info->time_format, '%H') ? '%I:%M:%S %p' : '%H:%M:%S'), forum_time(false));
0 ignored issues
show
Bug Best Practice introduced by
The property time_format does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
It seems like ElkArte\User::info->time_format can also be of type null; however, parameter $haystack of str_contains() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

343
						return $txt['invalid_registration'] . ' ' . Util::strftime('%d %b %Y ' . (str_contains(/** @scrutinizer ignore-type */ User::$info->time_format, '%H') ? '%I:%M:%S %p' : '%H:%M:%S'), forum_time(false));
Loading history...
344
					}
345
346
					// As long as it doesn't equal "N/A"...
347
					if ($value !== $txt['not_applicable'] && $value !== strtotime(Util::strftime('%Y-%m-%d', $cur_profile['date_registered'] + (User::$info->time_offset + $modSettings['time_offset']) * 3600)))
0 ignored issues
show
Bug Best Practice introduced by
The property time_offset does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
348
					{
349
						$value -= (User::$info->time_offset + $modSettings['time_offset']) * 3600;
350
					}
351
					else
352
					{
353
						$value = $cur_profile['date_registered'];
354
					}
355
356
					return true;
357
				},
358
			],
359
			'email_address' => [
360
				'type' => 'email',
361
				'label' => $txt['user_email_address'],
362
				'subtext' => $txt['valid_email'],
363
				'log_change' => true,
364
				'permission' => 'profile_identity',
365
				'input_validate' => function ($value) {
366
					global $context, $old_profile, $profile_vars, $modSettings;
367
368
					if (strtolower($value) === strtolower($old_profile['email_address']))
369
					{
370
						return false;
371
					}
372
373
					$isValid = ProfileFields::profileValidateEmail($value, $context['id_member']);
374
375
					// Do they need to re-validate? If so, schedule the function!
376
					if ($isValid === true && !empty($modSettings['send_validation_onChange']) && !allowedTo('moderate_forum'))
377
					{
378
						require_once(SUBSDIR . '/Auth.subs.php');
379
						$old_profile['validation_code'] = generateValidationCode(14);
380
						$profile_vars['validation_code'] = substr(hash('sha256', $old_profile['validation_code']), 0, 10);
381
						$profile_vars['is_activated'] = 2;
382
						$context['profile_execute_on_save'][] = 'profileSendActivation';
383
						unset($context['profile_execute_on_save']['reload_user']);
384
					}
385
386
					return $isValid;
387
				},
388
			],
389
			// Selecting group membership is complicated, so we treat it separately!
390
			'id_group' => [
391
				'type' => 'callback',
392
				'callback_func' => 'group_manage',
393
				'permission' => 'manage_membergroups',
394
				'preload' => 'profileLoadGroups',
395
				'log_change' => true,
396
				'input_validate' => 'profileSaveGroups',
397
			],
398
			'id_theme' => [
399
				'type' => 'callback',
400
				'callback_func' => 'theme_pick',
401
				'permission' => 'profile_extra',
402
				'enabled' => empty($settings['disable_user_variant']) || !empty($modSettings['theme_allow']) || allowedTo('admin_forum'),
403
				'preload' => static function () {
404
					global $context, $cur_profile, $txt;
405
406
					$db = database();
407
					$request = $db->query('', '
408
					SELECT value
409
					FROM {db_prefix}themes
410
					WHERE id_theme = {int:id_theme}
411
						AND variable = {string:variable}
412
					LIMIT 1', [
413
							'id_theme' => $cur_profile['id_theme'],
414
							'variable' => 'name',
415
						]
416
					);
417
					[$name] = $request->fetch_row();
418
					$request->free_result();
419
					$context['member']['theme'] = [
420
						'id' => $cur_profile['id_theme'],
421
						'name' => empty($cur_profile['id_theme']) ? $txt['theme_forum_default'] : $name
422
					];
423
424
					return true;
425
				},
426
				'input_validate' => static function (&$value) {
427
					$value = (int) $value;
428
					return true;
429
				},
430
			],
431
			'karma_good' => [
432
				'type' => 'callback',
433
				'callback_func' => 'karma_modify',
434
				'permission' => 'admin_forum',
435
				// Set karma_bad too!
436
				'input_validate' => static function (&$value) {
437
					global $profile_vars, $cur_profile;
438
439
					$value = (int) $value;
440
					if (isset($_POST['karma_bad']))
441
					{
442
						$profile_vars['karma_bad'] = $_POST['karma_bad'] !== '' ? (int) $_POST['karma_bad'] : 0;
443
						$cur_profile['karma_bad'] = $_POST['karma_bad'] !== '' ? (int) $_POST['karma_bad'] : 0;
444
					}
445
446
					return true;
447
				},
448
				'preload' => static function () {
449
					global $context, $cur_profile;
450
451
					$context['member']['karma'] = [
452
						'good' => (int) $cur_profile['karma_good'],
453
						'bad' => (int) $cur_profile['karma_bad']
454
					];
455
456
					return true;
457
				},
458
				'enabled' => !empty($modSettings['karmaMode']),
459
			],
460
			'lngfile' => [
461
				'type' => 'select',
462
				'options' => 'return $context[\'profile_languages\'];',
463
				'label' => $txt['preferred_language'],
464
				'permission' => 'profile_identity',
465
				'preload' => 'profileLoadLanguages',
466
				'enabled' => !empty($modSettings['userLanguage']),
467
				'value' => empty($cur_profile['lngfile']) ? Util::ucfirst($language) . '.php' : Util::ucfirst(basename($cur_profile['lngfile'], '.php')) . '.php',
468
				'input_validate' => static function (&$value) {
469
					global $context, $cur_profile;
470
471
					// Load the languages.
472
					profileLoadLanguages();
473
					if (isset($context['profile_languages'][$value]))
474
					{
475
						if ($context['user']['is_owner'] && empty($context['password_auth_failed']))
476
						{
477
							$_SESSION['language'] = $value;
478
						}
479
480
						return true;
481
					}
482
483
					$value = $cur_profile['lngfile'];
484
485
					return false;
486
				},
487
			],
488
			// The username is not always editable - so adjust it as such.
489
			'member_name' => [
490
				'type' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? 'text' : 'label',
491
				'label' => $txt['username'],
492
				'subtext' => allowedTo('admin_forum') && !isset($_GET['changeusername']) ? '[<a href="' . $scripturl . '?action=profile;u=' . $context['id_member'] . ';area=account;changeusername" class="em">' . $txt['username_change'] . '</a>]' : '',
493
				'log_change' => true,
494
				'permission' => 'profile_identity',
495
				'prehtml' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? '<div class="warningbox">' . $txt['username_warning'] . '</div>' : '',
496
				'input_validate' => static function (&$value) {
497
					global $context, $cur_profile;
498
499
					if (allowedTo('admin_forum'))
500
					{
501
						// We'll need this...
502
						require_once(SUBSDIR . '/Auth.subs.php');
503
504
						// Maybe they are trying to change their password as well?
505
						$resetPassword = true;
506
						if (isset($_POST['passwrd1'], $_POST['passwrd2']) && $_POST['passwrd1'] !== '' && $_POST['passwrd1'] === $_POST['passwrd2'] && validatePassword($_POST['passwrd1'], $value, [$cur_profile['real_name'], User::$info->username, User::$info->name, User::$info->email]) === null)
0 ignored issues
show
Bug Best Practice introduced by
The property username does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property email does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
introduced by
The condition validatePassword($_POST[...:info->email)) === null is always false.
Loading history...
Bug Best Practice introduced by
The property name does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
507
						{
508
							$resetPassword = false;
509
						}
510
511
						// Do the reset... this will email them too.
512
						if ($resetPassword)
0 ignored issues
show
introduced by
The condition $resetPassword is always true.
Loading history...
513
						{
514
							resetPassword($context['id_member'], $value);
515
						}
516
						elseif ($value !== null)
517
						{
518
							$errors = ErrorContext::context('change_username', 0);
519
520
							validateUsername($context['id_member'], $value, 'change_username');
521
522
							// No errors we can proceed normally
523
							if (!$errors->hasErrors())
524
							{
525
								updateMemberData($context['id_member'], ['member_name' => $value]);
526
							}
527
							else
528
							{
529
								// If there are "important" errors, and you are not an admin: log the first error
530
								// Otherwise grab all of them and do not log anything
531
								$error_severity = $errors->hasErrors(1) && User::$info->is_admin === false ? 1 : null;
532
								$prepared_errors = $errors->prepareErrors($error_severity);
533
								throw new Exception(reset($prepared_errors), $error_severity === null ? false : 'general');
534
							}
535
						}
536
					}
537
538
					return false;
539
				},
540
			],
541
			'passwrd1' => [
542
				'type' => 'password',
543
				'label' => ucwords($txt['choose_pass']),
544
				'subtext' => $txt['password_strength'],
545
				'size' => 20,
546
				'value' => '',
547
				'enabled' => true,
548
				'permission' => 'profile_identity',
549
				'save_key' => 'passwd',
550
				// Note this will only work if passwrd2 also exists!
551
				'input_validate' => static function (&$value) {
552
					global $cur_profile;
553
554
					// If we didn't try it, then ignore it!
555
					if ($value === '')
556
					{
557
						return false;
558
					}
559
560
					// Do the two entries for the password even match?
561
					if (!isset($_POST['passwrd2']) || $value !== $_POST['passwrd2'])
562
					{
563
						return 'bad_new_password';
564
					}
565
566
					// Let's get the validation function into play...
567
					require_once(SUBSDIR . '/Auth.subs.php');
568
					$passwordErrors = validatePassword($value, $cur_profile['member_name'], [$cur_profile['real_name'], User::$info->username, User::$info->name, User::$info->email]);
0 ignored issues
show
Bug Best Practice introduced by
The property username does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property name does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property email does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
569
570
					// Were there errors?
571
					if ($passwordErrors !== null)
0 ignored issues
show
introduced by
The condition $passwordErrors !== null is always true.
Loading history...
572
					{
573
						return 'password_' . $passwordErrors;
574
					}
575
576
					// Set up the new password variable... ready for storage.
577
					require_once(SUBSDIR . '/Auth.subs.php');
578
					$value = validateLoginPassword($value, '', $cur_profile['member_name'], true);
579
580
					return true;
581
				},
582
			],
583
			'passwrd2' => [
584
				'type' => 'password',
585
				'label' => ucwords($txt['verify_pass']),
586
				'enabled' => true,
587
				'size' => 20,
588
				'value' => '',
589
				'permission' => 'profile_identity',
590
				'is_dummy' => true,
591
			],
592
			'enable_otp' => [
593
				'type' => 'check',
594
				'value' => !empty($cur_profile['enable_otp']),
595
				'subtext' => $txt['otp_enabled_help'],
596
				'label' => $txt['otp_enabled'],
597
				'permission' => 'profile_identity',
598
			],
599
			'otp_secret' => [
600
				'type' => 'text',
601
				'label' => ucwords($txt['otp_token']),
602
				'subtext' => $txt['otp_token_help'],
603
				'enabled' => true,
604
				'size' => 20,
605
				'value' => empty($cur_profile['otp_secret']) ? '' : $cur_profile['otp_secret'],
606
				'postinput' => '<div style="display: inline-block;"><input type="button" value="' . $txt['otp_generate'] . '" onclick="generateSecret();"></div><div id="qrcode"></div>',
607
				'permission' => 'profile_identity',
608
			],
609
			// This does contact-related settings
610
			'receive_from' => [
611
				'type' => 'select',
612
				'options' => [
613
					$txt['receive_from_everyone'],
614
					$txt['receive_from_ignore'],
615
					$txt['receive_from_buddies'],
616
					$txt['receive_from_admins'],
617
				],
618
				'subtext' => $txt['receive_from_description'],
619
				'value' => empty($cur_profile['receive_from']) ? 0 : $cur_profile['receive_from'],
620
				'input_validate' => static function (&$value) {
621
					global $cur_profile, $profile_vars;
622
623
					// Simple validate and apply the two "sub settings"
624
					$value = max(min($value, 3), 0);
625
					$cur_profile['receive_from'] = $profile_vars['receive_from'] = max(min((int) $_POST['receive_from'], 4), 0);
626
627
					return true;
628
				},
629
			],
630
			// This does ALL the pm settings
631
			'pm_settings' => [
632
				'type' => 'callback',
633
				'callback_func' => 'pm_settings',
634
				'permission' => 'pm_read',
635
				'save_key' => 'pm_prefs',
636
				'preload' => static function () {
637
					global $context, $cur_profile;
638
639
					$context['display_mode'] = (int) $cur_profile['pm_prefs'] & 3;
640
					$context['send_email'] = $cur_profile['pm_email_notify'];
641
642
					return true;
643
				},
644
				'input_validate' => static function (&$value) {
645
					global $cur_profile, $profile_vars;
646
647
					// Simple validate and apply the two "sub settings"
648
					$value = max(min($value, 2), 0);
649
					$cur_profile['pm_email_notify'] = $profile_vars['pm_email_notify'] = max(min((int) $_POST['pm_email_notify'], 2), 0);
650
651
					return true;
652
				},
653
			],
654
			'posts' => [
655
				'type' => 'int',
656
				'label' => $txt['profile_posts'],
657
				'log_change' => true,
658
				'size' => 7,
659
				'permission' => 'moderate_forum',
660
				'input_validate' => static function (&$value) {
661
					// Account for comma_format presentation up front
662
					$check = strtr($value, [',' => '', '.' => '', ' ' => '']);
663
					if (!is_numeric($check))
664
					{
665
						return 'digits_only';
666
					}
667
					$value = $check !== '' ? $check : 0;
668
669
					return true;
670
				},
671
			],
672
			'real_name' => [
673
				'type' => !empty($modSettings['allow_editDisplayName']) || allowedTo('moderate_forum') ? 'text' : 'label',
674
				'label' => $txt['name'],
675
				'subtext' => $txt['display_name_desc'],
676
				'log_change' => true,
677
				'input_attr' => ['maxlength="60"'],
678
				'permission' => 'profile_identity',
679
				'enabled' => !empty($modSettings['allow_editDisplayName']) || allowedTo('moderate_forum'),
680
				'input_validate' => static function (&$value) {
681
					global $context, $cur_profile;
682
683
					$value = trim(preg_replace('~\s~u', ' ', $value));
684
					if (trim($value) === '')
685
					{
686
						return 'no_name';
687
					}
688
689
					if (Util::strlen($value) > 60)
690
					{
691
						return 'name_too_long';
692
					}
693
694
					if ($cur_profile['real_name'] !== $value)
695
					{
696
						require_once(SUBSDIR . '/Members.subs.php');
697
						if (isReservedName($value, $context['id_member']))
698
						{
699
							return 'name_taken';
700
						}
701
					}
702
703
					return true;
704
				},
705
			],
706
			'secret_question' => [
707
				'type' => 'text',
708
				'label' => $txt['secret_question'],
709
				'subtext' => $txt['secret_desc'],
710
				'size' => 50,
711
				'permission' => 'profile_identity',
712
			],
713
			'secret_answer' => [
714
				'type' => 'text',
715
				'label' => $txt['secret_answer'],
716
				'subtext' => $txt['secret_desc2'],
717
				'size' => 20,
718
				'postinput' => '<span class="smalltext" style="margin-left: 4ex;">[<a href="' . $scripturl . '?action=quickhelp;help=secret_why_blank" onclick="return reqOverlayDiv(this.href);">' . $txt['secret_why_blank'] . '</a>]</span>',
719
				'value' => '',
720
				'permission' => 'profile_identity',
721
				'input_validate' => static function (&$value) {
722
					global $cur_profile;
723
724
					if (empty($value))
725
					{
726
						require_once(SUBSDIR . '/Members.subs.php');
727
						$member = getBasicMemberData($cur_profile['id_member'], ['authentication' => true]);
728
729
						// No previous answer was saved, so that\'s all good
730
						if (empty($member['secret_answer']))
731
						{
732
							return true;
733
						}
734
735
						// There is a previous secret answer to the secret question, so let\'s put it back in the db...
736
						$value = $member['secret_answer'];
737
738
						// We have to tell the code is an error, otherwise an empty value will go into the db
739
						return false;
740
					}
741
742
					$value = $value !== '' ? md5($value) : '';
743
744
					return true;
745
				},
746
			],
747
			'signature' => [
748
				'type' => 'callback',
749
				'callback_func' => 'signature_modify',
750
				'permission' => 'profile_extra',
751
				'enabled' => str_starts_with($modSettings['signature_settings'], "1"),
752
				'preload' => 'profileLoadSignatureData',
753
				'input_validate' => 'profileValidateSignature',
754
			],
755
			'show_online' => [
756
				'type' => 'check',
757
				'label' => $txt['show_online'],
758
				'permission' => 'profile_identity',
759
				'enabled' => !empty($modSettings['allow_hideOnline']) || allowedTo('moderate_forum'),
760
			],
761
			// Pretty much a dummy entry - it populates all the theme settings.
762
			'theme_settings' => [
763
				'type' => 'callback',
764
				'callback_func' => 'theme_settings',
765
				'permission' => 'profile_extra',
766
				'is_dummy' => true,
767
				'preload' => static function () {
768
					global $context;
769
770
					Txt::load('Settings');
771
772
					// Can they disable censoring?
773
					$context['allow_no_censored'] = false;
774
					if (User::$info->is_admin || $context['user']['is_owner'])
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
775
					{
776
						$context['allow_no_censored'] = allowedTo('disable_censor');
777
					}
778
779
					return true;
780
				},
781
			],
782
			'time_format' => [
783
				'type' => 'callback',
784
				'callback_func' => 'timeformat_modify',
785
				'permission' => 'profile_extra',
786
				'preload' => static function () {
787
					global $context, $txt, $cur_profile, $modSettings;
788
789
					$context['easy_timeformats'] = [
790
						['format' => '', 'title' => $txt['timeformat_default']],
791
						['format' => '%B %d, %Y, %I:%M:%S %p', 'title' => $txt['timeformat_easy1']],
792
						['format' => '%B %d, %Y, %H:%M:%S', 'title' => $txt['timeformat_easy2']],
793
						['format' => '%Y-%m-%d, %H:%M:%S', 'title' => $txt['timeformat_easy3']],
794
						['format' => '%d %B %Y, %H:%M:%S', 'title' => $txt['timeformat_easy4']],
795
						['format' => '%d-%m-%Y, %H:%M:%S', 'title' => $txt['timeformat_easy5']]
796
					];
797
					$context['member']['time_format'] = $cur_profile['time_format'];
798
					$context['current_forum_time'] = standardTime(time() - User::$info->time_offset * 3600, false);
0 ignored issues
show
Bug Best Practice introduced by
The property time_offset does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
799
					$context['current_forum_time_js'] = Util::strftime('%Y,' . ((int) Util::strftime('%m', time() + $modSettings['time_offset'] * 3600) - 1) . ',%d,%H,%M,%S', time() + $modSettings['time_offset'] * 3600);
800
					$context['current_forum_time_hour'] = (int) Util::strftime('%H', forum_time(false));
801
802
					return true;
803
				},
804
			],
805
			'time_offset' => [
806
				'type' => 'callback',
807
				'callback_func' => 'timeoffset_modify',
808
				'permission' => 'profile_extra',
809
				'preload' => static function () {
810
					global $context, $cur_profile;
811
812
					$context['member']['time_offset'] = $cur_profile['time_offset'];
813
814
					return true;
815
				},
816
				'input_validate' => static function (&$value) {
817
					// Validate the time_offset...
818
					$value = (float) str_replace(',', '.', $value);
819
					if ($value < -23.5 || $value > 23.5)
820
					{
821
						return 'bad_offset';
822
					}
823
824
					return true;
825
				},
826
			],
827
			'usertitle' => [
828
				'type' => 'text',
829
				'label' => $txt['custom_title'],
830
				'log_change' => true,
831
				'input_attr' => ['maxlength="50"'],
832
				'size' => 50,
833
				'permission' => 'profile_title',
834
				'enabled' => !empty($modSettings['titlesEnable']),
835
				'input_validate' => static function ($value) {
836
					if (Util::strlen($value) > 50)
837
					{
838
						return 'user_title_too_long';
839
					}
840
841
					return true;
842
				},
843
			],
844
			'website_title' => [
845
				'type' => 'text',
846
				'label' => $txt['website_title'],
847
				'subtext' => $txt['include_website_url'],
848
				'size' => 50,
849
				'permission' => 'profile_extra',
850
				'link_with' => 'website',
851
			],
852
			'website_url' => [
853
				'type' => 'url',
854
				'label' => $txt['website_url'],
855
				'subtext' => $txt['complete_url'],
856
				'size' => 50,
857
				'permission' => 'profile_extra',
858
				// Fix the URL...
859
				'input_validate' => static function (&$value) {
860
					$value = addProtocol($value, ['http://', 'https://', 'ftp://', 'ftps://']);
861
					if (strlen($value) < 8)
862
					{
863
						$value = '';
864
					}
865
866
					return true;
867
				},
868
				'link_with' => 'website',
869
			],
870
		];
871
872
		call_integration_hook('integrate_load_profile_fields', [&$profile_fields]);
873
874
		$disabled_fields = empty($modSettings['disabled_profile_fields']) ? [] : explode(',', $modSettings['disabled_profile_fields']);
875
876
		// Hard to imagine this won't be necessary
877
		require_once(SUBSDIR . '/Members.subs.php');
878
879
		// For each of the above let's take out the bits which don't apply - to save memory and security!
880
		foreach ($profile_fields as $key => $field)
881
		{
882
			// Do we have permission to do this?
883
			if (isset($field['permission']) && !allowedTo(($context['user']['is_owner'] ? [$field['permission'] . '_own', $field['permission'] . '_any'] : $field['permission'] . '_any')) && !allowedTo($field['permission']))
884
			{
885
				unset($profile_fields[$key]);
886
			}
887
888
			// Is it enabled?
889
			if (isset($field['enabled']) && !$field['enabled'])
890
			{
891
				unset($profile_fields[$key]);
892
			}
893
894
			// Is it specifically disabled?
895
			if (in_array($key, $disabled_fields, true) || (isset($field['link_with']) && in_array($field['link_with'], $disabled_fields, true)))
896
			{
897
				unset($profile_fields[$key]);
898
			}
899
		}
900
	}
901
902
	/**
903
	 * Save the profile changes.
904
	 *
905
	 * @param string[] $fields
906
	 * @param string $hook
907
	 */
908
	public function saveProfileFields($fields, $hook): void
909
	{
910
		global $profile_fields, $profile_vars, $context, $old_profile, $post_errors, $cur_profile;
911
912
		// Use HttpReq for all request data access
913
		$req = HttpReq::instance();
914
915
		if (!empty($hook))
916
		{
917
			call_integration_hook('integrate_' . $hook . '_profile_fields', [&$fields]);
918
		}
919
920
		// Load them up.
921
		$this->loadProfileFields();
922
923
		// This makes things easier...
924
		$old_profile = $cur_profile;
925
926
		// This allows variables to call activities when they save
927
		// - by default, just to reload their settings
928
		$context['profile_execute_on_save'] = [];
929
		if ($context['user']['is_owner'])
930
		{
931
			$context['profile_execute_on_save']['reload_user'] = 'profileReloadUser';
932
		}
933
934
		// Assume we log nothing.
935
		$context['log_changes'] = [];
936
937
		// Cycle through the profile fields working out what to do!
938
		foreach ($fields as $key)
939
		{
940
			if (!isset($profile_fields[$key]))
941
			{
942
				continue;
943
			}
944
945
			$field = $profile_fields[$key];
946
947
			// Fetch the posted value (if any) using HttpReq, do not rely on $_POST
948
			$has_value = $req->hasPost($key);
949
			$preview_signature = $req->hasPost('preview_signature');
950
			if (!$has_value || !empty($field['is_dummy']) || ($preview_signature && $key === 'signature'))
951
			{
952
				continue;
953
			}
954
955
			// What gets updated?
956
			$db_key = $field['save_key'] ?? $key;
957
958
			// Work on a local copy of the submitted value
959
			$value = $req->post->{$key};
960
961
			// Right - we have something that is enabled, we can act upon and has a value
962
			// posted to it. Does it have a validation function?
963
			if (isset($field['input_validate']))
964
			{
965
				$is_valid = $field['input_validate']($value);
966
967
				// An error occurred - set it as such!
968
				if ($is_valid !== true)
969
				{
970
					// Is this an actual error?
971
					if ($is_valid !== false)
972
					{
973
						$post_errors[$key] = $is_valid;
974
						$profile_fields[$key]['is_error'] = $is_valid;
975
					}
976
977
					// Retain the old value.
978
					$cur_profile[$key] = $value;
979
					continue;
980
				}
981
			}
982
983
			// Are we doing a cast?
984
			$field['cast_type'] = empty($field['cast_type']) ? $field['type'] : $field['cast_type'];
985
986
			// Finally, clean up certain types.
987
			if ($field['cast_type'] === 'int')
988
			{
989
				$value = (int) $value;
990
			}
991
			elseif ($field['cast_type'] === 'float')
992
			{
993
				$value = (float) $value;
994
			}
995
			elseif ($field['cast_type'] === 'check')
996
			{
997
				$value = empty($value) ? 0 : 1;
998
			}
999
1000
			// If we got here, we're doing OK.
1001
			if ($field['type'] !== 'hidden' && (!isset($old_profile[$key]) || $value != $old_profile[$key]))
1002
			{
1003
				// Set the save variable.
1004
				$profile_vars[$db_key] = $value;
1005
1006
				// And update the user profile.
1007
				$cur_profile[$key] = $value;
1008
1009
				// Are we logging it?
1010
				if (!empty($field['log_change']) && isset($old_profile[$key]))
1011
				{
1012
					$context['log_changes'][$key] = [
1013
						'previous' => $old_profile[$key],
1014
						'new' => $value,
1015
					];
1016
				}
1017
			}
1018
1019
			// Logging group changes are a bit different...
1020
			if ($key === 'id_group' && $field['log_change'])
1021
			{
1022
				profileLoadGroups();
1023
1024
				// Any changes to a primary group?
1025
				$posted_id_group = $req->getPost('id_group', 'intval', $old_profile['id_group']);
1026
				if ((int) $posted_id_group !== (int) $old_profile['id_group'])
1027
				{
1028
					$context['log_changes']['id_group'] = [
1029
						'previous' => !empty($old_profile[$key]) && isset($context['member_groups'][$old_profile[$key]]) ? $context['member_groups'][$old_profile[$key]]['name'] : '',
1030
						'new' => !empty($posted_id_group) && isset($context['member_groups'][$posted_id_group]) ? $context['member_groups'][$posted_id_group]['name'] : '',
1031
					];
1032
				}
1033
1034
				// Prepare additional groups for comparison.
1035
				$additional_groups = [
1036
					'previous' => empty($old_profile['additional_groups']) ? [] : explode(',', $old_profile['additional_groups']),
1037
					'new' => $req->hasPost('additional_groups') ? array_diff((array) $req->post->additional_groups, [0]) : [],
1038
				];
1039
1040
				sort($additional_groups['previous']);
1041
				sort($additional_groups['new']);
1042
1043
				// What about additional groups?
1044
				if ($additional_groups['previous'] !== $additional_groups['new'])
1045
				{
1046
					foreach ($additional_groups as $type => $groups)
1047
					{
1048
						foreach ($groups as $id => $group)
1049
						{
1050
							if (isset($context['member_groups'][$group]))
1051
							{
1052
								$additional_groups[$type][$id] = $context['member_groups'][$group]['name'];
1053
							}
1054
							else
1055
							{
1056
								unset($additional_groups[$type][$id]);
1057
							}
1058
						}
1059
1060
						$additional_groups[$type] = implode(', ', $additional_groups[$type]);
1061
					}
1062
1063
					$context['log_changes']['additional_groups'] = $additional_groups;
1064
				}
1065
			}
1066
		}
1067
1068
		// @todo Temporary
1069
		if ($context['user']['is_owner'])
1070
		{
1071
			$changeOther = allowedTo(['profile_extra_any', 'profile_extra_own']);
1072
		}
1073
		else
1074
		{
1075
			$changeOther = allowedTo('profile_extra_any');
1076
		}
1077
1078
		if ($changeOther && empty($post_errors))
1079
		{
1080
			// Apply theme changes using HttpReq
1081
			$id_theme = $req->getPost('id_theme', 'intval', $old_profile['id_theme']);
1082
			makeThemeChanges($context['id_member'], (int) $id_theme);
1083
1084
			// Apply custom field changes for the active subaction (usually in query string)
1085
			$sa = $req->getQuery('sa', null, '');
1086
			if (!empty($sa))
1087
			{
1088
				makeCustomFieldChanges($context['id_member'], $sa, false);
1089
			}
1090
		}
1091
1092
		// Free memory!
1093
		unset($profile_fields);
1094
	}
1095
1096
	/**
1097
	 * Validate an email address.
1098
	 *
1099
	 * @param string $email
1100
	 * @param int $memID = 0
1101
	 *
1102
	 * @return bool|string
1103
	 */
1104
	public static function profileValidateEmail($email, $memID = 0)
1105
	{
1106
		// Check the name and email for validity.
1107
		$check = [];
1108
		$check['email'] = strtr($email, ['&#039;' => "'"]);
1109
		if (DataValidator::is_valid($check, ['email' => 'valid_email|required'], ['email' => 'trim']))
1110
		{
1111
			$email = $check['email'];
1112
		}
1113
		else
1114
		{
1115
			return empty($check['email']) ? 'no_email' : 'bad_email';
1116
		}
1117
1118
		// Email addresses should be and stay unique.
1119
		$num = isUniqueEmail($memID, $email);
1120
1121
		return ($num > 0) ? 'email_taken' : true;
1122
	}
1123
1124
	/**
1125
	 * Get the WHERE clause for retrieving profile fields.
1126
	 *
1127
	 * @param string $area
1128
	 * @param int $memID
1129
	 * @return string
1130
	 */
1131
	public function getProfileFieldWhereClause(string $area, int $memID): string
1132
	{
1133
		// Get the right restrictions in place...
1134
		$where = 'active = 1';
1135
		if ($area !== 'register' && !allowedTo('admin_forum'))
1136
		{
1137
			// If it's the owner, they can see two types of private fields, regardless.
1138
			if ($memID === User::$info->id)
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1139
			{
1140
				$where .= $area === 'summary' ? ' AND private < 3' : ' AND (private = 0 OR private = 2)';
1141
			}
1142
			else
1143
			{
1144
				$where .= $area === 'summary' ? ' AND private < 2' : ' AND private = 0';
1145
			}
1146
		}
1147
1148
		if ($area === 'register')
1149
		{
1150
			$where .= ' AND show_reg != 0';
1151
		}
1152
		elseif ($area !== 'summary')
1153
		{
1154
			$where .= ' AND show_profile = {string:area}';
1155
		}
1156
1157
		return $where;
1158
	}
1159
1160
	/**
1161
	 * Do any post-processing to the output HTML for custom fields.
1162
	 *
1163
	 * @param array $row
1164
	 * @param string $output_html
1165
	 * @param int|null $key
1166
	 *
1167
	 * @return string
1168
	 */
1169
	public function postProcessOutputHtml($row, $output_html, $key): string
1170
	{
1171
		global $scripturl, $settings;
1172
1173
		// Parse BBCode
1174
		if ($row['bbc'])
1175
		{
1176
			$bbc_parser = ParserWrapper::instance();
1177
1178
			$output_html = $bbc_parser->parseCustomFields($output_html);
1179
		}
1180
		// Allow for newlines at least
1181
		elseif ($row['field_type'] === 'textarea')
1182
		{
1183
			$output_html = strtr($output_html, ["\n" => '<br />']);
1184
		}
1185
1186
		// Enclosing the user input within some other text?
1187
		if (!empty($row['enclose']) && !empty($output_html))
1188
		{
1189
			$replacements = [
1190
				'{SCRIPTURL}' => $scripturl,
1191
				'{IMAGES_URL}' => $settings['images_url'],
1192
				'{DEFAULT_IMAGES_URL}' => $settings['default_images_url'],
1193
				'{INPUT}' => $output_html,
1194
			];
1195
1196
			if (in_array($row['field_type'], ['radio', 'select']))
1197
			{
1198
				$replacements['{KEY}'] = $row['col_name'] . '_' . ($key ?? 0);
1199
			}
1200
1201
			$output_html = strtr($row['enclose'], $replacements);
1202
		}
1203
1204
		return $output_html;
1205
	}
1206
}
1207