SecurityComponent   F
last analyzed

Complexity

Total Complexity 95

Size/Duplication

Total Lines 634
Duplicated Lines 4.42 %

Coupling/Cohesion

Components 2
Dependencies 12

Importance

Changes 0
Metric Value
dl 28
loc 634
rs 1.966
c 0
b 0
f 0
wmc 95
lcom 2
cbo 12

21 Methods

Rating   Name   Duplication   Size   Complexity  
A implementedEvents() 0 6 1
A requireSecure() 0 4 1
A requireAuth() 0 5 1
A blackHole() 0 8 2
A _throwException() 0 11 4
A _requireMethod() 0 7 4
B startup() 0 36 10
A _secureRequired() 0 19 6
C _authRequired() 22 52 12
A _validatePost() 0 17 3
B _validToken() 6 28 9
A _hashParts() 0 19 1
D _fieldsList() 0 62 13
A _unlocked() 0 4 1
A _sortedUnlocked() 0 8 1
B _debugPostTokenNotMatching() 0 41 7
A _debugCheckFields() 0 10 2
A generateToken() 0 21 3
A _callback() 0 8 3
B _matchExistingFields() 0 21 7
A _debugExpectedFields() 0 17 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SecurityComponent often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SecurityComponent, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11
 * @link          https://cakephp.org CakePHP(tm) Project
12
 * @since         0.10.8
13
 * @license       https://opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\Controller\Component;
16
17
use Cake\Controller\Component;
18
use Cake\Controller\Controller;
19
use Cake\Controller\Exception\AuthSecurityException;
20
use Cake\Controller\Exception\SecurityException;
21
use Cake\Core\Configure;
22
use Cake\Event\Event;
23
use Cake\Http\Exception\BadRequestException;
24
use Cake\Http\ServerRequest;
25
use Cake\Routing\Router;
26
use Cake\Utility\Hash;
27
use Cake\Utility\Security;
28
29
/**
30
 * The Security Component creates an easy way to integrate tighter security in
31
 * your application. It provides methods for various tasks like:
32
 *
33
 * - Restricting which HTTP methods your application accepts.
34
 * - Form tampering protection
35
 * - Requiring that SSL be used.
36
 * - Limiting cross controller communication.
37
 *
38
 * @link https://book.cakephp.org/3/en/controllers/components/security.html
39
 */
40
class SecurityComponent extends Component
41
{
42
    /**
43
     * Default message used for exceptions thrown
44
     */
45
    const DEFAULT_EXCEPTION_MESSAGE = 'The request has been black-holed';
46
47
    /**
48
     * Default config
49
     *
50
     * - `blackHoleCallback` - The controller method that will be called if this
51
     *   request is black-hole'd.
52
     * - `requireSecure` - List of actions that require an SSL-secured connection.
53
     * - `requireAuth` - List of actions that require a valid authentication key. Deprecated as of 3.2.2
54
     * - `allowedControllers` - Controllers from which actions of the current
55
     *   controller are allowed to receive requests.
56
     * - `allowedActions` - Actions from which actions of the current controller
57
     *   are allowed to receive requests.
58
     * - `unlockedFields` - Form fields to exclude from POST validation. Fields can
59
     *   be unlocked either in the Component, or with FormHelper::unlockField().
60
     *   Fields that have been unlocked are not required to be part of the POST
61
     *   and hidden unlocked fields do not have their values checked.
62
     * - `unlockedActions` - Actions to exclude from POST validation checks.
63
     *   Other checks like requireAuth(), requireSecure() etc. will still be applied.
64
     * - `validatePost` - Whether to validate POST data. Set to false to disable
65
     *   for data coming from 3rd party services, etc.
66
     *
67
     * @var array
68
     */
69
    protected $_defaultConfig = [
70
        'blackHoleCallback' => null,
71
        'requireSecure' => [],
72
        'requireAuth' => [],
73
        'allowedControllers' => [],
74
        'allowedActions' => [],
75
        'unlockedFields' => [],
76
        'unlockedActions' => [],
77
        'validatePost' => true,
78
    ];
79
80
    /**
81
     * Holds the current action of the controller
82
     *
83
     * @var string
84
     */
85
    protected $_action;
86
87
    /**
88
     * The Session object
89
     *
90
     * @var \Cake\Http\Session
91
     */
92
    public $session;
93
94
    /**
95
     * Component startup. All security checking happens here.
96
     *
97
     * @param \Cake\Event\Event $event An Event instance
98
     * @return mixed
99
     */
100
    public function startup(Event $event)
101
    {
102
        /** @var \Cake\Controller\Controller $controller */
103
        $controller = $event->getSubject();
104
        $request = $controller->request;
105
        $this->session = $request->getSession();
106
        $this->_action = $request->getParam('action');
107
        $hasData = ($request->getData() || $request->is(['put', 'post', 'delete', 'patch']));
108
        try {
109
            $this->_secureRequired($controller);
110
            $this->_authRequired($controller);
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Controller\Componen...ponent::_authRequired() has been deprecated with message: 3.2.2 This feature is confusing and not useful.

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...
111
112
            $isNotRequestAction = !$request->getParam('requested');
113
114
            if ($this->_action === $this->_config['blackHoleCallback']) {
115
                throw new AuthSecurityException(sprintf('Action %s is defined as the blackhole callback.', $this->_action));
116
            }
117
118
            if (
119
                !in_array($this->_action, (array)$this->_config['unlockedActions']) &&
120
                $hasData &&
121
                $isNotRequestAction &&
122
                $this->_config['validatePost']
123
            ) {
124
                $this->_validatePost($controller);
125
            }
126
        } catch (SecurityException $se) {
127
            return $this->blackHole($controller, $se->getType(), $se);
128
        }
129
130
        $request = $this->generateToken($request);
131
        if ($hasData && is_array($controller->getRequest()->getData())) {
132
            $request = $request->withoutData('_Token');
133
        }
134
        $controller->setRequest($request);
135
    }
136
137
    /**
138
     * Events supported by this component.
139
     *
140
     * @return array
141
     */
142
    public function implementedEvents()
143
    {
144
        return [
145
            'Controller.startup' => 'startup',
146
        ];
147
    }
148
149
    /**
150
     * Sets the actions that require a request that is SSL-secured, or empty for all actions
151
     *
152
     * @param string|array|null $actions Actions list
153
     * @return void
154
     */
155
    public function requireSecure($actions = null)
156
    {
157
        $this->_requireMethod('Secure', (array)$actions);
158
    }
159
160
    /**
161
     * Sets the actions that require whitelisted form submissions.
162
     *
163
     * Adding actions with this method will enforce the restrictions
164
     * set in SecurityComponent::$allowedControllers and
165
     * SecurityComponent::$allowedActions.
166
     *
167
     * @param string|array $actions Actions list
168
     * @return void
169
     * @deprecated 3.2.2 This feature is confusing and not useful.
170
     */
171
    public function requireAuth($actions)
172
    {
173
        deprecationWarning('SecurityComponent::requireAuth() will be removed in 4.0.0.');
174
        $this->_requireMethod('Auth', (array)$actions);
175
    }
176
177
    /**
178
     * Black-hole an invalid request with a 400 error or custom callback. If SecurityComponent::$blackHoleCallback
179
     * is specified, it will use this callback by executing the method indicated in $error
180
     *
181
     * @param \Cake\Controller\Controller $controller Instantiating controller
182
     * @param string $error Error method
183
     * @param \Cake\Controller\Exception\SecurityException|null $exception Additional debug info describing the cause
184
     * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise
185
     * @see \Cake\Controller\Component\SecurityComponent::$blackHoleCallback
186
     * @link https://book.cakephp.org/3/en/controllers/components/security.html#handling-blackhole-callbacks
187
     * @throws \Cake\Http\Exception\BadRequestException
188
     */
189
    public function blackHole(Controller $controller, $error = '', SecurityException $exception = null)
190
    {
191
        if (!$this->_config['blackHoleCallback']) {
192
            $this->_throwException($exception);
193
        }
194
195
        return $this->_callback($controller, $this->_config['blackHoleCallback'], [$error, $exception]);
196
    }
197
198
    /**
199
     * Check debug status and throw an Exception based on the existing one
200
     *
201
     * @param \Cake\Controller\Exception\SecurityException|null $exception Additional debug info describing the cause
202
     * @throws \Cake\Http\Exception\BadRequestException
203
     * @return void
204
     */
205
    protected function _throwException($exception = null)
206
    {
207
        if ($exception !== null) {
208
            if (!Configure::read('debug') && $exception instanceof SecurityException) {
209
                $exception->setReason($exception->getMessage());
210
                $exception->setMessage(self::DEFAULT_EXCEPTION_MESSAGE);
211
            }
212
            throw $exception;
213
        }
214
        throw new BadRequestException(self::DEFAULT_EXCEPTION_MESSAGE);
215
    }
216
217
    /**
218
     * Sets the actions that require a $method HTTP request, or empty for all actions
219
     *
220
     * @param string $method The HTTP method to assign controller actions to
221
     * @param array $actions Controller actions to set the required HTTP method to.
222
     * @return void
223
     */
224
    protected function _requireMethod($method, $actions = [])
225
    {
226
        if (isset($actions[0]) && is_array($actions[0])) {
227
            $actions = $actions[0];
228
        }
229
        $this->setConfig('require' . $method, empty($actions) ? ['*'] : $actions);
230
    }
231
232
    /**
233
     * Check if access requires secure connection
234
     *
235
     * @param \Cake\Controller\Controller $controller Instantiating controller
236
     * @return bool true if secure connection required
237
     */
238
    protected function _secureRequired(Controller $controller)
239
    {
240
        if (
241
            is_array($this->_config['requireSecure']) &&
242
            !empty($this->_config['requireSecure'])
243
        ) {
244
            $requireSecure = $this->_config['requireSecure'];
245
246
            if (in_array($this->_action, $requireSecure) || $requireSecure === ['*']) {
247
                if (!$this->getController()->getRequest()->is('ssl')) {
248
                    throw new SecurityException(
249
                        'Request is not SSL and the action is required to be secure'
250
                    );
251
                }
252
            }
253
        }
254
255
        return true;
256
    }
257
258
    /**
259
     * Check if authentication is required
260
     *
261
     * @param \Cake\Controller\Controller $controller Instantiating controller
262
     * @return bool true if authentication required
263
     * @deprecated 3.2.2 This feature is confusing and not useful.
264
     */
265
    protected function _authRequired(Controller $controller)
266
    {
267
        $request = $controller->getRequest();
268
        if (
269
            is_array($this->_config['requireAuth']) &&
270
            !empty($this->_config['requireAuth']) &&
271
            $request->getData()
272
        ) {
273
            deprecationWarning('SecurityComponent::requireAuth() will be removed in 4.0.0.');
274
            $requireAuth = $this->_config['requireAuth'];
275
276
            if (in_array($request->getParam('action'), $requireAuth) || $requireAuth == ['*']) {
277
                if ($request->getData('_Token') === null) {
278
                    throw new AuthSecurityException('\'_Token\' was not found in request data.');
279
                }
280
281
                if ($this->session->check('_Token')) {
282
                    $tData = $this->session->read('_Token');
283
284 View Code Duplication
                    if (
285
                        !empty($tData['allowedControllers']) &&
286
                        !in_array($request->getParam('controller'), $tData['allowedControllers'])
287
                    ) {
288
                        throw new AuthSecurityException(
289
                            sprintf(
290
                                'Controller \'%s\' was not found in allowed controllers: \'%s\'.',
291
                                $request->getParam('controller'),
292
                                implode(', ', (array)$tData['allowedControllers'])
293
                            )
294
                        );
295
                    }
296 View Code Duplication
                    if (
297
                        !empty($tData['allowedActions']) &&
298
                        !in_array($request->getParam('action'), $tData['allowedActions'])
299
                    ) {
300
                        throw new AuthSecurityException(
301
                            sprintf(
302
                                'Action \'%s::%s\' was not found in allowed actions: \'%s\'.',
303
                                $request->getParam('controller'),
304
                                $request->getParam('action'),
305
                                implode(', ', (array)$tData['allowedActions'])
306
                            )
307
                        );
308
                    }
309
                } else {
310
                    throw new AuthSecurityException('\'_Token\' was not found in session.');
311
                }
312
            }
313
        }
314
315
        return true;
316
    }
317
318
    /**
319
     * Validate submitted form
320
     *
321
     * @param \Cake\Controller\Controller $controller Instantiating controller
322
     * @throws \Cake\Controller\Exception\AuthSecurityException
323
     * @return bool true if submitted form is valid
324
     */
325
    protected function _validatePost(Controller $controller)
326
    {
327
        $token = $this->_validToken($controller);
328
        $hashParts = $this->_hashParts($controller);
329
        $check = hash_hmac('sha1', implode('', $hashParts), Security::getSalt());
330
331
        if (hash_equals($check, $token)) {
332
            return true;
333
        }
334
335
        $msg = self::DEFAULT_EXCEPTION_MESSAGE;
336
        if (Configure::read('debug')) {
337
            $msg = $this->_debugPostTokenNotMatching($controller, $hashParts);
338
        }
339
340
        throw new AuthSecurityException($msg);
341
    }
342
343
    /**
344
     * Check if token is valid
345
     *
346
     * @param \Cake\Controller\Controller $controller Instantiating controller
347
     * @throws \Cake\Controller\Exception\SecurityException
348
     * @return string fields token
349
     */
350
    protected function _validToken(Controller $controller)
351
    {
352
        $check = $controller->getRequest()->getData();
353
354
        $message = '\'%s\' was not found in request data.';
355
        if (!isset($check['_Token'])) {
356
            throw new AuthSecurityException(sprintf($message, '_Token'));
357
        }
358
        if (!isset($check['_Token']['fields'])) {
359
            throw new AuthSecurityException(sprintf($message, '_Token.fields'));
360
        }
361
        if (!isset($check['_Token']['unlocked'])) {
362
            throw new AuthSecurityException(sprintf($message, '_Token.unlocked'));
363
        }
364 View Code Duplication
        if (Configure::read('debug') && !isset($check['_Token']['debug'])) {
365
            throw new SecurityException(sprintf($message, '_Token.debug'));
366
        }
367 View Code Duplication
        if (!Configure::read('debug') && isset($check['_Token']['debug'])) {
368
            throw new SecurityException('Unexpected \'_Token.debug\' found in request data');
369
        }
370
371
        $token = urldecode($check['_Token']['fields']);
372
        if (strpos($token, ':')) {
373
            list($token, ) = explode(':', $token, 2);
374
        }
375
376
        return $token;
377
    }
378
379
    /**
380
     * Return hash parts for the Token generation
381
     *
382
     * @param \Cake\Controller\Controller $controller Instantiating controller
383
     * @return array
384
     */
385
    protected function _hashParts(Controller $controller)
386
    {
387
        $request = $controller->getRequest();
388
389
        // Start the session to ensure we get the correct session id.
390
        $session = $request->getSession();
391
        $session->start();
392
393
        $data = $request->getData();
394
        $fieldList = $this->_fieldsList($data);
395
        $unlocked = $this->_sortedUnlocked($data);
396
397
        return [
398
            Router::url($request->getRequestTarget()),
399
            serialize($fieldList),
400
            $unlocked,
401
            $session->id(),
402
        ];
403
    }
404
405
    /**
406
     * Return the fields list for the hash calculation
407
     *
408
     * @param array $check Data array
409
     * @return array
410
     */
411
    protected function _fieldsList(array $check)
412
    {
413
        $locked = '';
414
        $token = urldecode($check['_Token']['fields']);
415
        $unlocked = $this->_unlocked($check);
416
417
        if (strpos($token, ':')) {
418
            list($token, $locked) = explode(':', $token, 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $token is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
419
        }
420
        unset($check['_Token'], $check['_csrfToken']);
421
422
        $locked = explode('|', $locked);
423
        $unlocked = explode('|', $unlocked);
424
425
        $fields = Hash::flatten($check);
426
        $fieldList = array_keys($fields);
427
        $multi = $lockedFields = [];
428
        $isUnlocked = false;
429
430
        foreach ($fieldList as $i => $key) {
431
            if (preg_match('/(\.\d+){1,10}$/', $key)) {
432
                $multi[$i] = preg_replace('/(\.\d+){1,10}$/', '', $key);
433
                unset($fieldList[$i]);
434
            } else {
435
                $fieldList[$i] = (string)$key;
436
            }
437
        }
438
        if (!empty($multi)) {
439
            $fieldList += array_unique($multi);
440
        }
441
442
        $unlockedFields = array_unique(
443
            array_merge((array)$this->getConfig('disabledFields'), (array)$this->_config['unlockedFields'], $unlocked)
444
        );
445
446
        foreach ($fieldList as $i => $key) {
447
            $isLocked = (is_array($locked) && in_array($key, $locked));
448
449
            if (!empty($unlockedFields)) {
450
                foreach ($unlockedFields as $off) {
451
                    $off = explode('.', $off);
452
                    $field = array_values(array_intersect(explode('.', $key), $off));
453
                    $isUnlocked = ($field === $off);
454
                    if ($isUnlocked) {
455
                        break;
456
                    }
457
                }
458
            }
459
460
            if ($isUnlocked || $isLocked) {
461
                unset($fieldList[$i]);
462
                if ($isLocked) {
463
                    $lockedFields[$key] = $fields[$key];
464
                }
465
            }
466
        }
467
        sort($fieldList, SORT_STRING);
468
        ksort($lockedFields, SORT_STRING);
469
        $fieldList += $lockedFields;
470
471
        return $fieldList;
472
    }
473
474
    /**
475
     * Get the unlocked string
476
     *
477
     * @param array $data Data array
478
     * @return string
479
     */
480
    protected function _unlocked(array $data)
481
    {
482
        return urldecode($data['_Token']['unlocked']);
483
    }
484
485
    /**
486
     * Get the sorted unlocked string
487
     *
488
     * @param array $data Data array
489
     * @return string
490
     */
491
    protected function _sortedUnlocked($data)
492
    {
493
        $unlocked = $this->_unlocked($data);
494
        $unlocked = explode('|', $unlocked);
495
        sort($unlocked, SORT_STRING);
496
497
        return implode('|', $unlocked);
498
    }
499
500
    /**
501
     * Create a message for humans to understand why Security token is not matching
502
     *
503
     * @param \Cake\Controller\Controller $controller Instantiating controller
504
     * @param array $hashParts Elements used to generate the Token hash
505
     * @return string Message explaining why the tokens are not matching
506
     */
507
    protected function _debugPostTokenNotMatching(Controller $controller, $hashParts)
508
    {
509
        $messages = [];
510
        $expectedParts = json_decode(urldecode($controller->getRequest()->getData('_Token.debug')), true);
511
        if (!is_array($expectedParts) || count($expectedParts) !== 3) {
512
            return 'Invalid security debug token.';
513
        }
514
        $expectedUrl = Hash::get($expectedParts, 0);
515
        $url = Hash::get($hashParts, 0);
516
        if ($expectedUrl !== $url) {
517
            $messages[] = sprintf('URL mismatch in POST data (expected \'%s\' but found \'%s\')', $expectedUrl, $url);
518
        }
519
        $expectedFields = Hash::get($expectedParts, 1);
520
        $dataFields = Hash::get($hashParts, 1);
521
        if ($dataFields) {
522
            $dataFields = unserialize($dataFields);
523
        }
524
        $fieldsMessages = $this->_debugCheckFields(
525
            $dataFields,
526
            $expectedFields,
527
            'Unexpected field \'%s\' in POST data',
528
            'Tampered field \'%s\' in POST data (expected value \'%s\' but found \'%s\')',
529
            'Missing field \'%s\' in POST data'
530
        );
531
        $expectedUnlockedFields = Hash::get($expectedParts, 2);
532
        $dataUnlockedFields = Hash::get($hashParts, 2) ?: null;
533
        if ($dataUnlockedFields) {
534
            $dataUnlockedFields = explode('|', $dataUnlockedFields);
535
        }
536
        $unlockFieldsMessages = $this->_debugCheckFields(
537
            (array)$dataUnlockedFields,
538
            $expectedUnlockedFields,
539
            'Unexpected unlocked field \'%s\' in POST data',
540
            null,
541
            'Missing unlocked field: \'%s\''
542
        );
543
544
        $messages = array_merge($messages, $fieldsMessages, $unlockFieldsMessages);
545
546
        return implode(', ', $messages);
547
    }
548
549
    /**
550
     * Iterates data array to check against expected
551
     *
552
     * @param array $dataFields Fields array, containing the POST data fields
553
     * @param array $expectedFields Fields array, containing the expected fields we should have in POST
554
     * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected)
555
     * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected)
556
     * @param string $missingMessage Message string if missing field
557
     * @return array Messages
558
     */
559
    protected function _debugCheckFields($dataFields, $expectedFields = [], $intKeyMessage = '', $stringKeyMessage = '', $missingMessage = '')
560
    {
561
        $messages = $this->_matchExistingFields($dataFields, $expectedFields, $intKeyMessage, $stringKeyMessage);
562
        $expectedFieldsMessage = $this->_debugExpectedFields($expectedFields, $missingMessage);
563
        if ($expectedFieldsMessage !== null) {
564
            $messages[] = $expectedFieldsMessage;
565
        }
566
567
        return $messages;
568
    }
569
570
    /**
571
     * Manually add form tampering prevention token information into the provided
572
     * request object.
573
     *
574
     * @param \Cake\Http\ServerRequest $request The request object to add into.
575
     * @return \Cake\Http\ServerRequest The modified request.
576
     */
577
    public function generateToken(ServerRequest $request)
578
    {
579
        if ($request->is('requested')) {
580
            if ($this->session->check('_Token')) {
581
                $request = $request->withParam('_Token', $this->session->read('_Token'));
582
            }
583
584
            return $request;
585
        }
586
        $token = [
587
            'allowedControllers' => $this->_config['allowedControllers'],
588
            'allowedActions' => $this->_config['allowedActions'],
589
            'unlockedFields' => $this->_config['unlockedFields'],
590
        ];
591
592
        $this->session->write('_Token', $token);
593
594
        return $request->withParam('_Token', [
595
            'unlockedFields' => $token['unlockedFields'],
596
        ]);
597
    }
598
599
    /**
600
     * Calls a controller callback method
601
     *
602
     * @param \Cake\Controller\Controller $controller Instantiating controller
603
     * @param string $method Method to execute
604
     * @param array $params Parameters to send to method
605
     * @return mixed Controller callback method's response
606
     * @throws \Cake\Http\Exception\BadRequestException When a the blackholeCallback is not callable.
607
     */
608
    protected function _callback(Controller $controller, $method, $params = [])
609
    {
610
        if (!is_callable([$controller, $method])) {
611
            throw new BadRequestException('The request has been black-holed');
612
        }
613
614
        return call_user_func_array([&$controller, $method], empty($params) ? null : $params);
615
    }
616
617
    /**
618
     * Generate array of messages for the existing fields in POST data, matching dataFields in $expectedFields
619
     * will be unset
620
     *
621
     * @param array $dataFields Fields array, containing the POST data fields
622
     * @param array $expectedFields Fields array, containing the expected fields we should have in POST
623
     * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected)
624
     * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected)
625
     * @return array Error messages
626
     */
627
    protected function _matchExistingFields($dataFields, &$expectedFields, $intKeyMessage, $stringKeyMessage)
628
    {
629
        $messages = [];
630
        foreach ((array)$dataFields as $key => $value) {
631
            if (is_int($key)) {
632
                $foundKey = array_search($value, (array)$expectedFields);
633
                if ($foundKey === false) {
634
                    $messages[] = sprintf($intKeyMessage, $value);
635
                } else {
636
                    unset($expectedFields[$foundKey]);
637
                }
638
            } elseif (is_string($key)) {
639
                if (isset($expectedFields[$key]) && $value !== $expectedFields[$key]) {
640
                    $messages[] = sprintf($stringKeyMessage, $key, $expectedFields[$key], $value);
641
                }
642
                unset($expectedFields[$key]);
643
            }
644
        }
645
646
        return $messages;
647
    }
648
649
    /**
650
     * Generate debug message for the expected fields
651
     *
652
     * @param array $expectedFields Expected fields
653
     * @param string $missingMessage Message template
654
     * @return string|null Error message about expected fields
655
     */
656
    protected function _debugExpectedFields($expectedFields = [], $missingMessage = '')
657
    {
658
        if (count($expectedFields) === 0) {
659
            return null;
660
        }
661
662
        $expectedFieldNames = [];
663
        foreach ((array)$expectedFields as $key => $expectedField) {
664
            if (is_int($key)) {
665
                $expectedFieldNames[] = $expectedField;
666
            } else {
667
                $expectedFieldNames[] = $key;
668
            }
669
        }
670
671
        return sprintf($missingMessage, implode(', ', $expectedFieldNames));
672
    }
673
}
674