Completed
Push — master ( db2f26...08e4d5 )
by Brett
03:11
created

Audit::getEntry()   B

Complexity

Conditions 5
Paths 3

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 11
Code Lines 7

Code Coverage

Tests 6
CRAP Score 5

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 5
eloc 7
c 1
b 1
f 0
nc 3
nop 2
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 5
rs 8.8571
1
<?php
2
/**
3
 * This serves as both the Module for the MVC part of the audit and the configuration/entry point for the actual
4
 * audit process.
5
 *
6
 * @author    Steve Guns <[email protected]>
7
 * @package   com.bedezign.yii2.audit
8
 * @copyright 2014-2015 B&E DeZign
9
 */
10
11
namespace bedezign\yii2\audit;
12
13
use bedezign\yii2\audit\components\panels\Panel;
14
use bedezign\yii2\audit\models\AuditEntry;
15
use bedezign\yii2\audit\models\AuditError;
16
use Yii;
17
use yii\base\ActionEvent;
18
use yii\base\Application;
19
use yii\base\InvalidConfigException;
20
use yii\base\InvalidParamException;
21
use yii\base\Module;
22
use yii\helpers\ArrayHelper;
23
24
/**
25
 * Audit main module.
26
 *
27
 * This module is also responsible for starting the audit process.
28
 * To configure it you need to do 2 things:
29
 * - add a module configuration entry:
30
 *     'modules' => [
31
 *        'audit' => 'bedezign\yii2\audit\Audit',
32
 *     ]
33
 *   or optionally with configuration:
34
 *     'modules' => [
35
 *        'audit' => [
36
 *            'class' => 'bedezign\yii2\audit\Audit',
37
 *            'ignoreActions' => ['debug/*']
38
 *     ]
39
 * - If you want to auto track actions, be sure to add the module to the application bootstrapping:
40
 *    'bootstrap' => ['audit'],
41
 *
42
 * @package bedezign\yii2\audit
43
 * @property AuditEntry $entry
44
 *
45
 * @method void data($type, $data)                                                                      @see ExtraDataPanel::trackData()
46
 * @method \bedezign\yii2\audit\models\AuditError exception(\Exception $exception)                      @see ErrorPanel::log()
47
 * @method \bedezign\yii2\audit\models\AuditError errorMessage($message, $code, $file, $line, $trace)   @see ErrorPanel::logMessage()
48
 */
49
class Audit extends Module
50
{
51
    /**
52
     * @var string|boolean the layout that should be applied for views within this module. This refers to a view name
53
     * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]]
54
     * will be taken. If this is false, layout will be disabled within this module.
55
     */
56
    public $layout = 'main';
57
58
    /**
59
     * @var string name of the component to use for database access
60
     */
61
    public $db = 'db';
62
63
    /**
64
     * @var string[] Action or list of actions to track. '*' is allowed as the first or last character to use as wildcard.
65
     */
66
    public $trackActions = ['*'];
67
68
    /**
69
     * @var string[] Action or list of actions to ignore. '*' is allowed as the first or last character to use as wildcard (eg 'debug/*').
70
     */
71
    public $ignoreActions = [];
72
73
    /**
74
     * @var int Maximum age (in days) of the audit entries before they are truncated
75
     */
76
    public $maxAge = null;
77
78
    /**
79
     * @var string[] IP address or list of IP addresses with access to the viewer, null for everyone (if the IP matches)
80
     * An IP address can contain the wildcard `*` at the end so that it matches IP addresses with the same prefix.
81
     * For example, '192.168.*' matches all IP addresses in the segment '192.168.'.
82
     */
83
    public $accessIps = null;
84
85
    /**
86
     * @var string[] Role or list of roles with access to the viewer, null for everyone (if the user matches)
87
     */
88
    public $accessRoles = ['admin'];
89
90
    /**
91
     * @var int[] User ID or list of user IDs with access to the viewer, null for everyone (if the role matches)
92
     */
93
    public $accessUsers = null;
94
95
    /**
96
     * @var bool Compress extra data generated or just keep in text? For people who don't like binary data in the DB
97
     */
98
    public $compressData = true;
99
100
    /**
101
     * @var string The callback to use to convert a user id into an identifier (username, email, ...). Can also be html.
102
     */
103
    public $userIdentifierCallback = false;
104
105
    /**
106
     * @var string Will be called to translate text in the user filter into a (or more) user id's
107
     */
108
    public $userFilterCallback = false;
109
110
    /**
111
     * @var bool The module does batch saving of the data records by default. You can disable this if you are experiencing
112
     * `max_allowed_packet` errors when logging huge data quantities. Records will be saved per piece instead of all at once
113
     */
114
    public $batchSave = true;
115
116
    /**
117
     * @var array|Panel[] list of panels that should be active/tracking/available during the auditing phase.
118
     * If the value is a simple string, it is the identifier of an internal panel to activate (with default settings)
119
     * If the entry is a '<key>' => '<string>|<array>' it is either a new panel or a panel override (if you specify a core id).
120
     * It is important that the key is unique, as this is the identifier used to store any data associated with the panel.
121
     *
122
     * Please note:
123
     * - If you just want to change the configuration for a core panel, use the `$panelConfiguration`, it will be merged into this one
124
     * - If you add custom panels, please namespace them ("mynamespace/panelname").
125
     */
126
    public $panels = [
127
        'audit/request',
128
        'audit/db',
129
        'audit/log',
130
        'audit/mail',
131
        'audit/profiling',
132
        'audit/trail',
133
        'audit/javascript',
134
        // 'audit/asset',
135
        // 'audit/config',
136
137
        // These provide special functionality and get loaded to activate it
138
        'audit/error',      // Links the extra error reporting functions (`exception()` and `errorMessage()`)
139
        'audit/extra',      // Links the data functions (`data()`)
140
        'audit/curl',       // Links the curl tracking function (`curlBegin()`, `curlEnd()` and `curlExec()`)
141
    ];
142
143
    /**
144
     * Everything you add in here will be merged with the basic panel configuration.
145
     * This gives you an easy way to just add or modify panels/configurations without having to re-specify every panel.
146
     * This only accepts regular definitions ('<key>' => '<array>'), but the core class will be added if needed
147
     * Take a look at the [module configuration](docs/module-configuration.md) for more information.
148
     */
149
    public $panelsMerge = [];
150
151
    /**
152
     * @var LogTarget
153
     */
154
    public $logTarget;
155
156
    /**
157
     * @var array
158
     */
159
    private $_corePanels = [
160
        // Tracking/logging panels
161
        'audit/request'    => ['class' => 'bedezign\yii2\audit\panels\RequestPanel'],
162
        'audit/db'         => ['class' => 'bedezign\yii2\audit\panels\DbPanel'],
163
        'audit/log'        => ['class' => 'bedezign\yii2\audit\panels\LogPanel'],
164
        'audit/asset'      => ['class' => 'bedezign\yii2\audit\panels\AssetPanel'],
165
        'audit/config'     => ['class' => 'bedezign\yii2\audit\panels\ConfigPanel'],
166
        'audit/profiling'  => ['class' => 'bedezign\yii2\audit\panels\ProfilingPanel'],
167
168
        // Special other panels
169
        'audit/error'      => ['class' => 'bedezign\yii2\audit\panels\ErrorPanel'],
170
        'audit/javascript' => ['class' => 'bedezign\yii2\audit\panels\JavascriptPanel'],
171
        'audit/trail'      => ['class' => 'bedezign\yii2\audit\panels\TrailPanel'],
172
        'audit/mail'       => ['class' => 'bedezign\yii2\audit\panels\MailPanel'],
173
        'audit/extra'      => ['class' => 'bedezign\yii2\audit\panels\ExtraDataPanel'],
174
        'audit/curl'       => ['class' => 'bedezign\yii2\audit\panels\CurlPanel'],
175
        'audit/soap'       => ['class' => 'bedezign\yii2\audit\panels\SoapPanel'],
176
    ];
177
178
    /**
179
     * @var array
180
     */
181
    private $_panelFunctions = [];
182
183
    /**
184
     * @var \bedezign\yii2\audit\models\AuditEntry If activated this is the active entry
185
     */
186
    private $_entry = null;
187
188
    /**
189
     * @throws InvalidConfigException
190
     */
191 280
    public function init()
192
    {
193 114
        parent::init();
194 280
        $app = Yii::$app;
195
196
        // Before action triggers a new audit entry
197 114
        $app->on(Application::EVENT_BEFORE_ACTION, [$this, 'onBeforeAction']);
198
        // After request finalizes the audit entry.
199 114
        $app->on(Application::EVENT_AFTER_REQUEST, [$this, 'onAfterRequest']);
200
201
        // Activate the logging target
202 114
        $this->logTarget = $app->getLog()->targets['audit'] = new LogTarget($this);
203
204
        // Boot all active panels
205 114
        $this->normalizePanelConfiguration();
206 114
        $this->panels = $this->loadPanels(array_keys($this->panels));
207 114
    }
208
209
    /**
210
     * Called to evaluate if the current request should be logged
211
     * @param ActionEvent $event
212
     */
213 54
    public function onBeforeAction($event)
214
    {
215 42
        if (!empty($this->trackActions) && !$this->routeMatches($event->action->uniqueId, $this->trackActions)) {
216 9
            return;
217
        }
218 33
        if (!empty($this->ignoreActions) && $this->routeMatches($event->action->uniqueId, $this->ignoreActions)) {
219 42
            return;
220
        }
221
        // Still here, start audit
222 13
        $this->getEntry(true);
223 39
    }
224
225
    /**
226
     *
227
     */
228 50
    public function onAfterRequest()
229
    {
230 50
        if ($this->_entry) {
231 18
            $this->_entry->finalize();
232 18
        }
233 50
    }
234
235
    /**
236
     * Allows panels to register functions that can be called directly on the module
237
     * @param string    $name
238
     * @param callable  $callback
239
     */
240 114
    public function registerFunction($name, $callback)
241
    {
242 114
        if (isset($this->_panelFunctions[$name]))
243 114
            throw new InvalidParamException("The '$name'-function has already been defined.");
244
245 114
        $this->_panelFunctions[$name] = $callback;
246 114
    }
247
248
    /**
249
     * @param \yii\debug\Panel $panel
250
     */
251
    public function registerPanel(\yii\debug\Panel $panel)
252
    {
253
        $this->panels[$panel->id] = $panel;
254
    }
255
256
    /**
257
     * @param string $name
258
     * @param array $params
259
     * @return mixed
260
     */
261 21
    public function __call($name, $params)
262
    {
263 21
        if (!isset($this->_panelFunctions[$name]))
264 7
            throw new \yii\base\InvalidCallException("Unknown panel function '$name'");
265
266 7
        return call_user_func_array($this->_panelFunctions[$name], $params);
267
    }
268
269
    /**
270
     * @return \yii\db\Connection the database connection.
271
     */
272 70
    public function getDb()
273
    {
274 70
        return Yii::$app->{$this->db};
275
    }
276
277
    /**
278
     * @param bool $create
279
     * @param bool $new
280
     * @return AuditEntry|static
281
     */
282 91
    public function getEntry($create = false, $new = false)
283
    {
284 91
        $entry = new AuditEntry();
285 39
        $tableSchema = $entry->getDb()->schema->getTableSchema($entry->tableName());
286 39
        if ($tableSchema) {
287 91
            if ((!$this->_entry && $create) || $new) {
288 32
                $this->_entry = AuditEntry::create(true);
289
            }
290
        }
291
        return $this->_entry;
292
    }
293
294 14
    /**
295
     * @param $user_id
296 14
     * @return string
297
     */
298
    public function getUserIdentifier($user_id)
299 12
    {
300 1
        if (!$user_id) {
301
            return Yii::t('audit', 'Guest');
302 11
        }
303
        if ($this->userIdentifierCallback && is_callable($this->userIdentifierCallback)) {
304
            return call_user_func($this->userIdentifierCallback, $user_id);
305
        }
306
        return $user_id;
307
    }
308
309 1
    /**
310
     * Returns a list of all available panel identifiers
311 1
     * @return string[]
312
     */
313
    public function getPanelIdentifiers()
314
    {
315
        return array_unique(array_merge(array_keys($this->panels), array_keys($this->_corePanels)));
316
    }
317
318
    /**
319 280
     * Tries to assemble the configuration for the panels that the user wants for auditing
320
     * @param string[]          Set of panel identifiers that should be loaded
321 280
     * @return Panel[]
322 114
     */
323 114
    public function loadPanels($list)
324 114
    {
325 280
        $panels = [];
326
        foreach ($list as $panel) {
327
            $panels[$panel] = $this->getPanel($panel);
328
        }
329
        return $panels;
330
    }
331
332
    /**
333 142
     * @param string $identifier
334
     * @return null|Panel
335 114
     * @throws InvalidConfigException
336 114
     */
337 114
    public function getPanel($identifier)
338 1
    {
339 1
        $config = null;
340
        if (isset($this->panels[$identifier]))
341 114
            $config = $this->panels[$identifier];
342 114
        elseif (isset($this->_corePanels[$identifier]))
343
            $config = $this->_corePanels[$identifier];
344 114
345 114
        if (!$config)
346 114
            throw new InvalidConfigException("'$identifier' is not a valid panel identifier");
347 114
348
        if (is_array($config)) {
349
            $config['module'] = $this;
350 46
            $config['id'] = $identifier;
351
            return Yii::createObject($config);
352
        }
353
354
        return $config;
355
    }
356
357 280
    /**
358
     * Make sure the configured panels array is a uniform set of <identifier> => <config> entries.
359 280
     * @throws InvalidConfigException
360 280
     */
361 114
    protected function normalizePanelConfiguration()
362
    {
363 114
        $panels = [];
364 114
        foreach ($this->panels as $key => $value) {
365 114
            if (is_numeric($key)) {
366 114
                // The $value contains the identifier of a core panel
367
                if (!isset($this->_corePanels[$value]))
368
                    throw new InvalidConfigException("'$value' is not a valid panel identifier");
369
                $panels[$value] = $this->_corePanels[$value];
370
            }
371 114
            else {
372 114
                // The key contains the identifier and the value is either a class name or a full array
373
                $panels[$key] = is_string($value) ? ['class' => $value] : $value;
374
            }
375 114
        }
376 114
        $this->panels = ArrayHelper::merge($panels, $this->panelsMerge);
377
378
        // We now need one more iteration to add core classes to the panels added via the merge, if needed
379
        array_walk($this->panels, function(&$value, $key) {
380
           if (!isset($value['class'])) {
381
               if (isset($this->_corePanels[$key]))
382 114
                   $value = ArrayHelper::merge($value, $this->_corePanels[$key]);
383 280
               else
384
                   throw new InvalidConfigException("Invalid configuration for '$key'. No 'class' specified.");
385
           }
386
        });
387
    }
388 280
389
    /**
390 274
     * @return int|null|string
391 278
     */
392 114
    public static function findModuleIdentifier()
393 114
    {
394 114
        foreach (Yii::$app->modules as $name => $module) {
395 280
            $class = null;
396 280
            if (is_string($module))
397 114
                $class = $module;
398
            elseif (is_array($module)) {
399 165
                if (isset($module['class']))
400
                    $class = $module['class'];
401 114
            } else
402 114
                /** @var Module $module */
403 278
                $class = $module::className();
404 162
405 6
            $parts = explode('\\', $class);
406
            if ($class && strtolower(end($parts)) == 'audit')
407
                return $name;
408
        }
409
        return null;
410
    }
411
412 27
    /**
413
     * @param string $className
414 9
     * @return bool|string
415 27
     */
416 9
    public static function findPanelIdentifier($className)
417 27
    {
418
        $audit = Audit::getInstance();
419 9
        foreach ($audit->panels as $panel) {
420
            if ($panel->className() == $className) {
421
                return $panel->id;
422
            }
423
        }
424
        return false;
425
    }
426
427
    /**
428
     * Verifies a route against a given list and returns whether it matches or not.
429
     * Entries in the list are allowed to end with a '*', which means that a substring will be used for the match
430
     * instead of a full compare.
431
     *
432 64
     * @param string $route An application rout
433
     * @param string[] $list List of routes to compare against.
434 28
     * @return bool
435 28
     */
436 28
    protected function routeMatches($route, $list)
437 56
    {
438 18
        $list = ArrayHelper::toArray($list);
439 18
        foreach ($list as $compare) {
440 22
            $len = strlen($compare);
441 8
            if ($compare[$len - 1] == '*') {
442
                $compare = rtrim($compare, '*');
443 62
                if (substr($route, 0, $len - 1) === $compare)
444 6
                    return true;
445 6
            }
446 14
447 2
            if ($compare[0] == '*') {
448
                $compare = ltrim($compare, '*');
449 50
                if (substr($route, - ($len - 1)) === $compare)
450 26
                    return true;
451 36
            }
452 36
453
            if ($route === $compare)
454
                return true;
455
        }
456
        return false;
457
    }
458
459
}
460