Passed
Pull Request — 1.11.x (#5934)
by Angel Fernando Quiroz
10:15
created

AzureActiveDirectory::uninstall()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 12
rs 10
cc 2
nc 2
nop 0
1
<?php
2
/* For license terms, see /license.txt */
3
4
use Chamilo\PluginBundle\Entity\AzureActiveDirectory\AzureSyncState;
5
use Chamilo\UserBundle\Entity\User;
6
use Doctrine\ORM\Tools\SchemaTool;
7
use Doctrine\ORM\Tools\ToolsException;
8
use TheNetworg\OAuth2\Client\Provider\Azure;
9
10
/**
11
 * AzureActiveDirectory plugin class.
12
 *
13
 * @author Angel Fernando Quiroz Campos <[email protected]>
14
 *
15
 * @package chamilo.plugin.azure_active_directory
16
 */
17
class AzureActiveDirectory extends Plugin
18
{
19
    public const SETTING_ENABLE = 'enable';
20
    public const SETTING_APP_ID = 'app_id';
21
    public const SETTING_APP_SECRET = 'app_secret';
22
    public const SETTING_BLOCK_NAME = 'block_name';
23
    public const SETTING_FORCE_LOGOUT_BUTTON = 'force_logout';
24
    public const SETTING_MANAGEMENT_LOGIN_ENABLE = 'management_login_enable';
25
    public const SETTING_MANAGEMENT_LOGIN_NAME = 'management_login_name';
26
    public const SETTING_PROVISION_USERS = 'provisioning';
27
    public const SETTING_UPDATE_USERS = 'update_users';
28
    public const SETTING_GROUP_ID_ADMIN = 'group_id_admin';
29
    public const SETTING_GROUP_ID_SESSION_ADMIN = 'group_id_session_admin';
30
    public const SETTING_GROUP_ID_TEACHER = 'group_id_teacher';
31
    public const SETTING_EXISTING_USER_VERIFICATION_ORDER = 'existing_user_verification_order';
32
    public const SETTING_TENANT_ID = 'tenant_id';
33
    public const SETTING_DEACTIVATE_NONEXISTING_USERS = 'deactivate_nonexisting_users';
34
    public const SETTING_GET_USERS_DELTA = 'script_users_delta';
35
    public const SETTING_GET_USERGROUPS_DELTA = 'script_usergroups_delta';
36
37
    public const URL_TYPE_AUTHORIZE = 'login';
38
    public const URL_TYPE_LOGOUT = 'logout';
39
40
    public const EXTRA_FIELD_ORGANISATION_EMAIL = 'organisationemail';
41
    public const EXTRA_FIELD_AZURE_ID = 'azure_id';
42
    public const EXTRA_FIELD_AZURE_UID = 'azure_uid';
43
44
    public const API_PAGE_SIZE = 999;
45
46
    /**
47
     * AzureActiveDirectory constructor.
48
     */
49
    protected function __construct()
50
    {
51
        $settings = [
52
            self::SETTING_ENABLE => 'boolean',
53
            self::SETTING_APP_ID => 'text',
54
            self::SETTING_APP_SECRET => 'text',
55
            self::SETTING_BLOCK_NAME => 'text',
56
            self::SETTING_FORCE_LOGOUT_BUTTON => 'boolean',
57
            self::SETTING_MANAGEMENT_LOGIN_ENABLE => 'boolean',
58
            self::SETTING_MANAGEMENT_LOGIN_NAME => 'text',
59
            self::SETTING_PROVISION_USERS => 'boolean',
60
            self::SETTING_UPDATE_USERS => 'boolean',
61
            self::SETTING_GROUP_ID_ADMIN => 'text',
62
            self::SETTING_GROUP_ID_SESSION_ADMIN => 'text',
63
            self::SETTING_GROUP_ID_TEACHER => 'text',
64
            self::SETTING_EXISTING_USER_VERIFICATION_ORDER => 'text',
65
            self::SETTING_TENANT_ID => 'text',
66
            self::SETTING_DEACTIVATE_NONEXISTING_USERS => 'boolean',
67
            self::SETTING_GET_USERS_DELTA => 'boolean',
68
            self::SETTING_GET_USERGROUPS_DELTA => 'boolean',
69
        ];
70
71
        parent::__construct('2.5', 'Angel Fernando Quiroz Campos, Yannick Warnier', $settings);
72
    }
73
74
    /**
75
     * Instance the plugin.
76
     *
77
     * @staticvar null $result
78
     *
79
     * @return $this
80
     */
81
    public static function create()
82
    {
83
        static $result = null;
84
85
        return $result ? $result : $result = new self();
86
    }
87
88
    /**
89
     * @return string
90
     */
91
    public function get_name()
92
    {
93
        return 'azure_active_directory';
94
    }
95
96
    /**
97
     * @return Azure
98
     */
99
    public function getProvider()
100
    {
101
        $provider = new Azure([
102
            'clientId' => $this->get(self::SETTING_APP_ID),
103
            'clientSecret' => $this->get(self::SETTING_APP_SECRET),
104
            'redirectUri' => api_get_path(WEB_PLUGIN_PATH).'azure_active_directory/src/callback.php',
105
        ]);
106
107
        return $provider;
108
    }
109
110
    public function getProviderForApiGraph(): Azure
111
    {
112
        $provider = $this->getProvider();
113
        $provider->urlAPI = "https://graph.microsoft.com/v1.0/";
114
        $provider->resource = "https://graph.microsoft.com/";
115
        $provider->tenant = $this->get(AzureActiveDirectory::SETTING_TENANT_ID);
116
        $provider->authWithResource = false;
117
118
        return $provider;
119
    }
120
121
    /**
122
     * @param string $urlType Type of URL to generate
123
     *
124
     * @return string
125
     */
126
    public function getUrl($urlType)
127
    {
128
        if (self::URL_TYPE_LOGOUT === $urlType) {
129
            $provider = $this->getProvider();
130
131
            return $provider->getLogoutUrl(
132
                api_get_path(WEB_PATH)
133
            );
134
        }
135
136
        return api_get_path(WEB_PLUGIN_PATH).$this->get_name().'/src/callback.php';
137
    }
138
139
    /**
140
     * Create extra fields for user when installing.
141
     *
142
     * @throws ToolsException
143
     */
144
    public function install()
145
    {
146
        UserManager::create_extra_field(
147
            self::EXTRA_FIELD_ORGANISATION_EMAIL,
148
            ExtraField::FIELD_TYPE_TEXT,
149
            $this->get_lang('OrganisationEmail'),
150
            ''
151
        );
152
        UserManager::create_extra_field(
153
            self::EXTRA_FIELD_AZURE_ID,
154
            ExtraField::FIELD_TYPE_TEXT,
155
            $this->get_lang('AzureId'),
156
            ''
157
        );
158
        UserManager::create_extra_field(
159
            self::EXTRA_FIELD_AZURE_UID,
160
            ExtraField::FIELD_TYPE_TEXT,
161
            $this->get_lang('AzureUid'),
162
            ''
163
        );
164
165
        $em = Database::getManager();
166
167
        if ($em->getConnection()->getSchemaManager()->tablesExist(['course_home_notify_notification'])) {
168
            return;
169
        }
170
171
        $schemaTool = new SchemaTool($em);
172
        $schemaTool->createSchema(
173
            [
174
                $em->getClassMetadata(AzureSyncState::class),
175
            ]
176
        );
177
    }
178
179
    public function uninstall()
180
    {
181
        $em = Database::getManager();
182
183
        if (!$em->getConnection()->getSchemaManager()->tablesExist(['course_home_notify_notification'])) {
184
            return;
185
        }
186
187
        $schemaTool = new SchemaTool($em);
188
        $schemaTool->dropSchema(
189
            [
190
                $em->getClassMetadata(AzureSyncState::class),
191
            ]
192
        );
193
    }
194
195
    public function getExistingUserVerificationOrder(): array
196
    {
197
        $defaultOrder = [1, 2, 3];
198
199
        $settingValue = $this->get(self::SETTING_EXISTING_USER_VERIFICATION_ORDER);
200
        $selectedOrder = array_filter(
201
            array_map(
202
                'trim',
203
                explode(',', $settingValue)
204
            )
205
        );
206
        $selectedOrder = array_map('intval', $selectedOrder);
207
        $selectedOrder = array_filter(
208
            $selectedOrder,
209
            function ($position) use ($defaultOrder): bool {
210
                return in_array($position, $defaultOrder);
211
            }
212
        );
213
214
        if ($selectedOrder) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $selectedOrder of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
215
            return $selectedOrder;
216
        }
217
218
        return $defaultOrder;
219
    }
220
221
    public function getUserIdByVerificationOrder(array $azureUserData, string $azureUidKey = 'objectId'): ?int
222
    {
223
        $selectedOrder = $this->getExistingUserVerificationOrder();
224
225
        $extraFieldValue = new ExtraFieldValue('user');
226
        $positionsAndFields = [
227
            1 => $extraFieldValue->get_item_id_from_field_variable_and_field_value(
228
                AzureActiveDirectory::EXTRA_FIELD_ORGANISATION_EMAIL,
229
                $azureUserData['mail']
230
            ),
231
            2 => $extraFieldValue->get_item_id_from_field_variable_and_field_value(
232
                AzureActiveDirectory::EXTRA_FIELD_AZURE_ID,
233
                $azureUserData['mailNickname']
234
            ),
235
            3 => $extraFieldValue->get_item_id_from_field_variable_and_field_value(
236
                AzureActiveDirectory::EXTRA_FIELD_AZURE_UID,
237
                $azureUserData[$azureUidKey]
238
            ),
239
        ];
240
241
        foreach ($selectedOrder as $position) {
242
            if (!empty($positionsAndFields[$position]) && isset($positionsAndFields[$position]['item_id'])) {
243
                return (int) $positionsAndFields[$position]['item_id'];
244
            }
245
        }
246
247
        return null;
248
    }
249
250
    /**
251
     * @throws Exception
252
     */
253
    public function registerUser(
254
        array $azureUserInfo,
255
        string $azureUidKey = 'objectId'
256
    ) {
257
        if (empty($azureUserInfo)) {
258
            throw new Exception('Groups info not found.');
259
        }
260
261
        $userId = $this->getUserIdByVerificationOrder($azureUserInfo, $azureUidKey);
262
263
        if (empty($userId)) {
264
            // If we didn't find the user
265
            if ($this->get(self::SETTING_PROVISION_USERS) !== 'true') {
266
                throw new Exception('User not found when checking the extra fields from '.$azureUserInfo['mail'].' or '.$azureUserInfo['mailNickname'].' or '.$azureUserInfo[$azureUidKey].'.');
267
            }
268
269
            [
270
                $firstNme,
271
                $lastName,
272
                $username,
273
                $email,
274
                $phone,
275
                $authSource,
276
                $active,
277
                $extra,
278
            ] = $this->formatUserData($azureUserInfo, $azureUidKey);
279
280
            // If the option is set to create users, create it
281
            $userId = UserManager::create_user(
282
                $firstNme,
283
                $lastName,
284
                STUDENT,
285
                $email,
286
                $username,
287
                '',
288
                null,
289
                null,
290
                $phone,
291
                null,
292
                $authSource,
293
                null,
294
                $active,
295
                null,
296
                $extra,
297
                null,
298
                null
299
            );
300
301
            if (!$userId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userId of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
302
                throw new Exception(get_lang('UserNotAdded').' '.$azureUserInfo['userPrincipalName']);
303
            }
304
305
            return $userId;
306
        }
307
308
        if ($this->get(self::SETTING_UPDATE_USERS) === 'true') {
309
            [
310
                $firstNme,
311
                $lastName,
312
                $username,
313
                $email,
314
                $phone,
315
                $authSource,
316
                $active,
317
                $extra,
318
            ] = $this->formatUserData($azureUserInfo, $azureUidKey);
319
320
            $userId = UserManager::update_user(
321
                $userId,
322
                $firstNme,
323
                $lastName,
324
                $username,
325
                '',
326
                $authSource,
327
                $email,
328
                STUDENT,
329
                null,
330
                $phone,
331
                null,
332
                null,
333
                $active,
334
                null,
335
                0,
336
                $extra
337
            );
338
339
            if (!$userId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userId of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
340
                throw new Exception(get_lang('CouldNotUpdateUser').' '.$azureUserInfo['userPrincipalName']);
341
            }
342
        }
343
344
        return $userId;
345
    }
346
347
    /**
348
     * @return array<string, string|false>
349
     */
350
    public function getGroupUidByRole(): array
351
    {
352
        $groupUidList = [
353
            'admin' => $this->get(self::SETTING_GROUP_ID_ADMIN),
354
            'sessionAdmin' => $this->get(self::SETTING_GROUP_ID_SESSION_ADMIN),
355
            'teacher' => $this->get(self::SETTING_GROUP_ID_TEACHER),
356
        ];
357
358
        return array_filter($groupUidList);
359
    }
360
361
    /**
362
     * @return array<string, callable>
363
     */
364
    public function getUpdateActionByRole(): array
365
    {
366
        return [
367
            'admin' => function (User $user) {
368
                $user->setStatus(COURSEMANAGER);
369
370
                UserManager::addUserAsAdmin($user, false);
371
            },
372
            'sessionAdmin' => function (User $user) {
373
                $user->setStatus(SESSIONADMIN);
374
375
                UserManager::removeUserAdmin($user, false);
376
            },
377
            'teacher' => function (User $user) {
378
                $user->setStatus(COURSEMANAGER);
379
380
                UserManager::removeUserAdmin($user, false);
381
            },
382
        ];
383
    }
384
385
    /**
386
     * @throws Exception
387
     */
388
    private function formatUserData(
389
        array $azureUserInfo,
390
        string $azureUidKey
391
    ): array {
392
        $phone = null;
393
394
        if (isset($azureUserInfo['telephoneNumber'])) {
395
            $phone = $azureUserInfo['telephoneNumber'];
396
        } elseif (isset($azureUserInfo['businessPhones'][0])) {
397
            $phone = $azureUserInfo['businessPhones'][0];
398
        } elseif (isset($azureUserInfo['mobilePhone'])) {
399
            $phone = $azureUserInfo['mobilePhone'];
400
        }
401
402
        // If the option is set to create users, create it
403
        $firstNme = $azureUserInfo['givenName'];
404
        $lastName = $azureUserInfo['surname'];
405
        $email = $azureUserInfo['mail'];
406
        $username = $azureUserInfo['userPrincipalName'];
407
        $authSource = 'azure';
408
        $active = ($azureUserInfo['accountEnabled'] ? 1 : 0);
409
        $extra = [
410
            'extra_'.self::EXTRA_FIELD_ORGANISATION_EMAIL => $azureUserInfo['mail'],
411
            'extra_'.self::EXTRA_FIELD_AZURE_ID => $azureUserInfo['mailNickname'],
412
            'extra_'.self::EXTRA_FIELD_AZURE_UID => $azureUserInfo[$azureUidKey],
413
        ];
414
415
        return [
416
            $firstNme,
417
            $lastName,
418
            $username,
419
            $email,
420
            $phone,
421
            $authSource,
422
            $active,
423
            $extra,
424
        ];
425
    }
426
427
    public function getSyncState(string $title): ?AzureSyncState
428
    {
429
        $stateRepo = Database::getManager()->getRepository(AzureSyncState::class);
430
431
        return $stateRepo->findOneBy(['title' => $title]);
432
    }
433
434
    public function saveSyncState(string $title, $value)
435
    {
436
        $state = $this->getSyncState($title);
437
438
        if (!$state) {
439
            $state = new AzureSyncState();
440
            $state->setTitle($title);
441
442
            Database::getManager()->persist($state);
443
        }
444
445
        $state->setValue($value);
446
447
        Database::getManager()->flush();
448
    }
449
}
450