ManageThemes::action_list()   B
last analyzed

Complexity

Conditions 10
Paths 9

Size

Total Lines 82
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
cc 10
eloc 41
c 0
b 0
f 0
nc 9
nop 0
dl 0
loc 82
ccs 0
cts 43
cp 0
crap 110
rs 7.6666

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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