Passed
Push — development ( 8f3e46...8433eb )
by Spuds
01:17 queued 31s
created

File::cleanInts()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
ccs 2
cts 2
cp 1
crap 3
1
<?php
2
3
/**
4
 * This class handles display, edit, save, of forum settings (Settings.php).
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\SettingsForm\SettingsFormAdapter;
18
19
use ElkArte\Helper\FileFunctions;
20
use ElkArte\Helper\Util;
21
22
/**
23
 * Class File
24
 *
25
 * @package ElkArte\SettingsForm\SettingsFormAdapter
26
 */
27
class File extends Db
28
{
29
	/** @var int */
30
	private $last_settings_change;
31
32
	/** @var array */
33
	private $settingsArray = [];
34
35
	/** @var array */
36
	private $new_settings = [];
37
38
	/** @var FileFunctions */
39
	private $fileFunc;
40
41
	/**
42
	 * Helper method, it sets up the context for the settings which will be saved
43
	 * to the settings.php file
44
	 *
45
	 * What it does:
46
	 *
47
	 * - The basic usage of the six numbered key fields are
48
	 * - array(0 ,1, 2, 3, 4, 5
49
	 *    0 variable name - the name of the saved variable
50
	 *    1 label - the text to show on the settings page
51
	 *    2 saveto - file or db, where to save the variable name - value pair
52
	 *    3 type - type of data to display int, float, text, check, select, password
53
	 *    4 size - false or field size, if type is select, this needs to be an array of select options
54
	 *    5 help - '' or helptxt variable name
55
	 *  )
56
	 * - The following named keys are also permitted
57
	 *    'disabled' =>
58
	 *    'postinput' =>
59
	 *    'preinput' =>
60
	 *    'subtext' =>
61
	 *    'force_div_id' =>
62
	 *    'skip_verify_pass' =>
63
	 */
64
	public function prepare()
65 4
	{
66
		global $modSettings;
67 4
68
		$defines = [
69
			'boarddir',
70 4
			'sourcedir',
71
			'cachedir',
72
		];
73
74
		$safe_strings = [
75
			'mtitle',
76 4
			'mmessage',
77
			'mbname',
78
		];
79
80
		foreach ($this->configVars as $configVar)
81 4
		{
82
			$new_setting = $configVar;
83 4
84
			if (is_array($configVar) && isset($configVar[1]))
85 4
			{
86
				$varname = $configVar[0];
87 4
				global ${$varname};
88 4
89
				// Rewrite the definition a bit.
90
				$new_setting[0] = $configVar[3];
91 4
				$new_setting[1] = $configVar[0];
92 4
				$new_setting['text_label'] = $configVar[1];
93 4
94
				if (isset($configVar[4]))
95 4
				{
96
					$new_setting[2] = $configVar[4];
97 4
				}
98
99
				if (isset($configVar[5]))
100 4
				{
101
					$new_setting['helptext'] = $configVar[5];
102 4
				}
103
104
				// Special value needed from the settings file?
105
				if ($configVar[2] === 'file')
106 4
				{
107
					$value = in_array($varname, $defines, true) ? constant(strtoupper($varname)) : ${$varname};
108 4
109
					if (in_array($varname, $safe_strings, true))
110 4
					{
111
						$new_setting['mask'] = 'nohtml';
112 4
						$value = strtr($value, [Util::htmlspecialchars('<br />') => "\n"]);
113 4
					}
114
115 4
					$modSettings[$configVar[0]] = $value;
116
				}
117
			}
118 4
119
			$this->new_settings[] = $new_setting;
120 4
		}
121 4
122 4
		$this->setConfigVars($this->new_settings);
123
		parent::prepare();
124
	}
125
126
	/**
127
	 * Update the Settings.php file.
128
	 *
129
	 * Typically, this method is used from admin screens, just like this entire class.
130
	 * They're also available for addons and integrations.
131
	 *
132
	 * What it does:
133
	 *
134
	 * - updates the Settings.php file with the changes supplied in new_settings.
135
	 * - expects new_settings to be an associative array, with the keys as the
136
	 *   variable names in Settings.php, and the values the variable values.
137
	 * - does not escape or quote values.
138
	 * - preserves case, formatting, and additional options in file.
139
	 * - writes nothing if the resulting file would be less than 10 lines
140
	 *   in length (sanity check for read lock.)
141
	 * - check for changes to db_last_error and passes those off to a separate handler
142
	 * - attempts to create a backup file and will use it should the writing of the
143 2
	 *   new settings file fail
144
	 */
145 2
	public function save()
146
	{
147
		$this->fileFunc = FileFunctions::instance();
148 2
149
		$this->_cleanSettings();
150
151 2
		// When was Settings.php last changed?
152
		$this->last_settings_change = filemtime(BOARDDIR . '/Settings.php');
153
154 2
		// Load the settings file.
155
		$settingsFile = trim(file_get_contents(BOARDDIR . '/Settings.php'));
156 2
157
		// Break it up based on \r or \n, and then clean out extra characters.
158
		if (strpos($settingsFile, "\n") !== false)
159
		{
160
			$this->settingsArray = explode("\n", $settingsFile);
161
		}
162
		elseif (strpos($settingsFile, "\r") !== false)
163
		{
164
			$this->settingsArray = explode("\r", $settingsFile);
165
		}
166
		else
167 2
		{
168 2
			return;
169 2
		}
170 2
171
		$this->_prepareSettings();
172
		$this->_updateSettingsFile();
173
		$this->_extractDbVars();
174
	}
175 2
176
	/**
177 2
	 * For all known configuration values, ensures they are properly cast / escaped
178 2
	 */
179
	private function _cleanSettings()
180
	{
181
		$this->_fixCookieName();
182 2
		$this->_fixBoardUrl();
183
184
		// Any passwords?
185
		$config_passwords = [
186
			'db_passwd',
187
			'ssi_db_passwd',
188 2
			'cache_password',
189
		];
190
191
		// All the strings to write.
192
		$config_strs = [
193
			'mtitle',
194
			'mmessage',
195
			'language',
196
			'mbname',
197
			'boardurl',
198
			'cookiename',
199
			'webmaster_email',
200
			'db_name',
201
			'db_user',
202
			'db_server',
203
			'db_prefix',
204
			'ssi_db_user',
205
			'cache_accelerator',
206
			'cache_servers',
207 2
			'url_format',
208
			'cache_uid',
209
		];
210
211
		// These need HTML encoded. Be sure they all exist in $config_strs!
212
		$safe_strings = [
213
			'mtitle',
214 2
			'mmessage',
215
			'mbname',
216
		];
217
218
		// All the numeric variables.
219 2
		$config_ints = [
220
			'cache_enable',
221
		];
222
223
		// All the checkboxes.
224
		$config_bools = [
225 2
			'db_persist',
226
			'db_error_send',
227 2
			'maintenance',
228
		];
229 1
230
		// Now sort everything into a big array, and figure out arrays etc.
231
		$this->cleanPasswords($config_passwords);
232
233
		// Escape and update Setting strings
234 2
		$this->cleanStrings($config_strs, $safe_strings);
235
236 2
		// Ints are saved as integers
237
		$this->cleanInts($config_ints);
238 2
239
		// Convert checkbox selections to 0 / 1
240 2
		$this->cleanBools($config_bools);
241
	}
242
243
	/**
244 1
	 * Fix the cookie name by removing invalid characters
245
	 */
246
	private function _fixCookieName()
247
	{
248
		// Fix the darn stupid cookiename! (more may not be allowed, but these for sure!)
249
		if (isset($this->configValues['cookiename']))
250 2
		{
251
			$this->configValues['cookiename'] = preg_replace('~[,;\s\.$]+~u', '', $this->configValues['cookiename']);
252 2
		}
253
	}
254 1
255
	/**
256
	 * Fix the forum's URL if necessary so that it is a valid root url
257
	 */
258
	private function _fixBoardUrl()
259 2
	{
260
		if (isset($this->configValues['boardurl']))
261
		{
262 2
			if (substr($this->configValues['boardurl'], -10) === '/index.php')
263
			{
264 1
				$this->configValues['boardurl'] = substr($this->configValues['boardurl'], 0, -10);
265
			}
266
			elseif (substr($this->configValues['boardurl'], -1) === '/')
267 2
			{
268
				$this->configValues['boardurl'] = substr($this->configValues['boardurl'], 0, -1);
269
			}
270
271
			$this->configValues['boardurl'] = addProtocol($this->configValues['boardurl'], ['http://', 'https://', 'file://']);
272 2
		}
273
	}
274
275 2
	/**
276
	 * Clean passwords and add them to the new settings array
277
	 *
278
	 * @param array $config_passwords The array of config passwords to clean
279 2
	 */
280
	public function cleanPasswords(array $config_passwords)
281
	{
282
		foreach ($config_passwords as $configVar)
283
		{
284 2
			// Handle skip_verify_pass.  Only password[0] will exist from the form
285
			$key = $this->_array_key_exists__recursive($this->configVars, $configVar, 0);
286 2
			if ($key !== false
287
				&& !empty($this->configVars[$key]['skip_verify_pass'])
288
				&& $this->configValues[$configVar][0] !== '*#fakepass#*')
289
			{
290
				$this->new_settings[$configVar] = "'" . addcslashes($this->configValues[$configVar][0], '\'\\') . "'";
291
				continue;
292
			}
293
294
			// Validate the _confirm password box exists
295
			if (!isset($this->configValues[$configVar][1]))
296
			{
297
				continue;
298
			}
299 2
300
			// And that it has the same password
301
			if ($this->configValues[$configVar][0] !== $this->configValues[$configVar][1])
302
			{
303
				continue;
304
			}
305
306
			$this->new_settings[$configVar] = "'" . addcslashes($this->configValues[$configVar][0], '\'\\') . "'";
307
		}
308
	}
309 2
310
	/**
311 2
	 * Clean strings in the configuration values by escaping characters and applying safe transformations
312
	 * and add them to the new settings array
313 2
	 *
314
	 * @param array $config_strs The configuration strings to clean
315 1
	 * @param array $safe_strings The safe strings that should receive additional transformations
316
	 */
317
	public function cleanStrings(array $config_strs, array $safe_strings)
318
	{
319 2
		foreach ($config_strs as $configVar)
320
		{
321
			if (isset($this->configValues[$configVar]))
322
			{
323
				if (in_array($configVar, $safe_strings, true))
324
				{
325
					$this->new_settings[$configVar] = "'" . addcslashes(Util::htmlspecialchars(strtr($this->configValues[$configVar], ["\n" => '<br />', "\r" => '']), ENT_QUOTES), '\'\\') . "'";
326
				}
327
				else
328
				{
329 2
					$this->new_settings[$configVar] = "'" . addcslashes($this->configValues[$configVar], '\'\\') . "'";
330
				}
331
			}
332 2
		}
333
	}
334
335
	/**
336
	 * Clean/cast integer values in the configuration array and add them to the new settings array
337
	 *
338 2
	 * @param array $config_ints The array of configuration variables to clean
339
	 *
340 2
	 * @return void
341
	 */
342
	public function cleanInts(array $config_ints): void
343
	{
344 2
		foreach ($config_ints as $configVar)
345
		{
346
			if (isset($this->configValues[$configVar]))
347 2
			{
348
				$this->new_settings[$configVar] = (int) $this->configValues[$configVar];
349 2
			}
350
		}
351
	}
352 2
353
	/**
354
	 * Clean boolean values in the provided config array to be 0 or 1 and add them to the new settings array
355 2
	 *
356
	 * @param array $config_bools The array of boolean keys to clean.
357 2
	 * @return void
358
	 */
359 2
	public function cleanBools(array $config_bools): void
360 2
	{
361
		foreach ($config_bools as $key)
362
		{
363 2
			// Check boxes need to be part of this settings form
364
			if ($this->_array_value_exists__recursive($key, $this->getConfigVars()))
365
			{
366
				$this->new_settings[$key] = (int) !empty($this->configValues[$key]);
367
			}
368 2
		}
369
	}
370
371
	/**
372
	 * Recursively checks if a value exists in an array
373
	 *
374
	 * @param string $needle
375 2
	 * @param array $haystack
376
	 *
377 2
	 * @return bool
378
	 */
379
	private function _array_value_exists__recursive($needle, $haystack)
380
	{
381 2
		foreach ($haystack as $item)
382
		{
383
			if ($item === $needle || (is_array($item) && $this->_array_value_exists__recursive($needle, $item)))
384
			{
385
				return true;
386
			}
387
		}
388
389
		return false;
390
	}
391
392
	/**
393
	 * Recursively search for a value in a multidimensional array and return the key
394
	 *
395
	 * @param array $haystack The array to search in
396
	 * @param mixed $needle The value to search for
397
	 * @param mixed $index The index to compare against (optional)
398
	 * @return string|int|false The key of the found value, false if array search completed without finding value
399
	 */
400 2
	private function _array_key_exists__recursive($haystack, $needle, $index = null)
401
	{
402 2
		$aIt = new \RecursiveArrayIterator($haystack);
403
		$it = new \RecursiveIteratorIterator($aIt);
404
405
		while ($it->valid())
406
		{
407
			if (((isset($index) && $it->key() === $index) || (!isset($index)))
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && $it->key()...->current() === $needle, Probably Intended Meaning: IssetNode && $it->key() ...>current() === $needle)
Loading history...
408
				&& $it->current() === $needle)
409
			{
410 2
				return $aIt->key();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $aIt->key() also could return the type true which is incompatible with the documented return type false|integer|string.
Loading history...
411
			}
412 2
413
			$it->next();
414
		}
415 2
416
		// If the loop completed without finding the value, return false
417
		return false;
418
	}
419
420
	/**
421
	 * Updates / Validates the Settings array for later output.
422
	 *
423
	 * - Updates any values that have been changed.
424
	 * - Key/value pairs that did not exists are added at the end of the array.
425
	 * - Ensures the completed array is valid for later output
426 2
	 */
427
	private function _prepareSettings()
428
	{
429
		// Presumably, the file has to have stuff in it for this function to be called :P.
430
		if (count($this->settingsArray) < 10)
431
		{
432 2
			return;
433
		}
434
435 2
		// remove any /r's that made there way in here
436 2
		foreach ($this->settingsArray as $k => $dummy)
437
		{
438 2
			$this->settingsArray[$k] = strtr($dummy, ["\r" => '']) . "\n";
439 2
		}
440 2
441
		// go line by line and see what's changing
442 2
		for ($i = 0, $n = count($this->settingsArray); $i < $n; $i++)
443
		{
444
			// Don't trim or bother with it if it's not a variable.
445
			if (substr($this->settingsArray[$i], 0, 1) !== '$')
446
			{
447
				continue;
448
			}
449
450
			$this->settingsArray[$i] = trim($this->settingsArray[$i]) . "\n";
451 2
452 2
			// Look through the variables to set....
453
			foreach ($this->new_settings as $var => $val)
454
			{
455 2
				if (strncasecmp($this->settingsArray[$i], '$' . $var, 1 + strlen($var)) == 0)
456 2
				{
457
					$comment = strstr(substr(un_htmlspecialchars($this->settingsArray[$i]), strpos(un_htmlspecialchars($this->settingsArray[$i]), ';')), '#');
458
					$this->settingsArray[$i] = '$' . $var . ' = ' . $val . ';' . ($comment == '' ? '' : "\t\t" . rtrim($comment)) . "\n";
459 2
460 2
					// This one's been 'used', so to speak.
461
					unset($this->new_settings[$var]);
462
				}
463 2
			}
464
465
			// End of the file ... maybe
466
			if (strpos(trim($this->settingsArray[$i]), '?>') === 0)
467
			{
468
				$end = $i;
469
			}
470
		}
471
472
		// This should never happen, but apparently it is happening.
473
		if (empty($end) || $end < 10)
474 2
		{
475
			$end = count($this->settingsArray) - 1;
476
		}
477
478
		// Still more variables to go?  Then lets add them at the end.
479 2
		if (!empty($this->new_settings))
480
		{
481
			if (trim($this->settingsArray[$end]) === '?>')
482
			{
483
				$this->settingsArray[$end++] = '';
484 2
			}
485
			else
486
			{
487 2
				$end++;
488
			}
489
490 2
			// Add in any newly defined vars that were passed
491
			foreach ($this->new_settings as $var => $val)
492 2
			{
493
				$this->settingsArray[$end++] = '$' . $var . ' = ' . $val . ';' . "\n";
494
			}
495
		}
496 2
		else
497
		{
498 2
			$this->settingsArray[$end] = trim($this->settingsArray[$end]);
499
		}
500
	}
501
502
	/**
503
	 * Write out the contents of Settings.php file.
504
	 *
505 2
	 * This function will add the variables passed to it in $this->new_settings,
506
	 * to the Settings.php file.
507
	 */
508
	private function _updateSettingsFile()
509 2
	{
510 2
		global $context;
511
512
		// Sanity error checking: the file needs to be at least 12 lines.
513
		if (count($this->settingsArray) < 12)
514
		{
515
			return;
516
		}
517
518
		// Try to avoid a few pitfalls:
519
		//  - like a possible race condition,
520
		//  - or a failure to write at low diskspace
521
		//
522
		// Check before you act: if cache is enabled, we can do a simple write test
523
		// to validate that we even write things on this filesystem.
524
		if ((!defined('CACHEDIR') || !$this->fileFunc->fileExists(CACHEDIR)) && $this->fileFunc->fileExists(BOARDDIR . '/cache'))
525
		{
526
			$tmp_cache = BOARDDIR . '/cache';
527
		}
528
		else
529
		{
530
			$tmp_cache = CACHEDIR;
531
		}
532
533
		$test_fp = @fopen($tmp_cache . '/settings_update.tmp', 'wb+');
534
		if ($test_fp)
0 ignored issues
show
introduced by
$test_fp is of type false|resource, thus it always evaluated to false.
Loading history...
535
		{
536
			fclose($test_fp);
537
			$written_bytes = file_put_contents($tmp_cache . '/settings_update.tmp', 'test', LOCK_EX);
538
			$this->fileFunc->delete($tmp_cache . '/settings_update.tmp');
539
540
			if ($written_bytes !== 4)
541
			{
542
				// Oops. Low disk space, perhaps. Don't mess with Settings.php then.
543
				// No means no. :P
544
				return;
545
			}
546
		}
547
548
		// Protect me from what I want! :P
549
		clearstatcache();
550
		if (filemtime(BOARDDIR . '/Settings.php') === $this->last_settings_change)
551
		{
552
			// Save the old before we do anything
553
			$settings_backup_fail = !$this->fileFunc->isWritable(BOARDDIR . '/Settings_bak.php') || !@copy(BOARDDIR . '/Settings.php', BOARDDIR . '/Settings_bak.php');
554
			$settings_backup_fail = $settings_backup_fail ?: !$this->fileFunc->fileExists(BOARDDIR . '/Settings_bak.php') || filesize(BOARDDIR . '/Settings_bak.php') === 0;
555
556
			// Write out the new
557
			$write_settings = implode('', $this->settingsArray);
558
			$written_bytes = file_put_contents(BOARDDIR . '/Settings.php', $write_settings, LOCK_EX);
559
560
			// Survey says ...
561
			if (!$settings_backup_fail && $written_bytes !== strlen($write_settings))
562
			{
563
				// Well this is not good at all, lets see if we can save this
564
				$context['settings_message'] = 'settings_error';
565
566
				if ($this->fileFunc->fileExists(BOARDDIR . '/Settings_bak.php'))
567
				{
568
					@copy(BOARDDIR . '/Settings_bak.php', BOARDDIR . '/Settings.php');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for copy(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

568
					/** @scrutinizer ignore-unhandled */ @copy(BOARDDIR . '/Settings_bak.php', BOARDDIR . '/Settings.php');

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
569
				}
570
			}
571
572
			if (extension_loaded('Zend OPcache') && ini_get('opcache.enable') &&
573
				((ini_get('opcache.restrict_api') === '' || stripos(BOARDDIR, (string) ini_get('opcache.restrict_api')) !== 0)))
574
			{
575
				opcache_invalidate(BOARDDIR . '/Settings.php');
576
			}
577
		}
578
	}
579
580
	/**
581
	 * Find and save the new database-based settings, if any
582
	 */
583
	private function _extractDbVars()
584
	{
585
		// Now loop through the remaining (database-based) settings.
586
		$this->configVars = array_map(
587
			static function ($configVar) {
588
				// We just saved the file-based settings, so skip their definitions.
589
				if (!is_array($configVar) || $configVar[2] === 'file')
590
				{
591
					return '';
592
				}
593
594
				// Rewrite the definition a bit.
595
				if ($configVar[2] === 'db')
596
				{
597
					return [$configVar[3], $configVar[0]];
598
				}
599
600
				// This is a regular config var requiring no special treatment.
601
				return $configVar;
602
			}, $this->configVars
603
		);
604
605
		// Save the new database-based settings, if any.
606
		parent::save();
607
	}
608
}
609