Audit::shouldTrack()   B
last analyzed

Complexity

Conditions 7
Paths 12

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
nc 12
nop 2
dl 0
loc 14
ccs 6
cts 6
cp 1
crap 7
rs 8.8333
c 0
b 0
f 0
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 Yii;
16
use yii\base\ActionEvent;
17
use yii\base\Application;
18
use yii\base\InvalidConfigException;
19
use yii\base\InvalidParamException;
20
use yii\base\Module;
21
use yii\helpers\ArrayHelper;
22
23
/**
24
 * Audit main module.
25
 *
26
 * This module is also responsible for starting the audit process.
27
 * To configure it you need to do 2 things:
28
 * - add a module configuration entry:
29
 *     'modules' => [
30
 *        'audit' => 'bedezign\yii2\audit\Audit',
31
 *     ]
32
 *   or optionally with configuration:
33
 *     'modules' => [
34
 *        'audit' => [
35
 *            'class' => 'bedezign\yii2\audit\Audit',
36
 *            'ignoreActions' => ['debug/*']
37
 *     ]
38
 * - If you want to auto track actions, be sure to add the module to the application bootstrapping:
39
 *    'bootstrap' => ['audit'],
40
 *
41
 * @package bedezign\yii2\audit
42
 * @property AuditEntry $entry
43
 *
44
 * @method void data($type, $data)                                                                      @see ExtraDataPanel::trackData()
45
 * @method \bedezign\yii2\audit\models\AuditError exception(\Exception $exception)                      @see ErrorPanel::log()
46
 * @method \bedezign\yii2\audit\models\AuditError errorMessage($message, $code, $file, $line, $trace)   @see ErrorPanel::logMessage()
47
 */
48
class Audit extends Module
49
{
50
    /**
51
     * @var string|boolean the layout that should be applied for views within this module. This refers to a view name
52
     * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]]
53
     * will be taken. If this is false, layout will be disabled within this module.
54
     */
55
    public $layout = 'main';
56
57
    /**
58
     * @var string name of the component to use for database access
59
     */
60
    public $db = 'db';
61
62
    /**
63
     * @var string[] Action or list of actions to track. '*' is allowed as the first or last character to use as wildcard.
64
     */
65
    public $trackActions = ['*'];
66
67
    /**
68
     * @var string[] Action or list of actions to ignore. '*' is allowed as the first or last character to use as wildcard (eg 'debug/*').
69
     */
70
    public $ignoreActions = [];
71
72
    /**
73
     * @var string[] Action or list of actions to track if they cause an error. '*' is allowed as the first or last character to use as wildcard.
74
     */
75
    public $trackErrorActions = ['*'];
76
77
    /**
78
     * @var string[] Action or list of actions to ignore if they cause an error. '*' is allowed as the first or last character to use as wildcard (eg 'debug/*').
79
     */
80
    public $ignoreErrorActions = [];
81
82
    /**
83
     * @var int Maximum age (in days) of the audit entries before they are truncated
84
     */
85
    public $maxAge = null;
86
87
    /**
88
     * @var string[] IP address or list of IP addresses with access to the viewer, null for everyone (if the IP matches)
89
     * An IP address can contain the wildcard `*` at the end so that it matches IP addresses with the same prefix.
90
     * For example, '192.168.*' matches all IP addresses in the segment '192.168.'.
91
     */
92
    public $accessIps = null;
93
94
    /**
95
     * @var string[] Role or list of roles with access to the viewer, null for everyone (if the user matches)
96
     */
97
    public $accessRoles = ['admin'];
98
99
    /**
100
     * @var int[] User ID or list of user IDs with access to the viewer, null for everyone (if the role matches)
101
     */
102
    public $accessUsers = null;
103
104
    /**
105
     * @var bool Compress extra data generated or just keep in text? For people who don't like binary data in the DB
106
     */
107
    public $compressData = true;
108
109
    /**
110
     * @var string The callback to use to convert a user id into an identifier (username, email, ...). Can also be html.
111
     */
112
    public $userIdentifierCallback = false;
113
114
    /**
115
     * @var string The callback to get a user id.
116
     */
117
    public $userIdCallback = false;
118
119
    /**
120
     * @var string Will be called to translate text in the user filter into a (or more) user id's
121
     */
122
    public $userFilterCallback = false;
123
124
    /**
125
     * @var bool The module does batch saving of the data records by default. You can disable this if you are experiencing
126
     * `max_allowed_packet` errors when logging huge data quantities. Records will be saved per piece instead of all at once
127
     */
128
    public $batchSave = true;
129
130
    /**
131
     * @var array Default log levels to filter and process
132
     */
133
    public $logConfig = ['levels' => ['error', 'warning', 'info', 'profile']];
134
135
136
    /**
137
     * @var array|Panel[] list of panels that should be active/tracking/available during the auditing phase.
138
     * If the value is a simple string, it is the identifier of an internal panel to activate (with default settings)
139
     * If the entry is a '<key>' => '<string>|<array>' it is either a new panel or a panel override (if you specify a core id).
140
     * It is important that the key is unique, as this is the identifier used to store any data associated with the panel.
141
     *
142
     * Please note:
143
     * - If you just want to change the configuration for a core panel, use `$panelsMerge`, it will be merged into this one
144
     * - If you add custom panels, please namespace them ("mynamespace/panelname").
145
     */
146
    public $panels = [
147
        'audit/request',
148
        'audit/db',
149
        'audit/log',
150
        'audit/mail',
151
        'audit/profiling',
152
        'audit/trail',
153
        'audit/javascript',
154
        // 'audit/asset',
155
        // 'audit/config',
156
157
        // These provide special functionality and get loaded to activate it
158
        'audit/error',      // Links the extra error reporting functions (`exception()` and `errorMessage()`)
159
        'audit/extra',      // Links the data functions (`data()`)
160
        'audit/curl',       // Links the curl tracking function (`curlBegin()`, `curlEnd()` and `curlExec()`)
161
    ];
162
163
    /**
164
     * Everything you add in here will be merged with the basic panel configuration.
165
     * This gives you an easy way to just add or modify panels/configurations without having to re-specify all active panels.
166
     * This only accepts regular definitions ('<key>' => '<array>'), but the core class will be added if needed
167
     * Take a look at the [module configuration](docs/module-configuration.md) for more information.
168
     */
169
    public $panelsMerge = [];
170
171
    /**
172
     * @var LogTarget
173
     */
174
    public $logTarget;
175
176
    // Things required to keep the module yii2-debug compatible
177
    /* @see \yii\debug\Module::$traceLine (since 2.0.7) */
178
    public $traceLine = \yii\debug\Module::DEFAULT_IDE_TRACELINE;
179
     /* @see \yii\debug\Module::$tracePathMappings (since 2.1.6) */
180
    public $tracePathMappings = [];
181
182
    /**
183
     * @var array
184
     */
185
    private $_corePanels = [
186
        // Tracking/logging panels
187
        'audit/request'    => ['class' => 'bedezign\yii2\audit\panels\RequestPanel'],
188
        'audit/db'         => ['class' => 'bedezign\yii2\audit\panels\DbPanel'],
189
        'audit/log'        => ['class' => 'bedezign\yii2\audit\panels\LogPanel'],
190
        'audit/asset'      => ['class' => 'bedezign\yii2\audit\panels\AssetPanel'],
191
        'audit/config'     => ['class' => 'bedezign\yii2\audit\panels\ConfigPanel'],
192
        'audit/profiling'  => ['class' => 'bedezign\yii2\audit\panels\ProfilingPanel'],
193
194
        // Special other panels
195
        'audit/error'      => ['class' => 'bedezign\yii2\audit\panels\ErrorPanel'],
196
        'audit/javascript' => ['class' => 'bedezign\yii2\audit\panels\JavascriptPanel'],
197
        'audit/trail'      => ['class' => 'bedezign\yii2\audit\panels\TrailPanel'],
198
        'audit/mail'       => ['class' => 'bedezign\yii2\audit\panels\MailPanel'],
199
        'audit/extra'      => ['class' => 'bedezign\yii2\audit\panels\ExtraDataPanel'],
200
        'audit/curl'       => ['class' => 'bedezign\yii2\audit\panels\CurlPanel'],
201
        'audit/soap'       => ['class' => 'bedezign\yii2\audit\panels\SoapPanel'],
202
    ];
203
204
    /**
205
     * @var array
206 78
     */
207
    private $_panelFunctions = [];
208 78
209 78
    /**
210
     * @var \bedezign\yii2\audit\models\AuditEntry If activated this is the active entry
211
     */
212
    private $_entry = null;
213 78
214 78
    private static $_me = null;
215
216
    /**
217
     * @throws InvalidConfigException
218
     */
219 78
    public function init()
220
    {
221 78
        parent::init();
222
        $app = Yii::$app;
223
224 78
        // check if the module has been installed (prevents errors while installing)
225 75
        try {
226 75
            $this->getDb()->getTableSchema(AuditEntry::tableName());
227 3
        } catch (\Exception $e) {
228
            return;
229
        }
230
231 78
        // Before action triggers a new audit entry
232 78
        $app->on(Application::EVENT_BEFORE_ACTION, [$this, 'onBeforeAction']);
233 78
        // After request finalizes the audit entry.
234
        $app->on(Application::EVENT_AFTER_REQUEST, [$this, 'onAfterRequest']);
235
236
        $this->activateLogTarget();
237
238
        // Boot all active panels
239 84
        $this->normalizePanelConfiguration();
240
        $this->panels = $this->loadPanels(array_keys($this->panels));
241 84
    }
242 9
243
    public function shouldTrack($event, $isError = false)
244 75
    {
245 54
        $trackActions = $isError ? $this->trackErrorActions : $this->trackActions;
246
        $ignoreActions = $isError ? $this->ignoreErrorActions : $this->ignoreActions;
247
248 39
        if (!empty($trackActions) && !$this->routeMatches($event->action->uniqueId, $trackActions)) {
249 39
            return false;
250
        }
251
        if (!empty($ignoreActions) && $this->routeMatches($event->action->uniqueId, $ignoreActions)) {
252
            return false;
253
        }
254 57
255
        return true;
256 57
    }
257 57
258 57
    /**
259 57
     * Called to evaluate if the current request should be logged
260
     * @param ActionEvent $event
261
     */
262
    public function onBeforeAction($event)
263
    {
264
        if (!$this->shouldTrack($event)) {
265
            return;
266 78
        }
267
268 78
        // Still here, start audit
269 78
        $this->getEntry(true);
270
    }
271 78
272 78
    /**
273
     *
274
     */
275
    public function onAfterRequest()
276
    {
277
        if ($this->_entry) {
278
            $this->_entry->finalize();
279
        }
280
    }
281
282
    /**
283
     * Allows panels to register functions that can be called directly on the module
284
     * @param string    $name
285
     * @param callable  $callback
286
     */
287 21
    public function registerFunction($name, $callback)
288
    {
289 21
        if (isset($this->_panelFunctions[$name]))
290 21
            throw new InvalidParamException("The '$name'-function has already been defined.");
291
292 21
        $this->_panelFunctions[$name] = $callback;
293
    }
294
295
    public function hasMethod($name, $checkBehaviors = true)
296
    {
297
        if (isset($this->_panelFunctions[$name])) {
298 237
            return true;
299
        }
300 237
        return parent::hasMethod($name, $checkBehaviors);
301
    }
302
303
    /**
304
     * @param \yii\debug\Panel $panel
305
     */
306
    public function registerPanel(\yii\debug\Panel $panel)
307
    {
308 171
        $this->panels[$panel->id] = $panel;
309
    }
310 171
311 171
    /**
312 171
     * @param string $name
313 171
     * @param array $params
314 117
     * @return mixed
315 117
     */
316 171
    public function __call($name, $params)
317 171
    {
318
        if (!isset($this->_panelFunctions[$name]))
319
            throw new \yii\base\InvalidCallException("Unknown panel function '$name'");
320
321
        return call_user_func_array($this->_panelFunctions[$name], $params);
322
    }
323
324 18
    public function activateLogTarget()
325
    {
326 18
        $app = Yii::$app;
327
328
        // Activate the logging target
329 18
        if (empty($app->getLog()->targets['audit'])) {
330 3
            $this->logTarget = $app->getLog()->targets['audit'] = new LogTarget($this, $this->logConfig);
331
        } else {
332 15
            $this->logTarget = $app->getLog()->targets['audit'];
333
        }
334
    }
335
336
    /**
337
     * @return \yii\db\Connection the database connection.
338 120
     */
339
    public function getDb()
340 120
    {
341 3
        return Yii::$app->{$this->db};
342
    }
343 117
344
    public static function getInstance()
345
    {
346
        if (static::$_me) {
0 ignored issues
show
Bug introduced by Steve Guns
Since $_me is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $_me to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
347
            return self::$_me;
348
        }
349
350 3
        // This code assumes the audit module is already loaded and can thus look for a derived instance
351
        $loadedModules = Yii::$app->loadedModules;
352 3
        foreach ($loadedModules as $module) {
353
             if ($module instanceof self) {
354
                 return self::$_me = $module;
355
             }
356
        }
357
358
        // If we're still here, fall back onto the default implementation
359
        return parent::getInstance();
360 78
    }
361
362 78
363 78
    /**
364 78
     * @param bool $create
365 78
     * @param bool $new
366 78
     * @return AuditEntry
367
     */
368
    public function getEntry($create = false, $new = false)
369
    {
370
        $entry = new AuditEntry();
371
        $tableSchema = $entry->getDb()->schema->getTableSchema($entry->tableName());
372
        if ($tableSchema) {
373
            if ((!$this->_entry && $create) || $new) {
374 114
                $this->_entry = AuditEntry::create(true);
375
            }
376 114
        }
377 114
        return $this->_entry;
378 114
    }
379 3
380 3
    /**
381
     * @param AuditEntry $entry
382 114
     */
383 114
    public function setEntry($entry)
384
    {
385 114
        $this->_entry = $entry;
386 78
    }
387 78
388 78
    /**
389
     * @param $user_id
390
     * @return string
391 54
     */
392
    public function getUserIdentifier($user_id)
393
    {
394
        if (!$user_id) {
395
            return Yii::t('audit', 'Guest');
396
        }
397
        if ($this->userIdentifierCallback && is_callable($this->userIdentifierCallback)) {
398 78
            return call_user_func($this->userIdentifierCallback, $user_id);
399
        }
400 78
        return $user_id;
401 78
    }
402 78
403
    /**
404 78
     * @return int|mixed|null|string
405 78
     */
406 78
    public function getUserId()
407 78
    {
408
        if ($this->userIdCallback && is_callable($this->userIdCallback)) {
409
            return call_user_func($this->userIdCallback);
410
        }
411
        return (Yii::$app instanceof \yii\web\Application && Yii::$app->user) ? Yii::$app->user->id : null;
412 78
    }
413 78
414
    /**
415
     * Returns a list of all available panel identifiers
416 78
     * @return string[]
417 78
     */
418
    public function getPanelIdentifiers()
419
    {
420
        return array_unique(array_merge(array_keys($this->panels), array_keys($this->_corePanels)));
421
    }
422
423 78
    /**
424 78
     * Tries to assemble the configuration for the panels that the user wants for auditing
425
     * @param string[]          Set of panel identifiers that should be loaded
426
     * @return Panel[]
427
     */
428
    public function loadPanels($list)
429 87
    {
430
        $panels = [];
431 87
        foreach ($list as $panel) {
432 81
            $panels[$panel] = $this->getPanel($panel);
433 81
        }
434 81
        return $panels;
435 81
    }
436 78
437 78
    /**
438 78
     * @param string $identifier
439
     * @return null|Panel
440 3
     * @throws InvalidConfigException
441
     */
442 81
    public function getPanel($identifier)
443 81
    {
444 81
        $config = null;
445 6
        if (isset($this->panels[$identifier]))
446 6
            $config = $this->panels[$identifier];
447
        elseif (isset($this->_corePanels[$identifier]))
448
            $config = $this->_corePanels[$identifier];
449
450
        if (!$config)
451
            throw new InvalidConfigException("'$identifier' is not a valid panel identifier");
452
453 27
        if (is_array($config)) {
454
            $config['module'] = $this;
455 27
            $config['id'] = $identifier;
456 27
            return Yii::createObject($config);
457 27
        }
458 27
459
        return $config;
460 27
    }
461
462
    /**
463
     * Make sure the configured panels array is a uniform set of <identifier> => <config> entries.
464
     * @throws InvalidConfigException
465
     */
466
    protected function normalizePanelConfiguration()
467
    {
468
        $panels = [];
469
        foreach ($this->panels as $key => $value) {
470
            if (is_numeric($key)) {
471
                // The $value contains the identifier of a core panel
472
                if (!isset($this->_corePanels[$value]))
473 84
                    throw new InvalidConfigException("'$value' is not a valid panel identifier");
474
                $panels[$value] = $this->_corePanels[$value];
475 84
            }
476 84
            else {
477 84
                // The key contains the identifier and the value is either a class name or a full array
478 84
                $panels[$key] = is_string($value) ? ['class' => $value] : $value;
479 54
            }
480 54
        }
481 54
        $this->panels = ArrayHelper::merge($panels, $this->panelsMerge);
482 24
483
        // We now need one more iteration to add core classes to the panels added via the merge, if needed
484 78
        array_walk($this->panels, function(&$value, $key) {
485 18
           if (!isset($value['class'])) {
486 18
               if (isset($this->_corePanels[$key]))
487 18
                   $value = ArrayHelper::merge($value, $this->_corePanels[$key]);
488 6
               else
489
                   throw new InvalidConfigException("Invalid configuration for '$key'. No 'class' specified.");
490 66
           }
491 66
        });
492 60
    }
493 36
494
    /**
495
     * @return int|null|string
496
     */
497
    public static function findModuleIdentifier()
498
    {
499
        foreach (Yii::$app->modules as $name => $module) {
500
            $class = null;
501
            if (is_string($module))
502
                $class = $module;
503
            elseif (is_array($module)) {
504
                if (isset($module['class']))
505
                    $class = $module['class'];
506
            } else
507
                /** @var Module $module */
508
                $class = $module::className();
509
510
            $parts = explode('\\', $class);
511
            if ($class && strtolower(end($parts)) == 'audit')
512
                return $name;
513
        }
514
        return null;
515
    }
516
517
    /**
518
     * @param string $className
519
     * @return bool|string
520
     */
521
    public static function findPanelIdentifier($className)
522
    {
523
        $audit = Audit::getInstance();
524
        foreach ($audit->panels as $panel) {
525
            if ($panel->className() == $className) {
526
                return $panel->id;
527
            }
528
        }
529
        return false;
530
    }
531
532
    /**
533
     * Verifies a route against a given list and returns whether it matches or not.
534
     * Entries in the list are allowed to end with a '*', which means that a substring will be used for the match
535
     * instead of a full compare.
536
     *
537
     * @param string $route An application rout
538
     * @param string[] $list List of routes to compare against.
539
     * @return bool
540
     */
541
    protected function routeMatches($route, $list)
542
    {
543
        $list = ArrayHelper::toArray($list);
544
        foreach ($list as $compare) {
545
            $len = strlen($compare);
546
            if ($compare[$len - 1] == '*') {
547
                $compare = rtrim($compare, '*');
548
                if (substr($route, 0, $len - 1) === $compare)
549
                    return true;
550
            }
551
552
            if ($compare[0] == '*') {
553
                $compare = ltrim($compare, '*');
554
                if (substr($route, - ($len - 1)) === $compare)
555
                    return true;
556
            }
557
558
            if ($route === $compare)
559
                return true;
560
        }
561
        return false;
562
    }
563
564
}
565