ManageThemes::action_index()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 49
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 49
rs 9.536
c 0
b 0
f 0
cc 3
nc 3
nop 0
ccs 0
cts 16
cp 0
crap 12
1
<?php
2
3
/**
4
 * This file concerns itself almost completely with theme administration.
5
 * Its tasks include changing theme settings, installing and removing
6
 * themes, choosing the current theme, and editing themes.
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 Beta 1
16
 *
17
 *
18
 * @todo Update this for the new package manager?
19
 *
20
 * Creating and distributing theme packages:
21
 * There isn't that much required to package and distribute your own themes...
22
 * Do the following:
23
 *
24
 *  - Create a theme_info.xml file, with the root element theme-info.
25
 *  - Its name should go in a name element, just like description.
26
 *  - Your name should go in author. (email in the email attribute.)
27
 *  - Any support website for the theme should be in website.
28
 *  - Layers and templates (non-default) should go in those elements ;).
29
 *  - If the images dir isn't images, specify in the images' element.
30
 *  - Any extra rows for themes should go in extra, serialized. (as in array(variable => value).)
31
 *  - Tar and gzip the directory - and you're done!
32
 *  - Please include any special license in a license.txt file.
33
 */
34
35
namespace ElkArte\AdminController;
36
37
use ElkArte\AbstractController;
38
use ElkArte\Action;
39
use ElkArte\Cache\Cache;
40
use ElkArte\Exceptions\Exception;
41
use ElkArte\Helper\FileFunctions;
42
use ElkArte\Helper\Util;
43
use ElkArte\Languages\Txt;
44
use ElkArte\Profile\Profile;
45
use ElkArte\Themes\ThemeLoader;
46
use ElkArte\User;
47
use ElkArte\XmlArray;
48
49
/**
50
 * Class to deal with theme administration.
51
 *
52
 * Its tasks include changing theme settings, installing and removing
53
 * themes, choosing the current theme, and editing themes.
54
 *
55
 * @package Themes
56
 */
57
class ManageThemes extends AbstractController
58
{
59
	/** @var string Name of the theme */
60
	private string $theme_name;
61
62
	/** @var string Full path to the theme */
63
	private string $theme_dir;
64
65
	/** @var string|null The themes image url if any */
66
	private ?string $images_url;
67
68
	/**
69
	 * {@inheritDoc}
70
	 */
71
	public function trackStats($action = '')
72
	{
73
		if ($action === 'action_jsoption')
74
		{
75
			return false;
76
		}
77
78
		return parent::trackStats($action);
79
	}
80
81
	/**
82
	 * Subaction handler - manages the action and delegates control to the proper
83
	 * sub-action.
84
	 *
85
	 * What it does:
86
	 *
87
	 * - It loads both the Themes and Settings language files.
88
	 * - Checks the session by GET or POST to verify the data.
89
	 * - Requires the user to not be a guest.
90
	 * - Accessed via ?action=admin;area=theme.
91
	 *
92
	 * @see AbstractController::action_index()
93
	 */
94
	public function action_index()
95
	{
96
		global $txt, $context;
97
98
		if ($this->_req->hasQuery('api'))
99
		{
100
			$this->action_index_api();
101
102
			return;
103
		}
104
105
		// Load the important language files...
106
		Txt::load('ManageThemes+Settings');
107
108
		// No guests in here.
109
		is_not_guest();
110
111
		// Theme administration, removal, choice, or installation...
112
		$subActions = [
113
			'admin' => [$this, 'action_admin', 'permission' => 'admin_forum'],
114
			'list' => [$this, 'action_list', 'permission' => 'admin_forum'],
115
			'reset' => [$this, 'action_options', 'permission' => 'admin_forum'],
116
			'options' => [$this, 'action_options', 'permission' => 'admin_forum'],
117
			'install' => [$this, 'action_install', 'permission' => 'admin_forum'],
118
			'remove' => [$this, 'action_remove', 'permission' => 'admin_forum'],
119
			'pick' => [$this, 'action_pick', 'permission' => 'admin_forum'],
120
		];
121
122
		// Action controller
123
		$action = new Action('manage_themes');
124
125
		if (!empty($context['admin_menu_name']))
126
		{
127
			$context[$context['admin_menu_name']]['object']->prepareTabData([
128
				'title' => 'themeadmin_title',
129
				'description' => 'themeadmin_description',
130
				'prefix' => 'themeadmin',
131
			]);
132
		}
133
134
		// Follow the sa or just go to administration, call integrate_sa_manage_themes
135
		$subAction = $action->initialize($subActions, 'admin');
136
137
		// Default the page title to Theme Administration by default.
138
		$context['page_title'] = $txt['themeadmin_title'];
139
		$context['sub_action'] = $subAction;
140
141
		// Go to the action if you have permissions
142
		$action->dispatch($subAction);
143
	}
144
145
	/**
146
	 * Responds to an ajax button request, currently only for remove
147
	 *
148
	 * @uses generic_xml_buttons sub template
149
	 */
150
	public function action_index_api(): void
151
	{
152
		global $txt, $context;
153
154
		theme()->getTemplates()->load('Xml');
155
156
		// Remove any template layers that may have been created, this is XML!
157
		theme()->getLayers()->removeAll();
158
		$context['sub_template'] = 'generic_xml_buttons';
159
160
		// No guests in here.
161
		if ($this->user->is_guest)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
162
		{
163
			Txt::load('Errors');
164
			$context['xml_data'] = [
165
				'error' => 1,
166
				'text' => $txt['not_guests']
167
			];
168
169
			return;
170
		}
171
172
		// Theme administration, removal, choice, or installation...
173
		// Of all the actions we currently know only this
174
		$subActions = [
175
			// 'admin' => 'action_admin',
176
			// 'list' => 'action_list',
177
			// 'reset' => 'action_options',
178
			// 'options' => 'action_options',
179
			// 'install' => 'action_install',
180
			'remove' => 'action_remove_api',
181
			// 'pick' => 'action_pick',
182
		];
183
184
		// Follow the sa or just go to administration.
185
		$sa = $this->_req->getQuery('sa', 'trim|strval', '');
186
		if ($sa !== '' && $sa !== '0' && isset($subActions[$sa]))
187
		{
188
			$this->{$subActions[$sa]}();
189
		}
190
		else
191
		{
192
			Txt::load('Errors');
193
			$context['xml_data'] = [
194
				'error' => 1,
195
				'text' => $txt['error_sa_not_set']
196
			];
197
		}
198
	}
199
200
	/**
201
	 * This function lists the available themes and provides an interface
202
	 * to reset the paths of all the installed themes.
203
	 *
204
	 * @uses sub template list_themes, template ManageThemes
205
	 */
206
	public function action_list(): void
207
	{
208
		global $context, $boardurl, $txt;
209
210
		// Load in the helpers we need
211
		require_once(SUBSDIR . '/Themes.subs.php');
212
		Txt::load('Admin');
213
		$fileFunc = FileFunctions::instance();
214
215
		if ($this->_req->hasQuery('th'))
216
		{
217
			$this->action_setthemesettings();
218
			return;
219
		}
220
221
		// Saving?
222
		if ($this->_req->hasPost('save'))
223
		{
224
			checkSession();
225
			validateToken('admin-tl');
226
227
			$themes = installedThemes();
228
			$setValues = [];
229
230
			foreach ($themes as $id => $theme)
231
			{
232
				if ($fileFunc->isDir($this->_req->post->reset_dir . '/' . basename($theme['theme_dir'])))
233
				{
234
					$setValues[] = [$id, 0, 'theme_dir', realpath($this->_req->post->reset_dir . '/' . basename($theme['theme_dir']))];
235
					$setValues[] = [$id, 0, 'theme_url', $this->_req->post->reset_url . '/' . basename($theme['theme_dir'])];
236
					$setValues[] = [$id, 0, 'images_url', $this->_req->post->reset_url . '/' . basename($theme['theme_dir']) . '/' . basename($theme['images_url'])];
237
				}
238
239
				if (isset($theme['base_theme_dir']) && $fileFunc->isDir($this->_req->post->reset_dir . '/' . basename($theme['base_theme_dir'])))
240
				{
241
					$setValues[] = [$id, 0, 'base_theme_dir', realpath($this->_req->post->reset_dir . '/' . basename($theme['base_theme_dir']))];
242
					$setValues[] = [$id, 0, 'base_theme_url', $this->_req->post->reset_url . '/' . basename($theme['base_theme_dir'])];
243
					$setValues[] = [$id, 0, 'base_images_url', $this->_req->post->reset_url . '/' . basename($theme['base_theme_dir']) . '/' . basename($theme['base_images_url'])];
244
				}
245
246
				Cache::instance()->remove('theme_settings-' . $id);
247
			}
248
249
			updateThemeOptions($setValues);
250
251
			redirectexit('action=admin;area=theme;sa=list;' . $context['session_var'] . '=' . $context['session_id']);
252
		}
253
254
		theme()->getTemplates()->load('ManageThemes');
255
256
		$context['themes'] = installedThemes();
257
258
		// For each theme, make sure the directory exists and try to fetch the theme version
259
		foreach ($context['themes'] as $i => $theme)
260
		{
261
			$context['themes'][$i]['theme_dir'] = realpath($context['themes'][$i]['theme_dir']);
262
263
			if ($fileFunc->fileExists($context['themes'][$i]['theme_dir'] . '/index.template.php'))
264
			{
265
				// Fetch the header... a good 256 bytes should be more than enough.
266
				$fp = fopen($context['themes'][$i]['theme_dir'] . '/index.template.php', 'rb');
267
				$header = fread($fp, 256);
268
				fclose($fp);
269
270
				// Can we find a version comment, at all?
271
				if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1)
272
				{
273
					$context['themes'][$i]['version'] = $match[1];
274
				}
275
			}
276
277
			$context['themes'][$i]['valid_path'] = $fileFunc->isDir($context['themes'][$i]['theme_dir']);
278
		}
279
280
		// Off to the template we go
281
		$context['sub_template'] = 'list_themes';
282
		theme()->addJavascriptVar(['txt_theme_remove_confirm' => $txt['theme_remove_confirm']], true);
283
		$context['reset_dir'] = realpath(BOARDDIR . '/themes');
284
		$context['reset_url'] = $boardurl . '/themes';
285
286
		createToken('admin-tl');
287
		createToken('admin-tr', 'request');
288
	}
289
290
	/**
291
	 * Administrative global settings.
292
	 *
293
	 * What it does:
294
	 *
295
	 * - Saves and requests global theme settings. ($settings)
296
	 * - Loads the Admin language file.
297
	 * - Calls action_admin() if no theme is specified. (the theme center.)
298
	 * - Requires admin_forum permission.
299
	 * - Accessed with ?action=admin;area=theme;sa=list&th=xx.
300
	 *
301
	 * @event integrate_init_theme
302
	 */
303
	public function action_setthemesettings(): void
304
	{
305
		global $txt, $context, $settings, $modSettings;
306
307
		require_once(SUBSDIR . '/Themes.subs.php');
308
		$fileFunc = FileFunctions::instance();
309
310
		// Nothing chosen, back to the start you go
311
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
312
		if (empty($theme))
313
		{
314
			redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
315
		}
316
317
		// The theme's ID is needed
318
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
319
320
		// Validate inputs/user.
321
		if (empty($theme))
322
		{
323
			throw new Exception('no_theme', false);
324
		}
325
326
		// Select the best fitting tab.
327
		$context[$context['admin_menu_name']]['current_subsection'] = 'list';
328
		Txt::load('Admin');
329
330
		// Fetch the smiley sets...
331
		$sets = explode(',', 'none,' . $modSettings['smiley_sets_known']);
332
		$set_names = explode("\n", $txt['smileys_none'] . "\n" . $modSettings['smiley_sets_names']);
333
		$context['smiley_sets'] = ['' => $txt['smileys_no_default']];
334
		foreach ($sets as $i => $set)
335
		{
336
			$context['smiley_sets'][$set] = htmlspecialchars($set_names[$i], ENT_COMPAT);
337
		}
338
339
		$old_id = $settings['theme_id'];
340
		$old_settings = $settings;
341
		$old_js_inline = $context['js_inline'];
342
343
		new ThemeLoader($theme, false);
344
345
		// Also load the actual themes language file - in case of special settings.
346
		Txt::load('Settings', false, true);
347
348
		// And the custom language strings...
349
		Txt::load('ThemeStrings', false);
350
351
		// Let the theme take care of the settings.
352
		theme()->getTemplates()->load('Settings');
353
		theme()->getTemplates()->loadSubTemplate('settings');
354
355
		// Load the variants separately...
356
		if ($fileFunc->fileExists($settings['theme_dir'] . '/index.template.php'))
357
		{
358
			$variants = theme()->getSettings();
359
			$settings['theme_variants'] = $variants['theme_variants'] ?? [];
360
			call_integration_hook('integrate_init_theme', [$theme, &$settings]);
361
		}
362
363
		// Submitting!
364
		if ($this->_req->hasPost('save'))
365
		{
366
			// Allowed?
367
			checkSession();
368
			validateToken('admin-sts');
369
370
			$options = [];
371
			$options['options'] = empty($this->_req->post->options) ? [] : (array) $this->_req->post->options;
372
			$options['default_options'] = empty($this->_req->post->default_options) ? [] : (array) $this->_req->post->default_options;
373
374
			// Make sure items are cast correctly.
375
			foreach ($context['theme_settings'] as $item)
376
			{
377
				// Unwatch this item if this is just a separator.
378
				if (!is_array($item))
379
				{
380
					continue;
381
				}
382
383
				// Clean them up for the database
384
				foreach (['options', 'default_options'] as $option)
385
				{
386
					if (!isset($options[$option][$item['id']]))
387
					{
388
						continue;
389
					}
390
391
					// Checkbox.
392
					if (empty($item['type']))
393
					{
394
						$options[$option][$item['id']] = $options[$option][$item['id']] ? 1 : 0;
395
					}
396
397
					// Number
398
					elseif ($item['type'] === 'number')
399
					{
400
						$options[$option][$item['id']] = (int) $options[$option][$item['id']];
401
					}
402
				}
403
			}
404
405
			// Set up the SQL query.
406
			$inserts = [];
407
			foreach ($options['options'] as $opt => $val)
408
			{
409
				$inserts[] = [$theme, 0, $opt, is_array($val) ? implode(',', $val) : $val];
410
			}
411
412
			foreach ($options['default_options'] as $opt => $val)
413
			{
414
				$inserts[] = [1, 0, $opt, is_array($val) ? implode(',', $val) : $val];
415
			}
416
417
			// If we're actually inserting something...
418
			if (!empty($inserts))
419
			{
420
				updateThemeOptions($inserts);
421
			}
422
423
			// Clear and Invalidate the cache.
424
			Cache::instance()->remove('theme_settings-' . $theme);
425
			Cache::instance()->remove('theme_settings-1');
426
			updateSettings(['settings_updated' => time()]);
427
428
			redirectexit('action=admin;area=theme;sa=list;th=' . $theme . ';' . $context['session_var'] . '=' . $context['session_id']);
429
		}
430
431
		$context['sub_template'] = 'set_settings';
432
		$context['page_title'] = $txt['theme_settings'];
433
434
		foreach ($settings as $setting => $set)
435
		{
436
			if (!in_array($setting, ['theme_url', 'theme_dir', 'images_url', 'template_dirs']))
437
			{
438
				$settings[$setting] = Util::htmlspecialchars__recursive($set);
439
			}
440
		}
441
442
		$context['settings'] = $context['theme_settings'];
443
		$context['theme_settings'] = $settings;
444
445
		foreach ($context['settings'] as $i => $setting)
446
		{
447
			// Separators are dummies, so leave them alone.
448
			if (!is_array($setting))
449
			{
450
				continue;
451
			}
452
453
			// Create the right input fields for the data
454
			if (!isset($setting['type']) || $setting['type'] === 'bool')
455
			{
456
				$context['settings'][$i]['type'] = 'checkbox';
457
			}
458
			elseif ($setting['type'] === 'int' || $setting['type'] === 'integer')
459
			{
460
				$context['settings'][$i]['type'] = 'number';
461
			}
462
			elseif ($setting['type'] === 'string')
463
			{
464
				$context['settings'][$i]['type'] = 'text';
465
			}
466
467
			if (isset($setting['options']))
468
			{
469
				$context['settings'][$i]['type'] = 'list';
470
			}
471
472
			$context['settings'][$i]['value'] = $settings[$setting['id']] ?? '';
473
		}
474
475
		// Do we support variants?
476
		if (!empty($settings['theme_variants']))
477
		{
478
			$context['theme_variants'] = [];
479
			foreach ($settings['theme_variants'] as $variant)
480
			{
481
				// Have any text, old chap?
482
				$context['theme_variants'][$variant] = [
483
					'label' => $txt['variant_' . $variant] ?? $variant,
484
					'thumbnail' => !$fileFunc->fileExists($settings['theme_dir'] . '/images/theme/thumbnail.png') || $fileFunc->fileExists($settings['theme_dir'] . '/images/themes/thumbnail_' . $variant . '.png') ? $settings['images_url'] . '/thumbnail_' . $variant . '.png' : ($settings['images_url'] . '/thumbnail.png'),
485
				];
486
			}
487
488
			$context['default_variant'] = !empty($settings['default_variant']) && isset($context['theme_variants'][$settings['default_variant']]) ? $settings['default_variant'] : $settings['theme_variants'][0];
489
		}
490
491
		// Restore the current theme.
492
		new ThemeLoader($old_id, true);
493
494
		$settings = $old_settings;
495
		$context['js_inline'] = $old_js_inline;
496
497
		// Reinit just incase.
498
		theme()->getSettings();
499
500
		theme()->getTemplates()->load('ManageThemes');
501
502
		createToken('admin-sts');
503
	}
504
505
	/**
506
	 * This function allows administration of themes and their settings,
507
	 * as well as global theme settings.
508
	 *
509
	 * What it does:
510
	 *
511
	 * - Sets the settings theme_allow, theme_guests, and knownThemes.
512
	 * - Requires the admin_forum permission.
513
	 * - Accessed with ?action=admin;area=theme;sa=admin.
514
	 *
515
	 * @uses Themes template
516
	 * @uses Admin language file
517
	 */
518
	public function action_admin(): void
519
	{
520
		global $context, $modSettings;
521
522
		Txt::load('Admin');
523
524
		// Saving?
525
		if ($this->_req->hasPost('save'))
526
		{
527
			checkSession();
528
			validateToken('admin-tm');
529
530
			// What themes are being made as known to the members
531
			if (isset($this->_req->post->options['known_themes']))
532
			{
533
				foreach ($this->_req->post->options['known_themes'] as $key => $id)
534
				{
535
					$this->_req->post->options['known_themes'][$key] = (int) $id;
536
				}
537
			}
538
			else
539
			{
540
				throw new Exception('themes_none_selectable', false);
541
			}
542
543
			if (!in_array($this->_req->post->options['theme_guests'], $this->_req->post->options['known_themes']))
544
			{
545
				throw new Exception('themes_default_selectable', false);
546
			}
547
548
			// Commit the new settings.
549
			updateSettings([
550
				'theme_allow' => !empty($this->_req->post->options['theme_allow']),
551
				'theme_guests' => $this->_req->post->options['theme_guests'],
552
				'knownThemes' => implode(',', $this->_req->post->options['known_themes']),
553
			]);
554
555
			if ((int) $this->_req->post->theme_reset === 0 || in_array($this->_req->post->theme_reset, $this->_req->post->options['known_themes']))
556
			{
557
				require_once(SUBSDIR . '/Members.subs.php');
558
				updateMemberData(null, ['id_theme' => (int) $this->_req->post->theme_reset]);
559
			}
560
561
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=admin');
562
		}
563
		// If we aren't submitting - that is, if we are about to...
564
		else
565
		{
566
			$fileFunc = FileFunctions::instance();
567
568
			theme()->getTemplates()->load('ManageThemes');
569
			$context['sub_template'] = 'manage_themes';
570
571
			// Make our known themes a little easier to work with.
572
			$knownThemes = empty($modSettings['knownThemes']) ? [] : explode(',', $modSettings['knownThemes']);
573
574
			// Load up all the themes.
575
			require_once(SUBSDIR . '/Themes.subs.php');
576
			$context['themes'] = loadThemes($knownThemes);
0 ignored issues
show
Bug introduced by
It seems like $knownThemes can also be of type string[]; however, parameter $knownThemes of loadThemes() does only seem to accept integer[], 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

576
			$context['themes'] = loadThemes(/** @scrutinizer ignore-type */ $knownThemes);
Loading history...
577
578
			// Can we create a new theme?
579
			$context['can_create_new'] = $fileFunc->isWritable(BOARDDIR . '/themes');
580
			$context['new_theme_dir'] = substr(realpath(BOARDDIR . '/themes/default'), 0, -7);
581
582
			// Look for a nonexistent theme directory. (i.e., theme87.)
583
			$theme_dir = BOARDDIR . '/themes/theme';
584
			$i = 1;
585
			while ($fileFunc->isDir($theme_dir . $i))
586
			{
587
				$i++;
588
			}
589
590
			$context['new_theme_name'] = 'theme' . $i;
591
592
			createToken('admin-tm');
593
		}
594
	}
595
596
	/**
597
	 * Administrative global settings.
598
	 *
599
	 * - Accessed by ?action=admin;area=theme;sa=reset;
600
	 *
601
	 * @uses sub template set_options, template file Settings
602
	 * @uses template file ManageThemes
603
	 */
604
	public function action_options(): void
605
	{
606
		global $txt, $context, $settings, $modSettings;
607
608
		require_once(SUBSDIR . '/Themes.subs.php');
609
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
610
611
		// No theme selected, so show the theme list with theme option count and member count not using default
612
		if (empty($theme))
613
		{
614
			$context['themes'] = installedThemes();
615
616
			// How many options do we have set for guests?
617
			$guestOptions = countConfiguredGuestOptions();
618
			foreach ($guestOptions as $guest_option)
619
			{
620
				$context['themes'][$guest_option['id_theme']]['num_default_options'] = $guest_option['value'];
621
			}
622
623
			// How many options do we have set for members?
624
			$memberOptions = countConfiguredMemberOptions();
625
			foreach ($memberOptions as $member_option)
626
			{
627
				$context['themes'][$member_option['id_theme']]['num_members'] = $member_option['value'];
628
			}
629
630
			// There has to be a Settings template!
631
			$fileFunc = FileFunctions::instance();
632
			foreach ($context['themes'] as $k => $v)
633
			{
634
				if (empty($v['theme_dir']) || (!$fileFunc->fileExists($v['theme_dir'] . '/Settings.template.php') && empty($v['num_members'])))
635
				{
636
					unset($context['themes'][$k]);
637
				}
638
			}
639
640
			theme()->getTemplates()->load('ManageThemes');
641
			$context['sub_template'] = 'reset_list';
642
643
			createToken('admin-stor', 'request');
644
645
			return;
646
		}
647
648
		// Submit?
649
		$who = $this->_req->getPost('who', 'intval', 0);
650
		if (isset($this->_req->post->submit) && empty($who))
651
		{
652
			checkSession();
653
			validateToken('admin-sto');
654
655
			$_options = $this->_req->getPost('options', '', []);
656
			$_default_options = $this->_req->getPost('default_options', '', []);
657
658
			// Set up the query values.
659
			$setValues = [];
660
			foreach ($_options as $opt => $val)
661
			{
662
				$setValues[] = [$theme, -1, $opt, is_array($val) ? implode(',', $val) : $val];
663
			}
664
665
			$old_settings = [];
666
			foreach ($_default_options as $opt => $val)
667
			{
668
				$old_settings[] = $opt;
669
				$setValues[] = [1, -1, $opt, is_array($val) ? implode(',', $val) : $val];
670
			}
671
672
			// If we're actually inserting something...
673
			if (!empty($setValues))
674
			{
675
				// Are there options in non-default themes set that should be cleared?
676
				if (!empty($old_settings))
677
				{
678
					removeThemeOptions('custom', 'guests', $old_settings);
679
				}
680
681
				updateThemeOptions($setValues);
682
			}
683
684
			// Cache the theme settings
685
			Cache::instance()->remove('theme_settings-' . $theme);
686
			Cache::instance()->remove('theme_settings-1');
687
688
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset');
689
		}
690
691
		// Changing the current options for all members using this theme
692
		if (isset($this->_req->post->submit) && $who === 1)
693
		{
694
			checkSession();
695
			validateToken('admin-sto');
696
697
			$_options = $this->_req->getPost('options', '', []);
698
			$_options_master = $this->_req->getPost('options_master', '', []);
699
700
			$_default_options = $this->_req->getPost('default_options', '', []);
701
			$_default_options_master = $this->_req->getPost('default_options_master', '', []);
702
703
			$old_settings = [];
704
			foreach ($_default_options as $opt => $val)
705
			{
706
				if ($_default_options_master[$opt] == 0)
707
				{
708
					continue;
709
				}
710
711
				if ($_default_options_master[$opt] == 1)
712
				{
713
					// Delete it then insert for ease of database compatibility!
714
					removeThemeOptions('default', 'members', $opt);
715
					addThemeOptions(1, $opt, $val);
716
717
					$old_settings[] = $opt;
718
				}
719
				elseif ($_default_options_master[$opt] == 2)
720
				{
721
					removeThemeOptions('all', 'members', $opt);
722
				}
723
			}
724
725
			// Delete options from other themes.
726
			if (!empty($old_settings))
727
			{
728
				removeThemeOptions('custom', 'members', $old_settings);
729
			}
730
731
			foreach ($_options as $opt => $val)
732
			{
733
				if ($_options_master[$opt] == 0)
734
				{
735
					continue;
736
				}
737
738
				if ($_options_master[$opt] == 1)
739
				{
740
					// Delete it, then insert for ease of database compatibility - again!
741
					removeThemeOptions($theme, 'non_default', $opt);
742
					addThemeOptions($theme, $opt, $val);
743
				}
744
				elseif ($_options_master[$opt] == 2)
745
				{
746
					removeThemeOptions($theme, 'all', $opt);
747
				}
748
			}
749
750
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset');
751
		}
752
753
		// Remove all members options and use the defaults
754
		if ($this->_req->hasQuery('who') && $who === 2)
755
		{
756
			checkSession('get');
757
			validateToken('admin-stor', 'request');
758
759
			removeThemeOptions($theme, 'members');
760
761
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset');
762
		}
763
764
		$old_id = $settings['theme_id'];
765
		$old_settings = $settings;
766
767
		new ThemeLoader($theme, false);
768
		Txt::load('Profile');
769
770
		// @todo Should we just move these options so they are no longer theme dependant?
771
		Txt::load('PersonalMessage');
772
773
		// Let the theme take care of the settings.
774
		theme()->getTemplates()->load('Settings');
775
		theme()->getTemplates()->loadSubTemplate('options');
776
777
		// Set up for the template
778
		$context['sub_template'] = 'set_options';
779
		$context['page_title'] = $txt['theme_settings'];
780
		$context['options'] = $context['theme_options'];
781
		$context['theme_settings'] = $settings;
782
783
		// Load the options for these themes
784
		if (!$this->_req->hasQuery('who'))
785
		{
786
			$context['theme_options'] = loadThemeOptionsInto([1, $theme], -1, $context['theme_options']);
787
			$context['theme_options_reset'] = false;
788
		}
789
		else
790
		{
791
			$context['theme_options'] = [];
792
			$context['theme_options_reset'] = true;
793
		}
794
795
		// Prepare the options for the template
796
		foreach ($context['options'] as $i => $setting)
797
		{
798
			// Is this disabled?
799
			if ($setting['id'] === 'calendar_start_day' && empty($modSettings['cal_enabled']))
800
			{
801
				unset($context['options'][$i]);
802
				continue;
803
			}
804
			if (($setting['id'] === 'topics_per_page' || $setting['id'] === 'messages_per_page') && !empty($modSettings['disableCustomPerPage']))
805
			{
806
				unset($context['options'][$i]);
807
				continue;
808
			}
809
810
			// Type of field so we display the right input field
811
			if (!isset($setting['type']) || $setting['type'] === 'bool')
812
			{
813
				$context['options'][$i]['type'] = 'checkbox';
814
			}
815
			elseif ($setting['type'] === 'int' || $setting['type'] === 'integer')
816
			{
817
				$context['options'][$i]['type'] = 'number';
818
			}
819
			elseif ($setting['type'] === 'string')
820
			{
821
				$context['options'][$i]['type'] = 'text';
822
			}
823
824
			if (isset($setting['options']))
825
			{
826
				$context['options'][$i]['type'] = 'list';
827
			}
828
829
			$context['options'][$i]['value'] = $context['theme_options'][$setting['id']] ?? '';
830
		}
831
832
		// Restore the existing theme and its settings.
833
		new ThemeLoader($old_id, true);
834
		$settings = $old_settings;
835
836
		theme()->getTemplates()->load('ManageThemes');
837
		createToken('admin-sto');
838
	}
839
840
	/**
841
	 * Remove a theme from the database.
842
	 *
843
	 * What it does:
844
	 *
845
	 * - Removes an installed theme.
846
	 * - Requires an administrator.
847
	 * - Accessed with ?action=admin;area=theme;sa=remove.
848
	 * - Does not remove files
849
	 */
850
	public function action_remove(): void
851
	{
852
		global $modSettings, $context;
853
854
		require_once(SUBSDIR . '/Themes.subs.php');
855
856
		checkSession('get');
857
		validateToken('admin-tr', 'request');
858
859
		// The theme's ID must be an integer.
860
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
861
862
		// You can't delete the default theme!
863
		if ($theme === 1)
864
		{
865
			throw new Exception('no_access', false);
866
		}
867
868
		// It's no longer known
869
		$known = $this->_knownTheme($theme);
870
871
		// Remove it as an option everywhere
872
		deleteTheme($theme);
873
874
		// Fix it if the theme was the overall default theme.
875
		if ($modSettings['theme_guests'] === $theme)
876
		{
877
			updateSettings(['theme_guests' => '1', 'knownThemes' => $known]);
878
		}
879
		else
880
		{
881
			updateSettings(['knownThemes' => $known]);
882
		}
883
884
		redirectexit('action=admin;area=theme;sa=list;' . $context['session_var'] . '=' . $context['session_id']);
885
	}
886
887
	/**
888
	 * Small helper to return the list of known themes other than the current
889
	 *
890
	 * @param string $theme current theme
891
	 * @return string
892
	 */
893
	private function _knownTheme(string $theme): string
894
	{
895
		global $modSettings;
896
897
		$known = explode(',', $modSettings['knownThemes']);
898
		foreach ($known as $i => $knew)
899
		{
900
			if ($knew === $theme)
901
			{
902
				// I knew them at one time
903
				unset($known[$i]);
904
			}
905
		}
906
907
		return strtr(implode(',', $known), [',,' => ',']);
908
	}
909
910
	/**
911
	 * Remove a theme from the database in response to an ajax api request
912
	 *
913
	 * What it does:
914
	 *
915
	 * - Removes an installed theme.
916
	 * - Requires an administrator.
917
	 * - Accessed with ?action=admin;area=theme;sa=remove;api
918
	 */
919
	public function action_remove_api(): void
920
	{
921
		global $modSettings, $context, $txt;
922
923
		require_once(SUBSDIR . '/Themes.subs.php');
924
925
		// Validate what was sent
926
		if (checkSession('get', '', false))
927
		{
928
			Txt::load('Errors');
929
			$context['xml_data'] = [
930
				'error' => 1,
931
				'text' => $txt['session_verify_fail'],
932
			];
933
934
			return;
935
		}
936
937
		// Not just any John Smith can send in an api request
938
		if (!allowedTo('admin_forum'))
939
		{
940
			Txt::load('Errors');
941
			$context['xml_data'] = [
942
				'error' => 1,
943
				'text' => $txt['cannot_admin_forum'],
944
			];
945
946
			return;
947
		}
948
949
		// Even if you are John Smith, you still need a ticket
950
		if (!validateToken('admin-tr', 'request', true, false))
951
		{
952
			Txt::load('Errors');
953
			$context['xml_data'] = [
954
				'error' => 1,
955
				'text' => $txt['token_verify_fail'],
956
			];
957
958
			return;
959
		}
960
961
		// The theme's ID must be an integer.
962
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
963
964
		// You can't delete the default theme!
965
		if ($theme === 1)
966
		{
967
			Txt::load('Errors');
968
			$context['xml_data'] = [
969
				'error' => 1,
970
				'text' => $txt['no_access'],
971
			];
972
973
			return;
974
		}
975
976
		// It is a theme we know about?
977
		$known = $this->_knownTheme($theme);
978
979
		// Finally, remove it
980
		deleteTheme($theme);
981
982
		// Fix it if the theme was the overall default theme.
983
		if ($modSettings['theme_guests'] === $theme)
984
		{
985
			updateSettings(['theme_guests' => '1', 'knownThemes' => $known]);
986
		}
987
		else
988
		{
989
			updateSettings(['knownThemes' => $known]);
990
		}
991
992
		// Let them know it worked, all without a page refresh
993
		createToken('admin-tr', 'request');
994
		$context['xml_data'] = [
995
			'success' => 1,
996
			'token_var' => $context['admin-tr_token_var'],
997
			'token' => $context['admin-tr_token'],
998
		];
999
	}
1000
1001
	/**
1002
	 * Choose a theme from a list.
1003
	 * Allows a user or administrator to pick a new theme with an interface.
1004
	 *
1005
	 * What it does:
1006
	 *
1007
	 * - Can edit everyone's (u = 0) or guests' (u = -1).
1008
	 * - Uses the Themes template. (pick sub template.)
1009
	 * - Accessed with ?action=admin;area=theme;sa=pick.
1010
	 *
1011
	 * @uses Profile language text
1012
	 * @uses ManageThemes template
1013
	 * with centralized admin permissions on ManageThemes.
1014
	 */
1015
	public function action_pick(): void
1016
	{
1017
		global $txt, $context, $modSettings;
1018
1019
		require_once(SUBSDIR . '/Themes.subs.php');
1020
1021
		theme()->getTemplates()->load('ManageThemes');
1022
1023
		// 0 is reset all members, -1 is set forum default
1024
		$u = $this->_req->getQuery('u', 'intval');
1025
		$id = $this->_req->getQuery('id', 'intval');
1026
		$save = $this->_req->getPost('save');
1027
		$themePicked = $this->_req->getQuery('th', 'intval');
1028
		$variant = $this->_req->getQuery('vrt', 'Util::htmlspecialchars');
1029
1030
		$context['default_theme_id'] = $modSettings['theme_default'];
1031
1032
		$_SESSION['theme'] = 0;
1033
1034
		if (isset($id))
1035
		{
1036
			$themePicked = $id;
1037
		}
1038
1039
		// Saving a variant cause JS doesn't work - pretend it did ;)
1040
		if (isset($save))
1041
		{
1042
			// Which theme?
1043
			foreach ($save as $k => $v)
1044
			{
1045
				$themePicked = (int) $k;
1046
			}
1047
1048
			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 1043. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1049
			{
1050
				$variant = $this->_req->post->vrt[$k];
1051
			}
1052
		}
1053
1054
		// Have we made a decision, or are we just browsing?
1055
		if (isset($themePicked))
1056
		{
1057
			checkSession('get');
1058
1059
			//$th = $this->_req->getQuery('th', 'intval');
1060
			//$vrt = $this->_req->getQuery('vrt', 'Util::htmlspecialchars');
1061
1062
			// If changing members or guests - and there's a variant - assume changing the default variant.
1063
			if (!empty($variant) && ($u === 0 || $u === -1))
1064
			{
1065
				updateThemeOptions([$themePicked, 0, 'default_variant', $variant]);
1066
1067
				// Make it obvious that it's changed
1068
				Cache::instance()->remove('theme_settings-' . $themePicked);
1069
			}
1070
1071
			// For everyone.
1072
			if ($u === 0)
1073
			{
1074
				require_once(SUBSDIR . '/Members.subs.php');
1075
				updateMemberData(null, ['id_theme' => $themePicked]);
1076
1077
				// Remove any custom variants.
1078
				if (!empty($variant))
1079
				{
1080
					deleteVariants($themePicked);
1081
				}
1082
1083
				redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
1084
			}
1085
			// Change the default/guest theme.
1086
			elseif ($u === -1)
1087
			{
1088
				updateSettings(['theme_guests' => $themePicked]);
1089
1090
				redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
1091
			}
1092
		}
1093
1094
		$current_theme = 0;
1095
		if ($u === 0)
1096
		{
1097
			$context['current_member'] = 0;
1098
		}
1099
		// Guests and such...
1100
		elseif ($u === -1)
1101
		{
1102
			$context['current_member'] = -1;
1103
		}
1104
1105
		// Get the theme name and descriptions.
1106
		[$context['available_themes'], $guest_theme] = availableThemes($current_theme, $context['current_member']);
1107
1108
		// As long as we're not doing the default theme...
1109
		if (!isset($u) || $u >= 0)
1110
		{
1111
			if ($guest_theme !== 0)
1112
			{
1113
				$context['available_themes'][0] = $context['available_themes'][$guest_theme];
1114
			}
1115
1116
			$context['available_themes'][0]['id'] = 0;
1117
			$context['available_themes'][0]['name'] = $txt['theme_forum_default'];
1118
			$context['available_themes'][0]['selected'] = $current_theme === 0;
1119
			$context['available_themes'][0]['description'] = $txt['theme_global_description'];
1120
		}
1121
1122
		ksort($context['available_themes']);
1123
1124
		$context['page_title'] = $txt['theme_pick'];
1125
		$context['sub_template'] = 'pick';
1126
	}
1127
1128
	/**
1129
	 * Installs new themes, either from a gzip or copy of the default.
1130
	 *
1131
	 * What it does:
1132
	 *
1133
	 * - Puts themes in $boardurl/themes.
1134
	 * - Assumes the gzip has a root directory in it. (i.e., default.)
1135
	 * - Requires admin_forum.
1136
	 * - Accessed with ?action=admin;area=theme;sa=install.
1137
	 *
1138
	 * @uses ManageThemes template
1139
	 */
1140
	public function action_install()
1141
	{
1142
		global $boardurl, $txt, $context, $settings, $modSettings;
1143
1144
		checkSession('request');
1145
1146
		require_once(SUBSDIR . '/Themes.subs.php');
1147
		require_once(SUBSDIR . '/Package.subs.php');
1148
		$fileFunc = FileFunctions::instance();
1149
1150
		theme()->getTemplates()->load('ManageThemes');
1151
1152
		// Passed an ID, then the installation is complete, let's redirect and show them
1153
		$theme_id = $this->_req->getQuery('theme_id', 'intval');
1154
		if ($theme_id !== null)
1155
		{
1156
			$context['sub_template'] = 'installed';
1157
			$context['page_title'] = $txt['theme_installed'];
1158
			$context['installed_theme'] = [
1159
				'id' => $theme_id,
1160
				'name' => getThemeName($theme_id),
1161
			];
1162
1163
			return null;
1164
		}
1165
1166
		// How are we going to install this theme, from a dir, zip, copy of default?
1167
		if ((!empty($_FILES['theme_gz']) && (!isset($_FILES['theme_gz']['error']) || $_FILES['theme_gz']['error'] != 4)) || !empty($this->_req->query->theme_gz))
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! empty($_FILES['theme_...>_req->query->theme_gz), Probably Intended Meaning: ! empty($_FILES['theme_g..._req->query->theme_gz))
Loading history...
1168
		{
1169
			$method = 'upload';
1170
		}
1171
		elseif (isset($this->_req->post->theme_dir) && rtrim(realpath($this->_req->post->theme_dir), '/\\') != realpath(BOARDDIR . '/themes') && $fileFunc->isDir($this->_req->post->theme_dir))
1172
		{
1173
			$method = 'path';
1174
		}
1175
		else
1176
		{
1177
			$method = 'copy';
1178
		}
1179
1180
		// Copy the default theme?
1181
		if (!empty($this->_req->post->copy) && $method === 'copy')
1182
		{
1183
			$this->copyDefault();
1184
		}
1185
		// Install from another directory
1186
		elseif (isset($this->_req->post->theme_dir) && $method === 'path')
1187
		{
1188
			$this->installFromDir();
1189
		}
1190
		// Uploaded a zip file to install from
1191
		elseif ($method === 'upload')
1192
		{
1193
			$this->installFromZip();
1194
		}
1195
		else
1196
		{
1197
			throw new Exception('theme_install_general', false);
1198
		}
1199
1200
		// Something go wrong?
1201
		if ($this->theme_dir !== '' && basename($this->theme_dir) !== 'themes')
1202
		{
1203
			// Defaults.
1204
			$install_info = [
1205
				'theme_url' => $boardurl . '/themes/' . basename($this->theme_dir),
1206
				'images_url' => $this->images_url ?? $boardurl . '/themes/' . basename($this->theme_dir) . '/images',
1207
				'theme_dir' => $this->theme_dir,
1208
				'name' => $this->theme_name
1209
			];
1210
			$explicit_images = false;
1211
1212
			if ($fileFunc->fileExists($this->theme_dir . '/theme_info.xml'))
1213
			{
1214
				$theme_info = file_get_contents($this->theme_dir . '/theme_info.xml');
1215
1216
				// Parse theme-info.xml into an \ElkArte\XmlArray.
1217
				$theme_info_xml = new XmlArray($theme_info);
1218
1219
				// @todo Error message of some sort?
1220
				if (!$theme_info_xml->exists('theme-info[0]'))
1221
				{
1222
					return 'package_get_error_packageinfo_corrupt';
1223
				}
1224
1225
				$theme_info_xml = $theme_info_xml->path('theme-info[0]');
1226
				$theme_info_xml = $theme_info_xml->to_array();
1227
1228
				$xml_elements = [
1229
					'name' => 'name',
1230
					'theme_layers' => 'layers',
1231
					'theme_templates' => 'templates',
1232
					'based_on' => 'based-on',
1233
				];
1234
				foreach ($xml_elements as $var => $name)
1235
				{
1236
					if (!empty($theme_info_xml[$name]))
1237
					{
1238
						$install_info[$var] = $theme_info_xml[$name];
1239
					}
1240
				}
1241
1242
				if (!empty($theme_info_xml['images']))
1243
				{
1244
					$install_info['images_url'] = $install_info['theme_url'] . '/' . $theme_info_xml['images'];
1245
					$explicit_images = true;
1246
				}
1247
1248
				if (!empty($theme_info_xml['extra']))
1249
				{
1250
					$install_info += Util::unserialize($theme_info_xml['extra']);
1251
				}
1252
			}
1253
1254
			if (isset($install_info['based_on']))
1255
			{
1256
				if ($install_info['based_on'] === 'default')
1257
				{
1258
					$install_info['theme_url'] = $settings['default_theme_url'];
1259
					$install_info['images_url'] = $settings['default_images_url'];
1260
				}
1261
				elseif ($install_info['based_on'] != '')
1262
				{
1263
					$install_info['based_on'] = preg_replace('~[^A-Za-z0-9\-_ ]~', '', $install_info['based_on']);
1264
1265
					$temp = loadBasedOnTheme($install_info['based_on'], $explicit_images);
1266
1267
					// @todo An error otherwise?
1268
					if (is_array($temp))
1269
					{
1270
						$install_info = $temp + $install_info;
1271
1272
						if ($explicit_images === false && !empty($install_info['base_theme_url']))
1273
						{
1274
							$install_info['theme_url'] = $install_info['base_theme_url'];
1275
						}
1276
					}
1277
				}
1278
1279
				unset($install_info['based_on']);
1280
			}
1281
1282
			// Find the newest id_theme.
1283
			$id_theme = nextTheme();
1284
1285
			$inserts = [];
1286
			foreach ($install_info as $var => $val)
1287
			{
1288
				$inserts[] = [$id_theme, $var, $val];
1289
			}
1290
1291
			if (!empty($inserts))
1292
			{
1293
				addTheme($inserts);
1294
			}
1295
1296
			updateSettings(['knownThemes' => strtr($modSettings['knownThemes'] . ',' . $id_theme, [',,' => ','])]);
1297
1298
			redirectexit('action=admin;area=theme;sa=install;theme_id=' . $id_theme . ';' . $context['session_var'] . '=' . $context['session_id']);
1299
		}
1300
1301
		redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
1302
1303
		return null;
1304
	}
1305
1306
	/**
1307
	 * Make a copy of the default theme in a new directory
1308
	 */
1309
	public function copyDefault(): void
1310
	{
1311
		global $boardurl, $settings;
1312
1313
		$fileFunc = FileFunctions::instance();
1314
1315
		// Hopefully, the theme directory is writable, or we might have a problem.
1316
		if (!$fileFunc->chmod(BOARDDIR . '/themes'))
1317
		{
1318
			throw new Exception('theme_install_write_error', 'critical');
1319
		}
1320
1321
		// Make the new directory, standard characters only
1322
		$new_theme_name = preg_replace('~[^A-Za-z0-9_\- ]~', '', $this->_req->post->copy);
1323
		$this->theme_dir = BOARDDIR . '/themes/' . $new_theme_name;
1324
		$fileFunc->createDirectory($this->theme_dir, false);
1325
1326
		// Get some more time if we can
1327
		detectServer()->setTimeLimit(600);
1328
1329
		// Create the subdirectories for CSS, JavaScript and font files.
1330
		$fileFunc->createDirectory($this->theme_dir . '/css', false);
1331
		$fileFunc->createDirectory($this->theme_dir . '/scripts', false);
1332
		$fileFunc->createDirectory($this->theme_dir . '/webfonts', false);
1333
1334
		// Copy over the default non-theme files.
1335
		$to_copy = ['/index.php', '/index.template.php', '/scripts/theme.js', '/Theme.php'];
1336
		foreach ($to_copy as $file)
1337
		{
1338
			copy($settings['default_theme_dir'] . $file, $this->theme_dir . $file);
1339
			$fileFunc->chmod($this->theme_dir . $file);
1340
		}
1341
1342
		// And now the entire CSS, images and webfonts directories!
1343
		copytree($settings['default_theme_dir'] . '/css', $this->theme_dir . '/css');
1344
		copytree($settings['default_theme_dir'] . '/images', $this->theme_dir . '/images');
1345
		copytree($settings['default_theme_dir'] . '/webfonts', $this->theme_dir . '/webfonts');
1346
		package_flush_cache();
1347
1348
		$this->theme_name = $this->_req->post->copy;
1349
		$this->images_url = $boardurl . '/themes/' . basename($this->theme_dir) . '/images';
1350
		$this->theme_dir = realpath($this->theme_dir);
1351
1352
		// Let's get some data for the new theme (default theme (1), default settings (0)).
1353
		$theme_values = loadThemeOptionsInto(1, 0, [], ['theme_templates', 'theme_layers']);
1354
1355
		// Let's add a theme_info.xml to this theme.
1356
		write_theme_info($this->_req->post->copy, FORUM_VERSION, $this->theme_dir, $theme_values);
1357
1358
		// Finish by setting the namespace
1359
		$theme = file_get_contents($this->theme_dir . '/Theme.php');
1360
		$theme = str_replace('namespace ElkArte\Themes\DefaultTheme;', 'namespace ElkArte\Themes\\' . $new_theme_name . ';', $theme);
1361
		file_put_contents($this->theme_dir . '/Theme.php', $theme);
1362
	}
1363
1364
	/**
1365
	 * Install a theme from a directory on the server
1366
	 *
1367
	 * - Expects the directory is properly loaded with theme files
1368
	 */
1369
	public function installFromDir(): void
1370
	{
1371
		$fileFunc = FileFunctions::instance();
1372
1373
		if (!$fileFunc->isDir($this->_req->post->theme_dir) || !$fileFunc->fileExists($this->_req->post->theme_dir . '/theme_info.xml'))
1374
		{
1375
			throw new Exception('theme_install_error', false);
1376
		}
1377
1378
		$this->theme_name = basename($this->_req->post->theme_dir);
1379
		$this->theme_dir = $this->_req->post->theme_dir;
1380
	}
1381
1382
	/**
1383
	 * Install a new theme from an uploaded zip archive
1384
	 */
1385
	public function installFromZip(): void
1386
	{
1387
		$fileFunc = FileFunctions::instance();
1388
1389
		// Hopefully, the theme directory is writable, or we might have a problem.
1390
		if (!$fileFunc->chmod(BOARDDIR . '/themes'))
1391
		{
1392
			throw new Exception('theme_install_write_error', 'critical');
1393
		}
1394
1395
		// This happens when the admin session is gone and the user has to log in again
1396
		if (empty($_FILES['theme_gz']) && empty($this->_req->post->theme_gz))
1397
		{
1398
			return;
1399
		}
1400
1401
		// Set the default settings...
1402
		$this->theme_name = strtok(basename(isset($_FILES['theme_gz']) ? $_FILES['theme_gz']['name'] : $this->_req->post->theme_gz), '.');
1403
		$this->theme_name = preg_replace(['/\s/', '/\.[\.]+/', '/[^\w_\.\-]/'], ['_', '.', ''], $this->theme_name);
1404
1405
		$this->theme_dir = BOARDDIR . '/themes/' . $this->theme_name;
1406
1407
		if (isset($_FILES['theme_gz']) && is_uploaded_file($_FILES['theme_gz']['tmp_name']) && (ini_get('open_basedir') != '' || $fileFunc->fileExists($_FILES['theme_gz']['tmp_name'])))
1408
		{
1409
			read_tgz_file($_FILES['theme_gz']['tmp_name'], BOARDDIR . '/themes/' . $this->theme_name, false, true);
1410
		}
1411
		elseif (isset($this->_req->post->theme_gz))
1412
		{
1413
			read_tgz_file($this->_req->post->theme_gz, BOARDDIR . '/themes/' . $this->theme_name, false, true);
1414
		}
1415
	}
1416
1417
	/**
1418
	 * Set a theme option via JavaScript.
1419
	 *
1420
	 * What it does:
1421
	 *
1422
	 * - Sets a theme option without outputting anything.
1423
	 * - Can be used with JavaScript, via a dummy image... (which doesn't require
1424
	 *   the page to reload.)
1425
	 * - Requires someone who is logged in.
1426
	 * - Accessed via ?action=jsoption;var=variable;val=value;session_var=sess_id.
1427
	 * - Optionally contains &th=theme id
1428
	 * - Does not log access to the Who's Online log. (in index.php...)
1429
	 */
1430
	public function action_jsoption(): void
1431
	{
1432
		global $settings, $options;
1433
1434
		// Check the session id.
1435
		checkSession('get');
1436
1437
		// This good-for-nothing pixel is being used to keep the session alive.
1438
		$var = $this->_req->getQuery('var', 'trim|strval');
1439
		// Note: val could be a string or array depending on the option
1440
		$val = $this->_req->getQuery('val');
1441
		if ($var === null || $val === null)
1442
		{
1443
			redirectexit($settings['images_url'] . '/blank.png');
1444
		}
1445
1446
		// Sorry, guests can't go any further than this...
1447
		if ($this->user->is_guest || $this->user->id == 0)
1448
		{
1449
			obExit(false);
1450
		}
1451
1452
		$reservedVars = [
1453
			'actual_theme_url',
1454
			'actual_images_url',
1455
			'base_theme_dir',
1456
			'base_theme_url',
1457
			'default_images_url',
1458
			'default_theme_dir',
1459
			'default_theme_url',
1460
			'default_template',
1461
			'images_url',
1462
			'number_recent_posts',
1463
			'smiley_sets_default',
1464
			'theme_dir',
1465
			'theme_id',
1466
			'theme_layers',
1467
			'theme_templates',
1468
			'theme_url',
1469
			'name',
1470
		];
1471
1472
		// Can't change reserved vars.
1473
		if (in_array(strtolower($var), $reservedVars))
1474
		{
1475
			redirectexit($settings['images_url'] . '/blank.png');
1476
		}
1477
1478
		// Use a specific theme?
1479
		if ($this->_req->hasQuery('th') || $this->_req->hasQuery('id'))
1480
		{
1481
			// Invalidate the current themes cache too.
1482
			Cache::instance()->remove('theme_settings-' . $settings['theme_id'] . ':' . $this->user->id);
1483
1484
			$settings['theme_id'] = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval'));
1485
		}
1486
1487
		// If this is the admin preferences, the passed value will just be an element of it.
1488
		if ($var === 'admin_preferences')
1489
		{
1490
			if (!empty($options['admin_preferences']))
1491
			{
1492
				$options['admin_preferences'] = serializeToJson($options['admin_preferences'], static function ($array_form) {
1493
					global $context;
1494
1495
					$context['admin_preferences'] = $array_form;
1496
					require_once(SUBSDIR . '/Admin.subs.php');
1497
					updateAdminPreferences();
1498
				});
1499
			}
1500
			else
1501
			{
1502
				$options['admin_preferences'] = [];
1503
			}
1504
1505
			// New thingy...
1506
			$admin_key = $this->_req->getQuery('admin_key', 'trim|strval');
1507
			if ($admin_key !== null && strlen($admin_key) < 5)
1508
			{
1509
				$options['admin_preferences'][$admin_key] = $val;
1510
			}
1511
1512
			// Change the value to be something nice,
1513
			$val = json_encode($options['admin_preferences']);
1514
		}
1515
		// If this is the window min/max settings, the passed window name will just be an element of it.
1516
		elseif ($var === 'minmax_preferences')
1517
		{
1518
			if (!empty($options['minmax_preferences']))
1519
			{
1520
				$minmax_preferences = serializeToJson($options['minmax_preferences'], static function ($array_form) use ($settings) {
1521
					// Update the option.
1522
					require_once(SUBSDIR . '/Themes.subs.php');
1523
					updateThemeOptions([$settings['theme_id'], User::$info->id, 'minmax_preferences', json_encode($array_form)]);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1524
				});
1525
			}
1526
			else
1527
			{
1528
				$minmax_preferences = [];
1529
			}
1530
1531
			if (!is_array($minmax_preferences))
1532
			{
1533
				$minmax_preferences = [];
1534
			}
1535
1536
			// New value for them
1537
			$minmax_key = $this->_req->getQuery('minmax_key', 'trim|strval');
1538
			if ($minmax_key !== null && strlen($minmax_key) < 10)
1539
			{
1540
				$minmax_preferences[$minmax_key] = $val;
1541
			}
1542
1543
			// Change the value to be something nice,
1544
			$val = json_encode($minmax_preferences);
1545
		}
1546
1547
		// Update the option.
1548
		require_once(SUBSDIR . '/Themes.subs.php');
1549
		updateThemeOptions([$settings['theme_id'], $this->user->id, $var, is_array($val) ? implode(',', $val) : $val]);
1550
1551
		Cache::instance()->remove('theme_settings-' . $settings['theme_id'] . ':' . $this->user->id);
1552
1553
		// Don't output anything...
1554
		redirectexit($settings['images_url'] . '/blank.png');
1555
	}
1556
}
1557