-
Notifications
You must be signed in to change notification settings - Fork 114
/
Copy pathAuditing.php
230 lines (193 loc) · 7.53 KB
/
Auditing.php
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
<?php
/**
* This serves as both the Module for the MVC part of the auditing and the configuration/entry point for the actual
* auditing process.
*
* @author Steve Guns <steve@bedezign.com>
* @package com.bedezign.yii2.audit
* @copyright 2014-2015 B&E DeZign
*/
namespace bedezign\yii2\audit;
use yii\base\Application;
use yii\helpers\ArrayHelper;
use bedezign\yii2\audit;
use bedezign\yii2\audit\models;
/**
* Class Auditing
* @package bedezign\yii2\audit
*
* Auditing main module.
* This module is also responsible for starting the auditing process.
* To configure it you need to do 2 things:
* - add a module configuration entry:
* 'modules' => [
* 'audit' => 'bedezign\yii2\audit\Auditing',
* ]
* or optionally with configuration:
* 'modules' => [
* 'auditing' => [
* 'class' => 'bedezign\yii2\audit\Auditing',
* 'ignoreActions' => ['debug/*']
* ]
* - If you want to auto track actions, be sure to add the module to the application bootstrapping:
* 'bootstrap' => ['auditing'],
*
*/
class Auditing extends \yii\base\Module
{
/** @var string name of the component to use for database access */
public $db = 'db';
/** @var string[] List of actions to track. '*' is allowed as the last character to use as wildcard. */
public $trackActions = ['*'];
/** @var string[] Actions to ignore. '*' is allowed as the last character to use as wildcard (eg 'debug/*'). */
public $ignoreActions = [];
/** @var int Chance in % that the truncate operation will run, false to not run at all */
public $truncateChance = false;
/** @var int Maximum age (in days) of the audit entries before they are truncated */
public $maxAge = null;
/** @var int[] (List of) user(s) IDs with access to the viewer, null for everyone (if the role matches) */
public $accessUsers = null;
/** @var string[] (List of) role(s) with access to the viewer, null for everyone (if the user matches) */
public $accessRoles = 'admin';
/** @var bool Compress extra data generated or just keep in text? For people who don't like binary data in the DB */
public $compressData = true;
/** @var static The current instance */
private static $_current = null;
/** @var audit\models\AuditEntry If activated this is the active entry*/
private $_entry = null;
public function init()
{
static::$_current = $this;
parent::init();
// Allow the users to specify a simple string if there is only 1 entry
$this->trackActions = ArrayHelper::toArray($this->trackActions);
$this->ignoreActions = ArrayHelper::toArray($this->ignoreActions);
if ($this->accessRoles)
$this->accessRoles = ArrayHelper::toArray($this->accessRoles);
if ($this->accessUsers)
$this->accessUsers = ArrayHelper::toArray($this->accessUsers);
// Before action triggers a new audit entry
\Yii::$app->on(Application::EVENT_BEFORE_ACTION, [$this, 'onApplicationAction']);
// After request finalizes the audit entry and optionally does truncating
\Yii::$app->on(Application::EVENT_AFTER_REQUEST, [$this, 'onAfterRequest']);
// Register translation
$app = \Yii::$app;
if ($app->has('i18n')) {
$app->i18n->translations['audit'] = [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en',
'basePath' => '@bedezign/yii2/audit/messages',
];
}
}
/**
* Called to evaluate if the current request should be logged
* @param \yii\base\Event $event
*/
public function onApplicationAction($event)
{
$actionId = $event->action->uniqueId;
if (count($this->trackActions) && !$this->routeMatches($actionId, $this->trackActions))
return;
if (count($this->ignoreActions) && $this->routeMatches($actionId, $this->ignoreActions))
return;
// Still here, start auditing
$this->getEntry(true);
}
/**
* If the action was execute
*/
public function onAfterRequest()
{
if ($this->entry)
$this->_entry->finalize();
if ($this->truncateChance !== false && $this->maxAge !== null) {
if (rand(1, 100) <= $this->truncateChance)
$this->truncate();
}
}
/**
* Associate extra data with the current entry (if any)
* @param string $name
* @param mixed $data The data to associate with the current entry
* @param string $type Optional type argument
* @return \bedezign\yii2\audit\models\AuditData
*/
public function data($name, $data, $type = null)
{
$entry = $this->getEntry(false);
if (!$entry)
return null;
return $entry->addData($name, $data, $type);
}
/**
* @return \yii\db\Connection the database connection.
*/
public function getDb()
{
return \Yii::$app->{$this->db};
}
/**
* Returns the current module instance.
* Since we don't know how the module was linked into the application, this function allows us to retrieve
* the instance without that information. As soon as an instance was initialized, it is linked.
* @return static
*/
public static function current()
{
return static::$_current;
}
public function getEntry($create = false)
{
if (!$this->_entry && $create) {
$this->_entry = models\AuditEntry::create(true);
$this->_entry->save(false);
}
return $this->_entry;
}
/**
* Clean up the audit data according to the settings.
* Can be handy if you are offloading the data somewhere and want to keep only the most recent entries readily available
*/
public function truncate()
{
if ($this->maxAge === null)
return;
$entry = models\AuditEntry::tableName();
$errors = models\AuditError::tableName();
$data = models\AuditData::tableName();
$javascript = models\AuditJavascript::tableName();
$threshold = time() - ($this->maxAge * 86400);
models\AuditEntry::getDb()->createCommand(<<<SQL
DELETE FROM $entry, $errors, $data, $javascript USING $entry
INNER JOIN $errors ON $errors.audit_id = $entry.id
INNER JOIN $data ON $data.audit_id = $entry.id
INNER JOIN $javascript ON $javascript.audit_id = $entry.id
WHERE $entry.created < FROM_UNIXTIME($threshold)
SQL
)->execute();
}
/**
* Verifies a route against a given list and returns whether it matches or not.
* Entries in the list are allowed to end with a '*', which means that a substring will be used for the match
* instead of a full compare.
*
* @param string $route An application rout
* @param string[] $list List of routes to compare against.
* @return bool
*/
protected function routeMatches($route, $list)
{
foreach ($list as $compare) {
$len = strlen($compare);
if ($compare[$len - 1] == '*') {
$compare = rtrim($compare, '*');
if (substr($route, 0, $len - 1) === $compare)
return true;
}
if ($route === $compare)
return true;
}
return false;
}
}