Completed
Pull Request — master (#81)
by Sean
07:45
created

LDAPMemberExtension::generateLDAPUsername()   D

Complexity

Conditions 9
Paths 18

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 33
rs 4.909
cc 9
eloc 18
nc 18
nop 0
1
<?php
2
/**
3
 * Class LDAPMemberExtension.
4
 *
5
 * Adds mappings from AD attributes to SilverStripe {@link Member} fields.
6
 */
7
class LDAPMemberExtension extends DataExtension
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
8
{
9
    /**
10
     * @var array
11
     */
12
    private static $db = [
0 ignored issues
show
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
13
        // Unique user identifier, same field is used by SAMLMemberExtension
14
        'GUID' => 'Varchar(50)',
15
        'Username' => 'Varchar(64)',
16
        'IsExpired' => 'Boolean',
17
        'LastSynced' => 'SS_Datetime',
18
    ];
19
20
    /**
21
     * These fields are used by {@link LDAPMemberSync} to map specific AD attributes
22
     * to {@link Member} fields.
23
     *
24
     * @var array
25
     * @config
26
     */
27
    private static $ldap_field_mappings = [
0 ignored issues
show
Unused Code introduced by
The property $ldap_field_mappings is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
28
        'givenname' => 'FirstName',
29
        'samaccountname' => 'Username',
30
        'sn' => 'Surname',
31
        'mail' => 'Email',
32
    ];
33
34
    /**
35
     * The location (relative to /assets) where to save thumbnailphoto data.
36
     *
37
     * @var string
38
     * @config
39
     */
40
    private static $ldap_thumbnail_path = 'Uploads';
0 ignored issues
show
Unused Code introduced by
The property $ldap_thumbnail_path is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
41
42
    /**
43
     * When enabled, LDAP managed Member records (GUID flag)
44
     * have their data written back to LDAP on write.
45
     *
46
     * This requires setting write permissions on the user configured in the LDAP
47
     * credentials, which is why this is disabled by default.
48
     *
49
     * @var bool
50
     * @config
51
     */
52
    private static $update_ldap_from_local = false;
0 ignored issues
show
Unused Code introduced by
The property $update_ldap_from_local is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
53
54
    /**
55
     * If enabled, Member records with a Username field have the user created in LDAP
56
     * on write.
57
     *
58
     * This requires setting write permissions on the user configured in the LDAP
59
     * credentials, which is why this is disabled by default.
60
     *
61
     * @var bool
62
     * @config
63
     */
64
    private static $create_users_in_ldap = false;
0 ignored issues
show
Unused Code introduced by
The property $create_users_in_ldap is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
65
66
    /**
67
     * If enabled, deleting Member records mapped to LDAP deletes the LDAP user.
68
     *
69
     * This requires setting write permissions on the user configured in the LDAP
70
     * credentials, which is why this is disabled by default.
71
     *
72
     * @var bool
73
     * @config
74
     */
75
    private static $delete_users_in_ldap = false;
0 ignored issues
show
Unused Code introduced by
The property $delete_users_in_ldap is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
76
77
    /**
78
     * @param FieldList $fields
79
     */
80
    public function updateCMSFields(FieldList $fields)
81
    {
82
        // Redo LDAP metadata fields as read-only and move to LDAP tab.
83
        $ldapMetadata = [];
84
        $fields->replaceField('GUID', $ldapMetadata[] = new ReadonlyField('GUID'));
85
        $fields->replaceField('IsExpired', $ldapMetadata[] = new ReadonlyField(
86
            'IsExpired',
87
            _t('LDAPMemberExtension.ISEXPIRED', 'Has user\'s LDAP/AD login expired?'))
88
        );
89
        $fields->replaceField('LastSynced', $ldapMetadata[] = new ReadonlyField(
90
            'LastSynced',
91
            _t('LDAPMemberExtension.LASTSYNCED', 'Last synced'))
92
        );
93
        $fields->addFieldsToTab('Root.LDAP', $ldapMetadata);
94
95
        $message = '';
96
        if ($this->owner->GUID && $this->owner->config()->update_ldap_from_local) {
97
            $message = _t(
98
                'LDAPMemberExtension.CHANGEFIELDSUPDATELDAP',
99
                'Changing fields here will update them in LDAP.'
100
            );
101
        } elseif ($this->owner->GUID && !$this->owner->config()->update_ldap_from_local) {
102
            // Transform the automatically mapped fields into read-only. This doesn't
103
            // apply if updating LDAP from local is enabled, as changing data locally can be written back.
104
            foreach ($this->owner->config()->ldap_field_mappings as $name) {
105
                $field = $fields->dataFieldByName($name);
106
                if (!empty($field)) {
107
                    // Set to readonly, but not disabled so that the data is still sent to the
108
                    // server and doesn't break Member_Validator
109
                    $field->setReadonly(true);
110
                    $field->setTitle($field->Title()._t('LDAPMemberExtension.IMPORTEDFIELD', ' (imported)'));
111
                }
112
            }
113
            $message = _t(
114
                'LDAPMemberExtension.INFOIMPORTED',
115
                'This user is automatically imported from LDAP. '.
116
                    'Manual changes to imported fields will be removed upon sync.'
117
            );
118
        }
119
        if ($message) {
120
            $fields->addFieldToTab(
121
                'Root.Main',
122
                new LiteralField(
123
                    'Info',
124
                    sprintf('<p class="message warning">%s</p>', $message)
125
                ),
126
                'FirstName'
127
            );
128
        }
129
    }
130
131
    public function validate(ValidationResult $validationResult)
132
    {
133
        // We allow empty Username for registration purposes, as we need to
134
        // create Member records with empty Username temporarily. Forms should explicitly
135
        // check for Username not being empty if they require it not to be.
136
        if (empty($this->owner->Username) || !$this->owner->config()->create_users_in_ldap) {
137
            return;
138
        }
139
140
        if (!preg_match('/^[a-z0-9\.]+$/', $this->owner->Username)) {
141
            $validationResult->error(
142
                'Username must only contain lowercase alphanumeric characters and dots.',
143
                'bad'
144
            );
145
            throw new ValidationException($validationResult);
146
        }
147
    }
148
149
    /**
150
     * Generate a username for the user based on their name and/or email
151
     * @return string
152
     */
153
    public function generateLDAPUsername()
154
    {
155
        $service = Injector::inst()->get('LDAPService');
156
        if (!$service->enabled()) {
157
            return;
158
        }
159
160
        if (!$this->owner->FirstName && !$this->owner->Surname && !$this->owner->Email) {
161
            throw new ValidationException('Please ensure first name, surname, and email are set');
162
        }
163
164
        // Prepare the base string based on first name and surname.
165
        $baseArray = [];
166
        if ($this->owner->FirstName) $baseArray[] = strtolower($this->owner->FirstName);
167
        if ($this->owner->Surname) $baseArray[] = strtolower($this->owner->Surname);
168
        $baseUsername = implode('.', $baseArray);
169
170
        // Fallback to the first part of email.
171
        if (!$baseUsername) $baseUsername = preg_replace('/@.*/', '', $this->owner->Email);
172
173
        // Sanitise so it passes LDAP validator.
174
        $baseUsername = preg_replace('/[^a-z0-9\.]/', '', $baseUsername);
175
176
        // Ensure uniqueness.
177
        $suffix = 0;
178
        $tryUsername = $baseUsername;
179
        while ($service->getUserByUsername($tryUsername)) {
180
            $suffix++;
181
            $tryUsername = sprintf('%s%d', $baseUsername, $suffix);
182
        }
183
184
        return $tryUsername;
185
    }
186
187
    /**
188
     * Create the user in LDAP, provided this configuration is enabled
189
     * and a username was passed to a new Member record.
190
     */
191
    public function onBeforeWrite()
192
    {
193
        $service = Injector::inst()->get('LDAPService');
194
        if (
195
            !$service->enabled() ||
196
            !$this->owner->config()->create_users_in_ldap ||
197
            $this->owner->GUID
198
        ) {
199
            return;
200
        }
201
202
        // If a username wasn't provided, generate one
203
        if (!$this->owner->Username) {
204
            $this->owner->Username = $this->generateLDAPUsername();
205
        }
206
207
        $service->createLDAPUser($this->owner);
208
    }
209
210
    /**
211
     * Update the local data with LDAP, and ensure local membership is also set in
212
     * LDAP too. This writes into LDAP, provided that feature is enabled.
213
     */
214 View Code Duplication
    public function onAfterWrite()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
215
    {
216
        $service = Injector::inst()->get('LDAPService');
217
        if (
218
            !$service->enabled() ||
219
            !$this->owner->config()->update_ldap_from_local ||
220
            !$this->owner->GUID
221
        ) {
222
            return;
223
        }
224
225
        $service->updateLDAPFromMember($this->owner);
226
        $service->updateLDAPGroupsForMember($this->owner);
227
    }
228
229 View Code Duplication
    public function onAfterDelete() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
230
        $service = Injector::inst()->get('LDAPService');
231
        if (
232
            !$service->enabled() ||
233
            !$this->owner->config()->delete_users_in_ldap ||
234
            !$this->owner->GUID
235
        ) {
236
            return;
237
        }
238
239
        $service->deleteLDAPMember($this->owner);
240
    }
241
242
    /**
243
     * Triggered by {@link Member::logIn()} when successfully logged in,
244
     * this will update the Member record from AD data.
245
     */
246
    public function memberLoggedIn()
247
    {
248
        if ($this->owner->GUID) {
249
            Injector::inst()->get('LDAPService')->updateMemberFromLDAP($this->owner);
250
        }
251
    }
252
}
253