Completed
Push — patch_1-1-4 ( 567711...ded5b7 )
by Emanuele
17s
created

ManageThemes_Controller::_checkOpcache()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 2
nc 3
nop 0
dl 0
loc 4
ccs 0
cts 4
cp 0
crap 12
rs 10
c 0
b 0
f 0
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 file contains code covered by:
13
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
14
 * license:		BSD, See included LICENSE.TXT for terms and conditions.
15
 *
16
 * @version 1.1.4
17
 *
18
 *
19
 * @todo Update this for the new package manager?
20
 *
21
 * Creating and distributing theme packages:
22
 * There isn't that much required to package and distribute your own themes...
23
 * just do the following:
24
 *
25
 *  - create a theme_info.xml file, with the root element theme-info.
26
 *  - its name should go in a name element, just like description.
27
 *  - your name should go in author. (email in the email attribute.)
28
 *  - any support website for the theme should be in website.
29
 *  - layers and templates (non-default) should go in those elements ;).
30
 *  - if the images dir isn't images, specify in the images element.
31
 *  - any extra rows for themes should go in extra, serialized. (as in array(variable => value).)
32
 *  - tar and gzip the directory - and you're done!
33
 *  - please include any special license in a license.txt file.
34
 */
35
36
/**
37
 * Class to deal with theme administration.
38
 *
39
 * Its tasks include changing theme settings, installing and removing
40
 * themes, choosing the current theme, and editing themes.
41
 *
42
 * @package Themes
43
 */
44
class ManageThemes_Controller extends Action_Controller
45
{
46
	/**
47
	 * Holds the selected theme options
48
	 * @var mixed[]
49
	 */
50
	private $_options;
51
52
	/**
53
	 * Holds the selected default theme options
54
	 * @var mixed[]
55
	 */
56
	private $_default_options;
57
58
	/**
59
	 * Holds the selected master options for a theme
60
	 * @var mixed[]
61
	 */
62
	private $_options_master;
63
64
	/**
65
	 * Holds the selected default master options for a theme
66
	 * @var mixed[]
67
	 */
68
	private $_default_options_master;
69
70
	/**
71
	 * Name of the theme
72
	 * @var string
73
	 */
74
	private $theme_name;
75
76
	/**
77
	 * Full path to the theme
78
	 * @var string
79
	 */
80
	private $theme_dir;
81
82
	/**
83
	 * The themes images url if any
84
	 * @var string|null
85
	 */
86
	private $images_url;
87
88
	/**
89
	 * {@inheritdoc }
90
	 */
91
	public function trackStats($action = '')
92
	{
93
		if ($action === 'action_jsoption')
94
		{
95
			return false;
96
		}
97
98
		return parent::trackStats($action);
99
	}
100
101
	/**
102
	 * Subaction handler - manages the action and delegates control to the proper
103
	 * sub-action.
104
	 *
105
	 * What it does:
106
	 *
107
	 * - It loads both the Themes and Settings language files.
108
	 * - Checks the session by GET or POST to verify the sent data.
109
	 * - Requires the user to not be a guest.
110
	 * - Accessed via ?action=admin;area=theme.
111
	 *
112
	 * @see Action_Controller::action_index()
113
	 */
114
	public function action_index()
115
	{
116
		global $txt, $context;
117
118
		if (isset($this->_req->query->api))
119
		{
120
			$this->action_index_api();
121
			return;
122
		}
123
124
		// Load the important language files...
125
		loadLanguage('ManageThemes');
126
		loadLanguage('Settings');
127
128
		// No guests in here.
129
		is_not_guest();
130
131
		// Theme administration, removal, choice, or installation...
132
		$subActions = array(
133
			'admin' => array($this, 'action_admin', 'permission' => 'admin_forum'),
134
			'list' => array($this, 'action_list', 'permission' => 'admin_forum'),
135
			'reset' => array($this, 'action_options', 'permission' => 'admin_forum'),
136
			'options' => array($this, 'action_options', 'permission' => 'admin_forum'),
137
			'install' => array($this, 'action_install', 'permission' => 'admin_forum'),
138
			'remove' => array($this, 'action_remove', 'permission' => 'admin_forum'),
139
			'pick' => array($this, 'action_pick'), // @todo ugly having that in this controller
140
			'edit' => array($this, 'action_edit', 'permission' => 'admin_forum'),
141
			'copy' => array($this, 'action_copy', 'permission' => 'admin_forum'),
142
			'themelist' => array($this, 'action_themelist', 'permission' => 'admin_forum'),
143
			'browse' => array($this, 'action_browse', 'permission' => 'admin_forum'),
144
		);
145
146
		// Action controller
147
		$action = new Action('manage_themes');
148
149
		// @todo Layout Settings?
150
		if (!empty($context['admin_menu_name']))
151
		{
152
			$context[$context['admin_menu_name']]['tab_data'] = array(
153
				'title' => $txt['themeadmin_title'],
154
				'description' => $txt['themeadmin_description'],
155
				'tabs' => array(
156
					'admin' => array(
157
						'description' => $txt['themeadmin_admin_desc'],
158
					),
159
					'list' => array(
160
						'description' => $txt['themeadmin_list_desc'],
161
					),
162
					'reset' => array(
163
						'description' => $txt['themeadmin_reset_desc'],
164
					),
165
					'edit' => array(
166
						'description' => $txt['themeadmin_edit_desc'],
167
					),
168
					'themelist' => array(
169
						'description' => $txt['themeadmin_edit_desc'],
170
					),
171
					'browse' => array(
172
						'description' => $txt['themeadmin_edit_desc'],
173
					),
174
				),
175
			);
176
		}
177
178
		// Follow the sa or just go to administration, call integrate_sa_manage_themes
179
		$subAction = $action->initialize($subActions, 'admin');
180
181
		// Default the page title to Theme Administration by default.
182
		$context['page_title'] = $txt['themeadmin_title'];
183
		$context['sub_action'] = $subAction;
184
185
		// Go to the action, if you have permissions
186
		$action->dispatch($subAction);
187
	}
188
189
	/**
190
	 * Responds to an ajax button request, currently only for remove
191
	 *
192
	 * @uses generic_xml_buttons sub template
193
	 */
194
	public function action_index_api()
195
	{
196
		global $txt, $context, $user_info;
197
198
		loadTemplate('Xml');
199
200
		// Remove any template layers that may have been created, this is XML!
201
		Template_Layers::instance()->removeAll();
202
		$context['sub_template'] = 'generic_xml_buttons';
203
204
		// No guests in here.
205 View Code Duplication
		if ($user_info['is_guest'])
206
		{
207
			loadLanguage('Errors');
208
			$context['xml_data'] = array(
209
				'error' => 1,
210
				'text' => $txt['not_guests']
211
			);
212
213
			return;
214
		}
215
216
		// Theme administration, removal, choice, or installation...
217
		// Of all the actions we currently know only this
218
		$subActions = array(
219
		// 'admin' => 'action_admin',
220
		// 'list' => 'action_list',
221
		// 'reset' => 'action_options',
222
		// 'options' => 'action_options',
223
		// 'install' => 'action_install',
224
			'remove' => 'action_remove_api',
225
		// 'pick' => 'action_pick',
226
		// 'edit' => 'action_edit',
227
		// 'copy' => 'action_copy',
228
		// 'themelist' => 'action_themelist',
229
		// 'browse' => 'action_browse',
230
		);
231
232
		// Follow the sa or just go to administration.
233
		if (isset($this->_req->query->sa) && !empty($subActions[$this->_req->query->sa]))
234
			$this->{$subActions[$this->_req->query->sa]}();
235
		else
236
		{
237
			loadLanguage('Errors');
238
			$context['xml_data'] = array(
239
				'error' => 1,
240
				'text' => $txt['error_sa_not_set']
241
			);
242
			return;
243
		}
244
	}
245
246
	/**
247
	 * This function allows administration of themes and their settings,
248
	 * as well as global theme settings.
249
	 *
250
	 * What it does:
251
	 *
252
	 * - sets the settings theme_allow, theme_guests, and knownThemes.
253
	 * - requires the admin_forum permission.
254
	 * - accessed with ?action=admin;area=theme;sa=admin.
255
	 *
256
	 * @uses Themes template
257
	 * @uses Admin language file
258
	 */
259
	public function action_admin()
260
	{
261
		global $context, $modSettings;
262
263
		loadLanguage('Admin');
264
265
		// Saving?
266
		if (isset($this->_req->post->save))
267
		{
268
			checkSession();
269
			validateToken('admin-tm');
270
271
			// What themes are being made as known to the members
272
			if (isset($this->_req->post->options['known_themes']))
273
			{
274
				foreach ($this->_req->post->options['known_themes'] as $key => $id)
275
					$this->_req->post->options['known_themes'][$key] = (int) $id;
276
			}
277
			else
278
				throw new Elk_Exception('themes_none_selectable', false);
279
280
			if (!in_array($this->_req->post->options['theme_guests'], $this->_req->post->options['known_themes']))
281
				throw new Elk_Exception('themes_default_selectable', false);
282
283
			// Commit the new settings.
284
			updateSettings(array(
285
				'theme_allow' => !empty($this->_req->post->options['theme_allow']),
286
				'theme_guests' => $this->_req->post->options['theme_guests'],
287
				'knownThemes' => implode(',', $this->_req->post->options['known_themes']),
288
			));
289
290
			if ((int) $this->_req->post->theme_reset == 0 || in_array($this->_req->post->theme_reset, $this->_req->post->options['known_themes']))
291
			{
292
				require_once(SUBSDIR . '/Members.subs.php');
293
				updateMemberData(null, array('id_theme' => (int) $this->_req->post->theme_reset));
294
			}
295
296
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=admin');
297
		}
298
		// If we aren't submitting - that is, if we are about to...
299
		else
300
		{
301
			loadTemplate('ManageThemes');
302
			$context['sub_template'] = 'manage_themes';
303
304
			// Make our known themes a little easier to work with.
305
			$knownThemes = !empty($modSettings['knownThemes']) ? explode(',', $modSettings['knownThemes']) : array();
306
307
			// Load up all the themes.
308
			require_once(SUBSDIR . '/Themes.subs.php');
309
			$context['themes'] = loadThemes($knownThemes);
310
311
			// Can we create a new theme?
312
			$context['can_create_new'] = is_writable(BOARDDIR . '/themes');
313
			$context['new_theme_dir'] = substr(realpath(BOARDDIR . '/themes/default'), 0, -7);
314
315
			// Look for a non existent theme directory. (ie theme87.)
316
			$theme_dir = BOARDDIR . '/themes/theme';
317
			$i = 1;
318
			while (file_exists($theme_dir . $i))
319
				$i++;
320
			$context['new_theme_name'] = 'theme' . $i;
321
322
			createToken('admin-tm');
323
		}
324
	}
325
326
	/**
327
	 * This function lists the available themes and provides an interface
328
	 * to reset the paths of all the installed themes.
329
	 *
330
	 * @uses sub template list_themes, template ManageThemes
331
	 */
332
	public function action_list()
333
	{
334
		global $context, $boardurl, $txt;
335
336
		// Load in the helpers we need
337
		require_once(SUBSDIR . '/Themes.subs.php');
338
		loadLanguage('Admin');
339
340
		if (isset($this->_req->query->th))
341
			return $this->action_setthemesettings();
342
343
		// Saving?
344
		if (isset($this->_req->post->save))
345
		{
346
			checkSession();
347
			validateToken('admin-tl');
348
349
			$themes = installedThemes();
350
351
			$setValues = array();
352
			foreach ($themes as $id => $theme)
353
			{
354 View Code Duplication
				if (file_exists($this->_req->post->reset_dir . '/' . basename($theme['theme_dir'])))
355
				{
356
					$setValues[] = array($id, 0, 'theme_dir', realpath($this->_req->post->reset_dir . '/' . basename($theme['theme_dir'])));
357
					$setValues[] = array($id, 0, 'theme_url', $this->_req->post->reset_url . '/' . basename($theme['theme_dir']));
358
					$setValues[] = array($id, 0, 'images_url', $this->_req->post->reset_url . '/' . basename($theme['theme_dir']) . '/' . basename($theme['images_url']));
359
				}
360
361 View Code Duplication
				if (isset($theme['base_theme_dir']) && file_exists($this->_req->post->reset_dir . '/' . basename($theme['base_theme_dir'])))
362
				{
363
					$setValues[] = array($id, 0, 'base_theme_dir', realpath($this->_req->post->reset_dir . '/' . basename($theme['base_theme_dir'])));
364
					$setValues[] = array($id, 0, 'base_theme_url', $this->_req->post->reset_url . '/' . basename($theme['base_theme_dir']));
365
					$setValues[] = array($id, 0, 'base_images_url', $this->_req->post->reset_url . '/' . basename($theme['base_theme_dir']) . '/' . basename($theme['base_images_url']));
366
				}
367
368
				Cache::instance()->remove('theme_settings-' . $id);
369
			}
370
371
			if (!empty($setValues))
372
				updateThemeOptions($setValues);
373
374
			redirectexit('action=admin;area=theme;sa=list;' . $context['session_var'] . '=' . $context['session_id']);
375
		}
376
377
		loadTemplate('ManageThemes');
378
379
		$context['themes'] = installedThemes();
380
381
		// For each theme, make sure the directory exists, and try to fetch the theme version
382
		foreach ($context['themes'] as $i => $theme)
383
		{
384
			$context['themes'][$i]['theme_dir'] = realpath($context['themes'][$i]['theme_dir']);
385
386
			if (file_exists($context['themes'][$i]['theme_dir'] . '/index.template.php'))
387
			{
388
				// Fetch the header... a good 256 bytes should be more than enough.
389
				$fp = fopen($context['themes'][$i]['theme_dir'] . '/index.template.php', 'rb');
390
				$header = fread($fp, 256);
391
				fclose($fp);
392
393
				// Can we find a version comment, at all?
394
				if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1)
395
					$context['themes'][$i]['version'] = $match[1];
396
			}
397
398
			$context['themes'][$i]['valid_path'] = file_exists($context['themes'][$i]['theme_dir']) && is_dir($context['themes'][$i]['theme_dir']);
399
		}
400
401
		// Off to the template we go
402
		$context['sub_template'] = 'list_themes';
403
		addJavascriptVar(array('txt_theme_remove_confirm' => $txt['theme_remove_confirm']), true);
404
		$context['reset_dir'] = realpath(BOARDDIR . '/themes');
405
		$context['reset_url'] = $boardurl . '/themes';
406
407
		createToken('admin-tl');
408
		createToken('admin-tr', 'request');
409
	}
410
411
	/**
412
	 * Administrative global settings.
413
	 *
414
	 * - Accessed by ?action=admin;area=theme;sa=reset;
415
	 *
416
	 * @uses sub template set_options, template file Settings
417
	 * @uses template file ManageThemes
418
	 */
419
	public function action_options()
420
	{
421
		global $txt, $context, $settings, $modSettings;
422
423
		require_once(SUBSDIR . '/Themes.subs.php');
424
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
425
426
		if (empty($theme) && empty($this->_req->query->id))
427
		{
428
			$context['themes'] = installedThemes();
429
430
			// How many options do we have setup for guests?
431
			$guestOptions = countConfiguredGuestOptions();
432
			foreach ($guestOptions as $guest_option)
433
				$context['themes'][$guest_option['id_theme']]['num_default_options'] = $guest_option['value'];
434
435
			// How many options do we have setup for members?
436
			$memberOptions = countConfiguredMemberOptions();
437
			foreach ($memberOptions as $member_option)
438
				$context['themes'][$member_option['id_theme']]['num_members'] = $member_option['value'];
439
440
			// There has to be a Settings template!
441
			foreach ($context['themes'] as $k => $v)
442
				if (empty($v['theme_dir']) || (!file_exists($v['theme_dir'] . '/Settings.template.php') && empty($v['num_members'])))
443
					unset($context['themes'][$k]);
444
445
			loadTemplate('ManageThemes');
446
			$context['sub_template'] = 'reset_list';
447
448
			createToken('admin-stor', 'request');
449
			return;
450
		}
451
452
		// Submit?
453
		if (isset($this->_req->post->submit) && empty($this->_req->post->who))
454
		{
455
			checkSession();
456
			validateToken('admin-sto');
457
458
			$this->_options = $this->_req->getPost('options', '', array());
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_req->getPost('options', '', array()) of type * is incompatible with the declared type array<integer,*> of property $_options.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
459
			$this->_default_options = $this->_req->getPost('default_options', '', array());
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_req->getPost('de..._options', '', array()) of type * is incompatible with the declared type array<integer,*> of property $_default_options.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
460
461
			// Set up the query values.
462
			$setValues = array();
463
			foreach ($this->_options as $opt => $val)
464
				$setValues[] = array($theme, -1, $opt, is_array($val) ? implode(',', $val) : $val);
465
466
			$old_settings = array();
467
			foreach ($this->_default_options as $opt => $val)
468
			{
469
				$old_settings[] = $opt;
470
				$setValues[] = array(1, -1, $opt, is_array($val) ? implode(',', $val) : $val);
471
			}
472
473
			// If we're actually inserting something..
474
			if (!empty($setValues))
475
			{
476
				// Are there options in non-default themes set that should be cleared?
477
				if (!empty($old_settings))
478
					removeThemeOptions('custom', 'guests', $old_settings);
479
480
				updateThemeOptions($setValues);
481
			}
482
483
			// Cache the theme settings
484
			Cache::instance()->remove('theme_settings-' . $theme);
485
			Cache::instance()->remove('theme_settings-1');
486
487
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset');
488
		}
489
		// Changing the current options for all members using this theme
490
		elseif (isset($this->_req->post->submit) && $this->_req->post->who == 1)
491
		{
492
			checkSession();
493
			validateToken('admin-sto');
494
495
			$this->_options = $this->_req->getPost('options', '', array());
496
			$this->_options_master = $this->_req->getPost('options_master', '', array());
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_req->getPost('op...s_master', '', array()) of type * is incompatible with the declared type array<integer,*> of property $_options_master.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
497
498
			$this->_default_options = $this->_req->getPost('default_options', '', array());
499
			$this->_default_options_master = $this->_req->getPost('default_options_master', '', array());
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_req->getPost('de...s_master', '', array()) of type * is incompatible with the declared type array<integer,*> of property $_default_options_master.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
500
501
			$old_settings = array();
502
			foreach ($this->_default_options as $opt => $val)
503
			{
504
				if ($this->_default_options_master[$opt] == 0)
505
					continue;
506
				elseif ($this->_default_options_master[$opt] == 1)
507
				{
508
					// Delete then insert for ease of database compatibility!
509
					removeThemeOptions('default', 'members', $opt);
510
					addThemeOptions(1, $opt, $val);
511
512
					$old_settings[] = $opt;
513
				}
514
				elseif ($this->_default_options_master[$opt] == 2)
515
					removeThemeOptions('all', 'members', $opt);
516
			}
517
518
			// Delete options from other themes.
519
			if (!empty($old_settings))
520
				removeThemeOptions('custom', 'members', $old_settings);
521
522
			foreach ($this->_options as $opt => $val)
523
			{
524
				if ($this->_options_master[$opt] == 0)
525
					continue;
526
				elseif ($this->_options_master[$opt] == 1)
527
				{
528
					// Delete then insert for ease of database compatibility - again!
529
					removeThemeOptions($theme, 'non_default', $opt);
530
					addThemeOptions($theme, $opt, $val);
531
				}
532
				elseif ($this->_options_master[$opt] == 2)
533
					removeThemeOptions($theme, 'all', $opt);
534
			}
535
536
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset');
537
		}
538
		// Remove all members options and use the defaults
539
		elseif (!empty($this->_req->query->who) && $this->_req->query->who == 2)
540
		{
541
			checkSession('get');
542
			validateToken('admin-stor', 'request');
543
544
			removeThemeOptions($theme, 'members');
545
546
			redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset');
547
		}
548
549
		$old_id = $settings['theme_id'];
550
		$old_settings = $settings;
551
552
		loadTheme($theme, false);
553
		loadLanguage('Profile');
554
555
		// @todo Should we just move these options so they are no longer theme dependant?
556
		loadLanguage('PersonalMessage');
557
558
		// Let the theme take care of the settings.
559
		loadTemplate('Settings');
560
		loadSubTemplate('options');
561
562
		// Set up for the template
563
		$context['sub_template'] = 'set_options';
564
		$context['page_title'] = $txt['theme_settings'];
565
		$context['options'] = $context['theme_options'];
566
		$context['theme_settings'] = $settings;
567
568
		// Load the options for these theme
569
		if (empty($this->_req->query->who))
570
		{
571
			$context['theme_options'] = loadThemeOptionsInto(array(1, $theme), -1, $context['theme_options']);
572
			$context['theme_options_reset'] = false;
573
		}
574
		else
575
		{
576
			$context['theme_options'] = array();
577
			$context['theme_options_reset'] = true;
578
		}
579
580
		// Prepare the options for the template
581
		foreach ($context['options'] as $i => $setting)
582
		{
583
			// Is this disabled?
584
			if ($setting['id'] === 'calendar_start_day' && empty($modSettings['cal_enabled']))
585
			{
586
				unset($context['options'][$i]);
587
				continue;
588
			}
589
			elseif (($setting['id'] === 'topics_per_page' || $setting['id'] === 'messages_per_page') && !empty($modSettings['disableCustomPerPage']))
590
			{
591
				unset($context['options'][$i]);
592
				continue;
593
			}
594
595
			// Type of field so we display the right input field
596 View Code Duplication
			if (!isset($setting['type']) || $setting['type'] === 'bool')
597
				$context['options'][$i]['type'] = 'checkbox';
598
			elseif ($setting['type'] === 'int' || $setting['type'] === 'integer')
599
				$context['options'][$i]['type'] = 'number';
600
			elseif ($setting['type'] === 'string')
601
				$context['options'][$i]['type'] = 'text';
602
603
			if (isset($setting['options']))
604
				$context['options'][$i]['type'] = 'list';
605
606
			$context['options'][$i]['value'] = !isset($context['theme_options'][$setting['id']]) ? '' : $context['theme_options'][$setting['id']];
607
		}
608
609
		// Restore the existing theme.
610
		loadTheme($old_id, false);
611
		$settings = $old_settings;
612
613
		loadTemplate('ManageThemes');
614
		createToken('admin-sto');
615
	}
616
617
	/**
618
	 * Administrative global settings.
619
	 *
620
	 * What it does:
621
	 *
622
	 * - Saves and requests global theme settings. ($settings)
623
	 * - Loads the Admin language file.
624
	 * - Calls action_admin() if no theme is specified. (the theme center.)
625
	 * - Requires admin_forum permission.
626
	 * - Accessed with ?action=admin;area=theme;sa=list&th=xx.
627
	 *
628
	 * @event integrate_init_theme
629
	 */
630
	public function action_setthemesettings()
631
	{
632
		global $txt, $context, $settings, $modSettings;
633
634
		require_once(SUBSDIR . '/Themes.subs.php');
635
636
		// Nothing chosen, back to the start you go
637
		if (empty($this->_req->query->th) && empty($this->_req->query->id))
638
			return $this->action_admin();
639
640
		// The theme's ID is needed
641
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
642
643
		// Validate inputs/user.
644
		if (empty($theme))
645
			throw new Elk_Exception('no_theme', false);
646
647
		// Select the best fitting tab.
648
		$context[$context['admin_menu_name']]['current_subsection'] = 'list';
649
		loadLanguage('Admin');
650
651
		// Fetch the smiley sets...
652
		$sets = explode(',', 'none,' . $modSettings['smiley_sets_known']);
653
		$set_names = explode("\n", $txt['smileys_none'] . "\n" . $modSettings['smiley_sets_names']);
654
		$context['smiley_sets'] = array(
655
			'' => $txt['smileys_no_default']
656
		);
657
		foreach ($sets as $i => $set)
658
			$context['smiley_sets'][$set] = htmlspecialchars($set_names[$i], ENT_COMPAT, 'UTF-8');
659
660
		$old_id = $settings['theme_id'];
661
		$old_settings = $settings;
662
663
		loadTheme($theme, false);
664
665
		// Also load the actual themes language file - in case of special settings.
666
		loadLanguage('Settings', '', true, true);
667
668
		// And the custom language strings...
669
		loadLanguage('ThemeStrings', '', false, true);
670
671
		// Let the theme take care of the settings.
672
		loadTemplate('Settings');
673
		loadSubTemplate('settings');
674
675
		// Load the variants separately...
676
		$settings['theme_variants'] = array();
677
		if (file_exists($settings['theme_dir'] . '/index.template.php'))
678
		{
679
			$file_contents = implode("\n", file($settings['theme_dir'] . '/index.template.php'));
680
			if (preg_match('~\'theme_variants\'\s*=>(.+?\)),$~sm', $file_contents, $matches))
681
				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...
682
683
				call_integration_hook('integrate_init_theme', array($theme, &$settings));
684
		}
685
686
		// Submitting!
687
		if (isset($this->_req->post->save))
688
		{
689
			// Allowed?
690
			checkSession();
691
			validateToken('admin-sts');
692
693
			$options = array();
694
			$options['options'] = empty($this->_req->post->options) ? array() : (array) $this->_req->post->options;
695
			$options['default_options'] = empty($this->_req->post->default_options) ? array() : (array) $this->_req->post->default_options;
696
697
			// Make sure items are cast correctly.
698
			foreach ($context['theme_settings'] as $item)
699
			{
700
				// Unwatch this item if this is just a separator.
701
				if (!is_array($item))
702
					continue;
703
704
				// Clean them up for the database
705
				foreach (array('options', 'default_options') as $option)
706
				{
707
					if (!isset($options[$option][$item['id']]))
708
						continue;
709
					// Checkbox.
710 View Code Duplication
					elseif (empty($item['type']))
711
						$options[$option][$item['id']] = $options[$option][$item['id']] ? 1 : 0;
712
					// Number
713 View Code Duplication
					elseif ($item['type'] === 'number')
714
						$options[$option][$item['id']] = (int) $options[$option][$item['id']];
715
				}
716
			}
717
718
			// Set up the sql query.
719
			$inserts = array();
720 View Code Duplication
			foreach ($options['options'] as $opt => $val)
721
				$inserts[] = array($theme, 0, $opt, is_array($val) ? implode(',', $val) : $val);
722
723 View Code Duplication
			foreach ($options['default_options'] as $opt => $val)
724
				$inserts[] = array(1, 0, $opt, is_array($val) ? implode(',', $val) : $val);
725
726
			// If we're actually inserting something..
727
			if (!empty($inserts))
728
				updateThemeOptions($inserts);
729
730
			// Clear and Invalidate the cache.
731
			Cache::instance()->remove('theme_settings-' . $theme);
732
			Cache::instance()->remove('theme_settings-1');
733
			updateSettings(array('settings_updated' => time()));
734
735
			redirectexit('action=admin;area=theme;sa=list;th=' . $theme . ';' . $context['session_var'] . '=' . $context['session_id']);
736
		}
737
738
		$context['sub_template'] = 'set_settings';
739
		$context['page_title'] = $txt['theme_settings'];
740
741
		foreach ($settings as $setting => $dummy)
742
		{
743
			if (!in_array($setting, array('theme_url', 'theme_dir', 'images_url', 'template_dirs')))
744
				$settings[$setting] = htmlspecialchars__recursive($settings[$setting]);
745
		}
746
747
		$context['settings'] = $context['theme_settings'];
748
		$context['theme_settings'] = $settings;
749
750
		foreach ($context['settings'] as $i => $setting)
751
		{
752
			// Separators are dummies, so leave them alone.
753
			if (!is_array($setting))
754
				continue;
755
756
			// Create the right input fields for the data
757 View Code Duplication
			if (!isset($setting['type']) || $setting['type'] === 'bool')
758
				$context['settings'][$i]['type'] = 'checkbox';
759
			elseif ($setting['type'] === 'int' || $setting['type'] === 'integer')
760
				$context['settings'][$i]['type'] = 'number';
761
			elseif ($setting['type'] === 'string')
762
				$context['settings'][$i]['type'] = 'text';
763
764
			if (isset($setting['options']))
765
				$context['settings'][$i]['type'] = 'list';
766
767
			$context['settings'][$i]['value'] = !isset($settings[$setting['id']]) ? '' : $settings[$setting['id']];
768
		}
769
770
		// Do we support variants?
771
		if (!empty($settings['theme_variants']))
772
		{
773
			$context['theme_variants'] = array();
774 View Code Duplication
			foreach ($settings['theme_variants'] as $variant)
775
			{
776
				// Have any text, old chap?
777
				$context['theme_variants'][$variant] = array(
778
					'label' => isset($txt['variant_' . $variant]) ? $txt['variant_' . $variant] : $variant,
779
					'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'),
780
				);
781
			}
782
			$context['default_variant'] = !empty($settings['default_variant']) && isset($context['theme_variants'][$settings['default_variant']]) ? $settings['default_variant'] : $settings['theme_variants'][0];
783
		}
784
785
		// Restore the current theme.
786
		loadTheme($old_id, false);
787
788
		$settings = $old_settings;
789
790
		// Reinit just incase.
791
		if (function_exists('template_init'))
792
			$settings += template_init();
793
794
		loadTemplate('ManageThemes');
795
796
		// We like Kenny better than Token.
797
		createToken('admin-sts');
798
	}
799
800
	/**
801
	 * Remove a theme from the database.
802
	 *
803
	 * What it does:
804
	 *
805
	 * - Removes an installed theme.
806
	 * - Requires an administrator.
807
	 * - Accessed with ?action=admin;area=theme;sa=remove.
808
	 */
809
	public function action_remove()
810
	{
811
		global $modSettings, $context;
812
813
		require_once(SUBSDIR . '/Themes.subs.php');
814
815
		checkSession('get');
816
		validateToken('admin-tr', 'request');
817
818
		// The theme's ID must be an integer.
819
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
820
821
		// You can't delete the default theme!
822
		if ($theme == 1)
823
			throw new Elk_Exception('no_access', false);
824
825
		// Its no longer known
826
		$known = explode(',', $modSettings['knownThemes']);
827 View Code Duplication
		for ($i = 0, $n = count($known); $i < $n; $i++)
828
		{
829
			if ($known[$i] == $theme)
830
				unset($known[$i]);
831
		}
832
		$known = strtr(implode(',', $known), array(',,' => ','));
833
834
		// Remove it as an option everywhere
835
		deleteTheme($theme);
836
837
		// Fix it if the theme was the overall default theme.
838 View Code Duplication
		if ($modSettings['theme_guests'] == $theme)
839
			updateSettings(array('theme_guests' => '1', 'knownThemes' => $known));
840
		else
841
			updateSettings(array('knownThemes' => $known));
842
843
		redirectexit('action=admin;area=theme;sa=list;' . $context['session_var'] . '=' . $context['session_id']);
844
	}
845
846
	/**
847
	 * Remove a theme from the database in response to an ajax api request
848
	 *
849
	 * What it does:
850
	 *
851
	 * - Removes an installed theme.
852
	 * - Requires an administrator.
853
	 * - Accessed with ?action=admin;area=theme;sa=remove;api
854
	 */
855
	public function action_remove_api()
856
	{
857
		global $modSettings, $context, $txt;
858
859
		require_once(SUBSDIR . '/Themes.subs.php');
860
861
		// Validate what was sent
862 View Code Duplication
		if (checkSession('get', '', false))
863
		{
864
			loadLanguage('Errors');
865
			$context['xml_data'] = array(
866
				'error' => 1,
867
				'text' => $txt['session_verify_fail'],
868
			);
869
870
			return;
871
		}
872
873
		// Not just any John Smith can send in a api request
874
		if (!allowedTo('admin_forum'))
875
		{
876
			loadLanguage('Errors');
877
			$context['xml_data'] = array(
878
				'error' => 1,
879
				'text' => $txt['cannot_admin_forum'],
880
			);
881
			return;
882
		}
883
884
		// Even if you are John Smith, you still need a ticket
885
		if (!validateToken('admin-tr', 'request', true, false))
886
		{
887
			loadLanguage('Errors');
888
			$context['xml_data'] = array(
889
				'error' => 1,
890
				'text' => $txt['token_verify_fail'],
891
			);
892
893
			return;
894
		}
895
896
		// The theme's ID must be an integer.
897
		$theme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
898
899
		// You can't delete the default theme!
900
		if ($theme == 1)
901
		{
902
			loadLanguage('Errors');
903
			$context['xml_data'] = array(
904
				'error' => 1,
905
				'text' => $txt['no_access'],
906
			);
907
			return;
908
		}
909
910
		// It is a theme we know about?
911
		$known = explode(',', $modSettings['knownThemes']);
912 View Code Duplication
		for ($i = 0, $n = count($known); $i < $n; $i++)
913
		{
914
			if ($known[$i] == $theme)
915
				unset($known[$i]);
916
		}
917
918
		// Finally, remove it
919
		deleteTheme($theme);
920
921
		$known = strtr(implode(',', $known), array(',,' => ','));
922
923
		// Fix it if the theme was the overall default theme.
924 View Code Duplication
		if ($modSettings['theme_guests'] == $theme)
925
			updateSettings(array('theme_guests' => '1', 'knownThemes' => $known));
926
		else
927
			updateSettings(array('knownThemes' => $known));
928
929
		// Let them know it worked, all without a page refresh
930
		createToken('admin-tr', 'request');
931
		$context['xml_data'] = array(
932
			'success' => 1,
933
			'token_var' => $context['admin-tr_token_var'],
934
			'token' => $context['admin-tr_token'],
935
		);
936
	}
937
938
	/**
939
	 * Choose a theme from a list.
940
	 * Allows a user or administrator to pick a new theme with an interface.
941
	 *
942
	 * What it does:
943
	 *
944
	 * - Can edit everyone's (u = 0), guests' (u = -1), or a specific user's.
945
	 * - Uses the Themes template. (pick sub template.)
946
	 * - Accessed with ?action=admin;area=theme;sa=pick.
947
	 *
948
	 * @uses Profile language text
949
	 * @uses ManageThemes template
950
	 * @todo thought so... Might be better to split this file in ManageThemes and Themes,
951
	 * with centralized admin permissions on ManageThemes.
952
	 */
953
	public function action_pick()
954
	{
955
		global $txt, $context, $modSettings, $user_info, $scripturl, $settings;
956
957
		require_once(SUBSDIR . '/Themes.subs.php');
958
959
		if (!$modSettings['theme_allow'] && $settings['disable_user_variant'] && !allowedTo('admin_forum'))
960
			throw new Elk_Exception('no_access', false);
961
962
		loadLanguage('Profile');
963
		loadTemplate('ManageThemes');
964
965
		// Build the link tree.
966
		$context['linktree'][] = array(
967
			'url' => $scripturl . '?action=theme;sa=pick;u=' . (!empty($this->_req->query->u) ? (int) $this->_req->query->u : 0),
968
			'name' => $txt['theme_pick'],
969
		);
970
		$context['default_theme_id'] = $modSettings['theme_default'];
971
972
		$_SESSION['id_theme'] = 0;
973
974
		if (isset($this->_req->query->id))
975
			$this->_req->query->th = $this->_req->query->id;
976
977
		// Saving a variant cause JS doesn't work - pretend it did ;)
978
		if (isset($this->_req->post->save))
979
		{
980
			// Which theme?
981
			foreach ($this->_req->post->save as $k => $v)
982
				$this->_req->query->th = (int) $k;
983
984
			if (isset($this->_req->post->vrt[$k]))
985
				$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 981. 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...
986
		}
987
988
		// Have we made a decision, or are we just browsing?
989
		if (isset($this->_req->query->th))
990
		{
991
			checkSession('get');
992
993
			$th = $this->_req->getQuery('th', 'intval');
994
			$vrt = $this->_req->getQuery('vrt', 'cleanhtml');
995
			$u = $this->_req->getQuery('u', 'intval');
996
997
			// Save for this user.
998
			if (!isset($u) || !allowedTo('admin_forum'))
999
			{
1000
				require_once(SUBSDIR . '/Members.subs.php');
1001
				updateMemberData($user_info['id'], array('id_theme' => $th));
1002
1003
				// A variants to save for the user?
1004 View Code Duplication
				if (!empty($vrt))
1005
				{
1006
					updateThemeOptions(array($th, $user_info['id'], 'theme_variant', $vrt));
1007
1008
					Cache::instance()->remove('theme_settings-' . $th . ':' . $user_info['id']);
1009
1010
					$_SESSION['id_variant'] = 0;
1011
				}
1012
1013
				redirectexit('action=profile;area=theme');
1014
			}
1015
1016
			// If changing members or guests - and there's a variant - assume changing default variant.
1017
			if (!empty($vrt) && ($u === 0 || $u === -1))
1018
			{
1019
				updateThemeOptions(array($th, 0, 'default_variant', $vrt));
1020
1021
				// Make it obvious that it's changed
1022
				Cache::instance()->remove('theme_settings-' . $th);
1023
			}
1024
1025
			// For everyone.
1026
			if ($u === 0)
1027
			{
1028
				require_once(SUBSDIR . '/Members.subs.php');
1029
				updateMemberData(null, array('id_theme' => $th));
1030
1031
				// Remove any custom variants.
1032
				if (!empty($vrt))
1033
					deleteVariants($th);
1034
1035
				redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
1036
			}
1037
			// Change the default/guest theme.
1038
			elseif ($u === -1)
1039
			{
1040
				updateSettings(array('theme_guests' => $th));
1041
1042
				redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
1043
			}
1044
			// Change a specific member's theme.
1045
			else
1046
			{
1047
				// The forum's default theme is always 0 and we
1048
				if (isset($th) && $th == 0)
1049
					$th = $modSettings['theme_guests'];
1050
1051
				require_once(SUBSDIR . '/Members.subs.php');
1052
				updateMemberData($u, array('id_theme' => $th));
1053
1054 View Code Duplication
				if (!empty($vrt))
1055
				{
1056
					updateThemeOptions(array($th, $u, 'theme_variant', $vrt));
1057
					Cache::instance()->remove('theme_settings-' . $th . ':' . $u);
1058
1059
					if ($user_info['id'] == $u)
1060
						$_SESSION['id_variant'] = 0;
1061
				}
1062
1063
				redirectexit('action=profile;u=' . $u . ';area=theme');
1064
			}
1065
		}
1066
1067
		$u = $this->_req->getQuery('u', 'intval');
1068
1069
		// Figure out who the member of the minute is, and what theme they've chosen.
1070
		if (!isset($u) || !allowedTo('admin_forum'))
1071
		{
1072
			$context['current_member'] = $user_info['id'];
1073
			$current_theme = $user_info['theme'];
1074
		}
1075
		// Everyone can't chose just one.
1076
		elseif ($u === 0)
1077
		{
1078
			$context['current_member'] = 0;
1079
			$current_theme = 0;
1080
		}
1081
		// Guests and such...
1082
		elseif ($u === -1)
1083
		{
1084
			$context['current_member'] = -1;
1085
			$current_theme = $modSettings['theme_guests'];
1086
		}
1087
		// Someones else :P.
1088
		else
1089
		{
1090
			$context['current_member'] = $u;
1091
1092
			require_once(SUBSDIR . '/Members.subs.php');
1093
			$member = getBasicMemberData($context['current_member']);
1094
1095
			$current_theme = $member['id_theme'];
1096
		}
1097
1098
		// Get the theme name and descriptions.
1099
		list ($context['available_themes'], $guest_theme) = availableThemes($current_theme, $context['current_member']);
1100
1101
		// As long as we're not doing the default theme...
1102
		if (!isset($u) || $u >= 0)
1103
		{
1104
			if ($guest_theme != 0)
1105
				$context['available_themes'][0] = $context['available_themes'][$guest_theme];
1106
1107
			$context['available_themes'][0]['id'] = 0;
1108
			$context['available_themes'][0]['name'] = $txt['theme_forum_default'];
1109
			$context['available_themes'][0]['selected'] = $current_theme == 0;
1110
			$context['available_themes'][0]['description'] = $txt['theme_global_description'];
1111
		}
1112
1113
		ksort($context['available_themes']);
1114
1115
		$context['page_title'] = $txt['theme_pick'];
1116
		$context['sub_template'] = 'pick';
1117
	}
1118
1119
	/**
1120
	 * Installs new themes, either from a gzip or copy of the default.
1121
	 *
1122
	 * What it does:
1123
	 *
1124
	 * - Puts themes in $boardurl/themes.
1125
	 * - Assumes the gzip has a root directory in it. (ie default.)
1126
	 * - Requires admin_forum.
1127
	 * - Accessed with ?action=admin;area=theme;sa=install.
1128
	 *
1129
	 * @uses ManageThemes template
1130
	 */
1131
	public function action_install()
1132
	{
1133
		global $boardurl, $txt, $context, $settings, $modSettings;
1134
1135
		checkSession('request');
1136
1137
		require_once(SUBSDIR . '/Themes.subs.php');
1138
		require_once(SUBSDIR . '/Package.subs.php');
1139
1140
		loadTemplate('ManageThemes');
1141
1142
		// Passed an ID, then the install is complete, lets redirect and show them
1143
		if (isset($this->_req->query->theme_id))
1144
		{
1145
			$this->_req->query->theme_id = (int) $this->_req->query->theme_id;
1146
1147
			$context['sub_template'] = 'installed';
1148
			$context['page_title'] = $txt['theme_installed'];
1149
			$context['installed_theme'] = array(
1150
				'id' => $this->_req->query->theme_id,
1151
				'name' => getThemeName($this->_req->query->theme_id),
1152
			);
1153
1154
			return null;
1155
		}
1156
1157
		// How are we going to install this theme, from a dir, zip, copy of default?
1158
		if ((!empty($_FILES['theme_gz']) && (!isset($_FILES['theme_gz']['error']) || $_FILES['theme_gz']['error'] != 4)) || !empty($this->_req->query->theme_gz))
1159
			$method = 'upload';
1160
		elseif (isset($this->_req->query->theme_dir) && rtrim(realpath($this->_req->query->theme_dir), '/\\') != realpath(BOARDDIR . '/themes') && file_exists($this->_req->query->theme_dir))
1161
			$method = 'path';
1162
		else
1163
			$method = 'copy';
1164
1165
		// Copy the default theme?
1166
		if (!empty($this->_req->post->copy) && $method === 'copy')
1167
			$this->copyDefault();
1168
		// Install from another directory
1169
		elseif (isset($this->_req->post->theme_dir) && $method === 'path')
1170
			$this->installFromDir();
1171
		// Uploaded a zip file to install from
1172
		elseif ($method === 'upload')
1173
			$this->installFromZip();
1174
		else
1175
			throw new Elk_Exception('theme_install_general', false);
1176
1177
		// Something go wrong?
1178
		if ($this->theme_dir != '' && basename($this->theme_dir) !== 'themes')
1179
		{
1180
			// Defaults.
1181
			$install_info = array(
1182
				'theme_url' => $boardurl . '/themes/' . basename($this->theme_dir),
1183
				'images_url' => isset($this->images_url) ? $this->images_url : $boardurl . '/themes/' . basename($this->theme_dir) . '/images',
1184
				'theme_dir' => $this->theme_dir,
1185
				'name' => $this->theme_name
1186
			);
1187
			$explicit_images = false;
1188
1189
			if (file_exists($this->theme_dir . '/theme_info.xml'))
1190
			{
1191
				$theme_info = file_get_contents($this->theme_dir . '/theme_info.xml');
1192
1193
				// Parse theme-info.xml into an Xml_Array.
1194
				$theme_info_xml = new Xml_Array($theme_info);
1195
1196
				// @todo Error message of some sort?
1197
				if (!$theme_info_xml->exists('theme-info[0]'))
1198
					return 'package_get_error_packageinfo_corrupt';
1199
1200
				$theme_info_xml = $theme_info_xml->path('theme-info[0]');
1201
				$theme_info_xml = $theme_info_xml->to_array();
1202
1203
				$xml_elements = array(
1204
					'name' => 'name',
1205
					'theme_layers' => 'layers',
1206
					'theme_templates' => 'templates',
1207
					'based_on' => 'based-on',
1208
				);
1209
				foreach ($xml_elements as $var => $name)
1210
				{
1211
					if (!empty($theme_info_xml[$name]))
1212
						$install_info[$var] = $theme_info_xml[$name];
1213
				}
1214
1215
				if (!empty($theme_info_xml['images']))
1216
				{
1217
					$install_info['images_url'] = $install_info['theme_url'] . '/' . $theme_info_xml['images'];
1218
					$explicit_images = true;
1219
				}
1220
1221
				if (!empty($theme_info_xml['extra']))
1222
					$install_info += Util::unserialize($theme_info_xml['extra']);
1223
			}
1224
1225
			if (isset($install_info['based_on']))
1226
			{
1227
				if ($install_info['based_on'] === 'default')
1228
				{
1229
					$install_info['theme_url'] = $settings['default_theme_url'];
1230
					$install_info['images_url'] = $settings['default_images_url'];
1231
				}
1232
				elseif ($install_info['based_on'] != '')
1233
				{
1234
					$install_info['based_on'] = preg_replace('~[^A-Za-z0-9\-_ ]~', '', $install_info['based_on']);
1235
1236
					$temp = loadBasedOnTheme($install_info['based_on'], $explicit_images);
1237
1238
					// @todo An error otherwise?
1239
					if (is_array($temp))
1240
					{
1241
						$install_info = $temp + $install_info;
1242
1243
						if (empty($explicit_images) && !empty($install_info['base_theme_url']))
1244
							$install_info['theme_url'] = $install_info['base_theme_url'];
1245
					}
1246
				}
1247
1248
				unset($install_info['based_on']);
1249
			}
1250
1251
			// Find the newest id_theme.
1252
			$id_theme = nextTheme();
1253
1254
			$inserts = array();
1255
			foreach ($install_info as $var => $val)
1256
				$inserts[] = array($id_theme, $var, $val);
1257
1258
			if (!empty($inserts))
1259
				addTheme($inserts);
1260
1261
			updateSettings(array('knownThemes' => strtr($modSettings['knownThemes'] . ',' . $id_theme, array(',,' => ','))));
1262
1263
			redirectexit('action=admin;area=theme;sa=install;theme_id=' . $id_theme . ';' . $context['session_var'] . '=' . $context['session_id']);
1264
		}
1265
1266
		redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']);
1267
	}
1268
1269
	/**
1270
	 * Install a new theme from an uploaded zip archive
1271
	 */
1272
	public function installFromZip()
1273
	{
1274
		// Hopefully the themes directory is writable, or we might have a problem.
1275
		if (!is_writable(BOARDDIR . '/themes'))
1276
			throw new Elk_Exception('theme_install_write_error', 'critical');
1277
1278
		// This happens when the admin session is gone and the user has to login again
1279
		if (empty($_FILES['theme_gz']) && empty($this->_req->post->theme_gz))
1280
			return;
1281
1282
		// Set the default settings...
1283
		$this->theme_name = strtok(basename(isset($_FILES['theme_gz']) ? $_FILES['theme_gz']['name'] : $this->_req->post->theme_gz), '.');
1284
		$this->theme_name = preg_replace(array('/\s/', '/\.[\.]+/', '/[^\w_\.\-]/'), array('_', '.', ''), $this->theme_name);
1285
		$this->theme_dir = BOARDDIR . '/themes/' . $this->theme_name;
1286
1287
		if (isset($_FILES['theme_gz']) && is_uploaded_file($_FILES['theme_gz']['tmp_name']) && (ini_get('open_basedir') != '' || file_exists($_FILES['theme_gz']['tmp_name'])))
1288
			read_tgz_file($_FILES['theme_gz']['tmp_name'], BOARDDIR . '/themes/' . $this->theme_name, false, true);
1289
		elseif (isset($this->_req->post->theme_gz))
1290
		{
1291
			if (!isAuthorizedServer($this->_req->post->theme_gz))
1292
				throw new Elk_Exception('not_valid_server');
1293
1294
			read_tgz_file($this->_req->post->theme_gz, BOARDDIR . '/themes/' . $this->theme_name, false, true);
1295
		}
1296
	}
1297
1298
	/**
1299
	 * Install a theme from a directory on the server
1300
	 *
1301
	 * - Expects the directory is properly loaded with theme files
1302
	 */
1303
	public function installFromDir()
1304
	{
1305
		if (!is_dir($this->_req->post->theme_dir) || !file_exists($this->_req->post->theme_dir . '/theme_info.xml'))
1306
			throw new Elk_Exception('theme_install_error', false);
1307
1308
		$this->theme_name = basename($this->_req->post->theme_dir);
1309
		$this->theme_dir = $this->_req->post->theme_dir;
1310
	}
1311
1312
	/**
1313
	 * Make a copy of the default theme in a new directory
1314
	 */
1315
	public function copyDefault()
1316
	{
1317
		global $boardurl, $modSettings, $settings;
1318
1319
		// Hopefully the themes directory is writable, or we might have a problem.
1320
		if (!is_writable(BOARDDIR . '/themes'))
1321
			throw new Elk_Exception('theme_install_write_error', 'critical');
1322
1323
		// Make the new directory, standard characters only
1324
		$this->theme_dir = BOARDDIR . '/themes/' . preg_replace('~[^A-Za-z0-9_\- ]~', '', $this->_req->post->copy);
1325
		umask(0);
1326
		mkdir($this->theme_dir, 0777);
1327
1328
		// Get some more time if we can
1329
		detectServer()->setTimeLimit(600);
1330
1331
		// Create the subdirectories for css, javascript and font files.
1332
		mkdir($this->theme_dir . '/css', 0777);
1333
		mkdir($this->theme_dir . '/scripts', 0777);
1334
		mkdir($this->theme_dir . '/webfonts', 0777);
1335
1336
		// Copy over the default non-theme files.
1337
		$to_copy = array('/index.php', '/index.template.php', '/scripts/theme.js');
1338
		foreach ($to_copy as $file)
1339
		{
1340
			copy($settings['default_theme_dir'] . $file, $this->theme_dir . $file);
1341
			@chmod($this->theme_dir . $file, 0777);
1342
		}
1343
1344
		// And now the entire css, images and webfonts directories!
1345
		copytree($settings['default_theme_dir'] . '/css', $this->theme_dir . '/css');
1346
		copytree($settings['default_theme_dir'] . '/images', $this->theme_dir . '/images');
1347
		copytree($settings['default_theme_dir'] . '/webfonts', $this->theme_dir . '/webfonts');
1348
		package_flush_cache();
1349
1350
		$this->theme_name = $this->_req->post->copy;
1351
		$this->images_url = $boardurl . '/themes/' . basename($this->theme_dir) . '/images';
1352
		$this->theme_dir = realpath($this->theme_dir);
1353
1354
		// Lets get some data for the new theme (default theme (1), default settings (0)).
1355
		$theme_values = loadThemeOptionsInto(1, 0, array(), array('theme_templates', 'theme_layers'));
1356
1357
		// Lets add a theme_info.xml to this theme.
1358
		write_theme_info($this->_req->post->copy, $modSettings['elkVersion'], $this->theme_dir, $theme_values);
1359
	}
1360
1361
	/**
1362
	 * Set a theme option via javascript.
1363
	 *
1364
	 * What it does:
1365
	 *
1366
	 * - sets a theme option without outputting anything.
1367
	 * - can be used with javascript, via a dummy image... (which doesn't require
1368
	 *   the page to reload.)
1369
	 * - requires someone who is logged in.
1370
	 * - accessed via ?action=jsoption;var=variable;val=value;session_var=sess_id.
1371
	 * - optionally contains &th=theme id
1372
	 * - does not log access to the Who's Online log. (in index.php..)
1373
	 */
1374
	public function action_jsoption()
1375
	{
1376
		global $settings, $user_info, $options;
1377
1378
		// Check the session id.
1379
		checkSession('get');
1380
1381
		// This good-for-nothing pixel is being used to keep the session alive.
1382 View Code Duplication
		if (empty($this->_req->query->var) || !isset($this->_req->query->val))
1383
			redirectexit($settings['images_url'] . '/blank.png');
1384
1385
		// Sorry, guests can't go any further than this..
1386
		if ($user_info['is_guest'] || $user_info['id'] == 0)
1387
			obExit(false);
1388
1389
		$reservedVars = array(
1390
			'actual_theme_url',
1391
			'actual_images_url',
1392
			'base_theme_dir',
1393
			'base_theme_url',
1394
			'default_images_url',
1395
			'default_theme_dir',
1396
			'default_theme_url',
1397
			'default_template',
1398
			'images_url',
1399
			'number_recent_posts',
1400
			'smiley_sets_default',
1401
			'theme_dir',
1402
			'theme_id',
1403
			'theme_layers',
1404
			'theme_templates',
1405
			'theme_url',
1406
			'name',
1407
		);
1408
1409
		// Can't change reserved vars.
1410
		if (in_array(strtolower($this->_req->query->var), $reservedVars))
1411
			redirectexit($settings['images_url'] . '/blank.png');
1412
1413
		// Use a specific theme?
1414
		if (isset($this->_req->query->th) || isset($this->_req->query->id))
1415
		{
1416
			// Invalidate the current themes cache too.
1417
			Cache::instance()->remove('theme_settings-' . $settings['theme_id'] . ':' . $user_info['id']);
1418
1419
			$settings['theme_id'] = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval'));
1420
		}
1421
1422
		// If this is the admin preferences the passed value will just be an element of it.
1423
		if ($this->_req->query->var === 'admin_preferences')
1424
		{
1425 View Code Duplication
			if (!empty($options['admin_preferences']))
1426
			{
1427
				$options['admin_preferences'] = serializeToJson($options['admin_preferences'], function ($array_form) {
1428
					global $context;
1429
1430
					$context['admin_preferences'] = $array_form;
1431
					require_once(SUBSDIR . '/Admin.subs.php');
1432
					updateAdminPreferences();
1433
				});
1434
			}
1435
			else
1436
			{
1437
				$options['admin_preferences'] = array();
1438
			}
1439
1440
			// New thingy...
1441
			if (isset($this->_req->query->admin_key) && strlen($this->_req->query->admin_key) < 5)
1442
				$options['admin_preferences'][$this->_req->query->admin_key] = $this->_req->query->val;
1443
1444
			// Change the value to be something nice,
1445
			$this->_req->query->val = json_encode($options['admin_preferences']);
1446
		}
1447
		// If this is the window min/max settings, the passed window name will just be an element of it.
1448
		elseif ($this->_req->query->var === 'minmax_preferences')
1449
		{
1450 View Code Duplication
			if (!empty($options['minmax_preferences']))
1451
			{
1452
				$minmax_preferences = serializeToJson($options['minmax_preferences'], function ($array_form) {
1453
					global $settings, $user_info;
1454
1455
					// Update the option.
1456
					require_once(SUBSDIR . '/Themes.subs.php');
1457
					updateThemeOptions(array($settings['theme_id'], $user_info['id'], 'minmax_preferences', json_encode($array_form)));
1458
				});
1459
			}
1460
			else
1461
			{
1462
				$minmax_preferences = array();
1463
			}
1464
1465
			// New value for them
1466
			if (isset($this->_req->query->minmax_key) && strlen($this->_req->query->minmax_key) < 10)
1467
				$minmax_preferences[$this->_req->query->minmax_key] = $this->_req->query->val;
1468
1469
			// Change the value to be something nice,
1470
			$this->_req->query->val = json_encode($minmax_preferences);
1471
		}
1472
1473
		// Update the option.
1474
		require_once(SUBSDIR . '/Themes.subs.php');
1475
		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));
1476
1477
		Cache::instance()->remove('theme_settings-' . $settings['theme_id'] . ':' . $user_info['id']);
1478
1479
		// Don't output anything...
1480
		redirectexit($settings['images_url'] . '/blank.png');
1481
	}
1482
1483
	/**
1484
	 * Allows choosing, browsing, and editing a themes files.
1485
	 *
1486
	 * What it does:
1487
	 *
1488
	 * - Its subactions handle several features:
1489
	 *   - edit_template: display and edit a PHP template file
1490
	 *   - edit_style: display and edit a CSS file
1491
	 *   - edit_file: display and edit other files in the theme
1492
	 * - accessed via ?action=admin;area=theme;sa=edit
1493
	 *
1494
	 * @uses the ManageThemes template
1495
	 */
1496
	public function action_edit()
1497
	{
1498
		global $context;
1499
1500
		loadTemplate('ManageThemes');
1501
1502
		// We'll work hard with them themes!
1503
		require_once(SUBSDIR . '/Themes.subs.php');
1504
1505
		$selectedTheme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
1506
1507
		// Unfortunately we cannot edit an unknown theme.. redirect.
1508 View Code Duplication
		if (empty($selectedTheme))
1509
			redirectexit('action=admin;area=theme;sa=themelist');
1510
		// You're browsing around, aren't you
1511
		elseif (!isset($this->_req->query->filename) && !isset($this->_req->post->save))
1512
			redirectexit('action=admin;area=theme;sa=browse;th=' . $selectedTheme);
1513
1514
		// We don't have errors. Yet.
1515
		$context['session_error'] = false;
1516
1517
		// We're editing a theme file.
1518
		// Get the directory of the theme we are editing.
1519
		$context['theme_id'] = $selectedTheme;
1520
		$this->theme_dir = themeDirectory($context['theme_id']);
1521
1522
		$this->prepareThemeEditContext();
1523
1524
		// Saving?
1525
		if (isset($this->_req->post->save))
1526
		{
1527
			$this->_action_edit_submit();
1528
1529
			// Now lets get out of here!
1530
			return;
1531
		}
1532
1533
		// We're editing .css, .template.php, .{language}.php or others.
1534
		// Note: we're here sending $theme_dir as parameter to action_()
1535
		// controller functions, which isn't cool. To be refactored.
1536
		if (substr($this->_req->query->filename, -4) === '.css')
1537
			$this->_action_edit_style();
1538
		elseif (substr($this->_req->query->filename, -13) === '.template.php')
1539
			$this->_action_edit_template();
1540
		else
1541
			$this->_action_edit_file();
1542
1543
		// Create a special token to allow editing of multiple files.
1544
		createToken('admin-te-' . md5($selectedTheme . '-' . $this->_req->query->filename));
1545
	}
1546
1547
	/**
1548
	 * Displays for editing in admin panel a css file.
1549
	 *
1550
	 * This function is forwarded to, from
1551
	 * ?action=admin;area=theme;sa=edit
1552
	 */
1553
	private function _action_edit_style()
1554
	{
1555
		global $context, $settings;
1556
1557
		addJavascriptVar(array(
1558
			'previewData' => '',
1559
			'previewTimeout' => '',
1560
			'refreshPreviewCache' => '',
1561
			'editFilename' => $context['edit_filename'],
1562
			'theme_id' => $settings['theme_id'],
1563
		), true);
1564
1565
		// pick the template and send it the file
1566
		$context['sub_template'] = 'edit_style';
1567
		$context['entire_file'] = htmlspecialchars(strtr(file_get_contents($this->theme_dir . '/' . $this->_req->query->filename), array("\t" => '   ')), ENT_COMPAT, 'UTF-8');
1568
	}
1569
1570
	/**
1571
	 * Displays for editing in the admin panel a template file.
1572
	 *
1573
	 * This function is forwarded to, from
1574
	 * ?action=admin;area=theme;sa=edit
1575
	 */
1576
	private function _action_edit_template()
1577
	{
1578
		global $context;
1579
1580
		// Make sure the sub-template is set
1581
		$context['sub_template'] = 'edit_template';
1582
1583
		// Retrieve the contents of the file
1584
		$file_data = file($this->theme_dir . '/' . $this->_req->query->filename);
1585
1586
		// For a PHP template file, we display each function in separate boxes.
1587
		$j = 0;
1588
		$context['file_parts'] = array(array('lines' => 0, 'line' => 1, 'data' => '', 'function' => ''));
1589
		for ($i = 0, $n = count($file_data); $i < $n; $i++)
1590
		{
1591
			// @todo refactor this so the docblocks are in the function content window
1592
			if (substr($file_data[$i], 0, 9) === 'function ')
1593
			{
1594
				// Try to format the functions a little nicer...
1595
				$context['file_parts'][$j]['data'] = trim($context['file_parts'][$j]['data']);
1596
1597
				if (empty($context['file_parts'][$j]['lines']))
1598
					unset($context['file_parts'][$j]);
1599
1600
				// Start a new function block
1601
				$context['file_parts'][++$j] = array('lines' => 0, 'line' => $i, 'data' => '');
1602
			}
1603
1604
			$context['file_parts'][$j]['lines']++;
1605
			$context['file_parts'][$j]['data'] .= htmlspecialchars(strtr($file_data[$i], array("\t" => '   ')), ENT_COMPAT, 'UTF-8');
1606
		}
1607
1608
		$context['entire_file'] = htmlspecialchars(strtr(implode('', $file_data), array("\t" => '   ')), ENT_COMPAT, 'UTF-8');
1609
	}
1610
1611
	/**
1612
	 * Handles editing in admin of other types of files from a theme,
1613
	 * except templates and css.
1614
	 *
1615
	 * This function is forwarded to, from
1616
	 * ?action=admin;area=theme;sa=edit
1617
	 */
1618
	private function _action_edit_file()
1619
	{
1620
		global $context;
1621
1622
		// Simply set the template and the file contents.
1623
		$context['sub_template'] = 'edit_file';
1624
		$context['entire_file'] = htmlspecialchars(strtr(file_get_contents($this->theme_dir . '/' . $this->_req->query->filename), array("\t" => '   ')), ENT_COMPAT, 'UTF-8');
1625
	}
1626
1627
	/**
1628
	 * This function handles submission of a template file.
1629
	 * It checks the file for syntax errors, and if it passes, it saves it.
1630
	 *
1631
	 * This function is forwarded to, from
1632
	 * ?action=admin;area=theme;sa=edit
1633
	 */
1634
	private function _action_edit_submit()
1635
	{
1636
		global $context, $settings, $user_info;
1637
1638
		$selectedTheme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
1639
		if (empty($selectedTheme))
1640
		{
1641
			// This should never be happening. Never I say. But... in case it does :P
1642
			throw new Elk_Exception('theme_edit_missing');
1643
		}
1644
1645
		$theme_dir = themeDirectory($context['theme_id']);
1646
		$file = isset($this->_req->post->entire_file) ? $this->_req->post->entire_file : '';
1647
1648
		// You did submit *something*, didn't you?
1649
		if (empty($file))
1650
		{
1651
			// @todo a better error message
1652
			throw new Elk_Exception('theme_edit_missing');
1653
		}
1654
1655
		// Checking PHP syntax on css files is not a most constructive use of processing power :P
1656
		// We need to know what kind of file we have
1657
		$is_php = substr($this->_req->post->filename, -4) === '.php';
1658
		$is_template = substr($this->_req->post->filename, -13) === '.template.php';
1659
		$is_css = substr($this->_req->post->filename, -4) === '.css';
1660
1661
		// Check you up
1662
		if (checkSession('post', '', false) === '' && validateToken('admin-te-' . md5($selectedTheme . '-' . $this->_req->post->filename), 'post', false) === true)
1663
		{
1664
			// Consolidate the format in which we received the file contents
1665
			if (is_array($file))
1666
				$entire_file = implode("\n", $file);
1667
			else
1668
				$entire_file = $file;
1669
1670
			// Convert our tabs back to tabs!
1671
			$entire_file = rtrim(strtr($entire_file, array("\r" => '', '   ' => "\t")));
1672
1673
			// Errors? No errors!
1674
			$errors = array();
1675
1676
			// For PHP files, we check the syntax.
1677
			if ($is_php)
1678
			{
1679
				require_once(SUBSDIR . '/Modlog.subs.php');
1680
1681
				// Since we are running php code, let's track it, but only once in a while.
1682
				if (!recentlyLogged('editing_theme', 60))
1683
				{
1684
					logAction('editing_theme', array('member' => $user_info['id']), 'admin');
1685
1686
					// But the email only once every 60 minutes should be fine
1687
					if (!recentlyLogged('editing_theme', 3600))
1688
					{
1689
						require_once(SUBSDIR . '/Themes.subs.php');
1690
						require_once(SUBSDIR . '/Admin.subs.php');
1691
1692
						$theme_info = getBasicThemeInfos($context['theme_id']);
1693
						emailAdmins('editing_theme', array(
1694
							'EDIT_REALNAME' => $user_info['name'],
1695
							'FILE_EDITED' => $this->_req->post->filename,
1696
							'THEME_NAME' => $theme_info[$context['theme_id']],
1697
						));
1698
					}
1699
				}
1700
1701
				$validator = new Data_Validator();
1702
				$validator->validation_rules(array(
1703
					'entire_file' => 'php_syntax'
1704
				));
1705
				$validator->validate(array('entire_file' => $entire_file));
1706
1707
				// Retrieve the errors
1708
				$errors = $validator->validation_errors();
1709
			}
1710
1711
			// If successful so far, we'll take the plunge and save this piece of art.
1712
			if (empty($errors))
1713
			{
1714
				// Try to save the new file contents
1715
				$fp = fopen($theme_dir . '/' . $this->_req->post->filename, 'w');
1716
				fwrite($fp, $entire_file);
1717
				fclose($fp);
1718
1719
				if ($this->_checkOpcache())
1720
					opcache_invalidate($theme_dir . '/' . $_REQUEST['filename']);
1721
1722
				// We're done here.
1723
				redirectexit('action=admin;area=theme;th=' . $selectedTheme . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=browse;directory=' . dirname($this->_req->post->filename));
1724
			}
1725
			// I can't let you off the hook yet: syntax errors are a nasty beast.
1726
			else
1727
			{
1728
				// Pick the right sub-template for the next try
1729
				if ($is_template)
1730
					$context['sub_template'] = 'edit_template';
1731
				else
1732
					$context['sub_template'] = 'edit_file';
1733
1734
				// Fill contextual data for the template, the errors to show
1735
				foreach ($errors as $error)
1736
					$context['parse_error'][] = $error;
1737
1738
				// The format of the data depends on template/non-template file.
1739
				if (!is_array($file))
1740
					$file = array($file);
1741
1742
				// Send back the file contents
1743
				$context['entire_file'] = htmlspecialchars(strtr(implode('', $file), array("\t" => '   ')), ENT_COMPAT, 'UTF-8');
1744
1745
				foreach ($file as $i => $file_part)
1746
				{
1747
					$context['file_parts'][$i]['lines'] = strlen($file_part);
1748
					$context['file_parts'][$i]['data'] = $file_part;
1749
				}
1750
1751
				// Re-create token for another try
1752
				createToken('admin-te-' . md5($selectedTheme . '-' . $this->_req->post->filename));
1753
1754
				return;
1755
			}
1756
		}
1757
		// Session timed out.
1758
		else
1759
		{
1760
			loadLanguage('Errors');
1761
1762
			// Notify the template of trouble
1763
			$context['session_error'] = true;
1764
1765
			// Recycle the submitted data.
1766
			if (is_array($file))
1767
				$context['entire_file'] = htmlspecialchars(implode("\n", $file), ENT_COMPAT, 'UTF-8');
1768
			else
1769
				$context['entire_file'] = htmlspecialchars($file, ENT_COMPAT, 'UTF-8');
1770
1771
			$context['edit_filename'] = htmlspecialchars($this->_req->post->filename, ENT_COMPAT, 'UTF-8');
1772
1773
			// Choose sub-template
1774
			if ($is_template)
1775
				$context['sub_template'] = 'edit_template';
1776
			elseif ($is_css)
1777
			{
1778
				addJavascriptVar(array(
1779
					'previewData' => '\'\'',
1780
					'previewTimeout' => '\'\'',
1781
					'refreshPreviewCache' => '\'\'',
1782
					'editFilename' => JavaScriptEscape($context['edit_filename']),
1783
					'theme_id' => $settings['theme_id'],
1784
				));
1785
				$context['sub_template'] = 'edit_style';
1786
			}
1787
			else
1788
				$context['sub_template'] = 'edit_file';
1789
1790
			// Re-create the token so that it can be used
1791
			createToken('admin-te-' . md5($selectedTheme . '-' . $this->_req->post->filename));
1792
1793
			return;
1794
		}
1795
	}
1796
1797
	/**
1798
	 * Checks if  Zend Opcache is installed, active and its cmd functions available.
1799
	 *
1800
	 * @return bool
1801
	 */
1802
	private function _checkOpcache()
1803
	{
1804
		return (extension_loaded('Zend OPcache') && ini_get('opcache.enable') && stripos(BOARDDIR, ini_get('opcache.restrict_api')) !== 0);
1805
	}
1806
1807
	/**
1808
	 * Handles user browsing in theme directories.
1809
	 *
1810
	 * What it does:
1811
	 *
1812
	 * - The display will allow to choose a file for editing,
1813
	 * if it is writable.
1814
	 * - accessed with ?action=admin;area=theme;sa=browse
1815
	 */
1816
	public function action_browse()
1817
	{
1818
		global $context, $scripturl;
1819
1820
		loadTemplate('ManageThemes');
1821
1822
		// We'll work hard with them themes!
1823
		require_once(SUBSDIR . '/Themes.subs.php');
1824
1825
		$selectedTheme = $this->_req->getQuery('th', 'intval', $this->_req->getQuery('id', 'intval', 0));
1826
		if (empty($selectedTheme))
1827
			redirectexit('action=admin;area=theme;sa=themelist');
1828
1829
		// Get first the directory of the theme we are editing.
1830
		$context['theme_id'] = isset($this->_req->query->th) ? (int) $this->_req->query->th : (isset($this->_req->query->id) ? (int) $this->_req->query->id : 0);
1831
		$theme_dir = themeDirectory($context['theme_id']);
1832
1833
		// Eh? not trying to sneak a peek outside the theme directory are we
1834
		if (!file_exists($theme_dir . '/index.template.php') && !file_exists($theme_dir . '/css/index.css'))
1835
			throw new Elk_Exception('theme_edit_missing', false);
1836
1837
		// Now, where exactly are you?
1838
		if (isset($this->_req->query->directory))
1839
		{
1840
			if (substr($this->_req->query->directory, 0, 1) === '.')
1841
				$this->_req->query->directory = '';
1842
			else
1843
			{
1844
				$this->_req->query->directory = preg_replace(array('~^[\./\\:\0\n\r]+~', '~[\\\\]~', '~/[\./]+~'), array('', '/', '/'), $this->_req->query->directory);
1845
1846
				$temp = realpath($theme_dir . '/' . $this->_req->query->directory);
1847 View Code Duplication
				if (empty($temp) || substr($temp, 0, strlen(realpath($theme_dir))) != realpath($theme_dir))
1848
					$this->_req->query->directory = '';
1849
			}
1850
		}
1851
1852
		if (isset($this->_req->query->directory) && $this->_req->query->directory != '')
1853
		{
1854
			$context['theme_files'] = get_file_listing($theme_dir . '/' . $this->_req->query->directory, $this->_req->query->directory . '/');
1855
1856
			$temp = dirname($this->_req->query->directory);
1857
			array_unshift($context['theme_files'], array(
1858
				'filename' => $temp === '.' || $temp == '' ? '/ (..)' : $temp . ' (..)',
1859
				'is_writable' => is_writable($theme_dir . '/' . $temp),
1860
				'is_directory' => true,
1861
				'is_template' => false,
1862
				'is_image' => false,
1863
				'is_editable' => false,
1864
				'href' => $scripturl . '?action=admin;area=theme;th=' . $context['theme_id'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=browse;directory=' . $temp,
1865
				'size' => '',
1866
			));
1867
		}
1868
		else
1869
			$context['theme_files'] = get_file_listing($theme_dir, '');
1870
1871
		// finally, load the sub-template
1872
		$context['sub_template'] = 'browse';
1873
	}
1874
1875
	/**
1876
	 * List installed themes.
1877
	 * The listing will allow editing if the files are writable.
1878
	 */
1879
	public function action_themelist()
1880
	{
1881
		global $context;
1882
1883
		loadTemplate('ManageThemes');
1884
1885
		// We'll work hard with them themes!
1886
		require_once(SUBSDIR . '/Themes.subs.php');
1887
1888
		$context['themes'] = installedThemes();
1889
1890
		foreach ($context['themes'] as $key => $theme)
1891
		{
1892
			// There has to be a Settings template!
1893
			if (!file_exists($theme['theme_dir'] . '/index.template.php') && !file_exists($theme['theme_dir'] . '/css/index.css'))
1894
				unset($context['themes'][$key]);
1895
			else
1896
			{
1897 View Code Duplication
				if (!isset($theme['theme_templates']))
1898
					$templates = array('index');
1899
				else
1900
					$templates = explode(',', $theme['theme_templates']);
1901
1902
				foreach ($templates as $template)
1903
					if (file_exists($theme['theme_dir'] . '/' . $template . '.template.php'))
1904
					{
1905
						// Fetch the header... a good 256 bytes should be more than enough.
1906
						$fp = fopen($theme['theme_dir'] . '/' . $template . '.template.php', 'rb');
1907
						$header = fread($fp, 256);
1908
						fclose($fp);
1909
1910
						// Can we find a version comment, at all?
1911
						if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1)
1912
						{
1913
							$ver = $match[1];
1914
							if (!isset($context['themes'][$key]['version']) || $context['themes'][$key]['version'] > $ver)
1915
								$context['themes'][$key]['version'] = $ver;
1916
						}
1917
					}
1918
1919
				$context['themes'][$key]['can_edit_style'] = file_exists($theme['theme_dir'] . '/css/index.css');
1920
			}
1921
		}
1922
1923
		$context['sub_template'] = 'themelist';
1924
	}
1925
1926
	/**
1927
	 * Makes a copy of a template file in a new location
1928
	 *
1929
	 * @uses ManageThemes template, copy_template sub-template.
1930
	 */
1931
	public function action_copy()
1932
	{
1933
		global $context, $settings;
1934
1935
		loadTemplate('ManageThemes');
1936
		require_once(SUBSDIR . '/Themes.subs.php');
1937
1938
		$context[$context['admin_menu_name']]['current_subsection'] = 'edit';
1939
1940
		$context['theme_id'] = isset($this->_req->query->th) ? (int) $this->_req->query->th : (int) $this->_req->query->id;
1941
1942
		$theme_dirs = array();
1943
		$theme_dirs = loadThemeOptionsInto($context['theme_id'], null, $theme_dirs, array('base_theme_dir', 'theme_dir'));
1944
1945
		if (isset($this->_req->query->template) && preg_match('~[\./\\\\:\0]~', $this->_req->query->template) == 0)
1946
		{
1947 View Code Duplication
			if (!empty($theme_dirs['base_theme_dir']) && file_exists($theme_dirs['base_theme_dir'] . '/' . $this->_req->query->template . '.template.php'))
1948
				$filename = $theme_dirs['base_theme_dir'] . '/' . $this->_req->query->template . '.template.php';
1949
			elseif (file_exists($settings['default_theme_dir'] . '/' . $this->_req->query->template . '.template.php'))
1950
				$filename = $settings['default_theme_dir'] . '/' . $this->_req->query->template . '.template.php';
1951
			else
1952
				throw new Elk_Exception('no_access', false);
1953
1954
			$fp = fopen($theme_dirs['theme_dir'] . '/' . $this->_req->query->template . '.template.php', 'w');
1955
			fwrite($fp, file_get_contents($filename));
1956
			fclose($fp);
1957
1958
			if ($this->_checkOpcache())
1959
				opcache_invalidate($filename);
1960
1961
			redirectexit('action=admin;area=theme;th=' . $context['theme_id'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=copy');
1962
		}
1963
		elseif (isset($this->_req->query->lang_file) && preg_match('~^[^\./\\\\:\0]\.[^\./\\\\:\0]$~', $this->_req->query->lang_file) != 0)
1964
		{
1965 View Code Duplication
			if (!empty($theme_dirs['base_theme_dir']) && file_exists($theme_dirs['base_theme_dir'] . '/languages/' . $this->_req->query->lang_file . '.php'))
1966
				$filename = $theme_dirs['base_theme_dir'] . '/languages/' . $this->_req->query->template . '.php';
1967
			elseif (file_exists($settings['default_theme_dir'] . '/languages/' . $this->_req->query->template . '.php'))
1968
				$filename = $settings['default_theme_dir'] . '/languages/' . $this->_req->query->template . '.php';
1969
			else
1970
				throw new Elk_Exception('no_access', false);
1971
1972
			$fp = fopen($theme_dirs['theme_dir'] . '/languages/' . $this->_req->query->lang_file . '.php', 'w');
1973
			fwrite($fp, file_get_contents($filename));
1974
			fclose($fp);
1975
1976
			if ($this->_checkOpcache())
1977
				opcache_invalidate($filename);
1978
1979
			redirectexit('action=admin;area=theme;th=' . $context['theme_id'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=copy');
1980
		}
1981
1982
		$templates = array();
1983
		$lang_files = array();
1984
1985
		$dir = dir($settings['default_theme_dir']);
1986
		while ($entry = $dir->read())
1987
		{
1988
			if (substr($entry, -13) === '.template.php')
1989
				$templates[] = substr($entry, 0, -13);
1990
		}
1991
		$dir->close();
1992
1993
		$dir = dir($settings['default_theme_dir'] . '/languages');
1994
		while ($entry = $dir->read())
1995
		{
1996
			if (preg_match('~^([^\.]+\.[^\.]+)\.php$~', $entry, $matches))
1997
				$lang_files[] = $matches[1];
1998
		}
1999
		$dir->close();
2000
2001
		if (!empty($theme_dirs['base_theme_dir']))
2002
		{
2003
			$dir = dir($theme_dirs['base_theme_dir']);
2004
			while ($entry = $dir->read())
2005
			{
2006
				if (substr($entry, -13) === '.template.php' && !in_array(substr($entry, 0, -13), $templates))
2007
					$templates[] = substr($entry, 0, -13);
2008
			}
2009
			$dir->close();
2010
2011
			if (file_exists($theme_dirs['base_theme_dir'] . '/languages'))
2012
			{
2013
				$dir = dir($theme_dirs['base_theme_dir'] . '/languages');
2014
				while ($entry = $dir->read())
2015
				{
2016
					if (preg_match('~^([^\.]+\.[^\.]+)\.php$~', $entry, $matches) && !in_array($matches[1], $lang_files))
2017
						$lang_files[] = $matches[1];
2018
				}
2019
				$dir->close();
2020
			}
2021
		}
2022
2023
		natcasesort($templates);
2024
		natcasesort($lang_files);
2025
2026
		$context['available_templates'] = array();
2027
		foreach ($templates as $template)
2028
			$context['available_templates'][$template] = array(
2029
				'filename' => $template . '.template.php',
2030
				'value' => $template,
2031
				'already_exists' => false,
2032
				'can_copy' => is_writable($theme_dirs['theme_dir']),
2033
			);
2034
		$context['available_language_files'] = array();
2035
		foreach ($lang_files as $file)
2036
			$context['available_language_files'][$file] = array(
2037
				'filename' => $file . '.php',
2038
				'value' => $file,
2039
				'already_exists' => false,
2040
				'can_copy' => file_exists($theme_dirs['theme_dir'] . '/languages') ? is_writable($theme_dirs['theme_dir'] . '/languages') : is_writable($theme_dirs['theme_dir']),
2041
			);
2042
2043
		$dir = dir($theme_dirs['theme_dir']);
2044
		while ($entry = $dir->read())
2045
		{
2046
			if (substr($entry, -13) === '.template.php' && isset($context['available_templates'][substr($entry, 0, -13)]))
2047
			{
2048
				$context['available_templates'][substr($entry, 0, -13)]['already_exists'] = true;
2049
				$context['available_templates'][substr($entry, 0, -13)]['can_copy'] = is_writable($theme_dirs['theme_dir'] . '/' . $entry);
2050
			}
2051
		}
2052
		$dir->close();
2053
2054
		if (file_exists($theme_dirs['theme_dir'] . '/languages'))
2055
		{
2056
			$dir = dir($theme_dirs['theme_dir'] . '/languages');
2057
			while ($entry = $dir->read())
2058
			{
2059
				if (preg_match('~^([^\.]+\.[^\.]+)\.php$~', $entry, $matches) && isset($context['available_language_files'][$matches[1]]))
2060
				{
2061
					$context['available_language_files'][$matches[1]]['already_exists'] = true;
2062
					$context['available_language_files'][$matches[1]]['can_copy'] = is_writable($theme_dirs['theme_dir'] . '/languages/' . $entry);
2063
				}
2064
			}
2065
			$dir->close();
2066
		}
2067
2068
		$context['sub_template'] = 'copy_template';
2069
	}
2070
2071
	/**
2072
	 * This function makes necessary pre-checks and fills
2073
	 * the contextual data as needed by theme editing functions.
2074
	 */
2075
	private function prepareThemeEditContext()
2076
	{
2077
		global $context;
2078
2079
		// Eh? not trying to sneak a peek outside the theme directory are we
2080
		if (!file_exists($this->theme_dir . '/index.template.php') && !file_exists($this->theme_dir . '/css/index.css'))
2081
			throw new Elk_Exception('theme_edit_missing', false);
2082
2083
		// Get the filename from the appropriate spot
2084
		$filename = isset($this->_req->post->save) ? $this->_req->getPost('filename', 'strval', '') : $this->_req->getQuery('filename', 'strval', '');
2085
2086
		// You're editing a file: we have extra-checks coming up first.
2087
		if (substr($filename, 0, 1) === '.')
2088
			$filename = '';
2089
		else
2090
		{
2091
			$filename = preg_replace(array('~^[\./\\:\0\n\r]+~', '~[\\\\]~', '~/[\./]+~'), array('', '/', '/'), $filename);
2092
2093
			$temp = realpath($this->theme_dir . '/' . $filename);
2094 View Code Duplication
			if (empty($temp) || substr($temp, 0, strlen(realpath($this->theme_dir))) !== realpath($this->theme_dir))
2095
				$filename = '';
2096
		}
2097
2098
		// We shouldn't end up with no file
2099
		if (empty($filename))
2100
			throw new Elk_Exception('theme_edit_missing', false);
2101
2102
		// Initialize context
2103
		$context['allow_save'] = is_writable($this->theme_dir . '/' . $filename);
2104
		$context['allow_save_filename'] = strtr($this->theme_dir . '/' . $filename, array(BOARDDIR => '...'));
2105
		$context['edit_filename'] = htmlspecialchars($filename, ENT_COMPAT, 'UTF-8');
2106
	}
2107
}
2108