Completed
Push — master ( f7510a...037a4c )
by Bruno
14:09
created

Rauth::getCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 8
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
1
<?php
2
3
namespace SitePoint;
4
5
use SitePoint\Rauth\ArrayCache;
6
use SitePoint\Rauth\Cache;
7
8
class Rauth implements RauthInterface
9
{
10
    const REGEX = '/@auth-([\w-]+)\s?\s(.+)/';
11
12
    const MODE_OR = 'or';
13
    const MODE_AND = 'and';
14
    const MODE_NONE = 'none';
15
16
    const MODES = [
17
        self::MODE_AND,
18
        self::MODE_NONE,
19
        self::MODE_OR,
20
    ];
21
22
    /** @var string */
23
    private $defaultMode = self::MODE_OR;
24
25
    /** @var Cache */
26
    private $cache;
27
28
    public function __construct(Cache $c = null)
29
    {
30
        if ($c) {
31
            $this->cache = $c;
32
        }
33
    }
34
35
    /**
36
     * Set a default mode for auth blocks without one defined.
37
     *
38
     * Default is MODE_OR
39
     *
40
     * @param string $mode
41
     * @return RauthInterface
42
     */
43
    public function setDefaultMode(string $mode = null) : RauthInterface
44
    {
45
        if (!in_array($mode, self::MODES)) {
46
            throw new \InvalidArgumentException(
47
                'Mode ' . $mode . ' not accepted!'
48
            );
49
        }
50
        $this->defaultMode = $mode;
51
52
        return $this;
53
    }
54
55
    /**
56
     * Inject Cache instance
57
     *
58
     * @param Cache $c
59
     * @return RauthInterface
60
     */
61
    public function setCache(Cache $c) : RauthInterface
62
    {
63
        $this->cache = $c;
64
65
        return $this;
66
    }
67
68
    /**
69
     * Only used by the class.
70
     *
71
     * Could have user property directly, but having default cache is convenient
72
     *
73
     * @internal
74
     * @return null|Cache
75
     */
76
    private function getCache()
77
    {
78
        if ($this->cache === null) {
79
            $this->setCache(new ArrayCache());
80
        }
81
82
        return $this->cache;
83
    }
84
85
    /**
86
     * Used to extract the @auth blocks from a class or method
87
     *
88
     * The auth prefix is stripped, and the remaining values are saved as
89
     * key => value pairs.
90
     *
91
     * @param $class
92
     * @param string|null $method
93
     * @return array
94
     */
95
    public function extractAuth($class, string $method = null) : array
96
    {
97
        if (!is_string($class) && !is_object($class)) {
98
            throw new \InvalidArgumentException(
99
                'Class must be string or object!'
100
            );
101
        }
102
103
        $className = (is_string($class)) ? $class : get_class($class);
104
        $sig = ($method) ? $className . '::' . $method : $className;
105
106
        // Class auths haven't been cached yet
107 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...
108
            $r = new \ReflectionClass($className);
109
            preg_match_all(self::REGEX, $r->getDocComment(), $matchC);
110
            $this->getCache()->set($className, $this->normalize((array)$matchC));
111
        }
112
113
        // Method auths haven't been cached yet
114 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...
115
            $r = new \ReflectionMethod($className, $method);
116
            preg_match_all(self::REGEX, $r->getDocComment(), $matchC);
117
            $this->getCache()->set($sig, $this->normalize((array)$matchC));
118
        }
119
120
        return ($this->getCache()->get($sig) == [])
121
            ? $this->getCache()->get($className)
122
            : $this->getCache()->get($sig);
123
    }
124
125
    /**
126
     * Turns a pregexed array of auth blocks into a decent array
127
     *
128
     * Internal use only - @see Rauth::extractAuth
129
     *
130
     * @internal
131
     * @param array $matches
132
     * @return array
133
     */
134
    private function normalize(array $matches) : array
135
    {
136
        $keys = $matches[1];
137
        $values = $matches[2];
138
139
        $return = [];
140
141
        foreach ($keys as $i => $key) {
142
            $key = strtolower(trim($key));
143
144
            if ($key == 'mode') {
145
                $value = strtolower($values[$i]);
146
            } else {
147
                $value = array_map(
148
                    function ($el) {
149
                        return trim($el, ', ');
150
                    },
151
                    explode(',', $values[$i])
152
                );
153
            }
154
            $return[$key] = $value;
155
        }
156
157
        return $return;
158
    }
159
160
161
    /**
162
     * Either passes or fails an authorization attempt.
163
     *
164
     * The first two arguments are the class/method pair to inspect for @auth
165
     * tags, and `$attr` are attributes to compare the @auths against.
166
     *
167
     * Depending on the currently selected mode (either default - for that
168
     * you should @see Rauth::setDefaultMode, or defined in the @auths), it will
169
     * evaluate the arrays against one another and come to a conclusion.
170
     *
171
     * @param $class
172
     * @param string|null $method
173
     * @param array $attr
174
     * @return bool
175
     * @throws \InvalidArgumentException
176
     */
177
    public function authorize(
178
        $class,
179
        string $method = null,
180
        array $attr = []
181
    ) : bool {
182
    
183
        $auth = $this->extractAuth($class, $method);
184
185
        // Class / method has no rules - allow all
186
        if (empty($auth)) {
187
            return true;
188
        }
189
190
        // Store mode, remove from auth array
191
        $mode = $auth['mode'] ?? $this->defaultMode;
192
        unset($auth['mode']);
193
194
        // Handle bans, remove them from auth
195
        foreach ($auth as $set => $values) {
196
            if (strpos($set, 'ban-') === 0) {
197
                $key = str_replace('ban-', '', $set);
198
                if (isset($attr[$key]) && array_intersect(
199
                    (array)$attr[$key],
200
                    $values
201
                )
202
                ) {
203
                    return false;
204
                }
205
                unset($auth[$set]);
206
            }
207
        }
208
209
        switch ($mode) {
210
            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...
211
                $required = 0;
212
                $matches = 0;
213
                // All values in all arrays must match
214
                foreach ($auth as $set => $values) {
215
                    if (!isset($attr[$set])) {
216
                        return false;
217
                    }
218
                    $attr[$set] = (array)$attr[$set];
219
                    $required++;
220
                    sort($values);
221
                    sort($attr[$set]);
222
                    $matches += (int)($values == $attr[$set]);
223
                }
224
225
                return $required == $matches;
226 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...
227
                // There must be no overlap between any of the array values
228
229
                foreach ($auth as $set => $values) {
230
                    if (isset($attr[$set]) && count(
231
                        array_intersect(
232
                            (array)$attr[$set],
233
                            $values
234
                        )
235
                    )
236
                    ) {
237
                        return false;
238
                    }
239
                }
240
241
                return true;
242 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...
243
                // At least one match must be present
244
                foreach ($auth as $set => $values) {
245
                    if (isset($attr[$set]) && count(
246
                        array_intersect(
247
                            (array)$attr[$set],
248
                            $values
249
                        )
250
                    )
251
                    ) {
252
                        return true;
253
                    }
254
                }
255
256
                return false;
257
            default:
258
                throw new \InvalidArgumentException('Durrrr');
259
        }
260
261
    }
262
}
263