ProfileOptions::action_editNotificationSettings()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 15
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * This file has the primary job of showing and editing people's profiles.
5
 * It also allows the user to change some of their or another preferences,
6
 * and such things
7
 *
8
 * @package   ElkArte Forum
9
 * @copyright ElkArte Forum contributors
10
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
11
 *
12
 * This file contains code covered by:
13
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
14
 *
15
 * @version 2.0 dev
16
 *
17
 */
18
19
namespace ElkArte\Profile;
20
21
use ElkArte\AbstractController;
22
use ElkArte\Action;
23
use ElkArte\Cache\Cache;
24
use ElkArte\Exceptions\Exception;
25
use ElkArte\Languages\Txt;
26
use ElkArte\Member;
27
use ElkArte\MembersList;
28
29
/**
30
 * Options a user can set to customize their site experience
31
 *
32
 * - Does the job of showing and editing people's profiles.
33
 * - Interface to buddy list, ignore list, notifications, authentication options, forum profile
34
 * account settings, etc
35
 */
36
class ProfileOptions extends AbstractController
37
{
38
	/** @var int Member id for the profile being viewed */
39
	private $_memID = 0;
40
41
	/** @var Member The \ElkArte\Member object is stored here to avoid some global */
42
	private $_profile;
43
44
	/**
45
	 * Called before all other methods when coming from the dispatcher or
46
	 * action class.
47
	 *
48
	 * - If you initiate the class outside those methods, call this method.
49
	 * or setup the class yourself else a horrible fate awaits you
50
	 */
51
	public function pre_dispatch()
52
	{
53
		$this->_memID = currentMemberID();
54
		$this->_profile = MembersList::get($this->_memID);
0 ignored issues
show
Documentation Bug introduced by
It seems like ElkArte\MembersList::get($this->_memID) can also be of type anonymous//sources/ElkArte/MembersList.php$0. However, the property $_profile is declared as type ElkArte\Member. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
55
	}
56
57
	/**
58
	 * Default method, if another action is not called by the menu.
59
	 *
60
	 * @see AbstractController::action_index()
61
	 */
62
	public function action_index()
63
	{
64
		// action_account() is the first to do
65
		// these subactions are mostly routed to from the profile
66
		// menu though.
67
	}
68
69
	/**
70
	 * Show all the users buddies, as well as a add/delete interface.
71
	 *
72
	 * @throws Exception
73
	 */
74
	public function action_editBuddyIgnoreLists()
75
	{
76
		global $context, $txt, $modSettings;
77
78
		// Do a quick check to ensure people aren't getting here illegally!
79
		if (!$context['user']['is_owner'] || empty($modSettings['enable_buddylist']))
80
		{
81
			throw new Exception('no_access', false);
82
		}
83
84
		theme()->getTemplates()->load('ProfileOptions');
85
86
		// Can we email the user direct?
87
		$context['can_moderate_forum'] = allowedTo('moderate_forum');
88
		$context['can_send_email'] = allowedTo('send_email_to_members');
89
90
		$subActions = [
91
			'buddies' => [$this, 'action_editBuddies'],
92
			'ignore' => [$this, 'action_editIgnoreList'],
93
		];
94
95
		// Set a subaction
96
		$action = new Action('buddy_actions');
97
		$subAction = $action->initialize($subActions, 'buddies');
98
99
		// Create the tabs for the template.
100
		$context[$context['profile_menu_name']]['object']->prepareTabData([
101
			'title' => $txt['editBuddyIgnoreLists'],
102
			'description' => $txt['buddy_ignore_desc'],
103
			'class' => 'i-user',
104
		]);
105
106
		// Pass on to the actual function.
107
		$action->dispatch($subAction);
108
	}
109
110
	/**
111
	 * Show all the users buddies, as well as an add/delete interface.
112
	 *
113
	 * @uses template_editBuddies()
114
	 */
115
	public function action_editBuddies()
116
	{
117
		global $context;
118
119
		theme()->getTemplates()->load('ProfileOptions');
120
121
		// We want to view what we're doing :P
122
		$context['sub_template'] = 'editBuddies';
123
124
		// Use suggest finding the right buddies
125
		loadJavascriptFile('suggest.js', array('defer' => true));
126
127
		// For making changes!
128
		$buddiesArray = array_map('intval', explode(',', $this->_profile['buddy_list']));
0 ignored issues
show
Bug introduced by
It seems like $this->_profile['buddy_list'] can also be of type null; however, parameter $string of explode() 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

128
		$buddiesArray = array_map('intval', explode(',', /** @scrutinizer ignore-type */ $this->_profile['buddy_list']));
Loading history...
129
		$buddiesArray = array_filter($buddiesArray, static fn($value) => $value !== '');
130
131
		// Removing a buddy?
132
		$notMyBuddy = $this->_req->getQuery('remove', 'intval');
133
		if ($notMyBuddy !== null)
134
		{
135
			checkSession('get');
136
137
			call_integration_hook('integrate_remove_buddy', [$this->_memID]);
138
139
			$key = array_search($notMyBuddy, $buddiesArray, true);
140
			if ($key !== false)
141
			{
142
				unset($buddiesArray[$key]);
143
			}
144
145
			// Make the changes.
146
			$this->_profile['buddy_list'] = implode(',', $buddiesArray);
147
			require_once(SUBSDIR . '/Members.subs.php');
148
			updateMemberData($this->_memID, ['buddy_list' => $this->_profile['buddy_list']]);
149
150
			// Redirect off the page because we don't like all this ugly query stuff to stick in the history.
151
			redirectexit('action=profile;area=lists;sa=buddies;u=' . $this->_memID);
152
		}
153
		// Or adding a new one
154
		elseif (isset($this->_req->post->new_buddy))
155
		{
156
			checkSession();
157
158
			// Prepare the string for extraction...
159
			$new_buddy = strtr($this->_req->getPost('new_buddy', 'trim|htmlspecialchars[ENT_QUOTES]'), ['&quot;' => '"']);
160
			if ($new_buddy === '' || in_array($new_buddy, [$this->_profile['member_name'], $this->_profile['real_name']], true))
161
			{
162
				unset($new_buddy);
163
			}
164
165
			call_integration_hook('integrate_add_buddies', [$this->_memID, &$new_buddies]);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $new_buddies does not exist. Did you maybe mean $buddies?
Loading history...
166
167
			if (!empty($new_buddy))
168
			{
169
				// Now find out the id_member of the buddy.
170
				require_once(SUBSDIR . '/ProfileOptions.subs.php');
171
				$new_buddiesArray = getBuddiesID([$new_buddy]);
172
				$old_buddiesArray = explode(',', $this->_profile['buddy_list']);
173
174
				// Now update the current users buddy list.
175
				$this->_profile['buddy_list'] = implode(',', array_filter(array_unique(array_merge($new_buddiesArray, $old_buddiesArray))));
176
177
				require_once(SUBSDIR . '/Members.subs.php');
178
				updateMemberData($this->_memID, ['buddy_list' => $this->_profile['buddy_list']]);
179
			}
180
181
			// Back to the buddy list!
182
			redirectexit('action=profile;area=lists;sa=buddies;u=' . $this->_memID);
183
		}
184
185
		// Get all the users "buddies"...
186
		$buddies = [];
187
188
		if (!empty($buddiesArray))
189
		{
190
			require_once(SUBSDIR . '/Members.subs.php');
191
			$result = getBasicMemberData($buddiesArray, ['sort' => 'real_name', 'limit' => substr_count($this->_profile['buddy_list'], ',') + 1]);
0 ignored issues
show
Bug introduced by
It seems like $this->_profile['buddy_list'] can also be of type null; however, parameter $haystack of substr_count() 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

191
			$result = getBasicMemberData($buddiesArray, ['sort' => 'real_name', 'limit' => substr_count(/** @scrutinizer ignore-type */ $this->_profile['buddy_list'], ',') + 1]);
Loading history...
192
			foreach ($result as $row)
193
			{
194
				$buddies[] = (int) $row['id_member'];
195
			}
196
		}
197
198
		$context['buddy_count'] = count($buddies);
199
200
		// Load all the members up.
201
		MembersList::load($buddies, false, 'profile');
202
203
		// Set the context for each buddy.
204
		$context['buddies'] = [];
205
		foreach ($buddies as $buddy)
206
		{
207
			$context['buddies'][$buddy] = MembersList::get($buddy);
208
			$context['buddies'][$buddy]->loadContext();
209
		}
210
211
		call_integration_hook('integrate_view_buddies', [$this->_memID]);
212
	}
213
214
	/**
215
	 * Allows the user to view their ignore list,
216
	 *
217
	 * - Provides the option to manage members on it.
218
	 */
219
	public function action_editIgnoreList()
220
	{
221
		global $context;
222
223
		theme()->getTemplates()->load('ProfileOptions');
224
225
		// We want to view what we're doing :P
226
		$context['sub_template'] = 'editIgnoreList';
227
		loadJavascriptFile('suggest.js', array('defer' => true));
228
229
		// For making changes!
230
		$ignoreArray = array_map('intval', explode(',', $this->_profile['pm_ignore_list']));
0 ignored issues
show
Bug introduced by
It seems like $this->_profile['pm_ignore_list'] can also be of type null; however, parameter $string of explode() 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

230
		$ignoreArray = array_map('intval', explode(',', /** @scrutinizer ignore-type */ $this->_profile['pm_ignore_list']));
Loading history...
231
		$ignoreArray = array_filter($ignoreArray, static fn($value) => $value !== '');
232
233
		// Removing a member from the ignore list?
234
		$id_remove = $this->_req->getQuery('remove', 'intval');
235
		if (isset($id_remove))
236
		{
237
			checkSession('get');
238
239
			// Heh, I'm lazy, do it the easy way...
240
			$key = array_search($id_remove, $ignoreArray, true);
241
			if ($key !== false)
242
			{
243
				unset($ignoreArray[$key]);
244
			}
245
246
			// Make the changes.
247
			$this->_profile['pm_ignore_list'] = implode(',', $ignoreArray);
248
			require_once(SUBSDIR . '/Members.subs.php');
249
			updateMemberData($this->_memID, ['pm_ignore_list' => $this->_profile['pm_ignore_list']]);
250
251
			// Redirect off the page because we don't like all this ugly query stuff
252
			// to stick in the history.
253
			redirectexit('action=profile;area=lists;sa=ignore;u=' . $this->_memID);
254
		}
255
		elseif (isset($this->_req->post->new_ignore))
256
		{
257
			checkSession();
258
259
			// Prepare the string for extraction...
260
			$new_ignore = strtr($this->_req->getPost('new_ignore', 'trim|htmlspecialchars[ENT_QUOTES]'), ['&quot;' => '"']);
261
			if ($new_ignore === '' || in_array($new_ignore, [$this->_profile['member_name'], $this->_profile['real_name']], true))
262
			{
263
				unset($new_ignore);
264
			}
265
266
			if (!empty($new_ignore))
267
			{
268
				// Now find out the id_member for the members in question.
269
				require_once(SUBSDIR . '/ProfileOptions.subs.php');
270
				$ignoreArray = array_merge($ignoreArray, getBuddiesID([$new_ignore], false));
271
272
				// Now update the current users buddy list.
273
				$this->_profile['pm_ignore_list'] = implode(',', $ignoreArray);
274
				require_once(SUBSDIR . '/Members.subs.php');
275
				updateMemberData($this->_memID, ['pm_ignore_list' => $this->_profile['pm_ignore_list']]);
276
			}
277
278
			// Back to the list of pitiful people!
279
			redirectexit('action=profile;area=lists;sa=ignore;u=' . $this->_memID);
280
		}
281
282
		// Initialise the list of members we're ignoring.
283
		$ignored = [];
284
285
		if (!empty($ignoreArray))
286
		{
287
			require_once(SUBSDIR . '/Members.subs.php');
288
			$result = getBasicMemberData($ignoreArray, ['sort' => 'real_name', 'limit' => substr_count($this->_profile['pm_ignore_list'], ',') + 1]);
0 ignored issues
show
Bug introduced by
It seems like $this->_profile['pm_ignore_list'] can also be of type null; however, parameter $haystack of substr_count() 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

288
			$result = getBasicMemberData($ignoreArray, ['sort' => 'real_name', 'limit' => substr_count(/** @scrutinizer ignore-type */ $this->_profile['pm_ignore_list'], ',') + 1]);
Loading history...
289
			foreach ($result as $row)
290
			{
291
				$ignored[] = (int) $row['id_member'];
292
			}
293
		}
294
295
		$context['ignore_count'] = count($ignored);
296
297
		// Load all the members up.
298
		MembersList::load($ignored, false, 'profile');
299
300
		// Set the context for everyone we ignore.
301
		$context['ignore_list'] = [];
302
		foreach ($ignored as $ignore_member)
303
		{
304
			$context['ignore_list'][$ignore_member] = MembersList::get($ignore_member);
305
			$context['ignore_list'][$ignore_member]->loadContext();
306
		}
307
	}
308
309
	/**
310
	 * Allows the user to see or change their account info.
311
	 */
312
	public function action_account()
313
	{
314
		global $modSettings, $context, $txt;
315
316
		theme()->getTemplates()->load('ProfileOptions');
317
		$this->loadThemeOptions();
318
319
		if (allowedTo(['profile_identity_own', 'profile_identity_any']))
320
		{
321
			$profileFields = new ProfileFields();
322
			$profileFields->loadCustomFields($this->_memID, 'account');
323
		}
324
325
		$context['sub_template'] = 'edit_options';
326
		$context['page_desc'] = $txt['account_info'];
327
328
		if (!empty($modSettings['enableOTP']))
329
		{
330
			$fields = self::getFields('account_otp');
331
			setupProfileContext($fields['fields'], $fields['hook']);
332
333
			loadJavascriptFile('ext/qrcode.js');
334
			$context['load_google_authenticator'] = true;
335
		}
336
		else
337
		{
338
			$fields = self::getFields('account');
339
		}
340
341
		setupProfileContext($fields['fields'], $fields['hook']);
342
	}
343
344
	/**
345
	 * Load the options for a user.
346
	 */
347
	public function loadThemeOptions()
348
	{
349
		global $context, $cur_profile, $options;
350
351
		$default_options = $this->_req->getPost('default_options');
352
		$post_options = $this->_req->getPost('options');
353
		if (isset($default_options))
354
		{
355
			$post_options = isset($post_options) ? $post_options + $default_options : $default_options;
356
		}
357
358
		if ($context['user']['is_owner'])
359
		{
360
			$context['member']['options'] = $options + $this->_profile->options;
361
362
			if (isset($post_options) && is_array($post_options))
363
			{
364
				foreach ($post_options as $k => $v)
365
				{
366
					$context['member']['options'][$k] = $v;
367
				}
368
			}
369
		}
370
		else
371
		{
372
			require_once(SUBSDIR . '/Themes.subs.php');
373
			$context['member']['options'] = loadThemeOptionsInto(
374
				[1, (int) $cur_profile['id_theme']],
375
				[-1, $this->_memID], $context['member']['options']
376
			);
377
378
			if (isset($post_options))
379
			{
380
				foreach ($post_options as $var => $val)
381
				{
382
					$context['member']['options'][$var] = $val;
383
				}
384
			}
385
		}
386
	}
387
388
	/**
389
	 * Returns the profile fields for a given area
390
	 *
391
	 * @param string $area
392
	 * @return array
393
	 */
394
	public static function getFields($area)
395
	{
396
		global $modSettings;
397
398
		$fields = [
399
			'account' => [
400
				'fields' => [
401
					'member_name', 'real_name', 'date_registered', 'posts', 'lngfile', 'hr',
402
					'id_group', 'hr',
403
					'email_address', 'show_online', 'hr',
404
					'passwrd1', 'passwrd2', 'hr',
405
					'secret_question', 'secret_answer',
406
				],
407
				'hook' => 'account'
408
			],
409
			'account_otp' => [
410
				'fields' => [
411
					'member_name', 'real_name', 'date_registered', 'posts', 'lngfile', 'hr',
412
					'id_group', 'hr',
413
					'email_address', 'show_online', 'hr',
414
					'passwrd1', 'passwrd2', 'hr',
415
					'secret_question', 'secret_answer', 'hr',
416
					'enable_otp', 'otp_secret', 'hr'
417
				],
418
				'hook' => 'account'
419
			],
420
			'forumprofile' => [
421
				'fields' => [
422
					'avatar_choice', 'hr',
423
					'bday1', 'usertitle', 'hr',
424
					'signature', 'hr',
425
					'karma_good', 'hr',
426
					'website_title', 'website_url',
427
				],
428
				'hook' => 'forum'
429
			],
430
			'theme' => [
431
				'fields' => [
432
					'id_theme', 'smiley_set', 'hr',
433
					'time_format', 'time_offset', 'hr',
434
					'theme_settings',
435
				],
436
				'hook' => 'themepick'
437
			],
438
			'contactprefs' => [
439
				'fields' => [
440
					'receive_from',
441
					'hr',
442
					'pm_settings',
443
				],
444
				'hook' => 'pmprefs'
445
			],
446
			'registration' => [
447
				'fields' => empty($modSettings['registration_fields']) ? [] : explode(',', $modSettings['registration_fields']),
448
				'hook' => 'registration'
449
			]
450
		];
451
452
		return $fields[$area] ?? [];
453
	}
454
455
	/**
456
	 * Allow the user to change the forum options in their profile.
457
	 */
458
	public function action_forumProfile()
459
	{
460
		global $context, $txt;
461
462
		theme()->getTemplates()->load('ProfileOptions');
463
		$this->loadThemeOptions();
464
465
		if (allowedTo(['profile_extra_own', 'profile_extra_any']))
466
		{
467
			$profileFields = new ProfileFields();
468
			$profileFields->loadCustomFields($this->_memID, 'forumprofile');
469
		}
470
471
		$context['sub_template'] = 'edit_options';
472
		$context['page_desc'] = replaceBasicActionUrl($txt['forumProfile_info']);
473
		$context['show_preview_button'] = true;
474
475
		$fields = self::getFields('forumprofile');
476
		setupProfileContext($fields['fields'], $fields['hook']);
477
	}
478
479
	/**
480
	 * Allow the edit of *someone else's* personal message settings.
481
	 */
482
	public function action_pmprefs()
483
	{
484
		global $context, $txt;
485
486
		$this->loadThemeOptions();
487
		$profileFields = new ProfileFields();
488
		$profileFields->loadCustomFields($this->_memID, 'pmprefs');
489
		theme()->getTemplates()->load('ProfileOptions');
490
491
		$context['sub_template'] = 'edit_options';
492
		$context['page_desc'] = $txt['pm_settings_desc'];
493
494
		// Set up the profile context and call the 'integrate_pmprefs_profile_fields' hook
495
		$fields = self::getFields('contactprefs');
496
		setupProfileContext($fields['fields'], $fields['hook']);
497
	}
498
499
	/**
500
	 * Allow the user to pick a theme, Set time formats, and set
501
	 * overall look and layout options.
502
	 */
503
	public function action_themepick()
504
	{
505
		global $txt, $context;
506
507
		$this->loadThemeOptions();
508
509
		if (allowedTo(['profile_extra_own', 'profile_extra_any']))
510
		{
511
			$profileFields = new ProfileFields();
512
			$profileFields->loadCustomFields($this->_memID, 'theme');
513
		}
514
515
		theme()->getTemplates()->load('ProfileOptions');
516
517
		$context['sub_template'] = 'edit_options';
518
		$context['page_desc'] = $txt['theme_info'];
519
520
		// Set up profile look and layout, call 'integrate_themepick_profile_fields' hook
521
		$fields = self::getFields('theme');
522
		setupProfileContext($fields['fields'], $fields['hook']);
523
	}
524
525
	/**
526
	 * Choose a theme from a list of those that are available
527
	 *
528
	 * What it does:
529
	 *
530
	 * - Uses the Themes template. (pick sub template.)
531
	 * - Accessed with ?action=admin;area=theme;sa=pick.
532
	 * - Allows previewing of the theme and variants
533
	 */
534
	public function action_pick()
535
	{
536
		global $txt, $context, $modSettings, $scripturl;
537
538
		checkSession('get');
539
540
		// Basics
541
		Txt::load('ManageThemes');
542
		theme()->getTemplates()->load('ProfileOptions');
543
		require_once(SUBSDIR . '/Themes.subs.php');
544
545
		// Note JS values will be in post via the form, JS enabled they will be in get via link button
546
		$_SESSION['theme'] = 0;
547
		$_SESSION['id_variant'] = 0;
548
		$save = $this->_req->getPost('save');
549
		$u = $this->_req->getQuery('u', 'intval');
0 ignored issues
show
Unused Code introduced by
The assignment to $u is dead and can be removed.
Loading history...
550
		$themePicked = $this->_req->getQuery('th', 'intval');
551
		$variant = $this->_req->getQuery('vrt', 'cleanhtml');
552
553
		// Build the link tree
554
		$context['breadcrumbs'][] = [
555
			'url' => $scripturl . '?action=profile;sa=pick;u=' . $this->_memID,
556
			'name' => $txt['theme_pick'],
557
		];
558
		$context['default_theme_id'] = $modSettings['theme_default'];
559
560
		// Saving a theme/variant cause JS doesn't work - pretend it did ;)
561
		if (isset($save))
562
		{
563
			// Which theme?
564
			foreach ($save as $k => $v)
565
			{
566
				$themePicked = (int) $k;
567
			}
568
569
			if (isset($this->_req->post->vrt[$k]))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $k seems to be defined by a foreach iteration on line 564. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
570
			{
571
				$variant = $this->_req->post->vrt[$k];
572
			}
573
		}
574
575
		// Have we made a decision, or are we just previewing?
576
		if (isset($themePicked))
577
		{
578
			// Save the chosen theme.
579
			require_once(SUBSDIR . '/Members.subs.php');
580
			updateMemberData($this->_memID, ['id_theme' => $themePicked]);
581
582
			// Did they pick a variant as well?
583
			if (!empty($variant))
584
			{
585
				updateThemeOptions([$themePicked, $this->_memID, 'theme_variant', $variant]);
586
				Cache::instance()->remove('theme_settings-' . $themePicked . ':' . $this->_memID);
587
				$_SESSION['id_variant'] = 0;
588
			}
589
590
			redirectexit('action=profile;area=theme');
591
		}
592
593
		$context['current_member'] = $this->_memID;
594
		$current_theme = (int) $this->_profile['theme'];
595
596
		// Get all theme name and descriptions.
597
		[$context['available_themes'], $guest_theme] = availableThemes($current_theme, $context['current_member']);
598
599
		// As long as we're not doing the default theme...
600
		if ($guest_theme !== 0)
601
		{
602
			$context['available_themes'][0] = $context['available_themes'][$guest_theme];
603
		}
604
605
		$context['available_themes'][0]['id'] = 0;
606
		$context['available_themes'][0]['name'] = $txt['theme_forum_default'];
607
		$context['available_themes'][0]['selected'] = $current_theme === 0;
608
		$context['available_themes'][0]['description'] = $txt['theme_global_description'];
609
610
		ksort($context['available_themes']);
611
612
		$context['page_title'] = $txt['theme_pick'];
613
		$context['sub_template'] = 'pick';
614
	}
615
616
	/**
617
	 * Display the notification settings for the user and allow changes.
618
	 */
619
	public function action_notification()
620
	{
621
		global $txt, $context;
622
623
		theme()->getTemplates()->load('ProfileOptions');
624
625
		require_once(SUBSDIR . '/Profile.subs.php');
626
627
		$subActions = [
628
			'settings' => [$this, 'action_editNotificationSettings'],
629
			'boards' => [$this, 'action_editNotificationBoards'],
630
			'topics' => [$this, 'action_editNotificationTopics']
631
		];
632
633
		// Set a subaction
634
		$action = new Action('notification_actions');
635
		$subAction = $action->initialize($subActions, 'settings');
636
637
		// Create the header for the template.
638
		$context[$context['profile_menu_name']]['object']->prepareTabData([
639
			'title' => $txt['notify_settings'],
640
			'description' => $txt['notification_info'],
641
			'class' => 'i-contact',
642
		]);
643
644
		// Pass on to the actual function.
645
		$action->dispatch($subAction);
646
	}
647
648
	/**
649
	 * Generate the users existing notification options and allow for updates
650
	 */
651
	public function action_editNotificationSettings()
652
	{
653
		global $context;
654
655
		// Show the list of notification types and how they can subscribe to them
656
		$context['mention_types'] = getMemberNotificationsProfile($this->_memID);
657
658
		// What options are set?
659
		$context['member']['notify_announcements'] = $this->_profile['notify_announcements'];
660
		$context['member']['notify_send_body'] = $this->_profile['notify_send_body'];
661
		$context['member']['notify_types'] = $this->_profile['notify_types'];
662
		$context['member']['notify_regularity'] = $this->_profile['notify_regularity'];
663
		$context['member']['notify_from'] = $this->_profile['notify_from'];
664
665
		$this->loadThemeOptions();
666
	}
667
668
	/**
669
	 * Generate the users existing board notification list.
670
	 * Loads data into $context to be displayed wth template_board_notification_list
671
	 */
672
	public function action_editNotificationBoards()
673
	{
674
		global $txt, $scripturl, $context;
675
676
		require_once(SUBSDIR . '/Boards.subs.php');
677
678
		$context['mention_types'] = getMemberNotificationsProfile($this->_memID);
679
		unset($context['sub_template']);
680
681
		// Fine, start with the board list.
682
		$listOptions = [
683
			'id' => 'board_notification_list',
684
			'width' => '100%',
685
			'no_items_label' => $txt['notifications_boards_none'] . '<br /><br />' . $txt['notifications_boards_howto'],
686
			'no_items_align' => 'left',
687
			'base_href' => $scripturl . '?action=profile;u=' . $this->_memID . ';area=notification',
688
			'default_sort_col' => 'board_name',
689
			'get_items' => [
690
				'function' => fn($start, $items_per_page, $sort, $memID) => $this->list_getBoardNotifications($start, $items_per_page, $sort, $memID),
691
				'params' => [
692
					$this->_memID,
693
				],
694
			],
695
			'columns' => [
696
				'board_name' => [
697
					'header' => [
698
						'value' => $txt['notifications_boards'],
699
						'class' => 'lefttext',
700
					],
701
					'data' => [
702
						'function' => static function ($board) {
703
							global $txt;
704
705
							$link = $board['link'];
706
							if ($board['new'])
707
							{
708
								$link .= ' <a href="' . $board['href'] . '" class="new_posts">' . $txt['new'] . '</a>';
709
							}
710
711
							return $link;
712
						},
713
					],
714
					'sort' => [
715
						'default' => 'name',
716
						'reverse' => 'name DESC',
717
					],
718
				],
719
				'delete' => [
720
					'header' => [
721
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" />',
722
						'class' => 'centertext',
723
						'style' => 'width:4%;',
724
					],
725
					'data' => [
726
						'sprintf' => [
727
							'format' => '<input type="checkbox" name="notify_boards[]" value="%1$d" %2$s />',
728
							'params' => [
729
								'id' => false,
730
								'checked' => false,
731
							],
732
						],
733
						'class' => 'centertext',
734
					],
735
				],
736
			],
737
			'form' => [
738
				'href' => $scripturl . '?action=profile;area=notification;sa=boards',
739
				'include_sort' => true,
740
				'include_start' => true,
741
				'hidden_fields' => [
742
					'u' => $this->_memID,
743
					'sa' => $context['menu_item_selected'],
744
					$context['session_var'] => $context['session_id'],
745
				],
746
				'token' => $context['token_check'],
747
			],
748
			'additional_rows' => [
749
				[
750
					'class' => 'submitbutton',
751
					'position' => 'bottom_of_list',
752
					'value' => '
753
						<input type="submit" name="edit_notify_boards" value="' . $txt['notifications_boards_update'] . '" />
754
						<input type="hidden" name="save" value="save" />',
755
				],
756
				[
757
					'position' => 'after_title',
758
					'value' => getBoardNotificationsCount($this->_memID) == 0 ? $txt['notifications_boards_none'] . '<br />' . $txt['notifications_boards_howto'] : $txt['notifications_boards_current'],
759
				],
760
			],
761
		];
762
763
		// Create the board notification list.
764
		createList($listOptions);
765
	}
766
767
	/**
768
	 * Generate the users existing topic notification list.
769
	 * Loads data into $context to be displayed wth template_topic_notification_list
770
	 */
771
	public function action_editNotificationTopics()
772
	{
773
		global $txt, $scripturl, $context, $modSettings;
774
775
		require_once(SUBSDIR . '/Topic.subs.php');
776
777
		$context['mention_types'] = getMemberNotificationsProfile($this->_memID);
778
		unset($context['sub_template']);
779
780
		// Now do the topic notifications.
781
		$listOptions = [
782
			'id' => 'topic_notification_list',
783
			'width' => '100%',
784
			'items_per_page' => $modSettings['defaultMaxMessages'],
785
			'no_items_label' => $txt['notifications_topics_none'] . '<br /><br />' . $txt['notifications_topics_howto'],
786
			'no_items_align' => 'left',
787
			'base_href' => $scripturl . '?action=profile;u=' . $this->_memID . ';area=notification',
788
			'default_sort_col' => 'last_post',
789
			'get_items' => [
790
				'function' => fn($start, $items_per_page, $sort, $memID) => $this->list_getTopicNotifications($start, $items_per_page, $sort, $memID),
791
				'params' => [
792
					$this->_memID,
793
				],
794
			],
795
			'get_count' => [
796
				'function' => fn($memID) => $this->list_getTopicNotificationCount($memID),
797
				'params' => [
798
					$this->_memID,
799
				],
800
			],
801
			'columns' => [
802
				'subject' => [
803
					'header' => [
804
						'value' => $txt['notifications_topics'],
805
						'class' => 'lefttext',
806
					],
807
					'data' => [
808
						'function' => static function ($topic) {
809
							global $txt;
810
811
							$link = $topic['link'];
812
							if ($topic['new'])
813
							{
814
								$link .= ' <a href="' . $topic['new_href'] . '" class="new_posts">' . $txt['new'] . '</a>';
815
							}
816
817
							return $link . ('<br /><span class="smalltext"><em>' . $txt['in'] . ' ' . $topic['board_link'] . '</em></span>');
818
						},
819
					],
820
					'sort' => [
821
						'default' => 'ms.subject',
822
						'reverse' => 'ms.subject DESC',
823
					],
824
				],
825
				'started_by' => [
826
					'header' => [
827
						'value' => $txt['started_by'],
828
						'class' => 'lefttext',
829
					],
830
					'data' => [
831
						'db' => 'poster_link',
832
					],
833
					'sort' => [
834
						'default' => 'real_name_col',
835
						'reverse' => 'real_name_col DESC',
836
					],
837
				],
838
				'last_post' => [
839
					'header' => [
840
						'value' => $txt['last_post'],
841
						'class' => 'lefttext',
842
					],
843
					'data' => [
844
						'sprintf' => [
845
							'format' => '<span class="smalltext">%1$s<br />' . $txt['by'] . ' %2$s</span>',
846
							'params' => [
847
								'updated' => false,
848
								'poster_updated_link' => false,
849
							],
850
						],
851
					],
852
					'sort' => [
853
						'default' => 'ml.id_msg DESC',
854
						'reverse' => 'ml.id_msg',
855
					],
856
				],
857
				'delete' => [
858
					'header' => [
859
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" />',
860
						'class' => 'centertext',
861
						'style' => 'width:4%;',
862
					],
863
					'data' => [
864
						'sprintf' => [
865
							'format' => '<input type="checkbox" name="notify_topics[]" value="%1$d" />',
866
							'params' => [
867
								'id' => false,
868
							],
869
						],
870
						'class' => 'centertext',
871
					],
872
				],
873
			],
874
			'form' => [
875
				'href' => $scripturl . '?action=profile;area=notification',
876
				'include_sort' => true,
877
				'include_start' => true,
878
				'hidden_fields' => [
879
					'u' => $this->_memID,
880
					'sa' => $context['menu_item_selected'],
881
					$context['session_var'] => $context['session_id'],
882
				],
883
				'token' => $context['token_check'],
884
			],
885
			'additional_rows' => [
886
				[
887
					'class' => 'submitbutton',
888
					'position' => 'bottom_of_list',
889
					'value' => '
890
						<input type="submit" name="edit_notify_topics" value="' . $txt['notifications_update'] . '" />
891
						<input type="hidden" name="save" value="save" />',
892
				],
893
			],
894
		];
895
896
		// Create the topic notification list.
897
		createList($listOptions);
898
	}
899
900
	/**
901
	 * Callback for createList() in action_notification()
902
	 *
903
	 * @param int $start The item to start with (for pagination purposes)
904
	 * @param int $items_per_page The number of items to show per page
905
	 * @param string $sort A string indicating how to sort the results
906
	 * @param int $memID id_member
907
	 *
908
	 * @return array array of board notifications
909
	 * @uses template_ignoreboards()
910
	 */
911
	public function list_getBoardNotifications($start, $items_per_page, $sort, $memID)
912
	{
913
		// Return boards you see and their notification status for the list
914
		return boardNotifications($sort, $memID);
915
	}
916
917
	/**
918
	 * Callback for createList() in action_notification()
919
	 *
920
	 * @param int $start The item to start with (for pagination purposes)
921
	 * @param int $items_per_page The number of items to show per page
922
	 * @param string $sort A string indicating how to sort the results
923
	 * @param int $memID id_member
924
	 *
925
	 * @return array array of topic notifications
926
	 */
927
	public function list_getTopicNotifications($start, $items_per_page, $sort, $memID)
928
	{
929
		// Topic notifications, for the list
930
		return topicNotifications($start, $items_per_page, $sort, $memID);
931
	}
932
933
	/**
934
	 * Callback for createList() in action_notification()
935
	 *
936
	 * - Retrieve topic notifications count.
937
	 *
938
	 * @param int $memID id_member the id of the member who's notifications we are loading
939
	 * @return int
940
	 */
941
	public function list_getTopicNotificationCount($memID)
942
	{
943
		// Topic notifications count, for the list
944
		return topicNotificationCount($memID);
945
	}
946
947
	/**
948
	 * Allows the user to see the list of their ignored boards.
949
	 * (and un-ignore them)
950
	 */
951
	public function action_ignoreboards()
952
	{
953
		global $context, $modSettings, $cur_profile;
954
955
		// Have the admins enabled this option?
956
		if (empty($modSettings['allow_ignore_boards']))
957
		{
958
			throw new Exception('ignoreboards_disallowed', 'user');
959
		}
960
961
		theme()->getTemplates()->load('ProfileOptions');
962
963
		$context['sub_template'] = 'ignoreboards';
964
		require_once(SUBSDIR . '/Boards.subs.php');
965
		$context += getBoardList(['not_redirection' => true, 'ignore' => empty($cur_profile['ignore_boards']) ? [] : explode(',', $cur_profile['ignore_boards'])]);
966
967
		// Include a list of boards per category for easy toggling.
968
		foreach ($context['categories'] as $cat => &$category)
969
		{
970
			$context['boards_in_category'][$cat] = count($category['boards']);
971
			$category['child_ids'] = array_keys($category['boards']);
972
		}
973
974
		$this->loadThemeOptions();
975
	}
976
977
	/**
978
	 * Function to allow the user to choose group membership etc...
979
	 */
980
	public function action_groupMembership()
981
	{
982
		global $txt, $context;
983
984
		theme()->getTemplates()->load('ProfileOptions');
985
		$context['sub_template'] = 'groupMembership';
986
987
		$curMember = $this->_profile;
988
		$context['primary_group'] = (int) $curMember['id_group'];
989
		$msgName = $this->_req->getQuery('msg', 'trim');
990
991
		// Can they manage groups?
992
		$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
993
		$context['can_manage_protected'] = allowedTo('admin_forum');
994
		$context['can_edit_primary'] = $context['can_manage_protected'];
995
		$context['update_message'] = isset($msgName, $txt['group_membership_msg_' . $msgName]) ? $txt['group_membership_msg_' . $msgName] : '';
996
997
		// Get all the groups this user is a member of.
998
		$groups = array_map('intval', explode(',', $curMember['additional_groups']));
0 ignored issues
show
Bug introduced by
It seems like $curMember['additional_groups'] can also be of type null; however, parameter $string of explode() 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

998
		$groups = array_map('intval', explode(',', /** @scrutinizer ignore-type */ $curMember['additional_groups']));
Loading history...
999
		$groups[] = (int) $curMember['id_group'];
1000
1001
		// Ensure the query doesn't croak!
1002
		if (empty($groups))
1003
		{
1004
			$groups = [0];
1005
		}
1006
1007
		// Just to be sure...
1008
		$groups = array_map('intval', $groups);
1009
1010
		// Get all the membergroups they can join.
1011
		require_once(SUBSDIR . '/ProfileOptions.subs.php');
1012
		$context['groups'] = loadMembergroupsJoin($groups, $this->_memID);
1013
1014
		// Add registered members on the end.
1015
		$context['groups']['member'][0] = [
1016
			'id' => 0,
1017
			'name' => $txt['regular_members'],
1018
			'desc' => $txt['regular_members_desc'],
1019
			'type' => 0,
1020
			'is_primary' => $context['primary_group'] === 0,
1021
			'can_be_primary' => true,
1022
			'can_leave' => 0,
1023
		];
1024
1025
		// No changing primary one unless you have enough groups!
1026
		if (count($context['groups']['member']) < 2)
1027
		{
1028
			$context['can_edit_primary'] = false;
1029
		}
1030
1031
		// In the special case that someone is requesting membership of a group, setup some special context vars.
1032
		$groupRequest = $this->_req->getQuery('request', 'intval');
1033
		if (!isset($groupRequest, $context['groups']['available'][$groupRequest]))
1034
		{
1035
			return;
1036
		}
1037
1038
		if ($context['groups']['available'][$groupRequest]['type'] !== 2)
1039
		{
1040
			return;
1041
		}
1042
1043
		$context['group_request'] = $context['groups']['available'][$groupRequest];
1044
	}
1045
1046
	/**
1047
	 * This function actually makes all the group changes
1048
	 *
1049
	 * @return string
1050
	 * @throws Exception no_access
1051
	 */
1052
	public function action_groupMembership2()
1053
	{
1054
		global $context, $modSettings, $scripturl, $language;
1055
1056
		// Let's be extra cautious...
1057
		if (!$context['user']['is_owner'] || empty($modSettings['show_group_membership']))
1058
		{
1059
			isAllowedTo('manage_membergroups');
1060
		}
1061
1062
		$group_id = $this->_req->getPost('gid', 'intval', $this->_req->getQuery('gid', 'intval', null));
1063
1064
		if (!isset($group_id) && !isset($this->_req->post->primary))
1065
		{
1066
			throw new Exception('no_access', false);
1067
		}
1068
1069
		// GID may be from a link or a form
1070
		checkSession(isset($this->_req->query->gid) ? 'get' : 'post');
1071
1072
		require_once(SUBSDIR . '/Membergroups.subs.php');
1073
1074
		$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
1075
		$context['can_manage_protected'] = allowedTo('admin_forum');
1076
1077
		// By default, the new primary is the old one.
1078
		$newPrimary = (int) $this->_profile['id_group'];
1079
		$addGroups = array_flip(explode(',', $this->_profile['additional_groups']));
1080
		$canChangePrimary = (int) $this->_profile['id_group'] === 0;
1081
		$changeType = isset($this->_req->post->primary) ? 'primary' : (isset($this->_req->post->req) ? 'request' : 'free');
1082
1083
		// One way or another, we have a target group in mind...
1084
		$group_id = $group_id ?? (int) $this->_req->post->primary;
1085
		$foundTarget = $changeType === 'primary' && $group_id === 0;
1086
1087
		// Sanity check!!
1088
		if ($group_id === 1)
1089
		{
1090
			isAllowedTo('admin_forum');
1091
		}
1092
1093
		// What ever we are doing, we need to determine if changing primary is possible!
1094
		$groups_details = membergroupsById([$group_id, $this->_profile['id_group']], 0, true);
1095
1096
		// Protected groups require proper permissions!
1097
		if ($group_id !== 1 && $groups_details[$group_id]['group_type'] === 1)
1098
		{
1099
			isAllowedTo('admin_forum');
1100
		}
1101
1102
		foreach ($groups_details as $row)
1103
		{
1104
			// Is this the new group?
1105
			if ($row['id_group'] === $group_id)
1106
			{
1107
				$foundTarget = true;
1108
				$group_name = $row['group_name'];
1109
1110
				// Does the group type match what we're doing - are we trying to request a non-requestable group?
1111
				if ($changeType === 'request' && $row['group_type'] !== 2)
1112
				{
1113
					throw new Exception('no_access', false);
1114
				}
1115
1116
				// What about leaving a requestable group we are not a member of?
1117
				if ($changeType === 'free' && $row['group_type'] === 2 && $this->_profile['id_group'] !== $row['id_group'] && !isset($addGroups[$row['id_group']]))
1118
				{
1119
					throw new Exception('no_access', false);
1120
				}
1121
1122
				if ($changeType === 'free' && $row['group_type'] !== 3 && $row['group_type'] !== 2)
1123
				{
1124
					throw new Exception('no_access', false);
1125
				}
1126
1127
				// We can't change the primary group if this is hidden!
1128
				if ((int) $row['hidden'] === 2)
1129
				{
1130
					$canChangePrimary = false;
1131
				}
1132
			}
1133
1134
			// If this is their old primary, can we change it?
1135
			if ($row['id_group'] === $this->_profile['id_group'] && ($row['group_type'] > 1 || $context['can_manage_membergroups']) && $canChangePrimary)
1136
			{
1137
				$canChangePrimary = true;
1138
			}
1139
1140
			// If we are not doing a force primary move, don't do it automatically if current primary is not 0.
1141
			if ($changeType !== 'primary' && $this->_profile['id_group'] !== 0)
1142
			{
1143
				$canChangePrimary = false;
1144
			}
1145
1146
			// If this is the one we are acting on, can we even act?
1147
			if ((!$context['can_manage_protected'] && $row['group_type'] === 1) || (!$context['can_manage_membergroups'] && $row['group_type'] === 0))
1148
			{
1149
				$canChangePrimary = false;
1150
			}
1151
		}
1152
1153
		// Didn't find the target?
1154
		if (!$foundTarget)
1155
		{
1156
			throw new Exception('no_access', false);
1157
		}
1158
1159
		// Final security check, don't allow users to promote themselves to admin.
1160
		require_once(SUBSDIR . '/ProfileOptions.subs.php');
1161
		if ($context['can_manage_membergroups'] && !allowedTo('admin_forum'))
1162
		{
1163
			$disallow = checkMembergroupChange($group_id);
1164
			if ($disallow)
1165
			{
1166
				isAllowedTo('admin_forum');
1167
			}
1168
		}
1169
1170
		// If we're requesting, add the note then return.
1171
		if ($changeType === 'request')
1172
		{
1173
			if (logMembergroupRequest($group_id, $this->_memID))
1174
			{
1175
				throw new Exception('profile_error_already_requested_group');
1176
			}
1177
1178
			// Email all group moderators etc.
1179
			require_once(SUBSDIR . '/Mail.subs.php');
1180
1181
			// Do we have any group moderators?
1182
			require_once(SUBSDIR . '/Membergroups.subs.php');
1183
			$moderators = array_keys(getGroupModerators($group_id));
1184
1185
			// Otherwise, this is the backup!
1186
			if (empty($moderators))
1187
			{
1188
				require_once(SUBSDIR . '/Members.subs.php');
1189
				$moderators = membersAllowedTo('manage_membergroups');
1190
			}
1191
1192
			if (!empty($moderators))
1193
			{
1194
				require_once(SUBSDIR . '/Members.subs.php');
1195
				$members = getBasicMemberData($moderators, ['preferences' => true, 'sort' => 'lngfile']);
1196
1197
				foreach ($members as $member)
1198
				{
1199
					if ((int) $member['notify_types'] !== 4)
1200
					{
1201
						continue;
1202
					}
1203
1204
					// Check whether they are interested.
1205
					if (!empty($member['mod_prefs']))
1206
					{
1207
						[, , $pref_binary] = explode('|', $member['mod_prefs']);
1208
						if (!($pref_binary & 4))
1209
						{
1210
							continue;
1211
						}
1212
					}
1213
1214
					$replacements = [
1215
						'RECPNAME' => $member['member_name'],
1216
						'APPYNAME' => $this->_profile['member_name'],
1217
						'GROUPNAME' => $group_name,
1218
						'REASON' => $this->_req->post->reason,
1219
						'MODLINK' => $scripturl . '?action=moderate;area=groups;sa=requests',
1220
					];
1221
1222
					$emaildata = loadEmailTemplate('request_membership', $replacements, empty($member['lngfile']) || empty($modSettings['userLanguage']) ? $language : $member['lngfile']);
1223
					sendmail($member['email_address'], $emaildata['subject'], $emaildata['body'], null, null, false, 2);
1224
				}
1225
			}
1226
1227
			return $changeType;
1228
		}
1229
1230
		// Otherwise, we are leaving/joining a group.
1231
		if ($changeType === 'free')
1232
		{
1233
			// Are we leaving?
1234
			if ($this->_profile['id_group'] === $group_id || isset($addGroups[$group_id]))
1235
			{
1236
				if ($this->_profile['id_group'] === $group_id)
1237
				{
1238
					$newPrimary = 0;
1239
				}
1240
				else
1241
				{
1242
					unset($addGroups[$group_id]);
1243
				}
1244
			}
1245
			// ... if not, must be joining.
1246
			elseif ($canChangePrimary)
1247
			{
1248
				// Can we change the primary, and do we want to?
1249
				if ($this->_profile['id_group'] !== 0)
1250
				{
1251
					$addGroups[$this->_profile['id_group']] = -1;
1252
				}
1253
1254
				$newPrimary = $group_id;
1255
			}
1256
			// Otherwise it's an additional group...
1257
			else
1258
			{
1259
				$addGroups[$group_id] = -1;
1260
			}
1261
		}
1262
		// Finally, we must be setting the primary.
1263
		elseif ($canChangePrimary)
1264
		{
1265
			if ($this->_profile['id_group'] !== 0)
1266
			{
1267
				$addGroups[$this->_profile['id_group']] = -1;
1268
			}
1269
1270
			if (isset($addGroups[$group_id]))
1271
			{
1272
				unset($addGroups[$group_id]);
1273
			}
1274
1275
			$newPrimary = $group_id;
1276
		}
1277
1278
		// Finally, we can make the changes!
1279
		foreach (array_keys($addGroups) as $id)
1280
		{
1281
			if (empty($id))
1282
			{
1283
				unset($addGroups[$id]);
1284
			}
1285
		}
1286
1287
		$addGroups = implode(',', array_flip($addGroups));
1288
1289
		// Ensure that we don't cache permissions if the group is changing.
1290
		if ($context['user']['is_owner'])
1291
		{
1292
			$_SESSION['mc']['time'] = 0;
1293
		}
1294
		else
1295
		{
1296
			updateSettings(['settings_updated' => time()]);
1297
		}
1298
1299
		require_once(SUBSDIR . '/Members.subs.php');
1300
		updateMemberData($this->_memID, ['id_group' => $newPrimary, 'additional_groups' => $addGroups]);
1301
1302
		return $changeType;
1303
	}
1304
}
1305