Issues (1061)

Sources/Subs-Themes.php (1 issue)

1
<?php
2
3
/**
4
 * Helper file for handling themes.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 2020 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 RC2
14
 */
15
16
if (!defined('SMF'))
17
	die('No direct access...');
18
19
/**
20
 * Gets a single theme's info.
21
 *
22
 * @param int $id The theme ID to get the info from.
23
 * @return array The theme info as an array.
24
 */
25
function get_single_theme($id)
26
{
27
	global $smcFunc, $modSettings;
28
29
	// No data, no fun!
30
	if (empty($id))
31
		return false;
32
33
	// Make sure $id is an int.
34
	$id = (int) $id;
35
36
	// List of all possible  values.
37
	$themeValues = array(
38
		'theme_dir',
39
		'images_url',
40
		'theme_url',
41
		'name',
42
		'theme_layers',
43
		'theme_templates',
44
		'version',
45
		'install_for',
46
		'based_on',
47
	);
48
49
	// Make changes if you really want it.
50
	call_integration_hook('integrate_get_single_theme', array(&$themeValues, $id));
51
52
	$single = array(
53
		'id' => $id,
54
	);
55
56
	// Make our known/enable themes a little easier to work with.
57
	$knownThemes = !empty($modSettings['knownThemes']) ? explode(',', $modSettings['knownThemes']) : array();
58
	$enableThemes = !empty($modSettings['enableThemes']) ? explode(',', $modSettings['enableThemes']) : array();
59
60
	$request = $smcFunc['db_query']('', '
61
		SELECT id_theme, variable, value
62
		FROM {db_prefix}themes
63
		WHERE variable IN ({array_string:theme_values})
64
			AND id_theme = ({int:id_theme})
65
			AND id_member = {int:no_member}',
66
		array(
67
			'theme_values' => $themeValues,
68
			'id_theme' => $id,
69
			'no_member' => 0,
70
		)
71
	);
72
73
	while ($row = $smcFunc['db_fetch_assoc']($request))
74
	{
75
		$single[$row['variable']] = $row['value'];
76
77
		// Fix the path and tell if its a valid one.
78
		if ($row['variable'] == 'theme_dir')
79
		{
80
			$single['theme_dir'] = realpath($row['value']);
81
			$single['valid_path'] = file_exists($row['value']) && is_dir($row['value']);
82
		}
83
	}
84
85
	// Is this theme installed and enabled?
86
	$single['known'] = in_array($single['id'], $knownThemes);
87
	$single['enable'] = in_array($single['id'], $enableThemes);
88
89
	// It should at least return if the theme is a known one or if its enable.
90
	return $single;
91
}
92
93
/**
94
 * Loads and returns all installed themes.
95
 *
96
 * Stores all themes on $context['themes'] for easier use.
97
 *
98
 * $modSettings['knownThemes'] stores themes that the user is able to select.
99
 *
100
 * @param bool $enable_only Whether to fetch only enabled themes. Default is false.
101
 */
102
function get_all_themes($enable_only = false)
103
{
104
	global $modSettings, $context, $smcFunc;
105
106
	// Make our known/enable themes a little easier to work with.
107
	$knownThemes = !empty($modSettings['knownThemes']) ? explode(',', $modSettings['knownThemes']) : array();
108
	$enableThemes = !empty($modSettings['enableThemes']) ? explode(',', $modSettings['enableThemes']) : array();
109
110
	// List of all possible themes values.
111
	$themeValues = array(
112
		'theme_dir',
113
		'images_url',
114
		'theme_url',
115
		'name',
116
		'theme_layers',
117
		'theme_templates',
118
		'version',
119
		'install_for',
120
		'based_on',
121
	);
122
123
	// Make changes if you really want it.
124
	call_integration_hook('integrate_get_all_themes', array(&$themeValues, $enable_only));
125
126
	// So, what is it going to be?
127
	$query_where = $enable_only ? $enableThemes : $knownThemes;
128
129
	// Perform the query as requested.
130
	$request = $smcFunc['db_query']('', '
131
		SELECT id_theme, variable, value
132
		FROM {db_prefix}themes
133
		WHERE variable IN ({array_string:theme_values})
134
			AND id_theme IN ({array_string:query_where})
135
			AND id_member = {int:no_member}',
136
		array(
137
			'query_where' => $query_where,
138
			'theme_values' => $themeValues,
139
			'no_member' => 0,
140
		)
141
	);
142
143
	$context['themes'] = array();
144
145
	while ($row = $smcFunc['db_fetch_assoc']($request))
146
	{
147
		$context['themes'][$row['id_theme']]['id'] = (int) $row['id_theme'];
148
149
		// Fix the path and tell if its a valid one.
150
		if ($row['variable'] == 'theme_dir')
151
		{
152
			$context['themes'][$row['id_theme']][$row['variable']] = realpath($row['value']);
153
			$context['themes'][$row['id_theme']]['valid_path'] = file_exists(realpath($row['value'])) && is_dir(realpath($row['value']));
154
		}
155
156
		$context['themes'][$row['id_theme']]['known'] = in_array($row['id_theme'], $knownThemes);
157
		$context['themes'][$row['id_theme']]['enable'] = in_array($row['id_theme'], $enableThemes);
158
		$context['themes'][$row['id_theme']][$row['variable']] = $row['value'];
159
	}
160
161
	$smcFunc['db_free_result']($request);
162
}
163
164
/**
165
 * Loads and returns all installed themes.
166
 *
167
 * Stores all themes on $context['themes'] for easier use.
168
 *
169
 * $modSettings['knownThemes'] stores themes that the user is able to select.
170
 */
171
function get_installed_themes()
172
{
173
	global $modSettings, $context, $smcFunc;
174
175
	// Make our known/enable themes a little easier to work with.
176
	$knownThemes = !empty($modSettings['knownThemes']) ? explode(',', $modSettings['knownThemes']) : array();
177
	$enableThemes = !empty($modSettings['enableThemes']) ? explode(',', $modSettings['enableThemes']) : array();
178
179
	// List of all possible themes values.
180
	$themeValues = array(
181
		'theme_dir',
182
		'images_url',
183
		'theme_url',
184
		'name',
185
		'theme_layers',
186
		'theme_templates',
187
		'version',
188
		'install_for',
189
		'based_on',
190
	);
191
192
	// Make changes if you really want it.
193
	call_integration_hook('integrate_get_installed_themes', array(&$themeValues));
194
195
	// Perform the query as requested.
196
	$request = $smcFunc['db_query']('', '
197
		SELECT id_theme, variable, value
198
		FROM {db_prefix}themes
199
		WHERE variable IN ({array_string:theme_values})
200
			AND id_member = {int:no_member}',
201
		array(
202
			'theme_values' => $themeValues,
203
			'no_member' => 0,
204
		)
205
	);
206
207
	$context['themes'] = array();
208
209
	while ($row = $smcFunc['db_fetch_assoc']($request))
210
	{
211
		$context['themes'][$row['id_theme']]['id'] = (int) $row['id_theme'];
212
213
		// Fix the path and tell if its a valid one.
214
		if ($row['variable'] == 'theme_dir')
215
		{
216
			$context['themes'][$row['id_theme']][$row['variable']] = realpath($row['value']);
217
			$context['themes'][$row['id_theme']]['valid_path'] = file_exists(realpath($row['value'])) && is_dir(realpath($row['value']));
218
		}
219
220
		$context['themes'][$row['id_theme']]['known'] = in_array($row['id_theme'], $knownThemes);
221
		$context['themes'][$row['id_theme']]['enable'] = in_array($row['id_theme'], $enableThemes);
222
		$context['themes'][$row['id_theme']][$row['variable']] = $row['value'];
223
	}
224
225
	$smcFunc['db_free_result']($request);
226
}
227
228
/**
229
 * Reads an .xml file and returns the data as an array
230
 *
231
 * Removes the entire theme if the .xml file couldn't be found or read.
232
 *
233
 * @param string $path The absolute path to the xml file.
234
 * @return array An array with all the info extracted from the xml file.
235
 */
236
function get_theme_info($path)
237
{
238
	global $smcFunc, $sourcedir, $txt, $scripturl, $context;
239
	global $explicit_images;
240
241
	if (empty($path))
242
		return false;
243
244
	$xml_data = array();
245
	$explicit_images = false;
246
247
	// Perhaps they are trying to install a mod, lets tell them nicely this is the wrong function.
248
	if (file_exists($path . '/package-info.xml'))
249
	{
250
		loadLanguage('Errors');
251
252
		// We need to delete the dir otherwise the next time you try to install a theme you will get the same error.
253
		remove_dir($path);
254
255
		$txt['package_get_error_is_mod'] = str_replace('{MANAGEMODURL}', $scripturl . '?action=admin;area=packages;' . $context['session_var'] . '=' . $context['session_id'], $txt['package_get_error_is_mod']);
256
		fatal_lang_error('package_theme_upload_error_broken', false, $txt['package_get_error_is_mod']);
257
	}
258
259
	// Parse theme-info.xml into an xmlArray.
260
	require_once($sourcedir . '/Class-Package.php');
261
	$theme_info_xml = new xmlArray(file_get_contents($path . '/theme_info.xml'));
262
263
	// Error message, there isn't any valid info.
264
	if (!$theme_info_xml->exists('theme-info[0]'))
265
	{
266
		remove_dir($path);
267
		fatal_lang_error('package_get_error_packageinfo_corrupt', false);
268
	}
269
270
	// Check for compatibility with 2.1 or greater.
271
	if (!$theme_info_xml->exists('theme-info/install'))
272
	{
273
		remove_dir($path);
274
		fatal_lang_error('package_get_error_theme_not_compatible', false, SMF_FULL_VERSION);
275
	}
276
277
	// So, we have an install tag which is cool and stuff but we also need to check it and match your current SMF version...
278
	$the_version = SMF_VERSION;
279
	$install_versions = $theme_info_xml->path('theme-info/install/@for');
280
281
	// The theme isn't compatible with the current SMF version.
282
	if (!$install_versions || !matchPackageVersion($the_version, $install_versions))
283
	{
284
		remove_dir($path);
285
		fatal_lang_error('package_get_error_theme_not_compatible', false, SMF_FULL_VERSION);
286
	}
287
288
	$theme_info_xml = $theme_info_xml->path('theme-info[0]');
289
	$theme_info_xml = $theme_info_xml->to_array();
290
291
	$xml_elements = array(
292
		'theme_layers' => 'layers',
293
		'theme_templates' => 'templates',
294
		'based_on' => 'based-on',
295
		'version' => 'version',
296
	);
297
298
	// Assign the values to be stored.
299
	foreach ($xml_elements as $var => $name)
300
		if (!empty($theme_info_xml[$name]))
301
			$xml_data[$var] = $theme_info_xml[$name];
302
303
	// Add the supported versions.
304
	$xml_data['install_for'] = $install_versions;
305
306
	// Overwrite the default images folder.
307
	if (!empty($theme_info_xml['images']))
308
	{
309
		$xml_data['images_url'] = $path . '/' . $theme_info_xml['images'];
310
		$explicit_images = true;
311
	}
312
313
	if (!empty($theme_info_xml['extra']))
314
		$xml_data += $smcFunc['json_decode']($theme_info_xml['extra'], true);
315
316
	return $xml_data;
317
}
318
319
/**
320
 * Inserts a theme's data to the DataBase.
321
 *
322
 * Ends execution with fatal_lang_error() if an error appears.
323
 *
324
 * @param array $to_install An array containing all values to be stored into the DB.
325
 * @return int The newly created theme ID.
326
 */
327
function theme_install($to_install = array())
328
{
329
	global $smcFunc, $context, $modSettings;
330
	global $settings, $explicit_images;
331
332
	// External use? no problem!
333
	if ($to_install)
0 ignored issues
show
Bug Best Practice introduced by
The expression $to_install of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
334
		$context['to_install'] = $to_install;
335
336
	// One last check.
337
	if (empty($context['to_install']['theme_dir']) || basename($context['to_install']['theme_dir']) == 'Themes')
338
		fatal_lang_error('theme_install_invalid_dir', false);
339
340
	// OK, is this a newer version of an already installed theme?
341
	if (!empty($context['to_install']['version']))
342
	{
343
		$request = $smcFunc['db_query']('', '
344
			SELECT id_theme, variable, value
345
			FROM {db_prefix}themes
346
			WHERE id_member = {int:no_member}
347
				AND variable = {string:name}
348
				AND value LIKE {string:name_value}
349
			LIMIT 1',
350
			array(
351
				'no_member' => 0,
352
				'name' => 'name',
353
				'version' => 'version',
354
				'name_value' => '%' . $context['to_install']['name'] . '%',
355
			)
356
		);
357
358
		$to_update = $smcFunc['db_fetch_assoc']($request);
359
		$smcFunc['db_free_result']($request);
360
361
		// Got something, lets figure it out what to do next.
362
		if (!empty($to_update) && !empty($to_update['version']))
363
			switch (compareVersions($context['to_install']['version'], $to_update['version']))
364
			{
365
				case 1: // Got a newer version, update the old entry.
366
					$smcFunc['db_query']('', '
367
						UPDATE {db_prefix}themes
368
						SET value = {string:new_value}
369
						WHERE variable = {string:version}
370
							AND id_theme = {int:id_theme}',
371
						array(
372
							'new_value' => $context['to_install']['version'],
373
							'version' => 'version',
374
							'id_theme' => $to_update['id_theme'],
375
						)
376
					);
377
378
					// Done with the update, tell the user about it.
379
					$context['to_install']['updated'] = true;
380
381
					return $to_update['id_theme'];
382
					break; // Just for reference.
383
				case 0: // This is exactly the same theme.
384
				case -1: // The one being installed is older than the one already installed.
385
				default: // Any other possible result.
386
					fatal_lang_error('package_get_error_theme_no_new_version', false, array($context['to_install']['version'], $to_update['version']));
387
			}
388
	}
389
390
	if (!empty($context['to_install']['based_on']))
391
	{
392
		// No need for elaborated stuff when the theme is based on the default one.
393
		if ($context['to_install']['based_on'] == 'default')
394
		{
395
			$context['to_install']['theme_url'] = $settings['default_theme_url'];
396
			$context['to_install']['images_url'] = $settings['default_images_url'];
397
		}
398
399
		// Custom theme based on another custom theme, lets get some info.
400
		elseif ($context['to_install']['based_on'] != '')
401
		{
402
			$context['to_install']['based_on'] = preg_replace('~[^A-Za-z0-9\-_ ]~', '', $context['to_install']['based_on']);
403
404
			// Get the theme info first.
405
			$request = $smcFunc['db_query']('', '
406
				SELECT id_theme
407
				FROM {db_prefix}themes
408
				WHERE id_member = {int:no_member}
409
					AND (value LIKE {string:based_on} OR value LIKE {string:based_on_path})
410
				LIMIT 1',
411
				array(
412
					'no_member' => 0,
413
					'based_on' => '%/' . $context['to_install']['based_on'],
414
					'based_on_path' => '%' . "\\" . $context['to_install']['based_on'],
415
				)
416
			);
417
418
			$based_on = $smcFunc['db_fetch_assoc']($request);
419
			$smcFunc['db_free_result']($request);
420
421
			$request = $smcFunc['db_query']('', '
422
				SELECT variable, value
423
				FROM {db_prefix}themes
424
				WHERE variable IN ({array_string:theme_values})
425
					AND id_theme = ({int:based_on})
426
				LIMIT 1',
427
				array(
428
					'no_member' => 0,
429
					'theme__values' => array('theme_url', 'images_url', 'theme_dir',),
430
					'based_on' => $based_on['id_theme'],
431
				)
432
			);
433
			$temp = $smcFunc['db_fetch_assoc']($request);
434
			$smcFunc['db_free_result']($request);
435
436
			// Found the based on theme info, add it to the current one being installed.
437
			if (is_array($temp))
438
			{
439
				$context['to_install']['base_theme_url'] = $temp['theme_url'];
440
				$context['to_install']['base_theme_dir'] = $temp['theme_dir'];
441
442
				if (empty($explicit_images) && !empty($context['to_install']['base_theme_url']))
443
					$context['to_install']['theme_url'] = $context['to_install']['base_theme_url'];
444
			}
445
446
			// Nope, sorry, couldn't find any theme already installed.
447
			else
448
				fatal_lang_error('package_get_error_theme_no_based_on_found', false, $context['to_install']['based_on']);
449
		}
450
451
		unset($context['to_install']['based_on']);
452
	}
453
454
	// Find the newest id_theme.
455
	$result = $smcFunc['db_query']('', '
456
		SELECT MAX(id_theme)
457
		FROM {db_prefix}themes',
458
		array(
459
		)
460
	);
461
	list ($id_theme) = $smcFunc['db_fetch_row']($result);
462
	$smcFunc['db_free_result']($result);
463
464
	// This will be theme number...
465
	$id_theme++;
466
467
	// Last minute changes? although, the actual array is a context value you might want to use the new ID.
468
	call_integration_hook('integrate_theme_install', array(&$context['to_install'], $id_theme));
469
470
	$inserts = array();
471
	foreach ($context['to_install'] as $var => $val)
472
		$inserts[] = array($id_theme, $var, $val);
473
474
	if (!empty($inserts))
475
		$smcFunc['db_insert']('insert',
476
			'{db_prefix}themes',
477
			array('id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'),
478
			$inserts,
479
			array('id_theme', 'variable')
480
		);
481
482
	// Update the known and enable Theme's settings.
483
	$known = strtr($modSettings['knownThemes'] . ',' . $id_theme, array(',,' => ','));
484
	$enable = strtr($modSettings['enableThemes'] . ',' . $id_theme, array(',,' => ','));
485
	updateSettings(array('knownThemes' => $known, 'enableThemes' => $enable));
486
487
	return $id_theme;
488
}
489
490
/**
491
 * Removes a directory from the themes dir.
492
 *
493
 * This is a recursive function, it will call itself if there are subdirs inside the main directory.
494
 *
495
 * @param string $path The absolute path to the directory to be removed
496
 * @return bool true when success, false on error.
497
 */
498
function remove_dir($path)
499
{
500
	if (empty($path))
501
		return false;
502
503
	if (is_dir($path))
504
	{
505
		$objects = scandir($path);
506
507
		foreach ($objects as $object)
508
			if ($object != '.' && $object != '..')
509
			{
510
				if (filetype($path . '/' . $object) == 'dir')
511
					remove_dir($path . '/' . $object);
512
513
				else
514
					unlink($path . '/' . $object);
515
			}
516
	}
517
518
	reset($objects);
519
	rmdir($path);
520
}
521
522
/**
523
 * Removes a theme from the DB, includes all possible places where the theme might be used.
524
 *
525
 * @param int $themeID The theme ID
526
 * @return bool true when success, false on error.
527
 */
528
function remove_theme($themeID)
529
{
530
	global $smcFunc, $modSettings;
531
532
	// Can't delete the default theme, sorry!
533
	if (empty($themeID) || $themeID == 1)
534
		return false;
535
536
	$known = explode(',', $modSettings['knownThemes']);
537
	$enable = explode(',', $modSettings['enableThemes']);
538
539
	// Remove it from the themes table.
540
	$smcFunc['db_query']('', '
541
		DELETE FROM {db_prefix}themes
542
		WHERE id_theme = {int:current_theme}',
543
		array(
544
			'current_theme' => $themeID,
545
		)
546
	);
547
548
	// Update users preferences.
549
	$smcFunc['db_query']('', '
550
		UPDATE {db_prefix}members
551
		SET id_theme = {int:default_theme}
552
		WHERE id_theme = {int:current_theme}',
553
		array(
554
			'default_theme' => 0,
555
			'current_theme' => $themeID,
556
		)
557
	);
558
559
	// Some boards may have it as preferred theme.
560
	$smcFunc['db_query']('', '
561
		UPDATE {db_prefix}boards
562
		SET id_theme = {int:default_theme}
563
		WHERE id_theme = {int:current_theme}',
564
		array(
565
			'default_theme' => 0,
566
			'current_theme' => $themeID,
567
		)
568
	);
569
570
	// Remove it from the list of known themes.
571
	$known = array_diff($known, array($themeID));
572
573
	// And the enable list too.
574
	$enable = array_diff($enable, array($themeID));
575
576
	// Back to good old comma separated string.
577
	$known = strtr(implode(',', $known), array(',,' => ','));
578
	$enable = strtr(implode(',', $enable), array(',,' => ','));
579
580
	// Update the enableThemes list.
581
	updateSettings(array('enableThemes' => $enable, 'knownThemes' => $known));
582
583
	// Fix it if the theme was the overall default theme.
584
	if ($modSettings['theme_guests'] == $themeID)
585
		updateSettings(array('theme_guests' => '1'));
586
587
	return true;
588
}
589
590
/**
591
 * Generates a file listing for a given directory
592
 *
593
 * @param string $path The full path to the directory
594
 * @param string $relative The relative path (relative to the Themes directory)
595
 * @return array An array of information about the files and directories found
596
 */
597
function get_file_listing($path, $relative)
598
{
599
	global $scripturl, $txt, $context;
600
601
	// Is it even a directory?
602
	if (!is_dir($path))
603
		fatal_lang_error('error_invalid_dir', 'critical');
604
605
	$dir = dir($path);
606
	$entries = array();
607
	while ($entry = $dir->read())
608
		$entries[] = $entry;
609
	$dir->close();
610
611
	natcasesort($entries);
612
613
	$listing1 = array();
614
	$listing2 = array();
615
616
	foreach ($entries as $entry)
617
	{
618
		// Skip all dot files, including .htaccess.
619
		if (substr($entry, 0, 1) == '.' || $entry == 'CVS')
620
			continue;
621
622
		if (is_dir($path . '/' . $entry))
623
			$listing1[] = array(
624
				'filename' => $entry,
625
				'is_writable' => is_writable($path . '/' . $entry),
626
				'is_directory' => true,
627
				'is_template' => false,
628
				'is_image' => false,
629
				'is_editable' => false,
630
				'href' => $scripturl . '?action=admin;area=theme;th=' . $_GET['th'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=edit;directory=' . $relative . $entry,
631
				'size' => '',
632
			);
633
		else
634
		{
635
			$size = filesize($path . '/' . $entry);
636
			if ($size > 2048 || $size == 1024)
637
				$size = comma_format($size / 1024) . ' ' . $txt['themeadmin_edit_kilobytes'];
638
			else
639
				$size = comma_format($size) . ' ' . $txt['themeadmin_edit_bytes'];
640
641
			$listing2[] = array(
642
				'filename' => $entry,
643
				'is_writable' => is_writable($path . '/' . $entry),
644
				'is_directory' => false,
645
				'is_template' => preg_match('~\.template\.php$~', $entry) != 0,
646
				'is_image' => preg_match('~\.(jpg|jpeg|gif|bmp|png)$~', $entry) != 0,
647
				'is_editable' => is_writable($path . '/' . $entry) && preg_match('~\.(php|pl|css|js|vbs|xml|xslt|txt|xsl|html|htm|shtm|shtml|asp|aspx|cgi|py)$~', $entry) != 0,
648
				'href' => $scripturl . '?action=admin;area=theme;th=' . $_GET['th'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=edit;filename=' . $relative . $entry,
649
				'size' => $size,
650
				'last_modified' => timeformat(filemtime($path . '/' . $entry)),
651
			);
652
		}
653
	}
654
655
	return array_merge($listing1, $listing2);
656
}
657
658
?>