Passed
Push — master ( 7e81b0...7eb007 )
by Robbie
12:39 queued 11s
created

MethodRegistry::hasMethods()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace SilverStripe\MFA\Service;
4
5
use SilverStripe\Core\Config\Configurable;
6
use SilverStripe\Core\Injector\Injectable;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\MFA\BackupCode\Method;
9
use SilverStripe\MFA\Method\MethodInterface;
10
use UnexpectedValueException;
11
12
/**
13
 * A service class that holds the configuration for enabled MFA methods and facilitates providing these methods
14
 */
15
class MethodRegistry
16
{
17
    use Configurable;
18
    use Injectable;
19
20
    /**
21
     * List of configured MFA methods. These should be class names that implement MethodInterface
22
     *
23
     * @config
24
     * @var string[]
25
     */
26
    private static $methods = [];
0 ignored issues
show
introduced by
The private property $methods is not used, and could be removed.
Loading history...
27
28
    /**
29
     * A string referring to the classname of the method (implementing SilverStripe\MFA\Method\MethodInterface) that is
30
     * to be used as the back-up method for MFA. This alters the registration of this method to be required - a forced
31
     * registration once the user has registered at least one other method. Additionally it cannot be set as the default
32
     * method for a user to log in with.
33
     *
34
     * @config
35
     * @var string
36
     */
37
    private static $default_backup_method = Method::class;
0 ignored issues
show
introduced by
The private property $default_backup_method is not used, and could be removed.
Loading history...
38
39
    /**
40
     * Request cache of instantiated method instances
41
     *
42
     * @var MethodInterface[]
43
     */
44
    protected $methodInstances;
45
46
    /**
47
     * Get implementations of all configured methods
48
     *
49
     * @return MethodInterface[]
50
     * @throws UnexpectedValueException When an invalid method is registered
51
     * @throws UnexpectedValueException If a method was registered more than once
52
     * @throws UnexpectedValueException If multiple registered methods share a common URL segment
53
     */
54
    public function getMethods(): array
55
    {
56
        if (is_array($this->methodInstances)) {
0 ignored issues
show
introduced by
The condition is_array($this->methodInstances) is always true.
Loading history...
57
            return $this->methodInstances;
58
        }
59
60
        $configuredMethods = (array) $this->config()->get('methods');
61
        $configuredMethods = array_filter($configuredMethods);
62
        $this->ensureNoDuplicateMethods($configuredMethods);
63
64
        $allMethods = [];
65
        foreach ($configuredMethods as $method) {
66
            $method = Injector::inst()->get($method);
67
68
            if (!$method instanceof MethodInterface) {
69
                throw new UnexpectedValueException(sprintf(
70
                    'Given method "%s" does not implement %s',
71
                    $method,
72
                    MethodInterface::class
73
                ));
74
            }
75
76
            $allMethods[] = $method;
77
        }
78
        $this->ensureNoDuplicateURLSegments($allMethods);
79
80
        return $this->methodInstances = $allMethods;
81
    }
82
83
    /**
84
     * Helper method to indicate whether any MFA methods are registered
85
     *
86
     * @return bool
87
     */
88
    public function hasMethods(): bool
89
    {
90
        return count($this->getMethods()) > 0;
91
    }
92
93
    /**
94
     * Indicates whether the given method is registered as the back-up method for MFA
95
     *
96
     * @param MethodInterface $method
97
     * @return bool
98
     */
99
    public function isBackupMethod(MethodInterface $method): bool
100
    {
101
        $configuredBackupMethod = $this->config()->get('default_backup_method');
102
        return is_string($configuredBackupMethod) && is_a($method, $configuredBackupMethod);
103
    }
104
105
    /**
106
     * Get the configured backup method
107
     *
108
     * @return MethodInterface|null
109
     */
110
    public function getBackupMethod(): ?MethodInterface
111
    {
112
        foreach ($this->getMethods() as $method) {
113
            if ($this->isBackupMethod($method)) {
114
                return $method;
115
            }
116
        }
117
118
        return null;
119
    }
120
121
    /**
122
     * Fetches a Method by its URL Segment
123
     *
124
     * @param string $segment
125
     * @return MethodInterface|null
126
     */
127
    public function getMethodByURLSegment(string $segment): ?MethodInterface
128
    {
129
        foreach ($this->getMethods() as $method) {
130
            if ($method->getURLSegment() === $segment) {
131
                return $method;
132
            }
133
        }
134
135
        return null;
136
    }
137
138
    /**
139
     * Ensure that attempts to register a method multiple times do not occur
140
     *
141
     * @param array $configuredMethods
142
     * @throws UnexpectedValueException
143
     */
144
    private function ensureNoDuplicateMethods(array $configuredMethods): void
145
    {
146
        $uniqueMethods = array_unique($configuredMethods);
147
        if ($uniqueMethods === $configuredMethods) {
148
            return;
149
        }
150
151
        // Get the method class names that were added more than once and format them into a string so we can
152
        // tell the developer which classes were incorrectly configured
153
        $duplicates = array_unique(array_diff_key($configuredMethods, $uniqueMethods));
154
        $methodNames = implode('; ', $duplicates);
155
        throw new UnexpectedValueException(
156
            'Cannot register MFA methods more than once. Check your config: ' . $methodNames
157
        );
158
    }
159
160
    /**
161
     * Ensure that all registered methods have a unique URLSegment
162
     *
163
     * @param array $allMethods
164
     * @throws UnexpectedValueException
165
     */
166
    private function ensureNoDuplicateURLSegments(array $allMethods): void
167
    {
168
        $allURLSegments = array_map(function (MethodInterface $method) {
169
            return $method->getURLSegment();
170
        }, $allMethods);
171
        $uniqueURLSegments = array_unique($allURLSegments);
172
        if ($allURLSegments === $uniqueURLSegments) {
173
            return;
174
        }
175
176
        // Get the method URL segments that were added more than once and format them into a string so we can
177
        // tell the developer which classes were incorrectly configured
178
        $duplicates = array_unique(array_diff_key($allURLSegments, $uniqueURLSegments));
179
        $urlSegments = implode('; ', $duplicates);
180
        throw new UnexpectedValueException(
181
            'Cannot register multiple MFA methods with the same URL segment: ' . $urlSegments
182
        );
183
    }
184
}
185