Auth::callPlugin()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 22
ccs 7
cts 7
cp 1
rs 9.9332
cc 4
nc 3
nop 3
crap 4
1
<?php
2
3
namespace Vectorface\Auth;
4
5
use ArrayAccess;
6
use Psr\Log\LoggerAwareTrait;
7
use Psr\Log\LoggerInterface;
8
use Vectorface\Auth\Plugin\AuthPluginInterface;
9
use Exception;
10
11
/**
12
 * A class representing a loose Authentication and Authorization framework.
13
 *  - Authentication is handled explicitly with login, logout, and verify functions.
14
 *  - Authorization is handled in whatever capacity is implemented by loaded plugins.
15
 *
16
 * Plugin classes can share data using the Auth class itself as a shared source of data.
17
 */
18
class Auth implements ArrayAccess
19
{
20
    /**
21
     * Provides setLogger method, and protected logger property.
22
     */
23
    use LoggerAwareTrait;
24
25
    /**
26
     * Name of the method called when authenticating.
27
     */
28
    const ACTION_LOGIN = 'login';
29
30
    /**
31
     * Name of the method called when attempting to log out.
32
     */
33
    const ACTION_LOGOUT = 'logout';
34
35
    /**
36
     * Name of the session verification method.
37
     */
38
    const ACTION_VERIFY = 'verify';
39
40
    /**
41
     * A result that has no effect either positive or negative.
42
     */
43
    const RESULT_NOOP = null;
44
45
    /**
46
     * A result that acts as a provisional success; Pass if all other tests pass.
47
     */
48
    const RESULT_SUCCESS = 0;
49
50
    /**
51
     * A result that indicates forced immediate success, bypassing further tests. Should not be used lightly.
52
     */
53
    const RESULT_FORCE = 1;
54
55
    /**
56
     * A result that indicates failure. All failures are immediate.
57
     */
58
    const RESULT_FAILURE = -1;
59
60
    /**
61
     * An array of auth plugins.
62
     *
63
     * @var AuthPluginInterface[]
64
     */
65
    private $plugins = [];
66
67
    /**
68
     * Shared values, accessible using the ArrayAccess interface.
69
     *
70
     * @var mixed[]
71
     */
72
    private $shared = [];
73
74
    /**
75
     * Add a plugin to the Auth module.
76
     *
77
     * @param string|AuthPluginInterface The name of a plugin class to be registered, or a configured instance of a
0 ignored issues
show
Bug introduced by
The type Vectorface\Auth\The was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
78
     *                                   security plugin.
79
     * @return bool True if the plugin was added successfully.
80 11
     */
81
    public function addPlugin($plugin)
82 11
    {
83 1
        if (!($plugin instanceof AuthPluginInterface)) {
84 1
            if (!is_string($plugin)) {
85
                return false;
86 1
            }
87
            if (!in_array(AuthPluginInterface::class, class_implements($plugin, false))) {
88
                return false;
89 1
            }
90
            $plugin = new $plugin();
91 11
        }
92 11
        $plugin->setAuth($this);
93 11
        $this->plugins[] = $plugin;
94
        return true;
95
    }
96
97
    /**
98
     * Perform a login based on username and password.
99
     *
100
     * @param string $username A user identifier.
101
     * @param string $password The user's password.
102
     * @return bool True if the login was successful, false otherwise.
103
     * @throws AuthException
104 8
     */
105
    public function login($username, $password)
106 8
    {
107
        return $this->action(self::ACTION_LOGIN, [$username, $password]);
108
    }
109
110
    /**
111
     * Attempt to log the user out.
112
     *
113
     * @throws AuthException
114 2
     */
115
    public function logout()
116 2
    {
117
        return $this->action(self::ACTION_LOGOUT);
118
    }
119
120
    /**
121
     * Verify a user's saved data - check if the user is logged in.
122
     *
123
     * @return bool True if the user's session was verified successfully.
124
     * @throws AuthException
125 4
     */
126
    public function verify()
127 4
    {
128
        return $this->action(self::ACTION_VERIFY);
129
    }
130
131
    /**
132
     * Call an action on a plugin, with particular arguments.
133
     *
134
     * @param AuthPluginInterface $plugin The plugin on which to run the action.
135
     * @param string $action The plugin action, login, logout, or verify.
136
     * @param mixed[] $arguments A list of arguments to pass to the action.
137
     * @return int The result returned by the Auth plugin.
138
     * @throws AuthException
139 9
     */
140
    private function callPlugin(AuthPluginInterface $plugin, $action, $arguments)
141
    {
142
        /* This is a bit of defensive programming; It is not possible to hit this code. */
143
        // @codeCoverageIgnoreStart
144
        if (!in_array($action, array(self::ACTION_LOGIN, self::ACTION_LOGOUT, self::ACTION_VERIFY))) {
145
            throw new AuthException("Attempted to perform unknown action: $action");
146
        }
147
        // @codeCoverageIgnoreEnd
148 9
149
        $result = call_user_func_array([$plugin, $action], $arguments);
150
151 9
        /* Expected results are an integer or null; Anything else is incorrect. */
152 1
        if (!(is_int($result) || $result === null)) {
153 1
            throw new AuthException(sprintf(
154
                "Unknown %s result in %s plugin: %s",
155 1
                $action,
156 1
                get_class($plugin),
157
                print_r($result, true)
0 ignored issues
show
Bug introduced by
It seems like print_r($result, true) can also be of type true; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

157
                /** @scrutinizer ignore-type */ print_r($result, true)
Loading history...
158
            ));
159
        }
160 9
161
        return $result;
162
    }
163
164
    /**
165
     * Perform a generalized action: login, logout, or verify: Run said function on each plugin in order verifying
166
     * result along the way.
167
     *
168
     * @param string $action The action to be taken. One of login, logout, or verify.
169
     * @param mixed[] $arguments A list of arguments to pass to the action.
170
     * @return bool True if the action was successful, false otherwise.
171
     * @throws AuthException
172 9
     */
173
    private function action($action, $arguments = [])
174 9
    {
175
        $success = false;
176 9
177
        foreach ($this->plugins as $plugin) {
178 9
            try {
179 1
                $result = $this->callPlugin($plugin, $action, $arguments);
180 1
            } catch (AuthException $e) {
181 1
                throw $e;
182 1
            } catch (Exception $e) {
183 1
                $this->logException($e, "Fatal %s error in %s plugin", $action, get_class($plugin));
184
                return false;
185
            }
186 9
187 7
            if ($result === self::RESULT_SUCCESS /* 0 */) {
188 7
                $success = true;
189
            } elseif ($result !== self::RESULT_NOOP) {
190 5
                /* If not noop or success, possible options are hard fail (<=-1), or hard success (>=1) */
191
                return ($result >= self::RESULT_FORCE);
192
            } /* else result is NOOP. Do nothing. Result not defined for plugin. */
193 7
        }
194
        return $success;
195
    }
196
197
    /**
198
     * Passthrough function call for plugins.
199
     *
200
     * @param string $method The name of the method to be called.
201
     * @param array $args An array of arguments to be passed to the method.
202
     * @return mixed Returns whatever the passthrough function returns, or null or error or missing function.
203
     * @throws AuthException
204
     * @noinspection PhpRedundantCatchClauseInspection
205 5
     */
206
    public function __call($method, $args = [])
207 5
    {
208 5
        foreach ($this->plugins as $plugin) {
209
            if (is_callable([$plugin, $method])) {
210 5
                try {
211 1
                    return call_user_func_array([$plugin, $method], $args);
212 1
                } catch (AuthException $e) {
213 1
                    throw $e;
214 1
                } catch (Exception $e) {
215
                    $this->logException($e, "Exception caught calling %s->%s", get_class($plugin), $method);
216
                    return null;
217
                }
218
            }
219 1
        }
220 1
221
        if ($this->logger) {
222 1
            $this->logger->warning(__CLASS__ . ": $method not implemented by any loaded plugin");
223
        }
224
        return null;
225
    }
226
227
    /**
228
     * ArrayAccess offsetExists
229
     *
230
     * @link http://php.net/manual/en/arrayaccess.offsetexists.php
231 1
     * @inheritDoc
232
     */
233 1
    #[\ReturnTypeWillChange]
234
    public function offsetExists($offset)
235
    {
236
        return isset($this->shared[$offset]);
237
    }
238
239
    /**
240
     * ArrayAccess offsetGet
241
     *
242 1
     * @link http://php.net/manual/en/arrayaccess.offsetget.php
243
     * @inheritDoc
244 1
     */
245
    #[\ReturnTypeWillChange]
246
    public function offsetGet($offset)
247
    {
248
        return $this->shared[$offset] ?? null;
249
    }
250
251
    /**
252
     * ArrayAccess offsetSet
253 1
     *
254
     * @link http://php.net/manual/en/arrayaccess.offsetset.php
255 1
     * @inheritDoc
256 1
     */
257
    #[\ReturnTypeWillChange]
258
    public function offsetSet($offset, $value)
259
    {
260
        $this->shared[$offset] = $value;
261
    }
262
263
    /**
264 1
     * ArrayAccess offsetUnset
265
     *
266 1
     * @link http://php.net/manual/en/arrayaccess.offsetunset.php
267 1
     * @inheritDoc
268
     */
269
    #[\ReturnTypeWillChange]
270
    public function offsetUnset($offset)
271
    {
272
        unset($this->shared[$offset]);
273
    }
274 2
275
    /**
276 2
     * Get the logger instance set on this object
277
     *
278
     * @return LoggerInterface The logger for use in this Auth object, or null if not set.
279
     */
280
    public function getLogger()
281
    {
282
        return $this->logger; /* Defined as part of the LoggerAwareInterface */
283
    }
284
285
    /**
286
     * Log a message and exception in a semi-consistent form.
287
     *
288
     * Logs the message, and appends exception message and location.
289 2
     *
290
     * @param Exception $exc The exception to log.
291 2
     * @param string $message The exception message in printf style.
292 2
     * @param string ... Any number of string parameters corresponding to %s placeholders in the message string.
293 2
     */
294 2
    private function logException(Exception $exc, $message /*, ... */)
295 2
    {
296
        if ($this->logger) {
297 2
            $message .= ": %s (%s@%s)"; /* Append exception info to log string. */
298
            $args = array_merge(
299
                [$message],
300
                array_slice(func_get_args(), 2),
301
                [$exc->getMessage(), $exc->getFile(), $exc->getLine()]
302
            );
303
            $this->logger->warning(call_user_func_array('sprintf', $args));
304
        }
305
    }
306
}
307