EnabledMembers::formatMethodsColumn()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 8
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SilverStripe\MFA\Report;
6
7
use SilverStripe\Forms\DropdownField;
8
use SilverStripe\Forms\FieldList;
9
use SilverStripe\Forms\TextField;
10
use SilverStripe\MFA\Extension\MemberExtension;
11
use SilverStripe\MFA\Model\RegisteredMethod;
12
use SilverStripe\MFA\Service\MethodRegistry;
13
use SilverStripe\ORM\ArrayList;
14
use SilverStripe\ORM\DataList;
15
use SilverStripe\Reports\Report;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Reports\Report 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...
16
use SilverStripe\Security\Member;
17
18
if (!class_exists(Report::class)) {
19
    return;
20
}
21
22
class EnabledMembers extends Report
23
{
24
    protected $title = 'Multi-factor authentication status of members';
25
26
    protected $description = 'Identifies all members on this site, with columns added to indicate the status each has'
27
        . ' in regards to multi-factor authentication method registration.';
28
29
    protected $dataClass = Member::class;
30
31
    /**
32
     * Cached registered methods fetched for the current list of members. This should only be populated at render time
33
     * as methods will be fetched for the current "records" on the report
34
     *
35
     * @var DataList|null
36
     */
37
    private $registeredMethods = null;
38
39
    public function title(): string
40
    {
41
        return _t(__CLASS__ . '.TITLE', parent::title());
42
    }
43
44
    public function description(): string
45
    {
46
        return _t(__CLASS__ . '.DESCRIPTION', parent::description());
47
    }
48
49
    /**
50
     * Supplies the list displayed in the report
51
     *
52
     * @param array $params
53
     * @return DataList
54
     */
55
    public function sourceRecords($params): DataList
56
    {
57
        $sourceList = Member::get();
58
59
        // Reports treat 0 as empty, which is interpreted as "any", so we switch it to "no" and "yes" for filtering
60
        if (!empty($params['Skipped'])) {
61
            $sourceList = $sourceList->filter('HasSkippedMFARegistration', $params['Skipped'] === 'no' ? 0 : 1);
62
        }
63
64
        // Allow partial matching on standard member fields
65
        if (!empty($params['Member'])) {
66
            $sourceList = $sourceList->filterAny([
67
                'FirstName:PartialMatch' => $params['Member'],
68
                'Surname:PartialMatch' => $params['Member'],
69
                'Email:PartialMatch' => $params['Member'],
70
            ]);
71
        }
72
73
        // Apply "none", "any", or a specific registered method filter
74
        if (!empty($params['Methods'])) {
75
            if ($params['Methods'] === 'none') {
76
                $sourceList = $sourceList->filter('RegisteredMFAMethods.MethodClassName', null);
77
            } elseif ($params['Methods'] === 'any') {
78
                $sourceList = $sourceList->filter('RegisteredMFAMethods.MethodClassName:not', null);
79
            } else {
80
                $sourceList = $sourceList->filter('RegisteredMFAMethods.MethodClassName', $params['Methods']);
81
            }
82
        }
83
84
        $this->extend('updateSourceRecords', $sourceList);
85
86
        return $sourceList;
87
    }
88
89
    /**
90
     * List the columns configured to display in the resulting reports GridField
91
     *
92
     * @return array
93
     */
94
    public function columns(): array
95
    {
96
        $columns = singleton(Member::class)->summaryFields() + [
97
            'registeredMethods' => [
98
                'title' => _t(__CLASS__ . '.COLUMN_METHODS_REGISTERED', 'Registered methods'),
99
                'formatting' => [$this, 'formatMethodsColumn']
100
            ],
101
            'defaultMethodName' => [
102
                'title' => _t(__CLASS__ . '.COLUMN_METHOD_DEFAULT', 'Default method'),
103
                'formatting' => [$this, 'formatDefaultMethodColumn']
104
            ],
105
            'HasSkippedMFARegistration' => [
106
                'title' => _t(__CLASS__ . '.COLUMN_SKIPPED_REGISTRATION', 'Skipped registration'),
107
                'formatting' => function ($_, $record) {
108
                    return $record->HasSkippedMFARegistration ? 'Yes' : 'No';
109
                },
110
            ],
111
        ];
112
113
        $this->extend('updateColumns', $columns);
114
115
        return $columns;
116
    }
117
118
    /**
119
     * Provides the fields used to gather input to filter the report
120
     *
121
     * @return FieldList
122
     */
123
    public function parameterFields(): FieldList
124
    {
125
        $parameterFields = FieldList::create([
126
            TextField::create(
127
                'Member',
128
                singleton(Member::class)->i18n_singular_name()
129
            )->setDescription(_t(
130
                __CLASS__ . '.FILTER_MEMBER_DESCRIPTION',
131
                'Firstname, Surname, Email partial match search'
132
            )),
133
            DropdownField::create(
134
                'Methods',
135
                _t(__CLASS__ . '.COLUMN_METHODS_REGISTERED', 'Registered methods'),
136
                $this->getRegisteredMethodOptions()
137
            )->setHasEmptyDefault(true),
138
            DropdownField::create(
139
                'Skipped',
140
                _t(__CLASS__ . '.COLUMN_SKIPPED_REGISTRATION', 'Skipped registration'),
141
                [ 'no' => 'No', 'yes' => 'Yes' ]
142
            )->setHasEmptyDefault(true),
143
        ]);
144
145
        $this->extend('updateParameterFields', $parameterFields);
146
147
        return $parameterFields;
148
    }
149
150
    /**
151
     * Produce a string that indicates the names of registered methods for a given member
152
     *
153
     * @param null $_
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $_ is correct as it would always require null to be passed?
Loading history...
154
     * @param Member $record
155
     * @return string
156
     */
157
    public function formatMethodsColumn($_, Member $record): string
158
    {
159
        /** @var Member&MemberExtension $record */
160
        $methods = $this->getRegisteredMethodsForRecords();
161
162
        return implode(', ', array_map(function (RegisteredMethod $method) {
163
            return $method->getMethod()->getName();
164
        }, $methods->filter('MemberID', $record->ID)->toArray()));
165
    }
166
167
    /**
168
     * Produce a string that indicates the name of the default registered method for a member
169
     *
170
     * @param null $_
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $_ is correct as it would always require null to be passed?
Loading history...
171
     * @param Member&MemberExtension $record
172
     * @return string
173
     */
174
    public function formatDefaultMethodColumn($_, Member $record): string
175
    {
176
        /** @var RegisteredMethod|null $method */
177
        $method = $this->getRegisteredMethodsForRecords()->byID($record->DefaultRegisteredMethodID);
178
179
        if (!$method) {
180
            return '';
181
        }
182
183
        return $method->getMethod()->getName();
184
    }
185
186
    /**
187
     * Create an array mapping authentication method Class Names to Readable Names
188
     *
189
     * @return array
190
     */
191
    protected function getMethodClassToTitleMapping(): array
192
    {
193
        $mapping = [];
194
195
        foreach (MethodRegistry::singleton()->getMethods() as $method) {
196
            $mapping[get_class($method)] = $method->getName();
197
        }
198
199
        return $mapping;
200
    }
201
202
    /**
203
     * @return ArrayList
204
     */
205
    protected function getRegisteredMethodsForRecords(): ArrayList
206
    {
207
        if ($this->registeredMethods instanceof ArrayList) {
208
            return $this->registeredMethods;
209
        }
210
211
        // Get the members from the generated report field list
212
        /** @var DataList $members $members */
213
        $members = $this->getReportField()->getList();
214
215
        // Filter RegisteredMethods by the IDs of those members and convert it to an ArrayList (to prevent filters ahead
216
        // from executing the datalist more than once)
217
        $this->registeredMethods = ArrayList::create(
218
            RegisteredMethod::get()
219
                ->filter('MemberID', $members->column())
220
                ->exclude('MethodClassName', $this->getBackupMethodClass())
221
                ->toArray()
222
        );
223
224
        return $this->registeredMethods;
225
    }
226
227
    /**
228
     * Adds "None" and "Any" options to the registered method dropdown filter
229
     *
230
     * @return array
231
     */
232
    private function getRegisteredMethodOptions(): array
233
    {
234
        $methods = [
235
            'none' => _t(__CLASS__ . '.NONE', 'None'),
236
            'any' => _t(__CLASS__ . '.ANY_AT_LEAST_ONE', 'Any (at least one)'),
237
        ] + $this->getMethodClassToTitleMapping();
238
        unset($methods[$this->getBackupMethodClass()]);
239
240
        return $methods;
241
    }
242
243
    /**
244
     * Returns the PHP class name of the configured backup method in the MFA module
245
     *
246
     * @return string
247
     */
248
    private function getBackupMethodClass(): string
249
    {
250
        return get_class(singleton(MethodRegistry::class)->getBackupMethod());
251
    }
252
}
253