Completed
Push — master ( 4e378a...cfdcd7 )
by Yann
05:10
created

CallableCollectionVoter::voteOnAttribute()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 3
1
<?php
2
3
namespace Yokai\SecurityExtraBundle\Voter;
4
5
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
6
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
7
use Symfony\Component\Security\Core\User\UserInterface;
8
use Yokai\SecurityExtraBundle\Exception\LogicException;
9
10
/**
11
 * @author Yann Eugoné <[email protected]>
12
 */
13
class CallableCollectionVoter extends Voter
14
{
15
    /**
16
     * Attribute list this is supporting.
17
     *
18
     * @var string[]
19
     */
20
    private $supportedAttributes;
21
22
    /**
23
     * Subject types list this is supporting.
24
     *
25
     * @var string[]
26
     */
27
    private $supportedSubjects;
28
29
    /**
30
     * Callable collection this must call.
31
     *
32
     * @var callable[]
33
     */
34
    private $callables;
35
36
    /**
37
     * @param string[]   $supportedAttributes Attribute list this is supporting
38
     * @param string[]   $supportedSubjects   Subject types list this is supporting
39
     * @param callable[] $callables           Callable collection this must call
40
     */
41
    public function __construct($supportedAttributes, $supportedSubjects, $callables)
42
    {
43
        $this->supportedAttributes = $supportedAttributes;
44
        $this->supportedSubjects = $supportedSubjects;
45
        $this->callables = $callables;
46
    }
47
48
    /**
49
     * @inheritDoc
50
     */
51
    protected function supports($attribute, $subject)
52
    {
53
        // if at least one supported attribute is configured
54
        // check if provided attribute is in that list
55
        if (count($this->supportedAttributes) > 0 && !in_array($attribute, $this->supportedAttributes, true)) {
56
            return false;
57
        }
58
59
        // if there is no subject
60
        // of if there is not at least one supported subject
61
        // this is supporting
62
        if ($subject === null || count($this->supportedSubjects) === 0) {
63
            return true;
64
        }
65
66
        // iterate over supported subjects
67
        foreach ($this->supportedSubjects as $supportedSubject) {
68
            // if supported subject is a class (or interface)
69
            // this supports if subject is an instance of
70
            if (class_exists($supportedSubject)) {
71
                if ($subject instanceof $supportedSubject) {
72
                    return true;
73
                }
74
75
                continue;
76
            }
77
78
            // if supported subject is not a class, it must be an internal type
79
            // this support if subject type is the same
80
            if (gettype($subject) === $supportedSubject) {
81
                return true;
82
            }
83
        }
84
85
        // supported attribute but unsupported subject
86
87
        return false;
88
    }
89
90
    /**
91
     * @inheritDoc
92
     */
93
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
94
    {
95
        // iterate over configured callables
96
        foreach ($this->callables as $callable) {
97
            $callable = $this->normalizeCallable($callable, $subject);
0 ignored issues
show
Documentation introduced by
$callable is of type callable, but the function expects a string|object|array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
98
            $parameters = $this->gatherParameters($callable, $attribute, $subject, $token);
99
100
            // if one callable returns falsy result
101
            // this deny access
102
            if (!(bool) call_user_func_array($callable, $parameters)) {
103
                return false;
104
            }
105
        }
106
107
        // no callable returns falsy result
108
        // this grand access
109
110
        return true;
111
    }
112
113
    /**
114
     * Normalizes a callable.
115
     * Will return a callable array. See http://php.net/manual/en/language.types.callable.php .
116
     *
117
     * @param string|object|array $callable The callable to normalize
118
     * @param mixed               $subject  The subject being voting on
119
     *
120
     * @return array The normalized callable
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<object|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...
121
     * @throws \Exception
122
     */
123
    private function normalizeCallable($callable, $subject)
0 ignored issues
show
Complexity introduced by
This operation has 1400 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

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

Loading history...
124
    {
125
        // callable is a string
126
        // it should be a method to call on subject
127
        if (is_string($callable)) {
128
            if (!is_object($subject) || !is_callable([$subject, $callable])) {
129
                throw new LogicException(
130
                    sprintf(
131
                        'Provided string callable "%s", but subject "%s" has no callable method with that name.',
132
                        (string) $callable,
133
                        is_object($subject) ? get_class($subject) : gettype($subject)
134
                    )
135
                );
136
            }
137
138
            return [$subject, $callable];
139
        }
140
141
        // callable is an object
142
        // it should have an __invoke method
143
        if (is_object($callable)) {
144
            if (!is_callable([$callable, '__invoke'])) {
145
                throw new LogicException(
146
                    sprintf(
147
                        'Provided object callable "%s", but it has no "__invoke" method.',
148
                        is_object($callable) ? get_class($callable) : gettype($callable)
149
                    )
150
                );
151
            }
152
153
            return [$callable, '__invoke'];
154
        }
155
156
        // callable is an array
157
        // it should be an array with [0] and [1]
158
        if (is_array($callable)) {
159
            if (!isset($callable[0]) || !isset($callable[1]) || !is_callable([$callable[0], $callable[1]])) {
160
                throw new LogicException('Provided array callable, but it is not callable.');
161
            }
162
163
            return [$callable[0], $callable[1]];
164
        }
165
166
        throw new LogicException(
167
            sprintf(
168
                'Unable to normalize callable "%". Please review your configuration.',
169
                is_object($callable) ? get_class($callable) : gettype($callable)
170
            )
171
        );
172
    }
173
174
    /**
175
     * Analyzes callable and determine the required parameters.
176
     *
177
     * @param array          $callable  The callable for which to gather parameters
178
     * @param string         $attribute The attribute being voting for
179
     * @param mixed          $subject   The subject being voting on
180
     * @param TokenInterface $token     The authentication being voting for
181
     *
182
     * @return array The parameters list
183
     * @throws \Exception
184
     */
185
    private function gatherParameters($callable, $attribute, $subject, TokenInterface $token)
186
    {
187
        if ($callable[0] instanceof \Closure) {
188
            // don't know why but, it seems that ['\Closure', '__invoke'] is not ok with \ReflectionMethod
189
            $reflection = new \ReflectionFunction($callable[0]);
190
        } else {
191
            $reflection = new \ReflectionMethod(get_class($callable[0]), $callable[1]);
192
        }
193
194
        $parameters = [];
195
196
        // iterating over all parameters for method
197
        foreach ($reflection->getParameters() as $parameter) {
198
            $parameterType = $parameter->getType();
199
            if (method_exists($parameterType, 'getName')) {
200
                $parameterType = $parameterType->getName();
201
            } else {
202
                $parameterType = (string) $parameterType; // PHP < 7.1 supports
203
            }
204
            $parameterName = $parameter->getName();
205
            $parameterPosition = $parameter->getPosition();
206
            switch (true) {
207
                // attribute is a bit tricky, cannot use any type to determine whether or not it is required
208
                // if the parameter name is "attribute" this assume it should be provided
209
                // adding subject to required parameters
210
                case $parameterName === 'attribute':
211
                    $parameters[$parameterPosition] = $attribute;
212
                    break;
213
214
                // parameter looks like the subject being voting on
215
                // adding subject to required parameters
216
                case is_a($subject, $parameterType) || gettype($subject) === $parameterType:
217
                    $parameters[$parameterPosition] = $subject;
218
                    break;
219
220
                // parameter looks like a security token
221
                // adding token to required parameters
222
                case is_a($token, $parameterType):
223
                    $parameters[$parameterPosition] = $token;
224
                    break;
225
226
                // parameter looks like a security user
227
                // adding user to required parameters
228
                case is_a($token->getUser(), $parameterType):
229
                    $parameters[$parameterPosition] = $token->getUser();
230
                    break;
231
            }
232
        }
233
234
        // this gathered parameters, but the callable needs something more
235
        // calling with these parameters will probably results to an error
236
        // so throwing an exception is the only thing to do
237
        if ($reflection->getNumberOfRequiredParameters() !== count($parameters)) {
238
            throw new LogicException(
239
                sprintf(
240
                    'The callable method "%s"->"%s"() needs parameters that cannot be provided.',
241
                    get_class($callable[0]),
242
                    $callable[1]
243
                )
244
            );
245
        }
246
247
        // return sorted parameters
248
        ksort($parameters);
249
250
        return $parameters;
251
    }
252
}
253