summaryrefslogtreecommitdiffstats
path: root/inc/audit.inc.php
blob: 5a1c7691c3cb2272d9a2d4e8be645f66a8b2e76a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<?php

class Audit
{

	/** @var ?int */
	private static $overrideResponseCode = null;

	/**
	 * Logs the current POST parameters to the audit table, applying filtering to sensistive data.
	 *
	 * @param string $module The name of the module being executed.
	 */
	public static function run(string $module): void
	{
		$maxTotalLen = User::isLoggedIn() ? 1000000 : 10000;
		$hadLongString = true;
		$filtered = null;
		for ($maxlen = 1000; $hadLongString && $maxlen > 100; $maxlen -= 100) {
			$hadLongString = false;
			$filtered = self::processPostArray($_POST, $maxlen, $hadLongString);
			$filtered = json_encode($filtered);
			if (strlen($filtered) < $maxTotalLen)
				break;
			$filtered = null;
		}
		$data = [
			'dateline' => time(),
			'userid' => User::getId(),
			'ipaddr' => $_SERVER['REMOTE_ADDR'] ?? 'cli',
			'module' => $module,
			'action' => $_REQUEST['action'] ?? '',
			'data' => $filtered ?? 'EXCESS',
		];
		$ret = Database::exec('INSERT IGNORE INTO audit (dateline, userid, ipaddr, module, action, data)
			VALUES (:dateline, :userid, :ipaddr, :module, :action, :data)', $data, true);
		if ($ret) {
			$rowId = Database::lastInsertId();
			register_shutdown_function(function() use ($rowId) {
				$code = self::$overrideResponseCode ?? http_response_code();
				if ($code) {
					Database::exec("UPDATE IGNORE audit SET response = :code WHERE id = :id",
						['id' => $rowId, 'code' => $code], true);
				}
			});
		}
		if ($filtered === null)
			ErrorHandler::traceError('POST payload exceeded limit');
	}


	/**
	 * Process the provided (POST)array recursively, applying filters and truncation as needed.
	 *
	 * @param array $array The array to process
	 * @param int $maxlen The maximum length allowed for strings
	 * @param bool &$hadLongString A reference variable to track if a long string was encountered
	 * @return array The processed array with filtered and shortened values
	 */
	private static function processPostArray(array $array, int $maxlen, bool &$hadLongString): array
	{
		$filtered = [];
		foreach ($array as $key => $value) {
			if ($key === 'prevent_autofill' || $key === 'password_fake'
				|| $key === 'do' || $key === 'action' || $key === 'token')
				continue; // These don't matter
			$lkey = strtolower($key);
			if (strpos($lkey, 'pass') !== false || strpos($lkey, 'token') !== false
				|| substr($lkey, -3) === 'key' // privatekey, hmac-key, ...
				|| substr($lkey, 0, 2) === 'pw') {
				$value = '*****'; // Censor
			} elseif (is_array($value)) {
				$value = self::processPostArray($value, $maxlen, $hadLongString);
			} elseif (mb_strlen($value) > $maxlen) {
				// Shorten
				$hadLongString = true;
				$value = mb_substr($value, 0, $maxlen) . '...';
			}
			if (!empty($value)) {
				$filtered[$key] = $value;
			}
		}
		return $filtered;
	}

	/**
	 * Sets a custom HTTP response code to be used for logging in the audit table.
	 *
	 * @param int $responseCode The custom HTTP response code to be set.
	 */
	public static function overrideResponseCode(int $responseCode, bool $replaceExisting = true): void
	{
		if ($replaceExisting || self::$overrideResponseCode === null) {
			self::$overrideResponseCode = $responseCode;
		}
	}

}