Completed
Pull Request — master (#9147)
by Robin
21:59
created

Log::logException()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 6
nop 2
dl 0
loc 23
rs 8.7972
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright Copyright (c) 2016, ownCloud, Inc.
5
 *
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Bart Visscher <[email protected]>
8
 * @author Bernhard Posselt <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Johannes Schlichenmaier <[email protected]>
11
 * @author Juan Pablo Villafáñez <[email protected]>
12
 * @author Lukas Reschke <[email protected]>
13
 * @author Morris Jobke <[email protected]>
14
 * @author Olivier Paroz <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Thomas Müller <[email protected]>
17
 * @author Thomas Pulzer <[email protected]>
18
 * @author Victor Dubiniuk <[email protected]>
19
 *
20
 * @license AGPL-3.0
21
 *
22
 * This code is free software: you can redistribute it and/or modify
23
 * it under the terms of the GNU Affero General Public License, version 3,
24
 * as published by the Free Software Foundation.
25
 *
26
 * This program is distributed in the hope that it will be useful,
27
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
 * GNU Affero General Public License for more details.
30
 *
31
 * You should have received a copy of the GNU Affero General Public License, version 3,
32
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
33
 *
34
 */
35
36
namespace OC;
37
38
use InterfaSys\LogNormalizer\Normalizer;
39
40
use OC\Log\File;
41
use OCP\ILogger;
42
use OCP\Support\CrashReport\IRegistry;
43
use OCP\Util;
44
45
/**
46
 * logging utilities
47
 *
48
 * This is a stand in, this should be replaced by a Psr\Log\LoggerInterface
49
 * compatible logger. See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
50
 * for the full interface specification.
51
 *
52
 * MonoLog is an example implementing this interface.
53
 */
54
class Log implements ILogger {
55
56
	/** @var string */
57
	private $logger;
58
59
	/** @var SystemConfig */
60
	private $config;
61
62
	/** @var boolean|null cache the result of the log condition check for the request */
63
	private $logConditionSatisfied = null;
64
65
	/** @var Normalizer */
66
	private $normalizer;
67
68
	/** @var IRegistry */
69
	private $crashReporters;
70
71
	protected $methodsWithSensitiveParameters = [
72
		// Session/User
73
		'completeLogin',
74
		'login',
75
		'checkPassword',
76
		'checkPasswordNoLogging',
77
		'loginWithPassword',
78
		'updatePrivateKeyPassword',
79
		'validateUserPass',
80
		'loginWithToken',
81
		'{closure}',
82
83
		// TokenProvider
84
		'getToken',
85
		'isTokenPassword',
86
		'getPassword',
87
		'decryptPassword',
88
		'logClientIn',
89
		'generateToken',
90
		'validateToken',
91
92
		// TwoFactorAuth
93
		'solveChallenge',
94
		'verifyChallenge',
95
96
		// ICrypto
97
		'calculateHMAC',
98
		'encrypt',
99
		'decrypt',
100
101
		// LoginController
102
		'tryLogin',
103
		'confirmPassword',
104
105
		// LDAP
106
		'bind',
107
		'areCredentialsValid',
108
		'invokeLDAPMethod',
109
110
		// Encryption
111
		'storeKeyPair',
112
		'setupUser',
113
	];
114
115
	/**
116
	 * @param string $logger The logger that should be used
117
	 * @param SystemConfig $config the system config object
118
	 * @param Normalizer|null $normalizer
119
	 * @param IRegistry|null $registry
120
	 */
121
	public function __construct($logger = null, SystemConfig $config = null, $normalizer = null, IRegistry $registry = null) {
122
		// FIXME: Add this for backwards compatibility, should be fixed at some point probably
123
		if ($config === null) {
124
			$config = \OC::$server->getSystemConfig();
125
		}
126
127
		$this->config = $config;
128
129
		// FIXME: Add this for backwards compatibility, should be fixed at some point probably
130
		if ($logger === null) {
131
			$logType = $this->config->getValue('log_type', 'file');
132
			$this->logger = static::getLogClass($logType);
133
			call_user_func([$this->logger, 'init']);
134
		} else {
135
			$this->logger = $logger;
136
		}
137
		if ($normalizer === null) {
138
			$this->normalizer = new Normalizer();
139
		} else {
140
			$this->normalizer = $normalizer;
141
		}
142
		$this->crashReporters = $registry;
143
	}
144
145
	/**
146
	 * System is unusable.
147
	 *
148
	 * @param string $message
149
	 * @param array $context
150
	 * @return void
151
	 */
152
	public function emergency(string $message, array $context = []) {
153
		$this->log(Util::FATAL, $message, $context);
154
	}
155
156
	/**
157
	 * Action must be taken immediately.
158
	 *
159
	 * Example: Entire website down, database unavailable, etc. This should
160
	 * trigger the SMS alerts and wake you up.
161
	 *
162
	 * @param string $message
163
	 * @param array $context
164
	 * @return void
165
	 */
166
	public function alert(string $message, array $context = []) {
167
		$this->log(Util::ERROR, $message, $context);
168
	}
169
170
	/**
171
	 * Critical conditions.
172
	 *
173
	 * Example: Application component unavailable, unexpected exception.
174
	 *
175
	 * @param string $message
176
	 * @param array $context
177
	 * @return void
178
	 */
179
	public function critical(string $message, array $context = []) {
180
		$this->log(Util::ERROR, $message, $context);
181
	}
182
183
	/**
184
	 * Runtime errors that do not require immediate action but should typically
185
	 * be logged and monitored.
186
	 *
187
	 * @param string $message
188
	 * @param array $context
189
	 * @return void
190
	 */
191
	public function error(string $message, array $context = []) {
192
		$this->log(Util::ERROR, $message, $context);
193
	}
194
195
	/**
196
	 * Exceptional occurrences that are not errors.
197
	 *
198
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things
199
	 * that are not necessarily wrong.
200
	 *
201
	 * @param string $message
202
	 * @param array $context
203
	 * @return void
204
	 */
205
	public function warning(string $message, array $context = []) {
206
		$this->log(Util::WARN, $message, $context);
207
	}
208
209
	/**
210
	 * Normal but significant events.
211
	 *
212
	 * @param string $message
213
	 * @param array $context
214
	 * @return void
215
	 */
216
	public function notice(string $message, array $context = []) {
217
		$this->log(Util::INFO, $message, $context);
218
	}
219
220
	/**
221
	 * Interesting events.
222
	 *
223
	 * Example: User logs in, SQL logs.
224
	 *
225
	 * @param string $message
226
	 * @param array $context
227
	 * @return void
228
	 */
229
	public function info(string $message, array $context = []) {
230
		$this->log(Util::INFO, $message, $context);
231
	}
232
233
	/**
234
	 * Detailed debug information.
235
	 *
236
	 * @param string $message
237
	 * @param array $context
238
	 * @return void
239
	 */
240
	public function debug(string $message, array $context = []) {
241
		$this->log(Util::DEBUG, $message, $context);
242
	}
243
244
245
	/**
246
	 * Logs with an arbitrary level.
247
	 *
248
	 * @param int $level
249
	 * @param string $message
250
	 * @param array $context
251
	 * @return void
252
	 */
253
	public function log(int $level, string $message, array $context = []) {
254
		$minLevel = $this->getLogLevel($context);
255
256
		array_walk($context, [$this->normalizer, 'format']);
257
258
		$app = $context['app'] ?? 'no app in context';
259
260
		// interpolate $message as defined in PSR-3
261
		$replace = [];
262
		foreach ($context as $key => $val) {
263
			$replace['{' . $key . '}'] = $val;
264
		}
265
		$message = strtr($message, $replace);
266
267
		if ($level >= $minLevel) {
268
			$this->writeLog($app, $message, $level);
269
		}
270
	}
271
272
	private function getLogLevel($context) {
273
		/**
274
		 * check for a special log condition - this enables an increased log on
275
		 * a per request/user base
276
		 */
277
		if ($this->logConditionSatisfied === null) {
278
			// default to false to just process this once per request
279
			$this->logConditionSatisfied = false;
280
			if (!empty($logCondition)) {
0 ignored issues
show
Bug introduced by
The variable $logCondition seems only to be defined at a later point. As such the call to empty() seems to always evaluate to true.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
281
282
				// check for secret token in the request
283
				if (isset($logCondition['shared_secret'])) {
284
					$request = \OC::$server->getRequest();
285
286
					// if token is found in the request change set the log condition to satisfied
287
					if ($request && hash_equals($logCondition['shared_secret'], $request->getParam('log_secret', ''))) {
288
						$this->logConditionSatisfied = true;
289
					}
290
				}
291
292
				// check for user
293
				if (isset($logCondition['users'])) {
294
					$user = \OC::$server->getUserSession()->getUser();
295
296
					// if the user matches set the log condition to satisfied
297
					if ($user !== null && in_array($user->getUID(), $logCondition['users'], true)) {
298
						$this->logConditionSatisfied = true;
299
					}
300
				}
301
			}
302
		}
303
304
		// if log condition is satisfied change the required log level to DEBUG
305
		if ($this->logConditionSatisfied) {
306
			return Util::DEBUG;
307
		}
308
309
		if (isset($context['app'])) {
310
			$logCondition = $this->config->getValue('log.condition', []);
311
			$app = $context['app'];
312
313
			/**
314
			 * check log condition based on the context of each log message
315
			 * once this is met -> change the required log level to debug
316
			 */
317
			if (!empty($logCondition)
318
				&& isset($logCondition['apps'])
319
				&& in_array($app, $logCondition['apps'], true)) {
320
				return Util::DEBUG;
321
			}
322
		}
323
324
		return min($this->config->getValue('loglevel', Util::WARN), Util::FATAL);
325
	}
326
327
	private function filterTrace(array $trace) {
328
		$sensitiveValues = [];
329
		$trace = array_map(function (array $traceLine) use (&$sensitiveValues) {
330
			foreach ($this->methodsWithSensitiveParameters as $sensitiveMethod) {
331
				if (strpos($traceLine['function'], $sensitiveMethod) !== false) {
332
					$sensitiveValues = array_merge($sensitiveValues, $traceLine['args']);
333
					$traceLine['args'] = ['*** sensitive parameters replaced ***'];
334
					return $traceLine;
335
				}
336
			}
337
			return $traceLine;
338
		}, $trace);
339
		return array_map(function (array $traceLine) use ($sensitiveValues) {
340
			$traceLine['args'] = $this->removeValuesFromArgs($traceLine['args'], $sensitiveValues);
341
			return $traceLine;
342
		}, $trace);
343
	}
344
345
	private function removeValuesFromArgs($args, $values) {
346
		foreach($args as &$arg) {
347
			if (in_array($arg, $values, true)) {
348
				$arg = '*** sensitive parameter replaced ***';
349
			} else if (is_array($arg)) {
350
				$arg = $this->removeValuesFromArgs($arg, $values);
351
			}
352
		}
353
		return $args;
354
	}
355
356
	private function serializeException(\Throwable $exception) {
357
		$data = [
358
			'Exception' => get_class($exception),
359
			'Message' => $exception->getMessage(),
360
			'Code' => $exception->getCode(),
361
			'Trace' => $this->filterTrace($exception->getTrace()),
362
			'File' => $exception->getFile(),
363
			'Line' => $exception->getLine(),
364
		];
365
366
		if ($exception instanceof HintException) {
367
			$data['Hint'] = $exception->getHint();
368
		}
369
370
		if ($exception->getPrevious()) {
371
			$data['Previous'] = $this->serializeException($exception->getPrevious());
372
		}
373
374
		return $data;
375
	}
376
377
	/**
378
	 * Logs an exception very detailed
379
	 *
380
	 * @param \Exception|\Throwable $exception
381
	 * @param array $context
382
	 * @return void
383
	 * @since 8.2.0
384
	 */
385
	public function logException(\Throwable $exception, array $context = []) {
386
		$app = $context['app'] ?? 'no app in context';
387
		$level = $context['level'] ?? Util::ERROR;
388
389
		$data = $this->serializeException($exception);
390
		$data['CustomMessage'] = $context['message'] ?? '--';
391
392
		$minLevel = $this->getLogLevel($context);
393
394
		array_walk($context, [$this->normalizer, 'format']);
395
396
		if ($level >= $minLevel) {
397
			if ($this->logger !== File::class) {
398
				$data = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR);
399
			}
400
			$this->writeLog($app, $data, $level);
401
		}
402
403
		$context['level'] = $level;
404
		if (!is_null($this->crashReporters)) {
405
			$this->crashReporters->delegateReport($exception, $context);
406
		}
407
	}
408
409
	/**
410
	 * @param string $app
411
	 * @param string|array $entry
412
	 * @param int $level
413
	 */
414
	protected function writeLog(string $app, $entry, int $level) {
415
		call_user_func([$this->logger, 'write'], $app, $entry, $level);
416
	}
417
418
	/**
419
	 * @param string $logType
420
	 * @return string
421
	 * @internal
422
	 */
423
	public static function getLogClass(string $logType): string {
424
		switch (strtolower($logType)) {
425
			case 'errorlog':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
426
				return \OC\Log\Errorlog::class;
427
			case 'syslog':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
428
				return \OC\Log\Syslog::class;
429
			case 'file':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
430
				return \OC\Log\File::class;
431
432
			// Backwards compatibility for old and fallback for unknown log types
433
			case 'owncloud':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
434
			case 'nextcloud':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
435
			default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
436
				return \OC\Log\File::class;
437
		}
438
	}
439
}
440