ProfileFields::saveProfileFields()   F
last analyzed

Complexity

Conditions 40
Paths > 20000

Size

Total Lines 171
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 40
eloc 67
nc 491160
nop 2
dl 0
loc 171
rs 0
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

341
						return $txt['invalid_registration'] . ' ' . Util::strftime('%d %b %Y ' . (strpos(/** @scrutinizer ignore-type */ User::$info->time_format, '%H') !== false ? '%I:%M:%S %p' : '%H:%M:%S'), forum_time(false));
Loading history...
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...
342
					}
343
344
					// As long as it doesn't equal "N/A"...
345
					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...
346
					{
347
						$value -= (User::$info->time_offset + $modSettings['time_offset']) * 3600;
348
					}
349
					else
350
					{
351
						$value = $cur_profile['date_registered'];
352
					}
353
354
					return true;
355
				},
356
			],
357
			'email_address' => [
358
				'type' => 'email',
359
				'label' => $txt['user_email_address'],
360
				'subtext' => $txt['valid_email'],
361
				'log_change' => true,
362
				'permission' => 'profile_identity',
363
				'input_validate' => function ($value) {
364
					global $context, $old_profile, $profile_vars, $modSettings;
365
366
					if (strtolower($value) === strtolower($old_profile['email_address']))
367
					{
368
						return false;
369
					}
370
371
					$isValid = self::profileValidateEmail($value, $context['id_member']);
0 ignored issues
show
Bug Best Practice introduced by
The method ElkArte\Profile\ProfileF...:profileValidateEmail() is not static, but was called statically. ( Ignorable by Annotation )

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

371
					/** @scrutinizer ignore-call */ 
372
     $isValid = self::profileValidateEmail($value, $context['id_member']);
Loading history...
372
373
					// Do they need to re-validate? If so schedule the function!
374
					if ($isValid === true && !empty($modSettings['send_validation_onChange']) && !allowedTo('moderate_forum'))
375
					{
376
						require_once(SUBSDIR . '/Auth.subs.php');
377
						$old_profile['validation_code'] = generateValidationCode(14);
378
						$profile_vars['validation_code'] = substr(hash('sha256', $old_profile['validation_code']), 0, 10);
379
						$profile_vars['is_activated'] = 2;
380
						$context['profile_execute_on_save'][] = 'profileSendActivation';
381
						unset($context['profile_execute_on_save']['reload_user']);
382
					}
383
384
					return $isValid;
385
				},
386
			],
387
			// Selecting group membership is a complicated one, so we treat it separate!
388
			'id_group' => [
389
				'type' => 'callback',
390
				'callback_func' => 'group_manage',
391
				'permission' => 'manage_membergroups',
392
				'preload' => 'profileLoadGroups',
393
				'log_change' => true,
394
				'input_validate' => 'profileSaveGroups',
395
			],
396
			'id_theme' => [
397
				'type' => 'callback',
398
				'callback_func' => 'theme_pick',
399
				'permission' => 'profile_extra',
400
				'enabled' => empty($settings['disable_user_variant']) || !empty($modSettings['theme_allow']) || allowedTo('admin_forum'),
401
				'preload' => static function () {
402
					global $context, $cur_profile, $txt;
403
404
					$db = database();
405
					$request = $db->query('', '
406
					SELECT value
407
					FROM {db_prefix}themes
408
					WHERE id_theme = {int:id_theme}
409
						AND variable = {string:variable}
410
					LIMIT 1', [
411
							'id_theme' => $cur_profile['id_theme'],
412
							'variable' => 'name',
413
						]
414
					);
415
					[$name] = $request->fetch_row();
416
					$request->free_result();
417
					$context['member']['theme'] = [
418
						'id' => $cur_profile['id_theme'],
419
						'name' => empty($cur_profile['id_theme']) ? $txt['theme_forum_default'] : $name
420
					];
421
422
					return true;
423
				},
424
				'input_validate' => static function (&$value) {
425
					$value = (int) $value;
426
					return true;
427
				},
428
			],
429
			'karma_good' => [
430
				'type' => 'callback',
431
				'callback_func' => 'karma_modify',
432
				'permission' => 'admin_forum',
433
				// Set karma_bad too!
434
				'input_validate' => static function (&$value) {
435
					global $profile_vars, $cur_profile;
436
437
					$value = (int) $value;
438
					if (isset($_POST['karma_bad']))
439
					{
440
						$profile_vars['karma_bad'] = $_POST['karma_bad'] !== '' ? (int) $_POST['karma_bad'] : 0;
441
						$cur_profile['karma_bad'] = $_POST['karma_bad'] !== '' ? (int) $_POST['karma_bad'] : 0;
442
					}
443
444
					return true;
445
				},
446
				'preload' => static function () {
447
					global $context, $cur_profile;
448
449
					$context['member']['karma'] = [
450
						'good' => (int) $cur_profile['karma_good'],
451
						'bad' => (int) $cur_profile['karma_bad']
452
					];
453
454
					return true;
455
				},
456
				'enabled' => !empty($modSettings['karmaMode']),
457
			],
458
			'lngfile' => [
459
				'type' => 'select',
460
				'options' => 'return $context[\'profile_languages\'];',
461
				'label' => $txt['preferred_language'],
462
				'permission' => 'profile_identity',
463
				'preload' => 'profileLoadLanguages',
464
				'enabled' => !empty($modSettings['userLanguage']),
465
				'value' => empty($cur_profile['lngfile']) ? $language : $cur_profile['lngfile'],
466
				'input_validate' => static function (&$value) {
467
					global $context, $cur_profile;
468
469
					// Load the languages.
470
					profileLoadLanguages();
471
					if (isset($context['profile_languages'][$value]))
472
					{
473
						if ($context['user']['is_owner'] && empty($context['password_auth_failed']))
474
						{
475
							$_SESSION['language'] = $value;
476
						}
477
478
						return true;
479
					}
480
481
					$value = $cur_profile['lngfile'];
482
483
					return false;
484
				},
485
			],
486
			// The username is not always editable - so adjust it as such.
487
			'member_name' => [
488
				'type' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? 'text' : 'label',
489
				'label' => $txt['username'],
490
				'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>]' : '',
491
				'log_change' => true,
492
				'permission' => 'profile_identity',
493
				'prehtml' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? '<div class="warningbox">' . $txt['username_warning'] . '</div>' : '',
494
				'input_validate' => static function (&$value) {
495
					global $context, $cur_profile;
496
497
					if (allowedTo('admin_forum'))
498
					{
499
						// We'll need this...
500
						require_once(SUBSDIR . '/Auth.subs.php');
501
502
						// Maybe they are trying to change their password as well?
503
						$resetPassword = true;
504
						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
introduced by
The condition validatePassword($_POST[...:info->email)) === null is always false.
Loading history...
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...
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...
505
						{
506
							$resetPassword = false;
507
						}
508
509
						// Do the reset... this will email them too.
510
						if ($resetPassword)
0 ignored issues
show
introduced by
The condition $resetPassword is always true.
Loading history...
511
						{
512
							resetPassword($context['id_member'], $value);
513
						}
514
						elseif ($value !== null)
515
						{
516
							$errors = ErrorContext::context('change_username', 0);
517
518
							validateUsername($context['id_member'], $value, 'change_username');
519
520
							// No errors we can proceed normally
521
							if (!$errors->hasErrors())
522
							{
523
								updateMemberData($context['id_member'], ['member_name' => $value]);
524
							}
525
							else
526
							{
527
								// If there are "important" errors, and you are not an admin: log the first error
528
								// Otherwise grab all of them and do not log anything
529
								$error_severity = $errors->hasErrors(1) && User::$info->is_admin === false ? 1 : null;
530
								foreach ($errors->prepareErrors($error_severity) as $error)
531
								{
532
									throw new Exception($error, $error_severity === null ? false : 'general');
533
								}
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'] = $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' => strpos($modSettings['signature_settings'], (string) 1) === 0,
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)
909
	{
910
		global $profile_fields, $profile_vars, $context, $old_profile, $post_errors, $cur_profile;
911
912
		if (!empty($hook))
913
		{
914
			call_integration_hook('integrate_' . $hook . '_profile_fields', [&$fields]);
915
		}
916
917
		// Load them up.
918
		$this->loadProfileFields();
919
920
		// This makes things easier...
921
		$old_profile = $cur_profile;
922
923
		// This allows variables to call activities when they save
924
		// - by default just to reload their settings
925
		$context['profile_execute_on_save'] = [];
926
		if ($context['user']['is_owner'])
927
		{
928
			$context['profile_execute_on_save']['reload_user'] = 'profileReloadUser';
929
		}
930
931
		// Assume we log nothing.
932
		$context['log_changes'] = [];
933
934
		// Cycle through the profile fields working out what to do!
935
		foreach ($fields as $key)
936
		{
937
			if (!isset($profile_fields[$key]))
938
			{
939
				continue;
940
			}
941
942
			$field = $profile_fields[$key];
943
944
			if (!isset($_POST[$key]) || !empty($field['is_dummy']) || (isset($_POST['preview_signature']) && $key === 'signature'))
945
			{
946
				continue;
947
			}
948
949
			// What gets updated?
950
			$db_key = $field['save_key'] ?? $key;
951
952
			// Right - we have something that is enabled, we can act upon and has a value
953
			// posted to it. Does it have a validation function?
954
			if (isset($field['input_validate']))
955
			{
956
				$is_valid = $field['input_validate']($_POST[$key]);
957
958
				// An error occurred - set it as such!
959
				if ($is_valid !== true)
960
				{
961
					// Is this an actual error?
962
					if ($is_valid !== false)
963
					{
964
						$post_errors[$key] = $is_valid;
965
						$profile_fields[$key]['is_error'] = $is_valid;
966
					}
967
968
					// Retain the old value.
969
					$cur_profile[$key] = $_POST[$key];
970
					continue;
971
				}
972
			}
973
974
			// Are we doing a cast?
975
			$field['cast_type'] = empty($field['cast_type']) ? $field['type'] : $field['cast_type'];
976
977
			// Finally, clean up certain types.
978
			if ($field['cast_type'] === 'int')
979
			{
980
				$_POST[$key] = (int) $_POST[$key];
981
			}
982
			elseif ($field['cast_type'] === 'float')
983
			{
984
				$_POST[$key] = (float) $_POST[$key];
985
			}
986
			elseif ($field['cast_type'] === 'check')
987
			{
988
				$_POST[$key] = empty($_POST[$key]) ? 0 : 1;
989
			}
990
991
			// If we got here we're doing OK.
992
			if ($field['type'] !== 'hidden' && (!isset($old_profile[$key]) || $_POST[$key] != $old_profile[$key]))
993
			{
994
				// Set the save variable.
995
				$profile_vars[$db_key] = $_POST[$key];
996
997
				// And update the user profile.
998
				$cur_profile[$key] = $_POST[$key];
999
1000
				// Are we logging it?
1001
				if (!empty($field['log_change']) && isset($old_profile[$key]))
1002
				{
1003
					$context['log_changes'][$key] = [
1004
						'previous' => $old_profile[$key],
1005
						'new' => $_POST[$key],
1006
					];
1007
				}
1008
			}
1009
1010
			// Logging group changes are a bit different...
1011
			if ($key === 'id_group' && $field['log_change'])
1012
			{
1013
				profileLoadGroups();
1014
1015
				// Any changes to primary group?
1016
				if ((int) $_POST['id_group'] !== (int) $old_profile['id_group'])
1017
				{
1018
					$context['log_changes']['id_group'] = [
1019
						'previous' => !empty($old_profile[$key]) && isset($context['member_groups'][$old_profile[$key]]) ? $context['member_groups'][$old_profile[$key]]['name'] : '',
1020
						'new' => !empty($_POST[$key]) && isset($context['member_groups'][$_POST[$key]]) ? $context['member_groups'][$_POST[$key]]['name'] : '',
1021
					];
1022
				}
1023
1024
				// Prepare additional groups for comparison.
1025
				$additional_groups = [
1026
					'previous' => empty($old_profile['additional_groups']) ? [] : explode(',', $old_profile['additional_groups']),
1027
					'new' => empty($_POST['additional_groups']) ? [] : array_diff($_POST['additional_groups'], [0]),
1028
				];
1029
1030
				sort($additional_groups['previous']);
1031
				sort($additional_groups['new']);
1032
1033
				// What about additional groups?
1034
				if ($additional_groups['previous'] !== $additional_groups['new'])
1035
				{
1036
					foreach ($additional_groups as $type => $groups)
1037
					{
1038
						foreach ($groups as $id => $group)
1039
						{
1040
							if (isset($context['member_groups'][$group]))
1041
							{
1042
								$additional_groups[$type][$id] = $context['member_groups'][$group]['name'];
1043
							}
1044
							else
1045
							{
1046
								unset($additional_groups[$type][$id]);
1047
							}
1048
						}
1049
1050
						$additional_groups[$type] = implode(', ', $additional_groups[$type]);
1051
					}
1052
1053
					$context['log_changes']['additional_groups'] = $additional_groups;
1054
				}
1055
			}
1056
		}
1057
1058
		// @todo Temporary
1059
		if ($context['user']['is_owner'])
1060
		{
1061
			$changeOther = allowedTo(['profile_extra_any', 'profile_extra_own']);
1062
		}
1063
		else
1064
		{
1065
			$changeOther = allowedTo('profile_extra_any');
1066
		}
1067
1068
		if ($changeOther && empty($post_errors))
1069
		{
1070
			makeThemeChanges($context['id_member'], isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
1071
			if (!empty($_REQUEST['sa']))
1072
			{
1073
				makeCustomFieldChanges($context['id_member'], $_REQUEST['sa'], false);
1074
			}
1075
		}
1076
1077
		// Free memory!
1078
		unset($profile_fields);
1079
	}
1080
1081
	/**
1082
	 * Validate an email address.
1083
	 *
1084
	 * @param string $email
1085
	 * @param int $memID = 0
1086
	 *
1087
	 * @return bool|string
1088
	 */
1089
	public function profileValidateEmail($email, $memID = 0)
1090
	{
1091
		// Check the name and email for validity.
1092
		$check = [];
1093
		$check['email'] = strtr($email, ['&#039;' => "'"]);
1094
		if (DataValidator::is_valid($check, ['email' => 'valid_email|required'], ['email' => 'trim']))
1095
		{
1096
			$email = $check['email'];
1097
		}
1098
		else
1099
		{
1100
			return empty($check['email']) ? 'no_email' : 'bad_email';
1101
		}
1102
1103
		// Email addresses should be and stay unique.
1104
		$num = isUniqueEmail($memID, $email);
1105
1106
		return ($num > 0) ? 'email_taken' : true;
1107
	}
1108
1109
	/**
1110
	 * Get the WHERE clause for retrieving profile fields.
1111
	 *
1112
	 * @param string $area
1113
	 * @param int $memID
1114
	 * @return string
1115
	 */
1116
	public function getProfileFieldWhereClause(string $area, int $memID): string
1117
	{
1118
		// Get the right restrictions in place...
1119
		$where = 'active = 1';
1120
		if ($area !== 'register' && !allowedTo('admin_forum'))
1121
		{
1122
			// If it's the owner they can see two types of private fields, regardless.
1123
			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...
1124
			{
1125
				$where .= $area === 'summary' ? ' AND private < 3' : ' AND (private = 0 OR private = 2)';
1126
			}
1127
			else
1128
			{
1129
				$where .= $area === 'summary' ? ' AND private < 2' : ' AND private = 0';
1130
			}
1131
		}
1132
1133
		if ($area === 'register')
1134
		{
1135
			$where .= ' AND show_reg != 0';
1136
		}
1137
		elseif ($area !== 'summary')
1138
		{
1139
			$where .= ' AND show_profile = {string:area}';
1140
		}
1141
1142
		return $where;
1143
	}
1144
1145
	/**
1146
	 * Do any post-processing to the output HTML for custom fields.
1147
	 *
1148
	 * @param array $row
1149
	 * @param string $output_html
1150
	 * @param string $key
1151
	 *
1152
	 * @return string
1153
	 */
1154
	public function postProcessOutputHtml($row, $output_html, $key)
1155
	{
1156
		global $scripturl, $settings;
1157
1158
		// Parse BBCode
1159
		if ($row['bbc'])
1160
		{
1161
			$bbc_parser = ParserWrapper::instance();
1162
1163
			$output_html = $bbc_parser->parseCustomFields($output_html);
1164
		}
1165
		// Allow for newlines at least
1166
		elseif ($row['field_type'] === 'textarea')
1167
		{
1168
			$output_html = strtr($output_html, ["\n" => '<br />']);
1169
		}
1170
1171
		// Enclosing the user input within some other text?
1172
		if (!empty($row['enclose']) && !empty($output_html))
1173
		{
1174
			$replacements = [
1175
				'{SCRIPTURL}' => $scripturl,
1176
				'{IMAGES_URL}' => $settings['images_url'],
1177
				'{DEFAULT_IMAGES_URL}' => $settings['default_images_url'],
1178
				'{INPUT}' => $output_html,
1179
			];
1180
1181
			if (in_array($row['field_type'], ['radio', 'select']))
1182
			{
1183
				$replacements['{KEY}'] = $row['col_name'] . '_' . ($key ?? 0);
1184
			}
1185
1186
			$output_html = strtr($row['enclose'], $replacements);
1187
		}
1188
1189
		return $output_html;
1190
	}
1191
}
1192