ManageFeatures::layoutSettings_search()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Manage features and options administration page.
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
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 Beta 1
14
 *
15
 */
16
17
namespace ElkArte\AdminController;
18
19
use BBC\ParserWrapper;
20
use DateTimeZone;
21
use ElkArte\AbstractController;
22
use ElkArte\Action;
23
use ElkArte\Exceptions\Exception;
24
use ElkArte\Helper\DataValidator;
25
use ElkArte\Helper\Util;
26
use ElkArte\Hooks;
27
use ElkArte\Languages\Txt;
28
use ElkArte\Mentions\MentionType\AbstractNotificationMessage;
29
use ElkArte\MetadataIntegrate;
30
use ElkArte\Notifications\Notifications;
31
use ElkArte\SettingsForm\SettingsForm;
32
33
/**
34
 * Manage features and options administration page.
35
 *
36
 * This controller handles the pages which allow the admin
37
 * to see and change the basic feature settings of their site.
38 2
 */
39
class ManageFeatures extends AbstractController
40
{
41 2
	/**
42 2
	 * Pre-dispatch, called before other methods.
43
	 */
44
	public function pre_dispatch()
45
	{
46
		// We need this in a few places, so it's easier to have it loaded here
47
		require_once(SUBSDIR . '/ManageFeatures.subs.php');
48
	}
49
50
	/**
51
	 * This function passes control through to the relevant tab.
52
	 *
53
	 * @event integrate_sa_modify_features Use to add new Configuration tabs
54
	 * @see  AbstractController::action_index()
55
	 * @uses Help, ManageSettings languages
56
	 * @uses sub_template show_settings
57
	 */
58
	public function action_index()
59
	{
60
		global $context, $txt, $settings;
61
62
		// Often Helpful
63
		Txt::load('Help+ManageSettings+Mentions');
64
65
		// All the actions we know about.  These must exist in loadMenu() of the admin controller.
66
		$subActions = [
67
			'basic' => [
68
				'controller' => $this,
69
				'function' => 'action_basicSettings_display',
70
				'permission' => 'admin_forum'
71
			],
72
			'layout' => [
73
				'controller' => $this,
74
				'function' => 'action_layoutSettings_display',
75
				'permission' => 'admin_forum'
76
			],
77
			'pwa' => [
78
				'controller' => $this,
79
				'function' => 'action_pwaSettings_display',
80
				'enabled' => true,
81
				'permission' => 'admin_forum'
82
			],
83
			'karma' => [
84
				'controller' => $this,
85
				'function' => 'action_karmaSettings_display',
86
				'enabled' => featureEnabled('k'),
87
				'permission' => 'admin_forum'
88
			],
89
			'pmsettings' => [
90
				'controller' => $this,
91
				'function' => 'action_pmsettings',
92
				'permission' => 'admin_forum'
93
			],
94
			'likes' => [
95
				'controller' => $this,
96
				'function' => 'action_likesSettings_display',
97
				'enabled' => featureEnabled('l'),
98
				'permission' => 'admin_forum'
99
			],
100
			'mention' => [
101
				'controller' => $this,
102
				'function' => 'action_notificationsSettings_display',
103
				'permission' => 'admin_forum'
104
			],
105
			'sig' => [
106
				'controller' => $this,
107
				'function' => 'action_signatureSettings_display',
108
				'permission' => 'admin_forum'
109
			],
110
			'profile' => [
111
				'controller' => $this,
112
				'function' => 'action_profile',
113
				'enabled' => featureEnabled('cp'),
114
				'permission' => 'admin_forum'
115
			],
116
			'profileedit' => [
117
				'controller' => $this,
118
				'function' => 'action_profileedit',
119
				'permission' => 'admin_forum'
120
			],
121
		];
122
123
		// Set up the action control
124
		$action = new Action('modify_features');
125
126
		// By default, do the basic settings, call integrate_sa_modify_features
127
		$subAction = $action->initialize($subActions, 'basic');
128
129
		// Some final pieces for the template
130
		$context['sub_template'] = 'show_settings';
131
		$context['sub_action'] = $subAction;
132
		$context['page_title'] = $txt['modSettings_title'];
133
134
		// Load up all the tabs...
135
		$context[$context['admin_menu_name']]['object']->prepareTabData([
136
			'title' => 'modSettings_title',
137
			'help' => 'featuresettings',
138
			'description' => sprintf($txt['modSettings_desc'], getUrl('admin', ['action' => 'admin', 'area' => 'theme', 'sa' => 'list', 'th' => $settings['theme_id'], '{session_data}'])),
139
			// All valid $subActions will be added, here you just specify any special tab data
140
			'tabs' => [
141
				'mention' => [
142
					'description' => $txt['mentions_settings_desc'],
143
				],
144
				'sig' => [
145
					'description' => $txt['signature_settings_desc'],
146
				],
147
				'profile' => [
148
					'description' => $txt['custom_profile_desc'],
149
				],
150
				'pwa' => [
151
					'description' => $txt['pwa_settings_desc'],
152
				],
153
			],
154
		]);
155
156
		// Call the right function for this sub-action.
157
		$action->dispatch($subAction);
158
	}
159
160
	/**
161
	 * Config array for changing the basic forum settings
162
	 *
163
	 * - Accessed from ?action=admin;area=featuresettings;sa=basic;
164
	 *
165
	 * @event integrate_save_basic_settings
166
	 */
167
	public function action_basicSettings_display(): void
168
	{
169
		global $txt, $context, $modSettings;
170
171
		// Initialize the form
172
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
173
174
		// Initialize it with our settings
175
		$settingsForm->setConfigVars($this->_basicSettings());
176
177
		theme()->addJavascriptVar(['txt_invalid_response' => $txt['ajax_bad_response']], true);
178
179
		// Saving?
180
		if ($this->_req->hasQuery('save'))
181
		{
182
			checkSession();
183
184
			// Prevent absurd boundaries here - make it a day tops.
185
			if (isset($this->_req->post->lastActive))
186
			{
187
				$this->_req->post->lastActive = min((int) $this->_req->post->lastActive, 1440);
188
			}
189
190
			call_integration_hook('integrate_save_basic_settings');
191
192
			// Microdata needs to enable its integration
193
			if ($this->_req->isSet('metadata_enabled'))
194
			{
195
				Hooks::instance()->enableIntegration(MetadataIntegrate::class);
196
			}
197
			else
198
			{
199
				Hooks::instance()->disableIntegration(MetadataIntegrate::class);
200
			}
201
202
			// If they have changed Hive settings, let's clear them to avoid issues
203
			if (empty($modSettings['minify_css_js']) !== empty($this->_req->post->minify_css_js))
204
			{
205
				theme()->cleanHives();
206
			}
207
208
			$settingsForm->setConfigValues((array) $this->_req->post);
209
			$settingsForm->save();
210
211
			writeLog();
212
			redirectexit('action=admin;area=featuresettings;sa=basic');
213
		}
214
215
		if (isset($this->_req->post->cleanhives) && $this->getApi() === 'json')
216
		{
217
			$clean_hives_result = theme()->cleanHives();
218 2
219
			setJsonTemplate();
220 2
			$context['json_data'] = [
221
				'success' => $clean_hives_result,
222
				'response' => $clean_hives_result ? $txt['clean_hives_success'] : $txt['clean_hives_failed']
223
			];
224 2
225
			return;
226
		}
227
228
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'basic', 'save']);
229 2
		$context['settings_title'] = $txt['mods_cat_features'];
230
231 2
		$settingsForm->prepare();
232
	}
233 2
234
	/**
235 2
	 * Return basic feature settings.
236 2
	 *
237 2
	 * @event integrate_modify_basic_settings Adds to General features and Options
238
	 */
239
	private function _basicSettings()
240 2
	{
241
		global $txt;
242 2
243
		$config_vars = [
244
			// Basic stuff, titles, permissions...
245 2
			['check', 'allow_guestAccess'],
246 2
			['check', 'enable_buddylist'],
247
			['check', 'allow_editDisplayName'],
248
			['check', 'allow_hideOnline'],
249
			['check', 'titlesEnable'],
250 2
			'',
251
			// JavaScript and CSS options
252
			['select', 'jquery_source', ['auto' => $txt['jquery_auto'], 'local' => $txt['jquery_local'], 'cdn' => $txt['jquery_cdn']]],
253
			['check', 'minify_css_js', 'postinput' => '<a href="#" id="clean_hives" class="linkbutton">' . $txt['clean_hives'] . '</a>'],
254 2
			'',
255
			// Number formatting, timezones.
256
			['text', 'time_format'],
257
			['float', 'time_offset', 'subtext' => $txt['setting_time_offset_note'], 6, 'postinput' => $txt['hours']],
258 2
			'default_timezone' => ['select', 'default_timezone', []],
259 2
			'',
260
			// Who's online?
261
			['check', 'who_enabled'],
262
			['int', 'lastActive', 6, 'postinput' => $txt['minutes']],
263
			'',
264
			// Statistics.
265
			['check', 'trackStats'],
266 2
			['check', 'hitStats'],
267
			'',
268 2
			// Option-ish things... miscellaneous sorta.
269
			['check', 'metadata_enabled'],
270
			['check', 'allow_disableAnnounce'],
271
			['check', 'disallow_sendBody'],
272 2
			['select', 'enable_contactform', ['disabled' => $txt['contact_form_disabled'], 'registration' => $txt['contact_form_registration'], 'menu' => $txt['contact_form_menu']]],
273
		];
274 2
275
		// Get all the time zones.
276
		$all_zones = DateTimeZone::listIdentifiers();
277
		if (empty($all_zones))
278
		{
279
			unset($config_vars['default_timezone']);
280
		}
281
		else
282
		{
283
			// Make sure we set the value to the same as the printed value.
284
			foreach ($all_zones as $zone)
285
			{
286
				$config_vars['default_timezone'][2][$zone] = $zone;
287
			}
288
		}
289
290
		theme()->addInlineJavascript('
291
			document.getElementById("clean_hives").addEventListener("click", function(event) {return cleanHives(event);});', ['defer' => true]);
0 ignored issues
show
Bug introduced by
array('defer' => true) of type array<string,true> is incompatible with the type boolean expected by parameter $defer of ElkArte\Themes\Theme::addInlineJavascript(). ( Ignorable by Annotation )

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

291
			document.getElementById("clean_hives").addEventListener("click", function(event) {return cleanHives(event);});', /** @scrutinizer ignore-type */ ['defer' => true]);
Loading history...
292
293
		call_integration_hook('integrate_modify_basic_settings', [&$config_vars]);
294
295
		return $config_vars;
296
	}
297
298
	/**
299
	 * Allows modifying the global layout settings in the forum
300
	 *
301
	 * - Accessed through ?action=admin;area=featuresettings;sa=layout;
302
	 *
303
	 * @event integrate_save_layout_settings
304
	 */
305
	public function action_layoutSettings_display(): void
306
	{
307
		global $txt, $context, $modSettings;
308
309
		// Initialize the form
310
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
311
312
		// Initialize it with our settings
313
		$settingsForm->setConfigVars($this->_layoutSettings());
314
315
		// Saving?
316
		if ($this->_req->hasQuery('save'))
317
		{
318
			// Setting a custom frontpage, set the hook to the FrontpageInterface of the controller
319
			if (!empty($this->_req->post->front_page))
320
			{
321
				// Addons may have left this blank
322
				$modSettings['front_page'] = empty($modSettings['front_page']) ? 'MessageIndex_Controller' : $modSettings['front_page'];
323
324
				$front_page = (string) $this->_req->post->front_page;
325
				if (
326
					class_exists($modSettings['front_page'])
327
					&& in_array('validateFrontPageOptions', get_class_methods($modSettings['front_page']))
328
					&& !$front_page::validateFrontPageOptions($this->_req->post)
329
				)
330 2
				{
331
					$this->_req->post->front_page = '';
332 2
				}
333
			}
334 2
335 2
			checkSession();
336
337
			call_integration_hook('integrate_save_layout_settings');
338 2
339
			$settingsForm->setConfigValues((array) $this->_req->post);
340
			$settingsForm->save();
341 2
			writeLog();
342
343 2
			redirectexit('action=admin;area=featuresettings;sa=layout');
344
		}
345
346 2
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'layout', 'save']);
347
		$context['settings_title'] = $txt['mods_cat_layout'];
348 2
349 2
		$settingsForm->prepare();
350 2
	}
351
352
	/**
353
	 * Return layout settings.
354
	 *
355 2
	 * @event integrate_modify_layout_settings Adds options to Configuration->Layout
356
	 */
357 2
	private function _layoutSettings()
358
	{
359
		global $txt;
360
361
		$config_vars = array_merge(getFrontPageControllers(), [
362
			'',
363
			// Pagination stuff.
364
			['check', 'compactTopicPagesEnable'],
365
			['int', 'compactTopicPagesContiguous', 'subtext' => str_replace(' ', '&nbsp;', '"3" ' . $txt['to_display'] . ': <strong>1 ... 4 [5] 6 ... 9</strong>') . '<br />' . str_replace(' ', '&nbsp;', '"5" ' . $txt['to_display'] . ': <strong>1 ... 3 4 [5] 6 7 ... 9</strong>')],
366
			['int', 'defaultMaxMembers'],
367
			['check', 'displayMemberNames'],
368
			'',
369
			// Stuff that just is everywhere - today, search, online, etc.
370
			['select', 'todayMod', [$txt['today_disabled'], $txt['today_only'], $txt['yesterday_today'], $txt['relative_time']]],
371
			['check', 'onlineEnable'],
372
			['check', 'enableVBStyleLogin'],
373
			'',
374
			// Automagic image resizing.
375
			['int', 'max_image_width', 'subtext' => $txt['zero_for_no_limit']],
376
			['int', 'max_image_height', 'subtext' => $txt['zero_for_no_limit']],
377
			'',
378
			// This is like debugging sorta.
379
			['check', 'timeLoadPageEnable'],
380
		]);
381
382
		call_integration_hook('integrate_modify_layout_settings', [&$config_vars]);
383
384
		return $config_vars;
385
	}
386
387
	/**
388
	 * Display configuration settings page for progressive web application settings.
389
	 *
390
	 * - Accessed from ?action=admin;area=featuresettings;sa=pwa;
391
	 *
392
	 * @event integrate_save_pwa_settings
393
	 */
394
	public function action_pwaSettings_display(): void
395
	{
396
		global $txt, $context;
397
398
		// Initialize the form
399
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
400 2
401
		// Initialize it with our settings
402 2
		$settingsForm->setConfigVars($this->_pwaSettings());
403
404
		// Saving, lots of checks then
405
		if ($this->_req->hasQuery('save'))
406 2
		{
407 2
			checkSession();
408
409 2
			call_integration_hook('integrate_save_pwa_settings');
410 2
411
			// Don't allow it to be enabled if we don't have SSL
412
			$canUse = detectServer()->supportsSSL();
413 2
			if (!$canUse)
414
			{
415
				$this->_req->post->pwa_enabled = 0;
416
			}
417
418
			// And you must enable this if PWA is enabled
419
			if ($this->_req->getPost('pwa_enabled', 'intval') === 1)
420 2
			{
421
				$this->_req->post->pwa_manifest_enabled = 1;
422 2
			}
423
424
			$validator = new DataValidator();
425
			$validation_rules = [
426
				'pwa_theme_color' => 'valid_color',
427
				'pwa_background_color' => 'valid_color',
428
				'pwa_short_name' => 'max_length[12]'
429
			];
430
431
			// Only check the rest if they entered something.
432
			$valid_urls = ['pwa_small_icon', 'pwa_large_icon', 'favicon_icon', 'apple_touch_icon'];
433
			foreach ($valid_urls as $url)
434
			{
435
				if ($this->_req->getPost($url, 'trim') !== '')
436
				{
437
					$validation_rules[$url] = 'valid_url';
438
				}
439
			}
440
			$validator->validation_rules($validation_rules);
441
442
			if (!$validator->validate($this->_req->post))
443
			{
444
				// Some input error, let's tell them what is wrong
445
				$context['error_type'] = 'minor';
446
				$context['settings_message'] = [];
447
				foreach ($validator->validation_errors() as $error)
448
				{
449
					$context['settings_message'][] = $error;
450
				}
451
			}
452
			else
453
			{
454
				$settingsForm->setConfigValues((array) $this->_req->post);
455
				$settingsForm->save();
456
				redirectexit('action=admin;area=featuresettings;sa=pwa');
457
			}
458
		}
459
460
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'pwa', 'save']);
461
		$context['settings_title'] = $txt['pwa_settings'];
462
		theme()->addInlineJavascript('
463
			pwaPreview("pwa_small_icon");
464
			pwaPreview("pwa_large_icon");
465 2
			pwaPreview("favicon_icon");
466
			pwaPreview("apple_touch_icon");', true);
467 2
468
		$settingsForm->prepare();
469
	}
470
471 2
	/**
472 2
	 * Return PWA settings.
473
	 *
474 2
	 * @event integrate_modify_karma_settings Adds to Configuration->Pwa
475 2
	 */
476
	private function _pwaSettings()
477
	{
478
		global $txt;
479 2
480
		// PWA requires SSL
481
		$canUse = detectServer()->supportsSSL();
482
483 2
		$config_vars = [
484
			// PWA - On or off?
485 2
			['check', 'pwa_enabled', 'disabled' => !$canUse, 'invalid' => !$canUse, 'postinput' => !$canUse ? $txt['pwa_disabled'] : ''],
486
			'',
487
			['check', 'pwa_manifest_enabled', 'helptext' => $txt['pwa_manifest_enabled_desc']],
488
			['text', 'pwa_short_name', 12, 'mask' => 'nohtml', 'helptext' => $txt['pwa_short_name_desc'], 'maxlength' => 12],
489
			['color', 'pwa_theme_color', 'helptext' => $txt['pwa_theme_color_desc']],
490
			['color', 'pwa_background_color', 'helptext' => $txt['pwa_background_color_desc']],
491
			'',
492
			['url', 'pwa_small_icon', 'size' => 40, 'helptext' => $txt['pwa_small_icon_desc'], 'onchange' => "pwaPreview('pwa_small_icon');"],
493
			['url', 'pwa_large_icon', 'size' => 40, 'helptext' => $txt['pwa_large_icon_desc'], 'onchange' => "pwaPreview('pwa_large_icon');"],
494
			['title', 'other_icons_title'],
495
			['url', 'favicon_icon', 'size' => 40, 'helptext' => $txt['favicon_icon_desc'], 'onchange' => "pwaPreview('favicon_icon');"],
496
			['url', 'apple_touch_icon', 'size' => 40, 'helptext' => $txt['apple_touch_icon_desc'], 'onchange' => "pwaPreview('apple_touch_icon');"],
497
		];
498
499
		call_integration_hook('integrate_modify_pwa_settings', [&$config_vars]);
500
501
		return $config_vars;
502
	}
503
504
	/**
505
	 * Display configuration settings page for karma settings.
506
	 *
507
	 * - Accessed from ?action=admin;area=featuresettings;sa=karma;
508
	 *
509
	 * @event integrate_save_karma_settings
510
	 */
511
	public function action_karmaSettings_display(): void
512
	{
513
		global $txt, $context;
514
515
		// Initialize the form
516
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
517
518
		// Initialize it with our settings
519
		$settingsForm->setConfigVars($this->_karmaSettings());
520
521
		// Saving?
522
		if ($this->_req->hasQuery('save'))
523
		{
524
			checkSession();
525
526
			call_integration_hook('integrate_save_karma_settings');
527
528
			$settingsForm->setConfigValues((array) $this->_req->post);
529
			$settingsForm->save();
530
			redirectexit('action=admin;area=featuresettings;sa=karma');
531
		}
532
533
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'karma', 'save']);
534
		$context['settings_title'] = $txt['karma'];
535
536
		$settingsForm->prepare();
537
	}
538
539
	/**
540
	 * Return karma settings.
541
	 *
542
	 * @event integrate_modify_karma_settings Adds to Configuration->Karma
543
	 */
544
	private function _karmaSettings()
545
	{
546
		global $txt;
547
548
		$config_vars = [
549
			// Karma - On or off?
550
			['select', 'karmaMode', explode('|', $txt['karma_options'])],
551
			'',
552
			// Who can do it... and who is restricted by time limits?
553
			['int', 'karmaMinPosts', 6, 'postinput' => $txt['manageposts_posts']],
554
			['float', 'karmaWaitTime', 6, 'postinput' => $txt['hours']],
555
			['check', 'karmaTimeRestrictAdmins'],
556
			['check', 'karmaDisableSmite'],
557
			'',
558
			// What does it look like?  [smite]?
559
			['text', 'karmaLabel'],
560
			['text', 'karmaApplaudLabel', 'mask' => 'nohtml'],
561
			['text', 'karmaSmiteLabel', 'mask' => 'nohtml'],
562
		];
563
564
		call_integration_hook('integrate_modify_karma_settings', [&$config_vars]);
565
566
		return $config_vars;
567
	}
568
569
	/**
570
	 * Display configuration settings page for likes settings.
571
	 *
572
	 * - Accessed from ?action=admin;area=featuresettings;sa=likes;
573
	 *
574
	 * @event integrate_save_likes_settings
575
	 */
576
	public function action_likesSettings_display(): void
577
	{
578
		global $txt, $context;
579
580
		// Initialize the form
581
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
582
583
		// Initialize it with our settings
584
		$settingsForm->setConfigVars($this->_likesSettings());
585
586
		// Saving?
587
		if ($this->_req->hasQuery('save'))
588
		{
589
			checkSession();
590
591
			call_integration_hook('integrate_save_likes_settings');
592
593
			$settingsForm->setConfigValues((array) $this->_req->post);
594
			$settingsForm->save();
595
			redirectexit('action=admin;area=featuresettings;sa=likes');
596
		}
597
598
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'likes', 'save']);
599
		$context['settings_title'] = $txt['likes'];
600
601
		$settingsForm->prepare();
602
	}
603
604
	/**
605 2
	 * Return likes settings.
606
	 *
607 2
	 * @event integrate_modify_likes_settings Adds to Configuration->Likes
608
	 */
609 2
	private function _likesSettings()
610 2
	{
611
		global $txt;
612
613
		$config_vars = [
614 2
			// Likes - On or off?
615
			['check', 'likes_enabled'],
616
			'',
617
			// Who can do it... and who is restricted by count limits?
618 2
			['int', 'likeMinPosts', 6, 'postinput' => $txt['manageposts_posts']],
619 2
			['int', 'likeWaitTime', 6, 'postinput' => $txt['minutes']],
620 2
			['int', 'likeWaitCount', 6],
621
			['check', 'likeRestrictAdmins'],
622 2
			['check', 'likeAllowSelf'],
623
			['check', 'useLikesNotViews'],
624
			'',
625
			['int', 'likeDisplayLimit', 6]
626
		];
627
628
		call_integration_hook('integrate_modify_likes_settings', [&$config_vars]);
629
630
		return $config_vars;
631
	}
632
633
	/**
634
	 * Initializes the mentions settings admin page.
635 2
	 *
636
	 * - Accessed from ?action=admin;area=featuresettings;sa=mention;
637 2
	 *
638
	 * @event integrate_save_modify_mention_settings
639
	 */
640
	public function action_notificationsSettings_display(): void
641
	{
642
		global $txt, $context, $modSettings;
643
644
		Txt::load('Mentions');
645
646
		// Instantiate the form
647
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
648
649
		// Initialize it with our settings
650
		$settingsForm->setConfigVars($this->_notificationsSettings());
651
652
		// Some context stuff
653
		$context['page_title'] = $txt['mentions_settings'];
654
		$context['sub_template'] = 'show_settings';
655
656
		// Saving the settings?
657
		if ($this->_req->hasQuery('save'))
658
		{
659
			checkSession();
660
661
			call_integration_hook('integrate_save_modify_mention_settings');
662
663
			if (!empty($this->_req->post->mentions_enabled))
664
			{
665
				enableModules('mentions', ['post', 'display']);
666
			}
667
			else
668
			{
669
				disableModules('mentions', ['post', 'display']);
670
			}
671
672
			if (!empty($modSettings['hidden_notification_methods']))
673
			{
674
				foreach ($modSettings['hidden_notification_methods'] as $class)
675
				{
676
					$this->_req->post->notifications[$class::getType()] = $class::getSettings();
677
				}
678
			}
679
680
			if (empty($this->_req->post->notifications))
681
			{
682
				$notification_methods = serialize([]);
683
			}
684
			else
685
			{
686
				$notification_methods = [];
687
				foreach ($this->_req->post->notifications as $type => $notification)
688
				{
689
					if (!empty($notification['enable']))
690
					{
691
						$defaults = $notification['default'] ?? [];
692
						unset($notification['enable'], $notification['default']);
693
						foreach ($notification as $k => $v)
694
						{
695
							$notification[$k] = in_array($k, $defaults) ? Notifications::DEFAULT_LEVEL : $v;
696
						}
697
698
						$notification_methods[$type] = $notification;
699
					}
700
				}
701
702
				$notification_methods = serialize($notification_methods);
703
			}
704
705
			require_once(SUBSDIR . '/Mentions.subs.php');
706
			$enabled_mentions = [];
707
			$current_settings = Util::unserialize($modSettings['notification_methods']);
708
709
			// Fist hide what was visible
710
			$modules_toggle = ['enable' => [], 'disable' => []];
711
			foreach ($current_settings as $type => $val)
712
			{
713
				if (!isset($this->_req->post->notifications[$type]))
714
				{
715
					toggleMentionsVisibility($type, false);
716
					$modules_toggle['disable'][] = $type;
717
				}
718
			}
719
720
			// Then make visible what was hidden, but only if there is anything
721
			if (!empty($this->_req->post->notifications))
722
			{
723
				foreach ($this->_req->post->notifications as $type => $val)
724
				{
725
					if (!isset($current_settings[$type]))
726
					{
727
						toggleMentionsVisibility($type, true);
728
						$modules_toggle['enable'][] = $type;
729
					}
730
				}
731
732
				$enabled_mentions = array_keys($this->_req->post->notifications);
733
			}
734
735
			// Let's just keep it active, there are too many reasons it should be.
736
			require_once(SUBSDIR . '/ScheduledTasks.subs.php');
737
			toggleTaskStatusByName('user_access_mentions', true);
738
739
			// Disable or enable modules as needed
740
			foreach ($modules_toggle as $action => $toggles)
741
			{
742
				if (!empty($toggles))
743
				{
744
					// The modules associated with the notification (mentionmem, likes, etc.) area
745
					$modules = getMentionsModules($toggles);
746
747
					// The action will either be enabled to disable
748
					$function = $action . 'Modules';
749
750
					// Something like enableModule('mentions', array('post', 'display');
751
					foreach ($modules as $key => $val)
752
					{
753
						$function($key, $val);
754
					}
755
				}
756
			}
757
758
			updateSettings(['enabled_mentions' => implode(',', array_unique($enabled_mentions)), 'notification_methods' => $notification_methods]);
759
			$settingsForm->setConfigValues((array) $this->_req->post);
760
			$settingsForm->save();
761
			redirectexit('action=admin;area=featuresettings;sa=mention');
762
		}
763
764
		// Prepare the settings for display
765
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'mention', 'save']);
766
		$settingsForm->prepare();
767
	}
768
769
	/**
770
	 * Return mentions settings.
771 2
	 *
772
	 * @event integrate_modify_mention_settings Adds to Configuration->Mentions
773 2
	 */
774
	private function _notificationsSettings()
775
	{
776
		global $txt, $modSettings;
777 2
778 2
		Txt::load('Profile+UserNotifications');
779
		loadJavascriptFile('ext/jquery.multiselect.min.js');
780 2
		theme()->addInlineJavascript('
781 2
			$(\'.select_multiple\').multiselect({\'language_strings\': {\'Select all\': ' . JavaScriptEscape($txt['notify_select_all']) . '}});
782 2
			document.addEventListener("DOMContentLoaded", function() {
783
                 prepareNotificationOptions();
784 2
			});', true);
785 2
		loadCSSFile('multiselect.css');
786
787 2
		// Mentions settings
788 2
		$config_vars = [
789 2
			['title', 'mentions_settings'],
790
			['check', 'mentions_enabled'],
791
		];
792 2
793
		$notification_methods = Notifications::instance()->getNotifiers();
794 2
		$notification_classes = getAvailableNotifications();
795 2
		$current_settings = unserialize($modSettings['notification_methods'], ['allowed_classes' => false]);
796 2
797
		foreach ($notification_classes as $class)
798
		{
799 2
			// The canUse can be set by each notifier based on conditions, default is true;
800
			/* @var $class AbstractNotificationMessage */
801 2
			if ($class::canUse() === false)
802 2
			{
803 2
				continue;
804 2
			}
805
806
			if ($class::hasHiddenInterface() === true)
807
			{
808 2
				$modSettings['hidden_notification_methods'][] = $class;
809
				continue;
810 2
			}
811
812
			// Set up config enable/disable setting for all notifications.
813
			$title = strtolower($class::getType());
814
			$config_vars[] = ['title', 'setting_' . $title];
815
			$config_vars[] = ['check', 'notifications[' . $title . '][enable]', 'text_label' => $txt['setting_notify_enable_this']];
816
			$modSettings['notifications[' . $title . '][enable]'] = !empty($current_settings[$title]);
817
			$default_values = [];
818
			$is_default = [];
819
820
			// If it is enabled, show all the available ways, like email, notify, weekly ...
821
			foreach (array_keys($notification_methods) as $method_name)
822
			{
823
				$method_name = strtolower($method_name);
824
825
				// Are they excluding any, like don't let mailfail be allowed to send email!
826
				if ($class::isNotAllowed($method_name))
827
				{
828
					continue;
829
				}
830
831
				$config_vars[] = ['check', 'notifications[' . $title . '][' . $method_name . ']', 'text_label' => $txt['notify_' . $method_name]];
832
				$modSettings['notifications[' . $title . '][' . $method_name . ']'] = !empty($current_settings[$title][$method_name]);
833
				$default_values[] = [$method_name, $txt['notify_' . $method_name]];
834
				if (empty($current_settings[$title][$method_name]))
835
				{
836
					continue;
837
				}
838
839
				if ((int) $current_settings[$title][$method_name] !== Notifications::DEFAULT_LEVEL)
840
				{
841
					continue;
842
				}
843
844
				$is_default[] = $method_name;
845
			}
846
847
			$config_vars[] = ['select', 'notifications[' . $title . '][default]', $default_values, 'text_label' => $txt['default_active'], 'multiple' => true, 'value' => $is_default];
848
			$modSettings['notifications[' . $title . '][default]'] = $is_default;
849
		}
850
851
		call_integration_hook('integrate_modify_mention_settings', [&$config_vars]);
852
853
		return $config_vars;
854
	}
855
856
	/**
857
	 * Display configuration settings for signatures on forum.
858
	 *
859
	 * - Accessed from ?action=admin;area=featuresettings;sa=sig;
860
	 *
861
	 * @event integrate_save_signature_settings
862
	 */
863
	public function action_signatureSettings_display(): void
864
	{
865
		global $context, $txt, $modSettings;
866
867
		// Initialize the form
868
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
869
870
		// Initialize it with our settings
871
		$settingsForm->setConfigVars($this->_signatureSettings());
872
873
		// Set up the template.
874
		$context['page_title'] = $txt['signature_settings'];
875
		$context['sub_template'] = 'show_settings';
876
877
		// Disable the max smileys option if we don't allow smileys at all!
878
		theme()->addInlineJavascript('
879
			document.getElementById(\'signature_max_smileys\').disabled = !document.getElementById(\'signature_allow_smileys\').checked;', true);
880
881
		// Load all the signature settings.
882
		[$sig_limits, $sig_bbc] = explode(':', $modSettings['signature_settings']);
883
		$sig_limits = explode(',', $sig_limits);
884
		$disabledTags = empty($sig_bbc) ? [] : explode(',', $sig_bbc);
885
886
		// @todo temporary since it does not work, and seriously why would you do this?
887
		$disabledTags[] = 'footnote';
888
889
		// Applying to ALL signatures?!!
890
		if ($this->_req->hasQuery('apply'))
891
		{
892
			// Security!
893
			checkSession('get');
894
895
			// This is horrid - but I suppose some people will want the option to do it.
896
			$applied_sigs = $this->_req->getQuery('step', 'intval', 0);
897
			updateAllSignatures($applied_sigs);
898
899
			$settings_applied = true;
900
		}
901
902
		$context['signature_settings'] = [
903
			'enable' => $sig_limits[0] ?? 0,
904
			'max_length' => $sig_limits[1] ?? 0,
905
			'max_lines' => $sig_limits[2] ?? 0,
906
			'max_images' => $sig_limits[3] ?? 0,
907
			'allow_smileys' => isset($sig_limits[4]) && $sig_limits[4] == -1 ? 0 : 1,
908
			'max_smileys' => isset($sig_limits[4]) && $sig_limits[4] != -1 ? $sig_limits[4] : 0,
909
			'max_image_width' => $sig_limits[5] ?? 0,
910
			'max_image_height' => $sig_limits[6] ?? 0,
911
			'max_font_size' => $sig_limits[7] ?? 0,
912
			'repetition_guests' => $sig_limits[8] ?? 0,
913
			'repetition_members' => $sig_limits[9] ?? 0,
914
		];
915
916
		// Temporarily make each setting a modSetting!
917
		foreach ($context['signature_settings'] as $key => $value)
918
		{
919
			$modSettings['signature_' . $key] = $value;
920
		}
921
922
		// Make sure we check the right tags!
923
		$modSettings['bbc_disabled_signature_bbc'] = $disabledTags;
924
925
		// Saving?
926
		if ($this->_req->hasQuery('save'))
927
		{
928
			checkSession();
929
930
			// Clean up the tag stuff!
931
			$codes = ParserWrapper::instance()->getCodes();
932
			$bbcTags = $codes->getTags();
933
934
			$signature_bbc_enabledTags = $this->_req->getPost('signature_bbc_enabledTags', null, []);
935
			if (!is_array($signature_bbc_enabledTags))
936
			{
937
				$signature_bbc_enabledTags = [$signature_bbc_enabledTags];
938
			}
939
940
			// Do not mutate the request; keep a local copy for settings persistence
941
			$signature_bbc_enabledTags_local = $signature_bbc_enabledTags;
942
943
			$sig_limits = [];
944
			foreach (array_keys($context['signature_settings']) as $key)
945
			{
946
				if ($key === 'allow_smileys')
947
				{
948
					continue;
949
				}
950
				if ($key === 'max_smileys' && empty($this->_req->post->signature_allow_smileys))
951
				{
952
					$sig_limits[] = -1;
953
				}
954
				else
955
				{
956
					$current_key = $this->_req->getPost('signature_' . $key, 'intval');
957
					$sig_limits[] = empty($current_key) ? 0 : max(1, $current_key);
958
				}
959
			}
960
961
			call_integration_hook('integrate_save_signature_settings', [&$sig_limits, &$bbcTags]);
962
963
			// Build the combined signature settings string using locals (do not write back to request)
964
			$signature_settings_local = implode(',', $sig_limits) . ':' . implode(',', array_diff($bbcTags, $signature_bbc_enabledTags_local));
965
966
			// Even though we have practically no settings, let's keep the convention going!
967
			$save_vars = [];
968
			$save_vars[] = ['text', 'signature_settings'];
969
970
			$settingsForm->setConfigVars($save_vars);
971
			// Start from posted values but override with our local computed values
972
			$config_values = (array) $this->_req->post;
973
			$config_values['signature_bbc_enabledTags'] = $signature_bbc_enabledTags_local;
974
			$config_values['signature_settings'] = $signature_settings_local;
975
			$settingsForm->setConfigValues($config_values);
976
			$settingsForm->save();
977
			redirectexit('action=admin;area=featuresettings;sa=sig');
978
		}
979
980
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'sig', 'save']);
981
		$context['settings_title'] = $txt['signature_settings'];
982
		$context['settings_message'] = empty($settings_applied) ? sprintf($txt['signature_settings_warning'], getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'sig', 'apply', '{session_data}'])) : $txt['signature_settings_applied'];
983
984
		$settingsForm->prepare();
985
	}
986
987
	/**
988
	 * Return signature settings.
989
	 *
990
	 * - Used in admin center search and settings form
991
	 *
992
	 * @event integrate_modify_signature_settings Adds options to Signature Settings
993
	 */
994
	private function _signatureSettings()
995
	{
996
		global $txt;
997
998
		$config_vars = [
999
			// Are signatures even enabled?
1000
			['check', 'signature_enable'],
1001
			'',
1002
			// Tweaking settings!
1003
			['int', 'signature_max_length', 'subtext' => $txt['zero_for_no_limit']],
1004
			['int', 'signature_max_lines', 'subtext' => $txt['zero_for_no_limit']],
1005
			['int', 'signature_max_font_size', 'subtext' => $txt['zero_for_no_limit']],
1006
			['check', 'signature_allow_smileys', 'onclick' => "document.getElementById('signature_max_smileys').disabled = !this.checked;"],
1007
			['int', 'signature_max_smileys', 'subtext' => $txt['zero_for_no_limit']],
1008
			['select', 'signature_repetition_guests',
1009
				[
1010
					$txt['signature_always'],
1011
					$txt['signature_onlyfirst'],
1012
					$txt['signature_never'],
1013
				],
1014
			],
1015
			['select', 'signature_repetition_members',
1016
				[
1017
					$txt['signature_always'],
1018
					$txt['signature_onlyfirst'],
1019
					$txt['signature_never'],
1020
				],
1021
			],
1022
			'',
1023
			// Image settings.
1024
			['int', 'signature_max_images', 'subtext' => $txt['signature_max_images_note']],
1025
			['int', 'signature_max_image_width', 'subtext' => $txt['zero_for_no_limit']],
1026
			['int', 'signature_max_image_height', 'subtext' => $txt['zero_for_no_limit']],
1027
			'',
1028
			['bbc', 'signature_bbc'],
1029
		];
1030
1031
		call_integration_hook('integrate_modify_signature_settings', [&$config_vars]);
1032
1033
		return $config_vars;
1034
	}
1035
1036
	/**
1037
	 * Show all the custom profile fields available to the user.
1038
	 *
1039
	 * - Allows for drag/drop sorting of custom profile fields
1040
	 * - Accessed with ?action=admin;area=featuresettings;sa=profile
1041
	 *
1042
	 * @uses sub template show_custom_profile
1043
	 */
1044
	public function action_profile(): void
1045
	{
1046
		global $txt, $context;
1047
1048
		theme()->getTemplates()->load('ManageFeatures');
1049
		$context['page_title'] = $txt['custom_profile_title'];
1050
		$context['sub_template'] = 'show_custom_profile';
1051
1052
		// What about standard fields they can tweak?
1053
		$standard_fields = ['website', 'posts', 'warning_status', 'date_registered', 'action'];
1054
1055
		// What fields can't you put on the registration page?
1056
		$context['fields_no_registration'] = ['posts', 'warning_status', 'date_registered', 'action'];
1057
1058
		// Are we saving any standard field changes?
1059
		if ($this->_req->hasPost('save'))
1060
		{
1061
			checkSession();
1062
			validateToken('admin-scp');
1063
1064
			$changes = [];
1065
1066
			// Do the active ones first.
1067
			$disable_fields = array_flip($standard_fields);
1068
			if (!empty($this->_req->post->active))
1069
			{
1070
				foreach ($this->_req->post->active as $value)
1071
				{
1072
					if (isset($disable_fields[$value]))
1073
					{
1074
						unset($disable_fields[$value]);
1075
					}
1076
				}
1077
			}
1078
1079
			// What we have left!
1080
			$changes['disabled_profile_fields'] = empty($disable_fields) ? '' : implode(',', array_keys($disable_fields));
1081
1082
			// Things we want to show on registration?
1083
			$reg_fields = [];
1084
			if (!empty($this->_req->post->reg))
1085
			{
1086
				foreach ($this->_req->post->reg as $value)
1087
				{
1088
					if (!in_array($value, $standard_fields))
1089
					{
1090
						continue;
1091
					}
1092
1093
					if (isset($disable_fields[$value]))
1094
					{
1095
						continue;
1096
					}
1097
1098
					$reg_fields[] = $value;
1099
				}
1100
			}
1101
1102
			// What we have left!
1103
			$changes['registration_fields'] = empty($reg_fields) ? '' : implode(',', $reg_fields);
1104
1105
			updateSettings($changes);
1106
		}
1107
1108
		createToken('admin-scp');
1109
1110
		// Create a listing for all our standard fields
1111
		$listOptions = [
1112
			'id' => 'standard_profile_fields',
1113
			'title' => $txt['standard_profile_title'],
1114
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'profile']),
1115
			'get_items' => [
1116
				'function' => 'list_getProfileFields',
1117
				'params' => [
1118
					true,
1119
				],
1120
			],
1121
			'columns' => [
1122
				'field' => [
1123
					'header' => [
1124
						'value' => $txt['standard_profile_field'],
1125
					],
1126
					'data' => [
1127
						'db' => 'label',
1128
						'style' => 'width: 60%;',
1129
					],
1130
				],
1131
				'active' => [
1132
					'header' => [
1133
						'value' => $txt['custom_edit_active'],
1134
						'class' => 'centertext',
1135
					],
1136
					'data' => [
1137
						'function' => static function ($rowData) {
1138
							$isChecked = $rowData['disabled'] ? '' : ' checked="checked"';
1139
							$onClickHandler = $rowData['can_show_register'] ? sprintf('onclick="document.getElementById(\'reg_%1$s\').disabled = !this.checked;"', $rowData['id']) : '';
1140
1141
							return sprintf('<input type="checkbox" name="active[]" id="active_%1$s" value="%1$s" class="input_check" %2$s %3$s />', $rowData['id'], $isChecked, $onClickHandler);
1142
						},
1143
						'style' => 'width: 20%;',
1144
						'class' => 'centertext',
1145
					],
1146
				],
1147
				'show_on_registration' => [
1148
					'header' => [
1149
						'value' => $txt['custom_edit_registration'],
1150
						'class' => 'centertext',
1151
					],
1152
					'data' => [
1153
						'function' => static function ($rowData) {
1154
							$isChecked = $rowData['on_register'] && !$rowData['disabled'] ? ' checked="checked"' : '';
1155
							$isDisabled = $rowData['can_show_register'] ? '' : ' disabled="disabled"';
1156
1157
							return sprintf('<input type="checkbox" name="reg[]" id="reg_%1$s" value="%1$s" class="input_check" %2$s %3$s />', $rowData['id'], $isChecked, $isDisabled);
1158
						},
1159
						'style' => 'width: 20%;',
1160
						'class' => 'centertext',
1161
					],
1162
				],
1163
			],
1164
			'form' => [
1165
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'profile']),
1166
				'name' => 'standardProfileFields',
1167
				'token' => 'admin-scp',
1168
			],
1169
			'additional_rows' => [
1170
				[
1171
					'position' => 'below_table_data',
1172
					'value' => '<input type="submit" name="save" value="' . $txt['save'] . '" class="right_submit" />',
1173
				],
1174
			],
1175
		];
1176
		createList($listOptions);
1177
1178
		// And now we do the same for all of our custom ones
1179
		$token = createToken('admin-sort');
1180
		$listOptions = [
1181
			'id' => 'custom_profile_fields',
1182
			'title' => $txt['custom_profile_title'],
1183
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'profile']),
1184
			'default_sort_col' => 'vieworder',
1185
			'no_items_label' => $txt['custom_profile_none'],
1186
			'items_per_page' => 25,
1187
			'sortable' => true,
1188
			'get_items' => [
1189
				'function' => 'list_getProfileFields',
1190
				'params' => [
1191
					false,
1192
				],
1193
			],
1194
			'get_count' => [
1195
				'function' => 'list_getProfileFieldSize',
1196
			],
1197
			'columns' => [
1198
				'vieworder' => [
1199
					'header' => [
1200
						'value' => '',
1201
						'class' => 'hide',
1202
					],
1203
					'data' => [
1204
						'db' => 'vieworder',
1205
						'class' => 'hide',
1206
					],
1207
					'sort' => [
1208
						'default' => 'vieworder',
1209
					],
1210
				],
1211
				'field_name' => [
1212
					'header' => [
1213
						'value' => $txt['custom_profile_fieldname'],
1214
					],
1215
					'data' => [
1216
						'function' => static fn($rowData) => sprintf('<a href="%1$s">%2$s</a><div class="smalltext">%3$s</div>', getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'profileedit', 'fid' => (int) $rowData['id_field']]), $rowData['field_name'], $rowData['field_desc']),
1217
						'style' => 'width: 65%;',
1218
					],
1219
					'sort' => [
1220
						'default' => 'field_name',
1221
						'reverse' => 'field_name DESC',
1222
					],
1223
				],
1224
				'field_type' => [
1225
					'header' => [
1226
						'value' => $txt['custom_profile_fieldtype'],
1227
					],
1228
					'data' => [
1229
						'function' => static function ($rowData) {
1230
							global $txt;
1231
1232
							$textKey = sprintf('custom_profile_type_%1$s', $rowData['field_type']);
1233
1234
							return $txt[$textKey] ?? $textKey;
1235
						},
1236
						'style' => 'width: 10%;',
1237
					],
1238
					'sort' => [
1239
						'default' => 'field_type',
1240
						'reverse' => 'field_type DESC',
1241
					],
1242
				],
1243
				'cust' => [
1244
					'header' => [
1245
						'value' => $txt['custom_profile_active'],
1246
						'class' => 'centertext',
1247
					],
1248
					'data' => [
1249
						'function' => static function ($rowData) {
1250
							$isChecked = $rowData['active'] === '1' ? ' checked="checked"' : '';
1251
1252
							return sprintf('<input type="checkbox" name="cust[]" id="cust_%1$s" value="%1$s" class="input_check"%2$s />', $rowData['id_field'], $isChecked);
1253
						},
1254
						'style' => 'width: 8%;',
1255
						'class' => 'centertext',
1256
					],
1257
					'sort' => [
1258
						'default' => 'active DESC',
1259
						'reverse' => 'active',
1260
					],
1261
				],
1262
				'placement' => [
1263
					'header' => [
1264
						'value' => $txt['custom_profile_placement'],
1265
					],
1266
					'data' => [
1267
						'function' => static function ($rowData) {
1268
							global $txt;
1269
1270
							$placement = 'custom_profile_placement_';
1271
							switch ((int) $rowData['placement'])
1272
							{
1273
								case 0:
1274
									$placement .= 'standard';
1275
									break;
1276
								case 1:
1277
									$placement .= 'withicons';
1278
									break;
1279
								case 2:
1280
									$placement .= 'abovesignature';
1281
									break;
1282
								case 3:
1283
									$placement .= 'aboveicons';
1284
									break;
1285
							}
1286
1287
							return $txt[$placement];
1288
						},
1289
						'style' => 'width: 5%;',
1290
					],
1291
					'sort' => [
1292
						'default' => 'placement DESC',
1293
						'reverse' => 'placement',
1294
					],
1295
				],
1296
				'modify' => [
1297
					'data' => [
1298
						'sprintf' => [
1299
							'format' => '<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'profileedit']) . ';fid=%1$s">' . $txt['modify'] . '</a>',
1300
							'params' => [
1301
								'id_field' => false,
1302
							],
1303
						],
1304
						'style' => 'width: 5%;',
1305
					],
1306
				],
1307
			],
1308
			'form' => [
1309
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'profileedit']),
1310
				'name' => 'customProfileFields',
1311
				'token' => 'admin-scp',
1312
			],
1313
			'additional_rows' => [
1314
				[
1315
					'class' => 'submitbutton flow_flex_additional_row',
1316
					'position' => 'below_table_data',
1317
					'value' => '
1318
						<input type="submit" name="onoff" value="' . $txt['save'] . '" />
1319
						<input type="submit" name="new" value="' . $txt['custom_profile_make_new'] . '" />',
1320
				],
1321
				[
1322
					'position' => 'top_of_list',
1323
					'value' => '<p class="infobox">' . $txt['custom_profile_sort'] . '</p>',
1324
				],
1325
			],
1326
			'javascript' => '
1327
				$().elkSortable({
1328
					sa: "profileorder",
1329
					error: "' . $txt['admin_order_error'] . '",
1330
					title: "' . $txt['admin_order_title'] . '",
1331
					placeholder: "ui-state-highlight",
1332
					href: "?action=admin;area=featuresettings;sa=profile",
1333
					token: {token_var: "' . $token['admin-sort_token_var'] . '", token_id: "' . $token['admin-sort_token'] . '"}
1334
				});
1335
			',
1336
		];
1337
1338
		createList($listOptions);
1339
	}
1340
1341
	/**
1342
	 * Edit some profile fields?
1343
	 *
1344
	 * - Accessed with ?action=admin;area=featuresettings;sa=profileedit
1345
	 *
1346
	 * @uses sub template edit_profile_field
1347
	 */
1348
	public function action_profileedit(): void
1349
	{
1350
		global $txt, $context;
1351
1352
		theme()->getTemplates()->load('ManageFeatures');
1353
1354
		// Sort out the context!
1355
		$context['fid'] = $this->_req->getQuery('fid', 'intval', 0);
1356
		$context[$context['admin_menu_name']]['current_subsection'] = 'profile';
1357
		$context['page_title'] = $context['fid'] ? $txt['custom_edit_title'] : $txt['custom_add_title'];
1358
		$context['sub_template'] = 'edit_profile_field';
1359
1360
		// Any error messages to show?
1361
		if ($this->_req->hasQuery('msg'))
1362
		{
1363
			Txt::load('Errors');
1364
			$msg_key = $this->_req->getQuery('msg', 'trim|strval', '');
1365
			if (isset($txt['custom_option_' . $msg_key]))
1366
			{
1367
				$context['custom_option__error'] = $txt['custom_option_' . $msg_key];
1368
			}
1369
		}
1370
1371
		// Load the profile language for section names.
1372
		Txt::load('Profile');
1373
1374
		// Load up the profile field if one was supplied
1375
		if ($context['fid'])
1376
		{
1377
			$context['field'] = getProfileField($context['fid']);
1378
		}
1379
1380
		// Set up the default values as needed.
1381
		if (empty($context['field']))
1382
		{
1383
			$context['field'] = [
1384
				'name' => '',
1385
				'colname' => '???',
1386
				'desc' => '',
1387
				'profile_area' => 'forumprofile',
1388
				'reg' => false,
1389
				'display' => false,
1390
				'memberlist' => false,
1391
				'type' => 'text',
1392
				'max_length' => 255,
1393
				'rows' => 4,
1394
				'cols' => 30,
1395
				'bbc' => false,
1396
				'default_check' => false,
1397
				'default_select' => '',
1398
				'default_value' => '',
1399
				'options' => ['', '', ''],
1400
				'active' => true,
1401
				'private' => false,
1402
				'can_search' => false,
1403
				'mask' => 'nohtml',
1404
				'regex' => '',
1405
				'enclose' => '',
1406
				'placement' => 0,
1407
			];
1408
		}
1409
1410
		// All the JavaScript for this page... everything else is in admin.js
1411
		theme()->addJavascriptVar(['startOptID' => count($context['field']['options'])]);
1412
		theme()->addInlineJavascript('updateInputBoxes();', true);
1413
1414
		// Are we toggling which ones are active?
1415
		if (isset($this->_req->post->onoff))
1416
		{
1417
			checkSession();
1418
			validateToken('admin-scp');
1419
1420
			// Enable and disable custom fields as required.
1421
			$enabled = [0];
1422
			if (isset($this->_req->post->cust) && is_array($this->_req->post->cust))
1423
			{
1424
				foreach ($this->_req->post->cust as $id)
1425
				{
1426
					$enabled[] = (int) $id;
1427
				}
1428
			}
1429
1430
			updateRenamedProfileStatus($enabled);
1431
		}
1432
		// Are we saving?
1433
		elseif ($this->_req->hasPost('save'))
1434
		{
1435
			checkSession();
1436
			validateToken('admin-ecp');
1437
1438
			// Everyone needs a name - even the (bracket) unknown...
1439
			if (trim($this->_req->post->field_name) === '')
1440
			{
1441
				redirectexit('action=admin;area=featuresettings;sa=profileedit;fid=' . (int) $context['fid'] . ';msg=need_name');
1442
			}
1443
1444
			// Regex, you say?  Do a very basic test to see if the pattern is valid
1445
			if (!empty($this->_req->post->regex) && @preg_match($this->_req->post->regex, 'dummy') === false)
1446
			{
1447
				redirectexit('action=admin;area=featuresettings;sa=profileedit;fid=' . (int) $context['fid'] . ';msg=regex_error');
1448
			}
1449
1450
			$this->_req->post->field_name = $this->_req->getPost('field_name', 'Util::htmlspecialchars');
1451
			$this->_req->post->field_desc = $this->_req->getPost('field_desc', 'Util::htmlspecialchars');
1452
1453
			$rows = isset($this->_req->post->rows) ? (int) $this->_req->post->rows : 4;
1454
			$cols = isset($this->_req->post->cols) ? (int) $this->_req->post->cols : 30;
1455
1456
			// Checkboxes...
1457
			$show_reg = $this->_req->getPost('reg', 'intval', 0);
1458
			$show_display = isset($this->_req->post->display) ? 1 : 0;
1459
			$show_memberlist = isset($this->_req->post->memberlist) ? 1 : 0;
1460
			$bbc = isset($this->_req->post->bbc) ? 1 : 0;
1461
			$show_profile = $this->_req->post->profile_area;
1462
			$active = isset($this->_req->post->active) ? 1 : 0;
1463
			$private = $this->_req->getPost('private', 'intval', 0);
1464
			$can_search = isset($this->_req->post->can_search) ? 1 : 0;
1465
1466
			// Some masking stuff...
1467
			$mask = $this->_req->getPost('mask', 'strval', '');
1468
			if ($mask === 'regex' && isset($this->_req->post->regex))
1469
			{
1470
				$mask .= $this->_req->post->regex;
1471
			}
1472
1473
			$field_length = $this->_req->getPost('max_length', 'intval', 255);
1474
			$enclose = $this->_req->getPost('enclose', 'strval', '');
1475
			$placement = $this->_req->getPost('placement', 'intval', 0);
1476
1477
			// Select options?
1478
			$field_options = '';
1479
			$newOptions = [];
1480
1481
			// Set default
1482
			$default = '';
1483
1484
			switch ($this->_req->post->field_type)
1485
			{
1486
				case 'check':
1487
					$default = isset($this->_req->post->default_check) ? 1 : '';
1488
					break;
1489
				case 'select':
1490
				case 'radio':
1491
					if (!empty($this->_req->post->select_option))
1492
					{
1493
						foreach ($this->_req->post->select_option as $k => $v)
1494
						{
1495
							// Clean, clean, clean...
1496
							$v = Util::htmlspecialchars($v);
1497
							$v = strtr($v, [',' => '']);
1498
1499
							// Nada, zip, etc...
1500
							if (trim($v) === '')
1501
							{
1502
								continue;
1503
							}
1504
1505
							// Otherwise, save it boy.
1506
							$field_options .= $v . ',';
1507
1508
							// This is just for working out what happened with old options...
1509
							$newOptions[$k] = $v;
1510
1511
							// Is it default?
1512
							if (!isset($this->_req->post->default_select))
1513
							{
1514
								continue;
1515
							}
1516
1517
							if ($this->_req->post->default_select != $k)
1518
							{
1519
								continue;
1520
							}
1521
1522
							$default = $v;
1523
						}
1524
1525
						if (isset($_POST['default_select']) && $_POST['default_select'] === 'no_default')
1526
						{
1527
							$default = 'no_default';
1528
						}
1529
1530
						$field_options = substr($field_options, 0, -1);
1531
					}
1532
1533
					break;
1534
				default:
1535
					$default = $this->_req->post->default_value ?? '';
1536
			}
1537
1538 2
			// Come up with the unique name?
1539
			if (empty($context['fid']))
1540 2
			{
1541
				$colname = Util::substr(strtr($this->_req->post->field_name, [' ' => '']), 0, 6);
1542
				preg_match('~([\w_-]+)~', $colname, $matches);
1543
1544
				// If there is nothing to the name, then let's start our own - for foreign languages etc.
1545
				if (isset($matches[1]))
1546 2
				{
1547
					$colname = 'cust_' . strtolower($matches[1]);
1548 2
					$initial_colname = 'cust_' . strtolower($matches[1]);
1549
				}
1550
				else
1551
				{
1552
					$colname = 'cust_' . mt_rand(1, 999999);
1553
					$initial_colname = 'cust_' . mt_rand(1, 999999);
1554 2
				}
1555
1556 2
				$unique = ensureUniqueProfileField($colname, $initial_colname);
1557
1558
				// Still not a unique column name? Leave it up to the user, then.
1559
				if (!$unique)
1560
				{
1561
					throw new Exception('custom_option_not_unique');
1562 2
				}
1563
1564 2
				// And create a new field
1565
				$new_field = [
1566
					'col_name' => $colname,
1567
					'field_name' => $this->_req->post->field_name,
1568
					'field_desc' => $this->_req->post->field_desc,
1569
					'field_type' => $this->_req->post->field_type,
1570 2
					'field_length' => $field_length,
1571
					'field_options' => $field_options,
1572 2
					'show_reg' => $show_reg,
1573
					'show_display' => $show_display,
1574
					'show_memberlist' => $show_memberlist,
1575
					'show_profile' => $show_profile,
1576
					'private' => $private,
1577
					'active' => $active,
1578 2
					'default_value' => $default,
1579
					'rows' => $rows,
1580 2
					'cols' => $cols,
1581
					'can_search' => $can_search,
1582
					'bbc' => $bbc,
1583
					'mask' => $mask,
1584
					'enclose' => $enclose,
1585
					'placement' => $placement,
1586
					'vieworder' => list_getProfileFieldSize() + 1,
1587
				];
1588
				addProfileField($new_field);
1589
			}
1590
			// Work out what to do with the user data otherwise...
1591
			else
1592
			{
1593
				// Anything going to check or select is pointless keeping - as is anything coming from check!
1594
				if (($this->_req->post->field_type === 'check' && $context['field']['type'] !== 'check')
1595
					|| (($this->_req->post->field_type === 'select' || $this->_req->post->field_type === 'radio') && $context['field']['type'] !== 'select' && $context['field']['type'] !== 'radio')
1596
					|| ($context['field']['type'] === 'check' && $this->_req->post->field_type !== 'check'))
1597
				{
1598
					deleteProfileFieldUserData($context['field']['colname']);
1599
				}
1600
				// Otherwise - if the select is edited may need to adjust!
1601
				elseif ($this->_req->post->field_type === 'select' || $this->_req->post->field_type === 'radio')
1602
				{
1603
					$optionChanges = $context['field']['options'];
1604
					$takenKeys = [];
1605
1606
					// Work out what's changed!
1607
					foreach ($optionChanges as $k => $option)
1608
					{
1609
						if (trim($option) === '')
1610
						{
1611
							continue;
1612
						}
1613
1614
						// Still exists?
1615
						if (in_array($option, $newOptions))
1616
						{
1617
							$takenKeys[] = $k;
1618
						}
1619
					}
1620
1621
					// Finally - have we renamed it - or is it really gone?
1622
					foreach ($optionChanges as $k => $option)
1623
					{
1624
						// Just been renamed?
1625
						if (in_array($k, $takenKeys))
1626
						{
1627
							continue;
1628
						}
1629
1630
						if (empty($newOptions[$k]))
1631
						{
1632
							continue;
1633
						}
1634
1635
						updateRenamedProfileField($k, $newOptions, $context['field']['colname'], $option);
1636
					}
1637
				}
1638
1639
				// @todo Maybe we should adjust based on new text length limits?
1640
1641
				// And finally update an existing field
1642
				$field_data = [
1643
					'field_length' => $field_length,
1644
					'show_reg' => $show_reg,
1645
					'show_display' => $show_display,
1646
					'show_memberlist' => $show_memberlist,
1647
					'private' => $private,
1648
					'active' => $active,
1649
					'can_search' => $can_search,
1650
					'bbc' => $bbc,
1651
					'current_field' => $context['fid'],
1652
					'field_name' => $this->_req->post->field_name,
1653
					'field_desc' => $this->_req->post->field_desc,
1654
					'field_type' => $this->_req->post->field_type,
1655
					'field_options' => $field_options,
1656
					'show_profile' => $show_profile,
1657
					'default_value' => $default,
1658
					'mask' => $mask,
1659
					'enclose' => $enclose,
1660
					'placement' => $placement,
1661
					'rows' => $rows,
1662
					'cols' => $cols,
1663
				];
1664
1665
				updateProfileField($field_data);
1666
1667
				// Just clean up any old selects - these are a pain!
1668
				if (($this->_req->post->field_type == 'select' || $this->_req->post->field_type == 'radio') && !empty($newOptions))
1669
				{
1670
					deleteOldProfileFieldSelects($newOptions, $context['field']['colname']);
1671
				}
1672
			}
1673
		}
1674
		// Deleting?
1675
		elseif (isset($this->_req->post->delete) && $context['field']['colname'])
1676
		{
1677
			checkSession();
1678
			validateToken('admin-ecp');
1679
1680
			// Delete the old data first, then the field.
1681
			deleteProfileFieldUserData($context['field']['colname']);
1682
			deleteProfileField($context['fid']);
1683
		}
1684
1685
		// Rebuild display cache etc.
1686
		if (isset($this->_req->post->delete) || isset($this->_req->post->save) || isset($this->_req->post->onoff))
1687
		{
1688
			checkSession();
1689
1690
			// Update the display cache
1691
			updateDisplayCache();
1692
			redirectexit('action=admin;area=featuresettings;sa=profile');
1693
		}
1694
1695
		createToken('admin-ecp');
1696
	}
1697
1698
	/**
1699
	 * Editing personal messages settings
1700
	 *
1701
	 * - Accessed with ?action=admin;area=featuresettings;sa=pmsettings
1702
	 *
1703
	 * @event integrate_save_pmsettings_settings
1704
	 */
1705
	public function action_pmsettings(): void
1706
	{
1707
		global $txt, $context;
1708
1709
		// Initialize the form
1710
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
1711
1712
		// Initialize it with our settings
1713
		$settingsForm->setConfigVars($this->_pmSettings());
1714
1715
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
1716
		Txt::load('ManageMembers');
1717
1718
		$context['pm_limits'] = loadPMLimits();
1719
1720
		// Saving?
1721
		if ($this->_req->hasQuery('save'))
1722
		{
1723
			checkSession();
1724
1725
			require_once(SUBSDIR . '/Membergroups.subs.php');
1726
			foreach ($context['pm_limits'] as $group_id => $group)
1727
			{
1728
				if (!isset($this->_req->post->group[$group_id]))
1729
				{
1730
					continue;
1731
				}
1732
1733
				if ($this->_req->post->group[$group_id] == $group['max_messages'])
1734
				{
1735
					continue;
1736
				}
1737
1738
				updateMembergroupProperties(['current_group' => $group_id, 'max_messages' => $this->_req->post->group[$group_id]]);
1739
			}
1740
1741
			call_integration_hook('integrate_save_pmsettings_settings');
1742
1743
			$settingsForm->setConfigValues((array) $this->_req->post);
1744
			$settingsForm->save();
1745
			redirectexit('action=admin;area=featuresettings;sa=pmsettings');
1746
		}
1747
1748
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'featuresettings', 'sa' => 'pmsettings', 'save']);
1749
		$context['settings_title'] = $txt['personal_messages'];
1750
1751
		$settingsForm->prepare();
1752
	}
1753
1754
	/**
1755
	 * Return pm settings.
1756
	 *
1757
	 * - Used in admin center search and settings form
1758
	 *
1759
	 * @event integrate_modify_pmsettings_settings Adds / Modifies PM Settings
1760
	 */
1761
	private function _pmSettings()
1762
	{
1763
		global $txt;
1764
1765
		$config_vars = [
1766
			// Reporting of personal messages?
1767
			['check', 'enableReportPM'],
1768
			// Inline permissions.
1769
			['permissions', 'pm_send'],
1770
			// PM Settings
1771
			['title', 'antispam_PM'],
1772
			'pm1' => ['int', 'max_pm_recipients', 'postinput' => $txt['max_pm_recipients_note']],
1773
			'pm2' => ['int', 'pm_posts_verification', 'postinput' => $txt['pm_posts_verification_note']],
1774
			'pm3' => ['int', 'pm_posts_per_hour', 'postinput' => $txt['pm_posts_per_hour_note']],
1775
			['title', 'membergroups_max_messages'],
1776
			['desc', 'membergroups_max_messages_desc'],
1777
			['callback', 'pm_limits'],
1778
		];
1779
1780
		call_integration_hook('integrate_modify_pmsettings_settings', [&$config_vars]);
1781
1782
		return $config_vars;
1783
	}
1784
1785
	/**
1786
	 * Public method to return the basic settings, used for admin search
1787
	 */
1788
	public function basicSettings_search()
1789
	{
1790
		return $this->_basicSettings();
1791
	}
1792
1793
	/**
1794
	 * Public method to return the layout settings, used for admin search
1795
	 */
1796
	public function layoutSettings_search()
1797
	{
1798
		return $this->_layoutSettings();
1799
	}
1800
1801
	/**
1802
	 * Public method to return the karma settings, used for admin search
1803
	 */
1804
	public function karmaSettings_search()
1805
	{
1806
		global $modSettings;
1807
1808
		// Karma - On or off?
1809
		if (empty($modSettings['karmaMode']))
1810
		{
1811
			return ['check', 'dummy_karma'];
1812
		}
1813
1814
		return $this->_karmaSettings();
1815
	}
1816
1817
	/**
1818
	 * Public method to return the likes settings, used for admin search
1819
	 */
1820
	public function likesSettings_search()
1821
	{
1822
		global $modSettings;
1823
1824
		// Likes - On or off?
1825
		if (empty($modSettings['enable_likes']))
1826
		{
1827
			return ['check', 'dummy_likes'];
1828
		}
1829
1830
		return $this->_likesSettings();
1831
	}
1832
1833
	/**
1834
	 * Public method to return the mention settings, used for admin search
1835
	 */
1836
	public function mentionSettings_search()
1837
	{
1838
		return $this->_notificationsSettings();
1839
	}
1840
1841
	/**
1842
	 * Public method to return the signature settings, used for admin search
1843
	 */
1844
	public function signatureSettings_search()
1845
	{
1846
		return $this->_signatureSettings();
1847
	}
1848
1849
	/**
1850
	 * Public method to return the PM settings, used for admin search
1851
	 */
1852
	public function pmSettings_search()
1853
	{
1854
		return $this->_pmSettings();
1855
	}
1856
}
1857