Completed
Push — master ( 9e5158...e36c0e )
by Damian
10s
created

AuditHook::handle_manipulation()   D

Complexity

Conditions 19
Paths 85

Size

Total Lines 102
Code Lines 68

Duplication

Lines 42
Ratio 41.18 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 42
loc 102
rs 4.764
cc 19
eloc 68
nc 85
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Auditor;
4
5
/**
6
 * Provides logging hooks that are inserted into Framework objects.
7
 */
8
class AuditHook extends \SiteTreeExtension
9
{
10
11
	protected function getAuditLogger() {
12
		// We cannot use the 'dependencies' private property, because this will prevent us
13
		// from injecting a mock logger for testing. This is because by the time the testing framework
14
		// is instantiated, the part of the object graph where AuditLogger lives has already been created.
15
		// In other words, Framework does not permit hooking in early enough to adjust the graph when
16
		// 'dependencies' is used :-(
17
		return \Injector::inst()->get('AuditLogger');
18
	}
19
20
    /**
21
     * This will bind a new class dynamically so we can hook into manipulation
22
     * and capture it. It creates a new PHP file in the temp folder, then
23
     * loads it and sets it as the active DB class.
24
     */
25
    public static function bind_manipulation_capture()
26
    {
27
        global $databaseConfig;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
28
29
        $current = \DB::getConn();
0 ignored issues
show
Deprecated Code introduced by
The method DB::getConn() has been deprecated with message: since version 4.0 Use DB::get_conn instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
30
        if (!$current || !$current->currentDatabase() || @$current->isManipulationLoggingCapture) {
0 ignored issues
show
Bug introduced by
The property isManipulationLoggingCapture does not seem to exist in SS_Database.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
Deprecated Code introduced by
The method SS_Database::currentDatabase() has been deprecated with message: since version 4.0 Use getSelectedDatabase instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
31
            return;
32
        } // If not yet set, or its already captured, just return
33
34
        $type = get_class($current);
35
        $file = TEMP_FOLDER."/.cache.CLC.$type";
36
        $dbClass = 'AuditLoggerManipulateCapture_'.$type;
37
38
        if (!is_file($file)) {
39
            file_put_contents($file, "<?php
40
				class $dbClass extends $type {
41
					public \$isManipulationLoggingCapture = true;
42
43
					public function manipulate(\$manipulation) {
44
						\SilverStripe\Auditor\AuditHook::handle_manipulation(\$manipulation);
45
						return parent::manipulate(\$manipulation);
46
					}
47
				}
48
			");
49
        }
50
51
        require_once $file;
52
53
        /** @var SS_Database $captured */
54
        $captured = new $dbClass($databaseConfig);
55
56
        // Framework 3.2+ ORM needs some dependencies set
57
        if (method_exists($captured, 'setConnector')) {
58
            $captured->setConnector($current->getConnector());
59
            $captured->setQueryBuilder($current->getQueryBuilder());
60
            $captured->setSchemaManager($current->getSchemaManager());
61
        }
62
63
        // The connection might have had it's name changed (like if we're currently in a test)
64
        $captured->selectDatabase($current->currentDatabase());
0 ignored issues
show
Deprecated Code introduced by
The method SS_Database::currentDatabase() has been deprecated with message: since version 4.0 Use getSelectedDatabase instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
65
66
        \DB::setConn($captured);
0 ignored issues
show
Deprecated Code introduced by
The method DB::setConn() has been deprecated with message: since version 4.0 Use DB::set_conn instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
67
    }
68
69
    public static function handle_manipulation($manipulation)
0 ignored issues
show
Coding Style introduced by
handle_manipulation uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
70
    {
71
		$auditLogger = \Injector::inst()->get('AuditLogger');
72
73
        $currentMember = \Member::currentUser();
74
        if (!($currentMember && $currentMember->exists())) {
75
            return false;
76
        }
77
78
        foreach ($manipulation as $table => $details) {
79
            if (!in_array($details['command'], array('update', 'insert'))) {
80
                continue;
81
            }
82
83
            // logging writes to specific tables (just not when logging in, as it's noise)
84
            if (in_array($table, array('Member', 'Group', 'PermissionRole')) && !preg_match('/Security/', @$_SERVER['REQUEST_URI'])) {
85
                $data = $table::get()->byId($details['id']);
86
                if (!$data) {
87
                    continue;
88
                }
89
                $actionText = 'modified '.$table;
90
91
                $extendedText = '';
92
                if ($table == 'Group') {
93
                    $extendedText = sprintf(
94
                        'Effective permissions: %s',
95
                        implode(array_values($data->Permissions()->map('ID', 'Code')->toArray()), ', ')
96
                    );
97
                }
98
                if ($table == 'PermissionRole') {
99
                    $extendedText = sprintf(
100
                        'Effective groups: %s, Effective permissions: %s',
101
                        implode(array_values($data->Groups()->map('ID', 'Title')->toArray()), ', '),
102
                        implode(array_values($data->Codes()->map('ID', 'Code')->toArray()), ', ')
103
                    );
104
                }
105
                if ($table == 'Member') {
106
                    $extendedText = sprintf(
107
                        'Effective groups: %s',
108
                        implode(array_values($data->Groups()->map('ID', 'Title')->toArray()), ', ')
109
                    );
110
                }
111
112
                $auditLogger->info(sprintf(
113
                    '"%s" (ID: %s) %s (ID: %s, ClassName: %s, Title: "%s", %s)',
114
                    $currentMember->Email ?: $currentMember->Title,
115
                    $currentMember->ID,
116
                    $actionText,
117
                    $details['id'],
118
                    $data->ClassName,
119
                    $data->Title,
120
                    $extendedText
121
                ));
122
            }
123
124
            // log PermissionRole being added to a Group
125 View Code Duplication
            if ($table == 'Group_Roles') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
126
                $role = \PermissionRole::get()->byId($details['fields']['PermissionRoleID']);
127
                $group = Group::get()->byId($details['fields']['GroupID']);
128
129
                // if the permission role isn't already applied to the group
130
                if (!DB::query(sprintf(
131
                    'SELECT "ID" FROM "Group_Roles" WHERE "GroupID" = %s AND "PermissionRoleID" = %s',
132
                    $details['fields']['GroupID'],
133
                    $details['fields']['PermissionRoleID']
134
                ))->value()) {
135
                    $auditLogger->info(sprintf(
136
                        '"%s" (ID: %s) added PermissionRole "%s" (ID: %s) to Group "%s" (ID: %s)',
137
                        $currentMember->Email ?: $currentMember->Title,
138
                        $currentMember->ID,
139
                        $role->Title,
140
                        $role->ID,
141
                        $group->Title,
142
                        $group->ID
143
                    ));
144
                }
145
            }
146
147
            // log Member added to a Group
148 View Code Duplication
            if ($table == 'Group_Members') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
149
                $member = \Member::get()->byId($details['fields']['MemberID']);
150
                $group = \Group::get()->byId($details['fields']['GroupID']);
151
152
                // if the user isn't already in the group, log they've been added
153
                if (!\DB::query(sprintf(
154
                    'SELECT "ID" FROM "Group_Members" WHERE "GroupID" = %s AND "MemberID" = %s',
155
                    $details['fields']['GroupID'],
156
                    $details['fields']['MemberID']
157
                ))->value()) {
158
                    $auditLogger->info(sprintf(
159
                        '"%s" (ID: %s) added Member "%s" (ID: %s) to Group "%s" (ID: %s)',
160
                        $currentMember->Email ?: $currentMember->Title,
161
                        $currentMember->ID,
162
                        $member->Email ?: $member->Title,
163
                        $member->ID,
164
                        $group->Title,
165
                        $group->ID
166
                    ));
167
                }
168
            }
169
        }
170
    }
171
172
    /**
173
     * Log a record being published.
174
     */
175
    public function onAfterPublish(&$original)
176
    {
177
        $member = \Member::currentUser();
178
        if (!($member && $member->exists())) {
179
            return false;
180
        }
181
182
        $effectiveViewerGroups = '';
183
        if ($this->owner->CanViewType == 'OnlyTheseUsers') {
184
            $effectiveViewerGroups = implode(array_values($original->ViewerGroups()->map('ID', 'Title')->toArray()), ', ');
185
        }
186
        if (!$effectiveViewerGroups) {
187
            $effectiveViewerGroups = $this->owner->CanViewType;
188
        }
189
190
        $effectiveEditorGroups = '';
191
        if ($this->owner->CanEditType == 'OnlyTheseUsers' && $original->EditorGroups()->exists()) {
192
            $groups = array();
193
            foreach ($original->EditorGroups() as $group) {
194
                $groups[$group->ID] = $group->Title;
195
            }
196
            $effectiveEditorGroups = implode(array_values($groups), ', ');
197
        }
198
        if (!$effectiveEditorGroups) {
199
            $effectiveEditorGroups = $this->owner->CanEditType;
200
        }
201
202
        $this->getAuditLogger()->info(sprintf(
203
            '"%s" (ID: %s) published %s "%s" (ID: %s, Version: %s, ClassName: %s, Effective ViewerGroups: %s, Effective EditorGroups: %s)',
204
            $member->Email ?: $member->Title,
205
            $member->ID,
206
            $this->owner->singular_name(),
207
            $this->owner->Title,
208
            $this->owner->ID,
209
            $this->owner->Version,
210
            $this->owner->ClassName,
211
            $effectiveViewerGroups,
212
            $effectiveEditorGroups
213
        ));
214
    }
215
216
    /**
217
     * Log a record being unpublished.
218
     */
219 View Code Duplication
    public function onAfterUnpublish()
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...
220
    {
221
        $member = \Member::currentUser();
222
        if (!($member && $member->exists())) {
223
            return false;
224
        }
225
226
        $this->getAuditLogger()->info(sprintf(
227
            '"%s" (ID: %s) unpublished %s "%s" (ID: %s)',
228
            $member->Email ?: $member->Title,
229
            $member->ID,
230
            $this->owner->singular_name(),
231
            $this->owner->Title,
232
            $this->owner->ID
233
        ));
234
    }
235
236
    /**
237
     * Log a record being reverted to live.
238
     */
239 View Code Duplication
    public function onAfterRevertToLive()
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...
240
    {
241
        $member = \Member::currentUser();
242
        if (!($member && $member->exists())) {
243
            return false;
244
        }
245
246
        $this->getAuditLogger()->info(sprintf(
247
            '"%s" (ID: %s) reverted %s "%s" (ID: %s) to it\'s live version (#%d)',
248
            $member->Email ?: $member->Title,
249
            $member->ID,
250
            $this->owner->singular_name(),
251
            $this->owner->Title,
252
            $this->owner->ID,
253
            $this->owner->Version
254
        ));
255
    }
256
257
    /**
258
     * Log a record being duplicated.
259
     */
260 View Code Duplication
    public function onAfterDuplicate()
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...
261
    {
262
        $member = \Member::currentUser();
263
        if (!($member && $member->exists())) {
264
            return false;
265
        }
266
267
        $this->getAuditLogger()->info(sprintf(
268
            '"%s" (ID: %s) duplicated %s "%s" (ID: %s)',
269
            $member->Email ?: $member->Title,
270
            $member->ID,
271
            $this->owner->singular_name(),
272
            $this->owner->Title,
273
            $this->owner->ID
274
        ));
275
    }
276
277
    /**
278
     * Log a record being deleted.
279
     */
280 View Code Duplication
    public function onAfterDelete()
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...
281
    {
282
        $member = \Member::currentUser();
283
        if (!($member && $member->exists())) {
284
            return false;
285
        }
286
287
        $this->getAuditLogger()->info(sprintf(
288
            '"%s" (ID: %s) deleted %s "%s" (ID: %s)',
289
            $member->Email ?: $member->Title,
290
            $member->ID,
291
            $this->owner->singular_name(),
292
            $this->owner->Title,
293
            $this->owner->ID
294
        ));
295
    }
296
297
    /**
298
     * Log a record being restored to stage.
299
     */
300 View Code Duplication
    public function onAfterRestoreToStage()
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...
301
    {
302
        $member = \Member::currentUser();
303
        if (!($member && $member->exists())) {
304
            return false;
305
        }
306
307
        $this->getAuditLogger()->info(sprintf(
308
            '"%s" (ID: %s) restored %s "%s" to stage (ID: %s)',
309
            $member->Email ?: $member->Title,
310
            $member->ID,
311
            $this->owner->singular_name(),
312
            $this->owner->Title,
313
            $this->owner->ID
314
        ));
315
    }
316
317
    /**
318
     * Log successful login attempts.
319
     */
320 View Code Duplication
    public function memberLoggedIn()
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...
321
    {
322
        $this->getAuditLogger()->info(sprintf(
323
            '"%s" (ID: %s) successfully logged in',
324
            $this->owner->Email ?: $this->owner->Title,
325
            $this->owner->ID
326
        ));
327
    }
328
329
    /**
330
     * Log successfully restored sessions from "remember me" cookies ("auto login").
331
     */
332 View Code Duplication
    public function memberAutoLoggedIn()
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...
333
    {
334
        $this->getAuditLogger()->info(sprintf(
335
            '"%s" (ID: %s) successfully restored autologin session',
336
            $this->owner->Email ?: $this->owner->Title,
337
            $this->owner->ID
338
        ));
339
    }
340
341
    /**
342
     * Log failed login attempts.
343
     */
344
    public function authenticationFailed($data)
345
    {
346
        // LDAP authentication uses a "Login" POST field instead of Email.
347
        $login = isset($data['Login'])
348
            ? $data['Login']
349
            : (isset($data['Email']) ? $data['Email'] : '');
350
351
        if (empty($login)) {
352
            return $this->getAuditLogger()->warning(
353
                'Could not determine username/email of failed authentication. '.
354
				'This could be due to login form not using Email or Login field for POST data.'
355
            );
356
        }
357
358
        $this->getAuditLogger()->info(sprintf('Failed login attempt using email "%s"', $login));
359
    }
360
361
    public function onBeforeInit()
362
    {
363
        self::bind_manipulation_capture();
364
    }
365
366
    /**
367
     * Log permission failures (where the status is set after init of page).
368
     */
369
    public function onAfterInit()
370
    {
371
        // Suppress errors if dev/build necessary
372
        if (!\Security::database_is_ready()) {
373
            return false;
374
        }
375
        $currentMember = \Member::currentUser();
376
        if (!($currentMember && $currentMember->exists())) {
377
            return false;
378
        }
379
380
        $statusCode = $this->owner->getResponse()->getStatusCode();
381
382
        if (substr($statusCode, 0, 1) == '4') {
383
            $this->logPermissionDenied($statusCode, $currentMember);
384
        }
385
    }
386
387
    protected function logPermissionDenied($statusCode, $member)
0 ignored issues
show
Coding Style introduced by
logPermissionDenied uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
388
    {
389
        $this->getAuditLogger()->info(sprintf(
390
            'HTTP code %s - "%s" (ID: %s) is denied access to %s',
391
            $statusCode,
392
            $member->Email ?: $member->Title,
393
            $member->ID,
394
            $_SERVER['REQUEST_URI']
395
        ));
396
    }
397
398
    /**
399
     * Log successful logout.
400
     */
401 View Code Duplication
    public function memberLoggedOut()
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...
402
    {
403
        $this->getAuditLogger()->info(sprintf(
404
            '"%s" (ID: %s) successfully logged out',
405
            $this->owner->Email ?: $this->owner->Title,
406
            $this->owner->ID
407
        ));
408
    }
409
}
410