Completed
Pull Request — development (#2330)
by Joshua
10:25
created

ManageThemes_Controller::action_jsoption()   D

Complexity

Conditions 17
Paths 144

Size

Total Lines 82
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 306
Metric Value
dl 0
loc 82
ccs 0
cts 53
cp 0
rs 4.62
cc 17
eloc 44
nc 144
nop 0
crap 306

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
 * @name      ElkArte Forum
9
 * @copyright ElkArte Forum contributors
10
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
11
 *
12
 * This software is a derived product, based on:
13
 *
14
 * Simple Machines Forum (SMF)
15
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
16
 * license:		BSD, See included LICENSE.TXT for terms and conditions.
17
 *
18
 * @version 1.1 dev
19
 *
20
 *
21
 * @todo Update this for the new package manager?
22
 *
23
 * Creating and distributing theme packages:
24
 * There isn't that much required to package and distribute your own themes...
25
 * just do the following:
26
 *  - create a theme_info.xml file, with the root element theme-info.
27
 *  - its name should go in a name element, just like description.
28
 *  - your name should go in author. (email in the email attribute.)
29
 *  - any support website for the theme should be in website.
30
 *  - layers and templates (non-default) should go in those elements ;).
31
 *  - if the images dir isn't images, specify in the images element.
32
 *  - any extra rows for themes should go in extra, serialized. (as in array(variable => value).)
33
 *  - tar and gzip the directory - and you're done!
34
 *  - please include any special license in a license.txt file.
35
 */
36
37
if (!defined('ELK'))
38
	die('No access...');
39
40
/**
41
 * Class to deal with theme administration.
42
 *
43
 * Its tasks include changing theme settings, installing and removing
44
 * themes, choosing the current theme, and editing themes.
45
 *
46
 * @package Themes
47
 */
48
class ManageThemes_Controller extends Action_Controller
49
{
50
	/**
51
	 * Holds the selected theme options
52
	 * @var mixed[]
53
	 */
54
	private $_options;
55
56
	/**
57
	 * Holds the selected default theme options
58
	 * @var mixed[]
59
	 */
60
	private $_default_options;
61
62
	/**
63
	 * Holds the selected master options for a theme
64
	 * @var mixed[]
65
	 */
66
	private $_options_master;
67
68
	/**
69
	 * Holds the selected default master options for a theme
70
	 * @var mixed[]
71
	 */
72
	private $_default_options_master;
73
74
	/**
75
	 * Name of the theme
76
	 * @var string
77
	 */
78
	private $theme_name;
79
80
	/**
81
	 * Full path to the theme
82
	 * @var string
83
	 */
84
	private $theme_dir;
85
86
	/**
87
	 * The themes images url if any
88
	 * @var string|null
89
	 */
90
	private $images_url;
91
92
	/**
93
	 * Subaction handler - manages the action and delegates control to the proper
94
	 * sub-action.
95
	 *
96
	 * What it does:
97
	 * - It loads both the Themes and Settings language files.
98
	 * - Checks the session by GET or POST to verify the sent data.
99
	 * - Requires the user to not be a guest.
100
	 * - Accessed via ?action=admin;area=theme.
101
	 *
102
	 * @see Action_Controller::action_index()
103
	 */
104
	public function action_index()
105
	{
106
		global $txt, $context;
107
108
		if (isset($this->_req->query->api))
109
			return $this->action_index_api();
110
111
		// Load the important language files...
112
		loadLanguage('ManageThemes');
113
		loadLanguage('Settings');
114
115
		// No guests in here.
116
		is_not_guest();
117
118
		// Theme administration, removal, choice, or installation...
119
		$subActions = array(
120
			'admin' => array($this, 'action_admin', 'permission' => 'admin_forum'),
121
			'list' => array($this, 'action_list', 'permission' => 'admin_forum'),
122
			'reset' => array($this, 'action_options', 'permission' => 'admin_forum'),
123
			'options' => array($this, 'action_options', 'permission' => 'admin_forum'),
124
			'install' => array($this, 'action_install', 'permission' => 'admin_forum'),
125
			'remove' => array($this, 'action_remove', 'permission' => 'admin_forum'),
126
			'pick' => array($this, 'action_pick'),  // @todo ugly having that in this controller
127
			'edit' => array($this, 'action_edit', 'permission' => 'admin_forum'),
128
			'copy' => array($this, 'action_copy', 'permission' => 'admin_forum'),
129
			'themelist' => array($this, 'action_themelist', 'permission' => 'admin_forum'),
130
			'browse' => array($this, 'action_browse', 'permission' => 'admin_forum'),
131
		);
132
133
		// Action controller
134
		$action = new Action('manage_themes');
135
136
		// @todo Layout Settings?
137
		if (!empty($context['admin_menu_name']))
138
		{
139
			$context[$context['admin_menu_name']]['tab_data'] = array(
140
				'title' => $txt['themeadmin_title'],
141
				'description' => $txt['themeadmin_description'],
142
				'tabs' => array(
143
					'admin' => array(
144
						'description' => $txt['themeadmin_admin_desc'],
145
					),
146
					'list' => array(
147
						'description' => $txt['themeadmin_list_desc'],
148
					),
149
					'reset' => array(
150
						'description' => $txt['themeadmin_reset_desc'],
151
					),
152
					'edit' => array(
153
						'description' => $txt['themeadmin_edit_desc'],
154
					),
155
					'themelist' => array(
156
						'description' => $txt['themeadmin_edit_desc'],
157
					),
158
					'browse' => array(
159
						'description' => $txt['themeadmin_edit_desc'],
160
					),
161
				),
162
			);
163
		}
164
165
		// Follow the sa or just go to administration, call integrate_sa_manage_themes
166
		$subAction = $action->initialize($subActions, 'admin');
167
168
		// Default the page title to Theme Administration by default.
169
		$context['page_title'] = $txt['themeadmin_title'];
170
		$context['sub_action'] = $subAction;
171
172
		// Go to the action, if you have permissions
173
		$action->dispatch($subAction);
174
	}
175
176
	/**
177
	 * Responds to an ajax button request, currently only for remove
178
	 *
179
	 * @uses generic_xml_buttons sub template
180
	 */
181
	public function action_index_api()
182
	{
183
		global $txt, $context, $user_info;
184
185
		loadTemplate('Xml');
186
187
		// Remove any template layers that may have been created, this is XML!
188
		Template_Layers::getInstance()->removeAll();
189
		$context['sub_template'] = 'generic_xml_buttons';
190
191
		// No guests in here.
192
		if ($user_info['is_guest'])
193
		{
194
			loadLanguage('Errors');
195
			$context['xml_data'] = array(
196
				'error' => 1,
197
				'text' => $txt['not_guests']
198
			);
199
200
			return;
201
		}
202
203
		// Theme administration, removal, choice, or installation...
204
		// Of all the actions we currently know only this
205
		$subActions = array(
206
		// 	'admin' => 'action_admin',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
207
		// 	'list' => 'action_list',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
208
		// 	'reset' => 'action_options',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
209
		// 	'options' => 'action_options',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
210
		// 	'install' => 'action_install',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
211
			'remove' => 'action_remove_api',
212
		// 	'pick' => 'action_pick',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
213
		// 	'edit' => 'action_edit',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
214
		// 	'copy' => 'action_copy',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
215
		// 	'themelist' => 'action_themelist',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
216
		// 	'browse' => 'action_browse',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
217
		);
218
219
		// Follow the sa or just go to administration.
220
		if (isset($this->_req->query->sa) && !empty($subActions[$this->_req->query->sa]))
221
			$this->{$subActions[$this->_req->query->sa]}();
222
		else
223
		{
224
			loadLanguage('Errors');
225
			$context['xml_data'] = array(
226
				'error' => 1,
227
				'text' => $txt['error_sa_not_set']
228
			);
229
			return;
230
		}
231
	}
232
233
	/**
234
	 * This function allows administration of themes and their settings,
235
	 * as well as global theme settings.
236
	 *
237
	 * What it does:
238
	 * - sets the settings theme_allow, theme_guests, and knownThemes.
239
	 * - requires the admin_forum permission.
240
	 * - accessed with ?action=admin;area=theme;sa=admin.
241
	 *
242
	 * @uses Themes template
243
	 * @uses Admin language file
244
	 */
245
	public function action_admin()
246
	{
247
		global $context, $modSettings;
248
249
		loadLanguage('Admin');
250
251
		// Saving?
252
		if (isset($this->_req->post->save))
253
		{
254
			checkSession();
255
			validateToken('admin-tm');
256
257
			// What themes are being made as known to the members
258
			if (isset($this->_req->post->options['known_themes']))
259
			{
260
				foreach ($this->_req->post->options['known_themes'] as $key => $id)
261
					$this->_req->post->options['known_themes'][$key] = (int) $id;
262
			}
263
			else
264
				Errors::instance()->fatal_lang_error('themes_none_selectable', false);
265
266
			if (!in_array($this->_req->post->options['theme_guests'], $this->_req->post->options['known_themes']))
267
					Errors::instance()->fatal_lang_error('themes_default_selectable', false);
268
269
			// Commit the new settings.
270
			updateSettings(array(
271
				'theme_allow' => !empty($this->_req->post->options['theme_allow']),
272
				'theme_guests' => $this->_req->post->options['theme_guests'],
273
				'knownThemes' => implode(',', $this->_req->post->options['known_themes']),
274
			));
275
276
			if ((int) $this->_req->post->theme_reset == 0 || in_array($this->_req->post->theme_reset, $this->_req->post->options['known_themes']))
277
			{
278
				require_once(SUBSDIR . '/Members.subs.php');
279
				updateMemberData(null, array('id_theme' => (int) $this->_req->post->theme_reset));
280
			}
281
282
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=admin');
283
		}
284
		// If we aren't submitting - that is, if we are about to...
285
		else
286
		{
287
			loadTemplate('ManageThemes');
288
			$context['sub_template'] = 'manage_themes';
289
290
			// Make our known themes a little easier to work with.
291
			$knownThemes = !empty($modSettings['knownThemes']) ? explode(',', $modSettings['knownThemes']) : array();
292
293
			// Load up all the themes.
294
			require_once(SUBSDIR . '/Themes.subs.php');
295
			$context['themes'] = loadThemes($knownThemes);
296
297
			// Can we create a new theme?
298
			$context['can_create_new'] = is_writable(BOARDDIR . '/themes');
299
			$context['new_theme_dir'] = substr(realpath(BOARDDIR . '/themes/default'), 0, -7);
300
301
			// Look for a non existent theme directory. (ie theme87.)
302
			$theme_dir = BOARDDIR . '/themes/theme';
303
			$i = 1;
304
			while (file_exists($theme_dir . $i))
305
				$i++;
306
			$context['new_theme_name'] = 'theme' . $i;
307
308
			createToken('admin-tm');
309
		}
310
	}
311
312
	/**
313
	 * This function lists the available themes and provides an interface
314
	 * to reset the paths of all the installed themes.
315
	 *
316
	 * @uses sub template list_themes, template ManageThemes
317
	 */
318
	public function action_list()
319
	{
320
		global $context, $boardurl, $txt;
321
322
		// Load in the helpers we need
323
		require_once(SUBSDIR . '/Themes.subs.php');
324
		loadLanguage('Admin');
325
326
		if (isset($this->_req->query->th))
327
			return $this->action_setthemesettings();
328
329
		// Saving?
330
		if (isset($this->_req->post->save))
331
		{
332
			checkSession();
333
			validateToken('admin-tl');
334
335
			$themes = installedThemes();
336
337
			$setValues = array();
338
			foreach ($themes as $id => $theme)
339
			{
340
				if (file_exists($this->_req->post->reset_dir . '/' . basename($theme['theme_dir'])))
341
				{
342
					$setValues[] = array($id, 0, 'theme_dir', realpath($this->_req->post->reset_dir . '/' . basename($theme['theme_dir'])));
343
					$setValues[] = array($id, 0, 'theme_url', $this->_req->post->reset_url . '/' . basename($theme['theme_dir']));
344
					$setValues[] = array($id, 0, 'images_url', $this->_req->post->reset_url . '/' . basename($theme['theme_dir']) . '/' . basename($theme['images_url']));
345
				}
346
347
				if (isset($theme['base_theme_dir']) && file_exists($this->_req->post->reset_dir . '/' . basename($theme['base_theme_dir'])))
348
				{
349
					$setValues[] = array($id, 0, 'base_theme_dir', realpath($this->_req->post->reset_dir . '/' . basename($theme['base_theme_dir'])));
350
					$setValues[] = array($id, 0, 'base_theme_url', $this->_req->post->reset_url . '/' . basename($theme['base_theme_dir']));
351
					$setValues[] = array($id, 0, 'base_images_url', $this->_req->post->reset_url . '/' . basename($theme['base_theme_dir']) . '/' . basename($theme['base_images_url']));
352
				}
353
354
				Cache::instance()->remove('theme_settings-' . $id);
355
			}
356
357
			if (!empty($setValues))
358
				updateThemeOptions($setValues);
359
360
			redirectexit('action=admin;area=theme;sa=list;' . $context['session_var'] . '=' . $context['session_id']);
361
		}
362
363
		loadTemplate('ManageThemes');
364
365
		$context['themes'] = installedThemes();
366
367
		// For each theme, make sure the directory exists, and try to fetch the theme version
368
		foreach ($context['themes'] as $i => $theme)
369
		{
370
			$context['themes'][$i]['theme_dir'] = realpath($context['themes'][$i]['theme_dir']);
371
372
			if (file_exists($context['themes'][$i]['theme_dir'] . '/index.template.php'))
373
			{
374
				// Fetch the header... a good 256 bytes should be more than enough.
375
				$fp = fopen($context['themes'][$i]['theme_dir'] . '/index.template.php', 'rb');
376
				$header = fread($fp, 256);
377
				fclose($fp);
378
379
				// Can we find a version comment, at all?
380
				if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1)
381
					$context['themes'][$i]['version'] = $match[1];
382
			}
383
384
			$context['themes'][$i]['valid_path'] = file_exists($context['themes'][$i]['theme_dir']) && is_dir($context['themes'][$i]['theme_dir']);
385
		}
386
387
		// Off to the template we go
388
		$context['sub_template'] = 'list_themes';
389
		addJavascriptVar(array('txt_theme_remove_confirm' => $txt['theme_remove_confirm']), true);
390
		$context['reset_dir'] = realpath(BOARDDIR . '/themes');
391
		$context['reset_url'] = $boardurl . '/themes';
392
393
		createToken('admin-tl');
394
		createToken('admin-tr', 'request');
395
	}
396
397
	/**
398
	 * Administrative global settings.
399
	 *
400
	 * - Accessed by ?action=admin;area=theme;sa=reset;
401
	 *
402
	 * @uses sub template set_options, template file Settings
403
	 * @uses template file ManageThemes
404
	 */
405
	public function action_options()
406
	{
407
		global $txt, $context, $settings, $modSettings;
408
409
		require_once(SUBSDIR . '/Themes.subs.php');
410
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
411
412
		if (empty($theme) && empty($this->_req->query->id))
413
		{
414
			$context['themes'] = installedThemes();
415
416
			// How many options do we have setup for guests?
417
			$guestOptions = countConfiguredGuestOptions();
418
			foreach ($guestOptions as $guest_option)
419
				$context['themes'][$guest_option['id_theme']]['num_default_options'] = $guest_option['value'];
420
421
			// How many options do we have setup for members?
422
			$memberOptions = countConfiguredMemberOptions();
423
			foreach ($memberOptions as $member_option)
424
				$context['themes'][$member_option['id_theme']]['num_members'] = $member_option['value'];
425
426
			// There has to be a Settings template!
427
			foreach ($context['themes'] as $k => $v)
428
				if (empty($v['theme_dir']) || (!file_exists($v['theme_dir'] . '/Settings.template.php') && empty($v['num_members'])))
429
					unset($context['themes'][$k]);
430
431
			loadTemplate('ManageThemes');
432
			$context['sub_template'] = 'reset_list';
433
434
			createToken('admin-stor', 'request');
435
			return;
436
		}
437
438
		// Submit?
439
		if (isset($this->_req->post->submit) && empty($this->_req->post->who))
440
		{
441
			checkSession();
442
			validateToken('admin-sto');
443
444
			if (empty($this->_req->post->options))
445
				$this->_options = array();
446
447
			if (empty($this->_req->post->default_options))
448
				$this->_default_options = array();
449
450
			// Set up the query values.
451
			$setValues = array();
452
			foreach ($this->_options as $opt => $val)
453
				$setValues[] = array($theme, -1, $opt, is_array($val) ? implode(',', $val) : $val);
454
455
			$old_settings = array();
456
			foreach ($this->_default_options as $opt => $val)
457
			{
458
				$old_settings[] = $opt;
459
				$setValues[] = array(1, -1, $opt, is_array($val) ? implode(',', $val) : $val);
460
			}
461
462
			// If we're actually inserting something..
463
			if (!empty($setValues))
464
			{
465
				// Are there options in non-default themes set that should be cleared?
466
				if (!empty($old_settings))
467
					removeThemeOptions('custom', 'guests', $old_settings);
468
469
				updateThemeOptions($setValues);
470
			}
471
472
			// Cache the theme settings
473
			Cache::instance()->remove('theme_settings-' . $theme);
474
			Cache::instance()->remove('theme_settings-1');
475
476
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset');
477
		}
478
		// Changing the current options for all members using this theme
479
		elseif (isset($this->_req->post->submit) && $this->_req->post->who == 1)
480
		{
481
			checkSession();
482
			validateToken('admin-sto');
483
484
			$this->_options = empty($this->_req->post->options) ? array() : $this->_req->post->options;
485
			$this->_options_master = empty($this->_req->post->options_master) ? array() : $this->_req->post->options_master;
486
			$this->_default_options = empty($this->_req->post->default_options) ? array() : $this->_req->post->default_options;
487
			$this->_default_options_master = empty($this->_req->post->default_options_master) ? array() : $this->_req->post->default_options_master;
488
489
			$old_settings = array();
490
			foreach ($this->_default_options as $opt => $val)
491
			{
492
				if ($this->_default_options_master[$opt] == 0)
493
					continue;
494
				elseif ($this->_default_options_master[$opt] == 1)
495
				{
496
					// Delete then insert for ease of database compatibility!
497
					removeThemeOptions('default', 'members', $opt);
498
					addThemeOptions(1, $opt, $val);
499
500
					$old_settings[] = $opt;
501
				}
502
				elseif ($this->_default_options_master[$opt] == 2)
503
					removeThemeOptions('all', 'members', $opt);
504
			}
505
506
			// Delete options from other themes.
507
			if (!empty($old_settings))
508
				removeThemeOptions('custom', 'members', $old_settings);
509
510
			foreach ($this->_options as $opt => $val)
511
			{
512
				if ($this->_options_master[$opt] == 0)
513
					continue;
514
				elseif ($this->_options_master[$opt] == 1)
515
				{
516
					// Delete then insert for ease of database compatibility - again!
517
					removeThemeOptions($theme, 'non_default', $opt);
518
					addThemeOptions($theme, $opt, $val);
519
				}
520
				elseif ($this->_options_master[$opt] == 2)
521
					removeThemeOptions($theme, 'all', $opt);
522
			}
523
524
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset');
525
		}
526
		// Remove all members options and use the defaults
527
		elseif (!empty($this->_req->query->who) && $this->_req->query->who == 2)
528
		{
529
			checkSession('get');
530
			validateToken('admin-stor', 'request');
531
532
			removeThemeOptions($theme, 'members');
533
534
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset');
535
		}
536
537
		$old_id = $settings['theme_id'];
538
		$old_settings = $settings;
539
540
		loadTheme($theme, false);
541
		loadLanguage('Profile');
542
543
		// @todo Should we just move these options so they are no longer theme dependant?
544
		loadLanguage('PersonalMessage');
545
546
		// Let the theme take care of the settings.
547
		loadTemplate('Settings');
548
		loadSubTemplate('options');
549
550
		// Set up for the template
551
		$context['sub_template'] = 'set_options';
552
		$context['page_title'] = $txt['theme_settings'];
553
		$context['options'] = $context['theme_options'];
554
		$context['theme_settings'] = $settings;
555
556
		// Load the options for these theme
557
		if (empty($this->_req->query->who))
558
		{
559
			$context['theme_options'] = loadThemeOptionsInto(array(1, $theme), -1, $context['theme_options']);
560
			$context['theme_options_reset'] = false;
561
		}
562
		else
563
		{
564
			$context['theme_options'] = array();
565
			$context['theme_options_reset'] = true;
566
		}
567
568
		// Prepare the options for the template
569
		foreach ($context['options'] as $i => $setting)
570
		{
571
			// Is this disabled?
572
			if ($setting['id'] == 'calendar_start_day' && empty($modSettings['cal_enabled']))
573
			{
574
				unset($context['options'][$i]);
575
				continue;
576
			}
577
			elseif (($setting['id'] == 'topics_per_page' || $setting['id'] == 'messages_per_page') && !empty($modSettings['disableCustomPerPage']))
578
			{
579
				unset($context['options'][$i]);
580
				continue;
581
			}
582
583
			// Type of field so we display the right input field
584
			if (!isset($setting['type']) || $setting['type'] == 'bool')
585
				$context['options'][$i]['type'] = 'checkbox';
586
			elseif ($setting['type'] == 'int' || $setting['type'] == 'integer')
587
				$context['options'][$i]['type'] = 'number';
588
			elseif ($setting['type'] == 'string')
589
				$context['options'][$i]['type'] = 'text';
590
591
			if (isset($setting['options']))
592
				$context['options'][$i]['type'] = 'list';
593
594
			$context['options'][$i]['value'] = !isset($context['theme_options'][$setting['id']]) ? '' : $context['theme_options'][$setting['id']];
595
		}
596
597
		// Restore the existing theme.
598
		loadTheme($old_id, false);
599
		$settings = $old_settings;
600
601
		loadTemplate('ManageThemes');
602
		createToken('admin-sto');
603
	}
604
605
	/**
606
	 * Administrative global settings.
607
	 *
608
	 * What it does:
609
	 * - Saves and requests global theme settings. ($settings)
610
	 * - Loads the Admin language file.
611
	 * - Calls action_admin() if no theme is specified. (the theme center.)
612
	 * - Requires admin_forum permission.
613
	 * - Accessed with ?action=admin;area=theme;sa=list&th=xx.
614
	 */
615
	public function action_setthemesettings()
616
	{
617
		global $txt, $context, $settings, $modSettings;
618
619
		require_once(SUBSDIR . '/Themes.subs.php');
620
621
		// Nothing chosen, back to the start you go
622
		if (empty($this->_req->query->th) && empty($this->_req->query->id))
623
			return $this->action_admin();
624
625
		// The theme's ID is needed
626
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
627
628
		// Validate inputs/user.
629
		if (empty($theme))
630
			Errors::instance()->fatal_lang_error('no_theme', false);
631
632
		// Select the best fitting tab.
633
		$context[$context['admin_menu_name']]['current_subsection'] = 'list';
634
		loadLanguage('Admin');
635
636
		// Fetch the smiley sets...
637
		$sets = explode(',', 'none,' . $modSettings['smiley_sets_known']);
638
		$set_names = explode("\n", $txt['smileys_none'] . "\n" . $modSettings['smiley_sets_names']);
639
		$context['smiley_sets'] = array(
640
			'' => $txt['smileys_no_default']
641
		);
642
		foreach ($sets as $i => $set)
643
			$context['smiley_sets'][$set] = htmlspecialchars($set_names[$i], ENT_COMPAT, 'UTF-8');
644
645
		$old_id = $settings['theme_id'];
646
		$old_settings = $settings;
647
648
		loadTheme($theme, false);
649
650
		// Also load the actual themes language file - in case of special settings.
651
		loadLanguage('Settings', '', true, true);
652
653
		// And the custom language strings...
654
		loadLanguage('ThemeStrings', '', false, true);
655
656
		// Let the theme take care of the settings.
657
		loadTemplate('Settings');
658
		loadSubTemplate('settings');
659
660
		// Load the variants separately...
661
		$settings['theme_variants'] = array();
662
		if (file_exists($settings['theme_dir'] . '/index.template.php'))
663
		{
664
			$file_contents = implode("\n", file($settings['theme_dir'] . '/index.template.php'));
665
			if (preg_match('~\'theme_variants\'\s*=>(.+?\)),$~sm', $file_contents, $matches))
666
				eval('global $settings; $settings[\'theme_variants\'] = ' . $matches[1] . ';');
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
667
668
				call_integration_hook('integrate_init_theme', array($theme, &$settings));
669
		}
670
671
		// Submitting!
672
		if (isset($this->_req->post->save))
673
		{
674
			// Allowed?
675
			checkSession();
676
			validateToken('admin-sts');
677
678
			$options = array();
679
			$options['options'] = empty($this->_req->post->options) ? array() : (array) $this->_req->post->options;
680
			$options['default_options'] = empty($this->_req->post->default_options) ? array() : (array) $this->_req->post->default_options;
681
682
			// Make sure items are cast correctly.
683
			foreach ($context['theme_settings'] as $item)
684
			{
685
				// Unwatch this item if this is just a separator.
686
				if (!is_array($item))
687
					continue;
688
689
				// Clean them up for the database
690
				foreach (array('options', 'default_options') as $option)
691
				{
692
					if (!isset($options[$option][$item['id']]))
693
						continue;
694
					// Checkbox.
695
					elseif (empty($item['type']))
696
						$options[$option][$item['id']] = $options[$option][$item['id']] ? 1 : 0;
697
					// Number
698
					elseif ($item['type'] == 'number')
699
						$options[$option][$item['id']] = (int) $options[$option][$item['id']];
700
				}
701
			}
702
703
			// Set up the sql query.
704
			$inserts = array();
705
			foreach ($options['options'] as $opt => $val)
706
				$inserts[] = array($theme, 0, $opt, is_array($val) ? implode(',', $val) : $val);
707
708
			foreach ($options['default_options'] as $opt => $val)
709
				$inserts[] = array(1, 0, $opt, is_array($val) ? implode(',', $val) : $val);
710
711
			// If we're actually inserting something..
712
			if (!empty($inserts))
713
				updateThemeOptions($inserts);
714
715
			// Clear and Invalidate the cache.
716
			Cache::instance()->remove('theme_settings-' . $theme);
717
			Cache::instance()->remove('theme_settings-1');
718
			updateSettings(array('settings_updated' => time()));
719
720
			redirectexit('action=admin;area=theme;sa=list;th=' . $theme . ';' . $context['session_var'] . '=' . $context['session_id']);
721
		}
722
723
		$context['sub_template'] = 'set_settings';
724
		$context['page_title'] = $txt['theme_settings'];
725
726
		foreach ($settings as $setting => $dummy)
727
		{
728
			if (!in_array($setting, array('theme_url', 'theme_dir', 'images_url', 'template_dirs')))
729
				$settings[$setting] = htmlspecialchars__recursive($settings[$setting]);
730
		}
731
732
		$context['settings'] = $context['theme_settings'];
733
		$context['theme_settings'] = $settings;
734
735
		foreach ($context['settings'] as $i => $setting)
736
		{
737
			// Separators are dummies, so leave them alone.
738
			if (!is_array($setting))
739
				continue;
740
741
			// Create the right input fields for the data
742
			if (!isset($setting['type']) || $setting['type'] == 'bool')
743
				$context['settings'][$i]['type'] = 'checkbox';
744
			elseif ($setting['type'] == 'int' || $setting['type'] == 'integer')
745
				$context['settings'][$i]['type'] = 'number';
746
			elseif ($setting['type'] == 'string')
747
				$context['settings'][$i]['type'] = 'text';
748
749
			if (isset($setting['options']))
750
				$context['settings'][$i]['type'] = 'list';
751
752
			$context['settings'][$i]['value'] = !isset($settings[$setting['id']]) ? '' : $settings[$setting['id']];
753
		}
754
755
		// Do we support variants?
756
		if (!empty($settings['theme_variants']))
757
		{
758
			$context['theme_variants'] = array();
759
			foreach ($settings['theme_variants'] as $variant)
0 ignored issues
show
Bug introduced by
The expression $settings['theme_variants'] of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
760
			{
761
				// Have any text, old chap?
762
				$context['theme_variants'][$variant] = array(
763
					'label' => isset($txt['variant_' . $variant]) ? $txt['variant_' . $variant] : $variant,
764
					'thumbnail' => !file_exists($settings['theme_dir'] . '/images/thumbnail.png') || file_exists($settings['theme_dir'] . '/images/thumbnail_' . $variant . '.png') ? $settings['images_url'] . '/thumbnail_' . $variant . '.png' : ($settings['images_url'] . '/thumbnail.png'),
765
				);
766
			}
767
			$context['default_variant'] = !empty($settings['default_variant']) && isset($context['theme_variants'][$settings['default_variant']]) ? $settings['default_variant'] : $settings['theme_variants'][0];
768
		}
769
770
		// Restore the current theme.
771
		loadTheme($old_id, false);
772
773
		$settings = $old_settings;
774
775
		// Reinit just incase.
776
		if (function_exists('template_init'))
777
			$settings += template_init();
778
779
		loadTemplate('ManageThemes');
780
781
		// We like Kenny better than Token.
782
		createToken('admin-sts');
783
	}
784
785
	/**
786
	 * Remove a theme from the database.
787
	 *
788
	 * What it does:
789
	 * - Removes an installed theme.
790
	 * - Requires an administrator.
791
	 * - Accessed with ?action=admin;area=theme;sa=remove.
792
	 */
793
	public function action_remove()
794
	{
795
		global $modSettings, $context;
796
797
		require_once(SUBSDIR . '/Themes.subs.php');
798
799
		checkSession('get');
800
		validateToken('admin-tr', 'request');
801
802
		// The theme's ID must be an integer.
803
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
804
805
		// You can't delete the default theme!
806
		if ($theme == 1)
807
			Errors::instance()->fatal_lang_error('no_access', false);
808
809
		// Its no longer known
810
		$known = explode(',', $modSettings['knownThemes']);
811
		for ($i = 0, $n = count($known); $i < $n; $i++)
812
		{
813
			if ($known[$i] == $theme)
814
				unset($known[$i]);
815
		}
816
		$known = strtr(implode(',', $known), array(',,' => ','));
817
818
		// Remove it as an option everywhere
819
		deleteTheme($theme);
820
821
		// Fix it if the theme was the overall default theme.
822
		if ($modSettings['theme_guests'] == $theme)
823
			updateSettings(array('theme_guests' => '1', 'knownThemes' => $known));
824
		else
825
			updateSettings(array('knownThemes' => $known));
826
827
		redirectexit('action=admin;area=theme;sa=list;' . $context['session_var'] . '=' . $context['session_id']);
828
	}
829
830
	/**
831
	 * Remove a theme from the database in response to an ajax api request
832
	 *
833
	 * What it does:
834
	 * - Removes an installed theme.
835
	 * - Requires an administrator.
836
	 * - Accessed with ?action=admin;area=theme;sa=remove;api
837
	 */
838
	public function action_remove_api()
839
	{
840
		global $modSettings, $context, $txt;
841
842
		require_once(SUBSDIR . '/Themes.subs.php');
843
844
		// Validate what was sent
845
		if (checkSession('get', '', false))
846
		{
847
			loadLanguage('Errors');
848
			$context['xml_data'] = array(
849
				'error' => 1,
850
				'text' => $txt['session_verify_fail'],
851
			);
852
853
			return;
854
		}
855
856
		// Not just any John Smith can send in a api request
857
		if (!allowedTo('admin_forum'))
858
		{
859
			loadLanguage('Errors');
860
			$context['xml_data'] = array(
861
				'error' => 1,
862
				'text' => $txt['cannot_admin_forum'],
863
			);
864
			return;
865
		}
866
867
		// Even if you are John Smith, you still need a ticket
868
		if (!validateToken('admin-tr', 'request', true, false))
869
		{
870
			loadLanguage('Errors');
871
			$context['xml_data'] = array(
872
				'error' => 1,
873
				'text' => $txt['token_verify_fail'],
874
			);
875
876
			return;
877
		}
878
879
		// The theme's ID must be an integer.
880
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
881
882
		// You can't delete the default theme!
883
		if ($theme == 1)
884
		{
885
			loadLanguage('Errors');
886
			$context['xml_data'] = array(
887
				'error' => 1,
888
				'text' => $txt['no_access'],
889
			);
890
			return;
891
		}
892
893
		// It is a theme we know about?
894
		$known = explode(',', $modSettings['knownThemes']);
895
		for ($i = 0, $n = count($known); $i < $n; $i++)
896
		{
897
			if ($known[$i] == $theme)
898
				unset($known[$i]);
899
		}
900
901
		// Finally, remove it
902
		deleteTheme($theme);
903
904
		$known = strtr(implode(',', $known), array(',,' => ','));
905
906
		// Fix it if the theme was the overall default theme.
907
		if ($modSettings['theme_guests'] == $theme)
908
			updateSettings(array('theme_guests' => '1', 'knownThemes' => $known));
909
		else
910
			updateSettings(array('knownThemes' => $known));
911
912
		// Let them know it worked, all without a page refresh
913
		createToken('admin-tr', 'request');
914
		$context['xml_data'] = array(
915
			'success' => 1,
916
			'token_var' => $context['admin-tr_token_var'],
917
			'token' => $context['admin-tr_token'],
918
		);
919
	}
920
921
	/**
922
	 * Choose a theme from a list.
923
	 * Allows a user or administrator to pick a new theme with an interface.
924
	 *
925
	 * What it does:
926
	 * - Can edit everyone's (u = 0), guests' (u = -1), or a specific user's.
927
	 * - Uses the Themes template. (pick sub template.)
928
	 * - Accessed with ?action=admin;area=theme;sa=pick.
929
	 *
930
	 * @uses Profile language text
931
	 * @uses ManageThemes template
932
	 * @todo thought so... Might be better to split this file in ManageThemes and Themes,
933
	 * with centralized admin permissions on ManageThemes.
934
	 */
935
	public function action_pick()
0 ignored issues
show
Coding Style introduced by
action_pick uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
936
	{
937
		global $txt, $context, $modSettings, $user_info, $scripturl, $settings;
938
939
		require_once(SUBSDIR . '/Themes.subs.php');
940
941
		if (!$modSettings['theme_allow'] && $settings['disable_user_variant'] && !allowedTo('admin_forum'))
942
			Errors::instance()->fatal_lang_error('no_access', false);
943
944
		loadLanguage('Profile');
945
		loadTemplate('ManageThemes');
946
947
		// Build the link tree.
948
		$context['linktree'][] = array(
949
			'url' => $scripturl . '?action=theme;sa=pick;u=' . (!empty($this->_req->query->u) ? (int) $this->_req->query->u : 0),
950
			'name' => $txt['theme_pick'],
951
		);
952
		$context['default_theme_id'] = $modSettings['theme_default'];
953
954
		$_SESSION['id_theme'] = 0;
955
956
		if (isset($this->_req->query->id))
957
			$this->_req->query->th = $this->_req->query->id;
958
959
		// Saving a variant cause JS doesn't work - pretend it did ;)
960
		if (isset($this->_req->post->save))
961
		{
962
			// Which theme?
963
			foreach ($this->_req->post->save as $k => $v)
964
				$this->_req->query->th = (int) $k;
965
966
			if (isset($this->_req->post->vrt[$k]))
967
				$this->_req->query->vrt = $this->_req->post->vrt[$k];
0 ignored issues
show
Bug introduced by
The variable $k seems to be defined by a foreach iteration on line 963. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
968
		}
969
970
		// Have we made a decision, or are we just browsing?
971
		if (isset($this->_req->query->th))
972
		{
973
			checkSession('get');
974
975
			$th = $this->_req->getQuery('th', 'intval');
976
			$vrt = $this->_req->getQuery('vrt', 'cleanhtml');
977
			$u = $this->_req->getQuery('u', 'intval');
978
979
			// Save for this user.
980
			if (!isset($u) || !allowedTo('admin_forum'))
981
			{
982
				require_once(SUBSDIR . '/Members.subs.php');
983
				updateMemberData($user_info['id'], array('id_theme' => $th));
984
985
				// A variants to save for the user?
986
				if (!empty($vrt))
987
				{
988
					updateThemeOptions(array($th, $user_info['id'], 'theme_variant', $vrt));
989
990
					Cache::instance()->remove('theme_settings-' . $th . ':' . $user_info['id']);
991
992
					$_SESSION['id_variant'] = 0;
993
				}
994
995
				redirectexit('action=profile;area=theme');
996
			}
997
998
			// If changing members or guests - and there's a variant - assume changing default variant.
999
			if (!empty($vrt) && ($u === 0 || $u === -1))
1000
			{
1001
				updateThemeOptions(array($th, 0, 'default_variant', $vrt));
1002
1003
				// Make it obvious that it's changed
1004
				Cache::instance()->remove('theme_settings-' . $th);
1005
			}
1006
1007
			// For everyone.
1008
			if ($u === 0)
1009
			{
1010
				require_once(SUBSDIR . '/Members.subs.php');
1011
				updateMemberData(null, array('id_theme' => $th));
1012
1013
				// Remove any custom variants.
1014
				if (!empty($vrt))
1015
					deleteVariants($th);
1016
1017
				redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
1018
			}
1019
			// Change the default/guest theme.
1020
			elseif ($u === -1)
1021
			{
1022
				updateSettings(array('theme_guests' => $th));
1023
1024
				redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
1025
			}
1026
			// Change a specific member's theme.
1027
			else
1028
			{
1029
				// The forum's default theme is always 0 and we
1030
				if (isset($th) && $th == 0)
1031
					$th = $modSettings['theme_guests'];
1032
1033
				require_once(SUBSDIR . '/Members.subs.php');
1034
				updateMemberData($u, array('id_theme' => $th));
1035
1036
				if (!empty($vrt))
1037
				{
1038
					updateThemeOptions(array($th, $u, 'theme_variant', $vrt));
1039
					Cache::instance()->remove('theme_settings-' . $th . ':' . $u);
1040
1041
					if ($user_info['id'] == $u)
1042
						$_SESSION['id_variant'] = 0;
1043
				}
1044
1045
				redirectexit('action=profile;u=' . $u . ';area=theme');
1046
			}
1047
		}
1048
1049
		$u = $this->_req->getQuery('u', 'intval');
1050
1051
		// Figure out who the member of the minute is, and what theme they've chosen.
1052
		if (!isset($u) || !allowedTo('admin_forum'))
1053
		{
1054
			$context['current_member'] = $user_info['id'];
1055
			$current_theme = $user_info['theme'];
1056
		}
1057
		// Everyone can't chose just one.
1058
		elseif ($u === 0)
1059
		{
1060
			$context['current_member'] = 0;
1061
			$current_theme = 0;
1062
		}
1063
		// Guests and such...
1064
		elseif ($u === -1)
1065
		{
1066
			$context['current_member'] = -1;
1067
			$current_theme = $modSettings['theme_guests'];
1068
		}
1069
		// Someones else :P.
1070
		else
1071
		{
1072
			$context['current_member'] = $u;
1073
1074
			require_once(SUBSDIR . '/Members.subs.php');
1075
			$member = getBasicMemberData($context['current_member']);
1076
1077
			$current_theme = $member['id_theme'];
1078
		}
1079
1080
		// Get the theme name and descriptions.
1081
		list ($context['available_themes'], $guest_theme) = availableThemes($current_theme, $context['current_member']);
1082
1083
		// As long as we're not doing the default theme...
1084
		if (!isset($u) || $u >= 0)
1085
		{
1086
			if ($guest_theme != 0)
1087
				$context['available_themes'][0] = $context['available_themes'][$guest_theme];
1088
1089
			$context['available_themes'][0]['id'] = 0;
1090
			$context['available_themes'][0]['name'] = $txt['theme_forum_default'];
1091
			$context['available_themes'][0]['selected'] = $current_theme == 0;
1092
			$context['available_themes'][0]['description'] = $txt['theme_global_description'];
1093
		}
1094
1095
		ksort($context['available_themes']);
1096
1097
		$context['page_title'] = $txt['theme_pick'];
1098
		$context['sub_template'] = 'pick';
1099
	}
1100
1101
	/**
1102
	 * Installs new themes, either from a gzip or copy of the default.
1103
	 *
1104
	 * What it does:
1105
	 * - Puts themes in $boardurl/themes.
1106
	 * - Assumes the gzip has a root directory in it. (ie default.)
1107
	 * - Requires admin_forum.
1108
	 * - Accessed with ?action=admin;area=theme;sa=install.
1109
	 *
1110
	 * @uses ManageThemes template
1111
	 */
1112
	public function action_install()
0 ignored issues
show
Coding Style introduced by
action_install uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1113
	{
1114
		global $boardurl, $txt, $context, $settings, $modSettings;
1115
1116
		checkSession('request');
1117
1118
		require_once(SUBSDIR . '/Themes.subs.php');
1119
		require_once(SUBSDIR . '/Package.subs.php');
1120
1121
		loadTemplate('ManageThemes');
1122
1123
		// Passed an ID, then the install is complete, lets redirect and show them
1124
		if (isset($this->_req->query->theme_id))
1125
		{
1126
			$this->_req->query->theme_id = (int) $this->_req->query->theme_id;
1127
1128
			$context['sub_template'] = 'installed';
1129
			$context['page_title'] = $txt['theme_installed'];
1130
			$context['installed_theme'] = array(
1131
				'id' => $this->_req->query->theme_id,
1132
				'name' => getThemeName($this->_req->query->theme_id),
1133
			);
1134
1135
			return;
1136
		}
1137
1138
		// How are we going to install this theme, from a dir, zip, copy of default?
1139
		if ((!empty($_FILES['theme_gz']) && (!isset($_FILES['theme_gz']['error']) || $_FILES['theme_gz']['error'] != 4)) || !empty($this->_req->query->theme_gz))
1140
			$method = 'upload';
1141
		elseif (isset($this->_req->query->theme_dir) && rtrim(realpath($this->_req->query->theme_dir), '/\\') != realpath(BOARDDIR . '/themes') && file_exists($this->_req->query->theme_dir))
1142
			$method = 'path';
1143
		else
1144
			$method = 'copy';
1145
1146
		// Copy the default theme?
1147
		if (!empty($this->_req->post->copy) && $method == 'copy')
1148
			$this->copyDefault();
1149
		// Install from another directory
1150
		elseif (isset($this->_req->post->theme_dir) && $method == 'path')
1151
			$this->installFromDir();
1152
		// Uploaded a zip file to install from
1153
		elseif ($method == 'upload')
1154
			$this->InstallFromZip();
1155
		else
1156
			Errors::instance()->fatal_lang_error('theme_install_general', false);
1157
1158
		// Something go wrong?
1159
		if ($this->theme_dir != '' && basename($this->theme_dir) != 'themes')
1160
		{
1161
			// Defaults.
1162
			$install_info = array(
1163
				'theme_url' => $boardurl . '/themes/' . basename($this->theme_dir),
1164
				'images_url' => isset($this->images_url) ? $this->images_url : $boardurl . '/themes/' . basename($this->theme_dir) . '/images',
1165
				'theme_dir' => $this->theme_dir,
1166
				'name' => $this->theme_name
1167
			);
1168
			$explicit_images = false;
1169
1170
			if (file_exists($this->theme_dir . '/theme_info.xml'))
1171
			{
1172
				$theme_info = file_get_contents($this->theme_dir . '/theme_info.xml');
1173
1174
				// Parse theme-info.xml into an Xml_Array.
1175
				$theme_info_xml = new Xml_Array($theme_info);
1176
1177
				// @todo Error message of some sort?
1178
				if (!$theme_info_xml->exists('theme-info[0]'))
1179
					return 'package_get_error_packageinfo_corrupt';
1180
1181
				$theme_info_xml = $theme_info_xml->path('theme-info[0]');
1182
				$theme_info_xml = $theme_info_xml->to_array();
1183
1184
				$xml_elements = array(
1185
					'name' => 'name',
1186
					'theme_layers' => 'layers',
1187
					'theme_templates' => 'templates',
1188
					'based_on' => 'based-on',
1189
				);
1190
				foreach ($xml_elements as $var => $name)
1191
				{
1192
					if (!empty($theme_info_xml[$name]))
1193
						$install_info[$var] = $theme_info_xml[$name];
1194
				}
1195
1196
				if (!empty($theme_info_xml['images']))
1197
				{
1198
					$install_info['images_url'] = $install_info['theme_url'] . '/' . $theme_info_xml['images'];
1199
					$explicit_images = true;
1200
				}
1201
1202
				if (!empty($theme_info_xml['extra']))
1203
					$install_info += unserialize($theme_info_xml['extra']);
0 ignored issues
show
Security Object Injection introduced by
$theme_info_xml['extra'] can contain request data and is used in unserialized context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
1204
			}
1205
1206
			if (isset($install_info['based_on']))
1207
			{
1208
				if ($install_info['based_on'] == 'default')
1209
				{
1210
					$install_info['theme_url'] = $settings['default_theme_url'];
1211
					$install_info['images_url'] = $settings['default_images_url'];
1212
				}
1213
				elseif ($install_info['based_on'] != '')
1214
				{
1215
					$install_info['based_on'] = preg_replace('~[^A-Za-z0-9\-_ ]~', '', $install_info['based_on']);
1216
1217
					$temp = loadBasedOnTheme($install_info['based_on'], $explicit_images);
1218
1219
					// @todo An error otherwise?
1220
					if (is_array($temp))
1221
					{
1222
						$install_info = $temp + $install_info;
1223
1224
						if (empty($explicit_images) && !empty($install_info['base_theme_url']))
1225
							$install_info['theme_url'] = $install_info['base_theme_url'];
1226
					}
1227
				}
1228
1229
				unset($install_info['based_on']);
1230
			}
1231
1232
			// Find the newest id_theme.
1233
			$id_theme = nextTheme();
1234
1235
			$inserts = array();
1236
			foreach ($install_info as $var => $val)
1237
				$inserts[] = array($id_theme, $var, $val);
1238
1239
			if (!empty($inserts))
1240
				addTheme($inserts);
1241
1242
			updateSettings(array('knownThemes' => strtr($modSettings['knownThemes'] . ',' . $id_theme, array(',,' => ','))));
1243
		}
1244
1245
		redirectexit('action=admin;area=theme;sa=install;theme_id=' . $id_theme . ';' . $context['session_var'] . '=' . $context['session_id']);
0 ignored issues
show
Bug introduced by
The variable $id_theme does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1246
	}
1247
1248
	/**
1249
	 * Install a new theme from an uploaded zip archive
1250
	 */
1251
	public function installFromZip()
0 ignored issues
show
Coding Style introduced by
installFromZip uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1252
	{
1253
		global $context;
1254
1255
		// Hopefully the themes directory is writable, or we might have a problem.
1256
		if (!is_writable(BOARDDIR . '/themes'))
1257
			Errors::instance()->fatal_lang_error('theme_install_write_error', 'critical');
1258
1259
		// This happens when the admin session is gone and the user has to login again
1260
		if (empty($_FILES['theme_gz']) && empty($this->_req->post->theme_gz))
1261
			redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
1262
1263
		// Set the default settings...
1264
		$this->theme_name = strtok(basename(isset($_FILES['theme_gz']) ? $_FILES['theme_gz']['name'] : $this->_req->post->theme_gz), '.');
1265
		$this->theme_name = preg_replace(array('/\s/', '/\.[\.]+/', '/[^\w_\.\-]/'), array('_', '.', ''), $this->theme_name);
1266
		$this->theme_dir = BOARDDIR . '/themes/' . $this->theme_name;
1267
1268
		if (isset($_FILES['theme_gz']) && is_uploaded_file($_FILES['theme_gz']['tmp_name']) && (ini_get('open_basedir') != '' || file_exists($_FILES['theme_gz']['tmp_name'])))
1269
			read_tgz_file($_FILES['theme_gz']['tmp_name'], BOARDDIR . '/themes/' . $this->theme_name, false, true);
1270
		elseif (isset($this->_req->post->theme_gz))
1271
		{
1272
			if (!isAuthorizedServer($this->_req->post->theme_gz))
1273
				Errors::instance()->fatal_lang_error('not_valid_server');
1274
1275
			read_tgz_file($this->_req->post->theme_gz, BOARDDIR . '/themes/' . $this->theme_name, false, true);
1276
		}
1277
		else
1278
			redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
1279
	}
1280
1281
	/**
1282
	 * Install a theme from a directory on the server
1283
	 *
1284
	 * - Expects the directory is properly loaded with theme files
1285
	 */
1286
	public function installFromDir()
1287
	{
1288
		if (!is_dir($this->_req->post->theme_dir) || !file_exists($this->_req->post->theme_dir . '/theme_info.xml'))
1289
			Errors::instance()->fatal_lang_error('theme_install_error', false);
1290
1291
		$this->theme_name = basename($this->_req->post->theme_dir);
1292
		$this->theme_dir = $this->_req->post->theme_dir;
1293
	}
1294
1295
	/**
1296
	 * Make a copy of the default theme in a new directory
1297
	 */
1298
	public function copyDefault()
1299
	{
1300
		global $boardurl, $modSettings, $settings;
1301
1302
		// Hopefully the themes directory is writable, or we might have a problem.
1303
		if (!is_writable(BOARDDIR . '/themes'))
1304
			Errors::instance()->fatal_lang_error('theme_install_write_error', 'critical');
1305
1306
		// Make the new directory, standard characters only
1307
		$this->theme_dir = BOARDDIR . '/themes/' . preg_replace('~[^A-Za-z0-9_\- ]~', '', $this->_req->post->copy);
1308
		umask(0);
1309
		mkdir($this->theme_dir, 0777);
1310
1311
		// Get some more time if we can
1312
		setTimeLimit(600);
1313
1314
		// Create the subdirectories for css, javascript and font files.
1315
		mkdir($this->theme_dir . '/css', 0777);
1316
		mkdir($this->theme_dir . '/scripts', 0777);
1317
		mkdir($this->theme_dir . '/webfonts', 0777);
1318
1319
		// Copy over the default non-theme files.
1320
		$to_copy = array('/index.php', '/index.template.php', '/scripts/theme.js');
1321
		foreach ($to_copy as $file)
1322
		{
1323
			copy($settings['default_theme_dir'] . $file, $this->theme_dir . $file);
1324
			@chmod($this->theme_dir . $file, 0777);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1325
		}
1326
1327
		// And now the entire css, images and webfonts directories!
1328
		copytree($settings['default_theme_dir'] . '/css', $this->theme_dir . '/css');
1329
		copytree($settings['default_theme_dir'] . '/images', $this->theme_dir . '/images');
1330
		copytree($settings['default_theme_dir'] . '/webfonts', $this->theme_dir . '/webfonts');
1331
		package_flush_cache();
1332
1333
		$this->theme_name = $this->_req->post->copy;
1334
		$this->images_url = $boardurl . '/themes/' . basename($this->theme_dir) . '/images';
1335
		$this->theme_dir = realpath($this->theme_dir);
1336
1337
		// Lets get some data for the new theme (default theme (1), default settings (0)).
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1338
		$theme_values = loadThemeOptionsInto(1, 0, array(), array('theme_templates', 'theme_layers'));
1339
1340
		// Lets add a theme_info.xml to this theme.
1341
		write_theme_info($this->_req->query->copy, $modSettings['elkVersion'], $this->theme_dir, $theme_values);
1342
	}
1343
1344
	/**
1345
	 * Set a theme option via javascript.
1346
	 *
1347
	 * What it does:
1348
	 * - sets a theme option without outputting anything.
1349
	 * - can be used with javascript, via a dummy image... (which doesn't require
1350
	 *   the page to reload.)
1351
	 * - requires someone who is logged in.
1352
	 * - accessed via ?action=jsoption;var=variable;val=value;session_var=sess_id.
1353
	 * - optionally contains &th=theme id
1354
	 * - does not log access to the Who's Online log. (in index.php..)
1355
	 */
1356
	public function action_jsoption()
1357
	{
1358
		global $settings, $user_info, $options, $modSettings;
1359
1360
		// Check the session id.
1361
		checkSession('get');
1362
1363
		// This good-for-nothing pixel is being used to keep the session alive.
1364
		if (empty($this->_req->query->var) || !isset($this->_req->query->val))
1365
			redirectexit($settings['images_url'] . '/blank.png');
1366
1367
		// Sorry, guests can't go any further than this..
1368
		if ($user_info['is_guest'] || $user_info['id'] == 0)
1369
			obExit(false);
1370
1371
		$reservedVars = array(
1372
			'actual_theme_url',
1373
			'actual_images_url',
1374
			'base_theme_dir',
1375
			'base_theme_url',
1376
			'default_images_url',
1377
			'default_theme_dir',
1378
			'default_theme_url',
1379
			'default_template',
1380
			'images_url',
1381
			'number_recent_posts',
1382
			'smiley_sets_default',
1383
			'theme_dir',
1384
			'theme_id',
1385
			'theme_layers',
1386
			'theme_templates',
1387
			'theme_url',
1388
			'name',
1389
		);
1390
1391
		// Can't change reserved vars.
1392
		if (in_array(strtolower($this->_req->query->var), $reservedVars))
1393
			redirectexit($settings['images_url'] . '/blank.png');
1394
1395
		// Use a specific theme?
1396
		if (isset($this->_req->query->th) || isset($this->_req->query->id))
1397
		{
1398
			// Invalidate the current themes cache too.
1399
			Cache::instance()->remove('theme_settings-' . $settings['theme_id'] . ':' . $user_info['id']);
1400
1401
			$settings['theme_id'] = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval'));
1402
		}
1403
1404
		// If this is the admin preferences the passed value will just be an element of it.
1405
		if ($this->_req->query->var == 'admin_preferences')
1406
		{
1407
			$options['admin_preferences'] = !empty($options['admin_preferences']) ? unserialize($options['admin_preferences']) : array();
1408
1409
			// New thingy...
1410
			if (isset($this->_req->query->admin_key) && strlen($this->_req->query->admin_key) < 5)
1411
				$options['admin_preferences'][$this->_req->query->admin_key] = $this->_req->query->val;
1412
1413
			// Change the value to be something nice,
1414
			$this->_req->query->val = serialize($options['admin_preferences']);
1415
		}
1416
		// If this is the window min/max settings, the passed window name will just be an element of it.
1417
		elseif ($this->_req->query->var == 'minmax_preferences')
1418
		{
1419
			$options['minmax_preferences'] = !empty($options['minmax_preferences']) ? unserialize($options['minmax_preferences']) : array();
1420
1421
			// New value for them
1422
			if (isset($this->_req->query->minmax_key) && strlen($this->_req->query->minmax_key) < 10)
1423
				$options['minmax_preferences'][$this->_req->query->minmax_key] = $this->_req->query->val;
1424
1425
			// Change the value to be something nice,
1426
			$this->_req->query->val = serialize($options['minmax_preferences']);
1427
		}
1428
1429
		// Update the option.
1430
		require_once(SUBSDIR . '/Themes.subs.php');
1431
		updateThemeOptions(array($settings['theme_id'], $user_info['id'], $this->_req->query->var, is_array($this->_req->query->val) ? implode(',', $this->_req->query->val) : $this->_req->query->val));
1432
1433
		Cache::instance()->remove('theme_settings-' . $settings['theme_id'] . ':' . $user_info['id']);
1434
1435
		// Don't output anything...
1436
		redirectexit($settings['images_url'] . '/blank.png');
1437
	}
1438
1439
	/**
1440
	 * Allows choosing, browsing, and editing a themes files.
1441
	 *
1442
	 * What it does:
1443
	 * - Its subactions handle several features:
1444
	 *   - edit_template: display and edit a PHP template file
1445
	 *   - edit_style: display and edit a CSS file
1446
	 *   - edit_file: display and edit other files in the theme
1447
	 * - accessed via ?action=admin;area=theme;sa=edit
1448
	 *
1449
	 * @uses the ManageThemes template
1450
	 */
1451
	public function action_edit()
1452
	{
1453
		global $context;
1454
1455
		loadTemplate('ManageThemes');
1456
1457
		// We'll work hard with them themes!
1458
		require_once(SUBSDIR . '/Themes.subs.php');
1459
1460
		$selectedTheme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
1461
1462
		// Unfortunately we cannot edit an unkwown theme.. redirect.
1463
		if (empty($selectedTheme))
1464
			redirectexit('action=admin;area=theme;sa=themelist');
1465
		// You're browsing around, aren't you
1466
		elseif (!isset($this->_req->query->filename) && !isset($this->_req->post->save))
1467
			redirectexit('action=admin;area=theme;sa=browse;th=' . $selectedTheme);
1468
1469
		// We don't have errors. Yet.
1470
		$context['session_error'] = false;
1471
1472
		// We're editing a theme file.
1473
		// Get the directory of the theme we are editing.
1474
		$context['theme_id'] = $selectedTheme;
1475
		$this->theme_dir = themeDirectory($context['theme_id']);
1476
1477
		$this->prepareThemeEditContext();
1478
1479
		// Saving?
1480
		if (isset($this->_req->post->save))
1481
		{
1482
			$this->_action_edit_submit();
1483
1484
			// Now lets get out of here!
1485
			return;
1486
		}
1487
1488
		// We're editing .css, .template.php, .{language}.php or others.
1489
		// Note: we're here sending $theme_dir as parameter to action_()
1490
		// controller functions, which isn't cool. To be refactored.
1491
		if (substr($this->_req->query->filename, -4) == '.css')
1492
			$this->_action_edit_style();
1493
		elseif (substr($this->_req->query->filename, -13) == '.template.php')
1494
			$this->_action_edit_template();
1495
		else
1496
			$this->_action_edit_file();
1497
1498
		// Create a special token to allow editing of multiple files.
1499
		createToken('admin-te-' . md5($selectedTheme . '-' . $this->_req->query->filename));
1500
	}
1501
1502
	/**
1503
	 * Displays for editing in admin panel a css file.
1504
	 *
1505
	 * This function is forwarded to, from
1506
	 * ?action=admin;area=theme;sa=edit
1507
	 */
1508
	private function _action_edit_style()
1509
	{
1510
		global $context, $settings;
1511
1512
		addJavascriptVar(array(
1513
			'previewData' => '',
1514
			'previewTimeout' => '',
1515
			'refreshPreviewCache' => '',
1516
			'editFilename' => $context['edit_filename'],
1517
			'theme_id' => $settings['theme_id'],
1518
		), true);
1519
1520
		// pick the template and send it the file
1521
		$context['sub_template'] = 'edit_style';
1522
		$context['entire_file'] = htmlspecialchars(strtr(file_get_contents($this->theme_dir . '/' . $this->_req->query->filename), array("\t" => '   ')), ENT_COMPAT, 'UTF-8');
1523
	}
1524
1525
	/**
1526
	 * Displays for editing in the admin panel a template file.
1527
	 *
1528
	 * This function is forwarded to, from
1529
	 * ?action=admin;area=theme;sa=edit
1530
	 */
1531
	private function _action_edit_template()
1532
	{
1533
		global $context;
1534
1535
		// Make sure the sub-template is set
1536
		$context['sub_template'] = 'edit_template';
1537
1538
		// Retrieve the contents of the file
1539
		$file_data = file($this->theme_dir . '/' . $this->_req->query->filename);
1540
1541
		// For a PHP template file, we display each function in separate boxes.
1542
		$j = 0;
1543
		$context['file_parts'] = array(array('lines' => 0, 'line' => 1, 'data' => '', 'function' => ''));
1544
		for ($i = 0, $n = count($file_data); $i < $n; $i++)
1545
		{
1546
			// @todo refactor this so the docblocks are in the function content window
1547
			if (substr($file_data[$i], 0, 9) === 'function ')
1548
			{
1549
				// Try to format the functions a little nicer...
1550
				$context['file_parts'][$j]['data'] = trim($context['file_parts'][$j]['data']);
1551
1552
				if (empty($context['file_parts'][$j]['lines']))
1553
					unset($context['file_parts'][$j]);
1554
1555
				// Start a new function block
1556
				$context['file_parts'][++$j] = array('lines' => 0, 'line' => $i, 'data' => '');
1557
			}
1558
1559
			$context['file_parts'][$j]['lines']++;
1560
			$context['file_parts'][$j]['data'] .= htmlspecialchars(strtr($file_data[$i], array("\t" => '   ')), ENT_COMPAT, 'UTF-8');
1561
		}
1562
1563
		$context['entire_file'] = htmlspecialchars(strtr(implode('', $file_data), array("\t" => '   ')), ENT_COMPAT, 'UTF-8');
1564
	}
1565
1566
	/**
1567
	 * Handles editing in admin of other types of files from a theme,
1568
	 * except templates and css.
1569
	 *
1570
	 * This function is forwarded to, from
1571
	 * ?action=admin;area=theme;sa=edit
1572
	 */
1573
	private function _action_edit_file()
1574
	{
1575
		global $context;
1576
1577
		// Simply set the template and the file contents.
1578
		$context['sub_template'] = 'edit_file';
1579
		$context['entire_file'] = htmlspecialchars(strtr(file_get_contents($this->theme_dir . '/' . $this->_req->query->filename), array("\t" => '   ')), ENT_COMPAT, 'UTF-8');
1580
	}
1581
1582
	/**
1583
	 * This function handles submission of a template file.
1584
	 * It checks the file for syntax errors, and if it passes, it saves it.
1585
	 *
1586
	 * This function is forwarded to, from
1587
	 * ?action=admin;area=theme;sa=edit
1588
	 */
1589
	private function _action_edit_submit()
0 ignored issues
show
Coding Style introduced by
_action_edit_submit uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1590
	{
1591
		global $context, $settings, $user_info;
1592
1593
		$selectedTheme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
1594
		if (empty($selectedTheme))
1595
		{
1596
			// This should never be happening. Never I say. But... in case it does :P
1597
			Errors::instance()->fatal_lang_error('theme_edit_missing');
1598
		}
1599
1600
		$theme_dir = themeDirectory($context['theme_id']);
1601
		$file = isset($this->_req->post->entire_file) ? $this->_req->post->entire_file : '';
1602
1603
		// You did submit *something*, didn't you?
1604
		if (empty($file))
1605
		{
1606
			// @todo a better error message
1607
			Errors::instance()->fatal_lang_error('theme_edit_missing');
1608
		}
1609
1610
		// Checking PHP syntax on css files is not a most constructive use of processing power :P
1611
		// We need to know what kind of file we have
1612
		$is_php = substr($this->_req->post->filename, -4) == '.php';
1613
		$is_template = substr($this->_req->post->filename, -13) == '.template.php';
1614
		$is_css = substr($this->_req->post->filename, -4) == '.css';
1615
1616
		// Check you up
1617
		if (checkSession('post', '', false) == '' && validateToken('admin-te-' . md5($selectedTheme . '-' . $this->_req->post->filename), 'post', false) == true)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1618
		{
1619
			// Consolidate the format in which we received the file contents
1620
			if (is_array($file))
1621
				$entire_file = implode("\n", $file);
1622
			else
1623
				$entire_file = $file;
1624
1625
			// Convert our tabs back to tabs!
1626
			$entire_file = rtrim(strtr($entire_file, array("\r" => '', '   ' => "\t")));
1627
1628
			// Errors? No errors!
1629
			$errors = array();
1630
1631
			// For PHP files, we check the syntax.
1632
			if ($is_php)
1633
			{
1634
				require_once(SUBSDIR . '/Modlog.subs.php');
1635
1636
				// Since we are running php code, let's track it, but only once in a while.
1637
				if (!recentlyLogged('editing_theme', 60))
1638
				{
1639
					logAction('editing_theme', array('member' => $user_info['id']), 'admin');
1640
1641
					// But the email only once every 60 minutes should be fine
1642
					if (!recentlyLogged('editing_theme', 3600))
1643
					{
1644
						require_once(SUBSDIR . '/Themes.subs.php');
1645
						require_once(SUBSDIR . '/Admin.subs.php');
1646
1647
						$theme_info = getBasicThemeInfos($context['theme_id']);
1648
						emailAdmins('editing_theme', array(
1649
							'EDIT_REALNAME' => $user_info['name'],
1650
							'FILE_EDITED' => $this->_req->post->filename,
1651
							'THEME_NAME' => $theme_info[$context['theme_id']],
1652
						));
1653
					}
1654
				}
1655
1656
				$validator = new Data_Validator();
1657
				$validator->validation_rules(array(
1658
					'entire_file' => 'php_syntax'
1659
				));
1660
				$validator->validate(array('entire_file' => $entire_file));
1661
1662
				// Retrieve the errors
1663
				$errors = $validator->validation_errors();
1664
			}
1665
1666
			// If successful so far, we'll take the plunge and save this piece of art.
1667
			if (empty($errors))
1668
			{
1669
				// Try to save the new file contents
1670
				$fp = fopen($theme_dir . '/' . $this->_req->post->filename, 'w');
1671
				fwrite($fp, $entire_file);
1672
				fclose($fp);
1673
1674
				if (function_exists('opcache_invalidate'))
1675
					opcache_invalidate($theme_dir . '/' . $_REQUEST['filename']);
1676
1677
				// We're done here.
1678
				redirectexit('action=admin;area=theme;th=' . $selectedTheme . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=browse;directory=' . dirname($this->_req->post->filename));
1679
			}
1680
			// I can't let you off the hook yet: syntax errors are a nasty beast.
1681
			else
1682
			{
1683
				// Pick the right sub-template for the next try
1684
				if ($is_template)
1685
					$context['sub_template'] = 'edit_template';
1686
				else
1687
					$context['sub_template'] = 'edit_file';
1688
1689
				// Fill contextual data for the template, the errors to show
1690
				foreach ($errors as $error)
1691
					$context['parse_error'][] = $error;
1692
1693
				// The format of the data depends on template/non-template file.
1694
				if (!is_array($file))
1695
					$file = array($file);
1696
1697
				// Send back the file contents
1698
				$context['entire_file'] = htmlspecialchars(strtr(implode('', $file), array("\t" => '   ')), ENT_COMPAT, 'UTF-8');
1699
1700
				foreach ($file as $i => $file_part)
1701
				{
1702
					$context['file_parts'][$i]['lines'] = strlen($file_part);
1703
					$context['file_parts'][$i]['data'] = $file_part;
1704
				}
1705
1706
				// Re-create token for another try
1707
				createToken('admin-te-' . md5($selectedTheme . '-' . $this->_req->post->filename));
1708
1709
				return;
1710
			}
1711
		}
1712
		// Session timed out.
1713
		else
1714
		{
1715
			loadLanguage('Errors');
1716
1717
			// Notify the template of trouble
1718
			$context['session_error'] = true;
1719
1720
			// Recycle the submitted data.
1721
			if (is_array($file))
1722
				$context['entire_file'] = htmlspecialchars(implode("\n", $file), ENT_COMPAT, 'UTF-8');
1723
			else
1724
				$context['entire_file'] = htmlspecialchars($file, ENT_COMPAT, 'UTF-8');
1725
1726
			$context['edit_filename'] = htmlspecialchars($this->_req->post->filename, ENT_COMPAT, 'UTF-8');
1727
1728
			// Choose sub-template
1729
			if ($is_template)
1730
				$context['sub_template'] = 'edit_template';
1731
			elseif ($is_css)
1732
			{
1733
				addJavascriptVar(array(
1734
					'previewData' => '\'\'',
1735
					'previewTimeout' => '\'\'',
1736
					'refreshPreviewCache' => '\'\'',
1737
					'editFilename' => JavaScriptEscape($context['edit_filename']),
1738
					'theme_id' => $settings['theme_id'],
1739
				));
1740
				$context['sub_template'] = 'edit_style';
1741
			}
1742
			else
1743
				$context['sub_template'] = 'edit_file';
1744
1745
			// Re-create the token so that it can be used
1746
			createToken('admin-te-' . md5($selectedTheme . '-' . $this->_req->post->filename));
1747
1748
			return;
1749
		}
1750
	}
1751
1752
	/**
1753
	 * Handles user browsing in theme directories.
1754
	 *
1755
	 * What it does:
1756
	 * - The display will allow to choose a file for editing,
1757
	 * if it is writable.
1758
	 * - accessed with ?action=admin;area=theme;sa=browse
1759
	 */
1760
	public function action_browse()
1761
	{
1762
		global $context, $scripturl;
1763
1764
		loadTemplate('ManageThemes');
1765
1766
		// We'll work hard with them themes!
1767
		require_once(SUBSDIR . '/Themes.subs.php');
1768
1769
		$selectedTheme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
1770
		if (empty($selectedTheme))
1771
			redirectexit('action=admin;area=theme;sa=themelist');
1772
1773
		// Get first the directory of the theme we are editing.
1774
		$context['theme_id'] = isset($this->_req->query->th) ? (int) $this->_req->query->th : (isset($this->_req->query->id) ? (int) $this->_req->query->id : 0);
1775
		$theme_dir = themeDirectory($context['theme_id']);
1776
1777
		// Eh? not trying to sneak a peek outside the theme directory are we
1778
		if (!file_exists($theme_dir . '/index.template.php') && !file_exists($theme_dir . '/css/index.css'))
1779
			Errors::instance()->fatal_lang_error('theme_edit_missing', false);
1780
1781
		// Now, where exactly are you?
1782
		if (isset($this->_req->query->directory))
1783
		{
1784
			if (substr($this->_req->query->directory, 0, 1) === '.')
1785
				$this->_req->query->directory = '';
1786
			else
1787
			{
1788
				$this->_req->query->directory = preg_replace(array('~^[\./\\:\0\n\r]+~', '~[\\\\]~', '~/[\./]+~'), array('', '/', '/'), $this->_req->query->directory);
1789
1790
				$temp = realpath($theme_dir . '/' . $this->_req->query->directory);
1791
				if (empty($temp) || substr($temp, 0, strlen(realpath($theme_dir))) != realpath($theme_dir))
1792
					$this->_req->query->directory = '';
1793
			}
1794
		}
1795
1796
		if (isset($this->_req->query->directory) && $this->_req->query->directory != '')
1797
		{
1798
			$context['theme_files'] = get_file_listing($theme_dir . '/' . $this->_req->query->directory, $this->_req->query->directory . '/');
1799
1800
			$temp = dirname($this->_req->query->directory);
1801
			array_unshift($context['theme_files'], array(
1802
				'filename' => $temp == '.' || $temp == '' ? '/ (..)' : $temp . ' (..)',
1803
				'is_writable' => is_writable($theme_dir . '/' . $temp),
1804
				'is_directory' => true,
1805
				'is_template' => false,
1806
				'is_image' => false,
1807
				'is_editable' => false,
1808
				'href' => $scripturl . '?action=admin;area=theme;th=' . $context['theme_id'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=browse;directory=' . $temp,
1809
				'size' => '',
1810
			));
1811
		}
1812
		else
1813
			$context['theme_files'] = get_file_listing($theme_dir, '');
1814
1815
		// finally, load the sub-template
1816
		$context['sub_template'] = 'browse';
1817
	}
1818
1819
	/**
1820
	 * List installed themes.
1821
	 * The listing will allow editing if the files are writable.
1822
	 */
1823
	public function action_themelist()
1824
	{
1825
		global $context;
1826
1827
		loadTemplate('ManageThemes');
1828
1829
		// We'll work hard with them themes!
1830
		require_once(SUBSDIR . '/Themes.subs.php');
1831
1832
		$context['themes'] = installedThemes();
1833
1834
		foreach ($context['themes'] as $key => $theme)
1835
		{
1836
			// There has to be a Settings template!
1837
			if (!file_exists($theme['theme_dir'] . '/index.template.php') && !file_exists($theme['theme_dir'] . '/css/index.css'))
1838
				unset($context['themes'][$key]);
1839
			else
1840
			{
1841
				if (!isset($theme['theme_templates']))
1842
					$templates = array('index');
1843
				else
1844
					$templates = explode(',', $theme['theme_templates']);
1845
1846
				foreach ($templates as $template)
1847
					if (file_exists($theme['theme_dir'] . '/' . $template . '.template.php'))
1848
					{
1849
						// Fetch the header... a good 256 bytes should be more than enough.
1850
						$fp = fopen($theme['theme_dir'] . '/' . $template . '.template.php', 'rb');
1851
						$header = fread($fp, 256);
1852
						fclose($fp);
1853
1854
						// Can we find a version comment, at all?
1855
						if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1)
1856
						{
1857
							$ver = $match[1];
1858
							if (!isset($context['themes'][$key]['version']) || $context['themes'][$key]['version'] > $ver)
1859
								$context['themes'][$key]['version'] = $ver;
1860
						}
1861
					}
1862
1863
				$context['themes'][$key]['can_edit_style'] = file_exists($theme['theme_dir'] . '/css/index.css');
1864
			}
1865
		}
1866
1867
		$context['sub_template'] = 'themelist';
1868
	}
1869
1870
	/**
1871
	 * Makes a copy of a template file in a new location
1872
	 *
1873
	 * @uses ManageThemes template, copy_template sub-template.
1874
	 */
1875
	public function action_copy()
1876
	{
1877
		global $context, $settings;
1878
1879
		loadTemplate('ManageThemes');
1880
		require_once(SUBSDIR . '/Themes.subs.php');
1881
1882
		$context[$context['admin_menu_name']]['current_subsection'] = 'edit';
1883
1884
		$context['theme_id'] = isset($this->_req->query->th) ? (int) $this->_req->query->th : (int) $this->_req->query->id;
1885
1886
		$theme_dirs = array();
1887
		$theme_dirs = loadThemeOptionsInto($context['theme_id'], null, $theme_dirs, array('base_theme_dir', 'theme_dir'));
1888
1889
		if (isset($this->_req->query->template) && preg_match('~[\./\\\\:\0]~', $this->_req->query->template) == 0)
1890
		{
1891
			if (!empty($theme_dirs['base_theme_dir']) && file_exists($theme_dirs['base_theme_dir'] . '/' . $this->_req->query->template . '.template.php'))
1892
				$filename = $theme_dirs['base_theme_dir'] . '/' . $this->_req->query->template . '.template.php';
1893
			elseif (file_exists($settings['default_theme_dir'] . '/' . $this->_req->query->template . '.template.php'))
1894
				$filename = $settings['default_theme_dir'] . '/' . $this->_req->query->template . '.template.php';
1895
			else
1896
				Errors::instance()->fatal_lang_error('no_access', false);
1897
1898
			$fp = fopen($theme_dirs['theme_dir'] . '/' . $this->_req->query->template . '.template.php', 'w');
1899
			fwrite($fp, file_get_contents($filename));
0 ignored issues
show
Bug introduced by
The variable $filename does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1900
			fclose($fp);
1901
1902
			if (function_exists('opcache_invalidate'))
1903
				opcache_invalidate($filename);
1904
1905
			redirectexit('action=admin;area=theme;th=' . $context['theme_id'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=copy');
1906
		}
1907
		elseif (isset($this->_req->query->lang_file) && preg_match('~^[^\./\\\\:\0]\.[^\./\\\\:\0]$~', $this->_req->query->lang_file) != 0)
1908
		{
1909
			if (!empty($theme_dirs['base_theme_dir']) && file_exists($theme_dirs['base_theme_dir'] . '/languages/' . $this->_req->query->lang_file . '.php'))
1910
				$filename = $theme_dirs['base_theme_dir'] . '/languages/' . $this->_req->query->template . '.php';
1911
			elseif (file_exists($settings['default_theme_dir'] . '/languages/' . $this->_req->query->template . '.php'))
1912
				$filename = $settings['default_theme_dir'] . '/languages/' . $this->_req->query->template . '.php';
1913
			else
1914
				Errors::instance()->fatal_lang_error('no_access', false);
1915
1916
			$fp = fopen($theme_dirs['theme_dir'] . '/languages/' . $this->_req->query->lang_file . '.php', 'w');
1917
			fwrite($fp, file_get_contents($filename));
1918
			fclose($fp);
1919
1920
			if (function_exists('opcache_invalidate'))
1921
				opcache_invalidate($filename);
1922
1923
			redirectexit('action=admin;area=theme;th=' . $context['theme_id'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=copy');
1924
		}
1925
1926
		$templates = array();
1927
		$lang_files = array();
1928
1929
		$dir = dir($settings['default_theme_dir']);
1930
		while ($entry = $dir->read())
1931
		{
1932
			if (substr($entry, -13) == '.template.php')
1933
				$templates[] = substr($entry, 0, -13);
1934
		}
1935
		$dir->close();
1936
1937
		$dir = dir($settings['default_theme_dir'] . '/languages');
1938
		while ($entry = $dir->read())
1939
		{
1940
			if (preg_match('~^([^\.]+\.[^\.]+)\.php$~', $entry, $matches))
1941
				$lang_files[] = $matches[1];
1942
		}
1943
		$dir->close();
1944
1945
		if (!empty($theme_dirs['base_theme_dir']))
1946
		{
1947
			$dir = dir($theme_dirs['base_theme_dir']);
1948
			while ($entry = $dir->read())
1949
			{
1950
				if (substr($entry, -13) == '.template.php' && !in_array(substr($entry, 0, -13), $templates))
1951
					$templates[] = substr($entry, 0, -13);
1952
			}
1953
			$dir->close();
1954
1955
			if (file_exists($theme_dirs['base_theme_dir'] . '/languages'))
1956
			{
1957
				$dir = dir($theme_dirs['base_theme_dir'] . '/languages');
1958
				while ($entry = $dir->read())
1959
				{
1960
					if (preg_match('~^([^\.]+\.[^\.]+)\.php$~', $entry, $matches) && !in_array($matches[1], $lang_files))
1961
						$lang_files[] = $matches[1];
1962
				}
1963
				$dir->close();
1964
			}
1965
		}
1966
1967
		natcasesort($templates);
1968
		natcasesort($lang_files);
1969
1970
		$context['available_templates'] = array();
1971
		foreach ($templates as $template)
1972
			$context['available_templates'][$template] = array(
1973
				'filename' => $template . '.template.php',
1974
				'value' => $template,
1975
				'already_exists' => false,
1976
				'can_copy' => is_writable($theme_dirs['theme_dir']),
1977
			);
1978
		$context['available_language_files'] = array();
1979
		foreach ($lang_files as $file)
1980
			$context['available_language_files'][$file] = array(
1981
				'filename' => $file . '.php',
1982
				'value' => $file,
1983
				'already_exists' => false,
1984
				'can_copy' => file_exists($theme_dirs['theme_dir'] . '/languages') ? is_writable($theme_dirs['theme_dir'] . '/languages') : is_writable($theme_dirs['theme_dir']),
1985
			);
1986
1987
		$dir = dir($theme_dirs['theme_dir']);
1988
		while ($entry = $dir->read())
1989
		{
1990
			if (substr($entry, -13) == '.template.php' && isset($context['available_templates'][substr($entry, 0, -13)]))
1991
			{
1992
				$context['available_templates'][substr($entry, 0, -13)]['already_exists'] = true;
1993
				$context['available_templates'][substr($entry, 0, -13)]['can_copy'] = is_writable($theme_dirs['theme_dir'] . '/' . $entry);
1994
			}
1995
		}
1996
		$dir->close();
1997
1998
		if (file_exists($theme_dirs['theme_dir'] . '/languages'))
1999
		{
2000
			$dir = dir($theme_dirs['theme_dir'] . '/languages');
2001
			while ($entry = $dir->read())
2002
			{
2003
				if (preg_match('~^([^\.]+\.[^\.]+)\.php$~', $entry, $matches) && isset($context['available_language_files'][$matches[1]]))
2004
				{
2005
					$context['available_language_files'][$matches[1]]['already_exists'] = true;
2006
					$context['available_language_files'][$matches[1]]['can_copy'] = is_writable($theme_dirs['theme_dir'] . '/languages/' . $entry);
2007
				}
2008
			}
2009
			$dir->close();
2010
		}
2011
2012
		$context['sub_template'] = 'copy_template';
2013
	}
2014
2015
	/**
2016
	 * This function makes necessary pre-checks and fills
2017
	 * the contextual data as needed by theme editing functions.
2018
	 */
2019
	private function prepareThemeEditContext()
2020
	{
2021
		global $context;
2022
2023
		// Eh? not trying to sneak a peek outside the theme directory are we
2024
		if (!file_exists($this->theme_dir . '/index.template.php') && !file_exists($this->theme_dir . '/css/index.css'))
2025
			Errors::instance()->fatal_lang_error('theme_edit_missing', false);
2026
2027
		// Get the filename from the appropriate spot
2028
		$filename = isset($this->_req->post->save) ? $this->_req->getPost('filename', 'strval', '') : $this->_req->getQuery('filename', 'strval', '');
2029
2030
		// You're editing a file: we have extra-checks coming up first.
2031
		if (substr($filename, 0, 1) === '.')
2032
			$filename = '';
2033
		else
2034
		{
2035
			$filename = preg_replace(array('~^[\./\\:\0\n\r]+~', '~[\\\\]~', '~/[\./]+~'), array('', '/', '/'), $filename);
2036
2037
			$temp = realpath($this->theme_dir . '/' . $filename);
2038
			if (empty($temp) || substr($temp, 0, strlen(realpath($this->theme_dir))) !== realpath($this->theme_dir))
2039
				$filename = '';
2040
		}
2041
2042
		// We shouldn't end up with no file
2043
		if (empty($filename))
2044
			Errors::instance()->fatal_lang_error('theme_edit_missing', false);
2045
2046
		// Initialize context
2047
		$context['allow_save'] = is_writable($this->theme_dir . '/' . $filename);
2048
		$context['allow_save_filename'] = strtr($this->theme_dir . '/' . $filename, array(BOARDDIR => '...'));
2049
		$context['edit_filename'] = htmlspecialchars($filename, ENT_COMPAT, 'UTF-8');
2050
	}
2051
}