Completed
Push — feature/EVO-7278-tracking-info... ( d73a1e )
by
unknown
63:38
created

ActivityManager::getConfigValue()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
rs 8.8571
cc 5
eloc 9
nc 5
nop 2
1
<?php
2
namespace Graviton\AuditTrackingBundle\Manager;
3
4
use Graviton\AuditTrackingBundle\Document\AuditTracking;
5
use Graviton\SecurityBundle\Entities\SecurityUser;
6
use Guzzle\Http\Message\Header;
7
use Symfony\Bridge\Doctrine\ManagerRegistry;
8
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
9
use Symfony\Component\HttpFoundation\RequestStack;
10
use Symfony\Component\HttpFoundation\Response;
11
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
12
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
13
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\Security\Core\User\UserInterface;
16
17
/**
18
 * Class ActivityManager
19
 * @package Graviton\AuditTrackingBundle\Manager
20
 */
21
class ActivityManager
22
{
23
    /** Max char length of saved content data */
24
    const CONTENT_MAX_LENGTH = 2048;
25
26
    /** @var bool If log is enabled */
27
    private $enabled = false;
28
29
    /** @var Request $request */
30
    private $request;
31
32
    /** @var array */
33
    private $configurations;
34
35
    /** @var TokenStorage */
36
    protected $tokenStorage;
37
38
    /** @var SecurityUser */
39
    private $securityUser;
40
41
    /** @var array Events that shall be stored */
42
    private $events = [];
43
44
    /**
45
     * DBActivityListener constructor.
46
     * @param RequestStack $requestStack Sf request data
47
     * @param TokenStorage $tokenStorage Sf Auth token storage
48
     */
49
    public function __construct(
50
        RequestStack  $requestStack,
51
        TokenStorage  $tokenStorage) {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis of a multi-line function declaration must be on a new line
Loading history...
52
53
        $this->request = $requestStack ? $requestStack->getCurrentRequest() : false;
0 ignored issues
show
Documentation Bug introduced by
It seems like $requestStack ? $request...urrentRequest() : false can also be of type false. However, the property $request is declared as type object<Symfony\Component\HttpFoundation\Request>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
54
55
        $this->tokenStorage = $tokenStorage;
56
    }
57
58
    /**
59
     * Set permission and access configuration
60
     *
61
     * @param array $configurations key value config
62
     */
63
    public function setConfiguration(array $configurations)
64
    {
65
        $this->configurations = $configurations;
66
        if ($this->runTracking()) {
67
            $this->enabled = true;
68
        }
69
    }
70
71
    /**
72
     * Return casted value from configuration.
73
     *
74
     * @param string $key
75
     * @param string $cast
76
     * @return int|string
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|string|integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
77
     * @throws ParameterNotFoundException
78
     */
79
    private function getConfigValue($key, $cast='string')
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$cast" and equals sign; expected 1 but found 0
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$cast"; expected 1 but found 0
Loading history...
80
    {
81
        if (array_key_exists($key, $this->configurations)) {
82
            if ('bool' == $cast) {
83
                return (boolean) $this->configurations[$key];
84
            } elseif ('string' == $cast) {
85
                return (string) $this->configurations[$key];
86
            } elseif ('int' == $cast) {
87
                return (int) $this->configurations[$key];
88
            }
89
        }
90
        throw new ParameterNotFoundException('ActivityManager could not find required configuration: '.$key);
91
    }
92
93
    /**
94
     * Check if this the Call has to be logged
95
     *
96
     * @return bool
97
     */
98
    private function runTracking()
0 ignored issues
show
Coding Style introduced by
function runTracking() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
99
    {
100
        //Ignore if no request, import fixtures.
101
        if (!$this->request) {
102
            return false;
103
        }
104
105
        // Check if enable
106
        if (!$this->getConfigValue('log_enabled', 'bool')) {
107
            return false;
108
        }
109
        
110
        // We never log tracking service calls
111
        if ((strpos($this->request->getRequestUri(), '/_debug') !== false)) {
112
            return false;
113
        }
114
115
        // Check if we wanna log test and localhost calls
116
        if (!$this->getConfigValue('log_test_calls', 'bool')
117
            && !in_array($this->request->getHost(), ['localhost', '127.0.0.1'])) {
118
            return false;
119
        }
120
121
        return true;
122
    }
123
124
    /**
125
     * Last check before saving the Event into DB
126
     * 
127
     * @return bool|string
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|string.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
128
     */
129
    public function getSecurityUsername()
130
    {
131
        // No securityUser, no tracking
132
        if (!($this->securityUser = $this->getSecurityUser())) {
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getSecurityUser() of type object<Symfony\Component...ore\User\UserInterface> or false is incompatible with the declared type object<Graviton\Security...\Entities\SecurityUser> of property $securityUser.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
133
            return false;
134
        }
135
136
        // Check if we wanna log test and localhost calls
137
        if (!$this->getConfigValue('log_test_calls', 'bool')) {
138
            if (!$this->securityUser->hasRole(SecurityUser::ROLE_CONSULTANT)) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Security\Core\User\UserInterface as the method hasRole() does only exist in the following implementations of said interface: Graviton\SecurityBundle\Entities\SecurityUser.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
139
                return false;
140
            }
141
        }
142
        
143
        return $this->securityUser->getUsername();
144
    }
145
146
    /**
147
     * Incoming request done by user
148
     * @param Request $request sf response priority 1
149
     * @return void
150
     */
151
    public function registerRequestEvent(Request $request)
152
    {
153
        if (!$this->enabled) {
154
            return;
155
        }
156
        $content = substr($request->getContent(), 0, self::CONTENT_MAX_LENGTH);
157
158
        $data = ['ip' => $request->getClientIp()];
159
        
160
        if ($this->getConfigValue('request_headers', 'bool')) {
161
            $data['headers'] = $request->headers->all();
162
        }
163
        if ($length=$this->getConfigValue('request_content', 'int')) {
164
            $cnt = mb_check_encoding($content, 'UTF-8') ? $content : 'Content omitted, since it is not utf-8';
165
            $data['content'] = ($length==1) ? $cnt : substr($cnt, 0, $length);
166
        }
167
168
        $this->trackEvent('request', $request->getMethod(), $request->getRequestUri(), $data);
169
    }
170
171
    /**
172
     * The response returned to user
173
     *
174
     * @param Response $response sf response
175
     * @return void
176
     */
177
    public function registerResponseEvent(Response $response)
178
    {
179
        if (!$this->enabled) {
180
            return;
181
        }
182
        $data = [];
183
        $statusCode = '0';
184
185
        if (method_exists($response, 'getStatusCode')) {
186
            $statusCode = $response->getStatusCode();
187
        }
188
        if ($length=$this->getConfigValue('response_content', 'int') && method_exists($response, 'getContent')) {
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: $length = ($this->getCon...esponse, 'getContent')), Probably Intended Meaning: ($length = $this->getCon...response, 'getContent')
Loading history...
189
            $cnt = mb_check_encoding($response->getContent(), 'UTF-8') ?
190
                $response->getContent() : 'Content omitted, since it is not utf-8';
191
            $data['content'] = ($length==1) ? $cnt : substr($cnt, 0, $length);
192
            $data['header']  = $response->headers->all();
193
        }
194
        // Header links
195
        $location = $this->getHeaderLink($response->headers->get('link'), 'self');
196
197
        $this->trackEvent('response', $statusCode, $location, $data);
198
    }
199
200
    /**
201
     * Parse and extract customer header links
202
     *
203
     * @param string $links  sf header links
204
     * @param string $return desired key to be found
205
     * @return string
206
     */
207
    private function getHeaderLink($links, $return = 'self')
208
    {
209
        if (!$links) {
210
            return '';
211
        }
212
213
        $parts = [];
214
        $link = '';
215
        foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $links) as $key => $part) {
216
            if ($key%2) {
217
                $parts[str_replace(['rel=','"'], '', trim($part))] = $link;
218
            } else {
219
                $link = str_replace(['<','>'], '', $part);
220
            }
221
        }
222
223
        return  array_key_exists($return, $parts) ? $parts[$return] : '';
224
    }
225
226
    /**
227
     * Capture possible un-handled exceptions in php
228
     *
229
     * @param \Exception $exception
230
     * @return void
231
     */
232
    public function registerExceptionEvent(\Exception $exception)
233
    {
234
        if (!$this->enabled) {
235
            return;
236
        }
237
        $data = [
238
            'message'   => $exception->getMessage(),
239
            'trace'     => $exception->getTraceAsString()
240
        ];
241
        $this->trackEvent('exception', $exception->getCode(), '', $data);
242
    }
243
244
    /**
245
     * Any database events, update, save or delete
246
     *
247
     * @param string $action Is what was called in DbEvent
248
     * @param array $object Document object changed
249
     * @return void
250
     */
251
    public function registerDatabaseEvent($action, $object)
252
    {
253
        if (!$this->enabled) {
254
            return;
255
        }
256
        if ($object instanceof AuditTracking) {
257
            return;
258
        }
259
        if (method_exists($object, 'getId')) {
260
            $id = $object->getId();
0 ignored issues
show
Bug introduced by
The method getId cannot be called on $object (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
261
            $location = str_replace('MongoDBODMProxies\__CG__', '', get_class($object));
262
            if (strpos($location, 'Embedded') !== false) {
263
                return;
264
            }
265
        } else {
266
            $id = 'collection';
267
            $location = get_class(reset($object));
268
        }
269
        $data = [
270
            'id'    => $id,
271
            'count' => count($object),
272
            'data'  => ($id == 'collection') ? '' : substr(serialize($object), 0, self::CONTENT_MAX_LENGTH)
273
        ];
274
        $this->trackEvent($action, 'mongo', $location, $data);
275
    }
276
277
    
278
    /**
279
     * Find current user
280
     *
281
     * @return string|bool
0 ignored issues
show
Documentation introduced by
Should the return type not be UserInterface|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
282
     */
283 View Code Duplication
    private function getSecurityUser()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
284
    {
285
        /** @var PreAuthenticatedToken $token */
286
        if (($token = $this->tokenStorage->getToken())
287
            && ($user = $token->getUser()) instanceof UserInterface ) {
288
            return $user;
289
        }
290
        return false;
291
    }
292
293
    /**
294
     * Save the event to DB
295
     *
296
     * @param string $action   Performed by user
297
     * @param string $type     Method done
298
     * @param string $location Where was it executed
299
     * @param array  $data     With what parameters
300
     * @return void
301
     */
302
    private function trackEvent($action, $type, $location, $data) {
303
        $this->events[] =[
304
            'action'   => $action,
305
            'type'     => $type,
306
            'location' => $location,
307
            'data'     => $data
308
        ];
309
    }
310
311
    public function getEvents()
312
    {
313
        return $this->events;
314
    }
315
}