Issues (1061)

Sources/Subs-Admin.php (2 issues)

1
<?php
2
3
/**
4
 * This file contains functions that are specifically done by administrators.
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
 * Get a list of versions that are currently installed on the server.
21
 *
22
 * @param array $checkFor An array of what to check versions for - can contain one or more of 'gd', 'imagemagick', 'db_server', 'phpa', 'memcache', 'xcache', 'apc', 'php' or 'server'
23
 * @return array An array of versions (keys are same as what was in $checkFor, values are the versions)
24
 */
25
function getServerVersions($checkFor)
26
{
27
	global $txt, $db_connection, $sourcedir, $smcFunc, $modSettings;
28
29
	loadLanguage('Admin');
30
31
	$versions = array();
32
33
	// Is GD available?  If it is, we should show version information for it too.
34
	if (in_array('gd', $checkFor) && function_exists('gd_info'))
35
	{
36
		$temp = gd_info();
37
		$versions['gd'] = array('title' => $txt['support_versions_gd'], 'version' => $temp['GD Version']);
38
	}
39
40
	// Why not have a look at ImageMagick? If it's installed, we should show version information for it too.
41
	if (in_array('imagemagick', $checkFor) && (class_exists('Imagick') || function_exists('MagickGetVersionString')))
42
	{
43
		if (class_exists('Imagick'))
44
		{
45
			$temp = New Imagick;
46
			$temp2 = $temp->getVersion();
47
			$im_version = $temp2['versionString'];
48
			$extension_version = 'Imagick ' . phpversion('Imagick');
49
		}
50
		else
51
		{
52
			$im_version = MagickGetVersionString();
53
			$extension_version = 'MagickWand ' . phpversion('MagickWand');
54
		}
55
56
		// We already know it's ImageMagick and the website isn't needed...
57
		$im_version = str_replace(array('ImageMagick ', ' https://www.imagemagick.org'), '', $im_version);
58
		$versions['imagemagick'] = array('title' => $txt['support_versions_imagemagick'], 'version' => $im_version . ' (' . $extension_version . ')');
59
	}
60
61
	// Now lets check for the Database.
62
	if (in_array('db_server', $checkFor))
63
	{
64
		db_extend();
65
		if (!isset($db_connection) || $db_connection === false)
66
			trigger_error('getServerVersions(): you need to be connected to the database in order to get its server version', E_USER_NOTICE);
67
		else
68
		{
69
			$versions['db_engine'] = array(
70
				'title' => sprintf($txt['support_versions_db_engine'], $smcFunc['db_title']),
71
				'version' => $smcFunc['db_get_vendor'](),
72
			);
73
			$versions['db_server'] = array(
74
				'title' => sprintf($txt['support_versions_db'], $smcFunc['db_title']),
75
				'version' => $smcFunc['db_get_version'](),
76
			);
77
		}
78
	}
79
80
	// Check to see if we have any accelerators installed.
81
	require_once($sourcedir . '/ManageServer.php');
82
	$detected = loadCacheAPIs();
83
	foreach ($detected as $api => $object)
84
		if (in_array($api, $checkFor))
85
			$versions[$api] = array(
86
				'title' => isset($txt[$api . '_cache']) ? $txt[$api . '_cache'] : $api,
87
				'version' => $detected[$api]->getVersion(),
88
			);
89
90
	if (in_array('php', $checkFor))
91
		$versions['php'] = array(
92
			'title' => 'PHP',
93
			'version' => PHP_VERSION,
94
			'more' => '?action=admin;area=serversettings;sa=phpinfo',
95
		);
96
97
	if (in_array('server', $checkFor))
98
		$versions['server'] = array(
99
			'title' => $txt['support_versions_server'],
100
			'version' => $_SERVER['SERVER_SOFTWARE'],
101
		);
102
103
	return $versions;
104
}
105
106
/**
107
 * Search through source, theme and language files to determine their version.
108
 * Get detailed version information about the physical SMF files on the server.
109
 *
110
 * - the input parameter allows to set whether to include SSI.php and whether
111
 *   the results should be sorted.
112
 * - returns an array containing information on source files, templates and
113
 *   language files found in the default theme directory (grouped by language).
114
 *
115
 * @param array &$versionOptions An array of options. Can contain one or more of 'include_ssi', 'include_subscriptions', 'include_tasks' and 'sort_results'
116
 * @return array An array of file version info.
117
 */
118
function getFileVersions(&$versionOptions)
119
{
120
	global $boarddir, $sourcedir, $settings, $tasksdir;
121
122
	// Default place to find the languages would be the default theme dir.
123
	$lang_dir = $settings['default_theme_dir'] . '/languages';
124
125
	$version_info = array(
126
		'file_versions' => array(),
127
		'default_template_versions' => array(),
128
		'template_versions' => array(),
129
		'default_language_versions' => array(),
130
		'tasks_versions' => array(),
131
	);
132
133
	// Find the version in SSI.php's file header.
134
	if (!empty($versionOptions['include_ssi']) && file_exists($boarddir . '/SSI.php'))
135
	{
136
		$fp = fopen($boarddir . '/SSI.php', 'rb');
137
		$header = fread($fp, 4096);
138
		fclose($fp);
139
140
		// The comment looks rougly like... that.
141
		if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1)
142
			$version_info['file_versions']['SSI.php'] = $match[1];
143
		// Not found!  This is bad.
144
		else
145
			$version_info['file_versions']['SSI.php'] = '??';
146
	}
147
148
	// Do the paid subscriptions handler?
149
	if (!empty($versionOptions['include_subscriptions']) && file_exists($boarddir . '/subscriptions.php'))
150
	{
151
		$fp = fopen($boarddir . '/subscriptions.php', 'rb');
152
		$header = fread($fp, 4096);
153
		fclose($fp);
154
155
		// Found it?
156
		if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1)
157
			$version_info['file_versions']['subscriptions.php'] = $match[1];
158
		// If we haven't how do we all get paid?
159
		else
160
			$version_info['file_versions']['subscriptions.php'] = '??';
161
	}
162
163
	// Load all the files in the Sources directory, except this file and the redirect.
164
	$sources_dir = dir($sourcedir);
165
	while ($entry = $sources_dir->read())
166
	{
167
		if (substr($entry, -4) === '.php' && !is_dir($sourcedir . '/' . $entry) && $entry !== 'index.php')
168
		{
169
			// Read the first 4k from the file.... enough for the header.
170
			$fp = fopen($sourcedir . '/' . $entry, 'rb');
171
			$header = fread($fp, 4096);
172
			fclose($fp);
173
174
			// Look for the version comment in the file header.
175
			if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1)
176
				$version_info['file_versions'][$entry] = $match[1];
177
			// It wasn't found, but the file was... show a '??'.
178
			else
179
				$version_info['file_versions'][$entry] = '??';
180
		}
181
	}
182
	$sources_dir->close();
183
184
	// Load all the files in the tasks directory.
185
	if (!empty($versionOptions['include_tasks']))
186
	{
187
		$tasks_dir = dir($tasksdir);
188
		while ($entry = $tasks_dir->read())
189
		{
190
			if (substr($entry, -4) === '.php' && !is_dir($tasksdir . '/' . $entry) && $entry !== 'index.php')
191
			{
192
				// Read the first 4k from the file.... enough for the header.
193
				$fp = fopen($tasksdir . '/' . $entry, 'rb');
194
				$header = fread($fp, 4096);
195
				fclose($fp);
196
197
				// Look for the version comment in the file header.
198
				if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1)
199
					$version_info['tasks_versions'][$entry] = $match[1];
200
				// It wasn't found, but the file was... show a '??'.
201
				else
202
					$version_info['tasks_versions'][$entry] = '??';
203
			}
204
		}
205
		$tasks_dir->close();
206
	}
207
208
	// Load all the files in the default template directory - and the current theme if applicable.
209
	$directories = array('default_template_versions' => $settings['default_theme_dir']);
210
	if ($settings['theme_id'] != 1)
211
		$directories += array('template_versions' => $settings['theme_dir']);
212
213
	foreach ($directories as $type => $dirname)
214
	{
215
		$this_dir = dir($dirname);
216
		while ($entry = $this_dir->read())
217
		{
218
			if (substr($entry, -12) == 'template.php' && !is_dir($dirname . '/' . $entry))
219
			{
220
				// Read the first 768 bytes from the file.... enough for the header.
221
				$fp = fopen($dirname . '/' . $entry, 'rb');
222
				$header = fread($fp, 768);
223
				fclose($fp);
224
225
				// Look for the version comment in the file header.
226
				if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1)
227
					$version_info[$type][$entry] = $match[1];
228
				// It wasn't found, but the file was... show a '??'.
229
				else
230
					$version_info[$type][$entry] = '??';
231
			}
232
		}
233
		$this_dir->close();
234
	}
235
236
	// Load up all the files in the default language directory and sort by language.
237
	$this_dir = dir($lang_dir);
238
	while ($entry = $this_dir->read())
239
	{
240
		if (substr($entry, -4) == '.php' && $entry != 'index.php' && !is_dir($lang_dir . '/' . $entry))
241
		{
242
			// Read the first 768 bytes from the file.... enough for the header.
243
			$fp = fopen($lang_dir . '/' . $entry, 'rb');
244
			$header = fread($fp, 768);
245
			fclose($fp);
246
247
			// Split the file name off into useful bits.
248
			list ($name, $language) = explode('.', $entry);
249
250
			// Look for the version comment in the file header.
251
			if (preg_match('~(?://|/\*)\s*Version:\s+(.+?);\s*' . preg_quote($name, '~') . '(?:[\s]{2}|\*/)~i', $header, $match) == 1)
252
				$version_info['default_language_versions'][$language][$name] = $match[1];
253
			// It wasn't found, but the file was... show a '??'.
254
			else
255
				$version_info['default_language_versions'][$language][$name] = '??';
256
		}
257
	}
258
	$this_dir->close();
259
260
	// Sort the file versions by filename.
261
	if (!empty($versionOptions['sort_results']))
262
	{
263
		ksort($version_info['file_versions']);
264
		ksort($version_info['default_template_versions']);
265
		ksort($version_info['template_versions']);
266
		ksort($version_info['default_language_versions']);
267
		ksort($version_info['tasks_versions']);
268
269
		// For languages sort each language too.
270
		foreach ($version_info['default_language_versions'] as $language => $dummy)
271
			ksort($version_info['default_language_versions'][$language]);
272
	}
273
	return $version_info;
274
}
275
276
/**
277
 * Update the Settings.php file.
278
 *
279
 * The most important function in this file for mod makers happens to be the
280
 * updateSettingsFile() function, but it shouldn't be used often anyway.
281
 *
282
 * - Updates the Settings.php file with the changes supplied in config_vars.
283
 *
284
 * - Expects config_vars to be an associative array, with the keys as the
285
 *   variable names in Settings.php, and the values the variable values.
286
 *
287
 * - Correctly formats the values using smf_var_export().
288
 *
289
 * - Restores standard formatting of the file, if $rebuild is true.
290
 *
291
 * - Checks for changes to db_last_error and passes those off to a separate
292
 *   handler.
293
 *
294
 * - Creates a backup file and will use it should the writing of the
295
 *   new settings file fail.
296
 *
297
 * - Tries to intelligently trim quotes and remove slashes from string values.
298
 *   This is done for backwards compatibility purposes (old versions of this
299
 *   function expected strings to have been manually escaped and quoted). This
300
 *   behaviour can be controlled by the $keep_quotes parameter.
301
 *
302
 * @param array $config_vars An array of one or more variables to update.
303
 * @param bool|null $keep_quotes Whether to strip slashes & trim quotes from string values. Defaults to auto-detection.
304
 * @param bool $rebuild If true, attempts to rebuild with standard format. Default false.
305
 * @return bool True on success, false on failure.
306
 */
307
function updateSettingsFile($config_vars, $keep_quotes = null, $rebuild = false)
308
{
309
	// In this function we intentionally don't declare any global variables.
310
	// This allows us to work with everything cleanly.
311
312
	static $mtime;
313
314
	// Should we try to unescape the strings?
315
	if (empty($keep_quotes))
316
	{
317
		foreach ($config_vars as $var => $val)
318
		{
319
			if (is_string($val) && ($keep_quotes === false || strpos($val, '\'') === 0 && strrpos($val, '\'') === strlen($val) - 1))
320
				$config_vars[$var] = trim(stripcslashes($val), '\'');
321
		}
322
	}
323
324
	// Updating the db_last_error, then don't mess around with Settings.php
325
	if (isset($config_vars['db_last_error']))
326
	{
327
		updateDbLastError($config_vars['db_last_error']);
328
329
		if (count($config_vars) === 1 && empty($rebuild))
330
			return true;
331
332
		// Make sure we delete this from Settings.php, if present.
333
		$config_vars['db_last_error'] = 0;
334
	}
335
336
	// Rebuilding should not be undertaken lightly, so we're picky about the parameter.
337
	if (!is_bool($rebuild))
338
		$rebuild = false;
339
340
	$mtime = isset($mtime) ? (int) $mtime : (defined('TIME_START') ? TIME_START : $_SERVER['REQUEST_TIME']);
341
342
	/*****************
343
	 * PART 1: Setup *
344
	 *****************/
345
346
	// Typically Settings.php is in $boarddir, but maybe this is a custom setup...
347
	foreach (get_included_files() as $settingsFile)
348
		if (basename($settingsFile) === 'Settings.php')
349
			break;
350
351
	// Fallback in case Settings.php isn't loaded (e.g. while installing)
352
	if (basename($settingsFile) !== 'Settings.php')
353
		$settingsFile = (!empty($GLOBALS['boarddir']) && @realpath($GLOBALS['boarddir']) ? $GLOBALS['boarddir'] : (!empty($_SERVER['SCRIPT_FILENAME']) ? dirname($_SERVER['SCRIPT_FILENAME']) : dirname(__DIR__))) . '/Settings.php';
354
355
	// File not found? Attempt an emergency on-the-fly fix!
356
	if (!file_exists($settingsFile))
357
		@touch($settingsFile);
358
359
	// When was Settings.php last changed?
360
	$last_settings_change = filemtime($settingsFile);
361
362
	// Get the current values of everything in Settings.php.
363
	$settings_vars = get_current_settings($mtime, $settingsFile);
364
365
	// If Settings.php is empty for some reason, see if we can use the backup.
366
	if (empty($settings_vars) && file_exists(dirname($settingsFile) . '/Settings_bak.php'))
367
		$settings_vars = get_current_settings($mtime, dirname($settingsFile) . '/Settings_bak.php');
368
369
	// False means there was a problem with the file and we can't safely continue.
370
	if ($settings_vars === false)
371
		return false;
372
373
	// It works best to set everything afresh.
374
	$new_settings_vars = array_merge($settings_vars, $config_vars);
375
376
	// Are we using UTF-8?
377
	$utf8 = isset($GLOBALS['context']['utf8']) ? $GLOBALS['context']['utf8'] : (isset($GLOBALS['utf8']) ? $GLOBALS['utf8'] : (isset($settings_vars['db_character_set']) ? $settings_vars['db_character_set'] === 'utf8' : false));
378
379
	/*
380
	 * A big, fat array to define properties of all the Settings.php variables.
381
	 *
382
	 * - String keys are used to identify actual variables.
383
	 *
384
	 * - Integer keys are used for content not connected to any particular
385
	 *   variable, such as code blocks or the license block.
386
	 *
387
	 * - The content of the 'text' element is simply printed out, if it is used
388
	 *   at all. Use it for comments or to insert code blocks, etc.
389
	 *
390
	 * - The 'default' element, not surprisingly, gives a default value for
391
	 *   the variable.
392
	 *
393
	 * - The 'type' element defines the expected variable type or types. If
394
	 *   more than one type is allowed, this should be an array listing them.
395
	 *   Types should match the possible types returned by gettype().
396
	 *
397
	 * - If 'raw_default' is true, the default should be printed directly,
398
	 *   rather than being handled as a string. Use it if the default contains
399
	 *   code, e.g. 'dirname(__FILE__)'
400
	 *
401
	 * - If 'required' is true and a value for the variable is undefined,
402
	 *   the update will be aborted. (The only exception is during the SMF
403
	 *   installation process.)
404
	 *
405
	 * - If 'auto_delete' is 1 or true and the variable is empty, the variable
406
	 *   will be deleted from Settings.php. If 'auto_delete' is 0/false/null,
407
	 *   the variable will never be deleted. If 'auto_delete' is 2, behaviour
408
	 *   depends on $rebuild: if $rebuild is true, 'auto_delete' == 2 behaves
409
	 *   like 'auto_delete' == 1; if $rebuild is false, 'auto_delete' == 2
410
	 *   behaves like 'auto_delete' == 0.
411
	 *
412
	 * - The optional 'search_pattern' element defines a custom regular
413
	 *   expression to search for the existing entry in the file. This is
414
	 *   primarily useful for code blocks rather than variables.
415
	 *
416
	 * - The optional 'replace_pattern' element defines a custom regular
417
	 *   expression to decide where the replacement entry should be inserted.
418
	 *   Note: 'replace_pattern' should be avoided unless ABSOLUTELY necessary.
419
	 */
420
	$settings_defs = array(
421
		array(
422
			'text' => implode("\n", array(
423
				'',
424
				'/**',
425
				' * The settings file contains all of the basic settings that need to be present when a database/cache is not available.',
426
				' *',
427
				' * Simple Machines Forum (SMF)',
428
				' *',
429
				' * @package SMF',
430
				' * @author Simple Machines https://www.simplemachines.org',
431
				' * @copyright ' . SMF_SOFTWARE_YEAR . ' Simple Machines and individual contributors',
432
				' * @license https://www.simplemachines.org/about/smf/license.php BSD',
433
				' *',
434
				' * @version ' . SMF_VERSION,
435
				' */',
436
				'',
437
			)),
438
			'search_pattern' => '~/\*\*.*?@package\h+SMF\b.*?\*/\n{0,2}~s',
439
		),
440
		'maintenance' => array(
441
			'text' => implode("\n", array(
442
				'',
443
				'########## Maintenance ##########',
444
				'/**',
445
				' * The maintenance "mode"',
446
				' * Set to 1 to enable Maintenance Mode, 2 to make the forum untouchable. (you\'ll have to make it 0 again manually!)',
447
				' * 0 is default and disables maintenance mode.',
448
				' *',
449
				' * @var int 0, 1, 2',
450
				' * @global int $maintenance',
451
				' */',
452
			)),
453
			'default' => 0,
454
			'type' => 'integer',
455
		),
456
		'mtitle' => array(
457
			'text' => implode("\n", array(
458
				'/**',
459
				' * Title for the Maintenance Mode message.',
460
				' *',
461
				' * @var string',
462
				' * @global int $mtitle',
463
				' */',
464
			)),
465
			'default' => 'Maintenance Mode',
466
			'type' => 'string',
467
		),
468
		'mmessage' => array(
469
			'text' => implode("\n", array(
470
				'/**',
471
				' * Description of why the forum is in maintenance mode.',
472
				' *',
473
				' * @var string',
474
				' * @global string $mmessage',
475
				' */',
476
			)),
477
			'default' => 'Okay faithful users...we\'re attempting to restore an older backup of the database...news will be posted once we\'re back!',
478
			'type' => 'string',
479
		),
480
		'mbname' => array(
481
			'text' => implode("\n", array(
482
				'',
483
				'########## Forum Info ##########',
484
				'/**',
485
				' * The name of your forum.',
486
				' *',
487
				' * @var string',
488
				' */',
489
			)),
490
			'default' => 'My Community',
491
			'type' => 'string',
492
		),
493
		'language' => array(
494
			'text' => implode("\n", array(
495
				'/**',
496
				' * The default language file set for the forum.',
497
				' *',
498
				' * @var string',
499
				' */',
500
			)),
501
			'default' => 'english',
502
			'type' => 'string',
503
		),
504
		'boardurl' => array(
505
			'text' => implode("\n", array(
506
				'/**',
507
				' * URL to your forum\'s folder. (without the trailing /!)',
508
				' *',
509
				' * @var string',
510
				' */',
511
			)),
512
			'default' => 'http://127.0.0.1/smf',
513
			'type' => 'string',
514
		),
515
		'webmaster_email' => array(
516
			'text' => implode("\n", array(
517
				'/**',
518
				' * Email address to send emails from. (like [email protected].)',
519
				' *',
520
				' * @var string',
521
				' */',
522
			)),
523
			'default' => '[email protected]',
524
			'type' => 'string',
525
		),
526
		'cookiename' => array(
527
			'text' => implode("\n", array(
528
				'/**',
529
				' * Name of the cookie to set for authentication.',
530
				' *',
531
				' * @var string',
532
				' */',
533
			)),
534
			'default' => 'SMFCookie11',
535
			'type' => 'string',
536
		),
537
		'auth_secret' => array(
538
			'text' => implode("\n", array(
539
				'/**',
540
				' * Secret key used to create and verify cookies, tokens, etc.',
541
				' * Do not change this unless absolutely necessary, and NEVER share it.',
542
				' *',
543
				' * Note: Changing this will immediately log out all members of your forum',
544
				' * and break the token-based links in all previous email notifications,',
545
				' * among other possible effects.',
546
				' *',
547
				' * @var string',
548
				' */',
549
			)),
550
			'default' => null,
551
			'auto_delete' => 1,
552
			'type' => 'string',
553
		),
554
		'db_type' => array(
555
			'text' => implode("\n", array(
556
				'',
557
				'########## Database Info ##########',
558
				'/**',
559
				' * The database type',
560
				' * Default options: mysql, postgresql',
561
				' *',
562
				' * @var string',
563
				' */',
564
			)),
565
			'default' => 'mysql',
566
			'type' => 'string',
567
		),
568
		'db_port' => array(
569
			'text' => implode("\n", array(
570
				'/**',
571
				' * The database port',
572
				' * 0 to use default port for the database type',
573
				' *',
574
				' * @var int',
575
				' */',
576
			)),
577
			'default' => 0,
578
			'type' => 'integer',
579
		),
580
		'db_server' => array(
581
			'text' => implode("\n", array(
582
				'/**',
583
				' * The server to connect to (or a Unix socket)',
584
				' *',
585
				' * @var string',
586
				' */',
587
			)),
588
			'default' => 'localhost',
589
			'required' => true,
590
			'type' => 'string',
591
		),
592
		'db_name' => array(
593
			'text' => implode("\n", array(
594
				'/**',
595
				' * The database name',
596
				' *',
597
				' * @var string',
598
				' */',
599
			)),
600
			'default' => 'smf',
601
			'required' => true,
602
			'type' => 'string',
603
		),
604
		'db_user' => array(
605
			'text' => implode("\n", array(
606
				'/**',
607
				' * Database username',
608
				' *',
609
				' * @var string',
610
				' */',
611
			)),
612
			'default' => 'root',
613
			'required' => true,
614
			'type' => 'string',
615
		),
616
		'db_passwd' => array(
617
			'text' => implode("\n", array(
618
				'/**',
619
				' * Database password',
620
				' *',
621
				' * @var string',
622
				' */',
623
			)),
624
			'default' => '',
625
			'required' => true,
626
			'type' => 'string',
627
		),
628
		'ssi_db_user' => array(
629
			'text' => implode("\n", array(
630
				'/**',
631
				' * Database user for when connecting with SSI',
632
				' *',
633
				' * @var string',
634
				' */',
635
			)),
636
			'default' => '',
637
			'type' => 'string',
638
		),
639
		'ssi_db_passwd' => array(
640
			'text' => implode("\n", array(
641
				'/**',
642
				' * Database password for when connecting with SSI',
643
				' *',
644
				' * @var string',
645
				' */',
646
			)),
647
			'default' => '',
648
			'type' => 'string',
649
		),
650
		'db_prefix' => array(
651
			'text' => implode("\n", array(
652
				'/**',
653
				' * A prefix to put in front of your table names.',
654
				' * This helps to prevent conflicts',
655
				' *',
656
				' * @var string',
657
				' */',
658
			)),
659
			'default' => 'smf_',
660
			'required' => true,
661
			'type' => 'string',
662
		),
663
		'db_persist' => array(
664
			'text' => implode("\n", array(
665
				'/**',
666
				' * Use a persistent database connection',
667
				' *',
668
				' * @var bool',
669
				' */',
670
			)),
671
			'default' => false,
672
			'type' => 'boolean',
673
		),
674
		'db_error_send' => array(
675
			'text' => implode("\n", array(
676
				'/**',
677
				' * Send emails on database connection error',
678
				' *',
679
				' * @var bool',
680
				' */',
681
			)),
682
			'default' => false,
683
			'type' => 'boolean',
684
		),
685
		'db_mb4' => array(
686
			'text' => implode("\n", array(
687
				'/**',
688
				' * Override the default behavior of the database layer for mb4 handling',
689
				' * null keep the default behavior untouched',
690
				' *',
691
				' * @var null|bool',
692
				' */',
693
			)),
694
			'default' => null,
695
			'type' => array('NULL', 'boolean'),
696
		),
697
		'cache_accelerator' => array(
698
			'text' => implode("\n", array(
699
				'',
700
				'########## Cache Info ##########',
701
				'/**',
702
				' * Select a cache system. You want to leave this up to the cache area of the admin panel for',
703
				' * proper detection of apc, memcached, output_cache, smf, or xcache',
704
				' * (you can add more with a mod).',
705
				' *',
706
				' * @var string',
707
				' */',
708
			)),
709
			'default' => '',
710
			'type' => 'string',
711
		),
712
		'cache_enable' => array(
713
			'text' => implode("\n", array(
714
				'/**',
715
				' * The level at which you would like to cache. Between 0 (off) through 3 (cache a lot).',
716
				' *',
717
				' * @var int',
718
				' */',
719
			)),
720
			'default' => 0,
721
			'type' => 'integer',
722
		),
723
		'cache_memcached' => array(
724
			'text' => implode("\n", array(
725
				'/**',
726
				' * This is only used for memcache / memcached. Should be a string of \'server:port,server:port\'',
727
				' *',
728
				' * @var array',
729
				' */',
730
			)),
731
			'default' => '',
732
			'type' => 'string',
733
		),
734
		'cachedir' => array(
735
			'text' => implode("\n", array(
736
				'/**',
737
				' * This is only for the \'smf\' file cache system. It is the path to the cache directory.',
738
				' * It is also recommended that you place this in /tmp/ if you are going to use this.',
739
				' *',
740
				' * @var string',
741
				' */',
742
			)),
743
			'default' => 'dirname(__FILE__) . \'/cache\'',
744
			'raw_default' => true,
745
			'type' => 'string',
746
		),
747
		'image_proxy_enabled' => array(
748
			'text' => implode("\n", array(
749
				'',
750
				'########## Image Proxy ##########',
751
				'# This is done entirely in Settings.php to avoid loading the DB while serving the images',
752
				'/**',
753
				' * Whether the proxy is enabled or not',
754
				' *',
755
				' * @var bool',
756
				' */',
757
			)),
758
			'default' => true,
759
			'type' => 'boolean',
760
		),
761
		'image_proxy_secret' => array(
762
			'text' => implode("\n", array(
763
				'/**',
764
				' * Secret key to be used by the proxy',
765
				' *',
766
				' * @var string',
767
				' */',
768
			)),
769
			'default' => 'smfisawesome',
770
			'type' => 'string',
771
		),
772
		'image_proxy_maxsize' => array(
773
			'text' => implode("\n", array(
774
				'/**',
775
				' * Maximum file size (in KB) for individual files',
776
				' *',
777
				' * @var int',
778
				' */',
779
			)),
780
			'default' => 5192,
781
			'type' => 'integer',
782
		),
783
		'boarddir' => array(
784
			'text' => implode("\n", array(
785
				'',
786
				'########## Directories/Files ##########',
787
				'# Note: These directories do not have to be changed unless you move things.',
788
				'/**',
789
				' * The absolute path to the forum\'s folder. (not just \'.\'!)',
790
				' *',
791
				' * @var string',
792
				' */',
793
			)),
794
			'default' => 'dirname(__FILE__)',
795
			'raw_default' => true,
796
			'type' => 'string',
797
		),
798
		'sourcedir' => array(
799
			'text' => implode("\n", array(
800
				'/**',
801
				' * Path to the Sources directory.',
802
				' *',
803
				' * @var string',
804
				' */',
805
			)),
806
			'default' => 'dirname(__FILE__) . \'/Sources\'',
807
			'raw_default' => true,
808
			'type' => 'string',
809
		),
810
		'packagesdir' => array(
811
			'text' => implode("\n", array(
812
				'/**',
813
				' * Path to the Packages directory.',
814
				' *',
815
				' * @var string',
816
				' */',
817
			)),
818
			'default' => 'dirname(__FILE__) . \'/Packages\'',
819
			'raw_default' => true,
820
			'type' => 'string',
821
		),
822
		'tasksdir' => array(
823
			'text' => implode("\n", array(
824
				'/**',
825
				' * Path to the tasks directory.',
826
				' *',
827
				' * @var string',
828
				' */',
829
			)),
830
			'default' => '$sourcedir . \'/tasks\'',
831
			'raw_default' => true,
832
			'type' => 'string',
833
		),
834
		array(
835
			'text' => implode("\n", array(
836
				'',
837
				'# Make sure the paths are correct... at least try to fix them.',
838
				'if (!is_dir(realpath($boarddir)) && file_exists(dirname(__FILE__) . \'/agreement.txt\'))',
839
				'	$boarddir = dirname(__FILE__);',
840
				'if (!is_dir(realpath($sourcedir)) && is_dir($boarddir . \'/Sources\'))',
841
				'	$sourcedir = $boarddir . \'/Sources\';',
842
				'if (!is_dir(realpath($tasksdir)) && is_dir($sourcedir . \'/tasks\'))',
843
				'	$tasksdir = $sourcedir . \'/tasks\';',
844
				'if (!is_dir(realpath($packagesdir)) && is_dir($boarddir . \'/Packages\'))',
845
				'	$packagesdir = $boarddir . \'/Packages\';',
846
				'if (!is_dir(realpath($cachedir)) && is_dir($boarddir . \'/cache\'))',
847
				'	$cachedir = $boarddir . \'/cache\';',
848
			)),
849
			'search_pattern' => '~\n?(#[^\n]+)?(?:\n\h*if\s*\((?:\!file_exists\(\$(?>boarddir|sourcedir|tasksdir|packagesdir|cachedir)\)|\!is_dir\(realpath\(\$(?>boarddir|sourcedir|tasksdir|packagesdir|cachedir)\)\))[^;]+\n\h*\$(?>boarddir|sourcedir|tasksdir|packagesdir|cachedir)[^\n]+;)+~sm',
850
		),
851
		'db_character_set' => array(
852
			'text' => implode("\n", array(
853
				'',
854
				'######### Legacy Settings #########',
855
				'# UTF-8 is now the only character set supported in 2.1.',
856
			)),
857
			'default' => 'utf8',
858
			'type' => 'string',
859
		),
860
		'db_show_debug' => array(
861
			'text' => implode("\n", array(
862
				'',
863
				'######### Developer Settings #########',
864
				'# Show debug info.',
865
			)),
866
			'default' => false,
867
			'auto_delete' => 2,
868
			'type' => 'boolean',
869
		),
870
		array(
871
			'text' => implode("\n", array(
872
				'',
873
				'########## Error-Catching ##########',
874
				'# Note: You shouldn\'t touch these settings.',
875
				'if (file_exists((isset($cachedir) ? $cachedir : dirname(__FILE__)) . \'/db_last_error.php\'))',
876
				'	include((isset($cachedir) ? $cachedir : dirname(__FILE__)) . \'/db_last_error.php\');',
877
				'',
878
				'if (!isset($db_last_error))',
879
				'{',
880
				'	// File does not exist so lets try to create it',
881
				'	file_put_contents((isset($cachedir) ? $cachedir : dirname(__FILE__)) . \'/db_last_error.php\', \'<\' . \'?\' . "php\n" . \'$db_last_error = 0;\' . "\n" . \'?\' . \'>\');',
882
				'	$db_last_error = 0;',
883
				'}',
884
			)),
885
			// Designed to match both 2.0 and 2.1 versions of this code.
886
			'search_pattern' => '~\n?#+ Error.Catching #+\n[^\n]*?settings\.\n(?:\$db_last_error = \d{1,11};|if \(file_exists.*?\$db_last_error = 0;(?' . '>\s*}))(?=\n|\?' . '>|$)~s',
887
		),
888
		// Temporary variable used during the upgrade process.
889
		'upgradeData' => array(
890
			'default' => '',
891
			'auto_delete' => 1,
892
			'type' => 'string',
893
		),
894
		// This should be removed if found.
895
		'db_last_error' => array(
896
			'default' => 0,
897
			'auto_delete' => 1,
898
			'type' => 'integer',
899
		),
900
	);
901
902
	// Allow mods the option to define comments, defaults, etc., for their settings.
903
	// Check if function exists, in case we are calling from installer or upgrader.
904
	if (function_exists('call_integration_hook'))
905
		call_integration_hook('integrate_update_settings_file', array(&$settings_defs));
906
907
	// If Settings.php is empty or invalid, try to recover using whatever is in $GLOBALS.
908
	if ($settings_vars === array())
909
	{
910
		foreach ($settings_defs as $var => $setting_def)
911
			if (isset($GLOBALS[$var]))
912
				$settings_vars[$var] = $GLOBALS[$var];
913
914
		$new_settings_vars = array_merge($settings_vars, $config_vars);
915
	}
916
917
	// During install/upgrade, don't set anything until we're ready for it.
918
	if (defined('SMF_INSTALLING') && empty($rebuild))
919
	{
920
		foreach ($settings_defs as $var => $setting_def)
921
			if (!in_array($var, array_keys($new_settings_vars)) && !is_int($var))
922
				unset($settings_defs[$var]);
923
	}
924
925
	/*******************************
926
	 * PART 2: Build substitutions *
927
	 *******************************/
928
929
	$type_regex = array(
930
		'string' =>
931
			'(?:' .
932
				// match the opening quotation mark...
933
				'(["\'])' .
934
				// then any number of other characters or escaped quotation marks...
935
				'(?:.(?!\\1)|\\\(?=\\1))*.?' .
936
				// then the closing quotation mark.
937
				'\\1' .
938
				// Maybe there's a second string concatenated to this one.
939
				'(?:\s*\.\s*)*' .
940
			')+',
941
		// Some numeric values might have been stored as strings.
942
		'integer' =>  '["\']?[+-]?\d+["\']?',
943
		'double' =>  '["\']?[+-]?\d+\.\d+([Ee][+-]\d+)?["\']?',
944
		// Some boolean values might have been stored as integers.
945
		'boolean' =>  '(?i:TRUE|FALSE|(["\']?)[01]\b\\1)',
946
		'NULL' =>  '(?i:NULL)',
947
		// These use a PCRE subroutine to match nested arrays.
948
		'array' =>  'array\s*(\((?>[^()]|(?1))*\))',
949
		'object' =>  '\w+::__set_state\(array\s*(\((?>[^()]|(?1))*\))\)',
950
	);
951
952
	/*
953
	 * The substitutions take place in one of two ways:
954
	 *
955
	 *  1: The search_pattern regex finds a string in Settings.php, which is
956
	 *     temporarily replaced by a placeholder. Once all the placeholders
957
	 *     have been inserted, each is replaced by the final replacement string
958
	 *     that we want to use. This is the standard method.
959
	 *
960
	 *  2: The search_pattern regex finds a string in Settings.php, which is
961
	 *     then deleted by replacing it with an empty placeholder. Then after
962
	 *     all the real placeholders have been dealt with, the replace_pattern
963
	 *     regex finds where to insert the final replacement string that we
964
	 *     want to use. This method is for special cases.
965
	 */
966
	$prefix = mt_rand() . '-';
967
	$neg_index = -1;
968
	$substitutions = array(
969
		$neg_index-- => array(
970
			'search_pattern' => '~^\s*<\?(php\b)?\n?~',
971
			'placeholder' => '',
972
			'replace_pattern' => '~^~',
973
			'replacement' => '<' . "?php\n",
974
		),
975
		$neg_index-- => array(
976
			'search_pattern' => '~\S\K\s*(\?' . '>)?\s*$~',
977
			'placeholder' => "\n" . md5($prefix . '?' . '>'),
978
			'replacement' => "\n\n?" . '>',
979
		),
980
		// Remove the code that redirects to the installer.
981
		$neg_index-- => array(
982
			'search_pattern' => '~^if\s*\(file_exists\(dirname\(__FILE__\)\s*\.\s*\'/install\.php\'\)\)\s*(?:({(?>[^{}]|(?1))*})\h*|header(\((?' . '>[^()]|(?2))*\));\n)~m',
983
			'placeholder' => '',
984
		),
985
	);
986
987
	if (defined('SMF_INSTALLING'))
988
		$substitutions[$neg_index--] = array(
989
			'search_pattern' => '~/\*.*?SMF\s+1\.\d.*?\*/~s',
990
			'placeholder' => '',
991
		);
992
993
	foreach ($settings_defs as $var => $setting_def)
994
	{
995
		$placeholder = md5($prefix . $var);
996
		$replacement = '';
997
998
		if (!empty($setting_def['text']))
999
		{
1000
			// Special handling for the license block: always at the beginning.
1001
			if (strpos($setting_def['text'], "* @package SMF\n") !== false)
1002
			{
1003
				$substitutions[$var]['search_pattern'] = $setting_def['search_pattern'];
1004
				$substitutions[$var]['placeholder'] = '';
1005
				$substitutions[-1]['replacement'] .= $setting_def['text'] . "\n";
1006
			}
1007
			// Special handling for the Error-Catching block: always at the end.
1008
			elseif (strpos($setting_def['text'], 'Error-Catching') !== false)
1009
			{
1010
				$errcatch_var = $var;
1011
				$substitutions[$var]['search_pattern'] = $setting_def['search_pattern'];
1012
				$substitutions[$var]['placeholder'] = '';
1013
				$substitutions[-2]['replacement'] = "\n" . $setting_def['text'] . $substitutions[-2]['replacement'];
1014
			}
1015
			// The text is the whole thing (code blocks, etc.)
1016
			elseif (is_int($var))
1017
			{
1018
				// Remember the path correcting code for later.
1019
				if (strpos($setting_def['text'], '# Make sure the paths are correct') !== false)
1020
					$pathcode_var = $var;
1021
1022
				if (!empty($setting_def['search_pattern']))
1023
					$substitutions[$var]['search_pattern'] = $setting_def['search_pattern'];
1024
				else
1025
					$substitutions[$var]['search_pattern'] = '~' . preg_quote($setting_def['text'], '~') . '~';
1026
1027
				$substitutions[$var]['placeholder'] = $placeholder;
1028
1029
				$replacement .= $setting_def['text'] . "\n";
1030
			}
1031
			// We only include comments when rebuilding.
1032
			elseif (!empty($rebuild))
1033
				$replacement .= $setting_def['text'] . "\n";
1034
		}
1035
1036
		if (is_string($var))
1037
		{
1038
			// Ensure the value is good.
1039
			if (in_array($var, array_keys($new_settings_vars)))
1040
			{
1041
				// Objects without a __set_state method need a fallback.
1042
				if (is_object($new_settings_vars[$var]) && !method_exists($new_settings_vars[$var], '__set_state'))
1043
				{
1044
					if (method_exists($new_settings_vars[$var], '__toString'))
1045
						$new_settings_vars[$var] = (string) $new_settings_vars[$var];
1046
					else
1047
						$new_settings_vars[$var] = (array) $new_settings_vars[$var];
1048
				}
1049
1050
				// Normalize the type if necessary.
1051
				if (isset($setting_def['type']))
1052
				{
1053
					$expected_types = (array) $setting_def['type'];
1054
					$var_type = gettype($new_settings_vars[$var]);
1055
1056
					// Variable is not of an expected type.
1057
					if (!in_array($var_type, $expected_types))
1058
					{
1059
						// Passed in an unexpected array.
1060
						if ($var_type == 'array')
1061
						{
1062
							$temp = reset($new_settings_vars[$var]);
1063
1064
							// Use the first element if there's only one and it is a scalar.
1065
							if (count($new_settings_vars[$var]) === 1 && is_scalar($temp))
1066
								$new_settings_vars[$var] = $temp;
1067
1068
							// Or keep the old value, if that is good.
1069
							elseif (isset($settings_vars[$var]) && in_array(gettype($settings_vars[$var]), $expected_types))
1070
								$new_settings_vars[$var] = $settings_vars[$var];
1071
1072
							// Fall back to the default
1073
							else
1074
								$new_settings_vars[$var] = $setting_def['default'];
1075
						}
1076
1077
						// Cast it to whatever type was expected.
1078
						// Note: the order of the types in this loop matters.
1079
						foreach (array('boolean', 'integer', 'double', 'string', 'array') as $to_type)
1080
						{
1081
							if (in_array($to_type, $expected_types))
1082
							{
1083
								settype($new_settings_vars[$var], $to_type);
1084
								break;
1085
							}
1086
						}
1087
					}
1088
				}
1089
			}
1090
			// Abort if a required one is undefined (unless we're installing).
1091
			elseif (!empty($setting_def['required']) && !defined('SMF_INSTALLING'))
1092
				return false;
1093
1094
			// Create the search pattern.
1095
			if (!empty($setting_def['search_pattern']))
1096
				$substitutions[$var]['search_pattern'] = $setting_def['search_pattern'];
1097
			else
1098
			{
1099
				$var_pattern = array();
1100
1101
				if (isset($setting_def['type']))
1102
				{
1103
					foreach ((array) $setting_def['type'] as $type)
1104
						$var_pattern[] = $type_regex[$type];
1105
				}
1106
1107
				if (in_array($var, array_keys($config_vars)))
1108
				{
1109
					$var_pattern[] = @$type_regex[gettype($config_vars[$var])];
1110
1111
					if (is_string($config_vars[$var]) && strpos($config_vars[$var], dirname($settingsFile)) === 0)
1112
						$var_pattern[] = '(?:__DIR__|dirname\(__FILE__\)) . \'' . (preg_quote(str_replace(dirname($settingsFile), '', $config_vars[$var]), '~')) . '\'';
1113
				}
1114
1115
				if (in_array($var, array_keys($settings_vars)))
1116
				{
1117
					$var_pattern[] = @$type_regex[gettype($settings_vars[$var])];
1118
1119
					if (is_string($settings_vars[$var]) && strpos($settings_vars[$var], dirname($settingsFile)) === 0)
1120
						$var_pattern[] = '(?:__DIR__|dirname\(__FILE__\)) . \'' . (preg_quote(str_replace(dirname($settingsFile), '', $settings_vars[$var]), '~')) . '\'';
1121
				}
1122
1123
				if (!empty($setting_def['raw_default']) && $setting_def['default'] !== '')
1124
				{
1125
					$var_pattern[] = preg_replace('/\s+/', '\s+', preg_quote($setting_def['default'], '~'));
1126
1127
					if (strpos($setting_def['default'], 'dirname(__FILE__)') !== false)
1128
						$var_pattern[] = preg_replace('/\s+/', '\s+', preg_quote(str_replace('dirname(__FILE__)', '__DIR__', $setting_def['default']), '~'));
1129
1130
					if (strpos($setting_def['default'], '__DIR__') !== false)
1131
						$var_pattern[] = preg_replace('/\s+/', '\s+', preg_quote(str_replace('__DIR__', 'dirname(__FILE__)', $setting_def['default']), '~'));
1132
				}
1133
1134
				$var_pattern = array_unique($var_pattern);
1135
1136
				$var_pattern = count($var_pattern) > 1 ? '(?:' . (implode('|', $var_pattern)) . ')' : $var_pattern[0];
1137
1138
				$substitutions[$var]['search_pattern'] = '~(?<=^|\s)\h*\$' . preg_quote($var, '~') . '\s*=\s*' . $var_pattern . ';~' . (!empty($context['utf8']) ? 'u' : '');
1139
			}
1140
1141
			// Next create the placeholder or replace_pattern.
1142
			if (!empty($setting_def['replace_pattern']))
1143
				$substitutions[$var]['replace_pattern'] = $setting_def['replace_pattern'];
1144
			else
1145
				$substitutions[$var]['placeholder'] = $placeholder;
1146
1147
			// Now create the replacement.
1148
			// A setting to delete.
1149
			if (!empty($setting_def['auto_delete']) && empty($new_settings_vars[$var]))
1150
			{
1151
				if ($setting_def['auto_delete'] === 2 && empty($rebuild) && in_array($var, array_keys($new_settings_vars)))
1152
				{
1153
					$replacement .= '$' . $var . ' = ' . ($new_settings_vars[$var] === $setting_def['default'] && !empty($setting_def['raw_default']) ? sprintf($new_settings_vars[$var]) : smf_var_export($new_settings_vars[$var], true)) . ";";
1154
				}
1155
				else
1156
				{
1157
					$replacement = '';
1158
1159
					// This is just for cosmetic purposes. Removes the blank line.
1160
					$substitutions[$var]['search_pattern'] = str_replace('(?<=^|\s)', '\n?', $substitutions[$var]['search_pattern']);
1161
				}
1162
			}
1163
			// Add this setting's value.
1164
			elseif (in_array($var, array_keys($new_settings_vars)))
1165
			{
1166
				$replacement .= '$' . $var . ' = ' . ($new_settings_vars[$var] === $setting_def['default'] && !empty($setting_def['raw_default']) ? sprintf($new_settings_vars[$var]) : smf_var_export($new_settings_vars[$var], true)) . ";";
1167
			}
1168
			// Fall back to the default value.
1169
			elseif (isset($setting_def['default']))
1170
			{
1171
				$replacement .= '$' . $var . ' = ' . (!empty($setting_def['raw_default']) ? sprintf($setting_def['default']) : smf_var_export($setting_def['default'], true)) . ';';
1172
			}
1173
			// This shouldn't happen, but we've got nothing.
1174
			else
1175
				$replacement .= '$' . $var . ' = null;';
1176
		}
1177
1178
		$substitutions[$var]['replacement'] = $replacement;
1179
1180
		// We're done with this one.
1181
		unset($new_settings_vars[$var]);
1182
	}
1183
1184
	// Any leftovers to deal with?
1185
	foreach ($new_settings_vars as $var => $val)
1186
	{
1187
		$var_pattern = array();
1188
1189
		if (in_array($var, array_keys($config_vars)))
1190
			$var_pattern[] = $type_regex[gettype($config_vars[$var])];
1191
1192
		if (in_array($var, array_keys($settings_vars)))
1193
			$var_pattern[] = $type_regex[gettype($settings_vars[$var])];
1194
1195
		$var_pattern = array_unique($var_pattern);
1196
1197
		$var_pattern = count($var_pattern) > 1 ? '(?:' . (implode('|', $var_pattern)) . ')' : $var_pattern[0];
1198
1199
		$placeholder = md5($prefix . $var);
1200
1201
		$substitutions[$var]['search_pattern'] = '~(?<=^|\s)\h*\$' . preg_quote($var, '~') . '\s*=\s*' . $var_pattern . ';~' . (!empty($context['utf8']) ? 'u' : '');
1202
		$substitutions[$var]['placeholder'] = $placeholder;
1203
		$substitutions[$var]['replacement'] = '$' . $var . ' = ' . smf_var_export($val, true) . ";";
1204
	}
1205
1206
	// During an upgrade, some of the path variables may not have been declared yet.
1207
	if (defined('SMF_INSTALLING') && empty($rebuild))
1208
	{
1209
		preg_match_all('~^\h*\$(\w+)\s*=\s*~m', $substitutions[$pathcode_var]['replacement'], $matches);
1210
		$missing_pathvars = array_diff($matches[1], array_keys($substitutions));
1211
1212
		if (!empty($missing_pathvars))
1213
		{
1214
			foreach ($missing_pathvars as $var)
1215
			{
1216
				$substitutions[$pathcode_var]['replacement'] = preg_replace('~\nif[^\n]+\$' . $var . '[^\n]+\n\h*\$' . $var . ' = [^\n]+~', '', $substitutions[$pathcode_var]['replacement']);
1217
			}
1218
		}
1219
	}
1220
1221
	// It's important to do the numbered ones before the named ones, or messes happen.
1222
	uksort($substitutions, function($a, $b) {
1223
		if (is_int($a) && is_int($b))
1224
			return $a > $b;
1225
		elseif (is_int($a))
1226
			return -1;
1227
		elseif (is_int($b))
1228
			return 1;
1229
		else
1230
			return strcasecmp($b, $a);
1231
	});
1232
1233
	/******************************
1234
	 * PART 3: Content processing *
1235
	 ******************************/
1236
1237
	/* 3.a: Get the content of Settings.php and make sure it is good. */
1238
1239
	// Retrieve the contents of Settings.php and normalize the line endings.
1240
	$settingsText = trim(strtr(file_get_contents($settingsFile), array("\r\n" => "\n", "\r" => "\n")));
1241
1242
	// If Settings.php is empty or corrupt for some reason, see if we can recover.
1243
	if ($settingsText == '' || substr($settingsText, 0, 5) !== '<' . '?php')
1244
	{
1245
		// Try restoring from the backup.
1246
		if (file_exists(dirname($settingsFile) . '/Settings_bak.php'))
1247
			$settingsText = strtr(file_get_contents(dirname($settingsFile) . '/Settings_bak.php'), array("\r\n" => "\n", "\r" => "\n"));
1248
1249
		// Backup is bad too? Our only option is to create one from scratch.
1250
		if ($settingsText == '' || substr($settingsText, 0, 5) !== '<' . '?php' || substr($settingsText, -2) !== '?' . '>')
1251
		{
1252
			$settingsText = '<' . "?php\n";
1253
			foreach ($settings_defs as $var => $setting_def)
1254
			{
1255
				if (!empty($setting_def['text']) && strpos($substitutions[$var]['replacement'], $setting_def['text']) === false)
1256
					$substitutions[$var]['replacement'] = $setting_def['text'] . "\n" . $substitutions[$var]['replacement'];
1257
1258
				$settingsText .= $substitutions[$var]['replacement'] . "\n";
1259
			}
1260
			$settingsText .= "\n\n?" . '>';
1261
			$rebuild = true;
1262
		}
1263
	}
1264
1265
	// Settings.php is unlikely to contain any heredocs, but just in case...
1266
	if (preg_match_all('/<<<(\'?)(\w+)\'?\n(.*?)\n\2;$/m', $settingsText, $matches))
1267
	{
1268
		foreach ($matches[0] as $mkey => $heredoc)
1269
		{
1270
			if (!empty($matches[1][$mkey]))
1271
				$heredoc_replacements[$heredoc] = var_export($matches[3][$mkey], true) . ';';
1272
			else
1273
				$heredoc_replacements[$heredoc] = '"' . strtr(substr(var_export($matches[3][$mkey], true), 1, -1), array("\\'" => "'", '"' => '\"')) . '";';
1274
		}
1275
1276
		$settingsText = strtr($settingsText, $heredoc_replacements);
1277
	}
1278
1279
	/* 3.b: Loop through all our substitutions to insert placeholders, etc. */
1280
1281
	$last_var = null;
1282
	$bare_settingsText = $settingsText;
1283
	$force_before_pathcode = array();
1284
	foreach ($substitutions as $var => $substitution)
1285
	{
1286
		$placeholders[$var] = $substitution['placeholder'];
1287
1288
		if (!empty($substitution['placeholder']))
1289
		{
1290
			$simple_replacements[$substitution['placeholder']] = $substitution['replacement'];
1291
		}
1292
		elseif (!empty($substitution['replace_pattern']))
1293
		{
1294
			$replace_patterns[$var] = $substitution['replace_pattern'];
1295
			$replace_strings[$var] = $substitution['replacement'];
1296
		}
1297
1298
		if (strpos($substitutions[$pathcode_var]['replacement'], '$' . $var . ' = ') !== false)
1299
			$force_before_pathcode[] = $var;
1300
1301
		// Look before you leap.
1302
		preg_match_all($substitution['search_pattern'], $bare_settingsText, $matches);
1303
1304
		if ((is_string($var) || $var === $pathcode_var) && count($matches[0]) !== 1 && $substitution['replacement'] !== '')
1305
		{
1306
			// More than one instance of the variable = not good.
1307
			if (count($matches[0]) > 1)
1308
			{
1309
				if (is_string($var))
1310
				{
1311
					// Maybe we can try something more interesting?
1312
					$sp = substr($substitution['search_pattern'], 1);
1313
1314
					if (strpos($sp, '(?<=^|\s)') === 0)
1315
						$sp = substr($sp, 9);
1316
1317
					if (strpos($sp, '^') === 0 || strpos($sp, '(?<') === 0)
1318
						return false;
1319
1320
					// See if we can exclude `if` blocks, etc., to narrow down the matches.
1321
					// @todo Multiple layers of nested brackets might confuse this.
1322
					$sp = '~(?:^|//[^\n]+c\n|\*/|[;}]|' . implode('|', array_filter($placeholders)) . ')\s*' . (strpos($sp, '\K') === false ? '\K' : '') . $sp;
1323
1324
					preg_match_all($sp, $settingsText, $matches);
1325
				}
1326
				else
1327
					$sp = $substitution['search_pattern'];
1328
1329
				// Found at least some that are simple assignment statements.
1330
				if (count($matches[0]) > 0)
1331
				{
1332
					// Remove any duplicates.
1333
					if (count($matches[0]) > 1)
1334
						$settingsText = preg_replace($sp, '', $settingsText, count($matches[0]) - 1);
1335
1336
					// Insert placeholder for the last one.
1337
					$settingsText = preg_replace($sp, $substitution['placeholder'], $settingsText, 1);
1338
				}
1339
1340
				// All instances are inside more complex code structures.
1341
				else
1342
				{
1343
					// Only safe option at this point is to skip it.
1344
					unset($substitutions[$var], $new_settings_vars[$var], $settings_defs[$var], $simple_replacements[$substitution['placeholder']], $replace_patterns[$var], $replace_strings[$var]);
1345
1346
					continue;
1347
				}
1348
			}
1349
			// No matches found.
1350
			elseif (count($matches[0]) === 0)
1351
			{
1352
				$found = false;
1353
				$in_c = in_array($var, array_keys($config_vars));
1354
				$in_s = in_array($var, array_keys($settings_vars));
1355
1356
				// Is it in there at all?
1357
				if (!preg_match('~(^|\s)\$' . preg_quote($var, '~') . '\s*=\s*~', $bare_settingsText))
1358
				{
1359
					// It's defined by Settings.php, but not by code in the file.
1360
					// Probably done via an include or something. Skip it.
1361
					if ($in_s)
1362
						unset($substitutions[$var], $settings_defs[$var]);
1363
1364
					// Admin is explicitly trying to set this one, so we'll handle
1365
					// it as if it were a new custom setting being added.
1366
					elseif ($in_c)
1367
						$new_settings_vars[$var] = $config_vars[$var];
1368
1369
					continue;
1370
				}
1371
1372
				// It's in there somewhere, so check if the value changed type.
1373
				foreach (array('scalar', 'object', 'array') as $type)
1374
				{
1375
					// Try all the other scalar types first.
1376
					if ($type == 'scalar')
1377
						$sp = '(?:' . (implode('|', array_diff_key($type_regex, array($in_c ? gettype($config_vars[$var]) : ($in_s ? gettype($settings_vars[$var]) : PHP_INT_MAX) => '', 'array' => '', 'object' => '')))) . ')';
1378
1379
					// Maybe it's an object? (Probably not, but we should check.)
1380
					elseif ($type == 'object')
1381
					{
1382
						if (strpos($settingsText, '__set_state') === false)
1383
							continue;
1384
1385
						$sp = $type_regex['object'];
1386
					}
1387
1388
					// Maybe it's an array?
1389
					else
1390
						$sp = $type_regex['array'];
1391
1392
					if (preg_match('~(^|\s)\$' . preg_quote($var, '~') . '\s*=\s*' . $sp . '~', $bare_settingsText, $derp))
1393
					{
1394
						$settingsText = preg_replace('~(^|\s)\$' . preg_quote($var, '~') . '\s*=\s*' . $sp . '~', $substitution['placeholder'], $settingsText);
1395
						$found = true;
1396
						break;
1397
					}
1398
				}
1399
1400
				// Something weird is going on. Better just leave it alone.
1401
				if (!$found)
1402
				{
1403
					// $var? What $var? Never heard of it.
1404
					unset($substitutions[$var], $new_settings_vars[$var], $settings_defs[$var], $simple_replacements[$substitution['placeholder']], $replace_patterns[$var], $replace_strings[$var]);
1405
					continue;
1406
				}
1407
			}
1408
		}
1409
		// Good to go, so insert our placeholder.
1410
		else
1411
			$settingsText = preg_replace($substitution['search_pattern'], $substitution['placeholder'], $settingsText);
1412
1413
		// Once the code blocks are done, we want to compare to a version without comments.
1414
		if (is_int($last_var) && is_string($var))
1415
			$bare_settingsText = strip_php_comments($settingsText);
1416
1417
		$last_var = $var;
1418
	}
1419
1420
	// Rebuilding requires more work.
1421
	if (!empty($rebuild))
1422
	{
1423
		// Strip out the leading and trailing placeholders to prevent duplication.
1424
		$settingsText = str_replace(array($substitutions[-1]['placeholder'], $substitutions[-2]['placeholder']), '', $settingsText);
1425
1426
		// Strip out all our standard comments.
1427
		foreach ($settings_defs as $var => $setting_def)
1428
		{
1429
			if (isset($setting_def['text']))
1430
				$settingsText = strtr($settingsText, array($setting_def['text'] . "\n" => '', $setting_def['text'] => '',));
1431
		}
1432
1433
		// We need to refresh $bare_settingsText at this point.
1434
		$bare_settingsText = strip_php_comments($settingsText);
1435
1436
		// Fix up whitespace to make comparison easier.
1437
		foreach ($placeholders as $placeholder)
1438
		{
1439
			$bare_settingsText = str_replace(array($placeholder . "\n\n", $placeholder), $placeholder . "\n", $bare_settingsText);
1440
		}
1441
		$bare_settingsText = preg_replace('/\h+$/m', '', rtrim($bare_settingsText));
1442
1443
		/*
1444
		 * Divide the existing content into sections.
1445
		 * The idea here is to make sure we don't mess with the relative position
1446
		 * of any code blocks in the file, since that could break things. Within
1447
		 * each section, however, we'll reorganize the content to match the
1448
		 * default layout as closely as we can.
1449
		 */
1450
		$sections = array(array());
1451
		$section_num = 0;
1452
		$trimmed_placeholders = array_filter(array_map('trim', $placeholders));
1453
		$newsection_placeholders = array();
1454
		$all_custom_content = '';
1455
		foreach ($substitutions as $var => $substitution)
1456
		{
1457
			if (is_int($var) && ($var === -2 || $var > 0) && isset($trimmed_placeholders[$var]) && strpos($bare_settingsText, $trimmed_placeholders[$var]) !== false)
1458
				$newsection_placeholders[$var] = $trimmed_placeholders[$var];
1459
		}
1460
		foreach (preg_split('~(?<=' . implode('|', $trimmed_placeholders) . ')|(?=' . implode('|', $trimmed_placeholders) . ')~', $bare_settingsText) as $part)
1461
		{
1462
			$part = trim($part);
1463
1464
			if (empty($part))
1465
				continue;
1466
1467
			// Build a list of placeholders for this section.
1468
			if (in_array($part, $trimmed_placeholders) && !in_array($part, $newsection_placeholders))
1469
			{
1470
				$sections[$section_num][] = $part;
1471
			}
1472
			// Custom content and newsection_placeholders get their own sections.
1473
			else
1474
			{
1475
				if (!empty($sections[$section_num]))
1476
					++$section_num;
1477
1478
				$sections[$section_num][] = $part;
1479
1480
				++$section_num;
1481
1482
				if (!in_array($part, $trimmed_placeholders))
1483
					$all_custom_content .= "\n" . $part;
1484
			}
1485
		}
1486
1487
		// And now, rebuild the content!
1488
		$new_settingsText = '';
1489
		$done_defs = array();
1490
		$sectionkeys = array_keys($sections);
1491
		foreach ($sections as $sectionkey => $section)
1492
		{
1493
			// Custom content needs to be preserved.
1494
			if (count($section) === 1 && !in_array($section[0], $trimmed_placeholders))
1495
			{
1496
				$prev_section_end = $sectionkey < 1 ? 0 : strpos($settingsText, end($sections[$sectionkey - 1])) + strlen(end($sections[$sectionkey - 1]));
1497
				$next_section_start = $sectionkey == end($sectionkeys) ? strlen($settingsText) : strpos($settingsText, $sections[$sectionkey + 1][0]);
1498
1499
				$new_settingsText .= "\n" . substr($settingsText, $prev_section_end, $next_section_start - $prev_section_end) . "\n";
1500
			}
1501
			// Put the placeholders in this section into canonical order.
1502
			else
1503
			{
1504
				$section_parts = array_flip($section);
1505
				$pathcode_reached = false;
1506
				foreach ($settings_defs as $var => $setting_def)
1507
				{
1508
					if ($var === $pathcode_var)
1509
						$pathcode_reached = true;
1510
1511
					// Already did this setting, so move on to the next.
1512
					if (in_array($var, $done_defs))
1513
						continue;
1514
1515
					// Stop when we hit a setting definition that will start a later section.
1516
					if (isset($newsection_placeholders[$var]) && count($section) !== 1)
1517
						break;
1518
1519
					// Stop when everything in this section is done, unless it's the last.
1520
					// This helps maintain the relative position of any custom content.
1521
					if (empty($section_parts) && $sectionkey < (count($sections) - 1))
1522
						break;
1523
1524
					$p = trim($substitutions[$var]['placeholder']);
1525
1526
					// Can't do anything with an empty placeholder.
1527
					if ($p === '')
1528
						continue;
1529
1530
					// Does this need to be inserted before the path correction code?
1531
					if (strpos($new_settingsText, trim($substitutions[$pathcode_var]['placeholder'])) !== false && in_array($var, $force_before_pathcode))
1532
					{
1533
						$new_settingsText = strtr($new_settingsText, array($substitutions[$pathcode_var]['placeholder'] => $p . "\n" . $substitutions[$pathcode_var]['placeholder']));
1534
1535
						$bare_settingsText .= "\n" . $substitutions[$var]['placeholder'];
1536
						$done_defs[] = $var;
1537
						unset($section_parts[trim($substitutions[$var]['placeholder'])]);
1538
					}
1539
1540
					// If it's in this section, add it to the new text now.
1541
					elseif (in_array($p, $section))
1542
					{
1543
						$new_settingsText .= "\n" . $substitutions[$var]['placeholder'];
1544
						$done_defs[] = $var;
1545
						unset($section_parts[trim($substitutions[$var]['placeholder'])]);
1546
					}
1547
1548
					// Perhaps it is safe to reposition it anyway.
1549
					elseif (is_string($var) && strpos($new_settingsText, $p) === false && strpos($all_custom_content, '$' . $var) === false)
1550
					{
1551
						$new_settingsText .= "\n" . $substitutions[$var]['placeholder'];
1552
						$done_defs[] = $var;
1553
						unset($section_parts[trim($substitutions[$var]['placeholder'])]);
1554
					}
1555
1556
					// If this setting is missing entirely, fix it.
1557
					elseif (strpos($bare_settingsText, $p) === false)
1558
					{
1559
						// Special case if the path code is missing. Put it near the end,
1560
						// and also anything else that is missing that normally follows it.
1561
						if (!isset($newsection_placeholders[$pathcode_var]) && $pathcode_reached === true && $sectionkey < (count($sections) - 1))
1562
							break;
1563
1564
						$new_settingsText .= "\n" . $substitutions[$var]['placeholder'];
1565
						$bare_settingsText .= "\n" . $substitutions[$var]['placeholder'];
1566
						$done_defs[] = $var;
1567
						unset($section_parts[trim($substitutions[$var]['placeholder'])]);
1568
					}
1569
				}
1570
			}
1571
		}
1572
		$settingsText = $new_settingsText;
1573
1574
		// Restore the leading and trailing placeholders as necessary.
1575
		foreach (array(-1, -2) as $var)
1576
		{
1577
			if (!empty($substitutions[$var]['placeholder']) && strpos($settingsText, $substitutions[$var]['placeholder']) === false);
1578
			{
1579
				$settingsText = ($var == -1 ? $substitutions[$var]['placeholder'] : '') . $settingsText . ($var == -2 ? $substitutions[$var]['placeholder'] : '');
1580
			}
1581
		}
1582
	}
1583
	// Even if not rebuilding, there are a few variables that may need to be moved around.
1584
	else
1585
	{
1586
		$pathcode_pos = strpos($settingsText, $substitutions[$pathcode_var]['placeholder']);
1587
1588
		if ($pathcode_pos !== false)
1589
		{
1590
			foreach ($force_before_pathcode as $var)
1591
			{
1592
				if (!empty($substitutions[$var]['placeholder']) && strpos($settingsText, $substitutions[$var]['placeholder']) > $pathcode_pos)
1593
				{
1594
					$settingsText = strtr($settingsText, array(
1595
						$substitutions[$var]['placeholder'] => '',
1596
						$substitutions[$pathcode_var]['placeholder'] => $substitutions[$var]['placeholder'] . "\n" . $substitutions[$pathcode_var]['placeholder'],
1597
					));
1598
				}
1599
			}
1600
		}
1601
	}
1602
1603
	/* 3.c: Replace the placeholders with the final values */
1604
1605
	// Where possible, perform simple substitutions.
1606
	$settingsText = strtr($settingsText, $simple_replacements);
1607
1608
	// Deal with any complicated ones.
1609
	if (!empty($replace_patterns))
1610
		$settingsText = preg_replace($replace_patterns, $replace_strings, $settingsText);
1611
1612
	// Make absolutely sure that the path correction code is included.
1613
	if (strpos($settingsText, $substitutions[$pathcode_var]['replacement']) === false)
1614
		$settingsText = preg_replace('~(?=\n#+ Error.Catching #+)~', "\n" . $substitutions[$pathcode_var]['replacement'] . "\n", $settingsText);
1615
1616
	// If we did not rebuild, do just enough to make sure the thing is viable.
1617
	if (empty($rebuild))
1618
	{
1619
		// We need to refresh $bare_settingsText again, and remove the code blocks from it.
1620
		$bare_settingsText = $settingsText;
1621
		foreach ($substitutions as $var => $substitution)
1622
		{
1623
			if (!is_int($var))
1624
				break;
1625
1626
			if (isset($substitution['replacement']))
1627
				$bare_settingsText = str_replace($substitution['replacement'], '', $bare_settingsText);
1628
		}
1629
		$bare_settingsText = strip_php_comments($bare_settingsText);
1630
1631
		// Now insert any defined settings that are missing.
1632
		$pathcode_reached = false;
1633
		foreach ($settings_defs as $var => $setting_def)
1634
		{
1635
			if ($var === $pathcode_var)
1636
				$pathcode_reached = true;
1637
1638
			if (is_int($var))
1639
				continue;
1640
1641
			// Do nothing if it is already in there.
1642
			if (preg_match($substitutions[$var]['search_pattern'], $bare_settingsText))
1643
				continue;
1644
1645
			// Insert it either before or after the path correction code, whichever is appropriate.
1646
			if (!$pathcode_reached || in_array($var, $force_before_pathcode))
1647
			{
1648
				$settingsText = preg_replace($substitutions[$pathcode_var]['search_pattern'], $substitutions[$var]['replacement'] . "\n$0", $settingsText);
1649
			}
1650
			else
1651
			{
1652
				$settingsText = preg_replace($substitutions[$pathcode_var]['search_pattern'], "$0\n" . $substitutions[$var]['replacement'], $settingsText);
1653
			}
1654
		}
1655
	}
1656
1657
	// If we have any brand new settings to add, do so.
1658
	foreach ($new_settings_vars as $var => $val)
1659
	{
1660
		if (isset($substitutions[$var]) && !preg_match($substitutions[$var]['search_pattern'], $settingsText))
1661
		{
1662
			if (!isset($settings_defs[$var]) && strpos($settingsText, '# Custom Settings #') === false)
1663
				$settingsText = preg_replace('~(?=\n#+ Error.Catching #+)~', "\n\n######### Custom Settings #########\n", $settingsText);
1664
1665
			$settingsText = preg_replace('~(?=\n#+ Error.Catching #+)~', $substitutions[$var]['replacement'] . "\n", $settingsText);
1666
		}
1667
	}
1668
1669
	// This is just cosmetic. Get rid of extra lines of whitespace.
1670
	$settingsText = preg_replace('~\n\s*\n~', "\n\n", $settingsText);
1671
1672
	/**************************************
1673
	 * PART 4: Check syntax before saving *
1674
	 **************************************/
1675
1676
	$temp_sfile = tempnam(sm_temp_dir(), md5($prefix . 'Settings.php'));
1677
	file_put_contents($temp_sfile, $settingsText);
1678
1679
	$result = get_current_settings(filemtime($temp_sfile), $temp_sfile);
1680
1681
	unlink($temp_sfile);
1682
1683
	// If the syntax is borked, try rebuilding to see if that fixes it.
1684
	if ($result === false)
1685
		return empty($rebuild) ? updateSettingsFile($config_vars, $keep_quotes, true) : false;
1686
1687
	/******************************************
1688
	 * PART 5: Write updated settings to file *
1689
	 ******************************************/
1690
1691
	$success = safe_file_write($settingsFile, $settingsText, dirname($settingsFile) . '/Settings_bak.php', $last_settings_change);
1692
1693
	// Remember this in case updateSettingsFile is called twice.
1694
	$mtime = filemtime($settingsFile);
1695
1696
	return $success;
1697
}
1698
1699
/**
1700
 * Retrieves a copy of the current values of all settings defined in Settings.php.
1701
 *
1702
 * Importantly, it does this without affecting our actual global variables at all,
1703
 * and it performs safety checks before acting. The result is an array of the
1704
 * values as recorded in the settings file.
1705
 *
1706
 * @param int $mtime Timestamp of last known good configuration. Defaults to time SMF started.
1707
 * @param string $settingsFile The settings file. Defaults to SMF's standard Settings.php.
1708
 * @return array An array of name/value pairs for all the settings in the file.
1709
 */
1710
function get_current_settings($mtime = null, $settingsFile = null)
1711
{
1712
	$mtime = is_null($mtime) ? (defined('TIME_START') ? TIME_START : $_SERVER['REQUEST_TIME']) : (int) $mtime;
1713
1714
	if (!is_file($settingsFile))
1715
	{
1716
		foreach (get_included_files() as $settingsFile)
1717
			if (basename($settingsFile) === 'Settings.php')
1718
				break;
1719
1720
		if (basename($settingsFile) !== 'Settings.php')
1721
			return false;
1722
	}
1723
1724
	// If the file has been changed since the last known good configuration, bail out.
1725
	clearstatcache();
1726
	if (filemtime($settingsFile) > $mtime)
1727
		return false;
1728
1729
	// Strip out opening and closing PHP tags.
1730
	$settingsText = trim(file_get_contents($settingsFile));
1731
	if (substr($settingsText, 0, 5) == '<' . '?php')
1732
		$settingsText = substr($settingsText, 5);
1733
	if (substr($settingsText, -2) == '?' . '>')
1734
		$settingsText = substr($settingsText, 0, -2);
1735
1736
	// Since we're using eval, we need to manually replace these with strings.
1737
	$settingsText = strtr($settingsText, array(
1738
		'__FILE__' => var_export($settingsFile, true),
1739
		'__DIR__' => var_export(dirname($settingsFile), true),
1740
	));
1741
1742
	// Prevents warnings about constants that are already defined.
1743
	$settingsText = preg_replace_callback(
1744
		'~\bdefine\s*\(\s*(["\'])(\w+)\1~',
1745
		function ($matches)
1746
		{
1747
			return 'define(\'' . md5(mt_rand()) . '\'';
1748
		},
1749
		$settingsText
1750
	);
1751
1752
	// Handle eval errors gracefully in both PHP 5 and PHP 7
1753
	try
1754
	{
1755
		if($settingsText !== '' && @eval($settingsText) === false)
1756
			throw new ErrorException('eval error');
1757
1758
		unset($mtime, $settingsFile, $settingsText);
1759
		$defined_vars = get_defined_vars();
1760
	}
1761
	catch (Throwable $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1762
	catch (ErrorException $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1763
	if (isset($e))
1764
		return false;
1765
1766
	return $defined_vars;
1767
}
1768
1769
/**
1770
 * Writes data to a file, optionally making a backup, while avoiding race conditions.
1771
 *
1772
 * @param string $file The filepath of the file where the data should be written.
1773
 * @param string $data The data to be written to $file.
1774
 * @param string $backup_file The filepath where the backup should be saved. Default null.
1775
 * @param int $mtime If modification time of $file is more recent than this Unix timestamp, the write operation will abort. Defaults to time that the script started execution.
1776
 * @param bool $append If true, the data will be appended instead of overwriting the existing content of the file. Default false.
1777
 * @return bool Whether the write operation succeeded or not.
1778
 */
1779
function safe_file_write($file, $data, $backup_file = null, $mtime = null, $append = false)
1780
{
1781
	global $cachedir;
1782
1783
	// Sanity checks.
1784
	if (!file_exists($file) && !is_dir(dirname($file)))
1785
		return false;
1786
1787
	if (!is_int($mtime))
1788
		$mtime = $_SERVER['REQUEST_TIME'];
1789
1790
	$temp_dir = sm_temp_dir();
1791
1792
	// Our temp files.
1793
	$temp_sfile = tempnam($temp_dir, pathinfo($file, PATHINFO_FILENAME) . '.');
1794
1795
	if (!empty($backup_file))
1796
		$temp_bfile = tempnam($temp_dir, pathinfo($backup_file, PATHINFO_FILENAME) . '.');
1797
1798
	// We need write permissions.
1799
	$failed = false;
1800
	foreach (array($file, $backup_file) as $sf)
1801
	{
1802
		if (empty($sf))
1803
			continue;
1804
1805
		if (!file_exists($sf))
1806
			touch($sf);
1807
		elseif (!is_file($sf))
1808
			$failed = true;
1809
1810
		if (!$failed)
1811
			$failed = !smf_chmod($sf);
1812
	}
1813
1814
	// Now let's see if writing to a temp file succeeds.
1815
	if (!$failed && file_put_contents($temp_sfile, $data, LOCK_EX) !== strlen($data))
1816
		$failed = true;
1817
1818
	// Tests passed, so it's time to do the job.
1819
	if (!$failed)
1820
	{
1821
		// Back up the backup, just in case.
1822
		if (file_exists($backup_file))
1823
			$temp_bfile_saved = @copy($backup_file, $temp_bfile);
1824
1825
		// Make sure no one changed the file while we weren't looking.
1826
		clearstatcache();
1827
		if (filemtime($file) <= $mtime)
1828
		{
1829
			// Attempt to open the file.
1830
			$sfhandle = @fopen($file, 'c');
1831
1832
			// Let's do this thing!
1833
			if ($sfhandle !== false)
1834
			{
1835
				// Immediately get a lock.
1836
				flock($sfhandle, LOCK_EX);
1837
1838
				// Make sure the backup works before we do anything more.
1839
				$temp_sfile_saved = @copy($file, $temp_sfile);
1840
1841
				// Now write our data to the file.
1842
				if ($temp_sfile_saved)
1843
				{
1844
					if (empty($append))
1845
					{
1846
						ftruncate($sfhandle, 0);
1847
						rewind($sfhandle);
1848
					}
1849
1850
					$failed = fwrite($sfhandle, $data) !== strlen($data);
1851
				}
1852
				else
1853
					$failed = true;
1854
1855
				// If writing failed, put everything back the way it was.
1856
				if ($failed)
1857
				{
1858
					if (!empty($temp_sfile_saved))
1859
						@rename($temp_sfile, $file);
1860
1861
					if (!empty($temp_bfile_saved))
1862
						@rename($temp_bfile, $backup_file);
1863
				}
1864
				// It worked, so make our temp backup the new permanent backup.
1865
				elseif (!empty($backup_file))
1866
					@rename($temp_sfile, $backup_file);
1867
1868
				// And we're done.
1869
				flock($sfhandle, LOCK_UN);
1870
				fclose($sfhandle);
1871
			}
1872
		}
1873
	}
1874
1875
	// We're done with these.
1876
	@unlink($temp_sfile);
1877
	@unlink($temp_bfile);
1878
1879
	if ($failed)
1880
		return false;
1881
1882
	// Even though on normal installations the filemtime should invalidate any cached version
1883
	// it seems that there are times it might not. So let's MAKE it dump the cache.
1884
	if (function_exists('opcache_invalidate'))
1885
		opcache_invalidate($file, true);
1886
1887
	return true;
1888
}
1889
1890
/**
1891
 * A wrapper around var_export whose output matches SMF coding conventions.
1892
 *
1893
 * @todo Add special handling for objects?
1894
 *
1895
 * @param mixed $var The variable to export
1896
 * @return mixed A PHP-parseable representation of the variable's value
1897
 */
1898
function smf_var_export($var)
1899
{
1900
	/*
1901
	 * Old versions of updateSettingsFile couldn't handle multi-line values.
1902
	 * Even though technically we can now, we'll keep arrays on one line for
1903
	 * the sake of backwards compatibility.
1904
	 */
1905
	if (is_array($var))
1906
	{
1907
		$return = array();
1908
1909
		foreach ($var as $key => $value)
1910
			$return[] = var_export($key, true) . ' => ' . smf_var_export($value);
1911
1912
		return 'array(' . implode(', ', $return) . ')';
1913
	}
1914
1915
	// For the same reason, replace literal returns and newlines with "\r" and "\n"
1916
	elseif (is_string($var) && (strpos($var, "\n") !== false || strpos($var, "\r") !== false))
1917
	{
1918
		return strtr(preg_replace_callback('/[\r\n]+/', function($m) {
1919
			return '\' . "' . strtr($m[0], array("\r" => '\r', "\n" => '\n')) . '" . \'';
1920
		}, $var), array("'' . " => '', " . ''" => ''));
1921
	}
1922
1923
	// We typically use lowercase true/false/null.
1924
	elseif (in_array(gettype($var), array('boolean', 'NULL')))
1925
		return strtolower(var_export($var, true));
1926
1927
	// Nothing special.
1928
	else
1929
		return var_export($var, true);
1930
};
1931
1932
/**
1933
 * Deletes all PHP comments from a string.
1934
 * Useful when analyzing input from file_get_contents, etc.
1935
 *
1936
 * If the code contains any strings in nowdoc or heredoc syntax, they will be
1937
 * converted to single- or double-quote strings.
1938
 *
1939
 * @param string $code_str A string containing PHP code.
1940
 * @param string|null $line_ending One of "\r", "\n", or "\r\n". Leave unset for auto-detect.
1941
 * @return string A string of PHP code with no comments in it.
1942
 */
1943
function strip_php_comments($code_str, $line_ending = null)
1944
{
1945
	// What line ending should we use?
1946
	// Note: this depends on the string, not the host OS, so PHP_EOL isn't what we want.
1947
	if (!in_array($line_ending, array("\r", "\n", "\r\n")))
1948
	{
1949
		if (strpos($code_str, "\r\n") !== false)
1950
			$line_ending = "\r\n";
1951
		elseif (strpos($code_str, "\n") !== false)
1952
			$line_ending = "\n";
1953
		elseif (strpos($code_str, "\r") !== false)
1954
			$line_ending = "\r";
1955
	}
1956
1957
	// Everything is simpler if we convert heredocs to normal strings first.
1958
	if (preg_match_all('/<<<(\'?)(\w+)\'?'. $line_ending . '(.*?)'. $line_ending . '\2;$/m', $code_str, $matches))
1959
	{
1960
		foreach ($matches[0] as $mkey => $heredoc)
1961
		{
1962
			if (!empty($matches[1][$mkey]))
1963
				$heredoc_replacements[$heredoc] = var_export($matches[3][$mkey], true) . ';';
1964
			else
1965
				$heredoc_replacements[$heredoc] = '"' . strtr(substr(var_export($matches[3][$mkey], true), 1, -1), array("\\'" => "'", '"' => '\"')) . '";';
1966
		}
1967
1968
		$code_str = strtr($code_str, $heredoc_replacements);
1969
	}
1970
1971
	// Split before everything that could possibly delimit a comment or a string.
1972
	$parts = preg_split('~(?=#+|/(?=/|\*)|\*/|'. $line_ending . '|(?<!\\\)[\'"])~m', $code_str);
1973
1974
	$in_string = 0;
1975
	$in_comment = 0;
1976
	foreach ($parts as $partkey => $part)
1977
	{
1978
		$one_char = substr($part, 0, 1);
1979
		$two_char = substr($part, 0, 2);
1980
		$to_remove = 0;
1981
1982
		/*
1983
		 * Meaning of $in_string values:
1984
		 *	0: not in a string
1985
		 *	1: in a single quote string
1986
		 *	2: in a double quote string
1987
		 */
1988
		if ($one_char == "'")
1989
		{
1990
			if (!empty($in_comment))
1991
				$in_string = 0;
1992
			elseif (in_array($in_string, array(0, 1)))
1993
				$in_string = ($in_string ^ 1);
1994
		}
1995
		elseif ($one_char == '"')
1996
		{
1997
			if (!empty($in_comment))
1998
				$in_string = 0;
1999
			elseif (in_array($in_string, array(0, 2)))
2000
				$in_string = ($in_string ^ 2);
2001
		}
2002
2003
		/*
2004
		 * Meaning of $in_comment values:
2005
		 * 	0: not in a comment
2006
		 *	1: in a single line comment
2007
		 *	2: in a multi-line comment
2008
		 */
2009
		elseif ($one_char == '#' || $two_char == '//')
2010
		{
2011
			$in_comment = !empty($in_string) ? 0 : (empty($in_comment) ? 1 : $in_comment);
2012
		}
2013
		elseif (($line_ending === "\r\n" && $two_char === $line_ending) || $one_char == $line_ending)
2014
		{
2015
			if ($in_comment == 1)
2016
			{
2017
				$in_comment = 0;
2018
2019
				// If we've removed everything on this line, take out the line ending, too.
2020
				if ($parts[$partkey - 1] === $line_ending)
2021
					$to_remove = strlen($line_ending);
2022
			}
2023
		}
2024
		elseif ($two_char == '/*')
2025
		{
2026
			$in_comment = !empty($in_string) ? 0 : (empty($in_comment) ? 2 : $in_comment);
2027
		}
2028
		elseif ($two_char == '*/')
2029
		{
2030
			if ($in_comment == 2)
2031
			{
2032
				$in_comment = 0;
2033
2034
				// Delete the comment closing.
2035
				$to_remove = 2;
2036
			}
2037
		}
2038
2039
		if (empty($in_comment))
2040
			$parts[$partkey] = strlen($part) > $to_remove ? substr($part, $to_remove) : '';
2041
		else
2042
			$parts[$partkey] = '';
2043
	}
2044
2045
	return implode('', $parts);
2046
}
2047
2048
/**
2049
 * Saves the time of the last db error for the error log
2050
 * - Done separately from updateSettingsFile to avoid race conditions
2051
 *   which can occur during a db error
2052
 * - If it fails Settings.php will assume 0
2053
 *
2054
 * @param int $time The timestamp of the last DB error
2055
 */
2056
function updateDbLastError($time)
2057
{
2058
	global $boarddir, $cachedir;
2059
2060
	// Write out the db_last_error file with the error timestamp
2061
	if (!empty($cachedir) && is_writable($cachedir))
2062
		$errorfile = $cachedir . '/db_last_error.php';
2063
2064
	elseif (file_exists(dirname(__DIR__) . '/cache'))
2065
		$errorfile = dirname(__DIR__) . '/cache/db_last_error.php';
2066
2067
	else
2068
		$errorfile = dirname(__DIR__) . '/db_last_error.php';
2069
2070
	file_put_contents($errorfile, '<' . '?' . "php\n" . '$db_last_error = ' . $time . ';' . "\n" . '?' . '>', LOCK_EX);
2071
2072
	@touch($boarddir . '/' . 'Settings.php');
2073
}
2074
2075
/**
2076
 * Saves the admin's current preferences to the database.
2077
 */
2078
function updateAdminPreferences()
2079
{
2080
	global $options, $context, $smcFunc, $settings, $user_info;
2081
2082
	// This must exist!
2083
	if (!isset($context['admin_preferences']))
2084
		return false;
2085
2086
	// This is what we'll be saving.
2087
	$options['admin_preferences'] = $smcFunc['json_encode']($context['admin_preferences']);
2088
2089
	// Just check we haven't ended up with something theme exclusive somehow.
2090
	$smcFunc['db_query']('', '
2091
		DELETE FROM {db_prefix}themes
2092
		WHERE id_theme != {int:default_theme}
2093
			AND variable = {string:admin_preferences}',
2094
		array(
2095
			'default_theme' => 1,
2096
			'admin_preferences' => 'admin_preferences',
2097
		)
2098
	);
2099
2100
	// Update the themes table.
2101
	$smcFunc['db_insert']('replace',
2102
		'{db_prefix}themes',
2103
		array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'),
2104
		array($user_info['id'], 1, 'admin_preferences', $options['admin_preferences']),
2105
		array('id_member', 'id_theme', 'variable')
2106
	);
2107
2108
	// Make sure we invalidate any cache.
2109
	cache_put_data('theme_settings-' . $settings['theme_id'] . ':' . $user_info['id'], null, 0);
2110
}
2111
2112
/**
2113
 * Send all the administrators a lovely email.
2114
 * - loads all users who are admins or have the admin forum permission.
2115
 * - uses the email template and replacements passed in the parameters.
2116
 * - sends them an email.
2117
 *
2118
 * @param string $template Which email template to use
2119
 * @param array $replacements An array of items to replace the variables in the template
2120
 * @param array $additional_recipients An array of arrays of info for additional recipients. Should have 'id', 'email' and 'name' for each.
2121
 */
2122
function emailAdmins($template, $replacements = array(), $additional_recipients = array())
2123
{
2124
	global $smcFunc, $sourcedir, $language, $modSettings;
2125
2126
	// We certainly want this.
2127
	require_once($sourcedir . '/Subs-Post.php');
2128
2129
	// Load all members which are effectively admins.
2130
	require_once($sourcedir . '/Subs-Members.php');
2131
	$members = membersAllowedTo('admin_forum');
2132
2133
	// Load their alert preferences
2134
	require_once($sourcedir . '/Subs-Notify.php');
2135
	$prefs = getNotifyPrefs($members, 'announcements', true);
2136
2137
	$request = $smcFunc['db_query']('', '
2138
		SELECT id_member, member_name, real_name, lngfile, email_address
2139
		FROM {db_prefix}members
2140
		WHERE id_member IN({array_int:members})',
2141
		array(
2142
			'members' => $members,
2143
		)
2144
	);
2145
	$emails_sent = array();
2146
	while ($row = $smcFunc['db_fetch_assoc']($request))
2147
	{
2148
		if (empty($prefs[$row['id_member']]['announcements']))
2149
			continue;
2150
2151
		// Stick their particulars in the replacement data.
2152
		$replacements['IDMEMBER'] = $row['id_member'];
2153
		$replacements['REALNAME'] = $row['member_name'];
2154
		$replacements['USERNAME'] = $row['real_name'];
2155
2156
		// Load the data from the template.
2157
		$emaildata = loadEmailTemplate($template, $replacements, empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']);
2158
2159
		// Then send the actual email.
2160
		sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, $template, $emaildata['is_html'], 1);
2161
2162
		// Track who we emailed so we don't do it twice.
2163
		$emails_sent[] = $row['email_address'];
2164
	}
2165
	$smcFunc['db_free_result']($request);
2166
2167
	// Any additional users we must email this to?
2168
	if (!empty($additional_recipients))
2169
		foreach ($additional_recipients as $recipient)
2170
		{
2171
			if (in_array($recipient['email'], $emails_sent))
2172
				continue;
2173
2174
			$replacements['IDMEMBER'] = $recipient['id'];
2175
			$replacements['REALNAME'] = $recipient['name'];
2176
			$replacements['USERNAME'] = $recipient['name'];
2177
2178
			// Load the template again.
2179
			$emaildata = loadEmailTemplate($template, $replacements, empty($recipient['lang']) || empty($modSettings['userLanguage']) ? $language : $recipient['lang']);
2180
2181
			// Send off the email.
2182
			sendmail($recipient['email'], $emaildata['subject'], $emaildata['body'], null, $template, $emaildata['is_html'], 1);
2183
		}
2184
}
2185
2186
/**
2187
 * Locates the most appropriate temp directory.
2188
 *
2189
 * Systems using `open_basedir` restrictions may receive errors with
2190
 * `sys_get_temp_dir()` due to misconfigurations on servers. Other
2191
 * cases sys_temp_dir may not be set to a safe value. Additionally
2192
 * `sys_get_temp_dir` may use a readonly directory. This attempts to
2193
 * find a working temp directory that is accessible under the
2194
 * restrictions and is writable to the web service account.
2195
 *
2196
 * Directories checked against `open_basedir`:
2197
 *
2198
 * - `sys_get_temp_dir()`
2199
 * - `upload_tmp_dir`
2200
 * - `session.save_path`
2201
 * - `cachedir`
2202
 *
2203
 * @return string
2204
*/
2205
function sm_temp_dir()
2206
{
2207
	static $temp_dir = null;
2208
2209
	// Already did this.
2210
	if (!empty($temp_dir))
2211
		return $temp_dir;
2212
2213
	// Temp Directory options order.
2214
	$temp_dir_options = array(
2215
		0 => 'sys_get_temp_dir',
2216
		1 => 'upload_tmp_dir',
2217
		2 => 'session.save_path',
2218
		3 => 'cachedir'
2219
	);
2220
2221
	// Determine if we should detect a restriction and what restrictions that may be.
2222
	$open_base_dir = ini_get('open_basedir');
2223
	$restriction = !empty($open_base_dir) ? explode(':', $open_base_dir) : false;
2224
2225
	// Prevent any errors as we search.
2226
	$old_error_reporting = error_reporting(0);
2227
2228
	// Search for a working temp directory.
2229
	foreach ($temp_dir_options as $id_temp => $temp_option)
2230
	{
2231
		$possible_temp = sm_temp_dir_option($temp_option);
2232
2233
		// Check if we have a restriction preventing this from working.
2234
		if ($restriction)
2235
		{
2236
			foreach ($restriction as $dir)
2237
			{
2238
				if (strpos($possible_temp, $dir) !== false && is_writable($possible_temp))
2239
				{
2240
					$temp_dir = $possible_temp;
2241
					break;
2242
				}
2243
			}
2244
		}
2245
		// No restrictions, but need to check for writable status.
2246
		elseif (is_writable($possible_temp))
2247
		{
2248
			$temp_dir = $possible_temp;
2249
			break;
2250
		}
2251
	}
2252
2253
	// Fall back to sys_get_temp_dir even though it won't work, so we have something.
2254
	if (empty($temp_dir))
2255
		$temp_dir = sm_temp_dir_option('default');
2256
2257
	// Fix the path.
2258
	$temp_dir = substr($temp_dir, -1) === '/' ? $temp_dir : $temp_dir . '/';
2259
2260
	// Put things back.
2261
	error_reporting($old_error_reporting);
2262
2263
	return $temp_dir;
2264
}
2265
2266
/**
2267
 * Internal function for sm_temp_dir.
2268
 *
2269
 * @param string $option Which temp_dir option to use
2270
 * @return string The path to the temp directory
2271
 */
2272
function sm_temp_dir_option($option = 'sys_get_temp_dir')
2273
{
2274
	global $cachedir;
2275
2276
	if ($option === 'cachedir')
2277
		return rtrim($cachedir, '/');
2278
2279
	elseif ($option === 'session.save_path')
2280
		return rtrim(ini_get('session.save_path'), '/');
2281
2282
	elseif ($option === 'upload_tmp_dir')
2283
		return rtrim(ini_get('upload_tmp_dir'), '/');
2284
2285
	// This is the default option.
2286
	else
2287
		return sys_get_temp_dir();
2288
}
2289
2290
?>