Passed
Push — master ( 671ad7...f13b60 )
by Blizzz
15:36 queued 12s
created

Config::setValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Adam Williamson <[email protected]>
6
 * @author Aldo "xoen" Giambelluca <[email protected]>
7
 * @author Bart Visscher <[email protected]>
8
 * @author Brice Maron <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Daniel Kesselberg <[email protected]>
11
 * @author Frank Karlitschek <[email protected]>
12
 * @author Jakob Sack <[email protected]>
13
 * @author Jan-Christoph Borchardt <[email protected]>
14
 * @author Joas Schilling <[email protected]>
15
 * @author John Molakvoæ <[email protected]>
16
 * @author Lukas Reschke <[email protected]>
17
 * @author Michael Gapczynski <[email protected]>
18
 * @author Morris Jobke <[email protected]>
19
 * @author Philipp Schaffrath <[email protected]>
20
 * @author Robin Appelman <[email protected]>
21
 * @author Robin McCorkell <[email protected]>
22
 * @author Roeland Jago Douma <[email protected]>
23
 *
24
 * @license AGPL-3.0
25
 *
26
 * This code is free software: you can redistribute it and/or modify
27
 * it under the terms of the GNU Affero General Public License, version 3,
28
 * as published by the Free Software Foundation.
29
 *
30
 * This program is distributed in the hope that it will be useful,
31
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33
 * GNU Affero General Public License for more details.
34
 *
35
 * You should have received a copy of the GNU Affero General Public License, version 3,
36
 * along with this program. If not, see <http://www.gnu.org/licenses/>
37
 *
38
 */
39
namespace OC;
40
41
use OCP\HintException;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, OC\HintException. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
42
43
/**
44
 * This class is responsible for reading and writing config.php, the very basic
45
 * configuration file of Nextcloud.
46
 */
47
class Config {
48
	public const ENV_PREFIX = 'NC_';
49
50
	/** @var array Associative array ($key => $value) */
51
	protected $cache = [];
52
	/** @var array */
53
	protected $envCache = [];
54
	/** @var string */
55
	protected $configDir;
56
	/** @var string */
57
	protected $configFilePath;
58
	/** @var string */
59
	protected $configFileName;
60
	/** @var bool */
61
	protected $isReadOnly;
62
63
	/**
64
	 * @param string $configDir Path to the config dir, needs to end with '/'
65
	 * @param string $fileName (Optional) Name of the config file. Defaults to config.php
66
	 */
67
	public function __construct($configDir, $fileName = 'config.php') {
68
		$this->configDir = $configDir;
69
		$this->configFilePath = $this->configDir.$fileName;
70
		$this->configFileName = $fileName;
71
		$this->readData();
72
		$this->isReadOnly = $this->getValue('config_is_read_only', false);
73
	}
74
75
	/**
76
	 * Lists all available config keys
77
	 *
78
	 * Please note that it does not return the values.
79
	 *
80
	 * @return array an array of key names
81
	 */
82
	public function getKeys() {
83
		return array_keys($this->cache);
84
	}
85
86
	/**
87
	 * Returns a config value
88
	 *
89
	 * gets its value from an `NC_` prefixed environment variable
90
	 * if it doesn't exist from config.php
91
	 * if this doesn't exist either, it will return the given `$default`
92
	 *
93
	 * @param string $key key
94
	 * @param mixed $default = null default value
95
	 * @return mixed the value or $default
96
	 */
97
	public function getValue($key, $default = null) {
98
		$envKey = self::ENV_PREFIX . $key;
99
		if (isset($this->envCache[$envKey])) {
100
			return $this->envCache[$envKey];
101
		}
102
103
		if (isset($this->cache[$key])) {
104
			return $this->cache[$key];
105
		}
106
107
		return $default;
108
	}
109
110
	/**
111
	 * Sets and deletes values and writes the config.php
112
	 *
113
	 * @param array $configs Associative array with `key => value` pairs
114
	 *                       If value is null, the config key will be deleted
115
	 * @throws HintException
116
	 */
117
	public function setValues(array $configs) {
118
		$needsUpdate = false;
119
		foreach ($configs as $key => $value) {
120
			if ($value !== null) {
121
				$needsUpdate |= $this->set($key, $value);
122
			} else {
123
				$needsUpdate |= $this->delete($key);
124
			}
125
		}
126
127
		if ($needsUpdate) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $needsUpdate of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
128
			// Write changes
129
			$this->writeData();
130
		}
131
	}
132
133
	/**
134
	 * Sets the value and writes it to config.php if required
135
	 *
136
	 * @param string $key key
137
	 * @param mixed $value value
138
	 * @throws HintException
139
	 */
140
	public function setValue($key, $value) {
141
		if ($this->set($key, $value)) {
142
			// Write changes
143
			$this->writeData();
144
		}
145
	}
146
147
	/**
148
	 * This function sets the value
149
	 *
150
	 * @param string $key key
151
	 * @param mixed $value value
152
	 * @return bool True if the file needs to be updated, false otherwise
153
	 * @throws HintException
154
	 */
155
	protected function set($key, $value) {
156
		$this->checkReadOnly();
157
158
		if (!isset($this->cache[$key]) || $this->cache[$key] !== $value) {
159
			// Add change
160
			$this->cache[$key] = $value;
161
			return true;
162
		}
163
164
		return false;
165
	}
166
167
	/**
168
	 * Removes a key from the config and removes it from config.php if required
169
	 *
170
	 * @param string $key
171
	 * @throws HintException
172
	 */
173
	public function deleteKey($key) {
174
		if ($this->delete($key)) {
175
			// Write changes
176
			$this->writeData();
177
		}
178
	}
179
180
	/**
181
	 * This function removes a key from the config
182
	 *
183
	 * @param string $key
184
	 * @return bool True if the file needs to be updated, false otherwise
185
	 * @throws HintException
186
	 */
187
	protected function delete($key) {
188
		$this->checkReadOnly();
189
190
		if (isset($this->cache[$key])) {
191
			// Delete key from cache
192
			unset($this->cache[$key]);
193
			return true;
194
		}
195
		return false;
196
	}
197
198
	/**
199
	 * Loads the config file
200
	 *
201
	 * Reads the config file and saves it to the cache
202
	 *
203
	 * @throws \Exception If no lock could be acquired or the config file has not been found
204
	 */
205
	private function readData() {
206
		// Default config should always get loaded
207
		$configFiles = [$this->configFilePath];
208
209
		// Add all files in the config dir ending with the same file name
210
		$extra = glob($this->configDir.'*.'.$this->configFileName);
211
		if (is_array($extra)) {
0 ignored issues
show
introduced by
The condition is_array($extra) is always true.
Loading history...
212
			natsort($extra);
213
			$configFiles = array_merge($configFiles, $extra);
214
		}
215
216
		// Include file and merge config
217
		foreach ($configFiles as $file) {
218
			$fileExistsAndIsReadable = file_exists($file) && is_readable($file);
219
			$filePointer = $fileExistsAndIsReadable ? fopen($file, 'r') : false;
220
			if ($file === $this->configFilePath &&
221
				$filePointer === false) {
222
				// Opening the main config might not be possible, e.g. if the wrong
223
				// permissions are set (likely on a new installation)
224
				continue;
225
			}
226
227
			// Try to acquire a file lock
228
			if (!flock($filePointer, LOCK_SH)) {
229
				throw new \Exception(sprintf('Could not acquire a shared lock on the config file %s', $file));
230
			}
231
232
			unset($CONFIG);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $CONFIG seems to be never defined.
Loading history...
233
			include $file;
234
			if (!defined('PHPUNIT_RUN') && headers_sent()) {
235
				// syntax issues in the config file like leading spaces causing PHP to send output
236
				$errorMessage = sprintf('Config file has leading content, please remove everything before "<?php" in %s', basename($file));
237
				if (!defined('OC_CONSOLE')) {
238
					print(\OCP\Util::sanitizeHTML($errorMessage));
239
				}
240
				throw new \Exception($errorMessage);
241
			}
242
			if (isset($CONFIG) && is_array($CONFIG)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $CONFIG seems to never exist and therefore isset should always be false.
Loading history...
243
				$this->cache = array_merge($this->cache, $CONFIG);
244
			}
245
246
			// Close the file pointer and release the lock
247
			flock($filePointer, LOCK_UN);
248
			fclose($filePointer);
249
		}
250
251
		$this->envCache = getenv();
252
	}
253
254
	/**
255
	 * Writes the config file
256
	 *
257
	 * Saves the config to the config file.
258
	 *
259
	 * @throws HintException If the config file cannot be written to
260
	 * @throws \Exception If no file lock can be acquired
261
	 */
262
	private function writeData() {
263
		$this->checkReadOnly();
264
265
		if (!is_file(\OC::$configDir.'/CAN_INSTALL') && !isset($this->cache['version'])) {
266
			throw new HintException(sprintf('Configuration was not read or initialized correctly, not overwriting %s', $this->configFilePath));
267
		}
268
269
		// Create a php file ...
270
		$content = "<?php\n";
271
		$content .= '$CONFIG = ';
272
		$content .= var_export($this->cache, true);
273
		$content .= ";\n";
274
275
		touch($this->configFilePath);
276
		$filePointer = fopen($this->configFilePath, 'r+');
277
278
		// Prevent others not to read the config
279
		chmod($this->configFilePath, 0640);
280
281
		// File does not exist, this can happen when doing a fresh install
282
		if (!is_resource($filePointer)) {
283
			throw new HintException(
284
				"Can't write into config directory!",
285
				'This can usually be fixed by giving the webserver write access to the config directory.');
286
		}
287
288
		// Try to acquire a file lock
289
		if (!flock($filePointer, LOCK_EX)) {
290
			throw new \Exception(sprintf('Could not acquire an exclusive lock on the config file %s', $this->configFilePath));
291
		}
292
293
		// Write the config and release the lock
294
		ftruncate($filePointer, 0);
295
		fwrite($filePointer, $content);
296
		fflush($filePointer);
297
		flock($filePointer, LOCK_UN);
298
		fclose($filePointer);
299
300
		if (function_exists('opcache_invalidate')) {
301
			@opcache_invalidate($this->configFilePath, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for opcache_invalidate(). 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

301
			/** @scrutinizer ignore-unhandled */ @opcache_invalidate($this->configFilePath, true);

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...
302
		}
303
	}
304
305
	/**
306
	 * @throws HintException
307
	 */
308
	private function checkReadOnly(): void {
309
		if ($this->isReadOnly) {
310
			throw new HintException(
311
				'Config is set to be read-only via option "config_is_read_only".',
312
				'Unset "config_is_read_only" to allow changes to the config file.');
313
		}
314
	}
315
}
316