Completed
Push — master ( 832a22...d853d3 )
by Bruno
6s
created

Rauth::handleBans()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 22
rs 8.6737
cc 5
eloc 13
nc 4
nop 2
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
    public function __construct(Cache $c = null)
31
    {
32
        if ($c) {
33
            $this->cache = $c;
34
        }
35
    }
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
    public function setDefaultMode(string $mode = null) : RauthInterface
46
    {
47
        if (!in_array($mode, self::MODES)) {
48
            throw new \InvalidArgumentException(
49
                'Mode ' . $mode . ' not accepted!'
50
            );
51
        }
52
        $this->defaultMode = $mode;
53
54
        return $this;
55
    }
56
57
    /**
58
     * Inject Cache instance
59
     *
60
     * @param Cache $c
61
     * @return RauthInterface
62
     */
63
    public function setCache(Cache $c) : RauthInterface
64
    {
65
        $this->cache = $c;
66
67
        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
    private function getCache()
79
    {
80
        if ($this->cache === null) {
81
            $this->setCache(new ArrayCache());
82
        }
83
84
        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
    public function extractAuth($class, string $method = null) : array
98
    {
99
        if (!is_string($class) && !is_object($class)) {
100
            throw new \InvalidArgumentException(
101
                'Class must be string or object!'
102
            );
103
        }
104
105
        $className = (is_string($class)) ? $class : get_class($class);
106
        $sig = ($method) ? $className . '::' . $method : $className;
107
108
        // Class auths haven't been cached yet
109 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
            $r = new \ReflectionClass($className);
111
            preg_match_all(self::REGEX, $r->getDocComment(), $matchC);
112
            $this->getCache()->set($className, $this->normalize((array)$matchC));
113
        }
114
115
        // Method auths haven't been cached yet
116 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
            $r = new \ReflectionMethod($className, $method);
118
            preg_match_all(self::REGEX, $r->getDocComment(), $matchC);
119
            $this->getCache()->set($sig, $this->normalize((array)$matchC));
120
        }
121
122
        return ($this->getCache()->get($sig) == [])
123
            ? $this->getCache()->get($className)
124
            : $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
    private function normalize(array $matches) : array
137
    {
138
        $keys = $matches[1];
139
        $values = $matches[2];
140
141
        $return = [];
142
143
        foreach ($keys as $i => $key) {
144
            $key = strtolower(trim($key));
145
146
            if ($key == 'mode') {
147
                $value = strtolower($values[$i]);
148
            } else {
149
                $value = array_map(
150
                    function ($el) {
151
                        return trim($el, ', ');
152
                    },
153
                    explode(',', $values[$i])
154
                );
155
            }
156
            $return[$key] = $value;
157
        }
158
159
        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
    public function authorize(
181
        $class,
182
        string $method = null,
183
        array $attr = []
184
    ) : bool {
185
    
186
        $auth = $this->extractAuth($class, $method);
187
188
        // Class / method has no rules - allow all
189
        if (empty($auth)) {
190
            return true;
191
        }
192
193
        // Store mode, remove from auth array
194
        $mode = $auth['mode'] ?? $this->defaultMode;
195
        $e = new AuthException($mode);
196
        unset($auth['mode']);
197
198
        // Handle bans, remove them from auth
199
        $this->handleBans($auth, $attr);
200
201
        switch ($mode) {
202
            case self::MODE_AND:
0 ignored issues
show
Coding Style introduced by
case statements should not use curly braces.

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
                foreach ($auth as $set => $values) {
205
                    if (!isset($attr[$set])) {
206
                        $e->addReason(new Reason(
207
                            $set,
208
                            [],
209
                            $values
210
                        ));
211
                    } else {
212
                        $attr[$set] = (array)$attr[$set];
213
                        sort($values);
214
                        sort($attr[$set]);
215
                        if ($values != $attr[$set]) {
216
                            $e->addReason(new Reason(
217
                                $set,
218
                                $attr[$set],
219
                                $values
220
                            ));
221
                        }
222
                    }
223
                }
224
225
                if ($e->hasReasons()) {
226
                    throw $e;
227
                }
228
                return true;
229 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 should not use curly braces.

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
                foreach ($auth as $set => $values) {
233
                    if (isset($attr[$set]) && count(
234
                        array_intersect(
235
                            (array)$attr[$set],
236
                            $values
237
                        )
238
                    )
239
                    ) {
240
                        $e->addReason(new Reason(
241
                            $set,
242
                            (array)($attr[$set] ?? []),
243
                            $values
244
                        ));
245
                    }
246
                }
247
248
                if ($e->hasReasons()) {
249
                    throw $e;
250
                }
251
                return true;
252 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 should not use curly braces.

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
                foreach ($auth as $set => $values) {
255
                    if (isset($attr[$set]) && count(
256
                        array_intersect(
257
                            (array)$attr[$set],
258
                            $values
259
                        )
260
                    )
261
                    ) {
262
                        return true;
263
                    }
264
                    $e->addReason(new Reason(
265
                        $set,
266
                        (array)($attr[$set] ?? []),
267
                        $values
268
                    ));
269
                }
270
271
                throw $e;
272
            default:
0 ignored issues
show
Coding Style introduced by
As per PSR2, default statements should not use curly braces.

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