Rauth::normalize()   B
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3
Metric Value
dl 0
loc 25
ccs 15
cts 15
cp 1
rs 8.8571
cc 3
eloc 15
nc 3
nop 1
crap 3
1
<?php
2
3
namespace SitePoint;
4
5
use SitePoint\Rauth\ArrayCache;
6
use SitePoint\Rauth\Cache;
7
use SitePoint\Rauth\Exception\AuthException;
8
use SitePoint\Rauth\Exception\Reason;
9
10
class Rauth implements RauthInterface
11
{
12
    const REGEX = '/@auth-([\w-]+)\s?\s(.+)/';
13
14
    const MODE_OR = 'or';
15
    const MODE_AND = 'and';
16
    const MODE_NONE = 'none';
17
18
    const MODES = [
19
        self::MODE_AND,
20
        self::MODE_NONE,
21
        self::MODE_OR,
22
    ];
23
24
    /** @var string */
25
    private $defaultMode = self::MODE_OR;
26
27
    /** @var Cache */
28
    private $cache;
29
30 78
    public function __construct(Cache $c = null)
31
    {
32 78
        if ($c) {
33 1
            $this->cache = $c;
34
        }
35 78
    }
36
37
    /**
38
     * Set a default mode for auth blocks without one defined.
39
     *
40
     * Default is MODE_OR
41
     *
42
     * @param string $mode
43
     * @return RauthInterface
44
     */
45 2
    public function setDefaultMode(string $mode = null) : RauthInterface
46
    {
47 2
        if (!in_array($mode, self::MODES)) {
48 1
            throw new \InvalidArgumentException(
49 1
                'Mode ' . $mode . ' not accepted!'
50
            );
51
        }
52 1
        $this->defaultMode = $mode;
53
54 1
        return $this;
55
    }
56
57
    /**
58
     * Inject Cache instance
59
     *
60
     * @param Cache $c
61
     * @return RauthInterface
62
     */
63 74
    public function setCache(Cache $c) : RauthInterface
64
    {
65 74
        $this->cache = $c;
66
67 74
        return $this;
68
    }
69
70
    /**
71
     * Only used by the class.
72
     *
73
     * Could have user property directly, but having default cache is convenient
74
     *
75
     * @internal
76
     * @return null|Cache
77
     */
78 74
    private function getCache()
79
    {
80 74
        if ($this->cache === null) {
81 74
            $this->setCache(new ArrayCache());
82
        }
83
84 74
        return $this->cache;
85
    }
86
87
    /**
88
     * Used to extract the @auth blocks from a class or method
89
     *
90
     * The auth prefix is stripped, and the remaining values are saved as
91
     * key => value pairs.
92
     *
93
     * @param $class
94
     * @param string|null $method
95
     * @return array
96
     */
97 75
    public function extractAuth($class, string $method = null) : array
98
    {
99 75
        if (!is_string($class) && !is_object($class)) {
100 1
            throw new \InvalidArgumentException(
101 1
                'Class must be string or object!'
102
            );
103
        }
104
105 74
        $className = (is_string($class)) ? $class : get_class($class);
106 74
        $sig = ($method) ? $className . '::' . $method : $className;
107
108
        // Class auths haven't been cached yet
109 74 View Code Duplication
        if (!$this->getCache()->has($className)) {
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...
110 74
            $r = new \ReflectionClass($className);
111 74
            preg_match_all(self::REGEX, $r->getDocComment(), $matchC);
112 74
            $this->getCache()->set($className, $this->normalize((array)$matchC));
113
        }
114
115
        // Method auths haven't been cached yet
116 74 View Code Duplication
        if (!$this->getCache()->has($sig)) {
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...
117 64
            $r = new \ReflectionMethod($className, $method);
118 64
            preg_match_all(self::REGEX, $r->getDocComment(), $matchC);
119 64
            $this->getCache()->set($sig, $this->normalize((array)$matchC));
120
        }
121
122 74
        return ($this->getCache()->get($sig) == [])
123 18
            ? $this->getCache()->get($className)
124 74
            : $this->getCache()->get($sig);
125
    }
126
127
    /**
128
     * Turns a pregexed array of auth blocks into a decent array
129
     *
130
     * Internal use only - @see Rauth::extractAuth
131
     *
132
     * @internal
133
     * @param array $matches
134
     * @return array
135
     */
136 74
    private function normalize(array $matches) : array
137
    {
138 74
        $keys = $matches[1];
139 74
        $values = $matches[2];
140
141 74
        $return = [];
142
143 74
        foreach ($keys as $i => $key) {
144 72
            $key = strtolower(trim($key));
145
146 72
            if ($key == 'mode') {
147 48
                $value = strtolower($values[$i]);
148
            } else {
149 72
                $value = array_map(
150 72
                    function ($el) {
151 72
                        return trim($el, ', ');
152 72
                    },
153 72
                    explode(',', $values[$i])
154
                );
155
            }
156 72
            $return[$key] = $value;
157
        }
158
159 74
        return $return;
160
    }
161
162
163
    /**
164
     * Either passes or fails an authorization attempt.
165
     *
166
     * The first two arguments are the class/method pair to inspect for @auth
167
     * tags, and `$attr` are attributes to compare the @auths against.
168
     *
169
     * Depending on the currently selected mode (either default - for that
170
     * you should @see Rauth::setDefaultMode, or defined in the @auths), it will
171
     * evaluate the arrays against one another and come to a conclusion.
172
     *
173
     * @param $class
174
     * @param string|null $method
175
     * @param array $attr
176
     * @return bool
177
     * @throws \InvalidArgumentException
178
     * @throws AuthException
179
     */
180 68
    public function authorize(
181
        $class,
182
        string $method = null,
183
        array $attr = []
184
    ) : bool {
185
    
186 68
        $auth = $this->extractAuth($class, $method);
187
188
        // Class / method has no rules - allow all
189 67
        if (empty($auth)) {
190 2
            return true;
191
        }
192
193
        // Store mode, remove from auth array
194 65
        $mode = $auth['mode'] ?? $this->defaultMode;
195 65
        $e = new AuthException($mode);
196 65
        unset($auth['mode']);
197
198
        // Handle bans, remove them from auth
199 65
        $this->handleBans($auth, $attr);
200
201
        switch ($mode) {
202 63
            case self::MODE_AND:
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
203
                // All values in all arrays must match
204 17
                foreach ($auth as $set => $values) {
205 17
                    if (!isset($attr[$set])) {
206 15
                        $e->addReason(new Reason(
207
                            $set,
208 15
                            [],
209
                            $values
210
                        ));
211
                    } else {
212 5
                        $attr[$set] = (array)$attr[$set];
213 5
                        sort($values);
214 5
                        sort($attr[$set]);
215 5
                        if ($values != $attr[$set]) {
216 1
                            $e->addReason(new Reason(
217
                                $set,
218 17
                                $attr[$set],
219
                                $values
220
                            ));
221
                        }
222
                    }
223
                }
224
225 17
                if ($e->hasReasons()) {
226 15
                    throw $e;
227
                }
228 2
                return true;
229 46 View Code Duplication
            case self::MODE_NONE:
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...
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
230
                // There must be no overlap between any of the array values
231
232 13
                foreach ($auth as $set => $values) {
233 13
                    if (isset($attr[$set]) && count(
234
                        array_intersect(
235 13
                            (array)$attr[$set],
236
                            $values
237
                        )
238
                    )
239
                    ) {
240 4
                        $e->addReason(new Reason(
241
                            $set,
242 13
                            (array)($attr[$set] ?? []),
243
                            $values
244
                        ));
245
                    }
246
                }
247
248 13
                if ($e->hasReasons()) {
249 4
                    throw $e;
250
                }
251 9
                return true;
252 33 View Code Duplication
            case self::MODE_OR:
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...
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
253
                // At least one match must be present
254 32
                foreach ($auth as $set => $values) {
255 32
                    if (isset($attr[$set]) && count(
256
                        array_intersect(
257 32
                            (array)$attr[$set],
258
                            $values
259
                        )
260
                    )
261
                    ) {
262 19
                        return true;
263
                    }
264 22
                    $e->addReason(new Reason(
265
                        $set,
266 22
                        (array)($attr[$set] ?? []),
267
                        $values
268
                    ));
269
                }
270
271 13
                throw $e;
272
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
273 1
                throw new \InvalidArgumentException('Durrrr');
274
        }
275
276
    }
277
278 65
    private function handleBans(&$auth, $attr)
279
    {
280 65
        foreach ($auth as $set => $values) {
281 64
            if (strpos($set, 'ban-') === 0) {
282 19
                $key = str_replace('ban-', '', $set);
283 19
                if (isset($attr[$key]) && array_intersect(
284 19
                    (array)$attr[$key],
285
                    $values
286
                )
287
                ) {
288 2
                    $exception = new AuthException('ban');
289 2
                    throw $exception->addReason(new Reason(
290
                        $key,
291 2
                        (array)$attr[$key],
292
                        $values
293
                    ));
294
                }
295 64
                unset($auth[$set]);
296
            }
297
        }
298
299 63
    }
300
}
301