Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Handles synchronising members using the enrolment LTI.
19
 *
20
 * @package    enrol_lti
21
 * @copyright  2016 Mark Nelson <markn@moodle.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace enrol_lti\task;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
use core\task\scheduled_task;
30
use core_user;
31
use enrol_lti\data_connector;
32
use enrol_lti\helper;
33
use IMSGlobal\LTI\ToolProvider\Context;
34
use IMSGlobal\LTI\ToolProvider\ResourceLink;
35
use IMSGlobal\LTI\ToolProvider\ToolConsumer;
36
use IMSGlobal\LTI\ToolProvider\User;
37
use stdClass;
38
 
39
require_once($CFG->dirroot . '/user/lib.php');
40
 
41
/**
42
 * Task for synchronising members using the enrolment LTI.
43
 *
44
 * @package    enrol_lti
45
 * @copyright  2016 Mark Nelson <markn@moodle.com>
46
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47
 */
48
class sync_members extends scheduled_task {
49
 
50
    /** @var array Array of user photos. */
51
    protected $userphotos = [];
52
 
53
    /** @var data_connector $dataconnector A data_connector instance. */
54
    protected $dataconnector;
55
 
56
    /**
57
     * Get a descriptive name for this task.
58
     *
59
     * @return string
60
     */
61
    public function get_name() {
62
        return get_string('tasksyncmembers', 'enrol_lti');
63
    }
64
 
65
    /**
66
     * Performs the synchronisation of members.
67
     */
68
    public function execute() {
69
        if (!is_enabled_auth('lti')) {
70
            mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')));
71
            return;
72
        }
73
 
74
        // Check if the enrolment plugin is disabled - isn't really necessary as the task should not run if
75
        // the plugin is disabled, but there is no harm in making sure core hasn't done something wrong.
76
        if (!enrol_is_enabled('lti')) {
77
            mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti'));
78
            return;
79
        }
80
 
81
        $this->dataconnector = new data_connector();
82
 
83
        // Get all the enabled tools.
84
        $tools = helper::get_lti_tools(array('status' => ENROL_INSTANCE_ENABLED, 'membersync' => 1,
85
            'ltiversion' => 'LTI-1p0/LTI-2p0'));
86
        foreach ($tools as $tool) {
87
            mtrace("Starting - Member sync for published tool '$tool->id' for course '$tool->courseid'.");
88
 
89
            // Variables to keep track of information to display later.
90
            $usercount = 0;
91
            $enrolcount = 0;
92
            $unenrolcount = 0;
93
 
94
            // Fetch consumer records mapped to this tool.
95
            $consumers = $this->dataconnector->get_consumers_mapped_to_tool($tool->id);
96
 
97
            // Perform processing for each consumer.
98
            foreach ($consumers as $consumer) {
99
                mtrace("Requesting membership service for the tool consumer '{$consumer->getRecordId()}'");
100
 
101
                // Get members through this tool consumer.
102
                $members = $this->fetch_members_from_consumer($consumer);
103
 
104
                // Check if we were able to fetch the members.
105
                if ($members === false) {
106
                    mtrace("Skipping - Membership service request failed.\n");
107
                    continue;
108
                }
109
 
110
                // Fetched members count.
111
                $membercount = count($members);
112
                $usercount += $membercount;
113
                mtrace("$membercount members received.\n");
114
 
115
                // Process member information.
116
                list($users, $enrolledcount) = $this->sync_member_information($tool, $consumer, $members);
117
                $enrolcount += $enrolledcount;
118
 
119
                // Now sync unenrolments for the consumer.
120
                $unenrolcount += $this->sync_unenrol($tool, $consumer->getKey(), $users);
121
            }
122
 
123
            mtrace("Completed - Synced members for tool '$tool->id' in the course '$tool->courseid'. " .
124
                 "Processed $usercount users; enrolled $enrolcount members; unenrolled $unenrolcount members.\n");
125
        }
126
 
127
        // Sync the user profile photos.
128
        mtrace("Started - Syncing user profile images.");
129
        $countsyncedimages = $this->sync_profile_images();
130
        mtrace("Completed - Synced $countsyncedimages profile images.");
131
    }
132
 
133
    /**
134
     * Fetches the members that belong to a ToolConsumer.
135
     *
136
     * @param ToolConsumer $consumer
137
     * @return bool|User[]
138
     */
139
    protected function fetch_members_from_consumer(ToolConsumer $consumer) {
140
        $dataconnector = $this->dataconnector;
141
 
142
        // Get membership URL template from consumer profile data.
143
        $defaultmembershipsurl = null;
144
        if (isset($consumer->profile->service_offered)) {
145
            $servicesoffered = $consumer->profile->service_offered;
146
            foreach ($servicesoffered as $service) {
147
                if (isset($service->{'@id'}) && strpos($service->{'@id'}, 'tcp:ToolProxyBindingMemberships') !== false &&
148
                    isset($service->endpoint)) {
149
                    $defaultmembershipsurl = $service->endpoint;
150
                    if (isset($consumer->profile->product_instance->product_info->product_family->vendor->code)) {
151
                        $vendorcode = $consumer->profile->product_instance->product_info->product_family->vendor->code;
152
                        $defaultmembershipsurl = str_replace('{vendor_code}', $vendorcode, $defaultmembershipsurl);
153
                    }
154
                    $defaultmembershipsurl = str_replace('{product_code}', $consumer->getKey(), $defaultmembershipsurl);
155
                    break;
156
                }
157
            }
158
        }
159
 
160
        $members = false;
161
 
162
        // Fetch the resource link linked to the consumer.
163
        $resourcelink = $dataconnector->get_resourcelink_from_consumer($consumer);
164
        if ($resourcelink !== null) {
165
            // Try to perform a membership service request using this resource link.
166
            $members = $this->do_resourcelink_membership_request($resourcelink);
167
        }
168
 
169
        // If membership service can't be performed through resource link, fallback through context memberships.
170
        if ($members === false) {
171
            // Fetch context records that are mapped to this ToolConsumer.
172
            $contexts = $dataconnector->get_contexts_from_consumer($consumer);
173
 
174
            // Perform membership service request for each of these contexts.
175
            foreach ($contexts as $context) {
176
                $contextmembership = $this->do_context_membership_request($context, $resourcelink, $defaultmembershipsurl);
177
                if ($contextmembership) {
178
                    // Add $contextmembership contents to $members array.
179
                    if (is_array($members)) {
180
                        $members = array_merge($members, $contextmembership);
181
                    } else {
182
                        $members = $contextmembership;
183
                    }
184
                }
185
            }
186
        }
187
 
188
        return $members;
189
    }
190
 
191
    /**
192
     * Method to determine whether to sync unenrolments or not.
193
     *
194
     * @param int $syncmode The tool's membersyncmode.
195
     * @return bool
196
     */
197
    protected function should_sync_unenrol($syncmode) {
198
        return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_UNENROL_MISSING;
199
    }
200
 
201
    /**
202
     * Method to determine whether to sync enrolments or not.
203
     *
204
     * @param int $syncmode The tool's membersyncmode.
205
     * @return bool
206
     */
207
    protected function should_sync_enrol($syncmode) {
208
        return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_ENROL_NEW;
209
    }
210
 
211
    /**
212
     * Performs synchronisation of member information and enrolments.
213
     *
214
     * @param stdClass $tool
215
     * @param ToolConsumer $consumer
216
     * @param User[] $members
217
     * @return array An array of users from processed members and the number that were enrolled.
218
     */
219
    protected function sync_member_information(stdClass $tool, ToolConsumer $consumer, $members) {
220
        global $DB;
221
        $users = [];
222
        $enrolcount = 0;
223
 
224
        // Process member information.
225
        foreach ($members as $member) {
226
            // Set the user data.
227
            $user = new stdClass();
228
            $user->username = helper::create_username($consumer->getKey(), $member->ltiUserId);
229
            $user->firstname = core_user::clean_field($member->firstname, 'firstname');
230
            $user->lastname = core_user::clean_field($member->lastname, 'lastname');
231
            $user->email = core_user::clean_field($member->email, 'email');
232
 
233
            // Get the user data from the LTI consumer.
234
            $user = helper::assign_user_tool_data($tool, $user);
235
 
236
            $dbuser = core_user::get_user_by_username($user->username, 'id');
237
            if ($dbuser) {
238
                // If email is empty remove it, so we don't update the user with an empty email.
239
                if (empty($user->email)) {
240
                    unset($user->email);
241
                }
242
 
243
                $user->id = $dbuser->id;
244
                user_update_user($user);
245
 
246
                // Add the information to the necessary arrays.
247
                $users[$user->id] = $user;
248
                $this->userphotos[$user->id] = $member->image;
249
            } else {
250
                if ($this->should_sync_enrol($tool->membersyncmode)) {
251
                    // If the email was stripped/not set then fill it with a default one. This
252
                    // stops the user from being redirected to edit their profile page.
253
                    if (empty($user->email)) {
254
                        $user->email = $user->username .  "@example.com";
255
                    }
256
 
257
                    $user->auth = 'lti';
258
                    $user->id = user_create_user($user);
259
 
260
                    // Add the information to the necessary arrays.
261
                    $users[$user->id] = $user;
262
                    $this->userphotos[$user->id] = $member->image;
263
                }
264
            }
265
 
266
            // Sync enrolments.
267
            if ($this->should_sync_enrol($tool->membersyncmode)) {
268
                // Enrol the user in the course.
269
                if (helper::enrol_user($tool, $user->id) === helper::ENROLMENT_SUCCESSFUL) {
270
                    // Increment enrol count.
271
                    $enrolcount++;
272
                }
273
 
274
                // Check if this user has already been registered in the enrol_lti_users table.
275
                if (!$DB->record_exists('enrol_lti_users', ['toolid' => $tool->id, 'userid' => $user->id])) {
276
                    // Create an initial enrol_lti_user record that we can use later when syncing grades and members.
277
                    $userlog = new stdClass();
278
                    $userlog->userid = $user->id;
279
                    $userlog->toolid = $tool->id;
280
                    $userlog->consumerkey = $consumer->getKey();
281
 
282
                    $DB->insert_record('enrol_lti_users', $userlog);
283
                }
284
            }
285
        }
286
 
287
        return [$users, $enrolcount];
288
    }
289
 
290
    /**
291
     * Performs unenrolment of users that are no longer enrolled in the consumer side.
292
     *
293
     * @param stdClass $tool The tool record object.
294
     * @param string $consumerkey ensure we only unenrol users from this tool consumer.
295
     * @param array $currentusers The list of current users.
296
     * @return int The number of users that have been unenrolled.
297
     */
298
    protected function sync_unenrol(stdClass $tool, string $consumerkey, array $currentusers) {
299
        global $DB;
300
 
301
        $ltiplugin = enrol_get_plugin('lti');
302
 
303
        if (!$this->should_sync_unenrol($tool->membersyncmode)) {
304
            return 0;
305
        }
306
 
307
        if (empty($currentusers)) {
308
            return 0;
309
        }
310
 
311
        $unenrolcount = 0;
312
 
313
        $select = "toolid = :toolid AND " . $DB->sql_compare_text('consumerkey', 255) . " = :consumerkey";
314
        $ltiusersrs = $DB->get_recordset_select('enrol_lti_users', $select, ['toolid' => $tool->id, 'consumerkey' => $consumerkey],
315
            'lastaccess DESC', 'userid');
316
        // Go through the users and check if any were never listed, if so, remove them.
317
        foreach ($ltiusersrs as $ltiuser) {
318
            if (!array_key_exists($ltiuser->userid, $currentusers)) {
319
                $instance = new stdClass();
320
                $instance->id = $tool->enrolid;
321
                $instance->courseid = $tool->courseid;
322
                $instance->enrol = 'lti';
323
                $ltiplugin->unenrol_user($instance, $ltiuser->userid);
324
                // Increment unenrol count.
325
                $unenrolcount++;
326
            }
327
        }
328
        $ltiusersrs->close();
329
 
330
        return $unenrolcount;
331
    }
332
 
333
    /**
334
     * Performs synchronisation of user profile images.
335
     */
336
    protected function sync_profile_images() {
337
        $counter = 0;
338
        foreach ($this->userphotos as $userid => $url) {
339
            if ($url) {
340
                $result = helper::update_user_profile_image($userid, $url);
341
                if ($result === helper::PROFILE_IMAGE_UPDATE_SUCCESSFUL) {
342
                    $counter++;
343
                    mtrace("Profile image succesfully downloaded and created for user '$userid' from $url.");
344
                } else {
345
                    mtrace($result);
346
                }
347
            }
348
        }
349
        return $counter;
350
    }
351
 
352
    /**
353
     * Performs membership service request using an LTI Context object.
354
     *
355
     * If the context has a 'custom_context_memberships_url' setting, we use this to perform the membership service request.
356
     * Otherwise, if a context is associated with resource link, we try first to get the members using the
357
     * ResourceLink::doMembershipsService() method.
358
     * If we're still unable to fetch members from the resource link, we try to build a memberships URL from the memberships URL
359
     * endpoint template that is defined in the ToolConsumer profile and substitute the parameters accordingly.
360
     *
361
     * @param Context $context The context object.
362
     * @param ResourceLink $resourcelink The resource link object.
363
     * @param string $membershipsurltemplate The memberships endpoint URL template.
364
     * @return bool|User[] Array of User objects upon successful membership service request. False, otherwise.
365
     */
366
    protected function do_context_membership_request(Context $context, ResourceLink $resourcelink = null,
367
                                                     $membershipsurltemplate = '') {
368
        $dataconnector = $this->dataconnector;
369
 
370
        // Flag to indicate whether to save the context later.
371
        $contextupdated = false;
372
 
373
        // If membership URL is not set, try to generate using the default membership URL from the consumer profile.
374
        if (!$context->hasMembershipService()) {
375
            if (empty($membershipsurltemplate)) {
376
                mtrace("Skipping - No membership service available.\n");
377
                return false;
378
            }
379
 
380
            if ($resourcelink === null) {
381
                $resourcelink = $dataconnector->get_resourcelink_from_context($context);
382
            }
383
 
384
            if ($resourcelink !== null) {
385
                // Try to perform a membership service request using this resource link.
386
                $resourcelinkmembers = $this->do_resourcelink_membership_request($resourcelink);
387
                if ($resourcelinkmembers) {
388
                    // If we're able to fetch members using this resource link, return these.
389
                    return $resourcelinkmembers;
390
                }
391
            }
392
 
393
            // If fetching memberships through resource link failed and we don't have a memberships URL, build one from template.
394
            mtrace("'custom_context_memberships_url' not set. Fetching default template: $membershipsurltemplate");
395
            $membershipsurl = $membershipsurltemplate;
396
 
397
            // Check if we need to fetch tool code.
398
            $needstoolcode = strpos($membershipsurl, '{tool_code}') !== false;
399
            if ($needstoolcode) {
400
                $toolcode = false;
401
 
402
                // Fetch tool code from the resource link data.
403
                $lisresultsourcedidjson = $resourcelink->getSetting('lis_result_sourcedid');
404
                if ($lisresultsourcedidjson) {
405
                    $lisresultsourcedid = json_decode($lisresultsourcedidjson);
406
                    if (isset($lisresultsourcedid->data->typeid)) {
407
                        $toolcode = $lisresultsourcedid->data->typeid;
408
                    }
409
                }
410
 
411
                if ($toolcode) {
412
                    // Substitute fetched tool code value.
413
                    $membershipsurl = str_replace('{tool_code}', $toolcode, $membershipsurl);
414
                } else {
415
                    // We're unable to determine the tool code. End this processing.
416
                    return false;
417
                }
418
            }
419
 
420
            // Get context_id parameter and substitute, if applicable.
421
            $membershipsurl = str_replace('{context_id}', $context->getId(), $membershipsurl);
422
 
423
            // Get context_type and substitute, if applicable.
424
            if (strpos($membershipsurl, '{context_type}') !== false) {
425
                $contexttype = $context->type !== null ? $context->type : 'CourseSection';
426
                $membershipsurl = str_replace('{context_type}', $contexttype, $membershipsurl);
427
            }
428
 
429
            // Save this URL for the context's custom_context_memberships_url setting.
430
            $context->setSetting('custom_context_memberships_url', $membershipsurl);
431
            $contextupdated = true;
432
        }
433
 
434
        // Perform membership service request.
435
        $url = $context->getSetting('custom_context_memberships_url');
436
        mtrace("Performing membership service request from context with URL {$url}.");
437
        $members = $context->getMembership();
438
 
439
        // Save the context if membership request succeeded and if it has been updated.
440
        if ($members && $contextupdated) {
441
            $context->save();
442
        }
443
 
444
        return $members;
445
    }
446
 
447
    /**
448
     * Performs membership service request using ResourceLink::doMembershipsService() method.
449
     *
450
     * @param ResourceLink $resourcelink
451
     * @return bool|User[] Array of User objects upon successful membership service request. False, otherwise.
452
     */
453
    protected function do_resourcelink_membership_request(ResourceLink $resourcelink) {
454
        $members = false;
455
        $membershipsurl = $resourcelink->getSetting('ext_ims_lis_memberships_url');
456
        $membershipsid = $resourcelink->getSetting('ext_ims_lis_memberships_id');
457
        if ($membershipsurl && $membershipsid) {
458
            mtrace("Performing membership service request from resource link with membership URL: " . $membershipsurl);
459
            $members = $resourcelink->doMembershipsService(true);
460
        }
461
        return $members;
462
    }
463
}